Skip to content

feat(codegen-ts): publish requirements in shared models and inherit them by consumer key - #553

Merged
dmealing merged 7 commits into
mainfrom
fm/mo-export-requirements
Nov 11, 2026
Merged

dmealing merged 7 commits into
mainfrom
fm/mo-export-requirements

Conversation

@dmealing

@dmealing dmealing commented Nov 11, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Publisher: the shared-model generator accepts requirements: true (TypeScript sharedModelFile, Java/Kotlin generator arg, C# SharedModelGenerator.Requirements). Default off, so existing exports keep their bytes. When on, the selected requirement.* nodes are published beside the model. A selected requirement's @implementedBy targets are closed like extends and ref attrs, and an unclosed claim fails the publish. template.* is never published.
  • Consumer: requirements.inherit in .metaobjects/config.json (zod ConfigSchema in sdk, read by every port) lists the dependency packages whose requirements join the project ledger, using the same literal-package rule as scope.include. Dependency requirements not inherited still load for overlays but are left out of the requirement gate, summary, lint, requirement-tests generators and requirement pages. Inherited requirements never switch object coverage on. Ledger predicate is InLedger (C#), inLedger / in_ledger (TS, Python).
  • Also: a new top-level requirement declared into a dependency's package is refused with ERR_DEPENDENCY_PACKAGE_NOT_OWNED. Overlays of that requirement are still accepted. Shared-model and dependency conformance fixtures and tests added in all ports, plus docs/features/metadata-dependencies.md, docs/features/requirements.md and CHANGELOG.md.

Notes

  • Python consumes only. It reads requirements.inherit and applies the ledger rule, but it has no shared-model publisher today, so it cannot export requirements.
  • Documentation generators: the TypeScript requirements page (requirementsFile()) and the agent requirements page now list only the project's ledger, so they agree with the gate and the docs-site explorer (review finding F5). The other ports have no requirements documentation page, so there was nothing to filter there.
  • Test identities (F6): Java and C# read the ledger from the dependency set on the loaded root, so they filter by default. TypeScript requirementTestIdentities and Python requirement_test_identities take the collection's ledger predicate (inLedger / in_ledger), which the generators and the corpus runners pass. Called without it, they still return every loaded requirement, because a TS or Python root does not carry the dependency set.
  • No ADR. The FR-023 design and docs/features/metadata-dependencies.md carry the rule.
  • Compatibility: a consumer on a toolchain older than this change treats a published requirement as its own. The manifest's schema_version is unchanged.
  • no-mistakes: one run. The review gate's two ask-user findings (F5, F6) were decided by the maintainer and fixed in that run, together with F7 and F1–F4.
Validation

Risk Assessment

✅ Low: The opt-in publisher requirements option (default off, template.* still excluded) and the separate requirements.inherit consumer key are implemented consistently in all five ports. The ledger filter is applied at every consumer I traced: the gate, the lint, the summary, requirement-tests, and the TS documentation generators. Cross-port corpus cases cover both sides. The only defect found was stale case counts in the docs prose, which I fixed inline. I did not run tests, per the review-phase rule.

Testing

Drove the publish and consume flow live through the TS meta CLI on disposable publisher and consumer projects (removed after). Default publish omits requirements, opt-in publish includes them, template is always excluded, and inherit by package name puts the dependency requirement in the ledger. Wildcard-only inherit and scope.include both leave it out. Per-package TS suites for sdk, codegen-ts and cli pass. Whole-server bun test timed out at 10 min, so it was not completed as a single run. Documentation generator output and the Python, Java and C# ports were not exercised live.

  • Live validation: ✅ go - 7 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Publisher default: sharedModelFile without requirements option omits requirement.* and template.* from the exported artifact and manifest ✅ pass live manifest nodes [Customer, GreetingPayload] after meta gen in disposable publisher project
Publisher opt-in: requirements: true exports the requirement.functional node; template.output still excluded ✅ pass live publisher-requirements-on.pkg.json nodes [Customer, Customers, GreetingPayload]
Consumer deps sync of a requirements-carrying dependency succeeds and pins it in lock ✅ pass live meta deps sync output: synced acme-common 1.1.0; meta deps list shows 3 nodes
Consumer without requirements.inherit: imported requirement is not in the verify ledger, only own requirement ✅ pass live meta verify requirementLedger lists only app::OrdersTaken, total 1
Consumer requirements.inherit naming acme::common::** adds the dependency requirement to the ledger ✅ pass live meta verify requirementLedger lists acme::common::Customers (implementedBy Customer resolves) and app::OrdersTaken, total 2
Adversarial: requirements.inherit ["**"] names no package and must not inherit ✅ pass live meta verify ledger total 1 (OrdersTaken only)
Adversarial: scope.include naming the package must not inherit its requirements ✅ pass live meta verify ledger total 1 (OrdersTaken only)
Documentation generators (requirementsFile, agent requirements page) list only ledger requirements, consistent with verify ⏸️ untested no Not driven live: needs a docs generator wired into the disposable project. Covered only by codegen-ts unit tests (3413 pass). The ledger-only filter for docs is a reviewer decision (F5) not checked he…
Python, Java, C# ports: publisher option and consumer inherit behave the same ⏸️ untested no Only the TS CLI was driven live. Other ports' test suites were not run in this step.
Closure refusal message for a template-claiming requirement (F7) says templates are never exported ⏸️ untested no Not driven live; no test run for this message in this step.
Evidence: publisher manifest with requirements opt-in (nodes: Customer, Customers, GreetingPayload)
{
  "schema_version": 1,
  "name": "acme-common",
  "version": "1.1.0",
  "metamodelVersion": "1.3",
  "artifact": "acme-common.metaobjects.json",
  "integrity": "sha256-eca0f71a87b8e2d0c035fe0eada55a4d035d431ed1f227fba7932f50a99a3896",
  "packages": [
    "acme::common"
  ],
  "nodes": [
    "acme::common::Customer",
    "acme::common::Customers",
    "acme::common::GreetingPayload"
  ]
}
Evidence: consumer verify ledger under inherit ** (only OrdersTaken listed)
requirementCounts:
  total: 1
  functional: 1
  architectural: 0

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ docs/features/metadata-dependencies.md:419 - The documented consumer example was wrong. It used "requirements": { "inherit": ["acme::common"] }, and the rule bullet said that acme::common or acme::common::** inherits the package. explicitlyIncludes drops the final segment through packageOfResolutionKey, so a bare acme::common names the package acme. An adopter who copied the example would silently inherit nothing. Every fixture case uses acme::common::**, which agrees with the code, and sdk/src/config.ts documents the rule correctly. Changed the example to acme::common::** and rewrote the bullet (line 432): a bare package name names its parent package, and ** names nothing.
  • ℹ️ docs/features/metadata-dependencies.md:858 - The 'Verified by' text said all four ports assert the ledger arm through the requirement gate and the requirement-tests walk. The TS (dependency-ledger-conformance.test.ts) and Python ledger arms assert only scanRequirements/scan_requirements. Only Java and C# also assert the walk. Reworded the text to state that split.
  • ℹ️ server/typescript/packages/codegen-ts/src/requirement-walk.ts:73 - requirementLedgerKey sat between walkRequirements's JSDoc block and walkRequirements, so the walk's documentation was attached to the wrong function. Moved the function above that JSDoc block. No code change.
  • ℹ️ server/java/metadata/src/main/java/com/metaobjects/config/NeutralConfig.java:486 - The new parseRequirementsInherit was inserted under the /** {@code migrate.scope} ... */ javadoc. That left the doc comment dangling, and parseMigrateScope lost its doc. Moved the migrate.scope javadoc back onto parseMigrateScope.
  • ⚠️ server/typescript/packages/sdk/src/config.ts:215 - The new config doc says that an imported requirement not named in requirements.inherit is "not listed, checked, summarised or given a generated test". The TS documentation generators still list it. requirementsFile() (codegen-ts/src/generators/requirements-file.ts:51) and the agent requirements page (agent-requirements-page.ts:114) build rows from requirementRows(ctx.loadedRoot), which calls walkRequirements and never reads ctx.inLedger. Concrete case: a consumer depends on a model published with requirements: true and does not inherit it. The consumer's generated requirements.md/.toon then shows the publisher's requirements with their statuses. The docs-site model explorer reads the filtered requirementPayload and leaves them out, so two documentation surfaces of one run disagree. (Resolved in this run: fixed as decided by the maintainer.)
  • ⚠️ server/typescript/packages/codegen-ts/src/requirement-walk.ts:360 - The ports now disagree on the public 'test identities' API. Java RequirementTestIdentities.walk and C# RequirementTestIdentities.Walk now go through collectLedger/CollectLedger, which reads the dependency set on the root, so they return only ledger requirements. TS requirementTestIdentities(root) and Python requirement_test_identities(root) (server/python/src/metaobjects/codegen/requirement_walk.py:266) still walk every loaded requirement. Only the generators filter, through ctx.inLedger. Both TS and Python document the function as "every test the generator would emit". For a consumer with a requirements-carrying dependency it does not inherit, the TS/Python API returns identities the generator never writes. Java/C# do not. An adopter harness that checks witnesses against this list gets different answers per port. (Resolved in this run: fixed as decided by the maintainer.)
  • ℹ️ server/typescript/packages/codegen-ts/src/shared-model/build-shared-model.ts:276 - The closure check now follows @implementedBy through resolveClaimTarget, which also resolves root-level template.* nodes. Requirements may legitimately claim prompts. A selected requirement that claims a template.prompt therefore fails with "include it or exclude the referrer", but a template can never be published, so 'include it' is impossible advice. Refusing is right: the claim would dangle in the consumer. When the target is template.*, the message should say that templates are never exported and the requirement must be excluded. Same message in Java SharedModel.java (closure refusal) and C# SharedModel.cs (closure refusal).

🔧 Fix applied.
1 info still open:

  • ℹ️ docs/CONFORMANCE.md:475 - Fix-round 7355c32 added a seventh ledger case to dependency-conformance/cases.json. The matrix row and metadata-dependencies.md were updated to 36 resolution cases, 7 with a ledger arm, but the prose at lines 475-488 still said 35, 6, and 'A seventh case'. Corrected to 36, 7 and 'An eighth case', matching cases.json (47 cases total = 36 resolution + 11 check; 7 with expectLedger, plus the refusal case).
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 10 scenarios driven live against the product
Scenario Result Live Evidence
Publisher default: sharedModelFile without requirements option omits requirement.* and template.* from the exported artifact and manifest ✅ pass live manifest nodes [Customer, GreetingPayload] after meta gen in disposable publisher project
Publisher opt-in: requirements: true exports the requirement.functional node; template.output still excluded ✅ pass live publisher-requirements-on.pkg.json nodes [Customer, Customers, GreetingPayload]
Consumer deps sync of a requirements-carrying dependency succeeds and pins it in lock ✅ pass live meta deps sync output: synced acme-common 1.1.0; meta deps list shows 3 nodes
Consumer without requirements.inherit: imported requirement is not in the verify ledger, only own requirement ✅ pass live meta verify requirementLedger lists only app::OrdersTaken, total 1
Consumer requirements.inherit naming acme::common::** adds the dependency requirement to the ledger ✅ pass live meta verify requirementLedger lists acme::common::Customers (implementedBy Customer resolves) and app::OrdersTaken, total 2
Adversarial: requirements.inherit ["**"] names no package and must not inherit ✅ pass live meta verify ledger total 1 (OrdersTaken only)
Adversarial: scope.include naming the package must not inherit its requirements ✅ pass live meta verify ledger total 1 (OrdersTaken only)
Documentation generators (requirementsFile, agent requirements page) list only ledger requirements, consistent with verify ⏸️ untested no Not driven live: needs a docs generator wired into the disposable project. Covered only by codegen-ts unit tests (3413 pass). The ledger-only filter for docs is a reviewer decision (F5) not checked he…
Python, Java, C# ports: publisher option and consumer inherit behave the same ⏸️ untested no Only the TS CLI was driven live. Other ports' test suites were not run in this step.
Closure refusal message for a template-claiming requirement (F7) says templates are never exported ⏸️ untested no Not driven live; no test run for this message in this step.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • Publisher: meta gen (sharedModelFile, default requirements off) in a disposable project, artifact nodes inspected: requirement.functional and template.output excluded.
  • Publisher: same project with requirements: true, artifact and manifest nodes inspected: requirement included, template.output still excluded.
  • Consumer: meta deps sync against the published gen/ directory (new dependency, pinned lock).
  • Consumer: meta verify with no requirements.inherit -> ledger lists only the consumer's own OrdersTaken.
  • Consumer: meta verify with requirements.inherit ["acme::common::**"] -> ledger adds acme::common::Customers (live, resolves to Customer).
  • Adversarial: requirements.inherit: ["**"] -> imported requirement NOT in ledger (wildcard names no package).
  • Adversarial: scope.include: ["acme::common::**"] without inherit -> imported requirement NOT in ledger.
  • cd server/typescript/packages/sdk && bun test -> 432 pass, 0 fail.
  • cd server/typescript/packages/codegen-ts && bun test -> 3413 pass, 0 fail.
  • cd server/typescript/packages/cli && bun test -> 2006 run, 0 fail (11 skipped).
  • cd server/typescript && bun test (whole server suite) -> exceeded 10 min timeout, not completed; replaced by the per-package runs above.
  • Baseline scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (passed before this step).
⚠️ **Document** - 2 infos
  • ℹ️ Fixed stale port docs: C# and Java publish sections said requirement.* nodes are never published; now opt-in via requirements. C# and Java consumer sections said imported requirements count toward the gate unless scope.include names the package; now requirements.inherit. Doc-only edits in docs/ports/csharp.md and docs/ports/java.md.
  • ℹ️ Verified already-updated docs match code and corpus: CHANGELOG, docs/features/requirements.md, docs/features/metadata-dependencies.md, docs/CONFORMANCE.md (47 dependency cases, 36 resolution, 7 ledger, 11 check; 15 shared-model cases, 9 publish, 6 refusal), both fixture READMEs. Case counts confirmed against cases.json.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A publisher can now opt in to exporting the selected requirement.*
nodes alongside its shapes (`requirements: true` in TypeScript, the
`requirements` arg in Java and Kotlin, `Requirements` in C#). The
default is unchanged: shapes only. template.* is never exported.

The closure check now follows a selected requirement's @implementedBy
claims (owner segment, in the requirement's effective package), so an
exported requirement cannot claim a node the export leaves out.

The shared-model corpus gains four cases and a pinned artifact,
acme-common-v1-requirements.json, which the dependency corpus will
consume. Python has no publisher; its serializer arm now takes every
node the manifest lists, not only objects.
A new `requirements.inherit` key in .metaobjects/config.json names the
dependencies whose requirements join this project's ledger. A pattern
counts only when it names the dependency's package (the scope.include
rule; `**` names nothing). It is a separate key from scope.include
because that one also selects a package for code generation.

An imported requirement not named there is loaded, so an overlay of it
resolves, and is otherwise left alone: the requirement gate, its
summary and lint, and the requirement-tests generator skip it. An
imported requirement, inherited or not, never switches object coverage
on, the rule a shipped library already follows.

A new top-level requirement declared into a dependency's package is
now refused with ERR_DEPENDENCY_PACKAGE_NOT_OWNED, as an object is.

The dependency corpus gains seven cases and a ledger arm
(expectLedger, expectCoverageMeasured).
…docs

Java and C# read `requirements.inherit` and apply the same ledger rule
as TypeScript and Python: a dependency's requirement joins the ledger
only when the key names its package, an imported requirement never
switches object coverage on, and a new requirement declared into a
dependency's package is refused with ERR_DEPENDENCY_PACKAGE_NOT_OWNED.
Both read the ledger from the dependency set already on the loaded
root, so the requirement gate and the requirement-tests generators
(JUnit, Kotlin, C#) need no new arguments. Kotlin uses Java's gate.

Every port's dependency-conformance runner asserts the ledger arm
through the gate's scan and through the requirement-tests walk.

The `meta gen --list --probe` count for requirement-tests now uses the
ledger too.

Docs: the publisher's `requirements` option, a new "Inheriting a
dependency's requirements" section, the corpus case schema and counts,
and a CHANGELOG entry.
@dmealing
dmealing merged commit 9abd963 into main Nov 11, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-export-requirements branch November 11, 2026 06:35
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