feat(python): declarative codegen config with a targets registry - #269
Merged
Conversation
…try) Fable cross-port config investigation: ship metaobjects.config.yaml (declarative, Python-only) with a targets registry; schema keys identical to TS (outDir/ generators/entities); providers resolved config-relative (kills PYTHONPATH); no-arg gen/verify run all targets; flags stay back-compat. Cross-port doctrine: per-port file surface, one locked shared schema (ADR-0021 D3 / FR-025 / template-spec) — NOT a single physical cross-port YAML. Additive; no coordinated release. #265 interaction noted (config-declared extending-providers need #265). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: <session-url>
Bite-sized TDD plan (4 tasks) for the metaobjects.config.yaml targets registry: config loader + JSON Schema, gen config mode (all-targets, --target, cross-target dup guard, config-relative providers), verify --codegen config mode (per-target regen+diff), docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
… guard, config-relative providers Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…regate exit Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…-registry KNOWN_GAP Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…cross-target dup guard Final-review fixes: load_project_config now raises ConfigError (not a raw UnicodeDecodeError/OSError traceback) on an unreadable/non-UTF-8 config; the gen cross-target duplicate-output-path guard exempts the auto-emitted package __init__.py so two targets may share an outDir with disjoint entities. Adds a regression test for the verify --templates no-metadata_dir guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…Dir, not per target
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.
Intent
Ship GitHub #267: a declarative metaobjects.config.yaml (targets registry) for the Python codegen CLI. Python-only, additive — NO metamodel/vocabulary change, NO conformance-fixture change, NO coordinated release (npm/PyPI/NuGet/Maven unaffected). Closes #267.
What changed (branch fix/267-python-declarative-config, off main):
Deliberate decisions a diff-only reviewer would not know:
Process already run before this gate: subagent-driven-development — 4 tasks, each a fresh implementer + an independent per-task spec+quality review (all Approved); a final whole-branch review (opus) surfaced 2 Important findings (a non-UTF-8/unreadable-config traceback leak; the cross-target dup-guard init.py false-positive), one fix wave fixed both, one scoped re-review confirmed all addressed with no new breakage. Full Python suite green (1631 passed). Public repo — no private names / home paths in committed files or commit messages.
This is a Python-only change; the relevant validation is the Python suite (cd server/python && uv run pytest). No TS/Java/C#/Kotlin product code changed, so their compile/conformance lanes are not exercised by this branch.
What Changed
metaobjects.config.yamltargets registry for the Python codegen CLI — a newproject_config.pyloader (ProjectConfig/TargetConfig/load_project_config/ConfigError) plus a publishedmetaobjects-config.schema.json, with schema keys mirroring the TSmetaobjects.config.tsvocabulary (metadata / providers / targets..{outDir, generators, entities}).genandverify --codegengain a config mode that triggers only when no positional<metadata_dir>is given:genloads config+metadata once and runs every target into its own outDir (--config/--target, config-relative provider resolution, a cross-target duplicate-output guard exempting the auto-emitted__init__.py), whileverify --codegenregenerates each target to a temp dir and diffs, aggregating the exit — the legacy explicit<metadata_dir> --outflag path stays byte-identical.docs/features/cli.md, a pointer in the Python port doc, andcodegen/KNOWN_GAPS.md(targets-registry gap closed).Closes #267.
Risk Assessment
✅ Low: The prior finding (verify false-positive drift on a gen-supported shared-outDir config) is correctly resolved by making verify symmetric with gen via one whole-selection regen + per-unique-outDir diff; the legacy flag path is byte-identical, the fix is thoroughly tested (bug repro + real-drift/stale-extra preserved + closure widening + dup-guard parity), and no new material issues were introduced.
Testing
Targeted validation of #267's Python declarative config passed completely. All 36 new #267 tests and 44 existing CLI back-compat tests are green (cd server/python && uv run pytest). I then ran the real
metaobjectsconsole script end-to-end against a multi-target config and captured a full CLI transcript (artifact below) demonstrating every config-mode path works as a user would experience it: no-arg gen running all targets with metadata loaded once, --target scoping, per-target verify--codegen diffing (shared outDir verified as one unit labeled core+extra — the per-unique-outDir diffing from the final commit), drift detection, the load-bearing back-compat flag path that ignores the config, the init.py-exempting cross-target dup guard (and its real-duplicate contrast), config-relative provider resolution with no PYTHONPATH, and clean no-traceback errors for malformed/non-UTF-8/missing configs. One out-of-scope observation (not a #267 defect): a target whose entity allowlist omits an M:N junction fails with a raw M2MDerivationError traceback — but I confirmed the legacy flag path (--entities Auth,Person) fails identically, so this is pre-existing entity-filtering + M:N behavior, unrelated to and unchanged by this additive config work. No actionable issues found; the change satisfies the stated intent.Evidence: End-to-end CLI transcript (real metaobjects console script, 9 config-mode paths)
#267 Python declarative config — full demo of themetaobjectsCLI. Key exits: [1] no-arg gen => wrote 8 file(s) across 3 target(s), exit 0; [3] verify --codegen => [models]/[other]/[types] in sync, exit 0; [4] hand-edit => drifted: Node.py, exit 1; [5] back-compat flag pathgen meta --out flagout=> 7 files flat (config ignored), exit 0; [6] shared outDir disjoint entities => [core+extra] in sync, exit 0; [6b] shared outDir duplicate entity => 'duplicate output path across targets', exit 1; [7] provider resolves with env -u PYTHONPATH, exit 0; [8] malformed config => 'target only must declare a non-empty outDir string', exit 1, no traceback; [8b] non-UTF-8 => 'cannot read config file: utf-8 codec...', exit 1, no traceback; [9] no metadata_dir + no config => exit 2 with hint./tmp/no-mistakes-evidence/01KZ5133694KTBSPT4K1J35CTE/demo)Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
server/python/src/metaobjects/cli.py:750-_verify_one_targetregenerates a single target's entities into a temp dir and diffs against that target's committed outDir. Gen explicitly supports and tests two targets sharing an outDir with disjoint entities (test_gen_cross_target_shared_outdir_disjoint_entities_not_flagged→ both Program.py and Week.py land inshared/). But verify's per-target diff computesextra = committed files not in expected(line 750): when targetaregenerates only Program.py, the shared committed dir still holds Week.py (targetb's file), so verify reportsextra: Week.pyas drift for targeta(and symmetrically forb) — exit 1 on output thatmetaobjects genproduced correctly. Concrete path: SHARED_OUTDIR_DISJOINT_ENTITIES config →genexits 0 →verify --codegenexits 1 with spuriousextradrift on both targets. The per-target-diff model cannot distinguish 'another target's legitimate file' from a genuinely stale committed file when outDirs are shared. Resolution is a product decision: either make verify union-aware for shared outDirs, or document shared-outDir as gen-only / have verify warn — hence asking rather than auto-fixing. Primary disjoint-outDir paths (the common case) are correct and unaffected.🔧 Fix: fix(#267): verify --codegen diffs per unique outDir, not per target
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
uv run pytest tests/codegen/test_project_config.py tests/codegen/test_cli_config_gen.py tests/codegen/test_cli_config_verify.py — the three new #267 test files (36 tests: loader unit + gen config mode + verify config mode)uv run pytest tests/codegen/test_cli.py test_cli_verify_subverbs.py test_cli_verify_strict.py test_cli_providers.py test_cli_registry.py test_cli_staleness_nudge.py — existing CLI suite (44 tests) confirming the flag path is untouchedEnd-to-end CLI transcript on the real console script: (1) no-arg gen runs all targets into separate outDirs, metadata loaded once; (2) --target scopes to one target; (3) verify --codegen reports 'in sync' per target; (4) a hand-edited committed file is flagged as drift for its target (exit 1); (5) BACK-COMPAT: explicit <metadata_dir> --out ignores the config entirely (byte-identical flag path); (6) shared outDir + disjoint entities succeeds via the __init__.py exemption; (6b) shared outDir + duplicate entity trips the cross-target dup guard (exit 1); (7) a module:symbol provider resolves config-relative with env -u PYTHONPATH (no PYTHONPATH on the invoking shell); (8) malformed and (8b) non-UTF-8 config yield clean one-line ConfigError with no traceback; (9) no metadata_dir + no config yields exit 2 with a flag-vs-config hintverify --templates with no metadata_dir stays guarded (exit 2, 'not config-driven') — confirms config mode drives --codegen onlyConfirmed the M:N-through-junction resolution error when a target's entity allowlist omits the junction is PRE-EXISTING and identical on the legacy flag path (metaobjects gen meta --out <dir> --entities Auth,Person) — orthogonal to #267, not introduced by this change✅ **Document** - passed
✅ No issues found.
server/python/pyproject.toml- mypy --strict and ruff are configured for server/python but carry pre-existing baseline errors (67 mypy across ~20 files, including cli.py:255 which exists at the base commit; 31 ruff across untouched files), so neither is an enforced clean gate. The new project_config.py module is mypy/ruff-clean and the 2 F841 the change introduced were fixed; cleaning the baseline so these become real gates is an out-of-scope follow-up (touches ~20 unrelated files).✅ **Push** - passed
✅ No issues found.