Skip to content

fix: ADR-0039 — resolving accessors are the default (own*() breaks extends) — all 5 ports#138

Merged
dmealing merged 6 commits into
mainfrom
feat/own-fix-reference
Jul 1, 2026
Merged

fix: ADR-0039 — resolving accessors are the default (own*() breaks extends) — all 5 ports#138
dmealing merged 6 commits into
mainfrom
feat/own-fix-reference

Conversation

@dmealing

@dmealing dmealing commented Jul 1, 2026

Copy link
Copy Markdown
Member

Systemic fix of the own-accessor bug class (found via the Kotlin isArrayType bug): reading a field/node's effective property or iterating members via an own-only accessor silently dropped extends-inherited values, corrupting codegen + runtime.

Per ADR-0039: resolving/effective is the default everywhere; own*() kept only for the sanctioned codegen subclass-emit + own-mode serializer + overlay (all commented); @dbColumnType stays the one deliberately-own attr. Added a resolving isArray accessor (resolvedIsArray/isArrayType) in every port (isArray had no resolution path). Fixed the field/identity/relationship getters, effective serializer, loader validation, schema builder, runtime (PK/jsonb/extract), and all codegen isArray reads. JVM also fixed the high-blast filterIsInstance() source lookup across 8 Kotlin generators. Python fixed its attr()=own naming inversion (added get_meta_attr).

New shared fixture fixtures/conformance/extends-abstract-field-inheritance/ (abstract field with isArray/maxLength/precision/objectRef/storage + concrete extends; BaseEntity) FAILS pre-fix, passes post-fix in all ports; per-port codegen tests assert inherited values are honored. Zero golden changes (byte-identical for non-abstract-extends inputs — why it stayed hidden). TS d62aab5 · C# 0c16c92 · Py 61dee8e · JVM 5a82f37.

🤖 Generated with Claude Code

claude added 6 commits June 30, 2026 21:53
…erence)

extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray
live on the parent, reachable only through the resolving accessors. Reading a
field/node's effective property or member set through an own-only accessor
silently dropped everything inherited via extends — corrupting codegen, runtime,
effective serialization, and schema. Flip own→resolving everywhere except the
sanctioned emit-declared-here cases (codegen subclass-emit ownFields, own-mode
canonical serializer, overlay/super-resolution walks) and the deliberately
own-only @dbColumnType (physical, never inherited).

New: MetaData.resolvedIsArray() — the resolving accessor for the native isArray
flag (isArray is a boolean property, not an attr, so it has no attrs()-based
resolution path). All codegen/runtime/effective-serializer reads of a node's
array-ness now route through it.

Fixed helpers + sites:
- meta-field.ts getters (objectRef/column/default/defaultValue/maxLength/
  precision/scale/unique/isRequired) → attr() resolving.
- meta-identity.ts + meta-relationship.ts getters → resolving.
- naming.resolveColumnName, serializer-json effective isArray, object-serializer,
  validation-passes (field.object @objectref + field.map + flattened-array),
  migrate-ts expected-schema (@storage/@unique/@objectRef/@default/@autoSet/
  @maxLength/@precision/@scale/@localTime/isArray; @dbColumnType kept own) +
  referential-actions.isRequired, runtime-ts object-manager PK lookup,
  type-coercer jsonb (@storage resolve, @dbColumnType own), extract-object
  (@required/@values/@enumAlias/@normalize/isArray), codegen-ts inferred-types/
  zod-validators/payload/projection/output-parser/fr010/template-data/
  docs-data-builder/column-mapper, codegen-ts-react form-file.

Conformance fixture (fails on pre-fix code): abstract field.string(isArray+
@maxlength) / field.decimal(@precision/@scale) / field.object(@objectref+@storage
+isArray) each extended by a concrete field, plus a BaseEntity(uuid PK+timestamp)
extended by a concrete entity. The effective serializer + validation own-reads
both broke pre-fix (ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF + effective isArray
mismatch). Plus a codegen gate asserting the Drizzle schema honors every
inherited value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray
live on the parent, reachable only through the resolving accessors. Reading a
field/node's effective property or member set through an own-only accessor
silently dropped everything inherited via extends — corrupting codegen (EF
entities/DbContext/routes/payload/extract), effective serialization, and loader
validation. Flip own→resolving everywhere except the sanctioned emit-declared-here
cases (codegen subclass-emit OwnFields/OwnChildren, the own-mode canonical
serializer, overlay/super-resolution walks, projection OwnSources) and the
deliberately own-only @dbColumnType (physical, never inherited). Mirrors the TS
reference (d62aab5).

New: MetaData.ResolvedIsArray() — the resolving accessor for the native isArray
flag (isArray is a bool property, not an attr, so it has no Attrs()-based
resolution path); walks own→super, own true wins, cycle-guarded, cached. All
codegen/effective-serializer reads of a node's array-ness route through it.

Fixed helpers + sites:
- MetaField getters (ObjectRef/ValueType/Storage/DbColumn/Default/DefaultValue/
  MaxLength/Precision/Scale/Unique + the @required half of IsRequired) → resolving
  Attr(); DbColumnType + EnumValues KEPT own (commented).
- MetaSource + MetaRelationship getters → resolving (reconciled with the already-
  resolving MetaIdentity).
- SerializerJson effective-mode isArray → ResolvedIsArray (own mode keeps IsArray);
  ValidationPasses field.object @objectref + flattened-array isArray + field.map
  @valueType/@objectref → resolving.
- Codegen: CSharpNaming (IsRequired/IsLocalTime/RequiresSystemNet), EntityGenerator/
  DbContextGenerator/RoutesGenerator isArray, PayloadCodegen (@objectref + isArray),
  ExtractObject (@required/@values/@enumAlias/@normalize/@default/@coerceDefault/
  @xmlText/isArray), TemplateData, Fr010FieldMapping (dropped the dead OwnAttr
  ("isArray") clause; @required/@values/@default/@coerceDefault/@normalize/@xmlText
  → resolving).

Gate: the shared cross-port fixture
fixtures/conformance/extends-abstract-field-inheritance now passes in C#
(effective serializer + loader), and a new C# codegen honor-inheritance test
asserts the generated Contact entity honors inherited isArray/@maxLength/
@precision/@objectRef/@storage + the BaseEntity UUID PK. No golden changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
… port)

extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray
live on the parent, reachable only through the resolving accessors. Reading an
effective property or member set through an own-only accessor silently dropped
everything inherited via extends. Python's naming is INVERTED vs TS/Java —
attr(name) is OWN-ONLY, attrs().get(name) resolves — so this port both fixes the
own reads and makes the resolving form the obvious/default-named call.

Naming (the durable hardening): a full attr()->own_attr() rename was too invasive
(148 call sites, many legitimately own-only in the loader-validation of authored
declarations, plus the own-mode serializer/merge which already use own_meta_attrs/
own_children — NOT attr()). Instead:
  - added MetaData.get_meta_attr(name) — the RESOLVING attr read (own + inherited
    via the super chain); the default read to prefer for any effective property.
  - added MetaData.resolved_is_array() — isArray is a native boolean property (not
    an attr), so it resolves the native flag through the super chain OR the loaded
    @isarray attr. Mirrors the TS resolvedIsArray().
  - re-documented attr() LOUDLY as OWN-ONLY, calling out the TS/Java inversion and
    naming the only sanctioned own uses (own-mode serialize, overlay/merge + super
    walks, validating the own declaration, @dbColumnType).

Fixed effective-property reads (own -> resolving):
  - meta_field.object_ref: attr()->get_meta_attr (unifies with codegen attrs().get
    @objectref — both now resolve).
  - codegen fr010_field_mapping: is_array->resolved_is_array; coerceDefault/default/
    normalize (_attr_string) -> get_meta_attr.
  - codegen type_map.field_is_array + template_codegen/template_data (isArray/
    required/maxLength) -> resolving.
  - codegen fr019_shared_enum.is_provided (@provided) -> get_meta_attr.
  - runtime object_manager: identity @fields (PK lookup), @localTime, @column,
    field-is-array -> resolving; @dbColumnType kept own-only (commented — the one
    deliberately never-inherited attribute).
  - loader validation_passes: field.object @objectRef/@storage/isArray + field.map
    @valueType/@objectref + effective-children iteration -> resolving (a concrete
    field.object inheriting @objectref from an abstract parent no longer falsely
    trips ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF). The own-declaration validators
    (@values / @default / @dbColumnType membership) stay own-only, commented.
  - serializer_json: EFFECTIVE mode now resolves isArray through the super chain;
    own mode still emits the authored flag (round-trips the extends tree).

KEPT own-only (each commented): the sanctioned emit-declared-here case
(EntityModelGenerator.own_fields, pinned by test_entity_model.py — a Contact
subclass extends the generated BaseEntity and does NOT re-emit inherited id/
createdAt); the own-mode canonical serializer; overlay/merge + TPH discriminator
super-resolution walks; the loader's validate-the-own-declaration passes; and the
physical @dbColumnType.

The shared cross-port fixture fixtures/conformance/extends-abstract-field-
inheritance (unchanged) now passes — the effective serializer previously dropped
the inherited isArray:true on tags/addresses. New Python codegen gate
(test_extends_abstract_field_inheritance.py) asserts a concrete field extending an
abstract array/decimal/object field, and an entity inheriting fields via
BaseEntity, generate correctly (list[str] / list[Address] / Decimal / subclass-
emit). Existing goldens unchanged for non-abstract-extends inputs (full non-
integration suite green except 2 pre-existing, unrelated staleness-nudge failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
…in port)

extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray
live on the parent, reachable only through the resolving accessors. Reading a
field/node's effective property or member set through an own-only accessor
silently dropped everything inherited via extends — corrupting codegen (an
entity inheriting its source.rdb emitted NOTHING), effective serialization,
and loader validation. Mirrors the TS reference.

The isArray resolver
- MetaField.isArrayType() now FULLY resolves: it walks the extends super chain
  for the native isArray flag (resolvedNativeIsArray) as well as the @isarray
  attr, so a concrete field extending an abstract array field is array-typed.
  isArray() stays the own-only raw flag; all codegen array reads route through
  isArrayType().

Metadata core
- MetaObject.getSources() no-arg now RESOLVES (getSources(true)), matching
  getMetaFields()/getIdentities()/getRelationships(). The declaration-layer
  one-primary-source validation keeps own-only via explicit getSources(false).
- ValidationPhase: field.object @objectRef/@storage and field.map
  @valueType/@objectref reads flipped own→resolving (fixed the shared fixture's
  false ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF for a field.object extending an
  abstract AddressBag). @dbColumnType stays own-only (commented).
- CanonicalJsonSerializer: effective mode now inlines inherited array-ness
  (getNativeIsArray resolves via isArrayType in effective mode; own mode keeps
  the raw flag so the authored form round-trips).

Java codegen (codegen-spring)
- SpringTypeMapper / SpringPayloadGenerator / OutputFormatSpecEmitter: raw
  field.isArray() → resolving isArrayType(); OutputFormatSpecEmitter
  @enumDoc/@example/@Instruction flipped own→resolving (consistent with the
  already-resolving @values/@required).

Kotlin codegen (codegen-kotlin)
- New resolving source lookup KotlinGenUtil.firstRdbSource/hasRdbSource
  (getSources(true)); routed ALL 8 generators' own-only
  entity.children.filterIsInstance<RdbSource>() reads through it — an entity
  inheriting its source.rdb via extends now emits its table/controller/etc.
- KotlinTypeMapper: intAttr (@precision/@scale) and stringAttr default
  (@valueType) flipped own→resolving (consistent with stringMaxLength).
- KotlinExtractSchemaEmitter / KotlinOutputFormatSpecEmitter: raw .isArray →
  isArrayType(); @enumDoc/@example/@Instruction → resolving.
- KotlinEntityGenerator.validator(): raw field.children →
  getChildren(MetaValidator, true).

Kept own-only (each commented): @dbColumnType (never inherited by policy);
one-primary-source validation (declaration-layer); @isAbstract/@discriminator/
@provided/@kind/template-ref reads; origin.* reads (origin never inherits).

Tests
- New KotlinAbstractFieldExtendsInheritanceTest gates the two ADR-0039 shapes:
  a concrete entity inheriting its source.rdb via extends emits an Exposed
  table, and concrete fields extending an abstract array field / abstract
  decimal field inherit isArray / @precision+@scale. Verified it FAILS on
  pre-fix code.
- Shared fixtures/conformance/extends-abstract-field-inheritance now passes in
  the Java conformance runner (loader + effective serializer). Fixture unchanged.

All green: Java metadata+codegen-spring 158, Java conformance 394, Kotlin
codegen 266, Kotlin integration (Testcontainers) 92, Java integration
(Testcontainers) 86. No existing goldens/snapshots changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
@dmealing
dmealing merged commit cc4b4fd into main Jul 1, 2026
18 checks passed
@dmealing
dmealing deleted the feat/own-fix-reference branch July 1, 2026 02:30
dmealing added a commit that referenced this pull request Jul 1, 2026
#139)

* fix(csharp): finish ADR-0039 own-accessor sweep — flip remaining effective reads + comment sanctioned owns

Completes the C# ADR-0039 sweep after the first pass (field-property +
high-value sites, merged in #138). Resolving is the default; every
remaining own*() read is now either flipped to the resolving accessor
(when it reads an effective/inheritable value or scans for members) or
kept with an explicit sanctioned-case comment.

Flipped own -> resolving (effective reads / member scans; all
behavior-identical on existing fixtures — no golden changes):
- MetaField.ReadOnly: @readonly Own -> Attr (was a real bug — TS codegen
  reads it resolved; an inherited @readonly would have emitted a public
  setter + INSERT/UPDATE column).
- TemplateData: identity @fields / relationship @cardinality/@objectref
  now via the resolving MetaIdentity.Fields / MetaRelationship.Cardinality
  /ObjectRef getters (matches TS i.fields / r.cardinality / r.objectRef;
  the class-doc's "own to match TS" claim was inverted).
- Template/output generators (OutputParser/OutputPrompt/Extractor/
  RenderHelper/OutputFormatSpec/Payload generators, PayloadCodegen,
  ExtractDelegateEmitter, CSharpApiModelBuilder): ctx.Root.OwnChildren()
  root scans -> Children(); template attr reads (@payloadRef/@format/
  @kind/@textRef/@subjectRef/@maxChars/@promptStyle/@enumDoc/@example/
  @Instruction) and field @objectref -> Attr().
- EntityGenerator @stringformat, FilterAllowlistGenerator @filterable,
  Fr019SharedEnum @provided -> Attr() (all inheritable; TS resolves).

Kept + commented (sanctioned):
- TphPlan / EntityGenerator.TphBaseOf @discriminator/@discriminatorValue —
  super-chain walk that must read the own-declared layer to tell base
  from subtype (matches TS tphDiscriminator).
- CallableGenerator args-VO OwnFields — the callable binds exactly the
  params the VO declares (matches TS argsObject.ownChildren()).
- ExtractObject.DescendantObjects OwnChildren — physical tree walk,
  resolving would double-visit.
- MetaOrigin (origin never inherits, ADR-0029), MetaField.DbColumnType
  (@dbColumnType physical), MetaField.EnumValues (own; EffectiveEnumValues
  is the resolving companion), MetaLayout/MetaValidator/MetaAttr (authored
  leaf nodes, own == effective; matches TS), SerializerJson own-mode
  canonical serializer, SuperResolve + Parser overlay/merge + super
  walks, and the ValidationPasses / RegisteredValidation declared-layer
  validation walks (class-level notes; match TS ownAttr/ownChildren 1:1).

Tests: MetaObjects.Codegen.Tests 259, Conformance.Tests 674,
Render.Tests 290, Cli.Tests 42, IntegrationTests 82 (Testcontainers PG)
— all green. No golden changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n

* fix(jvm): ADR-0039 own-accessor sweep — remaining non-sanctioned own reads (Java + Kotlin)

Second-pass ADR-0039 sweep over the JVM tree (metadata, codegen-spring,
codegen-kotlin, omdb, core-spring): find every remaining own-only read, flip
the ones that read an INHERITABLE effective value/member set (bugs — extends is
a super-reference, so own drops inherited members), and comment every read that
is legitimately own.

BUGs flipped (own -> resolving):
- Kotlin controller/relations/m2m/exposed-table: identity + relationship member
  iteration was reading entity.children (own) via filterIsInstance — flipped to
  entity.getIdentities(true) / entity.relationships (identities & relationships
  ARE inheritable; an entity inheriting its PK identity or a relationship from a
  BaseEntity was silently dropped). Sites: KotlinSpringControllerGenerator PK,
  KotlinRelationsGenerator (manyRels, reverseFks, primaryKeyKotlinType),
  KotlinM2mSupport.primaryKeyField, KotlinExposedTableGenerator (composition FK +
  identity.reference decorations), KotlinPayloadGenerator @via relationship.
- KotlinStoredProcGenerator: @param (field) and @proc/@table/@procname (source)
  were own — flipped (@param is an inheritable field property; source attrs are
  inheritable).
- SpringRenderHelperGenerator.resolveNestedObjectRef: field @objectref was own —
  flipped (matches TS render-helper's resolving field .attr() and the already-
  resolving Kotlin sibling).
- M2MFields (shared codegen+runtime M:N FK derivation): junction
  identity.reference iteration and @references read were own — flipped to the
  resolving getIdentities()/getMetaAttr, matching the TS reference whose
  referenceIdentities() builds on the resolving identities() ("effective view,
  own + inherited"). Own would mis-derive M:N FK direction for an inherited
  junction reference.
- MetaSource.getPhysicalName: @proc/@table aliases were own — flipped to
  resolving, matching getTableName()/getEffectiveKind()/getRole() and the C#
  port's ADR-0039 resolving PhysicalName.

KEPT own + commented (sanctioned):
- template.* attr getters (MetaTemplate/OutputTemplate/PromptTemplate/
  ToolcallTemplate + render-helper/api-model @kind reads): templates read own by
  cross-port contract (TS/C# use ownAttr/OwnAttr) — a template's declared refs
  are authored-here, not inherited.
- origin.* reads (KotlinPayloadGenerator field.children<MetaOrigin>): origin
  NEVER inherits (ADR-0029).
- declaration-layer markers: @isAbstract (IOUtil/CanonicalJsonSerializer/
  KotlinGenUtil), TPH @discriminator/@discriminatorValue (TphPlan/KotlinTphPlan/
  TphHelper), @provided (Fr019SharedEnum) — describe THIS declaration, never
  inherited.
- @dbColumnType stays the physical own-only exception (validation site commented;
  the JVM type-mappers already resolve it in the codegen role, matching TS
  column-mapper — schema/migrate is TS-owned so no own-only schema reader exists
  on the JVM).
- root-level scans (MetaRoot.objects/fields/findObject, SymbolTable,
  Spring/omdb M2M findEntity, root.children template scans): root is never
  extended — own is the complete set (matches TS root.ownChildren()).
- super-resolution walks (MetaObject.getSources per-hop, discriminatorRoot) and
  the ValidationPhase structural tree-recursion + validate-own-declaration attr
  reads (class-level policy comment) + parser source-tagging.

Verification: Java metadata 1065, codegen-spring 158, omdb/core-spring 74,
Kotlin codegen 266, Java + Kotlin Testcontainers integration suites all green.
No golden/snapshot changed (only *.java/*.kt sources touched) — the flips only
affect INHERITED members, which no existing fixture exercised on these paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n

* fix(python): ADR-0039 sweep — remaining own reads → resolving (91 flips)

Comprehensive sweep of the Python port: flip remaining own reads of effective
properties / member iteration to resolving (get_meta_attr/attrs().get/children);
comment every sanctioned own (subclass-emit, own-serializer, overlay, origin,
@dbColumnType, declaration-layer). Template attrs left own pending cross-port
reconciliation. Testcontainers not run locally (pg8000).

* fix(ts): ADR-0039 sweep — remaining own reads → resolving (comprehensive)

Comprehensive TS sweep incl. codegen-ts-angular + cli: flip remaining own reads
of effective properties / member iteration to resolving; template attrs + root
scans → resolving; comment every sanctioned own (subclass-emit, own-serializer,
overlay, origin, @dbColumnType, declaration-layer). metadata+codegen-ts+migrate-ts
3551 pass/0 fail (runtime-ts Testcontainers gated on CI).

* fix(jvm): ADR-0039 — resolve template.* attrs (Java + Kotlin), matching TS + C#

The prior JVM sweep KEPT template.* attr reads own-only ("cross-port
contract reads templates own"), but that reflected the pre-sweep state.
TS + C# now RESOLVE template attrs — a template is a registered type that
can be an `extends` target, so its attrs inherit; resolving
(includeParentData=true, the default) is the ADR-0039 default.

Flipped to resolving:
- MetaTemplate / OutputTemplate / PromptTemplate / ToolcallTemplate getters
  (@payloadRef/@textRef/@format/@maxChars/@owner/@since/@requiredTags/
  @promptStyle/@requiredSlots/@maxTokens/@model/@responseRef/@toolname).
- Render-helper + api-model template @kind/@subjectRef/@htmlBodyRef/
  @textBodyRef/@textRef reads (SpringRenderHelperGenerator,
  JavaApiModelBuilder, KotlinRenderHelperGenerator, KotlinApiModelBuilder).
- Root-level template-node iteration → resolving getChildren(type, true)
  (Spring render/output-parser/output-prompt/payload + Kotlin
  render/output-prompt/output-parser/extractor/payload/api-model).

Left own (unchanged): ValidationPhase per-subtype required-attr checks
(eager-throw validation pass, analogous to source/origin/relationship
validation own-walks); field @objectref reads already resolve.

No golden change (no template-extends fixture exists yet).
Tests: metadata 1065, codegen-spring 158, codegen-kotlin 266 — all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n

* fix(python): ADR-0039 reconcile — template attrs resolve (match TS/C#/JVM)

Templates are registered types that CAN be an extends target (unlike origin.*,
which has a documented never-inherits rule), so per ADR-0039 default they resolve.
Flips the 24 template attr reads own→resolving (get_meta_attr), aligning Python
with the TS reference + C# + JVM. Byte-identical (no template-extends fixture).

---------

Co-authored-by: Doug Mealing <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.

2 participants