mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
* chore(design): pin impeccable rule ids and detector JSON shape as fixtures Real captures from a human-initiated `npx impeccable install` in a scratch directory (engine 0.1.3, linux-x64), never a runtime download: - test/fixtures/impeccable-antipatterns.json: upstream crates/live/assets/antipatterns.json at 87d8f6d6 (the state engine-v0.1.3 shipped), 61 rules, source commit recorded in `_source`. - test/fixtures/impeccable-detect-sample.json: `detect --json` over gstack's planted-slop fixture (source mode), paths normalized. - test/fixtures/review-eval-design-slop.dom.html + impeccable-detect-dom-sample.json: the same page served locally, dumped through the browse engine with the shared DOM-dump script, then scanned. Pins the load-bearing assumption that the static engine reads inline <style> in a .html file: the DOM scan yields the same id set as the source scan. - lib/dom-dump-script.ts: the one dump script both browser engines evaluate (IIFE, no single quotes). Folds CSSOM rgb() back to author hex so palette rules still fire, and removes inlined <link> nodes so the engine does not warn about an unresolvable stylesheet. Both verified against the engine. - test/fixtures/impeccable-detect-help.txt + impeccable-captures.meta.json: the flags, exit codes, finding fields, and re-capture protocol. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(design): typed slop catalog in lib/; AI_SLOP_BLACKLIST derived lib/design-catalog.ts is the single source of truth for gstack's design anti-pattern vocabulary: the 11 legacy blacklist lines (verbatim, flagged `legacyBlacklist`), every one of impeccable's 61 registry ids with gstack prose, tier, impact, confidence, grep heuristic, and /impeccable handoff, plus the gstack-only tells the LLM pass judges (hero metrics, identical cards, glassmorphism, missing states, unthemed browser surfaces, ...). `impeccableId` is set only when the id exists in the registry fixture, and `renderCatalog({style:'ids'})` brackets an id only then, so rendered prose never shows an id the detector cannot emit. Role-scoped font lists (OVERUSED_FONTS_DISPLAY, BANNED_FONTS, FONTS_BODY_UI_OK, FONTS_MONO_OK, FONTS_VERIFIED_FREE) live beside the entries. scripts/resolvers/constants.ts now derives AI_SLOP_BLACKLIST from the catalog. Generated output is byte-identical (bun run gen:skill-docs is a zero diff). Pure module: no I/O, no scripts/ imports, loading prints nothing, so bin/ can import it at runtime on every host. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(review): generate review/design-checklist.md from the catalog review/design-checklist.md was hand-written and its own header admitted it drifted from DESIGN_METHODOLOGY category 9. It is now rendered by scripts/resolvers/design-checklist.ts from lib/design-catalog.ts: category 1 lists every grep-detectable slop entry plus the legacy blacklist lines, sorted HIGH/MEDIUM/LOW, each with its heuristic and, where the detector knows the rule, its bracketed id (27 items, up from 6). The font blacklist renders from BANNED_FONTS. Categories 2-5, Instructions, Classification, Output Format, and Suppressions keep their prose. Title and slop heading are unchanged (test/skill-e2e-review.test.ts and hosts/opencode.ts key on them). gen-skill-docs writes the file for the Claude host only (a Claude-side runtime asset; other hosts copy or inline the render), honors --out-dir, and reports STALE/FRESH under --dry-run like sections do. test/design-checklist-sync.test.ts pins committed == generated, the host/out-dir scoping, and the dry-run freshness line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): modes, craft-floor reflexes, calibration, catalog in doctrine DESIGN_HARD_RULES: the classifier names four visitor modes (Persuade, Operate, Read, Experience, plus Hybrid per section) and keeps the MARKETING/LANDING PAGE and APP UI aliases; Read and Experience get three rules each; a "Reflexes no detector catches" block (browser surfaces, one authored motion moment, depth has an offset, tinted secondary text, space above headings, light-or-dark from the use scene) and the three-looks calibration follow the universal rules. The slop section renders the 11 legacy lines plus the detector rule ids and judgment tells from the catalog; in design-review, which also renders DESIGN_METHODOLOGY, it becomes a one-line pointer so the catalog is paid for once. Header counts are computed, not hardcoded. DESIGN_METHODOLOGY: category 9 renders the catalog in three registers (legacy lines verbatim, detector rules that need judgment with bracketed ids, gstack-only judgment tells as prose, polish-level ids on one line); categories 5 and 7 carry the browser-surface and one-motion-moment reflexes; the typography overused-face item points at [overused-font] with the role-scoped exception. The consultation Codex prompt's anti-slop line reads from the catalog. Budget: design-review eager 25.6K -> 27.0K (ceiling 27,984), plan-design-review unchanged at 17.4K; no carve-guard or context-budget re-baseline needed; ship goldens unchanged (ship never renders the hard rules). Derived from pbakaus/impeccable reference/craft-floor.md + new-work.md (Apache-2.0), rewritten. See NOTICE.md (commit 12). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design-consultation): font procedure, role-scoped overused list, color strategies The proposal section stops handing out a font menu. "Choosing faces: a procedure, not a menu" names the subject's world, shortlists per role, strikes the overused list for that role, verifies availability in-session, and states the loading strategy. {{OVERUSED_FONTS}} renders the role-scoped lists from lib/design-catalog.ts: overused as display (the detector's overused-font set plus the training-data defaults), fine as body/UI on an Operate or Read surface, mono for data and code, banned in any role, and a short verified-free list with its verification date. Color approaches become Restrained / Committed / Full palette / Drenched. The anti-convergence directive drops light-vs-dark as a dial (it comes from the use scene) and the three-looks calibration sits under Your Design Knowledge. The slop list is {{DESIGN_SLOP_BULLETS}}: prose from the catalog, no rule ids, polish-level tells omitted. design-html's "Never include (AI slop blacklist)" list keeps its literal (carve guard) and each line now carries a trailing <!-- id --> naming a catalog entry, pinned by test/design-catalog.test.ts so the last surviving duplicate is derived-by-test. Both resolvers are registered and listed in ARCHITECTURE.md. No carve-guard or budget re-baseline needed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(bin): gstack-design-detect wrapper + design_detector config key bin/gstack-design-detect.ts finds and runs an impeccable engine the user installed; it never installs, downloads, or executes anything that could download. `probe` reads only: config (design_detector off → DISABLED), IMPECCABLE_BIN (absolute, realpath outside the repo and cwd), a PATH walk (absolute entries outside the repo; a #! shim counts as launcher-present, never READY), the ~/.impeccable/bin/<newest semver>/ cache, and the engine installed beside a skill launcher (scripts/bin/<os>-<arch>/impeccable, the layout a real install produced). It reports IMPECCABLE_SKILL, host-aware IMPECCABLE_HOOK (+ HOOK_OTHER), the ignore lists from .impeccable/config*.json, IMPECCABLE_ENGINE_UNTESTED for versions outside the fixture set, and a hint only when a launcher exists without its engine. `scan` re-probes, refuses URLs and anything outside the repo root or the design-report allow-list (realpath, so symlinks cannot escape), derives `--changed <base>` targets NUL-safely through git and lib/frontend-scope.ts, batches 100 absolute paths per engine call with stdin ignored, a SIGKILL timeout, a 50 MB stdout cap, and sanitized length-capped fields, then prints one normalized JSON document (--format gstack) or the engine's bytes (--format raw); DETECT_TOP (fenced as untrusted content), DETECT_SUMMARY, and DETECT_EXIT go to stderr; exit code passes through with 1 over 2 over 0; exit 3 is a gstack bug. `rules` prints the mapped set. Every run appends a content-free line to the local analytics file. lib/design-detect-contract.ts owns every sentinel string, the limits, and the normalized-finding shape (pure module); test/design-detect-contract.test.ts asserts every sentinel-shaped token the agent can read exists there. lib/frontend-scope.ts mirrors gstack-diff-scope's frontend arm, pinned by a parity test that runs the bash script. bin/gstack-config gains design_detector (auto | off, default auto, invalid values rejected with the file unchanged). test/fixtures/fake-impeccable.ts is the env-driven engine stand-in; test/gstack-design-detect.test.ts covers READY/NOT_CACHED/ NOT_AVAILABLE/DISABLED, env trust (.env never loaded, in-repo IMPECCABLE_BIN ignored), newest-semver cache, hook and ignore detection, refusals, exit passthrough, raw byte-identity, normalization, the display cap, timeout, parse errors, diagnostics, --changed, and analytics. The egress scanner test records the wrapper as a documented non-sink. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): {{DESIGN_DETECTOR}} wired into design-review, ship review-lite, review army, design-html The user-installed impeccable engine becomes a deterministic pre-pass in four skills, through one resolver with three renders: {{DESIGN_DETECTOR}} (the probe block and how to read every sentinel), {{DESIGN_DETECTOR:phase0}} (design- review's mechanical scan), {{DESIGN_DETECTOR:gate}} (design-html's bounded slop gate). Every rendered invocation is `bun --no-env-file run <bin>/gstack-design- detect.ts ... --host <host>` and every scan ends with the DETECT_EXIT_CODE echo so exit 2 (findings) never aborts a block. design-review: probe in Setup; Phase 0 picks DOM mode (URL target) or source mode (diff-aware, no URL) once; source mode scans the changed frontend files in Setup, DOM mode never reads source (Rule 4). Phase 3 gains a DOM-dump step per page: both browser engines load the shared script from lib/dom-dump.js (Aside splices it into a double-quoted repl script; the fallback engine copies it into a temp dir for `$B eval --out --raw`), the dump is size-capped, run through gstack-redact (a HIGH finding skips the page), and persisted under $REPORT_DIR/dom/$RUN_ID/; one scan runs after the last page, labeled "static scan of the rendered DOM; cross-origin CSS not resolved". REPORT_DIR honors GSTACK_HOME so the wrapper's allow-list and the report dir agree; RUN_ID is set once in Setup. design-baseline.json is schemaVersion 2 with runId, targetSet, base, and a detector block (mode, engine, byRule, byPage), written temp+rename with a per-run copy; Regression Output diffs ids only when mode and target set match, caveats an engine change, and calls live-page count deltas advisory. Phase 7 hands deferred detector findings to the `handoff=` command the scan printed; Phase 9 recomputes the same way and deletes the dumps unless --keep-dom; Phase 10 reports `Detector: N → M`. ship review-lite gains step 0 (probe, `scan --changed <base>`, tier buckets, detector + checklist dedupe, advisory and ignored never count) and a `detector` count in its log payload; the PR body gets a Detector line (rule ids and counts only). The Review Army Design specialist runs the mechanical pass at the top of review/design-checklist.md, which now carries it. design- html probes after DESIGN_SETUP and runs the one-pass gate before screenshots. lib/dom-dump.js is generated by gen-skill-docs from lib/dom-dump-script.ts (Claude host, --out-dir aware, dry-run freshness) and pinned byte-equal, so the prose never carries the script. The contract gains DETECT_JSON, DOM_DUMP_OK, and the self-describing set; its test now checks both directions. Budget: design-review eager 25.6K → 28.5K. The plan's target was +2.5K; after the levers it named (ids-only detector rules, no inline script, trimmed prose) it lands at +2.87K, and the remainder is doctrine and detector wiring, so the ceiling moves to the captured 31,319 for design-review only (the full capture would also have loosened 21 ceilings this branch never touched; those stay). design-html skeleton re-baselined to 54,000 (measured 53,592). Codex and Factory ship goldens refreshed (review-lite step 0 and the PR-body line render inline there). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): catalog never-lines in the mockup prompt Ten catalog ids carry `mockupNever` (kicker-above-heading, icon-tile-stack, gradient-text, ai-color-palette, cream-palette, nested-cards, dark-glow, pulsing-dot, identical-cards, hero-metrics) and lib/design-catalog.ts exports their deduped plain-English names as MOCKUP_NEVER_NAMES. briefToPrompt() in the design binary appends "Never: <names>." before its fixed tail, so `$D generate | variants | evolve` stop reaching for purple gradients, icon tiles, and cream defaults before the comparison board opens. The binary still bundles (`bun build --compile design/src/cli.ts`); ./setup rebuilds it. design-html's Never-include list now covers every mockupNever id (kicker / icon tile, hero metric rows, gradient text, cream palette, nested and identical cards, glow and pulsing dots), each line tagged with its catalog ids; test/design-catalog.test.ts pins the exact ten flags, the deduped names, and that the template list is a superset. New design/test/brief.test.ts pins the prompt shape. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(lib): open DESIGN.md reader/writer + gstack-design-md bin lib/design-md.ts implements the open DESIGN.md format (google-labs-code/ design.md, Apache-2.0): YAML front matter with the five token groups (colors, typography, rounded, spacing, components) and eight canonical `##` sections in spec order (Overview, Colors, Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts), aliases mapped, extras preserved after them in their original order. parseDesignMd never throws (unparsable front matter → `unknown` with a reason); renderDesignMd re-emits the preserved front matter bytes and only `convert` writes fresh YAML through a small block-style emitter (Bun.YAML.stringify is flow style); upsertSection splices the body only; tokensFlat resolves `{path}` references to primitives and reports group, self, dangling, and cyclic refs as DESIGN_MD_TOKEN_REF_INVALID. convertLegacy turns gstack's pre-spec DESIGN.md into the open format: Product Context and Aesthetic Direction fold into Overview, Typography roles become display/body/label/mono tokens (mono carries fontFeature: tnum), Color hexes become colors (mode-qualified labels keep their qualifier; strategy lines are not colors), the Spacing scale and Layout radii become spacing and rounded, Motion / Grain Texture / Decisions Log survive as extras. The format marker lives inside the file: a YAML comment on line 2 of a spec file, an HTML comment on line 1 of a legacy file. bin/gstack-design-md.ts: `check` (DESIGN_MD_FORMAT + marker), `convert [--write]` (backup to DESIGN.md.legacy.bak, temp+rename, refuses ambiguous input with DESIGN_MD_CONVERT_REFUSED), `tokens` (flat JSON), `mark <spec|legacy-keep>`. Exit 3 + DESIGN_MD_INTERNAL_ERROR is a gstack bug. design/src/memory.ts: updateDesignMd upserts "Extracted Design Language" through the lib (front matter bytes untouched, canonical order kept, section replaced on rerun) and creates a spec skeleton with tokens from the extraction when no file exists; readDesignConstraints leads with the flat tokens and the Overview for spec files. The design binary still bundles. test/design-md.test.ts pins all of it against gstack's own DESIGN.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): write/read DESIGN.md in the open spec; persisted format choice gstack's design skills now write DESIGN.md in the open DESIGN.md format and read tokens from it. {{DESIGN_MD_CHECK}} renders the format check through bin/gstack-design-md.ts: design-consultation's Phase 0 settles the format once (spec → update tokens in the front matter; legacy without a marker → one AskUserQuestion: convert with a .legacy.bak, keep the legacy file, or start fresh; the answer is written into the file as the format marker so no skill asks again; a marker already present is obeyed silently; unknown → prose; missing → Phase 6 writes one). Phase 6's template is the spec form: YAML front matter with name, description, and exactly the five token groups (colors, typography.display/body/label/mono with fontFeature: tnum on mono, rounded, spacing, components with {path} references), then Overview (Creative North Star, product context, mode per surface, references, key characteristics), Colors (opening with the Restrained / Committed / Full palette / Drenched strategy), Typography, Layout, Elevation & Depth, Shapes, Components, Do's and Don'ts, plus gstack's Motion and Decisions Log as extras; the template ends with a check that the file parses as `spec`. design-review runs the `:calibrate` form in Setup: a spec file's flat tokens are the calibration source (a value present in the tokens is never a finding), the marker is respected, and conversion is never offered there; its DESIGN.md export writes the spec form. design-html's token extraction writes the spec form and respects an existing choice. review/design-checklist.md category 5 and ship's review-lite step 1 name `gstack-design-md tokens` as the calibration source; plan-design-review Pass 5 cites tokens by path when front matter exists. The contract owns the bin's DESIGN_MD_MARKER / REASON / WRITTEN / BACKUP lines; the contract test's pending list closes. Carve guard: design- consultation skeleton 66,500 → 67,500 (measured 67,014; +1,508 B against the 1.5 KB cap). Codex and Factory ship goldens refreshed (review-lite step 1). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): PRODUCT.md prefill + /impeccable handoffs design-consultation's context gathering and design-shotgun's auto-gather read PRODUCT.md (impeccable's product-context file) when it exists: it counts as the user's prior answers, gets confirmed in one line, and is never re-asked. Neither skill opens `.claude/skills/impeccable/**`; PRODUCT.md and DESIGN.md are the shared surface, and impeccable's prose never loads inside a gstack skill. Handoffs: ship's review-lite ends each NEEDS INPUT detector row with the `handoff=` command the scan printed (`/impeccable <cmd>`) when the probe reported IMPECCABLE_SKILL: present, recommending the command and never opening its files; design-review's Phase 7 does the same for deferred findings, and `design_detector: off` silences handoff lines with the rest. Codex and Factory ship goldens refreshed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore(design): convert gstack's own DESIGN.md to the open spec `gstack-design-md convert --write` on the repo's DESIGN.md: tokens in YAML front matter (typography.display/body/label/mono, colors with their light/dark qualifiers, spacing scale, rounded scale), Overview from Product Context and Aesthetic Direction, Colors / Typography / Layout as canonical sections, Motion, Grain Texture, and Decisions Log preserved as extras, format marker on line 2. Hand-checked; `check` reports spec with no token-reference errors. A Decisions Log row records the conversion and that DM Sans stays the body face: it is on the overused-as-display list, and body/UI use on an Operate surface is the allowed exception under the role-scoped rule. The pre-conversion file lives on as test/fixtures/design-md-legacy.md, which test/design-md.test.ts now uses for its legacy cases; the converted root file is asserted to be spec. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: NOTICE, Apache license text, README interop, project structure NOTICE.md names what gstack derived from impeccable (rule ids and names in the catalog and the registry fixture; the visitor modes, craft-floor reflexes, and calibration in the design resolvers; the font procedure in the consultation template) and from Google's DESIGN.md specification (the format lib/design-md.ts implements), states that gstack does not distribute or audit the impeccable engine, and points at licenses/Apache-2.0.txt (verbatim). README: the design-consultation, design-review, and design-html rows say what changes when impeccable or the open DESIGN.md format is in play, and a "Works with impeccable" paragraph explains the pre-pass, the shared ids, PRODUCT.md and DESIGN.md as the shared surface, the handoffs, the no-nag posture without impeccable, and the off switch. docs/skills.md gets the detector paragraph under /design-review. docs/PROJECT_STRUCTURE.md lists the new lib and bin files, NOTICE.md, and licenses/. docs/designs/IMPECCABLE_INTEROP.md promotes the CEO plan (its ~/.gstack copy is flipped to PROMOTED) with a "what shipped" summary. TODOS.md files the seven deferrals from the reviews: the design-review Phases 7-11 carve (the budget lever, with the +2.87K vs 2.5K landing recorded), the Bun .env audit across bin/*.ts, the Kiro bin/lib gap, the $D check slop rubric, taste-profile interplay, the CEO Section 11 bullets, and the scan cache. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: touchfiles, tiers, shim E2E, real-engine fixture Touchfiles: the catalog, contract, detector bin, checklist resolver, review- army resolver, and DESIGN.md lib join the dep lists of review-design-lite, design-review-fix, the design-consultation cases, and plan-design-review-no- ui-scope, so editing any of them re-selects the tests that read their output. Three new E2E keys: design-review-detector-shim (gate; source mode on a feature-branch diff), design-review-detector-shim-dom (gate; DOM mode: the slop fixture served on loopback, dumped through the browse binary with lib/dom-dump.js, persisted under a GSTACK_HOME-scoped REPORT_DIR, scanned once; self-skips when browse/dist/browse is absent), and design-html-slop-gate (periodic; one fix pass, at most two scans, remaining findings accepted with reason). Every case reaches the engine through test/fixtures/fake-impeccable.ts via IMPECCABLE_BIN from outside the temp repo, reads extracted skill sections (never a whole SKILL.md) with the installed bin path rewritten to this checkout, and asserts the probe ran, the right scan verb ran, `npx impeccable` never did, and the output carries FINDING rows tagged [ai-color-palette] and [low-contrast]. review-design-lite gets the fake engine and an eighth tally signal for a detector row; its 4-hit threshold is unchanged. test/gstack-design-detect.test.ts evaluates design-review's REPORT_DIR expression with GSTACK_HOME set and proves a dump under it is accepted by the wrapper's allow-list. The sample fixtures were real captures from commit 1 (engine 0.1.3), so there is nothing hand-written left to swap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): never execute a repository-controlled engine; allow-list --changed targets; sanitize engine text Pre-landing review findings (security + checklist), all reproduced before the fix: - A checked-out branch could commit `.claude/skills/impeccable/scripts/bin/<os>-<arch>/impeccable` and the probe would report READY and `scan` would run it, with the agent's full environment. Launchers and sibling engines under the repo or cwd now count as "skill present" only (IMPECCABLE_NOT_CACHED: repository-local install, and the hint never names a repository-local launcher to run); only HOME-rooted installs, IMPECCABLE_BIN, the cache, and PATH entries outside the repo qualify, all by realpath. The engine now sees a minimal environment (PATH, HOME, TMPDIR, locale, IMPECCABLE_*), never the agent's tokens. - `scan --changed <base>` pushed git-derived paths without the allow-list, so a committed symlink with a frontend extension handed a file outside the repo to the engine. Derived targets now go through the same allow-list as explicit ones and symlinks named by git are refused outright. - A repo-controlled `scripts/VERSION` with embedded newlines forged probe lines; the version is trusted only when it is semver, and every printed version is sanitized. Engine text containing the untrusted-content fence or a `SENTINEL:` prefix is neutralized with a zero-width space (neutralizeSentinels in the contract), so page text cannot close the envelope or forge a probe line. - A failing `git diff <base>...HEAD` (unknown or unfetched base) was swallowed and read as "no frontend changes"; it is now DETECT_REFUSED with exit 1. - The scan allow-list root follows `${GSTACK_HOME:-$HOME/.gstack}` like the templates and gstack-slug (config.yaml keeps gstack-config's STATE_ROOT precedence); a quoted or commented design_detector value reads correctly. Smaller: raw engine chunks are kept only in --format raw; diagnostics are capped (200 kept, 20 echoed); the engine identity hash reads size + 4 MB, not the whole binary; PROBE_STEP and ENGINE_STDERR are contract sentinels; the --verbose gate covers every probe step; analytics use one sentinel vocabulary; bare limits live in DETECT_LIMITS. The fake engine's knobs are IMPECCABLE_FAKE_* (so they pass the minimal env) and a shared test helper installs it. New tests cover each item above plus clean runs, `{}` parse errors, missing paths, and the 50 MB stdout cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): mark and updateDesignMd never rewrite the user's file; refuse a contradictory mark renderDesignMd re-sorted canonical section names into spec order on every render, so `gstack-design-md mark legacy-keep` (the "leave it alone" answer) and the design binary's mockup extraction reordered a legacy DESIGN.md (Typography and Layout jumped to the top) and normalized its whitespace, while the bin promised "body bytes untouched". `mark` now splices only the marker line (insertMarker) and `updateDesignMd` splices only its own section (spliceSection); every other byte of an existing file is preserved, and spec order applies only to files that open with front matter. `mark` refuses a choice that contradicts the file's format (spec on a non-spec file, legacy-keep on a spec file) with DESIGN_MD_CONVERT_REFUSED, exit 2, file unchanged. convertLegacy keeps intro prose under the title instead of rebuilding the preamble from the title alone. detectFormat returns a machine-readable `code` beside the prose reason (the bin no longer branches on reason text); the marker regexes derive from FORMAT_MARKER_PREFIX and FORMAT_CHOICES; the hop limit and legacy identity headings are named constants; slug is exported and reused; both writers use lib/fs-atomic.ts. Tests pin byte identity for mark and updateDesignMd on the legacy fixture, the refusal paths, and the preserved preamble. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): run the DOM dump in the page on both engines; align doctrine with the catalog The DOM-dump script is an arrow function, not a self-calling IIFE: Aside's `pg.evaluate($_DUMP)` receives the function and runs it in the page (the IIFE form executed in the repl sandbox, where `document` does not exist), and the fallback engine calls it with `$B js "($_DUMP)()" --out --raw`. Hygiene widens to every URL-bearing attribute (src, srcset per candidate, poster, action, formaction, data, ping, cite lose their query strings and fragments) and to data: URLs inside existing <style> nodes. The persist and scan blocks restate REPORT_DIR and RUN_ID literally instead of relying on a shell variable from an earlier block; the baseline's targetSet is defined per mode (repo-relative paths in source mode, page slugs in DOM mode) so DOM-mode deltas can match; the PR-body Detector line lists the states the probe can actually print. The DOM fixture is re-captured with the new script from outside the repo (the engine walks up from cwd for DESIGN.md, which the metadata now records). Doctrine contradictions the design specialist found: the landing-page motion rule matches the one-authored-moment reflex; the background rule names the catalog's halo/spotlight/stripe/grid slop instead of asking for gradients; the universal font rule is scoped to the display voice with the body/UI exceptions; "two typefaces max" allows the mono; the methodology's banned-font line renders BANNED_FONTS; Courier New is banned outright; the Brutalist, Retro-Futuristic, and Playful menu entries stop recommending system stacks, glow, and bounce; the coherence nudge uses the decoration vocabulary; Path A's gate names the display voice; font-loading prose points at the source the procedure verified; centered-everything is MEDIUM (an aggregate heuristic); the mockup guard reads "Never by default (unless the brief above asks for it)". The checklist's AUTO-FIX list renders the catalog's auto-fix rules; category 9 and the Hard Rules pointer count from the same partition helpers (detectorSlopEntries, judgmentTellEntries); the handoff list renders from HANDOFF_COMMANDS; a missing catalog id fails gen-skill-docs by name. gstack's own DESIGN.md gains border tokens and Decisions Log rows for its live-feed pulse and 11px mono labels. frontend-scope is case-sensitive like the bash arm. gen-skill-docs shares one emitGenerated helper for sections and lib-derived assets; renderCatalog keeps the one style with a caller. Tests: shared sliceBetween that fails on a missing end marker, the slop-gate fixture's real end marker, an isolated browse daemon for the DOM-mode E2E, the DOM hygiene test gated to CI or opt-in, docs notes for the two superseded plan sentences. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): an engine is a file named impeccable outside the project; DOM dumps scan without inline ignores Second review cycle, security + checklist: - IMPECCABLE_BIN=/bin/sh (or node) was READY, and `detect` with cwd=repoRoot made the interpreter run the repository's own `detect` file. Every engine candidate (env override, PATH entry, cache, sibling) is now judged by the realpath of the FILE and must be named impeccable[.exe]; PATH and cache candidates that resolve into the repository are skipped like the others. "Inside the project" means the repository, or cwd when cwd is a project directory: HOME and its ancestors are exempt, so a URL-mode review launched from HOME still finds the HOME-rooted installs. - A base for --changed that starts with `-` was spliced into git argv (`--output=<file>` made git write a file and report no changes); an option- like or missing base is DETECT_REFUSED (not a ref name), exit 1, and the parser no longer defaults a missing value to main. - DOM dumps are the audited page's bytes, so an in-file `impeccable-disable` comment there is page-controlled: batches under the designs root run with --no-inline-ignores, repository batches keep the project's own ignores. - neutralizeSentinels covers the shapes it missed (bare sentinels such as DETECT_TOP total= and IMPECCABLE_DISABLED, the DETECT_EXIT_CODE= echo, the `[rule-id] impact=` group header) in one precompiled alternation instead of 37 replaceAll passes per field; only kept findings are normalized, and the summary's total stays the engine's count. - The minimal engine environment compares keys case-insensitively on Windows (process.env enumerates Path, SystemRoot there) and passes PATHEXT, COMSPEC, HOMEDRIVE, HOMEPATH, PROGRAMDATA. - Bare 64s move into DETECT_LIMITS; the unused SentinelName type is gone; the header states the directory-target contract (the engine's own walk). Tests: an interpreter as IMPECCABLE_BIN never runs the repo's detect file; a PATH symlink into the repository is never READY; option-like and empty bases are refused with no file written; the designs-root batch carries --no-inline-ignores and the repo batch does not; the identity label is deterministic per binary; the bare-sentinel and header shapes are neutralized; the installed fake engine works without IMPECCABLE_FAKE_OUTPUT (the helper copies the sample beside it); two tests clean up in finally. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): text-level edits keep CRLF, one section-boundary rule, control characters quoted - insertMarker and spliceSection normalized every line ending to LF, so a CRLF DESIGN.md came back rewritten beyond the one line they promised to touch. Both detect the file's dominant line ending and restore it. - parseDesignMd and spliceSection each walked headings with their own fence tracking; they now share headingLines (and upsertSection shares headingMatches). An unclosed ``` is treated as prose for that file: it used to swallow every later section on a splice. - A token value carrying a control character (an LLM-extracted font family with an embedded newline) was emitted as a bare multi-line scalar that Bun.YAML rejects, turning a freshly written DESIGN.md into frontmatter-unparsable; needsQuotes routes it through the quoted form. - The marker-line regex variants are built once beside YAML_MARKER_RE; the dead setMarker export and a no-op ternary are gone; LEGACY_HEADINGS derives from the identity list; the header diagram names the text-level editors as the write path for user-owned files; the bin validates and prints the mark choices from FORMAT_CHOICES. Tests: CRLF round-trips for both editors, a fenced ## inside a section and an unclosed fence, and a newline-bearing scalar parsing back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): Aside dump script stays single-quoted; redaction gate sized to the dump cap; doctrine made consistent - The DOM-dump Aside block was the only double-quoted `aside repl` script in the tree (to splice the function text), which put the agent-filled <url> inside a double-quoted bash string: a same-origin href carrying $(...) would run in the reviewer's shell when Phase 3 opened that page. The script is single-quoted like every other Aside script and the function text enters through a closed-quote segment ('"$_DUMP"'); the fallback line is `$B js '('"$_DUMP"')()'`. A free test pins that no rendered Aside script opens with a double quote. - The persist block capped dumps at 10 MiB but ran gstack-redact with its 1 MiB default, so every real page between the two was deleted as DOM_DUMP_REDACTION_BLOCKED; the gate passes --max-bytes at the dump cap and blocks on any exit other than clean (0) or MEDIUM (2), so a redaction tool that fails to run can no longer fall through to "persist". - Dump hygiene removes <template> and <noscript> subtrees (invisible to the attribute walk), inline on* handlers, and the cross-origin <link> nodes already named in the note, so the file handed to the engine references no remote stylesheet. - Doctrine: the Codex design-voice prompts said "2-3 intentional motions" against the one-authored-moment rule; the overused-display heading scoped its ban to Persuade/Experience while the catalog and hard rules ban it everywhere; design-consultation's Important Rule 4 still said "as primary"; design-html's blacklist header is now "Never include by default" with the mockup/DESIGN.md/user-ask override the catalog grants; the slop gate honors Decisions Log and Do's and Don'ts blessings like /review does; the landing "poster" line says poster in stance, not type size; the design binary's variant dials no longer flip light/dark for variety; gstack's DESIGN.md rows name data labels (UI labels stay the DM Sans token) and call the skill-bar fill and hovers functional transitions. - design-review names how the base branch is found (gh pr view, then the repo default; never main) for the source-mode scan and the diff-aware mode. - frontend-scope matches the config globs at the repo root only, like the bash arm; the parity test carries nested samples. - Cleanups: renderCatalog's stale style option, an unused import, the identity-map bannedFontNames, the checklist header's "same entries" claim, the catalog header's consumer list, the orphaned main() docstring, the plan doc's IIFE bullet. design-html's skeleton ceiling is re-measured (54,184) for the two doctrine sentences. Tests: AUTO-FIX rendering from the catalog, the E2E slice markers checked in the free suite, the hygiene cases for templates/noscript/handlers/remote links, and the review E2E counting detector rows separately from the seven checklist plants. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): project means below HOME; only page dumps drop inline ignores; a whole-scan budget; prototype-safe rule counts Third review cycle + Red Team, all reproduced before the fix: - With no repository, the wrapper adopted cwd as the repo root, so a review launched from HOME (URL mode can run from anywhere) rejected every HOME-rooted install as "repository-local", reported the user's own skill install with the wrong hint, and, for targets, accepted all of HOME (~/.ssh/id_rsa scanned). A project directory is now one strictly below HOME: `git init ~` never turns the user's installs into repository files, and from HOME only the designs allow-list qualifies as a target. - --no-inline-ignores keyed on "not inside the repo", which misclassified dumps when GSTACK_HOME sits under the repo and stripped the design-html gate's own `<!-- impeccable-disable -->` from finalized.html. Targets are classified as project / dom-dump (designs/<audit>/dom/**, the page's bytes) / artifact (other designs/ files, gstack-authored); only dumps drop inline ignores. - A repository's .impeccable/config.json can hide rules from the review; detector.ignoreValues was never surfaced. The probe prints IMPECCABLE_IGNORED_VALUES beside the rules, and the prose stops calling repo-config ignores "a decision the user made". - An engine id named `constructor` corrupted byRule through Object.prototype and `__proto__` counts vanished; byRule is a null- prototype object and an id that fails the shape check is `unmapped` as a key too. - Batches ran with no total budget (10,000 un-ignored files: hours). The scan stops at 5x the per-batch timeout with DETECT_TIMEOUT and exit 1. - The scan JSON carries an `untrusted` list of the engine- and page-derived fields, so the agent reading past the fenced DETECT_TOP block is told what is evidence. - The PATH walk keeps launcher-present for a .cmd wrapper or a differently named real file (the name gate applies to READY only). Tests: probe and scan from a fake HOME (cache READY, HOME file refused, dump scanned without inline ignores), artifact vs dump batches, prototype-member ids, the whole-scan budget over 11 batches, ignoreValues surfaced, the `untrusted` field. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): edits follow a symlinked DESIGN.md, keep the BOM and the majority line ending, refuse an unclosed fence - `mark`, `convert --write`, and the design binary's extraction replaced a symlinked DESIGN.md (a docs-site layout) with a regular file and left the real target untouched; both writers resolve the link first. - A single stray CRLF flipped a whole LF file to CRLF: the editors now keep the majority ending. A UTF-8 BOM broke format detection and ended up mid-file after `mark`; it is recognized and kept at byte 0. - Re-running `mark` on a marked file deleted the blank line after the marker (`\s*$` matched across the newline); the marker regexes use `[ \t]*`. - Fences: readers follow markdown (an unclosed fence runs to EOF); the text-level editors refuse such a file with DesignMdEditRefused (DESIGN_MD_EDIT_REFUSED) instead of splicing the wrong section, and the design binary reports that and leaves the file alone. - needsQuotes also quotes a scalar containing ` #` (an inline-comment shape parsed back as a truncated value). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): dump hygiene covers CSS URLs, srcdoc, and handlers; dumps persist owner-only; ignore prose treats repo config as evidence - The dump script cuts query strings from CSS url() in style attributes, <style> nodes, and the inlined stylesheets (signed asset URLs), empties srcdoc, and covers background and xlink:href. - Persisted dumps are chmod 600; MEDIUM redaction findings persist (an authenticated page shows emails) and the prose says so; earlier runs' dumps are swept before the first dump of a run unless --keep-dom. - The Aside dump prose asks for `'` in a pasted URL to be percent-encoded (a bare single quote would end the script) and never to paste an unread URL. - Repo-config ignores are evidence, not settled decisions, in /review, /ship, and design-review's probe prose; the scan JSON's text fields are named as untrusted. - design-html's skeleton ceiling is re-measured (54,545); ship goldens refreshed for the checklist prose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-detect): audit directories scan as dumps; scans print probe lines on stderr; refused base always exits 1; PATH loses project entries Adversarial review (Claude subagent): - A DIRECTORY target under designs/ (the audit dir, which the prose hands the agent as REPORT_DIR) classified as an artifact, so the engine walked its dom/ subtree WITH inline ignores honored. Any directory under designs/ is now scanned as dumps. - A scan whose probe no longer finds an engine wrote its sentinel lines to stdout and exited 0, so `scan > "$_DJ"` captured "IMPECCABLE_NOT_AVAILABLE" as the scan result and the rendered bash read a clean scan. Probe lines go to stderr on every path; stdout is the JSON document or nothing. - A refused --changed base exited 0/2 when explicit targets were also given; it folds into the exit code (1 over 2 over 0). A trailing --changed no longer defaults to main. - A hand-edited `design_detector: Off` re-enabled the detector; the value is compared case-insensitively. - The engine inherited PATH entries inside the project (a direnv .envrc adding node_modules/.bin); those are filtered like every other project path. - DOM_DUMP_MISSING names the case where the dump script wrote nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design-md): markdown edge cases: rule-opened legacy files, spaced fences, ~~~ blocks, duplicate headings, YAML 1.2 numerics - insertMarker keyed on "starts with ---", so a legacy file opening with a horizontal rule got a `# gstack:` line rendered as a heading that the parser then never read back (the conversion question re-asked every run). It keys on parsed front matter. - A closing front-matter fence with trailing spaces (`--- `) made a valid spec file `unknown`; the closer is any whole `---` line. - `~~~` fences hid nothing, so a `## ` inside one was a section boundary and a splice corrupted the fence; both fence kinds are tracked and only the same kind closes an opener. - convertLegacy silently kept the first of two `## Layout` bodies (and one of `## Color` / `## Colors`); it refuses with DESIGN_MD_CONVERT_REFUSED and the bin leaves the file and writes no backup. - needsQuotes covers 0x / 0o / .inf / .nan (YAML 1.2 numerics that changed type on round-trip); emitYamlBlock throws on an object inside an array instead of writing "[object Object]". - The design binary coerces the model's extraction JSON at the parse boundary (null names, missing arrays) so the paid call's result survives. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(design): print and alternate stylesheets are not scanned as page CSS; no cross-run dump sweep; probe-state and design-system caveats in prose - The dump inlined every linked sheet's rules as active CSS, so a print sheet's 12pt black text or an alternate theme produced tiny-text and palette findings the user never sees; disabled and alternate sheets are skipped and a media-scoped sheet is wrapped in its @media block. - The cross-run dump sweep is gone: two same-day reviews shared REPORT_DIR and one run's sweep deleted the other's dumps mid-audit. Dumps stay per run, owner-only, deleted after Phase 9 unless --keep-dom (now defined in the prose), and an interrupted run's dumps wait for the user. - Prose: design-system-* rows in DOM mode compare the page to THIS repo's DESIGN.md and apply only to the repo's own app; an empty scan JSON with exit 0 means the probe state changed since Setup (read stderr); the persist block names a missing dump instead of mislabeling it as a redaction block. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * v1.82.0.0: impeccable interop, detector pre-pass, open DESIGN.md format Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: update project documentation for v1.82.0.0 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * changelog: name the measure behind the test-count row Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): drive the DOM hygiene test through Playwright Chromium directly Under the six-shard CI free suite the test's private browse daemon never answered its health probe (two minutes of retries), failed the shard, and starved two unrelated test files into failing before the runner's timeout. The test now launches the same Chromium through playwright-core and calls the dump function with page.evaluate, the way Aside's pg.evaluate does: no state file, no daemon, no health window. It self-skips when the Playwright Chromium bundle is absent. Two more hygiene rules are pinned along the way (print sheets keep their @media, alternate sheets are dropped). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): compare gen-skill-docs paths with forward slashes on Windows gen-skill-docs prints repo-relative paths with the OS separator, so the checklist render pins (`GENERATED: review/design-checklist.md`) failed on the Windows lane against `review\design-checklist.md`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(test): assemble the planted PEM block at runtime The quality gate scans every added line of the PR diff through gstack-redact; the redaction test's literal PEM header was a HIGH finding on our own test file. The block is now built from fragments, so the scanned file never carries a key-shaped line while the test still plants a HIGH finding. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design-detect): consent-gated engine install, checksum-pinned and receipted `gstack-design-detect.ts install` is the one download gstack makes, and only after a design skill's one-time question got a yes. It fetches the engine version gstack has tested (0.1.3) for this platform from impeccable's own GitHub release, verifies it against the checksum pinned in lib/design-detect-contract.ts (all five platforms, captured from the release's .sha256 sidecars; linux-x64 equals the fixture engine), writes an egress receipt before the fetch and refuses to download when the receipt cannot be written (fail-closed; the sink is registered in the wiring test's polarity table), caps the download at 32 MB, streams with the cap enforced, writes the file only after the hash matches, and places it under ~/.impeccable/bin/<version>/ (a trusted IMPECCABLE_HOME is honored; never inside a project). No skill, no hook, no launcher, no npx. --sha256 accepts a sidecar checksum for a version gstack has not pinned; --base allows a mirror (https, or http on loopback for tests). After a successful install the probe runs and its lines follow, so the skill sees READY at once. The probe ends with DESIGN_DETECTOR_INSTALL_OFFER (version, platform, bytes, destination) whenever it found no engine and the user has not answered the question; once design_detector_install_prompted is true it prints neither the offer nor the NOT_CACHED hint, which used to repeat on every run. The hint's npx wording is corrected: `npx impeccable detect --help` caches the engine for npx only, not where the probe looks. gstack-config gains design_detector_install_prompted (true|false, typo rejected, enumerated in list and defaults). Tests: a loopback mirror (async spawn, so the in-process server can answer) covers install, re-install as a verified no-op, checksum mismatch, 404, unpinned version, non-https base, design_detector off, and IMPECCABLE_HOME inside the repo; the offer and the silenced hint; pin completeness per platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(design): ask once before downloading impeccable's engine When the probe prints DESIGN_DETECTOR_INSTALL_OFFER the design skills ask the user one AskUserQuestion, in interactive sessions only (spawned or headless runs never install and never ask; Conductor gets the prose brief), before any other step: install the engine now, not now, never ask again (design_detector_install_prompted), or turn the detector off. A yes runs the receipted, checksum-pinned install and the skill continues with a READY probe. The brief says what impeccable is, what the one file is, where it goes, how it is verified and logged, and that no skill or hook comes with it; users who want the /impeccable skill run npx impeccable install themselves. design-review carries the brief inline (it is not carved). design-html keeps its skeleton small: the probe block points at a new read-on-demand section, sections/detector-install-offer.md, registered in its manifest and carve guard; its skeleton ceiling is re-measured (55,262) and its eager ceiling set to the measured 13,767. The review and ship passes state that they never offer an install. NOTICE.md, README, docs/skills.md, the interop design doc, and the CHANGELOG describe the new posture: gstack still never runs impeccable's installer or launcher; the one download is consented, pinned, and receipted. Ship goldens refreshed for the review-pass wording. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
740 lines
42 KiB
TypeScript
740 lines
42 KiB
TypeScript
// lib/design-catalog.ts — gstack's design anti-pattern vocabulary, typed.
|
|
// Derived in part from pbakaus/impeccable (Apache-2.0), modified. See NOTICE.md.
|
|
//
|
|
// Pure module: no I/O, no imports from scripts/. bin/ and lib/ travel together
|
|
// on every host, scripts/ is never linked, so anything runtime may import this
|
|
// and nothing here may import scripts/.
|
|
//
|
|
// lib/design-catalog.ts
|
|
// ├─ scripts/resolvers/constants.ts AI_SLOP_BLACKLIST: the 11 legacy lines, verbatim, in order
|
|
// ├─ scripts/resolvers/design.ts DESIGN_METHODOLOGY cat 9, DESIGN_HARD_RULES, DESIGN_DETECTOR
|
|
// │ (handoffs), OVERUSED_FONTS, DESIGN_SLOP_BULLETS, and the
|
|
// │ 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
|
|
// └─ 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
|
|
// test/fixtures/impeccable-antipatterns.json (test-enforced), and rendered
|
|
// prose brackets an id only in that case, so a reader never meets a bracketed
|
|
// id the detector cannot emit. Everything else is a gstack-only tell that the
|
|
// LLM pass judges. The four lists this file replaced (constants.ts, the
|
|
// consultation proposal section, design-html's blacklist, and the review
|
|
// checklist) had drifted apart; they now render from here.
|
|
|
|
export type SlopCategory =
|
|
| 'scaffold' | 'surface' | 'type' | 'color' | 'layout'
|
|
| 'motion' | 'copy' | 'states' | 'imagery' | 'browser-surface';
|
|
export type DetectMethod = 'engine' | 'grep' | 'render' | 'llm';
|
|
export type Confidence = 'HIGH' | 'MEDIUM' | 'LOW';
|
|
export type ReviewTier = 'auto-fix' | 'ask' | 'possible';
|
|
export type Impact = 'high' | 'medium' | 'polish';
|
|
export type FontRole = 'display' | 'body' | 'ui' | 'mono';
|
|
/** The `/impeccable <cmd>` commands a deferred finding may hand off to (one source for the type and the prose). */
|
|
export const HANDOFF_COMMANDS = ['typeset', 'layout', 'colorize', 'harden', 'clarify', 'polish', 'animate', 'quieter'] as const;
|
|
export type Handoff = (typeof HANDOFF_COMMANDS)[number];
|
|
|
|
export interface DesignSlopEntry {
|
|
/** kebab-case; equals impeccableId when the detector knows the rule */
|
|
id: string;
|
|
/** set only when the id is in test/fixtures/impeccable-antipatterns.json */
|
|
impeccableId?: string;
|
|
/** short label (compact renders, mockup "Never:" line) */
|
|
name: string;
|
|
/** the doctrine line, gstack voice (cat 9, checklist, consultation bullets) */
|
|
prose: string;
|
|
category: SlopCategory;
|
|
kind: 'slop' | 'quality';
|
|
detect: DetectMethod[];
|
|
/** design-checklist tier */
|
|
confidence: Confidence;
|
|
/** review-lite bucket */
|
|
tier: ReviewTier;
|
|
/** design-review triage */
|
|
impact: Impact;
|
|
/** grep hint rendered in design-checklist.md category 1 */
|
|
heuristic?: string;
|
|
/** overused-font names */
|
|
values?: string[];
|
|
/** roles the values are banned for; present iff values is */
|
|
roles?: FontRole[];
|
|
handoff?: Handoff;
|
|
source: 'gstack' | 'impeccable' | 'both';
|
|
/** the 11 originals; AI_SLOP_BLACKLIST derives from these verbatim */
|
|
legacyBlacklist?: true;
|
|
/** feeds the design binary's "Never:" prompt line */
|
|
mockupNever?: true;
|
|
}
|
|
|
|
/** Training-data defaults: never the display voice on any surface (body/UI on Operate/Read is the one exception, FONTS_BODY_UI_OK). */
|
|
const OVERUSED_DISPLAY = [
|
|
'Inter', 'Roboto', 'Arial', 'Helvetica', 'Open Sans', 'Lato', 'Montserrat', 'Poppins',
|
|
'Space Grotesk', 'Space Mono', 'Fraunces', 'Playfair Display', 'Cormorant', 'Lora', 'Crimson',
|
|
'Newsreader', 'Syne', 'IBM Plex Sans', 'IBM Plex Serif', 'DM Sans', 'DM Serif', 'Outfit',
|
|
'Plus Jakarta Sans', 'Instrument Sans', 'Geist',
|
|
];
|
|
|
|
export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
|
|
// ── The 11 legacy lines. Order and prose are load-bearing: AI_SLOP_BLACKLIST is this list. ──
|
|
{
|
|
id: 'ai-color-palette', impeccableId: 'ai-color-palette', name: 'Purple gradient palette',
|
|
prose: 'Purple/violet/indigo gradient backgrounds or blue-to-purple color schemes',
|
|
category: 'color', kind: 'slop', detect: ['engine', 'grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Look for `linear-gradient` with values in the `#6366f1` to `#8b5cf6` range, or CSS custom properties resolving to purple/violet.',
|
|
handoff: 'colorize', source: 'both', legacyBlacklist: true,
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'feature-grid-3col', name: 'The 3-column feature grid',
|
|
prose: '**The 3-column feature grid:** icon-in-colored-circle + bold title + 2-line description, repeated 3x symmetrically. THE most recognizable AI layout.',
|
|
category: 'scaffold', kind: 'slop', detect: ['grep', 'llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Look for a grid/flex container with exactly 3 children that each contain a circular element + heading + paragraph.',
|
|
handoff: 'layout', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'icon-circle-decoration', name: 'Icons in colored circles',
|
|
prose: 'Icons in colored circles as section decoration (SaaS starter template look)',
|
|
category: 'scaffold', kind: 'slop', detect: ['grep', 'llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Look for elements with `border-radius: 50%` + a background color used as decorative containers for icons.',
|
|
handoff: 'quieter', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'centered-everything', name: 'Centered everything',
|
|
prose: 'Centered everything (`text-align: center` on all headings, descriptions, cards)',
|
|
category: 'layout', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep for `text-align: center` density: if more than 60% of text containers center, flag it.',
|
|
handoff: 'layout', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'uniform-radius', name: 'Uniform bubbly border-radius',
|
|
prose: 'Uniform bubbly border-radius on every element (same large radius on everything)',
|
|
category: 'surface', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Aggregate `border-radius` values: if more than 80% share one value of 16px or more, flag it. Pill radius on everything is the extreme case.',
|
|
handoff: 'polish', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'decorative-blobs', name: 'Decorative blobs and dividers',
|
|
prose: 'Decorative blobs, floating circles, wavy SVG dividers (if a section feels empty, it needs better content, not decoration)',
|
|
category: 'imagery', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'emoji-decoration', name: 'Emoji as design elements',
|
|
prose: 'Emoji as design elements (rockets in headings, emoji as bullet points)',
|
|
category: 'imagery', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep headings, list items, and buttons for emoji code points used as icons or bullets.',
|
|
handoff: 'polish', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'side-tab', impeccableId: 'side-tab', name: 'Colored left-border on cards',
|
|
prose: 'Colored left-border on cards (`border-left: 3px solid <accent>`)',
|
|
category: 'surface', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep for `border-left: <n>px solid` on card, callout, or list-item selectors.',
|
|
handoff: 'polish', source: 'both', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'generic-hero-copy', name: 'Generic hero copy',
|
|
prose: 'Generic hero copy ("Welcome to [X]", "Unlock the power of...", "Your all-in-one solution for...")',
|
|
category: 'copy', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep HTML/JSX content for "Welcome to", "Unlock the power of", "Your all-in-one solution", "Revolutionize your", "Streamline your workflow".',
|
|
handoff: 'clarify', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'cookie-cutter-rhythm', name: 'Cookie-cutter section rhythm',
|
|
prose: 'Cookie-cutter section rhythm (hero → 3 features → testimonials → pricing → CTA, every section same height)',
|
|
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
{
|
|
id: 'system-font-primary', name: 'system-ui as the primary face',
|
|
prose: 'system-ui or `-apple-system` as the PRIMARY display/body font — the "I gave up on typography" signal. Pick a real typeface.',
|
|
category: 'type', kind: 'slop', detect: ['grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep `font-family` on body, headings, and base styles for `system-ui` or `-apple-system` as the first face in the stack.',
|
|
handoff: 'typeset', source: 'gstack', legacyBlacklist: true,
|
|
},
|
|
|
|
// ── Slop the detector knows (ids from the registry fixture). ──
|
|
{
|
|
id: 'border-accent-on-rounded', impeccableId: 'border-accent-on-rounded', name: 'Border accent on a rounded card',
|
|
prose: 'A colored edge on a rounded card: the side-tab in a costume. Signal state with a background tint, an icon, or a label.',
|
|
category: 'surface', kind: 'slop', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'polish', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'overused-font', impeccableId: 'overused-font', name: 'Overused display font',
|
|
prose: 'A training-data default as the display voice means you stopped looking. As body or UI on an Operate or Read surface, several of these are fine. Say which and why.',
|
|
category: 'type', kind: 'slop', detect: ['engine', 'grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep `font-family` for a listed face as the first face on display selectors (h1, h2, .hero, .display).',
|
|
values: OVERUSED_DISPLAY, roles: ['display'],
|
|
handoff: 'typeset', source: 'both',
|
|
},
|
|
{
|
|
id: 'flat-type-hierarchy', impeccableId: 'flat-type-hierarchy', name: 'Flat type hierarchy',
|
|
prose: 'Headings within a step of body size. Pick a scale and let the levels differ by more than a weight.',
|
|
category: 'type', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'gradient-text', impeccableId: 'gradient-text', name: 'Gradient text',
|
|
prose: 'Emphasis is weight or size. Gradient text is emphasis in a costume.',
|
|
category: 'color', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep for `background-clip: text` next to a gradient background.',
|
|
handoff: 'colorize', source: 'impeccable',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'cream-palette', impeccableId: 'cream-palette', name: 'Cream default palette',
|
|
prose: 'Cream ground, serif display, terracotta accent: look number one. Fine when the brief asked for it; a default when it did not.',
|
|
category: 'color', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'colorize', source: 'impeccable',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'nested-cards', impeccableId: 'nested-cards', name: 'Nested cards',
|
|
prose: 'A card inside a card is always wrong. Cards are the lazy container; nesting them is the lazy container squared.',
|
|
category: 'scaffold', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'impeccable',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'monotonous-spacing', impeccableId: 'monotonous-spacing', name: 'Monotonous spacing',
|
|
prose: 'One gap value between everything. Rhythm needs a large step and a small step, not a single beat.',
|
|
category: 'layout', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
handoff: 'layout', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'bounce-easing', impeccableId: 'bounce-easing', name: 'Bounce easing',
|
|
prose: 'Overshoot and bounce curves on UI motion. Exponential ease-out from an already-visible default.',
|
|
category: 'motion', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'polish',
|
|
heuristic: 'Grep transitions and keyframes for cubic-bezier curves with a control point past 1, or `bounce` in animation names.',
|
|
handoff: 'animate', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'pulsing-dot', impeccableId: 'pulsing-dot', name: 'Pulsing status dot',
|
|
prose: 'A small circle pulsing forever next to "Live" or "Online". Motion that says nothing new after the first loop.',
|
|
category: 'motion', kind: 'slop', detect: ['engine', 'grep'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
heuristic: 'Grep for infinite keyframe animations on small round elements.',
|
|
handoff: 'animate', source: 'impeccable',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'blinking-cursor', impeccableId: 'blinking-cursor', name: 'Blinking cursor effect',
|
|
prose: 'A fake terminal cursor blinking in marketing copy. Theater, not interface.',
|
|
category: 'motion', kind: 'slop', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
handoff: 'animate', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'shape-assembled-illustration', impeccableId: 'shape-assembled-illustration', name: 'Shape-assembled illustration',
|
|
prose: 'An illustration built from CSS shapes standing in for an asset. Produce the asset or ship nothing.',
|
|
category: 'imagery', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'dark-glow', impeccableId: 'dark-glow', name: 'Dark-mode glow',
|
|
prose: 'Glowing edges on dark surfaces: look number two. Depth has an offset; a zero-offset colored halo is decoration.',
|
|
category: 'surface', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep `box-shadow` for a zero x/y offset with a large blur and a saturated color.',
|
|
handoff: 'colorize', source: 'impeccable',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'radial-halo', impeccableId: 'radial-halo', name: 'Radial halo',
|
|
prose: 'A radial gradient halo behind the hero content. Look number two again.',
|
|
category: 'surface', kind: 'slop', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'radial-spotlight-glow', impeccableId: 'radial-spotlight-glow', name: 'Radial spotlight glow',
|
|
prose: 'A spotlight glow washing the top of the page. Same family as the halo.',
|
|
category: 'surface', kind: 'slop', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'marquee', impeccableId: 'marquee', name: 'Logo marquee',
|
|
prose: 'An infinitely scrolling logo strip. If the logos matter, show them still; if they do not, cut them.',
|
|
category: 'motion', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'animate', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'icon-tile-stack', impeccableId: 'icon-tile-stack', name: 'Icon tile above every heading',
|
|
prose: 'The rounded-square icon above every heading. Try side by side, or drop the container.',
|
|
category: 'scaffold', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'impeccable',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'italic-serif-display', impeccableId: 'italic-serif-display', name: 'Italic serif display',
|
|
prose: 'Look three: the italic display serif reaching for editorial credibility. Earn it with the content or set the display upright.',
|
|
category: 'type', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'hero-eyebrow-chip', impeccableId: 'hero-eyebrow-chip', name: 'Hero eyebrow chip',
|
|
prose: 'A pill-shaped label floating above the hero headline. The headline carries its own weight; cut the chip.',
|
|
category: 'scaffold', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'kicker-above-heading', impeccableId: 'kicker-above-heading', name: 'Kicker above heading',
|
|
prose: 'A kicker above a heading is the strongest default there is: the heading carries its own weight, so delete the label. If the user wants it anyway, comply and say the tradeoff once.',
|
|
category: 'scaffold', kind: 'slop', detect: ['engine', 'grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Look for a short uppercase, tracked element immediately before an h1 or h2.',
|
|
handoff: 'layout', source: 'impeccable',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'numbered-section-labels', impeccableId: 'numbered-section-labels', name: 'Numbered section labels',
|
|
prose: '01 / 02 / 03 over sections, unless the sequence is information the reader needs.',
|
|
category: 'scaffold', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
handoff: 'layout', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'em-dash-overuse', impeccableId: 'em-dash-overuse', name: 'Em-dash overuse',
|
|
prose: 'Em dashes in every other sentence. Advisory: a tell of generated copy, never a blocker on its own.',
|
|
category: 'copy', kind: 'slop', detect: ['engine'], confidence: 'LOW', tier: 'possible', impact: 'polish',
|
|
handoff: 'clarify', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'marketing-buzzword', impeccableId: 'marketing-buzzword', name: 'Marketing buzzwords',
|
|
prose: '"Seamless", "effortless", "supercharge", "streamline": words that describe nothing. Say what the product does.',
|
|
category: 'copy', kind: 'slop', detect: ['engine', 'grep'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep visible copy for seamless, effortless, supercharge, streamline, revolutionize, unlock, empower, elevate.',
|
|
handoff: 'clarify', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'aphoristic-cadence', impeccableId: 'aphoristic-cadence', name: 'Aphoristic cadence',
|
|
prose: 'Short. Punchy. Fragments. Every sentence a slogan. Write like a person explaining something.',
|
|
category: 'copy', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'clarify', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'oversized-h1', impeccableId: 'oversized-h1', name: 'Oversized h1',
|
|
prose: 'Display type past 6rem on a page that is not a poster. Size is not hierarchy.',
|
|
category: 'type', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep h1 and display selectors for font-size above 6rem or 96px.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'extreme-negative-tracking', impeccableId: 'extreme-negative-tracking', name: 'Extreme negative tracking',
|
|
prose: 'Letter-spacing below -0.04em on display type. Tight tracking is a taste; crushed tracking is a tell.',
|
|
category: 'type', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'polish',
|
|
heuristic: 'Grep `letter-spacing` for values below -0.04em.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'gpt-thin-border-wide-shadow', impeccableId: 'gpt-thin-border-wide-shadow', name: 'Thin border plus wide shadow',
|
|
prose: 'A hairline border and a wide soft shadow on the same card. Pick one way to lift the surface.',
|
|
category: 'surface', kind: 'slop', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
handoff: 'polish', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'repeating-stripes-gradient', impeccableId: 'repeating-stripes-gradient', name: 'Repeating stripes gradient',
|
|
prose: 'Diagonal stripe gradients as background texture. Texture from the brand or none.',
|
|
category: 'surface', kind: 'slop', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'codex-grid-background', impeccableId: 'codex-grid-background', name: 'Grid-paper background',
|
|
prose: 'A faint grid behind the hero. The blueprint look every generated dev tool ships.',
|
|
category: 'surface', kind: 'slop', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'theater-slop-phrase', impeccableId: 'theater-slop-phrase', name: 'Theater phrases',
|
|
prose: '"Built for the way you work", "Designed for teams like yours", "Meet your new...": phrases that perform a launch instead of describing one.',
|
|
category: 'copy', kind: 'slop', detect: ['engine', 'grep'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep copy for "built for", "designed for", "meet your new", "ship faster", "the future of".',
|
|
handoff: 'clarify', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'image-hover-transform', impeccableId: 'image-hover-transform', name: 'Image hover zoom',
|
|
prose: 'Scaling an image on hover. Motion with no information in it.',
|
|
category: 'motion', kind: 'slop', detect: ['engine', 'grep'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
heuristic: 'Grep `:hover` rules on images for `transform: scale`.',
|
|
handoff: 'animate', source: 'impeccable',
|
|
},
|
|
|
|
// ── gstack-only slop tells: the LLM pass judges these; no detector id, so no brackets. ──
|
|
{
|
|
id: 'gradient-cta', name: 'Gradient CTA button',
|
|
prose: 'Gradient buttons as the primary call to action. One solid color the palette owns.',
|
|
category: 'color', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep button and CTA selectors for gradient backgrounds.',
|
|
handoff: 'colorize', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'stock-photo-hero', name: 'Stock-photo hero',
|
|
prose: 'A generic stock-photo hero, or a gray placeholder div standing in for one. Show the product or show nothing.',
|
|
category: 'imagery', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'card-default-component', name: 'Cards as the default component',
|
|
prose: 'Rounded cards with drop shadows as the container for everything. App UI made of stacked cards is not layout.',
|
|
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'generic-testimonials', name: 'Generic testimonial section',
|
|
prose: 'A testimonial row with avatars, five stars, and quotes nobody said. Real names with real claims, or cut it.',
|
|
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'clarify', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'split-hero-template', name: 'Left-text right-image hero',
|
|
prose: 'The cookie-cutter hero: headline left, screenshot right, two buttons. The first template every generator reaches for.',
|
|
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'generic-cta-copy', name: 'Generic CTA labels',
|
|
prose: '"Get Started" and "Learn More" as the only calls to action. Name the outcome the click buys.',
|
|
category: 'copy', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep buttons and links for "Get Started" and "Learn More" with no more specific CTA on the page.',
|
|
handoff: 'clarify', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'hero-metrics', name: 'Hero metric template',
|
|
prose: 'Three big numbers with tiny labels under the hero ("10k+ users", "99.9%"). The template counts, not the product.',
|
|
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'clarify', source: 'gstack',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'identical-cards', name: 'Identical card grids',
|
|
prose: 'A grid of cards with the same shape, the same icon slot, the same two lines. Content of unequal weight given equal boxes.',
|
|
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'gstack',
|
|
mockupNever: true,
|
|
},
|
|
{
|
|
id: 'glassmorphism', name: 'Glassmorphism',
|
|
prose: 'Frosted-glass panels with blurred backdrops as the default surface. One translucent layer where it explains depth, not everywhere.',
|
|
category: 'surface', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep for `backdrop-filter: blur` on more than one container.',
|
|
handoff: 'quieter', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'hand-drawn-svg', name: 'Hand-drawn SVG illustration',
|
|
prose: 'Generated SVG doodles and mascots in place of art direction. Commission or license an asset, or ship none.',
|
|
category: 'imagery', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'modal-by-default', name: 'Modal by default',
|
|
prose: 'Every secondary action in a modal. Inline, a side panel, or a new page usually costs the user less.',
|
|
category: 'states', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'harden', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'monospace-costume', name: 'Monospace as costume',
|
|
prose: 'Monospace on labels and body copy to look technical. Mono is for code and data columns.',
|
|
category: 'type', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
heuristic: 'Grep `font-family` for a monospace stack on non-code, non-tabular selectors.',
|
|
handoff: 'typeset', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'content-stand-ins', name: 'Content stand-ins',
|
|
prose: 'Sparklines, progress rings, and fake avatars filling space where content should be. Real data or an honest empty state.',
|
|
category: 'imagery', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'harden', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'mode-by-category', name: 'Mode picked by category',
|
|
prose: 'Dark because it is a dev tool, light because it is health. Light or dark comes from the use scene: who, where, under what light.',
|
|
category: 'color', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
|
|
handoff: 'colorize', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'unthemed-browser-surfaces', name: 'Unthemed browser surfaces',
|
|
prose: 'Selection color, caret, scrollbars, focus rings, underline offset, tabular numerals left at browser defaults. Theme them from the palette.',
|
|
category: 'browser-surface', kind: 'slop', detect: ['grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
heuristic: 'Grep for `::selection`, `caret-color`, `accent-color`, `scrollbar-color`, `text-underline-offset`, `font-variant-numeric`: none present means none themed.',
|
|
handoff: 'polish', source: 'gstack',
|
|
},
|
|
{
|
|
id: 'missing-states', name: 'Missing states',
|
|
prose: 'Only the happy path is designed. Empty, loading, error, and long-content states are part of the component.',
|
|
category: 'states', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'high',
|
|
handoff: 'harden', source: 'gstack',
|
|
},
|
|
|
|
// ── Quality rules the detector knows. ──
|
|
{
|
|
id: 'organic-clip-path', impeccableId: 'organic-clip-path', name: 'Organic clip-path',
|
|
prose: 'A polygon clip-path approximating a photo edge or a blob. An asset with its own edge, or a rectangle.',
|
|
category: 'imagery', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'buried-raster', impeccableId: 'buried-raster', name: 'Buried raster',
|
|
prose: 'A photo under a near-opaque wash. If the image cannot be seen, it is not doing anything.',
|
|
category: 'imagery', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'quieter', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'broken-image', impeccableId: 'broken-image', name: 'Broken image',
|
|
prose: 'An image that fails to load. Nothing on the page is more visible.',
|
|
category: 'imagery', kind: 'quality', detect: ['engine', 'render'], confidence: 'HIGH', tier: 'ask', impact: 'high',
|
|
handoff: 'harden', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'script-error', impeccableId: 'script-error', name: 'Script error',
|
|
prose: 'A JavaScript error in the console on load. The page is not finished.',
|
|
category: 'states', kind: 'quality', detect: ['engine', 'render'], confidence: 'HIGH', tier: 'ask', impact: 'high',
|
|
handoff: 'harden', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'content-hidden-at-rest', impeccableId: 'content-hidden-at-rest', name: 'Content hidden at rest',
|
|
prose: 'Content at opacity 0 waiting for a scroll animation that may never fire. Content is visible by default.',
|
|
category: 'motion', kind: 'quality', detect: ['engine', 'render'], confidence: 'HIGH', tier: 'ask', impact: 'high',
|
|
handoff: 'animate', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'edge-flush-cards', impeccableId: 'edge-flush-cards', name: 'Edge-flush cards',
|
|
prose: 'Cards touching the viewport edge. Give the layout a gutter.',
|
|
category: 'layout', kind: 'quality', detect: ['engine', 'render'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'text-occlusion', impeccableId: 'text-occlusion', name: 'Text occlusion',
|
|
prose: 'Text covered by another element. Overlap is a bug until it is a choice.',
|
|
category: 'layout', kind: 'quality', detect: ['engine', 'render'], confidence: 'HIGH', tier: 'ask', impact: 'high',
|
|
handoff: 'harden', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'first-viewport-column-overflow', impeccableId: 'first-viewport-column-overflow', name: 'First-viewport overflow',
|
|
prose: 'A column wider than the first viewport. Horizontal scroll on arrival.',
|
|
category: 'layout', kind: 'quality', detect: ['engine', 'render'], confidence: 'HIGH', tier: 'ask', impact: 'high',
|
|
handoff: 'layout', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'gray-on-color', impeccableId: 'gray-on-color', name: 'Gray text on a colored surface',
|
|
prose: 'Secondary text on a colored surface is tinted from that hue. Never gray.',
|
|
category: 'color', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'colorize', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'low-contrast', impeccableId: 'low-contrast', name: 'Low contrast text',
|
|
prose: 'Text below WCAG AA contrast (4.5:1 body, 3:1 large). Fix the pair, not the opacity.',
|
|
category: 'color', kind: 'quality', detect: ['engine', 'render'], confidence: 'HIGH', tier: 'ask', impact: 'high',
|
|
handoff: 'colorize', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'layout-transition', impeccableId: 'layout-transition', name: 'Layout-property transition',
|
|
prose: '`transition: all`, or transitions on width, height, top, left. Animate transform and opacity.',
|
|
category: 'motion', kind: 'quality', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'auto-fix', impact: 'polish',
|
|
heuristic: 'Grep `transition` for `all` or layout properties.',
|
|
handoff: 'animate', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'line-length', impeccableId: 'line-length', name: 'Line length',
|
|
prose: 'Body measure outside 45 to 75 characters. Set a max-width on the text column.',
|
|
category: 'type', kind: 'quality', detect: ['engine', 'grep'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Check for `max-width` on body text wrappers.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'cramped-padding', impeccableId: 'cramped-padding', name: 'Cramped padding',
|
|
prose: 'Padding under 8px on text containers. Text needs room to breathe.',
|
|
category: 'layout', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'body-text-viewport-edge', impeccableId: 'body-text-viewport-edge', name: 'Body text at the viewport edge',
|
|
prose: 'Body text within a few pixels of the viewport edge on small screens.',
|
|
category: 'layout', kind: 'quality', detect: ['engine', 'render'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'layout', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'tight-leading', impeccableId: 'tight-leading', name: 'Tight leading',
|
|
prose: 'Body line-height under 1.4. Display type can run tight; paragraphs cannot.',
|
|
category: 'type', kind: 'quality', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Grep body and paragraph `line-height` for values below 1.4.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'skipped-heading', impeccableId: 'skipped-heading', name: 'Skipped heading level',
|
|
prose: 'h1 followed by h3 with no h2. Screen readers walk the hierarchy.',
|
|
category: 'type', kind: 'quality', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
heuristic: 'Check HTML/JSX for heading tags that skip a level within a file or component.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'heading-rhythm', impeccableId: 'heading-rhythm', name: 'Heading rhythm',
|
|
prose: 'More space above a heading than below it. Read the computed values.',
|
|
category: 'type', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'justified-text', impeccableId: 'justified-text', name: 'Justified text',
|
|
prose: 'Justified body text on the web leaves rivers. Left-align.',
|
|
category: 'type', kind: 'quality', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'auto-fix', impact: 'polish',
|
|
heuristic: 'Grep for `text-align: justify`.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'tiny-text', impeccableId: 'tiny-text', name: 'Tiny text',
|
|
prose: 'Body text under 16px. Bump to 16px.',
|
|
category: 'type', kind: 'quality', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'auto-fix', impact: 'medium',
|
|
heuristic: 'Grep `font-size` on body, p, and base styles for values under 16px (1rem at a 16px base).',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'undersized-ui-text', impeccableId: 'undersized-ui-text', name: 'Undersized UI text',
|
|
prose: 'Labels and controls under 12px. Nobody reads 10px.',
|
|
category: 'type', kind: 'quality', detect: ['engine'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'all-caps-body', impeccableId: 'all-caps-body', name: 'All-caps body text',
|
|
prose: 'Uppercase paragraphs. Caps are for short labels.',
|
|
category: 'type', kind: 'quality', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'auto-fix', impact: 'medium',
|
|
heuristic: 'Grep `text-transform: uppercase` on body and paragraph selectors.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'wide-tracking', impeccableId: 'wide-tracking', name: 'Wide tracking on body',
|
|
prose: 'Letter-spacing above 0.05em on body text. Tracked type is for small-caps labels.',
|
|
category: 'type', kind: 'quality', detect: ['engine', 'grep'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
|
|
heuristic: 'Grep body `letter-spacing` for values above 0.05em.',
|
|
handoff: 'typeset', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'text-overflow', impeccableId: 'text-overflow', name: 'Text overflow',
|
|
prose: 'Text spilling out of its container. Long content is the normal case.',
|
|
category: 'states', kind: 'quality', detect: ['engine', 'render'], confidence: 'HIGH', tier: 'ask', impact: 'high',
|
|
handoff: 'harden', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'repeated-container-text', impeccableId: 'repeated-container-text', name: 'Repeated container text',
|
|
prose: 'The same text repeated across sibling containers. Placeholder content that shipped.',
|
|
category: 'copy', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'clarify', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'clipped-overflow-container', impeccableId: 'clipped-overflow-container', name: 'Clipped overflow',
|
|
prose: 'A container clipping its own content with overflow hidden. Something is cut off.',
|
|
category: 'states', kind: 'quality', detect: ['engine', 'render'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'harden', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'design-system-font', impeccableId: 'design-system-font', name: 'Off-system font',
|
|
prose: 'A face DESIGN.md tokens do not name. Add the token or use one that exists.',
|
|
category: 'type', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'polish', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'design-system-color', impeccableId: 'design-system-color', name: 'Off-system color',
|
|
prose: 'A color DESIGN.md tokens do not name. Add the token or use one that exists.',
|
|
category: 'color', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'polish', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'design-system-radius', impeccableId: 'design-system-radius', name: 'Off-system radius',
|
|
prose: 'A radius DESIGN.md tokens do not name. Add the token or use one that exists.',
|
|
category: 'surface', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'polish', source: 'impeccable',
|
|
},
|
|
{
|
|
id: 'design-system-font-size', impeccableId: 'design-system-font-size', name: 'Off-system font size',
|
|
prose: 'A font size DESIGN.md tokens do not name. Add the token or use one on the scale.',
|
|
category: 'type', kind: 'quality', detect: ['engine'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
|
|
handoff: 'polish', source: 'impeccable',
|
|
},
|
|
];
|
|
|
|
// ── Mockup prompt ──
|
|
|
|
/**
|
|
* Plain-English names of the entries flagged `mockupNever`, deduped: the design
|
|
* binary appends "Never: <names>." to every image-generation prompt
|
|
* (design/src/brief.ts) so mockups stop reaching for these before the
|
|
* comparison board opens. Exactly ten ids carry the flag (test-enforced).
|
|
*/
|
|
export const MOCKUP_NEVER_NAMES: readonly string[] = [
|
|
...new Set(DESIGN_SLOP_CATALOG.filter(e => e.mockupNever).map(e => e.name)),
|
|
];
|
|
|
|
// ── Fonts ──
|
|
|
|
/** Never the display voice; the detector flags several as `overused-font`. */
|
|
export const OVERUSED_FONTS_DISPLAY: readonly string[] = OVERUSED_DISPLAY;
|
|
|
|
/** Never, in any role. */
|
|
export const BANNED_FONTS: readonly string[] = [
|
|
'Papyrus', 'Comic Sans', 'Lobster', 'Impact', 'Jokerman', 'Bleeding Cowboys', 'Permanent Marker',
|
|
'Bradley Hand', 'Brush Script', 'Hobo', 'Trajan', 'Raleway', 'Clash Display', 'Courier New',
|
|
];
|
|
|
|
/** On the overused list, yet fine as body or UI on an Operate or Read surface when the proposal says so. */
|
|
export const FONTS_BODY_UI_OK: readonly string[] = ['DM Sans', 'Instrument Sans', 'IBM Plex Sans'];
|
|
|
|
/** Mono for data and code. */
|
|
export const FONTS_MONO_OK: readonly string[] = ['JetBrains Mono', 'IBM Plex Mono', 'Fira Code'];
|
|
|
|
/**
|
|
* Freely available faces on no default list. Availability was verified at the
|
|
* last edit of this constant; a proposal re-verifies in-session before naming one.
|
|
*/
|
|
export const FONTS_VERIFIED_FREE = {
|
|
verified: '2026-09-08',
|
|
fontshare: ['Satoshi', 'General Sans', 'Clash Grotesk', 'Cabinet Grotesk'],
|
|
googleFonts: ['Instrument Serif', 'Source Sans 3', 'JetBrains Mono', 'Fira Code'],
|
|
} as const;
|
|
|
|
// ── Lookups ──
|
|
|
|
const BY_ID = new Map(DESIGN_SLOP_CATALOG.map(e => [e.id, e]));
|
|
const BY_IMPECCABLE_ID = new Map(
|
|
DESIGN_SLOP_CATALOG.filter(e => e.impeccableId).map(e => [e.impeccableId as string, e]),
|
|
);
|
|
|
|
export function catalogEntry(id: string): DesignSlopEntry | undefined {
|
|
return BY_ID.get(id);
|
|
}
|
|
|
|
/** The catalog entry for a detector rule id, or undefined when the id is unmapped. */
|
|
export function entryForImpeccableId(impeccableId: string): DesignSlopEntry | undefined {
|
|
return BY_IMPECCABLE_ID.get(impeccableId);
|
|
}
|
|
|
|
// ── Rendering ──
|
|
|
|
export interface RenderCatalogOptions {
|
|
kind?: 'slop' | 'quality';
|
|
/** drop entries whose impact is in this list (e.g. ['polish'] for a shorter list) */
|
|
omitImpact?: Impact[];
|
|
}
|
|
|
|
export function selectCatalog(o: RenderCatalogOptions): DesignSlopEntry[] {
|
|
return DESIGN_SLOP_CATALOG.filter(e =>
|
|
(!o.kind || e.kind === o.kind)
|
|
&& !(o.omitImpact && o.omitImpact.includes(e.impact)),
|
|
);
|
|
}
|
|
|
|
/** `- prose` bullets, no ids: the register the proposal skills render (design-consultation, design-shotgun). */
|
|
export function renderCatalog(o: RenderCatalogOptions): string {
|
|
return selectCatalog(o).map(e => `- ${e.prose}`).join('\n');
|
|
}
|
|
|
|
/** Slop the detector knows, minus the 11 legacy lines: what design doctrine renders as bracketed ids. */
|
|
export function detectorSlopEntries(o: { omitPolish?: boolean } = {}): DesignSlopEntry[] {
|
|
return DESIGN_SLOP_CATALOG.filter(e => e.kind === 'slop' && e.impeccableId && !e.legacyBlacklist && !(o.omitPolish && e.impact === 'polish'));
|
|
}
|
|
|
|
/** gstack-only slop tells (no detector rule), minus the legacy lines: the LLM pass is the detector. */
|
|
export function judgmentTellEntries(o: { omitPolish?: boolean } = {}): DesignSlopEntry[] {
|
|
return DESIGN_SLOP_CATALOG.filter(e => e.kind === 'slop' && !e.impeccableId && !e.legacyBlacklist && !(o.omitPolish && e.impact === 'polish'));
|
|
}
|
|
|
|
/** Catalog entries by id, throwing with the id when one is missing (a rename must fail loudly at gen time). */
|
|
export function catalogEntries(ids: string[]): DesignSlopEntry[] {
|
|
return ids.map(id => {
|
|
const e = BY_ID.get(id);
|
|
if (!e) throw new Error(`lib/design-catalog.ts: no entry with id "${id}"`);
|
|
return e;
|
|
});
|
|
}
|