diff --git a/AGENTS.md b/AGENTS.md index 842c21e8b..71af89cbf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,17 +6,17 @@ MetaObjects is a **cross-language metadata standard** for declaring typed entity The metamodel is the **durable spine**; generated code is the **disposable artifact**. Substrate is local-first: typed metadata lives in your repo, generated code is idiomatic per-language output with **no proprietary runtime**: the generated entity and model code imports nothing from MetaObjects at runtime, and the REST, prompt, client and runtime tiers import ordinary Apache-2.0 packages you can vendor, fork or replace. If `@metaobjectsdev/*` disappears tomorrow, you keep working code. -**Core vs helpers (ADR-0034 Amendment 3, 2026-09-22).** Only the core is a product promise: the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager` and its drivers; the HTTP adapters that mount it on a web framework are helpers, 2026-09-24 ruling); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser. Every generator that writes application code into an adopter's repo — routes, controllers, ORM wiring, DTOs, forms, grids, hooks, filter allowlists — is a **reference helper**: it must compile and pass its reference fixtures, and the adopter copies it with `meta eject` and owns the copy. Mechanical test: what the tool guarantees is core; what it writes into your repo is a helper. Every port can eject: `meta eject` (TypeScript), `metaobjects eject` (Python), `mvn metaobjects:eject` (Java/Kotlin), `dotnet meta eject` (C#). Not every generator ships a reference copy: `meta gen --list` marks the ones that do not `package-only` (in TypeScript: `callable`, `trace-helper`, `requirement-tests`, `template`, the docs tier and `shared-model`; ADR-0034 Amendment 3 correction, 2026-09-26). When you find a generator defect, fix the reference; do not describe it as a broken core guarantee. +**Core vs helpers (ADR-0034 Amendment 3, 2026-09-22).** Only the core is a product promise: the metamodel, loader, canonical format and registry; runtime metadata access (the `ObjectManager` and its drivers; the HTTP adapters that mount it on a web framework are helpers, 2026-09-24 ruling); schema migrations (`meta migrate`); the drift gates (`meta verify`); prompt render and the reply parser. Every generator that writes application code into an adopter's repo — routes, controllers, ORM wiring, DTOs, forms, grids, hooks, filter allowlists — is a **reference helper**: it must compile and pass its reference fixtures, and the adopter copies it with `meta eject` and owns the copy. Mechanical test: what the tool guarantees is core; what it writes into your repo is a helper. Every port can eject: `meta eject` (TypeScript), `metaobjects eject` (Python), `mvn metaobjects:eject` (Java/Kotlin), `dotnet meta eject` (C#). Not every generator ships a reference copy: `meta gen --list` marks the ones that do not `package-only` (in TypeScript: `callable`, `trace-helper`, `template`, the docs tier and `shared-model`; ADR-0034 Amendment 3 correction, 2026-09-26). When you find a generator defect, fix the reference; do not describe it as a broken core guarantee. ## Six pillars -The first four ship per-language today across the five ports (TS / C# / Java / Python / Kotlin), with cross-port conformance corpora verifying byte-identical behavior. The fifth ships its vocabulary in every port, its `meta verify` checks in the Node `meta` CLI, and its test scaffolding in TypeScript only. The sixth is content the other five compose, shipped as named opt-in artifacts: +The first four ship per-language today across the five ports (TS / C# / Java / Python / Kotlin), with cross-port conformance corpora verifying byte-identical behavior. The fifth ships its vocabulary, its `verify` checks and a `requirement-tests` generator in every port, and its authoring lint in TypeScript only. The sixth is content the other five compose, shipped as named opt-in artifacts: 1. **Codegen** *(reference helpers, not core — see "Core vs helpers" above; ejectable in every port)* — emit per-language code (Drizzle/Zod + Fastify for TS, EF Core + ASP.NET for C#, Spring REST + DTO + Repository for Java via `codegen-spring`, Pydantic + FastAPI for Python, KotlinPoet + Exposed + Spring for Kotlin via `codegen-kotlin`). Hand-edit-preserving regen via three-way merge. 2. **Runtime metadata** — load metadata at runtime, drive behavior dynamically (CRUD, validation, relationships, dynamic admin UIs, LLM tool registration). On Kysely (TS), a DB-API 2 driver via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin). **C# runtime metadata is on the roadmap** — its EF Core output is generated code, not a metadata-driven runtime; the runtime `MetaObjects` package depends only on YamlDotNet and ships no data access. 3. **Drift detection** — `meta verify` catches divergence between code and metadata (covers entity codegen, prompt templates, output parsers, schema). Quality-of-life on top of codegen + runtime. 4. **Prompt construction** — a prompt is code, not a string scattered across services. Declare a prompt's payload as a typed projection (payload bloat becomes a diff), keep its text external and provider-resolved, and render it deterministically: snapshot-testable, cache-stable (no whitespace change silently breaking exact-prefix prompt-cache hits), and drift-checked at build time so a renamed field can't degrade a prompt. Conformance-gated, so the guarantee holds in every language port. **Render + payload-VO codegen + `verify` + parser-on-receipt for a *responding* `template.prompt` — one carrying `@responseRef` (FR-006) — + the output-format prompt fragment & tolerant `extract` parser (FR-010) ship in all five ports today** (since 0.24.0 the whole inbound tier keys off `@responseRef`; a `template.output` is outbound-only and emits no parser — ADR-0052) — the library-side building blocks of the pillar are complete. The one remaining library-side piece is MCP exposure of declared prompts/tools (see `spec/roadmap.md`); the application-level consolidation (eval harness, end-to-end declared-prompt orchestration) and consumer adoption are exercised in adopter projects, not in this library repo. Designed in `docs/superpowers/specs/2026-05-22-fr-004-cross-language-prompt-construction-design.md`. -5. **Requirements and testing** — declare what the software is supposed to *do* in the same model as the entities, so a capability claim is checkable instead of prose. The other four pillars keep the code honest about the *model*; this one asks whether the thing you said the software does is actually built — an absence no test can fail on, because a test exercises code that exists. `requirement.functional` (existence: `meta verify` WARNS when nothing implements it) and `requirement.architectural` (universality: `meta verify` FAILS a live policy applied to nothing, and does not check that each claimed node complies) are registered vocabulary in all five ports, with the loader enforcing the closed `@status` enum. `@implementedBy` is **resolved, not trusted** — it names a real member of the real model, so a claim whose implementation was renamed or deleted fails the build instead of going quietly stale. `meta verify` reports the ledger on every run (unresolved links, entities no claim covers, gaps recorded versus gaps nobody has ruled on) plus an authoring lint whose findings can never fail a build; `meta docs` renders it for humans and for agents. **The port split, stated exactly: the vocabulary loads and validates in all five ports; the `meta verify` checks run in the Node `meta` CLI only (no other port's CLI runs them — `docs/CONFORMANCE.md` "Split coverage"); `requirementTests()` — which scaffolds a test stub per claim — is TypeScript-only.** A green run proves referential integrity, never that a status is true or an implementation correct (`docs/features/requirements.md`, "What a green run does not prove"). A project that declares no `requirement.*` nodes sees no change at all. Note the standing carve-out: `agent-context/skills/metaobjects-fit-assessment/SKILL.md` deliberately does NOT treat `requirement.*` as an assessment axis — see the ruling in that file before "finishing the job" there. +5. **Requirements and testing** — declare what the software is supposed to *do* in the same model as the entities, so a capability claim is checkable instead of prose. The other four pillars keep the code honest about the *model*; this one asks whether the thing you said the software does is actually built — an absence no test can fail on, because a test exercises code that exists. `requirement.functional` (existence: `meta verify` WARNS when nothing implements it) and `requirement.architectural` (universality: `meta verify` FAILS a live policy applied to nothing, and does not check that each claimed node complies) are registered vocabulary in all five ports, with the loader enforcing the closed `@status` enum. `@implementedBy` is **resolved, not trusted** — it names a real member of the real model, so a claim whose implementation was renamed or deleted fails the build instead of going quietly stale. `meta verify` reports the ledger on every run (unresolved links, entities no claim covers, gaps recorded versus gaps nobody has ruled on) plus an authoring lint whose findings can never fail a build; `meta docs` renders it for humans and for agents. **The port split, stated exactly: the vocabulary loads and validates in all five ports; the requirement gate runs in every port's `verify` (ADR-0057 — `docs/CONFORMANCE.md` "Split coverage"); every port ships an ejectable `requirement-tests` generator — which scaffolds a test per claim; the seven authoring-lint advisories are TypeScript-only.** A green run proves referential integrity, never that a status is true or an implementation correct (`docs/features/requirements.md`, "What a green run does not prove"). A project that declares no `requirement.*` nodes sees no change at all. Note the standing carve-out: `agent-context/skills/metaobjects-fit-assessment/SKILL.md` deliberately does NOT treat `requirement.*` as an assessment axis — see the ruling in that file before "finishing the job" there. 6. **Libraries** — reusable declared design, shipped as metadata and opted into by name (`"libraries": ["iam"]` in `.metaobjects/config.json`). Not a sixth verb: a library is the **reuse unit that composes the other five** — entities, requirements, the generators its design implies, its runtime packages — as one named, opt-in, drift-gated artifact. What makes it a pillar rather than a folder of YAML is the fifth: without requirements a library is a schema snippet; with them it is design an adopter's build is held to, which is the same test the requirements pillar passes (*does it change what an agent can be checked against?*). **A library is LAYERED and its core layer is INERT** — the core declares no `source.rdb`, and a sourceless object generates nothing and migrates to nothing (#248), so `["iam"]` adds zero tables and zero generated code while making the design present and resolvable; `["iam", "iam/db"]` is the separate opt-in that proposes the schema. **Copy is the expected mode** (`meta eject ` — ADR-0034's ruling applied to metadata), and an ejected copy still named in `libraries` is refused at load (`ERR_LIBRARY_PACKAGE_COLLISION`) rather than merging asymmetrically. Object coverage activates on ADOPTER-authored requirements only, so a library cannot volunteer a project for a gate it did not ask for. `iam` (preview) and `ai` (stable) ship today; rows appear in `meta gen --list` beside the generators. See [docs/features/libraries.md](docs/features/libraries.md) and `docs/superpowers/specs/2026-09-13-fr-043-feature-and-nfr-packages-design.md`. @@ -75,7 +75,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken. - **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres. **Cross-port conformance corpora** (every port runs the shared corpus): -- Metamodel: `fixtures/conformance/` (364 fixtures; 26 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green. +- Metamodel: `fixtures/conformance/` (364 fixtures; 28 shared corpora in total — per-corpus counts + the corpus x port matrix live in `docs/CONFORMANCE.md`). TS / C# / Java / Python all green. - Render: `fixtures/render-conformance/`. TS / C# / Java / Kotlin / Python byte-identical. - Persistence: `fixtures/persistence-conformance/`. **Query** scenarios run on every port (TS / C# / Java / Kotlin / Python), each provisioning its test DB by executing the committed, TS-produced `canonical/schema.postgres.sql` (Postgres only — Derby dropped for the cross-port query corpus, ADR-0015). The **migration** scenarios are exercised by **TS only** (TS owns schema migrations). **The corpus now gates WRITES, not just reads (SP-H):** an `op: roundtrip` scenario type INSERTs through each port's runtime/ORM write codec (NOT raw SQL), reads the row back, and asserts the wire-normalized value. The `AllTypes` entity (`roundtrip-all-types.yaml`) carries one field of **every** persistable `field.*` subtype — string/int/long/double/float/decimal/boolean/date/time/timestamp(+tz)/currency/enum/uuid/object — plus an **array-of-VO** `field.object @isArray @storage:jsonb` column (`labels`, written as 2-element / empty-`[]` / single-element arrays across the three rows) — so every subtype write+read (incl. the array-of-value-object jsonb codec) round-trips through every port against Testcontainers PG. (`field.byte`/`field.short`/`field.class` were cut as non-functional registration-only stubs — the matrix tracks only genuinely-supported subtypes; see `fixtures/registry-conformance/README.md` → "Per-subtype write-round-trip matrix".) - API-contract: `fixtures/api-contract-conformance/`. TS / C# / Java / Kotlin / Python all green — each port runs **two lanes**: a hand-rolled reference server AND its **generated** API artifact booted over HTTP (the deployed controller/routes; TS+C# full-stack vs Testcontainers PG, Java/Kotlin/Python generated controller + in-memory repo behind the consumer seam). The generated fan-out found 10 real deployment bugs golden snapshots missed. Three sub-corpora run the **generated lane only, on all five ports** — `write-through/`, `projection/` (F22: a view-only `object.projection` serves GET list + GET by id and answers every write verb with `405 {"error": "method_not_allowed"}`) and `report/` (FR-044: a view-backed `object.report` serves GET list, answers `POST` with that same `405`, and mounts no `/{id}` route). That is deliberate, not a gap: what is under test is whether a port's GENERATOR emits those routes, and a hand-rolled reference server would answer every scenario by construction. The `m2m/` sub-corpus also gates **TPH x M:N together** (base-declared, subtype-declared, abstract-mid-declared, a non-subtype source onto a subtype TARGET, and the cross-subtype source id answering `200 []`) — the two corpora were originally built disjoint (`tph/` had no relationships, `m2m/` no discriminators), which is precisely how that defect class survived. diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a82eec5..499da1f26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,78 @@ it until 1.1 ships._ (`-Dmeta.verify.noFieldLint=true` in Maven) or `META_NO_FIELD_LINT=1`. In the Node `meta` it is the `fields` section of `--format json|toon`. +- **The requirement gate runs in every port's `verify` (ADR-0057).** `metaobjects verify` + (Python), `mvn metaobjects:verify` (Java and Kotlin) and `dotnet meta verify` (C#) now run + the checks that `meta verify` runs over `requirement.*` nodes: on every run, with no + subverb, with the same codes, severities, requirement paths and message text, and the same + ledger summary line. The new `fixtures/requirement-check-conformance/` corpus (43 cases) + holds all of them to the TypeScript reference. + + **A project with no `requirement.*` node sees no change in any port: no line printed, no + exit code changed. A project that DOES declare requirements and runs a Python, Java, + Kotlin or C# `verify` now gets diagnostics it did not get before, and its `verify` may + now fail** where it passed: on a dangling `@implementedBy` on a `live` or `partial` + requirement, a link above the L4 floor, a level or nesting error, a dangling + `@supersededBy`, or a live architectural requirement applied to nothing. Those were + already errors in `meta verify`; the other ports loaded the same ledger and said nothing. + The seven requirement authoring-lint advisories stay TypeScript-only. See + [docs/features/requirements.md](docs/features/requirements.md), "The gate in every port". + + Metadata that does not load is not a project with no requirement, and the gate fails on + it. **Python: `metaobjects verify --out ` with no `--generators` now exits 1 on + metadata the loader refuses, where it exited 0**, because that invocation loads nothing + else; the gate's load is strict unless `--lax`, so an unknown attribute counts. `dotnet + meta verify` likewise prints the load errors itself when no gate in its own process loaded + the model (`--codegen` handed off to an owned `codegen/` project, or given no `--out`). +- **`--require-implementers`, in every port.** `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a + `live` or `partial` functional requirement that nothing implements) stays a warning by + default. `meta verify --require-implementers`, `metaobjects verify --require-implementers`, + `dotnet meta verify --require-implementers`, `mvn metaobjects:verify + -Dmeta.verify.requireImplementers=true`, or `META_REQUIRE_IMPLEMENTERS=1` with any of + them, reports it as an error under the same code. No other warning changes severity. +- **A `requirement-tests` generator in Python, Java, Kotlin and C#, ejectable in each.** + Python writes pytest, Java and Kotlin JUnit Jupiter, C# xUnit: one test per requirement + the filter selects (functional L4 and L5 by default), in one file per metamodel package + (an interface and a test class in the three compiled ports), rewritten whole on every + run. The project's code lives in a *witness* the generated test calls. A `live` or + `partial` requirement with no witness is a failing test that names the function to write; + a `planned` or `retired` one is skipped. Generated tests import their test framework and + nothing from MetaObjects. This is a reference helper and a recommended approach, not a + contract: `metaobjects eject requirement-tests`, `mvn metaobjects:eject + -Dnames=requirement-tests -Dport=java|kotlin` and `dotnet meta eject requirement-tests` + copy the generator with its default renderer, and the checks in `verify` are not + ejectable. What a ledger yields (each test's id, witness key, skip state and digest) is + the same in all five ports, pinned by the new + `fixtures/requirement-test-identity-conformance/` corpus (26 cases). Two limits: in + those four ports a package that loses its last requirement leaves its generated file + behind (`gen` does not remove it; the codegen drift gate reports it), and a witness for a + retired or deleted requirement stops compiling only in Kotlin, in Java when it carries + `@Override`, and in C# when it implements the member explicitly. See + [docs/features/requirements.md](docs/features/requirements.md), "Generated requirement + tests and witnesses". +- **The same filter and uncovered-warning options on the generator in every port.** Each + port takes a predicate over one requirement view (`subType`, `level`, `status`, `path`, + `package`, `implementedByTypes`) that replaces the default selection, and a switch for + the warning that names what the filter left out: `filter` / `warnUncovered` in + TypeScript and in Python's `requirementTests` config block, `` / + `` generator args in Java and Kotlin, `Filter` / `WarnUncovered` in C#. + The warning has one text in every port and names requirement paths. +- **TypeScript: `meta eject requirement-tests` works.** The generator now ships a reference + template, holding the generator and its default stub renderer in one file, so eject + copies it to `codegen/generators/requirement-tests.ts` instead of answering + `package-only`. +- **TypeScript: `requirementTests({ grain })`, and the test identity on the renderer hook.** + `grain: "member"` emits one stub per distinct `@implementedBy` reference that resolves, + where the default `"concern"` emits one per distinct `.`. A renderer now + receives the stub's identity (`package`, `unit`, `id`, `witnessKey`, `skip`) and the + `digest` of its requirement, and `requirementDigest`, `requirementTestIdentities` and + `witnessKeyOf` are exported from `@metaobjectsdev/codegen-ts`. The default stub's bytes + do not change. +- **Python: how to run the tool on a different interpreter than the project's.** + `metaobjects` still needs Python 3.11 to run; the tests it generates need only pytest and + parse on 3.9. `docs/ports/python.md`, "Selecting the interpreter", gives the `uv tool run + --python` and `pipx run --python` forms. + - **Metamodel 1.1: the reporting vocabulary (FR-044), loader-validated in all five ports.** Registered: `dimension.attribute`, `dimension.time` (`@grains`: `hour, day, week, month, quarter, year`, weeks start Monday), `measure.aggregate` (`@agg`: `count, sum, avg, min, max`), @@ -231,6 +303,25 @@ until you regenerate. ### Changed +- **TypeScript: `RequirementTestArgs` and `RequirementView` gained required fields — a + compile break for code that builds them by hand.** `RequirementTestArgs` has six new + required fields (`package`, `unit`, `id`, `witnessKey`, `skip`, `digest`) and + `RequirementView` a required `package`. A renderer or filter that only RECEIVES these is + unaffected. Application code that CONSTRUCTS one, for example a test of its own renderer, + stops compiling until it supplies them. +- **TypeScript: `requirementTests({ grain })` refuses an unknown grain.** Any value other + than `"concern"` or `"member"` throws when the generator is built, whatever the model + holds, instead of running as something in between. Every other port refuses the same way. +- **Python: project code under a standard-library module name is refused where a + `module:symbol` names it.** Affected: a project whose owned generator (a `module:symbol` + entry in `generators`) or `requirementTests.renderer` / `requirementTests.filter` hook lives + under a top-level name such as `types`, `json`, `code` or `platform`, as a package directory + or as a `.py` file. `gen` and `verify --codegen` now stop with `cannot import + '': the project package '' () shadows a standard-library module of the + same name; rename it` (`the project module` for a file). Before, an owned generator under + such a name loaded, by replacing the standard-library module for the rest of the process. + Rename the package or module, and the `module:symbol` that names it. + - **TypeScript: `runValidators` rejects more than it did — a behaviour change for `ObjectManager` users.** `ObjectManager.create`, `createMany`, `update`, `updateMany` and `validate` all go through it, so data that was accepted before can now raise a @@ -302,6 +393,11 @@ until you regenerate. now holds the expanded form in a single-file model too, where it held `::parts` before. Gated by the new `loader-relative-package-multi-root` conformance fixture and `reference-field-missing-multi-root-package` in `fixtures/field-lint-conformance/`. +- **Python: a YAML node that declares its own `package` under a packaged root now loads.** + A node carrying `package:` inside a document whose root already declared one raised + `NameError` at load, because the YAML desugar used a constant it never imported. Such a + document now loads, and the node resolves under the package it declares (an absolute one + as written, a `::`-relative one against its root's). - **Python: `MetaData.effective_package()`** returns the package a node resolves under, where `package` is `None` for an object that inherits its file's root package. See `docs/ports/python.md`, "Use". diff --git a/README.md b/README.md index 78e332da1..e5f52a94b 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ first-week wedge plan — and `meta init` picks up from there. |---|---|---|---| | TypeScript | **npm `1.0.13`** — the `@metaobjectsdev/*` packages | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) | | 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/) | -| 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/) | +| 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/) | | 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/) | | 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/) | @@ -153,7 +153,7 @@ first-week wedge plan — and `meta init` picks up from there. | DB-drift verify | `meta verify --db ` | Template-drift: `Verify.check`; schema-drift is TS-owned (ADR-0015) | Template-drift: `Verify.check`; startup: `MetadataStartupValidator` | `dotnet meta verify` (codegen-drift) | Schema-drift is TS-owned (ADR-0015) | | Template-drift verify | Yes | Yes (`Verify.check`) | Yes (via Java) | Yes (`dotnet meta verify`) | Yes (`metaobjects.render.verify`) | | YAML authoring (sigil-free → JSON) | Yes | Yes | Yes (via Java) | Yes | Yes | -| Capability requirements (`requirement.*`) | Registered + `meta verify` gate | Registered (loads + validates) | Registered (via Java) | Registered (loads + validates) | Registered (loads + validates) | +| Capability requirements (`requirement.*`) | `meta verify` gate + authoring lint; `requirementTests()` stubs | `mvn metaobjects:verify` gate; JUnit Jupiter `requirement-tests` | Gate via the Java Maven goal; JUnit Jupiter `requirement-tests` | `dotnet meta verify` gate; xUnit `requirement-tests` | `metaobjects verify` gate; pytest `requirement-tests` | | Reporting vocabulary (`dimension` / `measure` / `segment` / `object.report`, FR-044) | Registered; a view-backed report becomes a SQL view in `meta migrate`, is read by `ObjectManager`, and gets generated read-only list routes (no client hook yet) | Registered; OMDB reads a view-backed report; generates a read-only Spring controller, DTO, repository and filter allowlist | Registered (via Java); generates an Exposed table object, a row data class, a filter allowlist and a read-only Spring controller per view-backed report | Registered; generates a keyless EF Core row type, read-only routes and a filter allowlist per view-backed report | Registered; `ObjectManager` reads a view-backed report; generates a Pydantic row model, a read-only FastAPI router and a filter allowlist | | Libraries (`libraries: [...]`) | Yes | Yes | Yes (via Java) | Yes | Yes | | Metadata dependencies (`dependencies`) | Yes (`meta deps sync`, `path` transport) | Phase 2 | Phase 2 | Phase 2 | Yes (loads the synced snapshot) | @@ -226,9 +226,9 @@ sixth ships two libraries at their own stability labels: deterministically (snapshot-testable, cache-stable, drift-checked at build time, conformance-gated cross-language). See [`docs/features/templates-and-payloads.md`](docs/features/templates-and-payloads.md). -5. **Requirements and testing** *(vocabulary loads and validates in all five ports; - the `meta verify` checks run in the Node `meta` CLI; `requirementTests()` scaffolding - is TypeScript-only; dogfooded on maintainer-owned projects, no outside adopter yet)* — +5. **Requirements and testing** *(vocabulary, the `verify` checks and a `requirement-tests` + generator in all five ports; the authoring lint is TypeScript-only; dogfooded on + maintainer-owned projects, no outside adopter yet)* — declare what the software is supposed to *do* in the same model as the entities. The other four pillars keep the code honest about the model; this one asks whether a claimed capability is actually built. `@implementedBy` is **resolved, not trusted** — diff --git a/agent-context/skills/metaobjects-audit/references/requirements.md b/agent-context/skills/metaobjects-audit/references/requirements.md index 185649e66..a81dfbafd 100644 --- a/agent-context/skills/metaobjects-audit/references/requirements.md +++ b/agent-context/skills/metaobjects-audit/references/requirements.md @@ -4,8 +4,10 @@ This project declares `requirement.*` nodes, so the ledger is an auditable surfa for **truthfulness**, not volume. A large ledger that lies is worse than a small one that does not, because every later reader trusts it. -Run `meta verify` first. It settles referential integrity mechanically — do not spend audit -effort re-deriving what a green run already proves. +Run `verify` first — the project's own: `meta verify`, `metaobjects verify`, +`mvn metaobjects:verify` or `dotnet meta verify`. Every port runs the same requirement +gate. It settles referential integrity mechanically — do not spend audit effort re-deriving +what a green run already proves. ## What verify has already proven (do not re-check by hand) @@ -18,7 +20,9 @@ on `planned`, whose nodes do not exist yet). **The authoring lint** — printed under its own heading, advisory, and unable to change the exit code — settles the naming and prose defects you would otherwise find by reading every -entry: +entry. **Only the Node `meta verify` prints it.** On a Python, Java, Kotlin or C# project +whose pipeline runs that port's `verify` alone, nothing below has been proven: run +the Node `meta verify` over the same metadata, or audit these by hand. | code | what a clean run has already proven | |---|---| @@ -30,8 +34,10 @@ entry: | `WARN_REQUIREMENT_INERT_DOC_SLOT` | no `summary` is set — nothing reads it, and `@statement` is already the required one-liner | | `WARN_REQUIREMENT_TITLE_IS_AN_ID` | no `title` leads with a catalogue or ticket id | -Two limits, and each puts something back on your list: +Three limits, and each puts something back on your list: +- **It is one port's.** See above: establish which `verify` the project's CI runs before + crediting a green pipeline with the table. - **The lint is mutable.** `--no-requirement-lint` / `META_NO_REQUIREMENT_LINT=1` silences the advisory half while the gate still runs. **Establish whether the project mutes it** (§F of the checklist asks this) — against a muted lint the whole table above proves nothing. @@ -78,6 +84,15 @@ titles: one that restates `@statement`, or repeats the path in prose, is now vis a heading rather than a private authoring habit. An absent `title` is NOT a finding — the entry heads by its path alone, which is what every sibling surface addresses it by. +**7. Witnesses that do not test the claim.** Where the project generates requirement tests +(the `requirement-tests` generator, in any port), a passing generated test proves only that +its witness — or, in TypeScript, the hand-filled stub — ran. Sample the witnesses and read +them against the counterexample each generated test carries: one that asserts nothing, or asserts +something else, is the same defect `@verifiedBy` had. Check also for witnesses that have +gone stale silently: a Java witness that lacks the override annotation, a C# witness implemented +implicitly, and any Python witness keep existing after their requirement is retired or +deleted, and nothing reports them. + ## Scope — do NOT flag these as defects - **Unclaimed `object.value` / `object.projection`.** Exempt by design: a value is a shape, a diff --git a/agent-context/skills/metaobjects-authoring/references/requirements.md b/agent-context/skills/metaobjects-authoring/references/requirements.md index a0b215c1f..8ccfc08bd 100644 --- a/agent-context/skills/metaobjects-authoring/references/requirements.md +++ b/agent-context/skills/metaobjects-authoring/references/requirements.md @@ -179,5 +179,20 @@ ambition rather than work. implementedBy: ["game::turn::Turn", "game::world::Location"] ``` +**Which `verify` says what.** The gate — dangling references, links above the floor, +level and nesting errors, an unclaimed entity, a live requirement nothing implements — runs +in every port's `verify` (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, +`dotnet meta verify`). The authoring warnings named on this page (`WARN_REQUIREMENT_NAME_*`, +`WARN_REQUIREMENT_PROSE_*`, `WARN_REQUIREMENT_INERT_DOC_SLOT`, +`WARN_REQUIREMENT_TITLE_IS_AN_ID`) are printed by the Node `meta verify` only, so on another +port follow these rules without waiting for a warning. + +**A requirement's name and package end up in generated test names.** Every port's +`requirement-tests` generator names a test from the requirement's package and dotted path +(`acme::shop::Orders.Recorded` becomes `req_acme_shop_Orders_Recorded…`), and two +requirements whose names differ only in punctuation (`Orders.Recorded` beside +`Orders_Recorded`) are refused as a collision outside TypeScript. Another reason to keep +names plain identifiers. + There is **no `satisfies:` on a field or entity** — links live on the requirement node, not on the nodes it claims. Full reference: the repo's `spec/capability-ledger.md`. diff --git a/agent-context/skills/metaobjects-codegen/references/csharp.md b/agent-context/skills/metaobjects-codegen/references/csharp.md index a241eff89..03eeb622d 100644 --- a/agent-context/skills/metaobjects-codegen/references/csharp.md +++ b/agent-context/skills/metaobjects-codegen/references/csharp.md @@ -94,6 +94,26 @@ create/update re-reads the row via the view by primary key (read-your-writes). T view's DDL is emitted by the Node `meta migrate` from the same origin assembly as a projection view. +**Requirement tests (`requirement-tests`).** A recommended approach, not a contract. For a +model that declares `requirement.*` nodes it writes, per metamodel package, +`Requirements__Witnesses.g.cs` (an interface with one default member per +non-skipped test, each failing with `unimplemented requirement: …`) and +`Requirements__Tests.g.cs` (one xUnit `[Fact]` per requirement; `[Fact(Skip = …)]` +for a `planned` or `retired` one), in `.Requirements`. Both are rewritten whole +on every run, so never edit them: the project's code goes in one witness class +(`.RequirementWitnesses` by default) that implements every generated interface. +Implement each member EXPLICITLY (`void Requirements__Witnesses.req_…() { … }`): a +witness whose requirement is retired or deleted then stops compiling (CS0539), where an +implicit `public void req_…()` goes stale silently. Give the generator its own +`dotnet meta gen … --out --generators requirement-tests` run (a run has +one `--out`, and the test project needs xunit), and pass `--namespace` to +`verify --codegen` for that directory. Options (`TestNamespace`, `WitnessClass`, `Grain`, +`Filter`, `Renderer`, `WarnUncovered`) are public properties set in an owned +`codegen/Program.cs`; `Grain` needs `using MetaObjects.Core.Requirement;`. A filter +REPLACES the default of functional L4/L5. `dotnet meta eject requirement-tests` copies the +generator with its default rendering. The requirement checks in `dotnet meta verify` are +core and are not ejectable. + ## Docs — `dotnet meta docs` ```bash diff --git a/agent-context/skills/metaobjects-codegen/references/java.md b/agent-context/skills/metaobjects-codegen/references/java.md index df517b075..0f2a615e4 100644 --- a/agent-context/skills/metaobjects-codegen/references/java.md +++ b/agent-context/skills/metaobjects-codegen/references/java.md @@ -206,6 +206,24 @@ read-view (#214): OMDB routes reads through the replica view and writes to the t row via the view by primary key (read-your-writes). The replica view's DDL is emitted by `meta migrate` from the same origin assembly as a projection view. +**Requirement tests (`JUnitRequirementTestsGenerator`, stable name `requirement-tests`).** +A recommended approach, not a contract. For a model that declares `requirement.*` nodes it +writes, per metamodel package, `Requirements__Witnesses.java` (an interface with +one default member per non-skipped test, each failing with `unimplemented requirement: …`) +and `Requirements__Test.java` (one JUnit Jupiter `@Test` per requirement; +`@Disabled` for a `planned` or `retired` one). Both are rewritten whole on every run, so +never edit them: the project's code goes in the class `witnessClass` names, which +implements every generated interface and overrides the members it has witnesses for. +Annotate each witness `@Override`: a witness whose requirement is retired or deleted then +stops compiling, and without the annotation it goes stale silently. Args: `testPackage` +and `witnessClass` (required once the model holds a requirement), `grain` (`concern` or +`member`), `filter` and `renderer` (class names on the project classpath implementing +`RequirementTestFilter` / `RequirementTestRenderer`; a filter REPLACES the default of +functional L4/L5), `warnUncovered`. Jupiter only: a JUnit 4 project uses `renderer` or +ejects. `mvn metaobjects:eject -Dnames=requirement-tests -Dport=java` copies the generator +with its default rendering (pass `-Dport`: the name is ejectable on both JVM +ports). The requirement checks in `mvn metaobjects:verify` are core and are not ejectable. + ### Value-object jsonb columns A `field.object` with `@storage: jsonb` (single or `@isArray`) is a typed jsonb column diff --git a/agent-context/skills/metaobjects-codegen/references/kotlin.md b/agent-context/skills/metaobjects-codegen/references/kotlin.md index 1a53af36a..64b67df90 100644 --- a/agent-context/skills/metaobjects-codegen/references/kotlin.md +++ b/agent-context/skills/metaobjects-codegen/references/kotlin.md @@ -216,6 +216,25 @@ read-view (#214): reads route through the replica view and writes to the table ( (read-your-writes). The replica view's DDL is emitted by `meta migrate` from the same origin assembly as a projection view. +**Requirement tests (`KotlinRequirementTestsGenerator`, stable name `requirement-tests`).** +A recommended approach, not a contract. For a model that declares `requirement.*` nodes it +writes, per metamodel package, `Requirements__Witnesses.kt` (an interface with one +default member per non-skipped test, each failing with `unimplemented requirement: …`) and +`Requirements__Test.kt` (one JUnit Jupiter `@Test` per requirement; `@Disabled` for +a `planned` or `retired` one). Both are rewritten whole on every run, so never edit them: +the project's code goes in the class `witnessClass` names, which implements every generated +interface and overrides the members it has witnesses for. Write that class in Kotlin (a +Java witness class against these interfaces is untested). A witness whose requirement is +retired or deleted stops compiling, because `override` is mandatory. Args: `testPackage` +and `witnessClass` (required once the model holds a requirement), `grain` (`concern` or +`member`), `filter` and `renderer` (class names on the project classpath implementing +`RequirementTestFilter` / `RequirementTestRenderer`; a filter REPLACES the default of +functional L4/L5; a renderer returns Kotlin source), `warnUncovered`. Jupiter only: a +JUnit 4 project uses `renderer` or ejects. +`mvn metaobjects:eject -Dnames=requirement-tests -Dport=kotlin` copies the generator with +its default rendering (pass `-Dport`: the name is ejectable on both JVM ports). The +requirement checks in `mvn metaobjects:verify` are core and are not ejectable. + Metadata lives under `src/main/metaobjects/` in the same canonical JSON the other ports read — fused-key form, `source.rdb` + `@table`, `@column` for a renamed physical column. diff --git a/agent-context/skills/metaobjects-codegen/references/python.md b/agent-context/skills/metaobjects-codegen/references/python.md index 248ffe38b..62c3510e1 100644 --- a/agent-context/skills/metaobjects-codegen/references/python.md +++ b/agent-context/skills/metaobjects-codegen/references/python.md @@ -110,6 +110,24 @@ a renamed physical column). | `names` | `_names.py` — module-level `Final` constants mirroring the object's metadata tree. Every node carries its own `_TYPE`/`_SUB_TYPE`/`_NAME`; `_NAME` is the OBJECT's name, and a physical name sits under the member naming what it is: `_SOURCE__{TABLE,VIEW,MATERIALIZED_VIEW,PROC,FUNCTION}` (`` is `PRIMARY` or `REPLICA`, so a write-through entity's read view has a slot), plus `_SOURCE__{KIND,SCHEMA}`, a `__FIELD`/`_COLUMN` pair each, `_IDENTITY__*` / `_INDEX__*` carrying `_INDEX` (the database index name) for `identity.secondary` and `index.lookup`, and a complete `_COLUMNS_BY_FIELD`. No `_READ_ONLY` — it was derived from `@kind`, never declared; ask `_SOURCE__KIND`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (columns and keys only, no `_SOURCE_*` — it has no table and must never acquire one). Python has no static inheritance, so a module whose object extends another **imports and re-exports** the parent's constants (`AUTHOR_CREATED_AT_COLUMN: Final[str] = BASEENTITY_CREATED_AT_COLUMN`) instead of restating the literal; a TPH subtype re-exports `_SOURCE_PRIMARY_*` too, since it shares its base's table. **This port generates no SQL**, so nothing generated consumes these — they exist for the repository `Protocol` implementation you write. | | `template` | the generic Mustache `template` primitive. | +**Requirement tests (`requirement-tests`).** A recommended approach, not a contract. For a +model that declares `requirement.*` nodes it writes +`requirements/test__requirements.py`, one pytest file per metamodel package, +rewritten whole on every run, so never edit it. Each test calls a **witness**: a function +named by the test's witness key (`req_acme_shop_Orders_Recorded__object_entity`) in the +witness module, `tests.requirement_witnesses` by default. A `live` or `partial` +requirement with no witness fails, naming the function to write; `planned` and `retired` +are skipped. Nothing reports a witness whose requirement is gone, so delete it with the +requirement. Options live in the `requirementTests` block of `metaobjects.config.yaml`: +`witnessModule`, `grain` (`concern` or `member`), `filter` and `renderer` (`module:symbol`; +a filter REPLACES the default of functional L4/L5; a renderer imports `RenderedTest` from +`metaobjects.codegen.requirement_hooks`), `warnUncovered`. The generated tests need only +pytest and run on Python 3.9 or later, while the tool itself needs 3.11 +(`uv tool run --python 3.12 metaobjects gen` runs it on another interpreter). +`metaobjects eject requirement-tests` copies the generator with its default renderer to +`codegen/generators/requirement_tests.py`. The requirement checks in `metaobjects verify` +are core and are not ejectable. + **Projections + entity read-views.** An `object.projection` (read-only `source.rdb` `@kind: view` child) gets a read-only Pydantic model from the `entity` generator. diff --git a/agent-context/skills/metaobjects-codegen/references/typescript.md b/agent-context/skills/metaobjects-codegen/references/typescript.md index 8048ef9b5..9a41d39f6 100644 --- a/agent-context/skills/metaobjects-codegen/references/typescript.md +++ b/agent-context/skills/metaobjects-codegen/references/typescript.md @@ -125,11 +125,22 @@ REMOVED their `@metaobjectsdev/codegen-ts/generators` export, so an owned copy i path for those. The engine primitives come from the package main entry, `@metaobjectsdev/codegen-ts`. The `/generators` subpath itself is NOT deprecated. It exports the prompt tier (`promptRender`, `outputParser`, `outputPrompt`, `extractor`, -`renderHelper`) and `namesFile`, which you may import from there OR eject to own, and it is -the only home of the package-only generators — `traceHelperFile`, `callableFile`, -`requirementTests` — which ship no reference template (`meta gen --list` marks them +`renderHelper`), `namesFile` and `requirementTests`, which you may import from there OR +eject to own, and it is the only home of the package-only generators — `traceHelperFile`, +`callableFile` — which ship no reference template (`meta gen --list` marks them `package-only`). +**`requirementTests()` — one test stub per requirement the model claims.** A recommended +approach, not a contract: a `live` or `partial` requirement gets a stub that fails until +you write its assertion (the three-way merge keeps it), a `planned` or `retired` one a +skipped stub. Options: `filter` (a predicate over `subType`, `level`, `status`, `path`, +`package`, `implementedByTypes` that REPLACES the default of functional L4/L5), `grain` +(`"concern"`, the default, or `"member"`; anything else is refused), `warnUncovered`, +`renderers` / `resolveRenderer`. `meta eject requirement-tests` copies the generator and +its default stub renderer into `codegen/generators/requirement-tests.ts`; the requirement +walk, each test's identity and the claim digest stay in the package. The requirement +checks in `meta verify` are core and are not ejectable. + The table below is a per-emission reference, NOT the selection surface. Select with `meta gen --list --format json --probe`, which is generated from the live registry and reports a file count for your own model; a table in a document cannot do either. diff --git a/agent-context/skills/metaobjects-verify/references/requirements.md b/agent-context/skills/metaobjects-verify/references/requirements.md index 63f83ed67..c1e57d55a 100644 --- a/agent-context/skills/metaobjects-verify/references/requirements.md +++ b/agent-context/skills/metaobjects-verify/references/requirements.md @@ -3,6 +3,22 @@ This project declares `requirement.*` nodes, so `verify` checks them. **There is no subverb**: requirements are metadata, so they are checked on *every* `meta verify` run. +## Which command runs the gate + +Every port's `verify` runs the same gate, with the same codes, severities and message +text (a shared corpus pins them). Run the one for the project's server language: + +| port | command | +|---|---| +| TypeScript | `meta verify` | +| Python | `metaobjects verify` | +| Java, Kotlin | `mvn metaobjects:verify` (one goal for both) | +| C# | `dotnet meta verify --templates ` (or `--codegen --out `) | + +The gate is core: it is not a generator and cannot be ejected or replaced. What differs +by port is the **authoring lint** further down this page, which only the Node `meta verify` +prints. This page writes `meta verify` throughout; read it as the port's own command. + ## The split, and why it matters when you read a failure | | owns | @@ -65,6 +81,25 @@ So on a `planned` entry a dangling reference is the entry doing its job, not dri | `@implementedBy` above the L4 link floor | 1 | | live `requirement.architectural` claimed by nothing | 1 | | an entity no requirement claims | 0 (warning) | +| a `live`/`partial` functional requirement nothing implements | 0 (warning); **1 under the strict switch** | + +## The strict switch — `--require-implementers` + +`WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a `live` or `partial` functional requirement where +neither it nor anything nested under it names a node) is a warning by default. A project +whose ledger has caught up with its links can make it fail the build: + +| port | flag | environment | +|---|---|---| +| TypeScript | `meta verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Python | `metaobjects verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Java, Kotlin | `mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` | +| C# | `dotnet meta verify … --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + +It raises that one finding to an error and **keeps its code**. Every other warning stays a +warning. If CI went red on this code, check whether the switch is set before treating it +as a new defect: the fix is to add the `@implementedBy` link (or drop the entry to +`planned`), not to unset the switch. ## The error codes, and the fix for each @@ -91,8 +126,10 @@ reasons, so reading only the code you hit can send you the wrong way. The questi ## The authoring lint — a second section, never an error -`verify` also prints an **authoring lint** under its own heading, after the gate's own -warnings: +The Node `meta verify` also prints an **authoring lint** under its own heading, after the +gate's own warnings. It is TypeScript-only: `metaobjects verify`, `mvn metaobjects:verify` +and `dotnet meta verify` run the gate above and never print these codes, so on those ports +a clean run says nothing about them. ``` meta verify — requirements: 6 authoring warning(s) (advisory — does not fail the build): @@ -135,6 +172,11 @@ way to satisfy the check is to find any name that already exists. The attribute is retired. Tying a requirement to a test is the job of a generator that emits the test **from** the requirement, so the link is structural rather than a name someone chose. +Every port ships one, `requirement-tests` (see the codegen skill's reference for the +project's language). `verify` still never reads test results: the codegen drift gate +(`verify --codegen`) proves the generated tests match the ledger, a live test with nothing +behind it fails in the project's own test run, and a generated test that passes proves its +witness ran, not that the witness tests the claim. ## What a green run does NOT prove diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 1bcc00402..f82ef7817 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -1,6 +1,6 @@ # Conformance coverage -The MetaObjects standard ships **26 shared conformance corpora** under +The MetaObjects standard ships **28 shared conformance corpora** under [`fixtures/`](../fixtures/). Every port runs every corpus that is *applicable to it* and asserts the same expected behaviour against the same fixtures. **This page is the inverse index**: fixture → feature doc + per-port pass status, and it is the @@ -50,6 +50,8 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor | [`fixtures/metamodel-docs/`](../fixtures/metamodel-docs/) | 1 | ✓ (docs emit is TS-owned) | — | — | — | — | | [`fixtures/fmt-conformance/`](../fixtures/fmt-conformance/) (#304 — `meta fmt`) | 12 | ✓ (reference) | ✓ | inherits via Java | ✓ | ✓ | | [`fixtures/field-lint-conformance/`](../fixtures/field-lint-conformance/) (the `verify` field authoring lint) | 15 | ✓ (reference) | ✓ | inherits via Java | ✓ | ✓ | +| [`fixtures/requirement-check-conformance/`](../fixtures/requirement-check-conformance/) (the `verify` requirement gate, ADR-0057) | 43 cases | ✓ (reference) | ✓ | inherits via Java (one Maven `metaobjects:verify` goal) | ✓ | ✓ | +| [`fixtures/requirement-test-identity-conformance/`](../fixtures/requirement-test-identity-conformance/) (the identity, skip state and digest of each generated requirement test, and the filter seam, ADR-0057) | 26 cases | ✓ (reference) | ✓ | identity function inherits via Java; emitted names asserted by its own generator test | ✓ | ✓ | | [`fixtures/naming-conformance/`](../fixtures/naming-conformance/) | 8 cases | ✓ | ✓ | inherits via Java (`RouteNaming.pluralize`) | ✓ | ✓ | | [`fixtures/codegen-noop/`](../fixtures/codegen-noop/) (FR-044 — reporting vocabulary: what is lowered, what stays inert) | 1 model pair (`reporting/with` vs `reporting/without`) | ✓ (codegen + migrate) | ✓ | ✓ | ✓ | ✓ | @@ -73,13 +75,17 @@ the corpora above do two different jobs. Only the first is a promise to adopters `extract-conformance/`, `verify-conformance/`, `verify-strict-conformance/`, `persistence-conformance/` (runtime reads and writes, and the TS-owned migration scenarios), `agent-context-conformance/`, `metamodel-docs/`, `fmt-conformance/` - (the canonical serializer, surfaced per-file — ADR-0034's "canonical format" is core) - and `field-lint-conformance/` (the advisory field lint every port's `verify` prints). + (the canonical serializer, surfaced per-file — ADR-0034's "canonical format" is core), + `field-lint-conformance/` (the advisory field lint every port's `verify` prints) and + `requirement-check-conformance/` (the requirement gate every port's `verify` runs; a + drift gate is core, and it is not ejectable in any port). A red cell here is a MetaObjects bug. - **Template quality checks — not a promise.** The generated lane of `api-contract-conformance/`, `generator-registry-conformance/` (stable generator names), - `codegen-noop/` (vocabulary with no lowering yet emits nothing) and the codegen-compile - gate. They check that the reference generators are correct + `codegen-noop/` (vocabulary with no lowering yet emits nothing), + `requirement-test-identity-conformance/` (which tests the `requirement-tests` reference + generator yields from a ledger and what each is called; the text it writes is not + pinned) and the codegen-compile gate. They check that the reference generators are correct starting points; an adopter's ejected copy is theirs and is not gated. The hand-rolled reference-server lane of `api-contract-conformance/` still pins the wire contract that the runtimes and the client speak, which is core. @@ -97,16 +103,25 @@ are recorded here instead. enums travel in `registry-conformance`'s byte-matched manifest, which every port reproduces exactly, and accept/reject behaviour is pinned by `requirement-*` fixtures in `fixtures/conformance/`. A port that drifts on what it will load fails. -- *Checks — TypeScript only, by decision.* The `meta verify` diagnostics over - requirements ship in the TypeScript CLI; the other ports load and validate and stop - there. Same call as [ADR-0015](../spec/decisions/ADR-0015-single-shared-migrate-engine.md): - one implementation of a build-time gate rather than five, where the gate is not a - per-port runtime concern. `verify-conformance` therefore holds no requirement cases, - and that absence is deliberate rather than a gap. +- *Checks — gated in all five ports.* Every port's `verify` runs the requirement gate + ([ADR-0057](../spec/decisions/ADR-0057-requirement-checks-and-tests-in-every-port.md), + which reversed a TypeScript-only split recorded here). `requirement-check-conformance` + pins each diagnostic's code, severity, path and message text and the summary counts + against the TypeScript reference. Kotlin has no CLI of its own and runs the gate through + the Java Maven goal. `verify-conformance` still holds no requirement cases: that corpus + is template drift, and the gate has its own. +- *Generated tests — identity gated in all five ports, text in none.* + `requirement-test-identity-conformance` pins which tests a ledger yields, each one's id, + witness key, skip state and digest, and what a project's filter is shown. The file a + port writes is its reference helper's output and is not a cross-port contract. +- *Authoring lint — TypeScript only, by decision.* The seven `WARN_REQUIREMENT_*` + authoring advisories are printed by the Node `meta verify` and by no other port (ADR-0057, + Consequences). They never fail a build, and no corpus holds them. The structured + `--format json|toon` payload is TypeScript-only too. Stated as mechanisms rather than as a list of attribute names on purpose — the requirement vocabulary has a breaking change scheduled (FR-038), which moves what the -manifest contains without moving the boundary between the two halves. +manifest contains without moving these boundaries. **Does the generated code compile** (`codegen-compile-conformance`, all five ports): @@ -166,8 +181,10 @@ manifest contains without moving the boundary between the two halves. **How to tell a deliberate split from a real parity gap**, since the two look identical in the matrix — both show one port covered and four blank. Ask what the uncovered ports -*claim*. Here they claim nothing: they load requirement vocabulary and stop, exactly as the -feature doc says. Contrast `{{#hasField}}` in 0.23.1, where the JVM emitted `has()` +*claim*. For the requirement authoring lint they claim nothing: no other port's `verify` +prints one of those advisories, exactly as the feature doc says. (The requirement *checks* +were a split of this kind until ADR-0057 moved them into every port, and they stopped +being one the moment a second port made the claim.) Contrast `{{#hasField}}` in 0.23.1, where the JVM emitted `has()` onto generated payload records **and** `verify` accepted the section, while no render engine in any port implemented the other half — two ports shipping halves of one promise, with no fixture that could see it. A split is deliberate when no port makes a claim the corpus would @@ -225,7 +242,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker. | `template-*`, `error-template-*` | [features/templates-and-payloads.md](features/templates-and-payloads.md) | | `origin-*`, `error-origin-*` | [features/templates-and-payloads.md](features/templates-and-payloads.md) (payload origins) | | `projection-*`, `error-projection-*`, `field-readonly-on-view-projection` | [features/source-kinds.md](features/source-kinds.md) (projections + the object taxonomy, ADR-0028) | -| `requirement-*`, `error-unknown-attr-requirement` | [features/requirements.md](features/requirements.md) (vocabulary only — the `meta verify` checks are TS-owned; see "Split coverage" above) | +| `requirement-*`, `error-unknown-attr-requirement` | [features/requirements.md](features/requirements.md) (vocabulary only — the gate has its own corpus, `requirement-check-conformance/`; see "Split coverage" above) | | `reporting-*`, `error-dimension-*`, `error-measure-*`, `error-ratio-*`, `error-segment-*`, `error-report-*`, `error-relative-date-*` | [features/reporting.md](features/reporting.md) (the `dimension` / `measure` / `segment` / `object.report` vocabulary and its load-time rules; design in the [FR-044 spec](superpowers/specs/2026-10-02-fr-044-core-reporting-design.md)) | | `smoke-empty-metadata` | [features/entities.md](features/entities.md) | @@ -404,11 +421,48 @@ own those two functions), and `server/python/tests/codegen/test_route_path_naming.py` + `server/python/tests/unit/test_fr016_source_name_and_kind_aliases.py` (same split). +### `fixtures/requirement-check-conformance/` (43 cases) + +All 43 cases → [features/requirements.md](features/requirements.md) ("The gate in every +port"). Each case is a directory: `input/` metadata, an optional `options.json` +(`libraries`, `requireImplementers`) and an `expected.json` holding the gate's diagnostics +(severity, code, requirement path, message text) and the ledger summary, or `null` for a +model with no requirement. The expectations are written from the TypeScript reference and +a port that disagrees with one is wrong. The case list, and what each pins, is in the +[corpus README](../fixtures/requirement-check-conformance/README.md). + +**Every port's `verify` is held to it:** +`server/typescript/packages/cli/test/requirement-check-conformance.test.ts`, +`server/python/tests/conformance/test_requirement_check_conformance.py`, +`server/java/metadata/src/test/java/com/metaobjects/conformance/RequirementCheckConformanceTest.java` +(Kotlin inherits it, through the shared Maven goal) and +`server/csharp/MetaObjects.Conformance.Tests/RequirementCheckConformanceTests.cs`. + +### `fixtures/requirement-test-identity-conformance/` (26 cases) + +All 26 cases → [features/requirements.md](features/requirements.md) ("Generated +requirement tests and witnesses"). It pins the *identity* of every test the +`requirement-tests` generator yields from a ledger (package, path, unit, id, witness key, +status, skip state and digest), witness-key collisions, and the view a project's filter +receives, through eight named filters each port's runner implements in its own language. +It does not pin the text of a generated file, the uncovered warning, or a requirement name +outside ASCII. The case list is in the +[corpus README](../fixtures/requirement-test-identity-conformance/README.md). + +**All five ports run it:** +`server/typescript/packages/codegen-ts/test/requirement-test-identity-conformance.test.ts`, +`server/java/metadata/src/test/java/com/metaobjects/conformance/RequirementTestIdentityConformanceTest.java`, +`server/python/tests/conformance/test_requirement_test_identity_conformance.py` and +`server/csharp/MetaObjects.Conformance.Tests/RequirementTestIdentityConformanceTests.cs`. +Kotlin's identity function is the Java one; the names its generator emits are asserted by +`server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/KotlinRequirementTestsGeneratorTest.kt`. + ## Orphaned fixtures (tested but not yet documented) The fixtures in the nine corpora mapped above (metamodel 364 + yaml 16 + verify 31 + render 15 + persistence 39 + api-contract 78 + source-resolution 25 + scope 10 + -dependency 23) each map to a feature doc. None are orphaned today. The remaining +dependency 23) each map to a feature doc, and so do the two requirement corpora, whose +case lists live in their own READMEs. None are orphaned today. The remaining corpora in the totals table gate tooling contracts (registry manifests, provider composition, agent context, docs emit) rather than user-facing metamodel behaviour, so they have no feature-doc row. diff --git a/docs/features/cli.md b/docs/features/cli.md index 81d715d8a..626b935bd 100644 --- a/docs/features/cli.md +++ b/docs/features/cli.md @@ -146,6 +146,27 @@ Rules of the contract: The codes and message text are identical in every port, gated by [`fixtures/field-lint-conformance/`](../../fixtures/field-lint-conformance/README.md). +- **The requirement gate runs on every `verify`, in every port** + ([ADR-0057](../../spec/decisions/ADR-0057-requirement-checks-and-tests-in-every-port.md)). + It is not a subverb and nothing mutes it. A model with no `requirement.*` node gets no + line and no change to the exit code. A model with one gets the ledger summary on every + run, and an error (a link above the floor, a dangling reference on a `live` or `partial` + requirement, a live policy applied to nothing) exits non-zero. Unlike the lints above, + this one can fail the build. One of its warnings can be raised to an error, per port: + + | CLI | Flag | Environment | + |---|---|---| + | Node `meta verify` | `--require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + | `dotnet meta verify` | `--require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + | `mvn metaobjects:verify` | `-Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` | + | `metaobjects verify` | `--require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + + The switch reports `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a live functional requirement + that nothing implements) at severity `error`, under the same code, and changes no other + finding. The codes, severities and message text are identical in every port, gated by + [`fixtures/requirement-check-conformance/`](../../fixtures/requirement-check-conformance/README.md). + The seven requirement *authoring-lint* advisories are still printed by the Node `meta` + only. See [requirements.md](requirements.md#the-gate-in-every-port). ### The prompt directory: `--prompts` everywhere (F101) @@ -319,9 +340,12 @@ by the other port's docs command. Alongside its flag-only mode (`metaobjects gen --out `), the Python `metaobjects` CLI supports a declarative project config, -`metaobjects.config.yaml` (#267). The **schema keys are identical to the TS -`metaobjects.config.ts` vocabulary** — a polyglot adopter learns one -targets-registry shape regardless of port. A JSON Schema ships at +`metaobjects.config.yaml` (#267). Its **`targets` registry uses the TS +`metaobjects.config.ts` vocabulary** (named targets, each with its own `outDir`) — a +polyglot adopter learns one targets-registry shape regardless of port. The keys are not +identical beyond that: a Python target carries its own `generators` and `entities`, and +`requirementTests` is a block that exists only here, where TypeScript passes that +generator's options to `requirementTests({ … })` in code. A JSON Schema ships at [`server/python/src/metaobjects/codegen/metaobjects-config.schema.json`](../../server/python/src/metaobjects/codegen/metaobjects-config.schema.json) for editor autocomplete and non-Python validation. @@ -330,6 +354,12 @@ metadata: metaobjects # optional, default "metaobjects" — relative providers: # optional; "module:symbol" refs, resolved config-relative (no PYTHONPATH=) - my_project.providers:register_custom_types libraries: [ai] # optional; MetaObjects-shipped library packages (see below) +requirementTests: # optional; options of the `requirement-tests` generator, every key optional + witnessModule: tests.requirement_witnesses # the default + grain: concern # or: member + filter: codegen.requirement_hooks:include # "module:symbol", resolved config-relative + renderer: codegen.requirement_hooks:render # "module:symbol", resolved config-relative + warnUncovered: true # the default targets: api: outDir: src/generated/api diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index e9214ba20..e6340f4c1 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -289,7 +289,7 @@ generator code. The 20-line programmatic shape for each port is in | Port | Invocation | Programmatic — write a `Generator` | Declarative — template + scope | |---|---|---|---| -| **TypeScript** | `meta init` → `meta gen --list --probe` → `meta eject ` → `meta gen` (Bun/Node CLI) | **Yes.** `meta generator new ` writes a working generator of your own into `codegen/generators/` and wires it — the first move for an output nothing ships. To start from a reference instead: `meta init` scaffolds the LAYOUT and an empty selection (ADR-0034 Amendment 2); `meta eject ...` copies each generator you choose into `codegen/generators/*.ts` and prints the import to add to `metaobjects.config.ts`. Edit them freely. The prompt tier (`prompt-render`, `output-parser`, `extractor`, `output-prompt`, `render-helper`) ejects like the rest: you own which templates get a module and where it lands, while the render and extract engines those modules call stay in the package. Not every registered generator is ejectable: `callable`, `trace-helper`, `requirement-tests`, the `template` primitive, the docs tier (`docs`, `api-docs`, `mermaid-er`, whose door is `meta docs`) and `shared-model` ship no reference template, so they are **package-only** — `meta gen --list` marks them so and `meta eject` names them as such. `shared-model` (FR-023's publisher generator) stays package-only deliberately, since it emits a hash-pinned cross-port contract artifact. | **Yes** — `templateGenerator({ template, scope, outputPattern })` in the config's `generators: [...]`. No CLI flag: the config already takes generator values. | +| **TypeScript** | `meta init` → `meta gen --list --probe` → `meta eject ` → `meta gen` (Bun/Node CLI) | **Yes.** `meta generator new ` writes a working generator of your own into `codegen/generators/` and wires it — the first move for an output nothing ships. To start from a reference instead: `meta init` scaffolds the LAYOUT and an empty selection (ADR-0034 Amendment 2); `meta eject ...` copies each generator you choose into `codegen/generators/*.ts` and prints the import to add to `metaobjects.config.ts`. Edit them freely. The prompt tier (`prompt-render`, `output-parser`, `extractor`, `output-prompt`, `render-helper`) ejects like the rest: you own which templates get a module and where it lands, while the render and extract engines those modules call stay in the package. `requirement-tests` ejects as one file holding the generator and its default stub renderer, so the stub text is yours to change; the requirement walk, each test's identity and the claim digest stay in the package. Not every registered generator is ejectable: `callable`, `trace-helper`, the `template` primitive, the docs tier (`docs`, `api-docs`, `mermaid-er`, whose door is `meta docs`) and `shared-model` ship no reference template, so they are **package-only** — `meta gen --list` marks them so and `meta eject` names them as such. `shared-model` (FR-023's publisher generator) stays package-only deliberately, since it emits a hash-pinned cross-port contract artifact. | **Yes** — `templateGenerator({ template, scope, outputPattern })` in the config's `generators: [...]`. No CLI flag: the config already takes generator values. | | **Java / Kotlin** | `mvn metaobjects:generate` / `mvn metaobjects:verify` (`metaobjects-maven-plugin`) | **Yes.** Extend `FileEmittingGenerator` and read the model through `ModelWalk` (both in `metaobjects-codegen-base`) for one of your own. Every generator — built-in or your own — is named in `` and loaded from the project classpath: one seam, not two. There is no default suite, so `` is the complete list. Kotlin runs through the same goal. | **Yes** — `TemplateScopeGenerator` wired as an ordinary ``. No CLI flag: `` is already the seam. | | **C#** | `dotnet meta gen` / `dotnet meta verify` (.NET tool) | **Yes.** Implement `IGenerator` in the owned console project `codegen/` and list it in `codegen/Program.cs`; `dotnet meta gen` / `verify --codegen` hand off to that project whenever `codegen/Codegen.csproj` exists. `dotnet meta eject ` scaffolds the project, or write its two files by hand. An owned generator the `--generators` selection does not name still runs. | **Yes** — `dotnet meta gen --template-spec --template-root `. | | **Python** | `metaobjects gen` / `metaobjects verify` (console-script) | **Yes.** Name your generator as `module:symbol` in `--generators` or a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one. Read the model through `metaobjects.codegen.model_walk`. (`--provider module:symbol` registers **metamodel vocabulary**, not a generator.) | **Yes** — `metaobjects gen --template-spec --templates `. | @@ -442,6 +442,12 @@ can see when an upgrade changed the generator you copied. A copy imports the sam `metaobjects.codegen.*` modules the packaged one does, and those module paths are the surface an owned generator builds on. +`requirement-tests` ejects the same way, to `codegen/generators/requirement_tests.py`, +wired as `codegen.generators.requirement_tests:requirement_tests_generator`. The copy holds +the generator and its default renderer and keeps reading the `requirementTests` config +block; the requirement walk, the test identities and the digest stay in the package. See +[Generated requirement tests and witnesses](requirements.md#generated-requirement-tests-and-witnesses). + ### The runtime your generated code imports comes with it Owning a generator only helps if you also own the helper code its **output** calls. @@ -522,6 +528,14 @@ that runs `metaobjects:generate`, and the new `` for each `.test.ts`). So a name is an identifier, and two -habits break it: +generated TypeScript test stub (`requirements/.test.ts`) and part of the name of its +generated test in every other port. So a name is an identifier, and two habits break it: - **A `.` in the name** makes it indistinguishable from nesting. A single node named `Orders.Recorded` and a node `Orders` containing a node `Recorded` produce the *identical* @@ -160,7 +161,7 @@ cleanly. `verify` warns about them. ## Two kinds, opposite checks -| | check | what `meta verify` does | +| | check | what `verify` does | |---|---|---| | `requirement.functional` (levelled) | **existence** | **warns** when nothing in a live/partial node's subtree implements it; **fails** when a node it names no longer exists | | `requirement.architectural` (flat by default) | **universality** | **fails** a live/partial policy applied to nothing, or one naming a node that no longer exists; it does not check that each claimed node complies | @@ -201,6 +202,8 @@ branch of their own. ## What `meta verify` checks Requirements are metadata, so they are checked on **every** `meta verify` — no subverb. +The same gate runs in the other ports' `verify`; [The gate in every port](#the-gate-in-every-port) +gives each command and the full list of codes. The rule worth knowing before you read a failure: **a dangling `@implementedBy` is an error on `live`/`partial` and allowed on `planned`.** On `planned` the nodes do not exist *yet* — @@ -275,7 +278,7 @@ your agents never read it, restoring the entry buys you a coin flip. Point at it ``` meta verify — requirements: 235 entries (226 functional, 9 architectural) — - 173 live, 62 partial; 55/55 entities claimed. + 173 live, 62 partial; 55/55 entities claimed, counted over 14 metadata file(s). meta verify — requirements: 62 recorded gap(s) with no @disposition. ``` @@ -284,7 +287,8 @@ nothing — and a ledger that skipped an entire grain reads exactly like a compl ### The authoring lint -Alongside the gate, `verify` runs an **authoring lint** and prints it under its own heading: +Alongside the gate, `meta verify` runs an **authoring lint** and prints it under its own +heading. The lint is TypeScript-only: the other ports' `verify` run the gate and not the lint. ``` meta verify — requirements: 6 authoring warning(s) (advisory — does not fail the build): @@ -327,6 +331,105 @@ with, and a gate people argue with is a gate people mute. And it never asks whet statement is *true*, a description *useful*, or a counterexample *sufficient*; those are the judgements the ledger exists to record, and no check reaches them. +## The gate in every port + +The gate is the same in all five ports. Each port's `verify` runs it on every run, with no +subverb to ask for it, from the port's own core library +([ADR-0057](../../spec/decisions/ADR-0057-requirement-checks-and-tests-in-every-port.md)). +A shared corpus, +[`fixtures/requirement-check-conformance/`](../../fixtures/requirement-check-conformance/README.md), +holds every port to the TypeScript reference: the code, severity, requirement path and +message text of each diagnostic, and the summary counts. + +| Port | Command | Its lines start | +|---|---|---| +| TypeScript | `meta verify` | `meta verify — requirements:` | +| Python | `metaobjects verify` | `metaobjects verify — requirements:` | +| Java, Kotlin | `mvn metaobjects:verify` (one goal for both, in either mode) | `metaobjects:verify — requirements:` | +| C# | `dotnet meta verify ./metadata --templates ./prompts` | `dotnet meta verify — requirements:` | + +The C# tool has to be given one of its drift gates (`--templates `, or `--codegen +--out `); the requirement gate then runs beside whichever one it was given. Maven logs +the summary at `info`, warnings as warnings and errors as errors. + +**What it reports**, checked per requirement in this order: + +| Code | Severity | Fires when | +|---|---|---| +| `ERR_REQUIREMENT_BAD_LEVEL` | error | The level is outside 1 to 5, on a functional requirement or a levelled architectural one. | +| `ERR_REQUIREMENT_LEVEL_NESTING` | error | A levelled requirement sits at or above the level of the requirement it is nested under. | +| `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. | +| `ERR_REQUIREMENT_L4_NOT_OBJECT` | error | A functional L4 names a member. | +| `ERR_REQUIREMENT_L5_NOT_MEMBER` | error | A functional L5 names an object. | +| `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. | +| `ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS` | error | A `live` or `partial` architectural requirement that may name the model (a flat policy, or one at L4 or L5) names nothing. | +| `WARN_REQUIREMENT_DISPOSITION_NOT_APPLICABLE` | warn | `@disposition` on a status other than `planned` or `partial`. | +| `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` | warn | A `live` or `partial` functional requirement where neither it nor anything nested under it names a node. An error under [the strict switch](#requiring-implementers). | +| `WARN_REQUIREMENT_DEFERRED_UNTRACKED` | warn | `@disposition: deferred` with no `@trackedBy`. | +| `WARN_REQUIREMENT_OBJECT_UNCLAIMED` | warn | Coverage is measured and no requirement claims a concrete entity. The line has no requirement path, because its subject is the entity. | + +A diagnostic prints as ` []: `. + +**What counts as claimed.** A requirement that is not `planned` contributes its claims, and +each reference that resolves adds the object it names (a member reference adds the member's +owner). An **architectural** claim on a base also covers every object whose `extends` chain +reaches that base; a functional claim does not spread. What is counted is concrete +`object.entity` nodes: abstract entities, `object.value` and `object.projection` are not. + +**When coverage is measured.** Only when the project authored at least one requirement of +its own. A project whose only requirements came from a shipped library, with or without an +overlay on one of them, is not measured, and the summary says so instead of printing a +ratio. + +**The summary**, one line whenever the model declares a requirement: + +``` + entries ( functional, architectural) — planned, live, + partial, retired; / entities claimed, counted over metadata file(s). +``` + +A status with no entry is left out. When coverage is not measured the line ends +`coverage: not measured (no project-authored requirements).` TypeScript and Python end it +`, from dependencies.` when the project declares metadata dependencies. A second line +counts the recorded gaps with no `@disposition`, when there are any. Then come the +diagnostics, and when any is an error, ` error(s).` and a non-zero exit. +TypeScript caps the warnings it prints at `--limit` (20 by default); the other ports print +every one. The structured `--format json|toon` payload is TypeScript-only. + +**Metadata that does not load** is not a model with no requirement: `verify` fails on it in +every port. The load errors are printed once. Where the drift gate that ran loaded the model, +that gate prints them; where none did (`metaobjects verify` with no generators selected, +`dotnet meta verify --codegen` handed off to an owned `codegen/` project), the requirement +gate prints them itself. Its load is strict unless `--lax`, as the drift gates' is. + +### Known differences between ports + +- The package a requirement is taken to be in can differ between ports for two multi-file + shapes: a root document with no package loaded beside packaged ones, and a child merged + from a package-less document into a requirement that declares its own package. +- Python applies only the dependency-import rule to decide which of a project's own + entities are counted; TypeScript also applies the project's `scope` patterns. +- A `level` that is not an integer (`4.5`) is refused at load by the Python, Java and C# + loaders (`ERR_BAD_ATTR_VALUE`). The TypeScript loader lets it through, and the gate reports + it as `ERR_REQUIREMENT_BAD_LEVEL`. + +## Requiring implementers + +`WARN_REQUIREMENT_NOTHING_IMPLEMENTS` is a warning because a young ledger usually claims +more than it links. A project whose ledger has caught up can make it fail the build: + +| Port | Flag | Environment | +|---|---|---| +| TypeScript | `meta verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Python | `metaobjects verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Java, Kotlin | `mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` | +| C# | `dotnet meta verify ./metadata --templates ./prompts --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + +The switch raises that one finding to an error and keeps its code, so a report or a +suppression keyed on the code still matches. No other warning changes severity. It is not +called `--strict`, because `verify` already has `--lax` on a different axis and a `--strict` +beside it would read as that flag's opposite. + ## Recording gaps: `partial` is a feature, not a failure `partial` is the most valuable status in the enum, because it is the only one that says @@ -387,11 +490,239 @@ nothing. Pair it with `@trackedBy` to link the ticket it will be built under. -### What a green run does not prove +## Generated requirement tests and witnesses + +**This is a recommended approach, not a contract.** The `requirement-tests` generator, its +default renderer and the witness model described here are a reference helper +([ADR-0034 Amendment 3](../../spec/decisions/ADR-0034-codegen-scaffold-and-own.md#amendment-3-2026-09-22--generators-are-reference-helpers-the-core-is-what-metaobjects-guarantees)). +Every port ships one and every port can eject it, and an application may change its copy or +replace it. The checks in `verify` are the part MetaObjects guarantees. They are not +ejectable in any port, because they are the contract the ports share. + +The generator writes one test per requirement its filter selects. By default that is every +functional requirement at L4 or L5. A `live` or `partial` requirement gets a test that +fails until the project supplies the proof; a `planned` or `retired` one gets a skipped +test. + +Each test has an identity, and the identity is the same in all five ports: + +| Part | What it is | +|---|---| +| id | `:: []`, or ` []` when the requirement has no package. | +| unit | By default (`grain: concern`) one test per distinct `.` the requirement's resolved references name. Under `grain: member`, one test per distinct reference that resolves. A requirement that resolves no reference gets one test, with unit `*`. | +| witness key | An identifier-safe spelling of the id: `req_`, the package and path, then `__` and the unit unless the unit is `*`, with every run of characters outside `A-Z`, `a-z` and `0-9` written as one `_`. `acme::shop::Orders.Recorded [object.entity]` gives `req_acme_shop_Orders_Recorded__object_entity`. | +| digest | A SHA-256 over the requirement's subtype, level, status, statement, counterexample and `@implementedBy` list. It answers "did the claim change", so a title, a note or a disposition does not move it. | + +[`fixtures/requirement-test-identity-conformance/`](../../fixtures/requirement-test-identity-conformance/README.md) +pins those identities in every port. It does not pin the text of a generated file, which is +each port's own language and the application's to change. TypeScript's default stub keeps +the test name it always had, ` []`, with no package; there the identity is +what a renderer receives. + +### What each port writes + +| Port | Framework | Files | The project supplies | +|---|---|---|---| +| TypeScript | `bun:test` in the default stub | One stub per test: `requirements/..test.ts`, or `requirements/.test.ts` for a requirement that resolves no reference. | The assertion, written into the stub. The three-way merge keeps it. | +| Python | pytest | `requirements/test__requirements.py`, one per metamodel package. | A function named by the witness key, in the witness module (`tests.requirement_witnesses` by default). | +| Java | JUnit Jupiter | `Requirements__Witnesses.java` and `Requirements__Test.java`, in `testPackage`. | The class `witnessClass` names, implementing each generated interface. | +| Kotlin | JUnit Jupiter | `Requirements__Witnesses.kt` and `Requirements__Test.kt`, in `testPackage`. | As Java. | +| C# | xUnit | `Requirements__Witnesses.g.cs` and `Requirements__Tests.g.cs`. | The class `WitnessClass` names, implementing each generated interface. | + +`` is the metamodel package with each run of non-alphanumeric characters as one +`_` (`acme::shop` gives `acme_shop`), or `root` for a requirement with no package. + +Generated Java and Kotlin tests are JUnit Jupiter only. A JUnit 4 project uses the renderer +hook or ejects the generator. Generated tests import their test framework (and, in Python, +`importlib`) and nothing from MetaObjects. + +### Witnesses + +TypeScript keeps the model it had: you fill in the stub, and regeneration preserves what +you wrote. In the other four ports the generated file is machine-owned and rewritten whole +on every run, so your code goes somewhere else: in a **witness**, a function or method you +own that the generated test calls. A `live` or `partial` requirement with no witness is a +failing test that names what to write: + +``` +unimplemented requirement: acme::shop::Orders.Recorded [object.entity] - write + tests.requirement_witnesses.req_acme_shop_Orders_Recorded__object_entity() so that it + fails when: A placed order has no row. +``` + +**Python.** Create the witness module and add a function per test. There is nothing else to +set up: a missing module or function is "no witness", and the test fails with the message +above. A witness module that exists and fails to import raises its own error. + +```python +# tests/requirement_witnesses.py +def req_acme_shop_Orders_Recorded__object_entity(): + order = place_order() + assert find_order_row(order.id) is not None +``` + +**Java and Kotlin.** The generator writes an interface with one member per test that is not +skipped, each with a default body that fails. You write one class, with a public +no-argument constructor, that implements every generated interface. This is a one-time +setup with a cost: the generated tests construct that class with `new`, so the test module +does not compile until it exists. + +```java +public class Witnesses implements Requirements_acme_shop_Witnesses { + @Override + public void req_acme_shop_Orders_Recorded__object_entity() { + Order order = placeOrder(); + assertNotNull(findOrderRow(order.id())); + } +} +``` + +```kotlin +class Witnesses : Requirements_acme_shop_Witnesses { + override fun req_acme_shop_Orders_Recorded__object_entity() { + val order = placeOrder() + assertNotNull(findOrderRow(order.id)) + } +} +``` + +**C#.** The same shape: an interface with default members, and one class of yours, with a +public parameterless constructor, that the generated tests construct. + +```csharp +using Acme.Shop.Requirements; +using Xunit; + +namespace Acme.Shop; + +public class RequirementWitnesses : Requirements_acme_shop_Witnesses +{ + void Requirements_acme_shop_Witnesses.req_acme_shop_Orders_Recorded__object_entity() + { + var order = PlaceOrder(); + Assert.NotNull(FindOrderRow(order.Id)); + } +} +``` + +In the three compiled ports a requirement that becomes `live` adds an interface member with +a failing default, which is a red test and not a compile break. A requirement that is +retired or deleted removes its member, and whether a witness left behind then stops +compiling depends on how it was written: + +- **Kotlin:** always. `override` is mandatory. +- **Java:** only when the method carries `@Override`. Without it a stale method is an + ordinary method and compiles. +- **C#:** only when the member is implemented explicitly, as above. A `public void req_…()` + implements it implicitly, and a stale one keeps compiling. +- **Python:** never. There is no compile step, and a witness whose requirement is gone is + simply not called. + +The examples use the form that signals. A witness written in the other form goes stale +silently when its requirement is retired or deleted. + +Two cautions. In C#, a method that does not actually implement the interface member (an +implicit one that is not `public`, or one whose name is off by a character) leaves the +failing default in place, so the test reports `unimplemented requirement` for a witness you +believe you wrote: check the signature against the generated interface. In Kotlin, write +the witness class in Kotlin. A witness class written in Java is untested: unless your +Kotlin compiler is set to emit JVM default methods for interface members, Java sees every +member as abstract, and each new live requirement becomes a compile break instead of a red +test. + +### Choosing which requirements get a test + +Every port offers the same seams. Only the spelling differs. + +| Seam | TypeScript | Python | Java, Kotlin | C# | +|---|---|---|---|---| +| Grain: `concern` (default) or `member` | `grain` | `grain` | `` | `Grain` | +| Filter: a predicate that **replaces** the default | `filter` | `filter`, a `module:symbol` | ``, a class implementing `RequirementTestFilter` | `Filter`, an `IRequirementTestFilter` | +| The uncovered warning, on by default | `warnUncovered: false` | `warnUncovered: false` | `false` | `WarnUncovered = false` | +| Renderer: replaces the text of one test | `renderers`, `resolveRenderer` | `renderer`, a `module:symbol` | ``, a class implementing `RequirementTestRenderer` | `Renderer`, an `IRequirementTestRenderer` | +| Where the witnesses are | not applicable | `witnessModule` | ``, `` | `TestNamespace`, `WitnessClass` | + +Where they are set: in TypeScript, the options of `requirementTests({ … })` in +`metaobjects.config.ts`. In Python, the `requirementTests` block of +`metaobjects.config.yaml` (the flag-only `metaobjects gen --out ` mode runs the +generator with its defaults). In Java and Kotlin, the `` of the `` entry. +In C#, public properties set where the generator is constructed, in the owned +`codegen/Program.cs`; the packaged `dotnet meta gen --generators requirement-tests` runs +with the defaults. The per-port pages have a worked configuration each: +[TypeScript](../ports/typescript.md#requirement-tests), +[Python](../ports/python.md#requirement-tests), +[Java](../ports/java.md#requirement-tests--junitrequirementtestsgenerator), +[Kotlin](../ports/kotlin.md), [C#](../ports/csharp.md#requirement-tests). + +**The filter sees one view of a requirement, never the node:** `subType`, `level` (absent +when the requirement declares none, which is not the same as zero), `status`, `path`, +`package` and `implementedByTypes` (the distinct `.` of the references that +resolve). Python spells two of them `sub_type` and `implemented_by_types`; Java and Kotlin +read them through accessors and spell the package `pkg()`; C# capitalises them and types +`Level` as `int?`. + +**The uncovered warning** names the requirements the filter left out, so "no test here" is a +visible choice. Its text is the same in every port, and it names requirement paths: + +``` +3 requirement(s) matched no filter and get no test. If that is deliberate, set + warnUncovered: false to silence this. Uncovered: Shop, Shop.Orders, Shop.Billing. +``` + +It lists at most five paths and then `, and more`. The switch is spelled the way that +port spells it, and TypeScript says `stub` where the others say `test`. + +Three refusals. In every port, a grain other than `concern` or `member` is an error, never +a hybrid run. Outside TypeScript, two tests that would share one witness key refuse to +generate (`ERR_REQUIREMENT_WITNESS_KEY_COLLISION`, naming both ids). And in Python, Java +and Kotlin, where a renderer or filter is given by name, one that exists but fails to load +is reported with its real cause, not as missing. TypeScript and C# take the function or +object itself, so there is nothing to look up. + +### When a claim changes, and when a package empties + +Outside TypeScript each generated test carries its digest in a comment. Edit a statement, a +counterexample, a status, a level or an `@implementedBy` list and the committed file no +longer matches a fresh run, so the port's codegen drift gate (`metaobjects verify`, +`mvn metaobjects:verify`, `dotnet meta verify --codegen`) fails until you regenerate. That +is the prompt to re-read the witness against the new claim. + +**A known limit.** In Python, Java, Kotlin and C#, a package that loses its last +requirement leaves its generated test file behind. `gen` does not remove it. The port's +codegen drift gate reports it, and you delete it by hand. TypeScript's generator reconciles +these itself: it removes the stub of a requirement that is gone, and refuses, by name, one +that carries a hand edit. + +### Owning the generator + +| Port | Command | The copy | +|---|---|---| +| TypeScript | `meta eject requirement-tests` | `codegen/generators/requirement-tests.ts` | +| Python | `metaobjects eject requirement-tests` | `codegen/generators/requirement_tests.py` | +| Java | `mvn metaobjects:eject -Dnames=requirement-tests -Dport=java` | `JUnitRequirementTestsGenerator.java`, in a `codegen/` Maven module under your package | +| Kotlin | `mvn metaobjects:eject -Dnames=requirement-tests -Dport=kotlin` | `KotlinRequirementTestsGenerator.kt`, likewise | +| C# | `dotnet meta eject requirement-tests` | `codegen/generators/RequirementTestsGenerator.cs` | + +The copy holds the generator and its default renderer, so the text of a generated test is +yours to change. What stays in the package is what the ports agree on: the walk over the +ledger, the claim resolver, the identity function, the digest and the hook types. An owned +generator imports those like any other code. The Maven goal needs `-Dport` because the name +is ejectable on both JVM ports; it infers the port only when your project declares a +dependency on exactly one of `metaobjects-codegen-spring` and `metaobjects-codegen-kotlin`. +Each command prints what to wire. See [Own your codegen](own-your-codegen.md). + +## What a green run does not prove It proves **referential integrity**. It cannot prove a status is *true*, or that a node genuinely implements the requirement claiming it — no test can. +The generated tests do not change that. `verify` never reads test results: it checks the +ledger, the drift gate checks that the generated tests match it, and your own test run +supplies "passing". A generated test that passes proves the witness ran, not that the +witness tests the claim. And a witness written without the compile signal (a Java method +with no `@Override`, a C# member implemented implicitly, any Python function) goes stale +silently when its requirement is retired or deleted. + Coverage is also narrower than it sounds: entity grain only. `object.value` and `object.projection` are exempt, and fields, views, validators and identities are never required to be claimed. Green means "every entity is claimed by something", not "every node diff --git a/docs/llms/llms-full.txt b/docs/llms/llms-full.txt index 813e1b6d6..e0a313368 100644 --- a/docs/llms/llms-full.txt +++ b/docs/llms/llms-full.txt @@ -12,13 +12,13 @@ **One typed model of your app** — data, API, UI, prompt payloads, and what it's supposed to do — that your agent reads and writes. Two things happen to it. **Generate:** the boring parts are derived from it, in TypeScript, Java, Kotlin, C# and Python — at build time by reference generators you copy into your repo and own, or at runtime from the live model. **Verify:** the build fails when generated code drifts from the model and when a prompt's payload no longer matches what it's told — and it fails or warns when a feature someone marked done has nothing implementing it. That last one has no equivalent in a test suite: a test exercises code that exists, so nothing flags a claimed capability that was never built. It checks that the claim points at something real, not that the something is correct. It protects what the model declares; your hand-written logic is still yours. -Underneath those two verbs, the model is a cross-language metadata standard for declaring typed entity models. From a single metadata definition, MetaObjects drives six capabilities. The first four **ship in all five ports** (TypeScript, Java, Kotlin, C#, Python), though not uniformly deep (field ranking: drift > codegen > prompts > runtime metadata); the fifth loads in every port, runs its `meta verify` checks in the Node `meta` CLI and scaffolds tests in TypeScript only; the sixth ships two libraries at their own stability labels: +Underneath those two verbs, the model is a cross-language metadata standard for declaring typed entity models. From a single metadata definition, MetaObjects drives six capabilities. The first four **ship in all five ports** (TypeScript, Java, Kotlin, C#, Python), though not uniformly deep (field ranking: drift > codegen > prompts > runtime metadata); the fifth ships its vocabulary, `verify` checks and a `requirement-tests` generator in every port, with its authoring lint in TypeScript only; the sixth ships two libraries at their own stability labels: 1. **Code generation** -- idiomatic per-language code generated from the same metadata model. 2. **Runtime metadata access** -- load the metadata at runtime to drive dynamic behavior: CRUD operations, validation, relationships, dynamic admin UIs. (Typed tool payloads are declared today; MCP exposure of tools is on the roadmap.) 3. **Drift detection** -- catch divergence between code and metadata before it ships, surfacing drift as build-time breakage. 4. **Prompt construction** -- treat LLM prompts as governed metadata: a typed payload (a projection), external provider-resolved prompt text, byte-identical cross-language render, and build-time prompt-to-payload drift detection. -5. **Requirements and testing** -- declare what the software is supposed to *do* as a node in the same model, so `implementedBy` is resolved rather than trusted, `meta verify` reports the ledger on every run, and `requirementTests()` scaffolds a test stub per claim (TypeScript only). +5. **Requirements and testing** -- declare what the software is supposed to *do* as a node in the same model, so `implementedBy` is resolved rather than trusted, `verify` reports the ledger on every run in every port, and each port's `requirement-tests` generator scaffolds a test per claim. 6. **Libraries** -- reusable declared design opted into by name (`"libraries": ["iam"]`): entities, requirements and implied generators as one drift-gated artifact. Since 1.0.4, in all five ports; `ai` is `stable`, `iam` is `preview`. The metamodel is the **durable spine**; generated code is the **disposable artifact**. Substrate is local-first: typed metadata lives in your repo, and the generated code is idiomatic per-language output with **no proprietary runtime** — the entity/model tier is dependency-free, and the optional client, prompt-render, and runtime tiers are ordinary Apache-2.0 packages you could vendor or fork. If the `@metaobjectsdev/*` (npm) or `com.metaobjects:*` (Maven) packages disappeared tomorrow, your generated code keeps working in every language. @@ -47,7 +47,7 @@ This file is the short corpus; the deep, version-matched how-to is the scaffolde ## The six pillars -The pillars are views on the same metadata, not separate products, and each carries a maturity label rather than a blanket "ships today". The first four ship per language in all five ports, conformance-gated so behavior is byte-identical across ports — though they are not uniformly deep. In field materialization the ranking is drift > codegen > prompts > runtime metadata (the youngest of the four); see the capability matrix in the README for per-port coverage. The fifth, requirements and testing, loads and validates in every port, runs its `meta verify` checks in the Node `meta` CLI and scaffolds tests in TypeScript only, and has been dogfooded on maintainer-owned projects only — the split is stated below because it is easy to overclaim. The sixth, libraries, shipped in 1.0.4: `ai` is `stable` and `iam` is `preview`. +The pillars are views on the same metadata, not separate products, and each carries a maturity label rather than a blanket "ships today". The first four ship per language in all five ports, conformance-gated so behavior is byte-identical across ports — though they are not uniformly deep. In field materialization the ranking is drift > codegen > prompts > runtime metadata (the youngest of the four); see the capability matrix in the README for per-port coverage. The fifth, requirements and testing, loads, validates, runs its `verify` checks and scaffolds tests in every port, keeps its authoring lint in TypeScript only, and has been dogfooded on maintainer-owned projects only — the split is stated below because it is easy to overclaim. The sixth, libraries, shipped in 1.0.4: `ai` is `stable` and `iam` is `preview`. ### Codegen @@ -97,13 +97,13 @@ Because the render is conformance-gated, the determinism guarantee holds in ever The other four pillars keep the code honest about the *model*. This one asks a question none of them can: is the thing you said the software does actually built? A test exercises code that exists; there is no test that fails because a column nobody writes was never wired to anything. The absence has no address — so give it one. -A capability is declared as a node in the same model as the entities, not in a side document and not in a tool where the link to the code is a string. `requirement.functional` (existence — `meta verify` warns when nothing implements it) and `requirement.architectural` (universality — `meta verify` fails a live policy applied to nothing) are registered vocabulary in all five ports, with the loader enforcing the closed `@status` enum so a typo fails the load in every language rather than passing silently in four of them. The `meta verify` checks themselves run in the Node `meta` CLI; the other ports load and validate. Hierarchy is nesting, so regrouping moves a subtree. +A capability is declared as a node in the same model as the entities, not in a side document and not in a tool where the link to the code is a string. `requirement.functional` (existence — `meta verify` warns when nothing implements it) and `requirement.architectural` (universality — `meta verify` fails a live policy applied to nothing) are registered vocabulary in all five ports, with the loader enforcing the closed `@status` enum so a typo fails the load in every language rather than passing silently in four of them. The same checks run in every port's `verify` — `meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, `dotnet meta verify` — held to one shared corpus. Hierarchy is nesting, so regrouping moves a subtree. `@implementedBy` is **resolved, not trusted**: it names a real member of the real model, so a claim whose implementation was renamed or deleted fails the build instead of going quietly stale. `meta verify` reports the ledger on **every** run — a gate that says nothing when it passes cannot be told apart from a gate that checked nothing — covering unresolved links, entities no claim covers, and gaps recorded versus gaps nobody has ruled on, plus an authoring lint whose findings are warnings that can never fail a build. `meta docs` renders the ledger for humans and for agents. -**The port split, stated exactly:** the vocabulary is cross-port; the `meta verify` checks run in the Node `meta` CLI (the other ports' CLIs do not run them). A live claim naming nothing is a warning, and a green run proves referential integrity, never that a status is true or an implementation correct. **`requirementTests()` — which scaffolds a test stub per claim, carrying the statement and counterexample in, kept from rotting by `verify --codegen` — is TypeScript-only.** There is no five-language test generation. +**The port split, stated exactly:** the vocabulary is cross-port, and so is the gate: every port's `verify` runs the same requirement checks. The seven authoring-lint advisories are printed by the Node `meta` CLI only. A live claim naming nothing is a warning (an error under `--require-implementers`), and a green run proves referential integrity, never that a status is true or an implementation correct. **Every port ships a `requirement-tests` generator** — a test per claim, carrying the statement and counterexample in, kept from rotting by the port's codegen drift gate. It is a reference helper each port can eject, not a contract: TypeScript emits a stub you fill in, and Python (pytest), Java and Kotlin (JUnit Jupiter) and C# (xUnit) emit a machine-owned test that calls a witness you own. -What MetaObjects does not do is write the assertion. A generated stub is a place to put a proof and a guarantee that it stays in step with the claim; it is not a proof. Entirely opt-in — a project that declares no `requirement.*` nodes sees no change at all. +What MetaObjects does not do is write the assertion. A generated test is a place to put a proof and a guarantee that it stays in step with the claim; it is not a proof. Entirely opt-in — a project that declares no `requirement.*` nodes sees no change at all. ### Libraries @@ -121,7 +121,7 @@ Today the TypeScript toolchain publishes and TypeScript and Python projects cons | Language | Status | Notes | |---|---|---| -| TypeScript | Reference implementation, npm `1.0.13` | All six pillars, and the only port that runs the requirements `meta verify` checks and publishes shared models. 2500+ tests passing. Owns the canonical schema-migration toolchain used by every port (ADR-0015). Bun-first dev. | +| TypeScript | Reference implementation, npm `1.0.13` | All six pillars, and the only port that prints the requirements authoring lint and publishes shared models. 2500+ tests passing. Owns the canonical schema-migration toolchain used by every port (ADR-0015). Bun-first dev. | | Java | Maven Central `8.0.13` | Spring REST + DTO + repository-interface codegen, OMDB runtime persistence (pure data-access) with Spring-tx. Fully green across all conformance corpora. | | Kotlin | Maven Central `8.0.13` | KotlinPoet codegen + Exposed runtime + `metadata-ktx` facade. Ships via the Java reactor. | | C# | NuGet `1.0.13` (.NET tool) | Loader + canonical serializer + EF Core + ASP.NET codegen + render/verify. `dotnet meta` tool. | @@ -322,7 +322,7 @@ $ meta migrate --dialect sqlite --slug add-subscriber --db file:./dev.db --apply $ meta export --out ./snapshot.json ``` -Capability requirements (`requirement.functional` / `requirement.architectural`) are registered vocabulary in all five ports: what the system is supposed to do, declared in `metaobjects/` beside the entities it describes and loaded by the same loader -- no side file, no bespoke parser. `functional` is checked for existence (a warning when nothing implements it); `architectural` for universality (an error when a live policy is applied to nothing — compliance of each claimed node is not checked). Hierarchy is nesting, `@status` is a loader-enforced closed enum (`planned | live | partial | retired`), and a dangling `@implementedBy` is an ERROR on `live`/`partial` but CORRECT on `planned` -- an intention has nothing to point at yet. A requirement states what SHOULD be true and is never a journal of what happened, and `retired` satisfies that rule by stating a prohibition in force rather than narrating history: a capability that was BUILT AND THEN DELIBERATELY REMOVED keeps its entry, its `@statement` written as the rule that it must not return. Do NOT delete it -- that entry is the feature's whole reason to exist: given a brief for a capability that had been retired, agents working from the model alone proposed rebuilding it 24 times out of 24, each believing they were reusing rather than reviving. On `retired`, `@implementedBy` is REFUSED at load (`ERR_REQUIREMENT_RETIRED_HAS_IMPLEMENTORS`) and `@supersededBy` names the requirement that replaced it -- it is RESOLVED like any other reference, so it must be a full dotted path and is legal on `retired` only (`ERR_REQUIREMENT_SUPERSEDED_BY_NOT_RETIRED` otherwise). `@verifiedBy` and the `abandoned`/`superseded` statuses were RETIRED in `0.24.0` and now fail the load (`ERR_UNKNOWN_ATTR` / `ERR_BAD_ATTR_VALUE`); `meta upgrade` rewrites what it can and refuses what needs a decision. Object coverage is a WARNING, never a failure. Entirely opt-in -- a model with no `requirement.*` nodes produces no diagnostics, and no codegen, migrate or runtime path reads the type. The `meta verify` gate is the Node CLI; the other ports load and validate. See docs/features/requirements.md. +Capability requirements (`requirement.functional` / `requirement.architectural`) are registered vocabulary in all five ports: what the system is supposed to do, declared in `metaobjects/` beside the entities it describes and loaded by the same loader -- no side file, no bespoke parser. `functional` is checked for existence (a warning when nothing implements it); `architectural` for universality (an error when a live policy is applied to nothing — compliance of each claimed node is not checked). Hierarchy is nesting, `@status` is a loader-enforced closed enum (`planned | live | partial | retired`), and a dangling `@implementedBy` is an ERROR on `live`/`partial` but CORRECT on `planned` -- an intention has nothing to point at yet. A requirement states what SHOULD be true and is never a journal of what happened, and `retired` satisfies that rule by stating a prohibition in force rather than narrating history: a capability that was BUILT AND THEN DELIBERATELY REMOVED keeps its entry, its `@statement` written as the rule that it must not return. Do NOT delete it -- that entry is the feature's whole reason to exist: given a brief for a capability that had been retired, agents working from the model alone proposed rebuilding it 24 times out of 24, each believing they were reusing rather than reviving. On `retired`, `@implementedBy` is REFUSED at load (`ERR_REQUIREMENT_RETIRED_HAS_IMPLEMENTORS`) and `@supersededBy` names the requirement that replaced it -- it is RESOLVED like any other reference, so it must be a full dotted path and is legal on `retired` only (`ERR_REQUIREMENT_SUPERSEDED_BY_NOT_RETIRED` otherwise). `@verifiedBy` and the `abandoned`/`superseded` statuses were RETIRED in `0.24.0` and now fail the load (`ERR_UNKNOWN_ATTR` / `ERR_BAD_ATTR_VALUE`); `meta upgrade` rewrites what it can and refuses what needs a decision. Object coverage is a WARNING, never a failure. Entirely opt-in -- a model with no `requirement.*` nodes produces no diagnostics, and no codegen, migrate or runtime path reads the type. Every port's `verify` runs the gate; the authoring lint is the Node CLI only. See docs/features/requirements.md. Non-TS ports run codegen + codegen-verify in their own build tool: `mvn metaobjects:generate` / `mvn metaobjects:verify` (Java/Kotlin), `dotnet meta gen` / `dotnet meta verify` (C#), `metaobjects gen` / `metaobjects verify` (Python). Schema migration is always the Node `meta` CLI. diff --git a/docs/llms/llms.txt b/docs/llms/llms.txt index 75c69d0da..e444d5432 100644 --- a/docs/llms/llms.txt +++ b/docs/llms/llms.txt @@ -45,14 +45,14 @@ This `llms.txt` is the short index; the deep, version-matched how-to is the scaf MetaObjects pillars are capabilities of the same metadata spine, not separate products. Each carries a maturity label — what ships, in which ports, at what stability: - **Codegen, Runtime metadata, Drift detection, Prompt construction** — shipped in all five ports (TS / Java / Kotlin / C# / Python), gated by the cross-port conformance corpora, though not uniformly deep: see the [capability matrix](https://github.com/metaobjectsdev/metaobjects#capability-matrix) for per-port coverage, and note the field ranking is drift > codegen > prompts > runtime metadata (the youngest of the four). -- **Requirements and testing** — vocabulary loads and validates in all five ports; the `meta verify` checks run in the Node `meta` CLI; `requirementTests()` scaffolding is TypeScript-only; dogfooded on maintainer-owned projects, no outside adopter yet. +- **Requirements and testing** — vocabulary, the `verify` checks and a `requirement-tests` generator in all five ports; the authoring lint is TypeScript-only; dogfooded on maintainer-owned projects, no outside adopter yet. - **Libraries** — since 1.0.4, in all five ports: `ai` is `stable`, `iam` is `preview` (the same labels `meta gen --list` prints). - **Codegen** — reference generators you own (ejectable in every port) that emit per-language code from a single metadata model. Drizzle + Zod + Fastify (TypeScript), Spring REST + DTO + repository interfaces (the persistence impl is consumer-supplied) (Java via `codegen-spring`), KotlinPoet + Exposed + Spring (Kotlin via `codegen-kotlin`), EF Core + ASP.NET (C#), Pydantic + FastAPI (Python). Hand-edit-preserving regeneration via three-way merge. Includes M:N relationship codegen (FR-018) in all five ports — entity navigation, idiomatic ORM wiring, and REST traversal (`GET //{id}/`). - **Runtime metadata** — load metadata at runtime and drive behavior dynamically: CRUD, validation, relationships, dynamic admin UIs. (Typed tool payloads are declared today; MCP exposure of tools is on the roadmap.) Kysely (TS), a DB-API 2 driver (pg8000 / psycopg) via ObjectManager (Python), modernized JDBC + Spring-tx via OMDB (Java), Exposed (Kotlin); C# runtime metadata is on the roadmap (its EF Core output is generated code, not a metadata-driven runtime). Runtime queries return native in-process types (ADR-0019); wire canonicalization happens only at the serialization boundary. - **Drift detection** — catch divergence between generated code and metadata before it ships. `verify` is one verb with explicit subverbs (ADR-0021): `verify --codegen` (regen-and-diff against committed output), `verify --templates` (prompt `{{field}}` ↔ payload-VO drift), and `verify --db` (live-DB schema drift, Node `meta` only). Surfaces drift as build-time breakage rather than a production incident. - **Prompt construction** — treat LLM prompts as governed metadata instead of strings scattered across services. A typed payload declared as a projection (so payload bloat and token cost are a diff, not a mystery), external provider-resolved prompt text, and a logic-less Mustache engine that renders deterministically: snapshot-testable in CI, byte-stable so an exact-prefix prompt-cache hit doesn't break on a stray whitespace, and drift-checked at build time so a renamed field can't silently degrade a prompt. Conformance-gated, so the guarantee holds in every language port. Render + payload-VO codegen + `verify` + `template.output` parser-on-receipt (FR-006) + the output-format prompt fragment & tolerant `extract` parser (FR-010/FR-011) ship in all five ports today. -- **Requirements and testing** — declare what the software is supposed to *do* in the same model as the entities, so a capability claim is checkable instead of prose. `requirement.functional` / `requirement.architectural` are registered vocabulary in all five ports, with the loader enforcing the closed `@status` enum, and `@implementedBy` is **resolved, not trusted**: it names a real member of the real model, so a claim whose implementation was renamed or deleted fails the build rather than going quietly stale. `meta verify` reports the ledger on every run — unresolved links, entities no claim covers, gaps recorded versus gaps nobody has ruled on — and lints the authoring; `meta docs` renders it for humans and for agents. A live claim naming nothing is a warning; a green run proves the references resolve, never that an implementation is correct. **`requirementTests()`, which scaffolds a test stub per claim carrying the statement and counterexample in, is TypeScript-only**; the vocabulary loads in every port, and the `meta verify` checks run in the Node `meta` CLI. A project that declares no `requirement.*` nodes sees no change at all. +- **Requirements and testing** — declare what the software is supposed to *do* in the same model as the entities, so a capability claim is checkable instead of prose. `requirement.functional` / `requirement.architectural` are registered vocabulary in all five ports, with the loader enforcing the closed `@status` enum, and `@implementedBy` is **resolved, not trusted**: it names a real member of the real model, so a claim whose implementation was renamed or deleted fails the build rather than going quietly stale. `meta verify` reports the ledger on every run — unresolved links, entities no claim covers, gaps recorded versus gaps nobody has ruled on — and lints the authoring; `meta docs` renders it for humans and for agents. A live claim naming nothing is a warning; a green run proves the references resolve, never that an implementation is correct. **Every port ships a `requirement-tests` generator**, which scaffolds a test per claim carrying the statement and counterexample in (a stub you fill in for TypeScript, a test that calls a witness you own in the other four); the vocabulary and the `verify` checks are in every port too, and only the authoring lint is TypeScript-only. A project that declares no `requirement.*` nodes sees no change at all. - **Libraries** — reusable declared design you opt into by name (`"libraries": ["iam"]` in `.metaobjects/config.json`): entities, the requirements they promise and the generators they imply, as one drift-gated artifact. A library is layered and its core layer is inert: it declares no source, so it adds no tables until you also opt into its `/db` layer (`["iam", "iam/db"]`). `meta eject ` copies one into your repo to own. `ai` (the LLM-call trace envelope) is `stable`; `iam` (users, nestable groups, roles, grants) is `preview`. **Sharing a model across your own projects** — today the TypeScript toolchain publishes and TypeScript and Python projects consume, over a `path` dependency (a sibling checkout or submodule); Java, Kotlin and C# arrive in Phase 2. One project publishes part of its model with the `sharedModelFile()` generator; another declares it under `dependencies` in `.metaobjects/config.json`, runs `meta deps sync` to commit a hash-locked snapshot, and builds on it with `extends` and references. When the publisher's model moves, `meta verify --deps` reports it instead of two copies drifting apart. @@ -84,7 +84,7 @@ MetaObjects deliberately does **not** ship one universal binary. Schema operatio - `meta init` — scaffold `metaobjects/`, `.metaobjects/`, and `metaobjects.config.ts`, **plus** the agent context: a slim `.metaobjects/AGENTS.md` + `CLAUDE.md` (auto-wired into the project's root `CLAUDE.md`/`AGENTS.md`) and six `metaobjects-*` Claude Code skills under `.claude/skills/` scoped to the project's stack. `meta init --refresh-docs` updates only the agent docs. - `meta gen [...]` — TS codegen from entities defined under `metaobjects/`. Supports `--dry-run` and `--watch`. Generators (`entityFile()`, `queriesFile()`, `routesFile()`, `formFile()`, `tanstackQuery()`, `tanstackGrid()`, `barrel()`) are reference templates you own, not framework imports: `meta init` wires none, and `meta eject ...` copies the ones you choose (`entity`, `queries`, `routes`, `names`, `barrel`, `form`, `hooks`, `grid`, `grid-hook`, `routes-hono`) into `codegen/generators/` in your own repo (ADR-0034 scaffold-and-own) and prints the import and entry to add to `metaobjects.config.ts`. The shipped defaults target Fastify/Drizzle/React/TanStack and are retargeted by editing your copy, not by switching tools. Per-port codegen runs in that port's own build tool: `mvn metaobjects:generate` (Java/Kotlin), `dotnet meta gen` (C#), `metaobjects gen` (Python). - `meta verify` — drift check. `verify --codegen` (regen-and-diff vs committed output), `verify --templates` (prompt `{{field}}` ↔ payload-VO drift), `verify --db` (live-DB schema drift, Node `meta` only). Per-port codegen verify: `mvn metaobjects:verify -Dmeta.verify.mode=codegen|templates` (Java/Kotlin), `dotnet meta verify` (C#), `metaobjects verify` (Python). -- `meta verify` also checks any `requirement.*` nodes the model declares (capability requirements — what the system is supposed to do, as metadata). `@status` is a loader-enforced closed enum: `planned | live | partial | retired`. A dangling `@implementedBy` is an ERROR on `live`/`partial` and CORRECT on `planned` — an intention has nothing to point at yet. A requirement states what SHOULD be true and is never a journal of what happened, and **`retired` satisfies that by stating a prohibition in force**: a capability that was built and then deliberately removed keeps its entry, written as the rule that it must not come back (`@supersededBy` names what replaced it, and is RESOLVED, so it must be a full dotted path; `@implementedBy` is REFUSED on `retired`). Do NOT delete a retired capability's requirement — that entry is the whole point of the feature: given a brief for a capability that had been retired, agents working from the model alone proposed rebuilding it 24 times out of 24. `@verifiedBy` and the `abandoned`/`superseded` statuses were retired in `0.24.0` and now fail the load. Entirely opt-in: no `requirement.*` nodes means no diagnostics. Registered vocabulary in all five ports; the verify gate is the Node `meta` CLI. +- `meta verify` also checks any `requirement.*` nodes the model declares (capability requirements — what the system is supposed to do, as metadata). `@status` is a loader-enforced closed enum: `planned | live | partial | retired`. A dangling `@implementedBy` is an ERROR on `live`/`partial` and CORRECT on `planned` — an intention has nothing to point at yet. A requirement states what SHOULD be true and is never a journal of what happened, and **`retired` satisfies that by stating a prohibition in force**: a capability that was built and then deliberately removed keeps its entry, written as the rule that it must not come back (`@supersededBy` names what replaced it, and is RESOLVED, so it must be a full dotted path; `@implementedBy` is REFUSED on `retired`). Do NOT delete a retired capability's requirement — that entry is the whole point of the feature: given a brief for a capability that had been retired, agents working from the model alone proposed rebuilding it 24 times out of 24. `@verifiedBy` and the `abandoned`/`superseded` statuses were retired in `0.24.0` and now fail the load. Entirely opt-in: no `requirement.*` nodes means no diagnostics. Registered vocabulary in all five ports, and every port's `verify` runs the same gate (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, `dotnet meta verify`); the authoring lint is the Node `meta` CLI only. - `meta migrate --dialect --slug ` — diff metadata vs the committed schema snapshot and emit migration SQL files under `.metaobjects/migrations`; add `--db --apply` to run them. `--dialect` selects the diff pipeline, not just the SQL flavor — required offline and on `baseline`, auto-detected from the URL scheme when `--db` is given. On a brand-new database the first command is `meta migrate --from-db --db --dialect --slug init --apply` (diff against the empty database, emit `CREATE TABLE`, apply, record the snapshot) — **not** `meta migrate baseline`, which is for adopting a database that already has its schema. `meta migrate apply-pending --db --dialect ` replays committed migrations with no diff (fresh DB / CI). Supports SQLite (`file:`, `libsql:`), Postgres (`postgres:`, `postgresql:`), and Cloudflare D1 (TS only). `--dry-run` prints SQL to stdout. **Schema migration is owned by the Node `meta` CLI (ADR-0015) and used by every port regardless of backend language** — a JVM/Python/C# project needs no per-language migrate engine, but DOES need Node (or Bun) available to run it: no pre-built binary is published today. There is no Maven, .NET, or Python migrate command. - `meta export [--out ]` — flatten loaded metadata to one canonical JSON artifact. diff --git a/docs/ports/csharp.md b/docs/ports/csharp.md index 5bc987503..787977abc 100644 --- a/docs/ports/csharp.md +++ b/docs/ports/csharp.md @@ -393,6 +393,114 @@ the feature reference is at NuGet package to add. The generated parser uses the strict (case-sensitive) default options. +## Requirement tests + +Declare what the software must do as `requirement.*` nodes and `dotnet meta verify` checks +them on every run; the `requirement-tests` generator then writes one xUnit test per tested +requirement. The generator is a reference helper and a recommended approach, not a +contract: `dotnet meta eject requirement-tests` copies it to +`codegen/generators/RequirementTestsGenerator.cs` for you to change. The checks in `verify` +are core and are not ejectable. + +```bash +# The gate: runs beside whichever drift gate verify was given +dotnet meta verify ./metadata --templates ./prompts +dotnet meta verify ./metadata --templates ./prompts --require-implementers + +# The tests: their own run, into a test project that references xunit +dotnet meta gen ./metadata --out ./tests/Acme.Shop.Tests/Generated \ + --namespace Acme.Shop --generators requirement-tests +dotnet meta verify ./metadata --codegen --out ./tests/Acme.Shop.Tests/Generated \ + --namespace Acme.Shop --generators requirement-tests +``` + +A `gen` run has one `--out`, so give this generator a run of its own rather than adding it +to the run that writes your entities. Pass `--namespace` to `verify --codegen` as well: +without it the namespace is inferred from the first generated file in `--out`, and in a +directory that holds only these tests that is the tests' own namespace, which regenerates +one level too deep and reports drift. + +Per metamodel package the generator writes `Requirements__Witnesses.g.cs`, an +interface with one default member per test that is not skipped, and +`Requirements__Tests.g.cs`, one `[Fact]` per requirement (`[Fact(Skip = "…")]` for +a `planned` or `retired` one). Both are in `.Requirements` and are rewritten +whole on every run. The tests construct one class of yours, `.RequirementWitnesses` +by default, which implements every generated interface. Until that class exists the test +project does not compile. Implement each member **explicitly**: + +```csharp +using Acme.Shop.Requirements; +using Xunit; + +namespace Acme.Shop; + +public class RequirementWitnesses : Requirements_acme_shop_Witnesses +{ + void Requirements_acme_shop_Witnesses.req_acme_shop_Orders_Recorded__object_entity() + { + // fails when a placed order has no row + } +} +``` + +A requirement that becomes `live` adds a member whose default fails the test +(`unimplemented requirement: - write .() so that it fails when: +`), with no compile break. One that is retired or deleted removes its +member, and an explicit implementation of it then stops compiling (CS0539). A +`public void req_…()` implements the member implicitly, and a stale one keeps compiling, so +it goes stale silently. If a test reports `unimplemented requirement` for a witness you +believe you wrote, the method is not implementing the member: in the implicit form it must +be `public`, non-static, parameterless and named exactly as the interface names it. + +The six options are public properties on the generator. This port has no per-generator +option channel, so they are reachable only where the generator is constructed, in an owned +`codegen/Program.cs`. That file exists once you have ejected something (or written it by +hand); the packaged `dotnet meta gen --generators requirement-tests` runs with the defaults. + +```csharp +// codegen/Program.cs +using Codegen.Generators; +using MetaObjects.Codegen; +using MetaObjects.Core.Requirement; // RequirementTestGrain, IRequirementTestFilter + +IReadOnlyList generators = +[ + new RequirementTestsGenerator + { + WitnessClass = "Acme.Shop.Tests.Witnesses", + Grain = RequirementTestGrain.Member, + WarnUncovered = false, + }, +]; + +return CodegenCli.Run(args, generators); +``` + +The example is the file as it stands after `dotnet meta eject requirement-tests`: the class +is your owned copy, in `Codegen.Generators`. If `Program.cs` exists because you ejected +something else and you are constructing the packaged generator, the class is in +`MetaObjects.Codegen.Generators`: write it fully qualified, as +`new MetaObjects.Codegen.Generators.RequirementTestsGenerator { … }`. Do not add +`using MetaObjects.Codegen.Generators;`, because every generator you ejected has a packaged +class of the same simple name there, and the second `using` makes each `new Generator()` +already in the file ambiguous (CS0104). + +| Property | Meaning | +|---|---| +| `TestNamespace` | Namespace of the generated tests. Default `.Requirements`. | +| `WitnessClass` | Full name of your witness class. Default `.RequirementWitnesses`. | +| `Grain` | `RequirementTestGrain.Concern` (default): one test per distinct `.` a requirement claims. `Member`: one per distinct `implementedBy` reference that resolves. | +| `Filter` | An `IRequirementTestFilter` (`bool Include(RequirementView view)`) that **replaces** the default of functional requirements at L4 and L5. The view carries `SubType`, `Level` (`int?`, null when not declared), `Status`, `Path`, `Package` and `ImplementedByTypes`. | +| `Renderer` | An `IRequirementTestRenderer` (in `MetaObjects.Codegen`); it returns a `RenderedTest` to replace the text of one test, or `null` to keep the default. | +| `WarnUncovered` | `true` (default): one warning naming up to five requirements the filter left out. | + +The hook types, the identity function and the digest stay in the package when you eject; +the owned copy uses them from there. A package that loses its last requirement leaves its +two files behind: `gen` does not remove them, `dotnet meta verify --codegen` reports each as +`committed but a fresh regen would not emit it`, and you delete them. The model and the +other ports' spelling are in +[Generated requirement tests and witnesses](../features/requirements.md#generated-requirement-tests-and-witnesses). + ## Declarative template-codegen (`--template-spec`) Beyond the built-in EF Core / routes suite, `dotnet meta gen` runs **declarative @@ -484,6 +592,7 @@ on their list route, against an allowlist of their own fields. Remaining gaps ar | Declarative template-codegen | Yes — `dotnet meta gen --template-spec` (scope perEntity/perPackage/perModel + outputPattern; the cross-port JSON contract shared with Python) | | Migrations | Owned by the Node `meta` CLI (ADR-0015) — no C# migrate surface | | Drift verify | `dotnet meta verify` (template drift, FR-004) | +| Requirement gate + requirement tests | Yes (`dotnet meta verify`, on every run; `requirement-tests` emits xUnit and is ejectable) | | Runtime metadata | Loader API + render engine; ObjectManager-style runtime tier on the roadmap | ## Conformance status diff --git a/docs/ports/java.md b/docs/ports/java.md index 2ab4b67d6..62f848051 100644 --- a/docs/ports/java.md +++ b/docs/ports/java.md @@ -280,6 +280,7 @@ name in every port. | `filter-allowlist` | `SpringFilterAllowlistGenerator` | `metaobjects-codegen-spring` | One `FilterAllowlist.java` per writable entity: the filterable field set plus the operator set permitted per field, gated by field subtype (FR-009 §5, identical across ports). Only `@filterable: true` fields appear. Emitted even when no field is filterable (with empty constants), so the generated controller delegates to it unconditionally. | | `value-object` | `SpringValueObjectGenerator` | `metaobjects-codegen-spring` | One Java 21 `record` per concrete `object.value` and per sourceless `object.projection`, in the value object's own package. It carries jakarta bean-validation constraints plus `@Valid` on nested members, so a VO jsonb column POSTs and PATCHes with validation cascading to depth ≥ 2. It is THE Java type for the value object (ADR-0056): `Dto` / `Patch` bind to it, a render helper takes it, and a response parser returns it. The template tier declares no copy, so it needs this generator in the same run. | | `names` | `SpringNamesGenerator` | `metaobjects-codegen-spring` | One `Names.java` per object with a declared/inherited primary `source.rdb` — `public static final` physical database name constants (table/view name, schema, per-field columns). See "`Names`" below. | +| `requirement-tests` | `JUnitRequirementTestsGenerator` | `metaobjects-codegen-spring` | Per metamodel package that holds a tested requirement, two files in `testPackage`: `Requirements__Witnesses.java` (an interface with one default member per non-skipped test, failing with `unimplemented requirement: ...`) and `Requirements__Test.java` (one JUnit Jupiter `@Test` per requirement, calling that member on your `witnessClass`; a planned or retired requirement is `@Disabled`). Args: `testPackage` and `witnessClass` (required), `grain` (`concern` or `member`), `filter` and `renderer` (class names on your project classpath), `warnUncovered`. Your test classpath needs `org.junit.jupiter:junit-jupiter-api`. A model with no requirement writes nothing. See "Requirement tests" below. | | `entity` | `JavaObjectCodeGenerator` | `metaobjects-codegen-base` | Flavor-selected via the `flavor` generator arg (`com.metaobjects.generator.direct.object.javacode`). `flavor=pojoAware` emits `class extends PojoObject` — a concrete `MetaObjectAware` class whose inherited `getMetaData()` back-reference breaks a default Jackson/Gson mapper (see [Serializing generated objects](#serializing-generated-objects) below). `flavor=valueObject` emits a map-backed `class extends ValueObject` instead. Either flavor also emits a `Extractor` and a self-registering `ObjectClassBindingProvider`. For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. | | `output-parser` | `SpringOutputParserGenerator` | `metaobjects-codegen-spring` | One `