diff --git a/CHANGELOG.md b/CHANGELOG.md
index 49302bb73..c7d6deb31 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,41 @@ here.**
## [Unreleased]
+### Fixed
+
+- **A fresh `npm i @metaobjectsdev/cli` no longer reports high-severity advisories.**
+ `@metaobjectsdev/sdk` declared `zod` `^3.23.0`, which resolves to a release the zod advisory
+ GHSA-mrhx-c7xc-m3w5 covers (fixed in 4.6.6), and npm installed it beside the `zod` 4 the CLI
+ already used. The sdk now declares `^4.6.6` and reads its config with the zod 4 API; the
+ config it accepts is unchanged, and a rejected config still names each offending path, with
+ the reason now in zod 4's wording.
+- **C#: `dotnet meta verify --templates
` fails when `` does not exist.** It exited 0
+ with "OK", since nothing in an empty root can drift. It now exits 2 and names the directory.
+ A bare `dotnet meta verify ` runs the templates gate its help describes, reading
+ `prompts/` when that directory exists and `templates/` otherwise, where it used to exit 2 asking
+ for a root. The generated-code gate is `verify --codegen --out `, as before.
+- **C#: the generated create route no longer warns CS8600.** `Author input;` is now `Author? input;`.
+ The next regeneration changes that one line in every `*Routes.g.cs`.
+- **C#: the drift message says `gen` needs `--out`.** "Run 'dotnet meta gen'" on its own exits 2.
+- **Java and Kotlin: the stale-code message names a command that runs.** When the generate
+ configuration sits inside an ``, `mvn metaobjects:verify` now suggests
+ `mvn metaobjects:generate@`; a bare `mvn metaobjects:generate` fails there with "No
+ element was defined".
+
+### Documentation
+
+- The Java, Kotlin and C# quickstarts in `docs/ports/` work as written on a clean machine against
+ the published packages: the Java and Kotlin poms carry a Java level, the dependencies the
+ generated code compiles against and a plugin-level `` (Kotlin also its
+ `` and `kotlin-maven-plugin`), and the C# page fills the generator list before the
+ first `gen`, gives the app project, serializes enums as strings, marks fields `@filterable` and
+ ends in the real drift gate.
+- The README opens with a copy-paste quick start, and its Languages table, the port pages and the
+ Python floor name 1.1.0 / 8.1.0.
+- `CONTRIBUTING.md` says one thing about CI: GitHub Actions runs `scripts/ci-local.sh` — the
+ leak scan on every pull request, the conformance and integration lanes on `main`, nightly
+ and on release tags.
+
### Changed
- **Python: `metaobjects gen` and `verify --codegen` run ruff as its own binary.** Every emitted
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9b7cd3e79..2b2ffc2c0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -25,10 +25,12 @@ conformance corpora. This guide covers how to propose changes.
1. **Fork** the repo and create a branch from `main`.
2. Make your change following the discipline below — **tests first (TDD)**.
3. Run the gates locally with **`scripts/ci-local.sh --quick`** and make sure it's
- green. **This matters:** nothing in `.github/workflows/` runs any more — see the
+ green. **This matters:** on a pull request GitHub Actions runs only the leak scan
+ (`hygiene.yml`); the conformance and integration lanes run on `main`, nightly and on
+ release tags, so your local run is the only check of the rest before merge — see the
*Local CI* section below.
4. Open a **pull request** against `main` and fill out the PR template.
-5. A maintainer runs the full gates locally and approves before merge.
+5. A maintainer reviews the change and the checks on the pull request before merge.
## The cardinal rule: the metamodel is the spine
@@ -71,14 +73,15 @@ cd server/python && pytest # Python (in its .venv)
Cross-language persistence / api-contract corpora (Docker + Testcontainers) run via
`scripts/integration-test.sh`.
-### Local CI (this IS the CI — run it before opening/merging a PR)
+### Local CI (the checks GitHub runs — run it before opening a PR)
-GitHub Actions is enabled on this repository. The three check workflows in
-`.github/workflows/` run on GitHub as thin wrappers over `scripts/ci-local.sh`,
-the single definition of the checks: each wrapper checks out, installs the
-toolchains its lane needs, and calls the script, so a local run is the same
-check a workflow run would be. Run the script locally before opening or merging
-a PR, so nothing red leaves your machine:
+GitHub Actions is enabled on this repository. The check workflows in
+`.github/workflows/` are thin wrappers over `scripts/ci-local.sh`, the single
+definition of the checks: each wrapper checks out, installs the toolchains its
+lane needs, and calls the script, so a local run is the same check a workflow
+run would be. A pull request runs only the leak scan; the other lanes run on
+`main`, nightly and on release tags. Run the script locally before opening or
+merging a PR, so nothing red reaches `main`:
```bash
scripts/ci-local.sh # full parity: hygiene.yml's leak-scan, all-port
diff --git a/README.md b/README.md
index 8c7659fa3..4de1d9573 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,19 @@ at something real, not that the something is correct.
> **Scope.** It protects what the model declares; your hand-written logic is still
> yours.
+## Quick start
+
+```bash
+npm i @metaobjectsdev/cli && npx meta init # scaffold metaobjects/, the config and agent skills
+# declare an entity in metaobjects/ (a 10-line model: metaobjects.dev/getting-started)
+npx meta gen --list --probe --format text # what your model can generate
+npx meta eject entity queries names barrel # copy them in; add the lines it prints to metaobjects.config.ts
+npx meta gen && npx meta verify --codegen # generate, then fail if generated code has drifted
+```
+
+Java, Kotlin, C# and Python start the same way from their own package: see
+[`docs/ports/`](docs/ports/).
+
The metamodel is the **durable spine**; generated code is the **disposable
artifact**. Substrate is local-first: typed metadata lives in your repo as YAML or
JSON you own. The entity/model tier of the output is dependency-free, and the optional
@@ -133,11 +146,11 @@ first-week wedge plan — and `meta init` picks up from there.
| Language | Status | Quickstart | Source |
|---|---|---|---|
-| TypeScript | **npm `1.0.13`** — the `@metaobjectsdev/*` packages | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) |
-| Java | **Maven Central `8.0.13`** (`com.metaobjects:*` — the JVM major is npm major + 7) — loader + OMDB + render + Maven plugin all shipped; full conformance green | [`docs/ports/java.md`](docs/ports/java.md) | [`server/java/`](server/java/) |
-| Kotlin | **Maven Central `8.0.13`** — codegen tier on top of Java: 16 generators (entity, Exposed table, names, relations, repository, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller, requirement-tests, api-docs); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
-| C# | **NuGet `1.0.13`** — loader + conformance + EF Core codegen + render engine + `dotnet meta` CLI all shipped | [`docs/ports/csharp.md`](docs/ports/csharp.md) | [`server/csharp/`](server/csharp/) |
-| Python | **PyPI `1.0.13`** — loader + conformance + render + entity-model codegen + ObjectManager runtime shipped; schema migrations are TS-owned (ADR-0015) | [`docs/ports/python.md`](docs/ports/python.md) | [`server/python/`](server/python/) |
+| TypeScript | **npm `1.1.0`** — the `@metaobjectsdev/*` packages | [`docs/ports/typescript.md`](docs/ports/typescript.md) | [`server/typescript/`](server/typescript/) · [`client/web/`](client/web/) |
+| Java | **Maven Central `8.1.0`** (`com.metaobjects:*` — the JVM major is npm major + 7) — loader + OMDB + render + Maven plugin all shipped; full conformance green | [`docs/ports/java.md`](docs/ports/java.md) | [`server/java/`](server/java/) |
+| Kotlin | **Maven Central `8.1.0`** — codegen tier on top of Java: 16 generators (entity, Exposed table, names, relations, repository, output-parser, output-prompt, render-helper, extractor, filter-allowlist, validator, Spring config, storedProc, Spring controller, requirement-tests, api-docs); 24 / 24 persistence-conformance | [`docs/ports/kotlin.md`](docs/ports/kotlin.md) | [`server/java/codegen-kotlin/`](server/java/codegen-kotlin/) · [`server/java/metadata-ktx/`](server/java/metadata-ktx/) |
+| C# | **NuGet `1.1.0`** — loader + conformance + EF Core codegen + render engine + `dotnet meta` CLI all shipped | [`docs/ports/csharp.md`](docs/ports/csharp.md) | [`server/csharp/`](server/csharp/) |
+| Python | **PyPI `1.1.0`** — loader + conformance + render + entity-model codegen + ObjectManager runtime shipped; schema migrations are TS-owned (ADR-0015) | [`docs/ports/python.md`](docs/ports/python.md) | [`server/python/`](server/python/) |
## Capability matrix
@@ -308,7 +321,7 @@ metaobjects/
|---|---|
| TypeScript | `npm i @metaobjectsdev/cli && npx meta init` → [`docs/ports/typescript.md`](docs/ports/typescript.md) |
| Java | Add `metaobjects-maven-plugin` to your `pom.xml` → [`docs/ports/java.md`](docs/ports/java.md) |
-| Kotlin | Add `metaobjects-codegen-kotlin` + `metaobjects-metadata-ktx` → [`docs/ports/kotlin.md`](docs/ports/kotlin.md) |
+| Kotlin | Add `metaobjects-metadata-ktx` and the Maven plugin (it bundles the Kotlin generators) → [`docs/ports/kotlin.md`](docs/ports/kotlin.md) |
| C# | `dotnet tool install --global MetaObjects.Cli` → [`docs/ports/csharp.md`](docs/ports/csharp.md) |
| Python | `pip install metaobjects` → [`docs/ports/python.md`](docs/ports/python.md) |
diff --git a/bun.lock b/bun.lock
index 011a33813..c82f169af 100644
--- a/bun.lock
+++ b/bun.lock
@@ -419,7 +419,7 @@
"version": "1.1.0",
"dependencies": {
"@metaobjectsdev/metadata": "workspace:*",
- "zod": "^3.23.0",
+ "zod": "^4.6.6",
},
"devDependencies": {
"bun-types": "latest",
@@ -1576,8 +1576,6 @@
"@metaobjectsdev/runtime-ts/@libsql/client": ["@libsql/client@0.15.15", "", { "dependencies": { "@libsql/core": "^0.15.14", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w=="],
- "@metaobjectsdev/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
-
"@stryker-mutator/instrumenter/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
diff --git a/docs/ports/csharp.md b/docs/ports/csharp.md
index 0781ebb0e..8d79fbbfe 100644
--- a/docs/ports/csharp.md
+++ b/docs/ports/csharp.md
@@ -6,24 +6,40 @@ Postgres + Npgsql.
## Install
-Published to [NuGet](https://www.nuget.org/packages/MetaObjects) at `1.0.13` — four
-packages, version-locked to the C# port version:
+Published to [NuGet](https://www.nuget.org/packages/MetaObjects) at `1.1.0` — four
+packages, version-locked to the C# port version: `MetaObjects` (loader), `MetaObjects.Codegen`,
+`MetaObjects.Render` and the `MetaObjects.Cli` tool.
-```xml
-
-
-
-
-
-
-```
-
-Install the CLI as a .NET tool:
+Install the CLI as a .NET tool, then scaffold the generator list:
```bash
dotnet tool install --global MetaObjects.Cli
dotnet meta # bare invocation prints the usage banner
-dotnet meta init # writes codegen/Generators.cs (the generator list), Program.cs, Codegen.csproj
+dotnet meta init # writes codegen/Generators.cs (the generator list), codegen/Program.cs, codegen/Codegen.csproj
+```
+
+Your app is a separate project beside `codegen/`. `dotnet meta init` runs from the root of your
+repository, and the app's `.csproj` at that root must not compile `codegen/`, which has its own
+`Main` (without the `Compile Remove` the build fails with CS0579 and CS8802). This sample uses
+SQLite so it runs with nothing else installed; the same generated code runs on Postgres (see
+[Use](#use)):
+
+```xml
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
```
The C# CLI is invoked as `dotnet meta` (command `dotnet-meta`), or run directly
@@ -33,10 +49,9 @@ canonical Node `meta` CLI, which owns schema + TS codegen (ADR-0015).
## Configure
-Drop metadata under `metadata/`:
+Drop metadata under `metaobjects/`, for example `metaobjects/meta.blog.json`:
-```jsonc
-// metadata/meta.blog.json
+```json
{ "metadata.root": {
"package": "acme::blog",
"children": [
@@ -45,8 +60,9 @@ Drop metadata under `metadata/`:
"children": [
{ "source.rdb": { "@table": "authors" } },
{ "field.long": { "name": "id" } },
- { "field.string": { "name": "name", "@required": true, "@maxLength": 200 } },
+ { "field.string": { "name": "name", "@required": true, "@maxLength": 200, "@filterable": true } },
{ "field.string": { "name": "bio", "@maxLength": 2000 } },
+ { "field.enum": { "name": "role", "@values": ["writer", "editor"], "@required": true, "@filterable": true } },
{ "identity.primary": { "@fields": "id", "@generation": "increment" } }
]
}}
@@ -68,7 +84,7 @@ var registry = Provider.ComposeRegistry(new IMetaDataTypeProvider[] {
yourProvider, // adds your custom subtype/attrs
});
-var loader = MetaDataLoader.FromDirectory("./metadata", registry);
+var loader = MetaDataLoader.FromDirectory("./metaobjects", registry);
```
The provider object has the same four-member contract (`Id`, `Dependencies`,
@@ -111,14 +127,25 @@ using MetaObjects.Codegen;
return CodegenCli.Run(args, Generators.All);
```
+Replace the empty list `dotnet meta init` wrote with the one above before the first `gen`: with
+an empty list `gen` writes nothing, and `--out` is required.
+
```bash
# Run the list: EF Core entities + AppDbContext + CRUD minimal-API routes
-dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
+dotnet meta gen ./metaobjects --out ./Generated --namespace Acme.Blog
-# Drift-check templates against payloads (FR-004)
-dotnet meta verify ./metadata --templates ./prompts
+# The drift gate: regenerate into a temp tree and fail (exit 1, naming the files) if the
+# committed ./Generated differs. Run it in CI; the namespace is inferred from ./Generated.
+dotnet meta verify ./metaobjects --codegen --out ./Generated
```
+`verify --codegen` is the gate for generated code. `dotnet meta verify ./metaobjects
+--templates ./prompts` is a different gate, for prompt templates against their payloads
+(FR-004): it says nothing about generated code, and it fails with exit 2 when the directory it
+is given does not exist. A bare `dotnet meta verify ./metaobjects` runs that templates gate
+against `prompts/` when it exists, else `templates/`, and fails the same way when neither
+exists; pass both subverbs to run both.
+
`dotnet meta gen --list` is the catalog, and it shows what each generator requires. `routes`
requires `entity`, `db-context` and `filter-allowlist`, because the handlers it emits
reference their output; `gen` warns when one is missing from the list, since the result
@@ -308,19 +335,38 @@ regenerate. See the "Column naming" section of
```csharp
// Program.cs
+using System.Text.Json.Serialization;
using Acme.Blog;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddDbContext(opts =>
- opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
+builder.Services.AddDbContext(opts => opts.UseSqlite("Data Source=blog.db"));
+// Enum fields (role) travel as their symbols ("writer"), not integers: the routes return the
+// entity and the host owns serialization, so without this POST {"role":"writer"} is a 400.
+builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
var app = builder.Build();
+using (var scope = app.Services.CreateScope())
+ scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); // the sample creates its own table
-app.MapAuthorRoutes(); // generated — GET/POST/PUT/DELETE on /api/authors
+app.MapAuthorRoutes(); // generated — GET/POST/PATCH/PUT/DELETE on /api/authors
app.Run();
```
+```bash
+dotnet run --urls http://127.0.0.1:5077
+curl -X POST -H 'content-type: application/json' -d '{"name":"Ada","role":"writer"}' http://127.0.0.1:5077/api/authors # 201
+curl -g 'http://127.0.0.1:5077/api/authors?filter[role][eq]=writer' # 200, the row
+```
+
+A filter works on a field marked `@filterable` (`name` and `role` above); any other field answers
+400 `invalid_filter_field`.
+
+**Postgres.** Swap `UseSqlite(...)` for `UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))`
+and add `Npgsql.EntityFrameworkCore.PostgreSQL` instead of the SQLite package. `EnsureCreated`
+is for samples; on Postgres create the tables with the Node CLI's migrations
+(`meta migrate`, ADR-0015) — the C# port has no migrate surface.
+
EF Core does the rest — the generated entities and `AppDbContext` are plain EF Core with
no MetaObjects types in them. Note the one exception: generated **routes** emit
`using MetaObjects.Codegen.Runtime;` for the shared filter/sort helpers, so a project that
@@ -343,10 +389,10 @@ or per-file `DIFFERS`, `verify --codegen` does not treat it as drift, and
has the diff recipe for pulling an upstream fix into it.
**Consumer dependencies.** The generated `AppDbContext` and the `Program.cs`
-wiring above use EF Core (`AddDbContext`, `DbContext`, `UseNpgsql`), which
-MetaObjects does not pull in for you — add the two EF Core NuGet packages to
-the consuming app: `Microsoft.EntityFrameworkCore` and
-`Npgsql.EntityFrameworkCore.PostgreSQL`.
+wiring above use EF Core (`AddDbContext`, `DbContext`, `UseSqlite`), which
+MetaObjects does not pull in for you — add the EF Core provider package for your
+database to the consuming app (`Microsoft.EntityFrameworkCore.Sqlite` above,
+`Npgsql.EntityFrameworkCore.PostgreSQL` for Postgres).
```csharp
// Optional handwritten service over the generated DbContext
@@ -465,14 +511,14 @@ contract: `dotnet meta eject requirement-tests` copies it to
are core and are not ejectable.
```bash
-# The gate: runs beside whichever drift gate verify was given
-dotnet meta verify ./metadata --templates ./prompts
-dotnet meta verify ./metadata --templates ./prompts --require-implementers
+# The gate: runs on every verify, beside whichever drift gate it was given
+dotnet meta verify ./metaobjects --codegen --out ./Generated
+dotnet meta verify ./metaobjects --codegen --out ./Generated --require-implementers
# The tests: the same run as everything else codegen/Generators.cs declares. The entry's
# OutDir puts them in the test project, which references xunit; the app's --out does not.
-dotnet meta gen ./metadata --out ./Generated
-dotnet meta verify ./metadata --codegen --out ./Generated
+dotnet meta gen ./metaobjects --out ./Generated
+dotnet meta verify ./metaobjects --codegen --out ./Generated
```
Give the entry an `OutDir` in `codegen/Generators.cs` (below): the tests need xunit, which
diff --git a/docs/ports/java.md b/docs/ports/java.md
index 046b9b910..f64b45188 100644
--- a/docs/ports/java.md
+++ b/docs/ports/java.md
@@ -12,13 +12,18 @@ Prompt / template drift is still checked via the `metaobjects-render` `Verify` A
## Install
-Set `${metaobjects.version}` to the current Maven Central release (`8.0.13`) — both
-the dependency and plugin blocks below resolve it from one `` entry:
+Set `${metaobjects.version}` to the current Maven Central release (`8.1.0`) — both
+the dependency and plugin blocks below resolve it from one `` entry. The
+generated records need Java 17 or newer, so set the compiler level here too (Maven
+defaults to Java 8, where `mvn compile` fails with "records are not supported in -source 8"):
```xml
- 8.0.13
+ 8.1.0
+ 3.3.5
+ 21
+ UTF-8
@@ -37,6 +42,23 @@ the dependency and plugin blocks below resolve it from one `` entry:
metaobjects-render
${metaobjects.version}
+
+
+ com.metaobjects
+ metaobjects-codegen-spring
+ ${metaobjects.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ ${spring-boot.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ ${spring-boot.version}
+
```
@@ -44,6 +66,15 @@ For Spring integration: add `metaobjects-core-spring`.
## Configure
+Keep the `` at plugin level, as below, not inside the ``: the
+execution binds the goal to `generate-sources`, and the plugin-level configuration is what a
+bare `mvn metaobjects:generate` or `mvn metaobjects:verify` from the command line reads.
+Inside the execution those fail with "No element was defined", and only
+`mvn metaobjects:verify@generate` works.
+
+These two blocks go in your `pom.xml` beside your own ``, `` and
+``.
+
```xml
@@ -51,48 +82,48 @@ For Spring integration: add `metaobjects-core-spring`.
com.metaobjects
metaobjects-maven-plugin
${metaobjects.version}
+
+
+ src/main/metaobjects
+
+
+
+ com.metaobjects.generator.spring.SpringDtoGenerator
+
+ ${project.build.directory}/generated-sources/java
+
+
+
+ com.metaobjects.generator.spring.SpringControllerGenerator
+
+ ${project.build.directory}/generated-sources/java
+
+
+
+ com.metaobjects.generator.spring.SpringRepositoryGenerator
+
+ ${project.build.directory}/generated-sources/java
+
+
+
+ com.metaobjects.generator.spring.SpringValueObjectGenerator
+
+ ${project.build.directory}/generated-sources/java
+
+
+
+ com.metaobjects.generator.spring.SpringFilterAllowlistGenerator
+
+ ${project.build.directory}/generated-sources/java
+
+
+
+
generate
generate-sources
generate
-
-
- src/main/metaobjects
-
-
-
- com.metaobjects.generator.spring.SpringDtoGenerator
-
- ${project.build.directory}/generated-sources/java
-
-
-
- com.metaobjects.generator.spring.SpringControllerGenerator
-
- ${project.build.directory}/generated-sources/java
-
-
-
- com.metaobjects.generator.spring.SpringRepositoryGenerator
-
- ${project.build.directory}/generated-sources/java
-
-
-
- com.metaobjects.generator.spring.SpringValueObjectGenerator
-
- ${project.build.directory}/generated-sources/java
-
-
-
- com.metaobjects.generator.spring.SpringFilterAllowlistGenerator
-
- ${project.build.directory}/generated-sources/java
-
-
-
-
@@ -100,10 +131,9 @@ For Spring integration: add `metaobjects-core-spring`.
```
-Drop metadata under `src/main/metaobjects/`:
+Drop metadata under `src/main/metaobjects/`, for example `src/main/metaobjects/meta.blog.json`:
-```jsonc
-// src/main/metaobjects/meta.blog.json
+```json
{ "metadata.root": {
"package": "acme::blog",
"children": [
@@ -172,8 +202,23 @@ internal/test partial sets. The cross-port contract lives in
```bash
mvn compile # runs the generate goal (bound to generate-sources)
+mvn metaobjects:generate # or run it alone
+```
+
+The generators write the DTO records, the Spring controller, the repository and the filter
+allowlist under `target/generated-sources/java`, which Maven compiles with your code.
+
+## Verify
+
+```bash
+mvn metaobjects:verify # fails with the stale files named when the model moved and nobody regenerated
```
+It regenerates into a temporary directory and compares: change `src/main/metaobjects/` without
+regenerating and the build exits 1 with `generated code is stale` and a `[content-differs]` line
+per file; `mvn metaobjects:generate` (or `mvn compile`) and it is green again. The same goal also
+runs the requirement gate. Add it to your CI after `mvn compile`.
+
Schema migrations are not a Java-port concern — author them with the TypeScript
toolchain (`@metaobjectsdev/cli migrate`), then apply the resulting DDL to the
database OMDB connects to. OMDB itself is pure data-access; the former runtime
diff --git a/docs/ports/kotlin.md b/docs/ports/kotlin.md
index 96d3cd7bb..fab7bd9a8 100644
--- a/docs/ports/kotlin.md
+++ b/docs/ports/kotlin.md
@@ -15,9 +15,42 @@ Two modules:
## Install
+Every version below comes from one `` entry. Set the Java level (Maven defaults to
+Java 8, which the generated code does not target) and give the Kotlin compiler a `kotlin-stdlib`. The generated code also
+imports Spring (`@Configuration`, `ApplicationReadyEvent`) and Jakarta Validation
+(`@NotNull`, `@Size`), so `spring-boot-starter` and `spring-boot-starter-validation` are
+dependencies of the generated code, not just of your app. Put these in your `pom.xml` beside
+your own ``, `` and ``:
+
```xml
+
+ 8.1.0
+ 2.0.21
+ 3.3.5
+ 0.55.0
+ 2.17.2
+ 1.7.3
+ 21
+ UTF-8
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ org.springframework.boot
+ spring-boot-starter
+ ${spring-boot.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ ${spring-boot.version}
+
com.metaobjects
metaobjects-metadata
@@ -109,9 +142,16 @@ The 16 generators registered in `codegen-kotlin` (`GeneratorRegistry.kt`):
| `KotlinRequirementTestsGenerator` | Per metamodel package that holds a tested requirement, two files in `testPackage`: `Requirements__Witnesses.kt` (an interface whose default members fail with `unimplemented requirement: ...`) and `Requirements__Test.kt` (one JUnit Jupiter `@Test` per requirement, calling a project-owned witness). See "Requirement tests" below | every tested `requirement.*` (functional, level 4 or above by default) |
| `KotlinApiDocsGenerator` | The Kotlin SDK api reference, under `/`: `.md` per entity, value object, projection, served report and template, a `README.md` index and `AGENT-API.md`. A `` entry like any other, run by `metaobjects:generate` and checked by `metaobjects:verify`. Args: `outputDir` (required, generated files only), `apiSubDir` (default `api/kotlin`), `layout` (`flat` or `package`), `projectName` and `modelBaseUrl`, as for the Java `api-docs`. The `metaobjects:docs` goal was removed in 8.1.0 ([migration](../features/migrations/docs-are-generators.md#java-and-kotlin)). Ejectable, with the page model, builder, renderer and path math | the whole model, once |
-Maven wiring:
+Maven wiring. The plugin bundles `metaobjects-codegen-kotlin`, so it is not a dependency of your
+project. Keep `` at plugin level and bind the goal in ``: without the
+`` block `mvn compile` generates nothing and still exits 0, and with the configuration
+inside the execution a bare `mvn metaobjects:generate` or `mvn metaobjects:verify` fails with
+"No element was defined". The `kotlin-maven-plugin` that follows compiles what the first
+plugin generated; it has to be told about `generated-sources/kotlin` and your own `src/main/kotlin`.
```xml
+
+
com.metaobjects
metaobjects-maven-plugin
@@ -150,9 +190,39 @@ Maven wiring:
+
+
+ generate
+ generate-sources
+ generate
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+ compile
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.build.directory}/generated-sources/kotlin
+
+
+
+
+
+
+
```
+The `KotlinSpringConfigGenerator` reads its model from the classpath (``), so the
+same file goes under `src/main/resources/` as well as `src/main/metaobjects/`.
+
**Requirement tests.** `mvn metaobjects:verify` checks the model's `requirement.*` nodes on every run (the Java goal; Kotlin has no CLI of its own), and `-Dmeta.verify.requireImplementers=true` or `META_REQUIRE_IMPLEMENTERS=1` also fails the build on a live functional requirement that nothing implements. Those checks are core and are not ejectable. `KotlinRequirementTestsGenerator` is the other half: a reference helper and a recommended approach, not a contract. It is the Kotlin sibling of the Java `JUnitRequirementTestsGenerator` (the same args, the same test identities, one conformance corpus for both). It writes one JUnit Jupiter test per tested requirement; the test calls a **witness**, a function you write, and a live requirement with no witness fails. A JUnit 4 project replaces the text through the `renderer` arg, or ejects the generator.
```xml
@@ -352,7 +422,9 @@ reactor's 2.0.21; its round-trip test additionally needs docker. Run via
## Generate
```bash
-mvn compile # runs the codegen as part of generate-sources
+mvn compile # runs the codegen as part of generate-sources, then compiles it
+mvn metaobjects:generate # or run the codegen alone
+mvn metaobjects:verify # fails, naming the files, when generated code is stale against the model
```
Schema migrations are owned by the TypeScript toolchain — see the
diff --git a/docs/ports/python.md b/docs/ports/python.md
index 4b8a89d17..86f16c9a4 100644
--- a/docs/ports/python.md
+++ b/docs/ports/python.md
@@ -26,7 +26,7 @@ pip install metaobjects # or: uv add metaobjects
[project]
requires-python = ">=3.11"
dependencies = [
- "metaobjects>=0.19",
+ "metaobjects>=1.1",
]
```
diff --git a/docs/ports/typescript.md b/docs/ports/typescript.md
index 0afef639f..f5db3e0e0 100644
--- a/docs/ports/typescript.md
+++ b/docs/ports/typescript.md
@@ -1,6 +1,6 @@
# TypeScript port
-The reference implementation. Published to npm at `1.0.13` as 14
+The reference implementation. Published to npm at `1.1.0` as 14
`@metaobjectsdev/*` packages on the `latest` tag. Targets Node-compatible
runtimes; Bun-first dev workflow.
diff --git a/examples/showcase/.metaobjects/.gen-state/.hashes.json b/examples/showcase/.metaobjects/.gen-state/.hashes.json
index a1f97cfa9..22528756c 100644
--- a/examples/showcase/.metaobjects/.gen-state/.hashes.json
+++ b/examples/showcase/.metaobjects/.gen-state/.hashes.json
@@ -4,7 +4,7 @@
"generated/csharp/SubscriberBlurbPayload.g.cs": "30ae31959c0cd98ba4ab12b2e827fa48f3bf59a05aa34974ebc4a1fe930378be",
"generated/csharp/SubscriberFilterAllowlist.g.cs": "7428d47f6ccb64bb315266ee0564fa3803736623a34ab491435c042848c5d472",
"generated/csharp/SubscriberNames.g.cs": "91f6eba78657b31fceef7021c6ca6bd799524dcb5120acbc8fcbe18786787174",
- "generated/csharp/SubscriberRoutes.g.cs": "e98387adf0d9fd5c2743409e5820d72400373ca6a758176cba76112fa1efc02a",
+ "generated/csharp/SubscriberRoutes.g.cs": "e520084727642d242609cbf82b3598d7c996224c3d0aef250bd26c7540ab4423",
"generated/python/Subscriber.py": "9dd4c0acea47f99d1daf79930f9de2e400f30780ebce6c63f023be528a72b762",
"generated/python/SubscriberBlurbPayload.py": "a5ef8a08e63efbe0f73e39a21a368cff5150c68cc8beeea6e8aab0c755f8bbdd",
"generated/python/__init__.py": "daa04cec586768866d0996c779f62073e6773fe63750ff668327991fac2fd0f5",
diff --git a/examples/showcase/generated/csharp/SubscriberRoutes.g.cs b/examples/showcase/generated/csharp/SubscriberRoutes.g.cs
index 8381645a7..1a23437a7 100644
--- a/examples/showcase/generated/csharp/SubscriberRoutes.g.cs
+++ b/examples/showcase/generated/csharp/SubscriberRoutes.g.cs
@@ -96,7 +96,7 @@ await db.Subscribers.FindAsync(id) is { } found
if (__p.Value.ValueKind != System.Text.Json.JsonValueKind.Null) __present.Add(__p.Name);
foreach (var __req in new[] { "email", "status" })
if (!__present.Contains(__req)) return Results.BadRequest(new { error = "validation" });
- Subscriber input;
+ Subscriber? input;
try { input = System.Text.Json.JsonSerializer.Deserialize(__body.RootElement.GetRawText(), jsonOpts); }
catch (System.Text.Json.JsonException) { return Results.BadRequest(new { error = "validation" }); }
if (input is null) return Results.BadRequest(new { error = "validation" });
diff --git a/scripts/regen-showcase.ts b/scripts/regen-showcase.ts
index cdd1c71e5..787133435 100644
--- a/scripts/regen-showcase.ts
+++ b/scripts/regen-showcase.ts
@@ -380,6 +380,14 @@ function pristineProject(): string {
for (const item of INPUTS) cpSync(join(SHOWCASE, item), join(tmp, item), { recursive: true });
cpSync(join(SHOWCASE, ".metaobjects", "config.json"),
join(tmp, ".metaobjects", "config.json"), { recursive: true });
+ // A project with no `node_modules` anywhere above it switches Bun into auto-install: the
+ // config's `@metaobjectsdev/*` imports would then resolve to a copy fetched into the
+ // machine-wide cache, not this tree, and that copy's formatter dependency cannot find its
+ // sibling `@biomejs/wasm-nodejs` ("Biome formatter error"), so every ts file comes out
+ // unformatted and the gate reports the whole committed tree as stale. An empty
+ // `node_modules` turns auto-install off; the CLI's own package aliases then resolve the
+ // workspace packages, which is what this check is meant to exercise.
+ mkdirSync(join(tmp, "node_modules"));
return tmp;
}
diff --git a/server/csharp/MetaObjects.Cli.Tests/CliSurfaceTests.cs b/server/csharp/MetaObjects.Cli.Tests/CliSurfaceTests.cs
index c6ab48e9f..9a09ca4ad 100644
--- a/server/csharp/MetaObjects.Cli.Tests/CliSurfaceTests.cs
+++ b/server/csharp/MetaObjects.Cli.Tests/CliSurfaceTests.cs
@@ -72,4 +72,52 @@ public void A_value_flag_with_no_value_says_so_rather_than_calling_it_unknown()
Assert.Equal(2, exit);
Assert.Contains("--out needs a value", stderr);
}
+
+ private string WriteBlogModel()
+ {
+ var model = Path.Combine(_tmp, "metaobjects");
+ Directory.CreateDirectory(model);
+ File.WriteAllText(Path.Combine(model, "meta.blog.json"),
+ """{ "metadata.root": { "package": "acme::blog", "children": [ { "object.entity": { "name": "Author", "children": [ { "field.long": { "name": "id" } } ] } } ] } }""");
+ return model;
+ }
+
+ [Fact]
+ public void Verify_templates_naming_a_missing_directory_fails_instead_of_passing()
+ {
+ var model = WriteBlogModel();
+ var (exit, stdout, stderr) = CliProcess.Run(_tmp, "verify", model, "--templates", Path.Combine(_tmp, "no-such-prompts"));
+ Assert.Equal(2, exit);
+ Assert.Contains("templates directory not found", stderr);
+ Assert.DoesNotContain("OK", stdout);
+ }
+
+ [Fact]
+ public void Verify_prompts_naming_a_missing_directory_fails_too()
+ {
+ var model = WriteBlogModel();
+ var (exit, _, stderr) = CliProcess.Run(_tmp, "verify", model, "--templates", "--prompts", Path.Combine(_tmp, "no-such-prompts"));
+ Assert.Equal(2, exit);
+ Assert.Contains("templates directory not found", stderr);
+ }
+
+ [Fact]
+ public void A_bare_verify_with_neither_prompts_nor_templates_fails()
+ {
+ var model = WriteBlogModel();
+ var (exit, stdout, stderr) = CliProcess.Run(_tmp, "verify", model);
+ Assert.Equal(2, exit);
+ Assert.Contains("templates directory not found", stderr);
+ Assert.DoesNotContain("OK", stdout);
+ }
+
+ [Fact]
+ public void A_bare_verify_runs_the_templates_default_its_help_describes()
+ {
+ var model = WriteBlogModel();
+ Directory.CreateDirectory(Path.Combine(_tmp, "prompts"));
+ var (exit, stdout, stderr) = CliProcess.Run(_tmp, "verify", model);
+ Assert.True(exit == 0, $"exit={exit}\nstderr={stderr}");
+ Assert.Contains("running --templates (default)", stdout);
+ }
}
diff --git a/server/csharp/MetaObjects.Cli/Program.cs b/server/csharp/MetaObjects.Cli/Program.cs
index df4cf4f60..4aae48719 100644
--- a/server/csharp/MetaObjects.Cli/Program.cs
+++ b/server/csharp/MetaObjects.Cli/Program.cs
@@ -36,7 +36,10 @@
" gen --list list available generators (stable names) and exit\n" +
" verify [--templates [--prompts ]] [--codegen --out [--namespace ]] [--db] [--lax]\n" +
" drift gates (ADR-0021 D2 subverbs):\n" +
- " --templates template/prompt drift (default)\n" +
+ " --templates template/prompt drift (default); the\n" +
+ " directory is --prompts or the inline\n" +
+ " root and must exist; with none it reads\n" +
+ " prompts/ if present, else templates/\n" +
" --codegen regen-to-temp vs committed --out\n" +
" --namespace codegen regen namespace; inferred\n" +
" from the committed --out when omitted\n" +
@@ -422,15 +425,21 @@ static int RunVerify(string[] rest)
// falls back to the port-neutral .metaobjects/config.json ladder.
var resolvedMeta = ResolveMetadataDirOrExit(metadataDir);
- // The templates gate needs a root. Bare verify (defaults to templates) and an
- // explicit --templates both require it; surface a clear usage error if absent.
+ // The templates gate needs a root, and it must exist: a typo'd or absent directory used
+ // to verify clean, because nothing in an empty root can drift. With none named (bare
+ // verify, or a bare --templates) it reads the same default `gen` does: prompts/ when that
+ // directory exists, else templates/ — and neither existing fails the same way.
var wantsTemplates = templates || (!codegen && !db);
- if (wantsTemplates && templatesRoot is null)
+ if (wantsTemplates)
{
- Console.Error.WriteLine("usage: dotnet meta verify --templates ");
- Console.Error.WriteLine(" (the templates gate is the default; pass --codegen --out for codegen drift —");
- Console.Error.WriteLine(" --namespace is inferred from the committed output when omitted)");
- return 2;
+ templatesRoot ??= GenCommand.DefaultTemplateRoot();
+ if (!Directory.Exists(templatesRoot))
+ {
+ Console.Error.WriteLine($"dotnet meta verify --templates: templates directory not found: {templatesRoot}");
+ Console.Error.WriteLine(" (name the directory that holds your prompt bodies, e.g. --templates ./prompts;");
+ Console.Error.WriteLine(" with none named it reads prompts/ when that exists, else templates/)");
+ return 2;
+ }
}
// Advisory: nudge a re-scaffold if the copied-in agent context predates this build.
diff --git a/server/csharp/MetaObjects.Codegen.Tests/EntityGeneratorTests.cs b/server/csharp/MetaObjects.Codegen.Tests/EntityGeneratorTests.cs
index 6fcc8c493..119b12f82 100644
--- a/server/csharp/MetaObjects.Codegen.Tests/EntityGeneratorTests.cs
+++ b/server/csharp/MetaObjects.Codegen.Tests/EntityGeneratorTests.cs
@@ -126,7 +126,7 @@ public void Routes_generator_emits_crud_endpoints()
Assert.Contains("app.MapPost(prefix + \"/subscribers\", async (HttpContext http, AppDbContext db) =>", src);
Assert.Contains("foreach (var __req in new[] { \"email\" })", src);
Assert.Contains("if (!__present.Contains(__req)) return Results.BadRequest(new { error = \"validation\" });", src);
- Assert.Contains("Subscriber input;", src);
+ Assert.Contains("Subscriber? input;", src);
Assert.Contains("try { input = System.Text.Json.JsonSerializer.Deserialize(__body.RootElement.GetRawText(), jsonOpts); }", src);
Assert.Contains("catch (System.Text.Json.JsonException) { return Results.BadRequest(new { error = \"validation\" }); }", src);
// PATCH + PUT share the same update handler (matches TS reference). It takes the raw
diff --git a/server/csharp/MetaObjects.Codegen/CodegenDrift.cs b/server/csharp/MetaObjects.Codegen/CodegenDrift.cs
index 8e7ebc0b1..1d60595dd 100644
--- a/server/csharp/MetaObjects.Codegen/CodegenDrift.cs
+++ b/server/csharp/MetaObjects.Codegen/CodegenDrift.cs
@@ -81,7 +81,7 @@ public static IReadOnlyList Remedy(Result result)
{
var lines = new List();
if (result.DriftedFiles.Count > result.EditedOrphans.Count)
- lines.Add("Run 'dotnet meta gen' to regenerate, then commit the result.");
+ lines.Add("Run 'dotnet meta gen' to regenerate (it needs --out, the same directory you gave this command), then commit the result.");
if (result.EditedOrphans.Count > 0)
lines.Add("For the file(s) above that no generator emits any more and that were edited by hand, " +
"'dotnet meta gen' will NOT delete them. Delete each one, or move your edits into a file of your own first.");
diff --git a/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs b/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs
index c831b218a..7ec86477a 100644
--- a/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs
+++ b/server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs
@@ -793,7 +793,7 @@ private static void AppendCreateHandler(
// throws JsonException — return the cross-port 400 {error:"validation"} rather than an
// unhandled 500 (matches the no-required-keys path's automatic model-binding 400, and
// every other port). Program D — the VO columns make wrong-shaped bodies reachable.
- sb.AppendLine($" {cls} input;");
+ sb.AppendLine($" {cls}? input;");
sb.AppendLine(" try { input = System.Text.Json.JsonSerializer.Deserialize<" + cls + ">(__body.RootElement.GetRawText(), jsonOpts); }");
sb.AppendLine(" catch (System.Text.Json.JsonException) { return Results.BadRequest(new { error = \"validation\" }); }");
sb.AppendLine(" if (input is null) return Results.BadRequest(new { error = \"validation\" });");
diff --git a/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataVerifyMojo.java b/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataVerifyMojo.java
index 635802a6f..42219d552 100644
--- a/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataVerifyMojo.java
+++ b/server/java/maven-plugin/src/main/java/com/metaobjects/mojo/MetaDataVerifyMojo.java
@@ -7,6 +7,8 @@
import com.metaobjects.generator.verify.TemplateVerify;
import com.metaobjects.loader.MetaDataLoader;
import com.metaobjects.requirement.RequirementCheck;
+import org.apache.maven.model.Plugin;
+import org.apache.maven.model.PluginExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
@@ -82,9 +84,26 @@ public class MetaDataVerifyMojo extends AbstractMetaDataMojo {
/** What every line of this goal's requirement report starts with, as the field lint's does. */
private static final String PREFIX = "metaobjects:verify \u2014 ";
- /** The gen goal to suggest in the failure message ({@code groupId:artifactId:goal}). */
+ /** The gen goal to suggest in the failure message. */
private static final String GEN_GOAL = "metaobjects:generate";
+ /**
+ * The generate invocation to suggest in a failure message. A generate configuration that
+ * lives inside an {@code } is not seen by a bare {@code mvn metaobjects:generate}
+ * (it fails with "No element was defined"), so name the execution that holds it.
+ * With the configuration at plugin level, or when the module is not known, the bare goal works.
+ */
+ private String genGoal() {
+ Plugin plugin = project != null ? project.getPlugin(PLUGIN_KEY) : null;
+ if (plugin == null || plugin.getConfiguration() != null) return GEN_GOAL;
+ for (PluginExecution e : plugin.getExecutions()) {
+ if (e.getGoals().contains("generate") && !"none".equals(e.getPhase())) {
+ return GEN_GOAL + "@" + e.getId();
+ }
+ }
+ return GEN_GOAL;
+ }
+
/** Verify mode: {@code codegen} (default), {@code templates}. */
static final String MODE_CODEGEN = "codegen";
static final String MODE_TEMPLATES = "templates";
@@ -412,7 +431,7 @@ private void verifyCodegen() throws MojoExecutionException, MojoFailureException
if (!drift.isEmpty()) {
StringBuilder sb = new StringBuilder();
if (drift.size() > editedOrphans.size()) {
- sb.append("generated code is stale — run `mvn ").append(GEN_GOAL)
+ sb.append("generated code is stale — run `mvn ").append(genGoal())
.append("` and commit. Drifted files:");
} else {
// Every drifted file is an orphan that was edited by hand and that
@@ -426,7 +445,7 @@ private void verifyCodegen() throws MojoExecutionException, MojoFailureException
}
if (!editedOrphans.isEmpty()) {
sb.append("\n\nFor the file(s) above that no generator emits any more and that were "
- + "edited by hand, `mvn ").append(GEN_GOAL)
+ + "edited by hand, `mvn ").append(genGoal())
.append("` will NOT delete them. Delete each one, or move your edits into a "
+ "file of your own first.");
}
@@ -490,7 +509,7 @@ private List strandedOutsideTargets(Map ownHashes, List<
switch (OrphanSweep.classify(ownHashes, root, file)) {
case PRISTINE:
drift.add("[stale-in-repo] " + file + " (committed; no configured generator produces it; "
- + "'mvn " + GEN_GOAL + "' deletes it)");
+ + "'mvn " + genGoal() + "' deletes it)");
break;
case EDITED:
drift.add("[stale-in-repo] " + file + " (no configured generator emits it any more, and "
@@ -556,12 +575,12 @@ List compareTrees(GenTarget t, Map hashes, Path projectR
break;
case PRISTINE:
drift.add("[stale-in-repo] " + committedFile + " (committed; generator no longer "
- + "produces it; 'mvn " + GEN_GOAL + "' deletes it)");
+ + "produces it; 'mvn " + genGoal() + "' deletes it)");
break;
case EDITED:
if (t.allOverwrite) {
drift.add("[stale-in-repo] " + committedFile + " (no generator emits it any "
- + "more, and it was edited by hand: 'mvn " + GEN_GOAL
+ + "more, and it was edited by hand: 'mvn " + genGoal()
+ "' will overwrite and delete it, handEdits: overwrite)");
} else {
drift.add("[stale-in-repo] " + committedFile + " (no generator emits it any "
diff --git a/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyMojoManifestTest.java b/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyMojoManifestTest.java
index 9d37a4259..85295b4d0 100644
--- a/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyMojoManifestTest.java
+++ b/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyMojoManifestTest.java
@@ -318,4 +318,39 @@ public void sharedOutputDir_anotherConfiguredGeneratorsFiles_areNotDrift() throw
verify.execute();
}
+
+ /**
+ * A generate configuration that sits inside an {@code } is invisible to a bare
+ * {@code mvn metaobjects:generate} (it fails with "No element was defined"), so the
+ * stale message names the execution; with the configuration at plugin level the bare goal
+ * is the command that works.
+ */
+ @Test
+ public void staleMessage_namesTheGenerateCommandThatWorksForTheModule() throws Exception {
+ Path basedir = tempFolder.getRoot().toPath();
+ String outDir = tempFolder.newFolder("generated-sources").getAbsolutePath();
+ GeneratorParam dto = gen(DtoGenerator.class, outDir, "Dto.java");
+ MavenProject inExecution = module(basedir, dto);
+ generateIn(inExecution, dto);
+ Files.writeString(Path.of(outDir, "Dto.java"), MARKER_BODY + "// drift\n");
+
+ assertTrue(staleMessage(inExecution, dto).contains("run `mvn metaobjects:generate@codegen` and commit"));
+
+ Plugin atPluginLevel = inExecution.getModel().getBuild().getPlugins().get(0);
+ atPluginLevel.setConfiguration(configuration(dto));
+ assertTrue(staleMessage(inExecution, dto).contains("run `mvn metaobjects:generate` and commit"));
+ }
+
+ private String staleMessage(MavenProject module, GeneratorParam dto) throws Exception {
+ MetaDataVerifyMojo verify = new MetaDataVerifyMojo();
+ verify.project = module;
+ verify.setLoader(testLoader());
+ verify.setGenerators(List.of(dto));
+ try {
+ verify.execute();
+ } catch (MojoFailureException e) {
+ return e.getMessage();
+ }
+ throw new AssertionError("expected MojoFailureException");
+ }
}
diff --git a/server/typescript/packages/sdk/package.json b/server/typescript/packages/sdk/package.json
index bf5f31e8f..e63943b93 100644
--- a/server/typescript/packages/sdk/package.json
+++ b/server/typescript/packages/sdk/package.json
@@ -52,7 +52,7 @@
},
"dependencies": {
"@metaobjectsdev/metadata": "workspace:*",
- "zod": "^3.23.0"
+ "zod": "^4.6.6"
},
"devDependencies": {
"bun-types": "latest",
diff --git a/server/typescript/packages/sdk/src/config.ts b/server/typescript/packages/sdk/src/config.ts
index f6c27ff14..5de2ed832 100644
--- a/server/typescript/packages/sdk/src/config.ts
+++ b/server/typescript/packages/sdk/src/config.ts
@@ -146,7 +146,7 @@ export const ConfigSchema = z.object({
pending_promote: z.number().min(0).max(1).default(0.8),
drift_warn: z.number().min(0).max(1).default(0.7),
})
- .default({}),
+ .prefault({}),
sources: z.array(SourceSpecSchema).default([]),
/** Declared metadata dependencies (FR-023) — read from this file at every
* rung of the source ladder, by every port, not just TypeScript (DESIGN
@@ -281,20 +281,23 @@ export function splitUnknownTopLevelKeys(doc: unknown): { known: unknown; unknow
*/
export function describeConfigError(path: string, error: z.ZodError): string {
const lines = new Set();
- const visit = (issues: readonly z.ZodIssue[]): void => {
+ // zod 4 reports a union arm's issue paths relative to the union, so carry the prefix down.
+ const visit = (issues: readonly z.core.$ZodIssue[], prefix: readonly PropertyKey[]): void => {
for (const issue of issues) {
+ const full = [...prefix, ...issue.path];
if (issue.code === "invalid_union") {
- visit(issue.unionErrors.flatMap((e) => e.issues));
+ for (const arm of issue.errors) visit(arm, full);
continue;
}
- const at = issue.path.reduce(
- (acc, seg) => (typeof seg === "number" ? `${acc}[${seg}]` : acc === "" ? seg : `${acc}.${seg}`),
+ const at = full.reduce(
+ (acc, seg) =>
+ typeof seg === "number" ? `${acc}[${seg}]` : acc === "" ? String(seg) : `${acc}.${String(seg)}`,
"",
);
lines.add(` ${at === "" ? "(top level)" : at}: ${issue.message}`);
}
};
- visit(error.issues);
+ visit(error.issues, []);
const touchesSources = error.issues.some((i) => i.path[0] === "sources");
return [
`${path} is not a valid MetaObjects config:`,