framesmith 1.10: approvals that can be proven wrong


framesmith 1.10.0 is out, and it’s about a specific way a good idea rots.

Teams have started using framesmith canvases as a design gate: a screen may only be built or edited when an approved canvas describes it. Nice pattern. The agent can’t wander off and redesign your admin panel on a whim, because the approved canvas is the contract. Except a downstream session proved the gate couldn’t keep its own promise, and filed five issues in one sitting explaining exactly how.

Here’s where it came from, because the failure is the whole point of the release.

Two ways “approved” quietly stopped meaning anything

Two incidents did the damage.

An admin screen’s canvas still showed a STATUS column, “Rename” links, and rows that hadn’t existed in the shipped view for weeks. The design said one thing; the running app said another; nothing noticed. The canvas was still stamped approved.

The second one is worse. A canvas showed a radio group that was never built. The implementation shipped a <select>, and when someone spotted the difference, they annotated it instead of reconciling it. A note got added saying “this is actually a select.” The approved design and the shipped code disagreed, in writing, and everyone moved on.

Both were caught the only way they could be: a human squinting at two screens side by side. And the approvals file that was supposed to prevent all of this? It referenced canvases by name. That’s the root of it. A name is a point-in-time judgment (“I approved the admin canvas”) recorded as a permanent, unversioned fact. The judgment was true the day it was made. The canvas kept changing. The approval didn’t know.

The diagnosis in the issue was blunt, and I adopted it as the thesis for the whole release: if an approved canvas can silently stop describing reality, “approved” stops meaning anything.

Approvals bind to a hash, not a name

The fix is to make an approval a claim that can be checked, and therefore proven wrong.

canvas_version returns a stable content hash of the design itself: the node tree, tokens, components, fonts. It looks like sha256:8014d416f924378b. The important part is what doesn’t move it. Feedback arriving, critique stamps, provenance changes, none of that touches the hash. Only an actual design change does.

So you record { canvasId, versionHash } wherever your approvals already live. A YAML file, a PR comment, a database row, the workflow is yours. Then one call tells you whether the approved canvas is still the current design, or whether it drifted out from under the approval. Every canvas_list row carries the same hash, so a gate can populate its records straight from a listing.

An approval used to be a name, which is always true. Now it’s a hash, which is true only as long as the design hasn’t changed. That’s the entire idea: an approval you can falsify is worth something, and an approval you can’t is decoration.

Drift becomes a report, not a discovery

The hash tells you the canvas changed. It doesn’t tell you the app drifted away from the canvas. That’s a different check, and it used to be a human squinting.

canvas_check_drift re-imports the live route and compares what both sides are actually made of: text runs, controls, table shapes. Not styles, structure. Findings come back in plain words:

  • missing-in-page (the phantom STATUS column that lived only in the canvas)
  • control-mismatch (“the canvas has a radio group with 3 options; the page has a select”)
  • table-mismatch
  • missing-in-canvas

Both original incidents now reproduce as automated findings in the test suite, which is the standard I hold these to. If a bug motivated a feature, the bug becomes a fixture. And it’s tuned to not cry wolf: live figures don’t false-flag, because mostly-numeric text is data, not structure. A dashboard showing this month’s revenue isn’t “drift” just because the number changed. If what you want is the softer question, how different does it look, canvas_sync_from_url still answers that as a pixel percentage.

CI can finally demand the check

Here’s the part I want to be honest about, because it’s the real root cause. The tooling to catch both incidents already existed. Sync was there. What was missing wasn’t a tool. It was that nothing ever demanded the check run. A gate nobody is required to walk through is a hallway.

So 1.10 ships CLI subcommands with exit codes:

npx framesmith verify kpi-revenue-driver-control --hash sha256:8014d416f924378b
npx framesmith check-drift admin-stream-types --url "$STAGING_URL/admin/stream_types" --json

Exit 0 is pass, 1 is drift or mismatch, 2 is error. verify is Chrome-free and fast enough to sit in a pre-commit hook; check-drift hits a running route in CI. And npx framesmith with no arguments is still the MCP server, so this doesn’t change how the agent side works. Only those exact subcommands are intercepted.

The agent learns the ritual

A tool that can catch drift is useless if the agent doesn’t know to look. So the connect-time instructions and guidelines now teach the workflow: picking up a canvas that describes an already-shipped view starts with a drift check, and findings get reconciled deliberately. Update the canvas, flag the implementation, or ask. Never silently annotate a difference and move on. That last habit is precisely how a <select> ships against an approved radio group.

The smaller fixes, same session

The same session that found the big problem also turned up a hygiene wave, and these are the kind of papercuts worth naming:

  • The viewer’s quality score and live-reload are now keyed by content hash, so a hand-edited repo canvas can’t serve a stale evaluation that makes a correct fix look broken.
  • Writing shadow where the property is shadows used to be a silent no-op that cost me a debugging cycle. Op results now warn with the exact fix.
  • canvas_evaluate now reports the active genre, what it relaxed, and what a different genre would have relaxed. If you mis-stamped a screen, you see the tradeoff at decision time instead of after a wasted cycle.

Where the edges are

Drift detection compares structure, not styling, so a canvas and a page can pass a drift check and still look different. That’s what canvas_sync_from_url is for; use both. And the whole gate only has teeth if something runs the check. 1.10 gives you the exit codes; wiring them into CI or a hook is still on you. The tool can offer the gate. It can’t make you walk through it.

Get it

npm i -g framesmith@1.10.0     # or just: npx framesmith

No breaking changes. Existing canvases and tool calls work unchanged. Restart a running MCP server to pick up the two new tools and the CLI subcommands.

framesmith is open source (MIT), renders to plain HTML/CSS, stores designs as open JSON you own in your repo, and works with any MCP-compatible client.