Design check

Two engines decide whether an artifact this engine produces reads as designed or

Two engines decide whether an artifact this engine produces reads as designed or as generated. This page explains what each can see, what the calibration deliberately silences, and why the gate freezes what already exists instead of demanding it be rebuilt.

The prose rule these engines serve is .claude/rules/visual-design-discipline.md. Its sibling for text is the humanisation rule; the shape is the same, the medium is different.


The two engines

python scripts/visual-discipline-check.py <file-or-dir>          # regex only
python scripts/visual-discipline-check.py --deep <file-or-dir>   # both engines

The regex engine runs always. It matches patterns against file contents: forbidden fonts, the purple-to-pink hero gradient, oversized Tailwind radii, Lucide and Heroicons defaults, the banned hero colours, plus advisory layout and copy heuristics. It reads HTML, SVG and PPTX. It sees what is written down.

The deep engine runs on --deep. It is the impeccable CLI (Apache 2.0), pinned in scripts/.impeccable-version. It parses the HTML, resolves the CSS cascade, and computes real values: text contrast against the surface actually behind it, heading hierarchy, accent stripes on rounded corners, type floors, kickers, buzzwords and cadence tells in copy. It reads HTML and SVG. It sees what renders.

The difference is not academic. The regex engine can be told font-family: Inter and see it.

It cannot be told that #79839a on #ffffff is a 3.8:1 contrast ratio against a 4.5:1 accessibility floor, because that fact does not exist in any single line of the file. Every deep finding carries an impeccable: type prefix, so in a merged report you can always tell which engine made a claim.


Calibration

Three filters sit between the deep engine and a finding anyone acts on. All three are declared in config/visual-check-profiles.json, each entry carrying the reason it exists, because a silent suppression is indistinguishable from a missing rule.

Profiles

A profile is assigned by path, longest matching glob winning.

Profile Applies to Silences
screen the documentation site, dashboards, briefing HTML nothing
print A4 documents read as PDF the type floors, line measure, container-inset checks
doctype the five locked corporate templates and their renders the print set, plus the approved kicker, section numbering, tracked uppercase

The type floors are the largest single group. Impeccable enforces an 11px minimum, which is a screen floor: a CSS pixel on an A4 page is not a screen pixel, and 9px sets a normal print caption. On one measured document family, the print profile removed 1,184 findings that said nothing true about the artifact.

The doctype profile encodes a rule of precedence rather than a technical fact: where a detector rule and a locked corporate template disagree, the template wins. The kicker above a heading is an approved element of the xPager layout, and changing an approved template is a decision for its owner, not for a detector.

Plausibility bounds

The parser emits physically impossible readings on some CSS: an h1 measured at 2856px, a line-height of 0.11x. These are filtered on value, not by disabling the rule that produced them, so a genuine 96px oversized headline still lands.

Scope

Minified and vendored bundles are excluded. A regex inside a minified Mermaid build produced a broken-image finding on the first run; nobody designed that file.


The baseline is a ratchet

python scripts/visual-discipline-check.py baseline record --deep docs/
python scripts/visual-discipline-check.py baseline check --deep docs/

.visual-baseline.json records a count per file and per rule, the same shape as .lint-baseline.json does for lint debt. record freezes what exists. check fails only on findings above those counts, and never rewrites the file.

This is deliberate and it is the whole reason the integration was possible without a rewrite. Measured on first run, the documentation site carried 390 findings and the branded document family carried thousands. Demanding zero would have meant either rebuilding every existing artifact before the tool could be used at all, or turning the gate off. Freezing means existing work is left alone while new work is held to the standard, and each frozen finding surfaces the moment its file is next edited.

A file absent from the baseline is a new file: nothing is suppressed for it. That asymmetry is the point.

The baseline covers both engines. Freezing only the deep findings left pre-existing regex debt failing forever, and a gate that is always red is a gate nobody reads.


Where it runs

Surface Behaviour
CI (guards job) baseline check --deep docs/ fails the build on a regression
regenerate-docs-html.py --all prints the verdict; never blocks a regeneration
render-doctype.py checks the rendered HTML under the doctype profile; reports
marp_render.py checks the rendered deck under the screen profile; reports

Only CI gates. A renderer that refused to write a file because a heading level was skipped would block the very edit that fixes it.


Honest limits

Four, stated plainly so a clean result is not over-read.

PPTX, DOCX and PDF are not covered by the deep engine. It reads HTML, SVG and source text. Decks stay on the regex path. This is the largest remaining gap.

An unused CSS rule is not a finding. The cascade is resolved against real elements, so a declaration no element uses is correctly invisible. A clean result is not proof that a stylesheet is clean.

npx --yes fetches and executes third-party code at call time. The exact version pin is the only mitigation claimed here. It is the same exposure this engine already accepts for marp-cli, and it is weaker than a hash-verified install. Saying otherwise would be dressing a convention up as a control.

The CI step passes when the CLI cannot be fetched. A design gate that goes red because a package registry was slow teaches people to ignore it. It reports the degradation in one line and lets the build proceed on the regex verdict. What it will never do is pass a real regression quietly.

And one thing no detector decides: the first three fundamentals of the design rule are specificity density, committed stance, and hierarchy by intent. Those stay human.


An upstream defect worth knowing about

The impeccable CLI exits without waiting for its asynchronous stdout to drain. Written to a pipe, Node buffers 64 KiB and the process dies with the rest unflushed: a directory scan returned exactly 65,536 bytes of a 168,409-byte document, so the JSON failed to parse and every finding was lost. Redirected to a file it returns everything, because Node writes to a regular file synchronously.

scripts/utils/impeccable_engine.py writes to a temporary file rather than reading a pipe. The failure is worth naming because of its shape: the parse error was caught and reported as “deep design checks skipped”, the same message a missing Node produces, so every directory-sized scan would have silently degraded to nothing while looking like an environment problem.


HEADING OS · Design check · see also Extending the engine and Architecture.