Skip to content

Commit 5d005d0

Browse files
authored
docs: feature commits update the requirement ledger; recommend the strict requirements check; correct handler-code figure (#459)
1 parent 0cb66a6 commit 5d005d0

28 files changed

Lines changed: 94 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ here.**
1010

1111
## [Unreleased]
1212

13+
### Changed
14+
15+
- **Requirements guidance: a feature commit updates its requirement.** The agent context
16+
(the authoring and verify skills and the always-on file `meta init` writes) now tells an
17+
agent that a commit adding, changing or removing behaviour updates the ledger in the same
18+
commit, or says why not. `verify` checks that the ledger's links resolve, not that it is
19+
current, so nothing else enforces it.
20+
- **The adoption docs recommend the strict requirements check** (`--require-implementers`, or
21+
its per-port form) in the project's verify script or pre-push hook once the ledger is clean.
22+
23+
### Fixed
24+
25+
- The 1.1.0 entry's handler-code figure of 439 lines retired is corrected to about 460, as
26+
measured in one adopter's pull request.
27+
1328
## [1.1.2] — 2026-10-24
1429

1530
_npm `1.1.2` (full lockstep across all 14 `@metaobjectsdev/*` publish candidates), PyPI `1.1.2`, NuGet `1.1.2` and Maven Central `8.1.2`. Fixes #454 and #455. A bug-fix release: no vocabulary or `metamodelVersion` change._
@@ -205,7 +220,7 @@ every port's `verify`), the derived-key parity fix across the ports, and the lis
205220
nested-average fixes found by the first adopter to map real report pages onto the feature.
206221

207222
**How much smaller the code gets.** The size reduction comes in two steps, and each is its own
208-
number. The first landed on 1.0.x: 439 lines of handler code retired. The second is reporting,
223+
number. The first landed on 1.0.x: about 460 lines of handler code retired, measured in one adopter's pull request. The second is reporting,
209224
and it is a SQL reduction only: in an adopter, hand-written SQL went from 75 lines to 15 (-80%).
210225
Total hand-written lines did not fall, so do not read it as a total line reduction.
211226

‎agent-context/skills/metaobjects-authoring/SKILL.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,8 @@ Reach for it when the project needs to answer any of:
464464

465465
Every requirement must be **violable**: if you cannot say what breaking it looks like, it is a description, not a requirement.
466466

467+
**A commit that adds, changes or removes behaviour updates the ledger in the same commit** (the new or changed claim, its `@status` and `@implementedBy`), or says in the commit message why it does not. `verify` checks that links resolve, not that the ledger is current.
468+
467469
If the project declares any `requirement.*` node, `references/requirements.md` is installed with the full authoring rules. If it does not and you are adding the first one, the shape is:
468470

469471
```yaml

‎agent-context/skills/metaobjects-authoring/references/requirements.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ more than the rest.
88
- A requirement is PRESCRIPTIVE — it states what should be true, never what happened
99
- Do not delete a retired capability's entry
1010
- When you add an entity, claim it
11+
- A feature commit updates the ledger in the same commit
1112
- Every requirement states its counterexample
1213
- Four prose slots, and `statement` is the one that means "description"
1314
- `title` is a LABEL and is chartered here; `summary` is not
@@ -56,6 +57,14 @@ the requirement went stale.
5657
**2. When you add an entity, claim it.** Every `object.entity` should appear in some
5758
requirement's `implementedBy`, or `verify` says so.
5859

60+
**A commit that adds, changes or removes behaviour updates the ledger in the same commit.**
61+
`verify` checks that the ledger's links resolve, never that it is current, so nothing fails
62+
when a feature ships and its requirement does not move. In the same commit, write the new or
63+
changed claim, set its `@status` (`planned` to `live` when it lands, `retired` when it is
64+
removed) and keep its `@implementedBy` pointing at the real nodes (a retired entry carries
65+
none). If the commit should not
66+
touch the ledger, say why in the commit message.
67+
5968
**Every requirement states its counterexample.** One sentence: what breaking it looks like.
6069
*"Every entity has a uuid primary key"* is violable — point at one with a composite string
6170
key. *"Things are persisted"* is not, and is a description rather than a requirement. Same

‎agent-context/skills/metaobjects-verify/references/requirements.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ subverb**: requirements are metadata, so they are checked on *every* `meta verif
1010
- The status asymmetry — the one that surprises people
1111
- Exit codes
1212
- The strict switch — `--require-implementers`
13+
- Keeping the ledger current
1314
- The error codes, and the fix for each
1415
- The authoring lint — a second section, never an error
1516
- `verify` does not look at your tests
@@ -113,6 +114,17 @@ warning. If CI went red on this code, check whether the switch is set before tre
113114
as a new defect: the fix is to add the `@implementedBy` link (or drop the entry to
114115
`planned`), not to unset the switch.
115116

117+
Once the ledger is clean, set the switch where the project already runs `verify`: its verify
118+
script, CI job or pre-push hook. Until then a stale `live` claim is only a warning that
119+
nobody reads.
120+
121+
## Keeping the ledger current
122+
123+
`verify` proves the ledger's links resolve, not that the ledger is current. A commit that
124+
adds, changes or removes behaviour updates the ledger in the same commit: the new or changed
125+
claim, its `@status` and its `@implementedBy`. If the commit should not touch the ledger, the
126+
commit message says why.
127+
116128
## The error codes, and the fix for each
117129

118130
Match on the stable `code`, never the message text (ADR-0009). The runtime messages already

‎agent-context/templates/always-on.md.mustache‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ from.
5959
- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V<n>__`/`U<n>__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift.
6060
- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps.
6161
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db <url>` after any DB-touching work to catch that drift early — the URL is required, and the bare form exits 2.
62+
- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current.
6263
- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `<Entity>.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one.
6364
- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `<generators>` in the Maven plugin's `<configuration>` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators <names>` (TypeScript, C#, Python; a second Maven `<execution>` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject <name>...` copies one into your repo when you need to change what it emits.
6465
- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** OpenAPI, JSON Schema, Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new <name>` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes.

‎docs/features/requirements.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,12 @@ more than it links. A project whose ledger has caught up can make it fail the bu
430430
| Java, Kotlin | `mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` |
431431
| C# | `dotnet meta verify ./metadata --templates ./prompts --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` |
432432

433+
**Recommended once the ledger is clean:** set the switch in the project's verify script or
434+
pre-push hook. Every adopter ledger we measured passed the default gate, because the gate
435+
checks that links resolve, not that the ledger is current, and as a warning the finding goes
436+
unread. Pair it with the authoring rule that a commit adding, changing or removing behaviour
437+
updates its requirement in the same commit.
438+
433439
The switch raises that one finding to an error and keeps its code, so a report or a
434440
suppression keyed on the code still matches. No other warning changes severity. It is not
435441
called `--strict`, because `verify` already has `--lax` on a different axis and a `--strict`

‎docs/ports/csharp.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ are core and are not ejectable.
527527
# The gate: runs on every verify, beside whichever drift gate it was given
528528
dotnet meta verify ./metaobjects --codegen --out ./Generated
529529
dotnet meta verify ./metaobjects --codegen --out ./Generated --require-implementers
530+
# Recommended once the ledger is clean: use this form in the project's verify script or pre-push hook
530531

531532
# The tests: the same run as everything else codegen/Generators.cs declares. The entry's
532533
# OutDir puts them in the test project, which references xunit; the app's --out does not.

‎docs/ports/java.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ checks them on every run, in either mode: it logs the ledger summary, and an err
591591
dangling reference on a live requirement, a link above the floor, a live policy applied to
592592
nothing) fails the build. `-Dmeta.verify.requireImplementers=true` (or
593593
`META_REQUIRE_IMPLEMENTERS=1`) also fails it on a live functional requirement that nothing
594-
implements. Those checks are core and are not ejectable.
594+
implements; once the ledger is clean, set it in the build or pre-push hook that runs the goal. Those checks are core and are not ejectable.
595595

596596
This generator is the other half, and it is a reference helper and a recommended approach,
597597
not a contract: it writes one JUnit Jupiter test per tested requirement. The test does not

‎docs/ports/kotlin.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ plugin generated; it has to be told about `generated-sources/kotlin` and your ow
223223
The `KotlinSpringConfigGenerator` reads its model from the classpath (`<metadataResource>`), so the
224224
same file goes under `src/main/resources/` as well as `src/main/metaobjects/`.
225225

226-
**Requirement tests.** `mvn metaobjects:verify` checks the model's `requirement.*` nodes on every run (the Java goal; Kotlin has no CLI of its own), and `-Dmeta.verify.requireImplementers=true` or `META_REQUIRE_IMPLEMENTERS=1` also fails the build on a live functional requirement that nothing implements. Those checks are core and are not ejectable. `KotlinRequirementTestsGenerator` is the other half: a reference helper and a recommended approach, not a contract. It is the Kotlin sibling of the Java `JUnitRequirementTestsGenerator` (the same args, the same test identities, one conformance corpus for both). It writes one JUnit Jupiter test per tested requirement; the test calls a **witness**, a function you write, and a live requirement with no witness fails. A JUnit 4 project replaces the text through the `renderer` arg, or ejects the generator.
226+
**Requirement tests.** `mvn metaobjects:verify` checks the model's `requirement.*` nodes on every run (the Java goal; Kotlin has no CLI of its own), and `-Dmeta.verify.requireImplementers=true` or `META_REQUIRE_IMPLEMENTERS=1` also fails the build on a live functional requirement that nothing implements; once the ledger is clean, set it in the build or pre-push hook that runs the goal. Those checks are core and are not ejectable. `KotlinRequirementTestsGenerator` is the other half: a reference helper and a recommended approach, not a contract. It is the Kotlin sibling of the Java `JUnitRequirementTestsGenerator` (the same args, the same test identities, one conformance corpus for both). It writes one JUnit Jupiter test per tested requirement; the test calls a **witness**, a function you write, and a live requirement with no witness fails. A JUnit 4 project replaces the text through the `renderer` arg, or ejects the generator.
227227

228228
```xml
229229
<generator>

‎docs/ports/python.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,8 @@ Declare what the software must do as `requirement.*` nodes and `metaobjects veri
436436
them on every run: it prints the ledger summary, and an error (a dangling reference on a
437437
live requirement, a link above the floor, a live policy applied to nothing) exits non-zero.
438438
`--require-implementers` (or `META_REQUIRE_IMPLEMENTERS=1`) also fails the run on a live
439-
functional requirement that nothing implements. Those checks are core and are not
439+
functional requirement that nothing implements; once the ledger is clean, put it in the
440+
project's verify script or pre-push hook. Those checks are core and are not
440441
ejectable. A model with no requirement sees no change. Metadata that does not load is not
441442
that: `verify` prints the load errors and exits non-zero, also when no drift gate ran and so
442443
none loaded it.

0 commit comments

Comments
 (0)