* feat(#3028): responsive Markdown separators instead of fixed-width rules Stage banners, checkpoints, completion and error panels used fixed-width runs of box-drawing characters -- a 53-column heavy rule and a 62-column double-line box. Those runs are ordinary text to a Markdown-rendering host, so in a narrower pane they wrap and the border comes apart from the heading it framed. Shipped content now emits an ATX heading for a titled section and a blank-line-delimited --- for a break between sections, both of which adapt to the available width. The same convention is applied to the three code sites that built these strings at runtime: the UAT checkpoint renderer, the milestone-close audit report, and the TDD review checkpoint table. Removing the box also removes its only reason to exist -- the east-asian-width padding helpers that kept its right border aligned (checkpointBoxLine, displayWidth, isWideCodePoint, ZERO_WIDTH_MARK_RE, CHECKPOINT_BOX_WIDTH). RTL directional isolation is unchanged. The convention is specified in gsd-core/references/ui-brand.md and enforced across all shipped content by tests/responsive-separators.test.cjs. Refs #3028 * test(#3028): pin the heading form in checkpoint and audit-report assertions These suites asserted the exact box borders and the 62-column padded banner interior. With the box gone they assert the ### heading form, the --- break and the bolded instruction line, and each now carries a positive assertion that no box character remains -- which is what pins the fix rather than merely tolerating it. Language coverage is converted, not dropped: Japanese, Chinese, Korean, Hindi and Arabic all still assert their rendered banner, and the Arabic case still asserts the RTL directional isolates the box removal must not disturb. Adds a case for a banner longer than the old inner width, which previously produced a ragged border and now has none. Refs #3028 * chore(#3028): acknowledge execute-plan.md growth from the checkpoint display spec The checkpoint_protocol display spec described the drawn box; it now describes the heading, the --- break and the bolded action prompt, which costs 22 bytes (40111 -> 40133, 827 under the cap). Appended to the existing #3370 fragment rather than filed as a new one: a growth ack keys on the bare filename and #3370 already declares execute-plan.md, so a second source naming it would be a hard duplicate-key error. Same supersede-by-append route #3370 took for the spent #2652 fragment. Refs #3028 * docs(#3028): state the load-bearing half of the separator rule, and amend the zh-CN reference Review found three things. The rule as first written demanded a blank line above AND below every ---. Only the one above is load-bearing: it is what stops CommonMark reading the rule as a setext underline for the line above. The one below is cosmetic, because a thematic break is a leaf block. The rule now says that, with the reason, instead of asserting a stricter form the content does not keep. The zh-CN reference had received the mechanical box-to-heading swap but none of the prose behind it: it still claimed a 62-character checkpoint width and still listed --- among forbidden mixed banner styles, so it contradicted the convention it was translating. It now carries the separator section, the setext reasoning, the unconditional-vs-per-runtime rationale and a corrected anti-pattern list, in Chinese. The user guide asserted that a heading is not a degradation anywhere. That is an assertion, not a demonstration. It now says what was actually traded away in a plain terminal, points at the recorded rationale, and invites the report that would justify the capability flag instead. Refs #3028 * chore(#3028): backfill changeset PR number Refs #3028 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -1330,11 +1330,8 @@ function auditOpenArtifacts(cwd: string): AuditResult {
|
||||
function formatAuditReport(auditResult: AuditResult): string {
|
||||
const { counts, items, has_open_items, acknowledged } = auditResult;
|
||||
const lines: string[] = [];
|
||||
const hr = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
|
||||
|
||||
lines.push(hr);
|
||||
lines.push(' Milestone Close: Open Artifact Audit');
|
||||
lines.push(hr);
|
||||
lines.push('### Milestone Close: Open Artifact Audit');
|
||||
|
||||
// WARNING 3 (#3458 follow-up review): the acknowledged tally previously
|
||||
// existed only in `--json` output — the human report could not tell
|
||||
@@ -1343,12 +1340,12 @@ function formatAuditReport(auditResult: AuditResult): string {
|
||||
if (!has_open_items) {
|
||||
lines.push('');
|
||||
if (acknowledged.total > 0) {
|
||||
lines.push(` All artifact types clear (${acknowledged.total} previously acknowledged item${acknowledged.total !== 1 ? 's' : ''} still suppressed).`);
|
||||
lines.push(`All artifact types clear (${acknowledged.total} previously acknowledged item${acknowledged.total !== 1 ? 's' : ''} still suppressed).`);
|
||||
} else {
|
||||
lines.push(' All artifact types clear. Safe to proceed.');
|
||||
lines.push('All artifact types clear. Safe to proceed.');
|
||||
}
|
||||
lines.push('');
|
||||
lines.push(hr);
|
||||
lines.push('---');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -1459,12 +1456,12 @@ function formatAuditReport(auditResult: AuditResult): string {
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push(hr);
|
||||
lines.push(` ${counts.total} item${counts.total !== 1 ? 's' : ''} require decisions before close.`);
|
||||
lines.push('---');
|
||||
lines.push('');
|
||||
lines.push(`**${counts.total} item${counts.total !== 1 ? 's' : ''} require decisions before close.**`);
|
||||
if (acknowledged.total > 0) {
|
||||
lines.push(` ${acknowledged.total} previously acknowledged item${acknowledged.total !== 1 ? 's' : ''} also suppressed above the ${counts.total} open item${counts.total !== 1 ? 's' : ''}.`);
|
||||
lines.push(`${acknowledged.total} previously acknowledged item${acknowledged.total !== 1 ? 's' : ''} also suppressed above the ${counts.total} open item${counts.total !== 1 ? 's' : ''}.`);
|
||||
}
|
||||
lines.push(hr);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -890,7 +890,6 @@ function cmdTddReviewCheckpoint(projectDir: string, args: string[], raw: boolean
|
||||
const violations = rows.filter(r => r.status === 'FAIL').length;
|
||||
|
||||
// Build review table
|
||||
const sep = '━'.repeat(53);
|
||||
const tableHeader = '| Plan | RED | GREEN | REFACTOR | Status |';
|
||||
const tableDivider = '|------|-----|-------|----------|--------|';
|
||||
const tableRows = rows.map(r =>
|
||||
@@ -898,9 +897,7 @@ function cmdTddReviewCheckpoint(projectDir: string, args: string[], raw: boolean
|
||||
);
|
||||
|
||||
let table = [
|
||||
sep,
|
||||
` TDD REVIEW — Phase ${phase}`,
|
||||
sep,
|
||||
`### TDD REVIEW — Phase ${phase}`,
|
||||
'',
|
||||
`TDD Plans: ${tddPlanFiles.length} | Gate violations: ${violations}`,
|
||||
'',
|
||||
|
||||
63
src/uat.cts
63
src/uat.cts
@@ -421,8 +421,6 @@ interface CheckpointFrame {
|
||||
direction?: 'rtl';
|
||||
}
|
||||
|
||||
const CHECKPOINT_BOX_WIDTH = 64; // total column width of the ╔══...╗ border, borders stay byte-identical
|
||||
|
||||
const CHECKPOINT_FRAMES: Record<string, CheckpointFrame> = {
|
||||
english: {
|
||||
banner: 'CHECKPOINT: Verification Required',
|
||||
@@ -529,56 +527,6 @@ function resolveCheckpointFrame(responseLanguage: string | undefined): Checkpoin
|
||||
return (key && CHECKPOINT_FRAMES[key]) || CHECKPOINT_FRAMES.english;
|
||||
}
|
||||
|
||||
// Approximate terminal-cell width. East Asian Width W/F code points occupy two
|
||||
// cells, while Unicode combining marks occupy no additional cell beyond their
|
||||
// base character. Counting only W/F ranges is insufficient for scripts such as
|
||||
// Devanagari: Hindi vowel signs and viramas are combining marks, and treating
|
||||
// each as a full cell visibly shifts the checkpoint box's right border.
|
||||
function isWideCodePoint(codePoint: number): boolean {
|
||||
return (
|
||||
(codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
||||
codePoint === 0x2329 || codePoint === 0x232a ||
|
||||
(codePoint >= 0x2e80 && codePoint <= 0x303e) || // CJK Radicals .. CJK Symbols and Punctuation
|
||||
(codePoint >= 0x3041 && codePoint <= 0x33ff) || // Hiragana .. CJK Compatibility
|
||||
(codePoint >= 0x3400 && codePoint <= 0x4dbf) || // CJK Unified Ideographs Extension A
|
||||
(codePoint >= 0x4e00 && codePoint <= 0x9fff) || // CJK Unified Ideographs
|
||||
(codePoint >= 0xa000 && codePoint <= 0xa4cf) || // Yi Syllables
|
||||
(codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables
|
||||
(codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs
|
||||
(codePoint >= 0xfe30 && codePoint <= 0xfe4f) || // CJK Compatibility Forms
|
||||
(codePoint >= 0xff00 && codePoint <= 0xff60) || // Fullwidth Forms
|
||||
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
||||
(codePoint >= 0x20000 && codePoint <= 0x3fffd) // CJK Unified Ideographs Extension B+ / supplementary
|
||||
);
|
||||
}
|
||||
|
||||
// Non-spacing/enclosing marks and format controls occupy zero terminal cells.
|
||||
// Spacing combining marks (General_Category=Mc), such as Devanagari vowel
|
||||
// signs, still advance the cursor and must contribute one column.
|
||||
const ZERO_WIDTH_MARK_RE = /\p{gc=Mn}|\p{gc=Me}|\p{gc=Cf}/u;
|
||||
|
||||
// Iterates by Unicode code point (not UTF-16 code unit) so astral characters
|
||||
// are measured once, not as two surrogate units.
|
||||
function displayWidth(text: string): number {
|
||||
let width = 0;
|
||||
for (const ch of text) {
|
||||
if (ZERO_WIDTH_MARK_RE.test(ch)) continue;
|
||||
width += isWideCodePoint(ch.codePointAt(0) as number) ? 2 : 1;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
// Pads `text` into a `║ text… ║` line matching CHECKPOINT_BOX_WIDTH. Content
|
||||
// that overflows the box (a longer translated string) is left unpadded rather
|
||||
// than truncated — a slightly ragged border beats losing text.
|
||||
function checkpointBoxLine(text: string): string {
|
||||
const innerWidth = CHECKPOINT_BOX_WIDTH - 2;
|
||||
const content = ` ${text}`;
|
||||
const padLength = innerWidth - displayWidth(content);
|
||||
const padded = padLength > 0 ? content + ' '.repeat(padLength) : content;
|
||||
return `║${padded}║`;
|
||||
}
|
||||
|
||||
const RTL_ISOLATE = '\u2067';
|
||||
const POP_DIRECTIONAL_ISOLATE = '\u2069';
|
||||
|
||||
@@ -593,17 +541,15 @@ function buildCheckpoint(currentTest: { number: number; name: string; expected:
|
||||
const banner = isolateCheckpointFrameText(frame.banner, frame);
|
||||
const instruction = isolateCheckpointFrameText(frame.instruction, frame);
|
||||
return [
|
||||
'╔══════════════════════════════════════════════════════════════╗',
|
||||
checkpointBoxLine(banner),
|
||||
'╚══════════════════════════════════════════════════════════════╝',
|
||||
`### ${banner}`,
|
||||
'',
|
||||
`**Test ${currentTest.number}: ${currentTest.name}**`,
|
||||
'',
|
||||
currentTest.expected,
|
||||
'',
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
instruction,
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
'---',
|
||||
'',
|
||||
`**${instruction}**`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -1676,7 +1622,6 @@ export = {
|
||||
CHECKPOINT_FRAMES,
|
||||
CHECKPOINT_LANGUAGE_ALIASES,
|
||||
resolveCheckpointFrame,
|
||||
checkpointBoxLine,
|
||||
parseDeferredItems,
|
||||
parseDeferredItemsWithStatus,
|
||||
acknowledgeDeferredItem,
|
||||
|
||||
Reference in New Issue
Block a user