Capability requirements · vocabulary in all five ports · checked by the Node meta CLI
Check what your AI actually built.
A test exercises code that exists. That is the whole reason a passing suite can sit on top of a feature nobody finished: there is no test that fails because a column nobody writes was never wired to anything, and no linter that flags the handler you meant to add. The absence has no address — so give it one. A capability declared in MetaObjects is a node in the same model as your entities, and its link to the code is resolved, not trusted: name something that isn't there and the build stops.
The failure that comes after drift
Drift is when your code stops agreeing with your model. MetaObjects already turns that into a build error across your code, your prompts and your schema.
This is the other one. Your code can agree with your model perfectly, and the model can still describe a capability your software does not have. Agent-written work fails that way in three recognisable shapes:
Duplicate
It builds a capability that already exists, because nothing in the repo told it the capability was there.
Missing
It reports the feature done. Every layer is present and nothing ever writes the value. Nothing is wrong enough to fail.
Partial
It ships the legible 80% and reports done. The recovery path, the rollup, the retire operation — invisible to everyone.
Notice what all three have in common: nothing is wrong. No exception, no failing assertion, no type that stopped lining up — so every gate you own stays green and reports that it checked. The three shapes are invisible to your test suite by construction, not by oversight, and that is why the fix has to live somewhere else: in the model, next to the entities, where a claim can be resolved instead of read.
So give it one
A requirements document does not help — prose goes stale silently and nothing in your build has an opinion about it. Neither does a requirements tool, where the link from a claim to the code is a string: rename the class and the link still looks fine.
A capability declared in MetaObjects is a node in the same model as your entities, so its link is resolved, not trusted:
- requirement.functional:
name: subscriberCanBePausedWithoutErasingHistory
title: Pausing preserves history
level: 5
status: live
statement: "A subscriber's participation is switched by their own status, so they
can be paused while everything they did stays on the record."
counterexample: "A subscriber who can only be stopped by deletion, taking every
record of their activity with them."
description: >-
A status on the subscriber rather than a deletion, so withdrawing an
account leaves its history intact.
implementedBy: [acme::Subscriber.status]
titleis the short label,statementis what the capability is in a sentence, anddescriptionis the longer form. Three fields because an index, a doc page and a test each want a different one.- The counterexample says what a violation looks like. That single line is what an assertion gets written against.
implementedBynames the member — the real field in the real model — that carries it. Rename or delete that field and the build tells you which capability just lost its implementation.
That entry is on this page because it is checked: it lives in the repository that
builds this site, acme::Subscriber.status is a real field of a real entity,
and every release resolves the link before the page ships. Delete the field and the block
you just read stops building.
Which is exactly as far as a structural check goes, and that limit is the reason this page exists. A near-identical entry in a live product was wrong: the field it named existed, so the link resolved and every structural check passed. Nothing in the codebase ever wrote that column, so every row held the default forever. The capability could not be exercised at all.
Consistency is not completeness. Knowing where a capability is supposed to live is what lets you ask the only question that finds these: what would I have to break to make this claim fail?
What it found
Declared across three shipping products — roughly 700 capabilities — against suites that were passing at the time. A sample:
status column with no writer
anywhere. Every bot held the default, permanently.These were found in a codebase with 1,280 tests passing and zero failures, and the deepest retrofit of the three — a codebase where 93% of the history predates its requirements — produced the most of them. You do not need a greenfield project. This works on the code you already have, and best on the parts you are least sure about.
What you get, and what you build
Being exact about this matters, because the last part is the important one and it is not in the box yet.
MetaObjects ships
- Capabilities as registered vocabulary in all five language ports — a project that declares none sees no change at all.
implementedByresolved bymeta verify. Dangling on a live or partial claim is an error, not a note; a live claim that names nothing is a warning.meta verifyreporting claims whose implementation vanished, entities no claim covers, and gaps recorded versus gaps nobody has ruled on — with a summary on every run.requirementTests()scaffolding a test stub per claim, statement and counterexample carried in, kept from rotting byverify --codegen.meta docsrendering the ledger for humans and for agents — including a machine-readable index carrying a declared count.
You write
- The assertion. A generated stub is a place to put a proof and a guarantee it stays in step with the claim. It is not a proof.
- The proof that the assertion bites — a declared edit to your source that must turn that test red. If it survives, the test is decoration.
That second one is not a MetaObjects feature today. Two projects built their own independently, which is the best argument for moving it into the library — and the reason we would rather tell you now than have you find the gap.
Here is what the scaffold hands you for the entry above — generated by
requirementTests() from the claim itself, so the statement, the
counterexample and the member it claims arrive with it and cannot rot away from it.
The name is the link, and the body below it is yours:
// @generated by @metaobjectsdev/codegen-ts.
// The test IDENTITY is generated from the requirement; the BODY below is yours.
// Do not rename the test — the name is the link.
…
import { test, expect } from "bun:test";
…
/**
* A subscriber's participation is switched by their own status, so they can be paused while everything they did stays on the record.
*
* Counterexample: A subscriber who can only be stopped by deletion, taking every record of their activity with them.
*
* Claims:
* - acme::Subscriber.status (field.enum)
*/
test("subscriberCanBePausedWithoutErasingHistory [field.enum]", () => {
…
Show the whole generated file (23 lines)
// @generated by @metaobjectsdev/codegen-ts.
// The test IDENTITY is generated from the requirement; the BODY below is yours.
// Do not rename the test — the name is the link.
// Your body is never overwritten: MERGED where .metaobjects/.gen-state/ holds this
// file's snapshot body, REFUSED (run exits 1, body kept) where it does not. Those
// bodies are gitignored, so a fresh clone or CI is always the second case — see
// docs/features/own-your-codegen.md for the recovery.
import { test, expect } from "bun:test";
/**
* A subscriber's participation is switched by their own status, so they can be paused while everything they did stays on the record.
*
* Counterexample: A subscriber who can only be stopped by deletion, taking every record of their activity with them.
*
* Claims:
* - acme::Subscriber.status (field.enum)
*/
test("subscriberCanBePausedWithoutErasingHistory [field.enum]", () => {
expect.unreachable(
"unimplemented requirement stub: subscriberCanBePausedWithoutErasingHistory [field.enum] — " +
"replace this with an assertion that fails when: A subscriber who can only be stopped by deletion, taking every record of their activity with them.",
);
});
This part is TypeScript-only. The vocabulary loads and validates in all
five ports, and the meta verify checks run in the Node meta CLI.
The test scaffolding is TypeScript's alone.
Where it sits
- Codegen turns a model change into a compile error on the exact line your own logic needs updating. Refactoring stops being a bug hunt.
meta verifyturns divergence between your code, prompts, schema and the model into a failed build. Drift cannot merge.- Capabilities put what you said the software does on that same spine — resolved links, coverage reporting, and a generated check per claim.
The AI writes the code. The model keeps all of it honest — and now honest covers whether the feature is actually there.
Start with one capability you are not certain is fully built. Write down what it means and what a violation would look like, link it to the field or endpoint that carries it, then try to write a test that would fail if it were false. In three codebases, that exercise is where every finding above came from.
Get started · Full reference · Spec npm i @metaobjectsdev/cli && npx meta init