@metaobjectsdev/metadata 0.15.19 → 0.15.21-rc.1
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.
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +14 -1
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -6
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +1 -2
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +0 -14
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +50 -71
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/loader/validation-registry.d.ts.map +1 -1
- package/dist/loader/validation-registry.js +25 -20
- package/dist/loader/validation-registry.js.map +1 -1
- package/dist/naming-refs.d.ts +42 -26
- package/dist/naming-refs.d.ts.map +1 -1
- package/dist/naming-refs.js +67 -39
- package/dist/naming-refs.js.map +1 -1
- package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
- package/dist/persistence/origin/origin-constants.js +4 -3
- package/dist/persistence/origin/origin-constants.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/persistence/source/validate-source-parameter-ref.d.ts.map +1 -1
- package/dist/persistence/source/validate-source-parameter-ref.js +7 -18
- package/dist/persistence/source/validate-source-parameter-ref.js.map +1 -1
- package/dist/validation-types.d.ts +4 -2
- package/dist/validation-types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core-types.ts +14 -1
- package/src/errors.ts +5 -6
- package/src/index.ts +1 -1
- package/src/loader/meta-data-loader.ts +1 -2
- package/src/loader/validation-passes.ts +54 -72
- package/src/loader/validation-registry.ts +25 -17
- package/src/naming-refs.ts +66 -40
- package/src/persistence/origin/origin-constants.ts +4 -3
- package/src/persistence/origin/origin-definition.embedded.ts +1 -1
- package/src/persistence/source/validate-source-parameter-ref.ts +7 -19
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
return root
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
}
|
package/src/naming-refs.ts
CHANGED
|
@@ -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. `@
|
|
56
|
-
*
|
|
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
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* `
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
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
|
|
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
|
|
199
|
+
return { node: objects.find((c) => c.resolutionKey() === ref) };
|
|
195
200
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 {
|
|
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
|
|
44
|
-
// related rows the aggregate spans
|
|
45
|
-
//
|
|
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
|
|
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 {
|
|
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 =
|
|
73
|
+
const target = resolveObjectRef(root, ref, referrerPkg).node;
|
|
86
74
|
if (target === undefined) {
|
|
87
75
|
errors.push(
|
|
88
76
|
new ParseError(
|
package/src/validation-types.ts
CHANGED
|
@@ -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. */
|