Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ here.**

### Added

- **C# reads its generated-code directory from `.metaobjects/config.json`, so a plain
`dotnet meta verify` runs the codegen gate.** Set `"csharp": { "outDir": "Generated" }`
(relative to the directory holding `.metaobjects/`). `dotnet meta gen` writes there and
`dotnet meta verify` (bare or `--codegen`) diffs against it, so the two cannot disagree; an
explicit `--out <dir>` still works and wins. Before this a bare `dotnet meta verify` skipped
the codegen gate unless `--out` was passed, and exited 0 on stale generated code. A project
with no setting still gets the skip note, which now names the setting. A present `csharp`
block that is not an object, has an unknown key, or has an empty or non-string `outDir` is
refused. The other ports ignore the block; the TypeScript config reader declares the key so
the shared file stays free of unknown-key warnings.

- **Report export: `GET /<segment>?format=csv|json|xml` on a served report, in all five ports.**
A new standalone `report-export` catalog generator (importable as shipped, ejectable in every
port, drift-checked by `verify --codegen`, never implied by a library) generates one export
Expand Down Expand Up @@ -47,8 +58,7 @@ here.**
subverb still runs only the gates named. A gate the run has no configuration for is skipped
with a one-line note instead of failing: `meta verify` without a `metaobjects.config.ts` (a config that exists but fails to load exits 2),
`metaobjects verify` with no `metaobjects.config.yaml` and no `--prompts`, `dotnet meta verify`
without `--out <dir>` and `codegen/Generators.cs` (C# has no config-resolved output directory:
pass `--out <dir>` in CI to get the codegen gate), `mvn metaobjects:verify` without
without an output directory (`csharp.outDir`, see Added) and `codegen/Generators.cs`, `mvn metaobjects:verify` without
`-Dmeta.verify.templateRoot`. Pinned per port by tests that run a bare verify over drifted
input. **Behaviour change:** a CI step that ran a bare `verify` and relied on the narrower
default can now go red where it was green, because the gate it never ran now runs. Node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ build output (`codegen/bin/`, `codegen/obj/`) in `.gitignore`.

A bare `dotnet meta verify` runs `--templates` (the FR-004 prompt/template drift
gate, see the prompts reference) and `--codegen` (the codegen-output drift gate;
skipped with a note unless `--out <dir>` and `codegen/Generators.cs` are present).
skipped with a note unless an output directory, `csharp.outDir` in `.metaobjects/config.json` or `--out <dir>`, and `codegen/Generators.cs` are present).
**Schema migration + live-DB drift are NOT `dotnet meta`** — they run through the
Node `meta` tool (see the migration reference).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ building block. Every generator is a reference helper an adopter selects, ejects
(`server/typescript/packages/codegen-ts/src/db-emitting.ts`).
- **The drift gate per port** — regenerate into a temp tree and diff: `meta verify --codegen`
(TS); `mvn metaobjects:verify` (Java/Kotlin; `codegen` is the default mode); `dotnet meta
verify --codegen` (C#; bare `verify` runs it when `--out` and `codegen/Generators.cs` are present);
verify --codegen` (C#; bare `verify` runs it when `csharp.outDir` (or `--out`) and `codegen/Generators.cs` are present);
`metaobjects verify` (Python; `--codegen` is part of the default). Source: `docs/features/cli.md`.
- **Generated code compiles**: a codegen-compile gate compiles every port's emitted model tier
with its real compiler in CI (`docs/CONFORMANCE.md`, "Split coverage").
Expand Down
11 changes: 7 additions & 4 deletions docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ command surface splits in two:
| **Dependency sync** (`deps`) | **Node `meta`** | `meta deps sync \| check \| list` | **any backend** — resolves a declared metadata dependency's `path` transport into a committed snapshot + lock (FR-023), so `sync`/`check`/`list` themselves stay Node-only regardless of your server language. TypeScript and Python then LOAD that snapshot at every rung of their own source ladder — the two ports that implement dependency resolution in Phase 1a; Java, Kotlin, and C# don't read `dependencies` yet (Phase 2). See [`metadata-dependencies.md`](metadata-dependencies.md) |
| **Canonical format** (`fmt`) | every port | `meta fmt` / `dotnet meta fmt` / `mvn metaobjects:fmt` / `metaobjects fmt` | (#304) ships in **every** port — the canonical serializer already exists in all five, so formatting is local, not Node-only. `--check` lists non-canonical files and exits non-zero without changing anything. Formats each file STANDALONE — own-mode, declared-here layer only (ADR-0039); an `extends` onto another file's base is preserved as-is, never resolved or erred on; a file declaring `overlay: true` anywhere — whether or not a same-file base exists — is reported as skipped rather than merged (merging would drop the overlay marker, a structural change fmt never makes). A YAML file is always skipped — no canonical YAML emitter exists (ADR-0006). Before writing, every port reloads the whole project with the candidate swapped in and refuses the write unless the reloaded model's canonical form is byte-identical to the untouched baseline — fmt never changes meaning. See [Fixture corpus](#meta-fmt--canonical-formatting-304) below and `fixtures/fmt-conformance/README.md` |
| TS codegen | Node `meta` | `meta gen` | TS projects. **No `--template-spec` flag, deliberately** — `metaobjects.config.ts` already takes generator VALUES, so a declarative template generator is declared there (`templateGenerator()`, or `templateSpecToGenerators(parseTemplateSpec(spec))` to reuse a C#/Python spec file). Keeping it in the config is what lets `meta verify --codegen` regenerate WITH it, since that gate re-runs the config's generator list; see [declarative template scopes](codegen-concepts.md#declarative-template-scopes) |
| C# codegen | `dotnet meta` | `dotnet meta init` / `gen` / `verify --templates` / `verify --codegen` | a .NET tool (`ToolCommandName=dotnet-meta`); invoked `dotnet meta` so it never shadows the Node `meta`; ships the ADR-0021 D2 subverbs (`--db` rejected, exit 2; bare `verify` = `--templates` plus `--codegen` when `--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 |
| 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 |
| Java/Kotlin codegen | Maven plugin | `mvn metaobjects:generate` (`metaobjects:generate`) | Kotlin generators run through the same goal — see below. **No `--template-spec` flag, deliberately** — `<generator>` already loads a consumer class from the project classpath, so the declarative surface is `com.metaobjects.generator.template.TemplateScopeGenerator` wired as an ordinary `<generator>` with `<template>` / `<scope>` / `<outputPattern>` / `<templatesDir>` / `<format>` args (covers BOTH Java and Kotlin); see [declarative template scopes](codegen-concepts.md#declarative-template-scopes). The `generate` and `verify` goals are declared `threadSafe` and support parallel multi-module reactor builds (`mvn -T`) (#233) |
| Java/Kotlin verify | Maven plugin | `mvn metaobjects:verify -Dmeta.verify.mode=codegen\|templates` (`metaobjects:verify`) | parameter-driven ADR-0021 D2 modes (one goal covers BOTH Java + Kotlin): unset (default — `codegen` and `templates`; the template gate is skipped with a note when `templateRoot` is unset) / `codegen` (regen + fail on drift vs committed output, generator-neutral) / `templates` (`{{field}}`↔payload drift via the render `Verify` engine). `db` rejected ("schema verify is the migrate engine, ADR-0015") |
| Python codegen | console-script | `metaobjects gen` / `verify --codegen` / `verify --templates` | `[project.scripts] metaobjects` — **not** `meta` (that's the Node schema CLI); ships the ADR-0021 D2 subverbs (`--db` rejected, exit 2). **The generator list is `metaobjects.config.yaml`** (ADR-0034 Amendment 9, [below](#declarative-config-metaobjectsconfigyaml--python-codegen)): `gen` and `verify --codegen` run each target's `generators` (and its `templateSpec`, the declarative Mustache template-codegen surface shared with C#) into its `outDir`, and with no config they exit 2 and write nothing. `--generators` only narrows the list to declared entries. `metaobjects docs` was removed in 1.1.0: it prints the line that points at [the migration section](migrations/docs-are-generators.md#python) and exits 2 |
Expand Down Expand Up @@ -68,7 +68,8 @@ Rules of the contract:
note, never failed: Node `meta` needs a `metaobjects.config.ts` for codegen (a config that
exists but fails to load is not a missing config: it exits 2 with the load error);
`metaobjects verify` needs a `metaobjects.config.yaml` or `--prompts <dir>` for
templates; `dotnet meta verify` needs `--out <dir>` and `codegen/Generators.cs`
templates; `dotnet meta verify` needs an output directory (`csharp.outDir` in
`.metaobjects/config.json`, or `--out <dir>`) and `codegen/Generators.cs`
for codegen; `mvn metaobjects:verify` needs `-Dmeta.verify.templateRoot` for
templates. Naming a subverb still runs only the gates named. Bare `verify` also
prints a one-line note advertising the explicit subverbs. *Before this, a bare
Expand Down Expand Up @@ -212,8 +213,10 @@ meta`** port likewise ships the codegen-side subverbs: `verify --templates` (its
historical template/prompt drift gate), `verify
--codegen` (regenerate the configured generators to a temp dir and diff against
the committed `--out` tree, never touching it), and a **clean `--db` rejection
(exit 2)** — bare `dotnet meta verify` runs `--templates` and, when `--out` and
`codegen/Generators.cs` are present, `--codegen` (C# has no config-resolved output dir, so a bare run without `--out <dir>` skips the codegen gate with a note — pass `--out` in CI), and prints the subverb note. The **Java/Kotlin `mvn metaobjects:verify`** port expresses the same vocabulary as a
(exit 2)** — bare `dotnet meta verify` runs `--templates` and, when an output
directory and `codegen/Generators.cs` are present, `--codegen`. The output directory is
`csharp.outDir` in `.metaobjects/config.json` (the same setting `gen` writes to); `--out <dir>`
wins over it. With neither, the codegen gate is skipped with a note that names the setting. A bare run prints the subverb note. The **Java/Kotlin `mvn metaobjects:verify`** port expresses the same vocabulary as a
`mode` parameter (Maven goals are parameter-driven, not flag-driven). With `mode` unset the goal
runs both gates below, skipping the template gate with a note when no `templateRoot` is set.
`-Dmeta.verify.mode=codegen`
Expand Down
18 changes: 15 additions & 3 deletions docs/ports/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ 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.
an empty list `gen` writes nothing, and it needs an output directory (`--out`, or `csharp.outDir`
in `.metaobjects/config.json`, below).

```bash
# Run the list: EF Core entities + AppDbContext + CRUD minimal-API routes
Expand All @@ -181,8 +182,19 @@ dotnet meta verify ./metaobjects --codegen --out ./Generated
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. It also runs the codegen gate when `--out <dir>` and `codegen/Generators.cs` are
present, and skips it with a note otherwise (C# has no config-resolved output directory, so
pass `--out` in CI); name both subverbs to require both.
present, and skips it with a note otherwise; name both subverbs to require both.

The output directory is a project setting, so `gen` and `verify` cannot disagree about it.
Declare it once in `.metaobjects/config.json`, relative to the directory holding `.metaobjects/`:

```json
{ "schema_version": 1, "csharp": { "outDir": "Generated" } }
```

With that, `dotnet meta gen` writes there, `dotnet meta verify --codegen` diffs against it, and
a plain `dotnet meta verify` in CI runs the codegen gate with no flags. `--out <dir>` still works
and wins over the config. A project that sets neither gets the skip note, which names the setting.
The other ports ignore the `csharp` block.

`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
Expand Down
37 changes: 36 additions & 1 deletion server/csharp/MetaObjects.Cli.Tests/CliSurfaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ public void A_bare_verify_skips_the_codegen_gate_with_a_note_when_it_has_no_out_
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("codegen gate skipped: pass --out <dir>", stdout);
Assert.Contains("codegen gate skipped: set \"csharp\": { \"outDir\"", stdout);
Assert.Contains(".metaobjects/config.json", stdout);
}

[Fact]
Expand All @@ -140,4 +141,38 @@ public void A_bare_verify_skips_the_codegen_gate_with_a_note_when_there_is_no_ge
Assert.True(exit == 0, $"exit={exit}\nstderr={stderr}");
Assert.Contains("codegen gate skipped: no codegen/Generators.cs", stdout);
}

private void WriteOutDirConfig(string outDir)
{
Directory.CreateDirectory(Path.Combine(_tmp, ".metaobjects"));
File.WriteAllText(Path.Combine(_tmp, ".metaobjects", "config.json"),
"{ \"schema_version\": 1, \"csharp\": { \"outDir\": \"" + outDir + "\" } }");
}

[Fact]
public void A_bare_verify_with_a_configured_out_dir_reaches_the_codegen_gate()
{
// No --out: the directory comes from csharp.outDir, so the gate is attempted (and refuses
// here only because the project has a generator list but no codegen project to compile it).
var model = WriteBlogModel();
Directory.CreateDirectory(Path.Combine(_tmp, "prompts"));
Directory.CreateDirectory(Path.Combine(_tmp, "codegen"));
File.WriteAllText(Path.Combine(_tmp, "codegen", "Generators.cs"), "// list");
WriteOutDirConfig("generated");
var (exit, stdout, stderr) = CliProcess.Run(_tmp, "verify", model);
Assert.Equal(2, exit);
Assert.DoesNotContain("codegen gate skipped", stdout);
Assert.Contains("no codegen/Codegen.csproj", stderr);
}

[Fact]
public void A_bare_verify_with_a_configured_out_dir_and_no_generator_list_says_what_is_missing()
{
var model = WriteBlogModel();
Directory.CreateDirectory(Path.Combine(_tmp, "prompts"));
WriteOutDirConfig("generated");
var (exit, stdout, stderr) = CliProcess.Run(_tmp, "verify", model);
Assert.True(exit == 0, $"exit={exit}\nstderr={stderr}");
Assert.Contains("codegen gate skipped: no codegen/Generators.cs", stdout);
}
}
40 changes: 40 additions & 0 deletions server/csharp/MetaObjects.Cli.Tests/OneGeneratorListTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,46 @@ public void A_requirement_tests_entry_with_its_own_directory_is_written_and_chec
Assert.Contains("Requirements_acme_Tests.g.cs", drift.Stderr);
}

// ---- the project's csharp.outDir drives gen and verify; --out wins ---------------------------

[Fact]
public void A_configured_out_dir_is_where_gen_writes_and_verify_checks_and_out_overrides_it()
{
Directory.CreateDirectory(Path.Combine(_tmp, ".metaobjects"));
File.WriteAllText(Path.Combine(_tmp, ".metaobjects", "config.json"),
"""{ "schema_version": 1, "csharp": { "outDir": "generated" } }""");
IReadOnlyList<IGenerator> declared = [new EntityGenerator()];

var gen = OwnedRunner.Run(_tmp, declared, "gen");
Assert.True(gen.ExitCode == 0, $"exit={gen.ExitCode}\nstdout={gen.Stdout}\nstderr={gen.Stderr}");
Assert.Equal(["Subscriber.g.cs"], FilesUnder(OutDir));

var clean = OwnedRunner.Run(_tmp, declared, "verify", "--codegen");
Assert.True(clean.ExitCode == 0, $"exit={clean.ExitCode}\nstdout={clean.Stdout}\nstderr={clean.Stderr}");

File.AppendAllText(Path.Combine(OutDir, "Subscriber.g.cs"), "// edited\n");
var drift = OwnedRunner.Run(_tmp, declared, "verify", "--codegen");
Assert.Equal(1, drift.ExitCode);
Assert.Contains("Subscriber.g.cs", drift.Stderr);

// --out beats the config: gen writes there and leaves the configured directory alone.
var other = Path.Combine(_tmp, "elsewhere");
var genOther = OwnedRunner.Run(_tmp, declared, "gen", "--out", other);
Assert.True(genOther.ExitCode == 0, $"exit={genOther.ExitCode}\n{genOther.Stdout}{genOther.Stderr}");
Assert.Equal(["Subscriber.g.cs"], FilesUnder(other));
var cleanOther = OwnedRunner.Run(_tmp, declared, "verify", "--codegen", "--out", other);
Assert.True(cleanOther.ExitCode == 0, $"exit={cleanOther.ExitCode}\n{cleanOther.Stdout}{cleanOther.Stderr}");
}

[Fact]
public void Without_a_configured_out_dir_or_out_flag_gen_asks_for_the_setting()
{
var gen = OwnedRunner.Run(_tmp, [new EntityGenerator()], "gen");
Assert.Equal(2, gen.ExitCode);
Assert.Contains("csharp", gen.Stderr);
Assert.Contains("outDir", gen.Stderr);
}

// ---- a narrowed verify checks only what the narrowed run produces ---------------------------

[Fact]
Expand Down
Loading
Loading