Decree: if it’s not in the contract, it doesn't ship
Decree is a model-free verify step in product CI. It parses an app against a JSON design-system contract and fails the build when shipped UI sits outside that contract: invented variants, hardcoded hex, automated deprecation notices, and raw elements where the system already has a component. One stable error code per finding. There is no warn mode.
Open DecreeGitHub repository
A design system can be fully documented and still leak drift into production. Someone invents a variant. Someone pastes a hex. A deprecated component stays in the product because the replacement never reached the next pull request.
I have watched that happen on systems I owned. The documentation was fine. The product was not. Review catches some of it. The rest becomes what ships.
The leak lives in the product. That is where Decree runs.
What is a design system contract?
A design-system contract is the enforceable slice of the system: component names, tokens, and the optional layers a team turns on — component APIs, restyle rules, and native-element replacements. Specs 2 and DS Contracts author what a component is: anatomy, props, illegal combinations, bindings to Figma and code. That work is excellent. Decree sits next to it.
The design system team compiles that truth into decree.contract.json and ships it with the package. Product teams consume it. The contract they are judged against is the one that shipped.
How does decree verify fail a build?
decree verify is a local Node scan. It reads the product against the JSON contract and exits 1 on anything the contract does not allow. It does not call a model, send a prompt, or need an API key. On the included demos it returns in about a tenth of a second. That timing is from those demos, not a production-app benchmark. It belongs on every pull request.
An invented variant fails. A hardcoded hex fails. A deprecated name fails, and the finding carries the replacement. A raw <button> fails where the system has Button. Each finding gets a stable DECREE_* code.
The Harbor demo is the whole story in one checkout. A clean app passes. A dirty one comes back with six named findings: ghost on a Button that only has primary and secondary, a forbidden size combination, a style= restyle, a deprecation notice that Ghost should become Button, a MagicButton that never existed, and a raw button where the system already has one. An agent can invent all six of those before lunch. Verify names each of them.
Contract thickness is a team choice. Names and tokens are always on. Component APIs, restyle, and native-element replacement are opt-in. A layer that is on refuses. A layer that is off is not being asked. Depth is optional. Failure is not.
I built Decree to fail that build. The system is judged on the product, at merge, in CI. Fast enough to run every time. No model required to do it.
How is Decree different from a design-system linter?
Most design-system linters report hex, tokens, or class names, and many of those reports can be skipped as warnings. Decree refuses at merge. It judges the product against the same contract agents can ask over MCP, and it names each finding with a stable code that can sit on a baseline.
Color-only tools stop at paint. Decree can also refuse invented variants, illegal prop combinations, deprecated components and tokens, restyles, and raw <button> where Button exists. A visual-regression check compares pixels. Decree compares the product to the contract that shipped.
How do you turn this on without going green on day one?
Write a baseline of today’s findings: decree verify . --write-baseline decree.baseline.json. From then on CI fails only on new ones. Existing debt can sit. New drift cannot. A brownfield app can turn this on without going green on day one.
Fix a batch. Rewrite the baseline. Repeat. Then go absolute. That is the on-ramp. Debt can stay. Debt cannot grow.
What happens when you deprecate a component?
A deprecated component stays on the allowlist. It is still in the system. Any use fails with DECREE_DEPRECATED_COMPONENT and the replacement in the message. Tokens get the same treatment as DECREE_DEPRECATED_TOKEN. Existing use can sit on a baseline. New use blocks. Drop the name from the contract and the finding changes: then it is unknown.
That is the automated notice. The design system team marks the name in the source they already maintain. The compiled contract carries the replacement. Verify prints it on the pull request. Agents can ask the same contract over MCP before they write the old name again.
How do agents stay on the design system?
Agents can ask the same contract over MCP before they write a line. They get an allowlist: what exists, which tokens are real, and whether a snippet would pass the scanners CI runs. Invented UI gets refused at generation when the agent asks first.
The door that closes the claim is still the product pipeline. If verify is not in CI, the system is still optional. For the broader case of agents doing ongoing system work, see The case for A.I. Orchestration in Design Systems.
Does decree verify use a model?
No. decree verify is a local parse of the product against a JSON contract. There is no prompt, no API key, and no model in that path. Agents can use MCP to ask the same contract before they write. The merge door stays model-free.
Can a team still write plain HTML?
Yes. nativeElementMap is a replacement map. Map button to Button and a raw <button> fails as a stand-in for the system component. Tags that are not in the map are never native-element findings. Invented paint still fails on the token layer, including on a raw tag.
If it’s not in the contract, it doesn’t ship.
Decree on GitHub