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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,62 @@ generator catalog and the Atlas evaluation are 1.2 work.
`GeneratorRegistry.GeneratorInfo.ejectWith()`, the classes eject copies with a
generator.

- **C#: an ejected generator owns the emitters it writes through, and `api-docs` can be
ejected.** `dotnet meta eject` used to copy one file per generator, so an owned `entity`
still called four packaged writers, and an owned `output-parser`, `extractor` or
`output-prompt` still called the prompt tier's field mapping and format-spec writer. A
defect in any of them waited for a release. Eject now hands
over the writers with the generator, into `codegen/generators/`, in the same
`Codegen.Generators` namespace: `Fr019SharedEnum`, `XmlDocBuilder`, `M2MNavigation` and
`ReverseFinders` with `entity`, `db-context` and `routes` as each calls them;
`ExtractDelegateEmitter`, `Fr010FieldMapping` and `OutputFormatSpecEmitter` with
`output-parser`, `extractor` and `output-prompt`. A file two generators share is copied once
and never overwritten (`--force` replaces it, as it replaces the generator). What stays in
the package is the engine: the generator base, `CSharpNaming`, `ValueObjectNames` and
`PackageBindingResolver` (the naming and type-mapping tables), and the questions asked of
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
`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
`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**).
- **A copy you ejected before this release** keeps compiling and running: it finds the
packaged emitters as it always did. `dotnet meta gen --list` now marks it as missing the
ones it was not handed (`Fr019SharedEnum.cs missing`). To own them, first raise the
`MetaObjects.Codegen` version in `codegen/Codegen.csproj` to 1.1.0 or later (the 1.1.0
emitter files are written against the 1.1.0 package, and some call API 1.0.x does not
have), then eject the same name
into a scratch directory (`dotnet meta eject entity --root <scratch>`) and copy the emitter
files from its `codegen/generators/` into yours; your copy and your edits are untouched,
and it resolves the new files by namespace.
- **Eject warns about an older package pin.** Eject writes the sources of the tool you run
and never edits `codegen/Codegen.csproj`. When that file pins a `MetaObjects.Codegen` older
than the tool, `dotnet meta eject` now 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 raised. A 1.0.x project needs this before `dotnet meta eject api-docs`, and before
re-ejecting `names`, `render-helper` or a prompt-tier generator.
- **A gate for the rule.** For every ejectable generator the registry lists, a test ejects it
alone, compiles it against the public API, fails on any reference to a packaged emitter,
generator or docs writer outside a written engine surface, fails on a handed-over file
nothing calls, and compares its output with the packaged generator's over the shared
corpora. A registered generator with no eject source and no written exemption fails it, as
does a generator class the package ships that no registry entry builds. A second test
compiles the package without the handed-over emitters, so nothing that stays packaged
calls one.
The cross-port manifest lists `api-docs` for `csharp`.

- **The stock `entity`, `queries`, `routes` and `barrel` generators are importable again
(TypeScript).** `entityFile`, `queriesFile`, `routesFile` and `barrel` are exported from
`@metaobjectsdev/codegen-ts/generators` once more, beside `namesFile`, `routesFileHono`
Expand Down Expand Up @@ -744,6 +800,12 @@ until you regenerate.
One rule is relaxed: an authored `validator.length @min: 0` on a `@required` string now
admits the empty string, as the generated schema already did.

### Deprecated

- **C#: `Fr010FieldMapping.CSharpStringLiteral(string)` is `[Obsolete]`.** It forwards to
`CSharpNaming.CSharpStringLiteral(string)`, which every generator now calls (so an ejected
`names` needs no emitter). Same signature and result; switch the call when you next touch it.

### Removed

- **The standalone `mermaid-er` generator (TypeScript).** `mermaidErDiagram()`, its
Expand Down
93 changes: 90 additions & 3 deletions docs/features/own-your-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -1014,12 +1014,18 @@ ejectable generator's `.cs` file ships embedded in `MetaObjects.Codegen`, and a
byte-identical to the file the package compiles. Eject writes it to
`codegen/generators/<Name>Generator.cs`, renaming its namespace to `Codegen.Generators` and
adding the `using` lines that rename needs; nothing else changes, except in `routes` (see
[below](#ejecting-routes-hands-over-the-helper-runtime-too)).
[below](#ejecting-routes-hands-over-the-helper-runtime-too)). The emitters the generator
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. Once
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.
Expand All @@ -1031,14 +1037,95 @@ them. So ejecting `names` from a nine-generator selection changes one generator'
not the other eight. 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`. The `template` primitive is not ejectable; it has no emit logic of its own.
own`, and names an emitter that was edited or removed after eject
(`Fr019SharedEnum.cs DIFFERS: 0 behind, 1 of your own`). The `template` primitive is not
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
types (`IRequirementTestRenderer`, `IRequirementTestFilter`), the identity function and the
digest stay in the package.

### Ejecting a generator hands over the emitters it writes through

A generator that calls an emitter writes part of its output through it. If eject left the
emitter in the package, the copy would own the orchestration and the package would still own
the text, and a defect in that text would wait for a release. So eject copies the emitters
too, next to the generator in `codegen/generators/`, in the same `Codegen.Generators`
namespace:

| Eject | Also writes |
|---|---|
| `entity` | `Fr019SharedEnum.cs` (shared and provided enums), `M2MNavigation.cs` (the many-to-many navigation), `ReverseFinders.cs` (the reverse FK finders), `XmlDocBuilder.cs` (doc comments) |
| `db-context` | `Fr019SharedEnum.cs`, `M2MNavigation.cs`, `XmlDocBuilder.cs` |
| `routes` | `M2MNavigation.cs` |
| `output-parser`, `extractor` | `ExtractDelegateEmitter.cs`, `Fr010FieldMapping.cs`; `extractor` also `Fr019SharedEnum.cs` |
| `output-prompt` | `OutputFormatSpecEmitter.cs`, `Fr010FieldMapping.cs` |
| `api-docs` | the page model, builder, renderer and path math (below), `Fr010FieldMapping.cs`, `M2MNavigation.cs` |

The copies are the packaged source with the same namespace edit as the generator and no
other change. A file two generators share is written once: a later eject finds it, says
`[kept — already yours]` and leaves your edits alone, and `--force` replaces it with the
reference as it replaces the generator. The copies see one another as members of their own
namespace, and a member of the namespace you are in wins over a name a `using` brings in, so
an owned generator reaches its owned emitters and never the packaged ones of the same name.

**What stays in the package is the engine**, public and unchanged: the generator contract
(`IGenerator`, `GenContext`, `GenConfig`, `EmittedFile`), the naming and type-mapping tables
(`CSharpNaming`, `ValueObjectNames`, `PackageBindingResolver`), and the questions a generator
asks of the model (`FindInbound`, `TphPlan`, `InstanceArtifacts`, `ReportRows`). Anything that
returns emitted text is handed over; a table or a model question is not. The line is written
down and checked: a test ejects each generator the registry lists into an empty project,
compiles it against the public API and fails on any reference to a packaged emitter, generator
or docs writer that is not on that list. A second test compiles the package without the
handed-over emitters, so nothing that stays packaged calls one.

**A copy you ejected before the emitters were handed over** keeps compiling and running, with
the packaged emitters. To own them, first raise the `MetaObjects.Codegen` version in
`codegen/Codegen.csproj` to 1.1.0 or later: the 1.1.0 emitter files are written against the
1.1.0 package, and some call API that 1.0.x does not have. Then eject the same name
into a scratch directory and copy the emitter files into your project; your copy and your
edits are untouched:

```bash
scratch=$(mktemp -d)
dotnet meta eject entity --root "$scratch"
cp "$scratch"/codegen/generators/{Fr019SharedEnum,M2MNavigation,ReverseFinders,XmlDocBuilder}.cs codegen/generators/
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.

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.

**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
answers through its static `AppliesTo`. That holds in a project that owns one of those
generators too: an owned `routes` whose `AppliesTo` you changed is still documented as the
packaged one behaves. To document your own surface, edit the owned builder
(`CSharpApiModelBuilder.cs`), which is where the question is asked. The builder asks through
`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.

### Ejecting routes hands over the helper runtime too

Generated routes call a handful of helpers: the filter parser, the EF Core filter
Expand Down
2 changes: 1 addition & 1 deletion docs/ports/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ for a worked example.

## Generate

**Own a generator.** `dotnet meta eject <name>...` copies a reference generator into `codegen/generators/` and scaffolds an owned `codegen/` console project that `dotnet meta gen` then runs. See [Own your codegen → C#](../features/own-your-codegen.md#c-dotnet-meta-eject).
**Own a generator.** `dotnet meta eject <name>...` 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).

```bash
# Generate EF Core entities + AppDbContext + CRUD minimal-API routes
Expand Down
4 changes: 2 additions & 2 deletions fixtures/generator-registry-conformance/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@
"concept": "Per-entity/template SDK API reference (generated-code API; human + agent forms).",
"tier": "native",
"layer": "docs",
"note": "Run by `meta docs` as its api surface (ADR-0025), not by `meta gen`: a `meta gen` config that lists it is warned about and skipped.",
"ports": ["typescript"]
"note": "Run by `meta docs` as its api surface (ADR-0025), not by `meta gen`: a `meta gen` config that lists it is warned about and skipped. On csharp it is run by `dotnet meta docs`, and `dotnet meta gen` skips it, warning only when `--generators` names it.",
"ports": ["typescript", "csharp"]
},
"trace-helper": {
"concept": "Per-entity typed record<Entity>/call<Entity> trace helpers (extract + buildLlmCallRow + persist; LlmCallBase-derived entities only).",
Expand Down
Loading
Loading