Skip to content

Commit f0ca60b

Browse files
dmealingclaude
andcommitted
fix(java): pre-merge review — complete FR5a envelope coverage
Apply four review findings against the FR5a Java port: CRITICAL — ValidationPhase envelope migration completed for all 32 throw sites (was 2/32). Every post-load validation error now carries the offending node's source provenance via the (message, code, envelope) MetaDataException constructor. Helper signatures validateFromOrOfPath / validateViaPath gained an ErrorSource parameter so the calling origin's envelope flows through. Mirrors the C# ValidationPasses migration (28/28 sites) and gives parse-time + post-load errors equally precise provenance. CRITICAL — Inline @-attribute children in CanonicalJsonParser are now tagged with a JsonSource matching the parent body's JSONPath. Previously every inline attr (e.g. `@filterable: true`) defaulted to CodeSource.DEFAULT because parseInlineAttribute returns without touching the just-added MetaAttribute's source. A new private helper tagAttrChildWithJsonSource looks up the attr by name (own-only) after the base parser's call and stamps the current JSONPath. Both the inline-attribute loop in processAttributes and the synthesized @isAbstract path now invoke it. Added parserTagsInlineAttributeChildrenWithJsonSource to SourceOnNodeTest to lock the behavior. HIGH — MetaData.clone() now copies `source` and resets `sourceFrozen=false`. The overload() → clone() overlay path was previously dropping provenance back to CodeSource.DEFAULT; the freeze flag is intentionally NOT carried so FR5c can set a MergedSource envelope on the clone post-clone. LOWER-PRIORITY — SemanticDiff gained the (MetaData, MetaData) overload mirroring the C# port: canonical-serialize both sides, fast-path byte equality, fall through to the JsonElement structural compare. Existing SemanticDiffTest null-pair cases were disambiguated via (JsonElement) casts. Deferred (per task): loadFromStream catch-all envelope (low priority; file path already in message) and the `com.metaobjects.source` package collision (not blocking). Tests: 681/681 green (baseline 680/680 + 1 new test). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 75c00db commit f0ca60b

6 files changed

Lines changed: 167 additions & 39 deletions

File tree

server/java/metadata/src/main/java/com/metaobjects/MetaData.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,13 @@ public Object clone() {
15971597
// Used to provide support for OSGi and Maven Mojos
15981598
v.metaDataClassLoader = metaDataClassLoader;
15991599

1600+
// FR5a / ADR-0009 — preserve provenance across clone(). The overlay path
1601+
// (overload() → clone()) would otherwise reset source to CodeSource.DEFAULT,
1602+
// dropping the node's parse-time JsonSource. The clone inherits this node's
1603+
// source unfrozen so FR5c can later set a MergedSource envelope post-clone.
1604+
v.source = this.source;
1605+
v.sourceFrozen = false;
1606+
16001607
for (MetaData md : getChildren()) {
16011608
v.addChild((MetaData) md.clone());
16021609
}

server/java/metadata/src/main/java/com/metaobjects/loader/ValidationPhase.java

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ private static void validateSourceNode(MetaData node) {
269269
+ "' @kind '" + kind
270270
+ "' is not a valid value; allowed: table, view, materializedView,"
271271
+ " storedProc, tableFunction",
272-
ErrorCode.ERR_BAD_ATTR_VALUE);
272+
ErrorCode.ERR_BAD_ATTR_VALUE, node.getSource());
273273
}
274274
}
275275

@@ -282,7 +282,7 @@ private static void validateSourceNode(MetaData node) {
282282
+ ": source '" + node.getName()
283283
+ "' @role '" + role
284284
+ "' is not a valid value; allowed: primary, replica, index, cache, publish, mirror",
285-
ErrorCode.ERR_BAD_ATTR_VALUE);
285+
ErrorCode.ERR_BAD_ATTR_VALUE, node.getSource());
286286
}
287287
}
288288
}
@@ -355,7 +355,7 @@ private static void validateObjectPrimarySource(MetaObject obj) {
355355
+ ": object '" + obj.getName()
356356
+ "' declares " + sources.size()
357357
+ " source(s) but none has role \"" + MetaSource.ROLE_PRIMARY + "\"",
358-
ErrorCode.ERR_SOURCE_NO_PRIMARY);
358+
ErrorCode.ERR_SOURCE_NO_PRIMARY, obj.getSource());
359359
}
360360

361361
if (primaryCount > 1) {
@@ -365,7 +365,7 @@ private static void validateObjectPrimarySource(MetaObject obj) {
365365
+ "' declares " + primaryCount
366366
+ " sources with role \"" + MetaSource.ROLE_PRIMARY
367367
+ "\"; exactly one is required",
368-
ErrorCode.ERR_SOURCE_MULTIPLE_PRIMARY);
368+
ErrorCode.ERR_SOURCE_MULTIPLE_PRIMARY, obj.getSource());
369369
}
370370
}
371371

@@ -432,7 +432,7 @@ private static void validateRelationshipNode(MetaData node) {
432432
+ ": relationship '" + node.getName()
433433
+ "' @onDelete '" + onDelete
434434
+ "' is not a valid value; allowed: cascade, set-null, restrict, no-action",
435-
ErrorCode.ERR_BAD_ATTR_VALUE);
435+
ErrorCode.ERR_BAD_ATTR_VALUE, node.getSource());
436436
}
437437
}
438438

@@ -445,7 +445,7 @@ private static void validateRelationshipNode(MetaData node) {
445445
+ ": relationship '" + node.getName()
446446
+ "' @onUpdate '" + onUpdate
447447
+ "' is not a valid value; allowed: cascade, set-null, restrict, no-action",
448-
ErrorCode.ERR_BAD_ATTR_VALUE);
448+
ErrorCode.ERR_BAD_ATTR_VALUE, node.getSource());
449449
}
450450
}
451451
}
@@ -486,7 +486,7 @@ private static void validateObjectFieldStorageNode(MetaData node) {
486486
ErrorMessageConstants.ERR_STORAGE_WITHOUT_OBJECT_REF
487487
+ ": field.object '" + field.getName()
488488
+ "' has @storage but no @objectRef — @storage shape only applies to referenced objects",
489-
ErrorCode.ERR_STORAGE_WITHOUT_OBJECT_REF);
489+
ErrorCode.ERR_STORAGE_WITHOUT_OBJECT_REF, field.getSource());
490490
}
491491

492492
Object storageVal = node.getMetaAttr(ObjectField.ATTR_STORAGE, false).getValue();
@@ -496,7 +496,7 @@ private static void validateObjectFieldStorageNode(MetaData node) {
496496
+ ": field.object '" + field.getName()
497497
+ "' @storage=\"flattened\" cannot be combined with isArray=true"
498498
+ " (use @storage=\"jsonb\" for owned-array storage)",
499-
ErrorCode.ERR_STORAGE_FLATTENED_ARRAY);
499+
ErrorCode.ERR_STORAGE_FLATTENED_ARRAY, field.getSource());
500500
}
501501
}
502502

@@ -572,13 +572,13 @@ private static void validateOriginNode(MetaRoot root, MetaObject obj,
572572
ErrorMessageConstants.ERR_INVALID_ORIGIN
573573
+ ": origin.passthrough on " + obj.getName() + "." + field.getName()
574574
+ ": missing @from.",
575-
ErrorCode.ERR_INVALID_ORIGIN);
575+
ErrorCode.ERR_INVALID_ORIGIN, origin.getSource());
576576
}
577577
validateFromOrOfPath(from, root, obj.getName(), field.getName(),
578-
"origin.passthrough.@from");
578+
"origin.passthrough.@from", origin.getSource());
579579
String via = origin.getVia();
580580
if (via != null && !via.isEmpty()) {
581-
validateViaPath(via, root, obj.getName(), field.getName());
581+
validateViaPath(via, root, obj.getName(), field.getName(), origin.getSource());
582582
}
583583
return;
584584
}
@@ -594,7 +594,7 @@ private static void validateOriginNode(MetaRoot root, MetaObject obj,
594594
+ ": origin.aggregate on " + obj.getName() + "." + field.getName()
595595
+ " @agg '" + agg + "' is not a valid value; allowed: "
596596
+ "count, sum, avg, min, max",
597-
ErrorCode.ERR_BAD_ATTR_VALUE);
597+
ErrorCode.ERR_BAD_ATTR_VALUE, origin.getSource());
598598
}
599599

600600
String of = origin.getOf();
@@ -603,20 +603,20 @@ private static void validateOriginNode(MetaRoot root, MetaObject obj,
603603
ErrorMessageConstants.ERR_INVALID_ORIGIN
604604
+ ": origin.aggregate on " + obj.getName() + "." + field.getName()
605605
+ ": missing @of.",
606-
ErrorCode.ERR_INVALID_ORIGIN);
606+
ErrorCode.ERR_INVALID_ORIGIN, origin.getSource());
607607
}
608608
validateFromOrOfPath(of, root, obj.getName(), field.getName(),
609-
"origin.aggregate.@of");
609+
"origin.aggregate.@of", origin.getSource());
610610

611611
String via = origin.getVia();
612612
if (via == null || via.isEmpty()) {
613613
throw new MetaDataException(
614614
ErrorMessageConstants.ERR_INVALID_ORIGIN
615615
+ ": origin.aggregate on " + obj.getName() + "." + field.getName()
616616
+ ": missing @via (aggregates require a relationship path).",
617-
ErrorCode.ERR_INVALID_ORIGIN);
617+
ErrorCode.ERR_INVALID_ORIGIN, origin.getSource());
618618
}
619-
validateViaPath(via, root, obj.getName(), field.getName());
619+
validateViaPath(via, root, obj.getName(), field.getName(), origin.getSource());
620620
return;
621621
}
622622

@@ -630,7 +630,7 @@ private static void validateOriginNode(MetaRoot root, MetaObject obj,
630630
ErrorMessageConstants.ERR_INVALID_ORIGIN
631631
+ ": origin.collection on " + obj.getName() + "." + field.getName()
632632
+ ": missing @via.",
633-
ErrorCode.ERR_INVALID_ORIGIN);
633+
ErrorCode.ERR_INVALID_ORIGIN, origin.getSource());
634634
}
635635
}
636636
}
@@ -754,7 +754,7 @@ private static void validateIdentityNode(MetaIdentity identity) {
754754
ErrorMessageConstants.ERR_MISSING_REQUIRED_ATTR
755755
+ ": identity '" + identity.getName()
756756
+ "' is missing required @fields attribute",
757-
ErrorCode.ERR_MISSING_REQUIRED_ATTR);
757+
ErrorCode.ERR_MISSING_REQUIRED_ATTR, identity.getSource());
758758
}
759759
if (identity.hasMetaAttr(MetaIdentity.ATTR_GENERATION, false)) {
760760
Object v = identity.getMetaAttr(MetaIdentity.ATTR_GENERATION, false).getValue();
@@ -765,7 +765,7 @@ private static void validateIdentityNode(MetaIdentity identity) {
765765
+ ": identity '" + identity.getName()
766766
+ "' @generation '" + gen + "' is not a valid value;"
767767
+ " allowed: increment, uuid, assigned",
768-
ErrorCode.ERR_BAD_ATTR_VALUE);
768+
ErrorCode.ERR_BAD_ATTR_VALUE, identity.getSource());
769769
}
770770
}
771771
}
@@ -835,7 +835,7 @@ private static void validateDataGridLayout(MetaObject obj, DataGridLayout grid)
835835
+ ": layout.dataGrid '" + grid.getShortName()
836836
+ "' on '" + obj.getShortName()
837837
+ "' @defaultSortField '" + sortField + "' does not reference a real field",
838-
ErrorCode.ERR_BAD_DEFAULT_SORT_FIELD);
838+
ErrorCode.ERR_BAD_DEFAULT_SORT_FIELD, grid.getSource());
839839
}
840840
}
841841

@@ -873,7 +873,7 @@ private static void validateFilterClause(MetaObject obj, DataGridLayout grid,
873873
+ ": layout.dataGrid '" + grid.getShortName()
874874
+ "' on '" + obj.getShortName()
875875
+ "' @filter references '" + key + "' which is not a @filterable field",
876-
ErrorCode.ERR_BAD_ATTR_FILTER);
876+
ErrorCode.ERR_BAD_ATTR_FILTER, grid.getSource());
877877
}
878878
if (e.getValue() instanceof java.util.Map) {
879879
java.util.Set<String> allowed = allowedOpsFor(field);
@@ -886,7 +886,7 @@ private static void validateFilterClause(MetaObject obj, DataGridLayout grid,
886886
+ "' on '" + obj.getShortName()
887887
+ "' @filter op '" + op + "' is not valid for field '" + key
888888
+ "' (subtype " + field.getSubType() + "); allowed: " + allowed,
889-
ErrorCode.ERR_BAD_ATTR_FILTER);
889+
ErrorCode.ERR_BAD_ATTR_FILTER, grid.getSource());
890890
}
891891
}
892892
}
@@ -1052,7 +1052,7 @@ private static void validateTemplateNode(MetaRoot root, MetaTemplate template) {
10521052
+ "' @format '" + fmt
10531053
+ "' is not a valid value; allowed: "
10541054
+ TemplateConstants.ALLOWED_FORMATS,
1055-
ErrorCode.ERR_BAD_ATTR_VALUE);
1055+
ErrorCode.ERR_BAD_ATTR_VALUE, template.getSource());
10561056
}
10571057
}
10581058

@@ -1062,7 +1062,7 @@ private static void validateTemplateNode(MetaRoot root, MetaTemplate template) {
10621062
throw new MetaDataException(
10631063
ErrorMessageConstants.ERR_MISSING_REQUIRED_ATTR
10641064
+ ": template.prompt '" + template.getName() + "' is missing required @payloadRef",
1065-
ErrorCode.ERR_MISSING_REQUIRED_ATTR);
1065+
ErrorCode.ERR_MISSING_REQUIRED_ATTR, template.getSource());
10661066
}
10671067

10681068
// R2 + R3 only apply if @payloadRef is set
@@ -1074,7 +1074,7 @@ private static void validateTemplateNode(MetaRoot root, MetaTemplate template) {
10741074
ErrorMessageConstants.ERR_INVALID_TEMPLATE
10751075
+ ": template '" + template.getName() + "' @payloadRef '" + payloadRef
10761076
+ "' does not resolve to an object.value at root",
1077-
ErrorCode.ERR_INVALID_TEMPLATE);
1077+
ErrorCode.ERR_INVALID_TEMPLATE, template.getSource());
10781078
}
10791079

10801080
// R3 — every @requiredSlots member must be a field on the payload VO
@@ -1091,7 +1091,7 @@ private static void validateTemplateNode(MetaRoot root, MetaTemplate template) {
10911091
+ ": template.prompt '" + template.getName()
10921092
+ "' @requiredSlots includes '" + slot
10931093
+ "' which is not a field on payload '" + payloadRef + "'",
1094-
ErrorCode.ERR_INVALID_TEMPLATE);
1094+
ErrorCode.ERR_INVALID_TEMPLATE, template.getSource());
10951095
}
10961096
}
10971097
}
@@ -1117,15 +1117,16 @@ private static Set<String> collectPayloadFieldNames(MetaObject payloadVo) {
11171117
*/
11181118
private static void validateFromOrOfPath(String pathAttr, MetaRoot root,
11191119
String projectionName, String fieldName,
1120-
String label) {
1120+
String label,
1121+
com.metaobjects.source.ErrorSource envelope) {
11211122
int dotIdx = pathAttr.indexOf('.');
11221123
if (dotIdx < 1 || dotIdx == pathAttr.length() - 1) {
11231124
throw new MetaDataException(
11241125
ErrorMessageConstants.ERR_INVALID_ORIGIN
11251126
+ ": " + label + " \"" + pathAttr + "\" on "
11261127
+ projectionName + "." + fieldName
11271128
+ ": must be of form \"Entity.field\".",
1128-
ErrorCode.ERR_INVALID_ORIGIN);
1129+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
11291130
}
11301131
String entityName = pathAttr.substring(0, dotIdx);
11311132
String targetFieldName = pathAttr.substring(dotIdx + 1);
@@ -1137,7 +1138,7 @@ private static void validateFromOrOfPath(String pathAttr, MetaRoot root,
11371138
+ ": " + label + " \"" + pathAttr + "\" on "
11381139
+ projectionName + "." + fieldName
11391140
+ ": no such entity \"" + entityName + "\".",
1140-
ErrorCode.ERR_INVALID_ORIGIN);
1141+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
11411142
}
11421143

11431144
// Inherited fields included — getChildren(..., true) walks super data.
@@ -1155,7 +1156,7 @@ private static void validateFromOrOfPath(String pathAttr, MetaRoot root,
11551156
+ projectionName + "." + fieldName
11561157
+ ": no such field \"" + targetFieldName
11571158
+ "\" on " + entityName + ".",
1158-
ErrorCode.ERR_INVALID_ORIGIN);
1159+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
11591160
}
11601161
}
11611162

@@ -1167,15 +1168,16 @@ private static void validateFromOrOfPath(String pathAttr, MetaRoot root,
11671168
* which becomes the next hop's current entity.
11681169
*/
11691170
private static void validateViaPath(String viaAttr, MetaRoot root,
1170-
String projectionName, String fieldName) {
1171+
String projectionName, String fieldName,
1172+
com.metaobjects.source.ErrorSource envelope) {
11711173
String[] segments = viaAttr.split("\\.");
11721174
if (segments.length < 2) {
11731175
throw new MetaDataException(
11741176
ErrorMessageConstants.ERR_INVALID_ORIGIN
11751177
+ ": origin.@via \"" + viaAttr + "\" on "
11761178
+ projectionName + "." + fieldName
11771179
+ ": must be of form \"Entity.relationship[.relationship...]\".",
1178-
ErrorCode.ERR_INVALID_ORIGIN);
1180+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
11791181
}
11801182
String entityName = segments[0];
11811183
MetaObject currentObj = findRootObject(root, entityName);
@@ -1185,7 +1187,7 @@ private static void validateViaPath(String viaAttr, MetaRoot root,
11851187
+ ": origin.@via \"" + viaAttr + "\" on "
11861188
+ projectionName + "." + fieldName
11871189
+ ": no such entity \"" + entityName + "\".",
1188-
ErrorCode.ERR_INVALID_ORIGIN);
1190+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
11891191
}
11901192
for (int i = 1; i < segments.length; i++) {
11911193
String relName = segments[i];
@@ -1197,7 +1199,7 @@ private static void validateViaPath(String viaAttr, MetaRoot root,
11971199
+ projectionName + "." + fieldName
11981200
+ ": no such relationship \"" + relName
11991201
+ "\" on " + currentObj.getName() + ".",
1200-
ErrorCode.ERR_INVALID_ORIGIN);
1202+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
12011203
}
12021204
String refTarget = rel.hasMetaAttr(MetaRelationship.ATTR_OBJECT_REF)
12031205
? rel.getMetaAttr(MetaRelationship.ATTR_OBJECT_REF).getValueAsString()
@@ -1209,7 +1211,7 @@ private static void validateViaPath(String viaAttr, MetaRoot root,
12091211
+ projectionName + "." + fieldName
12101212
+ ": relationship \"" + relName + "\" on "
12111213
+ currentObj.getName() + " is missing @objectRef.",
1212-
ErrorCode.ERR_INVALID_ORIGIN);
1214+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
12131215
}
12141216
MetaObject nextObj = findRootObject(root, refTarget);
12151217
if (nextObj == null) {
@@ -1219,7 +1221,7 @@ private static void validateViaPath(String viaAttr, MetaRoot root,
12191221
+ projectionName + "." + fieldName
12201222
+ ": relationship \"" + relName
12211223
+ "\" points to non-existent entity \"" + refTarget + "\".",
1222-
ErrorCode.ERR_INVALID_ORIGIN);
1224+
ErrorCode.ERR_INVALID_ORIGIN, envelope);
12231225
}
12241226
currentObj = nextObj;
12251227
}

0 commit comments

Comments
 (0)