diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f71c20e7..8b9f08676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,16 @@ loading. A port that reports `1.2` does not know that vocabulary; upgrade every ### Added +- **Java, Kotlin and C#: publish a shared model (FR-023).** A `shared-model` generator in every one + of the three ports writes the artifact `.metaobjects.json` and the manifest + `metaobjects.pkg.json`, byte-identical to the TypeScript `sharedModelFile()` and gated by + `fixtures/shared-model-conformance/` (the engine is `SharedModel` in the Java `metadata` module + and `MetaObjects.Config.SharedModel` in C#). Java and Kotlin run it through `metaobjects:generate` + (version from the project's ``) and eject it with `mvn metaobjects:eject`; C# declares + `SharedModelGenerator` in `codegen/Generators.cs` (version from ``) and ejects it with + `dotnet meta eject shared-model`. `generator-registry-conformance` now lists `java`, `kotlin` and + `csharp` under `shared-model`. The Node `meta deps sync` remains the only writer of the lock and + the snapshot. - **Java and Kotlin: a Spring app loads a shared model's committed snapshot at runtime, and `metaobjects:verify` checks dependency drift.** `metaobjects:generate` now ships `.metaobjects/deps.lock.json` and `.metaobjects/deps/**` as resources (target path `.metaobjects`) diff --git a/README.md b/README.md index a5749c8e2..4748a8cb9 100644 --- a/README.md +++ b/README.md @@ -276,11 +276,11 @@ sixth ships two libraries at their own stability labels: into its `/db` layer (`["iam", "iam/db"]`). Copy one into your repo with `meta eject `. See [`docs/features/libraries.md`](docs/features/libraries.md). -**Sharing a model across your own projects** *(the TypeScript toolchain 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 +**Sharing a model across your own projects** *(TypeScript, Java, Kotlin and C# projects +publish; 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 its `shared-model` +generator (`sharedModelFile()` in TypeScript), and another declares it in `dependencies`, syncs it into a committed, hash-locked snapshot and builds on it with `extends` and references. When the publisher's model moves, `meta verify --deps` says so, instead of two copies drifting apart. See [`docs/features/metadata-dependencies.md`](docs/features/metadata-dependencies.md). diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index 87ac9847c..0a680b69d 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -50,7 +50,7 @@ regenerate with `ls -d fixtures//*/ | wc -l` for directory-shaped corpor | [`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; check arm, all 11) | inherits via Java | ✓ (resolution arm, all 26; check arm, all 11, as `verify --deps`) | ✓ (resolution arm, all 26 identically; check arm, all 11, as `deps check` and `verify --deps`; 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/shared-model-conformance/`](../fixtures/shared-model-conformance/) | 11 cases (7 publish + 4 refusal) | ✓ (reference implementation) | ✓ (all 11; `SharedModelConformanceTest`) | inherits via Java (`KotlinSharedModelGeneratorTest` pins the generator) | ✓ (all 11; `SharedModelConformanceTests`) | ✓ (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) | — | — | — | — | | [`fixtures/fmt-conformance/`](../fixtures/fmt-conformance/) (#304 — `meta fmt`) | 12 | ✓ (reference) | ✓ | inherits via Java | ✓ | ✓ | @@ -514,7 +514,9 @@ the manifest records the toolchain's `metamodelVersion`, so they do not move wit the serializer arm**: for each publish case it loads `files`, takes the nodes the pinned manifest lists, and `serialize_shared_document` must reproduce the artifact byte for byte (and the sha256 the manifest records). Python has no publisher, so selection and the refusals are TypeScript's. -Java, Kotlin and C# have no runner (Phase 2). +**Java runs all 11**, in `metadata`'s `SharedModelConformanceTest` against the `SharedModel` engine +the `shared-model` generator calls; **Kotlin** runs through that same engine and Maven goals and adds +only `KotlinSharedModelGeneratorTest`. **C# runs all 11**, in `SharedModelConformanceTests`. > **ADR-0055 (deferred overlay application) — SHIPPED in every port.** The nine cases added > for it (`overlay-mixed-file-base-in-later-file`, `overlay-mixed-file-base-in-earlier-file`, diff --git a/docs/features/metadata-dependencies.md b/docs/features/metadata-dependencies.md index 7bde769f9..b7eb6ca81 100644 --- a/docs/features/metadata-dependencies.md +++ b/docs/features/metadata-dependencies.md @@ -408,8 +408,8 @@ publisher reachable. ## Publishing a dependency: `sharedModelFile()` -A publisher wires the `shared-model` generator (TypeScript only, in Phase 1a) to -select a subset of its own metadata by the same include/exclude scope-pattern +A publisher wires the `shared-model` generator (TypeScript, Java, Kotlin and C#; Python +consumes only) to select a subset of its own metadata by the same include/exclude scope-pattern grammar `scope` already uses, and emit it as one canonical-JSON artifact plus a manifest: @@ -580,16 +580,18 @@ 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, Kotlin and C# consume.** +**TypeScript, Java, Kotlin and C# publish; 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 (``/``, 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. +extra configuration. Java, Kotlin and C# can also *publish*: each ships a `shared-model` +generator (Java and Kotlin through the Maven plugin's `metaobjects:generate`, C# through +`dotnet meta gen`; all three ejectable) that writes the artifact and manifest, byte-identical to +the TypeScript one and gated by `fixtures/shared-model-conformance/`. The Node `meta deps sync` +stays the only writer of the lock and the snapshot, in every port. A Python project that wants +to publish a shared model runs a TypeScript `sharedModelFile()` generator against its own +metadata. **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 `` form reads the @@ -710,8 +712,7 @@ 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. -- **Java, Kotlin, Python and C# as publishers** — Java, Kotlin and C# consume (S3 and S6); - publishing is TypeScript only. +- **Python as a publisher** — Java, Kotlin and C# publish (S8 and S9); Python consumes only. - **`npm` and `python` transports in the Python and C# check.** Both ports' check resolves `path` only; an `npm` or `python` dependency reports `unresolved` and names the Node `meta deps check`, because resolving it needs the consumer's installed packages (a check that cannot check diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md index b74b1cbb1..9f2ef9935 100644 --- a/docs/features/own-your-codegen.md +++ b/docs/features/own-your-codegen.md @@ -358,6 +358,10 @@ import { sharedModelFile } from "./codegen/generators/shared-model.js"; generators: [sharedModelFile({ name: "acme-common", include: ["acme::common::**"] })], ``` +Java, Kotlin and C# publish with the same generator and eject it with their own commands; see +[Java and Kotlin](../ports/java.md#publishing-a-shared-model), [Kotlin](../ports/kotlin.md#publishing-a-shared-model-shared-model) +and [C#](../ports/csharp.md#publishing-a-shared-model-shared-model). + An unchanged copy publishes exactly what the packaged generator publishes, and a consumer's sync accepts it. The two functions are in your project now, so they are yours to change, and a consumer is the one that checks them: the selection, the closure, the bytes, the hash and diff --git a/docs/llms/llms-full.txt b/docs/llms/llms-full.txt index ce7886bf3..30d79d649 100644 --- a/docs/llms/llms-full.txt +++ b/docs/llms/llms-full.txt @@ -117,7 +117,7 @@ A library is **layered, and its core layer is inert**: the core declares no `sou ### Sharing a model across your own projects -Today only the TypeScript toolchain publishes; TypeScript, Python, Java, Kotlin and C# projects consume, from an installed npm or Python package or over a `path` dependency (a sibling checkout or submodule). Maven and NuGet transports are design only. One project publishes part of its model with the `sharedModelFile()` generator; another declares it under `dependencies` in `.metaobjects/config.json`, runs `meta deps sync` to commit a hash-locked snapshot, and builds on it with `extends`, overlays and references. When the publisher's model moves, `meta verify --deps` reports it instead of two copies drifting apart. See docs/features/metadata-dependencies.md. +TypeScript, Java, Kotlin and C# projects publish; TypeScript, Python, Java, Kotlin and C# projects consume, from an installed npm or Python package or over a `path` dependency (a sibling checkout or submodule). Maven and NuGet transports are design only. One project publishes part of its model with its `shared-model` generator (`sharedModelFile()` in TypeScript); another declares it under `dependencies` in `.metaobjects/config.json`, runs `meta deps sync` to commit a hash-locked snapshot, and builds on it with `extends`, overlays and references. When the publisher's model moves, `meta verify --deps` reports it instead of two copies drifting apart. See docs/features/metadata-dependencies.md. --- diff --git a/docs/llms/llms.txt b/docs/llms/llms.txt index 55839597a..eb0760826 100644 --- a/docs/llms/llms.txt +++ b/docs/llms/llms.txt @@ -63,7 +63,7 @@ MetaObjects pillars are capabilities of one metadata model, not separate product - **Requirements and testing** — declare what the software is supposed to *do* in the same model as the entities, so a capability claim is checkable instead of prose. `requirement.functional` / `requirement.architectural` are registered vocabulary in all five ports, and the loader enforces the closed `@status` enum. `@implementedBy` is checked against the model, not taken on faith: it must name a real member of the real model, so a claim whose implementation was renamed or deleted fails the build instead of going quietly stale. `meta verify` reports the ledger on every run — unresolved links, entities no claim covers, gaps recorded versus gaps nobody has ruled on — and lints the authoring; the documentation generators render it for humans and for agents (`requirementsFile()`, `agentDocsFile()` and their counterparts in the other ports). A live claim naming nothing is a warning; a green run proves the references resolve, never that an implementation is correct. **Every port ships a `requirement-tests` generator.** It scaffolds a test per claim, carrying the statement and counterexample in: a stub you fill in for TypeScript, a test that calls a witness you own in the other four. The vocabulary and the `verify` checks are in every port too. Only the authoring lint is TypeScript-only. A project that declares no `requirement.*` nodes sees no change at all. - **Libraries** — reusable declared design you opt into by name (`"libraries": ["iam"]` in `.metaobjects/config.json`): entities, the requirements they promise and the generators they imply, as one drift-gated artifact. A library is layered and its core layer is inert: it declares no source, so it adds no tables until you also opt into its `/db` layer (`["iam", "iam/db"]`). `meta eject ` copies one into your repo to own. `ai` (the LLM-call trace envelope) is `stable`; `iam` (users, nestable groups, roles, grants) is `preview`. -**Sharing a model across your own projects** — today only the TypeScript toolchain publishes; TypeScript, Python, Java, Kotlin and C# projects consume, from an installed npm or Python package or over a `path` dependency (a sibling checkout or submodule). Maven and NuGet transports are design only. One project publishes part of its model with the `sharedModelFile()` generator; another declares it under `dependencies` in `.metaobjects/config.json`, runs `meta deps sync` to commit a hash-locked snapshot, and builds on it with `extends` and references. When the publisher's model moves, `meta verify --deps` reports it instead of two copies drifting apart. +**Sharing a model across your own projects** — TypeScript, Java, Kotlin and C# projects publish; TypeScript, Python, Java, Kotlin and C# projects consume, from an installed npm or Python package or over a `path` dependency (a sibling checkout or submodule). Maven and NuGet transports are design only. One project publishes part of its model with its `shared-model` generator (`sharedModelFile()` in TypeScript); another declares it under `dependencies` in `.metaobjects/config.json`, runs `meta deps sync` to commit a hash-locked snapshot, and builds on it with `extends` and references. When the publisher's model moves, `meta verify --deps` reports it instead of two copies drifting apart. ## Implementations (npm `1.2.0` / Maven Central `8.2.0`) diff --git a/docs/ports/csharp.md b/docs/ports/csharp.md index f8406b5c7..dc01f83ab 100644 --- a/docs/ports/csharp.md +++ b/docs/ports/csharp.md @@ -283,6 +283,33 @@ the attribute, node and file, and gen generates anyway. `dotnet meta gen --stric `verify` does (ADR-0023): the same `ERR_UNKNOWN_ATTR` lines with a gen-specific hint (drop `--strict`), exit 1, before any file is written. `verify` itself stays strict unless you pass `--lax`. +## Publishing a shared model (`shared-model`) + +A C# project can publish part of its model for another repository to consume (FR-023). The +`shared-model` generator writes `.metaobjects.json` and the manifest +`metaobjects.pkg.json` that pins it by `sha256`, byte-identical to what the TypeScript generator +writes (`fixtures/shared-model-conformance/`). Declare it in `codegen/Generators.cs`: + +```csharp +new SharedModelGenerator +{ + ModelName = "acme-common", + Include = ["acme::common::**"], + Files = ["metaobjects/meta.common.json"], + OutDir = "shared", +} +``` + +`ModelName`, `Include` and `Files` are required; `Exclude` removes patterns from `Include`, and +`Version` overrides the version recorded in the manifest, which otherwise is the project's +`` (from the project file, else the nearest `Directory.Build.props`). It writes nothing and +names the option to change when the selection is empty, is not closed over `extends` and reference +attributes, or needs vocabulary outside the library's. Give `OutDir` a directory that holds nothing +else. `requirement.*` and `template.*` nodes are never published. `dotnet meta eject shared-model` +copies the generator into your project; the artifact bytes stay in the package, because a copy that +changed them would publish a model a consumer's sync refuses. The consumer runs the Node `meta deps +sync`, which stays the only writer of the lock and the snapshot. + ## Write your own generator A generator is a class that implements `IGenerator` (`MetaObjects.Codegen/Generator.cs`): a diff --git a/docs/ports/java.md b/docs/ports/java.md index 02da98cc6..c168e6ff6 100644 --- a/docs/ports/java.md +++ b/docs/ports/java.md @@ -423,6 +423,7 @@ name in every port. | `value-object` | `SpringValueObjectGenerator` | `metaobjects-codegen-spring` | One Java 21 `record` per concrete `object.value` and per sourceless `object.projection`, in the value object's own package. It carries jakarta bean-validation constraints plus `@Valid` on nested members, so a VO jsonb column POSTs and PATCHes with validation cascading to depth ≥ 2. It is THE Java type for the value object (ADR-0056): `Dto` / `Patch` bind to it, a render helper takes it, and a response parser returns it. The template tier declares no copy, so it needs this generator in the same run. | | `names` | `SpringNamesGenerator` | `metaobjects-codegen-spring` | One `Names.java` per object with a declared/inherited primary `source.rdb` — `public static final` physical database name constants (table/view name, schema, per-field columns). See "`Names`" below. | | `requirement-tests` | `JUnitRequirementTestsGenerator` | `metaobjects-codegen-spring` | Per metamodel package that holds a tested requirement, two files in `testPackage`: `Requirements__Witnesses.java` (an interface with one default member per non-skipped test, failing with `unimplemented requirement: ...`) and `Requirements__Test.java` (one JUnit Jupiter `@Test` per requirement, calling that member on your `witnessClass`; a planned or retired requirement is `@Disabled`). Args: `testPackage` and `witnessClass` (required), `grain` (`concern` or `member`), `filter` and `renderer` (class names on your project classpath), `warnUncovered`. Your test classpath needs `org.junit.jupiter:junit-jupiter-api`. A model with no requirement writes nothing. See "Requirement tests" below. | +| `shared-model` | `SharedModelGenerator` | `metaobjects-codegen-base` | FR-023 publishing: writes `.metaobjects.json` (the shared model artifact) and `metaobjects.pkg.json` (its manifest) into `outputDir`, from the top-level nodes your `include` (and optional `exclude`) scope selects out of `files`. Args: `outputDir`, `name`, `include` and `files` (comma-separated paths, resolved against the module basedir) are required; `version` is optional and defaults to the Maven project's ``. A selection that is empty, not closed over `extends` and reference attributes, or that uses vocabulary outside the core set is refused with the reason, and nothing is written. See "Publishing a shared model" below. | | `entity` | `JavaObjectCodeGenerator` | `metaobjects-codegen-base` | Flavor-selected via the `flavor` generator arg (`com.metaobjects.generator.direct.object.javacode`). `flavor=pojoAware` emits `class extends PojoObject` — a concrete `MetaObjectAware` class whose inherited `getMetaData()` back-reference breaks a default Jackson/Gson mapper (see [Serializing generated objects](#serializing-generated-objects) below). `flavor=valueObject` emits a map-backed `class extends ValueObject` instead. Either flavor also emits a self-registering `ObjectClassBindingProvider`. It writes no `Extractor`: list `extractor` for those (8.0 wrote them from `entity` when no `extractor` was listed; see [the migration](../features/migrations/docs-are-generators.md#java-and-kotlin)). For a plain default-Jackson-friendly type, use the `codegen-spring` record surface instead — never `pojoAware`. | | `output-parser` | `SpringOutputParserGenerator` | `metaobjects-codegen-spring` | One `