Skip to content

Commit 9b8ecf7

Browse files
committed
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
1 parent cc4b4fd commit 9b8ecf7

35 files changed

Lines changed: 209 additions & 46 deletions

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/Fr019SharedEnum.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ internal object Fr019SharedEnum {
6161
return sup
6262
}
6363

64-
/** Own-only read of `@provided` on an enum declaration. */
64+
/**
65+
* Read `@provided` on an enum declaration. ADR-0039: `@provided` is a
66+
* declaration-layer marker (like `@isAbstract`) — describes THIS declaration,
67+
* never inherited through extends. KEPT own-only.
68+
*/
6569
fun isProvided(decl: EnumField): Boolean {
6670
if (!decl.hasMetaAttr(EnumField.ATTR_PROVIDED, false)) return false
6771
val raw = runCatching { decl.getMetaAttr(EnumField.ATTR_PROVIDED, false).value }.getOrNull()

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinExposedTableGenerator.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -843,8 +843,9 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
843843

844844
for (entity in loader.metaObjects) {
845845
if (entity.subType != MetaObject.SUBTYPE_ENTITY) continue
846-
for (child in entity.children) {
847-
if (child !is MetaRelationship) continue
846+
// ADR-0039: relationships are inheritable — RESOLVE via entity.relationships;
847+
// entity.children (own-only) would miss an inherited relationship.
848+
for (child in entity.relationships) {
848849
if (child.subType != CompositionRelationship.SUBTYPE_COMPOSITION) continue
849850

850851
val objectRef = child.objectRef ?: continue
@@ -1016,7 +1017,9 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
10161017
val acc = linkedMapOf<String, MutableMap<String, RefDecoration>>()
10171018
for (entity in loader.metaObjects) {
10181019
if (entity.subType != MetaObject.SUBTYPE_ENTITY) continue
1019-
for (child in entity.children) {
1020+
// ADR-0039: identity.reference children are inheritable — RESOLVE via
1021+
// getIdentities(true); entity.children (own-only) would miss inherited FK refs.
1022+
for (child in entity.getIdentities(true)) {
10201023
if (child !is ReferenceIdentity) continue
10211024
val fields = child.fields
10221025
if (fields.size != 1) {

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinExtractorGenerator.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ open class KotlinExtractorGenerator : MultiFileDirectGeneratorBase<MetaObject>()
7171
val outRoot = Paths.get(outDir.absolutePath)
7272

7373
// Stable name order — matches the sibling generators' deterministic emission.
74+
// ADR-0039: root-level declaration scan — root is never extended (matches the TS
75+
// reference's root.ownChildren()), so own children is correct.
7476
val outputs = loader.root.children
7577
.filterIsInstance<OutputTemplate>()
7678
.sortedBy { it.name }

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinGenUtil.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,12 @@ internal object KotlinGenUtil {
5252
}
5353

5454
/**
55-
* True if [obj] has an own `@isAbstract` attribute set to boolean-true. Reads only the
56-
* own attribute (not inherited) — matches the ValidationPhase convention so concrete
57-
* subtypes extending an abstract base still emit. Shared by every instance/write
58-
* generator so the "never emit write artifacts for an abstract entity" invariant has a
59-
* single definition.
55+
* True if [obj] has an own `@isAbstract` attribute set to boolean-true. ADR-0039:
56+
* `@isAbstract` is a declaration-layer marker — it describes THIS declaration and must
57+
* NOT be inherited (a concrete subtype extending an abstract base is itself concrete
58+
* and MUST emit). KEPT own-only, matching the ValidationPhase / GeneratorUtil.isAbstract
59+
* convention. Shared by every instance/write generator so the "never emit write
60+
* artifacts for an abstract entity" invariant has a single definition.
6061
*/
6162
fun isAbstractEntity(obj: MetaObject): Boolean {
6263
if (!obj.hasMetaAttr(MetaData.ATTR_IS_ABSTRACT, false)) return false

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinM2mSupport.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ object KotlinM2mSupport {
108108

109109
/** The single primary-key field name of an entity (defaults to `id`). */
110110
private fun primaryKeyField(entity: MetaObject): String {
111-
val pk = entity.children
111+
// ADR-0039: identities are inheritable — RESOLVE via getIdentities(true);
112+
// entity.children (own-only) would miss an inherited primary identity.
113+
val pk = entity.getIdentities(true)
112114
.filterIsInstance<com.metaobjects.identity.MetaIdentity>()
113115
.firstOrNull { it.isPrimary }
114116
return pk?.fields?.firstOrNull() ?: "id"

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinOutputParserGenerator.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ open class KotlinOutputParserGenerator : MultiFileDirectGeneratorBase<MetaObject
8181
val outRoot = Paths.get(outDir.absolutePath)
8282

8383
// Stable name order — matches TS/C#/Python deterministic emission.
84+
// ADR-0039: root-level declaration scan — root is never extended (matches the TS
85+
// reference's root.ownChildren()), so own children is correct.
8486
val outputs = loader.root.children
8587
.filterIsInstance<OutputTemplate>()
8688
.sortedBy { it.name }

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinOutputPromptGenerator.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ open class KotlinOutputPromptGenerator : MultiFileDirectGeneratorBase<MetaObject
7474
val outRoot = Paths.get(outDir.absolutePath)
7575

7676
// Stable name order — matches TS/C#/Python/Java deterministic emission.
77+
// ADR-0039: root-level declaration scan — root is never extended (matches the TS
78+
// reference's root.ownChildren()), so own children is correct.
7779
val outputs = loader.root.children
7880
.filterIsInstance<OutputTemplate>()
7981
.sortedBy { it.name }

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinPayloadGenerator.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ open class KotlinPayloadGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
6363
// typed as its generated enum class (reusing the entity enum scheme); two fields sharing an
6464
// abstract enum super collapse onto ONE emitted file.
6565
val emittedEnumFqns = mutableSetOf<String>()
66+
// ADR-0039: root-level declaration scan — root is never extended (matches the TS
67+
// reference's root.ownChildren()), so own children is correct.
6668
for (md in loader.root.children) {
6769
if (md !is MetaTemplate) continue
6870
emit(md, loader, outRoot, emittedNestedFqns, emittedEnumFqns)
@@ -148,6 +150,8 @@ open class KotlinPayloadGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
148150
emittedNestedFqns: MutableSet<String>,
149151
emittedEnumFqns: MutableSet<String>,
150152
): TypeName {
153+
// ADR-0039/ADR-0029: origin.* NEVER inherits — a derived field's origin is
154+
// declared-here, so read OWN children (field.children), not resolving.
151155
val origin = field.children.filterIsInstance<MetaOrigin>().firstOrNull()
152156

153157
if (origin != null) {
@@ -300,8 +304,9 @@ open class KotlinPayloadGenerator : MultiFileDirectGeneratorBase<MetaObject>() {
300304
val via = origin.via ?: return fallbackType()
301305
val (parentName, relName) = KotlinGenUtil.splitDottedRef(via) ?: return fallbackType()
302306
val parent = KotlinGenUtil.resolveObjectByShortOrFqn(loader, parentName) ?: return fallbackType()
303-
val relationship = parent.children
304-
.filterIsInstance<MetaRelationship>()
307+
// ADR-0039: relationships are inheritable — RESOLVE via parent.relationships;
308+
// parent.children (own-only) would miss a relationship inherited via extends.
309+
val relationship = parent.relationships
305310
.firstOrNull { it.name == relName || it.name.substringAfterLast("::") == relName }
306311
?: return fallbackType()
307312
val targetRef = relationship.objectRef ?: return fallbackType()

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinRelationsGenerator.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ open class KotlinRelationsGenerator : MultiFileDirectGeneratorBase<MetaObject>()
7070
// ADR-0039: resolving (an inherited source.rdb still means the entity is persisted).
7171
if (!KotlinGenUtil.hasRdbSource(entity)) continue
7272

73-
val manyRels = entity.children
74-
.filterIsInstance<MetaRelationship>()
73+
// ADR-0039: relationships are inheritable — RESOLVE via entity.relationships;
74+
// entity.children (own-only) would miss an inherited relationship.
75+
val manyRels = entity.relationships
7576
.filter {
7677
it.subType == CompositionRelationship.SUBTYPE_COMPOSITION &&
7778
it.cardinality == MetaRelationship.CARDINALITY_MANY
@@ -233,7 +234,9 @@ open class KotlinRelationsGenerator : MultiFileDirectGeneratorBase<MetaObject>()
233234
*/
234235
protected open fun reverseFksFor(entity: MetaObject): List<ReverseFk> {
235236
val out = mutableListOf<ReverseFk>()
236-
for (child in entity.children) {
237+
// ADR-0039: identity.reference children are inheritable — RESOLVE via
238+
// getIdentities(true); entity.children (own-only) would miss inherited FK refs.
239+
for (child in entity.getIdentities(true)) {
237240
if (child !is ReferenceIdentity) continue
238241
val fields = child.fields
239242
if (fields.size != 1) continue // single-column FKs only
@@ -258,7 +261,9 @@ open class KotlinRelationsGenerator : MultiFileDirectGeneratorBase<MetaObject>()
258261
* metadata.
259262
*/
260263
protected open fun primaryKeyKotlinType(entity: MetaObject): TypeName {
261-
val primary = entity.children
264+
// ADR-0039: identities are inheritable — RESOLVE via getIdentities(true);
265+
// entity.children (own-only) would miss an inherited primary identity.
266+
val primary = entity.getIdentities(true)
262267
.filterIsInstance<MetaIdentity>()
263268
.firstOrNull { it.isPrimary } ?: return LONG
264269
val pkFieldName = primary.fields.firstOrNull() ?: return LONG

server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinRenderHelperGenerator.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ open class KotlinRenderHelperGenerator : MultiFileDirectGeneratorBase<MetaObject
7979
val provider = FilesystemProvider(Paths.get(templateRoot))
8080

8181
// Stable name order — matches the other ports' deterministic emission.
82+
// ADR-0039: root-level declaration scan — root is never extended (matches the TS
83+
// reference's root.ownChildren()), so own children is correct.
8284
val outputs = loader.root.children
8385
.filterIsInstance<OutputTemplate>()
8486
.sortedBy { it.name }
@@ -254,6 +256,9 @@ open class KotlinRenderHelperGenerator : MultiFileDirectGeneratorBase<MetaObject
254256
* `object.value` is found.
255257
*/
256258
private fun resolveNestedObjectRef(loader: MetaDataLoader, field: ObjectField): MetaObject? {
259+
// ADR-0039: @objectRef is an inheritable effective field property — RESOLVE
260+
// (includeParentData=true) so a field.object inheriting @objectRef via extends
261+
// still resolves its target VO.
257262
if (!field.hasMetaAttr(MetaObject.ATTR_OBJECT_REF, true)) return null
258263
val ref = field.getMetaAttr(MetaObject.ATTR_OBJECT_REF, true).valueAsString
259264
if (ref.isNullOrEmpty()) return null
@@ -294,7 +299,11 @@ open class KotlinRenderHelperGenerator : MultiFileDirectGeneratorBase<MetaObject
294299
// Local helpers
295300
// -------------------------------------------------------------------------
296301

297-
/** Resolve `@kind` (own attr), defaulting to `document`. */
302+
// ADR-0039: template.* attrs are read OWN-ONLY by cross-port contract (TS
303+
// render-helper uses tmpl.ownAttr). A template's declared refs are authored-here,
304+
// not inherited into an effective form. KEPT own to match the reference.
305+
306+
/** Resolve `@kind` (OWN attr — templates read own, ADR-0039), defaulting to `document`. */
298307
private fun kindOf(template: MetaTemplate): String {
299308
if (template is OutputTemplate && template.hasMetaAttr(TemplateConstants.ATTR_KIND, false)) {
300309
val v = template.getMetaAttr(TemplateConstants.ATTR_KIND, false).valueAsString
@@ -303,7 +312,7 @@ open class KotlinRenderHelperGenerator : MultiFileDirectGeneratorBase<MetaObject
303312
return TemplateConstants.KIND_DEFAULT
304313
}
305314

306-
/** Read an own attr value, or null if absent. */
315+
/** Read an OWN template attr value, or null if absent (templates read own — ADR-0039). */
307316
private fun attr(template: MetaTemplate, attr: String): String? =
308317
if (template.hasMetaAttr(attr, false))
309318
template.getMetaAttr(attr, false).valueAsString

0 commit comments

Comments
 (0)