Skip to content

Commit dd48285

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

15 files changed

Lines changed: 96 additions & 113 deletions

File tree

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +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.
76-
val outputs = loader.root.children
77-
.filterIsInstance<OutputTemplate>()
74+
// ADR-0039: root-scan discipline — resolving children accessor.
75+
val outputs = loader.root.getChildren(OutputTemplate::class.java, true)
7876
.sortedBy { it.name }
7977

8078
for (tmpl in outputs) {

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +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.
86-
val outputs = loader.root.children
87-
.filterIsInstance<OutputTemplate>()
84+
// ADR-0039: root-scan discipline — resolving children accessor.
85+
val outputs = loader.root.getChildren(OutputTemplate::class.java, true)
8886
.sortedBy { it.name }
8987

9088
for (tmpl in outputs) {

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +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.
79-
val outputs = loader.root.children
80-
.filterIsInstance<OutputTemplate>()
77+
// ADR-0039: root-scan discipline — resolving children accessor.
78+
val outputs = loader.root.getChildren(OutputTemplate::class.java, true)
8179
.sortedBy { it.name }
8280

8381
for (tmpl in outputs) {

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +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.
68-
for (md in loader.root.children) {
69-
if (md !is MetaTemplate) continue
66+
// ADR-0039: root-scan discipline — resolving children accessor.
67+
for (md in loader.root.getChildren(MetaTemplate::class.java, true)) {
7068
emit(md, loader, outRoot, emittedNestedFqns, emittedEnumFqns)
7169
}
7270
}

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +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.
84-
val outputs = loader.root.children
85-
.filterIsInstance<OutputTemplate>()
82+
// ADR-0039: root-scan discipline — resolving children accessor.
83+
val outputs = loader.root.getChildren(OutputTemplate::class.java, true)
8684
.sortedBy { it.name }
8785

8886
for (tmpl in outputs) {
@@ -299,23 +297,23 @@ open class KotlinRenderHelperGenerator : MultiFileDirectGeneratorBase<MetaObject
299297
// Local helpers
300298
// -------------------------------------------------------------------------
301299

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.
300+
// ADR-0039: template.* attrs RESOLVE through extends (includeParentData=true, the
301+
// default) — a template is a registered type that can be an `extends` target, so its
302+
// refs inherit. Matches the TS reference + C#.
305303

306-
/** Resolve `@kind` (OWN attr — templates read own, ADR-0039), defaulting to `document`. */
304+
/** Resolve `@kind` (RESOLVING attr — ADR-0039), defaulting to `document`. */
307305
private fun kindOf(template: MetaTemplate): String {
308-
if (template is OutputTemplate && template.hasMetaAttr(TemplateConstants.ATTR_KIND, false)) {
309-
val v = template.getMetaAttr(TemplateConstants.ATTR_KIND, false).valueAsString
306+
if (template is OutputTemplate && template.hasMetaAttr(TemplateConstants.ATTR_KIND)) {
307+
val v = template.getMetaAttr(TemplateConstants.ATTR_KIND).valueAsString
310308
if (!v.isNullOrEmpty()) return v
311309
}
312310
return TemplateConstants.KIND_DEFAULT
313311
}
314312

315-
/** Read an OWN template attr value, or null if absent (templates read own — ADR-0039). */
313+
/** Read a template attr value (RESOLVING — ADR-0039), or null if absent. */
316314
private fun attr(template: MetaTemplate, attr: String): String? =
317-
if (template.hasMetaAttr(attr, false))
318-
template.getMetaAttr(attr, false).valueAsString
315+
if (template.hasMetaAttr(attr))
316+
template.getMetaAttr(attr).valueAsString
319317
else null
320318

321319
/** Resolve a `@payloadRef` to its `object.value` (rejects entities — payloads must be VOs). */

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,9 @@ class KotlinApiModelBuilder {
7777
}
7878

7979
// Templates: one unit per template.output under the model root.
80-
// ADR-0039: root-level declaration scan — root is never extended (matches the TS
81-
// reference's root.ownChildren()), so own children is correct.
82-
for (child in loader.root.children) {
83-
if (child is OutputTemplate) {
84-
units.add(buildTemplateUnit(child, loader))
85-
}
80+
// ADR-0039: root-scan discipline — resolving children accessor.
81+
for (child in loader.root.getChildren(OutputTemplate::class.java, true)) {
82+
units.add(buildTemplateUnit(child, loader))
8683
}
8784

8885
return KotlinApiModel(project, units)
@@ -437,10 +434,10 @@ class KotlinApiModelBuilder {
437434
}
438435

439436
private fun isEmailKind(tmpl: MetaTemplate): Boolean {
440-
// ADR-0039: template.* attrs read OWN-ONLY by cross-port contract (a template's
441-
// declared refs are authored-here, not inherited into an effective form).
442-
if (!tmpl.hasMetaAttr(TemplateConstants.ATTR_KIND, false)) return false
443-
val v = runCatching { tmpl.getMetaAttr(TemplateConstants.ATTR_KIND, false).valueAsString }.getOrNull()
437+
// ADR-0039: template.* attrs RESOLVE through extends (includeParentData=true,
438+
// the default) — matching the TS reference + C#.
439+
if (!tmpl.hasMetaAttr(TemplateConstants.ATTR_KIND)) return false
440+
val v = runCatching { tmpl.getMetaAttr(TemplateConstants.ATTR_KIND).valueAsString }.getOrNull()
444441
return TemplateConstants.KIND_EMAIL.equals(v, ignoreCase = true)
445442
}
446443
}

server/java/codegen-spring/src/main/java/com/metaobjects/generator/apidocs/JavaApiModelBuilder.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.metaobjects.generator.apidocs;
22

3-
import com.metaobjects.MetaData;
43
import com.metaobjects.generator.spring.LlmTraceHelperGenerator;
54
import com.metaobjects.generator.spring.SpringControllerGenerator;
65
import com.metaobjects.generator.spring.SpringDtoGenerator;
@@ -44,8 +43,8 @@
4443
* read DTO only (no VALIDATION / DATA_ACCESS / REST / FILTER — those
4544
* generators gate on a writable table entity and skip a projection). A value
4645
* object ({@code object.value}) yields MODEL only.</li>
47-
* <li><b>Templates</b> (iterated via {@code loader.getRoot().getChildren()}
48-
* filtered to {@link MetaTemplate}): each template yields PAYLOAD / RENDER /
46+
* <li><b>Templates</b> (iterated via the resolving
47+
* {@code loader.getRoot().getChildren(MetaTemplate.class, true)}): each template yields PAYLOAD / RENDER /
4948
* PROMPT / OUTPUT_PARSER symbols gated by the matching {@code appliesTo}.</li>
5049
* </ul>
5150
*
@@ -90,10 +89,9 @@ public JavaApiModel build(MetaDataLoader loader, String project) {
9089
}
9190

9291
// Templates: one unit per template.* node under the model root.
93-
for (MetaData child : loader.getRoot().getChildren()) {
94-
if (child instanceof MetaTemplate tmpl) {
95-
units.add(buildTemplateUnit(tmpl, loader));
96-
}
92+
// ADR-0039: root-scan discipline — resolving children accessor.
93+
for (MetaTemplate tmpl : loader.getRoot().getChildren(MetaTemplate.class, true)) {
94+
units.add(buildTemplateUnit(tmpl, loader));
9795
}
9896

9997
return new JavaApiModel(project, units);
@@ -289,13 +287,13 @@ parser, ApiSymbolKind.OUTPUT_PARSER, fqn(promptsPkg, parser),
289287

290288
/**
291289
* True when the template's {@code @kind} is {@code email}. ADR-0039: template.*
292-
* attrs read OWN-ONLY by cross-port contract (a template's declared refs are
293-
* authored-here, not inherited); default {@code document}.
290+
* attrs RESOLVE through extends (includeParentData=true, the default) — matching
291+
* the TS reference + C#; default {@code document}.
294292
*/
295293
private static boolean isEmailKind(MetaTemplate tmpl) {
296294
if (tmpl instanceof OutputTemplate
297-
&& tmpl.hasMetaAttr(TemplateConstants.ATTR_KIND, false)) {
298-
String v = tmpl.getMetaAttr(TemplateConstants.ATTR_KIND, false).getValueAsString();
295+
&& tmpl.hasMetaAttr(TemplateConstants.ATTR_KIND)) {
296+
String v = tmpl.getMetaAttr(TemplateConstants.ATTR_KIND).getValueAsString();
299297
return TemplateConstants.KIND_EMAIL.equals(v);
300298
}
301299
return false;

server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/SpringOutputParserGenerator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,10 @@ public void execute(MetaDataLoader loader) {
116116
Path outRoot = Paths.get(outDir.getAbsolutePath());
117117

118118
// Stable name order — matches the other ports' deterministic emission.
119+
// ADR-0039: root-scan discipline — resolving children accessor.
119120
List<MetaTemplate> outputs = new ArrayList<>();
120-
for (MetaData child : loader.getRoot().getChildren()) {
121-
if (child instanceof MetaTemplate t && TemplateConstants.SUBTYPE_OUTPUT.equals(t.getSubType())) {
121+
for (MetaTemplate t : loader.getRoot().getChildren(MetaTemplate.class, true)) {
122+
if (TemplateConstants.SUBTYPE_OUTPUT.equals(t.getSubType())) {
122123
outputs.add(t);
123124
}
124125
}

server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/SpringOutputPromptGenerator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ public void execute(MetaDataLoader loader) {
8282
Path outRoot = Paths.get(outDir.getAbsolutePath());
8383

8484
// Stable name order — matches the other ports' deterministic emission.
85+
// ADR-0039: root-scan discipline — resolving children accessor.
8586
List<MetaTemplate> outputs = new ArrayList<>();
86-
for (MetaData child : loader.getRoot().getChildren()) {
87-
if (child instanceof MetaTemplate t && TemplateConstants.SUBTYPE_OUTPUT.equals(t.getSubType())) {
87+
for (MetaTemplate t : loader.getRoot().getChildren(MetaTemplate.class, true)) {
88+
if (TemplateConstants.SUBTYPE_OUTPUT.equals(t.getSubType())) {
8889
outputs.add(t);
8990
}
9091
}

server/java/codegen-spring/src/main/java/com/metaobjects/generator/spring/SpringPayloadGenerator.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,9 @@ public void execute(MetaDataLoader loader) {
119119
// Stable name order — matches the other ports' deterministic emission.
120120
// Iterate ALL MetaTemplate subtypes (prompt / output / toolcall);
121121
// every template with a @payloadRef gets a payload record.
122-
List<MetaTemplate> templates = new ArrayList<>();
123-
for (MetaData child : loader.getRoot().getChildren()) {
124-
if (child instanceof MetaTemplate t) {
125-
templates.add(t);
126-
}
127-
}
122+
// ADR-0039: root-scan discipline — resolving children accessor.
123+
List<MetaTemplate> templates =
124+
new ArrayList<>(loader.getRoot().getChildren(MetaTemplate.class, true));
128125
templates.sort(Comparator.comparing(MetaTemplate::getName));
129126

130127
for (MetaTemplate tmpl : templates) {

0 commit comments

Comments
 (0)