1111
1212import type { MetaData } from "../shared/meta-data.js" ;
1313import { ParseError } from "../errors.js" ;
14- import type { ErrorSource } from "../source.js" ;
14+ import { resolvedSource , type ErrorSource } from "../source.js" ;
1515import {
1616 TYPE_OBJECT ,
1717 TYPE_FIELD ,
@@ -101,10 +101,15 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
101101 if ( typeof payloadRef !== "string" ) continue ; // absence handled by the required-attr schema check
102102 const payload = root . ownChildren ( ) . find ( ( c ) => c . type === TYPE_OBJECT && c . name === payloadRef ) ;
103103 if ( ! payload || payload . subType !== OBJECT_SUBTYPE_VALUE ) {
104+ // FR5d — @payloadRef is a reference; emit format=resolved with
105+ // referrer=template FQN, target=the unresolved payloadRef string.
104106 errors . push (
105107 new ParseError (
106108 `template "${ tmpl . name } " @payloadRef "${ payloadRef } " does not resolve to an object.value at root` ,
107- { code : "ERR_INVALID_TEMPLATE" , source : tmpl . source } ,
109+ {
110+ code : "ERR_INVALID_TEMPLATE" ,
111+ source : resolvedSource ( tmpl . source , tmpl . fqn ( ) , payloadRef ) ,
112+ } ,
108113 ) ,
109114 ) ;
110115 continue ;
@@ -116,11 +121,17 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
116121 const slotList = Array . isArray ( slots ) ? slots : typeof slots === "string" ? [ slots ] : [ ] ;
117122 for ( const slot of slotList ) {
118123 if ( typeof slot === "string" && ! fieldNames . has ( slot ) ) {
124+ // FR5d — @requiredSlots is a field-on-payload reference; emit
125+ // format=resolved with referrer=template FQN, target=`payloadRef.slot`
126+ // (the dotted ref that did not resolve to a payload field).
119127 errors . push (
120128 new ParseError (
121129 `template "${ tmpl . name } " @requiredSlots "${ slot } " is not a field on payload "${ payloadRef } ". ` +
122130 `Available fields: ${ [ ...fieldNames ] . join ( ", " ) } ` ,
123- { code : "ERR_INVALID_TEMPLATE" , source : tmpl . source } ,
131+ {
132+ code : "ERR_INVALID_TEMPLATE" ,
133+ source : resolvedSource ( tmpl . source , tmpl . fqn ( ) , `${ payloadRef } .${ slot } ` ) ,
134+ } ,
124135 ) ,
125136 ) ;
126137 }
@@ -196,18 +207,28 @@ function _findRelationship(obj: MetaData, name: string): MetaData | undefined {
196207function _validateFromPath (
197208 fromAttr : string ,
198209 root : MetaData ,
199- projectionName : string ,
210+ projection : MetaData ,
200211 fieldName : string ,
201212 originSource : ErrorSource ,
202213 errors : ParseError [ ] ,
203214 label : string = "origin.passthrough.@from" ,
204215) : void {
216+ const projectionName = projection . name ;
217+ // FR5d — referrer is `<projection-FQN>::<fieldName>` (the canonical
218+ // "where the broken reference lives" identifier).
219+ const referrer = `${ projection . fqn ( ) } ::${ fieldName } ` ;
205220 const dotIdx = fromAttr . indexOf ( "." ) ;
206221 if ( dotIdx < 1 || dotIdx === fromAttr . length - 1 ) {
222+ // Malformed shape (not "Entity.field") — not a reference resolution
223+ // failure per se, but emit format=resolved with target=the bad string
224+ // so consumers see the same envelope shape across all FR5d sites.
207225 errors . push (
208226 new ParseError (
209227 `${ label } "${ fromAttr } " on ${ projectionName } .${ fieldName } : must be of form "Entity.field".` ,
210- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
228+ {
229+ code : "ERR_INVALID_ORIGIN" ,
230+ source : resolvedSource ( originSource , referrer , fromAttr ) ,
231+ } ,
211232 ) ,
212233 ) ;
213234 return ;
@@ -216,20 +237,28 @@ function _validateFromPath(
216237 const targetFieldName = fromAttr . slice ( dotIdx + 1 ) ;
217238 const sourceObj = _findObject ( root , entityName ) ;
218239 if ( ! sourceObj ) {
240+ // FR5d — entity half of the ref didn't resolve. target = full ref.
219241 errors . push (
220242 new ParseError (
221243 `${ label } "${ fromAttr } " on ${ projectionName } .${ fieldName } : no such entity "${ entityName } ".` ,
222- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
244+ {
245+ code : "ERR_INVALID_ORIGIN" ,
246+ source : resolvedSource ( originSource , referrer , fromAttr ) ,
247+ } ,
223248 ) ,
224249 ) ;
225250 return ;
226251 }
227252 const sourceField = _findField ( sourceObj , targetFieldName ) ;
228253 if ( ! sourceField ) {
254+ // FR5d — entity resolved, field on it did not. target = full ref.
229255 errors . push (
230256 new ParseError (
231257 `${ label } "${ fromAttr } " on ${ projectionName } .${ fieldName } : no such field "${ targetFieldName } " on ${ entityName } .` ,
232- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
258+ {
259+ code : "ERR_INVALID_ORIGIN" ,
260+ source : resolvedSource ( originSource , referrer , fromAttr ) ,
261+ } ,
233262 ) ,
234263 ) ;
235264 }
@@ -238,17 +267,23 @@ function _validateFromPath(
238267function _validateViaPath (
239268 viaAttr : string ,
240269 root : MetaData ,
241- projectionName : string ,
270+ projection : MetaData ,
242271 fieldName : string ,
243272 originSource : ErrorSource ,
244273 errors : ParseError [ ] ,
245274) : void {
275+ const projectionName = projection . name ;
276+ // FR5d — referrer is `<projection-FQN>::<fieldName>`.
277+ const referrer = `${ projection . fqn ( ) } ::${ fieldName } ` ;
246278 const segments = viaAttr . split ( "." ) ;
247279 if ( segments . length < 2 ) {
248280 errors . push (
249281 new ParseError (
250282 `origin.@via "${ viaAttr } " on ${ projectionName } .${ fieldName } : must be of form "Entity.relationship[.relationship...]".` ,
251- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
283+ {
284+ code : "ERR_INVALID_ORIGIN" ,
285+ source : resolvedSource ( originSource , referrer , viaAttr ) ,
286+ } ,
252287 ) ,
253288 ) ;
254289 return ;
@@ -259,18 +294,32 @@ function _validateViaPath(
259294 errors . push (
260295 new ParseError (
261296 `origin.@via "${ viaAttr } " on ${ projectionName } .${ fieldName } : no such entity "${ entityName } ".` ,
262- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
297+ {
298+ code : "ERR_INVALID_ORIGIN" ,
299+ source : resolvedSource ( originSource , referrer , viaAttr ) ,
300+ } ,
263301 ) ,
264302 ) ;
265303 return ;
266304 }
305+ // FR5d — track the deepest-valid-prefix as we walk. The prefix grows
306+ // segment-by-segment; on a hop failure the error message names the prefix
307+ // that DID resolve, so authors can fix multi-hop typos quickly.
308+ // After the entity lookup above, the deepest valid prefix is just the
309+ // entity name; each successful relationship hop appends a segment.
310+ const validSegments : string [ ] = [ entityName ] ;
267311 for ( const relName of relSegments ) {
268312 const rel = _findRelationship ( currentObj , relName ) ;
269313 if ( ! rel ) {
314+ const prefix = validSegments . join ( "." ) ;
270315 errors . push (
271316 new ParseError (
272- `origin.@via "${ viaAttr } " on ${ projectionName } .${ fieldName } : no such relationship "${ relName } " on ${ currentObj . name } .` ,
273- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
317+ `origin.@via "${ viaAttr } " on ${ projectionName } .${ fieldName } : no such relationship "${ relName } " on ${ currentObj . name } . ` +
318+ `Deepest valid prefix was "${ prefix } ".` ,
319+ {
320+ code : "ERR_INVALID_ORIGIN" ,
321+ source : resolvedSource ( originSource , referrer , viaAttr ) ,
322+ } ,
274323 ) ,
275324 ) ;
276325 return ;
@@ -280,21 +329,32 @@ function _validateViaPath(
280329 errors . push (
281330 new ParseError (
282331 `origin.@via "${ viaAttr } " on ${ projectionName } .${ fieldName } : relationship "${ relName } " on ${ currentObj . name } is missing @objectRef.` ,
283- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
332+ {
333+ code : "ERR_INVALID_ORIGIN" ,
334+ source : resolvedSource ( originSource , referrer , viaAttr ) ,
335+ } ,
284336 ) ,
285337 ) ;
286338 return ;
287339 }
288340 const nextObj = _findObject ( root , refTarget ) ;
289341 if ( ! nextObj ) {
342+ // FR5d — relationship's @objectRef points at a missing entity. This
343+ // is the @objectRef -resolution edge of the via-path walk (the "5th
344+ // site" in FR5d's scope list for @objectRef references encountered
345+ // transitively).
290346 errors . push (
291347 new ParseError (
292348 `origin.@via "${ viaAttr } " on ${ projectionName } .${ fieldName } : relationship "${ relName } " points to non-existent entity "${ refTarget } ".` ,
293- { code : "ERR_INVALID_ORIGIN" , source : originSource } ,
349+ {
350+ code : "ERR_INVALID_ORIGIN" ,
351+ source : resolvedSource ( originSource , referrer , refTarget ) ,
352+ } ,
294353 ) ,
295354 ) ;
296355 return ;
297356 }
357+ validSegments . push ( relName ) ;
298358 currentObj = nextObj ;
299359 }
300360}
@@ -307,6 +367,8 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
307367 if ( origin . subType === ORIGIN_SUBTYPE_PASSTHROUGH ) {
308368 const from = origin . ownAttr ( ORIGIN_PASSTHROUGH_ATTR_FROM ) ;
309369 if ( typeof from !== "string" || from === "" ) {
370+ // Missing-attr (not a reference resolution failure) — keep the
371+ // node's own source envelope (json/yaml/merged).
310372 errors . push (
311373 new ParseError (
312374 `origin.passthrough on ${ obj . name } .${ field . name } : missing @from.` ,
@@ -315,10 +377,10 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
315377 ) ;
316378 continue ;
317379 }
318- _validateFromPath ( from , root , obj . name , field . name , origin . source , errors ) ;
380+ _validateFromPath ( from , root , obj , field . name , origin . source , errors ) ;
319381 const via = origin . ownAttr ( ORIGIN_PASSTHROUGH_ATTR_VIA ) ;
320382 if ( typeof via === "string" && via !== "" ) {
321- _validateViaPath ( via , root , obj . name , field . name , origin . source , errors ) ;
383+ _validateViaPath ( via , root , obj , field . name , origin . source , errors ) ;
322384 }
323385 } else if ( origin . subType === ORIGIN_SUBTYPE_AGGREGATE ) {
324386 const of_ = origin . ownAttr ( ORIGIN_AGGREGATE_ATTR_OF ) ;
@@ -331,7 +393,7 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
331393 ) ;
332394 continue ;
333395 }
334- _validateFromPath ( of_ , root , obj . name , field . name , origin . source , errors , "origin.aggregate.@of" ) ;
396+ _validateFromPath ( of_ , root , obj , field . name , origin . source , errors , "origin.aggregate.@of" ) ;
335397 const via = origin . ownAttr ( ORIGIN_AGGREGATE_ATTR_VIA ) ;
336398 if ( typeof via !== "string" || via === "" ) {
337399 errors . push (
@@ -342,7 +404,7 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
342404 ) ;
343405 continue ;
344406 }
345- _validateViaPath ( via , root , obj . name , field . name , origin . source , errors ) ;
407+ _validateViaPath ( via , root , obj , field . name , origin . source , errors ) ;
346408 }
347409 }
348410 }
0 commit comments