enhance(#3028): responsive Markdown separators instead of fixed-width rules (#3789)

* 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:
Tom Boucher
2026-08-23 22:38:12 -04:00
committed by GitHub
parent 107eb8c1d9
commit cf15682d1c
74 changed files with 849 additions and 699 deletions

View File

@@ -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');
}

View File

@@ -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}`,
'',

View File

@@ -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,