Skip to content

Commit ca63007

Browse files
committed
docs(requirements): the decision record and port pages state the final behaviour
ADR-0057 says what is realized and states the stale-witness signal per language. The port pages, the changelog, the ledger spec and the corpus README are corrected where a review found them wrong or silent: the owned Program.cs advice for C#, the Python refusal of a project package under a standard-library name, how hooks and providers are imported, the Kotlin generator count, which loaders refuse a non-integer level, bare-name binding, and what verify does with metadata that does not load. Refs docs/superpowers/plans/2026-10-05-requirements-slice-1-checks-and-test-generators.md and ADR-0057.
1 parent 5d1dcb3 commit ca63007

9 files changed

Lines changed: 90 additions & 31 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ it until 1.1 ships._
5757
already errors in `meta verify`; the other ports loaded the same ledger and said nothing.
5858
The seven requirement authoring-lint advisories stay TypeScript-only. See
5959
[docs/features/requirements.md](docs/features/requirements.md), "The gate in every port".
60+
61+
Metadata that does not load is not a project with no requirement, and the gate fails on
62+
it. **Python: `metaobjects verify <dir> --out <dir>` with no `--generators` now exits 1 on
63+
metadata the loader refuses, where it exited 0**, because that invocation loads nothing
64+
else; the gate's load is strict unless `--lax`, so an unknown attribute counts. `dotnet
65+
meta verify` likewise prints the load errors itself when no gate in its own process loaded
66+
the model (`--codegen` handed off to an owned `codegen/` project, or given no `--out`).
6067
- **`--require-implementers`, in every port.** `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a
6168
`live` or `partial` functional requirement that nothing implements) stays a warning by
6269
default. `meta verify --require-implementers`, `metaobjects verify --require-implementers`,
@@ -305,6 +312,15 @@ until you regenerate.
305312
- **TypeScript: `requirementTests({ grain })` refuses an unknown grain.** Any value other
306313
than `"concern"` or `"member"` throws when the generator is built, whatever the model
307314
holds, instead of running as something in between. Every other port refuses the same way.
315+
- **Python: project code under a standard-library module name is refused where a
316+
`module:symbol` names it.** Affected: a project whose owned generator (a `module:symbol`
317+
entry in `generators`) or `requirementTests.renderer` / `requirementTests.filter` hook lives
318+
under a top-level name such as `types`, `json`, `code` or `platform`, as a package directory
319+
or as a `<name>.py` file. `gen` and `verify --codegen` now stop with `cannot import
320+
'<module>': the project package '<name>' (<path>) shadows a standard-library module of the
321+
same name; rename it` (`the project module` for a file). Before, an owned generator under
322+
such a name loaded, by replacing the standard-library module for the rest of the process.
323+
Rename the package or module, and the `module:symbol` that names it.
308324

309325
- **TypeScript: `runValidators` rejects more than it did — a behaviour change for
310326
`ObjectManager` users.** `ObjectManager.create`, `createMany`, `update`, `updateMany` and

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ first-week wedge plan — and `meta init` picks up from there.
133133
|---|---|---|---|
134134
| TypeScript | **npm `1.0.13`** — the `@metaobjectsdev/*` packages | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) |
135135
| Java | **Maven Central `8.0.13`** (`com.metaobjects:*` — the JVM major is npm major + 7) — loader + OMDB + render + Maven plugin all shipped; full conformance green | [`docs/ports/java.md`](docs/ports/java.md) | [`server/java/`](server/java/) |
136-
| Kotlin | **Maven Central `8.0.13`** — codegen tier on top of Java: 14 generators (entity, Exposed table, relations, repository, payload, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
136+
| Kotlin | **Maven Central `8.0.13`** — codegen tier on top of Java: 15 generators (entity, Exposed table, names, relations, repository, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller, requirement-tests); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
137137
| C# | **NuGet `1.0.13`** — loader + conformance + EF Core codegen + render engine + `dotnet meta` CLI all shipped | [`docs/ports/csharp.md`](docs/ports/csharp.md) | [`server/csharp/`](server/csharp/) |
138138
| Python | **PyPI `1.0.13`** — loader + conformance + render + entity-model codegen + ObjectManager runtime shipped; schema migrations are TS-owned (ADR-0015) | [`docs/ports/python.md`](docs/ports/python.md) | [`server/python/`](server/python/) |
139139

‎docs/features/cli.md‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,12 @@ by the other port's docs command.
340340

341341
Alongside its flag-only mode (`metaobjects gen <metadata_dir> --out <dir>`), the
342342
Python `metaobjects` CLI supports a declarative project config,
343-
`metaobjects.config.yaml` (#267). The **schema keys are identical to the TS
344-
`metaobjects.config.ts` vocabulary** — a polyglot adopter learns one
345-
targets-registry shape regardless of port. A JSON Schema ships at
343+
`metaobjects.config.yaml` (#267). Its **`targets` registry uses the TS
344+
`metaobjects.config.ts` vocabulary** (named targets, each with its own `outDir`) — a
345+
polyglot adopter learns one targets-registry shape regardless of port. The keys are not
346+
identical beyond that: a Python target carries its own `generators` and `entities`, and
347+
`requirementTests` is a block that exists only here, where TypeScript passes that
348+
generator's options to `requirementTests({ … })` in code. A JSON Schema ships at
346349
[`server/python/src/metaobjects/codegen/metaobjects-config.schema.json`](../../server/python/src/metaobjects/codegen/metaobjects-config.schema.json)
347350
for editor autocomplete and non-Python validation.
348351

@@ -351,6 +354,12 @@ metadata: metaobjects # optional, default "metaobjects" — relative
351354
providers: # optional; "module:symbol" refs, resolved config-relative (no PYTHONPATH=)
352355
- my_project.providers:register_custom_types
353356
libraries: [ai] # optional; MetaObjects-shipped library packages (see below)
357+
requirementTests: # optional; options of the `requirement-tests` generator, every key optional
358+
witnessModule: tests.requirement_witnesses # the default
359+
grain: concern # or: member
360+
filter: codegen.requirement_hooks:include # "module:symbol", resolved config-relative
361+
renderer: codegen.requirement_hooks:render # "module:symbol", resolved config-relative
362+
warnUncovered: true # the default
354363
targets:
355364
api:
356365
outDir: src/generated/api

‎docs/features/requirements.md‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ the summary at `info`, warnings as warnings and errors as errors.
358358
|---|---|---|
359359
| `ERR_REQUIREMENT_BAD_LEVEL` | error | The level is outside 1 to 5, on a functional requirement or a levelled architectural one. |
360360
| `ERR_REQUIREMENT_LEVEL_NESTING` | error | A levelled requirement sits at or above the level of the requirement it is nested under. |
361-
| `ERR_REQUIREMENT_LINK_ABOVE_FLOOR` | error | `@implementedBy` on a levelled requirement at L1 to L3. Nothing else is reported for that node. |
361+
| `ERR_REQUIREMENT_LINK_ABOVE_FLOOR` | error | `@implementedBy` on a levelled requirement at L1 to L3. Nothing further is reported for that node: the checks below this row are skipped, and the two above it have already run. |
362362
| `ERR_REQUIREMENT_L4_NOT_OBJECT` | error | A functional L4 names a member. |
363363
| `ERR_REQUIREMENT_L5_NOT_MEMBER` | error | A functional L5 names an object. |
364364
| `ERR_REQUIREMENT_DANGLING_REF` | error | An `@implementedBy` reference does not resolve on `live` or `partial`, or `@supersededBy` does not name a requirement in the ledger. |
@@ -396,15 +396,22 @@ diagnostics, and when any is an error, `<prefix> <n> error(s).` and a non-zero e
396396
TypeScript caps the warnings it prints at `--limit` (20 by default); the other ports print
397397
every one. The structured `--format json|toon` payload is TypeScript-only.
398398

399+
**Metadata that does not load** is not a model with no requirement: `verify` fails on it in
400+
every port. The load errors are printed once. Where the drift gate that ran loaded the model,
401+
that gate prints them; where none did (`metaobjects verify` with no generators selected,
402+
`dotnet meta verify --codegen` handed off to an owned `codegen/` project), the requirement
403+
gate prints them itself. Its load is strict unless `--lax`, as the drift gates' is.
404+
399405
### Known differences between ports
400406

401407
- The package a requirement is taken to be in can differ between ports for two multi-file
402408
shapes: a root document with no package loaded beside packaged ones, and a child merged
403409
from a package-less document into a requirement that declares its own package.
404410
- Python applies only the dependency-import rule to decide which of a project's own
405411
entities are counted; TypeScript also applies the project's `scope` patterns.
406-
- A `level` that is not an integer is refused by some loaders and reaches the gate in
407-
TypeScript.
412+
- A `level` that is not an integer (`4.5`) is refused at load by the Python, Java and C#
413+
loaders (`ERR_BAD_ATTR_VALUE`). The TypeScript loader lets it through, and the gate reports
414+
it as `ERR_REQUIREMENT_BAD_LEVEL`.
408415

409416
## Requiring implementers
410417

‎docs/ports/csharp.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,11 @@ return CodegenCli.Run(args, generators);
479479
The example is the file as it stands after `dotnet meta eject requirement-tests`: the class
480480
is your owned copy, in `Codegen.Generators`. If `Program.cs` exists because you ejected
481481
something else and you are constructing the packaged generator, the class is in
482-
`MetaObjects.Codegen.Generators`, so add `using MetaObjects.Codegen.Generators;`.
482+
`MetaObjects.Codegen.Generators`: write it fully qualified, as
483+
`new MetaObjects.Codegen.Generators.RequirementTestsGenerator { … }`. Do not add
484+
`using MetaObjects.Codegen.Generators;`, because every generator you ejected has a packaged
485+
class of the same simple name there, and the second `using` makes each `new <Name>Generator()`
486+
already in the file ambiguous (CS0104).
483487

484488
| Property | Meaning |
485489
|---|---|

‎docs/ports/python.md‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,9 @@ them on every run: it prints the ledger summary, and an error (a dangling refere
391391
live requirement, a link above the floor, a live policy applied to nothing) exits non-zero.
392392
`--require-implementers` (or `META_REQUIRE_IMPLEMENTERS=1`) also fails the run on a live
393393
functional requirement that nothing implements. Those checks are core and are not
394-
ejectable. A model with no requirement sees no change.
394+
ejectable. A model with no requirement sees no change. Metadata that does not load is not
395+
that: `verify` prints the load errors and exits non-zero, also when no generators are
396+
selected and so no drift gate loaded it.
395397

396398
The `requirement-tests` generator is the other half. It is a reference helper and a
397399
recommended approach, not a contract: `metaobjects eject requirement-tests` copies it, with
@@ -407,7 +409,7 @@ targets:
407409
requirementTests: # every key optional
408410
witnessModule: tests.requirement_witnesses # the default
409411
grain: concern # or: member
410-
filter: codegen.requirement_hooks:include # module:symbol, resolved like `providers`
412+
filter: codegen.requirement_hooks:include # module:symbol, relative to this file's directory
411413
renderer: codegen.requirement_hooks:render # module:symbol
412414
warnUncovered: true # the default
413415
```
@@ -440,10 +442,15 @@ or deleted is simply never called again: nothing reports it, and you delete it b
440442
The block is read in config mode. The flag-only form
441443
(`metaobjects gen ./metaobjects --out ./tests/generated --generators requirement-tests`)
442444
runs the generator with its defaults. A `filter` or `renderer` whose module raises on
443-
import is reported with the real cause. `metaobjects verify` goes out of sync when a claim
444-
changes, because each test carries a digest of its requirement. A package that loses its
445-
last requirement leaves its file behind: `gen` does not remove it, `verify` lists it as
446-
`extra:`, and you delete it. The model and the other ports' spelling are in
445+
import is reported with the real cause. Both hooks, like a `providers` entry, are found
446+
relative to the config file's directory, with no `PYTHONPATH=`. They differ in how they are
447+
imported: a provider is imported with plain `importlib`, and a hook goes through the resolver
448+
an owned generator is imported with, which loads the project's own copy of a module when
449+
another copy is already cached and refuses a project package or module whose top-level name
450+
is a standard-library module name (`types`, `json`): rename it. `metaobjects verify` goes
451+
out of sync when a claim changes, because each test carries a digest of its requirement. A
452+
package that loses its last requirement leaves its file behind: `gen` does not remove it,
453+
`verify` lists it as `extra:`, and you delete it. The model and the other ports' spelling are in
447454
[Generated requirement tests and witnesses](../features/requirements.md#generated-requirement-tests-and-witnesses).
448455

449456
### Declarative template-codegen (`--template-spec`)
@@ -645,7 +652,9 @@ each as `<Name>.py`. So the prompt+parse story is two generators:
645652

646653
**Own a generator.** Every generator is a reference helper you can copy and change:
647654
`metaobjects eject <name>` copies it into `codegen/generators/`, and you wire the copy as
648-
`module:symbol` in place of its name. See
655+
`module:symbol` in place of its name. A project package or module whose top-level name is a
656+
standard-library module name (`types`, `json`) is refused there with an error naming it:
657+
rename it. See
649658
[Own your codegen → Python](../features/own-your-codegen.md#python-metaobjects-eject).
650659

651660
Ejecting `routes` also hands over the helper runtime the generated routers call:

‎fixtures/requirement-check-conformance/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ it is wrong, unless the reference is shown to be wrong first.
6262
requirement that is not `planned`, whatever its level and whatever the gate says about
6363
the reference's grain. `link-above-floor`, `l4-names-member`, `l5-names-object` and
6464
`arch-levelled-tree` each report an error on a claim and no unclaimed entity.
65-
- **`ERR_REQUIREMENT_LINK_ABOVE_FLOOR` ends that requirement's checks.** Nothing else is
66-
reported for the node (`link-above-floor`).
65+
- **`ERR_REQUIREMENT_LINK_ABOVE_FLOOR` ends that requirement's checks.** Nothing further is
66+
reported for the node (`link-above-floor`): the level and nesting checks run before it, so
67+
a node can carry those as well.
6768
- **A grain error replaces the dangling check for that reference.** `l4-names-member` and
6869
`l5-names-object` each hold a reference that does not resolve and report it once.
6970
- **Existence is about naming, not resolving.** A requirement whose subtree holds any

‎spec/capability-ledger.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,11 @@ supersession chain stays walkable.
233233
### `implementedBy` resolution, severity conditional on status
234234

235235
References resolve against the loaded model through the loader's own resolver, so the
236-
package-local contract (ADR-0042) applies uniformly. A ledger entry has no package of its
237-
own, so a **bare** name binds only a root-level object: fail-closed, and a bare name that
238-
exists in two packages resolves to nothing rather than to a coin flip.
236+
package-local contract (ADR-0042) applies uniformly. A **bare** name binds in the ledger
237+
entry's own package first (the package the loader resolved for it: its own, or its file's),
238+
then to an unpackaged root-level node of that name. It never reaches into another package:
239+
fail-closed, and a bare name that exists in two other packages resolves to nothing rather
240+
than to a coin flip.
239241

240242
| status | dangling reference |
241243
|---|---|

‎spec/decisions/ADR-0057-requirement-checks-and-tests-in-every-port.md‎

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ TypeScript only:
2323
- **The test scaffold.** `requirementTests()` emits one test stub per claim, which the project
2424
fills in by hand and the three-way merge preserves.
2525

26-
The split was a decision, and the only place it was recorded as one is `docs/CONFORMANCE.md`,
27-
"Split coverage" (`docs/features/requirements.md` and the pillar summaries stated the split
28-
without the reasoning):
26+
The split was a decision, recorded as one in two places: `docs/CONFORMANCE.md`, "Split
27+
coverage", quoted below, and `spec/capability-ledger.md`, in a section titled "The verify gate
28+
is TypeScript-only, on purpose" (`docs/features/requirements.md` and the pillar summaries stated
29+
the split without the reasoning):
2930

3031
> *Checks — TypeScript only, by decision.* The `meta verify` diagnostics over requirements ship
3132
> in the TypeScript CLI; the other ports load and validate and stop there. Same call as ADR-0015:
@@ -113,8 +114,11 @@ them is open.
113114
member per test that is not skipped and a failing default for each. The project names one
114115
class that implements it. There is no run-time lookup by name (ADR-0001). A requirement that
115116
becomes live adds a failing default, which is a red test and not a compile break. A
116-
requirement that is retired or deleted removes its member, so a stale override stops
117-
compiling.
117+
requirement that is retired or deleted removes its member. Whether a witness left behind
118+
then stops compiling depends on the language: always in Kotlin, where `override` is
119+
mandatory; in Java only when the witness method carries `@Override`; in C# only when the
120+
witness implements the member explicitly. A witness written in the other form goes stale
121+
silently, and each generated header names the form that does not.
118122
14. **A Python generated test looks its witness up by name** in a configured module. Python has
119123
no static binding to offer, and this is test code, so it is the one by-name lookup in the
120124
design.
@@ -162,11 +166,16 @@ them is open.
162166

163167
## Realization status
164168

165-
At acceptance, TypeScript has the gate, the generator and (with this ADR) the strict switch. The
166-
two corpora and the Python, Java, C# and Kotlin implementations land in the order the plan
167-
gives. `docs/CONFORMANCE.md`, `docs/features/requirements.md` and the pillar summaries still
168-
state the TypeScript-only split until the ports that make it false have landed; they are
169-
corrected with them, not ahead of them.
169+
Realized by the change this ADR was accepted with. All five ports run the gate in their
170+
`verify` and ship a `requirement-tests` generator, ejectable through each port's own mechanism.
171+
Both corpora are asserted by every port; Kotlin has no CLI of its own and takes the gate and the
172+
identity function from Java, with its emitted names asserted by its own generator test.
173+
`docs/CONFORMANCE.md`, `docs/features/requirements.md`, `spec/capability-ledger.md` and the
174+
pillar summaries were corrected in the same change and no longer state the TypeScript-only
175+
split.
176+
177+
Two things remain TypeScript-only: the seven authoring-lint advisories (Consequences), and the
178+
structured `--format` payload of `verify`.
170179

171180
## Alternatives considered
172181

@@ -178,7 +187,9 @@ corrected with them, not ahead of them.
178187
Object coverage already works the other way, with one code and a named severity constant.
179188
- **A run-time lookup by name for JVM and .NET witnesses, as Python does.** Rejected: it is
180189
runtime reflection (ADR-0001), and a witness for a deleted requirement would go on existing
181-
unnoticed. The interface makes that a compile error.
190+
unnoticed. The interface makes that a compile error in Kotlin always, in Java when the
191+
witness method carries `@Override`, and in C# when the witness implements the member
192+
explicitly (clause 13).
182193
- **Port the three-way merge to four more languages.** Rejected: none of those ports has one,
183194
and the witness model keeps hand-written code out of the generated file altogether.
184195
- **Emit JUnit 4 as well as Jupiter.** Rejected: the renderer hook and eject already cover a

0 commit comments

Comments
 (0)