* 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:
@@ -15,7 +15,6 @@ const {
|
||||
CHECKPOINT_FRAMES,
|
||||
CHECKPOINT_LANGUAGE_ALIASES,
|
||||
resolveCheckpointFrame,
|
||||
checkpointBoxLine,
|
||||
parseDeferredItems,
|
||||
} = require('../gsd-core/bin/lib/uat.cjs');
|
||||
|
||||
@@ -932,12 +931,12 @@ describe('uat render-checkpoint', () => {
|
||||
|
||||
assert.ok(japanese.includes('チェックポイント'));
|
||||
assert.ok(japanese.includes('`pass`'));
|
||||
// Structural lines (borders, separators, Test N heading, expected content) are untouched.
|
||||
assert.ok(japanese.includes('╔══════════════════════════════════════════════════════════════╗'));
|
||||
assert.ok(japanese.includes('╚══════════════════════════════════════════════════════════════╝'));
|
||||
assert.ok(japanese.includes('──────────────────────────────────────────────────────────────'));
|
||||
// Structural lines (heading marker, separator, Test N heading, expected content) are untouched.
|
||||
assert.ok(japanese.includes('### チェックポイント: 検証が必要です'));
|
||||
assert.ok(japanese.includes('---'));
|
||||
assert.ok(japanese.includes('**Test 1: Sample**'));
|
||||
assert.ok(japanese.includes('Something happens.'));
|
||||
assert.ok(!/[╔╗╚╝║]/.test(japanese), 'the box border must be gone (#3028)');
|
||||
assert.notStrictEqual(japanese, english);
|
||||
});
|
||||
|
||||
@@ -1116,91 +1115,73 @@ describe('uat render-checkpoint', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// Regression: #2402 review medium finding — checkpointBoxLine() padded using
|
||||
// JS string `.length` (UTF-16 code units), not display width. The property
|
||||
// below supplies an independent, category-labelled cell-width oracle rather
|
||||
// than copying the implementation's Unicode range logic.
|
||||
describe('checkpoint banner padding uses terminal display width (#2402, #2530)', () => {
|
||||
test('property: category-labelled strings are padded to a 62-cell interior', () => {
|
||||
const oneCell = fc.constantFrom(
|
||||
{ text: 'a', width: 1 },
|
||||
{ text: '7', width: 1 },
|
||||
{ text: ' ', width: 1 },
|
||||
{ text: '\u093e', width: 1 }, // Mc: DEVANAGARI VOWEL SIGN AA
|
||||
{ text: '\u093f', width: 1 }, // Mc: DEVANAGARI VOWEL SIGN I
|
||||
{ text: '\u0949', width: 1 }, // Mc: DEVANAGARI VOWEL SIGN CANDRA O
|
||||
);
|
||||
const zeroCell = fc.constantFrom(
|
||||
{ text: '\u0301', width: 0 }, // Mn: COMBINING ACUTE ACCENT
|
||||
{ text: '\u093c', width: 0 }, // Mn: DEVANAGARI SIGN NUKTA
|
||||
{ text: '\u20dd', width: 0 }, // Me: COMBINING ENCLOSING CIRCLE
|
||||
{ text: '\u200d', width: 0 }, // Cf: ZERO WIDTH JOINER
|
||||
{ text: '\u2066', width: 0 }, // Cf: LEFT-TO-RIGHT ISOLATE
|
||||
{ text: '\u2069', width: 0 }, // Cf: POP DIRECTIONAL ISOLATE
|
||||
);
|
||||
const twoCell = fc.constantFrom(
|
||||
{ text: '界', width: 2 },
|
||||
{ text: '語', width: 2 },
|
||||
{ text: '한', width: 2 },
|
||||
);
|
||||
// Regression: #3028 — the checkpoint renderer no longer draws a 64-column
|
||||
// double-line box (checkpointBoxLine/displayWidth/isWideCodePoint/
|
||||
// ZERO_WIDTH_MARK_RE/CHECKPOINT_BOX_WIDTH were removed from src/uat.cts).
|
||||
// These cases now pin the heading form (`### {banner}`) directly instead of
|
||||
// a padded box interior; the localized-language coverage that used to prove
|
||||
// display-width-correct padding now proves the banner text is emitted
|
||||
// intact, unpadded, and box-free.
|
||||
describe('checkpoint banner renders as a heading, not a box (#2402, #2530, #3028)', () => {
|
||||
test('exact rendered banner heading for Japanese/Chinese/Korean (regression pin)', () => {
|
||||
const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' };
|
||||
const japanese = buildCheckpoint(currentTest, 'Japanese');
|
||||
const chinese = buildCheckpoint(currentTest, 'Chinese');
|
||||
const korean = buildCheckpoint(currentTest, 'Korean');
|
||||
|
||||
fc.assert(fc.property(
|
||||
fc.array(fc.oneof(oneCell, zeroCell, twoCell), { maxLength: 35 }),
|
||||
(cells) => {
|
||||
const text = cells.map((cell) => cell.text).join('');
|
||||
const textWidth = cells.reduce((sum, cell) => sum + cell.width, 0);
|
||||
const padding = ' '.repeat(Math.max(0, 60 - textWidth));
|
||||
assert.strictEqual(
|
||||
checkpointBoxLine(text),
|
||||
`║ ${text}${padding}║`,
|
||||
);
|
||||
},
|
||||
));
|
||||
});
|
||||
assert.strictEqual(japanese.split('\n')[0], '### チェックポイント: 検証が必要です');
|
||||
assert.strictEqual(chinese.split('\n')[0], '### 检查点:需要验证');
|
||||
assert.strictEqual(korean.split('\n')[0], '### 체크포인트: 검증 필요');
|
||||
|
||||
test('padding boundary: width limit-1, limit, and limit+1', () => {
|
||||
for (const width of [59, 60, 61]) {
|
||||
const text = 'a'.repeat(width);
|
||||
assert.strictEqual(
|
||||
checkpointBoxLine(text),
|
||||
`║ ${text}${' '.repeat(Math.max(0, 60 - width))}║`,
|
||||
`unexpected rendering at text width ${width}`,
|
||||
);
|
||||
for (const output of [japanese, chinese, korean]) {
|
||||
assert.ok(!/[╔╗╚╝║]/.test(output), 'the box border must be gone (#3028)');
|
||||
}
|
||||
});
|
||||
|
||||
test('exact rendered banner lines for Japanese/Chinese/Korean (regression pin)', () => {
|
||||
test('exact rendered Hindi banner heading ignores combining-mark cell width (regression pin)', () => {
|
||||
const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' };
|
||||
assert.strictEqual(
|
||||
buildCheckpoint(currentTest, 'Japanese').split('\n')[1],
|
||||
'║ チェックポイント: 検証が必要です ║',
|
||||
);
|
||||
assert.strictEqual(
|
||||
buildCheckpoint(currentTest, 'Chinese').split('\n')[1],
|
||||
'║ 检查点:需要验证 ║',
|
||||
);
|
||||
assert.strictEqual(
|
||||
buildCheckpoint(currentTest, 'Korean').split('\n')[1],
|
||||
'║ 체크포인트: 검증 필요 ║',
|
||||
);
|
||||
});
|
||||
|
||||
test('exact rendered Hindi banner line ignores combining-mark cell width (regression pin)', () => {
|
||||
const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' };
|
||||
assert.strictEqual(
|
||||
buildCheckpoint(currentTest, 'Hindi').split('\n')[1],
|
||||
`║ चेकपॉइंट: सत्यापन आवश्यक${' '.repeat(40)}║`,
|
||||
);
|
||||
const hindi = buildCheckpoint(currentTest, 'Hindi');
|
||||
assert.strictEqual(hindi.split('\n')[0], '### चेकपॉइंट: सत्यापन आवश्यक');
|
||||
assert.ok(!/[╔╗╚╝║]/.test(hindi), 'the box border must be gone (#3028)');
|
||||
});
|
||||
|
||||
test('exact rendered Arabic frame is isolated inside the LTR checkpoint layout', () => {
|
||||
const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' };
|
||||
const arabic = buildCheckpoint(currentTest, 'Arabic');
|
||||
// The one behavior the box removal must not disturb: the RTL banner and
|
||||
// instruction text stay wrapped in directional isolates.
|
||||
assert.strictEqual(
|
||||
arabic.split('\n')[1],
|
||||
`║ \u2067نقطة تحقق: المراجعة مطلوبة\u2069${' '.repeat(34)}║`,
|
||||
arabic.split('\n')[0],
|
||||
`### نقطة تحقق: المراجعة مطلوبة`,
|
||||
);
|
||||
assert.ok(arabic.includes('\u2067اكتب `pass` أو صف المشكلة.\u2069'));
|
||||
assert.ok(arabic.includes('اكتب `pass` أو صف المشكلة.'));
|
||||
assert.ok(!/[╔╗╚╝║]/.test(arabic), 'the box border must be gone (#3028)');
|
||||
});
|
||||
|
||||
test('emits an over-long banner intact (no box to overflow)', (t) => {
|
||||
// Previously a banner exceeding the 64-column inner width produced a
|
||||
// ragged, unpadded border. Now there is no border to overflow — the
|
||||
// full heading text is emitted intact regardless of length. None of the
|
||||
// shipped frames are long enough to exercise this, so a synthetic frame
|
||||
// is registered on the exported (mutable) lookup tables for the
|
||||
// duration of the test.
|
||||
const longBanner = `${'X'.repeat(80)}: Verification required well beyond the old 64-column box width`;
|
||||
const frameKey = '__test_overlong_frame__3028__';
|
||||
const aliasKey = '__test_overlong_alias__3028__';
|
||||
CHECKPOINT_FRAMES[frameKey] = {
|
||||
banner: longBanner,
|
||||
instruction: 'Type `pass` or describe what\'s wrong.',
|
||||
};
|
||||
CHECKPOINT_LANGUAGE_ALIASES[aliasKey] = frameKey;
|
||||
t.after(() => {
|
||||
delete CHECKPOINT_FRAMES[frameKey];
|
||||
delete CHECKPOINT_LANGUAGE_ALIASES[aliasKey];
|
||||
});
|
||||
const currentTest = { number: 1, name: 'Sample', expected: 'Something happens.' };
|
||||
const output = buildCheckpoint(currentTest, aliasKey);
|
||||
assert.strictEqual(output.split('\n')[0], `### ${longBanner}`,
|
||||
'an over-long banner must be emitted in full, not truncated or wrapped');
|
||||
assert.ok(!/[╔╗╚╝║]/.test(output), 'no box characters should appear regardless of banner length');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1224,7 +1205,9 @@ awaiting: user response
|
||||
assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`);
|
||||
assert.ok(result.output.includes('**Test 2: Submit form validation**'));
|
||||
assert.ok(result.output.includes('Empty submit keeps controls visible.'));
|
||||
assert.ok(result.output.includes("Type `pass` or describe what's wrong."));
|
||||
// The instruction line renders as a bold line preceded by a `---` thematic
|
||||
// break, not inside a box border (#3028).
|
||||
assert.ok(result.output.includes("---\n\n**Type `pass` or describe what's wrong.**"));
|
||||
});
|
||||
|
||||
test('strips protocol leak lines from current test copy', () => {
|
||||
@@ -1440,14 +1423,14 @@ awaiting: user response
|
||||
assert.ok(result.output.includes('Verificación requerida'), 'banner should be in Spanish');
|
||||
assert.ok(result.output.includes('Escribe `pass`'), 'instruction line should be in Spanish');
|
||||
|
||||
// Structure/IDs stay untranslated: box borders, the Test N: name line, and the
|
||||
// expected content are preserved verbatim.
|
||||
assert.ok(result.output.includes('╔══════════════════════════════════════════════════════════════╗'));
|
||||
assert.ok(result.output.includes('╚══════════════════════════════════════════════════════════════╝'));
|
||||
assert.ok(result.output.includes('──────────────────────────────────────────────────────────────'));
|
||||
// Structure/IDs stay untranslated: the heading marker, the `---` separator,
|
||||
// the Test N: name line, and the expected content are preserved verbatim.
|
||||
assert.ok(result.output.includes('### PUNTO DE CONTROL: Verificación requerida'));
|
||||
assert.ok(result.output.includes('---'));
|
||||
assert.ok(result.output.includes('**Test 2: Submit form validation**'));
|
||||
assert.ok(result.output.includes('Empty submit keeps controls visible.'));
|
||||
assert.ok(result.output.includes('Validation error copy is shown.'));
|
||||
assert.ok(!/[╔╗╚╝║]/.test(result.output), 'the box border must be gone (#3028)');
|
||||
});
|
||||
|
||||
// Regression guard for the "unset ⇒ byte-identical English" acceptance criterion.
|
||||
@@ -1471,17 +1454,15 @@ awaiting: user response
|
||||
assert.strictEqual(result.success, true, `render-checkpoint failed: ${result.error}`);
|
||||
|
||||
const expected = [
|
||||
'╔══════════════════════════════════════════════════════════════╗',
|
||||
'║ CHECKPOINT: Verification Required ║',
|
||||
'╚══════════════════════════════════════════════════════════════╝',
|
||||
'### CHECKPOINT: Verification Required',
|
||||
'',
|
||||
'**Test 2: Submit form validation**',
|
||||
'',
|
||||
'Empty submit keeps controls visible.\nValidation error copy is shown.',
|
||||
'',
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
'Type `pass` or describe what\'s wrong.',
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
'---',
|
||||
'',
|
||||
'**Type `pass` or describe what\'s wrong.**',
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(result.output, expected);
|
||||
|
||||
Reference in New Issue
Block a user