diff --git a/AGENTS.md b/AGENTS.md
index 6b51e229a..396cd39d7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -66,7 +66,7 @@ PyPI has had no product change since `0.25.0` — nothing is broken.
**All five ports ship loader + canonical serializer + conformance + codegen + render + payload-VO + `verify`:**
- **TypeScript** — `codegen-ts` (Vite-style plugins; Drizzle, Zod, Fastify) + `runtime-ts` + `migrate-ts` + the universal web client packages (`runtime-web`, `react`, `tanstack`).
-- **C#** — `MetaObjects` (loader + canonical serializer + conformance) + `MetaObjects.Render` (Mustache + payload-VO + `verify`) + `MetaObjects.Codegen` (EF Core entities + `AppDbContext` + CRUD minimal-API routes). Schema migrations are TS-owned (ADR-0015): the C# migrate engine and the `migrate`/`--from-db` CLI surface were removed; the C# CLI is `gen`/`verify` only, packaged as a .NET tool invoked **`dotnet meta`** (not a bare `meta` — that name belongs to the Node schema CLI). EF Core runtime data-access stays per-port.
+- **C#** — `MetaObjects` (loader + canonical serializer + conformance) + `MetaObjects.Render` (Mustache + payload-VO + `verify`) + `MetaObjects.Codegen` (EF Core entities + `AppDbContext` + CRUD minimal-API routes). Schema migrations are TS-owned (ADR-0015): the C# migrate engine and the `migrate`/`--from-db` CLI surface were removed; the C# CLI is codegen (`init`/`gen`/`eject`), `verify` and `fmt` only, packaged as a .NET tool invoked **`dotnet meta`** (not a bare `meta` — that name belongs to the Node schema CLI). EF Core runtime data-access stays per-port.
- **Java** — `metadata` + `omdb` + `om` + `core-spring` + `metadata-ktx` (Kotlin facade) + `codegen-spring` (Spring controllers + DTOs + repositories + filter allowlists + payload records + output parsers) + `codegen-mustache` + `codegen-plantuml` + `render` + `maven-plugin` (`metaobjects:generate`/`metaobjects:editor` + a `metaobjects:verify` **codegen-drift** goal — distinct from the removed live-DB `metaobjects:verify`; Kotlin generators run through `metaobjects:generate` via the shared SPI). FR-003 (OMDB runtime persistence + binding registry + typed jsonb + Spring-tx + source/origin metamodel) fully shipped, including Plan 4 (engine-debt remediation: atomic mapping cache, JDBC codec registry, `inTransaction` template). **Schema migrations are owned by the TypeScript toolchain** (`@metaobjectsdev/cli migrate`); the Java port's diff-and-converge migration engine and its `metaobjects:migrate` / live-DB-drift `metaobjects:verify` Maven goals were removed, and per ADR-0015 Decision 2 the dev/test runtime auto-create path (`MetaClassDBValidatorService` + the drivers' `createTable`/`createIndex`/`createForeignKey`/`createSequence` DDL) is **also** removed — **OMDB is pure data-access** (CRUD/query/codec/transactions only).
- **Python** — `metaobjects` (loader + canonical serializer + conformance + render + verify + codegen) + an `ObjectManager` runtime layer. The `migrate` module was removed (schema is TS-owned, ADR-0015); Python is pure data-access (codegen + ObjectManager runtime), with a **`metaobjects` console-script** (`gen`/`verify` codegen — no `migrate`). All five conformance corpora green.
- **Kotlin** — `codegen-kotlin` (KotlinPoet on JVM): entity + Exposed table + Spring controller + payload + relations + filter allowlist + validator + stored-proc + output-parser generators. `integration-tests-kotlin` runs the persistence-conformance corpus through Exposed against Testcontainers Postgres.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef7c00ce1..45cb6b224 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -282,18 +282,17 @@ generator catalog and the Atlas evaluation are 1.2 work.
the model (`FindInbound`, `TphPlan`, `InstanceArtifacts`, `ReportRows`). `template` is not
ejectable: it has no emit logic of its own. Packaged output is unchanged and gated to be.
- **`api-docs` is a generator you can own.** `dotnet meta eject api-docs` copies the page
- generator with its page model, builder, renderer and path math. `dotnet meta docs` hands
- off to `dotnet run --project codegen -- docs` once `codegen/generators/ApiDocsGenerator.cs`
- exists, so your copy writes the reference. What the reference documents is still what the
- packaged generators emit for (`GeneratorApplicability.Packaged`): an owned `routes` whose
+ generator with its page model, builder, renderer and path math. The api reference is an
+ entry in `codegen/Generators.cs` like any other (see **Removed**: `dotnet meta docs` is
+ gone), with `OutDir`, `ApiSubDir`, `ProjectName` and `ModelBaseUrl` as init properties;
+ `dotnet meta gen` writes the pages and `verify --codegen` checks them. List your copy in
+ place of the packaged entry. What the reference documents is still what the packaged
+ generators emit for (`GeneratorApplicability.Packaged`): an owned `routes` whose
`AppliesTo` you changed is documented as the packaged one behaves, unless you edit the
owned builder.
- `dotnet meta gen` and `verify --codegen` leave a docs generator out. `gen` says so only
- when a `--generators` selection names `api-docs`; an owned `Program.cs` that lists it runs
- without a warning.
- New public API in `MetaObjects.Codegen`: `IDocsGenerator`, `DocsRunFacts`,
- `GeneratorApplicability`, `GenContext.Applicability` and `GenContext.Docs`,
- `CodegenCli.RunDocs`, and `ValueObjectNames.ResolveValueObject` (moved from
+ New public API in `MetaObjects.Codegen`: `GeneratorApplicability`,
+ `GenContext.Applicability`, the `ApiDocsGenerator` init properties and
+ `ApiDocsGenerator.DefaultApiSubDir`, and `ValueObjectNames.ResolveValueObject` (moved from
`RenderHelperGenerator`, where it was `internal`). `CSharpStringLiteral` moved from
`Fr010FieldMapping` to `CSharpNaming`, so `names` needs no emitter; the old member stays as an
`[Obsolete]` forwarder (see **Deprecated**).
@@ -340,13 +339,14 @@ generator catalog and the Atlas evaluation are 1.2 work.
`instance_artifacts`). `template` is not ejectable: it has no emit logic of its own. Packaged
output is unchanged and gated to be.
- **`api-docs` is a generator you can own.** `metaobjects eject api-docs` copies the page
- generator with its page model, builder, renderer and path math. The api reference is now a
- generator (`docs_surface = "api"`) that `metaobjects docs` runs: an owned copy is listed
- under the new `docs.generators` block of `metaobjects.config.yaml` and runs in place of the
- packaged one. `gen` and `verify --codegen` skip a docs generator they find under a target
- and say so: the packaged `api-docs` name is to be removed (`metaobjects docs` already runs
- it), an owned copy is to be listed under `docs.generators`. The reference asks `shared_predicates` which objects get a router (the new
- `emits_writable_router` and the moved `is_read_only_routed`, beside `emits_router`), the
+ generator with its page model, builder, renderer and path math. The api reference is an
+ ordinary generator listed in a target's `generators` (see **Removed**: `metaobjects docs`
+ is gone), with its options in the new top-level `apiDocs` block of
+ `metaobjects.config.yaml` (`apiSubDir`, `projectName`, `modelBaseUrl`); `metaobjects gen`
+ writes the pages into that target's `outDir` and `verify --codegen` checks them. An owned
+ copy is listed in place of `api-docs`. The reference reads `shared_predicates` to decide
+ which objects get a router (the new `emits_writable_router` and the moved
+ `is_read_only_routed`, beside `emits_router`), the
module your owned `routes` and `filter-allowlist` read, so one edit to the owned predicate
moves the router, the allowlist and the pages together. The cross-port manifest lists
`api-docs` for `python`.
@@ -356,9 +356,10 @@ generator catalog and the Atlas evaluation are 1.2 work.
`fr019_shared_enum`, `m2m_codegen`, and the api reference as `api_docs_model`,
`api_docs_builder`, `api_docs_renderer` and `api_docs_paths`. `metaobjects.apidocs` keeps
`naming` and exports `PythonApiModelBuilder`, `PythonApiDocsRenderer` and the path
- functions as before. New public API: `DocsRunFacts`, `GenContext.docs`, `DEFAULT_API_SUBDIR`
- and `DOCS_SURFACE_API` in `metaobjects.codegen.generator`, `ProjectConfig.docs_generators`,
- `GeneratorEntry.docs_surface`, and `metaobjects.codegen.docs_run`.
+ functions as before. New public API: `DEFAULT_API_SUBDIR` in
+ `metaobjects.codegen.generator`, `ApiDocsGenerator(api_subdir, project_name, model_base_url)`,
+ `GeneratorBuildContext.api_docs`, and `ApiDocsConfig`, `ProjectConfig.api_docs` and
+ `TargetConfig.template_spec` in `metaobjects.codegen.project_config`.
- **A copy you ejected before this release** keeps running: it imports the emitters from
their old paths, which are deprecated aliases (see **Deprecated**), so it calls the
packaged emitters as it always did. `metaobjects gen --list` marks it `DIFFERS: N behind`.
@@ -383,7 +384,7 @@ generator catalog and the Atlas evaluation are 1.2 work.
one documented difference). Another fails on a helper module no registry entry hands over,
or a generator module no registry entry names.
The nine Python entries of the shared eject contract's `eject.pending` list are closed, and the
- contract test runs `api-docs` through `metaobjects docs`, the way it is run.
+ contract test runs `api-docs` through `metaobjects gen`, the way it is run.
- **The stock `entity`, `queries`, `routes` and `barrel` generators are importable again
(TypeScript).** `entityFile`, `queriesFile`, `routesFile` and `barrel` are exported from
@@ -1070,15 +1071,92 @@ until you regenerate.
`metaobjects.codegen.extract_delegate_emitter`, `.fr010_field_mapping`,
`.output_format_spec_emitter`, `.extract_schema_emitter`, `metaobjects.codegen.generators.fr019_shared_enum`,
`.m2m_codegen`, and `metaobjects.apidocs.api_model`, `.builder`, `.renderer`, `.paths` are
- each one-line modules that become the new module and warn (`DeprecationWarning`) on import.
+ each one-line modules that become the new module and warn (`FutureWarning`, which Python
+ shows by default, unlike a `DeprecationWarning` raised inside an imported module) on import.
Same objects, so nothing else changes; import from `metaobjects.codegen.runtime` (the api
reference modules are `api_docs_model`, `api_docs_builder`, `api_docs_renderer` and
- `api_docs_paths` there). The old paths will be removed in a later release.
+ `api_docs_paths` there). The old paths will be removed in 2.0.
`metaobjects.codegen.generators.router_generator.is_read_only_routed` moved to
`metaobjects.codegen.runtime.shared_predicates`; the router module still re-exports it.
+- **C#: `CodegenCli.ComposeGenerators` is `[Obsolete]`.** It composed an owned list with a
+ `--generators` selection by ADDING packaged generators the list did not name. Since 1.1.0 a
+ selection only narrows the declared list (`CodegenCli.SelectDeclared`, ADR-0034 Amendment 9),
+ and `CodegenCli.Run` no longer calls it. It still behaves as in 1.0 for a caller of its own.
+
### Removed
+- **C#: `dotnet meta docs`, and `dotnet meta gen` without a generator list.** Every generator,
+ the api reference included, is an entry in `codegen/Generators.cs`, and `dotnet meta gen`
+ runs that list and nothing else (ADR-0034 Amendment 9). `dotnet meta docs` prints a line
+ naming `dotnet meta gen` and the migration section, and exits 2. `dotnet meta gen` and
+ `dotnet meta verify --codegen` in a project with no `codegen/Generators.cs` exit 2 with
+ `no codegen/Generators.cs` and write nothing: the in-process `gen
--out
+ --generators ` form, which chose the generators on the command line, is gone. `gen`
+ given `--project` or `--model-base-url` (1.0 docs flags) fails with `found 1.0 docs
+ configuration` before writing anything.
+ - **New:** `dotnet meta init` writes `codegen/Generators.cs` (the list, a static `All`),
+ `codegen/Program.cs` (two lines: `using MetaObjects.Codegen;` and
+ `return CodegenCli.Run(args, Generators.All);`) and `codegen/Codegen.csproj`, each only
+ when missing; `dotnet meta eject` writes the same three files when they are missing, with
+ the copies it ejected already listed. `--generators` now narrows a run to declared entries
+ and refuses a name the list does not declare. Each entry may name its own output directory
+ (`IGenerator.OutDir`, a default interface member returning `null`, which writes under
+ `--out`), and `verify --codegen` compares each entry's directory; `RequirementTestsGenerator`
+ takes one (`OutDir`), so the xUnit tests go to the test project in the same run. A
+ `verify --codegen --generators` narrowing compares only what the named entries write. A
+ blank `--generators` exits 2 rather than running the whole list. The
+ owned runner now also refuses an unknown flag and an unknown `--baseline` value, as
+ `dotnet meta gen` did.
+ - **A 1.0 `codegen/Program.cs` is refused.** One that does not run `Generators.All` would
+ ignore the list, so `dotnet meta gen` and `verify --codegen` exit 2 with
+ `codegen/Program.cs does not run Generators.All` and write nothing; `init` and `eject` say
+ to replace it, and warn when `codegen/Codegen.csproj` pins an older `MetaObjects.Codegen`.
+ - **A template spec is an entry too.** `MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator`
+ (`Spec`, `TemplateRoot`, `OutDir`) declares one in `codegen/Generators.cs`. `gen` and
+ `verify --codegen` no longer read `--template-spec` or discover a project-root
+ `template-spec.json`; a run that finds either with no such entry fails with
+ `found 1.0 template-spec configuration` before writing anything.
+ - Removed public API, all added after 1.0.13 and never released: `IDocsGenerator`,
+ `DocsRunFacts`, `GenContext.Docs`, `CodegenCli.RunDocs`, `CodegenCli.DocsOutcome`,
+ `CodegenCli.DocsGeneratorSkipped`, `CodegenHandoff.HasOwnedDocs`. `MetaObjects.Cli.DocsCommand`
+ (the removed command's logic) is gone with the command.
+ - **To upgrade**, run `dotnet meta init`, move each generator you ran into
+ `codegen/Generators.cs` with the api reference's flags as `ApiDocsGenerator` init
+ properties, and replace `dotnet meta docs` with `dotnet meta gen` in scripts: before and
+ after, and each flag's new home, are in
+ [docs-are-generators.md, "C#"](docs/features/migrations/docs-are-generators.md#c).
+- **Python: `metaobjects docs`, and `metaobjects gen` without a config.** Every generator, the
+ api reference included, is an entry in a target's `generators` in `metaobjects.config.yaml`,
+ and `metaobjects gen` runs that list and nothing else (ADR-0034 Amendment 9). `metaobjects
+ docs` prints a line naming `metaobjects gen` and the migration section, and exits 2.
+ `metaobjects gen` and `metaobjects verify --codegen` with no `metaobjects.config.yaml` exit 2
+ with `no metaobjects.config.yaml` and write nothing: the positional
+ `gen --out --generators ` form and the no-config fallback are gone,
+ and with a config the positional `` and `--out` are refused with a line naming
+ `metadata` and the targets' `outDir`. A config with a top-level `docs` block fails `gen` and
+ `verify --codegen` with `found 1.0 docs configuration` before anything is written.
+ - **New:** the top-level `apiDocs` block (`apiSubDir`, `projectName`, `modelBaseUrl`) carries
+ the api reference's options, and a target's `templateSpec: ` declares a template-spec
+ whose generators write into that target, replacing the `--template-spec` flag, which is now
+ refused. A project-root `template-spec.json` that no target declares, which the 1.0 flag form
+ found on its own, fails `gen` and `verify --codegen` with
+ `found 1.0 template-spec configuration`. `--generators` narrows a run to declared entries and refuses a name no target
+ declares; `--column-naming`, `--entities`, `--provider`, `--templates` and `--baseline` now
+ apply to the config's list.
+ - **New refusals.** A target with no `generators` key exits 2 with the migration line
+ (`generators: []` still writes nothing and says so). An `--entities` name that matches no
+ object exits 2, where it used to warn and exit 0. A blank `--generators` (an unset shell
+ variable) exits 2 rather than running the whole list. A `generators` entry holding a comma
+ (`"entity, routes"`) is refused: list each generator as its own entry.
+ - Removed public API, all added after 1.0.13 and never released: `DocsRunFacts`,
+ `GenContext.docs`, `DOCS_SURFACE_API`, `ProjectConfig.docs_generators`,
+ `GeneratorEntry.docs_surface` and the `metaobjects.codegen.docs_run` module.
+ - **To upgrade**, list each generator in a target of `metaobjects.config.yaml`, `api-docs` in a
+ target whose `outDir` is the docs directory, move the docs flags into `apiDocs`, and replace
+ `metaobjects docs` with `metaobjects gen` in scripts: before and after, and each flag's new
+ home, are in
+ [docs-are-generators.md, "Python"](docs/features/migrations/docs-are-generators.md#python).
- **Java and Kotlin: the `metaobjects:docs` goal.** Documentation is generated the way code
is: the api reference is a `JavaApiDocsGenerator` or `KotlinApiDocsGenerator` ``
entry in the list `metaobjects:generate` runs, with its options in `` (see
diff --git a/agent-context/skills/metaobjects-audit/references/csharp.md b/agent-context/skills/metaobjects-audit/references/csharp.md
index 41944575c..0021f5894 100644
--- a/agent-context/skills/metaobjects-audit/references/csharp.md
+++ b/agent-context/skills/metaobjects-audit/references/csharp.md
@@ -26,9 +26,9 @@ Generated files are named `.g.cs` and carry an `@generated` guard commen
The metadata source directory is the first positional arg (``); `--out`
is required (errors if absent, no default). The runner overwrites only files bearing
this header; it refuses to touch files without it. Locate the output directory from
-`codegen/Generators.cs` (the project's generator list, each entry's `OutDir` init
-property — scaffolded by `dotnet meta init`, new in 1.1.0) or, for a project that predates
-it, from how the team invokes `dotnet meta gen` (or a CI script).
+how the team invokes `dotnet meta gen` (or a CI script) — the arg is explicit. What is
+generated is the list in `codegen/Generators.cs`; an entry with its own `OutDir`
+(`ApiDocsGenerator`, `RequirementTestsGenerator`) writes there instead of under `--out`.
---
@@ -38,7 +38,7 @@ it, from how the team invokes `dotnet meta gen` (or a CI script).
dotnet meta gen metaobjects --out Generated --namespace Acme.Generated
dotnet meta gen --list # list all registered generators
-dotnet meta gen --generators entity,db-context,routes # select a subset
+dotnet meta gen metaobjects --out Generated --generators entity # narrow to declared entries
dotnet meta verify --codegen # regenerate to temp, diff vs committed output
dotnet meta verify --templates # check {{field}} refs vs payload VO (bare default)
@@ -60,26 +60,28 @@ rejected (exit 2).
| `FromSqlInterpolated(` outside `.g.cs` | stored-proc call — candidate for the `callable` generator |
| `// keep in sync with` / `// mirrors the` | second-source-of-truth comment — always a finding |
| `HasPrecision(` hand-coded | `field.decimal` with `@precision`/`@scale` drives this from the `entity` generator |
-| a table/column string in raw-SQL EF calls, or `nameof(Entity.Prop)` standing in for a column | second spelling of a declared physical name — reference `Names.g.cs` (`AuthorNames.SourcePrimaryTable` / `Column`, emitted when `names` is named in `--generators` — `Names.Name` is the OBJECT's name, not the table); an EF property inside LINQ is the typed handle — correct |
+| a table/column string in raw-SQL EF calls, or `nameof(Entity.Prop)` standing in for a column | second spelling of a declared physical name — reference `Names.g.cs` (`AuthorNames.SourcePrimaryTable` / `Column`, emitted when `codegen/Generators.cs` lists `names` — `Names.Name` is the OBJECT's name, not the table); an EF property inside LINQ is the typed handle — correct |
---
## Owned generators
-`dotnet meta eject ...` copies a reference generator into `codegen/generators/`
-and scaffolds an owned `codegen/` console project; once `codegen/Codegen.csproj` exists,
-`dotnet meta gen` and `dotnet meta verify --codegen` hand off to it. Ejecting `routes` also
+`dotnet meta eject ...` copies a reference generator into `codegen/generators/`;
+the copy is listed in `codegen/Generators.cs`, the generator list `dotnet meta gen` and
+`dotnet meta verify --codegen` run through the `codegen/` console project. Ejecting `routes` also
copies the helper runtime the generated routes call (`FilterParser`, `EfCoreFilterDispatch`,
`ValueObjectValidator`, `ConstraintErrors`, `Iso8601TimestampConverter`) into
`codegen/runtime/` under the namespace `Codegen.Runtime`. That folder is owned code, not a
hand-rolled filter parser: do not flag it. `dotnet meta gen --list` marks each owned copy
`identical` or `DIFFERS`. A generator the project WRITES is an `IGenerator` listed in
-`codegen/Generators.cs` the same way.
+`codegen/Generators.cs` the same way. With no `codegen/Generators.cs`, `dotnet meta gen`
+exits 2 and generates nothing.
**A finding of the form "the built-ins do not emit the shape this project needs", or a
hand-written layer the model fully describes, resolves to a generator the project writes**
-(ADR-0034 Amendment 4), or to a declarative template (`dotnet meta gen --template-spec
- --template-root `) where the shape is what matters. Recommend it; it is the
+(ADR-0034 Amendment 4), or to a declarative template (a `TemplateSpecGenerator { Spec =
+"", TemplateRoot = "" }` entry in `codegen/Generators.cs`) where the shape is what
+matters. Recommend it; it is the
primary path. Worked template example: `docs/ports/csharp.md`.
To re-scaffold the agent-context into a C# project, use the Node `meta` CLI (the
@@ -124,6 +126,6 @@ version across these packages is an intra-port skew finding.
(`?search=`, `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard gating) is
TS-only — do NOT flag its absence in C#.
- **Output-parser codegen ships in C#.** `output-parser` / `extractor` / `render-helper`
- generators are available (`dotnet meta gen --generators output-parser`). Absence of
+ generators are available (list `OutputParserGenerator` in `codegen/Generators.cs`). Absence of
wired output parsers where a **responding `template.prompt`** (`@responseRef`) exists IS a
finding. ADR-0052: a `template.output` is outbound only and generates no parser.
diff --git a/agent-context/skills/metaobjects-audit/references/python.md b/agent-context/skills/metaobjects-audit/references/python.md
index 57368c30a..47beec1af 100644
--- a/agent-context/skills/metaobjects-audit/references/python.md
+++ b/agent-context/skills/metaobjects-audit/references/python.md
@@ -19,8 +19,8 @@ SQLAlchemy Core or asyncpg), or a hand-written repository implementation.
## Finding generated files
-Generated files land under the `--out` directory passed to `metaobjects gen` and
-carry an `@generated` guard comment:
+Generated files land under each target's `outDir` in `metaobjects.config.yaml` (the
+generator list `metaobjects gen` runs) and carry an `@generated` guard comment:
```python
# @generated by metaobjects
@@ -29,20 +29,21 @@ carry an `@generated` guard comment:
The runner overwrites only files bearing this header. Common layout: one Pydantic
model file per entity (e.g. `generated/author.py`) and one FastAPI `APIRouter` file
per writable entity (e.g. `generated/author_router.py`). There is no enforced
-directory structure beyond `--out`.
+directory structure beyond the target's `outDir`.
---
## Run codegen + verify
```bash
-metaobjects gen ./metaobjects --out ./generated
-metaobjects gen ./metaobjects --out ./generated --generators entity,routes,filter-allowlist
-metaobjects verify --codegen # regenerate to temp, diff vs committed --out tree (bare default)
+metaobjects gen # run every target's generators (metaobjects.config.yaml)
+metaobjects gen --generators entity # narrow to declared entries
+metaobjects verify --codegen # regenerate to temp, diff vs each committed outDir (bare default)
metaobjects verify --templates # {{field}} refs vs payload VO
```
-`--generators` accepts stable generator names: `entity`, `routes`, `filter-allowlist`,
+With no `metaobjects.config.yaml`, `gen` exits 2 and generates nothing. A target's
+`generators` takes stable generator names: `entity`, `routes`, `filter-allowlist`,
`names`, `output-parser`, `extractor`, `output-prompt`, `render-helper`, `trace-helper`,
`template`. (There is no `payload` generator since ADR-0056: a template's payload and
response types are the value objects' own `entity` models.) Schema migration + live-DB drift run through the **Node `meta` CLI**
@@ -61,7 +62,7 @@ regardless of server language — see the migration reference.
| `def get_all_` / `def create_` / `def update_` / `def delete_` in non-generated files | hand-rolled CRUD — compare to the generated router |
| `# keep in sync with` / `# mirrors the` | second-source-of-truth comment — always a finding |
| `try: ... except KeyError` / `?? ''` around format strings in prompt code | silent-degradation hack around a prompt payload — flag it |
-| a table/column string in the repository implementation (SQLAlchemy Core, asyncpg/psycopg SQL) | second spelling of a declared physical name — reference `_names.py` (`AUTHOR_SOURCE_PRIMARY_TABLE` / `AUTHOR__COLUMN`, emitted when `names` is named in `--generators`); no typed handle exists on this port, so that is never the reason to waive it |
+| a table/column string in the repository implementation (SQLAlchemy Core, asyncpg/psycopg SQL) | second spelling of a declared physical name — reference `_names.py` (`AUTHOR_SOURCE_PRIMARY_TABLE` / `AUTHOR__COLUMN`, emitted when a target lists `names`); no typed handle exists on this port, so that is never the reason to waive it |
---
@@ -69,14 +70,14 @@ regardless of server language — see the migration reference.
Python generators are owned the same way as on every port. `metaobjects eject `
copies a reference into `codegen/generators/`, and a generator the project WRITES is wired
-the same way — a `module:symbol` entry in `--generators` or in `metaobjects.config.yaml`,
+the same way — a `module:symbol` entry in a target's `generators` in `metaobjects.config.yaml`,
reading the model through `metaobjects.codegen.model_walk`. (`--provider module:symbol`
registers **metamodel vocabulary**, not a generator — do not read it as one.)
**A finding of the form "the built-ins do not emit the shape this project needs", or a
hand-written layer the model fully describes, resolves to a generator the project writes**
-(ADR-0034 Amendment 4), or to a declarative template (`metaobjects gen --template-spec
- --templates `) where the shape is what matters. Recommend it; it is the
+(ADR-0034 Amendment 4), or to a declarative template (a target's `templateSpec: `,
+refs under `metaobjects gen --templates `) where the shape is what matters. Recommend it; it is the
primary path. Worked template example: `docs/ports/python.md`.
To re-scaffold the agent-context into a Python project, use the Node `meta` CLI (the
diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md
index b4fe9bcaf..0a93a084f 100644
--- a/agent-context/skills/metaobjects-codegen/SKILL.md
+++ b/agent-context/skills/metaobjects-codegen/SKILL.md
@@ -89,8 +89,8 @@ what it emits.
| Port | Registration | Model helpers |
|---|---|---|
| TypeScript | import it in `metaobjects.config.ts`, add it to `generators: [...]` (the scaffold does both) | `@metaobjectsdev/codegen-ts`: `objectRefTarget`, `enumValues`, `effectivePackage`, `packageToPath`, `servedPath`, `toCamelCase`/`toPascalCase`/`toSnakeCase`/`pluralize`, `isAbstract`, `hasAnyRdbSource`, `servesReadApi`/`servesWriteApi` |
-| Python | a `module:symbol` entry in `--generators` or a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one, never the class | `metaobjects.codegen.model_walk` |
-| C# | `new YourGenerator()` in the owned `codegen/Generators.cs` list, which `codegen/Program.cs` passes to `CodegenCli.Run`; `dotnet meta gen` / `verify --codegen` hand off to `codegen/` whenever `codegen/Codegen.csproj` exists | node accessors, `ValueObjectNames.ResolveFieldRef`, `CSharpNaming.RoutePath` |
+| Python | a `module:symbol` entry in a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one, never the class | `metaobjects.codegen.model_walk` |
+| C# | `new Codegen.Generators.YourGenerator()` in `codegen/Generators.cs` (`dotnet meta init` writes it); `dotnet meta gen` / `verify --codegen` run that list | node accessors, `ValueObjectNames.ResolveFieldRef`, `CSharpNaming.RoutePath` |
| Java / Kotlin | `` plus ``, from a module on the plugin's classpath; extend `FileEmittingGenerator` | `com.metaobjects.generator.ModelWalk` |
**Verify it** — nothing to register. `verify --codegen` (`mvn metaobjects:verify` on the
diff --git a/agent-context/skills/metaobjects-codegen/references/csharp.md b/agent-context/skills/metaobjects-codegen/references/csharp.md
index 0693a4aeb..601539ff7 100644
--- a/agent-context/skills/metaobjects-codegen/references/csharp.md
+++ b/agent-context/skills/metaobjects-codegen/references/csharp.md
@@ -22,6 +22,7 @@ Per the always-on descriptor:
```bash
dotnet tool install --global MetaObjects.Cli # provides `dotnet meta`
dotnet add package MetaObjects.Codegen # the codegen generators
+dotnet meta init # writes codegen/: Generators.cs, Program.cs, Codegen.csproj
```
`dotnet meta` is a .NET tool invoked as `dotnet meta ` (the underlying
@@ -29,18 +30,38 @@ command is `dotnet-meta`).
## Run
+The generator list is `codegen/Generators.cs` (ADR-0034 Amendment 9) — every generator the
+project runs, documentation included, each with its options as init properties.
+`codegen/Program.cs` is two lines that run it; `dotnet meta init` writes both and
+`codegen/Codegen.csproj`. Nothing is generated that the list does not declare.
+
+```csharp
+// codegen/Generators.cs
+using MetaObjects.Codegen;
+
+public static class Generators
+{
+ public static IReadOnlyList All { get; } =
+ [
+ new MetaObjects.Codegen.Generators.EntityGenerator(),
+ new MetaObjects.Codegen.Generators.DbContextGenerator(),
+ new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(),
+ new MetaObjects.Codegen.Generators.RoutesGenerator(),
+ ];
+}
+```
+
```bash
-dotnet meta gen metaobjects \
- --out Generated \
- --namespace Acme.Generated
+dotnet meta gen metaobjects --out Generated --namespace Acme.Generated # runs the list
dotnet meta gen --list # list registered generators
-dotnet meta gen metaobjects --out Generated --generators entity,db-context,routes # select a subset
+dotnet meta gen metaobjects --out Generated --generators entity # narrow to declared entries
dotnet meta verify metaobjects --codegen --out Generated # codegen-drift gate (regenerate + diff vs committed)
```
-`dotnet meta verify --codegen` re-runs the SELECTION and diffs, so it takes the same
-`--generators` the `gen` that produced the output used; with none named it reports that
-there is nothing to check.
+`gen` and `verify --codegen` always run the list through `dotnet run --project codegen`; with
+no `codegen/Generators.cs` they exit 2 and write nothing. `--generators` narrows a run to the
+declared entries named and never adds one: naming an entry the list does not declare is
+refused. `verify --codegen` regenerates the same list, so it never disagrees with `gen`.
`dotnet meta verify` defaults to `--templates` (the FR-004 prompt/template drift
gate, see the prompts reference); `--codegen` is the codegen-output drift gate.
@@ -58,7 +79,7 @@ set and no separate docs command. `--generators ` narrows a run to entrie
list already declares, by stable name; it never adds one, and naming an entry the list
does not declare is refused. `dotnet meta gen --list` is the catalog of what the port
registers, not what this project runs. Output lands under `--namespace` in
-`--output-dir` unless a generator's own init properties say otherwise — documentation
+`--out` unless a generator's own init properties say otherwise — documentation
does (below).
| Stable name | Output |
@@ -69,7 +90,7 @@ does (below).
| `filter-allowlist` | per-entity `FilterAllowlist` (FR-009 — the server-side field+operator allowlist the routes validate against). |
| `callable` | `.callable.g.cs` — an FR-015 calling method for a `source.rdb @kind="storedProc"|"tableFunction"`, via EF `FromSqlInterpolated` (args from the `@parameterRef` value object in declaration order). |
| `output-parser` / `extractor` / `output-prompt` / `render-helper` | the prompt-pillar artifacts for a **responding `template.prompt`** — one carrying `@responseRef` (ADR-0052: these tiers are INBOUND; `template.output` is outbound only and emits no parser). The strict parser, the tolerant `extract`, the **output-format prompt fragment** (`output-prompt`; presentation via `@promptStyle: guide`/`inline`/`exampleOnly`), and the typed render helper. They reference the value objects' POCOs from `entity` and declare no payload type — wire `entity` too (`--list` says so). See the **prompts** reference. |
-| `names` | `Names.g.cs` — `public abstract class Names` of `const` physical database names, MIRRORING THE METADATA TREE: the object's own `Type`/`SubType`/`Name` (`Name` is the OBJECT's name, never a physical one), then per `source.rdb` keyed by `@role` — `SourcePrimaryType`/`SubType`/`Kind`, `SourcePrimarySchema` when declared, and the physical name under the alias for its `@kind` (`SourcePrimaryTable` / `SourceReplicaView` / `SourcePrimaryProc` / `…Function` / `…MaterializedView`) — then a `Field`/`Column` pair per field, then each identity and index (`IdentityPkName`, and `IdentityUqCustEmailIndex` / `IndexIxCustStatusIndex` — the DATABASE name, carried for `identity.secondary` and `index.lookup` only), then a complete `ColumnsByField`. There is no `ReadOnly`: it is a derivation over `@kind`, so ask `SourcePrimaryKind`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (its own identity + columns, and NO source members — it has no table). A class whose object extends another one **inherits** it (`class CopayAuthNames : AuthNames`) rather than restating its constants — a C# `const` is inherited, so `CopayAuthNames.IdColumn` and `CopayAuthNames.SourcePrimaryTable` resolve through the base (`Type`/`SubType`/`Name` are redeclared with `new`, since every artifact carries its own). `abstract`, not `static`, precisely so it can be inherited; it still cannot be instantiated. In the run, `entity` and `db-context` reference the constants (`[Table(OrderNames.SourcePrimaryTable)]`, `[Column(OrderNames.StatusColumn)]`, `.ToView(OrderNames.SourceReplicaView)`); a `--generators` selection omitting `names` falls back to literals. |
+| `names` | `Names.g.cs` — `public abstract class Names` of `const` physical database names, MIRRORING THE METADATA TREE: the object's own `Type`/`SubType`/`Name` (`Name` is the OBJECT's name, never a physical one), then per `source.rdb` keyed by `@role` — `SourcePrimaryType`/`SubType`/`Kind`, `SourcePrimarySchema` when declared, and the physical name under the alias for its `@kind` (`SourcePrimaryTable` / `SourceReplicaView` / `SourcePrimaryProc` / `…Function` / `…MaterializedView`) — then a `Field`/`Column` pair per field, then each identity and index (`IdentityPkName`, and `IdentityUqCustEmailIndex` / `IndexIxCustStatusIndex` — the DATABASE name, carried for `identity.secondary` and `index.lookup` only), then a complete `ColumnsByField`. There is no `ReadOnly`: it is a derivation over `@kind`, so ask `SourcePrimaryKind`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (its own identity + columns, and NO source members — it has no table). A class whose object extends another one **inherits** it (`class CopayAuthNames : AuthNames`) rather than restating its constants — a C# `const` is inherited, so `CopayAuthNames.IdColumn` and `CopayAuthNames.SourcePrimaryTable` resolve through the base (`Type`/`SubType`/`Name` are redeclared with `new`, since every artifact carries its own). `abstract`, not `static`, precisely so it can be inherited; it still cannot be instantiated. In the run, `entity` and `db-context` reference the constants (`[Table(OrderNames.SourcePrimaryTable)]`, `[Column(OrderNames.StatusColumn)]`, `.ToView(OrderNames.SourceReplicaView)`); a run without `names` falls back to literals. |
| `template` | the generic Mustache `templateGenerator()` primitive. |
Metadata lives under `metaobjects/` (or wherever you point `--metadata-dir`) in the
@@ -121,11 +142,13 @@ on every run, so never edit them: the project's code goes in one witness class
(`.RequirementWitnesses` by default) that implements every generated interface.
Implement each member EXPLICITLY (`void Requirements__Witnesses.req_…() { … }`): a
witness whose requirement is retired or deleted then stops compiling (CS0539), where an
-implicit `public void req_…()` goes stale silently. Give the generator its own
-`dotnet meta gen … --out --generators requirement-tests` run (a run has
-one `--out`, and the test project needs xunit), and pass `--namespace` to
-`verify --codegen` for that directory. Options (`TestNamespace`, `WitnessClass`, `Grain`,
-`Filter`, `Renderer`, `WarnUncovered`) are init properties set on the entry in an owned
+implicit `public void req_…()` goes stale silently. List it in `codegen/Generators.cs` with
+its own `OutDir`, the test project's directory, since the tests need xunit:
+`new MetaObjects.Codegen.Generators.RequirementTestsGenerator { OutDir = "tests/Acme.Shop.Tests/Generated" }`.
+The plain `dotnet meta gen ./metadata --out ./Generated` then writes them there and
+`dotnet meta verify --codegen` checks them there, in the same run as everything else.
+Options (`OutDir`, `TestNamespace`, `WitnessClass`, `Grain`, `Filter`, `Renderer`,
+`WarnUncovered`) are init properties set on the entry in an owned
`codegen/Generators.cs`; `Grain` needs `using MetaObjects.Core.Requirement;`. A filter
REPLACES the default of functional L4/L5. `dotnet meta eject requirement-tests` copies the
generator with its default rendering. The requirement checks in `dotnet meta verify` are
@@ -144,7 +167,7 @@ new ApiDocsGenerator { OutDir = "Docs" } // → Docs/api/csharp (AGENT-API.md
`dotnet meta gen` writes it, alongside everything else `codegen/Generators.cs` declares.
`AGENT-API.md` has the exact imports, signatures, and payload field shapes for the
generated code. **Before calling any generated code, read `api/csharp/AGENT-API.md`.**
-Options (init properties): `OutDir` (required), `ApiSubDir` (default `api/csharp`),
+Options (init properties): `OutDir` (give it a directory of generated files only; unset, the pages land under `--out`), `ApiSubDir` (default `api/csharp`),
`ProjectName`, `ModelBaseUrl`. To write only the documentation,
`dotnet meta gen --generators api-docs`. Migrating a project that ran `dotnet meta docs`:
`docs/features/migrations/docs-are-generators.md`, section "C#".
@@ -159,10 +182,8 @@ what runs. (The other ports leave a repository seam; C# does not.)
## Write your own generator
For any output the model describes and no reference emits, write an `IGenerator` in the
-owned console project `codegen/`. `dotnet meta gen` and `dotnet meta verify --codegen`
-hand off to it whenever `codegen/Codegen.csproj` exists; `dotnet meta eject `
-scaffolds that project (or write `Codegen.csproj` — an `Exe` referencing
-`MetaObjects.Codegen` at your tool's version — and `Program.cs` yourself).
+console project `codegen/` (`dotnet meta init` writes it) and list it in
+`codegen/Generators.cs`; `dotnet meta gen` and `dotnet meta verify --codegen` run that list.
```csharp
// codegen/generators/FieldListGenerator.cs
@@ -182,10 +203,8 @@ public sealed class FieldListGenerator : IGenerator
}
```
-List it in `codegen/Generators.cs` — `IReadOnlyList Generators = [new
-FieldListGenerator()];` — which `codegen/Program.cs` passes to `CodegenCli.Run`. An owned
-generator a `--generators` selection does not name still exists in the list but does not
-run that invocation. Model reads: `Fields()`, `Attr(n)`,
+List it in `codegen/Generators.cs` as `new Codegen.Generators.FieldListGenerator()`; every
+entry runs, and `--generators` only narrows. Model reads: `Fields()`, `Attr(n)`,
`ResolvedIsArray()`, `EffectiveEnumValues`, `MaxLength` resolve; `IsArray`, `OwnAttr`,
`EnumValues` do not. `ValueObjectNames.ResolveFieldRef(field, ctx.Root)` resolves an
`@objectRef`; `CSharpNaming.RoutePath(obj)` is the REST segment; constants come from
diff --git a/agent-context/skills/metaobjects-codegen/references/owning-generators.md b/agent-context/skills/metaobjects-codegen/references/owning-generators.md
index d67ccc2f3..30845017e 100644
--- a/agent-context/skills/metaobjects-codegen/references/owning-generators.md
+++ b/agent-context/skills/metaobjects-codegen/references/owning-generators.md
@@ -19,7 +19,7 @@ it there, in the same change, and keep going:
|---|---|
| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own, and `docs-site`, whose builder stays in the package (`meta eject docs-site` copies the site's templates and assets into `codegen/docs-site/`); an ejected generator comes with the template modules that write its output (`codegen/templates/`): the text of what it emits is yours to change too. An ejected `docs` or `api-docs` goes in `generators`, like any other owned copy — there is no separate docs list, and `meta gen` runs it. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. |
| **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. |
-| **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Generators.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). |
+| **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Generators.cs` on C#, a `module:symbol` entry in a target's `generators` on Python — or a template spec. |
Then do **not**:
diff --git a/agent-context/skills/metaobjects-codegen/references/python.md b/agent-context/skills/metaobjects-codegen/references/python.md
index 8adbe6cdd..d2ff1bab7 100644
--- a/agent-context/skills/metaobjects-codegen/references/python.md
+++ b/agent-context/skills/metaobjects-codegen/references/python.md
@@ -30,41 +30,54 @@ pip install "pydantic[email]>=2" fastapi
## Run
+The generator list is `metaobjects.config.yaml` (ADR-0034 Amendment 9): each target names
+its `outDir` and the generators that write into it, documentation included. Nothing is
+generated that the config does not declare.
+
+```yaml
+# metaobjects.config.yaml
+metadata: metadata
+targets:
+ app:
+ outDir: generated
+ generators: [entity, filter-allowlist, routes]
+```
+
```bash
-metaobjects gen ./metadata --out ./generated [--package ]
-metaobjects gen ./metadata --out ./generated --generators entity,routes,filter-allowlist
-metaobjects verify ./metadata --codegen # codegen-drift gate (regenerate into a
- # temp dir + diff vs the committed --out tree)
+metaobjects gen # runs every target's generators into its outDir
+metaobjects gen --generators entity # narrow to declared entries (never adds one)
+metaobjects verify --codegen # regenerate the same list into a temp dir + diff
```
-`metaobjects verify` defaults to `--codegen` (the codegen-output drift gate; it shares
-the exact `gen` code path so the two can't diverge). `--templates` is the prompt/template
-drift gate (see the prompts reference). Schema migration + live-DB drift are **not**
-`metaobjects` — they run through the Node `meta` tool (see the migration reference).
+With no `metaobjects.config.yaml`, `gen` and `verify --codegen` exit 2 and write nothing.
+The 1.0 form `metaobjects gen --out --generators ` is not read;
+`--target`, `--entities`, `--column-naming`, `--provider`, `--templates` and `--baseline`
+still apply to the list. `metaobjects verify` defaults to `--codegen` (it shares the exact
+`gen` code path so the two can't diverge). `--templates` is the prompt/template drift gate
+(see the prompts reference). Schema migration + live-DB drift are **not** `metaobjects` —
+they run through the Node `meta` tool (see the migration reference).
## Docs — one more entry in a target's `generators`
There is no `metaobjects docs` command (removed in 1.1.0, ADR-0034 Amendment 9): the api
-reference is the `api-docs` generator, named in a target's `generators` in
-`metaobjects.config.yaml` like any other:
+reference is the `api-docs` generator, named in a target's `generators` like any other, in a
+target whose `outDir` holds generated files only:
```yaml
targets:
docs:
- outDir: ./docs
+ outDir: docs # → docs/api/python
generators: [api-docs]
-apiDocs:
- apiSubDir: api/python # default
+apiDocs: # optional: apiSubDir (default api/python), projectName, modelBaseUrl
+ projectName: acme
```
-`metaobjects gen` writes it (→ `./docs/api/python`: `AGENT-API.md` + per-entity pages),
-alongside everything else that target's `generators` declares. `AGENT-API.md` has the
-exact imports, signatures, and payload field shapes for the generated code. **Before
-calling any generated code, read `api/python/AGENT-API.md`.** Options live in the
-top-level `apiDocs` block: `apiSubDir` (default `api/python`), `projectName`,
-`modelBaseUrl`. To write only the documentation, `metaobjects gen --generators api-docs`.
-Migrating a project that ran `metaobjects docs`:
-`docs/features/migrations/docs-are-generators.md`, section "Python".
+`metaobjects gen` writes it (`AGENT-API.md` + per-entity pages), alongside everything else
+the config declares, and `verify --codegen` checks the pages like code. `AGENT-API.md` has
+the exact imports, signatures, and payload field shapes for the generated code. **Before
+calling any generated code, read `api/python/AGENT-API.md`.** To write only the
+documentation, `metaobjects gen --generators api-docs`. Migrating a project that ran
+`metaobjects docs`: `docs/features/migrations/docs-are-generators.md`, section "Python".
**The neutral model pages are a Node `meta gen` generator, not something this port emits.**
`meta docs`'s language-neutral model surface was retired along with the rest of
@@ -102,9 +115,8 @@ def field_list(): # the module:symbol target: an instance, or a function return
return FieldList()
```
-Wire it as `codegen.generators.field_list:field_list` in `--generators` (beside any stable
-names) or in a target's `generators` in `metaobjects.config.yaml`; `metaobjects verify
---codegen` with the same selection gates it. Read the model through
+Wire it as `codegen.generators.field_list:field_list` in a target's `generators` in
+`metaobjects.config.yaml`, beside any stable names; `metaobjects verify --codegen` gates it. Read the model through
`metaobjects.codegen.model_walk` (`is_required`, `max_length`, `field_is_array`,
`enum_values`, `object_ref_target`, `description`, `primary_key_fields`, `package_of`,
`route_path`, `per_entity` / `per_package` / `per_model`) — never through a field's `attr(n)`,
@@ -113,12 +125,10 @@ which is OWN-ONLY in Python and drops what a field inherits. Output can be any f
## Generators
-Wire generators by their stable name — **`--generators ` is REQUIRED**. There is
-no default set: a run that names none is a usage error and writes nothing (ADR-0034
-Amendment 2). `metaobjects gen --list` is the catalog. `verify --codegen` re-runs the
-SELECTION and diffs, so it takes the same `--generators`; with none named it reports that
-there is nothing to check.
-Output lands under `--out` (with the `@generated` guard header). Metadata is the same
+List generators by their stable name in a target's `generators`. There is no default set
+(ADR-0034 Amendment 2): a target whose list is empty writes nothing and says so.
+`metaobjects gen --list` is the catalog. `verify --codegen` regenerates the same list and diffs.
+Output lands under the target's `outDir` (with the `@generated` guard header). Metadata is the same
canonical JSON every port reads (fused-key form, `source.rdb` + `@table`, `@column` for
a renamed physical column).
diff --git a/agent-context/skills/metaobjects-codegen/references/template-or-code.md b/agent-context/skills/metaobjects-codegen/references/template-or-code.md
index 70e4ac937..a81e972a5 100644
--- a/agent-context/skills/metaobjects-codegen/references/template-or-code.md
+++ b/agent-context/skills/metaobjects-codegen/references/template-or-code.md
@@ -54,20 +54,17 @@ path is `TemplateScopeGenerator`, wired the same way with `` / `` already loads a consumer class from the project classpath.
-**C# and Python** have both. Programmatic: on C#, an `IGenerator` listed in the owned
-`codegen/Program.cs` (which `dotnet meta gen` / `verify --codegen` hand off to); on Python,
-a `module:symbol` entry in `--generators` or in `metaobjects.config.yaml`. (Python's
+**C# and Python** have both. Programmatic: on C#, an `IGenerator` listed in
+`codegen/Generators.cs` (the list `dotnet meta gen` / `verify --codegen` run); on Python,
+a `module:symbol` entry in a target's `generators` in `metaobjects.config.yaml`. (Python's
`--provider module:symbol` registers **metamodel vocabulary**, not a generator.)
-Declarative: `--template-spec ` — plus `--templates ` on Python or
-`--template-root ` on C# — and your entries are appended to your `--generators`
-selection. Worked examples with the full JSON: `docs/ports/python.md` and
-`docs/ports/csharp.md`.
-
-**The spec is auto-discovered, and that is load-bearing.** With no `--template-spec`, both
-ports read `/template-spec.json` — projectRoot being the metadata dir's parent.
-Keep it there: `verify --codegen` accepts no `--template-spec` flag, so the conventional path
-is how the drift gate learns your template generators exist. Put the spec somewhere else and
-reach it only by flag, and `verify` regenerates without it and reports its output as stale.
+Declarative: a JSON template-spec. On Python it is declared on a target
+(`templateSpec: `, refs resolved under `--templates `) and runs into that target's
+`outDir`. On C# it is an entry in `codegen/Generators.cs`:
+`new MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator { Spec = "template-spec.json",
+TemplateRoot = "templates" }`. Neither port reads a spec it was not given there: a leftover
+project-root `template-spec.json` with nothing declaring a spec fails the run. Worked examples
+with the full JSON: `docs/ports/python.md` and `docs/ports/csharp.md`.
So on every port, "I need a shape the built-ins do not emit" is answered by a generator of
your own or a template. Do not conclude the port cannot be customized.
diff --git a/agent-context/skills/metaobjects-codegen/references/typescript-mysql.md b/agent-context/skills/metaobjects-codegen/references/typescript-mysql.md
index 7c1e7618e..9cf2d06f0 100644
--- a/agent-context/skills/metaobjects-codegen/references/typescript-mysql.md
+++ b/agent-context/skills/metaobjects-codegen/references/typescript-mysql.md
@@ -7,8 +7,8 @@ MySQL is a **code generation and runtime** dialect. `meta gen` generates Drizzle
`mysqlTable` entities, Zod schemas, queries and Fastify/Hono routes for it, and the
`ObjectManager` reads and writes it. **`meta migrate` does not own a MySQL schema** — it
refuses `--dialect mysql` — so you write and evolve the DDL yourself, and `meta verify --db`,
-`--replay` and the `agent/schema.md` docs page do not apply. `meta verify --codegen`,
-`--templates` and `--docs` work as on any dialect.
+`--replay` and the `agent/schema.md` docs page do not apply. `meta verify --codegen`
+and `--templates` work as on any dialect.
## Contents
- TypeScript setup
diff --git a/agent-context/skills/metaobjects-codegen/references/typescript-templates.md b/agent-context/skills/metaobjects-codegen/references/typescript-templates.md
index c93099933..c4473a918 100644
--- a/agent-context/skills/metaobjects-codegen/references/typescript-templates.md
+++ b/agent-context/skills/metaobjects-codegen/references/typescript-templates.md
@@ -37,8 +37,8 @@ export default defineConfig({
- Abstract objects are excluded from every scope.
**Reusing a C#/Python spec.** Those ports declare the same generators as a JSON
-template-spec because their registries are closed and the flag is their only seam. Parse
-it and spread it:
+template-spec, because their generator lists are code (C#) or YAML (Python) rather than a
+TypeScript config. Parse it and spread it:
```ts
import { parseTemplateSpec, templateSpecToGenerators } from "@metaobjectsdev/codegen-ts";
diff --git a/agent-context/skills/metaobjects-prompts/references/csharp.md b/agent-context/skills/metaobjects-prompts/references/csharp.md
index 7a0e9f631..c0a2b8b76 100644
--- a/agent-context/skills/metaobjects-prompts/references/csharp.md
+++ b/agent-context/skills/metaobjects-prompts/references/csharp.md
@@ -22,10 +22,16 @@ naming convention, and the parser and the record can't silently drift.
`OutputParserGenerator` (stable name `output-parser`) runs as part of
`dotnet meta gen`, alongside `entity`, which emits the `@responseRef` value object's POCO
-it parses into (ADR-0056 — the template tier declares no copy):
+it parses into (ADR-0056 — the template tier declares no copy). List both in
+`codegen/Generators.cs`, the generator list `dotnet meta gen` runs:
+
+```csharp
+new MetaObjects.Codegen.Generators.EntityGenerator(),
+new MetaObjects.Codegen.Generators.OutputParserGenerator(),
+```
```bash
-dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog --generators entity,output-parser
+dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
```
## What it emits
@@ -67,10 +73,10 @@ For every responding `template.prompt`, `MetaObjects.Codegen`'s `OutputPromptGen
declaring a static `ResponseFormat` class with a `RenderFormat()` /
`RenderFormat(PromptOverrides)` pair, backed by the render engine's
`OutputFormatRenderer` — the "produce your answer like this" fragment for the model. It runs as part of the same `dotnet meta gen` invocation as the entity
-and parser generators:
+and parser generators, once `codegen/Generators.cs` lists it too:
-```bash
-dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog --generators entity,output-parser,output-prompt
+```csharp
+new MetaObjects.Codegen.Generators.OutputPromptGenerator(),
```
`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`)
diff --git a/agent-context/skills/metaobjects-prompts/references/python.md b/agent-context/skills/metaobjects-prompts/references/python.md
index 2799f7f95..e3e24ffec 100644
--- a/agent-context/skills/metaobjects-prompts/references/python.md
+++ b/agent-context/skills/metaobjects-prompts/references/python.md
@@ -21,12 +21,16 @@ silently drift.
## Wire the generators
-Select `output-parser` with `entity`, which emits the `@responseRef` value object's
+List `output-parser` with `entity`, which emits the `@responseRef` value object's
Pydantic model it parses into (ADR-0056 — the template tier declares no copy; `--list`
-marks the parser `(requires: entity)` and a run without `entity` warns):
-
-```bash
-metaobjects gen ./metadata --out ./generated --generators entity,output-parser
+marks the parser `(requires: entity)` and a run without `entity` warns), in a target of
+`metaobjects.config.yaml`, the generator list `metaobjects gen` runs:
+
+```yaml
+targets:
+ app:
+ outDir: generated
+ generators: [entity, output-parser]
```
`metaobjects gen --list` shows every generator name; the programmatic
@@ -72,8 +76,8 @@ For every responding `template.prompt`, the `output-prompt` generator (run via
`render_output_format()` — the "produce your answer like this" fragment for the
model:
-```bash
-metaobjects gen ./metadata --out ./generated --generators output-prompt
+```yaml
+ generators: [entity, output-parser, output-prompt] # the same target
```
`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`)
diff --git a/agent-context/skills/metaobjects-runtime-ui/references/csharp.md b/agent-context/skills/metaobjects-runtime-ui/references/csharp.md
index 029fb235e..671794f9b 100644
--- a/agent-context/skills/metaobjects-runtime-ui/references/csharp.md
+++ b/agent-context/skills/metaobjects-runtime-ui/references/csharp.md
@@ -103,8 +103,8 @@ the Java, Kotlin, and Python backends byte-for-byte.
Inside LINQ keep the property (`db.Authors.Where(a => a.Name == …)`): it is type-checked
against the model, and a string constant there trades a compile error for a runtime one.
Where LINQ does not reach — raw SQL, a migration script, a log line — take the physical
-name from the generated `Names.g.cs`. `names` is opt-in — name it in
-`--generators` — and once selected the generated entity and `AppDbContext` already read
+name from the generated `Names.g.cs`. `names` is opt-in — list it in
+`codegen/Generators.cs` — and once listed the generated entity and `AppDbContext` already read
it, so it cannot disagree with the
mapping: `AuthorNames.SourcePrimaryTable` is the table (the member is named for the
source's `@kind`, so a view reads `SourceReplicaView` / `SourcePrimaryView` and a stored
diff --git a/agent-context/skills/metaobjects-runtime-ui/references/python.md b/agent-context/skills/metaobjects-runtime-ui/references/python.md
index f5a2bfd37..7fea94304 100644
--- a/agent-context/skills/metaobjects-runtime-ui/references/python.md
+++ b/agent-context/skills/metaobjects-runtime-ui/references/python.md
@@ -107,7 +107,7 @@ resolve the column themselves, so that path never needs a physical name. The mom
repository `Protocol` is backed by your own SQLAlchemy Core / asyncpg / psycopg code, it
does — and nothing Python generates carries one: the Pydantic models, create/patch
shapes, router and allowlist all key by field. Take it from the generated
-`_names.py` (`names` is opt-in — name it in `--generators`):
+`_names.py` (`names` is opt-in — list it in a target's `generators`):
```python
from generated.author_names import (
diff --git a/docs/features/cli.md b/docs/features/cli.md
index 6f0aa7ed1..ea4eb083f 100644
--- a/docs/features/cli.md
+++ b/docs/features/cli.md
@@ -36,10 +36,10 @@ 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 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`). `gen` also accepts `--template-spec ` (+ `--template-root `, default `prompts/` when it exists, else `templates/`) — the declarative Mustache template-codegen surface (the cross-port JSON contract shared with Python), **auto-discovered at `/template-spec.json`** when the flag is absent. Prefer the conventional path: `verify --codegen` takes no `--template-spec`, so discovery is how the drift gate sees your template generators at all; 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`). **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 = "", TemplateRoot = "" }`; `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** — `` already loads a consumer class from the project classpath, so the declarative surface is `com.metaobjects.generator.template.TemplateScopeGenerator` wired as an ordinary `` with `` / `` / `` / `` / `` 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): `codegen` (default, back-compat — 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). `gen` also accepts `--template-spec ` (+ `--templates `, default `prompts/` when it exists, else `templates/`) — the declarative Mustache template-codegen surface (the cross-port JSON contract shared with C#), **auto-discovered at `/template-spec.json`** when the flag is absent (and REFUSED in declarative-config mode, where a spec has no target to write into). Prefer the conventional path: `verify --codegen` takes no `--template-spec`, so discovery is how the drift gate sees your template generators at all; see [declarative template scopes](codegen-concepts.md#declarative-template-scopes) |
+| 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 |
## `verify` is one verb with explicit subverbs (ADR-0021 D2)
@@ -331,14 +331,13 @@ stale page in place, green. A hand-written file in the docs directory is left al
## Declarative config (`metaobjects.config.yaml`) — Python codegen
-Alongside its flag-only mode (`metaobjects gen --out `), the
-Python `metaobjects` CLI supports a declarative project config,
-`metaobjects.config.yaml` (#267). Its **`targets` registry uses the TS
-`metaobjects.config.ts` vocabulary** (named targets, each with its own `outDir`) — a
-polyglot adopter learns one targets-registry shape regardless of port. The keys are not
-identical beyond that: a Python target carries its own `generators` and `entities`, and
-`requirementTests` is a block that exists only here, where TypeScript passes that
-generator's options to `requirementTests({ … })` in code. A JSON Schema ships at
+The Python `metaobjects` CLI reads its generator list from a declarative project config,
+`metaobjects.config.yaml` (#267; since 1.1.0 the only list, ADR-0034 Amendment 9). Its
+**`targets` registry uses the TS `metaobjects.config.ts` vocabulary** (named targets, each
+with its own `outDir`) — a polyglot adopter learns one targets-registry shape regardless of
+port. The keys are not identical beyond that: a Python target carries its own `generators`,
+`entities` and `templateSpec`, and `requirementTests` and `apiDocs` are blocks that exist only
+here, where TypeScript passes those generators' options in code. A JSON Schema ships at
[`server/python/src/metaobjects/codegen/metaobjects-config.schema.json`](../../server/python/src/metaobjects/codegen/metaobjects-config.schema.json)
for editor autocomplete and non-Python validation.
@@ -353,46 +352,54 @@ requirementTests: # optional; options of the `requirement-tests`
filter: codegen.requirement_hooks:include # "module:symbol", resolved config-relative
renderer: codegen.requirement_hooks:render # "module:symbol", resolved config-relative
warnUncovered: true # the default
-docs: # optional; options of `metaobjects docs`
- generators: # owned docs generators as "module:symbol", run in place of the packaged one
- - codegen.generators.api_docs:api_docs_generator
+apiDocs: # optional; options of the `api-docs` generator, every key optional
+ apiSubDir: api/python # the default
+ projectName: shop # default "project"
+ modelBaseUrl: https://docs.example.com/model
targets:
api:
outDir: src/generated/api
- generators: [entity, routes] # REQUIRED; stable names from `metaobjects gen --list`.
- # There is no default suite — see ADR-0034 Amendment 2.
+ generators: [entity, filter-allowlist, routes] # stable names from `metaobjects gen --list`,
+ # or an owned copy as "module:symbol". Nothing runs that is not listed.
admin:
outDir: src/generated/admin
+ generators: [entity]
entities: [Author, Book] # optional allowlist; omit = every entity
+ docs:
+ outDir: docs/generated
+ generators: [api-docs] # the api reference is an entry like any other
+ templateSpec: template-spec.json # optional; a declarative template-spec, relative to this file
```
-- **`metaobjects gen`** with no positional `` runs config mode:
- it loads the config and metadata once and runs every target into its own
- `outDir`, with a cross-target guard against two targets writing the same
- output path. `--target ` scopes the run to a single target.
-- **`metaobjects verify --codegen`** — including bare `verify`, since
- `--codegen` is the Python default (see above) — runs the matching config
- mode with no positional ``: it regenerates the whole selection
- into a temp tree (the exact `gen` pipeline, including the cross-target
- duplicate-output-path guard) and diffs each **unique `outDir`** against the
- union of the co-resident targets' regen, aggregating the exit code (non-zero
- if *any* outDir has drifted). Targets sharing an `outDir` are verified
- together, so a shared `outDir` is never a false-positive `extra`. `--target`
- widens to the `outDir`-sharing closure (an `outDir` is verified as a unit).
- Strict-attr loading (ADR-0023) still applies unless `--lax` is passed.
-- **`--config `** picks the config file explicitly on either command;
- with no positional metadata dir and no `--config`, both commands default to
- looking for `./metaobjects.config.yaml` in the current directory.
+- **`metaobjects gen`** loads the config and metadata once and runs every target into its
+ own `outDir`, with a cross-target guard against two targets writing the same output path.
+ `--target ` scopes the run to a single target; `--generators ` narrows it to
+ the declared entries named (an owned entry matches its registry name too) and refuses a
+ name no target declares. A target whose `generators` is empty writes nothing and says so.
+- **`metaobjects verify --codegen`** — including bare `verify`, since `--codegen` is the
+ Python default (see above) — regenerates the same list into a temp tree (the exact `gen`
+ pipeline, including the cross-target duplicate-output-path guard) and diffs each **unique
+ `outDir`** against the union of the co-resident targets' regen, aggregating the exit code
+ (non-zero if *any* outDir has drifted). Targets sharing an `outDir` are verified together,
+ so a shared `outDir` is never a false-positive `extra`. `--target` widens to the
+ `outDir`-sharing closure (an `outDir` is verified as a unit). Strict-attr loading
+ (ADR-0023) still applies unless `--lax` is passed.
+- **`--config `** picks the config file explicitly on either command; without it both
+ look for `./metaobjects.config.yaml` in the current directory. **No config is an error**:
+ `gen` and `verify --codegen` print `no metaobjects.config.yaml`, exit 2 and write nothing.
+- **Run options still apply to the list:** `--column-naming`, `--entities` (narrows each
+ target's entities), `--provider` (added to `providers`), `--templates` (`--prompts` on
+ `verify`), `--baseline`.
- **Providers resolve config-relative.** A `providers:` entry is imported
with the config file's own directory prepended to `sys.path`, so a
consumer provider module living beside the config resolves with no
- `PYTHONPATH=` needed (unlike the flag-only `--provider module:symbol`
- path, which relies on the ambient environment).
-- **Back-compat is load-bearing.** Passing an explicit positional
- `` (with `--out` on `gen`, or `--out` on `verify --codegen`)
- keeps the original flag-only path byte-identical — the config file is never
- consulted. Config mode activates only when no positional ``
- is given.
+ `PYTHONPATH=` needed (unlike `--provider module:symbol`, which relies on the ambient
+ environment).
+- **Not read since 1.1.0.** The positional `` and `--out` of `gen` (and `--out`
+ of `verify --codegen`) are refused with a line naming `metadata` and the targets' `outDir`;
+ `--template-spec` is refused in favour of a target's `templateSpec`; a top-level `docs`
+ block fails `gen` with `found 1.0 docs configuration`. See
+ [the migration section](migrations/docs-are-generators.md#python).
## `libraries` — opting into a MetaObjects-shipped library package
diff --git a/docs/features/codegen-concepts.md b/docs/features/codegen-concepts.md
index 83a433696..5a4c318a6 100644
--- a/docs/features/codegen-concepts.md
+++ b/docs/features/codegen-concepts.md
@@ -271,13 +271,13 @@ The **CLI-port surface** (Python and C#) exposes the same declarative
walk through a JSON **template-spec** instead of code/XML wiring — the cross-port
contract shape (`{"generators": [{ name, template, scope, outputPattern, format? }]}`,
JSON-schema'd beside the TS port at
-`codegen-ts/src/template-codegen/template-spec.schema.json`). Pass it on `gen`:
-`metaobjects gen --out --template-spec spec.json --templates `
-(Python), or `dotnet meta gen --out --template-spec spec.json --template-root ` (C#).
-The named generators are appended to the SELECTION (there is no default suite —
-ADR-0034 Amendment 2), resolving template refs
-under the templates root (`--templates` in Python, `--template-root` in C#; default
-`templates`). `scope` must be one of
+`codegen-ts/src/template-codegen/template-spec.schema.json`). In Python it is declared on
+the target its output belongs in (`templateSpec: spec.json` in `metaobjects.config.yaml`,
+run with `metaobjects gen --templates `); in C# it is an entry in `codegen/Generators.cs`
+(`new MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator { Spec = "spec.json",
+TemplateRoot = "templates" }`). Either way the spec is part of the declared generator list
+(ADR-0034 Amendment 9), resolving template refs under the templates root (`--templates` in
+Python, `TemplateRoot` in C#; default `prompts/` when it exists, else `templates`). `scope` must be one of
`perEntity`/`perPackage`/`perModel` and `format` (when present) one of the
registered escaper formats; a `target` field is **rejected** (the CLI ports have no
output-target concept). The same neutral data dict, output-pattern grammar, and
@@ -287,11 +287,10 @@ overwrite a file lacking the `@generated` marker — a regenerable template must
that header in its own body. The JVM's `TemplateScopeGenerator` writes directly and
carries no such obligation.)
-**The spec is auto-discovered.** With no `--template-spec`, both CLI ports read
-`/template-spec.json` (projectRoot = the metadata dir's parent, the anchor
-`.metaobjects/` already uses); the flag overrides it. This is what lets `verify --codegen`
-regenerate WITH your template generators — it accepts no `--template-spec` of its own, so a
-flag-only setup would leave the gate resolving a different generator list from `gen` and
-convicting gen's own output. The ports with a code seam (TypeScript, JVM) declare template
-generators in the build config the gate already re-runs, reaching the same guarantee a
-different way.
+**The spec is declared, never discovered.** Through 1.0 both CLI ports also read a
+`--template-spec` flag or a project-root `template-spec.json` on their own. Since 1.1.0 a spec
+runs only where the generator list declares it, so `verify --codegen` regenerates exactly what
+`gen` wrote. A run that still finds the conventional `template-spec.json` with nothing
+declaring a spec, or the C# `--template-spec` flag, fails before writing anything and names the
+migration section. The ports with a code seam (TypeScript, JVM) declare template generators in
+the build config the gate already re-runs, which is the same guarantee.
diff --git a/docs/features/metadata-sources.md b/docs/features/metadata-sources.md
index dd1685bd9..185b94b93 100644
--- a/docs/features/metadata-sources.md
+++ b/docs/features/metadata-sources.md
@@ -70,7 +70,7 @@ than one entry, or a single entry that resolves to a FILE rather than a
directory, is refused with a clear diagnostic naming the limit — rather than
silently loading a subset of the declared sources, or (for the single-file case)
failing deep inside the loader with an opaque, uncoded error. Declare exactly one
-directory `path` to use `dotnet meta gen`/`verify`/`docs`, or route around the
+directory `path` to use `dotnet meta gen`/`verify`, or route around the
limit with an explicit `` CLI argument.
`MetaDataLoader.Load(IReadOnlyList)` (`MetaDataLoader.cs`) is the
documented follow-up that would lift both restrictions. The other three CLI
diff --git a/docs/features/migrations/docs-are-generators.md b/docs/features/migrations/docs-are-generators.md
index edab2a89d..6116da114 100644
--- a/docs/features/migrations/docs-are-generators.md
+++ b/docs/features/migrations/docs-are-generators.md
@@ -224,6 +224,276 @@ export default defineConfig({
listed. `requirementsFile()` writes nothing for a model that declares no `requirement.*`
node, so drop it if yours declares none. Then run `meta gen`.
+## C#
+
+**What you see.** `dotnet meta docs` prints this line and exits 2:
+
+```
+dotnet meta docs was removed in 1.1.0. `dotnet meta gen` writes documentation now: declare each docs generator in codegen/Generators.cs. See docs/features/migrations/docs-are-generators.md, section "C#".
+```
+
+`dotnet meta gen` (and `dotnet meta verify --codegen`) in a project with no
+`codegen/Generators.cs` prints this and exits 2, writing nothing:
+
+```
+dotnet meta gen: no codegen/Generators.cs. The generator list lives there in 1.1.0, documentation included; `dotnet meta init` writes it. See docs/features/migrations/docs-are-generators.md, section "C#".
+```
+
+`dotnet meta gen` given `--project` or `--model-base-url` fails before writing anything,
+with a line that starts `dotnet meta gen: found 1.0 docs configuration (` and names each
+flag it was given.
+
+**Before (1.0).** The code generators chosen on the command line, and the api reference
+written by a second command:
+
+```
+dotnet meta gen ./metaobjects --out ./Generated --generators entity,db-context
+dotnet meta docs ./metaobjects --out ./docs/generated --project shop --model-base-url https://docs.example.com/model
+```
+
+A project that had ejected a generator also had `codegen/Program.cs`, with the list inline:
+
+```csharp
+using Codegen.Generators;
+using MetaObjects.Codegen;
+
+IReadOnlyList generators =
+[
+ new EntityGenerator(),
+];
+
+return CodegenCli.Run(args, generators);
+```
+
+and ran `dotnet meta gen ./metaobjects --out ./Generated --generators entity,db-context`, where
+`--generators` added the packaged `db-context` to its own `entity`.
+
+**After (1.1.0).** Run `dotnet meta init` once. It writes `codegen/Generators.cs`,
+`codegen/Program.cs` and `codegen/Codegen.csproj`, each only when missing. Put every
+generator in `codegen/Generators.cs`, the api reference included, with its options as init
+properties. These entries write the same files to the same paths as the 1.0 commands above:
+
+```csharp
+// codegen/Generators.cs
+using MetaObjects.Codegen;
+
+public static class Generators
+{
+ public static IReadOnlyList All { get; } =
+ [
+ new MetaObjects.Codegen.Generators.EntityGenerator(),
+ new MetaObjects.Codegen.Generators.DbContextGenerator(),
+ new MetaObjects.Codegen.Generators.ApiDocsGenerator
+ {
+ OutDir = "docs/generated",
+ ProjectName = "shop",
+ ModelBaseUrl = "https://docs.example.com/model",
+ },
+ ];
+}
+```
+
+```csharp
+// codegen/Program.cs
+using MetaObjects.Codegen;
+return CodegenCli.Run(args, Generators.All);
+```
+
+```
+dotnet meta gen ./metaobjects --out ./Generated
+```
+
+A project that already has `codegen/Program.cs` with an inline list: move each entry into
+`Generators.All`, add an entry for each name its `--generators` used to add (an owned copy is
+`new Codegen.Generators.()`, a packaged one `new MetaObjects.Codegen.Generators.()`),
+and replace `Program.cs` with the two lines above. `dotnet meta init` writes
+`Generators.cs` beside an existing `Program.cs` and leaves `Program.cs` alone. Until
+`Program.cs` runs `Generators.All`, `dotnet meta gen` prints this and exits 2, writing nothing
+(`dotnet meta verify --codegen` in place of `dotnet meta gen` for the gate):
+
+```
+dotnet meta gen: codegen/Program.cs does not run Generators.All, so it would ignore the generator list in codegen/Generators.cs. Replace it with the two-line entry `using MetaObjects.Codegen;` and `return CodegenCli.Run(args, Generators.All);`. See docs/features/migrations/docs-are-generators.md, section "C#".
+```
+
+`init` and `eject` say the same, and warn when `codegen/Codegen.csproj` still pins an older
+`MetaObjects.Codegen`: set that version to the tool's, or the 1.0 codegen runs.
+
+**A template spec.** Through 1.0, `dotnet meta gen` and `verify --codegen` also read a
+declarative template-spec on their own: `--template-spec ` on `gen`, or
+`template-spec.json` at the project root (the metadata directory's parent) with no flag.
+Neither is read in 1.1.0. A run that finds either, with no spec entry in the list, fails
+before writing anything:
+
+```
+dotnet meta gen: found 1.0 template-spec configuration (template-spec.json). Nothing reads it any more, so this run would not render its templates. Declare it in codegen/Generators.cs as `new MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator { Spec = "template-spec.json" }`. See docs/features/migrations/docs-are-generators.md, section "C#".
+```
+
+(`--template-spec` in place of `template-spec.json` when the flag was given;
+`dotnet meta verify --codegen` in place of `dotnet meta gen` for the gate.)
+
+Before (1.0):
+
+```
+dotnet meta gen ./metaobjects --out ./Generated --generators entity --template-root ./templates
+# with ./template-spec.json beside ./metaobjects, or: --template-spec ./template-spec.json
+```
+
+After (1.1.0), the spec as one more entry; it writes the same files to the same paths:
+
+```csharp
+// codegen/Generators.cs
+public static IReadOnlyList All { get; } =
+[
+ new MetaObjects.Codegen.Generators.EntityGenerator(),
+ new MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator
+ {
+ Spec = "template-spec.json",
+ TemplateRoot = "templates",
+ },
+];
+```
+
+```
+dotnet meta gen ./metaobjects --out ./Generated
+```
+
+`Spec` and `TemplateRoot` are taken from the directory gen runs in, as `--out` is. Omit
+`TemplateRoot` to take the run's `--template-root`, else the 1.0 default: `prompts/` when it
+exists, else `templates/`.
+
+**Each 1.0 flag, and where it goes.**
+
+| 1.0 | 1.1.0 |
+|---|---|
+| `dotnet meta gen --generators a,b,c` (the selection) | one entry per generator in `codegen/Generators.cs`; `--generators` now only narrows a run to declared entries, and naming one the list does not declare is refused |
+| `dotnet meta docs --out ` | `OutDir` on the `ApiDocsGenerator` entry, taken from the directory gen runs in, as `--out` is |
+| `dotnet meta docs --project ` (default: the metadata directory's name) | `ProjectName`, default `project`; set the metadata directory's name to keep 1.0's pages byte for byte |
+| `dotnet meta docs --model-base-url ` | `ModelBaseUrl` |
+| the `api/csharp` subdirectory under `--out` | `ApiSubDir`, same default |
+| `dotnet meta gen --template-spec `, or a project-root `template-spec.json` | a `TemplateSpecGenerator { Spec = "" }` entry |
+| `dotnet meta gen --template-root ` | `TemplateRoot` on the spec entry, and the template root `render-helper` is constructed with in the list |
+| `dotnet meta docs --namespace ` | the gen run's `--namespace`, as before |
+| whether you ran `dotnet meta docs` | whether `ApiDocsGenerator` is listed |
+| an owned api-docs copy, run by `dotnet meta docs` (1.1.0 release candidates only) | `new Codegen.Generators.ApiDocsGenerator { ... }` in the list, in place of the packaged one |
+
+**Give the api-docs entry an `OutDir` that holds generated files only.**
+`dotnet meta verify --codegen` now checks the pages, as it checks code: it regenerates each
+entry's `OutDir` and compares the whole tree, so a hand-written file there reads as stale.
+
+**Scripts and CI.** Replace `dotnet meta docs ...` with `dotnet meta gen`. To write only the
+documentation, name the docs entry:
+
+```
+dotnet meta gen ./metaobjects --out ./Generated --generators api-docs
+```
+
+Drop `--generators` from a `dotnet meta gen` or `verify --codegen` line that used it to choose
+the generators: the list chooses them now.
+
+## Python
+
+**What you see.** `metaobjects docs` prints this line and exits 2:
+
+```
+metaobjects docs was removed in 1.1.0. `metaobjects gen` writes documentation now: declare each docs generator in a target's `generators` in metaobjects.config.yaml. See docs/features/migrations/docs-are-generators.md, section "Python".
+```
+
+`metaobjects gen` (and `metaobjects verify --codegen`) with no `metaobjects.config.yaml`
+prints this and exits 2, writing nothing. That includes the 1.0 form
+`metaobjects gen --out --generators `:
+
+```
+metaobjects gen: no metaobjects.config.yaml. The generator list lives there in 1.1.0, documentation included. See docs/features/migrations/docs-are-generators.md, section "Python".
+```
+
+A config that still has a top-level `docs` block fails `gen` before it writes anything:
+
+```
+metaobjects gen: found 1.0 docs configuration (the docs block in metaobjects.config.yaml). Nothing reads it any more, so this run would write no documentation. Declare each docs generator in a target's `generators` in metaobjects.config.yaml. See docs/features/migrations/docs-are-generators.md, section "Python".
+```
+
+A target with no `generators` key fails `gen` (exit 2) before anything is written; list its
+generators, or write `generators: []` for a target that writes nothing:
+
+```
+metaobjects gen: target(s) 'api' declare no `generators` key. In 1.1.0 a target runs only the generators it lists: list them, or write `generators: []` for a target that writes nothing. See docs/features/migrations/docs-are-generators.md, section "Python".
+```
+
+**Before (1.0).** Code from a config file, or from flags with no config, and the api
+reference from a second command:
+
+```yaml
+# metaobjects.config.yaml
+targets:
+ api:
+ outDir: src/generated
+ generators: [entity, filter-allowlist, routes]
+```
+
+```
+metaobjects gen
+metaobjects docs --out docs/generated --project shop --model-base-url https://docs.example.com/model
+```
+
+or, with no config:
+
+```
+metaobjects gen ./metaobjects --out src/generated --generators entity,filter-allowlist,routes
+```
+
+**After (1.1.0).** One config file lists every generator, the api reference included, each
+in the target whose `outDir` it writes into. The api reference's options are the top-level
+`apiDocs` block. This file writes the same files to the same paths as the 1.0 commands above:
+
+```yaml
+# metaobjects.config.yaml
+metadata: metaobjects
+targets:
+ api:
+ outDir: src/generated
+ generators: [entity, filter-allowlist, routes]
+ docs:
+ outDir: docs/generated
+ generators: [api-docs] # an owned copy: "codegen.generators.api_docs:api_docs_generator"
+apiDocs:
+ projectName: shop
+ modelBaseUrl: https://docs.example.com/model
+```
+
+```
+metaobjects gen
+```
+
+**Each 1.0 flag or key, and where it goes.**
+
+| 1.0 | 1.1.0 |
+|---|---|
+| `metaobjects docs --out ` | `outDir` of the target that lists `api-docs` |
+| `metaobjects docs --api-subdir ` (default `api/python`) | `apiDocs.apiSubDir`, same default |
+| `metaobjects docs --project ` (default: the name of the directory `docs` ran in, or of `` when given) | `apiDocs.projectName`, default `project`; set that name to keep 1.0's pages byte for byte |
+| `metaobjects docs --model-base-url ` | `apiDocs.modelBaseUrl` |
+| `metaobjects docs --provider ` | top-level `providers` (`--provider` on `gen` still adds one) |
+| whether you ran `metaobjects docs` | whether a target lists `api-docs` |
+| `docs.generators` (an owned api-docs copy; 1.1.0 release candidates only) | the owned `module:symbol` in a target's `generators`, in place of `api-docs` |
+| `gen ` | top-level `metadata` |
+| `gen --out ` | the target's `outDir` |
+| `gen --generators a,b,c` (the selection) | the target's `generators`; `--generators` now only narrows a run to declared entries, and naming one no target declares is refused |
+| `gen --template-spec `, or the `template-spec.json` beside `` that 1.0 found on its own | `templateSpec: ` on the target its output belongs in; a leftover `template-spec.json` that no target declares fails the run with a line that starts `metaobjects gen: found 1.0 template-spec configuration (` |
+| `gen --entities a,b` | the target's `entities` (`--entities` still narrows a run) |
+
+**Give the docs target an `outDir` that holds generated files only.**
+`metaobjects verify --codegen` now checks the pages, as it checks code: it regenerates each
+target's `outDir` and compares the files it would write.
+
+**Scripts and CI.** Replace `metaobjects docs ...` with `metaobjects gen`, and
+`metaobjects gen --out ...` with `metaobjects gen` once the config
+declares the list. To write only the documentation, name the docs entry (or its target):
+
+```
+metaobjects gen --generators api-docs
+metaobjects gen --target docs
+```
+
## Java and Kotlin
**What you see.** A pom that binds the `docs` goal fails with:
diff --git a/docs/features/own-your-codegen.md b/docs/features/own-your-codegen.md
index c9f866ef8..1e31a7293 100644
--- a/docs/features/own-your-codegen.md
+++ b/docs/features/own-your-codegen.md
@@ -549,7 +549,7 @@ Restore the file to generated output and the collision is raised again on the ne
Two write paths deliberately **bypass** the guard, and the reason is the same in both:
the guard is sound only where our own emitter always writes the marker. `DocsMojo`'s API
pages render from `templates/api/*.mustache`, and `TemplateScopeGenerator` emits whatever
-a user's `--template-spec` renders (SQL, markdown, CSV) — neither content is ours to
+a user's template-spec renders (SQL, markdown, CSV) — neither content is ours to
require a marker of, and guarding them would make the first run write and every run after
refuse, freezing the artifact while the build stayed green.
@@ -638,8 +638,8 @@ generator code. The 20-line programmatic shape for each port is in
|---|---|---|---|
| **TypeScript** | `meta init` → `meta gen --list --probe` → `meta eject ` → `meta gen` (Bun/Node CLI) | **Yes.** `meta generator new ` writes a working generator of your own into `codegen/generators/` and wires it — the first move for an output nothing ships. To use a stock generator as shipped, import its factory from its package (`meta gen --list` prints the line) and wire it. To change one: `meta init` scaffolds the LAYOUT and an empty selection (ADR-0034 Amendment 2); `meta eject ...` copies each generator you choose into `codegen/generators/*.ts` and prints the import to put in `metaobjects.config.ts`. Edit them freely. Every stock generator can be ejected except two: the `template` primitive, which has no emit logic of its own to own (the same ruling in every port), and `docs-site`, whose builder stays in the package while `meta eject docs-site` copies the site's templates and assets into `codegen/docs-site/`. `meta gen --list` marks both **package-only** and `meta eject` names them as such. An ejected generator comes with the code that writes its output: the template modules it calls are copied into `codegen/templates/` ([what the copy holds](#what-the-copy-holds)), so the prompt tier, `callable`, `trace-helper`, `cube-model`, `shared-model`, the UI tier and the docs tier are as ownable as `entity` and `routes`. What stays in the package is the engine: the generator base, the model walks (including the requirement walk, each test's identity and the claim digest), the naming and type-mapping tables, and the render and extract engines of `@metaobjectsdev/render`. `requirement-tests` is one file holding the generator and its default stub renderer. The documentation generators (`docs`, `api-docs`, `requirements`, `agent-docs`, `metamodel-docs` and `docs-site`) are entries in `generators` that write to the `docs` target, and `meta gen` runs them ([details](#the-docs-generators-and-shared-model-typescript)). | **Yes** — `templateGenerator({ template, scope, outputPattern })` in the config's `generators: [...]`. No CLI flag: the config already takes generator values. |
| **Java / Kotlin** | `mvn metaobjects:generate` / `mvn metaobjects:verify` (`metaobjects-maven-plugin`) | **Yes.** Extend `FileEmittingGenerator` and read the model through `ModelWalk` (both in `metaobjects-codegen-base`) for one of your own. Every generator — built-in or your own — is named in `` and loaded from the project classpath: one seam, not two. There is no default suite, so `` is the complete list. Kotlin runs through the same goal. | **Yes** — `TemplateScopeGenerator` wired as an ordinary ``. No CLI flag: `` is already the seam. |
-| **C#** | `dotnet meta gen` / `dotnet meta verify` (.NET tool) | **Yes.** Implement `IGenerator` in the owned console project `codegen/` and list it in `codegen/Program.cs`; `dotnet meta gen` / `verify --codegen` hand off to that project whenever `codegen/Codegen.csproj` exists. `dotnet meta eject ` scaffolds the project, or write its two files by hand. An owned generator the `--generators` selection does not name still runs. | **Yes** — `dotnet meta gen --template-spec --template-root `. |
-| **Python** | `metaobjects gen` / `metaobjects verify` (console-script) | **Yes.** Name your generator as `module:symbol` in `--generators` or a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one. Read the model through `metaobjects.codegen.model_walk`. (`--provider module:symbol` registers **metamodel vocabulary**, not a generator.) | **Yes** — `metaobjects gen --template-spec --templates `. |
+| **C#** | `dotnet meta init` → `dotnet meta gen` / `dotnet meta verify` (.NET tool) | **Yes.** Implement `IGenerator` in the console project `codegen/` and list it in `codegen/Generators.cs`, the generator list `dotnet meta gen` / `verify --codegen` run through `dotnet run --project codegen`. `dotnet meta init` (or a first `dotnet meta eject `) writes the project. `--generators` only narrows the list. | **Yes** — a `TemplateSpecGenerator { Spec = "", TemplateRoot = "" }` entry in `codegen/Generators.cs`. |
+| **Python** | `metaobjects gen` / `metaobjects verify` (console-script) | **Yes.** Name your generator as `module:symbol` in a target's `generators` in `metaobjects.config.yaml`, the generator list `gen` runs; the symbol is an instance or a function returning one. Read the model through `metaobjects.codegen.model_walk`. (`--provider module:symbol` registers **metamodel vocabulary**, not a generator.) | **Yes** — `templateSpec: ` on a target, with `metaobjects gen --templates `. |
So "I need a shape the built-ins do not emit" has an answer on **every** port, in code or
in a template. A template is a real path, not a consolation prize: it renders against the
@@ -680,19 +680,13 @@ output *shape* is what you are iterating on, or when you want the same output ac
languages; reach for a generator when the logic is gnarly or the run is hot. Full
tradeoff table: [`codegen-concepts.md` §3](codegen-concepts.md).
-**The spec file is discovered, not just flagged.** With no `--template-spec`, both ports
-look for **`/template-spec.json`** — projectRoot being the metadata dir's
-parent, the same anchor `.metaobjects/` already uses. The flag overrides it.
-
-That matters for more than typing: `verify --codegen` takes **no** `--template-spec` flag,
-so discovery is how the drift gate learns about your template generators. Before it existed,
-`gen` honoured the flag and `verify` never looked — so verify regenerated without them and
-convicted their committed output, with a remedy that loops. Keep the spec at the
-conventional path and both verbs resolve the same one.
-
-Passing `--template-spec` explicitly still works and still wins; just make sure any CI that
-runs `verify --codegen` can find the spec, which the conventional path guarantees and a
-flag-only setup does not.
+**The spec is declared in the generator list.** In Python it is a target's `templateSpec`
+in `metaobjects.config.yaml`; in C# it is a `TemplateSpecGenerator { Spec = "...",
+TemplateRoot = "..." }` entry in `codegen/Generators.cs`. Because `verify --codegen`
+regenerates the same list, the drift gate always knows about your template generators.
+Through 1.0 both ports also discovered a project-root `template-spec.json` (and C# read a
+`--template-spec` flag); neither is read now, and a run that finds the conventional file with
+nothing declaring a spec fails and names the migration section.
*(Full command/flag matrix and rationale: [`docs/features/cli.md`](cli.md), locked per
ADR-0015. Schema migrations are TypeScript-owned across all ports.)*
@@ -751,7 +745,7 @@ provider class** in each language: a JSON file could express the declarative sur
| Port | How the CLI / build tool loads your provider |
|---|---|
| **TypeScript** | `metaobjects.config.ts` → `providers: [myProvider]`. Threaded into `meta gen` (the docs site included), `meta verify`, **and** the offline `meta migrate` paths (baseline + generate). |
-| **Python** | `metaobjects gen \| verify \| docs --provider module:symbol` (repeatable). The symbol resolves to a `Provider` (or a list, or a zero-arg factory returning one) and is composed **on top of** the core providers — parity with TS `config.providers`. E.g. `metaobjects gen ./metaobjects --out ./gen --provider myapp.providers:view_provider`. A declarative `metaobjects.config.yaml` `providers:` list is also supported (resolved config-relative, no `PYTHONPATH=` — mirroring TS's config-file providers); see [`cli.md`](cli.md). |
+| **Python** | `metaobjects gen \| verify --provider module:symbol` (repeatable). The symbol resolves to a `Provider` (or a list, or a zero-arg factory returning one) and is composed **on top of** the core providers — parity with TS `config.providers`. E.g. `metaobjects gen --provider myapp.providers:view_provider`. A declarative `metaobjects.config.yaml` `providers:` list is also supported (resolved config-relative, no `PYTHONPATH=` — mirroring TS's config-file providers); see [`cli.md`](cli.md). |
| **Java / Kotlin** | Put your compiled `MetaDataTypeProvider` on the **project classpath** with a `META-INF/services/com.metaobjects.registry.MetaDataTypeProvider` entry. `metaobjects-maven-plugin` builds the loader with the project classloader, so **Java ServiceLoader auto-discovers it** — no plugin config needed. |
| **C#** | The loader accepts providers like every port; a first-class `dotnet meta` consumer-provider hook is tracked for a future release ([#158](https://github.com/metaobjectsdev/metaobjects/issues/158)). Today, extend the metamodel from an app that constructs the loader directly. |
@@ -775,9 +769,9 @@ between needs no change, and can drop the ones it never modified:
metaobjects eject entity routes # copies into codegen/generators/entity.py, routes.py
```
-Each copy is the packaged generator module, verbatim. Wire it in `metaobjects.config.yaml`
-(or `--generators`) as `module:symbol`, the same form `providers` accepts, in place of the
-packaged name:
+Each copy is the packaged generator module, verbatim. Wire it in a target's `generators` in
+`metaobjects.config.yaml`, the generator list `metaobjects gen` runs, as `module:symbol`, the
+same form `providers` accepts, in place of the packaged name:
```yaml
targets:
@@ -949,27 +943,29 @@ yours, for a copy you have not changed. Eject does not decide which applies (the
### `api-docs`: the api reference as an owned generator
-`metaobjects docs` writes the Python api reference (`api/python`). `metaobjects eject
+`api-docs` writes the Python api reference (`api/python`) into the `outDir` of the target
+that lists it, with its options in the top-level `apiDocs` block. `metaobjects eject
api-docs` copies the generator that writes it, with the page model (`api_docs_model.py`), the
builder that decides what to document (`api_docs_builder.py`), the renderer
(`api_docs_renderer.py`) and the page-path math (`api_docs_paths.py`). Change which nodes
get a page, how a page reads, or where it lands, by editing them.
-A docs generator is not a code generator, so it is wired in its own block, never in a
-target's `generators`:
+The copy is wired like any other, in place of `api-docs` in the target that writes the
+docs; the `apiDocs` options reach it through the build context, as they reach the packaged
+one:
```yaml
-docs:
- generators: [codegen.generators.api_docs:api_docs_generator]
+targets:
+ docs:
+ outDir: docs/generated
+ generators: [codegen.generators.api_docs:api_docs_generator]
+apiDocs:
+ projectName: shop
```
-`metaobjects docs` runs the copy in place of the packaged generator. Without the block it
-runs the packaged one, and the positional `metaobjects docs ` form reads no
-config, so it always does. An entry that is not a docs generator, or a second one for the
-`api` surface, is refused by name. `gen` and `verify --codegen` skip a docs generator
-they find under a target and say so (`'api-docs' writes the api reference, so gen skipped
-it.`), with what to do about the entry: the packaged `api-docs` name is removed, because
-`metaobjects docs` already runs it, and an owned copy moves to `docs.generators`.
+`metaobjects gen` writes the pages and `metaobjects verify --codegen` checks them, like code
+(ADR-0034 Amendment 9). There is no `metaobjects docs`: see
+[the migration section](migrations/docs-are-generators.md#python).
**It describes the code your generators emit.** The page builder asks `shared_predicates`
which objects get a router, the same module your owned `routes` and `filter-allowlist`
@@ -1340,7 +1336,7 @@ port that ships it.
## C#: `dotnet meta eject`
```bash
-dotnet meta eject entity routes # copies into codegen/generators/, scaffolds codegen/ once
+dotnet meta eject entity routes # copies into codegen/generators/; writes codegen/ files that are missing
dotnet meta gen --list # the catalog, with owned copies marked
```
@@ -1353,23 +1349,25 @@ adding the `using` lines that rename needs; nothing else changes, except in `rou
writes its output through come with it
([below](#ejecting-a-generator-hands-over-the-emitters-it-writes-through)).
-On first use eject also writes `codegen/Codegen.csproj`, a console project referencing
-`MetaObjects.Codegen` at the tool's version, and `codegen/Program.cs`, which lists your
-generators with `new` and calls the public runner. After that, both files are yours: eject
-never touches them again, even with `--force`, and prints what to add instead. That includes
-the package version: eject writes the sources of the tool you run, so when
-`codegen/Codegen.csproj` pins an older `MetaObjects.Codegen` than the tool, eject prints a
-warning with the pinned version, the tool's version and the line to change. The owned
-project does not compile until the pin is the tool's version or later. Once
-`codegen/Codegen.csproj` exists, `dotnet meta gen` and `dotnet meta verify --codegen` hand
-off to `dotnet run --project codegen` with the same arguments, so your owned generators
-run everywhere the tool did.
-
-`Program.cs` lists only what you own; keep naming the whole suite in `--generators`. Each
-owned copy replaces the packaged generator of the same name, every other selected name
-still runs from the package, and an owned generator the selection does not name runs after
-them. So ejecting `names` from a nine-generator selection changes one generator's output,
-not the other eight. The owned project finds metadata exactly as the tool does, including
+The generator list is `codegen/Generators.cs` (ADR-0034 Amendment 9), compiled by the
+console project `codegen/Codegen.csproj` (referencing `MetaObjects.Codegen` at the tool's
+version) and run by `codegen/Program.cs`, two lines that pass `Generators.All` to the public
+runner. `dotnet meta init` writes the three files; eject writes each one that is missing,
+and when it writes `Generators.cs` the list holds the copies it just ejected, as
+`new Codegen.Generators.()`. After that all three are yours: eject never touches them
+again, even with `--force`, and prints the entry to add instead, in place of the packaged
+`new MetaObjects.Codegen.Generators.()` if the list has one. That includes the package
+version: eject writes the sources of the tool you run, so when `codegen/Codegen.csproj` pins
+an older `MetaObjects.Codegen` than the tool, eject prints a warning with the pinned
+version, the tool's version and the line to change. The owned project does not compile until
+the pin is the tool's version or later. `dotnet meta gen` and `dotnet meta verify --codegen`
+always run `dotnet run --project codegen` with the same arguments, so the list runs
+everywhere the tool does.
+
+The list names every generator the project runs, owned or packaged, so ejecting `names` from
+a nine-entry list changes one entry, not the other eight. `--generators` narrows a run to the
+declared entries named, matching an owned copy by its registry name too, and never adds
+one. The owned project finds metadata exactly as the tool does, including
`.metaobjects/config.json`'s `sources` and `libraries`. Eject never overwrites a copy without `--force`, and
`dotnet meta gen --list` marks owned copies `identical` or `DIFFERS: N behind, M of your
own`, and names an emitter that was edited or removed after eject
@@ -1377,8 +1375,8 @@ own`, and names an emitter that was edited or removed after eject
ejectable; it has no emit logic of its own.
`dotnet meta eject requirement-tests` copies `RequirementTestsGenerator.cs`, which holds the
-generator and its default rendering. Its options are public properties you set in
-`codegen/Program.cs` (`new RequirementTestsGenerator { WitnessClass = "…" }`); the hook
+generator and its default rendering. Its options are init properties you set on its entry in
+`codegen/Generators.cs` (`new Codegen.Generators.RequirementTestsGenerator { WitnessClass = "…" }`); the hook
types (`IRequirementTestRenderer`, `IRequirementTestFilter`), the identity function and the
digest stay in the package.
@@ -1432,19 +1430,18 @@ dotnet meta gen --list # no longer reports an emitter missing
### `api-docs`: the api reference as an owned generator
-`dotnet meta docs` writes the C# api reference. `dotnet meta eject api-docs` copies the
-generator that writes it, with the page model (`CSharpApiModel.cs`), the builder that decides
-what to document (`CSharpApiModelBuilder.cs`), the renderer (`CSharpApiDocsRenderer.cs`) and
-the page-path math (`DocsPaths.cs`). Change which nodes get a page, how a page reads, or
-where it lands, by editing them.
+`api-docs` writes the C# api reference, as an entry in `codegen/Generators.cs` whose init
+properties are its options: `OutDir` (where the pages go, taken from the directory gen runs
+in; unset, they go under `--out`), `ApiSubDir` (default `api/csharp`), `ProjectName` and
+`ModelBaseUrl`. `dotnet meta eject api-docs` copies the generator that writes it, with the
+page model (`CSharpApiModel.cs`), the builder that decides what to document
+(`CSharpApiModelBuilder.cs`), the renderer (`CSharpApiDocsRenderer.cs`) and the page-path math
+(`DocsPaths.cs`). Change which nodes get a page, how a page reads, or where it lands, by
+editing them, and list the copy in place of the packaged entry:
-Once `codegen/generators/ApiDocsGenerator.cs` exists, `dotnet meta docs` hands off to
-`dotnet run --project codegen -- docs` with the same arguments, and the reference is written
-by your copy. A project that owns `codegen/` but not `api-docs` keeps the packaged reference:
-the hand-off for `docs` is keyed to the generator, not to the project, because an owned
-project pinned to an older `MetaObjects.Codegen` does not know the `docs` command. The owned
-copy needs `MetaObjects.Codegen` 1.1.0 or later for the same reason; eject warns when
-`codegen/Codegen.csproj` pins less.
+```csharp
+new Codegen.Generators.ApiDocsGenerator { OutDir = "docs/generated", ProjectName = "shop" },
+```
**It describes what the packaged generators emit.** The reference documents a route, an
allowlist, a render helper only if the packaged generator that emits it would, which each
@@ -1455,11 +1452,9 @@ packaged one behaves. To document your own surface, edit the owned builder
`GenContext.Applicability` (`GeneratorApplicability.Packaged`), never through a generator
class, which is why the copy depends on nothing but the engine.
-`Program.cs` lists `new ApiDocsGenerator()` beside the code generators. `dotnet meta gen` and
-`verify --codegen` skip it: it writes pages under the docs directory, not code under `--out`.
-The skip is silent while the generator is only in that list. When a `--generators` selection
-names `api-docs`, `gen` says so (`"api-docs" writes the api reference, so gen skipped it.
-Run it with dotnet meta docs.`), with or without an owned project.
+`dotnet meta gen` writes the pages and `dotnet meta verify --codegen` checks them, comparing
+the whole `OutDir` tree, so give the entry a directory that holds generated files only. There
+is no `dotnet meta docs`: see [the migration section](migrations/docs-are-generators.md#c).
### Ejecting routes hands over the helper runtime too
diff --git a/docs/features/requirements.md b/docs/features/requirements.md
index 03239cc6c..486b77714 100644
--- a/docs/features/requirements.md
+++ b/docs/features/requirements.md
@@ -399,8 +399,8 @@ every one. The structured `--format json|toon` payload is TypeScript-only.
**Metadata that does not load** is not a model with no requirement: `verify` fails on it in
every port. The load errors are printed once. Where the drift gate that ran loaded the model,
-that gate prints them; where none did (`metaobjects verify` with no generators selected,
-`dotnet meta verify --codegen` handed off to an owned `codegen/` project), the requirement
+that gate prints them; where none did (`metaobjects verify --templates` alone,
+`dotnet meta verify --codegen` run by the project's `codegen/` project), the requirement
gate prints them itself. Its load is strict unless `--lax`, as the drift gates' is.
### Known differences between ports
@@ -645,11 +645,10 @@ Every port offers the same seams. Only the spelling differs.
Where they are set: in TypeScript, the options of `requirementTests({ … })` in
`metaobjects.config.ts`. In Python, the `requirementTests` block of
-`metaobjects.config.yaml` (the flag-only `metaobjects gen --out ` mode runs the
-generator with its defaults). In Java and Kotlin, the `` of the `` entry.
-In C#, public properties set where the generator is constructed, in the owned
-`codegen/Program.cs`; the packaged `dotnet meta gen --generators requirement-tests` runs
-with the defaults. The per-port pages have a worked configuration each:
+`metaobjects.config.yaml` (with no block, the generator runs with its defaults). In Java and
+Kotlin, the `` of the `` entry. In C#, init properties on its entry in
+`codegen/Generators.cs`, the generator list `dotnet meta gen` runs; its `OutDir` puts the
+xUnit tests in the test project in that same run. The per-port pages have a worked configuration each:
[TypeScript](../ports/typescript.md#requirement-tests),
[Python](../ports/python.md#requirement-tests),
[Java](../ports/java.md#requirement-tests--junitrequirementtestsgenerator),
diff --git a/docs/ports/csharp.md b/docs/ports/csharp.md
index ed99726e1..b2055aee3 100644
--- a/docs/ports/csharp.md
+++ b/docs/ports/csharp.md
@@ -23,6 +23,7 @@ Install the CLI as a .NET tool:
```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
```
The C# CLI is invoked as `dotnet meta` (command `dotnet-meta`), or run directly
@@ -81,28 +82,68 @@ for a worked example.
## Generate
-**Own a generator.** `dotnet meta eject ...` copies a reference generator into `codegen/generators/` and scaffolds an owned `codegen/` console project that `dotnet meta gen` then runs. Every generator `dotnet meta gen --list` shows is ejectable except `template`, the Mustache primitive, which has no emit logic of its own to own. An ejected generator comes with the emitters it writes its output through (the enum, navigation, doc-comment and prompt-tier writers), so the copy owns the code that writes the file and not a shell around the package's; the naming and type-mapping tables (`CSharpNaming`, `ValueObjectNames`) stay in the package as the engine. `dotnet meta eject api-docs` does the same for the api reference `dotnet meta docs` writes. Ejecting into a project whose `codegen/Codegen.csproj` pins an older `MetaObjects.Codegen` than the tool prints a warning naming the line to change. See [Own your codegen → C#](../features/own-your-codegen.md#c-dotnet-meta-eject).
+**The generator list.** Every generator a project runs is an entry in `codegen/Generators.cs`,
+with its options as init properties, the api reference included (ADR-0034 Amendment 9).
+`dotnet meta init` writes that file with an empty list, beside `codegen/Program.cs` (two
+lines that run it) and `codegen/Codegen.csproj`. `dotnet meta gen` runs the list through
+`dotnet run --project codegen`; nothing is generated that the list does not declare, and a
+project with no `codegen/Generators.cs` gets an error, not output.
+
+```csharp
+// codegen/Generators.cs
+using MetaObjects.Codegen;
+
+public static class Generators
+{
+ public static IReadOnlyList All { get; } =
+ [
+ new MetaObjects.Codegen.Generators.EntityGenerator(),
+ new MetaObjects.Codegen.Generators.DbContextGenerator(),
+ new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(),
+ new MetaObjects.Codegen.Generators.RoutesGenerator(),
+ ];
+}
+```
+
+```csharp
+// codegen/Program.cs
+using MetaObjects.Codegen;
+return CodegenCli.Run(args, Generators.All);
+```
```bash
-# Generate EF Core entities + AppDbContext + CRUD minimal-API routes
-dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog \
- --generators entity,db-context,filter-allowlist,routes
+# Run the list: EF Core entities + AppDbContext + CRUD minimal-API routes
+dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
# Drift-check templates against payloads (FR-004)
dotnet meta verify ./metadata --templates ./prompts
```
-Codegen is opt-in: with no `--generators`, `gen` writes nothing. `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, since the result would not compile.
+`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
+would not compile. `--generators ` narrows a run to the declared entries named; it
+never adds one, and naming an entry the list does not declare is refused.
+
+**Own a generator.** `dotnet meta eject ...` copies a reference generator into
+`codegen/generators/`. Every generator `dotnet meta gen --list` shows is ejectable except
+`template`, the Mustache primitive, which has no emit logic of its own to own. An ejected
+generator comes with the emitters it writes its output through (the enum, navigation,
+doc-comment and prompt-tier writers), so the copy owns the code that writes the file and not
+a shell around the package's; the naming and type-mapping tables (`CSharpNaming`,
+`ValueObjectNames`) stay in the package as the engine. Eject writes the three `codegen/` files
+when they are missing, with the copy already listed as `new Codegen.Generators.()`;
+when `codegen/Generators.cs` exists it prints that entry for you to add in place of the
+packaged one. `dotnet meta eject api-docs` does the same for the api reference. Ejecting into
+a project whose `codegen/Codegen.csproj` pins an older `MetaObjects.Codegen` than the tool
+prints a warning naming the line to change. See
+[Own your codegen → C#](../features/own-your-codegen.md#c-dotnet-meta-eject).
Schema migrations are owned by the Node `meta` CLI (ADR-0015) — the C# CLI is
`gen` + `verify` only.
-`gen` also accepts `--template-spec ` (+ `--template-root `, default
-`templates`) — the declarative Mustache template-codegen surface; see
-[Declarative template-codegen](#declarative-template-codegen---template-spec) below.
+A declarative Mustache template-spec is one more entry in `codegen/Generators.cs`; see
+[Declarative template-codegen](#declarative-template-codegen-templatespecgenerator) below.
The codegen emits:
@@ -128,10 +169,10 @@ The codegen emits:
### `Names` — the physical names, as constants
-`names` is opt-in — select it with `--generators names` on `dotnet meta gen`
-(`dotnet meta gen --list` names the whole catalog). Selecting a generator is not
+`names` is opt-in — list `new MetaObjects.Codegen.Generators.NamesGenerator()` in
+`codegen/Generators.cs` (`dotnet meta gen --list` names the whole catalog). Listing a generator is not
owning it: to change what `names` emits, `dotnet meta eject names` copies its source
-into `codegen/generators/` (see "Own a generator" above). When selected, a project gets
+into `codegen/generators/` (see "Own a generator" above). When listed, a project gets
`Names.g.cs`. It carries the physical
database names for one object as `const string`s:
@@ -409,18 +450,17 @@ are core and are not ejectable.
dotnet meta verify ./metadata --templates ./prompts
dotnet meta verify ./metadata --templates ./prompts --require-implementers
-# The tests: their own run, into a test project that references xunit
-dotnet meta gen ./metadata --out ./tests/Acme.Shop.Tests/Generated \
- --namespace Acme.Shop --generators requirement-tests
-dotnet meta verify ./metadata --codegen --out ./tests/Acme.Shop.Tests/Generated \
- --namespace Acme.Shop --generators requirement-tests
+# 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
```
-A `gen` run has one `--out`, so give this generator a run of its own rather than adding it
-to the run that writes your entities. Pass `--namespace` to `verify --codegen` as well:
-without it the namespace is inferred from the first generated file in `--out`, and in a
-directory that holds only these tests that is the tests' own namespace, which regenerates
-one level too deep and reports drift.
+Give the entry an `OutDir` in `codegen/Generators.cs` (below): the tests need xunit, which
+the project compiling `--out` does not reference, so they go into the test project. `gen`
+writes them there and `verify --codegen` compares them there, in the same run that writes
+and checks your entities. Point `OutDir` at a directory that holds generated files only:
+`verify --codegen` compares the whole tree.
Per metamodel package the generator writes `Requirements__Witnesses.g.cs`, an
interface with one default member per test that is not skipped, and
@@ -454,41 +494,38 @@ it goes stale silently. If a test reports `unimplemented requirement` for a witn
believe you wrote, the method is not implementing the member: in the implicit form it must
be `public`, non-static, parameterless and named exactly as the interface names it.
-The six options are public properties on the generator. This port has no per-generator
-option channel, so they are reachable only where the generator is constructed, in an owned
-`codegen/Program.cs`. That file exists once you have ejected something (or written it by
-hand); the packaged `dotnet meta gen --generators requirement-tests` runs with the defaults.
+The seven options are init properties on the generator, set on its entry in
+`codegen/Generators.cs`:
```csharp
-// codegen/Program.cs
-using Codegen.Generators;
+// codegen/Generators.cs
using MetaObjects.Codegen;
using MetaObjects.Core.Requirement; // RequirementTestGrain, IRequirementTestFilter
-IReadOnlyList generators =
-[
- new RequirementTestsGenerator
- {
- WitnessClass = "Acme.Shop.Tests.Witnesses",
- Grain = RequirementTestGrain.Member,
- WarnUncovered = false,
- },
-];
-
-return CodegenCli.Run(args, generators);
+public static class Generators
+{
+ public static IReadOnlyList All { get; } =
+ [
+ new MetaObjects.Codegen.Generators.RequirementTestsGenerator
+ {
+ OutDir = "tests/Acme.Shop.Tests/Generated",
+ WitnessClass = "Acme.Shop.Tests.Witnesses",
+ Grain = RequirementTestGrain.Member,
+ WarnUncovered = false,
+ },
+ ];
+}
```
-The example is the file as it stands after `dotnet meta eject requirement-tests`: the class
-is your owned copy, in `Codegen.Generators`. If `Program.cs` exists because you ejected
-something else and you are constructing the packaged generator, the class is in
-`MetaObjects.Codegen.Generators`: write it fully qualified, as
-`new MetaObjects.Codegen.Generators.RequirementTestsGenerator { … }`. Do not add
-`using MetaObjects.Codegen.Generators;`, because every generator you ejected has a packaged
-class of the same simple name there, and the second `using` makes each `new Generator()`
-already in the file ambiguous (CS0104).
+After `dotnet meta eject requirement-tests` the entry names your owned copy instead,
+`new Codegen.Generators.RequirementTestsGenerator { … }`. Write each entry fully
+qualified, as above: an owned copy and the packaged class share a simple name, so adding
+`using MetaObjects.Codegen.Generators;` beside `using Codegen.Generators;` makes each
+`new Generator()` ambiguous (CS0104).
| Property | Meaning |
|---|---|
+| `OutDir` | Where the tests are written, relative to the directory the run starts in. Default: the run's `--out`. Set it to the test project's directory. |
| `TestNamespace` | Namespace of the generated tests. Default `.Requirements`. |
| `WitnessClass` | Full name of your witness class. Default `.RequirementWitnesses`. |
| `Grain` | `RequirementTestGrain.Concern` (default): one test per distinct `.` a requirement claims. `Member`: one per distinct `implementedBy` reference that resolves. |
@@ -503,18 +540,22 @@ two files behind: `gen` does not remove them, `dotnet meta verify --codegen` rep
other ports' spelling are in
[Generated requirement tests and witnesses](../features/requirements.md#generated-requirement-tests-and-witnesses).
-## Declarative template-codegen (`--template-spec`)
+## Declarative template-codegen (`TemplateSpecGenerator`)
Beyond the built-in EF Core / routes suite, `dotnet meta gen` runs **declarative
Mustache template generators** from a JSON template-spec — the cross-port contract
shared with the Python port (see
[`docs/features/codegen-concepts.md`](../features/codegen-concepts.md#declarative-template-scopes)
and the neutral data dict in
-[`docs/features/codegen-data-shapes.md`](../features/codegen-data-shapes.md)):
+[`docs/features/codegen-data-shapes.md`](../features/codegen-data-shapes.md)). The spec is
+declared where every generator is, as an entry in `codegen/Generators.cs`:
-```bash
-dotnet meta gen ./metadata --out ./Generated \
- --template-spec ./template-spec.json --template-root ./templates
+```csharp
+new MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator
+{
+ Spec = "template-spec.json", // from the directory gen runs in, as --out is
+ TemplateRoot = "templates", // optional; default the run's --template-root, else prompts/ when it exists, else templates/
+},
```
```jsonc
@@ -523,36 +564,26 @@ dotnet meta gen ./metadata --out ./Generated \
{ "name": "entity-doc",
"scope": "perEntity", // perEntity | perPackage | perModel
"outputPattern": "{package}/{Name}.md",
- "template": "entity-doc", // resolved under --template-root
+ "template": "entity-doc", // resolved under TemplateRoot
"format": "markdown" } // optional; a registered escaper format
]}
```
-**The spec is auto-discovered.** Omit `--template-spec` and the CLI reads
-`/template-spec.json`, where projectRoot is the metadata dir's **parent** —
-the same anchor `.metaobjects/` uses (`GenCommand.ProjectRootFor`). The flag overrides it.
-
-Prefer the conventional path over the flag, because **`dotnet meta verify --codegen`
-accepts no `--template-spec`**: discovery is how the drift gate learns your template
-generators exist. A spec reachable only by flag leaves `verify` regenerating a different
-generator list from `gen` and convicting your committed template output as
-"committed but a fresh regen would not emit it".
-
-```bash
-dotnet meta gen ./metadata --out ./Generated --template-root ./templates
-dotnet meta verify --codegen ./metadata --out ./Generated --template-root ./templates
-# ^ both resolve /template-spec.json — the gate agrees with the generator
-```
+`dotnet meta verify --codegen` regenerates the same entry, so the gate and the generator
+agree. Through 1.0 the CLI also read `--template-spec` or a project-root `template-spec.json`
+on its own; neither is read now, and a run that finds either with no `TemplateSpecGenerator`
+entry fails before writing anything, with a line that starts
+`dotnet meta gen: found 1.0 template-spec configuration (`. See
+[the migration section](../features/migrations/docs-are-generators.md#c).
Each spec entry derives the neutral template data dict for its scope
(`MetaObjects.Codegen.TemplateCodegen.TemplateData`) and names each file via the
-`outputPattern` placeholders (`{name}`, `{Name}`, `{package}`). The named generators
-are **appended** to the `--generators` selection (there is no default suite) and gated
-byte-identical against the shared
-`fixtures/template-codegen-conformance/` corpus. A `target` field is rejected (C# has
-no output-target concept); a bad template ref or wrong `--template-root` surfaces as a
-clean error, not a stack trace. For output to be regenerable, the **template** must emit
-the `@generated` header itself (the write path refuses to overwrite files lacking it).
+`outputPattern` placeholders (`{name}`, `{Name}`, `{package}`). The entry's files land under
+`--out`, or under its own `OutDir` when it sets one, and are gated byte-identical against the
+shared `fixtures/template-codegen-conformance/` corpus. A `target` field is rejected (C# has no
+output-target concept); a missing spec file, a bad template ref or a wrong `TemplateRoot`
+surfaces as a clean error, not a stack trace. For output to be regenerable, the **template**
+must emit the `@generated` header itself (the write path refuses to overwrite files lacking it).
## Angular 18 frontend
@@ -591,9 +622,10 @@ on their list route, against an allowlist of their own fields. Remaining gaps ar
| Templates + render (FR-004) | Yes (`MetaObjects.Render`) |
| Output parser codegen (FR-006) | Yes (`OutputParserGenerator` — `Parse`/`TryParse` BCL pattern) |
| Payload-VO codegen | Yes — the payload IS the value object's own POCO from `EntityGenerator` (ADR-0056); no separate payload generator |
-| Declarative template-codegen | Yes — `dotnet meta gen --template-spec` (scope perEntity/perPackage/perModel + outputPattern; the cross-port JSON contract shared with Python) |
+| Declarative template-codegen | Yes — a `TemplateSpecGenerator` entry in `codegen/Generators.cs` (scope perEntity/perPackage/perModel + outputPattern; the cross-port JSON contract shared with Python) |
| Migrations | Owned by the Node `meta` CLI (ADR-0015) — no C# migrate surface |
-| Drift verify | `dotnet meta verify` (template drift, FR-004) |
+| Drift verify | `dotnet meta verify` (template drift, FR-004; `--codegen` regenerates the list in `codegen/Generators.cs` and diffs it) |
+| API reference docs | Yes — the `ApiDocsGenerator` entry in `codegen/Generators.cs` (`OutDir`, `ApiSubDir`, `ProjectName`, `ModelBaseUrl`); `dotnet meta docs` was removed in 1.1.0 ([migration](../features/migrations/docs-are-generators.md#c)) |
| Requirement gate + requirement tests | Yes (`dotnet meta verify`, on every run; `requirement-tests` emits xUnit and is ejectable) |
| Runtime metadata | Loader API + render engine; ObjectManager-style runtime tier on the roadmap |
diff --git a/docs/ports/python.md b/docs/ports/python.md
index 630a5be7a..5e5d28001 100644
--- a/docs/ports/python.md
+++ b/docs/ports/python.md
@@ -110,27 +110,38 @@ for a worked example.
The `metaobjects` console-script (installed by `pip install metaobjects`) runs
codegen and the drift gate — there is no `python -m metaobjects.codegen`
-module entry point:
+module entry point. What it generates is the list in `metaobjects.config.yaml`
+(ADR-0034 Amendment 9): each target names its `outDir` and the generators that write
+into it, documentation included, and nothing else is generated.
-```bash
-metaobjects gen ./metaobjects --out ./generated \
- --generators entity,names,filter-allowlist,routes # codegen → write
-metaobjects verify ./metaobjects --out ./generated \
- --generators entity,names,filter-allowlist,routes # drift gate; bare verify means --codegen
+```yaml
+# metaobjects.config.yaml
+metadata: metaobjects
+targets:
+ app:
+ outDir: generated
+ generators: [entity, names, filter-allowlist, routes]
```
-Codegen is opt-in, so both commands name the generators. `verify` regenerates exactly
-the suite you name and compares, so pass it the same list `gen` ran. With no
-`--generators`, `gen` writes nothing and `verify` reports that there is nothing to
-check and exits 0: in CI that is a gate that checks nothing. `routes` needs `entity`
-and `filter-allowlist` beside it (the router imports both), and `metaobjects gen`
-warns when one is missing.
+```bash
+metaobjects gen # runs every target's generators into its outDir
+metaobjects verify # drift gate; bare verify means --codegen, which regenerates the same list
+```
-For multi-target projects (several `outDir`s, per-target generator/entity
-selection, config-relative provider resolution), `metaobjects gen`/`verify` also
-read a declarative [`metaobjects.config.yaml`](../features/cli.md) (#267) — run
-either with no positional `` to use it; the flag path above stays
-byte-identical.
+`verify --codegen` regenerates exactly the list `gen` runs and compares, so the two can
+never disagree about what was generated. A target whose `generators` is empty writes
+nothing and says so. With no `metaobjects.config.yaml` at all, `gen` and `verify --codegen`
+fail with exit 2 and write nothing: there is no list to run. `routes` needs `entity` and
+`filter-allowlist` beside it (the router imports both), and `metaobjects gen` warns when one
+is missing.
+
+The 1.0 form `metaobjects gen --out --generators ` is not read:
+the metadata directory is `metadata` and each output directory is a target's `outDir`.
+`--generators ` now narrows a run to the declared entries named and never adds one
+(naming one no target declares is refused); `--target ` runs one target;
+`--entities`, `--column-naming`, `--provider`, `--templates`, `--baseline` and `--config`
+work as before. See [the migration section](../features/migrations/docs-are-generators.md#python)
+and the [config reference](../features/cli.md).
### Reports
@@ -149,15 +160,18 @@ report with no view source, or an abstract one, generates nothing. The contract
### Taking one tier and not the rest
-`--generators ` selects exactly the named generators from the available
-catalog (`--list` names all of them). This is the answer for a project that wants
+A target lists exactly the generators it runs, from the catalog (`--list` names all of
+them). This is the answer for a project that wants
one tier without adopting the others — most often a **schema-only adopter** whose
tables come from `meta migrate` and whose application code is not generated at all,
but which still has physical table and column names hard-coded across its data
layer:
-```bash
-metaobjects gen ./metaobjects --out ./generated --generators names
+```yaml
+targets:
+ names:
+ outDir: generated
+ generators: [names]
```
It emits one `_names.py` per object and nothing else. Each carries
@@ -249,11 +263,11 @@ out. Serve the app with `uvicorn my_app.main:app --reload` (`pip install uvicorn
### `_names.py` — the physical names, as constants
-The `names` generator is opt-in — select it with `--generators names` on
-`metaobjects gen` (`--list` names the whole catalog). Selecting a generator is not
+The `names` generator is opt-in — list `names` in a target's `generators` in
+`metaobjects.config.yaml` (`metaobjects gen --list` names the whole catalog). Listing a generator is not
owning it: to change what `names` emits, `metaobjects eject names` copies its source
into `codegen/generators/` ([Own your codegen → Python](../features/own-your-codegen.md#python-metaobjects-eject)).
-When selected, a project gets `_names.py`. The module
+When listed, a project gets `_names.py`. The module
mirrors the metadata that declared it: every node it describes — the object,
each `source.rdb` child, each `identity.*` and `index.*` child — carries its
own `_TYPE`, `_SUB_TYPE` and `_NAME`, and a source's physical name sits under
@@ -395,8 +409,8 @@ live requirement, a link above the floor, a live policy applied to nothing) exit
`--require-implementers` (or `META_REQUIRE_IMPLEMENTERS=1`) also fails the run on a live
functional requirement that nothing implements. Those checks are core and are not
ejectable. A model with no requirement sees no change. Metadata that does not load is not
-that: `verify` prints the load errors and exits non-zero, also when no generators are
-selected and so no drift gate loaded it.
+that: `verify` prints the load errors and exits non-zero, also when no drift gate ran and so
+none loaded it.
The `requirement-tests` generator is the other half. It is a reference helper and a
recommended approach, not a contract: `metaobjects eject requirement-tests` copies it, with
@@ -442,9 +456,7 @@ or deleted is simply never called again: nothing reports it, and you delete it b
| `renderer` | A function taking a `RequirementTestArgs` and returning a `RenderedTest(imports, source)` to replace the text of one test, or `None` to keep the default. Import both types from `metaobjects.codegen.requirement_hooks`, never from an ejected copy of the generator. |
| `warnUncovered` | `true` (default): one warning naming up to five requirements the filter left out. |
-The block is read in config mode. The flag-only form
-(`metaobjects gen ./metaobjects --out ./tests/generated --generators requirement-tests`)
-runs the generator with its defaults. A `filter` or `renderer` whose module raises on
+With no `requirementTests` block the generator runs with its defaults. A `filter` or `renderer` whose module raises on
import is reported with the real cause. Both hooks, like a `providers` entry, are found
relative to the config file's directory, with no `PYTHONPATH=`. They differ in how they are
imported: a provider is imported with plain `importlib`, and a hook goes through the resolver
@@ -456,18 +468,23 @@ package that loses its last requirement leaves its file behind: `gen` does not r
`verify` lists it as `extra:`, and you delete it. The model and the other ports' spelling are in
[Generated requirement tests and witnesses](../features/requirements.md#generated-requirement-tests-and-witnesses).
-### Declarative template-codegen (`--template-spec`)
+### Declarative template-codegen (`templateSpec`)
-Beyond the built-in Pydantic/FastAPI suite, the `metaobjects gen` console-script
-runs **declarative Mustache template generators** from a JSON template-spec — the
-cross-port contract shared with the C# port (see
+Beyond the built-in Pydantic/FastAPI suite, `metaobjects gen` runs **declarative Mustache
+template generators** from a JSON template-spec — the cross-port contract shared with the
+C# port (see
[`docs/features/codegen-concepts.md`](../features/codegen-concepts.md#declarative-template-scopes)
and the neutral data dict in
-[`docs/features/codegen-data-shapes.md`](../features/codegen-data-shapes.md)):
+[`docs/features/codegen-data-shapes.md`](../features/codegen-data-shapes.md)). The spec is
+declared on the target its output belongs in, like any other entry:
-```bash
-metaobjects gen ./metaobjects --out ./generated \
- --template-spec ./template-spec.json --templates ./templates
+```yaml
+# metaobjects.config.yaml
+targets:
+ docs:
+ outDir: generated
+ generators: [] # the spec's generators run after these
+ templateSpec: template-spec.json # relative to this file
```
```jsonc
@@ -481,38 +498,30 @@ metaobjects gen ./metaobjects --out ./generated \
]}
```
-**The spec is auto-discovered.** Omit `--template-spec` and the CLI reads
-`/template-spec.json`, where projectRoot is the metadata dir's **parent** —
-the same anchor `.metaobjects/` uses. The flag overrides it.
-
-Prefer the conventional path over the flag, because **`verify --codegen` accepts no
-`--template-spec`**: discovery is how the drift gate learns your template generators exist.
-A spec reachable only by flag leaves `verify` regenerating a different generator list from
-`gen` and reporting your committed template output as stale.
-
```bash
-metaobjects gen ./metaobjects --out ./generated --templates ./templates
-metaobjects verify --codegen ./metaobjects --out ./generated --templates-root ./templates
-# ^ both resolve /template-spec.json — the gate agrees with the generator
+metaobjects gen --templates ./templates
+metaobjects verify --codegen --prompts ./templates
+# ^ both read the target's templateSpec — the gate regenerates what gen wrote
```
(`gen` spells the templates dir `--templates`; on `verify` that name is the *subverb*, so
-the directory flag there is `--templates-root`.)
+the directory flag there is `--prompts`, with `--templates-root` its deprecated alias.)
-`--template-spec` is **refused in declarative-config mode** (`metaobjects.config.yaml`,
-no positional metadata dir) rather than silently ignored: a spec entry names no `target`
-while config mode writes per target, so there is no outDir to render into. A *discovered*
-spec is ignored there rather than refused.
+The `--template-spec` flag and the 1.0 discovery of `/template-spec.json` are
+not read: a spec is part of the list only where a target declares it, so `gen` and
+`verify --codegen` always agree about it. A project-root `template-spec.json` that no target
+declares fails both before anything is written, with a line that starts
+`metaobjects gen: found 1.0 template-spec configuration (`.
Each spec entry derives the neutral template data dict for its scope and names
each file via the `outputPattern` placeholders (`{name}`, `{Name}`, `{package}`).
-The named generators are **appended** to the `--generators` selection (there is no
-default suite) and gated byte-identical
-against the shared `fixtures/template-codegen-conformance/` corpus. Output is
-format-agnostic (text/markdown/csv/json/xml/html), so the template-spec pass emits
-no `__init__.py` into its tree. A `target` field is rejected (the Python port has
-no output-target concept); for output to be regenerable, the **template** must emit
-the `@generated` header itself (the write path refuses to overwrite files lacking
+The named generators run after the target's `generators`, into the same `outDir`, and
+`--generators ` narrows to them by their spec `name` like any declared entry. They
+are gated byte-identical against the shared `fixtures/template-codegen-conformance/`
+corpus. Output is format-agnostic (text/markdown/csv/json/xml/html), so the template-spec
+pass emits no `__init__.py` into its tree. A `target` field inside the spec is rejected (the
+spec's target is the one that declares it); for output to be regenerable, the **template**
+must emit the `@generated` header itself (the write path refuses to overwrite files lacking
it).
### Universal browser-client hookup (React / Angular 18)
@@ -667,8 +676,8 @@ field mapping, extract delegate, extract schema and format-spec writers), copied
`codegen/runtime/` and read from there, so the copy owns the code that writes the file and
not a shell around the package's. The naming and type-mapping tables, the model walks and
the generator base stay in the package as the engine. `metaobjects eject api-docs` does the
-same for the api reference `metaobjects docs` writes: the copy is wired under
-`docs.generators` in `metaobjects.config.yaml`, never in a target's `generators`.
+same for the api reference: the copy replaces `api-docs` in the `generators` of the target
+that writes the docs, like any other owned copy.
Ejecting `routes` also hands over the helper runtime the generated routers call:
`filter_parser` and `constraint_errors` are copied into `codegen/runtime/`, and the owned
@@ -684,15 +693,22 @@ and `ObjectManager`. A project that has not ejected `routes` keeps importing
Every template-tier generator that imports a model (`output-parser`, `extractor`,
`render-helper`) needs `entity` in the same run: `metaobjects gen --list` marks them
-`(requires: entity)`, and `--generators` warns when `entity` is missing.
+`(requires: entity)`, and `gen` warns when a target lists one without `entity`.
`render-helper` additionally needs an on-disk template root for its build-time drift
-gate, and takes it from **`--templates `** — the same flag the `--template-spec`
-pass uses. Pass it whenever `render-helper` is in the selection:
+gate, and takes it from **`--templates `** — the same flag a target's `templateSpec`
+resolves its template refs under. Pass it whenever a target lists `render-helper`:
+
+```yaml
+# metaobjects.config.yaml
+targets:
+ app:
+ outDir: src/app/generated
+ generators: [entity, render-helper]
+```
```
-metaobjects gen metaobjects/ --out src/app/generated \
- --generators entity,render-helper --templates prompts
+metaobjects gen --templates prompts
```
Omit it and the root is `prompts/` when that directory exists, else `templates/` — the
@@ -806,9 +822,10 @@ import lines are stable.
| Templates + render (FR-004) | Yes (`metaobjects.render`) |
| Payload-VO codegen | Yes — the `entity` generator's model for each value object IS the payload type (ADR-0056); no separate payload generator |
| Output parser codegen (FR-006) | Yes (`output_parser_generator` — Pydantic throw-only; imports the `@responseRef` value object's model) |
-| Declarative template-codegen | Yes — `metaobjects gen --template-spec` (scope perEntity/perPackage/perModel + outputPattern; the cross-port JSON contract shared with C#) |
+| Declarative template-codegen | Yes — a target's `templateSpec` in `metaobjects.config.yaml` (scope perEntity/perPackage/perModel + outputPattern; the cross-port JSON contract shared with C#) |
| Migrations | TS-only by design (ADR-0015) — no Python `migrate` command; consume the canonical `schema.postgres.sql` |
-| Drift verify | Yes — template / payload drift (`metaobjects.render.verify`) |
+| Drift verify | Yes — template / payload drift (`metaobjects.render.verify`); `verify --codegen` regenerates the config's list and diffs each target's `outDir` |
+| API reference docs | Yes — `api-docs` in a target's `generators`, options in the top-level `apiDocs` block (`apiSubDir`, `projectName`, `modelBaseUrl`); `metaobjects docs` was removed in 1.1.0 ([migration](../features/migrations/docs-are-generators.md#python)) |
| Requirement gate + requirement tests | Yes (`metaobjects verify`, on every run; `requirement-tests` emits pytest and is ejectable) |
| Runtime metadata | Yes (`metaobjects.runtime.ObjectManager` — DB-API 2 driver, pg8000/psycopg) + loader API + render engine |
diff --git a/docs/recipes/mysql.md b/docs/recipes/mysql.md
index 4a4103dbc..106141aff 100644
--- a/docs/recipes/mysql.md
+++ b/docs/recipes/mysql.md
@@ -18,7 +18,7 @@ MySQL is a **code generation and runtime** target. It is not a schema target:
| Generated code | `dialect: "mysql"`: Drizzle `mysqlTable` entities, Zod schemas, queries, Fastify/Hono routes | DTOs, controllers and repository interfaces are dialect-neutral |
| Runtime | the generated routes (`runtime-ts`); `ObjectManager` over `kyselyDriver` (Kysely `MysqlDialect`) or `drizzleDriver` (`drizzle-orm/mysql2`) | OMDB `MySQLDriver` |
| Schema | you own it: no `meta migrate`, `meta verify --db` or `--replay`, and no `agent/schema.md` page | you own it |
-| Drift gates | `meta verify --codegen`, `--templates`, `--docs` work as on any dialect | `mvn metaobjects:verify` (codegen drift) |
+| Drift gates | `meta verify --codegen` and `--templates` work as on any dialect | `mvn metaobjects:verify` (codegen drift) |
These tests exercise the whole list against a real MySQL 8.4 server:
diff --git a/docs/recipes/write-your-own-generator.md b/docs/recipes/write-your-own-generator.md
index cf196227e..57c5c9ba5 100644
--- a/docs/recipes/write-your-own-generator.md
+++ b/docs/recipes/write-your-own-generator.md
@@ -105,13 +105,24 @@ def field_list(): # the module:symbol target — a function returning the gener
return FieldList()
```
+List it under a target's `generators` in `metaobjects.config.yaml`, the generator list
+`metaobjects gen` runs:
+
+```yaml
+# metaobjects.config.yaml
+metadata: metaobjects
+targets:
+ main:
+ outDir: gen
+ generators: [codegen.generators.field_list:field_list]
+```
+
```bash
-metaobjects gen metaobjects --out gen --generators codegen.generators.field_list:field_list
-metaobjects verify --codegen metaobjects --out gen --generators codegen.generators.field_list:field_list
+metaobjects gen
+metaobjects verify --codegen
```
-or list `codegen.generators.field_list:field_list` under a target's `generators` in
-`metaobjects.config.yaml`. The symbol must be the generator INSTANCE or a function
+The symbol must be the generator INSTANCE or a function
returning one — not the class: a class has a `generate` attribute, so it would be taken as
the generator itself. `metaobjects.codegen.model_walk` is the one import for model reads
(`is_required`, `max_length`, `field_is_array`, `enum_values`, `object_ref_target`,
@@ -120,9 +131,10 @@ the generator itself. `metaobjects.codegen.model_walk` is the one import for mod
### C#
-C# generators run from an owned console project, `codegen/`. `dotnet meta gen` and
-`dotnet meta verify --codegen` hand off to it whenever `codegen/Codegen.csproj` exists.
-`dotnet meta eject ` scaffolds that project; to start from none, write the two files:
+C# generators run from a console project, `codegen/`, whose generator list is
+`codegen/Generators.cs`. `dotnet meta gen` and `dotnet meta verify --codegen` always run it
+(`dotnet run --project codegen`). `dotnet meta init` writes the project; by hand it is three
+files:
`codegen/Codegen.csproj`
@@ -136,23 +148,30 @@ C# generators run from an owned console project, `codegen/`. `dotnet meta gen` a
-
+
```
-`codegen/Program.cs`
+`codegen/Generators.cs`
```csharp
-using Codegen.Generators;
using MetaObjects.Codegen;
-IReadOnlyList generators =
-[
- new FieldListGenerator(),
-];
+public static class Generators
+{
+ public static IReadOnlyList All { get; } =
+ [
+ new Codegen.Generators.FieldListGenerator(),
+ ];
+}
+```
+
+`codegen/Program.cs`
-return CodegenCli.Run(args, generators);
+```csharp
+using MetaObjects.Codegen;
+return CodegenCli.Run(args, Generators.All);
```
`codegen/generators/FieldListGenerator.cs`
@@ -180,8 +199,8 @@ dotnet meta gen metaobjects --out gen
dotnet meta verify --codegen metaobjects --out gen
```
-An owned generator the `--generators` selection does not name still runs, so a project
-with only generators of its own needs no `--generators` at all. Model reads:
+Every entry in `Generators.All` runs; `--generators ` narrows a run to the entries
+named and never adds one. Model reads:
`field.Attr(name)` resolves; `IsArray` is the own flag, `ResolvedIsArray()` resolves;
`field.EffectiveEnumValues`, `field.MaxLength`; `ValueObjectNames.ResolveFieldRef(field,
ctx.Root)` resolves an `@objectRef` package-locally; `CSharpNaming.RoutePath(obj)` is the
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md
index b4fe9bcaf..0a93a084f 100644
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md
+++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md
@@ -89,8 +89,8 @@ what it emits.
| Port | Registration | Model helpers |
|---|---|---|
| TypeScript | import it in `metaobjects.config.ts`, add it to `generators: [...]` (the scaffold does both) | `@metaobjectsdev/codegen-ts`: `objectRefTarget`, `enumValues`, `effectivePackage`, `packageToPath`, `servedPath`, `toCamelCase`/`toPascalCase`/`toSnakeCase`/`pluralize`, `isAbstract`, `hasAnyRdbSource`, `servesReadApi`/`servesWriteApi` |
-| Python | a `module:symbol` entry in `--generators` or a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one, never the class | `metaobjects.codegen.model_walk` |
-| C# | `new YourGenerator()` in the owned `codegen/Generators.cs` list, which `codegen/Program.cs` passes to `CodegenCli.Run`; `dotnet meta gen` / `verify --codegen` hand off to `codegen/` whenever `codegen/Codegen.csproj` exists | node accessors, `ValueObjectNames.ResolveFieldRef`, `CSharpNaming.RoutePath` |
+| Python | a `module:symbol` entry in a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one, never the class | `metaobjects.codegen.model_walk` |
+| C# | `new Codegen.Generators.YourGenerator()` in `codegen/Generators.cs` (`dotnet meta init` writes it); `dotnet meta gen` / `verify --codegen` run that list | node accessors, `ValueObjectNames.ResolveFieldRef`, `CSharpNaming.RoutePath` |
| Java / Kotlin | `` plus ``, from a module on the plugin's classpath; extend `FileEmittingGenerator` | `com.metaobjects.generator.ModelWalk` |
**Verify it** — nothing to register. `verify --codegen` (`mvn metaobjects:verify` on the
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md
index d67ccc2f3..30845017e 100644
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md
+++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md
@@ -19,7 +19,7 @@ it there, in the same change, and keep going:
|---|---|
| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own, and `docs-site`, whose builder stays in the package (`meta eject docs-site` copies the site's templates and assets into `codegen/docs-site/`); an ejected generator comes with the template modules that write its output (`codegen/templates/`): the text of what it emits is yours to change too. An ejected `docs` or `api-docs` goes in `generators`, like any other owned copy — there is no separate docs list, and `meta gen` runs it. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. |
| **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. |
-| **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Generators.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). |
+| **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Generators.cs` on C#, a `module:symbol` entry in a target's `generators` on Python — or a template spec. |
Then do **not**:
diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md
index 70e4ac937..a81e972a5 100644
--- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md
+++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md
@@ -54,20 +54,17 @@ path is `TemplateScopeGenerator`, wired the same way with `` / `` already loads a consumer class from the project classpath.
-**C# and Python** have both. Programmatic: on C#, an `IGenerator` listed in the owned
-`codegen/Program.cs` (which `dotnet meta gen` / `verify --codegen` hand off to); on Python,
-a `module:symbol` entry in `--generators` or in `metaobjects.config.yaml`. (Python's
+**C# and Python** have both. Programmatic: on C#, an `IGenerator` listed in
+`codegen/Generators.cs` (the list `dotnet meta gen` / `verify --codegen` run); on Python,
+a `module:symbol` entry in a target's `generators` in `metaobjects.config.yaml`. (Python's
`--provider module:symbol` registers **metamodel vocabulary**, not a generator.)
-Declarative: `--template-spec ` — plus `--templates ` on Python or
-`--template-root ` on C# — and your entries are appended to your `--generators`
-selection. Worked examples with the full JSON: `docs/ports/python.md` and
-`docs/ports/csharp.md`.
-
-**The spec is auto-discovered, and that is load-bearing.** With no `--template-spec`, both
-ports read `/template-spec.json` — projectRoot being the metadata dir's parent.
-Keep it there: `verify --codegen` accepts no `--template-spec` flag, so the conventional path
-is how the drift gate learns your template generators exist. Put the spec somewhere else and
-reach it only by flag, and `verify` regenerates without it and reports its output as stale.
+Declarative: a JSON template-spec. On Python it is declared on a target
+(`templateSpec: `, refs resolved under `--templates `) and runs into that target's
+`outDir`. On C# it is an entry in `codegen/Generators.cs`:
+`new MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator { Spec = "template-spec.json",
+TemplateRoot = "templates" }`. Neither port reads a spec it was not given there: a leftover
+project-root `template-spec.json` with nothing declaring a spec fails the run. Worked examples
+with the full JSON: `docs/ports/python.md` and `docs/ports/csharp.md`.
So on every port, "I need a shape the built-ins do not emit" is answered by a generator of
your own or a template. Do not conclude the port cannot be customized.
diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md
index b4fe9bcaf..0a93a084f 100644
--- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md
+++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md
@@ -89,8 +89,8 @@ what it emits.
| Port | Registration | Model helpers |
|---|---|---|
| TypeScript | import it in `metaobjects.config.ts`, add it to `generators: [...]` (the scaffold does both) | `@metaobjectsdev/codegen-ts`: `objectRefTarget`, `enumValues`, `effectivePackage`, `packageToPath`, `servedPath`, `toCamelCase`/`toPascalCase`/`toSnakeCase`/`pluralize`, `isAbstract`, `hasAnyRdbSource`, `servesReadApi`/`servesWriteApi` |
-| Python | a `module:symbol` entry in `--generators` or a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one, never the class | `metaobjects.codegen.model_walk` |
-| C# | `new YourGenerator()` in the owned `codegen/Generators.cs` list, which `codegen/Program.cs` passes to `CodegenCli.Run`; `dotnet meta gen` / `verify --codegen` hand off to `codegen/` whenever `codegen/Codegen.csproj` exists | node accessors, `ValueObjectNames.ResolveFieldRef`, `CSharpNaming.RoutePath` |
+| Python | a `module:symbol` entry in a target's `generators` in `metaobjects.config.yaml`; the symbol is an instance or a function returning one, never the class | `metaobjects.codegen.model_walk` |
+| C# | `new Codegen.Generators.YourGenerator()` in `codegen/Generators.cs` (`dotnet meta init` writes it); `dotnet meta gen` / `verify --codegen` run that list | node accessors, `ValueObjectNames.ResolveFieldRef`, `CSharpNaming.RoutePath` |
| Java / Kotlin | `` plus ``, from a module on the plugin's classpath; extend `FileEmittingGenerator` | `com.metaobjects.generator.ModelWalk` |
**Verify it** — nothing to register. `verify --codegen` (`mvn metaobjects:verify` on the
diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md
index d67ccc2f3..30845017e 100644
--- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md
+++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md
@@ -19,7 +19,7 @@ it there, in the same change, and keep going:
|---|---|
| **TypeScript** | Your ejected copy under `codegen/generators/` — and, for routes, the HTTP adapter it calls: `meta eject routes` / `routes-hono` / `entity` also copy that adapter's source (mount helpers, filter parser, error envelopes, pagination) into `codegen/runtime/`, and the ejected output imports it from there, so a bug in a route's behaviour is fixed in that copy. If you are running a generator you never ejected (imported from its package), `meta eject ` it first and swap the import for the local one eject prints. Every stock generator can be ejected except the `template` primitive, which has no emit logic of its own to own, and `docs-site`, whose builder stays in the package (`meta eject docs-site` copies the site's templates and assets into `codegen/docs-site/`); an ejected generator comes with the template modules that write its output (`codegen/templates/`): the text of what it emits is yours to change too. An ejected `docs` or `api-docs` goes in `generators`, like any other owned copy — there is no separate docs list, and `meta gen` runs it. A generator `meta gen --list` marks `package-only` has no reference template: replace it in `generators` with your own `Generator`, starting from its source in the installed package. |
| **Java / Kotlin** | Your own generator class. Subclass the reference generator and override the `protected` method that emits the wrong piece; when that piece is `private`, copy the reference generator's source (Apache-2.0) into your own package and edit it. Put the class in a codegen module that the module running `metaobjects:generate` depends on — the plugin loads `` from that module's compile classpath, and `provided` scope keeps it out of your packaged app — then point `` at it. Full steps: the Java and Kotlin references. |
-| **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Generators.cs` on C#, a `module:symbol` entry on Python — or a template spec (`--template-spec`). |
+| **C# / Python** | Your owned copy (`dotnet meta eject ` / `metaobjects eject `, then edit it), or a generator of your own that replaces the artifact — an `IGenerator` in `codegen/Generators.cs` on C#, a `module:symbol` entry in a target's `generators` on Python — or a template spec. |
Then do **not**:
diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md
index 70e4ac937..a81e972a5 100644
--- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md
+++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md
@@ -54,20 +54,17 @@ path is `TemplateScopeGenerator`, wired the same way with `` / `` already loads a consumer class from the project classpath.
-**C# and Python** have both. Programmatic: on C#, an `IGenerator` listed in the owned
-`codegen/Program.cs` (which `dotnet meta gen` / `verify --codegen` hand off to); on Python,
-a `module:symbol` entry in `--generators` or in `metaobjects.config.yaml`. (Python's
+**C# and Python** have both. Programmatic: on C#, an `IGenerator` listed in
+`codegen/Generators.cs` (the list `dotnet meta gen` / `verify --codegen` run); on Python,
+a `module:symbol` entry in a target's `generators` in `metaobjects.config.yaml`. (Python's
`--provider module:symbol` registers **metamodel vocabulary**, not a generator.)
-Declarative: `--template-spec ` — plus `--templates ` on Python or
-`--template-root ` on C# — and your entries are appended to your `--generators`
-selection. Worked examples with the full JSON: `docs/ports/python.md` and
-`docs/ports/csharp.md`.
-
-**The spec is auto-discovered, and that is load-bearing.** With no `--template-spec`, both
-ports read `/template-spec.json` — projectRoot being the metadata dir's parent.
-Keep it there: `verify --codegen` accepts no `--template-spec` flag, so the conventional path
-is how the drift gate learns your template generators exist. Put the spec somewhere else and
-reach it only by flag, and `verify` regenerates without it and reports its output as stale.
+Declarative: a JSON template-spec. On Python it is declared on a target
+(`templateSpec: `, refs resolved under `--templates `) and runs into that target's
+`outDir`. On C# it is an entry in `codegen/Generators.cs`:
+`new MetaObjects.Codegen.TemplateCodegen.TemplateSpecGenerator { Spec = "template-spec.json",
+TemplateRoot = "templates" }`. Neither port reads a spec it was not given there: a leftover
+project-root `template-spec.json` with nothing declaring a spec fails the run. Worked examples
+with the full JSON: `docs/ports/python.md` and `docs/ports/csharp.md`.
So on every port, "I need a shape the built-ins do not emit" is answered by a generator of
your own or a template. Do not conclude the port cannot be customized.
diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/python.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/python.md
index 57368c30a..47beec1af 100644
--- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/python.md
+++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/python.md
@@ -19,8 +19,8 @@ SQLAlchemy Core or asyncpg), or a hand-written repository implementation.
## Finding generated files
-Generated files land under the `--out` directory passed to `metaobjects gen` and
-carry an `@generated` guard comment:
+Generated files land under each target's `outDir` in `metaobjects.config.yaml` (the
+generator list `metaobjects gen` runs) and carry an `@generated` guard comment:
```python
# @generated by metaobjects
@@ -29,20 +29,21 @@ carry an `@generated` guard comment:
The runner overwrites only files bearing this header. Common layout: one Pydantic
model file per entity (e.g. `generated/author.py`) and one FastAPI `APIRouter` file
per writable entity (e.g. `generated/author_router.py`). There is no enforced
-directory structure beyond `--out`.
+directory structure beyond the target's `outDir`.
---
## Run codegen + verify
```bash
-metaobjects gen ./metaobjects --out ./generated
-metaobjects gen ./metaobjects --out ./generated --generators entity,routes,filter-allowlist
-metaobjects verify --codegen # regenerate to temp, diff vs committed --out tree (bare default)
+metaobjects gen # run every target's generators (metaobjects.config.yaml)
+metaobjects gen --generators entity # narrow to declared entries
+metaobjects verify --codegen # regenerate to temp, diff vs each committed outDir (bare default)
metaobjects verify --templates # {{field}} refs vs payload VO
```
-`--generators` accepts stable generator names: `entity`, `routes`, `filter-allowlist`,
+With no `metaobjects.config.yaml`, `gen` exits 2 and generates nothing. A target's
+`generators` takes stable generator names: `entity`, `routes`, `filter-allowlist`,
`names`, `output-parser`, `extractor`, `output-prompt`, `render-helper`, `trace-helper`,
`template`. (There is no `payload` generator since ADR-0056: a template's payload and
response types are the value objects' own `entity` models.) Schema migration + live-DB drift run through the **Node `meta` CLI**
@@ -61,7 +62,7 @@ regardless of server language — see the migration reference.
| `def get_all_` / `def create_` / `def update_` / `def delete_` in non-generated files | hand-rolled CRUD — compare to the generated router |
| `# keep in sync with` / `# mirrors the` | second-source-of-truth comment — always a finding |
| `try: ... except KeyError` / `?? ''` around format strings in prompt code | silent-degradation hack around a prompt payload — flag it |
-| a table/column string in the repository implementation (SQLAlchemy Core, asyncpg/psycopg SQL) | second spelling of a declared physical name — reference `_names.py` (`AUTHOR_SOURCE_PRIMARY_TABLE` / `AUTHOR__COLUMN`, emitted when `names` is named in `--generators`); no typed handle exists on this port, so that is never the reason to waive it |
+| a table/column string in the repository implementation (SQLAlchemy Core, asyncpg/psycopg SQL) | second spelling of a declared physical name — reference `