Skip to content

Commit 198d75d

Browse files
authored
feat(csharp): consume shared dependency models at build time (#508)
* feat(csharp): consume a shared model at build time (FR-023 S6) dotnet meta 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 under `dep:<name>/<artifact>` source ids. Same ERR_DEPENDENCY_* codes as TypeScript and Python; the Node `meta deps sync` stays the only writer of lock and snapshot. Imported nodes load but are not generated: the codegen runner, report rows, shared enums, API-docs model, template scope walk, the verify --codegen regeneration and the requirement gate's coverage denominator all leave them out unless scope.include names their package. An imported value object a generated entity holds is still emitted, since the entity's property type needs it. C# runs all 23 cases of fixtures/dependency-conformance/ on the neutral ladder and the explicit metadata-directory rung, plus codegen, CLI and unit tests. * no-mistakes(review): Emit imported value shapes reached by local shapes, templates * fix(csharp): run only the dependency corpus's resolution arm after rebasing onto main main's corpus now carries 11 deps-check cases (expectCheck) beside the 24 resolution cases; C# has no check yet, so its runner skips them. Update the Phase 2 prose and the changelog count to match. * no-mistakes(review): apply reviewer inline fixes * no-mistakes(document): Docs now say C# consumes shared models (FR-023 S6); stale "C# doesn't read dependencies" claims fixed in README, cli, metadata-sources, metadata-dependencies, spec/roadmap, and the agent-context codegen reference plus its five mirrored fixture copies.
1 parent 7b67aba commit 198d75d

36 files changed

Lines changed: 1467 additions & 100 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,19 @@ here.**
108108
`fixtures/dependency-conformance/` (the 11 check cases pin the Node `meta deps check`); Kotlin loads through the same loader and adds a codegen test.
109109
Spring runtime auto-load and `verify` drift for dependencies are later slices.
110110

111+
- **C# consumes a shared model at build time (FR-023 Phase 2).** `dotnet meta` now reads
112+
`dependencies` from `.metaobjects/config.json` at every rung of its source ladder, verifies the
113+
committed snapshot against `deps.lock.json` offline, and loads the snapshots ahead of the
114+
project's own files, so an `extends`, `overlay: true` or plain FQN reference onto a dependency's
115+
node resolves. Errors carry the same codes as TypeScript and Python
116+
(`ERR_DEPENDENCY_SNAPSHOT_STALE`, `_METAMODEL_INCOMPATIBLE`, `_NODE_COLLISION`,
117+
`_PACKAGE_NOT_OWNED`), and a load error in a dependency's file names `dep:<name>/<artifact>`.
118+
Imported nodes load but are not generated: every C# generator, the `verify --codegen`
119+
regeneration and the requirement gate's coverage denominator leave them out unless
120+
`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/`.
121+
The Node `meta deps sync` stays the only writer of the lock and the snapshot.
122+
See [docs/features/metadata-dependencies.md](docs/features/metadata-dependencies.md).
123+
111124
- **Own your generators, and give back the ones you did not change.** A team is expected to
112125
write generators of its own, because a generator it owns is what keeps output from drifting.
113126
Two things now help it carry fewer copies than it needs. `meta gen` prints one line naming

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ sixth ships two libraries at their own stability labels:
277277
`meta eject <library>`. See [`docs/features/libraries.md`](docs/features/libraries.md).
278278

279279
**Sharing a model across your own projects** *(the TypeScript toolchain publishes;
280-
TypeScript, Python, Java and Kotlin projects consume, from an installed npm or Python
281-
package or over a `path` dependency such as a sibling checkout; C# arrives in Phase 2)* — one project publishes
280+
TypeScript, Python, Java, Kotlin and C# projects consume, from an installed npm or Python
281+
package or over a `path` dependency such as a sibling checkout)* — one project publishes
282282
part of its model (a common `Customer`, an audited base, a set of enums) with
283283
`sharedModelFile()`, and another declares it in `dependencies`, syncs it into a
284284
committed, hash-locked snapshot and builds on it with `extends` and references. When the

‎agent-context/skills/metaobjects-codegen/references/dependencies-and-shared-models.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ as package data), run `meta gen`, then release the package as usual; consumers
4444
declare an `npm` or `python` dependency on it. With no `version` option the
4545
manifest takes the nearest `package.json` version, else a `pyproject.toml`
4646
`[project]` version — pass `version` when neither is the package consumers install.
47-
Only TypeScript can run it in Phase 1a. TypeScript, Python, Java and
48-
Kotlin (through Java's loader and Maven plugin) can *consume* a dependency, and
49-
only the TypeScript toolchain can *publish* one; C# does not read
50-
`dependencies` yet (Phase 2).
47+
Only TypeScript can run it in Phase 1a. TypeScript, Python, Java, Kotlin (through
48+
Java's loader and Maven plugin) and C# can *consume* a dependency, and only the
49+
TypeScript toolchain can *publish* one.
5150
Full detail: `docs/features/metadata-dependencies.md`.

‎docs/CONFORMANCE.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ regenerate with `ls -d fixtures/<corpus>/*/ | wc -l` for directory-shaped corpor
4848
| [`fixtures/provider-composition-conformance/`](../fixtures/provider-composition-conformance/) | 9 (5 error-shape + 4 compose-load) | ✓ | ✓ | — (JVM registry via Java) | ✓ | ✓ |
4949
| [`fixtures/source-resolution-conformance/`](../fixtures/source-resolution-conformance/) | 25 cases | ✓ (reference implementation) | ✓ | inherits via Java | ✓ | ✓ |
5050
| [`fixtures/scope-conformance/`](../fixtures/scope-conformance/) | 10 cases | ✓ (reference implementation) | ✓ | inherits via Java | ✓ | ✓ |
51-
| [`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) |
51+
| [`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) |
5252
| [`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) |
5353
| [`fixtures/agent-context-conformance/`](../fixtures/agent-context-conformance/) | 4 | ✓ (the emitter is TS-owned) | — | — | — | — |
5454
| [`fixtures/metamodel-docs/`](../fixtures/metamodel-docs/) | 1 | ✓ (docs emit is TS-owned) | — | — | — | — |
@@ -465,7 +465,7 @@ above: one committed `cases.json`, no per-port fixture, no ledger. The corpus ha
465465

466466
- **Resolution (26 cases).** Every case's `expectFiles`/`expectImported`/`expectSelected`/
467467
`expectMigrateGoverned`/`expectLoadError` assertions. **TypeScript (the reference
468-
implementation), Python and Java all run all 26, identically**, with no exemption. The two
468+
implementation), Python, Java and C# all run all 26, identically**, with no exemption. The two
469469
overlay cases that need a view child as incidental content use `view.currency` (the one
470470
concrete `view.*` subtype registered cross-port), not `view.text` — a prior revision used
471471
`view.text`, which Python did not register then, and carried a since-discharged allowlist
@@ -482,7 +482,11 @@ above: one committed `cases.json`, no per-port fixture, no ledger. The corpus ha
482482
the only writer of snapshots and the lock. `cases.json` also carries an `npm` and a `python`
483483
dependency loading from their committed snapshot with nothing installed, which both ports run.
484484

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

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

‎docs/features/cli.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ command surface splits in two:
3333
| **Template/prompt drift** (`verify --templates`) | **Node `meta`** | `meta verify --templates` | TS reference (ADR-0021 D2) — `{{field}}`↔payload; part of the bare-`verify` default |
3434
| **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 |
3535
| **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) |
36-
| **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) |
36+
| **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) |
3737
| **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` |
3838
| 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) |
3939
| 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 |

‎docs/features/metadata-dependencies.md‎

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -569,16 +569,31 @@ Then `meta deps sync` as for any other installed package. Nothing about the regi
569569
reaches `.metaobjects/`: the lock records the transport (`npm`/`python` + `dir`), never a
570570
URL.
571571

572-
**Only TypeScript publishes in Phase 1a — TypeScript, Python, Java and Kotlin consume.**
573-
TypeScript, Python, Java and Kotlin can *consume* a dependency (load its snapshot, extend
574-
it, overlay it); C# doesn't read `dependencies` yet (Phase 2). Java reads it at every rung
575-
of the Maven plugin's source ladder (`<sourceDir>`/`<sources>`, the neutral config, the default
576-
directory), and Kotlin inherits that through the same plugin and loader, so a Spring
577-
or Kotlin build consumes a shared model with no extra configuration. Only the TypeScript
578-
toolchain can *publish*: it alone can generate the artifact a publisher ships. A Python,
579-
Java or Kotlin project that wants to publish a shared model runs a TypeScript
580-
`sharedModelFile()` generator against its own metadata the same way any TypeScript consumer
581-
would; a C# project cannot yet participate as either a publisher or a consumer.
572+
**Only TypeScript publishes in Phase 1a — TypeScript, Python, Java, Kotlin and C# consume.**
573+
TypeScript, Python, Java, Kotlin and C# can *consume* a dependency (load its snapshot, extend
574+
it, overlay it). Java reads it at every rung of the Maven plugin's source ladder
575+
(`<sourceDir>`/`<sources>`, the neutral config, the default directory), and Kotlin inherits that
576+
through the same plugin and loader, so a Spring or Kotlin build consumes a shared model with no
577+
extra configuration. Only the TypeScript toolchain can *publish*: it alone can generate the
578+
artifact a publisher ships, and the Node `meta deps sync` is the only writer of the lock and the
579+
snapshot. A Python, Java, Kotlin or C# project that wants to publish a shared model runs a
580+
TypeScript `sharedModelFile()` generator against its own metadata the same way any TypeScript
581+
consumer would.
582+
583+
**C#.** `dotnet meta` reads `dependencies` from `.metaobjects/config.json` at every rung of its
584+
source ladder — C# has no native config surface, so the explicit `<metadataDir>` form reads the
585+
project's file too — verifies the committed snapshot against `deps.lock.json` offline, and loads
586+
the snapshots first under `dep:<name>/<artifact>` source ids. It never writes either: run
587+
`meta deps sync` (the Node CLI) to refresh them, and a stale snapshot fails every command with
588+
`ERR_DEPENDENCY_SNAPSHOT_STALE`. An imported node is excluded from every C# generator, the
589+
`verify --codegen` regeneration and the requirement gate's entity-coverage denominator unless
590+
`scope.include` names its package; the one thing C# still emits from an imported package is the
591+
POCO of a *value object* (and the shared enums it uses) that a generated type reaches — an entity
592+
or local value object that holds it, or a local template whose `@payloadRef` / `@responseRef` names
593+
it — because that generated type needs it to compile. `inScope` is Python's formula (no `matchesScope` on the
594+
project's own objects), and C# has no entity-name positional on `gen`, so the TypeScript
595+
"refuse an explicitly named imported object" check has no C# surface. Known edge: a local entity
596+
that `extends` an imported concrete entity in a table-per-hierarchy mapping is not specially handled.
582597

583598
## Vendoring vs. depending
584599

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

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

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

682696
## Verified by
683697

‎docs/features/metadata-sources.md‎

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

6565
A config file that exists but is malformed is an error at its own rung; it never

‎docs/ports/csharp.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ Drop metadata under `metaobjects/`, for example `metaobjects/meta.blog.json`:
106106
}}
107107
```
108108

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

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

0 commit comments

Comments
 (0)