Repository navigation
feat(cli): one generator list and one gen command for C# and Python - #440
Merged
Merged
Conversation
…are removed ADR-0034 Amendment 9 for C# and Python. Every generator, the api reference included, is an entry in the port's generator list, and the gen command runs exactly that list. C#: the list is codegen/Generators.cs, run by a two-line codegen/Program.cs. New `dotnet meta init` writes the codegen project; eject writes the three files when missing. `dotnet meta gen` and `verify --codegen` run the list and refuse a project without one. `--generators` only narrows declared entries. ApiDocsGenerator takes OutDir, ApiSubDir, ProjectName and ModelBaseUrl as init properties; IGenerator.OutDir lets an entry write outside --out, and the drift gate compares each directory. `dotnet meta docs` prints the removed-command line; 1.0 docs flags on gen fail before writing. Python: metaobjects.config.yaml is the list. gen and verify --codegen are config-only; the positional --out form and --template-spec are not read (targets gain templateSpec). api-docs is a target entry with a top-level apiDocs block. `metaobjects docs` and a 1.0 docs block fail with the fixed lines. Moved-module aliases warn with FutureWarning naming 2.0, and the owned-router eject test compares lines in order. Docs, skills, fixture copies, the showcase script and the CHANGELOG follow.
…st, never discovered One list means one declaration (ADR-0034 Amendment 9). C#: the owned runner no longer reads --template-spec or discovers a project-root template-spec.json; a spec runs as a TemplateSpecGenerator entry in codegen/Generators.cs (Spec, TemplateRoot, OutDir). A run that finds the 1.0 configuration with no such entry fails before writing, with a fixed line naming the migration section. Python already required a target's templateSpec; a leftover template-spec.json that no target declares now fails gen and verify --codegen the same way instead of being skipped. The C# migration section gains the template-spec before and after, and the port, concepts, own-your-codegen, cli and skill pages follow.
…ments after rebase
…; requirement-tests OutDir
…OutDir and template root
… token, changelog
… garbled sentence; regen agent-context fixtures
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Changed
codegen/Generators.cs, andcodegen/Program.csis a two-line entry that callsCodegenCli.Run(args, Generators.All).dotnet meta genandverify --codegenrun that list and nothing else.--generatorsonly narrows the declared list and refuses names it does not declare. Python reads the same model throughmetaobjects gen,verify, and a top-levelapiDocsblock inmetaobjects.config.yaml.dotnet meta docsandmetaobjects docs(and the docs marker and Pythoncodegen/docs_run.py) are gone. Runningdocsnow exits with a message that namesgenand the migration section. A 1.0-shapecodegen/Program.csthat does not runGenerators.All, or a 1.0--project/--model-base-urldocs flag, makesgenfail with a clear error and write nothing.docs/features/migrations/docs-are-generators.md, with matching updates to the CLI, codegen, ports, and recipe docs, the agent-context skills, and their conformance fixtures. Recorded the 1.1.0 release notes inCHANGELOG.md.Validation
Risk Assessment
Testing
Full regression passes: Python suite 3141 passed, C# Cli 291 passed, Codegen 841 passed with 1 skipped. Live Python and C# scenarios for generator-list behavior, narrowing identity, refusals, fresh-checkout verify, and drift detection all pass against the built tools in disposable projects under /tmp. Overall result is green.
Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 4 issues found → auto-fixed (5) ✅
server/csharp/MetaObjects.Cli/InitCommand.cs:109- Intent says: "gen fails clearly on a 1.0-shape config and never silently generates nothing". A 1.0 C# project that had ejected a generator already hascodegen/Program.cswith an inline list (IReadOnlyList<IGenerator> generators = [...]; return CodegenCli.Run(args, generators);) and aCodegen.csprojpinned to 1.0.x. The failure sequence is: (1)dotnet meta genrefuses with 'no codegen/Generators.cs ...dotnet meta initwrites it'. (2) The user runsdotnet meta init. It writes an emptyGenerators.cs, prints 'kept codegen/Program.cs (already yours)' and claims Generators.cs 'is the generator listdotnet meta genruns'. (3)RequireGeneratorList(Program.cs:141) checks only that the two files exist, so gen hands off to the old Program.cs. The old Program.cs never referencesGenerators.All. Every entry the user then adds to Generators.cs, such as theApiDocsGeneratorthe migration tells them to add, is ignored without a word, and gen exits 0. With the old pin left in place, the 1.0CodegenCli.Runruns instead, including its additive--generatorsand itsdocscommand.initdoes not warn about an older pin, thoughejectdoes (EjectCommand.cs:246). The same false claim appears in eject's 'codegen/Generators.cs lists the generator(s) you just ejected ...dotnet meta gen... run that list' (EjectCommand.cs:234) when only Generators.cs was missing. The migration doc says to replace Program.cs by hand, but no tool enforces it. Possible remedies: have init and eject reuseOlderPinWarningfor an existing csproj, and have init, eject orRequireGeneratorListreport or refuse a Program.cs that does not runGenerators.All. Each remedy adds new detection or refusal behavior, so it needs a ruling.server/python/src/metaobjects/cli.py:1758-verify --codegen --generators <subset>now narrows the regen (selection passed at cli.py:1731), but the diff still covers every committed outDir in full. Example: configapi: {outDir: gen, generators: [entity, routes]}anddocs: {outDir: docs, generators: [api-docs]}. Runmetaobjects gen, thenmetaobjects verify --codegen --generators api-docs. The api target regenerates nothing (cli.py:1380 skips it), so its temp slot is empty. Every committed entity and router file is in the manifest, sois_oursreturns true and each one is reported asextra:. The gate fails with exit 1 on output that is in sync.--entitiesnarrowing (_narrow_entitieson the closure) has the same effect for other entities' files. C# has the same defect:CodegenDrift.Compute(server/csharp/MetaObjects.Codegen/CodegenDrift.cs:84) always adds the run's--outroot and diffs it whole.dotnet meta verify --codegen --out Generated --generators api-docs, with api-docs on its ownOutDir, therefore reports every committed code file as '- committed but a fresh regen would not emit it'. Suggested fix: when a selection or entity filter narrows the run, skip directories no selected entry wrote to, and suppress the extra/'-' verdict, because those files cannot be attributed to the narrowed set. Related: a C# entryOutDirnested inside--out(or the reverse) makes the outer root's whole-tree diff report the inner entry's files as '-' drift.docs/ports/csharp.md:461- OnlyApiDocsGeneratoroverridesIGenerator.OutDir, so a C# list that includesRequirementTestsGeneratorcannot give its xUnit output a directory of its own. The docs here and in agent-context/skills/metaobjects-codegen/references/csharp.md:146 tell users to run two narrowed commands:gen --out <tests> --generators requirement-tests, and the code run 'names its own entries the same way'. That is the 1.0 selection pattern the intent replaces ('one command runs the list'). A plaindotnet meta gen ./metaobjects --out Generatedruns the whole list and writes the xUnit tests into the app's--out, which does not reference xunit, so that build breaks. A plainverify --codegen --out Generatedregenerates them too, so after the two-run workflow it reports the missing test files as drift. Remedy options: giveRequirementTestsGenerator(or the reference generators generally) anOutDirinit property, as api-docs has, or accept and document the limitation. Either is a product decision, so it needs a ruling.server/python/src/metaobjects/cli.py:2496- Several verify help strings still describe the 1.0 flag/config split.verify --generatorssays it 'must MATCH thegenthat produced --out ... with none named there is nothing to check'; it now narrows the declared list, and with none named the whole list is checked.verify --configsays 'Config mode runs when no <metadata_dir> is given'. Theverify metadata_dirhelp says 'Omit to use a metaobjects.config.yaml (--codegen config mode, Python: declarative codegen config with a targets registry (parity with metaobjects.config.ts) #267)'.verify --outsays 'committed output directory to diff against', but_verify_codegen_confignow refuses it. On gen,--templatesstill says it is 'used by --template-spec', but that flag is refused. A related gap:verify --codegen <metadata_dir>given without--outsilently ignores the positional for the codegen gate, which loads the config'smetadata, whilegenrefuses the same positional.🔧 Fix applied.
10 issues (5 warnings, 5 infos) still open:
server/python/src/metaobjects/cli.py:1758-verify --codegen --generators <subset>now narrows the regen (selection passed at cli.py:1731), but the diff still covers every committed outDir in full. Example: configapi: {outDir: gen, generators: [entity, routes]}anddocs: {outDir: docs, generators: [api-docs]}. Runmetaobjects gen, thenmetaobjects verify --codegen --generators api-docs. The api target regenerates nothing (cli.py:1380 skips it), so its temp slot is empty. Every committed entity and router file is in the manifest, sois_oursreturns true and each one is reported asextra:. The gate fails with exit 1 on output that is in sync.--entitiesnarrowing (_narrow_entitieson the closure) has the same effect for other entities' files. C# has the same defect:CodegenDrift.Compute(server/csharp/MetaObjects.Codegen/CodegenDrift.cs:84) always adds the run's--outroot and diffs it whole.dotnet meta verify --codegen --out Generated --generators api-docs, with api-docs on its ownOutDir, therefore reports every committed code file as '- committed but a fresh regen would not emit it'. Suggested fix: when a selection or entity filter narrows the run, skip directories no selected entry wrote to, and suppress the extra/'-' verdict, because those files cannot be attributed to the narrowed set. Related: a C# entryOutDirnested inside--out(or the reverse) makes the outer root's whole-tree diff report the inner entry's files as '-' drift.server/python/src/metaobjects/cli.py:2496- Several verify help strings still describe the 1.0 flag/config split.verify --generatorssays it 'must MATCH thegenthat produced --out ... with none named there is nothing to check'; it now narrows the declared list, and with none named the whole list is checked.verify --configsays 'Config mode runs when no <metadata_dir> is given'. Theverify metadata_dirhelp says 'Omit to use a metaobjects.config.yaml (--codegen config mode, Python: declarative codegen config with a targets registry (parity with metaobjects.config.ts) #267)'.verify --outsays 'committed output directory to diff against', but_verify_codegen_confignow refuses it. On gen,--templatesstill says it is 'used by --template-spec', but that flag is refused. A related gap:verify --codegen <metadata_dir>given without--outsilently ignores the positional for the codegen gate, which loads the config'smetadata, whilegenrefuses the same positional.server/python/src/metaobjects/cli.py:1756- Introduced in this run's fix round.if narrowed and not expected: continuetreats a narrowed run that produced nothing as an outDir no selected entry targets. Example:metaobjects verify --codegen --entities Prgram(a typo) on a project whose gen/Program.py was hand-edited. The narrowed regen emits nothing, because run_gen only warns 'No entities to generate'. The outDir is then skipped and verify exits 0 with drift present. The cause is the same when --entities names only a sourceless object. Fix: skip an outDir only when no selected target/entry writes into it (decide from the closure and selection, not from the emitted file set). Also refuse --entities names that match no object. Add a regression test: an --entities name that matches nothing, plus a hand-edited file, must not exit 0.server/csharp/MetaObjects.Codegen/CodegenDrift.cs:91- A fix-round sibling of the new RequirementTestsGenerator.OutDir. CodegenDrift.Compute errors when any root directory is missing (CodegenDrift.cs:91-104). CodegenRunner.Run creates only config.OutDir (CodegenRunner.cs:68). An entry's OutDir is created only when a file is written (CodegenRunner.cs:220), and RequirementTestsGenerator returns [] when the model declares no requirement (RequirementTestsGenerator.cs:106). With the documentedRequirementTestsGenerator { OutDir = "tests/Acme.Shop.Tests/Generated" }and no requirement declared yet, gen exits 0 and writes nothing. Everyverify --codegenthen exits 2 with 'no committed generated output ... Run dotnet meta gen first', and running gen does not fix it. A fresh clone hits the same error, because git does not track an empty directory. Sibling: an empty declared list. gen returns early at CodegenCli.cs:629 before --out is created, so init, then gen (exit 0), then verify --codegen (exit 2) loops. Fix in CodegenDrift.Compute: treat a missing root as an empty committed tree, or treat an empty suite as clean.server/python/src/metaobjects/cli.py:1349- Reported by a sub-reviewer and not independently re-traced. A target that omits thegeneratorskey now exits 0 with 'declares no generators; nothing written' (cli.py:1381). Before this change, an omitted key raised NoGeneratorsSelectedError and exited 1, and the schema callsgeneratorsrequired. Example: the 1.0-shape configtargets: {py: {outDir: gen}}makesmetaobjects genexit 0 and write nothing. The intent requires that gen 'fails clearly on a 1.0-shape config and never silently generates nothing'. An explicitgenerators: []is a separate case and is deliberate (test_cli.py:206). Fix: refuse a target with nogeneratorskey, exit 2 with the migration line, and add a test.server/csharp/MetaObjects.Codegen/CodegenCli.cs:613- The run's --template-root reaches only SelectDeclared (CodegenCli.cs:614). A declaredTemplateSpecGenerator { Spec = ... }with no TemplateRoot falls back to DefaultTemplateRoot() (TemplateSpecGenerator.cs:30). With both prompts/ and templates/ present,dotnet meta gen --template-root templatesrenders the prompts/ bodies and exits 0, and verify --codegen agrees. Fix: pass the run's templateRoot to a spec entry that sets none, or refuse the flag when the entry sets its own.server/python/src/metaobjects/cli.py:1355- Reported by a sub-reviewer and not re-traced.zip(gens, t.generators)assumes one resolved generator per config token, but the resolver splits on commas. Sogenerators: ["entity, routes"]resolves two generators against one token, and zip silently dropsroutes. gen and verify then both exit 0 without router files. Fix: refuse a comma inside a list entry, or resolve each token separately.server/python/src/metaobjects/cli.py:1532- This is fix-round narrowed verify. The shared enums.py is built from the matched entities only (entity_model.py:829-840). Soverify --codegen --entities Programreportsdrifted: enums.pyon in-sync output when another entity consumes a shared enum. Fix: under --entities, compare only the per-entity paths emitted for the named entities.server/python/src/metaobjects/cli.py:1243---generators ""(for example an unset shell variable) makes _selection_from_args return None. The run then covers the whole declared list instead of refusing an empty narrowing. Fix: refuse an empty or blank --generators.agent-context/skills/metaobjects-audit/references/csharp.md:27- The audit reference still names ApiDocsGenerator as the only entry with its own OutDir. RequirementTestsGenerator now takes one too. Name both.🔧 Fix applied.
4 issues (2 warnings, 2 infos) still open:
server/csharp/MetaObjects.Codegen/CodegenDrift.cs:91- A fix-round sibling of the new RequirementTestsGenerator.OutDir. CodegenDrift.Compute errors when any root directory is missing (CodegenDrift.cs:91-104). CodegenRunner.Run creates only config.OutDir (CodegenRunner.cs:68). An entry's OutDir is created only when a file is written (CodegenRunner.cs:220), and RequirementTestsGenerator returns [] when the model declares no requirement (RequirementTestsGenerator.cs:106). With the documentedRequirementTestsGenerator { OutDir = "tests/Acme.Shop.Tests/Generated" }and no requirement declared yet, gen exits 0 and writes nothing. Everyverify --codegenthen exits 2 with 'no committed generated output ... Run dotnet meta gen first', and running gen does not fix it. A fresh clone hits the same error, because git does not track an empty directory. Sibling: an empty declared list. gen returns early at CodegenCli.cs:629 before --out is created, so init, then gen (exit 0), then verify --codegen (exit 2) loops. Fix in CodegenDrift.Compute: treat a missing root as an empty committed tree, or treat an empty suite as clean.docs/features/migrations/docs-are-generators.md:457- The Python migration section gives the wrong module path for an owned api-docs copy. Line 457 sayscodegen.generators.api_docs_generator:api_docs_generator.metaobjects eject api-docswrites the copy tocodegen/generators/api_docs.py:owned_stemturns the stable nameapi-docsintoapi_docs(eject.py:67-78). Its own wiring line is thereforecodegen.generators.api_docs:api_docs_generator. That form is what docs/features/own-your-codegen.md:961 and the tests use (test_docs_generators.py:23, test_one_generator_list.py:137). An adopter or agent who follows the migration section, which the intent says must be agent-followable, gets an import failure frommetaobjects gen. Fix: change the comment tocodegen.generators.api_docs:api_docs_generator. No other changed doc uses the wrong form.CHANGELOG.md:1142- The 1.1.0 CHANGELOG Python entry does not mention four new refusals added in this branch's fix rounds. These change exit codes in scripts and CI:generatorskey exits 2 with the migration line (cli.py:1231-1239). Before, it either exited 1 (NoGeneratorsSelectedError) or wrote nothing.--entitiesname that matches no object exits 2 (cli.py:1255-1276). Before, it warned 'No entities to generate' and exited 0.--generatorsexits 2.generatorsentry that contains a comma is refused.The migration doc covers only the first. Fix: add one line naming each refusal to the Python bullet near CHANGELOG.md:1142. The C# blank
--generatorsrefusal (CodegenCli.cs:342-346) is also unlisted.server/csharp/MetaObjects.Codegen/CodegenDrift.cs:88- This case was left behind by round 2's fix for cs-entry-outdir-never-created. That fix treats a missing entryOutDiras an empty committed tree. The run's own--outis still a hard error when it is missing (CodegenDrift.cs:88, :102), even when no entry writes there.Reproduction:
codegen/Generators.cslists only entries with their ownOutDir, for exampleApiDocsGenerator { OutDir = "docs/generated" }plusRequirementTestsGenerator { OutDir = "tests/X/Generated" }.dotnet meta gen ./metaobjects --out ./Generatedcreates an empty./Generated(CodegenRunner createsconfig.OutDirunconditionally)../Generated.dotnet meta verify --codegenexits 2 with 'no committed generated output at "./Generated" ... Run dotnet meta gen first', and running gen does not fix it.The same happens when the entries that write under
--outemit nothing. Fix at the line 88 condition: add the--outroot only when an entry without its ownOutDiris in the suite, for both narrowed and full runs. Otherwise treat a missing--outas an empty tree, as entry roots now are.🔧 Fix applied.
1 warning still open:
docs/ports/python.md:483- The worked template-spec example in the Python port page declares a target with onlyoutDirandtemplateSpecand nogeneratorskey:This branch's fix round made
metaobjects genandverify --codegenrefuse any target wheret.generators is None(cli.py:1231-1239), with exit 2 and the line "target(s) 'docs' declare nogeneratorskey". An adopter or agent who copies the documented example therefore gets exit 2 and no output. The passing test for this path (test_one_generator_list.pytest_a_template_spec_on_a_target_writes_into_its_out_dir_and_verifies_clean) writesgenerators: []besidetemplateSpec.Fix: add
generators: []to the example (for examplegenerators: [] # the spec's generators run after these). No other changed doc has a templateSpec-only target: cli.md:368-371 listsgenerators: [api-docs], and the agent-context references give no YAML example.🔧 Fix applied.
2 issues (1 warning, 1 info) still open:
server/csharp/MetaObjects.Codegen/CodegenCli.cs:656- The C#--generatorsnarrowing (ADR-0034 Amendment 9 rule 5, new in this branch) computes cross-generator facts from the narrowed suite. It should compute them from the declared list. As a result, a narrowed run changes what the selected entries emit.RunGensetsIncludeNames = generators.Any(g => g.Name == "names")(CodegenCli.cs:227), andRunGenArgspasses it the narrowedsuite(:656).EntityGenerator,DbContextGenerator,RoutesGeneratorandCallableGeneratorall switch onctx.Config.IncludeNames, between[Table(OrderNames.SourcePrimaryTable)]and the literal[Table("orders")].Concrete sequence:
codegen/Generators.csdeclares[EntityGenerator, NamesGenerator, DbContextGenerator].dotnet meta gen ./metaobjects --out ./Generated. This writes entity files that reference the Names constants.dotnet meta gen ./metaobjects --out ./Generated --generators entity. The skills and port docs advertise this exact form: "narrow to declared entries". It silently rewrites every<Entity>.g.cswith literal names. The files are pristine per the manifest, so they are overwritten, and the run exits 0.dotnet meta verify --codegennow reports drift on every entity file.In the other direction, after a full gen,
dotnet meta verify --codegen --out ./Generated --generators entityregenerates withIncludeNames = false(:755) and reports false~drift on every entity file. The added testVerify_narrowed_to_an_entry_with_its_own_directory_does_not_convict_the_rest_of_the_list(OneGeneratorListTests.cs:423) does not catch this, because its list declares nonames.Sibling sites with the same invariant (derive from the declared list, not the narrowing):
UnsatisfiedRequiresadvisory runs onRegistryNames(suite, ...).--generators output-parsertherefore warns "Add entity to codegen/Generators.cs" even when the list already declaresentity.RunVerifyArgsbuildsIncludeNamesfrom the narrowedsuite.The Python port already avoids this: its requires warning runs on each target's full list, and its narrowed verify takes content from a declared-list regen.
Fix at the earliest shared boundary,
RunGenArgs/RunVerifyArgs, which holddeclared:includeNamesfromdeclared, and pass it intoRunGen(an optional parameter) and into the verifyGenConfig.RegistryNames(declared, ...).Regression test: declare
[entity, names]and run a full gen. Thengen --generators entitymust leave the entity files byte-identical, andverify --codegen --generators entitymust exit 0.server/csharp/MetaObjects.Codegen/CodegenCli.cs:643- InRunGenArgs, the message "the --generators selection matched no declared entry; nothing written" can never print.SelectDeclaredreturns an error for any selection that names an undeclared entry (:357-364), and for an empty selection (:342-346). So whensuite.Count == 0the selection is always null and the list is empty. Simplify the branch to the single "declares no generators; nothing written" message.🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainscd server/csharp && dotnet test MetaObjects.Cli.Tests (291 passed, 0 failed)cd server/csharp && dotnet test MetaObjects.Codegen.Tests (841 passed, 1 skipped, 0 failed)cd server/python && uv run pytest -q tests (3141 passed, 4 warnings, exit 0)Live Python scenarios in /tmp/mo-live-py: 1.0 docs-block config refused exit 2; generators [] writes nothing; generators [entity] writes only its files, verify in sync, hand edit detected; undeclared, blank, and unknown-entity narrowing refused; narrowed gen identical to full runLive C# scenarios in /tmp/mo-live-cs via dotnet-meta built from the worktree with a local ProjectReference: init scaffold; narrowed --generators entity byte-identical to full run; blank and undeclared refusals exit 2; dotnet meta docs refused exit 2; OutDir-only list verifies clean on fresh checkout; hand-edit drift exit 1✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.
Validation runs and size
ci_mode: local).fixtures/agent-context-conformanceexpected files): +137 / -137.