@metaobjectsdev/metadata 0.6.0 → 0.7.0-rc.10
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/README.md +54 -3
- package/dist/attr-schema-validate.js +7 -7
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/export-json.d.ts +6 -7
- package/dist/core/export-json.d.ts.map +1 -1
- package/dist/core/export-json.js +15 -17
- package/dist/core/export-json.js.map +1 -1
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/parser-yaml.d.ts.map +1 -1
- package/dist/core/parser-yaml.js +36 -11
- package/dist/core/parser-yaml.js.map +1 -1
- package/dist/core/yaml-desugar.d.ts.map +1 -1
- package/dist/core/yaml-desugar.js +54 -5
- package/dist/core/yaml-desugar.js.map +1 -1
- package/dist/core/yaml-positions-walker.d.ts +21 -0
- package/dist/core/yaml-positions-walker.d.ts.map +1 -0
- package/dist/core/yaml-positions-walker.js +75 -0
- package/dist/core/yaml-positions-walker.js.map +1 -0
- package/dist/core/yaml-positions.d.ts +19 -0
- package/dist/core/yaml-positions.d.ts.map +1 -0
- package/dist/core/yaml-positions.js +60 -0
- package/dist/core/yaml-positions.js.map +1 -0
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +7 -4
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +32 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -5
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/json-path.d.ts +8 -0
- package/dist/json-path.d.ts.map +1 -0
- package/dist/json-path.js +39 -0
- package/dist/json-path.js.map +1 -0
- package/dist/loader/meta-data-loader.d.ts +47 -6
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +126 -8
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/meta-data-source.d.ts +6 -2
- package/dist/loader/meta-data-source.d.ts.map +1 -1
- package/dist/loader/meta-data-source.js +10 -6
- package/dist/loader/meta-data-source.js.map +1 -1
- package/dist/loader/shortcuts.d.ts +9 -0
- package/dist/loader/shortcuts.d.ts.map +1 -0
- package/dist/loader/shortcuts.js +19 -0
- package/dist/loader/shortcuts.js.map +1 -0
- package/dist/loader/sources/directory-source.d.ts +15 -0
- package/dist/loader/sources/directory-source.d.ts.map +1 -0
- package/dist/loader/sources/directory-source.js +80 -0
- package/dist/loader/sources/directory-source.js.map +1 -0
- package/dist/loader/sources/file-source.d.ts +12 -0
- package/dist/loader/sources/file-source.d.ts.map +1 -0
- package/dist/loader/sources/file-source.js +46 -0
- package/dist/loader/sources/file-source.js.map +1 -0
- package/dist/loader/sources/index.d.ts +5 -0
- package/dist/loader/sources/index.d.ts.map +1 -0
- package/dist/loader/sources/index.js +5 -0
- package/dist/loader/sources/index.js.map +1 -0
- package/dist/loader/sources/uri-source.d.ts +9 -0
- package/dist/loader/sources/uri-source.d.ts.map +1 -0
- package/dist/loader/sources/uri-source.js +42 -0
- package/dist/loader/sources/uri-source.js.map +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +92 -28
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming.d.ts +15 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +20 -6
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts +17 -4
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +371 -44
- package/dist/parser-core.js.map +1 -1
- package/dist/parser-json.d.ts.map +1 -1
- package/dist/parser-json.js +10 -2
- package/dist/parser-json.js.map +1 -1
- package/dist/persistence/source/validate-source-roles.js +2 -2
- package/dist/persistence/source/validate-source-roles.js.map +1 -1
- package/dist/semantic-diff.d.ts +5 -0
- package/dist/semantic-diff.d.ts.map +1 -0
- package/dist/semantic-diff.js +49 -0
- package/dist/semantic-diff.js.map +1 -0
- package/dist/shared/meta-data.d.ts +10 -0
- package/dist/shared/meta-data.d.ts.map +1 -1
- package/dist/shared/meta-data.js +23 -0
- package/dist/shared/meta-data.js.map +1 -1
- package/dist/source.d.ts +96 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +38 -0
- package/dist/source.js.map +1 -0
- package/dist/subtype-rules.js +1 -1
- package/dist/subtype-rules.js.map +1 -1
- package/dist/super-resolve.d.ts +2 -0
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +1 -1
- package/dist/super-resolve.js.map +1 -1
- package/dist/template/template-constants.d.ts +3 -1
- package/dist/template/template-constants.d.ts.map +1 -1
- package/dist/template/template-constants.js +22 -6
- package/dist/template/template-constants.js.map +1 -1
- package/dist/template/template-schema.d.ts.map +1 -1
- package/dist/template/template-schema.js +41 -1
- package/dist/template/template-schema.js.map +1 -1
- package/package.json +1 -1
- package/src/attr-schema-validate.ts +7 -7
- package/src/core/export-json.ts +15 -18
- package/src/core/index.ts +8 -2
- package/src/core/parser-yaml.ts +38 -11
- package/src/core/yaml-desugar.ts +58 -4
- package/src/core/yaml-positions-walker.ts +101 -0
- package/src/core/yaml-positions.ts +80 -0
- package/src/core-types.ts +7 -4
- package/src/errors.ts +57 -8
- package/src/index.ts +28 -3
- package/src/json-path.ts +46 -0
- package/src/loader/meta-data-loader.ts +168 -10
- package/src/loader/meta-data-source.ts +10 -6
- package/src/loader/shortcuts.ts +31 -0
- package/src/loader/sources/directory-source.ts +90 -0
- package/src/{core → loader/sources}/file-source.ts +3 -3
- package/src/loader/sources/index.ts +6 -0
- package/src/loader/sources/uri-source.ts +44 -0
- package/src/loader/validation-passes.ts +96 -29
- package/src/naming.ts +39 -7
- package/src/parser-core.ts +412 -46
- package/src/parser-json.ts +11 -2
- package/src/persistence/source/validate-source-roles.ts +2 -2
- package/src/semantic-diff.ts +48 -0
- package/src/shared/meta-data.ts +28 -0
- package/src/source.ts +99 -0
- package/src/subtype-rules.ts +1 -1
- package/src/super-resolve.ts +3 -1
- package/src/template/template-constants.ts +23 -6
- package/src/template/template-schema.ts +43 -0
- package/src/core/file-meta-data-loader.ts +0 -89
package/src/parser-core.ts
CHANGED
|
@@ -27,9 +27,13 @@ import { TypeId, TypeRegistry } from "./registry.js";
|
|
|
27
27
|
import type { MetaData } from "./shared/meta-data.js";
|
|
28
28
|
import { MetaRoot } from "./shared/meta-root.js";
|
|
29
29
|
import { MetaAttr } from "./core/attr/meta-attr.js";
|
|
30
|
-
import { inferAttrSubType } from "./serializer-json.js";
|
|
30
|
+
import { canonicalSerialize, inferAttrSubType } from "./serializer-json.js";
|
|
31
31
|
import { ParseError, type ErrorCode } from "./errors.js";
|
|
32
|
+
import { resolvedSource, type ErrorSource, type LoaderWarning, type Contributor } from "./source.js";
|
|
33
|
+
import { semanticDiff } from "./semantic-diff.js";
|
|
32
34
|
import { resolveSuperRef } from "./super-resolve.js";
|
|
35
|
+
import { JsonPathBuilder } from "./json-path.js";
|
|
36
|
+
import { getYamlPosition, type YamlPosition } from "./core/yaml-positions.js";
|
|
33
37
|
import {
|
|
34
38
|
TYPE_ATTR,
|
|
35
39
|
TYPE_FIELD,
|
|
@@ -80,27 +84,62 @@ export interface ParseOptions {
|
|
|
80
84
|
* another file parsed later.
|
|
81
85
|
*/
|
|
82
86
|
deferSuperResolution?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* FR5b — discriminant for the source-on-node envelope's `format` field.
|
|
89
|
+
* Defaults to `"json"` (parseJson supplies nothing). parseYaml passes
|
|
90
|
+
* `"yaml"` so populateNodeSource emits `format: "yaml"` and, when the
|
|
91
|
+
* desugar attached one, the optional `yamlPosition`.
|
|
92
|
+
*/
|
|
93
|
+
sourceFormat?: "json" | "yaml";
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
export interface ParseResult {
|
|
86
97
|
root: MetaRoot;
|
|
87
98
|
warnings: string[];
|
|
88
99
|
errors: ParseError[];
|
|
100
|
+
/**
|
|
101
|
+
* FR5c — envelope-shaped warnings (e.g. WARN_DUPLICATE_DECLARATION) produced
|
|
102
|
+
* during the parse/merge pipeline. Distinct from the legacy `warnings:
|
|
103
|
+
* string[]` channel: those messages get wrapped in a `WARN_LEGACY` envelope
|
|
104
|
+
* at the loader boundary, while envelope warnings already carry their own
|
|
105
|
+
* `code` + `source` and are surfaced unchanged. Defaults to `[]`.
|
|
106
|
+
*/
|
|
107
|
+
envelopeWarnings: LoaderWarning[];
|
|
89
108
|
}
|
|
90
109
|
|
|
91
110
|
// ---------------------------------------------------------------------------
|
|
92
|
-
// Internal helper — build
|
|
93
|
-
//
|
|
111
|
+
// Internal helper — build a parse-phase ErrorSource envelope.
|
|
112
|
+
//
|
|
113
|
+
// FR5a / ADR-0009: ParseError carries a `source: ErrorSource` envelope. For
|
|
114
|
+
// errors raised mid-parse, the envelope's jsonPath comes from the parser's
|
|
115
|
+
// module-level JsonPathBuilder (synced with the walk); files[0] is the parsed
|
|
116
|
+
// source id. Falls back to a code-source envelope if invoked outside a buildTree
|
|
117
|
+
// run (defensive — every callsite below runs inside buildTree).
|
|
94
118
|
// ---------------------------------------------------------------------------
|
|
95
119
|
|
|
96
|
-
export function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
export function errSource(): ErrorSource {
|
|
121
|
+
if (_currentPath !== undefined && _currentSourceId !== undefined) {
|
|
122
|
+
// FR5b (finalized 2026-05-27, all four ports) — buildTree-emitted errors
|
|
123
|
+
// from a YAML input now emit `format: "yaml"` (was `"json"` interim while
|
|
124
|
+
// each port shipped yamlPosition tracking). The optional `yamlPosition`
|
|
125
|
+
// rides along when the desugar's position map covers the current node.
|
|
126
|
+
if (_currentFormat === "yaml") {
|
|
127
|
+
return {
|
|
128
|
+
format: "yaml",
|
|
129
|
+
files: [_currentSourceId],
|
|
130
|
+
jsonPath: _currentPath.toString(),
|
|
131
|
+
...(_currentYamlPosition !== undefined
|
|
132
|
+
? { yamlPosition: _currentYamlPosition }
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
format: "json",
|
|
138
|
+
files: [_currentSourceId],
|
|
139
|
+
jsonPath: _currentPath.toString(),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { format: "code", caller: "parser-core" };
|
|
104
143
|
}
|
|
105
144
|
|
|
106
145
|
// ---------------------------------------------------------------------------
|
|
@@ -111,14 +150,10 @@ function reportProblem(
|
|
|
111
150
|
msg: string,
|
|
112
151
|
strict: boolean,
|
|
113
152
|
warnings: string[],
|
|
114
|
-
|
|
115
|
-
path: string,
|
|
116
|
-
code?: ErrorCode,
|
|
153
|
+
code: ErrorCode,
|
|
117
154
|
): void {
|
|
118
155
|
if (strict) {
|
|
119
|
-
|
|
120
|
-
if (code !== undefined) throw new ParseError(msg, { ...opts, code });
|
|
121
|
-
else throw new ParseError(msg, opts);
|
|
156
|
+
throw new ParseError(msg, { code, source: errSource() });
|
|
122
157
|
}
|
|
123
158
|
warnings.push(msg);
|
|
124
159
|
}
|
|
@@ -196,6 +231,56 @@ let _deferSuperResolution = false;
|
|
|
196
231
|
// _deferSuperResolution.
|
|
197
232
|
let _currentErrors: ParseError[] | undefined;
|
|
198
233
|
|
|
234
|
+
// FR5c — envelope-warnings sink for the merge phase. Set at buildTree entry
|
|
235
|
+
// so deeply-nested merge sites can emit WARN_DUPLICATE_DECLARATION without
|
|
236
|
+
// threading another parameter through every helper signature. Safe under
|
|
237
|
+
// the same synchronous-buildTree reentrancy argument as the others.
|
|
238
|
+
let _currentEnvelopeWarnings: LoaderWarning[] | undefined;
|
|
239
|
+
|
|
240
|
+
// FR5a / ADR-0009 — Module-level JSONPath builder + source id, set at
|
|
241
|
+
// buildTree entry and updated by recursive descent (push on the way down,
|
|
242
|
+
// pop on the way back up). Used by populateNodeSource() to stamp every
|
|
243
|
+
// constructed MetaData with `{ format: "json"|"yaml", files: [sourceId],
|
|
244
|
+
// jsonPath, [yamlPosition?] }`.
|
|
245
|
+
// Safe because buildTree is synchronous — same reentrancy argument as
|
|
246
|
+
// _currentErrors above.
|
|
247
|
+
let _currentPath: JsonPathBuilder | undefined;
|
|
248
|
+
let _currentSourceId: string | undefined;
|
|
249
|
+
// FR5b — source format discriminant + current node's YAML position, set
|
|
250
|
+
// by the per-child iteration in processChildren (and at root) right before
|
|
251
|
+
// the parseNodeFresh call. The position is read from the wrapper object's
|
|
252
|
+
// position-by-key map (attached by the YAML walker in core/yaml-positions.ts
|
|
253
|
+
// and preserved through core/yaml-desugar.ts).
|
|
254
|
+
let _currentFormat: "json" | "yaml" = "json";
|
|
255
|
+
let _currentYamlPosition: YamlPosition | undefined;
|
|
256
|
+
|
|
257
|
+
/** FR5a/FR5b — stamp the source-provenance envelope on a freshly-created
|
|
258
|
+
* node. No-op when invoked outside buildTree's setup (defensive — the
|
|
259
|
+
* module-level state will always be populated during a normal parse).
|
|
260
|
+
*
|
|
261
|
+
* FR5b (finalized 2026-05-27) — YAML-input nodes get `format: "yaml"`
|
|
262
|
+
* envelopes (was `"json"` interim). The optional `yamlPosition` rides
|
|
263
|
+
* along when the desugar's position map covers the current node. */
|
|
264
|
+
function populateNodeSource(node: MetaData): void {
|
|
265
|
+
if (_currentPath === undefined || _currentSourceId === undefined) return;
|
|
266
|
+
if (_currentFormat === "yaml") {
|
|
267
|
+
node.setSource({
|
|
268
|
+
format: "yaml",
|
|
269
|
+
files: [_currentSourceId],
|
|
270
|
+
jsonPath: _currentPath.toString(),
|
|
271
|
+
...(_currentYamlPosition !== undefined
|
|
272
|
+
? { yamlPosition: _currentYamlPosition }
|
|
273
|
+
: {}),
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
node.setSource({
|
|
278
|
+
format: "json",
|
|
279
|
+
files: [_currentSourceId],
|
|
280
|
+
jsonPath: _currentPath.toString(),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
199
284
|
/**
|
|
200
285
|
* buildTree — the shared registry-driven tree-builder.
|
|
201
286
|
*
|
|
@@ -210,14 +295,29 @@ let _currentErrors: ParseError[] | undefined;
|
|
|
210
295
|
export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
211
296
|
const warnings: string[] = [];
|
|
212
297
|
const errors: ParseError[] = [];
|
|
298
|
+
const envelopeWarnings: LoaderWarning[] = [];
|
|
213
299
|
const strict = opts.strict ?? false;
|
|
214
300
|
const source = opts.sourceName;
|
|
215
301
|
_deferSuperResolution = opts.deferSuperResolution === true;
|
|
216
302
|
_currentErrors = errors;
|
|
303
|
+
// FR5c — module-level handle so the deeply-nested merge code paths can
|
|
304
|
+
// emit envelope warnings without threading another parameter through the
|
|
305
|
+
// entire walk. Safe because buildTree is fully synchronous.
|
|
306
|
+
_currentEnvelopeWarnings = envelopeWarnings;
|
|
307
|
+
// FR5a — start a fresh JSONPath stack rooted at "$"; sourceId is the
|
|
308
|
+
// source's id (from FileSource / InMemoryStringSource via opts.sourceName).
|
|
309
|
+
// Falls back to "<unknown>" when no name was supplied (e.g. ad-hoc parseJson
|
|
310
|
+
// calls from tests).
|
|
311
|
+
_currentPath = new JsonPathBuilder();
|
|
312
|
+
_currentSourceId = source ?? "<unknown>";
|
|
313
|
+
// FR5b — propagate the per-parse source-format discriminant. parseJson
|
|
314
|
+
// omits the option (defaults to "json"); parseYaml supplies "yaml".
|
|
315
|
+
_currentFormat = opts.sourceFormat ?? "json";
|
|
316
|
+
_currentYamlPosition = undefined;
|
|
217
317
|
|
|
218
318
|
try {
|
|
219
319
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
220
|
-
throw new ParseError("Top-level metadata must be an object", {
|
|
320
|
+
throw new ParseError("Top-level metadata must be an object", { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() });
|
|
221
321
|
}
|
|
222
322
|
|
|
223
323
|
const topLevel = parsed as Record<string, unknown>;
|
|
@@ -226,12 +326,12 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
226
326
|
const wrapperKeys = Object.keys(topLevel).filter((k) => k !== JSON_KEY_SCHEMA);
|
|
227
327
|
|
|
228
328
|
if (wrapperKeys.length === 0) {
|
|
229
|
-
throw new ParseError("Top-level metadata object has no type wrapper key", {
|
|
329
|
+
throw new ParseError("Top-level metadata object has no type wrapper key", { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() });
|
|
230
330
|
}
|
|
231
331
|
if (wrapperKeys.length > 1) {
|
|
232
332
|
throw new ParseError(
|
|
233
333
|
`Top-level metadata object must have exactly one wrapper key (found: ${wrapperKeys.join(", ")})`,
|
|
234
|
-
{
|
|
334
|
+
{ code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() },
|
|
235
335
|
);
|
|
236
336
|
}
|
|
237
337
|
|
|
@@ -239,9 +339,14 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
239
339
|
const rootData = topLevel[rootKey];
|
|
240
340
|
|
|
241
341
|
if (typeof rootData !== "object" || rootData === null || Array.isArray(rootData)) {
|
|
342
|
+
// The error context references the rootKey wrapper; push it so the
|
|
343
|
+
// envelope's jsonPath includes it (matches the legacy `path` slot).
|
|
344
|
+
_currentPath!.pushKey(rootKey);
|
|
345
|
+
const src = errSource();
|
|
346
|
+
_currentPath!.pop();
|
|
242
347
|
throw new ParseError(
|
|
243
348
|
`Top-level wrapper "${rootKey}" must contain an object`,
|
|
244
|
-
{
|
|
349
|
+
{ code: "ERR_TOP_LEVEL_NOT_OBJECT", source: src },
|
|
245
350
|
);
|
|
246
351
|
}
|
|
247
352
|
|
|
@@ -253,12 +358,27 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
253
358
|
const rootTypeCode = opts.registry.allSubTypesOf(rootType).length > 0
|
|
254
359
|
? "ERR_UNKNOWN_SUBTYPE" as const
|
|
255
360
|
: "ERR_UNKNOWN_TYPE" as const;
|
|
361
|
+
_currentPath!.pushKey(rootKey);
|
|
362
|
+
const src = errSource();
|
|
363
|
+
_currentPath!.pop();
|
|
256
364
|
throw new ParseError(
|
|
257
365
|
`Unknown root type "${rootType}.${rootSubType}" — not registered`,
|
|
258
|
-
{
|
|
366
|
+
{ code: rootTypeCode, source: src },
|
|
259
367
|
);
|
|
260
368
|
}
|
|
261
369
|
|
|
370
|
+
// FR5a — push the wrapper-key segment onto the JSONPath stack so all
|
|
371
|
+
// descendants emit jsonPath strings rooted at "$.<rootKey>". The merge-mode
|
|
372
|
+
// path keeps the existing root's source untouched; only NEW children get
|
|
373
|
+
// populated with the current source's id (correct: the existing root was
|
|
374
|
+
// already stamped from the file that created it).
|
|
375
|
+
_currentPath!.pushKey(rootKey);
|
|
376
|
+
// FR5b — look up the root wrapper's YAML position (when in yaml mode)
|
|
377
|
+
// so parseNodeFresh stamps `source.yamlPosition` on the root node.
|
|
378
|
+
if (_currentFormat === "yaml") {
|
|
379
|
+
_currentYamlPosition = getYamlPosition(topLevel, rootKey);
|
|
380
|
+
}
|
|
381
|
+
|
|
262
382
|
if (opts.intoRoot !== undefined) {
|
|
263
383
|
// --- Merge mode: parse root's attrs/children into the existing root ---
|
|
264
384
|
// The JSON root's own package/super/reserved-keys are not re-applied to the
|
|
@@ -278,7 +398,8 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
278
398
|
source,
|
|
279
399
|
rootKey,
|
|
280
400
|
);
|
|
281
|
-
|
|
401
|
+
_currentPath!.pop();
|
|
402
|
+
return { root: opts.intoRoot, warnings, errors, envelopeWarnings };
|
|
282
403
|
}
|
|
283
404
|
|
|
284
405
|
// --- Fresh root mode: create a new root from the JSON ---
|
|
@@ -302,10 +423,16 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
302
423
|
source,
|
|
303
424
|
rootKey,
|
|
304
425
|
) as MetaRoot;
|
|
305
|
-
|
|
426
|
+
_currentPath!.pop();
|
|
427
|
+
return { root, warnings, errors, envelopeWarnings };
|
|
306
428
|
} finally {
|
|
307
429
|
_deferSuperResolution = false;
|
|
308
430
|
_currentErrors = undefined;
|
|
431
|
+
_currentEnvelopeWarnings = undefined;
|
|
432
|
+
_currentPath = undefined;
|
|
433
|
+
_currentSourceId = undefined;
|
|
434
|
+
_currentFormat = "json";
|
|
435
|
+
_currentYamlPosition = undefined;
|
|
309
436
|
}
|
|
310
437
|
}
|
|
311
438
|
|
|
@@ -341,10 +468,12 @@ function parseNodeFresh(
|
|
|
341
468
|
subType = SUBTYPE_BASE;
|
|
342
469
|
} else {
|
|
343
470
|
const msg = `Unknown type "${type}.${subType}" — not registered`;
|
|
344
|
-
errors.push(new ParseError(msg, {
|
|
471
|
+
errors.push(new ParseError(msg, { code: "ERR_UNKNOWN_TYPE", source: errSource() }));
|
|
345
472
|
const rawName = nodeData[RESERVED_KEY_NAME];
|
|
346
473
|
const name = typeof rawName === "string" ? rawName : "";
|
|
347
|
-
|
|
474
|
+
const stub = new MetaRoot(new TypeId(type, subType), name);
|
|
475
|
+
populateNodeSource(stub);
|
|
476
|
+
return stub;
|
|
348
477
|
}
|
|
349
478
|
}
|
|
350
479
|
|
|
@@ -355,6 +484,10 @@ function parseNodeFresh(
|
|
|
355
484
|
// --- Create the model ---
|
|
356
485
|
const def = registry.find(type, subType)!;
|
|
357
486
|
const model = def.factory(def.typeId, name);
|
|
487
|
+
// FR5a — stamp the source provenance envelope using the parser's current
|
|
488
|
+
// JSONPath stack + source id. setSource happens BEFORE freeze (the parser
|
|
489
|
+
// is the only caller during loading; freeze runs in the loader after).
|
|
490
|
+
populateNodeSource(model);
|
|
358
491
|
|
|
359
492
|
// --- Apply reserved keys (package, extends, abstract, isArray) ---
|
|
360
493
|
applyReservedKeys(model, nodeData, strict, source, path, warnings, inheritedContextPkg);
|
|
@@ -390,9 +523,15 @@ function parseNodeFresh(
|
|
|
390
523
|
if (superModel !== undefined) {
|
|
391
524
|
model.setSuperResolved(superModel);
|
|
392
525
|
} else {
|
|
526
|
+
// FR5d — emit format=resolved with referrer + target. referrer is the
|
|
527
|
+
// declaring node's FQN (we just built it above); target is the
|
|
528
|
+
// unresolved supertype ref string.
|
|
393
529
|
throw new ParseError(
|
|
394
530
|
`the SuperClass '${model.superRef}' does not exist in file '${source ?? "<unknown>"}'`,
|
|
395
|
-
{
|
|
531
|
+
{
|
|
532
|
+
code: "ERR_UNRESOLVED_SUPER",
|
|
533
|
+
source: resolvedSource(errSource(), model.fqn(), model.superRef),
|
|
534
|
+
},
|
|
396
535
|
);
|
|
397
536
|
}
|
|
398
537
|
} else if (model.superRef !== undefined && accumRoot === undefined) {
|
|
@@ -401,8 +540,6 @@ function parseNodeFresh(
|
|
|
401
540
|
`super on root node ('${model.superRef}') is not supported and will be ignored`,
|
|
402
541
|
strict,
|
|
403
542
|
warnings,
|
|
404
|
-
source,
|
|
405
|
-
path,
|
|
406
543
|
"ERR_UNRESOLVED_SUPER",
|
|
407
544
|
);
|
|
408
545
|
}
|
|
@@ -439,6 +576,46 @@ function parseNodeInto(
|
|
|
439
576
|
source: string | undefined,
|
|
440
577
|
path: string,
|
|
441
578
|
): void {
|
|
579
|
+
// FR5c — capture pre-merge state so we can decide whether this contribution
|
|
580
|
+
// produced any semantic change. The "new contributor" is the file currently
|
|
581
|
+
// being parsed (_currentSourceId). The "base contributor" is whichever
|
|
582
|
+
// file(s) the existing target already records on its source envelope.
|
|
583
|
+
const newContributorFile = _currentSourceId ?? "<unknown>";
|
|
584
|
+
const targetIsRoot = target instanceof MetaRoot;
|
|
585
|
+
// The root is a synthetic accumulator: it is not a metadata-author node —
|
|
586
|
+
// every file declares { "metadata.root": ... } and the loader merges into
|
|
587
|
+
// a single root. We don't run FR5c diagnostics on the root itself; the
|
|
588
|
+
// merge attribution applies to author-meaningful nodes (object/field/etc).
|
|
589
|
+
const fr5cActive = !targetIsRoot && target.name !== "";
|
|
590
|
+
|
|
591
|
+
let preMergeShape: string | undefined;
|
|
592
|
+
let preMergeAttrSnapshot: Map<string, AttrValue> | undefined;
|
|
593
|
+
if (fr5cActive) {
|
|
594
|
+
preMergeShape = canonicalSerialize(target);
|
|
595
|
+
// Snapshot of own attrs (name → value) — used to detect ERR_MERGE_CONFLICT
|
|
596
|
+
// when a new contribution sets an attr the target already declared to a
|
|
597
|
+
// different non-empty value.
|
|
598
|
+
preMergeAttrSnapshot = new Map(target.ownAttrs());
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// FR5c — detect attribute-level merge conflicts: for each @-prefixed inline
|
|
602
|
+
// attr in nodeData, if the target already has an own attr of that name
|
|
603
|
+
// with a non-empty value, AND the new value differs from the existing
|
|
604
|
+
// value (both sides non-empty), emit ERR_MERGE_CONFLICT with a `format:
|
|
605
|
+
// "merged"` envelope. Last-writer-wins is preserved for non-conflicting
|
|
606
|
+
// cases (one side unset, same value, etc.) — those carry through to the
|
|
607
|
+
// existing applyInlineAttrsAndUnknownKeys logic below.
|
|
608
|
+
if (fr5cActive && preMergeAttrSnapshot !== undefined) {
|
|
609
|
+
detectAttrMergeConflicts(
|
|
610
|
+
target,
|
|
611
|
+
nodeData,
|
|
612
|
+
preMergeAttrSnapshot,
|
|
613
|
+
newContributorFile,
|
|
614
|
+
errors,
|
|
615
|
+
path,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
442
619
|
// Apply inline attrs (not reserved keys — those stay on the existing model)
|
|
443
620
|
applyInlineAttrsAndUnknownKeys(target, nodeData, strict, source, path, warnings, registry);
|
|
444
621
|
|
|
@@ -448,6 +625,168 @@ function parseNodeInto(
|
|
|
448
625
|
const effectivePkg = inheritedContextPkg;
|
|
449
626
|
|
|
450
627
|
processChildren(target, nodeData, accumRoot, effectivePkg, registry, warnings, errors, strict, source, path);
|
|
628
|
+
|
|
629
|
+
// FR5c — after merge: compare pre/post shape. If no semantic change → emit
|
|
630
|
+
// WARN_DUPLICATE_DECLARATION (the new contribution declared the same thing
|
|
631
|
+
// the target already had). If there was a change → upgrade the target's
|
|
632
|
+
// source envelope to `format: "merged"` with both contributors listed
|
|
633
|
+
// (ADR-0009 §Source-on-node: overlay merge with semantic change updates
|
|
634
|
+
// source; duplicate-with-no-change leaves source unchanged + emits warning).
|
|
635
|
+
if (fr5cActive && preMergeShape !== undefined) {
|
|
636
|
+
const postMergeShape = canonicalSerialize(target);
|
|
637
|
+
const preParsed = JSON.parse(preMergeShape) as Record<string, unknown>;
|
|
638
|
+
const postParsed = JSON.parse(postMergeShape) as Record<string, unknown>;
|
|
639
|
+
const changed = semanticDiff(preParsed, postParsed);
|
|
640
|
+
|
|
641
|
+
// Pull the existing contributor file(s) off the target's current source
|
|
642
|
+
// envelope (set at parse-fresh time or by a previous merge).
|
|
643
|
+
const existing = target.source;
|
|
644
|
+
const existingFiles = "files" in existing ? [...existing.files] : [];
|
|
645
|
+
|
|
646
|
+
if (changed) {
|
|
647
|
+
// Real overlay — upgrade source to merged with contributors.
|
|
648
|
+
const allFiles = [...new Set([...existingFiles, newContributorFile])];
|
|
649
|
+
// Alphabetical sort — matches DirectorySource ordering and gives a
|
|
650
|
+
// deterministic cross-port file list (ADR-0009 §"Cross-port adoption").
|
|
651
|
+
allFiles.sort();
|
|
652
|
+
const contributors: Contributor[] = allFiles.map((f, i) => ({
|
|
653
|
+
file: f,
|
|
654
|
+
role: i === 0 ? "overlay-base" : "overlay-extension",
|
|
655
|
+
}));
|
|
656
|
+
const mergedSource: ErrorSource = {
|
|
657
|
+
format: "merged",
|
|
658
|
+
files: allFiles,
|
|
659
|
+
jsonPath:
|
|
660
|
+
"jsonPath" in existing && existing.jsonPath !== undefined
|
|
661
|
+
? existing.jsonPath
|
|
662
|
+
: (_currentPath?.toString() ?? "$"),
|
|
663
|
+
contributors,
|
|
664
|
+
};
|
|
665
|
+
target.setSource(mergedSource);
|
|
666
|
+
} else if (
|
|
667
|
+
existingFiles.length > 0 &&
|
|
668
|
+
!existingFiles.includes(newContributorFile)
|
|
669
|
+
) {
|
|
670
|
+
// Identical re-declaration from a different file → warn.
|
|
671
|
+
const allFiles = [...new Set([...existingFiles, newContributorFile])];
|
|
672
|
+
allFiles.sort();
|
|
673
|
+
const contributors: Contributor[] = allFiles.map((f, i) => ({
|
|
674
|
+
file: f,
|
|
675
|
+
role: i === 0 ? "overlay-base" : "overlay-extension",
|
|
676
|
+
}));
|
|
677
|
+
const warnSource: ErrorSource = {
|
|
678
|
+
format: "merged",
|
|
679
|
+
files: allFiles,
|
|
680
|
+
jsonPath:
|
|
681
|
+
"jsonPath" in existing && existing.jsonPath !== undefined
|
|
682
|
+
? existing.jsonPath
|
|
683
|
+
: (_currentPath?.toString() ?? "$"),
|
|
684
|
+
contributors,
|
|
685
|
+
};
|
|
686
|
+
_currentEnvelopeWarnings?.push({
|
|
687
|
+
code: "WARN_DUPLICATE_DECLARATION",
|
|
688
|
+
message: `duplicate declaration of ${target.fqn()} with no semantic change`,
|
|
689
|
+
source: warnSource,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** FR5c — for every @-prefixed inline attr in `nodeData`, check whether the
|
|
696
|
+
* target already declares the same attr (in `preAttrs`) with a different
|
|
697
|
+
* non-empty value. If so, emit ERR_MERGE_CONFLICT with a `format: "merged"`
|
|
698
|
+
* envelope naming both contributors. The merge itself proceeds (existing
|
|
699
|
+
* last-writer-wins) so the loader sees one canonical tree; the error
|
|
700
|
+
* surfaces the conflict so a consumer can fix the metadata. */
|
|
701
|
+
function detectAttrMergeConflicts(
|
|
702
|
+
target: MetaData,
|
|
703
|
+
nodeData: Record<string, unknown>,
|
|
704
|
+
preAttrs: Map<string, AttrValue>,
|
|
705
|
+
newContributorFile: string,
|
|
706
|
+
errors: ParseError[],
|
|
707
|
+
path: string,
|
|
708
|
+
): void {
|
|
709
|
+
const existingFiles =
|
|
710
|
+
"files" in target.source ? [...target.source.files] : [];
|
|
711
|
+
|
|
712
|
+
function isEmptyValue(v: unknown): boolean {
|
|
713
|
+
if (v === undefined || v === null) return true;
|
|
714
|
+
if (typeof v === "string" && v === "") return true;
|
|
715
|
+
if (Array.isArray(v) && v.length === 0) return true;
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function attrValuesEqual(a: AttrValue, b: AttrValue): boolean {
|
|
720
|
+
// String/number/boolean — direct compare.
|
|
721
|
+
if (a === b) return true;
|
|
722
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
723
|
+
if (a.length !== b.length) return false;
|
|
724
|
+
for (let i = 0; i < a.length; i++) {
|
|
725
|
+
if (a[i] !== b[i]) return false;
|
|
726
|
+
}
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
if (
|
|
730
|
+
typeof a === "object" && a !== null && !Array.isArray(a) &&
|
|
731
|
+
typeof b === "object" && b !== null && !Array.isArray(b)
|
|
732
|
+
) {
|
|
733
|
+
// Object attrs — structural compare via JSON (key order independent
|
|
734
|
+
// through Object.keys().sort()).
|
|
735
|
+
try {
|
|
736
|
+
const ak = Object.keys(a).sort();
|
|
737
|
+
const bk = Object.keys(b).sort();
|
|
738
|
+
if (ak.length !== bk.length) return false;
|
|
739
|
+
for (let i = 0; i < ak.length; i++) {
|
|
740
|
+
if (ak[i] !== bk[i]) return false;
|
|
741
|
+
if (JSON.stringify((a as Record<string, unknown>)[ak[i]!]) !==
|
|
742
|
+
JSON.stringify((b as Record<string, unknown>)[bk[i]!])) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return true;
|
|
747
|
+
} catch {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
for (const key of Object.keys(nodeData)) {
|
|
755
|
+
if (!key.startsWith(ATTR_PREFIX)) continue;
|
|
756
|
+
if (RESERVED_KEYS.has(key)) continue;
|
|
757
|
+
const attrName = key.slice(ATTR_PREFIX.length);
|
|
758
|
+
if (RESERVED_KEYS.has(attrName)) continue;
|
|
759
|
+
|
|
760
|
+
const newValRaw = nodeData[key];
|
|
761
|
+
const existingVal = preAttrs.get(attrName);
|
|
762
|
+
|
|
763
|
+
if (existingVal === undefined) continue;
|
|
764
|
+
if (isEmptyValue(newValRaw) || isEmptyValue(existingVal)) continue;
|
|
765
|
+
// Both sides set a non-empty value — compare. If equal, last-writer-wins
|
|
766
|
+
// is a no-op; if different, that's a conflict.
|
|
767
|
+
if (attrValuesEqual(existingVal, newValRaw as AttrValue)) continue;
|
|
768
|
+
|
|
769
|
+
const allFiles = [...new Set([...existingFiles, newContributorFile])];
|
|
770
|
+
allFiles.sort();
|
|
771
|
+
const contributors: Contributor[] = allFiles.map((f, i) => ({
|
|
772
|
+
file: f,
|
|
773
|
+
role: i === 0 ? "overlay-base" : "overlay-extension",
|
|
774
|
+
}));
|
|
775
|
+
const conflictSource: ErrorSource = {
|
|
776
|
+
format: "merged",
|
|
777
|
+
files: allFiles,
|
|
778
|
+
jsonPath: `${_currentPath?.toString() ?? path}.${ATTR_PREFIX}${attrName}`,
|
|
779
|
+
contributors,
|
|
780
|
+
};
|
|
781
|
+
errors.push(
|
|
782
|
+
new ParseError(
|
|
783
|
+
`attr '${ATTR_PREFIX}${attrName}' conflicts: existing value ` +
|
|
784
|
+
`${JSON.stringify(existingVal)} differs from new value ` +
|
|
785
|
+
`${JSON.stringify(newValRaw)} on ${target.fqn()}`,
|
|
786
|
+
{ code: "ERR_MERGE_CONFLICT", source: conflictSource },
|
|
787
|
+
),
|
|
788
|
+
);
|
|
789
|
+
}
|
|
451
790
|
}
|
|
452
791
|
|
|
453
792
|
// ---------------------------------------------------------------------------
|
|
@@ -487,7 +826,7 @@ function createOrFindMetaData(
|
|
|
487
826
|
if (existing === undefined) {
|
|
488
827
|
throw new ParseError(
|
|
489
828
|
`Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`,
|
|
490
|
-
{
|
|
829
|
+
{ code: "ERR_OVERLAY_NO_TARGET", source: errSource() },
|
|
491
830
|
);
|
|
492
831
|
}
|
|
493
832
|
existing.setIsMerge(true);
|
|
@@ -524,7 +863,7 @@ function applyReservedKeys(
|
|
|
524
863
|
const rawPkg = nodeData[RESERVED_KEY_PACKAGE];
|
|
525
864
|
if (rawPkg !== undefined) {
|
|
526
865
|
if (typeof rawPkg !== "string") {
|
|
527
|
-
reportProblem(`"${RESERVED_KEY_PACKAGE}" must be a string at ${path}`, strict, warnings,
|
|
866
|
+
reportProblem(`"${RESERVED_KEY_PACKAGE}" must be a string at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
528
867
|
} else {
|
|
529
868
|
const expandedPkg = contextPkg !== undefined ? expandPackageForPath(contextPkg, rawPkg) : rawPkg;
|
|
530
869
|
model.setPackage(expandedPkg);
|
|
@@ -535,7 +874,7 @@ function applyReservedKeys(
|
|
|
535
874
|
const rawExtends = nodeData[RESERVED_KEY_EXTENDS];
|
|
536
875
|
if (rawExtends !== undefined) {
|
|
537
876
|
if (typeof rawExtends !== "string") {
|
|
538
|
-
reportProblem(`"${RESERVED_KEY_EXTENDS}" must be a string at ${path}`, strict, warnings,
|
|
877
|
+
reportProblem(`"${RESERVED_KEY_EXTENDS}" must be a string at ${path}`, strict, warnings, "ERR_UNRESOLVED_SUPER");
|
|
539
878
|
} else {
|
|
540
879
|
model.setSuper(rawExtends);
|
|
541
880
|
}
|
|
@@ -545,7 +884,7 @@ function applyReservedKeys(
|
|
|
545
884
|
const rawAbstract = nodeData[RESERVED_KEY_ABSTRACT];
|
|
546
885
|
if (rawAbstract !== undefined) {
|
|
547
886
|
if (typeof rawAbstract !== "boolean") {
|
|
548
|
-
reportProblem(`"${RESERVED_KEY_ABSTRACT}" must be a boolean at ${path}`, strict, warnings,
|
|
887
|
+
reportProblem(`"${RESERVED_KEY_ABSTRACT}" must be a boolean at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
549
888
|
} else {
|
|
550
889
|
model.setIsAbstract(rawAbstract);
|
|
551
890
|
}
|
|
@@ -555,7 +894,7 @@ function applyReservedKeys(
|
|
|
555
894
|
const rawIsArray = nodeData[RESERVED_KEY_IS_ARRAY];
|
|
556
895
|
if (rawIsArray !== undefined) {
|
|
557
896
|
if (typeof rawIsArray !== "boolean") {
|
|
558
|
-
reportProblem(`"${RESERVED_KEY_IS_ARRAY}" must be a boolean at ${path}`, strict, warnings,
|
|
897
|
+
reportProblem(`"${RESERVED_KEY_IS_ARRAY}" must be a boolean at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
559
898
|
} else {
|
|
560
899
|
model.setIsArray(rawIsArray);
|
|
561
900
|
}
|
|
@@ -587,7 +926,7 @@ function applyInlineAttrsAndUnknownKeys(
|
|
|
587
926
|
model.name !== "" ? `${model.type}.${model.subType} '${model.name}'` : `${model.type}.${model.subType}`;
|
|
588
927
|
reportProblem(
|
|
589
928
|
`Unknown key '${key}' on ${displayName} at ${path} (must be reserved or ${ATTR_PREFIX}-prefixed)`,
|
|
590
|
-
strict, warnings,
|
|
929
|
+
strict, warnings, "ERR_UNKNOWN_ATTR",
|
|
591
930
|
);
|
|
592
931
|
continue;
|
|
593
932
|
}
|
|
@@ -608,14 +947,14 @@ function applyInlineAttrsAndUnknownKeys(
|
|
|
608
947
|
`Reserved structural key '${attrName}' must not be ${ATTR_PREFIX}-prefixed ` +
|
|
609
948
|
`on ${displayName} at ${path} (write it bare)`;
|
|
610
949
|
if (strict) {
|
|
611
|
-
throw new ParseError(msg, {
|
|
950
|
+
throw new ParseError(msg, { code: "ERR_RESERVED_ATTR", source: errSource() });
|
|
612
951
|
}
|
|
613
952
|
// Lax mode: route through the module-level errors sink so the loader
|
|
614
953
|
// sees this as a hard error (parity with attr-schema-validate's
|
|
615
954
|
// ERR_BAD_ATTR_VALUE direct pushes). Falls back to warnings only if
|
|
616
955
|
// _currentErrors isn't bound (unreachable when called from buildTree).
|
|
617
956
|
if (_currentErrors !== undefined) {
|
|
618
|
-
_currentErrors.push(new ParseError(msg, {
|
|
957
|
+
_currentErrors.push(new ParseError(msg, { code: "ERR_RESERVED_ATTR", source: errSource() }));
|
|
619
958
|
} else {
|
|
620
959
|
warnings.push(msg);
|
|
621
960
|
}
|
|
@@ -630,7 +969,7 @@ function applyInlineAttrsAndUnknownKeys(
|
|
|
630
969
|
} catch (err) {
|
|
631
970
|
reportProblem(
|
|
632
971
|
`Failed to convert attribute "${ATTR_PREFIX}${attrName}" at ${path}: ${(err as Error).message}`,
|
|
633
|
-
strict, warnings,
|
|
972
|
+
strict, warnings, "ERR_BAD_ATTR_VALUE",
|
|
634
973
|
);
|
|
635
974
|
}
|
|
636
975
|
}
|
|
@@ -703,16 +1042,23 @@ function processChildren(
|
|
|
703
1042
|
if (rawChildren === undefined) return;
|
|
704
1043
|
|
|
705
1044
|
if (!Array.isArray(rawChildren)) {
|
|
706
|
-
reportProblem(`"${RESERVED_KEY_CHILDREN}" must be an array at ${path}`, strict, warnings,
|
|
1045
|
+
reportProblem(`"${RESERVED_KEY_CHILDREN}" must be an array at ${path}`, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
707
1046
|
return;
|
|
708
1047
|
}
|
|
709
1048
|
|
|
1049
|
+
// FR5a — push the "children" segment once; each iteration pushes/pops a
|
|
1050
|
+
// numeric index + the wrapper-key segment so nested nodes see the correct
|
|
1051
|
+
// jsonPath when populateNodeSource() is called during their construction.
|
|
1052
|
+
_currentPath?.pushKey(RESERVED_KEY_CHILDREN);
|
|
1053
|
+
|
|
710
1054
|
for (let i = 0; i < rawChildren.length; i++) {
|
|
711
1055
|
const childEntry = rawChildren[i];
|
|
712
1056
|
const childPath = `${path}.${RESERVED_KEY_CHILDREN}[${i}]`;
|
|
1057
|
+
_currentPath?.pushIndex(i);
|
|
713
1058
|
|
|
714
1059
|
if (typeof childEntry !== "object" || childEntry === null || Array.isArray(childEntry)) {
|
|
715
|
-
reportProblem(`Child at ${childPath} must be an object`, strict, warnings,
|
|
1060
|
+
reportProblem(`Child at ${childPath} must be an object`, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
1061
|
+
_currentPath?.pop();
|
|
716
1062
|
continue;
|
|
717
1063
|
}
|
|
718
1064
|
|
|
@@ -724,19 +1070,32 @@ function processChildren(
|
|
|
724
1070
|
childKeys.length === 0
|
|
725
1071
|
? `Child at ${childPath} has no type wrapper key`
|
|
726
1072
|
: `Child at ${childPath} has multiple keys (${childKeys.join(", ")}) — each child must have exactly one wrapper key`;
|
|
727
|
-
reportProblem(msg, strict, warnings,
|
|
1073
|
+
reportProblem(msg, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
1074
|
+
_currentPath?.pop();
|
|
728
1075
|
continue;
|
|
729
1076
|
}
|
|
730
1077
|
|
|
731
1078
|
const childKey = childKeys[0]!;
|
|
732
1079
|
const childData = childRecord[childKey];
|
|
733
1080
|
const childNodePath = `${childPath}.${childKey}`;
|
|
1081
|
+
_currentPath?.pushKey(childKey);
|
|
1082
|
+
// FR5b — set the current node's YAML position (if any) before the
|
|
1083
|
+
// create/merge call. Save the parent's position to restore after the
|
|
1084
|
+
// recursion returns (children may push deeper positions during their
|
|
1085
|
+
// own processChildren walk).
|
|
1086
|
+
const savedYamlPosition = _currentYamlPosition;
|
|
1087
|
+
if (_currentFormat === "yaml") {
|
|
1088
|
+
_currentYamlPosition = getYamlPosition(childRecord, childKey);
|
|
1089
|
+
}
|
|
734
1090
|
|
|
735
1091
|
if (typeof childData !== "object" || childData === null || Array.isArray(childData)) {
|
|
736
1092
|
reportProblem(
|
|
737
1093
|
`Child wrapper "${childKey}" at ${childNodePath} must contain an object`,
|
|
738
|
-
strict, warnings,
|
|
1094
|
+
strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT",
|
|
739
1095
|
);
|
|
1096
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
1097
|
+
_currentPath?.pop(); // pop array index
|
|
1098
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
740
1099
|
continue;
|
|
741
1100
|
}
|
|
742
1101
|
|
|
@@ -758,9 +1117,12 @@ function processChildren(
|
|
|
758
1117
|
errors.push(
|
|
759
1118
|
new ParseError(
|
|
760
1119
|
`Unknown type "${childType}.${childSubType}" — not registered`,
|
|
761
|
-
{
|
|
1120
|
+
{ code: childTypeCode, source: errSource() },
|
|
762
1121
|
),
|
|
763
1122
|
);
|
|
1123
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
1124
|
+
_currentPath?.pop(); // pop array index
|
|
1125
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
764
1126
|
continue; // skip this child
|
|
765
1127
|
}
|
|
766
1128
|
}
|
|
@@ -789,7 +1151,11 @@ function processChildren(
|
|
|
789
1151
|
parent.addChild(childModel);
|
|
790
1152
|
}
|
|
791
1153
|
}
|
|
1154
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
1155
|
+
_currentPath?.pop(); // pop array index
|
|
1156
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
792
1157
|
}
|
|
1158
|
+
_currentPath?.pop(); // pop the "children" key
|
|
793
1159
|
}
|
|
794
1160
|
|
|
795
1161
|
// ---------------------------------------------------------------------------
|
|
@@ -821,7 +1187,7 @@ function parseAttrChild(
|
|
|
821
1187
|
if (typeof attrName !== "string" || attrName === "") {
|
|
822
1188
|
reportProblem(
|
|
823
1189
|
`attr child at ${path} requires a non-empty "${RESERVED_KEY_NAME}" string`,
|
|
824
|
-
strict, warnings,
|
|
1190
|
+
strict, warnings, "ERR_MISSING_REQUIRED_ATTR",
|
|
825
1191
|
);
|
|
826
1192
|
return;
|
|
827
1193
|
}
|
|
@@ -829,7 +1195,7 @@ function parseAttrChild(
|
|
|
829
1195
|
if (attrValue === undefined) {
|
|
830
1196
|
reportProblem(
|
|
831
1197
|
`attr child "${attrName}" at ${path} is missing "${RESERVED_KEY_VALUE}"`,
|
|
832
|
-
strict, warnings,
|
|
1198
|
+
strict, warnings, "ERR_MISSING_REQUIRED_ATTR",
|
|
833
1199
|
);
|
|
834
1200
|
return;
|
|
835
1201
|
}
|
|
@@ -852,7 +1218,7 @@ function parseAttrChild(
|
|
|
852
1218
|
} catch (err) {
|
|
853
1219
|
reportProblem(
|
|
854
1220
|
`Failed to convert attr child "${attrName}" value at ${path}: ${(err as Error).message}`,
|
|
855
|
-
strict, warnings,
|
|
1221
|
+
strict, warnings, "ERR_BAD_ATTR_VALUE",
|
|
856
1222
|
);
|
|
857
1223
|
return;
|
|
858
1224
|
}
|