Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1b27c96
docs: settle the catalog's two rulings, and open FR-043 on packages
dmealing Sep 13, 2026
2034422
docs(fr-043): libraries as a pillar — approved, with iam as the provi…
dmealing Sep 13, 2026
72b1033
docs(fr-043): a library is a reference you copy, not a dependency you…
dmealing Sep 13, 2026
9fba484
docs(fr-043): Amendment 1 — libraries are layered, and the core model…
dmealing Sep 13, 2026
7f9ad17
docs(plan): prove the overlay mechanism before building on it
dmealing Sep 13, 2026
7cdd886
test(conformance): gate the two overlay behaviours the library design…
dmealing Sep 13, 2026
f6624b6
docs(fr-043): Amendment 2 — Q1 answered, and `overlay: true` licenses…
dmealing Sep 13, 2026
8cad535
docs(plan): overlay-mechanism conformance complete — five ports green
dmealing Sep 13, 2026
40780a1
docs(plan): opt-in codegen and the generator catalog
dmealing Sep 13, 2026
c355de8
feat(catalog): layer joins the generator manifest, and the catalog is…
dmealing Sep 13, 2026
bb3d80a
feat(catalog): layer is gated in all five ports
dmealing Sep 13, 2026
83abe5e
feat(cli): meta gen --list is the catalog, and --probe answers it aga…
dmealing Sep 13, 2026
fc82004
test(catalog): resolve every compatibility declaration against what t…
dmealing Sep 13, 2026
93628de
feat(codegen): warn on an unsatisfied requires edge and on two api fr…
dmealing Sep 13, 2026
49932d0
feat(cli): meta eject takes many names and reports one consolidated i…
dmealing Sep 13, 2026
840a2fc
feat(cli): meta init scaffolds the layout and an empty documented sel…
dmealing Sep 13, 2026
29f84b4
feat(csharp,python): no default generator suite — --generators is req…
dmealing Sep 13, 2026
c344cf0
docs(catalog): codegen is opt-in — ADR-0034 Amendment 2 and the selec…
dmealing Sep 13, 2026
e1ad72c
feat(loader): `overlay: true` licenses an attribute override (FR-043 …
dmealing Sep 13, 2026
ee7cb13
feat(library): iam ships, libraries are LAYERED, and the selection is…
dmealing Sep 13, 2026
244be9c
docs(fr-043): record which Phase 1 items have shipped
dmealing Sep 13, 2026
bc3eddf
feat(verify): a library's ledger cannot volunteer you for coverage — …
dmealing Sep 13, 2026
df64b35
feat(cli): a library is a catalog row — one table, one namespace, one…
dmealing Sep 13, 2026
700d2f0
feat(library): a generator keys on a declared ANCHOR, and an ejected …
dmealing Sep 13, 2026
84f0af4
docs(fr-043): libraries are the sixth pillar, and the layer model leads
dmealing Sep 13, 2026
129f6c0
docs(fr-043): Phase 1 is shipped — and three things building it contr…
dmealing Sep 13, 2026
c6f312b
no-mistakes(document): Refreshed status timestamp to current date; al…
dmealing Sep 13, 2026
bfe3b5b
fix(library): eject staleness keys on the package-relative path, and …
dmealing Sep 13, 2026
bf6e3a4
no-mistakes(document): Documentation stale references fixed: opt-in c…
dmealing Sep 13, 2026
306136d
docs(ports): neither the C# nor the Python port has an `eject` verb
dmealing Sep 13, 2026
6f11da4
docs(agent-context): ten shipped-skill claims promised a default suit…
dmealing Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ 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 that runs without any MetaObjects dependency at runtime. If `@metaobjectsdev/*` disappears tomorrow, you keep working code.

## Five pillars
## 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 and its `verify` checks in every port, and its test scaffolding in TypeScript only:
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 and its `verify` checks in every port, and its test scaffolding in TypeScript only. The sixth is content the other five compose, shipped as named opt-in artifacts:

1. **Codegen** — emit idiomatic 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), EF Core (C#).
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` (fails when *nothing* implements it) and `requirement.architectural` (fails when something *violates* it) 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 and the `verify` checks are cross-port; `requirementTests()` — which scaffolds a test stub per claim — is TypeScript-only.** 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 <library>` — 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`.

## Status

_Last refreshed 2026-09-12._
_Last refreshed 2026-09-13._

**1.0 gating — the quiet period is RETIRED (2026-09-06).** `docs/1.0-readiness.md` §G3 no
longer asks for "one coordinated release with no metamodel-breaking change." It measured a
Expand Down Expand Up @@ -68,7 +70,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/` (322 fixtures; 22 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/` (325 fixtures; 22 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.
Expand Down
149 changes: 149 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,155 @@ here.**

## [Unreleased]

### Added

- **Libraries: reusable declared design you opt into** (FR-043, the sixth pillar).
`"libraries": ["iam"]` in `.metaobjects/config.json` brings a shipped, requirement-backed
model into your project. Two ship: **`iam`** (`preview`) — users, nestable typed groups,
roles as permission bundles, grants global or scoped to a group, nine entities and eleven
requirements — and **`ai`** (`stable`), the LLM-call trace envelope that already existed.

**A library is LAYERED, and the 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** — the design is present and
resolvable, and nothing else happens until you add `["iam", "iam/db"]`. A layer token
implies its core; a token whose layer is unknown is dropped whole rather than reduced to
it, because answering a mistyped `iam/database` with an inert core and no tables is the
worst of the available outcomes.

**Copy is the expected mode.** `meta eject <library>` copies every layer into your first
DECLARED source root with a provenance header, and `meta eject --list` reports how far
your copy has drifted from the shipped tree — nodes changed, only-upstream, only-yours —
matched by name with the package neutralized, because renaming the package is something
you are invited to do. Ejecting and leaving the library in `libraries` is refused at load
(`ERR_LIBRARY_PACKAGE_COLLISION`): both trees merge, and the merge is asymmetric —
additions take effect, deletions do not. Its mirror `ERR_LIBRARY_PACKAGE_NOT_OWNED`
refuses a NEW node declared into a library's package; an `overlay: true` amendment stays
open. See [libraries.md](docs/features/libraries.md).

`meta gen --list` carries `kind: "library"` rows beside the generators — one door, one
namespace — with `useWhen`, `layers`, `provides`, and under `--probe` what your selection
actually added here.

- **`overlay: true` licenses an attribute override.** `ERR_MERGE_CONFLICT` now fires only
on an UNMARKED conflicting redeclaration. The flag is the author saying "I know about the
other declaration and I mean to change it", and without this an adopter could not disagree
with a library's shipped requirement without ejecting the whole ledger. All four loaders
(Kotlin inherits the JVM's); one new conformance fixture takes over the unmarked-conflict
error branch, so the coverage moved rather than being deleted.

### Changed

- **`libraries` moved to `.metaobjects/config.json`**, out of `metaobjects.config.ts`,
outright and with no dual-read — a sweep of the estate found zero uses of the key. Which
designs a project adopts is a fact about the PROJECT, not about how one port generates
code from it. A config still carrying the old key gets a pointed error rather than
silence.

- **`library/ai` is SPLIT into `model` + `db` layers, and its requirements are new.**
Opting into `"ai"` alone no longer proposes `CREATE TABLE llm_call` — that moved to
`"ai/db"`. Breaking-ish for an `ai` adopter tracking the library: add `"ai/db"` to keep
the table. The concrete-`LlmCall` wart was previously carried as accepted on the grounds
that splitting would change what existing adopters get; the estate sweep found there are
none, so it was closed rather than documented.

- **`trace-helper` keys on a declared ANCHOR, not a hard-coded entity name** — and the
name it hard-coded was never actually matching the shipped base. It compared
`"LlmCallBase"` against the SHORT name, so any adopter entity of that name in any package
emitted a helper writing columns that entity does not declare. It now resolves the anchor
its library's manifest declares and compares by node identity, in all three ports that
ship it. Two self-extinguishing warnings cover the halves of the choice: a library opted
into whose implied generator is not wired, and a generator wired whose library is not.

- **Object coverage activates on ADOPTER-authored requirements only.** A library shipping
its own ledger would otherwise switch the unclaimed-entity gate on across a project that
has never written a requirement. Library entries are still counted and still checked;
they simply cannot volunteer you. `meta verify` prints `coverage: not measured (no
project-authored requirements)` rather than a ratio, and the JSON omits the pair rather
than zeroing it — `0/0 claimed` and "not measured" mean opposite things.

**A project that already has a ledger sees its coverage denominator grow** to include an
opted-in library's entities. They are all claimed by the library's own ledger, so no new
warnings appear, but the printed numbers move.

- **A shipped library's files carry a stable `library:<ref>.yaml` source id in every
build.** It was the file's basename in a checkout and `library:…` when embedded, so one
node's error envelope read differently depending on how the library was resolved — and
collided with an adopter file of that name.

### Fixed

- **Both shipped libraries failed `meta verify`'s requirement gate**, in metadata an
adopter cannot fix: every L4 in `ai` claimed FIELDS (`ERR_REQUIREMENT_L4_NOT_OBJECT`),
and both libraries wrote their concerns as SIBLINGS of the L2 segment their own comments
said they were children of, leaving that L2 claiming nothing in its whole subtree. The
load test proved they LOAD clean, which is a different claim, and nothing checked the
other one. Both ledgers are fixed as the model intends — concerns nested under their L2,
each L4 naming the OBJECT with its fields in an L5 child — and a new standalone gate
holds every shipped library, and every future one, to zero loader errors, zero loader
warnings, zero gate findings, zero lint findings, no unruled gaps, and every entity
claimed by its own ledger.

### Changed

- **Codegen is OPT-IN: no port ships a default generator suite** (ADR-0034 Amendment 2).
A new project got code it never asked for — TypeScript's `meta init` copied and wired
five generators, C# ran nine for a caller who named none, Python eight; Java never had
a default set and has been right all along. Deciding which code an application needs
belongs to whoever is building it, increasingly an LLM in the repo, which is well able
to make that call given a truthful catalog and is badly served by a default that
pre-empts it.

**This is a PATCH and no existing project changes by one byte.** An adopter already has
their owned copies on disk and their selection committed in their own config; `meta gen`
keeps running exactly that list, `verify --codegen` keeps checking exactly that output,
and re-running `init` never clobbers a file that exists. What changes is what a *new*
project starts with. `docs/compatibility-policy.md` is narrowed in the same change: the
scaffold-and-own promise is the LAYOUT and the INTERFACES, not which generators a fresh
scaffold wires.

What this means per port:
- **TypeScript** — `meta init` scaffolds `codegen/generators/` **empty**, a config with
`generators: []`, and no dependencies. `dbImport` and the throwing `src/db.ts` stub are
gone with it: both existed only because the scaffold wired `routesFile()`, whose output
emits `import { db } from …`. `dbImport` is now a declared `configKey` on the `routes`
catalog entry, reported by `meta eject routes` to the adopter who chose routes.
- **C# / Python** — `--generators` is REQUIRED; a run that names none is a usage error
and writes nothing. `verify --codegen` re-runs the SELECTION, so with none named it
reports that there is nothing to check rather than regenerating a suite the project
never ran. Python's `verify` gains `--generators`, matching C#.

- **`meta gen --list` is now the generator CATALOG, and `--probe` answers it against your
own model.** `--list --format json` emits one document per generator: its `layer`,
`framework`, what it emits, what it `requires`, the consolidated install set, the config
keys it reads, and whether this project already owns a copy. `--probe` constructs every
generator and dry-runs it against the loaded model, reporting how many files each would
emit — so `output-parser: 3, callable: 0, requirement-tests: 7` replaces a category
label, and cannot go stale, because it runs the generators rather than describing them.

- **`meta eject` takes many names and reports one consolidated install set.** A real
selection is several generators, and three separate invocations produced three separate
install lines for the same package. `--format json` carries, per file, the import line
and the entry to wire, plus one install set with third-party ranges read from the runtime
package's own `peerDependencies`. An unknown name refuses the whole call before writing
anything.

### Added

- **`layer` joins the cross-port generator manifest** — `model` / `persistence` / `api` /
`client` / `docs` / `capability` — gated by all five ports' registry-conformance tests
exactly as `tier` is. Five TypeScript generators that were in no manifest at all (`form`,
`hooks`, `grid`, `grid-hook`, `requirement-tests`) join it too, so the catalog describes
34 generators where the manifest described 29.

- **`meta gen` audits the selection.** Two self-extinguishing warnings, neither a build
failure: a wired generator whose `requires` are not wired (its output will import a
module nothing emits), and two `api`-layer generators declaring different frameworks
(`routes` + `routes-hono` emit to different paths, so nothing conflicts and two complete
HTTP surfaces appear silently). There is deliberately no equivalent rule on the `client`
layer: `@metaobjectsdev/tanstack` peers on `react`, so a form generator plus the TanStack
hook/grid generators is the intended composition.

### Fixed

- **Generated files no longer point at a plugin point that does not exist ([#367]).**
Expand Down
Loading
Loading