fix: pre-landing review fixes

- /design-review files only craft-gate lint lines on a saved page, whose
  linked CSS the lint cannot load (false missing-focus-visible blocks)
- missing-aria-current counts route links in one pass (unclosed navs were
  superlinear); inline-handler tells report at the value's line; a shell
  with no <body> tag is an empty mount node
- /design-html says the copy tier reads HTML only, and that a supplied
  figure may also need an invented-proof waiver
- tests pin the five content-realism ids at ask and a waived copy tell

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-27 03:52:57 +00:00
1 parent 8c021d1242
commit b5c44b972a
9 files changed
+39 -14

No files matched your search

+2 -2
View File
@@ -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 <path-to-finalized.html> --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: <grade> <rule-id> <file>:<line> <evidence> | fix: <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, `<!-- gstack-lint-disable <rule-id>: <reason about this page, 24+ chars> -->`, 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, `<!-- gstack-lint-disable <rule-id>: <reason about this page, 24+ chars> -->`, 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.
+2 -2
View File
@@ -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 <path-to-finalized.html> --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: <grade> <rule-id> <file>:<line> <evidence> | fix: <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, `<!-- gstack-lint-disable <rule-id>: <reason about this page, 24+ chars> -->`, 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, `<!-- gstack-lint-disable <rule-id>: <reason about this page, 24+ chars> -->`, 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.
+1 -1
View File
@@ -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 <path-to-page.html>
```
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
+1 -1
View File
@@ -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 <path-to-page.html>
```
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
+1 -1
View File
@@ -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
+16 -6
View File
@@ -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);
+1
View File
@@ -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');
});
+1 -1
View File
@@ -95,7 +95,7 @@ describe('design-lint eighth cycle: a file costs its length', () => {
}, 60_000);
test('repeated unclosed <script>, <nav>, <style> and <template> openers cost the copy tier their length', () => {
for (const tag of ['<script>', '<nav>', '<style>', '<template>']) {
for (const tag of ['<script>', '<nav>', '<style>', '<template>', '<nav><a href="/a">A</a>']) {
const doc = parseHtml('x.html', HEAD + '</head><body>' + tag.repeat(60_000) + '</body></html>');
const reporter = new Reporter('page', '/');
expect(timed(() => runCopy({ docs: [doc] } as never, reporter)), tag).toBeLessThan(3000);
+14
View File
@@ -393,6 +393,20 @@ describe('design-lint: the copy tier', () => {
expect(copy(shell, 'component').some((f) => f.rule === 'empty-mount-node')).toBe(false);
});
// Value: protects=the five content-realism ids sit at ask so a page waiver answers a supplied name or figure, as the owner chose; fails_when=one moves back to blocks, or a waiver beside a copy-tier tell is refused or ignored; why_new=option (a) moved them and no test pinned the tier or the waived grade; seam=none; no_claim=how far one page waiver reaches
test('the content-realism ids sit at ask, and a waiver about the page answers a copy-tier tell', () => {
for (const id of ['placeholder-name', 'placeholder-brand', 'lorem', 'round-number', 'needs-proof']) expect(RULES[id].tier, id).toBe('ask');
const page = '<!doctype html><html lang="en"><head><title>Freight ledger</title></head><body>\n<!-- gstack-lint-disable placeholder-brand: Nexus is the freight customer this ledger page was built for -->\n<p>Nexus closes its freight ledger in two days.</p></body></html>';
expect(copy(page).filter((f) => f.rule === 'placeholder-brand').map((f) => [f.grade, f.line, f.waiver])).toEqual([['waived', 3, 'Nexus is the freight customer this ledger page was built for']]);
});
// Value: protects=an inline handler's tell reports at the line its code is on, and a shell with no <body> tag is still an empty mount node; fails_when=the handler position is the attribute's start, or the <title> text counts as body text; why_new=the /ship review found both; seam=none; no_claim=every implicit-body layout
test('an inline handler reports at its value\'s line; a shell without a <body> tag is an empty mount node', () => {
expect(copy('<button\nonclick="alert(1)">Go</button>').map((f) => [f.rule, f.line])).toEqual([['alert', 2]]);
const bare = '<!doctype html><html lang="en"><head><title>App</title></head>\n<div id="root"></div><script src="/app.js"></script></html>';
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 = () => <article className="p-4"><p>Lorem ipsum in a component</p><img src="c.png" /></article>;' });
expect(lintStatic([dir], { cwd: dir, warn: () => {} }).findings.some((f) => f.source === 'copy')).toBe(false);