mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
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>
133 lines
5.6 KiB
TypeScript
133 lines
5.6 KiB
TypeScript
// lib/design-detect-contract.ts — the one owner of the design-detector vocabulary.
|
|
//
|
|
// Pure module: no I/O, no imports from scripts/. Every sentinel the wrapper
|
|
// (bin/gstack-design-detect.ts) or the DESIGN.md tool (bin/gstack-design-md.ts)
|
|
// prints, and every one the skill prose reads, is a constant here, so the two
|
|
// sides cannot drift: gen-time resolvers import these strings into SKILL.md
|
|
// prose, the bins import them at runtime, and test/design-detect-contract.test.ts
|
|
// asserts that every sentinel-shaped token in generated docs exists here.
|
|
//
|
|
// probe ──► one of: IMPECCABLE_READY | IMPECCABLE_NOT_CACHED | IMPECCABLE_NOT_AVAILABLE | IMPECCABLE_DISABLED
|
|
// ──► always: IMPECCABLE_SKILL, IMPECCABLE_HOOK, IMPECCABLE_IGNORED_RULES, IMPECCABLE_IGNORED_FILES
|
|
// ──► maybe: IMPECCABLE_HOOK_OTHER, IMPECCABLE_CONFIG_UNREADABLE, IMPECCABLE_ENV_IGNORED,
|
|
// IMPECCABLE_ENGINE_UNTESTED, DESIGN_DETECTOR_HINT
|
|
// scan ──► stdout: one JSON document (--format gstack) or engine bytes (--format raw)
|
|
// ──► stderr: DETECT_TOP block, DETECT_SUMMARY, DETECT_EXIT, DETECT_REFUSED / DETECT_NO_TARGETS /
|
|
// DETECT_TIMEOUT / DETECT_PARSE_ERROR / DETECT_OUTPUT_TOO_LARGE
|
|
// any ──► exit 3 + DESIGN_DETECT_INTERNAL_ERROR: a gstack bug, never retried
|
|
|
|
export const SENTINEL = {
|
|
READY: 'IMPECCABLE_READY',
|
|
NOT_CACHED: 'IMPECCABLE_NOT_CACHED',
|
|
NOT_AVAILABLE: 'IMPECCABLE_NOT_AVAILABLE',
|
|
DISABLED: 'IMPECCABLE_DISABLED',
|
|
SKILL: 'IMPECCABLE_SKILL',
|
|
HOOK: 'IMPECCABLE_HOOK',
|
|
HOOK_OTHER: 'IMPECCABLE_HOOK_OTHER',
|
|
IGNORED_RULES: 'IMPECCABLE_IGNORED_RULES',
|
|
IGNORED_FILES: 'IMPECCABLE_IGNORED_FILES',
|
|
CONFIG_UNREADABLE: 'IMPECCABLE_CONFIG_UNREADABLE',
|
|
ENV_IGNORED: 'IMPECCABLE_ENV_IGNORED',
|
|
ENGINE_UNTESTED: 'IMPECCABLE_ENGINE_UNTESTED',
|
|
HINT: 'DESIGN_DETECTOR_HINT',
|
|
DETECT_EXIT: 'DETECT_EXIT',
|
|
DETECT_EXIT_CODE: 'DETECT_EXIT_CODE',
|
|
DETECT_SUMMARY: 'DETECT_SUMMARY',
|
|
DETECT_TOP: 'DETECT_TOP',
|
|
DETECT_REFUSED: 'DETECT_REFUSED',
|
|
DETECT_NO_TARGETS: 'DETECT_NO_TARGETS',
|
|
DETECT_TIMEOUT: 'DETECT_TIMEOUT',
|
|
DETECT_PARSE_ERROR: 'DETECT_PARSE_ERROR',
|
|
DETECT_OUTPUT_TOO_LARGE: 'DETECT_OUTPUT_TOO_LARGE',
|
|
INTERNAL_ERROR: 'DESIGN_DETECT_INTERNAL_ERROR',
|
|
/** printed by rendered bash: the temp file holding a scan's JSON */
|
|
DETECT_JSON: 'DETECT_JSON',
|
|
/** printed by rendered bash after a DOM dump is persisted */
|
|
DOM_DUMP_OK: 'DOM_DUMP_OK',
|
|
DOM_DUMP_REDACTION_BLOCKED: 'DOM_DUMP_REDACTION_BLOCKED',
|
|
DOM_DUMP_TOO_LARGE: 'DOM_DUMP_TOO_LARGE',
|
|
DESIGN_MD_FORMAT: 'DESIGN_MD_FORMAT',
|
|
DESIGN_MD_CONVERT_REFUSED: 'DESIGN_MD_CONVERT_REFUSED',
|
|
DESIGN_MD_INTERNAL_ERROR: 'DESIGN_MD_INTERNAL_ERROR',
|
|
DESIGN_MD_TOKEN_REF_INVALID: 'DESIGN_MD_TOKEN_REF_INVALID',
|
|
} as const;
|
|
|
|
export type SentinelName = keyof typeof SENTINEL;
|
|
|
|
/**
|
|
* Sentinels whose line explains itself after the colon (a path, a version, a
|
|
* reason). Prose need not teach them; the agent notes them and moves on. The
|
|
* contract test requires every OTHER sentinel to be taught somewhere the agent
|
|
* reads.
|
|
*/
|
|
export const SELF_DESCRIBING_SENTINELS: readonly string[] = [
|
|
SENTINEL.HOOK_OTHER, SENTINEL.IGNORED_FILES, SENTINEL.CONFIG_UNREADABLE, SENTINEL.ENV_IGNORED,
|
|
SENTINEL.ENGINE_UNTESTED, SENTINEL.DETECT_EXIT, SENTINEL.DETECT_REFUSED, SENTINEL.DETECT_NO_TARGETS,
|
|
SENTINEL.DETECT_TIMEOUT, SENTINEL.DETECT_PARSE_ERROR, SENTINEL.DETECT_OUTPUT_TOO_LARGE,
|
|
];
|
|
|
|
/** Engine versions the committed fixtures were captured from. */
|
|
export const TESTED_ENGINE_VERSIONS: readonly string[] = ['0.1.3'];
|
|
|
|
/** Rules the engine reports but never counts (they never change its exit code). */
|
|
export const ADVISORY_RULE_IDS: readonly string[] = ['em-dash-overuse'];
|
|
|
|
export const DETECT_LIMITS = {
|
|
/** default engine wall clock; GSTACK_DESIGN_DETECT_TIMEOUT_MS overrides */
|
|
timeoutMs: 120_000,
|
|
/** absolute paths per engine invocation */
|
|
batch: 100,
|
|
/** engine stdout above this is DETECT_OUTPUT_TOO_LARGE */
|
|
stdoutBytes: 50 * 1024 * 1024,
|
|
/** normalized findings kept; the rest is `truncated: true` */
|
|
findings: 5_000,
|
|
/** locations printed in the DETECT_TOP block */
|
|
topLocations: 50,
|
|
/** rendered-DOM dump above this is DOM_DUMP_TOO_LARGE */
|
|
domDumpBytes: 10 * 1024 * 1024,
|
|
field: { id: 64, message: 120, snippet: 120, value: 200, file: 4096, diagnostic: 400 },
|
|
} as const;
|
|
|
|
/** Markers around any engine text the skill may quote (page text can echo through it). */
|
|
export const UNTRUSTED_BEGIN = '═══ BEGIN UNTRUSTED CONTENT (design detector output) ═══';
|
|
export const UNTRUSTED_END = '═══ END UNTRUSTED CONTENT ═══';
|
|
|
|
export interface NormalizedFinding {
|
|
/** catalog id (equals impeccableId when mapped; the engine's id, sanitized, when not) */
|
|
id: string;
|
|
impeccableId: string;
|
|
file: string;
|
|
line: number;
|
|
snippet: string;
|
|
value?: string;
|
|
message: string;
|
|
category: string;
|
|
kind: 'slop' | 'quality' | 'unknown';
|
|
impact: 'high' | 'medium' | 'polish';
|
|
tier: 'auto-fix' | 'ask' | 'possible';
|
|
handoff?: string;
|
|
advisory: boolean;
|
|
unmapped?: true;
|
|
}
|
|
|
|
export interface ScanResult {
|
|
schemaVersion: 1;
|
|
engine: string;
|
|
engineVersion: string;
|
|
targets: number;
|
|
/** engine exit code after precedence (1 over 2 over 0) */
|
|
exit: number;
|
|
total: number;
|
|
counted: number;
|
|
advisory: number;
|
|
/** rule ids the project config ignores (never present in findings) */
|
|
ignoredRules: string[];
|
|
byRule: Record<string, number>;
|
|
findings: NormalizedFinding[];
|
|
truncated: boolean;
|
|
diagnostics: string[];
|
|
}
|
|
|
|
/** The bash a skill renders after a scan so exit 2 (findings) never aborts the block. */
|
|
export const DETECT_EXIT_ECHO = `; echo "${SENTINEL.DETECT_EXIT_CODE}=$?"`;
|