@metaobjectsdev/metadata 0.15.19 → 0.15.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/core-types.d.ts.map +1 -1
  2. package/dist/core-types.js +14 -1
  3. package/dist/core-types.js.map +1 -1
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/errors.js +5 -6
  7. package/dist/errors.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  13. package/dist/loader/meta-data-loader.js +1 -2
  14. package/dist/loader/meta-data-loader.js.map +1 -1
  15. package/dist/loader/validation-passes.d.ts +0 -14
  16. package/dist/loader/validation-passes.d.ts.map +1 -1
  17. package/dist/loader/validation-passes.js +50 -71
  18. package/dist/loader/validation-passes.js.map +1 -1
  19. package/dist/loader/validation-registry.d.ts.map +1 -1
  20. package/dist/loader/validation-registry.js +25 -20
  21. package/dist/loader/validation-registry.js.map +1 -1
  22. package/dist/naming-refs.d.ts +42 -26
  23. package/dist/naming-refs.d.ts.map +1 -1
  24. package/dist/naming-refs.js +67 -39
  25. package/dist/naming-refs.js.map +1 -1
  26. package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
  27. package/dist/persistence/origin/origin-constants.js +4 -3
  28. package/dist/persistence/origin/origin-constants.js.map +1 -1
  29. package/dist/persistence/origin/origin-definition.embedded.js +1 -1
  30. package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
  31. package/dist/persistence/source/validate-source-parameter-ref.d.ts.map +1 -1
  32. package/dist/persistence/source/validate-source-parameter-ref.js +7 -18
  33. package/dist/persistence/source/validate-source-parameter-ref.js.map +1 -1
  34. package/dist/validation-types.d.ts +4 -2
  35. package/dist/validation-types.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/core-types.ts +14 -1
  38. package/src/errors.ts +5 -6
  39. package/src/index.ts +1 -1
  40. package/src/loader/meta-data-loader.ts +1 -2
  41. package/src/loader/validation-passes.ts +54 -72
  42. package/src/loader/validation-registry.ts +25 -17
  43. package/src/naming-refs.ts +66 -40
  44. package/src/persistence/origin/origin-constants.ts +4 -3
  45. package/src/persistence/origin/origin-definition.embedded.ts +1 -1
  46. package/src/persistence/source/validate-source-parameter-ref.ts +7 -19
  47. package/src/validation-types.ts +4 -2
@@ -14,7 +14,7 @@ import type { MetaData } from "../shared/meta-data.js";
14
14
  import type { MetaObject } from "../core/object/meta-object.js";
15
15
  import type { MetaReferenceIdentity } from "../core/identity/meta-identity.js";
16
16
  import { ParseError } from "../errors.js";
17
- import { refMatchesObject, resolveObjectRef, REF_BEARING_ATTR_NAMES } from "../naming-refs.js";
17
+ import { resolveObjectRef, didYouMeanHint } from "../naming-refs.js";
18
18
  import { PACKAGE_SEPARATOR, CHILD_REF_SEPARATOR } from "../shared/structural.js";
19
19
  import { resolvedSource, type ErrorSource } from "../source.js";
20
20
  import {
@@ -222,11 +222,14 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
222
222
  }
223
223
  }
224
224
 
225
+ // ADR-0042 — a bare @payloadRef/@responseRef resolves in the template's package.
226
+ const referrerPkg = tmpl.package ?? tmpl.fileDefaultPackage ?? "";
225
227
  // ADR-0039: resolving — a template may inherit @payloadRef via extends.
226
228
  const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
227
229
  if (typeof payloadRef !== "string") continue; // absence handled by the required-attr schema check
228
230
  // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
229
- const payload = root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
231
+ // ADR-0042: resolveObjectRef prefers the referrer's own package before a root-level object.
232
+ const payload = resolveObjectRef(root, payloadRef, referrerPkg).node;
230
233
  if (!payload || payload.subType !== OBJECT_SUBTYPE_VALUE) {
231
234
  // FR5d — @payloadRef is a reference; emit format=resolved with
232
235
  // referrer=template FQN, target=the unresolved payloadRef string.
@@ -245,7 +248,8 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
245
248
  const responseRef = tmpl.attr(TEMPLATE_ATTR_RESPONSE_REF);
246
249
  if (typeof responseRef === "string") {
247
250
  // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
248
- const resVo = root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, responseRef));
251
+ // ADR-0042: resolveObjectRef prefers the referrer's own package before a root-level object.
252
+ const resVo = resolveObjectRef(root, responseRef, referrerPkg).node;
249
253
  if (!resVo || resVo.subType !== OBJECT_SUBTYPE_VALUE) {
250
254
  errors.push(
251
255
  new ParseError(
@@ -282,56 +286,12 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
282
286
  return errors;
283
287
  }
284
288
 
285
- // ---------------------------------------------------------------------------
286
- // Cross-package reference ambiguity (the object-ref contract)
287
- // ---------------------------------------------------------------------------
288
-
289
- /**
290
- * A BARE object reference (no `::`) that names an object present in MORE THAN ONE
291
- * package, with NO match in the referrer's own package, is ambiguous → emit
292
- * ERR_AMBIGUOUS_REF (the author must qualify it with the package). An FQN ref is
293
- * exact (never ambiguous); a bare ref that matches the referrer's own package, or
294
- * exactly one package anywhere, resolves fine. Covers every object-ref-bearing
295
- * attr in `REF_BEARING_ATTR_NAMES` (@objectRef / @references / @from / @of / @via
296
- * / @parameterRef / @payloadRef / @responseRef) — the dotted `.child` tail of the
297
- * origin heads is stripped to the entity OWNER. `extends` is intentionally NOT
298
- * covered: its FR-032 super-resolver is same-package/root-strict and never
299
- * matches a packaged object by bare name, so a bare cross-package `extends` is
300
- * unresolved (ERR_UNRESOLVED_SUPER), not ambiguous.
301
- */
302
- export function validateCrossPackageRefs(root: MetaData): ParseError[] {
303
- const errors: ParseError[] = [];
304
- for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
305
- const referrerPkg = obj.package ?? obj.fileDefaultPackage ?? "";
306
- const visit = (node: MetaData): void => {
307
- for (const attrName of REF_BEARING_ATTR_NAMES) {
308
- const raw = node.ownAttr(attrName);
309
- if (typeof raw !== "string") continue;
310
- // Owner = the object part; strip any FR-024 dotted `.child` tail. An FQN
311
- // owner (has `::`) is resolved exactly and can never be ambiguous.
312
- const dot = raw.indexOf(CHILD_REF_SEPARATOR);
313
- const owner = dot === -1 ? raw : raw.slice(0, dot);
314
- if (owner.includes(PACKAGE_SEPARATOR) || owner === "") continue;
315
- if (!resolveObjectRef(root, owner, referrerPkg).ambiguous) continue;
316
- const pkgs = root
317
- .children()
318
- .filter((c) => c.type === TYPE_OBJECT && c.name === owner)
319
- .map((c) => c.resolutionKey());
320
- errors.push(
321
- new ParseError(
322
- `${attrName} "${raw}" on ${obj.fqn()}: bare reference "${owner}" is ambiguous — it names an object ` +
323
- `in multiple packages (${pkgs.join(", ")}) and none is in the referrer's package "${referrerPkg}". ` +
324
- `Qualify it with the package (FQN).`,
325
- { code: "ERR_AMBIGUOUS_REF", source: resolvedSource(node.source, obj.fqn(), owner) },
326
- ),
327
- );
328
- }
329
- for (const c of node.ownChildren()) visit(c);
330
- };
331
- visit(obj);
332
- }
333
- return errors;
334
- }
289
+ // ADR-0042 — the cross-package ambiguity pass (ERR_AMBIGUOUS_REF) is RETIRED.
290
+ // A bare reference now resolves package-locally (referrer's package, else
291
+ // root-level) at every ref site via resolveObjectRef / refMatchesObject, so
292
+ // cross-package ambiguity is unreachable; an unresolved ref fails closed with
293
+ // its per-attr code (ERR_INVALID_RELATIONSHIP / ERR_INVALID_REFERENCE /
294
+ // ERR_UNRESOLVED_OBJECT_REF / ERR_INVALID_ORIGIN / ERR_INVALID_TEMPLATE).
335
295
 
336
296
  // ---------------------------------------------------------------------------
337
297
  // @filterable without index validation
@@ -418,11 +378,11 @@ export function validateFilterableHasSupportedOps(root: MetaData): ParseError[]
418
378
  // allowedValues on the origin.aggregate @agg attr schema — not here.
419
379
  // ---------------------------------------------------------------------------
420
380
 
421
- function _findObject(root: MetaData, name: string): MetaData | undefined {
422
- // FR-032 origin heads are FQN-qualified after the desugar/sweep; match on
423
- // the effective FQN resolution key (with bare back-compat).
424
- // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
425
- return root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
381
+ function _findObject(root: MetaData, name: string, referrerPkg: string): MetaData | undefined {
382
+ // ADR-0042 package-local resolution an FQN resolves exactly on its
383
+ // resolution key; a bare name resolves in the referrer's package, else a
384
+ // root-level object. Shares the single resolveObjectRef matcher.
385
+ return resolveObjectRef(root, name, referrerPkg).node;
426
386
  }
427
387
 
428
388
  function _findField(obj: MetaData, name: string): MetaData | undefined {
@@ -490,6 +450,8 @@ function _validateFromPath(
490
450
  // FR5d — referrer is `<projection-FQN>::<fieldName>` (the canonical
491
451
  // "where the broken reference lives" identifier).
492
452
  const referrer = `${projection.fqn()}::${fieldName}`;
453
+ // ADR-0042 — a bare @from/@of head resolves in the projection's package.
454
+ const referrerPkg = projection.package ?? projection.fileDefaultPackage ?? "";
493
455
  const dotIdx = fromAttr.indexOf(".");
494
456
  if (dotIdx < 1 || dotIdx === fromAttr.length - 1) {
495
457
  // Malformed shape (not "Entity.field") — not a reference resolution
@@ -508,7 +470,7 @@ function _validateFromPath(
508
470
  }
509
471
  const entityName = fromAttr.slice(0, dotIdx);
510
472
  const targetFieldName = fromAttr.slice(dotIdx + 1);
511
- const sourceObj = _findObject(root, entityName);
473
+ const sourceObj = _findObject(root, entityName, referrerPkg);
512
474
  if (!sourceObj) {
513
475
  // FR5d — entity half of the ref didn't resolve. target = full ref.
514
476
  errors.push(
@@ -555,6 +517,8 @@ function _validateViaPath(
555
517
  const projectionName = projection.name;
556
518
  // FR5d — referrer is `<projection-FQN>::<fieldName>`.
557
519
  const referrer = `${projection.fqn()}::${fieldName}`;
520
+ // ADR-0042 — a bare @via HEAD resolves in the projection's package.
521
+ const referrerPkg = projection.package ?? projection.fileDefaultPackage ?? "";
558
522
  const segments = viaAttr.split(".");
559
523
  if (segments.length < 2) {
560
524
  errors.push(
@@ -569,7 +533,7 @@ function _validateViaPath(
569
533
  return undefined;
570
534
  }
571
535
  const [entityName, ...relSegments] = segments as [string, ...string[]];
572
- let currentObj = _findObject(root, entityName);
536
+ let currentObj = _findObject(root, entityName, referrerPkg);
573
537
  if (!currentObj) {
574
538
  errors.push(
575
539
  new ParseError(
@@ -624,7 +588,13 @@ function _validateViaPath(
624
588
  );
625
589
  return undefined;
626
590
  }
627
- const nextObj = _findObject(root, refTarget);
591
+ // ADR-0042 the hop target (@objectRef/@references) resolves in the package
592
+ // of the entity that DECLARES the relationship/reference, i.e. currentObj.
593
+ const nextObj = _findObject(
594
+ root,
595
+ refTarget,
596
+ currentObj.package ?? currentObj.fileDefaultPackage ?? "",
597
+ );
628
598
  if (!nextObj) {
629
599
  // FR5d — relationship's @objectRef points at a missing entity. This
630
600
  // is the @objectRef-resolution edge of the via-path walk (the "5th
@@ -682,14 +652,18 @@ function _hopCardinality(rel: MetaData): string | undefined {
682
652
  * anchor `Product` (what the author wrote), never `BaseEntity` (where the
683
653
  * child physically lives).
684
654
  */
685
- function _refNamedOwner(node: MetaData, root: MetaData): MetaData | undefined {
655
+ function _refNamedOwner(node: MetaData, root: MetaData, referrerPkg: string): MetaData | undefined {
686
656
  const ref = node.superRef;
687
657
  if (ref === undefined) return undefined;
688
- const lastSep = ref.lastIndexOf("::");
689
- const tail = lastSep === -1 ? ref : ref.slice(lastSep + 2);
690
- const dot = tail.indexOf(".");
691
- if (dot <= 0) return undefined;
692
- return _findObject(root, tail.slice(0, dot));
658
+ // Owner = everything before the child dot in the FINAL ::-segment (the object
659
+ // the extends anchors at). ADR-0042: resolve it AS AUTHORED an FQN owner
660
+ // (`acme::Customer`) resolves exactly, a bare owner (`Product`) resolves in
661
+ // the referrer's package. Do NOT strip the package to a bare tail.
662
+ const lastSep = ref.lastIndexOf(PACKAGE_SEPARATOR);
663
+ const segStart = lastSep === -1 ? 0 : lastSep + PACKAGE_SEPARATOR.length;
664
+ const dotInSeg = ref.indexOf(CHILD_REF_SEPARATOR, segStart);
665
+ if (dotInSeg <= segStart) return undefined; // no dotted child owner
666
+ return _findObject(root, ref.slice(0, dotInSeg), referrerPkg);
693
667
  }
694
668
 
695
669
  function _deriveBaseEntity(
@@ -700,6 +674,8 @@ function _deriveBaseEntity(
700
674
  errors: ParseError[],
701
675
  ): MetaData | undefined {
702
676
  if (obj.subType !== OBJECT_SUBTYPE_PROJECTION) return obj;
677
+ // ADR-0042 — a bare extends owner resolves in this projection's package.
678
+ const referrerPkg = obj.package ?? obj.fileDefaultPackage ?? "";
703
679
 
704
680
  // 1) The extended identity anchors the base entity (declared, not inferred).
705
681
  // The anchor is the entity NAMED in the ref's owner part — see _refNamedOwner.
@@ -708,7 +684,7 @@ function _deriveBaseEntity(
708
684
  for (const identity of obj.ownChildren().filter((c) => c.type === TYPE_IDENTITY)) {
709
685
  const extended = identity.superResolved;
710
686
  if (extended !== undefined && extended.type === TYPE_IDENTITY) {
711
- const named = _refNamedOwner(identity, root);
687
+ const named = _refNamedOwner(identity, root, referrerPkg);
712
688
  if (named !== undefined) return named;
713
689
  const owner = extended.parent;
714
690
  if (owner !== undefined && owner.type === TYPE_OBJECT) return owner;
@@ -723,7 +699,7 @@ function _deriveBaseEntity(
723
699
  for (const f of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
724
700
  const sup = f.superResolved;
725
701
  if (sup === undefined) continue;
726
- const named = _refNamedOwner(f, root);
702
+ const named = _refNamedOwner(f, root, referrerPkg);
727
703
  const owner = named ?? sup.parent;
728
704
  if (
729
705
  owner !== undefined &&
@@ -1459,6 +1435,8 @@ export function validateRelationships(root: MetaData): ParseError[] {
1459
1435
  const errors: ParseError[] = [];
1460
1436
  // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
1461
1437
  for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
1438
+ // ADR-0042 — a bare @through resolves in the declaring entity's package.
1439
+ const referrerPkg = obj.package ?? obj.fileDefaultPackage ?? "";
1462
1440
  // ADR-0039: own — a relationship is validated on the entity that DECLARES it
1463
1441
  // (the M:N slim-vocabulary rules apply to own-declared relationships; its
1464
1442
  // inheritable attrs are read resolving below).
@@ -1521,7 +1499,11 @@ export function validateRelationships(root: MetaData): ParseError[] {
1521
1499
  }
1522
1500
 
1523
1501
  // Rule (a): @symmetric is valid only on a self-join (@objectRef == declaring entity).
1524
- const isSelfJoin = typeof objectRef === "string" && stripPackage(objectRef) === obj.name;
1502
+ // ADR-0042: resolve @objectRef and compare NODE IDENTITY a bare "Widget"
1503
+ // in this package is self, but an FQN "other::Widget" (a different same-short-
1504
+ // name entity) is NOT (comparing stripped short names would misclassify it).
1505
+ const isSelfJoin =
1506
+ typeof objectRef === "string" && resolveObjectRef(root, objectRef, referrerPkg).node === obj;
1525
1507
  if (symmetric && !isSelfJoin) {
1526
1508
  errors.push(
1527
1509
  new ParseError(
@@ -1533,11 +1515,11 @@ export function validateRelationships(root: MetaData): ParseError[] {
1533
1515
  }
1534
1516
 
1535
1517
  // Rule (c): @through must name an entity declaring exactly two identity.reference children.
1536
- const junction = _findObject(root, through as string);
1518
+ const junction = _findObject(root, through as string, referrerPkg);
1537
1519
  if (!junction) {
1538
1520
  errors.push(
1539
1521
  new ParseError(
1540
- `relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" does not resolve to an entity.`,
1522
+ `relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" does not resolve to an entity.${didYouMeanHint(root, String(through))}`,
1541
1523
  { code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${obj.fqn()}::${rel.name}`, String(through)) },
1542
1524
  ),
1543
1525
  );
@@ -8,34 +8,35 @@
8
8
 
9
9
  import type { MetaData } from "../shared/meta-data.js";
10
10
  import { ParseError } from "../errors.js";
11
- import { refMatchesObject } from "../naming-refs.js";
11
+ import { didYouMeanHint } from "../naming-refs.js";
12
12
  import { TYPE_OBJECT } from "../shared/base-types.js";
13
+ import { PACKAGE_SEPARATOR } from "../shared/structural.js";
13
14
  import type { TypeRegistry } from "../registry.js";
14
15
  import type { LoaderCode, SymbolTable, ValidationContext } from "../validation-types.js";
15
16
 
16
17
  /** A symbol table of every top-level object, built once per load (the binder analogue). */
17
18
  class SymbolTableImpl implements SymbolTable {
18
- private readonly byRef = new Map<string, MetaData>();
19
+ private readonly byKey = new Map<string, MetaData>();
19
20
 
20
21
  static build(root: MetaData): SymbolTableImpl {
21
22
  const t = new SymbolTableImpl();
22
23
  // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
23
24
  for (const child of root.children()) {
24
25
  if (child.type !== TYPE_OBJECT) continue;
25
- if (child.name) t.byRef.set(child.name, child);
26
- t.byRef.set(child.fqn(), child);
27
- t.byRef.set(child.resolutionKey(), child);
26
+ // ADR-0042: key by the canonical resolution key ONLY (the FQN, or the bare
27
+ // name for a root-level/empty-package object) — NO bare-name fallback, so a
28
+ // bare ref never binds a same-named object in another package.
29
+ t.byKey.set(child.resolutionKey(), child);
28
30
  }
29
31
  return t;
30
32
  }
31
33
 
32
- resolveObject(ref: string): MetaData | undefined {
33
- const hit = this.byRef.get(ref);
34
- if (hit) return hit;
35
- for (const obj of this.byRef.values()) {
36
- if (refMatchesObject(obj, ref)) return obj;
37
- }
38
- return undefined;
34
+ /** ADR-0042 package-local resolution: FQN exact resolution-key match; bare →
35
+ * the referrer's own package (`<referrerPkg>::<ref>`), else a root-level object. */
36
+ resolveObject(ref: string, referrerPkg: string): MetaData | undefined {
37
+ if (ref.includes(PACKAGE_SEPARATOR)) return this.byKey.get(ref);
38
+ const localKey = referrerPkg !== "" ? `${referrerPkg}${PACKAGE_SEPARATOR}${ref}` : ref;
39
+ return this.byKey.get(localKey) ?? this.byKey.get(ref);
39
40
  }
40
41
  }
41
42
 
@@ -54,10 +55,17 @@ class ValidationContextImpl implements ValidationContext {
54
55
  */
55
56
  export function runRegisteredValidation(root: MetaData, registry: TypeRegistry): ParseError[] {
56
57
  const ctx = new ValidationContextImpl(SymbolTableImpl.build(root));
57
- walk(root);
58
+ walk(root, "");
58
59
  return ctx.errors;
59
60
 
60
- function walk(node: MetaData): void {
61
+ function walk(node: MetaData, referrerPkg: string): void {
62
+ // ADR-0042: a top-level object establishes the package context for its
63
+ // subtree; nested ref-bearing nodes (relationship/field.object/
64
+ // identity.reference) resolve BARE refs against it. Nested nodes carry no
65
+ // `package`, so they inherit the enclosing object's (via fileDefaultPackage,
66
+ // else the threaded context).
67
+ const pkg =
68
+ node.type === TYPE_OBJECT ? (node.package ?? node.fileDefaultPackage ?? referrerPkg) : referrerPkg;
61
69
  const def = registry.find(node.type, node.subType);
62
70
  if (def) {
63
71
  for (const desc of def.references ?? []) {
@@ -66,7 +74,7 @@ export function runRegisteredValidation(root: MetaData, registry: TypeRegistry):
66
74
  const raw = node.attr(desc.attr);
67
75
  if (typeof raw !== "string" || raw === "") continue; // absence is the required-attr pass's job
68
76
  const entityRef = desc.dottedFieldPath ? (raw.split(".")[0] ?? raw) : raw;
69
- const target = ctx.symbols.resolveObject(entityRef);
77
+ const target = ctx.symbols.resolveObject(entityRef, pkg);
70
78
  // Qualify the node name with its owning entity (e.g. "Order.items") so the error is
71
79
  // locatable from the message alone, not just the source envelope.
72
80
  const qname = node.parent?.name ? `${node.parent.name}.${node.name}` : node.name;
@@ -74,7 +82,7 @@ export function runRegisteredValidation(root: MetaData, registry: TypeRegistry):
74
82
  ctx.error(
75
83
  desc.errorCode,
76
84
  node,
77
- `${node.type}.${node.subType} "${qname}" @${desc.attr} "${raw}" does not resolve to an object.`,
85
+ `${node.type}.${node.subType} "${qname}" @${desc.attr} "${raw}" does not resolve to an object.${didYouMeanHint(root, entityRef)}`,
78
86
  );
79
87
  } else if (
80
88
  target.type !== desc.targetType ||
@@ -93,6 +101,6 @@ export function runRegisteredValidation(root: MetaData, registry: TypeRegistry):
93
101
  }
94
102
  // ADR-0039: own — structural walk visiting every physical node once at its
95
103
  // declaration site (an inherited child is validated on its declaring parent).
96
- for (const child of node.ownChildren()) walk(child);
104
+ for (const child of node.ownChildren()) walk(child, pkg);
97
105
  }
98
106
  }
@@ -29,7 +29,7 @@
29
29
  import type { MetaData } from "./shared/meta-data.js";
30
30
  import { PACKAGE_SEPARATOR, PACKAGE_PARENT, CHILD_REF_SEPARATOR } from "./shared/structural.js";
31
31
  import { TYPE_OBJECT } from "./shared/base-types.js";
32
- import { RELATIONSHIP_ATTR_OBJECT_REF } from "./core/relationship/relationship-constants.js";
32
+ import { RELATIONSHIP_ATTR_OBJECT_REF, RELATIONSHIP_ATTR_THROUGH } from "./core/relationship/relationship-constants.js";
33
33
  import { FIELD_ATTR_OBJECT_REF } from "./core/field/field-constants.js";
34
34
  import { IDENTITY_REFERENCE_ATTR_REFERENCES } from "./core/identity/identity-constants.js";
35
35
  import {
@@ -52,12 +52,15 @@ const PARENT_PREFIX = PACKAGE_PARENT + PACKAGE_SEPARATOR; // "..::"
52
52
  * a dotted relationship/field tail — expandRef preserves the tail);
53
53
  * `@parameterRef`/`@payloadRef`/`@responseRef` reference value-objects. These are
54
54
  * expanded by the YAML desugar and rejected (when still relative) by the
55
- * canonical-JSON guard. `@sourceRefField` (a FK FIELD name, not an object ref)
56
- * and `@through` are intentionally NOT in this set (out of scope for FR-032 T-slice).
55
+ * canonical-JSON guard. `@through` (the M:N junction ref) is in the set per
56
+ * ADR-0042 §4 it desugars to FQN and resolves package-local like every other
57
+ * object ref. `@sourceRefField` (a FK FIELD name, not an object ref) is NOT in
58
+ * the set.
57
59
  */
58
60
  export const REF_BEARING_ATTR_NAMES: ReadonlySet<string> = new Set<string>([
59
61
  RELATIONSHIP_ATTR_OBJECT_REF, // = FIELD_ATTR_OBJECT_REF (same spelling "objectRef")
60
62
  FIELD_ATTR_OBJECT_REF,
63
+ RELATIONSHIP_ATTR_THROUGH, // ADR-0042: the M:N junction ref joins the desugar+resolution set.
61
64
  IDENTITY_REFERENCE_ATTR_REFERENCES,
62
65
  ORIGIN_PASSTHROUGH_ATTR_FROM,
63
66
  ORIGIN_PASSTHROUGH_ATTR_VIA, // = ORIGIN_AGGREGATE_ATTR_VIA = ORIGIN_COLLECTION_ATTR_VIA ("via")
@@ -146,56 +149,79 @@ export function expandRef(raw: string, packageContext: string): string {
146
149
  }
147
150
 
148
151
  /**
149
- * FR-032 — does a root-level object `node` satisfy an (already-expanded)
150
- * object reference `ref`? After the YAML desugar + corpus sweep every ref is
151
- * fully qualified, so resolution is a pure FQN match. Objects keep a BARE
152
- * `fqn()` per the FR5d cross-port contract, so the canonical FQN accessor is
153
- * `resolutionKey()` (`<package | fileDefaultPackage>::<name>`) this mirrors
154
- * `super-resolve`'s `findInTree`. The bare `name`/`fqn()` arms cover legacy
155
- * same-tree refs and root-level (empty-package) objects.
152
+ * ADR-0042 — does root-level object `node` satisfy object reference `ref`,
153
+ * declared by a node whose effective package is `referrerPkg`? A single
154
+ * package-local matcher:
155
+ * - **FQN** `ref` (contains `::`) EXACT match on `resolutionKey()`. No
156
+ * bare-tail fallback, so an FQN pointing at one package never binds a
157
+ * same-named object in another.
158
+ * - **bare** `ref` (no `::`) → the referrer's OWN package
159
+ * (`<referrerPkg>::<ref>`), else a **root-level** (empty-package) object
160
+ * whose resolution key IS `ref`. No cross-package bare resolution, no
161
+ * globally-unique scan.
156
162
  *
157
- * This is the single matcher the non-super resolvers (origin `@from`/`@of`/
158
- * `@via` heads, template `@payloadRef`/`@responseRef`, source `@parameterRef`)
159
- * share, so FQN matching behaves identically everywhere a ref resolves.
163
+ * Objects keep a BARE `fqn()` per the FR5d cross-port contract, so the canonical
164
+ * FQN accessor is `resolutionKey()` (`<package | fileDefaultPackage>::<name>`)
165
+ * this mirrors `super-resolve`'s `findInTree`. This is the single matcher the
166
+ * non-super resolvers (origin `@from`/`@of`/`@via` heads, template
167
+ * `@payloadRef`/`@responseRef`, source `@parameterRef`, relationship `@through`)
168
+ * share, so resolution behaves identically everywhere a ref resolves.
169
+ *
170
+ * `referrerPkg` defaults to `""` — the fail-closed FQN-exact-plus-root-level
171
+ * behavior. The LOADER always passes the real referrer package (bare refs there
172
+ * must resolve package-locally); downstream CODEGEN resolvers, which run on
173
+ * already-validated metadata whose refs are FQN, may omit it. An empty
174
+ * `referrerPkg` never binds a bare ref across a package boundary (fail-closed),
175
+ * so omission can only fail to resolve — never mis-resolve.
160
176
  */
161
- export function refMatchesObject(node: MetaData, ref: string): boolean {
162
- return node.resolutionKey() === ref || node.fqn() === ref || node.name === ref;
163
- }
164
-
165
- /** The effective package of a root-level object (its `resolutionKey()` minus the
166
- * trailing `::<name>`; "" for a root-level/empty-package object). */
167
- function objectPackage(node: MetaData): string {
177
+ export function refMatchesObject(node: MetaData, ref: string, referrerPkg = ""): boolean {
168
178
  const key = node.resolutionKey();
169
- const i = key.lastIndexOf(PACKAGE_SEPARATOR);
170
- return i === -1 ? "" : key.slice(0, i);
179
+ if (ref.includes(PACKAGE_SEPARATOR)) return key === ref;
180
+ if (referrerPkg !== "" && key === `${referrerPkg}${PACKAGE_SEPARATOR}${ref}`) return true;
181
+ return key === ref; // root-level (empty-package) object whose key is the bare name
171
182
  }
172
183
 
173
184
  /**
174
- * Resolve a metadata OBJECT reference under the cross-package contract:
175
- * - **FQN** (`ref` contains `::`) EXACT match on `resolutionKey()`/`fqn()`.
176
- * Never a bare-tail fallback, so an FQN pointing at one package never binds a
177
- * same-named object in another (the cross-port bug this closes).
178
- * - **bare** (no `::`) prefer an object of that name in the REFERRER's own
179
- * package; else a UNIQUE object of that name across all packages; else
180
- * (>1 across OTHER packages, none in the referrer's) → `ambiguous`.
181
- *
182
- * `referrerPkg` is the effective package of the node carrying the ref. Returns
183
- * `{ node }` on a unique resolution, `{ ambiguous: true }` when a bare ref is
184
- * cross-package-ambiguous, or `{}` (node undefined) when nothing matches. The
185
- * SINGLE resolver every object-ref site shares so the contract is uniform.
185
+ * Resolve a metadata OBJECT reference under the ADR-0042 package-local contract
186
+ * (see `refMatchesObject` for the matcher). `referrerPkg` is the effective
187
+ * package of the node carrying the ref. Returns `{ node }` on resolution, or
188
+ * `{}` (node undefined) when nothing matches there is NO ambiguous outcome
189
+ * (bare = package-local, so ambiguity is unreachable). The SINGLE resolver every
190
+ * object-ref site shares so the contract is uniform.
186
191
  */
187
192
  export function resolveObjectRef(
188
193
  root: MetaData,
189
194
  ref: string,
190
195
  referrerPkg: string,
191
- ): { node?: MetaData | undefined; ambiguous?: boolean } {
196
+ ): { node?: MetaData | undefined } {
192
197
  const objects = root.children().filter((c) => c.type === TYPE_OBJECT);
193
198
  if (ref.includes(PACKAGE_SEPARATOR)) {
194
- return { node: objects.find((c) => c.resolutionKey() === ref || c.fqn() === ref) };
199
+ return { node: objects.find((c) => c.resolutionKey() === ref) };
195
200
  }
196
- const matches = objects.filter((c) => c.name === ref);
197
- if (matches.length <= 1) return { node: matches[0] };
198
- const own = matches.find((c) => objectPackage(c) === referrerPkg);
201
+ // Bare: PREFER the referrer's own package, THEN a root-level (empty-package)
202
+ // object mirroring the loader symbol table's `byKey.get(localKey) ?? get(ref)`
203
+ // so both resolvers agree when a root-level object shares the bare name.
204
+ const localKey = referrerPkg !== "" ? `${referrerPkg}${PACKAGE_SEPARATOR}${ref}` : ref;
205
+ const own = objects.find((c) => c.resolutionKey() === localKey);
199
206
  if (own !== undefined) return { node: own };
200
- return { ambiguous: true };
207
+ return { node: localKey !== ref ? objects.find((c) => c.resolutionKey() === ref) : undefined };
208
+ }
209
+
210
+ /**
211
+ * ADR-0042 §5 — a did-you-mean suffix for an UNRESOLVED object reference: the
212
+ * FQNs of same-short-name objects that DO exist (typically in other packages),
213
+ * so the author can qualify a bare ref they meant to point across a package
214
+ * boundary. Returns "" when no same-short-name object exists (nothing to
215
+ * suggest). Appended to the per-attr unresolved-ref error message.
216
+ */
217
+ export function didYouMeanHint(root: MetaData, ref: string): string {
218
+ const { owner } = splitChildTail(ref);
219
+ const sep = owner.lastIndexOf(PACKAGE_SEPARATOR);
220
+ const shortName = sep === -1 ? owner : owner.slice(sep + PACKAGE_SEPARATOR.length);
221
+ const candidates = root
222
+ .children()
223
+ .filter((c) => c.type === TYPE_OBJECT && c.name === shortName)
224
+ .map((c) => c.resolutionKey());
225
+ if (candidates.length === 0) return "";
226
+ return ` An object named "${shortName}" exists in: ${candidates.join(", ")}. Qualify it with its package (FQN).`;
201
227
  }
@@ -40,9 +40,10 @@ export const ORIGIN_COLLECTION_ATTR_VIA = "via";
40
40
  export const ORIGIN_AGGREGATE_ATTR_AGG = "agg";
41
41
  export const ORIGIN_AGGREGATE_ATTR_OF = "of";
42
42
  export const ORIGIN_AGGREGATE_ATTR_VIA = "via";
43
- // @filter (attr.filter object): optional scoping predicate restricting which
44
- // related rows the aggregate spans rendered as SQL FILTER (WHERE …). Same
45
- // structured shape as a preset filter; consumed by the projection view emitter.
43
+ // @filter (attr.filter object): an optional PORTABLE structured predicate scoping
44
+ // which related rows the aggregate spans (same shape as a preset filter). Codegen
45
+ // renders it per target the projection view emitter turns it into SQL
46
+ // FILTER (WHERE …) / SQLite CASE WHEN.
46
47
  export const ORIGIN_AGGREGATE_ATTR_FILTER = "filter";
47
48
 
48
49
  // aggregate function vocabulary
@@ -90,7 +90,7 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
90
90
  "name": "filter",
91
91
  "min": 0,
92
92
  "max": 1,
93
- "description": "Optional scoping filter restricting which related rows the aggregate spans, rendered as a SQL FILTER (WHERE ...) clause. Structured filter object desugared to canonical { field: { op: value } } form at parse time."
93
+ "description": "Optional structured predicate scoping which related rows the aggregate spans. A portable attr.filter object (eq/ne/in/isNull with and/or), desugared to canonical { field: { op: value } } at parse time; codegen renders it per target (e.g. SQL FILTER (WHERE ...) or SQLite CASE WHEN for a relational view)."
94
94
  }
95
95
  ]
96
96
  },
@@ -24,7 +24,7 @@ import {
24
24
  OBJECT_SUBTYPE_VALUE,
25
25
  OBJECT_SUBTYPE_ENTITY,
26
26
  } from "../../core/object/object-constants.js";
27
- import { PACKAGE_SEPARATOR } from "../../shared/structural.js";
27
+ import { resolveObjectRef } from "../../naming-refs.js";
28
28
  import { MetaSource } from "./meta-source.js";
29
29
  import {
30
30
  SOURCE_ATTR_PARAMETER_REF,
@@ -41,25 +41,13 @@ const CALLABLE_KINDS = new Set<string>([
41
41
  export function validateSourceParameterRef(root: MetaData): ParseError[] {
42
42
  const errors: ParseError[] = [];
43
43
 
44
- // Pre-index every object by name, fqn AND effective FQN resolution key so
45
- // resolution costs O(1) per source. FR-032 — @parameterRef is FQN-qualified
46
- // after the desugar/sweep, but objects keep a BARE fqn() per the FR5d
47
- // contract, so the FQN form `<package | fileDefaultPackage>::<name>` only
48
- // matches via resolutionKey() (these fixtures declare package at metadata.root
49
- // only, so obj.package is undefined and the file-default must be folded in).
50
- const objectIndex = new Map<string, MetaData>();
51
- // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
52
- for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
53
- objectIndex.set(obj.name, obj);
54
- const fqn = obj.package !== undefined && obj.package !== ""
55
- ? `${obj.package}${PACKAGE_SEPARATOR}${obj.name}`
56
- : obj.name;
57
- objectIndex.set(fqn, obj);
58
- objectIndex.set(obj.resolutionKey(), obj);
59
- }
60
-
61
44
  // ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
62
45
  for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
46
+ // ADR-0042: a bare @parameterRef resolves package-local (this object's
47
+ // package, else root-level); an FQN resolves exactly. Shares the single
48
+ // resolveObjectRef matcher — NO bare-name-anywhere fallback (which would
49
+ // silently bind a same-named value-object in another package).
50
+ const referrerPkg = obj.package ?? obj.fileDefaultPackage ?? "";
63
51
  // ADR-0039: own — declaration-layer source iteration (mirrors validateSourceRoles).
64
52
  for (const source of obj.ownChildren().filter(
65
53
  (c): c is MetaSource =>
@@ -82,7 +70,7 @@ export function validateSourceParameterRef(root: MetaData): ParseError[] {
82
70
  continue;
83
71
  }
84
72
 
85
- const target = objectIndex.get(ref);
73
+ const target = resolveObjectRef(root, ref, referrerPkg).node;
86
74
  if (target === undefined) {
87
75
  errors.push(
88
76
  new ParseError(
@@ -30,9 +30,11 @@ export interface ReferenceDescriptor {
30
30
  readonly errorCode: LoaderCode;
31
31
  }
32
32
 
33
- /** Resolve a ref string to its object node. */
33
+ /** Resolve a ref string to its object node under the ADR-0042 package-local
34
+ * contract. `referrerPkg` is the effective package of the node that declares
35
+ * the ref (a bare ref resolves in that package, else root-level). */
34
36
  export interface SymbolTable {
35
- resolveObject(ref: string): MetaData | undefined;
37
+ resolveObject(ref: string, referrerPkg: string): MetaData | undefined;
36
38
  }
37
39
 
38
40
  /** Handed to every validator: the symbol table + an error sink. */