Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
ca98c68
docs(plans): FR-023 phase 1a implementation plan
dmealing Sep 11, 2026
b282a00
docs(plans): FR-023 phase 1a — pre-flight rulings written into the ta…
dmealing Sep 11, 2026
1fee2e0
feat(conformance): FR-023 dependency corpus skeleton, pinned artifact…
dmealing Sep 11, 2026
dc5d072
feat(metadata): serializeSharedDocument — node-level packages, no roo…
dmealing Sep 11, 2026
bb84e31
docs(plans): FR-023 phase 1a — LIB Address member order matches the p…
dmealing Sep 11, 2026
bfff6d4
docs: count the dependency-conformance corpus (22 shared corpora)
dmealing Sep 11, 2026
12df4ab
feat(loader): FileSource accepts an explicit source id (FR-023 proven…
dmealing Sep 11, 2026
72ea876
fix(loader): an explicit empty FileSource id is honoured in Python as…
dmealing Sep 11, 2026
d20d8a3
refactor(metadata): scope-pattern grammar lives in metadata; Python p…
dmealing Sep 11, 2026
1918a02
feat(config): `dependencies` in .metaobjects/config.json — sdk schema…
dmealing Sep 11, 2026
25297ae
docs(fr-023): exclusion keys on `packages` with a loud refusal; runti…
dmealing Sep 11, 2026
24b3fcb
refactor(deps): retire `mode`, the local override and three supersede…
dmealing Sep 12, 2026
4987236
docs(fr-023): pre-flight rulings — the refusal covers both loadMemory…
dmealing Sep 12, 2026
822ed0e
docs(fr-023): correct a plan test example that could not catch its ow…
dmealing Sep 12, 2026
98dbb95
feat(deps): lock and manifest schemas with sha256 integrity (FR-023)
dmealing Sep 12, 2026
516b9b5
test(deps): cover writeLock/readLock byte determinism and dependencyS…
dmealing Sep 12, 2026
7e994d0
feat(sdk): resolveCollection loads dependency snapshots first and com…
dmealing Sep 12, 2026
df12984
fix(sdk): DependencyManifest ends the barrel's two-Manifest collision…
dmealing Sep 12, 2026
dc1ada8
docs(fr-023): the refusal names object and package; DependencyManifes…
dmealing Sep 12, 2026
605935e
test(deps): the loader's existing errors are the first cross-repo dri…
dmealing Sep 12, 2026
9887a57
docs(fr-023): the shared-enums predicate must reach both call sites
dmealing Sep 12, 2026
33d9dd1
docs(fr-023): the load-failure task adds seven cases, not six
dmealing Sep 12, 2026
902e8dc
feat(codegen-ts): imported nodes are not generated unless scope.inclu…
dmealing Sep 12, 2026
75c25db
docs(fr-023): the import predicate must reach the two indirect scope …
dmealing Sep 12, 2026
a645925
docs(fr-023): the codegen-selection task's run commands name real tes…
dmealing Sep 12, 2026
9d1e5d3
docs(fr-023): point the scaffold example at a test that exists
dmealing Sep 12, 2026
d28244e
feat(migrate): imported tables leave both sides of the schema diff, a…
dmealing Sep 12, 2026
27749df
docs(fr-023): the requirements task's helper reference, and both its …
dmealing Sep 12, 2026
d4ce356
docs(fr-023): remove a worked-out-loud false start from the schema ta…
dmealing Sep 12, 2026
6692fc6
docs(fr-023): the dependency note lands on stdout, not stderr
dmealing Sep 12, 2026
4afbd7e
feat(cli): the requirements ledger does not count imported entities t…
dmealing Sep 12, 2026
3f804d7
docs(fr-023): the ledger task's regression test lives under the unit …
dmealing Sep 12, 2026
a9c4fc1
docs(fr-023): two more run commands name tests that exist
dmealing Sep 12, 2026
79c6c25
docs(fr-023): the overlay task's run list, and a coverage gap worth k…
dmealing Sep 12, 2026
34eb5c3
docs(fr-023): qualify the unchanged-behaviour guarantee on the second…
dmealing Sep 12, 2026
d6ff3d2
feat(codegen-ts): sharedModelFile() emits a publisher's flattened sha…
dmealing Sep 12, 2026
eb9f003
fix(codegen-ts): replace a raw NUL byte in shared-model-file.ts's ded…
dmealing Sep 12, 2026
ded60c2
docs(fr-023): correct three stale references in the phase-1a plan
dmealing Sep 12, 2026
69762ea
feat(cli): meta deps sync and list — a committed snapshot pinned by a…
dmealing Sep 12, 2026
06490cc
fix(cli): deps sync — guard a corrupted lock, gate deps flags in --he…
dmealing Sep 12, 2026
8519386
test(cli): make the deps sync name-filter test discriminate on filter…
dmealing Sep 12, 2026
f6d8648
docs(fr-023): the deps option table carries no format key
dmealing Sep 12, 2026
0514696
feat(cli): meta deps check and verify --deps fail when the installed …
dmealing Sep 12, 2026
382eadb
docs(cli): document the readManifestDir invariant checkDependencies's…
dmealing Sep 12, 2026
3b81345
feat(cli): meta verify reports an unflagged cross-file redeclaration …
dmealing Sep 12, 2026
a58fc5b
fix(metadata): declaredTopLevelKeys expands a relative package the sa…
dmealing Sep 12, 2026
ab5ffb6
docs(fr-023): correct two stale claims in the phase-1a plan
dmealing Sep 12, 2026
e449517
feat(python): dependencies resolve, load first and are excluded from …
dmealing Sep 12, 2026
77f0f0e
test(python): add the stale-snapshot CLI scenario and fix a non-discr…
dmealing Sep 12, 2026
1d7fd88
feat(cli): meta init tracks .metaobjects/deps and deps.lock.json and …
dmealing Sep 12, 2026
5403df4
fix(cli): drop the orphaned package.meta.json entry from meta init's …
dmealing Sep 12, 2026
b8c2d12
docs: metadata dependencies — declare, sync, build on, and what fails…
dmealing Sep 12, 2026
cc09489
docs(fr-023): correct the every-port-consumes overclaim and the eject…
dmealing Sep 12, 2026
efbfa39
docs(fr-023): the plan and constraints overstated port coverage
dmealing Sep 12, 2026
782ec0f
fix(codegen-ts): re-sync the three owned entity-template copies
dmealing Sep 12, 2026
2a86843
docs(fr-023): the deletion gate checked prose, not just code
dmealing Sep 12, 2026
ae27939
fix(cli): meta docs --site resolves against a dependency's snapshot
dmealing Sep 12, 2026
76e48ce
docs(fr-023): qualify the inScope formula as TypeScript's, add Python's
dmealing Sep 12, 2026
03c4e9e
docs(fr-023): the overlay lint runs on every verify, not just with de…
dmealing Sep 12, 2026
2092fca
fix(dependency-conformance): discharge the view.text Python allowlist
dmealing Sep 12, 2026
0d96748
test(cli): assert shared-model-file.ts constants stay in sync with sdk
dmealing Sep 12, 2026
837e84c
test: close two FR-023 coverage gaps (reference entity scope, zero-de…
dmealing Sep 12, 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
14 changes: 10 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,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/` (313 fixtures; 21 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/` (314 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 Expand Up @@ -288,11 +288,17 @@ File-naming: `meta.<concept>.json`. Each file declares its `package`:
```
metaobjects/
├── meta.user.json # STRUCTURAL (always present)
├── meta.user.ui.json # UI overlay (views, layouts)
└── meta.user.db.json # DB overlay (sources, dbColumns)
├── meta.user.ui.json # UI overlay (views, layouts) — overlay: true
└── meta.user.db.json # DB overlay (sources, dbColumns) — overlay: true
```

All three share the same `package` and object `name`. The Loader merges them. Use only when team-level concerns justify the file proliferation. Default to single-file-per-domain.
All three share the same `package` and object `name`. The Loader merges them. `meta.user.ui.json`
and `meta.user.db.json`'s top-level object declaration must carry `overlay: true`: the loader's
merge doesn't require it (a same-`(type, name)` redeclaration merges either way), but leaving it
off is exactly what `meta verify`'s overlay authoring lint (`docs/features/metadata-dependencies.md`)
flags as advisory — and it's what would turn a renamed/removed `User` into a silent second object
instead of a loud `ERR_OVERLAY_NO_TARGET`. Use only when team-level concerns justify the file
proliferation. Default to single-file-per-domain.

**`BaseEntity` pattern**: shared abstract bases live in `meta.common.json`. Concrete entities use `extends: "BaseEntity"` to inherit `id` + `createdAt` without redeclaring.

Expand Down
100 changes: 100 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,106 @@ here.**

## [Unreleased]

### Added

- **Metadata dependencies (FR-023, Phase 1a — TypeScript + Python).** A project can
now build on a metadata model published by another repository or package instead
of copy-pasting it:
- **`dependencies`** in `.metaobjects/config.json` — `[{ name, path }]` today (the
`npm`/`python` transport keys are reserved in the schema and refused by `sync`
with `ERR_DEPENDENCY_UNRESOLVED`, "not supported by this toolchain yet").
- **`meta deps sync [<name>…] [--dry-run]` / `meta deps check` / `meta deps
list`** — `sync` resolves each dependency's `path`, validates its manifest and
artifact hash, re-loads the artifact standalone with core providers, and writes
the committed snapshot (`.metaobjects/deps/<name>/`) + lock
(`.metaobjects/deps.lock.json`); `check` compares the currently-installed
artifact's hash against the lock, read-only; `list` prints the lock.
- **`verify --deps`** — the same drift comparison as `meta deps check`, as a
gated `verify` subverb (needs the publisher's `path` reachable, so it is never
part of the bare-`verify` default). `drifted` or `unresolved` fails with the new
`ERR_DEPENDENCY_UPSTREAM_DRIFT`.
- **`sharedModelFile()`** (`@metaobjectsdev/codegen-ts`) — the publisher-side
generator: selects a subset of a project's own metadata by the `scope`
pattern grammar, closure-checks it, and emits one canonical-JSON artifact +
`metaobjects.pkg.json` manifest. Registered and discoverable (`meta gen
--list`), but deliberately **not** offered by `meta eject --list` — the
artifact is a contract whose bytes a cross-port corpus pins and whose hash
consumers verify, so a user-owned editable copy would invite a silent break.
- **An overlay authoring lint** in `meta verify` — advisory, runs on every
invocation (no subverb): a top-level `(type, resolutionKey)` declared in two or
more collection files (dependency artifacts included) where more than one
declaration lacks `overlay: true`. Never fails the build; mute with
`--no-overlay-lint` / `META_NO_OVERLAY_LINT=1`.
- **`ERR_DEPENDENCY_PACKAGE_NOT_OWNED`** — a project may `extends` or `overlay:
true` a dependency's node freely, but declaring a *brand-new* top-level node
into a package a dependency owns is refused by name, naming the fix.
- New error codes altogether: `ERR_DEPENDENCY_UNRESOLVED`,
`ERR_DEPENDENCY_MANIFEST_INVALID`, `ERR_DEPENDENCY_SNAPSHOT_STALE`,
`ERR_DEPENDENCY_NODE_COLLISION`, `ERR_DEPENDENCY_METAMODEL_INCOMPATIBLE`,
`ERR_DEPENDENCY_UPSTREAM_DRIFT`, `ERR_DEPENDENCY_PACKAGE_NOT_OWNED`.
- New exports: `declaredTopLevelKeys` (`@metaobjectsdev/metadata` — the raw,
pre-parse walk the overlay lint and the loader's own overlay-only partition
share) and `serializeSharedDocument` (the canonical shared-artifact
serializer `sharedModelFile()` targets; Python's `serialize_shared_document`
is byte-identical, though Python has no publisher CLI wired to it yet —
`sharedModelFile()` itself is TypeScript-only in Phase 1a). `FileSource`'s
constructor takes an optional `{ id }`, so a dependency's synced snapshot
loads with `dep:<name>/<artifact>` provenance instead of reading like a
local file.
- Deliberately **not** built in Phase 1a — see `docs/features/metadata-dependencies.md`
("Deferred"): the `npm`/`python`/`maven`/`nuget` transports, a local
co-development override, a usage-aware breaking-change classifier, cross-boundary
codegen imports (`packageBindings`), a runtime `ObjectManager` scope predicate,
and Java/Kotlin/C# as dependency consumers or publishers (Phase 2).
- No registered vocabulary changed — `metamodelVersion` stays `1.0`.
Docs: `docs/features/metadata-dependencies.md`. Corpus:
`fixtures/dependency-conformance/` (23 cases, TS + Python).

### Changed

- **A project that declares `scope.include` now sees its requirements-ledger
denominator narrow to that scope — even with zero dependencies.** The ledger's
["coverable" object count](docs/features/requirements.md) is computed by
`Collection.inScope`, which is `matchesScope(fqn, scope) && …` regardless of
whether the project has any `dependencies` at all. Previously the ledger counted
every non-abstract entity in the loaded model; a project that scopes its own
codegen with `scope.include` now has its ledger coverage counted only over that
declared scope too. This is a real behavior change for an existing project that
already declares `scope.include` and reads its `meta verify` coverage numbers —
they may drop even though nothing was deleted.
- **`meta verify` now runs the overlay authoring lint for EVERY project, not just
one with `dependencies` declared.** `runOverlayLintAdvisory()` is unconditional —
it lints `collection.files`, which for a zero-dependency project is simply its
own files — so a project with no dependencies at all, but with a top-level
`(type, resolutionKey)` redeclared across two or more of its own files (e.g. the
["Optional layered overlay pattern"](AGENTS.md) — a `meta.user.json` +
`meta.user.ui.json` + `meta.user.db.json` split), now gets new advisory
`WARN_OVERLAY_IMPLICIT` output on `meta verify` it did not get before this
release. Advisory only — never fails the build, mute with `--no-overlay-lint` /
`META_NO_OVERLAY_LINT=1` — but this is a second, real carve-out from "a project
with no dependencies behaves identically," alongside the ledger-denominator one
above.
- **`Collection`** (`@metaobjectsdev/sdk`) gains `dependencies`, `ownFiles`,
`fileIds`, `importedPackages`, `importedNodes`, `imported(fqn)`,
`declaredMigrateScope`; `inScope` and `inMigrateScope` are now the COMPOSED
predicates described above (default-exclusion of imported metadata) — byte-for-byte
identical to their prior behavior for a project with no `dependencies` and no
declared `scope`/`migrate.scope`.
- **The scope-pattern grammar (`compileScope`/`matchesScope`/`compilePattern`)
moved from `@metaobjectsdev/sdk` to `@metaobjectsdev/metadata`** — pure,
browser-safe string code, so `@metaobjectsdev/codegen-ts` (the `sharedModelFile()`
publisher side) can use it without a `sdk` dependency. `sdk` re-exports it
unchanged; existing importers of `compileScope`/`matchesScope` from `sdk` are
unaffected.
- **`scopeExpectedSchema`** (`@metaobjectsdev/migrate-ts`) takes a third, optional
`{ imported }` argument: an imported object the scope does not admit is removed
from the *expected* schema before `declaredSchemas` is computed — so importing a
table-backed entity can never turn the publisher's other tables into `DROP`
candidates, and never proposes creating the imported table.
- **`scanRequirements`** (`@metaobjectsdev/cli`) takes an optional second argument,
`{ coverable }` — the predicate the ledger denominator change above threads
through.

### Deprecated

- **`@metaobjectsdev/sdk`: the v0.3 `package.meta.json` prototype and workspace discovery** —
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ metaobjects/
├── README.md # you are here
├── CLAUDE.md # project instructions for Claude
├── spec/ # canonical metamodel docs, ADRs, roadmap
├── fixtures/ # 21 cross-language conformance corpora — the oracle
│ ├── conformance/ # metamodel (loader + serializer + navigation), 313 fixtures
├── fixtures/ # 22 cross-language conformance corpora — the oracle
│ ├── conformance/ # metamodel (loader + serializer + navigation), 314 fixtures
│ ├── yaml-conformance/ # YAML authoring desugar
│ ├── render-conformance/ # FR-004 byte-identical render oracle
│ ├── verify-conformance/ # FR-004 template-drift gate
Expand Down
26 changes: 26 additions & 0 deletions agent-context/skills/metaobjects-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,32 @@ across files (same `package` + same `name` → merged; last-writer-wins on attr
conflicts, structural children accumulate). Use `extends` to share shape between
distinct entities; use `overlay` to split one entity's declaration across files.

**Extending and overlaying a metadata dependency's nodes is the expected way to
build on a shared model.** A project may declare `dependencies` in
`.metaobjects/config.json` and `meta deps sync` a publisher's metadata into a
committed snapshot — that snapshot loads BEFORE your own files, so a foreign
abstract resolves via `extends` and a foreign node re-opens via `overlay: true`
exactly like a local one. Two rules are specific to that boundary:

- **Say `overlay: true` on every amendment to a node you don't own.** Within one
project the parser merges a same-`(type, package::name)` redeclaration whether
or not it carries the flag; only the flagged form fails loudly
(`ERR_OVERLAY_NO_TARGET`) when the target is gone. Skip the flag on a
dependency's node and its removal upstream silently becomes a new, disconnected
local object instead of a build failure — always flag a contribution to a node
you did not declare.
- **A brand-new top-level node declared into a dependency's package is refused**,
not silently excluded: `ERR_DEPENDENCY_PACKAGE_NOT_OWNED`, naming the object,
the package, and the fix (declare it in your own package and `extends` the
dependency's node, or give it that node's exact name with `overlay: true` if you
meant to amend it). A *local node in your own package* is unaffected — this
refusal fires only for a package a dependency owns.

What fails when upstream changes (a node removed/renamed, a member's shape
changed) is the loader's existing errors, unchanged — see
`docs/features/metadata-dependencies.md` for the full table and what `meta deps
sync`/`verify --deps` check that the loader cannot.

## Discriminator inheritance (TPH)

When several concrete entities are variants of one thing and should share a
Expand Down
40 changes: 40 additions & 0 deletions agent-context/skills/metaobjects-codegen/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,46 @@ instance/write artifacts regardless.
Per-entity opt-outs exist (e.g. skipping client-side artifacts for a given
entity) and are set as attributes on the entity in metadata, not in code.

## A dependency's metadata is load-only by default — codegen excludes it

A project may declare `dependencies` in `.metaobjects/config.json` and `meta deps
sync` a publisher's metadata into a committed snapshot (TypeScript + Python,
Phase 1a). That snapshot's nodes load so your own model can resolve against them
(`extends`, `overlay: true`, plain FQN references) — but codegen (and `verify
--codegen`, and the requirements ledger's denominator) **excludes them by
default**. A node is "imported" when its metadata *package* is one a dependency
owns; an imported node is generated only when your own `scope.include` names that
package **literally** (`acme::common::**` or `acme::common::Address` name
`acme::common`; a bare `acme::**` or `**` do not — they match the package's nodes,
which is weaker than naming it). Naming the package in `scope.include` (and, if
you own its tables, `migrate.scope`) is how a consumer takes over a shared model —
the "I instantiate this metadata myself" case, no separate mode needed.

Running `meta gen <Name>` (or a Python `entities: [...]`) on a name that resolves
to nothing but excluded imports is refused by name (exit 2) rather than silently
generating nothing — the message names the dependency and the `scope.include` fix.

## Publishing a shared model: `sharedModelFile()`

The other side of the same feature: `@metaobjectsdev/codegen-ts` ships
`sharedModelFile({ name, include, exclude?, files?, version?, target? })`, a
generator a publisher wires to select a subset of its own metadata (by the same
scope-pattern grammar as `scope`) and emit it as one canonical-JSON artifact +
manifest — the thing a consumer's `meta deps sync` copies. It closure-checks the
selection (every reference from a selected node must resolve to another selected
node, or the build fails naming the pair) and re-loads the emitted artifact with
core providers only, so a Phase 1a export needing non-core vocabulary fails at
publish time. It is registered and shows up in `meta gen --list` like any other
generator — but **it is deliberately not offered by `meta eject --list`** (unlike
the four ADR-0034 scaffold-and-own generators). The artifact is a contract whose
bytes a cross-port corpus pins and whose hash consumers verify; a user-owned,
editable copy would invite an artifact that silently stops matching what
consumers expect. Only TypeScript can run it in Phase 1a. **Phase 1a is TypeScript and Python
only, full stop** — TypeScript and Python are the only two ports that can
*consume* a dependency at all today, and only the TypeScript toolchain can
*publish* one; Java, Kotlin, and C# do not read `dependencies` yet (Phase 2).
Full detail: `docs/features/metadata-dependencies.md`.

## You don't have to generate everything — pick your layers

Codegen is **granular and à la carte, not all-or-nothing.** The most powerful
Expand Down
Loading
Loading