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>
This commit is contained in:
Garry Tan
2026-09-08 17:31:30 +00:00
co-authored by Claude Fable 5.1
parent b4d88a0126
commit da6f0ff2f6
32 changed files with 413 additions and 219 deletions
+27 -26
View File
@@ -29,12 +29,9 @@ 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';
export type Handoff = 'typeset' | 'layout' | 'colorize' | 'harden' | 'clarify' | 'polish' | 'animate' | 'quieter';
/** The `/impeccable <cmd>` commands a deferred finding may hand off to. */
export const HANDOFF_COMMANDS: readonly Handoff[] = [
'typeset', 'layout', 'colorize', 'harden', 'clarify', 'polish', 'animate', 'quieter',
];
/** 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 */
@@ -103,7 +100,7 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
{
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: 'HIGH', tier: 'ask', impact: 'medium',
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,
},
@@ -666,7 +663,7 @@ 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 (for body)',
'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. */
@@ -705,33 +702,37 @@ export function entryForImpeccableId(impeccableId: string): DesignSlopEntry | un
export interface RenderCatalogOptions {
kind?: 'slop' | 'quality';
category?: SlopCategory;
/** drop entries whose impact is in this list (e.g. ['polish'] for a shorter list) */
omitImpact?: Impact[];
/**
* bullets: `- prose` (design-consultation, design-shotgun: prose only, no ids)
* ids: `- [impeccableId] prose`, bracket only when the detector knows the id
* compact: `id: name; id: name; ...` on one line
*/
style: 'bullets' | 'ids' | 'compact';
}
export function selectCatalog(o: Omit<RenderCatalogOptions, 'style'>): DesignSlopEntry[] {
export function selectCatalog(o: RenderCatalogOptions): DesignSlopEntry[] {
return DESIGN_SLOP_CATALOG.filter(e =>
(!o.kind || e.kind === o.kind)
&& (!o.category || e.category === o.category)
&& !(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 {
const entries = selectCatalog(o);
switch (o.style) {
case 'bullets':
return entries.map(e => `- ${e.prose}`).join('\n');
case 'ids':
return entries.map(e => (e.impeccableId ? `- [${e.impeccableId}] ${e.prose}` : `- ${e.prose}`)).join('\n');
case 'compact':
return entries.map(e => `${e.impeccableId ?? e.id}: ${e.name}`).join('; ');
}
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;
});
}