design-craft: reconcile ids with the design lint (layout-transition is static-tier; legacy transition-all exceptions map to it)

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 17:38:44 +00:00
1 parent 2ad579839e
commit 8cb08f20a0
5 files changed
+11 -6

No files matched your search

+2
View File
@@ -6,12 +6,14 @@
- **/design-review audits a Persuade page or a redesign for craft, or on request** (`sections/craft-audit.md`, read in Phases 1-6; design-review is now carved on every host, so the audit is one lazy section, not inlined text). It runs `bin/gstack-design-craft` on each page's served HTML (or the repo file when no app runs) and files each `CRAFT_CHECK` line as a category 9 finding (no `CRAFT_RESULT` line means the check was unavailable, and a framework mount-node shell is judged by eye too; the saved page holds only inline CSS and JS, so `100vh`, `transition: all`, scroll listeners and `alert()` are also judged by eye; it also holds session data, so it is deleted in the block that checks it, whatever the exit code, and never copied to the report; `--accepted` takes that screen's finalized.json, only for a page /design-html finalized, and never covers `needs-proof` or `lorem`; a claim without proof is filed and asked about, never marked or cut); checks what the tool cannot see (claims only from the user, realistic sample content, the ship floor, a designed 404, cookie consent where required, the motion rules); files a failed Persuade check (one primary action, named objections, proof beside each claim, a real risk reversal, an index/noindex decision) as a deferred High finding, since its fix needs the team's copy or indexing call; and in Phase 8 breaks impact ties with Elaya's fix order and reads the installed Tailwind major version before touching its config. The audit's rules render from /design-html's craft section, so the build and the audit cannot drift.
- **/plan-design-review**: a Persuade plan gets the same five checks in Pass 1, Pass 4 rejects unsourced proof, and the runner checklist names them.
- **The craft tells are catalog entries.** Placeholder people and brands, lorem, round sample numbers, `[Needs proof:` markers, "Oops" errors, scroll-listener reveals, dead `href="#"`, `alert()`, images without alt, `100vh`, and a missing title, description, favicon, skip link or `aria-current` live in `lib/design-catalog.ts` as `checkerOnly` entries: one id, one short line and the fix, reported by the craft check and kept out of Methodology category 9, `review/design-checklist.md` and the consultation bullets, so no skill carries their prose. `lib/design-craft.ts` prints only catalog ids, with each entry's `fix` text; `transition: all` now reports under the existing `layout-transition` id instead of `transition-all`.
- **The craft check's `transition: all` tell is `layout-transition`, the design lint's id for it** (`lib/design-lint/rules/copy.ts` leaves it to the lint's static tier, as it does `vh-height`). A `craft_exceptions` entry recorded as `transition-all` is read as `layout-transition`, so an exception accepted before the rename still applies.
### Tests
- `test/helpers/placeholder-allowlist.ts` lists `<path-to-page.html>` (path grammar), the saved page the craft audit passes to the check.
- The design-review and plan-design-review paid cases and judges that read a design resolver now also select on `scripts/resolvers/design-craft.ts`, and `design-html-slop-gate` on `lib/design-catalog.ts`, where the craft check's ids and fixes now live.
- `test/design-craft.test.ts`: every id the check prints is a grep-detectable catalog entry, and the fix it prints is the catalog's. design-review gets its first carve guard (skeleton, union and parity ratio at their measured sizes).
- `test/design-craft.test.ts` also pins that `checkerOnly` entries reach no rendered list while the craft check prints each one, that a change to `scripts/resolvers/design-craft.ts` selects the design-review and plan-design-review cases, and the exception cells a stale record, an over-count tell without `--accepted` and a malformed record on disk reach. New `test/design-craft-resolvers.test.ts`: `{{DESIGN_CRAFT_RULE}}` renders one paragraph with its bullets and fails generation on a missing or ambiguous lead, and both skills carry the same five Persuade checks; the generated craft audit keeps its outcome, exception and session-data rules (and the design-review carve guard checks the last one). `test/design-craft.test.ts` also covers the needs-proof and stale-record exception cells.
- `test/design-lint.test.ts` reads the craft ids from `CRAFT_IDS` instead of scanning `lib/design-craft.ts`, and every one is a lint rule (`layout-transition` and `vh-height` belong to the static tier); `test/design-craft.test.ts` covers the legacy `transition-all` exception.
### Gate integrity
- Size budgets raised to measured sizes only (Garry 2026-10-25 "Do it" (GSTA-92)): the SKILL.md pins of design-review on 12 hosts (+578 Claude, +608 on the 11 external hosts: the Section index the carve test requires and the STOP pointer) and plan-design-review on the 11 external hosts (+1,082: the five Persuade plan checks). The craft tells are catalog entries the craft check reports with their fix, kept out of every rendered skill list, so design-consultation and the review checklist do not grow. No other threshold, budget, timeout, retry or trial count changed.
+1 -1
View File
@@ -130,7 +130,7 @@ export function parseCraftExceptions(json: string): CraftException[] {
const list = record.craft_exceptions ?? [];
const valid = (e: any) => typeof e?.id === 'string' && Number.isInteger(e.count) && e.count > 0 && typeof e.reason === 'string' && e.reason.trim() !== '';
if (!Array.isArray(list) || !list.every(valid)) throw new Error('craft_exceptions must be a list of { id, count, reason } with a positive count and a reason');
return list.map(({ id, count, reason }) => ({ id, count, reason: reason.replace(/\s+/g, ' ').trim() }));
return list.map(({ id, count, reason }) => ({ id: id === 'transition-all' ? 'layout-transition' : id, count, reason: reason.replace(/\s+/g, ' ').trim() }));
}
/** A finding is accepted when an exception names its id with at least its count; a higher count stays an issue and says how many were accepted. */
+1 -1
View File
@@ -13,7 +13,7 @@ import type { Doc } from '../html';
import type { Reporter } from '../report';
import { ruleMeta } from './ids';
const STATIC_OWNS = new Set(['transition-all', 'vh-height']);
const STATIC_OWNS = new Set(['layout-transition', 'vh-height']);
/** The elements the craft check reads from open tag to close tag with a lazy scan. */
const SPANNED = new Set(['script', 'style', 'template', 'nav']);
+3
View File
@@ -173,6 +173,9 @@ describe('accepted craft exceptions (finalized.json craft_exceptions)', () => {
const record = JSON.parse(block.replace(/<number of refinement iterations>|<n>/g, '1'));
expect(parseCraftExceptions(JSON.stringify(record))).toEqual([{ id: '<craft id>', count: 1, reason: '<why accepted>' }]);
expect(parseCraftExceptions('{"mode":"freeform"}')).toEqual([]);
const legacy = parseCraftExceptions('{"craft_exceptions":[{"id":"transition-all","count":1,"reason":"recorded before the rename"}]}');
expect(legacy).toEqual([{ id: 'layout-transition', count: 1, reason: 'recorded before the rename' }]);
expect(acceptFindings(checkCraft('<style>a { transition: all .2s }</style>'), legacy).accepted).toEqual([{ id: 'layout-transition', count: 1, reason: 'recorded before the rename' }]);
expect(parseCraftExceptions('{"craft_exceptions":[{"id":"oops","count":1,"reason":"quoted\\nfrom the user"}]}')).toEqual([{ id: 'oops', count: 1, reason: 'quoted from the user' }]);
for (const bad of ['[]', 'null', '{"craft_exceptions":{}}', '{"craft_exceptions":[{"id":"oops","count":0,"reason":"x"}]}', '{"craft_exceptions":[{"id":"oops","count":1,"reason":" "}]}', '{"craft_exceptions":[{"id":"oops","count":1.5,"reason":"x"}]}']) {
expect(() => parseCraftExceptions(bad)).toThrow();
+4 -4
View File
@@ -17,6 +17,7 @@ import { collectFiles, contextWaivers, lintStatic, MAX_FILE_BYTES } from '../lib
import type { LintContext } from '../lib/design-lint/context';
import { browserAvailable } from './helpers/browser-available';
import { RULES } from '../lib/design-lint/rules/ids';
import { CRAFT_IDS } from '../lib/design-craft';
import { FAMILIES } from '../lib/design-lint/rules';
import { findWaivers, reasonIsBoilerplate, resolveWaivers, applyWaivers, suspectedRules } from '../lib/design-lint/waivers';
@@ -398,11 +399,10 @@ describe('design-lint: third review cycle', () => {
});
test('every id gstack-design-craft can emit has a lint rule or belongs to the static tier', () => {
const src = fs.readFileSync(path.join(ROOT, 'lib', 'design-craft.ts'), 'utf8');
const ids = [...new Set([...src.matchAll(/id: '([a-z-]+)'/g)].map((m) => m[1]))];
const ids = [...CRAFT_IDS];
expect(ids.length).toBeGreaterThan(15);
expect(ids.filter((id) => !(id in RULES) && id !== 'transition-all')).toEqual([]);
expect(ids.filter((id) => id in RULES && RULES[id].gate !== 'craft')).toEqual(['vh-height']);
expect(ids.filter((id) => !(id in RULES))).toEqual([]);
expect(ids.filter((id) => RULES[id].gate !== 'craft')).toEqual(['layout-transition', 'vh-height']);
});
});