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