Skip to content

feat: enforce eject contract across generators and gate final releases on it - #435

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

dmealing merged 8 commits into
mainfrom
fm/mo-1-1-0-eject-guard

Conversation

@dmealing

@dmealing dmealing commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Adds an eject contract to fixtures/generator-registry-conformance/registry.json: each generator is marked "eject": "full" or exempt with a reason, and eject.pending lists 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.
  • Registers seven generators that no registry listed: TypeScript requirements and agent-docs, and Java plantuml, html-docs, ai-docs, json-schema and mustache. None of the seven ships eject source yet. The Java PlantUML generator no longer throws on a single-segment package.
  • Adds a release gate (scripts/eject-release-gate.mjs) that refuses a final version while eject.pending has entries. It runs in scripts/release.mjs, scripts/finish-release.mjs and the four publish workflows. Prerelease versions are still allowed.

Validation runs and size

  • Validation runs: 1 completed. An earlier run on this branch was cancelled part-way through its test step on the maintainer's order, because the machine was overloaded by several concurrent runs. That restart is infrastructure-caused and is not counted as a second run. The review fixes that run had already made are kept as commits on this branch.
  • Net hand-written lines: +3847 / -208 = +3639. Of the added lines, 477 are the shared manifest (fixtures/generator-registry-conformance/registry.json). The regenerated agent-context fixtures (+8 / -4) are not counted.
  • Pending list at this head: 49 entries (TypeScript 22, Java 10, Python 9, Kotlin 8). This change closes none of them by itself; the nine C# entries left the list when the C# emitter hand-over merged.
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.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Run the TypeScript eject contract test against the shared manifest and see census, completeness and depth hold, with open gaps recorded as pending ✅ pass live r2/item1.txt (33 pass, 0 fail, 0 skip)
Run the C#, Java, Kotlin and Python eject contract tests and see each port's guard pass ✅ pass live r2/item4.txt (17 pass), Maven run (Java 7 + Kotlin 7 pass), r2/item6.txt (16 pass)
Run the release gate CLI for a final 1.1.0 and see it refuse with exit 1, naming the 49 pending entries ✅ pass live r2/item3b.txt
Run the release gate CLI for release candidates (1.1.0-rc.3, and PEP 440 1.1.0rc3) and see them allowed with exit 0 ✅ pass live CLI run in this step: both versions printed 'may be released', exit 0
Adversarial: run the gate for a final version through a symlinked repo path and see it still refuses with exit 1 instead of silently exiting 0 ✅ pass live CLI run in this step via a temporary symlink to the worktree: exit 1 with the refusal text
Adversarial: remove a real pending TypeScript entry from the manifest and see the guard fail ✅ pass live r2/mutA.txt: the depth and census checks fail
Adversarial: add a bogus pending entry for a generator with no gap and see the guard fail (checked in both directions) ✅ pass live r2/mutB.txt: the pending-entry well-formed check fails
Run the release-gate and finish-release scripts' tests, including the ordering test that holds the four publish workflows to the gate ✅ pass live r2/item3a.txt (72 pass)
Run the quick local CI script and see it pass end to end ✅ pass live r2/item7-ci-quick.txt: LOCAL CI PASSED
Evidence: Release gate refuses final 1.1.0 and allows RC
^[[31m✗^[[0m 1.1.0 is a final release, and the eject contract does not hold yet (ADR-0034 Amendment 8).
  49 entries are left in eject.pending of fixtures/generator-registry-conformance/registry.json: java 10, kotlin 8, python 9, typescript 22.
  Close each gap and delete its entry; a port's eject-contract test names what is left.
  A release candidate (a version with a prerelease suffix, e.g. -rc.1) is not refused.
Evidence: Manifest mutation A: dropped real pending entry caught
dropping {"generator":"entity","port":"typescript","assertion":"depth","gap":3,"outside":["codegen-ts/src/templates/drizzle-schema.ts","codegen-ts/src/templates/entity-constants.ts","codegen-ts/src/templates/entity-file.ts","codegen-ts/src/templates/enums-file.ts","codegen-ts/src/templates/filter-allowlist.ts","codegen-ts/src/templates/filter-type.ts","codegen-ts/src/templates/inferred-types.ts","codegen-ts/src/templates/projection-decl.ts","codegen-ts/src/templates/tph-discriminator.ts","codegen-ts/src/templates/value-object-file.ts","codegen-ts/src/templates/zod-validators.ts"]}
error: expect(received).toEqual(expected)
(fail) eject contract — depth > an owned copy imports only the engine surface [5794.97ms]
 32 pass
 1 fail
Evidence: Manifest mutation B: bogus pending entry caught
error: expected in the catalog by the shared file; a pending census entry must list each
(fail) eject contract — census > the catalog exposes the names the shared file gives this port [0.29ms]
error: {"generator":"nonexistent-gen","port":"typescript","assertion":"census","gap":1}
(fail) eject contract — the shared file > every pending entry is well formed, and listed once [0.41ms]
 31 pass
 2 fail
Evidence: scripts/ci-local.sh --quick output
metaobjects local CI  (mode: quick — TS + drift gates only)

── ▶ leak-scan (security) ──────────────────────────────────────────────
leak-scan: clean

── ▶ pom-version parity ──────────────────────────────────────────────
check-pom-versions: ✓ 3 parent + 1 property pom(s) match reactor 8.1.0

── ▶ bun-version parity ──────────────────────────────────────────────
check-bun-version: bun 1.3.14 matches the workflow pin.

── ▶ uv.lock version parity ──────────────────────────────────────────────
check-uv-lock-version: uv.lock agrees with pyproject.toml (1.1.0)

── ▶ publish-intent parity ──────────────────────────────────────────────
publish-intent parity: OK (lockstep 1.1.0; 2 declared source-only)

── ▶ publish-set parity ──────────────────────────────────────────────
   1. @metaobjectsdev/metadata  (server/typescript/packages/metadata)
   2. @metaobjectsdev/render  (server/typescript/packages/render)
   3. @metaobjectsdev/codegen-ts  (server/typescript/packages/codegen-ts)
   4. @metaobjectsdev/runtime-ts  (server/typescript/packages/runtime-ts)
   5. @metaobjectsdev/migrate-ts  (server/typescript/packages/migrate-ts)
   6. @metaobjectsdev/sdk  (server/typescript/packages/sdk)
   7. @metaobjectsdev/docs-site  (server/typescript/packages/docs-site)
   8. @metaobjectsdev/runtime-web  (client/web/packages/runtime-web)
   9. @metaobjectsdev/codegen-ts-react  (server/typescript/packages/codegen-ts-react)
  10. @metaobjectsdev/codegen-ts-tanstack  (server/typescript/packages/codegen-ts-tanstack)
  11. @metaobjectsdev/react  (client/web/packages/react)
  12. @metaobjectsdev/tanstack  (client/web/packages/tanstack)
  13. @metaobjectsdev/cli  (server/typescript/packages/cli)
  14. @metaobjectsdev/ai-runtime  (server/typescript/packages/ai-runtime)
publish set: OK (lockstep 1.1.0; 14 packages, tier-ordered, closed over sibling deps)
ok:   happy path: private + off-lockstep excluded, order [metadata, render, docs-site, cli]
ok:   untiered member: threw on "publish order undeclared"
ok:   unclosed set: threw on "not closed over its own runtime dependencies"
ok:   inverted tier order: threw on "publishes a dependency AFTER its dependent"
ok:   regression: docs-site (#6) publishes before cli (#12) in the real tree
ok:   regression: docs-site has a declared tier

publish-set tests: all passed

── ▶ no committed pre-release version ──────────────────────────────────────────────
check-no-prerelease-versions: ✓ no pre-release version in any version declaration

── ▶ script-name hook collisions ──────────────────────────────────────────────
script-name-hooks: ✓ 10 root scripts, no lifecycle-hook collisions

── ▶ ci lane selection ──────────────────────────────────────────────
ok:   [server/typescript/packages/cli/src/x.ts] -> 'ts'
ok:   [client/web/packages/react/src/y.tsx] -> 'ts'
ok:   [server/java/metadata/src/main/java/A.java] -> 'java'
ok:   [server/java/codegen-kotlin/src/main/kotlin/B.kt] -> 'java'
ok:   [server/python/src/metaobjects/loader.py] -> 'python'
ok:   [server/csharp/MetaObjects/Loader.cs] -> 'csharp'
ok:   [fixtures/conformance/foo/meta.json] -> 'ts java python csharp'
ok:   [spec/metamodel.md] -> 'ts java python csharp'
ok:   [scripts/ci-local.sh] -> 'ts java python csharp'
ok:   [.github/workflows/local-ci.yml] -> 'ts java python csharp'
ok:   [agent-context/skills/x.md] -> 'ts java python csharp'
ok:   [weird-new-toplevel/file.txt] -> 'ts java python csharp'
ok:   [docs/features/cli.md] -> ''
ok:   [README.md CHANGELOG.md] -> ''
ok:   [server/typescript/a.ts server/python/b.py] -> 'ts python'
ok:   [docs/x.md fixtures/y.json] -> 'ts java python csharp'
ALL PASS
ok:   union all -> 'ts java python csharp'
ok:   union reorders to canonical -> 'ts java python csharp'
ok:   union deduplicates -> 'ts java'
ok:   union single -> 'python'
ok:   union empty -> ''
ok:   union drops unknown tokens -> 'ts'
ok:   every lane green -> nothing to re-run -> ''
ok:   docs-only run skips a red java -> java still not green -> 'java'
ok:   one red ts lane selects the whole ts port -> 'ts'
ok:   skipped is walked past to the real verdict -> ''
ok:   cancelled is not green -> 'csharp'
ok:   a lane with no verdict in the window is not green -> 'python'
ok:   no history at all -> everything -> 'ts java python csharp'
ok:   every port-testing job in .github/workflows/local-ci.yml is mapped by lane_port -> ''
ok:   every lane in lane_port exists as a job in .github/workflows/local-ci.yml -> ''
all checks passed

── ▶ extract field kinds ──────────────────────────────────────────────
extract FieldKind: 4 ports agree on [STRING, INT, LONG, DOUBLE, BOOLEAN, ENUM, OBJECT] (1 sanctioned deviation(s): csharp)

── ▶ no-magic gate wired (5 ports) ──────────────────────────────────────────────
no-magic gate coverage: 5 ports wired; 13 shapes tracked, 0 known gaps

── ▶ test-file references resolve ──────────────────────────────────────────────
test-file references: 32 named, all resolve

── ▶ metamodel-version bump ──────────────────────────────────────────────
  metamodel-version: additive change since v1.0.13; declared 1.1 (1.0 → 1.1) — ok.
ok:   no change classifies as nothing
ok:   a removed subtype is BREAKING
ok:   an added subtype is ADDITIVE
ok:   a removed attr is BREAKING
ok:   an added OPTIONAL attr is ADDITIVE
ok:   an added REQUIRED attr is BREAKING
ok:   optional → required is BREAKING
ok:   required → optional is ADDITIVE
ok:   a changed valueType is BREAKING
ok:   a changed isArray is BREAKING
ok:   an enum member REMOVED is BREAKING
ok:   an enum member ADDED is ADDITIVE
ok:   an OPEN attr becoming a closed enum is BREAKING
ok:   a closed enum OPENING is ADDITIVE
ok:   a removed child rule is BREAKING
ok:   an added OPTIONAL child rule is ADDITIVE
ok:   an added MANDATORY child rule is BREAKING
ok:   raising min is BREAKING
ok:   lowering min is ADDITIVE
ok:   capping an unbounded max is BREAKING
ok:   lifting a cap is ADDITIVE
ok:   lowering a finite max is BREAKING
ok:   a removed commonAttr is BREAKING
ok:   a changed default subtype is BREAKING (it changes what an unqualified declaration MEANS)
ok:   a removed default subtype is BREAKING
ok:   an added default subtype is ADDITIVE
ok:   a type description change is PROSE ONLY
ok:   a `rules` change is PROSE ONLY — this is exactly #210, and why prose only warns
ok:   an attr description change is PROSE ONLY
ok:   no change requires no bump
ok:   additive requires a minor
ok:   post-1.0 breaking requires a major
ok:   pre-1.0 breaking requires a minor, not a major
ok:   post-1.0 a correction requires a minor, NOT a major
ok:   a correction must still move the version
ok:   pre-1.0 the correction flag changes nothing
ok:   a correction over `none` is still none
ok:   a correction over `additive` is still a minor
ok:   an unmoved version must NOT satisfy a minor
ok:   1.0 → 1.1 satisfies a minor
ok:   a major over-satisfies a minor
ok

... [472781 bytes truncated] ...

erator
src/super-resolve.ts:229:9
-       if (inPackage !== undefined) return inPackage;
+       if (inPackage === undefined) return inPackage;

[Survived] ArrayDeclaration
src/super-resolve.ts:296:31
-     const pending: MetaData[] = [];
+     const pending: MetaData[] = ["Stryker was here"];

[Survived] ConditionalExpression
src/super-resolve.ts:298:9
-       if (n.superRef !== undefined) pending.push(n);
+       if (true) pending.push(n);

[Survived] ConditionalExpression
src/super-resolve.ts:302:40
-       if (node.superRef === undefined || node.superResolved !== undefined) return;
+       if (node.superRef === undefined || false) return;

[Survived] ConditionalExpression
src/super-resolve.ts:303:9
-       if (attempted.has(node)) return; // already resolved-or-failed this pass — never re-report
+       if (false) return; // already resolved-or-failed this pass — never re-report

[Survived] LogicalOperator
src/super-resolve.ts:305:26
-       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "";
+       const effectivePkg = (node.package ?? node.fileDefaultPackage) && "";

[Survived] LogicalOperator
src/super-resolve.ts:305:26
-       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "";
+       const effectivePkg = node.package && node.fileDefaultPackage ?? "";

[Survived] StringLiteral
src/super-resolve.ts:305:69
-       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "";
+       const effectivePkg = node.package ?? node.fileDefaultPackage ?? "Stryker was here!";

[Survived] ConditionalExpression
src/super-resolve.ts:311:9
-       if (isChildTargetingRef(node.superRef)) {
+       if (true) {

[Survived] ConditionalExpression
src/super-resolve.ts:313:11
-         if (parsed !== undefined) {
+         if (true) {

[Survived] ConditionalExpression
src/super-resolve.ts:315:13
-           if (ownerNode !== undefined) resolveNode(ownerNode);
+           if (true) resolveNode(ownerNode);

[Survived] ObjectLiteral
src/super-resolve.ts:346:19
-             target: { type: target.type, subType: target.subType },
+             target: {},

[Survived] ObjectLiteral
src/super-resolve.ts:347:21
-             referrer: { type: node.type, subType: node.subType },
+             referrer: {},

[Survived] StringLiteral
src/super-resolve.ts:365:15
-           kind: "unresolved",
+           kind: "",

Ran 0.99 tests per mutant on average.
--------------------|------------------|----------|-----------|------------|----------|----------|
                    | % Mutation score |          |           |            |          |          |
File                |  total | covered | # killed | # timeout | # survived | # no cov | # errors |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
All files           |  52.76 |   52.76 |      887 |        12 |        805 |        0 |        0 |
 parser-core.ts     |  45.81 |   45.81 |      492 |         0 |        582 |        0 |        0 |
 provider.ts        |  61.54 |   61.54 |       39 |         1 |         25 |        0 |        0 |
 serializer-json.ts |  60.26 |   60.26 |      133 |         5 |         91 |        0 |        0 |
 subtype-rules.ts   |  69.23 |   69.23 |      117 |         0 |         52 |        0 |        0 |
 super-resolve.ts   |  67.07 |   67.07 |      106 |         6 |         55 |        0 |        0 |
--------------------|--------|---------|----------|-----------|------------|----------|----------|
^[[32m00:39:04 (2826037) INFO MutationTestReportHelper^[[39m Final mutation score of 52.76 is greater than or equal to break threshold 35
^[[32m00:39:05 (2826037) INFO HtmlReporter^[[39m Your report can be found at: file://~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/typescript/packages/metadata/reports/mutation/mutation.html
^[[32m00:39:05 (2826037) INFO MutationTestExecutor^[[39m Done in 5 minutes and 7 seconds.

── ▶ doc-template drift ──────────────────────────────────────────────
synced: ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/templates/docs/entity-page.md.mustache -> ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/typescript/packages/codegen-ts/templates/docs/entity-page.md.mustache
synced: ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/templates/docs/template-page.md.mustache -> ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/typescript/packages/codegen-ts/templates/docs/template-page.md.mustache
synced: ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/templates/api/agent-api.md.mustache -> ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/typescript/packages/codegen-ts/templates/api/agent-api.md.mustache
synced: ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/templates/api/entity-api.md.mustache -> ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/typescript/packages/codegen-ts/templates/api/entity-api.md.mustache
synced: ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/templates/api/index.md.mustache -> ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/typescript/packages/codegen-ts/templates/api/index.md.mustache
generated ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/typescript/packages/codegen-ts/src/render-engine/embedded-templates.generated.ts (5 template(s): api/agent-api.md, api/entity-api.md, api/index.md, docs/entity-page.md, docs/template-page.md)
doc templates synced from canonical root: ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/templates (docs api)

── ▶ embedded-library drift ──────────────────────────────────────────────
wrote 6 embedded library file(s) + 2 manifest(s) to server/typescript/packages/metadata/src/library/embedded-library.generated.ts, server/java/metadata/src/main/java/com/metaobjects/library/EmbeddedLibrary.java and server/csharp/MetaObjects/Library/EmbeddedLibrary.cs

── ⊘ --quick: SKIPPING the other-language ports (csharp/java/python/kotlin),
        the full Java reactor, the docker integration suite and the cube live
        check. Run the full `scripts/ci-local.sh` (no flag) before opening/merging
        a PR or tagging.

══════════════════════ SUMMARY ══════════════════════
  ✓ leak-scan (security)
  ✓ pom-version parity
  ✓ bun-version parity
  ✓ uv.lock version parity
  ✓ publish-intent parity
  ✓ publish-set parity
  ✓ no committed pre-release version
  ✓ script-name hook collisions
  ✓ ci lane selection
  ✓ extract field kinds
  ✓ no-magic gate wired (5 ports)
  ✓ test-file references resolve
  ✓ metamodel-version bump
  ✓ positioning claims
  ✓ hosted assess prompt builds
  ✓ migration guides vs registry
  ✓ dependencies installed
  ✓ peer-range bounds
  ✓ owned template copies current
  ✓ reference templates lint
  ✓ shipped doc examples load
  ✓ no retired $apiPrefix
  ✓ site payload is true
  ✓ site reference is fresh
  ✓ release tag gate
  ✓ scripts/ typecheck
  ✓ requirements ledger verifies
  ✓ requirements cover vocabulary
  ✓ metamodel scaffolder
  ✓ requirement harness
  ✓ fixture-lint
  ✓ ts build + typecheck
  ✓ conformance: typescript
  ✓ ts unit suites
  ✓ completeness-gate (mutation)
  ✓ doc-template drift
  ✓ embedded-library drift
  ⊘ csharp/java/python/kotlin conformance (--quick)
  ⊘ java-reactor (--quick)
  ⊘ integration-tests (--quick)
  ⊘ cube-model live check (--quick)
══════════════════════════════════════════════════════
LOCAL CI PASSED (with 4 skipped — see ⊘ above).
Evidence: TypeScript eject contract test
bun test v1.3.14 (0d9b296a)

 33 pass
 0 fail
 1418 expect() calls
Ran 33 tests across 1 file. [12.18s]
Evidence: Python eject contract test
................                                                         [100%]
16 passed in 14.35s
Evidence: C# eject contract test
Test run for ~/.no-mistakes/worktrees/5b8c6c97e760/01M4Q40JZWJYYV8VQ65MHHE5F4/server/csharp/MetaObjects.Codegen.Tests/bin/Debug/net8.0/MetaObjects.Codegen.Tests.dll (.NETCoreApp,Version=v8.0)
Microsoft (R) Test Execution Command Line Tool Version 17.8.0 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:    17, Skipped:     0, Total:    17, Duration: 5 s - MetaObjects.Codegen.Tests.dll (net8.0)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ 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 is import.meta.url === pathToFileURL(process.argv[1]).href. Node resolves the main module's import.meta.url to its real path and leaves process.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.0 exits 0 with no output; the same command by the real path exits 1 and lists the 49 entries. Reach: the four hand-typed publishes in docs/RELEASING.md, where the operator is told the gate "must exit 0"; a silent 0 satisfies that. Not affected: the documented relative form node scripts/eject-release-gate.mjs <version> from the repository root (also probed from a symlinked working directory: exit 1), the four workflows (relative path), and release.mjs / finish-release.mjs (they import the function). Same cause on macOS, where os.tmpdir() is a symlink: the test "exits 1 with the reason for a refused version, 0 otherwise, 2 for a bad argument" in scripts/eject-release-gate.test.ts spawns 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 at scripts/publish-set.mjs:138 is 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_surface compares OutsideEngineSurface(...) with the pending outside lists. 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-engine MetaObjects.Codegen type and an engine type, assert the scan returns only the first. Java and Kotlin: run referencedClasses over 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.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Run the TypeScript eject contract test against the shared manifest and see census, completeness and depth hold, with open gaps recorded as pending ✅ pass live r2/item1.txt (33 pass, 0 fail, 0 skip)
Run the C#, Java, Kotlin and Python eject contract tests and see each port's guard pass ✅ pass live r2/item4.txt (17 pass), Maven run (Java 7 + Kotlin 7 pass), r2/item6.txt (16 pass)
Run the release gate CLI for a final 1.1.0 and see it refuse with exit 1, naming the 49 pending entries ✅ pass live r2/item3b.txt
Run the release gate CLI for release candidates (1.1.0-rc.3, and PEP 440 1.1.0rc3) and see them allowed with exit 0 ✅ pass live CLI run in this step: both versions printed 'may be released', exit 0
Adversarial: run the gate for a final version through a symlinked repo path and see it still refuses with exit 1 instead of silently exiting 0 ✅ pass live CLI run in this step via a temporary symlink to the worktree: exit 1 with the refusal text
Adversarial: remove a real pending TypeScript entry from the manifest and see the guard fail ✅ pass live r2/mutA.txt: the depth and census checks fail
Adversarial: add a bogus pending entry for a generator with no gap and see the guard fail (checked in both directions) ✅ pass live r2/mutB.txt: the pending-entry well-formed check fails
Run the release-gate and finish-release scripts' tests, including the ordering test that holds the four publish workflows to the gate ✅ pass live r2/item3a.txt (72 pass)
Run the quick local CI script and see it pass end to end ✅ pass live r2/item7-ci-quick.txt: LOCAL CI PASSED
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • cd 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), and 1.1.0 through 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 failed
  • Manifest mutation B: added a bogus pending entry, so the TypeScript guard failed; both mutations reverted with git checkout
  • scripts/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.

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#.
@dmealing
dmealing merged commit ce9b01c into main Oct 12, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-1-1-0-eject-guard branch October 12, 2026 04:54
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