@metaobjectsdev/metadata 1.0.2 → 1.0.4-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/parser-yaml.d.ts.map +1 -1
- package/dist/core/parser-yaml.js +4 -0
- package/dist/core/parser-yaml.js.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.d.ts +3 -1
- package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.js +110 -15
- package/dist/core/relationship/derive-m2m-fields.js.map +1 -1
- package/dist/core/relationship/find-reference.d.ts +21 -5
- package/dist/core/relationship/find-reference.d.ts.map +1 -1
- package/dist/core/relationship/find-reference.js +29 -10
- package/dist/core/relationship/find-reference.js.map +1 -1
- package/dist/core/relationship/relationship-definition.embedded.js +8 -8
- package/dist/core/relationship/relationship-definition.embedded.js.map +1 -1
- package/dist/core/relationship/resolve-relationship-reference.d.ts +27 -0
- package/dist/core/relationship/resolve-relationship-reference.d.ts.map +1 -0
- package/dist/core/relationship/resolve-relationship-reference.js +84 -0
- package/dist/core/relationship/resolve-relationship-reference.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +31 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/dist/json-path.d.ts +14 -0
- package/dist/json-path.d.ts.map +1 -1
- package/dist/json-path.js +13 -0
- package/dist/json-path.js.map +1 -1
- package/dist/library/embedded-library.generated.d.ts +2 -0
- package/dist/library/embedded-library.generated.d.ts.map +1 -1
- package/dist/library/embedded-library.generated.js +11 -1
- package/dist/library/embedded-library.generated.js.map +1 -1
- package/dist/library/index.d.ts +2 -0
- package/dist/library/index.d.ts.map +1 -0
- package/dist/library/index.js +24 -0
- package/dist/library/index.js.map +1 -0
- package/dist/library/library-sources.d.ts +99 -6
- package/dist/library/library-sources.d.ts.map +1 -1
- package/dist/library/library-sources.js +165 -43
- package/dist/library/library-sources.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts +44 -21
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +118 -88
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/sources/file-source.d.ts +7 -1
- package/dist/loader/sources/file-source.d.ts.map +1 -1
- package/dist/loader/sources/file-source.js +3 -2
- package/dist/loader/sources/file-source.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +2 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +162 -22
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming.d.ts +7 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +10 -0
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts +103 -0
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +202 -18
- package/dist/parser-core.js.map +1 -1
- package/dist/scope.d.ts +16 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +78 -0
- package/dist/scope.js.map +1 -0
- package/dist/serializer-json.d.ts +1 -0
- package/dist/serializer-json.d.ts.map +1 -1
- package/dist/serializer-json.js +43 -1
- package/dist/serializer-json.js.map +1 -1
- package/dist/vocabulary-rewrite-yaml.d.ts +2 -0
- package/dist/vocabulary-rewrite-yaml.d.ts.map +1 -0
- package/dist/vocabulary-rewrite-yaml.js +22 -0
- package/dist/vocabulary-rewrite-yaml.js.map +1 -0
- package/package.json +7 -7
- package/src/core/parser-yaml.ts +4 -0
- package/src/core/relationship/derive-m2m-fields.ts +115 -15
- package/src/core/relationship/find-reference.ts +34 -11
- package/src/core/relationship/relationship-definition.embedded.ts +8 -8
- package/src/core/relationship/resolve-relationship-reference.ts +99 -0
- package/src/errors.ts +31 -0
- package/src/index.ts +18 -5
- package/src/json-path.ts +16 -1
- package/src/library/embedded-library.generated.ts +12 -1
- package/src/library/index.ts +23 -0
- package/src/library/library-sources.ts +197 -47
- package/src/loader/meta-data-loader.ts +154 -102
- package/src/loader/sources/file-source.ts +10 -2
- package/src/loader/validation-passes.ts +181 -23
- package/src/naming.ts +11 -0
- package/src/parser-core.ts +319 -22
- package/src/scope.ts +97 -0
- package/src/serializer-json.ts +46 -1
- package/src/vocabulary-rewrite-yaml.ts +21 -0
|
@@ -13,12 +13,19 @@ import { TypeId, TypeRegistry } from "../registry.js";
|
|
|
13
13
|
import { coreProviders } from "../core-types.js";
|
|
14
14
|
import { composeRegistry } from "../provider.js";
|
|
15
15
|
import { TYPE_METADATA, SUBTYPE_ROOT } from "../shared/base-types.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
PACKAGE_SEPARATOR,
|
|
18
|
+
RESERVED_KEY_CHILDREN,
|
|
19
|
+
RESERVED_KEY_NAME,
|
|
20
|
+
RESERVED_KEY_OVERLAY,
|
|
21
|
+
RESERVED_KEY_PACKAGE,
|
|
22
|
+
TYPE_SUBTYPE_SEPARATOR,
|
|
23
|
+
} from "../shared/structural.js";
|
|
17
24
|
import { ParseError } from "../errors.js";
|
|
18
25
|
import type { LoaderWarning } from "../source.js";
|
|
19
26
|
import { codeSource, resolvedSource } from "../source.js";
|
|
20
27
|
import { parseJson } from "../parser-json.js";
|
|
21
|
-
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter, validateRetiredRequirementLinks } from "./validation-passes.js";
|
|
28
|
+
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateOneSideReferenceResolution, validateIndexLookupFields, validateProjectionFilter, validateRetiredRequirementLinks } from "./validation-passes.js";
|
|
22
29
|
import { runRegisteredValidation } from "./validation-registry.js";
|
|
23
30
|
import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
|
|
24
31
|
import { validateSourceEscapes } from "../persistence/source/validate-source-escapes.js";
|
|
@@ -34,7 +41,8 @@ import { validateIdentityPassthrough } from "../core/identity/validate-identity-
|
|
|
34
41
|
import { validateAttrSchema } from "../attr-schema-validate.js";
|
|
35
42
|
import type { MetaDataFormat, MetaDataSource } from "./meta-data-source.js";
|
|
36
43
|
import { InMemoryStringSource } from "./meta-data-source.js";
|
|
37
|
-
import type { ParseOptions, ParseResult } from "../parser-core.js";
|
|
44
|
+
import type { ParseOptions, ParseResult, PendingOverlay } from "../parser-core.js";
|
|
45
|
+
import { expandPackageForPath, applyPendingOverlays } from "../parser-core.js";
|
|
38
46
|
|
|
39
47
|
// Local mirror of DirectorySource's options shape. Deliberately inlined here
|
|
40
48
|
// (instead of `import type`'d from ./sources/directory-source.js) so the
|
|
@@ -97,6 +105,113 @@ function makeSyntheticRoot(): MetaRoot {
|
|
|
97
105
|
return new MetaRoot(new TypeId(TYPE_METADATA, SUBTYPE_ROOT), "");
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// declaredTopLevelKeys — structural pre-parse walk (#160, generalized)
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/** One root-level declaration as `declaredTopLevelKeys` structurally scans it —
|
|
113
|
+
* before registry-driven parsing, super resolution, or merge. */
|
|
114
|
+
export interface DeclaredTopLevelKey {
|
|
115
|
+
/** The wrapper key's TYPE segment (e.g. "object" for "object.entity"). A bare
|
|
116
|
+
* wrapper key with no "." is its own type — this walk never consults the
|
|
117
|
+
* registry for a default subType, because it never needs the subType. */
|
|
118
|
+
type: string;
|
|
119
|
+
/** The resolution key the declaration would carry once parsed: the child's
|
|
120
|
+
* own `package` if set (expanded against the root's `package` via
|
|
121
|
+
* `expandPackageForPath` when it's a relative, `::`-prefixed path — e.g.
|
|
122
|
+
* root `acme` + own `::garage` → `acme::garage`), else the root's
|
|
123
|
+
* `package` verbatim, then `::`, then its `name`. Exactly
|
|
124
|
+
* `rootChildResolutionKey` (parser-core.ts) — including the relative-path
|
|
125
|
+
* expansion, reusing `expandPackageForPath` rather than reimplementing it,
|
|
126
|
+
* because a second copy is exactly how this walk and the real parser
|
|
127
|
+
* silently disagreed on a relative package (task 17 fix-round-1: this
|
|
128
|
+
* function used to return `::garage::Garage` for a fixture the real
|
|
129
|
+
* loader resolves to `acme::garage::Garage`). */
|
|
130
|
+
key: string;
|
|
131
|
+
/** Whether the declaration's body carries `overlay: true`. */
|
|
132
|
+
overlay: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Structurally scan a source's raw content (JSON via `JSON.parse`; sigil-free
|
|
137
|
+
* authoring YAML via the raw YAML walker — `overlay: true` is a bare key
|
|
138
|
+
* before desugar) and report every top-level declaration under
|
|
139
|
+
* `metadata.root.children`: its type, the resolution key it would carry once
|
|
140
|
+
* parsed, and whether it carries `overlay: true`.
|
|
141
|
+
*
|
|
142
|
+
* A declaration with no (string, non-empty) `name` is skipped — a resolution
|
|
143
|
+
* key can't be computed for it, and the real parser will report it as a
|
|
144
|
+
* loader error rather than silently reusing or creating a node. Malformed
|
|
145
|
+
* root shapes (root missing, not an object, `children` absent or not an
|
|
146
|
+
* array) return `[]` rather than throwing — the real parse loop is where a
|
|
147
|
+
* genuine structural error surfaces; this walk must never crash a caller
|
|
148
|
+
* that is only trying to answer "what does this file declare".
|
|
149
|
+
*
|
|
150
|
+
* Written for the #160 overlay-only source partition, which ADR-0055 retired —
|
|
151
|
+
* overlays are now applied in a post-parse pass, so no ordering decision needs
|
|
152
|
+
* this walk. Its remaining caller is the `meta verify` overlay authoring lint,
|
|
153
|
+
* which needs per-file declaration provenance precisely because the MERGED tree
|
|
154
|
+
* has already lost which file contributed which declaration.
|
|
155
|
+
*/
|
|
156
|
+
export async function declaredTopLevelKeys(
|
|
157
|
+
content: string,
|
|
158
|
+
format: MetaDataFormat,
|
|
159
|
+
): Promise<ReadonlyArray<DeclaredTopLevelKey>> {
|
|
160
|
+
// Strip UTF-8 BOM if present (mirrors parseJson / parseYaml).
|
|
161
|
+
const normalized = content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
162
|
+
let parsed: unknown;
|
|
163
|
+
if (format === "json") {
|
|
164
|
+
parsed = JSON.parse(normalized);
|
|
165
|
+
} else if (format === "yaml") {
|
|
166
|
+
const { parseYamlWithPositions } = await import("../core/yaml-positions-walker.js");
|
|
167
|
+
parsed = parseYamlWithPositions(normalized).value;
|
|
168
|
+
} else {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
return declaredTopLevelKeysFromParsedRoot(parsed);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The structural walk `declaredTopLevelKeys` performs once content is
|
|
175
|
+
* structurally parsed — split out so it takes no format/BOM concerns. */
|
|
176
|
+
function declaredTopLevelKeysFromParsedRoot(parsed: unknown): ReadonlyArray<DeclaredTopLevelKey> {
|
|
177
|
+
if (typeof parsed !== "object" || parsed === null) return [];
|
|
178
|
+
const parsedRecord = parsed as Record<string, unknown>;
|
|
179
|
+
// Canonical JSON always fuses the subType onto the root wrapper key
|
|
180
|
+
// ("metadata.root"); sigil-free YAML authoring writes the bare type
|
|
181
|
+
// ("metadata") and leaves the default subType to the registry — this
|
|
182
|
+
// structural walk runs BEFORE desugar/registry resolution, so it must
|
|
183
|
+
// accept both spellings rather than only ever matching JSON's.
|
|
184
|
+
const explicitRootKey = `${TYPE_METADATA}${TYPE_SUBTYPE_SEPARATOR}${SUBTYPE_ROOT}`; // "metadata.root"
|
|
185
|
+
const rootBody = parsedRecord[explicitRootKey] ?? parsedRecord[TYPE_METADATA];
|
|
186
|
+
if (typeof rootBody !== "object" || rootBody === null) return [];
|
|
187
|
+
const rawRootPkg = (rootBody as Record<string, unknown>)[RESERVED_KEY_PACKAGE];
|
|
188
|
+
const rootPkg = typeof rawRootPkg === "string" ? rawRootPkg : "";
|
|
189
|
+
const children = (rootBody as Record<string, unknown>)[RESERVED_KEY_CHILDREN];
|
|
190
|
+
if (!Array.isArray(children)) return [];
|
|
191
|
+
|
|
192
|
+
const declared: DeclaredTopLevelKey[] = [];
|
|
193
|
+
for (const child of children) {
|
|
194
|
+
if (typeof child !== "object" || child === null) continue;
|
|
195
|
+
// Each child is a single-key wrapper: { "object.entity": { ... } }.
|
|
196
|
+
for (const [wrapperKey, body] of Object.entries(child as Record<string, unknown>)) {
|
|
197
|
+
if (typeof body !== "object" || body === null) continue;
|
|
198
|
+
const bodyRecord = body as Record<string, unknown>;
|
|
199
|
+
const name = bodyRecord[RESERVED_KEY_NAME];
|
|
200
|
+
if (typeof name !== "string" || name === "") continue;
|
|
201
|
+
const dotIdx = wrapperKey.indexOf(TYPE_SUBTYPE_SEPARATOR);
|
|
202
|
+
const type = dotIdx < 0 ? wrapperKey : wrapperKey.slice(0, dotIdx);
|
|
203
|
+
const rawOwnPkg = bodyRecord[RESERVED_KEY_PACKAGE];
|
|
204
|
+
const pkg =
|
|
205
|
+
typeof rawOwnPkg === "string" && rawOwnPkg !== ""
|
|
206
|
+
? expandPackageForPath(rootPkg, rawOwnPkg)
|
|
207
|
+
: rootPkg;
|
|
208
|
+
const key = pkg !== "" ? `${pkg}${PACKAGE_SEPARATOR}${name}` : name;
|
|
209
|
+
declared.push({ type, key, overlay: bodyRecord[RESERVED_KEY_OVERLAY] === true });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return declared;
|
|
213
|
+
}
|
|
214
|
+
|
|
100
215
|
// ---------------------------------------------------------------------------
|
|
101
216
|
// MetaDataLoader class
|
|
102
217
|
// ---------------------------------------------------------------------------
|
|
@@ -330,92 +445,6 @@ export class MetaDataLoader {
|
|
|
330
445
|
MetaDataLoader._yamlParser = mod.parseYaml;
|
|
331
446
|
}
|
|
332
447
|
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// #160 — overlay-only source partition (stable, overlay-only sources last)
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Stable-partition `sources` so that "overlay-only" sources — every top-level
|
|
339
|
-
* object declaration carries `overlay: true`, i.e. the source declares no base
|
|
340
|
-
* objects of its own and only re-opens objects declared elsewhere — are parsed
|
|
341
|
-
* LAST. Preserves original order within each group.
|
|
342
|
-
*
|
|
343
|
-
* A source whose content can't be read or structurally scanned stays in the
|
|
344
|
-
* base group (never overlay-only) — this partition must never crash the loader;
|
|
345
|
-
* any genuine read/parse failure surfaces later, in the real parse loop.
|
|
346
|
-
*/
|
|
347
|
-
private static async _partitionOverlayLast(
|
|
348
|
-
sources: MetaDataSource[],
|
|
349
|
-
): Promise<MetaDataSource[]> {
|
|
350
|
-
const base: MetaDataSource[] = [];
|
|
351
|
-
const overlayOnly: MetaDataSource[] = [];
|
|
352
|
-
for (const source of sources) {
|
|
353
|
-
let isOverlayOnly = false;
|
|
354
|
-
try {
|
|
355
|
-
const content = await source.read();
|
|
356
|
-
isOverlayOnly = await MetaDataLoader._isOverlayOnlySource(
|
|
357
|
-
content,
|
|
358
|
-
source.format,
|
|
359
|
-
);
|
|
360
|
-
} catch {
|
|
361
|
-
isOverlayOnly = false;
|
|
362
|
-
}
|
|
363
|
-
(isOverlayOnly ? overlayOnly : base).push(source);
|
|
364
|
-
}
|
|
365
|
-
return [...base, ...overlayOnly];
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Structurally scan a source's raw content (JSON via JSON.parse; sigil-free
|
|
370
|
-
* authoring YAML via the raw YAML walker — `overlay: true` is a bare key
|
|
371
|
-
* before desugar) and report whether every top-level object declaration under
|
|
372
|
-
* `metadata.root.children` carries `overlay: true` (and there is at least one).
|
|
373
|
-
*/
|
|
374
|
-
private static async _isOverlayOnlySource(
|
|
375
|
-
content: string,
|
|
376
|
-
format: MetaDataFormat,
|
|
377
|
-
): Promise<boolean> {
|
|
378
|
-
// Strip UTF-8 BOM if present (mirrors parseJson / parseYaml).
|
|
379
|
-
const normalized = content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
380
|
-
let parsed: unknown;
|
|
381
|
-
if (format === "json") {
|
|
382
|
-
parsed = JSON.parse(normalized);
|
|
383
|
-
} else if (format === "yaml") {
|
|
384
|
-
const { parseYamlWithPositions } = await import(
|
|
385
|
-
"../core/yaml-positions-walker.js"
|
|
386
|
-
);
|
|
387
|
-
parsed = parseYamlWithPositions(normalized).value;
|
|
388
|
-
} else {
|
|
389
|
-
return false;
|
|
390
|
-
}
|
|
391
|
-
return MetaDataLoader._rootIsOverlayOnly(parsed);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/** True when the structurally-parsed root has ≥1 child and every top-level
|
|
395
|
-
* child node carries `overlay: true` (declares no base objects). */
|
|
396
|
-
private static _rootIsOverlayOnly(parsed: unknown): boolean {
|
|
397
|
-
if (typeof parsed !== "object" || parsed === null) return false;
|
|
398
|
-
const rootKey = `${TYPE_METADATA}.${SUBTYPE_ROOT}`; // "metadata.root"
|
|
399
|
-
const rootBody = (parsed as Record<string, unknown>)[rootKey];
|
|
400
|
-
if (typeof rootBody !== "object" || rootBody === null) return false;
|
|
401
|
-
const children = (rootBody as Record<string, unknown>)[RESERVED_KEY_CHILDREN];
|
|
402
|
-
if (!Array.isArray(children) || children.length === 0) return false;
|
|
403
|
-
return children.every((child) => {
|
|
404
|
-
if (typeof child !== "object" || child === null) return false;
|
|
405
|
-
// Each child is a single-key wrapper: { "object.projection": { ... } }.
|
|
406
|
-
const bodies = Object.values(child as Record<string, unknown>);
|
|
407
|
-
return (
|
|
408
|
-
bodies.length > 0 &&
|
|
409
|
-
bodies.every(
|
|
410
|
-
(body) =>
|
|
411
|
-
typeof body === "object" &&
|
|
412
|
-
body !== null &&
|
|
413
|
-
(body as Record<string, unknown>)[RESERVED_KEY_OVERLAY] === true,
|
|
414
|
-
)
|
|
415
|
-
);
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
|
|
419
448
|
// ---------------------------------------------------------------------------
|
|
420
449
|
// load — async pipeline over MetaDataSource[]
|
|
421
450
|
// ---------------------------------------------------------------------------
|
|
@@ -457,20 +486,17 @@ export class MetaDataLoader {
|
|
|
457
486
|
await MetaDataLoader._ensureYamlParser();
|
|
458
487
|
}
|
|
459
488
|
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
// Directory discovery order is not guaranteed to present base files first
|
|
468
|
-
// (basename sort can put an overlay-only file first), so stable-partition
|
|
469
|
-
// overlay-only sources to the END here, making the merge order-independent.
|
|
470
|
-
// Stable within each group preserves last-writer-wins overlay semantics.
|
|
471
|
-
sources = await MetaDataLoader._partitionOverlayLast(sources);
|
|
472
|
-
|
|
489
|
+
// ADR-0055 — sources are parsed in the order given. Overlays are not applied
|
|
490
|
+
// during the walk: each parse QUEUES its `overlay: true` declarations and the
|
|
491
|
+
// loader applies them below, once every base exists. That retired the #160
|
|
492
|
+
// overlay-only source partition, whose file-level predicate could not rescue a
|
|
493
|
+
// MIXED file (plain + overlay declarations together) and therefore left the
|
|
494
|
+
// load order-dependent — and divergent across ports, since every port's
|
|
495
|
+
// directory walk orders files differently.
|
|
473
496
|
let root: MetaRoot | undefined;
|
|
497
|
+
// ADR-0055 — every source's queue, concatenated in parse order, which is
|
|
498
|
+
// already "source order, then declaration order within a source" (G2).
|
|
499
|
+
const pendingOverlays: PendingOverlay[] = [];
|
|
474
500
|
|
|
475
501
|
// Parse all sources with super resolution DEFERRED so cross-file super
|
|
476
502
|
// refs work — one source may declare a super target that's defined in a
|
|
@@ -495,6 +521,10 @@ export class MetaDataLoader {
|
|
|
495
521
|
registry: this._registry,
|
|
496
522
|
strict: this._strict,
|
|
497
523
|
deferSuperResolution: true,
|
|
524
|
+
// ADR-0055 — queue overlays; this loader drains them after every source
|
|
525
|
+
// is parsed. Without it each document would drain its own queue and an
|
|
526
|
+
// overlay could still not reach a base declared in a later source.
|
|
527
|
+
deferOverlays: true,
|
|
498
528
|
sourceName: source.id,
|
|
499
529
|
};
|
|
500
530
|
if (root !== undefined) parseOpts.intoRoot = root;
|
|
@@ -507,6 +537,7 @@ export class MetaDataLoader {
|
|
|
507
537
|
// source). The legacy `warnings` channel still flows into the
|
|
508
538
|
// WARN_LEGACY-wrapping path below for unchanged behavior.
|
|
509
539
|
envelopeWarnings.push(...parseResult.envelopeWarnings);
|
|
540
|
+
pendingOverlays.push(...parseResult.pendingOverlays);
|
|
510
541
|
root = parseResult.root;
|
|
511
542
|
} catch (err) {
|
|
512
543
|
errors.push(
|
|
@@ -517,6 +548,21 @@ export class MetaDataLoader {
|
|
|
517
548
|
}
|
|
518
549
|
}
|
|
519
550
|
|
|
551
|
+
// ADR-0055 — apply every queued overlay now: all plain declarations from all
|
|
552
|
+
// sources are in the tree (G1), so a base/overlay relation no longer depends
|
|
553
|
+
// on file order (G4). This runs BEFORE super resolution so a node an overlay
|
|
554
|
+
// contributes is visible to every `extends` (G5) — the ordering #160's
|
|
555
|
+
// partition was reaching for, now guaranteed rather than approximated.
|
|
556
|
+
if (root !== undefined && pendingOverlays.length > 0) {
|
|
557
|
+
const applied = applyPendingOverlays(pendingOverlays, {
|
|
558
|
+
registry: this._registry,
|
|
559
|
+
strict: this._strict,
|
|
560
|
+
});
|
|
561
|
+
warnings.push(...applied.warnings);
|
|
562
|
+
errors.push(...applied.errors);
|
|
563
|
+
envelopeWarnings.push(...applied.envelopeWarnings);
|
|
564
|
+
}
|
|
565
|
+
|
|
520
566
|
// Second pass: resolve every deferred super ref against the full tree.
|
|
521
567
|
// Unresolved refs are always errors (matches the original eager-throw
|
|
522
568
|
// behavior — broken metadata is broken regardless of strict mode).
|
|
@@ -606,6 +652,12 @@ export class MetaDataLoader {
|
|
|
606
652
|
// are invalid on a 1:N relationship.
|
|
607
653
|
errors.push(...validateRelationships(root));
|
|
608
654
|
|
|
655
|
+
// Rule (e) — #368: a `@cardinality: one` relationship must resolve to
|
|
656
|
+
// exactly one identity.reference when its declaring entity holds more
|
|
657
|
+
// than one onto the same target; an unresolvable case is a load error
|
|
658
|
+
// naming the candidates (ADR-0029 §5).
|
|
659
|
+
errors.push(...validateOneSideReferenceResolution(root));
|
|
660
|
+
|
|
609
661
|
// index.lookup @fields resolution — each index.lookup must name ≥1 field,
|
|
610
662
|
// and every field must exist in the entity's effective (resolved) field set
|
|
611
663
|
// (ADR-0039: resolving accessor, so inherited fields via extends are visible).
|
|
@@ -27,16 +27,24 @@ async function getReadText(): Promise<(path: string) => Promise<string>> {
|
|
|
27
27
|
return _readText;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Options for {@link FileSource}. */
|
|
31
|
+
export interface FileSourceOptions {
|
|
32
|
+
/** Explicit source id (e.g. `dep:<name>/<artifact>` for a dependency snapshot).
|
|
33
|
+
* Defaults to `basename(path)` when omitted. */
|
|
34
|
+
id?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
/** A metadata source backed by a file on disk. */
|
|
31
38
|
export class FileSource implements MetaDataSource {
|
|
32
39
|
readonly id: string;
|
|
33
40
|
readonly format: MetaDataFormat;
|
|
34
41
|
private readonly _path: string;
|
|
35
42
|
|
|
36
|
-
constructor(path: string) {
|
|
43
|
+
constructor(path: string, opts?: FileSourceOptions) {
|
|
37
44
|
this._path = path;
|
|
38
45
|
// basename() for readable error messages; cross-platform (handles both / and \). Full path retained for read().
|
|
39
|
-
|
|
46
|
+
// An explicit id (FR-023: dependency snapshots load with `dep:<name>/<artifact>`) overrides the default.
|
|
47
|
+
this.id = opts?.id ?? basename(path);
|
|
40
48
|
this.format = inferFormat(path);
|
|
41
49
|
}
|
|
42
50
|
|
|
@@ -129,6 +129,11 @@ import {
|
|
|
129
129
|
CARDINALITY_ONE,
|
|
130
130
|
CARDINALITY_MANY,
|
|
131
131
|
} from "../core/relationship/relationship-constants.js";
|
|
132
|
+
import {
|
|
133
|
+
referenceCandidatesFor,
|
|
134
|
+
resolveRelationshipReference,
|
|
135
|
+
} from "../core/relationship/resolve-relationship-reference.js";
|
|
136
|
+
import type { MetaRoot } from "../shared/meta-root.js";
|
|
132
137
|
import { stripPackage } from "../naming.js";
|
|
133
138
|
import {
|
|
134
139
|
FILTER_COMPOSE_OR,
|
|
@@ -1938,11 +1943,29 @@ export function validateDataGridFilterValues(root: MetaData): ParseError[] {
|
|
|
1938
1943
|
// (c) When @through is present: the named entity must exist and declare exactly
|
|
1939
1944
|
// two identity.reference children; @sourceRefField (if present) must match
|
|
1940
1945
|
// one of those references' FK fields → ERR_INVALID_RELATIONSHIP.
|
|
1941
|
-
// (d) @through / @
|
|
1942
|
-
//
|
|
1946
|
+
// (d) @through / @symmetric are invalid on a non-M:N relationship
|
|
1947
|
+
// (@cardinality != "many", or no @through) → ERR_INVALID_RELATIONSHIP.
|
|
1948
|
+
// @sourceRefField is also invalid there, EXCEPT on @cardinality: "one"
|
|
1949
|
+
// (#368: it then names which of several identity.reference nodes onto
|
|
1950
|
+
// the same target the relationship navigates).
|
|
1943
1951
|
//
|
|
1944
|
-
//
|
|
1945
|
-
//
|
|
1952
|
+
// ADR-0039: resolving, not own-only (#368 fix round 3) — every rule above
|
|
1953
|
+
// validates a property of the relationship's OWN declaration (its attrs,
|
|
1954
|
+
// plus for rule (c) the @through target resolved against the root), so a
|
|
1955
|
+
// relationship inherited via extends must be validated wherever it's
|
|
1956
|
+
// visible, or a child entity that only SEES the relationship through
|
|
1957
|
+
// inheritance could carry a violation no pass ever examines. That makes an
|
|
1958
|
+
// inherited, UNMODIFIED relationship visited once per inheriting entity —
|
|
1959
|
+
// checked once below (keyed on the relationship node's own identity), not
|
|
1960
|
+
// once per entity, using the DECLARING entity (rel.parent) rather than
|
|
1961
|
+
// whichever entity's effective view got there first for every piece of
|
|
1962
|
+
// context a rule reads (message text, @through's package per ADR-0042, rule
|
|
1963
|
+
// (a)'s self-join comparison). That keeps each check's result independent of
|
|
1964
|
+
// which entity triggered it, which is what makes "checked once" both
|
|
1965
|
+
// sufficient and correct. An override replaces the node in place
|
|
1966
|
+
// (MetaData._effectiveChildren), so it is never the same object as what it
|
|
1967
|
+
// overrides and is never skipped against it — a genuinely different
|
|
1968
|
+
// declaration is always independently checked.
|
|
1946
1969
|
// ---------------------------------------------------------------------------
|
|
1947
1970
|
|
|
1948
1971
|
// The junction's reference view: the validator and the runtime/codegen FK
|
|
@@ -1970,14 +1993,36 @@ function _countJunctionReferences(junction: MetaData): number {
|
|
|
1970
1993
|
|
|
1971
1994
|
export function validateRelationships(root: MetaData): ParseError[] {
|
|
1972
1995
|
const errors: ParseError[] = [];
|
|
1996
|
+
// #368 fix round 3 — the outer loop below is now resolving (obj.relationships()),
|
|
1997
|
+
// so a relationship inherited unmodified by N entities is reached N times. Its
|
|
1998
|
+
// own attrs never change based on who inherits it, so re-validating it more
|
|
1999
|
+
// than once would report the identical finding N times — pure noise. Checked
|
|
2000
|
+
// is keyed on the relationship NODE's own object identity: MetaData._effectiveChildren
|
|
2001
|
+
// reuses the super's child object in place for an unmodified inherited child,
|
|
2002
|
+
// so the same physical declaration IS the same object everywhere it's visible,
|
|
2003
|
+
// while an override replaces it with a genuinely different object (correctly
|
|
2004
|
+
// NOT skipped — a distinct declaration is a distinct finding).
|
|
2005
|
+
const checked = new Set<MetaData>();
|
|
1973
2006
|
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
1974
2007
|
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
1975
|
-
// ADR-
|
|
1976
|
-
|
|
1977
|
-
//
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2008
|
+
// ADR-0039: resolving — see the header comment above: rule (d) (like rule
|
|
2009
|
+
// (e)) must see a relationship inherited via extends, not just this
|
|
2010
|
+
// entity's own declarations. `checked` absorbs the resulting revisits.
|
|
2011
|
+
for (const rel of (obj as MetaObject).relationships()) {
|
|
2012
|
+
if (checked.has(rel)) continue;
|
|
2013
|
+
checked.add(rel);
|
|
2014
|
+
|
|
2015
|
+
// Every rule below validates a property of the relationship's OWN
|
|
2016
|
+
// declaration (its attrs, plus for rule (c) the @through target), so
|
|
2017
|
+
// context — the entity name in messages, the package a bare @through
|
|
2018
|
+
// resolves in (ADR-0042), and rule (a)'s self-join comparison — is
|
|
2019
|
+
// always the entity that DECLARES `rel` (rel.parent), never `obj` (the
|
|
2020
|
+
// entity whose effective view happened to reach it first). This keeps
|
|
2021
|
+
// the check's result independent of iteration order/inheritance depth,
|
|
2022
|
+
// which is what makes checking each node exactly once correct.
|
|
2023
|
+
const declaringEntity = rel.parent ?? obj;
|
|
2024
|
+
const referrerPkg = declaringEntity.package ?? declaringEntity.fileDefaultPackage ?? "";
|
|
2025
|
+
|
|
1981
2026
|
// ADR-0039: resolving — a relationship may inherit its M:N attrs via extends.
|
|
1982
2027
|
const through = rel.attr(RELATIONSHIP_ATTR_THROUGH);
|
|
1983
2028
|
const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
@@ -1999,16 +2044,23 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
1999
2044
|
if (hasThrough) {
|
|
2000
2045
|
errors.push(
|
|
2001
2046
|
new ParseError(
|
|
2002
|
-
`relationship "${
|
|
2047
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_THROUGH} but is not a M:N ` +
|
|
2003
2048
|
`relationship (requires @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_MANY}").`,
|
|
2004
2049
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2005
2050
|
),
|
|
2006
2051
|
);
|
|
2007
2052
|
}
|
|
2008
|
-
|
|
2053
|
+
// #368: @sourceRefField also disambiguates a `@cardinality: one`
|
|
2054
|
+
// relationship when the entity holds more than one identity.reference
|
|
2055
|
+
// onto the same target. Only the M:N *junction* reading is rejected
|
|
2056
|
+
// here; rule (e) — validateOneSideReferenceResolution, below in this
|
|
2057
|
+
// file — checks that it names a real local reference.
|
|
2058
|
+
if (hasSourceRefField && cardinality !== CARDINALITY_ONE) {
|
|
2009
2059
|
errors.push(
|
|
2010
2060
|
new ParseError(
|
|
2011
|
-
`relationship "${
|
|
2061
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is neither a M:N ` +
|
|
2062
|
+
`relationship (requires @${RELATIONSHIP_ATTR_THROUGH} with @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_MANY}") ` +
|
|
2063
|
+
`nor a @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_ONE}" relationship.`,
|
|
2012
2064
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2013
2065
|
),
|
|
2014
2066
|
);
|
|
@@ -2016,7 +2068,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2016
2068
|
if (symmetric) {
|
|
2017
2069
|
errors.push(
|
|
2018
2070
|
new ParseError(
|
|
2019
|
-
`relationship "${
|
|
2071
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but is not a M:N relationship.`,
|
|
2020
2072
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2021
2073
|
),
|
|
2022
2074
|
);
|
|
@@ -2028,7 +2080,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2028
2080
|
if (symmetric && hasSourceRefField) {
|
|
2029
2081
|
errors.push(
|
|
2030
2082
|
new ParseError(
|
|
2031
|
-
`relationship "${
|
|
2083
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets both @${RELATIONSHIP_ATTR_SYMMETRIC} and ` +
|
|
2032
2084
|
`@${RELATIONSHIP_ATTR_SOURCE_REF_FIELD}; they are mutually exclusive.`,
|
|
2033
2085
|
{ code: "ERR_BAD_ATTR_VALUE", source: rel.source },
|
|
2034
2086
|
),
|
|
@@ -2040,12 +2092,13 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2040
2092
|
// in this package is self, but an FQN "other::Widget" (a different same-short-
|
|
2041
2093
|
// name entity) is NOT (comparing stripped short names would misclassify it).
|
|
2042
2094
|
const isSelfJoin =
|
|
2043
|
-
typeof objectRef === "string" &&
|
|
2095
|
+
typeof objectRef === "string" &&
|
|
2096
|
+
resolveObjectRef(root, objectRef, referrerPkg).node === declaringEntity;
|
|
2044
2097
|
if (symmetric && !isSelfJoin) {
|
|
2045
2098
|
errors.push(
|
|
2046
2099
|
new ParseError(
|
|
2047
|
-
`relationship "${
|
|
2048
|
-
`"${String(objectRef)}" is not the declaring entity "${
|
|
2100
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but @${RELATIONSHIP_ATTR_OBJECT_REF} ` +
|
|
2101
|
+
`"${String(objectRef)}" is not the declaring entity "${declaringEntity.name}"; @${RELATIONSHIP_ATTR_SYMMETRIC} is self-join-only.`,
|
|
2049
2102
|
{ code: "ERR_BAD_ATTR_VALUE", source: rel.source },
|
|
2050
2103
|
),
|
|
2051
2104
|
);
|
|
@@ -2056,8 +2109,8 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2056
2109
|
if (!junction) {
|
|
2057
2110
|
errors.push(
|
|
2058
2111
|
new ParseError(
|
|
2059
|
-
`relationship "${
|
|
2060
|
-
{ code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${
|
|
2112
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" does not resolve to an entity.${didYouMeanHint(root, String(through))}`,
|
|
2113
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${declaringEntity.fqn()}::${rel.name}`, String(through)) },
|
|
2061
2114
|
),
|
|
2062
2115
|
);
|
|
2063
2116
|
continue;
|
|
@@ -2069,7 +2122,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2069
2122
|
if (junction.subType !== OBJECT_SUBTYPE_ENTITY) {
|
|
2070
2123
|
errors.push(
|
|
2071
2124
|
new ParseError(
|
|
2072
|
-
`relationship "${
|
|
2125
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" resolves to ` +
|
|
2073
2126
|
`${junction.type}.${junction.subType}, not an entity — a junction is a persisted join table ` +
|
|
2074
2127
|
`and must be object.entity.`,
|
|
2075
2128
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
@@ -2081,7 +2134,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2081
2134
|
if (refCount !== 2) {
|
|
2082
2135
|
errors.push(
|
|
2083
2136
|
new ParseError(
|
|
2084
|
-
`relationship "${
|
|
2137
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" must declare exactly two ` +
|
|
2085
2138
|
`identity.reference children (one per FK side); found ${refCount}.`,
|
|
2086
2139
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2087
2140
|
),
|
|
@@ -2094,7 +2147,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2094
2147
|
if (!fkFields.includes(sourceRefField as string)) {
|
|
2095
2148
|
errors.push(
|
|
2096
2149
|
new ParseError(
|
|
2097
|
-
`relationship "${
|
|
2150
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} "${sourceRefField}" does not match ` +
|
|
2098
2151
|
`any identity.reference FK field on junction "${through}". Available: ${fkFields.join(", ") || "(none)"}.`,
|
|
2099
2152
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2100
2153
|
),
|
|
@@ -2106,6 +2159,111 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2106
2159
|
return errors;
|
|
2107
2160
|
}
|
|
2108
2161
|
|
|
2162
|
+
// ---------------------------------------------------------------------------
|
|
2163
|
+
// Rule (e) — #368: a `@cardinality: one` relationship must resolve to exactly
|
|
2164
|
+
// one identity.reference. Two references onto the same target are
|
|
2165
|
+
// indistinguishable from the relationship's @objectRef alone, so the resolver
|
|
2166
|
+
// would silently emit the first one's FK column. ADR-0029 §5: a second path
|
|
2167
|
+
// is a load error naming the candidates.
|
|
2168
|
+
//
|
|
2169
|
+
// Registered alongside validateRelationships (the M:N slim-vocabulary pass,
|
|
2170
|
+
// above) — same deferred-resolution timing (after all files load + extends
|
|
2171
|
+
// resolution).
|
|
2172
|
+
//
|
|
2173
|
+
// Scope differs deliberately from rule (d) — in SUBJECT, not in which
|
|
2174
|
+
// relationships each pass walks (both walk the EFFECTIVE set; rule (d) is
|
|
2175
|
+
// no longer own-scoped, or an M:N declaration reached only via extends
|
|
2176
|
+
// would go unchecked). Rule (d) validates attrs that travel with the
|
|
2177
|
+
// relationship's OWN declaration (@through/@symmetric/@sourceRefField), so
|
|
2178
|
+
// it checks each declaration EXACTLY ONCE — deduped by node identity, and
|
|
2179
|
+
// reported against the entity that declares it — because those attrs don't
|
|
2180
|
+
// change meaning depending on who inherits the relationship. Rule (e)
|
|
2181
|
+
// instead validates whether THIS entity's reference set resolves the
|
|
2182
|
+
// relationship uniquely, which is a property of the EFFECTIVE entity, not of
|
|
2183
|
+
// wherever the relationship happens to be declared. A child entity that
|
|
2184
|
+
// extends a clean parent and adds a second identity.reference onto the same
|
|
2185
|
+
// target makes an INHERITED relationship ambiguous on the child even though
|
|
2186
|
+
// the parent (and the relationship's own declaration) are untouched — own-
|
|
2187
|
+
// scoping this pass would leave that case unchecked, and codegen/runtime
|
|
2188
|
+
// (which resolve against the effective entity) would silently drop the
|
|
2189
|
+
// relation (#368 fix round 2). If a parent and a child are both genuinely
|
|
2190
|
+
// ambiguous, both are reported — two entities are broken, not one error
|
|
2191
|
+
// duplicated.
|
|
2192
|
+
// ---------------------------------------------------------------------------
|
|
2193
|
+
|
|
2194
|
+
export function validateOneSideReferenceResolution(root: MetaRoot): ParseError[] {
|
|
2195
|
+
const errors: ParseError[] = [];
|
|
2196
|
+
for (const obj of root.objects()) {
|
|
2197
|
+
// ADR-0039: resolving — see the scope note above: rule (e) checks THIS
|
|
2198
|
+
// entity's effective reference set against every relationship it can see,
|
|
2199
|
+
// including one only inherited via extends.
|
|
2200
|
+
for (const rel of obj.relationships()) {
|
|
2201
|
+
// ADR-0039: resolving — @cardinality/@objectRef may be inherited via extends.
|
|
2202
|
+
if (rel.attr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_ONE) continue;
|
|
2203
|
+
const objectRef = rel.attr(RELATIONSHIP_ATTR_OBJECT_REF);
|
|
2204
|
+
if (typeof objectRef !== "string" || objectRef === "") continue;
|
|
2205
|
+
|
|
2206
|
+
const candidates = referenceCandidatesFor(obj, objectRef);
|
|
2207
|
+
|
|
2208
|
+
const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
2209
|
+
const declared = typeof sourceRefField === "string" && sourceRefField !== ""
|
|
2210
|
+
? sourceRefField
|
|
2211
|
+
: undefined;
|
|
2212
|
+
|
|
2213
|
+
if (declared !== undefined) {
|
|
2214
|
+
// A declared @sourceRefField short-circuits the ladder at ANY
|
|
2215
|
+
// candidate count — checked independently of resolveRelationshipReference,
|
|
2216
|
+
// whose step 1 ("exactly one candidate -> that one") would otherwise
|
|
2217
|
+
// silently return the lone candidate even when it disagrees with the
|
|
2218
|
+
// declared field. The author named a specific FK; it must exist,
|
|
2219
|
+
// whether there are zero, one, or many candidates.
|
|
2220
|
+
const matchesDeclared = candidates.some((c) => c.fields[0] === declared);
|
|
2221
|
+
if (matchesDeclared) continue;
|
|
2222
|
+
errors.push(
|
|
2223
|
+
new ParseError(
|
|
2224
|
+
`relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} ` +
|
|
2225
|
+
`"${declared}", which names no identity.reference targeting "${objectRef}". ` +
|
|
2226
|
+
`Candidates: ${formatReferenceCandidates(candidates)}.`,
|
|
2227
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2228
|
+
),
|
|
2229
|
+
);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// No @sourceRefField declared: ambiguity only exists with 2+ candidates —
|
|
2234
|
+
// resolveRelationshipReference's name-pairing step (ladder step 3) decides.
|
|
2235
|
+
if (candidates.length <= 1) continue;
|
|
2236
|
+
const resolved = resolveRelationshipReference(obj, rel.name, objectRef);
|
|
2237
|
+
if (resolved) continue;
|
|
2238
|
+
|
|
2239
|
+
errors.push(
|
|
2240
|
+
new ParseError(
|
|
2241
|
+
`relationship "${obj.name}.${rel.name}" is ambiguous: "${obj.name}" declares ` +
|
|
2242
|
+
`${candidates.length} identity.reference nodes targeting "${objectRef}" and the ` +
|
|
2243
|
+
`relationship name does not pair with exactly one. Candidates: ${formatReferenceCandidates(candidates)}. ` +
|
|
2244
|
+
`Set @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} to the FK field this relationship navigates.`,
|
|
2245
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2246
|
+
),
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return errors;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* Render a candidate reference as `name(fkField)`, or `name(fieldA, fieldB)`
|
|
2255
|
+
* for a composite reference — so two composite references sharing a first
|
|
2256
|
+
* column (e.g. both starting `tenantId`) still print distinguishably.
|
|
2257
|
+
*
|
|
2258
|
+
* NOTE: this is display only. Matching (both here and in
|
|
2259
|
+
* resolveRelationshipReference) still keys on `fields[0]` alone — a
|
|
2260
|
+
* composite reference cannot actually be disambiguated by @sourceRefField.
|
|
2261
|
+
* That's a documented limitation, not fixed by this rendering change.
|
|
2262
|
+
*/
|
|
2263
|
+
function formatReferenceCandidates(candidates: readonly MetaReferenceIdentity[]): string {
|
|
2264
|
+
return candidates.map((c) => `${c.name}(${c.fields.join(", ")})`).join(", ");
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2109
2267
|
// NOTE: identity.reference @references resolution moved to the validation registry
|
|
2110
2268
|
// (defaultValidationRegistry → a declarative reference descriptor with dottedFieldPath).
|
|
2111
2269
|
|