Repository navigation
feat: enforce eject contract across generators and gate final releases on it - #435
Merged
Merged
Conversation
Every stock generator is meant to be ejectable, with the owned copy holding its emit. Three pieces of work each made a named list ejectable and no test stated the rule, so the newest generators shipped as thin copies with every lane green. This adds the test, in all five ports, driven by one shared file. The shared file is fixtures/generator-registry-conformance/registry.json. Each generator now carries "eject": "full" or an exemption with its reason and ruling. A top-level "eject" block holds, per port, the packaged modules an owned copy may still import (the engine surface), the exempt non-generator emitters, and a "pending" list of every place the contract does not hold yet. Each port's eject-contract test makes three assertions: - Census: every generator implementation the port ships is in its registry. - Completeness: every registered generator ships eject source unless exempt, and the other lists of ejectable generators are derived from the registry or held equal to it. - Depth: each generator, ejected alone, compiles or loads, references nothing in the codegen packages outside the engine surface, and emits what the packaged generator emits. A pending entry that passes fails the test, and so does a gap that is not listed. PENDING_LIST_MAY_BE_NON_EMPTY in the shared file skips each port's "the pending list is empty" test until the release branch sets it false: 1.1.0 does not ship with entries left. The census registered seven generators that no registry listed: TypeScript requirements and agent-docs, and Java plantuml, html-docs, ai-docs, json-schema and the deprecated mustache. Registering PlantUML put it in two registry-driven suites for the first time; both failed on a StringIndexOutOfBoundsException for any single-segment package, which is fixed here. The tests that asserted a gap are gone, with the hand-typed lists they pinned. ADR-0034 Amendment 8 records the six rulings, the engine surface, the exemptions, the gap list and the release gate, and supersedes FR-040 section 4.2 and the eject-in-every-port design's thin-copy rule. This closes none of the pending gaps. It also found one the audit did not list: an owned Java entity constructs the packaged extractor generator when no extractor is in the run (gap 13).
…s entries
The eject contract's release rule was a switch a person had to remember to
flip. It is now enforced by the release scripts, and the guard's scans are
tighter.
- scripts/eject-release-gate.mjs refuses a version with no prerelease suffix
while fixtures/generator-registry-conformance/registry.json has an entry in
eject.pending, or while PENDING_LIST_MAY_BE_NON_EMPTY is anything but false.
scripts/release.mjs calls it first in its preflight and
scripts/finish-release.mjs calls it as gate 0, before the tag. A release
candidate is allowed. An unreadable manifest is refused.
- The "pending list is empty" test now reports as skipped in TypeScript and
C# while the switch is on, as it already did in Java, Kotlin and Python. It
used to return early and count as a pass.
- The TypeScript reference scan also counts type-only imports, inline
import("...") types and dynamic import() of a package module. The Python
scan also counts importlib.import_module and __import__ on a string literal.
Each scan has a test over a file whose answer is known.
- The tighter TypeScript scan found the owned requirement-tests generator
importing two types from the packaged generator and its template. The
renderer's argument record and signature are now declared beside the
requirement walk. The package's exports are unchanged. relation-resolver.ts
and render-context.ts join the TypeScript engine surface: an owned copy
imports their types.
- ADR-0034 Amendment 8 states the limits of the guard with the reason for
each, lists the engine-surface names that were a judgment call, and records
that gap 13 closes with the Java work on gaps 4, 5 and 7.
The Java registry-conformance test rebuilt the set of pending census names for every manifest entry. It now builds it once. Also splits a comment in the TypeScript generators barrel back into its two subjects.
…losed Rebased onto the change that hands the C# emitters over on eject and makes api-docs ejectable there. All nine C# pending entries now pass the guard, so they leave the list: the api-docs census entry and eight depth entries (gaps 4 and 5). 49 entries are left: typescript 22, java 10, python 9, kotlin 8. - C# eject now writes the emitters beside the generator, so the contract test compiles and scans every file eject writes under codegen/generators. It used to require exactly one file. - DocsRunFacts, GeneratorApplicability and IDocsGenerator join the C# engine surface: the owned api-docs copy reads the docs run's facts and asks the packaged generators whether they apply, and writes no output through them. - ADR-0034 Amendment 8: gaps 4, 5 and 6 are closed in C#.
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
fixtures/generator-registry-conformance/registry.json: each generator is marked"eject": "full"or exempt with a reason, andeject.pendinglists known gaps (generator, port, assertion). Every port has an eject-contract test that checks each shipped generator is registered, ships eject source unless exempt, and emits what the packaged generator emits. Unlisted gaps fail the test, and listed gaps that have closed also fail it.requirementsandagent-docs, and Javaplantuml,html-docs,ai-docs,json-schemaandmustache. None of the seven ships eject source yet. The Java PlantUML generator no longer throws on a single-segment package.scripts/eject-release-gate.mjs) that refuses a final version whileeject.pendinghas entries. It runs inscripts/release.mjs,scripts/finish-release.mjsand the four publish workflows. Prerelease versions are still allowed.Validation runs and size
fixtures/generator-registry-conformance/registry.json). The regenerated agent-context fixtures (+8 / -4) are not counted.Validation
Risk Assessment
✅ Low: The change is tests, a shared manifest, docs, seven registry rows and a release gate that only refuses and fails closed on every path I traced (missing or malformed manifest, unparseable version, symlinked script path); the round 1 real-path fix is correct and its symlink test fails without it.
Testing
I ran the author's plan items 1-7 against the real guard tests, the release-gate CLI and the quick CI script. I also tried to break the guard by editing the shared manifest both ways. Everything passed with zero skipped eject-contract tests, and the worktree is clean. I did not run the flagless full CI script, as the plan says, so the full C#/Java/Python conformance lanes beyond the eject tests were not exercised.
Evidence: Release gate refuses final 1.1.0 and allows RC
Evidence: Manifest mutation A: dropped real pending entry caught
Evidence: Manifest mutation B: bogus pending entry caught
Evidence: scripts/ci-local.sh --quick output
Evidence: TypeScript eject contract test
Evidence: Python eject contract test
Evidence: C# eject contract test
Pipeline
Updates from git push no-mistakes
✅ **Intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
scripts/eject-release-gate.mjs:100- The gate's command-line block can be skipped, so the command prints nothing and exits 0 for a final version while entries are pending. The entry guard isimport.meta.url === pathToFileURL(process.argv[1]).href. Node resolves the main module'simport.meta.urlto its real path and leavesprocess.argv[1]as typed, so the two differ whenever the script path goes through a symlink. Probed in this worktree with 49 entries pending:node <symlink-to-repo>/scripts/eject-release-gate.mjs 1.1.0exits 0 with no output; the same command by the real path exits 1 and lists the 49 entries. Reach: the four hand-typed publishes indocs/RELEASING.md, where the operator is told the gate "must exit 0"; a silent 0 satisfies that. Not affected: the documented relative formnode scripts/eject-release-gate.mjs <version>from the repository root (also probed from a symlinked working directory: exit 1), the four workflows (relative path), andrelease.mjs/finish-release.mjs(they import the function). Same cause on macOS, whereos.tmpdir()is a symlink: the test "exits 1 with the reason for a refused version, 0 otherwise, 2 for a bad argument" inscripts/eject-release-gate.test.tsspawns the script by an absolute path under it and would read 0. Fix: compare real paths,realpathSync(process.argv[1]) === fileURLToPath(import.meta.url), and add a case to that test that runs the gate through a symlinked directory and expects exit 1. The same guard atscripts/publish-set.mjs:138is unchanged code and outside this change.🔧 Fix applied.
1 info still open:
server/csharp/MetaObjects.Codegen.Tests/EjectContractTests.cs:181- The C# depth scan can now pass while returning nothing.Depth_an_owned_copy_references_only_the_engine_surfacecomparesOutsideEngineSurface(...)with the pendingoutsidelists. Commit cfb55ed removed the last C# pending entries, so the expected value is[]for every generator, and a scan that finds no references at all passes every row. No other C# test exercises the scan. TypeScript and Python each hold their scan to a file with a known answer (eject-contract.test.ts:449,test_eject_contract.py:225). C# has no such test. Sibling sites: Java (server/java/maven-plugin/src/test/java/com/metaobjects/mojo/EjectContractTest.java:416, scan at:378) and Kotlin (server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/generator/kotlin/EjectContractTest.kt:196) are covered today only by their pending depth entries, and lose that cover when the entries close, which is the state the release gate requires. Observable failure: an owned copy that names a packaged emitter type is reported whole. Independent expected result: a probe that names one emit type and one engine type yields exactly the emit type. Fix: add one probe per port. C#: compile a snippet naming a non-engineMetaObjects.Codegentype and an engine type, assert the scan returns only the first. Java and Kotlin: runreferencedClassesover the bytes of a compiled class known to reference a non-engine generator class, assert that class is in the result.✅ **Test** - passed
✅ No issues found.
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainscd server/typescript/packages/cli && bun test test/eject-contract.test.ts(33 pass)cd server/typescript/packages/codegen-ts && bun test test/generator-registry.test.ts(17 pass)bun test scripts/finish-release.test.ts scripts/eject-release-gate.test.ts(72 pass)node scripts/eject-release-gate.mjs 1.1.0(exit 1, 49 pending entries listed),1.1.0-rc.3(exit 0),1.1.0rc3(exit 0, PEP 440), and1.1.0through a symlinked repo path (exit 1)dotnet test MetaObjects.Codegen.Tests --filter FullyQualifiedName~EjectContract(17 pass)mvn -pl maven-plugin -am test -Dtest=EjectContractTest ...(Java 7 + Kotlin 7 pass)uv run pytest tests/conformance/test_eject_contract.py -q(16 pass)Manifest mutation A: removed a real TypeScript pending entry, so the TypeScript guard failedManifest mutation B: added a bogus pending entry, so the TypeScript guard failed; both mutations reverted with git checkoutscripts/ci-local.sh --quick(LOCAL CI PASSED, 4 non-quick lanes skipped by design)Baseline:scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains(passed before this step)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.