Skip to content

feat(python): eject owns the emitters its generators write through - #436

Merged
dmealing merged 9 commits into
mainfrom
fm/mo-1-1-0-eject-python
Oct 12, 2026
Merged

dmealing merged 9 commits into
mainfrom
fm/mo-1-1-0-eject-python

Conversation

@dmealing

@dmealing dmealing commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • metaobjects eject now copies the emitter modules a generator writes through (shared-enum, M:N, extract delegate/schema, field mapping, output-format spec) into codegen/runtime/, so an owned generator no longer calls packaged emitters. gen --list marks each copy identical or DIFFERS, and eject reports how a kept runtime copy compares with the packaged one.
  • api-docs is an ejectable docs generator. metaobjects docs runs it, and an owned copy is listed under the new docs.generators config block. gen and verify --codegen skip docs generators they find under a target.
  • The emitters moved into metaobjects.codegen.runtime (api reference as api_docs_* modules). The old import paths remain as one-line shims that emit DeprecationWarning; the API docs entry points (metaobjects.apidocs) keep their exported names.
Validation

Risk Assessment

✅ Low: The change is a verbatim module move plus import rewrites (each moved file diffed against its original), one predicate swap in the api-docs builder that is equivalent for entities, and a bounded new docs-run path; the round 3-5 fixes (R7 hint, R8 two-case warning, R9 ADR text) match their rulings in the current code, and the only new item is a dropped warning channel that no packaged generator uses.

Testing

Ran the plan's Python eject, docs and registry lanes (307 tests), the shared eject release gate (rc passes; final correctly refused on other ports' pending entries, with no python entries named), the TS registry conformance test (45 pass) and the gates lane (passed). Live: drove the real metaobjects CLI in a disposable project to eject generators, import them, run docs and generate with an owned copy, which matched packaged output byte for byte. Full Python suite passed (3107) as an automated run, not live. Overall: everything driven live passes; no regressions found. The TS registry test and full Python suite are untested as live product scenarios.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Eject each newly ejectable Python generator into an empty project and get owned copies plus their runtime helpers ✅ pass live Live metaobjects eject runs for api-docs, output-prompt, render-helper, extractor, names, routes, entity (exit 0, copies written under codegen/generators and codegen/runtime)
Ejected generators import cleanly from the adopter project ✅ pass live python -c importlib on codegen.generators.{api_docs,names,output_prompt,render_helper,routes,entity,extractor} in the temp project
Template generator is refused by eject (exempt per ruling) ✅ pass live metaobjects eject template -> 'unknown or non-ejectable generator(s): template; ejectable: ...'
Owned entity copy produces the same output as the packaged entity ✅ pass live metaobjects gen --generators codegen.generators.entity:entity_model vs --generators entity, diff -r identical
metaobjects docs writes the Python api reference from metadata, deterministically ✅ pass live metaobjects docs --out wrote api/python/README.md, AGENT-API.md, demo/Widget.md; second run identical
Adversarial: final 1.1.0 release refused while other ports still have pending eject entries, and python is not blamed ✅ pass live node scripts/eject-release-gate.mjs 1.1.0 exit 1 listing java 10, kotlin 8, typescript 22; no python entries; rc.1 exit 0
Cross-port manifest: TS generator registry conformance still green with api-docs gaining python ⏸️ untested no Test-suite run only (bun test, 45 pass); not driven against the live product, so not counted as a live pass
Full Python regression suite ⏸️ untested no Automated regression suite (3107 passed) only; not driven against the live product, so not counted as a live pass
Evidence: Full Python suite log (3107 passed)
........................................................................ [  2%]
........................................................................ [  4%]
........................................................................ [  6%]
........................................................................ [  9%]
........................................................................ [ 11%]
........................................................................ [ 13%]
........................................................................ [ 16%]
........................................................................ [ 18%]
........................................................................ [ 20%]
........................................................................ [ 23%]
........................................................................ [ 25%]
........................................................................ [ 27%]
........................................................................ [ 30%]
........................................................................ [ 32%]
........................................................................ [ 34%]
........................................................................ [ 37%]
........................................................................ [ 39%]
........................................................................ [ 41%]
........................................................................ [ 44%]
........................................................................ [ 46%]
........................................................................ [ 48%]
........................................................................ [ 50%]
........................................................................ [ 53%]
........................................................................ [ 55%]
........................................................................ [ 57%]
........................................................................ [ 60%]
........................................................................ [ 62%]
........................................................................ [ 64%]
........................................................................ [ 67%]
........................................................................ [ 69%]
........................................................................ [ 71%]
........................................................................ [ 74%]
........................................................................ [ 76%]
........................................................................ [ 78%]
........................................................................ [ 81%]
........................................................................ [ 83%]
........................................................................ [ 85%]
........................................................................ [ 88%]
........................................................................ [ 90%]
........................................................................ [ 92%]
........................................................................ [ 95%]
........................................................................ [ 97%]
........................................................................ [ 99%]
...........                                                              [100%]
3107 passed in 887.51s (0:14:47)
EXIT=0

Validation runs and size

Runs: 1 counted validation run (five review rounds inside it, each finding applied by the pipeline). An earlier run was cancelled before it finished because the shared build host was overloaded. That cancellation was infrastructure-caused, so it is not counted.

Net hand-written line change (against main, moved lines listed separately):

added removed net
Code (server/python/src) +488 -129 +359
Tests +619 -29 +590
Docs (CHANGELOG, own-your-codegen, python port page, ADR-0034, cli reference) +206 -29 +177

Moved, not hand-written: about 2,090 lines of emitter modules (the shared-enum, M:N, prompt-tier and api-reference emitters) relocated verbatim into metaobjects.codegen.runtime, with import-line edits only. The old import paths stay as ten three-line deprecated aliases.

Limits accepted by the maintainer

  • api-docs learns which objects get a router from shared_predicates, the module an owned routes and filter-allowlist read. A gate rewritten inline inside an owned routes.py, and the template-tier gates behind the packaged find_inbound walk, are not seen by the docs.
  • An owned routes emits its helper runtime into the generated package, so its router modules are compared by line after mapping those import lines; every other generator is compared byte for byte.
  • metaobjects docs <metadata_dir> (the positional form) reads no config and always runs the packaged api-docs; only the config form honours docs.generators.

Open info findings and why they stand

  • The DeprecationWarning on the old import paths is hidden by Python's default filters in a normal gen; the visible signal is gen --list marking the copy DIFFERS: N behind, which the docs state. Ruled: no action.
  • run_docs gives docs generators a no-op warn channel; no docs generator emits a warning today.

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ⚠️ server/python/tests/codegen/test_eject_alone.py:46 - Unrequired component: a second, port-local definition of the Python engine surface and a second import scanner. ENGINE_CODEGEN / ENGINE_APIDOCS (lines 46-62), _outside_the_engine (103-136) and test_an_ejected_generator_imports_only_the_engine (139-154) restate the rule that fixtures/generator-registry-conformance/registry.json (eject.engineSurface.python.engine) already defines and that tests/conformance/test_eject_contract.py::test_depth_an_owned_copy_imports_only_the_engine_surface already asserts for every ejectable generator. test_template_is_the_only_registered_generator_without_an_eject_source (line 322) likewise restates the manifest-driven completeness assertion. The audit the intent accepts (section 7) asks for one guard driven by one shared file; no intent requirement needs a second list. The two lists already disagree: this file allows metaobjects.codegen.model_walk, which the manifest does not list, and the manifest allows metaobjects.codegen.generator_registry, which this file does not. An import of model_walk in a generator would pass here and fail the contract test. Recommended remedy: remove the two allowlists, the scanner and those two tests, and keep what the contract test does not have (the run with packaged emit modules made unimportable, the written == copied check, and the codegen/runtime/ census). docs/features/own-your-codegen.md and the CHANGELOG entry describe this test as rejecting imports outside the surface, so their wording would follow.
  • ⚠️ server/python/tests/codegen/test_eject_alone.py:317 - Source-content-only classification in a new test. test_every_generator_module_is_a_registry_entry_or_a_model_walk decides which modules under codegen/generators/ are deprecated aliases with a raw substring over implementation source: &#34;codegen._moved&#34; not in p.read_text(). A real generator or emitter module that mentions codegen._moved in a comment or docstring is silently dropped from the census, which is the gap the census exists to catch. Classify by behaviour instead: import metaobjects.codegen.generators.&lt;stem&gt; and treat it as an alias when the returned module's __name__ is not that path (the alias makes the old name be the new module, as test_moved_modules.py already proves). Oracle: the import system's result, independent of the stub's text.
  • ℹ️ server/python/src/metaobjects/codegen/project_config.py:50 - DOCS_KEYS is commented "Same contract as above" (kept in step with the shipped schema by test_schema_and_loader_accept_EXACTLY_the_same_keys), but that test (tests/codegen/test_project_config.py:210-217) compares only the top-level, targets and requirementTests key sets. A key added to DOCS_KEYS without the schema's docs.properties (or the reverse), or a dropped additionalProperties: false on the docs block, passes. Add the docs block's key-set and additionalProperties assertions beside the requirementTests ones.
  • ℹ️ docs/features/cli.md:372 - The Python config reference (the annotated metaobjects.config.yaml example, lines 368-386) lists every top-level key except the new docs: block. docs.generators is documented in own-your-codegen.md and docs/ports/python.md, but a reader of the key reference will not find it. Add the docs: { generators: [...] } entry with its one-line meaning.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ server/python/src/metaobjects/codegen/eject.py:163 - Upgrade trap: eject keeps an owned runtime copy that the newly ejected generator cannot import from, and still exits 0. This change adds is_read_only_routed and emits_writable_router to the handed-over shared_predicates module (runtime/shared_predicates.py:99, :112) and makes two generators require them through the OWNED copy: runtime/api_docs_builder.py:64 (from .shared_predicates import emits_writable_router, is_read_only_routed) and generators/router_generator.py:84 (from ..runtime.shared_predicates import emits_router, is_read_only_routed). Concrete sequence: a project that ejected routes, entity, filter-allowlist or extractor from a 1.1.0 build that already handed over shared_predicates (the earlier hand-over recorded in the 1.1.0 CHANGELOG) owns codegen/runtime/shared_predicates.py without those two functions. After upgrading, metaobjects eject api-docs prints runtime codegen/runtime/shared_predicates.py: kept your copy and succeeds; metaobjects docs then fails with cannot import name &#39;emits_writable_router&#39;. metaobjects eject routes --force fails the same way on gen for is_read_only_routed. The new docs recommend exactly this pairing ("Eject them together"), and every test in the change ejects into an empty project, so none covers a pre-existing shared copy. The failure is loud, and gen --list marks the runtime DIFFERS, but eject itself gives no hint. Exposure is limited to projects that ejected from a build with the earlier hand-over. Smallest remedy: when eject keeps an existing runtime copy, report its _compare status against the packaged module in that line (kept your copy — DIFFERS: N behind ...), and add one sentence to the CHANGELOG entry and own-your-codegen.md saying an existing shared_predicates.py needs the two new functions. That changes user-facing eject output, so the remedy needs the maintainer's decision.
  • ℹ️ server/python/src/metaobjects/codegen/_moved.py:21 - The deprecated-path warning is not shown in a normal run. alias() warns with DeprecationWarning and stacklevel=3, which attributes the warning to the importing module (the adopter's codegen.generators.&lt;name&gt;). Python's default filters show DeprecationWarning only when it is attributed to __main__, so metaobjects gen with a pre-move owned copy prints nothing. own-your-codegen.md and the CHANGELOG say the copy keeps running "and a DeprecationWarning names the new path"; that is true only under -W or pytest. The visible signal an adopter gets is gen --list marking the copy DIFFERS: N behind, which the docs also state. No action needed unless the maintainer wants the old paths' removal announced in normal CLI output.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ server/python/src/metaobjects/codegen/eject.py:163 - Upgrade trap: eject keeps an owned runtime copy that the newly ejected generator cannot import from, and still exits 0. This change adds is_read_only_routed and emits_writable_router to the handed-over shared_predicates module (runtime/shared_predicates.py:99, :112) and makes two generators require them through the OWNED copy: runtime/api_docs_builder.py:64 (from .shared_predicates import emits_writable_router, is_read_only_routed) and generators/router_generator.py:84 (from ..runtime.shared_predicates import emits_router, is_read_only_routed). Concrete sequence: a project that ejected routes, entity, filter-allowlist or extractor from a 1.1.0 build that already handed over shared_predicates (the earlier hand-over recorded in the 1.1.0 CHANGELOG) owns codegen/runtime/shared_predicates.py without those two functions. After upgrading, metaobjects eject api-docs prints runtime codegen/runtime/shared_predicates.py: kept your copy and succeeds; metaobjects docs then fails with cannot import name &#39;emits_writable_router&#39;. metaobjects eject routes --force fails the same way on gen for is_read_only_routed. The new docs recommend exactly this pairing ("Eject them together"), and every test in the change ejects into an empty project, so none covers a pre-existing shared copy. The failure is loud, and gen --list marks the runtime DIFFERS, but eject itself gives no hint. Exposure is limited to projects that ejected from a build with the earlier hand-over. Smallest remedy: when eject keeps an existing runtime copy, report its _compare status against the packaged module in that line (kept your copy — DIFFERS: N behind ...), and add one sentence to the CHANGELOG entry and own-your-codegen.md saying an existing shared_predicates.py needs the two new functions. That changes user-facing eject output, so the remedy needs the maintainer's decision.
  • ℹ️ server/python/src/metaobjects/codegen/_moved.py:21 - The deprecated-path warning is not shown in a normal run. alias() warns with DeprecationWarning and stacklevel=3, which attributes the warning to the importing module (the adopter's codegen.generators.&lt;name&gt;). Python's default filters show DeprecationWarning only when it is attributed to __main__, so metaobjects gen with a pre-move owned copy prints nothing. own-your-codegen.md and the CHANGELOG say the copy keeps running "and a DeprecationWarning names the new path"; that is true only under -W or pytest. The visible signal an adopter gets is gen --list marking the copy DIFFERS: N behind, which the docs also state. No action needed unless the maintainer wants the old paths' removal announced in normal CLI output.
  • ⚠️ server/python/src/metaobjects/codegen/eject.py:170 - The refresh hint the round-2 fix (R5) introduced can recommend a command that discards the adopter's edits or targets the wrong directory. Two cases, both in the hint built at lines 170-176:
  1. Deletion-only edits read as "no edits". The branch is chosen by own from _line_diff (line 206), which counts copy lines the reference lacks. An edit that only removes lines adds none, so own == 0. Concrete state: an owned codegen/runtime/shared_predicates.py from the earlier hand-over where the adopter deleted the line or kind in READ_ONLY_ROUTED_KINDS from emits_router (still valid Python; views stop getting routers). metaobjects eject api-docs prints kept your copy — DIFFERS: N behind, 0 of your own and then refresh it: cp &#34;&lt;installed&gt;/shared_predicates.py&#34; codegen/runtime/. Running that command silently restores the deleted rule. The docs and CHANGELOG word this conditionally ("cp ... when you have not edited it"); the CLI line states it unconditionally. The unedited case of test_eject_onto_an_older_shared_copy_keeps_it_and_says_how_to_refresh_it (tests/codegen/test_eject_independence.py:184) asserts the unconditional form.

  2. With --root, the commands are relative to the root, not the working directory. rel = rt.relative_to(root) (line 162) feeds both cp &#34;...&#34; {rel.parent}/ and diff -u {rel} .... metaobjects eject api-docs --root services/api prints cp &#34;...&#34; codegen/runtime/, which fails from the working directory, or overwrites a different project's copy if the working directory has its own codegen/runtime/.

Smallest remedy: when own == 0, word the line conditionally (for example if you have not changed it: cp ...; otherwise apply the hunks from: diff -u ...), and build both commands from a path that is valid from the working directory. This changes CLI output the maintainer ruled on in R5, so the wording needs the maintainer's decision.

🔧 Fix applied.
2 infos still open:

  • ℹ️ server/python/src/metaobjects/codegen/_moved.py:21 - The deprecated-path warning is not shown in a normal run. alias() warns with DeprecationWarning and stacklevel=3, which attributes the warning to the importing module (the adopter's codegen.generators.&lt;name&gt;). Python's default filters show DeprecationWarning only when it is attributed to __main__, so metaobjects gen with a pre-move owned copy prints nothing. own-your-codegen.md and the CHANGELOG say the copy keeps running "and a DeprecationWarning names the new path"; that is true only under -W or pytest. The visible signal an adopter gets is gen --list marking the copy DIFFERS: N behind, which the docs also state. No action needed unless the maintainer wants the old paths' removal announced in normal CLI output.
  • ℹ️ server/python/src/metaobjects/codegen/runner.py:141 - The skip warning gives advice that fails for the packaged generator name. When a target lists the stock name (generators: [entity, api-docs], the exact case test_gen_skips_a_docs_generator_under_a_target_and_says_so exercises, or gen --generators api-docs), run_gen prints &#39;api-docs&#39; writes the api reference, so gen skipped it. List it under docs.generators in metaobjects.config.yaml. Following that with the same name (docs: {generators: [api-docs]}) is refused by _parse_docs (project_config.py:243): &#39;generators&#39; takes owned generators in &#39;module:symbol&#39; form (metaobjects eject api-docs prints the entry). The user is steered toward ejecting, when the correct action for the packaged generator is to remove the entry, because metaobjects docs already runs the packaged api-docs with no config. The advice is right only for an owned module:symbol copy wired under a target. Nothing is written wrongly and the follow-up error is clear, so impact is limited to a misleading hint. Suggested wording: say metaobjects docs already runs the packaged one, and that only an owned copy (module:symbol) belongs under docs.generators. This changes user-facing CLI text, and the test at tests/codegen/test_docs_generators.py:163 asserts docs.generators appears in the output, so the wording is the maintainer's call.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ℹ️ server/python/src/metaobjects/codegen/_moved.py:21 - The deprecated-path warning is not shown in a normal run. alias() warns with DeprecationWarning and stacklevel=3, which attributes the warning to the importing module (the adopter's codegen.generators.&lt;name&gt;). Python's default filters show DeprecationWarning only when it is attributed to __main__, so metaobjects gen with a pre-move owned copy prints nothing. own-your-codegen.md and the CHANGELOG say the copy keeps running "and a DeprecationWarning names the new path"; that is true only under -W or pytest. The visible signal an adopter gets is gen --list marking the copy DIFFERS: N behind, which the docs also state. No action needed unless the maintainer wants the old paths' removal announced in normal CLI output.

  • ⚠️ spec/decisions/ADR-0034-codegen-scaffold-and-own.md:831 - ADR-0034 Amendment 8 still records the Python gaps this change closes as pending. The change removes all nine Python entries from eject.pending in fixtures/generator-registry-conformance/registry.json and registers api-docs in the Python registry, and CHANGELOG.md:279 says those entries are closed. The ADR was not updated, so three places now contradict the code:

  • line 831, gap 4 (prompt-tier emitters): Closed in C#. Pending in Python, Java and Kotlin

  • line 832, gap 5 (fragment emitters): same state text

  • line 833, gap 6 (docs command emitter as api-docs): same state text

  • lines 912-914: the Java, Kotlin and Python registries do not hold it yet, and the sentence that the Python registry-conformance test subtracts a pending census name

The commit that wrote the amendment recorded Closed in C# in the same column, so the column is kept as per-port state. A later session that reads the ADR will conclude the Python emitters and api-docs are still packaged. Fix: change the three State cells to Closed in C# and Python. Pending in Java and Kotlin, and drop Python from the two sentences at lines 912-914 (it now behaves as C# does: the pending entry is deleted, so nothing is subtracted). Documentation only; no code or test change.

🔧 Fix applied.
3 infos still open:

  • ℹ️ server/python/src/metaobjects/codegen/_moved.py:21 - The deprecated-path warning is not shown in a normal run. alias() warns with DeprecationWarning and stacklevel=3, which attributes the warning to the importing module (the adopter's codegen.generators.&lt;name&gt;). Python's default filters show DeprecationWarning only when it is attributed to __main__, so metaobjects gen with a pre-move owned copy prints nothing. own-your-codegen.md and the CHANGELOG say the copy keeps running "and a DeprecationWarning names the new path"; that is true only under -W or pytest. The visible signal an adopter gets is gen --list marking the copy DIFFERS: N behind, which the docs also state. No action needed unless the maintainer wants the old paths' removal announced in normal CLI output.
  • ℹ️ server/python/src/metaobjects/codegen/_moved.py:21 - Carried forward, already ruled (ignored in rounds 3-5), code unchanged: the deprecated-path DeprecationWarning uses stacklevel=3, so a normal metaobjects gen with a pre-move owned copy prints nothing. The visible signal is gen --list marking the copy DIFFERS. No action.
  • ℹ️ server/python/src/metaobjects/codegen/docs_run.py:82 - run_docs hands every docs generator a GenContext whose warn is lambda _msg: None, so a diagnostic from a docs generator is dropped without trace. run_gen collects the same channel (_warn_collector, runner.py:55) and the CLI prints each as warning: .... The packaged api-docs never calls ctx.warn, so nothing is lost today. The gap is reached only by an owned copy listed under docs.generators that calls ctx.warn(...) (the documented diagnostics channel of the Generator contract): metaobjects docs exits 0 and prints nothing for it. This is original change code, not a fix-round regression. Remedy, if wanted: add a warnings: list[str] to DocsOutcome, pass a collecting warn (prefix with the generator name as run_gen does), and print them in _cmd_docs (cli.py:1087) before the file list. No other site builds a docs GenContext.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Eject each newly ejectable Python generator into an empty project and get owned copies plus their runtime helpers ✅ pass live Live metaobjects eject runs for api-docs, output-prompt, render-helper, extractor, names, routes, entity (exit 0, copies written under codegen/generators and codegen/runtime)
Ejected generators import cleanly from the adopter project ✅ pass live python -c importlib on codegen.generators.{api_docs,names,output_prompt,render_helper,routes,entity,extractor} in the temp project
Template generator is refused by eject (exempt per ruling) ✅ pass live metaobjects eject template -> 'unknown or non-ejectable generator(s): template; ejectable: ...'
Owned entity copy produces the same output as the packaged entity ✅ pass live metaobjects gen --generators codegen.generators.entity:entity_model vs --generators entity, diff -r identical
metaobjects docs writes the Python api reference from metadata, deterministically ✅ pass live metaobjects docs --out wrote api/python/README.md, AGENT-API.md, demo/Widget.md; second run identical
Adversarial: final 1.1.0 release refused while other ports still have pending eject entries, and python is not blamed ✅ pass live node scripts/eject-release-gate.mjs 1.1.0 exit 1 listing java 10, kotlin 8, typescript 22; no python entries; rc.1 exit 0
Cross-port manifest: TS generator registry conformance still green with api-docs gaining python ⏸️ untested no Test-suite run only (bun test, 45 pass); not driven against the live product, so not counted as a live pass
Full Python regression suite ⏸️ untested no Automated regression suite (3107 passed) only; not driven against the live product, so not counted as a live pass
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • .venv/bin/python -m pytest on the 14 eject/docs/registry/conformance files named in the plan (183 passed)
  • .venv/bin/python -m pytest on the 8 emitter-importing router/entity/template-tier files (124 passed)
  • node scripts/eject-release-gate.mjs 1.1.0-rc.1 (exit 0, may be released)
  • node scripts/eject-release-gate.mjs 1.1.0 (exit 1; reports java 10, kotlin 8, typescript 22 pending; no python entries named)
  • bun test ./server/typescript/packages/codegen-ts/test/golden/generator-registry-conformance.test.ts (45 pass)
  • scripts/ci-local.sh --only gates (LOCAL CI PASSED)
  • Live: metaobjects eject of api-docs, output-prompt, render-helper, extractor, names, routes, entity into a temp project (copies created; re-eject reports kept-your-copy identical; template refused as non-ejectable)
  • Live: owned copies import cleanly from the project
  • Live: metaobjects docs --out with demo metadata writes api/python README, AGENT-API and Widget.md; repeat run identical
  • Live: metaobjects gen --generators codegen.generators.entity:entity_model output byte-identical to packaged entity output
  • Full Python suite python -m pytest -q -p no:warnings in server/python (3107 passed, EXIT=0; log in evidence dir)
  • ADR-0034 Amendment 8 rows 4-6 wording checked (Python closed)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…h; api-docs is ejectable

An ejected Python generator used to be one file over packaged emitters, so a defect in
the shared-enum, M:N, prompt-tier or api-reference writers still waited for a release.
Eject now copies those emitters with the generator into codegen/runtime/, through each
registry entry's existing runtime list, and the copy reads them from there. The emitters
moved into metaobjects.codegen.runtime; their old import paths stay as deprecated aliases
so a copy ejected earlier keeps running. Naming and type-mapping tables, the model walks
and the generator base stay in the package as the engine.

api-docs is registered for Python and ejectable. The api reference is a generator
(docs_surface = "api") that `metaobjects docs` runs; an owned copy is listed under the new
docs.generators config block. `gen` and `verify --codegen` skip a docs generator and say
so. The page builder reads the router gates from shared_predicates (new
emits_writable_router, moved is_read_only_routed), the module an owned routes and
filter-allowlist read, so the reference describes an owned copy.

test_eject_alone derives its list from the registry: each ejectable generator is ejected
alone into an empty project, checked against a written engine surface, run with every
packaged emit module unimportable, and compared byte for byte with the packaged
generator. Packaged output is unchanged.
…h; api-docs is ejectable

An ejected Python generator used to be one file over packaged emitters, so a defect in
the shared-enum, M:N, prompt-tier or api-reference writers still waited for a release.
Eject now copies those emitters with the generator into codegen/runtime/, through each
registry entry's existing runtime list, and the copy reads them from there. The emitters
moved into metaobjects.codegen.runtime; their old import paths stay as deprecated aliases
so a copy ejected earlier keeps running. Naming and type-mapping tables, the model walks
and the generator base stay in the package as the engine.

api-docs is registered for Python and ejectable. The api reference is a generator
(docs_surface = "api") that `metaobjects docs` runs; an owned copy is listed under the new
docs.generators config block. `gen` and `verify --codegen` skip a docs generator and say
so. The page builder reads the router gates from shared_predicates (new
emits_writable_router, moved is_read_only_routed), the module an owned routes and
filter-allowlist read, so the reference describes an owned copy.

test_eject_alone derives its list from the registry: each ejectable generator is ejected
alone into an empty project, checked against a written engine surface, run with every
packaged emit module unimportable, and compared byte for byte with the packaged
generator. Packaged output is unchanged.
Tree-preserving merge: the content of 3f482e7 is already carried, rebased onto main
with the eject guard, by the parent commit. This keeps the live head a descendant of the
head the cancelled run submitted.
@dmealing
dmealing merged commit 4bbd6cc into main Oct 12, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-1-1-0-eject-python branch October 12, 2026 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant