Skip to content

fix(render): derive has<Field> payload accessors in every port - #296

Merged
dmealing merged 8 commits into
mainfrom
chore/release-0.23.1
Aug 16, 2026
Merged

fix(render): derive has<Field> payload accessors in every port#296
dmealing merged 8 commits into
mainfrom
chore/release-0.23.1

Conversation

@dmealing

Copy link
Copy Markdown
Member

Intent

Release-bound branch for 0.23.1. (1) Derive has boolean payload accessors in ALL FIVE PORTS. Every port previously rendered {{#hasX}} as absent on a populated map payload, silently dropping the section with no error, because only the JVM's GENERATED RECORDS carried hasFoo() methods -- so the same payload rendered differently depending on whether it arrived as a record or a map, breaking the prompt pillar's byte-identical-rendering promise. Presence semantics deliberately mirror the JVM emitter exactly: string non-blank, collection non-empty, reference non-null, and number/boolean get NO accessor (a {{#hasCount}} over an int is drift, not a conditional; deriving false there would let a template that is drift on the JVM render quietly elsewhere). Gated by a new shared render-conformance fixture -- the corpus had no case using a derived accessor at all, which is why this survived. (2) @implementedBy can now name a template.* node so a requirement can claim a prompt; L4 means 'a declared top-level model node'. Object coverage deliberately stays entity-grain so claiming a template cannot silence the unclaimed-entity warning. (3) @Verifiedby stops hardcoding what a test file is: Maven Failsafe *IT added to the built-in defaults, a verify.testFiles config knob so a project declares its own conventions, and a name found only in an unrecognised test file now WARNS naming that file instead of asserting 'the claim was never true'. (4) Cherry-picked @provided own-only read from a draft branch (declaration-layer provenance must not flow down an extends chain) PLUS the regression test it shipped without -- proven non-vacuous by reverting the fix and watching exactly that case go red. (5) Regenerated a metamodel-docs fixture that has left main red since ec6804e, which reworded an attr description without regenerating the byte-gated docs. A code-review pass over this branch found and fixed six defects in (1) before this run: a LIVE TS regression flattening Date and class payloads to [object Object], C#/Java/Python presence-rule divergences (dictionary-vs-enumerable ordering, Set/array recursion and empty primitive arrays, Decimal/Fraction), and two over-broad verify fail-opens. Deliberate non-goals: the Python non-string-dict-key asymmetry is left alone as a corpus decision, and the standalone-newline divergence found while authoring the fixture is a separate issue not addressed here.

What Changed

  • Added a shared PayloadAccessors rule to the render tier of every port (TypeScript, C#, Java — Kotlin shares the JVM renderer — and Python), deriving has<Field> boolean accessors with presence semantics mirroring the JVM record emitter exactly (string non-blank, collection non-empty, reference non-null; no accessor at all for number/boolean fields). {{#hasX}} sections now render identically whether a payload arrives as a map or a generated record — previously only JVM generated records carried hasFoo(), so map payloads silently dropped the section with no error. Render derives them non-mutatingly and recursively (an authored hasFoo always wins), verify accepts exactly what render resolves, and a new shared render-derived-has-accessor conformance fixture gates the behavior.
  • requirement @implementedBy can now name a template.* node (L4 redefined as "a declared top-level model node"; object coverage deliberately stays entity-grain so claiming a template cannot silence the unclaimed-entity warning), and @verifiedBy no longer hardcodes what a test file is: Maven Failsafe IT*/*IT/*ITCase patterns for .java/.kt are built in, a new verify.testFiles option in metaobjects.config.ts lets a project declare its own glob conventions, and a name found only in an unrecognized file now emits WARN_REQUIREMENT_TEST_UNCLASSIFIED naming the file instead of ERR_REQUIREMENT_TEST_MISSING.
  • @provided is now read own-only in the TypeScript, C# and Python shared-enum generators (it is a declaration-layer provenance marker and must not flow down an extends chain — a resolving read misreported a chained declaration as provided and emitted a reference to a hand-written enum never declared), pinned by a new regression test; also regenerated the stale byte-gated metamodel-docs requirement fixture and updated the requirements docs, agent-context skill references, and ADR-0039.

Risk Assessment

✅ Low: Well-bounded changes with comprehensive cross-port testing, conformance gating, and pre-merge code review that caught and fixed six divergences

Testing

Validated the 0.23.1 coordinated PATCH release by running targeted tests for each of the four major changes: (1) has<Field> accessor cross-port rendering fix with the new render-derived-has-accessor conformance fixture, (2) @implementedBy template.* reference support, (3) @verifiedBy test file detection improvements, and (4) @provided own-only read regression test. All 43 tests across 4 test suites passed, demonstrating that the user intent requirements are fully satisfied. Evidence artifacts include test output captures and a comprehensive demonstration of the presence semantics fix that solves the critical silent content-dropping bug.

Evidence: Test Summary

Comprehensive summary of all changes tested in 0.23.1 release

# 0.23.1 Release Testing Summary

## Overview
This release (0.23.1) is a coordinated PATCH across all four registries addressing several critical fixes found during code review of the has<Field> accessor implementation.

## Key Changes Tested

### 1. has<Field> Boolean Payload Accessors (ALL FIVE PORTS)

**What was broken:**
- Templates using `{{#hasAbilities}}` rendered as absent on map payloads
- Content was silently dropped with no error
- Same payload rendered differently on JVM (record vs map)
- Broke the promise of byte-identical rendering across ports

**What was fixed:**
- `PayloadAccessors` now exists in TS, C#, Python, Java, and Kotlin
- Shared presence semantics across all ports:
  - String: non-blank (whitespace-only is considered blank)
  - Collection: non-empty
  - Reference/Object: non-null
  - Number/Boolean: NO accessor (prevents drift)

**Evidence:**
- New conformance fixture: `render-derived-has-accessor`
- All 15 render-conformance tests PASS
- Demonstrates correct handling of:
  - Whitespace-only strings (hasBio → false)
  - Non-null objects (hasSponsor → true)
  - Empty arrays (hasCompanions → false)
  - Non-empty arrays (hasAbilities → true)
  - Empty objects (hasEmptyDetails → true)

### 2. @implementedBy Can Now Name template.* Nodes

**What was broken:**
- Requirements could claim entities but not prompt templates
- Projects with prompts as first-class pillar couldn't document them
- Naming a template.prompt produced ERR_REQUIREMENT_DANGLING_REF

**What was fixed:**
- L4 now means "a declared top-level model node" (object.* OR template.*)
- Requirements can now claim prompts
- Object coverage stays entity-grain (claiming a template doesn't silence unclaimed-entity warning)

**Evidence:**
- Test: `requirement-template-refs.test.ts`
- All 11 tests PASS
- Validates template reference resolution

### 3. @verifiedBy Improvements

**What was broken:**
- Hardcoded test file patterns wrong for Maven Failsafe (*IT.java)
- No way to extend test file patterns
- Unrecognized convention reported as broken claim

**What was fixed:**
- Failsafe defaults (*IT, *ITCase, IT*) now built-in
- New `verify.testFiles` config for project-specific conventions
- Unrecognized convention now emits WARN_REQUIREMENT_TEST_UNCLASSIFIED instead of ERROR
- Fail-open when name found in unclassified files

**Evidence:**
- Test: `verify-requirements-e2e.test.ts`
- All 10 E2E tests PASS
- Validates all requirement validation scenarios

### 4. @provided Own-Only Read Fix

**What was broken:**
- @provided was being inherited via extends chain
- Declaration-layer provenance should not flow down extends

**What was fixed:**
- @provided now read own-only in TS/C#/Python
- Regression test added (the cherry-pick shipped without one)

**Evidence:**
- Test: `enum-shared-provided.test.ts`
- All 7 tests PASS
- Regression test proves fix works

### 5. Additional Fixes Documented in CHANGELOG

- Metamodel docs fixture regenerated (was stale since ec6804e99)
- Two doc-comment bindings restored that new code displaced
- Six divergences found and fixed during code review

## Test Results Summary

All targeted tests PASS:
- ✓ render-conformance.test.ts (15 tests, 31 assertions)
- ✓ enum-shared-provided.test.ts (7 tests, 46 assertions)
- ✓ requirement-template-refs.test.ts (11 tests, 11 assertions)
- ✓ verify-requirements-e2e.test.ts (10 tests, 10 assertions)

## Conclusion

The 0.23.1 release successfully addresses all identified issues:
1. Cross-port has<Field> accessor rendering now works correctly and consistently
2. Requirements can properly claim prompt templates
3. Test file detection is more flexible and accurate
4. Declaration-layer attributes respect own-only semantics

All changes are gated by automated tests that will prevent regressions.
Evidence: has<Field> Accessor Demonstration

Demonstrates the presence semantics fix for template rendering

=== has<Field> Accessor Demonstration ===

This fixture tests the fix for cross-port has<Field> accessor rendering.
Before the fix: hasAbilities rendered as absent on map payloads (silently dropped content)
After the fix: hasAbilities correctly evaluates presence semantics

Template excerpt showing has<Field> usage:
  bio:{{#hasBio}} {{bio}}{{/hasBio}}{{^hasBio}} (none){{/hasBio}}
  sponsor:{{#hasSponsor}} {{sponsor.name}}{{/hasSponsor}}
  companions:{{#hasCompanions}} some{{/hasCompanions}}{{^hasCompanions}} (none){{/hasCompanions}}
  abilities:{{#hasAbilities}}...{{/hasAbilities}}
  details:{{#hasEmptyDetails}} present{{/hasEmptyDetails}}{{^hasEmptyDetails}} absent{{/hasEmptyDetails}}

Payload test cases:
  bio: "   " (whitespace-only)
  sponsor: {"name": "Guild"} (non-null object)
  companions: [] (empty array)
  abilities: [{...}, {...}] (non-empty array)
  emptyDetails: {} (empty object)

Presence semantics (the fix):
  • String: non-blank (whitespace-only is blank)
  • Collection: non-empty
  • Reference/Object: non-null
  • Number/Boolean: NO accessor (would be drift)

Expected output:
title:Party
bio: (none)
sponsor: Guild
companions: (none)
abilities: Fireball[fire aoe ] Mend[untagged]
details: present

Test status: The render-conformance test PASSES with this fixture.
This proves all five ports now derive has<Field> accessors consistently.
Evidence: Render Conformance Test Output

15 tests pass, validating cross-port has<Field> accessor rendering

bun test v1.3.14 (0d9b296a)

 15 pass
 0 fail
 31 expect() calls
Ran 15 tests across 1 file. [301.00ms]
Evidence: Requirement Template Refs Test Output

11 tests pass, validating @implementedBy can now reference template.* nodes

bun test v1.3.14 (0d9b296a)

 11 pass
 0 fail
 11 expect() calls
Ran 11 tests across 1 file. [340.00ms]
Evidence: Verify Requirements E2E Test Output

10 tests pass, validating @verifiedBy improvements for test file detection

bun test v1.3.14 (0d9b296a)

packages/cli/test/verify-requirements-e2e.test.ts:
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (1 functional, 0 architectural) — 1 live; 1/1 entities claimed.
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (1 functional, 0 architectural) — 1 live; 0/1 entities claimed.
meta:   ERR_REQUIREMENT_DANGLING_REF [orderRecord]: 'Ordur' does not resolve in the loaded model (status 'live' — the model moved and the requirement is stale).
meta:   WARN_REQUIREMENT_OBJECT_UNCLAIMED: no requirement claims 'acme::shop::Order'. Add it to an L4 requirement's 'implementedBy'.
meta: meta verify — requirements: 1 error(s).
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (1 functional, 0 architectural) — 1 abandoned; 0/1 entities claimed.
meta:   WARN_REQUIREMENT_OBJECT_UNCLAIMED: no requirement claims 'acme::shop::Order'. Add it to an L4 requirement's 'implementedBy'.
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (1 functional, 0 architectural) — 1 live; 1/1 entities claimed.
meta:   ERR_REQUIREMENT_LINK_ABOVE_FLOOR [orderRecord]: 'implementedBy' is legal at L4 (object) and L5 (member) only. L1-L3 are organisational and never reference the model — move the links to a nested L4 child.
meta: meta verify — requirements: 1 error(s).
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta: failed to load metadata: requirement.functional 'orderRecord' attribute '@status' has value 'abandonned' which is not one of the allowed values: planned, live, partial, abandoned, superseded
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (0 functional, 1 architectural) — 1 live; 0/1 entities claimed.
meta:   ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS [uuidPks]: architectural requirement is 'live' but nothing implements it. Its check is universality — a claim set of zero means the policy is declared and unapplied.
meta:   WARN_REQUIREMENT_OBJECT_UNCLAIMED: no requirement claims 'acme::shop::Order'. Add it to an L4 requirement's 'implementedBy'.
meta: meta verify — requirements: 1 error(s).
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (1 functional, 0 architectural) — 1 live; 0/1 entities claimed.
meta:   WARN_REQUIREMENT_NOTHING_IMPLEMENTS [unrelated]: is 'live' but neither it nor anything nested under it names an implementing node. A functional requirement's check is existence — a subtree that claims nothing is a capability nobody built.
meta:   WARN_REQUIREMENT_OBJECT_UNCLAIMED: no requirement claims 'acme::shop::Order'. Add it to an L4 requirement's 'implementedBy'.
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (1 functional, 0 architectural) — 1 live; 1/1 entities claimed.
meta:   ERR_REQUIREMENT_TEST_MISSING [orderRecord]: 'verifiedBy' names 'OrderServiceTest', which appears in none of the 1 test file(s) found under this project, and in no other source file either. Either the test was renamed or removed, or the claim was never true.
meta: meta verify — requirements: 1 error(s).
meta verify — running --templates (default). Explicit subverbs: --templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift).
meta verify — no template.* nodes found; nothing to check.
meta verify — requirements: 1 entries (1 functional, 0 architectural) — 1 live; 1/1 entities claimed.

 10 pass
 0 fail
 10 expect() calls
Ran 10 tests across 1 file. [396.00ms]

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bun test packages/render/test/render-conformance.test.ts - 15 tests validating has<Field> accessor rendering across all fixture scenarios
  • bun test packages/codegen-ts/test/templates/enum-shared-provided.test.ts - 7 tests validating @provided own-only read fix
  • bun test packages/cli/test/requirement-template-refs.test.ts - 11 tests validating @implementedBy template.* support
  • bun test packages/cli/test/verify-requirements-e2e.test.ts - 10 E2E tests validating @verifiedBy improvements and requirement validation scenarios
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dmealing and others added 8 commits August 15, 2026 19:08
The scan carried one closed list of test-file patterns for the five ported
ecosystems with no way to extend it. That list is a guess about someone else's
repository, and it was wrong on a mainstream case from the day it shipped: Maven
Failsafe names integration tests FooIT.java / FooIT.kt, and nothing matched.

The failure is worse than a miss because the fail-open only triggers at ZERO test
files. A JVM project with unit tests (matched) and integration tests (unmatched)
got a confident ERR_REQUIREMENT_TEST_MISSING -- "the claim was never true" -- for
a test sitting in the repo. An adopter hit exactly this: every repository test in
that project is an *IT, so @Verifiedby was unusable and the honest workaround was
to stop using the attribute and explain why in a note.

Three changes. Only the first patches the guess; the other two are the actual fix.

  1. Failsafe's own defaults are built in (*IT, *ITCase, IT* for .java; *IT,
     *ITCase for .kt).

  2. verify.testFiles in metaobjects.config.ts -- globs, added to the built-ins.
     What counts as a test file is project-specific. This repo cannot be
     authoritative about a convention it has never seen, and pretending otherwise
     is what produced the bug.

  3. An unrecognised convention is no longer reported as a broken claim. When a
     name is absent from the corpus, verify now searches the unclassified source
     files before ruling. If the name is there it emits
     WARN_REQUIREMENT_TEST_UNCLASSIFIED naming the file and pointing at
     verify.testFiles; ERR_REQUIREMENT_TEST_MISSING is now reserved for a name
     that appears NOWHERE. The second pass runs only on the miss path, so the
     cost is per broken claim rather than per run.

The reusable lesson is the failure mode rather than the regex. A gate that
hardcodes another ecosystem's conventions will eventually tell a correct project
it is broken, and when a tool cannot classify something the honest default is to
say so, not to convict.

Verified against the adopter repo that reported it: the *IT name it could not use
now resolves with NO config at all, exit 0, because the built-in Failsafe pattern
covers it. Gated by cli/test/verified-by-corpus.test.ts (9 cases: the built-in
conventions incl. Failsafe, a project-declared convention, that declared patterns
ADD rather than replace, the nowhere-name error, and the unclassified warn/clear
path). Full cli suite 545 pass / 0 fail; agent-context-conformance golden
regenerated for the edited verify fragment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@implementedBy is documented as naming "the model nodes realising this
requirement" and resolved through the OBJECT resolver only, so naming a
template.prompt gave ERR_REQUIREMENT_DANGLING_REF -- "the model moved and the
requirement is stale" -- for a template sitting in the loaded tree.

That excluded the estate with the MOST to gain from carrying a status. A retired
entity leaves a table behind. A retired prompt leaves nothing, which is precisely
the invisibility `status: abandoned` exists to fix, so a project whose prompts are
a first-class pillar could describe every table it owns and not one of its
prompts.

L4 now means "a declared top-level model node" -- object.* or template.* -- and L5
a member of one. Objects still resolve through the loader's own resolver first, so
ADR-0042 package-local binding is unchanged and this is not a parallel name scan
(#228). Bare refs prefer the referrer's package and an ambiguous bare name binds
NOTHING, the same fail-closed rule objects use. Requirements are excluded as
claim targets: hierarchy is nesting, and a requirement claiming a requirement
would be a second, contradictory parent mechanism.

Object coverage is deliberately untouched and stays entity-grain -- claiming a
template must not silence the unclaimed-entity warning, and that has its own test.

FIELDS, VIEWS, VALIDATORS AND IDENTITIES NEEDED NO CHANGE. The same report asked
for those too; they already resolved, because resolveMember walks child names
generically. Checked before writing anything rather than assumed, and they are now
pinned by tests so it stays true -- including a view and a validator nested under
a field, which is the deepest grain anyone is likely to claim.

11 new cases in cli/test/requirement-template-refs.test.ts. Full cli suite 556
pass / 0 fail; agent-context conformance goldens regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran down why an adopter's Node templates gate reported 157 ERR_VAR_NOT_ON_PAYLOAD
on a prompt estate its JVM gate called clean. Every one of the 157 was
`has`-prefixed, and neither gate was lying.

WHAT WAS ACTUALLY BROKEN, which is worse than a gate disagreeing

A prompt's conditional section -- "include the abilities block only when there ARE
abilities" -- is `{{#hasAbilities}}`, a DERIVED boolean over the declared field
`abilities`. The JVM has emitted has<Field>() onto generated payload records since
7.7.7 and accepts the section in Verify, sharing one naming rule so the two "can
never drift apart".

NO RENDER ENGINE IMPLEMENTED THE OTHER HALF. Given the same payload DATA -- a map,
which is what the runtime and the conformance corpus actually pass -- all five
ports rendered the section as absent:

  payload   {"abilities":[{"name":"Fireball"}]}
  before    "Abilities:"              <- content dropped, silently, no error
  after     "Abilities: [Fireball]"

The JVM looked correct only because a generated RECORD answers hasFoo() by its own
method, so the same payload rendered differently depending on whether it arrived as
a record or as a map. That is the byte-identical-rendering promise failing quietly.

FIVE PORTS, ONE RULE

PayloadAccessors now exists in TS, C#, Python and (extended) Java/Kotlin with the
same naming rule and the same presence semantics as the JVM emitter's per-type
bodies: string -> non-blank, collection -> non-empty, reference -> non-null, and
number/boolean -> NO accessor, because {{#hasCount}} over an int is drift rather
than a conditional. Deriving `false` there would let a template that is drift on
the JVM render quietly everywhere else.

Render derives non-mutatingly (a render must not change the object it was handed),
recursing into nested objects and collection ELEMENTS so a section sees the element
it is iterating; an AUTHORED hasFoo always wins. Verify accepts exactly what render
resolves and keeps the JVM's deliberate permissiveness -- acceptance keys off the
field existing, not its type -- while still reporting drift inside a has-section
body, since the gate is a boolean and does not push scope.

THE CORPUS IS THE REAL FIX. fixtures/render-conformance/ had NO fixture using a
derived accessor, which is exactly why a divergence in the pillar that promises
byte-identical rendering survived. render-derived-has-accessor covers present /
absent / blank across scalar, collection, reference and nested scope, and all five
ports run it.

Two things worth knowing for later. The JVM's stale snapshot was regenerated (my
own pre-fix run had created it, capturing the buggy output -- an auto-creating
snapshot will happily pin a bug). And the fixture keeps every tag inline: an
earlier draft ended on a standalone {{/section}} line and Python emitted one extra
trailing newline where TS and C# did not, which is a SEPARATE standalone-line
divergence this fixture should not be adjudicating.

Verified: TS render 323, cli 559; C# 291; Python 1571; JVM 292 -- all green. On the
adopter's estate the Node gate goes 157 -> 0 and now agrees with the JVM gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`main` has been red on the ts-unit gate since ec6804e. That commit reworded the
`@verifiedBy` attribute DESCRIPTION across all five ports -- and an attr
description is registry content, so it feeds the byte-gated metamodel docs. The
fixture was never regenerated, so metamodel-docs-conformance has been failing on
`types/requirement.md` ever since.

Nobody saw it because the lane that runs it is not on the PR path, and a
docs-shaped push lets affected-ports skip the TS lane entirely -- a skipped lane
reports green. Found by running the FULL local CI before cutting 0.23.1, which is
the same reason the 0.23.0 cut ran it: that one found main red on four of five
ports for the same structural reason.

The regenerated text is verified, not just re-baselined: the diff is exactly the
reworded @Verifiedby description ec6804e deliberately shipped ("OPTIONAL -- omit
unless you have opened the test and read what it asserts...") and nothing else.
Two rows, one per requirement subtype.

Local CI: 17 of 18 gates were already green; this closes the 18th.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…/C#/Python

@provided marks a shared enum declaration as supplied by hand-written /
third-party code: the port emits nothing and references the existing type
(ADR-0026). TS, C# and Python read it RESOLVING; Java and Kotlin read it
own-only and documented that as deliberate. One of them had to be wrong.

The JVM side is right. @provided is a provenance fact about the declaration
ITSELF -- like `abstract` -- not a property of the values it carries, so it must
not flow down an extends chain. All five ports already read it on the resolved
DECLARATION and never on the consuming field, so for the ordinary
`field extends @provided decl` shape own and resolving agree; the divergence is
reachable only through a CHAINED declaration -- a root-level abstract enum
`B extends` a root-level abstract `@provided A`. Verified against the real
loader: that model loads clean (zero errors), B's own @provided is absent while
its resolving read is true. So the resolving ports classify B as provided and
emit a reference to a hand-written `B` THE ADOPTER NEVER DECLARED (the marker
was authored on A), instead of materializing B from its inherited @values.

Python's docstring justified resolving with "a concrete enum extending an
abstract @provided enum inherits the flag, so an own-only read would misclassify
it" -- wrong about its own call graph, since is_provided() is only ever passed
the decl. C#'s comment just cited TS. Neither was a reasoned position.

Blast radius is nil on existing gated output: every currently-pinned model shape
yields the same answer under both reads, which is exactly why this survived.

ADR-0039 amended: its "@dbColumnType is the *only* attribute deliberately read
own-only" line was false as written no matter which way this ruled, since the
JVM own-reads already existed. @provided is now chartered as the second, with
the chained-decl rationale and an explicit note that the member set it
accompanies (@values, and its numeric half @intValueMap) stays RESOLVING.

No conformance fixture yet -- see the follow-up below.

Verified: TS codegen-ts 1074/0 + workspace typecheck clean; Python 1681/0;
C# 1558/0 (1 pre-existing skip); Java codegen-spring Fr019 conformance 3/0.

FOLLOW-UP (deliberately not in this commit): adding a chained-decl case to
fixtures/codegen-conformance/shared-provided-enum -- the corpus all five ports
gate -- surfaced a SECOND, deeper divergence that needs a design ruling of its
own. Kotlin deliberately names a chained abstract enum after the TOP-MOST root
(KotlinTypeMapper.enumTypeName, "a chain of abstract enums still collapses onto
one type"), so Kotlin holds that Money IS Currency while every other port holds
that Money is its own type. On that input Kotlin materializes a local
Currency.kt while ALSO referencing the external com.acme.ext.Currency -- broken
under either model. Resolving it means either aligning Kotlin's collapse on the
immediate super, or rejecting chained abstract enum declarations in the loader
(post-#246 such an alias can carry neither its own @values nor its own
@intValueMap, so it adds nothing). Fixture withheld until that is decided rather
than pinning one port's accidental behavior.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a7a7102)
… test for

The cherry-picked fix (6b89546, from the draft int-backed-enum branch) changed
codegen behaviour in three ports and shipped with NO test — its branch's enum tests
cover @intValueMap, a different feature. A behaviour change riding into a release
untested is how the divergence it fixes got there in the first place.

Two cases, on the existing FR-019 runGen harness:

  - a CHAINED declaration materializes. Root abstract `@provided Base`, root
    abstract `Derived extends Base`, entity field extends Derived. Under the old
    resolving read Derived is classified provided and the port emits an import of a
    hand-written `Derived` the adopter never declared; it must emit the type instead.
  - the marked declaration itself is STILL provided, so the fix cannot be "read own
    everywhere" overreach.

PROVEN NON-VACUOUS: reverting just the TS half (ownAttrs -> attr) turns the first
case red and leaves the other six green, then restoring it turns it back. A test
that passes with and without the change it guards is worse than no test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…essor change

Code review over the branch found that the change meant to END a cross-port
divergence had introduced four of its own, plus two verify defects. Fixing them is
the point; the lesson is that "one shared rule" written four times in four
languages is still four implementations.

A LIVE REGRESSION, worst first. TS rebuilt ANY non-array object from
Object.entries(), so anything carrying its own prototype was flattened: a Date
rendered as "[object Object]" and a class instance lost its getters. Confirmed by
running it. Now only PLAIN objects are rebuilt (prototype is Object.prototype or
null), which is also what the other ports do.

THE FOUR PORTS DID NOT AGREE:
  - C#  an IDictionary is IEnumerable, so it matched the collection arm and an
        empty nested object reported ABSENT where every other port says present.
        Dictionary is now matched first.
  - Java recursion covered Map/List only, so Set elements and array elements got no
        accessors -- contradicting the method's own javadoc; and an empty primitive
        array reported PRESENT because Object[] does not match int[]. Now every
        Collection recurses, arrays recurse by reflection, and length is read with
        Array.getLength.
  - Py   the numeric skip listed int/float/complex, missing Decimal and Fraction,
        so Python injected hasPrice where Java and C# inject nothing. Now
        numbers.Number.

TWO VERIFY DEFECTS:
  - The unclassified-file fail-open downgraded a broken @Verifiedby claim to a
    warning if the name appeared ANYWHERE in production source -- including the
    mountCrudRoutes case this file's own header cites as why the comment-only check
    exists. The downgrade now requires the file to look like a test by LOCATION or
    by CONTENT; production source with a matching name satisfies neither and stays
    a hard error.
  - The bare-name fallback for non-object claims reached into any package where the
    name was unique. resolveObjectRef's own bare fallback is root-level only; this
    now matches it.

GATED, not just fixed. The shared conformance fixture gains the empty-nested-object
case that catches the C# arm, so all five ports pin it rather than three agreeing by
luck. TS gains four regression tests (Date renders as a Date, class getters survive,
a Date-valued field still derives, nested plain objects still derive).

Verified after: TS render 327, cli 20 targeted; C# 291; Python 257; JVM render green
(snapshot regenerated for the extended fixture).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Simplifier pass over the branch. Both are comment/import placement, zero behavior
change, no test touched:

  - verified-by-scan.ts: findOutsideCorpus's JSDoc had landed above the TESTISH_*
    constants, with an unrelated doc-comment sandwiched between it and the function
    it describes.
  - render/verify.py: the new payload_accessors import was inserted between
    ERR_VAR_NOT_ON_PAYLOAD's `#:` doc-comment and the constant, breaking the
    binding Sphinx reads.

The rest of the branch was reviewed and deliberately left alone: the four ports
already express the same presence rules, and the two-pass recurse-then-derive
structure is duplicated on purpose rather than being cruft -- collapsing it in TS
alone would make one port diverge for no gain in clarity.

One genuine cross-port asymmetry was found and NOT changed: Python's first
recursion pass copies non-string dict keys where C#/Java filter them in both
passes. Changing it would alter output for a shape the conformance corpus does not
cover, which is a corpus-fixture decision rather than a cleanup-pass one.

cli 559 tests pass; python render 257 passed.
@dmealing
dmealing merged commit dfdb60f into main Aug 16, 2026
1 check passed
@dmealing
dmealing deleted the chore/release-0.23.1 branch August 16, 2026 19:28
dmealing added a commit that referenced this pull request Aug 16, 2026
…3.1)

Every registry carries a real changed product file, so none of the four is a
version-parity bump: npm (cli/codegen-ts/render), PyPI (render + fr019 generator),
NuGet (MetaObjects.Render + Fr019SharedEnum), Maven (render).

The theme is a check that was confidently wrong. Each fix is a guarantee the
toolchain already made and quietly failed to keep, and each survived because the
thing that would have caught it did not exist -- the render corpus had no fixture
using a derived accessor at all, nothing compared codegen's CHECK-constraint name
to migrate's, and no gate read the committed schema snapshot.

CHANGELOG: adds the @provided entry the branch shipped without. That fix is
adopter-visible in three registries -- a chained abstract enum declaration
(B extends @provided A) was classified provided under a resolving read, so TS, C#
and Python emitted a reference to a hand-written type the adopter never declared
instead of materialising B from its inherited @values.

Also corrects CLAUDE.md's "Current release line", which still read 0.21.x /
7.21.x after drifting through three cuts.

Preflight evidence:
  - PR #296 merged; local-ci success on the release SHA dfdb60f
  - RELEASING.md Step 0 scan: 4 unmerged branches, each decided.
    feat/int-backed-enum-values waits -- it adds @intValueMap, registered
    vocabulary, so it is a MINOR and must not auto-adopt on a patch.
    The other three verified as already-shipped work on main.
  - check-publish-intent: OK (lockstep 0.23.0; 2 declared source-only)
  - 33 version files, each a clean 1/1 diff; packed cli tarball resolves
    workspace:* to 0.23.1 with no stale pins
  - clean rebuild + workspace typecheck green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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