diff --git a/AGENTS.md b/AGENTS.md index c67e8237e..443922977 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. @@ -288,11 +288,17 @@ File-naming: `meta..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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 302d43de6..80b00e398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 […] [--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//`) + 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:/` 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** — diff --git a/README.md b/README.md index 2bca767ac..b6b593e99 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/agent-context/skills/metaobjects-authoring/SKILL.md b/agent-context/skills/metaobjects-authoring/SKILL.md index ca8f93f2a..86bfc2dfd 100644 --- a/agent-context/skills/metaobjects-authoring/SKILL.md +++ b/agent-context/skills/metaobjects-authoring/SKILL.md @@ -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 diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index ce26d4474..247af390d 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -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 ` (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 diff --git a/agent-context/skills/metaobjects-verify/SKILL.md b/agent-context/skills/metaobjects-verify/SKILL.md index b4aeb6191..2afec0070 100644 --- a/agent-context/skills/metaobjects-verify/SKILL.md +++ b/agent-context/skills/metaobjects-verify/SKILL.md @@ -105,9 +105,33 @@ If this project declares `requirement.functional` / `requirement.architectural` `meta verify` — there is no subverb — and the severity of a broken link depends on the requirement's `@status`, which is the part that surprises people reading a failure. +## The overlay authoring lint runs on every run, dependencies or not + +`meta verify` always runs an ADVISORY overlay lint, no subverb needed and +regardless of whether this project declares `dependencies` in +`.metaobjects/config.json` (`docs/features/metadata-dependencies.md`): a +top-level `(type, resolutionKey)` declared in two or more collection files — +dependency artifacts included when there are any, since they are the base a +consumer's own files amend, but a plain multi-file **local** overlay (no +dependencies at all — e.g. the layered-overlay pattern in `AGENTS.md`, or any +project that re-opens its own node from a second file) triggers it exactly the +same way — where MORE THAN ONE declaration lacks `overlay: true`. Exactly one +unflagged declaration is the base and is fine; every additional one is a +finding naming its file, because it is exactly the state that turns loud +(`ERR_OVERLAY_NO_TARGET`) the day the target is renamed or removed, instead of +quietly forking into a second, disconnected object. Never fails the build; +mute it with `--no-overlay-lint` or `META_NO_OVERLAY_LINT=1` the same way the +anti-pattern pass is muted. + +**A stale dependency snapshot is a load-time failure, not a `verify` finding.** +If `.metaobjects/deps//` disagrees with `.metaobjects/deps.lock.json` — a +missing snapshot, a hand-edited one, or a lock entry with no matching config +declaration — every command (not just `verify`) fails to even LOAD the metadata, +with `ERR_DEPENDENCY_SNAPSHOT_STALE` naming the fix: run `meta deps sync`. + ## The `verify` subverbs -`verify` has three drift checks. Run them in CI. +`verify` has four drift checks. Run the ones your project uses in CI. - **`--db`** — schema drift. Introspects the live database and fails if it has diverged from metadata. This is a **schema concern, so it is the Node toolchain's @@ -128,6 +152,20 @@ requirement's `@status`, which is the part that surprises people reading a failu if any reference isn't on the payload VO. This is the build-time gate for the prompt-construction pillar. +- **`--deps`** — dependency drift (FR-023, **Node `meta` only** — but **any + backend**, like `--db`: the check reads only `.metaobjects/config.json` and has + no backend dependency of its own). Re-resolves each declared dependency's `path` + right now and compares its installed artifact's hash against + `.metaobjects/deps.lock.json` — the same comparison `meta deps check` runs. Only + TypeScript and Python additionally LOAD the resulting snapshot and honour the + default-exclusion it implies — a Java/Kotlin/C# project can run this gate today + even though its own codegen can't yet consume a dependency. Never part of the + bare-`verify` default: it + needs the publisher's `path` reachable, which CI checking out only your own repo + may not have. Reports `current` / `drifted` / `unresolved` per dependency; + `drifted` or `unresolved` fails with `ERR_DEPENDENCY_UPSTREAM_DRIFT`, fix is + "review the artifact diff, then `meta deps sync`." + **Only `--db` is Node-universal.** `--codegen` / `--templates` run through each port's own build tool, not the Node `meta`: diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 28ecb21fd..fa7708b6a 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -1,6 +1,6 @@ # Conformance coverage -The MetaObjects standard ships **21 shared conformance corpora** under +The MetaObjects standard ships **22 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 @@ -25,7 +25,7 @@ regenerate with `ls -d fixtures//*/ | wc -l`. | Corpus | Fixtures | TS | Java | Kotlin | C# | Python | |---|---|---|---|---|---|---| -| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 313 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ | +| [`fixtures/conformance/`](../fixtures/conformance/) (metamodel) | 314 | ✓ | ✓ | inherits via `metadata-ktx` | ✓ | ✓ | | [`fixtures/yaml-conformance/`](../fixtures/yaml-conformance/) | 15 | 15 / 15 | 14 / 15 (1 ledgered: `yaml-quoted-leading-zero` — Java pipeline strips quotes off `"007"`) | inherits via Java | 14 / 15 (1 ledgered: `error-yaml-coerced-hex-in-string` — YamlDotNet doesn't coerce `0xFF`) | 15 / 15 | | [`fixtures/verify-conformance/`](../fixtures/verify-conformance/) | 31 | ✓ | ✓ | inherits via Java | ✓ | ✓ | | [`fixtures/verify-strict-conformance/`](../fixtures/verify-strict-conformance/) | 1 | ✓ | — | — | — | ✓ | @@ -43,7 +43,8 @@ regenerate with `ls -d fixtures//*/ | wc -l`. | [`fixtures/generator-registry-conformance/`](../fixtures/generator-registry-conformance/) | 1 canonical manifest | ✓ | ✓ | ✓ | ✓ | ✓ | | [`fixtures/provider-composition-conformance/`](../fixtures/provider-composition-conformance/) | 9 (5 error-shape + 4 compose-load) | ✓ | ✓ | — (JVM registry via Java) | ✓ | ✓ | | [`fixtures/source-resolution-conformance/`](../fixtures/source-resolution-conformance/) | 25 cases | ✓ (reference implementation) | ✓ | inherits via Java | ✓ | ✓ | -| [`fixtures/scope-conformance/`](../fixtures/scope-conformance/) | 10 cases | ✓ (reference implementation) | — | — | — | — | +| [`fixtures/scope-conformance/`](../fixtures/scope-conformance/) | 10 cases | ✓ (reference implementation) | — | — | — | ✓ | +| [`fixtures/dependency-conformance/`](../fixtures/dependency-conformance/) | 23 cases | ✓ (reference implementation) | — (Phase 2) | — (Phase 2) | — (Phase 2) | ✓ (2 of 23 assert a documented Python-only vocabulary gap instead of the corpus's full contract — see below) | | [`fixtures/agent-context-conformance/`](../fixtures/agent-context-conformance/) | 4 | ✓ (the emitter is TS-owned) | — | — | — | — | | [`fixtures/metamodel-docs/`](../fixtures/metamodel-docs/) | 1 | ✓ (docs emit is TS-owned) | — | — | — | — | @@ -118,7 +119,7 @@ unit-test runners (`bun test`, `dotnet test`, `pytest`, `mvn test`) pull Docker. ## Fixture-to-doc mapping -### `fixtures/conformance/` — metamodel loader + canonical serializer (313) +### `fixtures/conformance/` — metamodel loader + canonical serializer (314) | Fixture prefix | Feature doc | |---|---| @@ -229,20 +230,41 @@ matching is case-sensitive.** These are exactly the rules four independent implementations would otherwise each get slightly wrong — the failure mode that produced the cross-port `LIKE`/`ILIKE` divergence fixed in 0.21.6. -**TypeScript is the only port with a runner today.** The reference implementation is -[`server/typescript/packages/sdk/src/scope.ts`](../server/typescript/packages/sdk/src/scope.ts) -(`compilePattern` / `compileScope` / `matchesScope`), and the corpus was authored -against it. Java, Kotlin, C# and Python have no runner yet; when each gains one, this corpus is -what it implements against — it exists now precisely so those four land on one -grammar rather than four. +**TypeScript and Python run it today.** The reference implementation is +[`server/typescript/packages/metadata/src/scope.ts`](../server/typescript/packages/metadata/src/scope.ts) +(`compilePattern` / `compileScope` / `matchesScope`; `@metaobjectsdev/sdk` re-exports +it unchanged, since it moved there from `sdk` when FR-023 needed it from +`codegen-ts` without a `sdk` dependency), and the corpus was authored against it. +Python's port (`server/python/src/metaobjects/scope.py`, `matches_scope` using +`re.fullmatch`) runs the same corpus. Java, Kotlin and C# have no runner yet; when +each gains one, this corpus is what it implements against — it exists now precisely +so those three land on one grammar rather than three. + +### `fixtures/dependency-conformance/` (23 cases) + +All 23 cases → [features/metadata-dependencies.md](features/metadata-dependencies.md) +(declaring a dependency, `meta deps sync`, the committed snapshot + lock, default +exclusion of imported metadata, overlay/extends across the boundary, and every load- +and resolution-time failure). File-shaped like `scope-conformance/` above: one +committed `cases.json`, no per-port fixture, no ledger. + +**TypeScript (the reference implementation) and Python both run all 23 cases, +identically** — every case's `expectFiles`/`expectImported`/`expectSelected`/ +`expectMigrateGoverned`/`expectLoadError` assertions apply to both runners with no +exemption. The two overlay cases that need a view child as incidental content use +`view.currency` (the one concrete `view.*` subtype registered cross-port), not +`view.text` — a prior revision used `view.text`, which Python doesn't register, and +carried a since-discharged allowlist for it. Java, Kotlin and C# have no runner — +Phase 1a is TypeScript + Python only; those three ports arrive in Phase 2. ## Orphaned fixtures (tested but not yet documented) -The fixtures in the eight corpora mapped above (metamodel 313 + yaml 15 + verify 31 -+ render 15 + persistence 33 + api-contract 41 + source-resolution 25 + scope 10) each -map to a feature doc. 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. +The fixtures in the nine corpora mapped above (metamodel 314 + yaml 15 + verify 31 ++ render 15 + persistence 33 + api-contract 41 + source-resolution 25 + scope 10 + +dependency 23) each map to a feature doc. 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. If you add a new fixture and don't see a clear home for it, either: diff --git a/docs/README.md b/docs/README.md index 7903aad34..e645de4e2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,6 +38,7 @@ docs/ │ ├── generated-mutations.md # generated POST/PATCH mutation surface │ ├── image-upload.md # view.image form control (TS-web) │ ├── metadata-sources.md # where metadata comes from: sources, scope, discovery +│ ├── metadata-dependencies.md # building on a metadata model published elsewhere (FR-023) │ └── own-your-codegen.md # scaffold-and-own generator ownership (ADR-0034) └── ports/ # one file per language/framework port ├── typescript.md @@ -60,6 +61,7 @@ this tree is documentation, not the source of truth. | Compare what TS vs Java vs Kotlin vs C# vs Python emit for the same metadata | any [`features/*.md`](features/) — every feature shows all five ports side-by-side | | Author metadata in YAML instead of JSON | [`features/yaml-authoring.md`](features/yaml-authoring.md) | | Point the toolchain at metadata that lives somewhere other than `metaobjects/`, or scope what a project generates and migrates | [`features/metadata-sources.md`](features/metadata-sources.md) | +| Build on a metadata model another repository publishes (`dependencies`, `meta deps sync`, overlay/extend across the boundary) | [`features/metadata-dependencies.md`](features/metadata-dependencies.md) | | Record what the system is supposed to do, and stop agents reviving retired features | [`features/requirements.md`](features/requirements.md) | | Wire prompt construction (FR-004) | [`features/templates-and-payloads.md`](features/templates-and-payloads.md) | | Share a metadata shape across multiple instances (abstracts, `extends:`) | [`features/abstracts-and-inheritance.md`](features/abstracts-and-inheritance.md) | diff --git a/docs/features/abstracts-and-inheritance.md b/docs/features/abstracts-and-inheritance.md index 4a005cb90..a067e28b5 100644 --- a/docs/features/abstracts-and-inheritance.md +++ b/docs/features/abstracts-and-inheritance.md @@ -283,6 +283,24 @@ the same entity's declaration is split across files (e.g., domain code in one file, persistence overlay in another). See [`loaders.md`](loaders.md) for the overlay merge semantics. +### The same rule, across a repository boundary + +Nothing above changes when the base you `extends` or the node you `overlay` lives in +a [metadata dependency](metadata-dependencies.md) rather than your own tree — +dependency files load before yours in the same `loader.load(...)`, so a foreign +abstract resolves and a foreign node re-opens exactly like a local one. **One rule +is stricter across that boundary, though: say `overlay: true` on every amendment to +a node you do not 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 disappears. Skip the flag +on a dependency's node and an upstream removal turns your amendment into a silent +new local object under the same name instead of a build failure — the flag is what +makes that loud. Declaring a brand-new top-level node into a dependency's package +(rather than amending one it already exports) is refused outright +(`ERR_DEPENDENCY_PACKAGE_NOT_OWNED`); see +[`metadata-dependencies.md`](metadata-dependencies.md) for the full rule and what +fails when the base you extended, or the node you overlaid, changes upstream. + ## When to use abstracts vs. new subtypes vs. attr extensions This is the same decision as diff --git a/docs/features/cli.md b/docs/features/cli.md index 6c783ac1b..c84685762 100644 --- a/docs/features/cli.md +++ b/docs/features/cli.md @@ -33,6 +33,7 @@ command surface splits in two: | **Template/prompt drift** (`verify --templates`) | **Node `meta`** | `meta verify --templates` | TS reference (ADR-0021 D2) — `{{field}}`↔payload; the bare-`verify` default | | **Vocabulary upgrade** (`upgrade`) | **Node `meta`** | `meta upgrade [--to ] [--apply]` | **any backend** — rewrites RETIRED metadata vocabulary (`@violation` → `@counterexample`, `@readOnly` → `@mutability`, dropping `@verifiedBy`) and resolves ATTRIBUTE CONTRADICTIONS (`@fields` beside `@expr` on an index key). Node-only because it edits the metadata documents themselves, which every port shares; a non-TS project runs `npx meta upgrade` against its own `metaobjects/`. **Canonical JSON and YAML alike.** Previews by default. Retirements needing a human decision are refused and the run exits non-zero, so CI cannot record a partial migration as finished | | **Vocabulary search** (`types`) | **Node `meta`** | `meta types [query]` | **any backend** — apropos/`kubectl explain` over the live metamodel registry (names + descriptions + when-to-use); the vocabulary is cross-port identical (registry-conformance) | +| **Dependency sync** (`deps`) | **Node `meta`** | `meta deps sync \| check \| list` | **any backend** — resolves a declared metadata dependency's `path` transport into a committed snapshot + lock (FR-023), so `sync`/`check`/`list` themselves stay Node-only regardless of your server language. TypeScript and Python then LOAD that snapshot at every rung of their own source ladder — the two ports that implement dependency resolution in Phase 1a; Java, Kotlin, and C# don't read `dependencies` yet (Phase 2). See [`metadata-dependencies.md`](metadata-dependencies.md) | | TS codegen | Node `meta` | `meta gen` | TS projects. **No `--template-spec` flag, deliberately** — `metaobjects.config.ts` already takes generator VALUES, so a declarative template generator is declared there (`templateGenerator()`, or `templateSpecToGenerators(parseTemplateSpec(spec))` to reuse a C#/Python spec file). Keeping it in the config is what lets `meta verify --codegen` regenerate WITH it, since that gate re-runs the config's generator list; see [declarative template scopes](codegen-concepts.md#declarative-template-scopes) | | C# codegen | `dotnet meta` | `dotnet meta gen` / `verify --templates` / `verify --codegen` | a .NET tool (`ToolCommandName=dotnet-meta`); invoked `dotnet meta` so it never shadows the Node `meta`; ships the ADR-0021 D2 subverbs (`--db` rejected, exit 2; bare `verify` = `--templates`). `gen` also accepts `--template-spec ` (+ `--template-root `, default `templates`) — the declarative Mustache template-codegen surface (the cross-port JSON contract shared with Python), **auto-discovered at `/template-spec.json`** when the flag is absent. Prefer the conventional path: `verify --codegen` takes no `--template-spec`, so discovery is how the drift gate sees your template generators at all; see [declarative template scopes](codegen-concepts.md#declarative-template-scopes) | | Java/Kotlin codegen | Maven plugin | `mvn metaobjects:generate` (`metaobjects:generate`) | Kotlin generators run through the same goal — see below. **No `--template-spec` flag, deliberately** — `` already loads a consumer class from the project classpath, so the declarative surface is `com.metaobjects.generator.template.TemplateScopeGenerator` wired as an ordinary `` with `