diff --git a/design-html/sections/craft.md b/design-html/sections/craft.md index 131eb1f3b..6cb3cb19a 100644 --- a/design-html/sections/craft.md +++ b/design-html/sections/craft.md @@ -59,7 +59,7 @@ ### Design lint -At Step 4, after the Slop Gate and before the screenshots, run the free lint on the finalized page (`--scope component` for a component file alone): +At Step 4, after the Slop Gate and before the screenshots, run the free lint on the finalized page (`--scope component` for a component file alone; its copy tier reads HTML only, so judge those tells by eye there): ```bash ~/.claude/skills/gstack/bin/gstack-design-lint --render @@ -67,7 +67,7 @@ At Step 4, after the Slop Gate and before the screenshots, run the free lint on It prints one `LINT: : | fix: ` line per tell (the realism rules above among them), then `LINT_SUMMARY: blocks=N ask=N waived=N polish=N render=…`. Read the exit code: 2, fix every `blocks` line (never waivable); 1, fix each `ask` line or waive it; 0, list the `polish` lines as notes. No `LINT_SUMMARY` line: say the lint was unavailable and continue. A `render` line or `render=skipped`: say the page was not measured; the static lines stand. Same contract as the Slop Gate: one surgical fix pass, one rerun. Rerun once when the refinement loop exits; report only new lines. -**Waivers live in the page.** A tell that is right for this page (a real customer named Nexus, a supplied 50% figure) gets a comment beside it, ``, and prints as `waived` with its reason on every later run, an evolve included. Waive only what the user supplied or confirmed, never a line you could fix. +**Waivers live in the page.** A tell that is right for this page (a real customer named Nexus, a supplied 50% figure, which `invented-proof` may also flag) gets a comment per rule, ``, and prints as `waived` with its reason on every later run, an evolve included. Waive only what the user supplied or confirmed, never a line you could fix. **Before this can ship.** Open the Step 4 hand-off, and repeat before Step 5's "Copy to project": every `[Needs proof: …]` marker, the dependencies the page cannot own (legal URLs, form endpoint, og:image, indexing, analytics and cookie consent where required), every waiver, and every claim the lint made you change. diff --git a/design-html/sections/craft.md.tmpl b/design-html/sections/craft.md.tmpl index cbed4fe2c..a0dbcc799 100644 --- a/design-html/sections/craft.md.tmpl +++ b/design-html/sections/craft.md.tmpl @@ -57,7 +57,7 @@ ### Design lint -At Step 4, after the Slop Gate and before the screenshots, run the free lint on the finalized page (`--scope component` for a component file alone): +At Step 4, after the Slop Gate and before the screenshots, run the free lint on the finalized page (`--scope component` for a component file alone; its copy tier reads HTML only, so judge those tells by eye there): ```bash {{BIN_DIR}}/gstack-design-lint --render @@ -65,7 +65,7 @@ At Step 4, after the Slop Gate and before the screenshots, run the free lint on It prints one `LINT: : | fix: ` line per tell (the realism rules above among them), then `LINT_SUMMARY: blocks=N ask=N waived=N polish=N render=…`. Read the exit code: 2, fix every `blocks` line (never waivable); 1, fix each `ask` line or waive it; 0, list the `polish` lines as notes. No `LINT_SUMMARY` line: say the lint was unavailable and continue. A `render` line or `render=skipped`: say the page was not measured; the static lines stand. Same contract as the Slop Gate: one surgical fix pass, one rerun. Rerun once when the refinement loop exits; report only new lines. -**Waivers live in the page.** A tell that is right for this page (a real customer named Nexus, a supplied 50% figure) gets a comment beside it, ``, and prints as `waived` with its reason on every later run, an evolve included. Waive only what the user supplied or confirmed, never a line you could fix. +**Waivers live in the page.** A tell that is right for this page (a real customer named Nexus, a supplied 50% figure, which `invented-proof` may also flag) gets a comment per rule, ``, and prints as `waived` with its reason on every later run, an evolve included. Waive only what the user supplied or confirmed, never a line you could fix. **Before this can ship.** Open the Step 4 hand-off, and repeat before Step 5's "Copy to project": every `[Needs proof: …]` marker, the dependencies the page cannot own (legal URLs, form endpoint, og:image, indexing, analytics and cookie consent where required), every waiver, and every claim the lint made you change. diff --git a/design-review/sections/craft-audit.md b/design-review/sections/craft-audit.md index 462ca19ea..74363a93b 100644 --- a/design-review/sections/craft-audit.md +++ b/design-review/sections/craft-audit.md @@ -12,7 +12,7 @@ With a URL, save the page in the script that opens it. Aside: add `await fs.writ ~/.claude/skills/gstack/bin/gstack-design-lint ``` -Each `blocks` or `ask` line is one category 9 finding under its rule id and line; `polish` lines are notes. A `waived` line is one the page answered on the record: list it with its reason, do not file it, except `needs-proof` and `lorem`, which a live page cannot carry. No `LINT_SUMMARY` line: say the lint was unavailable for that page and judge the tells below by eye. `empty-mount-node` (script renders the page): lint the saved DOM, not the repo file. A saved page holds only inline CSS and JS: judge tells in linked stylesheets by eye. A hit real user content explains (a customer named Nexus) is noted, not filed. Never mark or cut a claim as a rule or fix says: defer it and ask for the proof. +Each `blocks` or `ask` line is one category 9 finding under its rule id and line; `polish` lines are notes. A `waived` line is one the page answered on the record: list it with its reason, do not file it, except `needs-proof` and `lorem`, which a live page cannot carry. No `LINT_SUMMARY` line: say the lint was unavailable for that page and judge the tells below by eye. `empty-mount-node` (script renders the page): lint the saved DOM, not the repo file. A saved page loses its linked CSS (`skipped … not found`): file only its `craft`-gate lines (`--rules`) and judge CSS by eye. A hit real user content explains (a customer named Nexus) is noted, not filed. Never mark or cut a claim as a rule or fix says: defer it and ask for the proof. ### 2. What the lint cannot see diff --git a/design-review/sections/craft-audit.md.tmpl b/design-review/sections/craft-audit.md.tmpl index 28df0acd9..c36a27552 100644 --- a/design-review/sections/craft-audit.md.tmpl +++ b/design-review/sections/craft-audit.md.tmpl @@ -10,7 +10,7 @@ With a URL, save the page in the script that opens it. Aside: add `await fs.writ {{BIN_DIR}}/gstack-design-lint ``` -Each `blocks` or `ask` line is one category 9 finding under its rule id and line; `polish` lines are notes. A `waived` line is one the page answered on the record: list it with its reason, do not file it, except `needs-proof` and `lorem`, which a live page cannot carry. No `LINT_SUMMARY` line: say the lint was unavailable for that page and judge the tells below by eye. `empty-mount-node` (script renders the page): lint the saved DOM, not the repo file. A saved page holds only inline CSS and JS: judge tells in linked stylesheets by eye. A hit real user content explains (a customer named Nexus) is noted, not filed. Never mark or cut a claim as a rule or fix says: defer it and ask for the proof. +Each `blocks` or `ask` line is one category 9 finding under its rule id and line; `polish` lines are notes. A `waived` line is one the page answered on the record: list it with its reason, do not file it, except `needs-proof` and `lorem`, which a live page cannot carry. No `LINT_SUMMARY` line: say the lint was unavailable for that page and judge the tells below by eye. `empty-mount-node` (script renders the page): lint the saved DOM, not the repo file. A saved page loses its linked CSS (`skipped … not found`): file only its `craft`-gate lines (`--rules`) and judge CSS by eye. A hit real user content explains (a customer named Nexus) is noted, not filed. Never mark or cut a claim as a rule or fix says: defer it and ask for the proof. ### 2. What the lint cannot see diff --git a/lib/design-catalog.ts b/lib/design-catalog.ts index 96ee04d84..e07aa09b7 100644 --- a/lib/design-catalog.ts +++ b/lib/design-catalog.ts @@ -12,7 +12,7 @@ // │ design-html anti-slop line (catalogEntries) // ├─ scripts/resolvers/design-checklist.ts review/design-checklist.md (generated) // ├─ bin/gstack-design-detect.ts normalizes engine findings by impeccableId -// ├─ lib/design-lint/rules/ids.ts copy-tier ids print the catalog's `fix` (test-enforced) +// ├─ lib/design-lint/rules/ids.ts copy-tier ids but empty-mount-node print the catalog's `fix` (test-enforced) // └─ design/src/brief.ts MOCKUP_NEVER_NAMES in the image-generation prompt // // Rule ids. An entry's `impeccableId` is set only when that id exists in diff --git a/lib/design-lint/rules/copy.ts b/lib/design-lint/rules/copy.ts index 8be644ac7..814618e2d 100644 --- a/lib/design-lint/rules/copy.ts +++ b/lib/design-lint/rules/copy.ts @@ -88,7 +88,10 @@ function codeOf(doc: Doc): Array<{ code: string; pos: number }> { doc.tags.forEach((t, i) => { if (t.close) return; if (t.name === 'script' && close[i] >= 0) out.push({ code: doc.src.slice(t.end, doc.tags[close[i]].pos), pos: t.end }); - for (const m of t.attrs.matchAll(/(?:^|\s)on[a-z]+\s*=\s*(?:"([^"]*)"|'([^']*)')/gi)) out.push({ code: m[1] ?? m[2], pos: t.pos + 1 + t.name.length + m.index! }); + for (const m of t.attrs.matchAll(/(?:^|\s)on[a-z]+\s*=\s*(?:"([^"]*)"|'([^']*)')/gi)) { + const code = m[1] ?? m[2]; + out.push({ code, pos: t.pos + 1 + t.name.length + m.index! + m[0].length - 1 - code.length }); + } }); return out; } @@ -129,12 +132,19 @@ export function run(ctx: LintContext, r: Reporter): void { ?? doc.tags.find((t) => !t.close && getAttr(t.attrs, 'role') === 'main' && getAttr(t.attrs, 'id')); if (!skipTarget || skipTarget !== getAttr(mainTag?.attrs ?? '', 'id')) hit('missing-skip-link', bodyTag?.pos ?? at0, ''); const { close } = treeOf(doc); - const navs = doc.tags.map((t, i) => [t, i] as const).filter(([t]) => !t.close && t.name === 'nav'); - const routeLinks = navs.flatMap(([, i]) => doc.tags.slice(i, close[i] < 0 ? doc.tags.length : close[i])) - .filter((t) => !t.close && t.name === 'a' && /^[^#]/.test(getAttr(t.attrs, 'href') ?? '')).length; - if (routeLinks >= 2 && !doc.tags.some((t) => !t.close && getAttr(t.attrs, 'aria-current') === 'page')) hit('missing-aria-current', navs[0][0].pos, ''); + let firstNav = -1, navEnd = -1, routeLinks = 0; + for (let i = 0; i < doc.tags.length && routeLinks < 2; i++) { + const t = doc.tags[i]; + if (t.close) continue; + if (t.name === 'nav') { + if (firstNav < 0) firstNav = i; + navEnd = Math.max(navEnd, close[i] < 0 ? doc.tags.length : close[i]); + } else if (i < navEnd && t.name === 'a' && /^[^#]/.test(getAttr(t.attrs, 'href') ?? '')) routeLinks++; + } + if (routeLinks >= 2 && !doc.tags.some((t) => !t.close && getAttr(t.attrs, 'aria-current') === 'page')) hit('missing-aria-current', doc.tags[firstNav].pos, ''); } - const bodyFrom = bodyTag?.pos ?? 0; + const headClose = head >= 0 ? treeOf(doc).close[head] : -1; + const bodyFrom = bodyTag?.pos ?? (headClose >= 0 ? doc.tags[headClose].end : 0); const shellText = visible(doc, new Set(['template', 'noscript'])); const bodyStart = shellText.at.findIndex((p) => p >= bodyFrom); const bodyText = bodyStart < 0 ? '' : shellText.text.slice(bodyStart); diff --git a/test/design-craft-resolvers.test.ts b/test/design-craft-resolvers.test.ts index ada0527f1..d05d961df 100644 --- a/test/design-craft-resolvers.test.ts +++ b/test/design-craft-resolvers.test.ts @@ -48,6 +48,7 @@ describe('craft resolvers', () => { expect(audit).toContain('`empty-mount-node` (script renders the page): lint the saved DOM, not the repo file.'); expect(audit).toContain('A `waived` line is one the page answered on the record: list it with its reason, do not file it, except `needs-proof` and `lorem`'); expect(audit).not.toContain('gstack-design-craft'); + expect(audit).toContain('A saved page loses its linked CSS (`skipped … not found`): file only its `craft`-gate lines'); expect(audit).toContain('Never mark or cut a claim as a rule or fix says: defer it and ask for the proof.'); expect(audit).toContain('never copy it to the report directory'); }); diff --git a/test/design-lint-eighth-cycle.test.ts b/test/design-lint-eighth-cycle.test.ts index 76737f663..e4a3cdd34 100644 --- a/test/design-lint-eighth-cycle.test.ts +++ b/test/design-lint-eighth-cycle.test.ts @@ -95,7 +95,7 @@ describe('design-lint eighth cycle: a file costs its length', () => { }, 60_000); test('repeated unclosed '; + expect(copy(bare).filter((f) => f.rule === 'empty-mount-node').map((f) => f.line)).toEqual([2]); + }); + test('a component file gets no copy findings', () => { const dir = tmpTree({ 'Card.tsx': 'export const Card = () =>

Lorem ipsum in a component

;' }); expect(lintStatic([dir], { cwd: dir, warn: () => {} }).findings.some((f) => f.source === 'copy')).toBe(false);