design-lint: adversarial review tests and docs

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-26 14:39:27 +00:00
1 parent fd67b88c5e
commit f16be82fd3
4 files changed
+37 -6

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@
- `test/design-lint.test.ts`: the parsers (nested CSS, tokens, var() fallbacks, colour spaces, JSX pseudo-HTML), the rule table against `docs/design-lint.md`, the output lines and exit codes, waiver acceptance and every refusal reason, the waiver fixture. One test per rule family (`test/design-lint-<family>.test.ts`) holds a negative control and a planted positive for every rule id. `test/design-lint-render.test.ts` drives the browse daemon when it is built and skips with a named reason otherwise. `test/design-lint-recorded.test.ts` lints the repository's recorded design slop page (the review eval's ground truth) and pins which rule answers each annotated issue, the same gate set hallmark's sloplint reports on the same files.
### Gate integrity
- No threshold, budget, timeout, trial count or assertion was weakened. New code only; no template, catalog or eval changed.
- No threshold, budget, timeout, trial count or assertion was weakened. No template, catalog or eval changed. Gate edits in the diff: `test/fixtures/module-size-ratchet.json` registers the 27 new `lib/design-lint` modules under the existing 800-line cap, and `test/helpers/free-fixtures-data.ts` maps `test/fixtures/design-lint/**` to the design-lint test files; `lib/aside-render.ts` gains the viewport on eval steps and http(s) `file`, described above.
## [1.91.229.0] - 2026-10-26
+4 -4
View File
@@ -70,7 +70,7 @@ gstack/
├── freeze/ # /freeze skill; bin/check-freeze.sh (PreToolUse edit-boundary hook; sources careful/bin/hook-extract.sh, fails closed)
├── guard/, unfreeze/ # /guard (careful + freeze in one), /unfreeze
├── gstack-upgrade/ # /gstack-upgrade skill + migrations/ (run after ./setup during an upgrade)
├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-design-detect.ts = probe/scan through a user-installed impeccable engine; gstack-design-md.ts = open DESIGN.md check/convert/tokens/mark; gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, gstack-memorable, etc.)
├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-design-detect.ts = probe/scan through a user-installed impeccable engine; gstack-design-md.ts = open DESIGN.md check/convert/tokens/mark; gstack-design-lint = the free deterministic design lint over HTML/CSS/components, --render measures in the browser (docs/design-lint.md); gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, gstack-memorable, etc.)
├── document-release/ # /document-release skill (every-ship pre-verification audit; standalone doc updates + Diataxis coverage map)
├── document-generate/ # /document-generate skill (Diataxis doc generator: tutorial/how-to/reference/explanation)
├── cso/ # /cso skill (OWASP Top 10 + STRIDE security audit)
@@ -88,7 +88,7 @@ gstack/
│ └── dist/ # Compiled binary
├── agents-digest/ # Committed 2KB instruction-tier rules digest (gstack-AGENTS.md) for rules-reading hosts
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-lint/ = the static and render tiers behind bin/gstack-design-lint, ported from hallmark's sloplint; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
│ └── diagram-render/ # Vendored mermaid + excalidraw runtimes, built into one offline bundle the renderer loads
├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide)
├── docs/designs/ # Design documents (incl. IMPECCABLE_INTEROP.md = the design detector / catalog / open DESIGN.md record, and fork-port-residual-2026-09/ evaluation evidence)
@@ -102,7 +102,7 @@ gstack/
├── SKILL.md # Generated from SKILL.md.tmpl (don't edit directly)
├── SKILL.md.tmpl # Template: edit this, run gen:skill-docs
├── ETHOS.md # Builder philosophy (Boil the Ocean, Search Before Building)
├── NOTICE.md # Third-party notices: material derived from impeccable and the DESIGN.md spec (both Apache-2.0)
├── licenses/ # Verbatim license texts for the notices above (Apache-2.0.txt)
├── NOTICE.md # Third-party notices: material derived from impeccable and the DESIGN.md spec (Apache-2.0), Elaya's design skills and hallmark's sloplint (MIT)
├── licenses/ # Verbatim license texts for the notices above (Apache-2.0.txt, MIT-elaya.txt, MIT-hallmark.txt)
└── package.json # Build scripts for browse
```
+4 -1
View File
@@ -37,7 +37,8 @@ gstack-design-lint <file-or-dir ...> [--render] [--url <http://…>] [--scope pa
relative stylesheet href may not climb out of the project (the nearest
directory holding a `package.json` or `.git`), by its path and by its real
path, so a symlink inside the project that points outside it is not read
either; outside any project it is bounded only by the disk, and only `.css`
either; outside any project it is bounded by the working directory the
lint runs in, and only `.css`
files are read.
- Markup inside an HTML comment is dead for every rule, a commented-out
`<style>` or `<script>` included; a comment that never closes runs to the
@@ -54,6 +55,8 @@ gstack-design-lint <file-or-dir ...> [--render] [--url <http://…>] [--scope pa
descendant-joined) up to 8192 characters, and a sheet may store at most
four times its length plus 64 KB of expanded selectors; past either bound
the nested selector stands as written, so a sheet costs its length too.
Nesting is read 64 levels deep; a rule nested below that is dropped, not
parsed.
- A page is linted with the stylesheets it links (`<link rel=stylesheet>`,
`@import`, and anything they reach); a directory of components shares the
project's tokens and global state styles, so a focus ring declared once in
+28
View File
@@ -297,6 +297,34 @@ describe('design-lint eighth cycle: rule judgements', () => {
expect(stripTags(' '.repeat(5000) + 'hello world', false, 4).trim()).toBe('hello');
}, 60_000);
test('an escaped quote does not rewrite the rest of a sheet, and a declaration string may hold ; or }', () => {
const rules = parseCss('.q::before { content: "\\""; } .a { color: red } .b { content: "a;b}c"; color: blue } .c {}', 'x.css');
expect(rules.map((x) => x.selector)).toEqual(['.q::before', '.a', '.b', '.c']);
expect(rules[2].decls.map((d) => d.prop + '=' + d.value)).toEqual(['content="a;b}c"', 'color=blue']);
expect(parseCss(".x { font-family: 'It\\'s'; } .y { color: red }", 'y.css').map((x) => x.selector)).toEqual(['.x', '.y']);
});
test('a @starting-style block waives content-hidden-at-rest only for the selectors it names, whole', () => {
const sheet = '@starting-style { .reveal-wrap { opacity: 0 } } .reveal { opacity: 0; transform: translateY(24px) } .reveal-wrap { opacity: 0; transform: translateY(24px) } .fade { opacity: 0; transform: translateY(8px); @starting-style { opacity: 0 } }';
const { findings } = lintDir({ 'theme.css': TOKENS + '\n' + sheet, 'index.html': HEAD + '<link rel="stylesheet" href="theme.css"></head><body><main><h1>The ledger</h1><p class="reveal">a</p><p class="reveal-wrap">b</p><p class="fade">c</p></main></body></html>' });
expect(only(findings, 'content-hidden-at-rest').map((x) => x.evidence.split(' ')[0])).toEqual(['.reveal']);
});
test('aria-hidden must be "true" to count as hidden decoration', () => {
const page = (hidden: string) => HEAD + '</head><body><main><h1>The ledger</h1><svg ' + hidden + ' width="10" height="10"></svg></main></body></html>';
expect(only(lintDir({ 'index.html': page('aria-hidden="false"') }).findings, 'unlabeled-decoration').length).toBe(1);
expect(only(lintDir({ 'index.html': page('aria-hidden="true"') }).findings, 'unlabeled-decoration').length).toBe(0);
});
test('outside any project a relative stylesheet href is bounded by the working directory', () => {
const base = tree({ 'outside/secret.css': '.leak { color: #000; background: #fff }', 'site/index.html': HEAD + '<link rel="stylesheet" href="../outside/secret.css"></head><body><main><h1>The ledger</h1></main></body></html>' });
const site = path.join(base, 'site');
expect(lintStatic([site], { cwd: site, ...quiet }).findings.some((x) => x.file.includes('secret.css'))).toBe(false);
expect(lintStatic([site], { cwd: base, ...quiet }).findings.some((x) => x.file.includes('secret.css'))).toBe(true);
const inside = tree({ 'site/css/a.css': '.leak { color: #000; background: #fff }', 'site/index.html': HEAD + '<link rel="stylesheet" href="css/a.css"></head><body><main><h1>The ledger</h1></main></body></html>' });
expect(lintStatic([path.join(inside, 'site')], { cwd: path.join(inside, 'site'), ...quiet }).findings.some((x) => x.file.includes('a.css'))).toBe(true);
});
test('the emoji classes spell their variation selector and joiner as escapes', () => {
expect(fs.readFileSync(path.join(import.meta.dir, '../lib/design-lint/rules/assets.ts'), 'utf8')).not.toMatch(/[\uFE0F\u200D]/);
});