← The Machine Room · Case file 005: audit-kit Used on a real CRM · Sheet 1 of 9
Case file: audit-kit, a design-drift audit for web interfaces Drawing no. AH-CS-005

Thousands of design-drift warnings became one fix list, each traced to the line of CSS that caused it.

An audit kit for teams whose interface has drifted from its design system. It reads what the browser actually rendered and which CSS rule won, so every finding names the element, the value, the token it should use and the line to change.

Rendered page → measured styles → design tokens → grouped by cause → one fix list
15tools write one report: Markdown for reviewers, JSON for agents and CI Verified
4,306 raw findings
→≈250
Findings after grouping by the CSS rule that caused them, measured on KorCRM, case file 002 Verified
Drawn byAbu Huraira Mukhtar
RoleDesigner and sole developer
EvidenceWhat the browser rendered
GroupingBy the CSS rule behind each drift
ChecksTokens, spacing, accessibility
OutputMarkdown and JSON reports
StatusUsed on a real CRM
SourceSource code and project records
Sheet 2 of 9: In briefThe case in four parts

01 · The short version

The problem
Design reviews ran on screenshots and opinion. A gap two pixels off the grid or a button a shade off the brand colour is hard to see and harder to prove, so “the spacing looks off” started an argument and ended without a decision. Automated checks went the other way and buried the team in thousands of warnings.
What I built
An audit kit that opens each screen in a real browser, reads what each element actually rendered and checks it against the design tokens. Each finding names the element, the value, the token it should use and the CSS rule that caused it. Findings with one cause become one fix.
How it went
On KorCRM, a real CRM, the first full run returned 4,306 findings. Grouped by the CSS rule behind them, they came to about 250 Verified, pointing at a few dozen declarations to change. Each finding keeps its ID from run to run, so the next run shows what closed.
What you’d get
The same audit on your design system: your tokens and components, run in your CI on every pull request, with one report your designers and engineers both read. Every finding cites its line of CSS, so a review ends with a fix.
Sheet 3 of 9: Field notesThe before-state

02 · Reviews ran on opinion

In a design review, one person says the spacing looks off, another says it looks fine, and neither can prove it.

Checking by eye does not scale. One review round on KorCRM ran to 127 screenshots in 19 folders. The drifts that matter are the ones eyes miss: a card gap of 18 px among gaps of 16, or a button in #6d28d9 where the brand token says #7c3aed. Two purples that close cannot be told apart across two screenshots, so the argument ends without a record and opens again next sprint.

Automating the check fails the other way. The first full automated run on KorCRM returned 4,306 findings, most of them repeats of a few dozen shared rules across hundreds of elements. A list that long tells nobody which few lines to change, so it goes unread.

Either way, review time goes on opinion and the drift ships.

General notes
1. One review round: 127 screenshots in 19 folders
2. First automated run: 4,306 findings
3. Top repeats: 4 px flagged 295 times and 12 px 272 times, both from stock Tailwind gap classes
4. The colour and gap examples are typical drifts that eyes miss
Fig. 1: The review loop, before the auditBefore the system
Sheet 4 of 9: Before and afterOne review, step by step

03 · Evidence at every step

The audit gives every step of a review a record a person can cite: what the browser rendered, set against what the design tokens say it should be.

StepBeforeWith the system
Find the driftScrub screenshots by eye, 127 of them for one roundA headless browser opens every route and records what each element actually rendered
Check the tokensHold two purples side by side and judgeEach value is checked against the design tokens, and a drift names the token it should use
Find the causeSearch the stylesheets for the rule behind itThe browser reports which CSS rule won, with the file and line it came from
Decide what to fixA report per element: 4,306 findings on KorCRMFindings are grouped by the rule that caused them, so one change closes every element it touches
Follow upNew screenshots and the same argument next sprintEach finding keeps its ID on every run, so a fix is confirmed and a regression shows as new
Gate the releaseDrift ships unless someone spots itA critical or high-severity finding fails the build with exit code 1
What the team receivesRedrawn from the kit’s real output, with sample pages and values
Spacing drift reportFacsimile
Score 94 of 100Passed
Medium

/pricing · .cta-button · padding-block

Observed12px
On the grid8px or 16px
SuggestionRound to the nearest 8 pt step
Finding ID3f9a1c07e2b4
1. A finding. It names the page, the element, the property and the value, with the nearest value on the grid and an ID that stays the same on every run.
Grouped by causeFacsimile
.gap-3 { gap: 12px }
DriftOff the 8 px grid
On the grid8px or 16px
Flagged272 times, one per element
272 findings1 fix
2. One cause, one fix. On KorCRM, the 12 px gap from one stock Tailwind class was flagged 272 times. Grouped by the rule that caused it, it becomes one finding.
Screenshot, markedFacsimile
1Button #6d28d9, token #7c3aed
2Card gap 18 px, grid 16 px
3. On the screenshot. Findings are boxed and numbered on the screenshot, so a designer sees which element each one means.
Sheet 5 of 9: Approval plateWhat the case rests on

04 · 4,306 warnings, about 250 to act on

4,306 → ≈250Findings on KorCRM, case file 002, before and after grouping by the CSS rule that caused them. They pointed at a few dozen declarationsVerified
Same IDA finding’s ID is a fingerprint of what drifted, so it stays the same on every run and every machine. A ticket can cite it, and the next run shows whether it closedVerified: mechanism
15 toolsToken, spacing, accessibility, performance and screenshot checks, all writing one report: Markdown for people, JSON for agents and CIVerified
0–6 exit codesA critical or high-severity finding fails the build with exit 1, and every other outcome has its own code, so CI acts on the code aloneVerified

How I’d start in your team

I’d start with one screen your reviewers already argue about, and your design tokens.

One run shows how far that screen has drifted, which few CSS rules cause most of it and what a clean result looks like. The first version runs in the repository and CI your engineers already use, with the tokens and components your designers already keep.

Discuss a system like this
Sheet 6 of 9: General arrangementDrawing 005-A

05 · Fifteen tools, one report format

audit-kit is fifteen single-purpose command-line tools in strict TypeScript, and every one writes the same JSON report. One parser reads them all, and the same report goes to reviewers, agents and CI.

dom-probe drives a headless Chromium through Playwright. For each tagged element it records the computed styles and the bounding box and, through the Chrome DevTools Protocol, which declaration won each property; local source maps walk that answer back to the authored file. token-audit, spacing-audit and tokens-lint read that one metrics file, and a11y-audit, lighthouse-audit and semantic-diff open the same routes from the same config.

The report, called the envelope, carries a summary with a 0 to 100 score and a pass flag, the findings and any artifacts. JSON goes to stdout and logs go to stderr, so every stdout line parses. An MCP server exposes the tool registry to agents, and mark draws numbered boxes on screenshots so a multimodal model can point at a finding by its number.

As built
TypeScript, strict; Node 20 and 22
Playwright, Chrome DevTools Protocol, source-map
Token files parsed with ts-morph, with no eval
15 tools; 17 binaries with the meta-CLI and the MCP server Verified
16 categories and 5 severities, both closed lists
Fig. 2: The kit’s general arrangementTool names are the source code’s
Specimen: real output from a sample page, captured in July 2026, frames abbreviated. The [high] rows are a light reformat of the real findings; severities, routes, selectors, rules and values are verbatim.
$ dom-probe --config audit.config.json --out metrics.json ↳ crawled 4 routes · 4 nodes · cdp-css on $ token-audit --metrics metrics.json --tokens tokens.json --format ndjson --out drift.json {"type":"start","tool":"token-audit","schemaVersion":"1.0.0"} {"type":"finding","finding":{"severity":"medium","category":"color","location":{"route":"/pricing","selector":".cta-button"},"title":"Computed value drifts from token system","observed":{"value":"#6d28d9"},"expected":{"anyOf":["color.brand (#7c3aed)","color.surface (#0b0b0f)"]}}} {"type":"finding","finding":{"severity":"medium","category":"typography","location":{"route":"/pricing","selector":".cta-button"},"observed":{"value":"18px"}}} {"type":"finding","finding":{"severity":"medium","category":"shadow","location":{"route":"/dashboard","selector":".stat-tile"},"observed":{"value":"0 4px 12px rgba(0,0,0,0.15)"}}} ··· 7 more finding frames ··· {"type":"summary","summary":{"totalFindings":10,"bySeverity":{"high":0,"medium":10,"low":0},"score":80,"passed":true}} {"type":"end","exitCode":0} $ tokens-lint --metrics metrics.json tool: tokens-lint | passed: false | score: 85 | total: 3 bySeverity: {"critical":0,"high":3,"medium":0,"low":0,"info":0} [high] /pricing .cta-button no-hex-in-color (backgroundColor: #6d28d9) [high] /pricing .cta-button no-px-in-font-size (fontSize: 18px) [high] /dashboard .stat-tile no-raw-shadow (boxShadow: 0 4px 12px rgba(0,0,0,.15)) ✗ 3 high-severity violations, exit 1
The flare
Shipped #6d28d9 against the token #7c3aed: two purples an eye cannot split across two screenshots
No swatch is drawn here; the finding is the evidence
stdout carries the report alone; logs go to stderr (ADR-026)
Detail A, see fig. 2, finding IDs

An ID that holds across runs

A random ID per finding demos well and makes “is yesterday’s drift still open?” impossible to answer.

Chose a 12-hex sha256 of the tool, category, route, selector, property, observed and expected value, leaving out the bounding box and timestamps, because a review needs “this exact drift” to mean one thing across runs and machines. Cost: a second instance of an identical drift on the same selector shares the ID. Gained: findings can be cited, compared between runs and re-checked with --verify <id>. ADR-005
Detail B, see fig. 2, the envelope

An append-only contract

The envelope sits upstream of every tool and every agent that reads them. One renamed field would break fifteen tools and a parser somewhere out of sight.

Chose an append-only contract after the first phase, so schemaVersion has stayed 1.0.0, because the first broken downstream agent costs more than any cleanup a version 2 would buy. Cost: no field is renamed or removed before a major version, and two tools keep their own copy of one selector list to stay sealed. Gained: anything written against 1.0.0 still parses every tool’s output. ADR-003, ADR-007
Detail C, see sheet 7, step 5

Rules as data

tokens-lint bans raw hex, px and shadow values in critical properties, and agents are expected to write its rules.

Chose a pure-JSON rule schema with a closed set of built-in matchers, because executable rules would give any agent-written ruleset a way to run code in CI. Cost: a new kind of check needs a new matcher in the source. Gained: an agent can write rules, and no rule can run code. ADR-021
Detail D, see sheet 7, step 1

Degrade by route

The DevTools Protocol can fail to attach on a slow first load.

Chose to log a diagnostic for that route and carry on, because one slow route should not cost a whole run. Cost: findings on that route group by property and value, without a cause. Gained: every run finishes with a report. ADR-051
Sheet 7 of 9: Section A-A, one runDrawing 005-B: cut through the KorCRM run

06 · Every finding traced to its cause

Follow the first full run on KorCRM through the kit, from the browser to the agent. The left edge of the drawing shows which part of the kit does each step.

Fig. 3: Section A-A, the first full run on KorCRMCounts from the run’s decision log
Beat 1: dom-probe, Playwright and CDP

1Probe

A headless browser opens each route and records what every tagged element actually rendered: its computed styles and its box. It also asks the Chrome DevTools Protocol which declaration won each property, and walks that answer back through local source maps to the file that was written. On a 50-element route this adds 16 ms, 3% over a run without it; if CDP will not attach, that route logs a diagnostic and the run goes on.

Beat 2: one metrics file, three judges

2Compare

token-audit, spacing-audit and tokens-lint read the same metrics.json and write the same envelope. A drift finding names the route, selector, property, observed value and the nearest legal token, in one of sixteen categories and five severities.

Beat 3: the first full run

3Count

On KorCRM, the first full run returned 4,306 findings: 782 spacing, 2,714 lint, 656 token and 154 others. Each was a real occurrence, and together they were unreadable. At four thousand findings nobody acts, person or agent.

Beat 4: group by cause, ADR-050 to 052

4Attribute

Muting findings would have hidden real drift, so each finding is traced to the source declaration that caused it and grouped by that cause. The 12 px gap from one stock Tailwind class, .gap-3, was flagged 272 times; grouped, it is one finding with affectedNodeCount: 272. The 4,306 came to about 250 grouped findings, pointing at a few dozen declarations.

Beat 5: exit codes 0 to 6

5Gate

Every tool exits the same way: 0 clean, 1 drift found, and 2 to 6 for a usage error, a missing input, a missing dependency, an invalid report and a timeout. By default a critical or high finding fails the run, so CI branches on the code with no text to parse.

Beat 6: MCP, mark, --verify

6Close the loop

The MCP server exposes every registered tool to agents, and mark draws numbered boxes on screenshots so a model can point at finding 7 and mean it. Each ID is a content hash, so --verify <id> re-checks one finding with the same answer on any machine. The dashed arc is the product: every finding traced back to the declaration that caused it.

Sheet 8 of 9: Revision historyFindings settled at their cause

07 · Found in the build, settled at the cause

Each row is a problem the build found, in design review or on a real interface, and the change that settled it at its cause.

Most became contracts the tools enforce: grouping by cause, IDs that hold across runs, rules that cannot run code, a stdout that always parses and sign-in paths that never reach a report. Each row traces to a logged decision.

Rev.
Date
What we found
What changed
Trace
A
2026-04-25
NoiseThe first real run returned 4,306 findings from about 30 to 50 root-cause declarations. The top repeats were 4px 295 times and 12px 272 times, from the stock Tailwind classes .gap-1 and .gap-3.
Group by causedom-probe records which declaration won, source maps walk it home, and findings group by cause. Coverage stays as affectedNodeCount and per-instance detail. 4,306 → ≈250.
ADR-050, 051, 052
B
2026-04-24
IdentityA finding has to be tracked across runs: still open, or a new regression? Random UUIDs cannot answer that, and counters shift when the input order changes.
Content hashA 12-hex sha256 of the finding’s identity fields, with the bounding box and timestamps left out, so a finding can be cited.
ADR-005
C
2026-04-25
SafetyExecutable rule configs for tokens-lint would give every agent-written ruleset a way to run code inside a tool that gates CI.
Rules as dataA declarative JSON rule schema with a closed matcher set. An agent can write rules; no rule can run code.
ADR-021
D
2026-04-25
StreamIn piped runs, Pino log lines landed among the NDJSON frames on stdout, and an agent’s parser stops on a line with no type.
Split channelsLogs go to stderr; stdout carries the envelope or its frames and nothing else, so every line parses.
ADR-026
E
2026-04-25
PathsPlaywright storageState paths, where sign-in sessions live on disk, could reach any report or run manifest the kit writes.
Redact at sourceThe path is replaced with a redaction marker before any output channel sees it.
ADR-011
Engineering recordChecked in the source code
703Tests passing of 704, one skipped, in 50 test files run in isolationVerified
1.0.0The envelope’s schemaVersion, unchanged since the first phase: the contract grows by additionVerified
+16 msTo record which declaration won, on a 50-element route: 3% over a run without itVerified

The source is 17,606 lines of strict TypeScript with 13,538 lines of tests, and it names no client project: the project’s rules treat a project name in the source as a bug, which keeps the kit reusable.

Discipline
51 logged decisions, 64 gotchas
CI: Node 20 and 22 on Ubuntu and macOS
Publish dry run checked in CI
Sheet 9 of 9: Close-outFor your team

08 · What a version for your team adds

The kit turns a design review into evidence: each finding names its element, its value, the token it should use and the rule that caused it, with an ID that holds from run to run.

On KorCRM it grouped 4,306 raw findings into about 250. A version for your team starts from the same method, evidence from the rendered page traced to its cause, and adds what your design system and your release process need.

Scope
Measured on: KorCRM, case file 002
Grouping: 4,306 findings to about 250
Runs: command line, with exit codes for CI
Version: 0.5.0, 703 tests passing
Row
Part
As built
Your version adds
A
Design tokens
Your token filesReads tokens from TypeScript, JSON, DTCG or Figma variables and checks what rendered against them
Your componentsRules for your component library, agreed with your designers, on top of your tokens
B
Where it runs
The command lineAny build can call it, and drift returns exit code 1
Every pull requestRuns in your CI, posts its findings on the pull request and blocks a merge on high-severity drift
C
Reports
Two formatsMarkdown for people and JSON for agents, from the same run
One shared reportA report your designers and engineers read together, linked from each pull request
D
Causes
Grouped by causeFindings traced to the winning CSS rule, with local source maps followed to the authored file
Your source mapsResolved from your dev server and build, so each finding opens on the component file
E
Follow-up
Stable IDsThe same drift keeps its ID on every run and every machine
A drift historyOpen, closed and new findings tracked per release, so drift is measured over time
Discuss a system like this
Drawn & built by
Abu Huraira Mukhtar, designer and sole developer
A. H. M.
Sheet 9 of 9
End of case file 005
The Drawing Office: after hours

Settle design reviews with evidence.

If your reviews keep arguing about spacing and colour, the fix starts with one screen and your design tokens. Bring them.

Contact me Back to the Machine Room