From 12aa1e51d66fafbc5029d581d064ee2eb4a7dede Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 19 Oct 2026 22:00:42 -0700 Subject: [PATCH] test(workspace): accept only #249 decision markers and state the lint scope The overlay lint accepted any D-number, so another issue's ruling such as "D5 (#245)" could mark a concept override. A marker now has to be a D-number with no other issue cited; the vocabulary rule that cited #245 reads "#245 decision 5". TESTING.md now states that the lint covers workspace.css only, lists the unmarked overrides still in images.css, and says PRs C and D run the lint over each page sheet they keep. --- TESTING.md | 20 +++++++++++++++----- tests/vendor/concept-css.test.mjs | 11 +++++++++++ tools/vendor/sync-concept-css.mjs | 11 +++++++++-- web/js/workspace/workspace.css | 2 +- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/TESTING.md b/TESTING.md index 001ab96..0e91696 100644 --- a/TESTING.md +++ b/TESTING.md @@ -319,11 +319,21 @@ other page or canvas script loads workspace CSS. **Overlay decision-marker lint.** `lintOverlay` in `tools/vendor/sync-concept-css.mjs` fails any `workspace.css` rule whose selector is also a concept selector unless the -rule carries a `/* D */` comment, before its selector or inside its block. The lint -test fails if `workspace.css` is missing. The match is by exact selector, so a narrower -selector over a concept rule (for example `.ca-tools dialog.prompt-sender`) passes the -lint. Such rules, and the page-stylesheet leftovers such as `library.css` -`.row-actions`, name the PR that retires them, by convention. +rule carries a `/* D */` comment, before its selector or inside its block. The +D-number must be one of this spec's (#249): a comment that cites any other issue, such +as `D5 (#245)`, is that issue's ruling and is not a marker, so write `#245 decision 5` +for those. The lint test fails if `workspace.css` is missing. The match is by exact +selector, so a narrower selector over a concept rule (for example +`.ca-tools dialog.prompt-sender`) passes the lint. Such rules, and the page-stylesheet +leftovers such as `library.css` `.row-actions`, name the PR that retires them, by +convention. + +The lint covers `workspace.css` only. The page stylesheets that load before it +(`images.css`, `library.css`, `inspector.css`) are not linted yet, and `images.css` +still has unmarked concept-selector overrides (`.image-grid`, `.prompt-text`, +`.prompt-actions`, `.ca-tools .prompt-actions button`). PRs C and D retire those +sheets: each one runs `lintOverlay` over every page sheet it keeps, and adds that +sheet to the real-file lint test. **Comparison method.** Production must equal the concept except for the recorded deviations in the #249 design (spec ยง8, D-numbers). It is never compared against diff --git a/tests/vendor/concept-css.test.mjs b/tests/vendor/concept-css.test.mjs index 1de1bce..0f6b27c 100644 --- a/tests/vendor/concept-css.test.mjs +++ b/tests/vendor/concept-css.test.mjs @@ -55,6 +55,17 @@ test('overlay lint requires a decision marker on concept selectors', () => { assert.equal(lintOverlay(conceptCss, '/* D1 */ .card { color: blue; }\n.row { gap: 1px; }').violations.length, 1); }); +test('overlay lint accepts only this spec\'s (#249) decision numbers', () => { + const conceptCss = '.card { color: red; }\n'; + assert.deepEqual(lintOverlay(conceptCss, '/* D12 of #249 */ .card { color: blue; }').violations, []); + assert.deepEqual(lintOverlay(conceptCss, '/* #249 D3: reason */ .card { color: blue; }').violations, []); + // Another issue's decision numbers are not #249 rulings, so they never mark a concept override. + assert.equal(lintOverlay(conceptCss, '/* D5 (#245) */ .card { color: blue; }').violations.length, 1); + assert.equal(lintOverlay(conceptCss, '/* D2 of #240 and #249 */ .card { color: blue; }').violations.length, 1); + assert.equal(lintOverlay(conceptCss, '/* #245 decision 5 */ .card { color: blue; }').violations.length, 1); + assert.equal(lintOverlay(conceptCss, '/* AD5 */ .card { color: blue; }').violations.length, 1); +}); + test('overlay rules on concept selectors carry a decision marker', () => { const overlay = join(root, 'web/js/workspace/workspace.css'); assert.ok(existsSync(overlay), 'web/js/workspace/workspace.css is missing'); diff --git a/tools/vendor/sync-concept-css.mjs b/tools/vendor/sync-concept-css.mjs index 0592c29..524b318 100644 --- a/tools/vendor/sync-concept-css.mjs +++ b/tools/vendor/sync-concept-css.mjs @@ -6,6 +6,13 @@ import { repositoryRoot } from './vendor-viewerjs.mjs'; const CONCEPT = 'docs/concepts/workspace/style.css'; const COPY = 'web/js/workspace/concept.css'; const MARKER = /\/\*[^*]*\bD\d+\b[^*]*\*\//; +const SPEC_ISSUE = '#249'; + +// A decision marker names one of this spec's (#249) D-numbers. A comment that cites any other issue is that issue's +// ruling (for example #245's own D5), so it never marks a concept override here. +function isDecisionMarker(comment) { + return MARKER.test(comment) && (comment.match(/#\d+\b/g) ?? []).every(issue => issue === SPEC_ISSUE); +} // Minimal tokenizer: walks top-level rules and the bodies of @media/@supports, // returning each rule's normalized selectors and the comments attached to it @@ -84,8 +91,8 @@ export function lintOverlay(conceptCss, overlayCss) { const violations = []; for (const rule of parseRules(overlayCss)) { const shared = rule.selectors.filter(selector => owned.has(selector)); - if (shared.length && !rule.comments.some(comment => MARKER.test(comment))) { - violations.push(`${shared.join(', ')}: overlay rule on a concept selector needs a /* D */ decision marker`); + if (shared.length && !rule.comments.some(isDecisionMarker)) { + violations.push(`${shared.join(', ')}: overlay rule on a concept selector needs a /* D */ (#249) decision marker`); } } return { violations }; diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css index b8f59c4..d888b57 100644 --- a/web/js/workspace/workspace.css +++ b/web/js/workspace/workspace.css @@ -43,7 +43,7 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; } /* Long tag names wrap inside the Tag cell so the Actions column keeps its intrinsic width; on two-column screens the minimum keeps the concept's longest sample ("architecture", about 5em) on one line. */ .vocabulary-main td:first-child { overflow-wrap: anywhere; min-width: calc(6em + 20px); } -/* D5 (#245): bounded lists continue with Load more; never a scroll anchor, so appended rows push the end away. */ +/* Tags & categories (#245 decision 5): bounded lists continue with Load more; never a scroll anchor, so appended rows push the end away. */ .vocabulary-main .vocabulary-continuation { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding-block: 15px 0; overflow-anchor: none; } .vocabulary-main .vocabulary-continuation p { margin: 0; flex: 1 1 200px; font-size: 12px; color: var(--ca-muted); } .vocabulary-main .vocabulary-continuation .vocabulary-error { color: var(--ca-error-text); }