@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/dist/parser-core.js
CHANGED
|
@@ -25,34 +25,55 @@
|
|
|
25
25
|
import { TypeId, TypeRegistry } from "./registry.js";
|
|
26
26
|
import { MetaRoot } from "./shared/meta-root.js";
|
|
27
27
|
import { MetaAttr } from "./core/attr/meta-attr.js";
|
|
28
|
-
import { inferAttrSubType } from "./serializer-json.js";
|
|
28
|
+
import { canonicalSerialize, inferAttrSubType } from "./serializer-json.js";
|
|
29
29
|
import { ParseError } from "./errors.js";
|
|
30
|
+
import { resolvedSource } from "./source.js";
|
|
31
|
+
import { semanticDiff } from "./semantic-diff.js";
|
|
30
32
|
import { resolveSuperRef } from "./super-resolve.js";
|
|
33
|
+
import { JsonPathBuilder } from "./json-path.js";
|
|
34
|
+
import { getYamlPosition } from "./core/yaml-positions.js";
|
|
31
35
|
import { TYPE_ATTR, TYPE_FIELD, TYPE_OBJECT, TYPE_VALIDATOR, SUBTYPE_BASE, } from "./shared/base-types.js";
|
|
32
36
|
import { RESERVED_KEYS, RESERVED_KEY_NAME, RESERVED_KEY_PACKAGE, RESERVED_KEY_EXTENDS, RESERVED_KEY_ABSTRACT, RESERVED_KEY_OVERLAY, RESERVED_KEY_IS_ARRAY, RESERVED_KEY_CHILDREN, RESERVED_KEY_VALUE, JSON_KEY_SCHEMA, ATTR_PREFIX, TYPE_SUBTYPE_SEPARATOR, PACKAGE_SEPARATOR, } from "./shared/structural.js";
|
|
33
37
|
import { ATTR_SUBTYPE_PROPERTIES } from "./core/attr/attr-constants.js";
|
|
34
38
|
// ---------------------------------------------------------------------------
|
|
35
|
-
// Internal helper — build
|
|
36
|
-
//
|
|
39
|
+
// Internal helper — build a parse-phase ErrorSource envelope.
|
|
40
|
+
//
|
|
41
|
+
// FR5a / ADR-0009: ParseError carries a `source: ErrorSource` envelope. For
|
|
42
|
+
// errors raised mid-parse, the envelope's jsonPath comes from the parser's
|
|
43
|
+
// module-level JsonPathBuilder (synced with the walk); files[0] is the parsed
|
|
44
|
+
// source id. Falls back to a code-source envelope if invoked outside a buildTree
|
|
45
|
+
// run (defensive — every callsite below runs inside buildTree).
|
|
37
46
|
// ---------------------------------------------------------------------------
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
export function errSource() {
|
|
48
|
+
if (_currentPath !== undefined && _currentSourceId !== undefined) {
|
|
49
|
+
// FR5b (finalized 2026-05-27, all four ports) — buildTree-emitted errors
|
|
50
|
+
// from a YAML input now emit `format: "yaml"` (was `"json"` interim while
|
|
51
|
+
// each port shipped yamlPosition tracking). The optional `yamlPosition`
|
|
52
|
+
// rides along when the desugar's position map covers the current node.
|
|
53
|
+
if (_currentFormat === "yaml") {
|
|
54
|
+
return {
|
|
55
|
+
format: "yaml",
|
|
56
|
+
files: [_currentSourceId],
|
|
57
|
+
jsonPath: _currentPath.toString(),
|
|
58
|
+
...(_currentYamlPosition !== undefined
|
|
59
|
+
? { yamlPosition: _currentYamlPosition }
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
format: "json",
|
|
65
|
+
files: [_currentSourceId],
|
|
66
|
+
jsonPath: _currentPath.toString(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { format: "code", caller: "parser-core" };
|
|
45
70
|
}
|
|
46
71
|
// ---------------------------------------------------------------------------
|
|
47
72
|
// Internal helper — report a problem: throw in strict mode, push warning otherwise
|
|
48
73
|
// ---------------------------------------------------------------------------
|
|
49
|
-
function reportProblem(msg, strict, warnings,
|
|
74
|
+
function reportProblem(msg, strict, warnings, code) {
|
|
50
75
|
if (strict) {
|
|
51
|
-
|
|
52
|
-
if (code !== undefined)
|
|
53
|
-
throw new ParseError(msg, { ...opts, code });
|
|
54
|
-
else
|
|
55
|
-
throw new ParseError(msg, opts);
|
|
76
|
+
throw new ParseError(msg, { code, source: errSource() });
|
|
56
77
|
}
|
|
57
78
|
warnings.push(msg);
|
|
58
79
|
}
|
|
@@ -107,6 +128,54 @@ let _deferSuperResolution = false;
|
|
|
107
128
|
// Safe because buildTree is synchronous — same reentrancy argument as
|
|
108
129
|
// _deferSuperResolution.
|
|
109
130
|
let _currentErrors;
|
|
131
|
+
// FR5c — envelope-warnings sink for the merge phase. Set at buildTree entry
|
|
132
|
+
// so deeply-nested merge sites can emit WARN_DUPLICATE_DECLARATION without
|
|
133
|
+
// threading another parameter through every helper signature. Safe under
|
|
134
|
+
// the same synchronous-buildTree reentrancy argument as the others.
|
|
135
|
+
let _currentEnvelopeWarnings;
|
|
136
|
+
// FR5a / ADR-0009 — Module-level JSONPath builder + source id, set at
|
|
137
|
+
// buildTree entry and updated by recursive descent (push on the way down,
|
|
138
|
+
// pop on the way back up). Used by populateNodeSource() to stamp every
|
|
139
|
+
// constructed MetaData with `{ format: "json"|"yaml", files: [sourceId],
|
|
140
|
+
// jsonPath, [yamlPosition?] }`.
|
|
141
|
+
// Safe because buildTree is synchronous — same reentrancy argument as
|
|
142
|
+
// _currentErrors above.
|
|
143
|
+
let _currentPath;
|
|
144
|
+
let _currentSourceId;
|
|
145
|
+
// FR5b — source format discriminant + current node's YAML position, set
|
|
146
|
+
// by the per-child iteration in processChildren (and at root) right before
|
|
147
|
+
// the parseNodeFresh call. The position is read from the wrapper object's
|
|
148
|
+
// position-by-key map (attached by the YAML walker in core/yaml-positions.ts
|
|
149
|
+
// and preserved through core/yaml-desugar.ts).
|
|
150
|
+
let _currentFormat = "json";
|
|
151
|
+
let _currentYamlPosition;
|
|
152
|
+
/** FR5a/FR5b — stamp the source-provenance envelope on a freshly-created
|
|
153
|
+
* node. No-op when invoked outside buildTree's setup (defensive — the
|
|
154
|
+
* module-level state will always be populated during a normal parse).
|
|
155
|
+
*
|
|
156
|
+
* FR5b (finalized 2026-05-27) — YAML-input nodes get `format: "yaml"`
|
|
157
|
+
* envelopes (was `"json"` interim). The optional `yamlPosition` rides
|
|
158
|
+
* along when the desugar's position map covers the current node. */
|
|
159
|
+
function populateNodeSource(node) {
|
|
160
|
+
if (_currentPath === undefined || _currentSourceId === undefined)
|
|
161
|
+
return;
|
|
162
|
+
if (_currentFormat === "yaml") {
|
|
163
|
+
node.setSource({
|
|
164
|
+
format: "yaml",
|
|
165
|
+
files: [_currentSourceId],
|
|
166
|
+
jsonPath: _currentPath.toString(),
|
|
167
|
+
...(_currentYamlPosition !== undefined
|
|
168
|
+
? { yamlPosition: _currentYamlPosition }
|
|
169
|
+
: {}),
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
node.setSource({
|
|
174
|
+
format: "json",
|
|
175
|
+
files: [_currentSourceId],
|
|
176
|
+
jsonPath: _currentPath.toString(),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
110
179
|
/**
|
|
111
180
|
* buildTree — the shared registry-driven tree-builder.
|
|
112
181
|
*
|
|
@@ -121,27 +190,47 @@ let _currentErrors;
|
|
|
121
190
|
export function buildTree(parsed, opts) {
|
|
122
191
|
const warnings = [];
|
|
123
192
|
const errors = [];
|
|
193
|
+
const envelopeWarnings = [];
|
|
124
194
|
const strict = opts.strict ?? false;
|
|
125
195
|
const source = opts.sourceName;
|
|
126
196
|
_deferSuperResolution = opts.deferSuperResolution === true;
|
|
127
197
|
_currentErrors = errors;
|
|
198
|
+
// FR5c — module-level handle so the deeply-nested merge code paths can
|
|
199
|
+
// emit envelope warnings without threading another parameter through the
|
|
200
|
+
// entire walk. Safe because buildTree is fully synchronous.
|
|
201
|
+
_currentEnvelopeWarnings = envelopeWarnings;
|
|
202
|
+
// FR5a — start a fresh JSONPath stack rooted at "$"; sourceId is the
|
|
203
|
+
// source's id (from FileSource / InMemoryStringSource via opts.sourceName).
|
|
204
|
+
// Falls back to "<unknown>" when no name was supplied (e.g. ad-hoc parseJson
|
|
205
|
+
// calls from tests).
|
|
206
|
+
_currentPath = new JsonPathBuilder();
|
|
207
|
+
_currentSourceId = source ?? "<unknown>";
|
|
208
|
+
// FR5b — propagate the per-parse source-format discriminant. parseJson
|
|
209
|
+
// omits the option (defaults to "json"); parseYaml supplies "yaml".
|
|
210
|
+
_currentFormat = opts.sourceFormat ?? "json";
|
|
211
|
+
_currentYamlPosition = undefined;
|
|
128
212
|
try {
|
|
129
213
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
130
|
-
throw new ParseError("Top-level metadata must be an object", {
|
|
214
|
+
throw new ParseError("Top-level metadata must be an object", { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() });
|
|
131
215
|
}
|
|
132
216
|
const topLevel = parsed;
|
|
133
217
|
// --- Find the wrapper key (skip $schema) ---
|
|
134
218
|
const wrapperKeys = Object.keys(topLevel).filter((k) => k !== JSON_KEY_SCHEMA);
|
|
135
219
|
if (wrapperKeys.length === 0) {
|
|
136
|
-
throw new ParseError("Top-level metadata object has no type wrapper key", {
|
|
220
|
+
throw new ParseError("Top-level metadata object has no type wrapper key", { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() });
|
|
137
221
|
}
|
|
138
222
|
if (wrapperKeys.length > 1) {
|
|
139
|
-
throw new ParseError(`Top-level metadata object must have exactly one wrapper key (found: ${wrapperKeys.join(", ")})`, {
|
|
223
|
+
throw new ParseError(`Top-level metadata object must have exactly one wrapper key (found: ${wrapperKeys.join(", ")})`, { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() });
|
|
140
224
|
}
|
|
141
225
|
const rootKey = wrapperKeys[0];
|
|
142
226
|
const rootData = topLevel[rootKey];
|
|
143
227
|
if (typeof rootData !== "object" || rootData === null || Array.isArray(rootData)) {
|
|
144
|
-
|
|
228
|
+
// The error context references the rootKey wrapper; push it so the
|
|
229
|
+
// envelope's jsonPath includes it (matches the legacy `path` slot).
|
|
230
|
+
_currentPath.pushKey(rootKey);
|
|
231
|
+
const src = errSource();
|
|
232
|
+
_currentPath.pop();
|
|
233
|
+
throw new ParseError(`Top-level wrapper "${rootKey}" must contain an object`, { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: src });
|
|
145
234
|
}
|
|
146
235
|
const rootDataObj = rootData;
|
|
147
236
|
const { type: rootType, subType: rootSubType } = splitTypeKey(rootKey, opts.registry);
|
|
@@ -150,7 +239,21 @@ export function buildTree(parsed, opts) {
|
|
|
150
239
|
const rootTypeCode = opts.registry.allSubTypesOf(rootType).length > 0
|
|
151
240
|
? "ERR_UNKNOWN_SUBTYPE"
|
|
152
241
|
: "ERR_UNKNOWN_TYPE";
|
|
153
|
-
|
|
242
|
+
_currentPath.pushKey(rootKey);
|
|
243
|
+
const src = errSource();
|
|
244
|
+
_currentPath.pop();
|
|
245
|
+
throw new ParseError(`Unknown root type "${rootType}.${rootSubType}" — not registered`, { code: rootTypeCode, source: src });
|
|
246
|
+
}
|
|
247
|
+
// FR5a — push the wrapper-key segment onto the JSONPath stack so all
|
|
248
|
+
// descendants emit jsonPath strings rooted at "$.<rootKey>". The merge-mode
|
|
249
|
+
// path keeps the existing root's source untouched; only NEW children get
|
|
250
|
+
// populated with the current source's id (correct: the existing root was
|
|
251
|
+
// already stamped from the file that created it).
|
|
252
|
+
_currentPath.pushKey(rootKey);
|
|
253
|
+
// FR5b — look up the root wrapper's YAML position (when in yaml mode)
|
|
254
|
+
// so parseNodeFresh stamps `source.yamlPosition` on the root node.
|
|
255
|
+
if (_currentFormat === "yaml") {
|
|
256
|
+
_currentYamlPosition = getYamlPosition(topLevel, rootKey);
|
|
154
257
|
}
|
|
155
258
|
if (opts.intoRoot !== undefined) {
|
|
156
259
|
// --- Merge mode: parse root's attrs/children into the existing root ---
|
|
@@ -161,7 +264,8 @@ export function buildTree(parsed, opts) {
|
|
|
161
264
|
const contextPkg = (typeof newRootPkg === "string" ? newRootPkg : opts.intoRoot.package) ?? "";
|
|
162
265
|
parseNodeInto(rootDataObj, opts.intoRoot, opts.intoRoot, // accumulating root for super resolution
|
|
163
266
|
contextPkg, opts.registry, warnings, errors, strict, source, rootKey);
|
|
164
|
-
|
|
267
|
+
_currentPath.pop();
|
|
268
|
+
return { root: opts.intoRoot, warnings, errors, envelopeWarnings };
|
|
165
269
|
}
|
|
166
270
|
// --- Fresh root mode: create a new root from the JSON ---
|
|
167
271
|
// The cast is safe within the core provider: `metadata.root` is the only
|
|
@@ -174,11 +278,17 @@ export function buildTree(parsed, opts) {
|
|
|
174
278
|
const root = parseNodeFresh(rootType, rootSubType, rootDataObj, undefined, // no accumulating root yet — built as we go
|
|
175
279
|
"", // no inherited context pkg yet for the root itself
|
|
176
280
|
opts.registry, warnings, errors, strict, source, rootKey);
|
|
177
|
-
|
|
281
|
+
_currentPath.pop();
|
|
282
|
+
return { root, warnings, errors, envelopeWarnings };
|
|
178
283
|
}
|
|
179
284
|
finally {
|
|
180
285
|
_deferSuperResolution = false;
|
|
181
286
|
_currentErrors = undefined;
|
|
287
|
+
_currentEnvelopeWarnings = undefined;
|
|
288
|
+
_currentPath = undefined;
|
|
289
|
+
_currentSourceId = undefined;
|
|
290
|
+
_currentFormat = "json";
|
|
291
|
+
_currentYamlPosition = undefined;
|
|
182
292
|
}
|
|
183
293
|
}
|
|
184
294
|
// ---------------------------------------------------------------------------
|
|
@@ -200,10 +310,12 @@ parent) {
|
|
|
200
310
|
}
|
|
201
311
|
else {
|
|
202
312
|
const msg = `Unknown type "${type}.${subType}" — not registered`;
|
|
203
|
-
errors.push(new ParseError(msg, {
|
|
313
|
+
errors.push(new ParseError(msg, { code: "ERR_UNKNOWN_TYPE", source: errSource() }));
|
|
204
314
|
const rawName = nodeData[RESERVED_KEY_NAME];
|
|
205
315
|
const name = typeof rawName === "string" ? rawName : "";
|
|
206
|
-
|
|
316
|
+
const stub = new MetaRoot(new TypeId(type, subType), name);
|
|
317
|
+
populateNodeSource(stub);
|
|
318
|
+
return stub;
|
|
207
319
|
}
|
|
208
320
|
}
|
|
209
321
|
// --- Determine name ---
|
|
@@ -212,6 +324,10 @@ parent) {
|
|
|
212
324
|
// --- Create the model ---
|
|
213
325
|
const def = registry.find(type, subType);
|
|
214
326
|
const model = def.factory(def.typeId, name);
|
|
327
|
+
// FR5a — stamp the source provenance envelope using the parser's current
|
|
328
|
+
// JSONPath stack + source id. setSource happens BEFORE freeze (the parser
|
|
329
|
+
// is the only caller during loading; freeze runs in the loader after).
|
|
330
|
+
populateNodeSource(model);
|
|
215
331
|
// --- Apply reserved keys (package, extends, abstract, isArray) ---
|
|
216
332
|
applyReservedKeys(model, nodeData, strict, source, path, warnings, inheritedContextPkg);
|
|
217
333
|
// --- Inherit package from context if not explicitly set ---
|
|
@@ -243,12 +359,18 @@ parent) {
|
|
|
243
359
|
model.setSuperResolved(superModel);
|
|
244
360
|
}
|
|
245
361
|
else {
|
|
246
|
-
|
|
362
|
+
// FR5d — emit format=resolved with referrer + target. referrer is the
|
|
363
|
+
// declaring node's FQN (we just built it above); target is the
|
|
364
|
+
// unresolved supertype ref string.
|
|
365
|
+
throw new ParseError(`the SuperClass '${model.superRef}' does not exist in file '${source ?? "<unknown>"}'`, {
|
|
366
|
+
code: "ERR_UNRESOLVED_SUPER",
|
|
367
|
+
source: resolvedSource(errSource(), model.fqn(), model.superRef),
|
|
368
|
+
});
|
|
247
369
|
}
|
|
248
370
|
}
|
|
249
371
|
else if (model.superRef !== undefined && accumRoot === undefined) {
|
|
250
372
|
// Root node has a super ref — not resolvable against itself.
|
|
251
|
-
reportProblem(`super on root node ('${model.superRef}') is not supported and will be ignored`, strict, warnings,
|
|
373
|
+
reportProblem(`super on root node ('${model.superRef}') is not supported and will be ignored`, strict, warnings, "ERR_UNRESOLVED_SUPER");
|
|
252
374
|
}
|
|
253
375
|
// --- Process inline attributes and other keys ---
|
|
254
376
|
applyInlineAttrsAndUnknownKeys(model, nodeData, strict, source, path, warnings, registry);
|
|
@@ -267,6 +389,36 @@ parent) {
|
|
|
267
389
|
// extends, package) — those belong to the original model's identity.
|
|
268
390
|
// ---------------------------------------------------------------------------
|
|
269
391
|
function parseNodeInto(nodeData, target, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path) {
|
|
392
|
+
// FR5c — capture pre-merge state so we can decide whether this contribution
|
|
393
|
+
// produced any semantic change. The "new contributor" is the file currently
|
|
394
|
+
// being parsed (_currentSourceId). The "base contributor" is whichever
|
|
395
|
+
// file(s) the existing target already records on its source envelope.
|
|
396
|
+
const newContributorFile = _currentSourceId ?? "<unknown>";
|
|
397
|
+
const targetIsRoot = target instanceof MetaRoot;
|
|
398
|
+
// The root is a synthetic accumulator: it is not a metadata-author node —
|
|
399
|
+
// every file declares { "metadata.root": ... } and the loader merges into
|
|
400
|
+
// a single root. We don't run FR5c diagnostics on the root itself; the
|
|
401
|
+
// merge attribution applies to author-meaningful nodes (object/field/etc).
|
|
402
|
+
const fr5cActive = !targetIsRoot && target.name !== "";
|
|
403
|
+
let preMergeShape;
|
|
404
|
+
let preMergeAttrSnapshot;
|
|
405
|
+
if (fr5cActive) {
|
|
406
|
+
preMergeShape = canonicalSerialize(target);
|
|
407
|
+
// Snapshot of own attrs (name → value) — used to detect ERR_MERGE_CONFLICT
|
|
408
|
+
// when a new contribution sets an attr the target already declared to a
|
|
409
|
+
// different non-empty value.
|
|
410
|
+
preMergeAttrSnapshot = new Map(target.ownAttrs());
|
|
411
|
+
}
|
|
412
|
+
// FR5c — detect attribute-level merge conflicts: for each @-prefixed inline
|
|
413
|
+
// attr in nodeData, if the target already has an own attr of that name
|
|
414
|
+
// with a non-empty value, AND the new value differs from the existing
|
|
415
|
+
// value (both sides non-empty), emit ERR_MERGE_CONFLICT with a `format:
|
|
416
|
+
// "merged"` envelope. Last-writer-wins is preserved for non-conflicting
|
|
417
|
+
// cases (one side unset, same value, etc.) — those carry through to the
|
|
418
|
+
// existing applyInlineAttrsAndUnknownKeys logic below.
|
|
419
|
+
if (fr5cActive && preMergeAttrSnapshot !== undefined) {
|
|
420
|
+
detectAttrMergeConflicts(target, nodeData, preMergeAttrSnapshot, newContributorFile, errors, path);
|
|
421
|
+
}
|
|
270
422
|
// Apply inline attrs (not reserved keys — those stay on the existing model)
|
|
271
423
|
applyInlineAttrsAndUnknownKeys(target, nodeData, strict, source, path, warnings, registry);
|
|
272
424
|
// The effective package for children: use inheritedContextPkg (from the new
|
|
@@ -274,6 +426,155 @@ function parseNodeInto(nodeData, target, accumRoot, inheritedContextPkg, registr
|
|
|
274
426
|
// being merged into, and new children inherit from the NEW context.
|
|
275
427
|
const effectivePkg = inheritedContextPkg;
|
|
276
428
|
processChildren(target, nodeData, accumRoot, effectivePkg, registry, warnings, errors, strict, source, path);
|
|
429
|
+
// FR5c — after merge: compare pre/post shape. If no semantic change → emit
|
|
430
|
+
// WARN_DUPLICATE_DECLARATION (the new contribution declared the same thing
|
|
431
|
+
// the target already had). If there was a change → upgrade the target's
|
|
432
|
+
// source envelope to `format: "merged"` with both contributors listed
|
|
433
|
+
// (ADR-0009 §Source-on-node: overlay merge with semantic change updates
|
|
434
|
+
// source; duplicate-with-no-change leaves source unchanged + emits warning).
|
|
435
|
+
if (fr5cActive && preMergeShape !== undefined) {
|
|
436
|
+
const postMergeShape = canonicalSerialize(target);
|
|
437
|
+
const preParsed = JSON.parse(preMergeShape);
|
|
438
|
+
const postParsed = JSON.parse(postMergeShape);
|
|
439
|
+
const changed = semanticDiff(preParsed, postParsed);
|
|
440
|
+
// Pull the existing contributor file(s) off the target's current source
|
|
441
|
+
// envelope (set at parse-fresh time or by a previous merge).
|
|
442
|
+
const existing = target.source;
|
|
443
|
+
const existingFiles = "files" in existing ? [...existing.files] : [];
|
|
444
|
+
if (changed) {
|
|
445
|
+
// Real overlay — upgrade source to merged with contributors.
|
|
446
|
+
const allFiles = [...new Set([...existingFiles, newContributorFile])];
|
|
447
|
+
// Alphabetical sort — matches DirectorySource ordering and gives a
|
|
448
|
+
// deterministic cross-port file list (ADR-0009 §"Cross-port adoption").
|
|
449
|
+
allFiles.sort();
|
|
450
|
+
const contributors = allFiles.map((f, i) => ({
|
|
451
|
+
file: f,
|
|
452
|
+
role: i === 0 ? "overlay-base" : "overlay-extension",
|
|
453
|
+
}));
|
|
454
|
+
const mergedSource = {
|
|
455
|
+
format: "merged",
|
|
456
|
+
files: allFiles,
|
|
457
|
+
jsonPath: "jsonPath" in existing && existing.jsonPath !== undefined
|
|
458
|
+
? existing.jsonPath
|
|
459
|
+
: (_currentPath?.toString() ?? "$"),
|
|
460
|
+
contributors,
|
|
461
|
+
};
|
|
462
|
+
target.setSource(mergedSource);
|
|
463
|
+
}
|
|
464
|
+
else if (existingFiles.length > 0 &&
|
|
465
|
+
!existingFiles.includes(newContributorFile)) {
|
|
466
|
+
// Identical re-declaration from a different file → warn.
|
|
467
|
+
const allFiles = [...new Set([...existingFiles, newContributorFile])];
|
|
468
|
+
allFiles.sort();
|
|
469
|
+
const contributors = allFiles.map((f, i) => ({
|
|
470
|
+
file: f,
|
|
471
|
+
role: i === 0 ? "overlay-base" : "overlay-extension",
|
|
472
|
+
}));
|
|
473
|
+
const warnSource = {
|
|
474
|
+
format: "merged",
|
|
475
|
+
files: allFiles,
|
|
476
|
+
jsonPath: "jsonPath" in existing && existing.jsonPath !== undefined
|
|
477
|
+
? existing.jsonPath
|
|
478
|
+
: (_currentPath?.toString() ?? "$"),
|
|
479
|
+
contributors,
|
|
480
|
+
};
|
|
481
|
+
_currentEnvelopeWarnings?.push({
|
|
482
|
+
code: "WARN_DUPLICATE_DECLARATION",
|
|
483
|
+
message: `duplicate declaration of ${target.fqn()} with no semantic change`,
|
|
484
|
+
source: warnSource,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** FR5c — for every @-prefixed inline attr in `nodeData`, check whether the
|
|
490
|
+
* target already declares the same attr (in `preAttrs`) with a different
|
|
491
|
+
* non-empty value. If so, emit ERR_MERGE_CONFLICT with a `format: "merged"`
|
|
492
|
+
* envelope naming both contributors. The merge itself proceeds (existing
|
|
493
|
+
* last-writer-wins) so the loader sees one canonical tree; the error
|
|
494
|
+
* surfaces the conflict so a consumer can fix the metadata. */
|
|
495
|
+
function detectAttrMergeConflicts(target, nodeData, preAttrs, newContributorFile, errors, path) {
|
|
496
|
+
const existingFiles = "files" in target.source ? [...target.source.files] : [];
|
|
497
|
+
function isEmptyValue(v) {
|
|
498
|
+
if (v === undefined || v === null)
|
|
499
|
+
return true;
|
|
500
|
+
if (typeof v === "string" && v === "")
|
|
501
|
+
return true;
|
|
502
|
+
if (Array.isArray(v) && v.length === 0)
|
|
503
|
+
return true;
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
function attrValuesEqual(a, b) {
|
|
507
|
+
// String/number/boolean — direct compare.
|
|
508
|
+
if (a === b)
|
|
509
|
+
return true;
|
|
510
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
511
|
+
if (a.length !== b.length)
|
|
512
|
+
return false;
|
|
513
|
+
for (let i = 0; i < a.length; i++) {
|
|
514
|
+
if (a[i] !== b[i])
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
if (typeof a === "object" && a !== null && !Array.isArray(a) &&
|
|
520
|
+
typeof b === "object" && b !== null && !Array.isArray(b)) {
|
|
521
|
+
// Object attrs — structural compare via JSON (key order independent
|
|
522
|
+
// through Object.keys().sort()).
|
|
523
|
+
try {
|
|
524
|
+
const ak = Object.keys(a).sort();
|
|
525
|
+
const bk = Object.keys(b).sort();
|
|
526
|
+
if (ak.length !== bk.length)
|
|
527
|
+
return false;
|
|
528
|
+
for (let i = 0; i < ak.length; i++) {
|
|
529
|
+
if (ak[i] !== bk[i])
|
|
530
|
+
return false;
|
|
531
|
+
if (JSON.stringify(a[ak[i]]) !==
|
|
532
|
+
JSON.stringify(b[bk[i]])) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
for (const key of Object.keys(nodeData)) {
|
|
545
|
+
if (!key.startsWith(ATTR_PREFIX))
|
|
546
|
+
continue;
|
|
547
|
+
if (RESERVED_KEYS.has(key))
|
|
548
|
+
continue;
|
|
549
|
+
const attrName = key.slice(ATTR_PREFIX.length);
|
|
550
|
+
if (RESERVED_KEYS.has(attrName))
|
|
551
|
+
continue;
|
|
552
|
+
const newValRaw = nodeData[key];
|
|
553
|
+
const existingVal = preAttrs.get(attrName);
|
|
554
|
+
if (existingVal === undefined)
|
|
555
|
+
continue;
|
|
556
|
+
if (isEmptyValue(newValRaw) || isEmptyValue(existingVal))
|
|
557
|
+
continue;
|
|
558
|
+
// Both sides set a non-empty value — compare. If equal, last-writer-wins
|
|
559
|
+
// is a no-op; if different, that's a conflict.
|
|
560
|
+
if (attrValuesEqual(existingVal, newValRaw))
|
|
561
|
+
continue;
|
|
562
|
+
const allFiles = [...new Set([...existingFiles, newContributorFile])];
|
|
563
|
+
allFiles.sort();
|
|
564
|
+
const contributors = allFiles.map((f, i) => ({
|
|
565
|
+
file: f,
|
|
566
|
+
role: i === 0 ? "overlay-base" : "overlay-extension",
|
|
567
|
+
}));
|
|
568
|
+
const conflictSource = {
|
|
569
|
+
format: "merged",
|
|
570
|
+
files: allFiles,
|
|
571
|
+
jsonPath: `${_currentPath?.toString() ?? path}.${ATTR_PREFIX}${attrName}`,
|
|
572
|
+
contributors,
|
|
573
|
+
};
|
|
574
|
+
errors.push(new ParseError(`attr '${ATTR_PREFIX}${attrName}' conflicts: existing value ` +
|
|
575
|
+
`${JSON.stringify(existingVal)} differs from new value ` +
|
|
576
|
+
`${JSON.stringify(newValRaw)} on ${target.fqn()}`, { code: "ERR_MERGE_CONFLICT", source: conflictSource }));
|
|
577
|
+
}
|
|
277
578
|
}
|
|
278
579
|
// ---------------------------------------------------------------------------
|
|
279
580
|
// createOrFindMetaData — per-node merge logic.
|
|
@@ -293,7 +594,7 @@ function createOrFindMetaData(type, subType, nodeData, parent, accumRoot, inheri
|
|
|
293
594
|
const existing = name !== "" ? parent.ownChildByTypeAndName(type, name) : undefined;
|
|
294
595
|
if (isOverlayNode) {
|
|
295
596
|
if (existing === undefined) {
|
|
296
|
-
throw new ParseError(`Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`, {
|
|
597
|
+
throw new ParseError(`Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`, { code: "ERR_OVERLAY_NO_TARGET", source: errSource() });
|
|
297
598
|
}
|
|
298
599
|
existing.setIsMerge(true);
|
|
299
600
|
parseNodeInto(nodeData, existing, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
|
|
@@ -317,7 +618,7 @@ function applyReservedKeys(model, nodeData, strict, source, path, warnings, cont
|
|
|
317
618
|
const rawPkg = nodeData[RESERVED_KEY_PACKAGE];
|
|
318
619
|
if (rawPkg !== undefined) {
|
|
319
620
|
if (typeof rawPkg !== "string") {
|
|
320
|
-
reportProblem(`"${RESERVED_KEY_PACKAGE}" must be a string at ${path}`, strict, warnings,
|
|
621
|
+
reportProblem(`"${RESERVED_KEY_PACKAGE}" must be a string at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
321
622
|
}
|
|
322
623
|
else {
|
|
323
624
|
const expandedPkg = contextPkg !== undefined ? expandPackageForPath(contextPkg, rawPkg) : rawPkg;
|
|
@@ -328,7 +629,7 @@ function applyReservedKeys(model, nodeData, strict, source, path, warnings, cont
|
|
|
328
629
|
const rawExtends = nodeData[RESERVED_KEY_EXTENDS];
|
|
329
630
|
if (rawExtends !== undefined) {
|
|
330
631
|
if (typeof rawExtends !== "string") {
|
|
331
|
-
reportProblem(`"${RESERVED_KEY_EXTENDS}" must be a string at ${path}`, strict, warnings,
|
|
632
|
+
reportProblem(`"${RESERVED_KEY_EXTENDS}" must be a string at ${path}`, strict, warnings, "ERR_UNRESOLVED_SUPER");
|
|
332
633
|
}
|
|
333
634
|
else {
|
|
334
635
|
model.setSuper(rawExtends);
|
|
@@ -338,7 +639,7 @@ function applyReservedKeys(model, nodeData, strict, source, path, warnings, cont
|
|
|
338
639
|
const rawAbstract = nodeData[RESERVED_KEY_ABSTRACT];
|
|
339
640
|
if (rawAbstract !== undefined) {
|
|
340
641
|
if (typeof rawAbstract !== "boolean") {
|
|
341
|
-
reportProblem(`"${RESERVED_KEY_ABSTRACT}" must be a boolean at ${path}`, strict, warnings,
|
|
642
|
+
reportProblem(`"${RESERVED_KEY_ABSTRACT}" must be a boolean at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
342
643
|
}
|
|
343
644
|
else {
|
|
344
645
|
model.setIsAbstract(rawAbstract);
|
|
@@ -348,7 +649,7 @@ function applyReservedKeys(model, nodeData, strict, source, path, warnings, cont
|
|
|
348
649
|
const rawIsArray = nodeData[RESERVED_KEY_IS_ARRAY];
|
|
349
650
|
if (rawIsArray !== undefined) {
|
|
350
651
|
if (typeof rawIsArray !== "boolean") {
|
|
351
|
-
reportProblem(`"${RESERVED_KEY_IS_ARRAY}" must be a boolean at ${path}`, strict, warnings,
|
|
652
|
+
reportProblem(`"${RESERVED_KEY_IS_ARRAY}" must be a boolean at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
352
653
|
}
|
|
353
654
|
else {
|
|
354
655
|
model.setIsArray(rawIsArray);
|
|
@@ -368,7 +669,7 @@ function applyInlineAttrsAndUnknownKeys(model, nodeData, strict, source, path, w
|
|
|
368
669
|
continue;
|
|
369
670
|
if (!key.startsWith(ATTR_PREFIX)) {
|
|
370
671
|
const displayName = model.name !== "" ? `${model.type}.${model.subType} '${model.name}'` : `${model.type}.${model.subType}`;
|
|
371
|
-
reportProblem(`Unknown key '${key}' on ${displayName} at ${path} (must be reserved or ${ATTR_PREFIX}-prefixed)`, strict, warnings,
|
|
672
|
+
reportProblem(`Unknown key '${key}' on ${displayName} at ${path} (must be reserved or ${ATTR_PREFIX}-prefixed)`, strict, warnings, "ERR_UNKNOWN_ATTR");
|
|
372
673
|
continue;
|
|
373
674
|
}
|
|
374
675
|
// Inline attribute (@-prefixed) — materialize into a MetaAttr instance.
|
|
@@ -384,14 +685,14 @@ function applyInlineAttrsAndUnknownKeys(model, nodeData, strict, source, path, w
|
|
|
384
685
|
const msg = `Reserved structural key '${attrName}' must not be ${ATTR_PREFIX}-prefixed ` +
|
|
385
686
|
`on ${displayName} at ${path} (write it bare)`;
|
|
386
687
|
if (strict) {
|
|
387
|
-
throw new ParseError(msg, {
|
|
688
|
+
throw new ParseError(msg, { code: "ERR_RESERVED_ATTR", source: errSource() });
|
|
388
689
|
}
|
|
389
690
|
// Lax mode: route through the module-level errors sink so the loader
|
|
390
691
|
// sees this as a hard error (parity with attr-schema-validate's
|
|
391
692
|
// ERR_BAD_ATTR_VALUE direct pushes). Falls back to warnings only if
|
|
392
693
|
// _currentErrors isn't bound (unreachable when called from buildTree).
|
|
393
694
|
if (_currentErrors !== undefined) {
|
|
394
|
-
_currentErrors.push(new ParseError(msg, {
|
|
695
|
+
_currentErrors.push(new ParseError(msg, { code: "ERR_RESERVED_ATTR", source: errSource() }));
|
|
395
696
|
}
|
|
396
697
|
else {
|
|
397
698
|
warnings.push(msg);
|
|
@@ -404,7 +705,7 @@ function applyInlineAttrsAndUnknownKeys(model, nodeData, strict, source, path, w
|
|
|
404
705
|
model.setMetaAttr(attr);
|
|
405
706
|
}
|
|
406
707
|
catch (err) {
|
|
407
|
-
reportProblem(`Failed to convert attribute "${ATTR_PREFIX}${attrName}" at ${path}: ${err.message}`, strict, warnings,
|
|
708
|
+
reportProblem(`Failed to convert attribute "${ATTR_PREFIX}${attrName}" at ${path}: ${err.message}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
408
709
|
}
|
|
409
710
|
}
|
|
410
711
|
}
|
|
@@ -457,14 +758,20 @@ function processChildren(parent, nodeData, accumRoot, inheritedContextPkg, regis
|
|
|
457
758
|
if (rawChildren === undefined)
|
|
458
759
|
return;
|
|
459
760
|
if (!Array.isArray(rawChildren)) {
|
|
460
|
-
reportProblem(`"${RESERVED_KEY_CHILDREN}" must be an array at ${path}`, strict, warnings,
|
|
761
|
+
reportProblem(`"${RESERVED_KEY_CHILDREN}" must be an array at ${path}`, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
461
762
|
return;
|
|
462
763
|
}
|
|
764
|
+
// FR5a — push the "children" segment once; each iteration pushes/pops a
|
|
765
|
+
// numeric index + the wrapper-key segment so nested nodes see the correct
|
|
766
|
+
// jsonPath when populateNodeSource() is called during their construction.
|
|
767
|
+
_currentPath?.pushKey(RESERVED_KEY_CHILDREN);
|
|
463
768
|
for (let i = 0; i < rawChildren.length; i++) {
|
|
464
769
|
const childEntry = rawChildren[i];
|
|
465
770
|
const childPath = `${path}.${RESERVED_KEY_CHILDREN}[${i}]`;
|
|
771
|
+
_currentPath?.pushIndex(i);
|
|
466
772
|
if (typeof childEntry !== "object" || childEntry === null || Array.isArray(childEntry)) {
|
|
467
|
-
reportProblem(`Child at ${childPath} must be an object`, strict, warnings,
|
|
773
|
+
reportProblem(`Child at ${childPath} must be an object`, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
774
|
+
_currentPath?.pop();
|
|
468
775
|
continue;
|
|
469
776
|
}
|
|
470
777
|
const childRecord = childEntry;
|
|
@@ -473,14 +780,27 @@ function processChildren(parent, nodeData, accumRoot, inheritedContextPkg, regis
|
|
|
473
780
|
const msg = childKeys.length === 0
|
|
474
781
|
? `Child at ${childPath} has no type wrapper key`
|
|
475
782
|
: `Child at ${childPath} has multiple keys (${childKeys.join(", ")}) — each child must have exactly one wrapper key`;
|
|
476
|
-
reportProblem(msg, strict, warnings,
|
|
783
|
+
reportProblem(msg, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
784
|
+
_currentPath?.pop();
|
|
477
785
|
continue;
|
|
478
786
|
}
|
|
479
787
|
const childKey = childKeys[0];
|
|
480
788
|
const childData = childRecord[childKey];
|
|
481
789
|
const childNodePath = `${childPath}.${childKey}`;
|
|
790
|
+
_currentPath?.pushKey(childKey);
|
|
791
|
+
// FR5b — set the current node's YAML position (if any) before the
|
|
792
|
+
// create/merge call. Save the parent's position to restore after the
|
|
793
|
+
// recursion returns (children may push deeper positions during their
|
|
794
|
+
// own processChildren walk).
|
|
795
|
+
const savedYamlPosition = _currentYamlPosition;
|
|
796
|
+
if (_currentFormat === "yaml") {
|
|
797
|
+
_currentYamlPosition = getYamlPosition(childRecord, childKey);
|
|
798
|
+
}
|
|
482
799
|
if (typeof childData !== "object" || childData === null || Array.isArray(childData)) {
|
|
483
|
-
reportProblem(`Child wrapper "${childKey}" at ${childNodePath} must contain an object`, strict, warnings,
|
|
800
|
+
reportProblem(`Child wrapper "${childKey}" at ${childNodePath} must contain an object`, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
801
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
802
|
+
_currentPath?.pop(); // pop array index
|
|
803
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
484
804
|
continue;
|
|
485
805
|
}
|
|
486
806
|
const childDataObj = childData;
|
|
@@ -498,7 +818,10 @@ function processChildren(parent, nodeData, accumRoot, inheritedContextPkg, regis
|
|
|
498
818
|
const childTypeCode = explicit && registry.allSubTypesOf(childType).length > 0
|
|
499
819
|
? "ERR_UNKNOWN_SUBTYPE"
|
|
500
820
|
: "ERR_UNKNOWN_TYPE";
|
|
501
|
-
errors.push(new ParseError(`Unknown type "${childType}.${childSubType}" — not registered`, {
|
|
821
|
+
errors.push(new ParseError(`Unknown type "${childType}.${childSubType}" — not registered`, { code: childTypeCode, source: errSource() }));
|
|
822
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
823
|
+
_currentPath?.pop(); // pop array index
|
|
824
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
502
825
|
continue; // skip this child
|
|
503
826
|
}
|
|
504
827
|
}
|
|
@@ -513,7 +836,11 @@ function processChildren(parent, nodeData, accumRoot, inheritedContextPkg, regis
|
|
|
513
836
|
parent.addChild(childModel);
|
|
514
837
|
}
|
|
515
838
|
}
|
|
839
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
840
|
+
_currentPath?.pop(); // pop array index
|
|
841
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
516
842
|
}
|
|
843
|
+
_currentPath?.pop(); // pop the "children" key
|
|
517
844
|
}
|
|
518
845
|
// ---------------------------------------------------------------------------
|
|
519
846
|
// Attr child node — materialize into a MetaAttr instance (NOT a child).
|
|
@@ -530,11 +857,11 @@ function parseAttrChild(parent, attrType, attrSubType, attrData, registry, warni
|
|
|
530
857
|
const attrName = attrData[RESERVED_KEY_NAME];
|
|
531
858
|
const attrValue = attrData[RESERVED_KEY_VALUE];
|
|
532
859
|
if (typeof attrName !== "string" || attrName === "") {
|
|
533
|
-
reportProblem(`attr child at ${path} requires a non-empty "${RESERVED_KEY_NAME}" string`, strict, warnings,
|
|
860
|
+
reportProblem(`attr child at ${path} requires a non-empty "${RESERVED_KEY_NAME}" string`, strict, warnings, "ERR_MISSING_REQUIRED_ATTR");
|
|
534
861
|
return;
|
|
535
862
|
}
|
|
536
863
|
if (attrValue === undefined) {
|
|
537
|
-
reportProblem(`attr child "${attrName}" at ${path} is missing "${RESERVED_KEY_VALUE}"`, strict, warnings,
|
|
864
|
+
reportProblem(`attr child "${attrName}" at ${path} is missing "${RESERVED_KEY_VALUE}"`, strict, warnings, "ERR_MISSING_REQUIRED_ATTR");
|
|
538
865
|
return;
|
|
539
866
|
}
|
|
540
867
|
// Resolve the attr node's own subtype (fall back to base if unregistered).
|
|
@@ -551,7 +878,7 @@ function parseAttrChild(parent, attrType, attrSubType, attrData, registry, warni
|
|
|
551
878
|
node.setAttr(RESERVED_KEY_VALUE, desugared);
|
|
552
879
|
}
|
|
553
880
|
catch (err) {
|
|
554
|
-
reportProblem(`Failed to convert attr child "${attrName}" value at ${path}: ${err.message}`, strict, warnings,
|
|
881
|
+
reportProblem(`Failed to convert attr child "${attrName}" value at ${path}: ${err.message}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
555
882
|
return;
|
|
556
883
|
}
|
|
557
884
|
parent.setMetaAttr(node);
|