Skip to content
Merged
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ here.**
`fixtures/dependency-conformance/` (the 11 check cases pin the Node `meta deps check`); Kotlin loads through the same loader and adds a codegen test.
Spring runtime auto-load and `verify` drift for dependencies are later slices.

- **C# consumes a shared model at build time (FR-023 Phase 2).** `dotnet meta` now reads
`dependencies` from `.metaobjects/config.json` at every rung of its source ladder, verifies the
committed snapshot against `deps.lock.json` offline, and loads the snapshots ahead of the
project's own files, so an `extends`, `overlay: true` or plain FQN reference onto a dependency's
node resolves. Errors carry the same codes as TypeScript and Python
(`ERR_DEPENDENCY_SNAPSHOT_STALE`, `_METAMODEL_INCOMPATIBLE`, `_NODE_COLLISION`,
`_PACKAGE_NOT_OWNED`), and a load error in a dependency's file names `dep:<name>/<artifact>`.
Imported nodes load but are not generated: every C# generator, the `verify --codegen`
regeneration and the requirement gate's coverage denominator leave them out unless
`scope.include` names their package. C# runs the 26 resolution cases (the 11 check cases pin the Node `meta deps check`) of `fixtures/dependency-conformance/`.
The Node `meta deps sync` stays the only writer of the lock and the snapshot.
See [docs/features/metadata-dependencies.md](docs/features/metadata-dependencies.md).

- **Own your generators, and give back the ones you did not change.** A team is expected to
write generators of its own, because a generator it owns is what keeps output from drifting.
Two things now help it carry fewer copies than it needs. `meta gen` prints one line naming
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ sixth ships two libraries at their own stability labels:
`meta eject <library>`. See [`docs/features/libraries.md`](docs/features/libraries.md).

**Sharing a model across your own projects** *(the TypeScript toolchain publishes;
TypeScript, Python, Java and Kotlin projects consume, from an installed npm or Python
package or over a `path` dependency such as a sibling checkout; C# arrives in Phase 2)* — one project publishes
TypeScript, Python, Java, Kotlin and C# projects consume, from an installed npm or Python
package or over a `path` dependency such as a sibling checkout)* — one project publishes
part of its model (a common `Customer`, an audited base, a set of enums) with
`sharedModelFile()`, and another declares it in `dependencies`, syncs it into a
committed, hash-locked snapshot and builds on it with `extends` and references. When the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ as package data), run `meta gen`, then release the package as usual; consumers
declare an `npm` or `python` dependency on it. With no `version` option the
manifest takes the nearest `package.json` version, else a `pyproject.toml`
`[project]` version — pass `version` when neither is the package consumers install.
Only TypeScript can run it in Phase 1a. TypeScript, Python, Java and
Kotlin (through Java's loader and Maven plugin) can *consume* a dependency, and
only the TypeScript toolchain can *publish* one; C# does not read
`dependencies` yet (Phase 2).
Only TypeScript can run it in Phase 1a. TypeScript, Python, Java, Kotlin (through
Java's loader and Maven plugin) and C# can *consume* a dependency, and only the
TypeScript toolchain can *publish* one.
Full detail: `docs/features/metadata-dependencies.md`.
10 changes: 7 additions & 3 deletions docs/CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
| [`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) | ✓ | inherits via Java | ✓ | ✓ |
| [`fixtures/dependency-conformance/`](../fixtures/dependency-conformance/) | 37 cases (26 resolution + 11 check) + 14 transport cases | ✓ (reference implementation; all three arms) | ✓ (resolution arm, all 26; no check, so the 11 check cases are not run) | inherits via Java | — (Phase 2) | ✓ (resolution arm, all 26 identically; no check yet, so the 11 check cases are not run; the transport arm is Node-only) |
| [`fixtures/dependency-conformance/`](../fixtures/dependency-conformance/) | 37 cases (26 resolution + 11 check) + 14 transport cases | ✓ (reference implementation; all three arms) | ✓ (resolution arm, all 26; no check, so the 11 check cases are not run) | inherits via Java | ✓ (resolution arm, all 26; no check, so the 11 check cases are not run) | ✓ (resolution arm, all 26 identically; no check yet, so the 11 check cases are not run; the transport arm is Node-only) |
| [`fixtures/shared-model-conformance/`](../fixtures/shared-model-conformance/) | 11 cases (7 publish + 4 refusal) | ✓ (reference implementation) | — (Phase 2) | — (Phase 2) | — (Phase 2) | ✓ (serializer arm: the 7 publish cases' document bytes; no publisher) |
| [`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) | — | — | — | — |
Expand Down Expand Up @@ -465,7 +465,7 @@ above: one committed `cases.json`, no per-port fixture, no ledger. The corpus ha

- **Resolution (26 cases).** Every case's `expectFiles`/`expectImported`/`expectSelected`/
`expectMigrateGoverned`/`expectLoadError` assertions. **TypeScript (the reference
implementation), Python and Java all run all 26, identically**, with no exemption. The two
implementation), Python, Java and C# all run all 26, identically**, 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 did not register then, and carried a since-discharged allowlist
Expand All @@ -482,7 +482,11 @@ above: one committed `cases.json`, no per-port fixture, no ledger. The corpus ha
the only writer of snapshots and the lock. `cases.json` also carries an `npm` and a `python`
dependency loading from their committed snapshot with nothing installed, which both ports run.

C# has no runner yet; it arrives in a later Phase 2 slice.
**C#** (S6 of FR-023 Phase 2) runs the resolution arm on two rungs of the source ladder: the
neutral one (`Resolve(null, dir)`), and the explicit metadata directory a
`dotnet meta gen <metadataDir>` takes, where the case has one. Its runner is
`DependencyConformanceTests`; it skips the 11 check cases (C# has no `deps check` yet) and the
transport cases (C# never syncs).

### `fixtures/shared-model-conformance/` (11 cases)

Expand Down
2 changes: 1 addition & 1 deletion docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ command surface splits in two:
| **Template/prompt drift** (`verify --templates`) | **Node `meta`** | `meta verify --templates` | TS reference (ADR-0021 D2) — `{{field}}`↔payload; part of the bare-`verify` default |
| **Vocabulary upgrade** (`upgrade`) | **Node `meta`** | `meta upgrade [--to <version>] [--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 (`path`, or an installed `npm` / `python` package) into a committed snapshot + lock (FR-023), so `sync`/`check`/`list` themselves stay Node-only regardless of your server language. TypeScript, Python and Java then LOAD that snapshot at every rung of their own source ladder (Kotlin loads through Java's loader and Maven plugin); C# doesn't read `dependencies` yet (Phase 2). On a project whose own metadata Node `meta` cannot read (a Maven `src/main/resources/metadata` directory, a C# metadata directory, or another port's vocabulary), `sync` and `check` prove the locked dependencies load together and say that the port's own `verify` is the full check. See [`metadata-dependencies.md`](metadata-dependencies.md) |
| **Dependency sync** (`deps`) | **Node `meta`** | `meta deps sync \| check \| list` | **any backend** — resolves a declared metadata dependency (`path`, or an installed `npm` / `python` package) into a committed snapshot + lock (FR-023), so `sync`/`check`/`list` themselves stay Node-only regardless of your server language. TypeScript, Python and Java then LOAD that snapshot at every rung of their own source ladder (Kotlin loads through Java's loader and Maven plugin); C# loads the snapshot through `dotnet meta` (Phase 2 S6). On a project whose own metadata Node `meta` cannot read (a Maven `src/main/resources/metadata` directory, a C# metadata directory, or another port's vocabulary), `sync` and `check` prove the locked dependencies load together and say that the port's own `verify` is the full check. See [`metadata-dependencies.md`](metadata-dependencies.md) |
| **Canonical format** (`fmt`) | every port | `meta fmt` / `dotnet meta fmt` / `mvn metaobjects:fmt` / `metaobjects fmt` | (#304) ships in **every** port — the canonical serializer already exists in all five, so formatting is local, not Node-only. `--check` lists non-canonical files and exits non-zero without changing anything. Formats each file STANDALONE — own-mode, declared-here layer only (ADR-0039); an `extends` onto another file's base is preserved as-is, never resolved or erred on; a file declaring `overlay: true` anywhere — whether or not a same-file base exists — is reported as skipped rather than merged (merging would drop the overlay marker, a structural change fmt never makes). A YAML file is always skipped — no canonical YAML emitter exists (ADR-0006). Before writing, every port reloads the whole project with the candidate swapped in and refuses the write unless the reloaded model's canonical form is byte-identical to the untouched baseline — fmt never changes meaning. See [Fixture corpus](#meta-fmt--canonical-formatting-304) below and `fixtures/fmt-conformance/README.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 init` / `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` plus `--codegen` when an output directory (`csharp.outDir` in `.metaobjects/config.json`, or `--out`) and `codegen/Generators.cs` are present). **The generator list is `codegen/Generators.cs`** (ADR-0034 Amendment 9): `dotnet meta init` writes it with `codegen/Program.cs` (two lines that run it) and `codegen/Codegen.csproj`, and `gen` / `verify --codegen` always run it through `dotnet run --project codegen`. With no `codegen/Generators.cs` they exit 2 and write nothing; `--generators` only narrows the list to declared entries. Every entry may set its own output directory (`ApiDocsGenerator { OutDir = ... }`), and `verify --codegen` compares each one. A declarative Mustache template-spec (the cross-port JSON contract shared with Python) is one more entry, `TemplateSpecGenerator { Spec = "<json>", TemplateRoot = "<dir>" }`; `gen` no longer reads `--template-spec` or a project-root `template-spec.json`, and a run that finds either with no such entry fails before writing anything; see [declarative template scopes](codegen-concepts.md#declarative-template-scopes). `dotnet meta docs` was removed in 1.1.0: it prints the line that points at [the migration section](migrations/docs-are-generators.md#c) and exits 2 |
Expand Down
48 changes: 31 additions & 17 deletions docs/features/metadata-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,16 +569,31 @@ Then `meta deps sync` as for any other installed package. Nothing about the regi
reaches `.metaobjects/`: the lock records the transport (`npm`/`python` + `dir`), never a
URL.

**Only TypeScript publishes in Phase 1a — TypeScript, Python, Java and Kotlin consume.**
TypeScript, Python, Java and Kotlin can *consume* a dependency (load its snapshot, extend
it, overlay it); C# doesn't read `dependencies` yet (Phase 2). Java reads it at every rung
of the Maven plugin's source ladder (`<sourceDir>`/`<sources>`, the neutral config, the default
directory), and Kotlin inherits that through the same plugin and loader, so a Spring
or Kotlin build consumes a shared model with no extra configuration. Only the TypeScript
toolchain can *publish*: it alone can generate the artifact a publisher ships. A Python,
Java or Kotlin project that wants to publish a shared model runs a TypeScript
`sharedModelFile()` generator against its own metadata the same way any TypeScript consumer
would; a C# project cannot yet participate as either a publisher or a consumer.
**Only TypeScript publishes in Phase 1a — TypeScript, Python, Java, Kotlin and C# consume.**
TypeScript, Python, Java, Kotlin and C# can *consume* a dependency (load its snapshot, extend
it, overlay it). Java reads it at every rung of the Maven plugin's source ladder
(`<sourceDir>`/`<sources>`, the neutral config, the default directory), and Kotlin inherits that
through the same plugin and loader, so a Spring or Kotlin build consumes a shared model with no
extra configuration. Only the TypeScript toolchain can *publish*: it alone can generate the
artifact a publisher ships, and the Node `meta deps sync` is the only writer of the lock and the
snapshot. A Python, Java, Kotlin or C# project that wants to publish a shared model runs a
TypeScript `sharedModelFile()` generator against its own metadata the same way any TypeScript
consumer would.

**C#.** `dotnet meta` reads `dependencies` from `.metaobjects/config.json` at every rung of its
source ladder — C# has no native config surface, so the explicit `<metadataDir>` form reads the
project's file too — verifies the committed snapshot against `deps.lock.json` offline, and loads
the snapshots first under `dep:<name>/<artifact>` source ids. It never writes either: run
`meta deps sync` (the Node CLI) to refresh them, and a stale snapshot fails every command with
`ERR_DEPENDENCY_SNAPSHOT_STALE`. An imported node is excluded from every C# generator, the
`verify --codegen` regeneration and the requirement gate's entity-coverage denominator unless
`scope.include` names its package; the one thing C# still emits from an imported package is the
POCO of a *value object* (and the shared enums it uses) that a generated type reaches — an entity
or local value object that holds it, or a local template whose `@payloadRef` / `@responseRef` names
it — because that generated type needs it to compile. `inScope` is Python's formula (no `matchesScope` on the
project's own objects), and C# has no entity-name positional on `gen`, so the TypeScript
"refuse an explicitly named imported object" check has no C# surface. Known edge: a local entity
that `extends` an imported concrete entity in a table-per-hierarchy mapping is not specially handled.

## Vendoring vs. depending

Expand Down Expand Up @@ -613,10 +628,10 @@ cross-port contract every consumer gets today.
## Phase 2: Java, Kotlin and C#

Tracked in [#482](https://github.com/metaobjectsdev/metaobjects/issues/482). Today TypeScript,
Python, Java and Kotlin consume a dependency (Java and Kotlin shipped in slice S3); C# ignores the
`dependencies` key, and a model there that references a node a dependency would supply fails
loudly (`ERR_UNRESOLVED_OBJECT_REF`), never silently. Phase 2 adds no new contract. The manifest, the
lock, the committed snapshot and the 24-case resolution corpus are plain files that every port
Python, Java, Kotlin and C# consume a dependency (Java and Kotlin shipped in slice S3, C# in S6).
A port that does not read the lock fails loudly on a model that references a node a dependency
would supply (`ERR_UNRESOLVED_OBJECT_REF`), never silently. Phase 2 adds no new contract. The manifest, the
lock, the committed snapshot and the 26-case resolution corpus are plain files that every port
already has the pieces to read; what is missing is each port *loading* the snapshot.

Two rulings shape it:
Expand Down Expand Up @@ -675,9 +690,8 @@ but **not** built yet — do not assume any of the following exists:
- **A runtime `ObjectManager` scope predicate.** Nothing at runtime refuses to serve
an imported entity — "what's imported" is a load-time/codegen concept today, not a
runtime one.
- **C# as a dependency consumer, and Java, Kotlin and C# as publishers** — Java and
Kotlin consume from Phase 2 slice S3; C# arrives in a later slice. Spring apps do not yet
auto-load the snapshot at runtime (S4).
- **Java, Kotlin, Python and C# as publishers** — Java, Kotlin and C# consume (S3 and S6);
publishing is TypeScript only. Spring apps do not yet auto-load the snapshot at runtime (S4).

## Verified by

Expand Down
6 changes: 3 additions & 3 deletions docs/features/metadata-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ sources.** It answers a different question than the ladder above — not "where
my metadata live" but "what does my metadata build on that lives elsewhere" — so a
TypeScript project using a declared `sources` list and a Python project using its
own `metadata:` key both get their declared dependencies resolved the same way, on
top of whichever rung supplied their own files. **TypeScript, Python, Java and Kotlin**
(Kotlin through Java's loader and Maven plugin) — C# doesn't read `dependencies` yet
(Phase 2). See
top of whichever rung supplied their own files. **TypeScript, Python, Java, Kotlin and C#**
(Kotlin through Java's loader and Maven plugin) — each reads `dependencies` at every rung
of its own source ladder. See
[`metadata-dependencies.md`](metadata-dependencies.md).

A config file that exists but is malformed is an error at its own rung; it never
Expand Down
12 changes: 12 additions & 0 deletions docs/ports/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ Drop metadata under `metaobjects/`, for example `metaobjects/meta.blog.json`:
}}
```

### Building on a shared model (optional)

A model published from another repository is declared under `dependencies` in
`.metaobjects/config.json` and synced by the Node CLI (`meta deps sync` writes
`.metaobjects/deps.lock.json` and a committed snapshot under `.metaobjects/deps/`). `dotnet meta`
reads both, at every rung of the source ladder, and never writes them: a snapshot that no longer
matches the lock fails with `ERR_DEPENDENCY_SNAPSHOT_STALE`. The snapshot loads first, so your
own metadata can `extends`, overlay or reference its nodes, but an imported node is not generated
(and is not counted by the requirement gate) unless `scope.include` names its package. What C#
does and does not do is in
[Metadata dependencies](../features/metadata-dependencies.md).

### Custom providers (optional)

If your app needs a metamodel subtype the core doesn't ship, declare an
Expand Down
Loading
Loading