Skip to content

Commit 260b814

Browse files
committed
Merge worktree-fr5d-java-2026-05-27 — FR5d Java (resolved envelopes; surfaces cross-port FQN divergence — Java emits package-qualified vs TS/C#/Python bare, to reconcile at finalize)
2 parents eeed2bc + 40fefd3 commit 260b814

5 files changed

Lines changed: 618 additions & 24 deletions

File tree

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"language": "java",
3-
"_comment": "FR5a envelope-shape gates: empty ledger.",
4-
"fixtures": []
3+
"_comment": "FR5d (2026-05-27): Java port emits format=resolved for reference-resolution errors (extends:, @via, @of, @payloadRef, @requiredSlots). Fixtures still encode the FR5a-era format=json envelope per the cross-port-safety stance — these will migrate once all four ports ship FR5d, at which point the ledger entries can be removed.",
4+
"fixtures": [
5+
"error-extends-nonexistent",
6+
"error-origin-bad-via-path",
7+
"error-template-payload-ref-not-value",
8+
"error-template-payload-ref-unresolved",
9+
"error-template-required-slot-missing"
10+
]
511
}

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,21 @@ private void resolvePendingExtends() {
208208
}
209209
}
210210
if (superData == null) {
211-
// FR5a / ADR-0009 — pass the unresolved child's source envelope so
212-
// the cross-port harness can report the offending node's
213-
// file + JSONPath rather than a root-of-file shape.
211+
// FR5d — emit format=resolved with referrer + target. The referrer's
212+
// parse-time source supplies files + jsonPath (the location of the
213+
// broken `extends:` on disk); referrer = the declaring node's FQN;
214+
// target = the unresolved supertype ref. Mirrors TS
215+
// `resolveDeferredSupers` in server/typescript/packages/metadata/
216+
// src/loader/meta-data-loader.ts.
217+
com.metaobjects.source.ErrorSource envelope =
218+
com.metaobjects.source.ResolvedSource.from(
219+
p.child.getSource(), p.child.getName(), p.superName);
214220
throw new com.metaobjects.MetaDataException(
215221
"Invalid MetaData [" + p.typeName + "][" + p.child.getShortName()
216222
+ "], the SuperClass [" + p.superName + "] does not exist (deferred resolution)"
217223
+ " in file [" + p.filename + "]",
218224
com.metaobjects.ErrorCode.ERR_UNRESOLVED_SUPER,
219-
p.child.getSource());
225+
envelope);
220226
}
221227
p.child.setSuperData(superData);
222228
}

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

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.metaobjects.origin.PassthroughOrigin;
3535
import com.metaobjects.relationship.MetaRelationship;
3636
import com.metaobjects.source.MetaSource;
37+
import com.metaobjects.source.ResolvedSource;
3738
import com.metaobjects.template.MetaTemplate;
3839
import com.metaobjects.template.OutputTemplate;
3940
import com.metaobjects.template.PromptTemplate;
@@ -572,11 +573,11 @@ private static void validateOriginNode(MetaRoot root, MetaObject obj,
572573
+ ": missing @from.",
573574
ErrorCode.ERR_INVALID_ORIGIN, origin.getSource());
574575
}
575-
validateFromOrOfPath(from, root, obj.getName(), field.getName(),
576+
validateFromOrOfPath(from, root, obj, field.getName(),
576577
"origin.passthrough.@from", origin.getSource());
577578
String via = origin.getVia();
578579
if (via != null && !via.isEmpty()) {
579-
validateViaPath(via, root, obj.getName(), field.getName(), origin.getSource());
580+
validateViaPath(via, root, obj, field.getName(), origin.getSource());
580581
}
581582
return;
582583
}
@@ -603,7 +604,7 @@ private static void validateOriginNode(MetaRoot root, MetaObject obj,
603604
+ ": missing @of.",
604605
ErrorCode.ERR_INVALID_ORIGIN, origin.getSource());
605606
}
606-
validateFromOrOfPath(of, root, obj.getName(), field.getName(),
607+
validateFromOrOfPath(of, root, obj, field.getName(),
607608
"origin.aggregate.@of", origin.getSource());
608609

609610
String via = origin.getVia();
@@ -614,7 +615,7 @@ private static void validateOriginNode(MetaRoot root, MetaObject obj,
614615
+ ": missing @via (aggregates require a relationship path).",
615616
ErrorCode.ERR_INVALID_ORIGIN, origin.getSource());
616617
}
617-
validateViaPath(via, root, obj.getName(), field.getName(), origin.getSource());
618+
validateViaPath(via, root, obj, field.getName(), origin.getSource());
618619
return;
619620
}
620621

@@ -1068,11 +1069,14 @@ private static void validateTemplateNode(MetaRoot root, MetaTemplate template) {
10681069

10691070
MetaObject payloadVo = findRootObject(root, payloadRef);
10701071
if (payloadVo == null || !MetaObject.SUBTYPE_VALUE.equals(payloadVo.getSubType())) {
1072+
// FR5d — @payloadRef is a reference; emit format=resolved with
1073+
// referrer=template FQN, target=the unresolved payloadRef string.
10711074
throw new MetaDataException(
10721075
ErrorMessageConstants.ERR_INVALID_TEMPLATE
10731076
+ ": template '" + template.getName() + "' @payloadRef '" + payloadRef
10741077
+ "' does not resolve to an object.value at root",
1075-
ErrorCode.ERR_INVALID_TEMPLATE, template.getSource());
1078+
ErrorCode.ERR_INVALID_TEMPLATE,
1079+
ResolvedSource.from(template.getSource(), template.getName(), payloadRef));
10761080
}
10771081

10781082
// R3 — every @requiredSlots member must be a field on the payload VO
@@ -1084,12 +1088,17 @@ private static void validateTemplateNode(MetaRoot root, MetaTemplate template) {
10841088
for (String slot : required) {
10851089
if (slot == null || slot.isEmpty()) continue;
10861090
if (!available.contains(slot)) {
1091+
// FR5d — @requiredSlots is a field-on-payload reference; emit
1092+
// format=resolved with referrer=template FQN, target=`payloadRef.slot`
1093+
// (the dotted ref that did not resolve to a payload field).
10871094
throw new MetaDataException(
10881095
ErrorMessageConstants.ERR_INVALID_TEMPLATE
10891096
+ ": template.prompt '" + template.getName()
10901097
+ "' @requiredSlots includes '" + slot
10911098
+ "' which is not a field on payload '" + payloadRef + "'",
1092-
ErrorCode.ERR_INVALID_TEMPLATE, template.getSource());
1099+
ErrorCode.ERR_INVALID_TEMPLATE,
1100+
ResolvedSource.from(template.getSource(), template.getName(),
1101+
payloadRef + "." + slot));
10931102
}
10941103
}
10951104
}
@@ -1112,31 +1121,48 @@ private static Set<String> collectPayloadFieldNames(MetaObject payloadVo) {
11121121
* and aggregate's @of). The entity must exist at the root, and the field
11131122
* must exist on that entity (inherited fields are included via the standard
11141123
* children() traversal).
1124+
*
1125+
* <p>FR5d: emits {@code format=resolved} envelopes for every throw. The
1126+
* referrer FQN format is {@code "<projection-FQN>::<fieldName>"} (mirrors
1127+
* the TS {@code _validateFromPath} contract); the target is the bad ref
1128+
* string itself.</p>
1129+
*
1130+
* @param projection the projection node that owns the field carrying the origin
11151131
*/
11161132
private static void validateFromOrOfPath(String pathAttr, MetaRoot root,
1117-
String projectionName, String fieldName,
1133+
MetaObject projection, String fieldName,
11181134
String label,
11191135
com.metaobjects.source.ErrorSource envelope) {
1136+
// FR5d — referrer is `<projection-FQN>::<fieldName>` (the canonical
1137+
// "where the broken reference lives" identifier).
1138+
String projectionName = projection.getName();
1139+
String referrer = projection.getName() + "::" + fieldName;
11201140
int dotIdx = pathAttr.indexOf('.');
11211141
if (dotIdx < 1 || dotIdx == pathAttr.length() - 1) {
1142+
// Malformed shape (not "Entity.field") — not a reference resolution
1143+
// failure per se, but emit format=resolved with target=the bad string
1144+
// so consumers see the same envelope shape across all FR5d sites.
11221145
throw new MetaDataException(
11231146
ErrorMessageConstants.ERR_INVALID_ORIGIN
11241147
+ ": " + label + " \"" + pathAttr + "\" on "
11251148
+ projectionName + "." + fieldName
11261149
+ ": must be of form \"Entity.field\".",
1127-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1150+
ErrorCode.ERR_INVALID_ORIGIN,
1151+
ResolvedSource.from(envelope, referrer, pathAttr));
11281152
}
11291153
String entityName = pathAttr.substring(0, dotIdx);
11301154
String targetFieldName = pathAttr.substring(dotIdx + 1);
11311155

11321156
MetaObject sourceObj = findRootObject(root, entityName);
11331157
if (sourceObj == null) {
1158+
// FR5d — entity half of the ref didn't resolve. target = full ref.
11341159
throw new MetaDataException(
11351160
ErrorMessageConstants.ERR_INVALID_ORIGIN
11361161
+ ": " + label + " \"" + pathAttr + "\" on "
11371162
+ projectionName + "." + fieldName
11381163
+ ": no such entity \"" + entityName + "\".",
1139-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1164+
ErrorCode.ERR_INVALID_ORIGIN,
1165+
ResolvedSource.from(envelope, referrer, pathAttr));
11401166
}
11411167

11421168
// Inherited fields included — getChildren(..., true) walks super data.
@@ -1148,13 +1174,15 @@ private static void validateFromOrOfPath(String pathAttr, MetaRoot root,
11481174
}
11491175
}
11501176
if (!fieldExists) {
1177+
// FR5d — entity resolved, field on it did not. target = full ref.
11511178
throw new MetaDataException(
11521179
ErrorMessageConstants.ERR_INVALID_ORIGIN
11531180
+ ": " + label + " \"" + pathAttr + "\" on "
11541181
+ projectionName + "." + fieldName
11551182
+ ": no such field \"" + targetFieldName
11561183
+ "\" on " + entityName + ".",
1157-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1184+
ErrorCode.ERR_INVALID_ORIGIN,
1185+
ResolvedSource.from(envelope, referrer, pathAttr));
11581186
}
11591187
}
11601188

@@ -1166,16 +1194,20 @@ private static void validateFromOrOfPath(String pathAttr, MetaRoot root,
11661194
* which becomes the next hop's current entity.
11671195
*/
11681196
private static void validateViaPath(String viaAttr, MetaRoot root,
1169-
String projectionName, String fieldName,
1197+
MetaObject projection, String fieldName,
11701198
com.metaobjects.source.ErrorSource envelope) {
1199+
// FR5d — referrer is `<projection-FQN>::<fieldName>`.
1200+
String projectionName = projection.getName();
1201+
String referrer = projection.getName() + "::" + fieldName;
11711202
String[] segments = viaAttr.split("\\.");
11721203
if (segments.length < 2) {
11731204
throw new MetaDataException(
11741205
ErrorMessageConstants.ERR_INVALID_ORIGIN
11751206
+ ": origin.@via \"" + viaAttr + "\" on "
11761207
+ projectionName + "." + fieldName
11771208
+ ": must be of form \"Entity.relationship[.relationship...]\".",
1178-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1209+
ErrorCode.ERR_INVALID_ORIGIN,
1210+
ResolvedSource.from(envelope, referrer, viaAttr));
11791211
}
11801212
String entityName = segments[0];
11811213
MetaObject currentObj = findRootObject(root, entityName);
@@ -1185,19 +1217,30 @@ private static void validateViaPath(String viaAttr, MetaRoot root,
11851217
+ ": origin.@via \"" + viaAttr + "\" on "
11861218
+ projectionName + "." + fieldName
11871219
+ ": no such entity \"" + entityName + "\".",
1188-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1189-
}
1220+
ErrorCode.ERR_INVALID_ORIGIN,
1221+
ResolvedSource.from(envelope, referrer, viaAttr));
1222+
}
1223+
// FR5d — track the deepest-valid-prefix as we walk. The prefix grows
1224+
// segment-by-segment; on a hop failure the error message names the prefix
1225+
// that DID resolve, so authors can fix multi-hop typos quickly. After the
1226+
// entity lookup above, the deepest valid prefix is just the entity name;
1227+
// each successful relationship hop appends a segment.
1228+
java.util.List<String> validSegments = new java.util.ArrayList<>();
1229+
validSegments.add(entityName);
11901230
for (int i = 1; i < segments.length; i++) {
11911231
String relName = segments[i];
11921232
MetaRelationship rel = findRelationship(currentObj, relName);
11931233
if (rel == null) {
1234+
String prefix = String.join(".", validSegments);
11941235
throw new MetaDataException(
11951236
ErrorMessageConstants.ERR_INVALID_ORIGIN
11961237
+ ": origin.@via \"" + viaAttr + "\" on "
11971238
+ projectionName + "." + fieldName
11981239
+ ": no such relationship \"" + relName
1199-
+ "\" on " + currentObj.getName() + ".",
1200-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1240+
+ "\" on " + currentObj.getName() + ". "
1241+
+ "Deepest valid prefix was \"" + prefix + "\".",
1242+
ErrorCode.ERR_INVALID_ORIGIN,
1243+
ResolvedSource.from(envelope, referrer, viaAttr));
12011244
}
12021245
String refTarget = rel.hasMetaAttr(MetaRelationship.ATTR_OBJECT_REF)
12031246
? rel.getMetaAttr(MetaRelationship.ATTR_OBJECT_REF).getValueAsString()
@@ -1209,18 +1252,25 @@ private static void validateViaPath(String viaAttr, MetaRoot root,
12091252
+ projectionName + "." + fieldName
12101253
+ ": relationship \"" + relName + "\" on "
12111254
+ currentObj.getName() + " is missing @objectRef.",
1212-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1255+
ErrorCode.ERR_INVALID_ORIGIN,
1256+
ResolvedSource.from(envelope, referrer, viaAttr));
12131257
}
12141258
MetaObject nextObj = findRootObject(root, refTarget);
12151259
if (nextObj == null) {
1260+
// FR5d — relationship's @objectRef points at a missing entity. This
1261+
// is the @objectRef-resolution edge of the via-path walk (the "5th
1262+
// site" in FR5d's scope list for @objectRef references encountered
1263+
// transitively). Target = the @objectRef value (the missing entity name).
12161264
throw new MetaDataException(
12171265
ErrorMessageConstants.ERR_INVALID_ORIGIN
12181266
+ ": origin.@via \"" + viaAttr + "\" on "
12191267
+ projectionName + "." + fieldName
12201268
+ ": relationship \"" + relName
12211269
+ "\" points to non-existent entity \"" + refTarget + "\".",
1222-
ErrorCode.ERR_INVALID_ORIGIN, envelope);
1270+
ErrorCode.ERR_INVALID_ORIGIN,
1271+
ResolvedSource.from(envelope, referrer, refTarget));
12231272
}
1273+
validSegments.add(relName);
12241274
currentObj = nextObj;
12251275
}
12261276
}

server/java/metadata/src/main/java/com/metaobjects/source/ResolvedSource.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,60 @@ public ResolvedSource(List<String> files) {
4141
this(files, null, null, null);
4242
}
4343

44+
/**
45+
* FR5d — build a {@code format: "resolved"} envelope from a referrer node's
46+
* source envelope (typically a {@link JsonSource} or {@link YamlSource}
47+
* parse-time source) plus the referrer FQN and unresolved target string.
48+
*
49+
* <p>The resolved envelope carries:</p>
50+
* <ul>
51+
* <li>{@code files}: the referrer's source files (so editors can jump to it),</li>
52+
* <li>{@code jsonPath}: the referrer's jsonPath when known (the location of
53+
* the broken reference on disk),</li>
54+
* <li>{@code referrer}: the FQN of the metadata node that declared the broken
55+
* reference (e.g. {@code "acme::Premium"}),</li>
56+
* <li>{@code target}: the unresolved reference string itself (e.g.
57+
* {@code "BaseEntity"}, {@code "Program.weeks.invalid"},
58+
* {@code "DoesNotExist"}).</li>
59+
* </ul>
60+
*
61+
* <p>{@code referrerSource} may be any {@link ErrorSource} variant — we read its
62+
* files/jsonPath best-effort and fall back to an empty {@code files: []} when
63+
* the referrer's source is itself a {@link CodeSource}/{@link DatabaseSource}
64+
* envelope. Mirrors the TS {@code resolvedSource(referrerSource, referrer, target)}
65+
* helper in {@code server/typescript/packages/metadata/src/source.ts}.</p>
66+
*
67+
* @param referrerSource the source envelope of the node declaring the broken reference
68+
* @param referrer the FQN of the referrer node; may be {@code null}
69+
* @param target the unresolved reference string; may be {@code null}
70+
* @return a {@code format: "resolved"} envelope populated from the referrer
71+
*/
72+
public static ResolvedSource from(ErrorSource referrerSource, String referrer, String target) {
73+
List<String> files;
74+
String jsonPath;
75+
if (referrerSource instanceof JsonSource js) {
76+
files = js.files();
77+
jsonPath = js.jsonPath();
78+
} else if (referrerSource instanceof YamlSource ys) {
79+
files = ys.files();
80+
jsonPath = ys.jsonPath();
81+
} else if (referrerSource instanceof MergedSource ms) {
82+
files = ms.files();
83+
jsonPath = ms.jsonPath();
84+
} else if (referrerSource instanceof ResolvedSource rs) {
85+
files = rs.files();
86+
jsonPath = rs.jsonPath();
87+
} else if (referrerSource instanceof DatabaseSource ds) {
88+
files = List.of();
89+
jsonPath = ds.jsonPath();
90+
} else {
91+
// CodeSource or null
92+
files = List.of();
93+
jsonPath = null;
94+
}
95+
return new ResolvedSource(files, jsonPath, referrer, target);
96+
}
97+
4498
@Override
4599
public String format() {
46100
return "resolved";

0 commit comments

Comments
 (0)