Repository navigation
feat(python): eject owns the emitters its generators write through - #436
Merged
Merged
Conversation
…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.
…t, cwd-valid paths
…aged and owned docs generators
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
metaobjects ejectnow copies the emitter modules a generator writes through (shared-enum, M:N, extract delegate/schema, field mapping, output-format spec) intocodegen/runtime/, so an owned generator no longer calls packaged emitters.gen --listmarks each copyidenticalorDIFFERS, and eject reports how a kept runtime copy compares with the packaged one.api-docsis an ejectable docs generator.metaobjects docsruns it, and an owned copy is listed under the newdocs.generatorsconfig block.genandverify --codegenskip docs generators they find under a target.metaobjects.codegen.runtime(api reference asapi_docs_*modules). The old import paths remain as one-line shims that emitDeprecationWarning; 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.
metaobjects eject template-> 'unknown or non-ejectable generator(s): template; ejectable: ...'metaobjects gen --generators codegen.generators.entity:entity_modelvs--generators entity, diff -r identicalmetaobjects docs --outwrote api/python/README.md, AGENT-API.md, demo/Widget.md; second run identicalnode scripts/eject-release-gate.mjs 1.1.0exit 1 listing java 10, kotlin 8, typescript 22; no python entries; rc.1 exit 0Evidence: Full Python suite log (3107 passed)
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):server/python/src)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-docslearns which objects get a router fromshared_predicates, the module an ownedroutesandfilter-allowlistread. A gate rewritten inline inside an ownedroutes.py, and the template-tier gates behind the packagedfind_inboundwalk, are not seen by the docs.routesemits 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 packagedapi-docs; only the config form honoursdocs.generators.Open info findings and why they stand
DeprecationWarningon the old import paths is hidden by Python's default filters in a normalgen; the visible signal isgen --listmarking the copyDIFFERS: N behind, which the docs state. Ruled: no action.run_docsgives docs generators a no-opwarnchannel; no docs generator emits a warning today.Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
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) andtest_an_ejected_generator_imports_only_the_engine(139-154) restate the rule thatfixtures/generator-registry-conformance/registry.json(eject.engineSurface.python.engine) already defines and thattests/conformance/test_eject_contract.py::test_depth_an_owned_copy_imports_only_the_engine_surfacealready 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 allowsmetaobjects.codegen.model_walk, which the manifest does not list, and the manifest allowsmetaobjects.codegen.generator_registry, which this file does not. An import ofmodel_walkin 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, thewritten == copiedcheck, and thecodegen/runtime/census).docs/features/own-your-codegen.mdand 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_walkdecides which modules undercodegen/generators/are deprecated aliases with a raw substring over implementation source:"codegen._moved" not in p.read_text(). A real generator or emitter module that mentionscodegen._movedin a comment or docstring is silently dropped from the census, which is the gap the census exists to catch. Classify by behaviour instead: importmetaobjects.codegen.generators.<stem>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, astest_moved_modules.pyalready proves). Oracle: the import system's result, independent of the stub's text.server/python/src/metaobjects/codegen/project_config.py:50-DOCS_KEYSis commented "Same contract as above" (kept in step with the shipped schema bytest_schema_and_loader_accept_EXACTLY_the_same_keys), but that test (tests/codegen/test_project_config.py:210-217) compares only the top-level,targetsandrequirementTestskey sets. A key added toDOCS_KEYSwithout the schema'sdocs.properties(or the reverse), or a droppedadditionalProperties: falseon thedocsblock, passes. Add thedocsblock's key-set andadditionalPropertiesassertions beside therequirementTestsones.docs/features/cli.md:372- The Python config reference (the annotatedmetaobjects.config.yamlexample, lines 368-386) lists every top-level key except the newdocs:block.docs.generatorsis documented inown-your-codegen.mdanddocs/ports/python.md, but a reader of the key reference will not find it. Add thedocs: { 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 addsis_read_only_routedandemits_writable_routerto the handed-overshared_predicatesmodule (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) andgenerators/router_generator.py:84(from ..runtime.shared_predicates import emits_router, is_read_only_routed). Concrete sequence: a project that ejectedroutes,entity,filter-allowlistorextractorfrom a 1.1.0 build that already handed overshared_predicates(the earlier hand-over recorded in the 1.1.0 CHANGELOG) ownscodegen/runtime/shared_predicates.pywithout those two functions. After upgrading,metaobjects eject api-docsprintsruntime codegen/runtime/shared_predicates.py: kept your copyand succeeds;metaobjects docsthen fails withcannot import name 'emits_writable_router'.metaobjects eject routes --forcefails the same way ongenforis_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, andgen --listmarks the runtimeDIFFERS, 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_comparestatus against the packaged module in that line (kept your copy — DIFFERS: N behind ...), and add one sentence to the CHANGELOG entry andown-your-codegen.mdsaying an existingshared_predicates.pyneeds 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 withDeprecationWarningandstacklevel=3, which attributes the warning to the importing module (the adopter'scodegen.generators.<name>). Python's default filters showDeprecationWarningonly when it is attributed to__main__, sometaobjects genwith a pre-move owned copy prints nothing.own-your-codegen.mdand the CHANGELOG say the copy keeps running "and aDeprecationWarningnames the new path"; that is true only under-Wor pytest. The visible signal an adopter gets isgen --listmarking the copyDIFFERS: 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 addsis_read_only_routedandemits_writable_routerto the handed-overshared_predicatesmodule (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) andgenerators/router_generator.py:84(from ..runtime.shared_predicates import emits_router, is_read_only_routed). Concrete sequence: a project that ejectedroutes,entity,filter-allowlistorextractorfrom a 1.1.0 build that already handed overshared_predicates(the earlier hand-over recorded in the 1.1.0 CHANGELOG) ownscodegen/runtime/shared_predicates.pywithout those two functions. After upgrading,metaobjects eject api-docsprintsruntime codegen/runtime/shared_predicates.py: kept your copyand succeeds;metaobjects docsthen fails withcannot import name 'emits_writable_router'.metaobjects eject routes --forcefails the same way ongenforis_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, andgen --listmarks the runtimeDIFFERS, 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_comparestatus against the packaged module in that line (kept your copy — DIFFERS: N behind ...), and add one sentence to the CHANGELOG entry andown-your-codegen.mdsaying an existingshared_predicates.pyneeds 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 withDeprecationWarningandstacklevel=3, which attributes the warning to the importing module (the adopter'scodegen.generators.<name>). Python's default filters showDeprecationWarningonly when it is attributed to__main__, sometaobjects genwith a pre-move owned copy prints nothing.own-your-codegen.mdand the CHANGELOG say the copy keeps running "and aDeprecationWarningnames the new path"; that is true only under-Wor pytest. The visible signal an adopter gets isgen --listmarking the copyDIFFERS: 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:Deletion-only edits read as "no edits". The branch is chosen by
ownfrom_line_diff(line 206), which counts copy lines the reference lacks. An edit that only removes lines adds none, soown == 0. Concrete state: an ownedcodegen/runtime/shared_predicates.pyfrom the earlier hand-over where the adopter deleted the lineor kind in READ_ONLY_ROUTED_KINDSfromemits_router(still valid Python; views stop getting routers).metaobjects eject api-docsprintskept your copy — DIFFERS: N behind, 0 of your ownand thenrefresh it: cp "<installed>/shared_predicates.py" 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. Theuneditedcase oftest_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.With
--root, the commands are relative to the root, not the working directory.rel = rt.relative_to(root)(line 162) feeds bothcp "..." {rel.parent}/anddiff -u {rel} ....metaobjects eject api-docs --root services/apiprintscp "..." codegen/runtime/, which fails from the working directory, or overwrites a different project's copy if the working directory has its owncodegen/runtime/.Smallest remedy: when
own == 0, word the line conditionally (for exampleif 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 withDeprecationWarningandstacklevel=3, which attributes the warning to the importing module (the adopter'scodegen.generators.<name>). Python's default filters showDeprecationWarningonly when it is attributed to__main__, sometaobjects genwith a pre-move owned copy prints nothing.own-your-codegen.mdand the CHANGELOG say the copy keeps running "and aDeprecationWarningnames the new path"; that is true only under-Wor pytest. The visible signal an adopter gets isgen --listmarking the copyDIFFERS: 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 casetest_gen_skips_a_docs_generator_under_a_target_and_says_soexercises, orgen --generators api-docs),run_genprints'api-docs' writes the api reference, so gen skipped it. List it underdocs.generatorsin metaobjects.config.yaml. Following that with the same name (docs: {generators: [api-docs]}) is refused by_parse_docs(project_config.py:243):'generators' takes owned generators in 'module:symbol' form (metaobjects eject api-docsprints the entry). The user is steered toward ejecting, when the correct action for the packaged generator is to remove the entry, becausemetaobjects docsalready runs the packagedapi-docswith no config. The advice is right only for an ownedmodule:symbolcopy 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: saymetaobjects docsalready runs the packaged one, and that only an owned copy (module:symbol) belongs underdocs.generators. This changes user-facing CLI text, and the test attests/codegen/test_docs_generators.py:163assertsdocs.generatorsappears 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 withDeprecationWarningandstacklevel=3, which attributes the warning to the importing module (the adopter'scodegen.generators.<name>). Python's default filters showDeprecationWarningonly when it is attributed to__main__, sometaobjects genwith a pre-move owned copy prints nothing.own-your-codegen.mdand the CHANGELOG say the copy keeps running "and aDeprecationWarningnames the new path"; that is true only under-Wor pytest. The visible signal an adopter gets isgen --listmarking the copyDIFFERS: 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 fromeject.pendinginfixtures/generator-registry-conformance/registry.jsonand registersapi-docsin the Python registry, andCHANGELOG.md:279says 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 Kotlinline 832, gap 5 (fragment emitters): same state text
line 833, gap 6 (docs command emitter as
api-docs): same state textlines 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 nameThe 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 andapi-docsare still packaged. Fix: change the three State cells toClosed 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 withDeprecationWarningandstacklevel=3, which attributes the warning to the importing module (the adopter'scodegen.generators.<name>). Python's default filters showDeprecationWarningonly when it is attributed to__main__, sometaobjects genwith a pre-move owned copy prints nothing.own-your-codegen.mdand the CHANGELOG say the copy keeps running "and aDeprecationWarningnames the new path"; that is true only under-Wor pytest. The visible signal an adopter gets isgen --listmarking the copyDIFFERS: 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-pathDeprecationWarningusesstacklevel=3, so a normalmetaobjects genwith a pre-move owned copy prints nothing. The visible signal isgen --listmarking the copyDIFFERS. No action.server/python/src/metaobjects/codegen/docs_run.py:82-run_docshands every docs generator aGenContextwhosewarnislambda _msg: None, so a diagnostic from a docs generator is dropped without trace.run_gencollects the same channel (_warn_collector, runner.py:55) and the CLI prints each aswarning: .... The packagedapi-docsnever callsctx.warn, so nothing is lost today. The gap is reached only by an owned copy listed underdocs.generatorsthat callsctx.warn(...)(the documented diagnostics channel of theGeneratorcontract):metaobjects docsexits 0 and prints nothing for it. This is original change code, not a fix-round regression. Remedy, if wanted: add awarnings: list[str]toDocsOutcome, pass a collectingwarn(prefix with the generator name asrun_gendoes), and print them in_cmd_docs(cli.py:1087) before the file list. No other site builds a docsGenContext.✅ **Test** - passed
✅ No issues found.
metaobjects eject template-> 'unknown or non-ejectable generator(s): template; ejectable: ...'metaobjects gen --generators codegen.generators.entity:entity_modelvs--generators entity, diff -r identicalmetaobjects docs --outwrote api/python/README.md, AGENT-API.md, demo/Widget.md; second run identicalnode scripts/eject-release-gate.mjs 1.1.0exit 1 listing java 10, kotlin 8, typescript 22; no python entries; rc.1 exit 0scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains.venv/bin/python -m pyteston the 14 eject/docs/registry/conformance files named in the plan (183 passed).venv/bin/python -m pyteston 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 ejectof api-docs, output-prompt, render-helper, extractor, names, routes, entity into a temp project (copies created; re-eject reports kept-your-copy identical;templaterefused as non-ejectable)Live: owned copies import cleanly from the projectLive:metaobjects docs --outwith demo metadata writes api/python README, AGENT-API and Widget.md; repeat run identicalLive:metaobjects gen --generators codegen.generators.entity:entity_modeloutput byte-identical to packagedentityoutputFull Python suitepython -m pytest -q -p no:warningsin 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.