@metaobjectsdev/metadata 0.6.0-rc.1 → 0.7.0-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/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 +13 -4
- package/dist/core/parser-yaml.js.map +1 -1
- package/dist/errors.d.ts +28 -8
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +31 -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 +106 -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 +27 -27
- 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 +2 -4
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +111 -42
- 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 +68 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +13 -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/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 +15 -4
- package/src/errors.ts +42 -8
- package/src/index.ts +28 -3
- package/src/json-path.ts +46 -0
- package/src/loader/meta-data-loader.ts +148 -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 +28 -24
- package/src/naming.ts +39 -7
- package/src/parser-core.ts +113 -43
- 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 +61 -0
- package/src/subtype-rules.ts +1 -1
- package/src/super-resolve.ts +3 -1
- package/src/core/file-meta-data-loader.ts +0 -89
package/src/naming.ts
CHANGED
|
@@ -28,6 +28,31 @@ export function toSnakeCase(s: string): string {
|
|
|
28
28
|
.toLowerCase();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function toKebabCase(s: string): string {
|
|
32
|
+
return toSnakeCase(s).replace(/_/g, "-");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Column-naming strategy applied by the persistence layer to fields with no
|
|
37
|
+
* explicit `@column` override. Persistence-layer config (set on
|
|
38
|
+
* ObjectManager / buildExpectedSchema / codegen config), not a metadata attr —
|
|
39
|
+
* the same metadata can drive snake_case (PG convention) or literal (EF
|
|
40
|
+
* convention) consumers. TS port defaults to snake_case; C# port defaults to
|
|
41
|
+
* literal.
|
|
42
|
+
*/
|
|
43
|
+
export type ColumnNamingStrategy = "snake_case" | "literal" | "kebab-case";
|
|
44
|
+
|
|
45
|
+
/** Single source of truth for the TS-port default. */
|
|
46
|
+
export const DEFAULT_COLUMN_NAMING_STRATEGY: ColumnNamingStrategy = "snake_case";
|
|
47
|
+
|
|
48
|
+
export function applyColumnNamingStrategy(name: string, strategy: ColumnNamingStrategy): string {
|
|
49
|
+
switch (strategy) {
|
|
50
|
+
case "literal": return name;
|
|
51
|
+
case "kebab-case": return toKebabCase(name);
|
|
52
|
+
case "snake_case": return toSnakeCase(name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
export function pluralize(s: string): string {
|
|
32
57
|
if (/(s|x|z|ch|sh)$/i.test(s)) return s + "es";
|
|
33
58
|
if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
|
|
@@ -35,20 +60,24 @@ export function pluralize(s: string): string {
|
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
export function resolveTableName(entity: MetaData): string {
|
|
38
|
-
// Primary
|
|
63
|
+
// Primary source carries the physical table/view name (@table). Writability
|
|
64
|
+
// (table vs view/storedProc/tableFunction) only affects write-routing — for
|
|
65
|
+
// SELECT-side name resolution, a read-only primary source is the right answer.
|
|
39
66
|
const source = entity.ownChildren().find(
|
|
40
|
-
(c): c is MetaSource =>
|
|
41
|
-
c instanceof MetaSource && c.isWritable() && c.role === SOURCE_ROLE_PRIMARY,
|
|
67
|
+
(c): c is MetaSource => c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY,
|
|
42
68
|
);
|
|
43
69
|
const name = source?.tableName;
|
|
44
70
|
if (typeof name === "string" && name !== "") return name;
|
|
45
71
|
return pluralize(toSnakeCase(entity.name));
|
|
46
72
|
}
|
|
47
73
|
|
|
48
|
-
export function resolveColumnName(
|
|
74
|
+
export function resolveColumnName(
|
|
75
|
+
field: MetaData,
|
|
76
|
+
strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
77
|
+
): string {
|
|
49
78
|
const col = field.ownAttr(FIELD_ATTR_COLUMN);
|
|
50
79
|
if (typeof col === "string" && col) return col;
|
|
51
|
-
return
|
|
80
|
+
return applyColumnNamingStrategy(field.name, strategy);
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
/**
|
|
@@ -75,12 +104,15 @@ export interface EntityNameMap {
|
|
|
75
104
|
dbToJs: Map<string, string>;
|
|
76
105
|
}
|
|
77
106
|
|
|
78
|
-
export function buildNameMap(
|
|
107
|
+
export function buildNameMap(
|
|
108
|
+
entity: MetaData,
|
|
109
|
+
strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
110
|
+
): EntityNameMap {
|
|
79
111
|
const jsToDb = new Map<string, string>();
|
|
80
112
|
const dbToJs = new Map<string, string>();
|
|
81
113
|
for (const child of entity.ownChildren()) {
|
|
82
114
|
if (child.type !== TYPE_FIELD) continue;
|
|
83
|
-
const dbCol = resolveColumnName(child);
|
|
115
|
+
const dbCol = resolveColumnName(child, strategy);
|
|
84
116
|
jsToDb.set(child.name, dbCol);
|
|
85
117
|
dbToJs.set(dbCol, child.name);
|
|
86
118
|
}
|
package/src/parser-core.ts
CHANGED
|
@@ -29,7 +29,9 @@ import { MetaRoot } from "./shared/meta-root.js";
|
|
|
29
29
|
import { MetaAttr } from "./core/attr/meta-attr.js";
|
|
30
30
|
import { inferAttrSubType } from "./serializer-json.js";
|
|
31
31
|
import { ParseError, type ErrorCode } from "./errors.js";
|
|
32
|
+
import type { ErrorSource } from "./source.js";
|
|
32
33
|
import { resolveSuperRef } from "./super-resolve.js";
|
|
34
|
+
import { JsonPathBuilder } from "./json-path.js";
|
|
33
35
|
import {
|
|
34
36
|
TYPE_ATTR,
|
|
35
37
|
TYPE_FIELD,
|
|
@@ -89,18 +91,24 @@ export interface ParseResult {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
// ---------------------------------------------------------------------------
|
|
92
|
-
// Internal helper — build
|
|
93
|
-
//
|
|
94
|
+
// Internal helper — build a parse-phase ErrorSource envelope.
|
|
95
|
+
//
|
|
96
|
+
// FR5a / ADR-0009: ParseError carries a `source: ErrorSource` envelope. For
|
|
97
|
+
// errors raised mid-parse, the envelope's jsonPath comes from the parser's
|
|
98
|
+
// module-level JsonPathBuilder (synced with the walk); files[0] is the parsed
|
|
99
|
+
// source id. Falls back to a code-source envelope if invoked outside a buildTree
|
|
100
|
+
// run (defensive — every callsite below runs inside buildTree).
|
|
94
101
|
// ---------------------------------------------------------------------------
|
|
95
102
|
|
|
96
|
-
export function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
export function errSource(): ErrorSource {
|
|
104
|
+
if (_currentPath !== undefined && _currentSourceId !== undefined) {
|
|
105
|
+
return {
|
|
106
|
+
format: "json",
|
|
107
|
+
files: [_currentSourceId],
|
|
108
|
+
jsonPath: _currentPath.toString(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { format: "code", caller: "parser-core" };
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
// ---------------------------------------------------------------------------
|
|
@@ -111,14 +119,10 @@ function reportProblem(
|
|
|
111
119
|
msg: string,
|
|
112
120
|
strict: boolean,
|
|
113
121
|
warnings: string[],
|
|
114
|
-
|
|
115
|
-
path: string,
|
|
116
|
-
code?: ErrorCode,
|
|
122
|
+
code: ErrorCode,
|
|
117
123
|
): void {
|
|
118
124
|
if (strict) {
|
|
119
|
-
|
|
120
|
-
if (code !== undefined) throw new ParseError(msg, { ...opts, code });
|
|
121
|
-
else throw new ParseError(msg, opts);
|
|
125
|
+
throw new ParseError(msg, { code, source: errSource() });
|
|
122
126
|
}
|
|
123
127
|
warnings.push(msg);
|
|
124
128
|
}
|
|
@@ -196,6 +200,27 @@ let _deferSuperResolution = false;
|
|
|
196
200
|
// _deferSuperResolution.
|
|
197
201
|
let _currentErrors: ParseError[] | undefined;
|
|
198
202
|
|
|
203
|
+
// FR5a / ADR-0009 — Module-level JSONPath builder + source id, set at
|
|
204
|
+
// buildTree entry and updated by recursive descent (push on the way down,
|
|
205
|
+
// pop on the way back up). Used by populateNodeSource() to stamp every
|
|
206
|
+
// constructed MetaData with `{ format: "json", files: [sourceId], jsonPath }`.
|
|
207
|
+
// Safe because buildTree is synchronous — same reentrancy argument as
|
|
208
|
+
// _currentErrors above.
|
|
209
|
+
let _currentPath: JsonPathBuilder | undefined;
|
|
210
|
+
let _currentSourceId: string | undefined;
|
|
211
|
+
|
|
212
|
+
/** Set the parsed-from-JSON provenance envelope on a freshly-created node.
|
|
213
|
+
* No-op when the parser is invoked outside buildTree's setup (defensive — the
|
|
214
|
+
* module-level state will always be populated during a normal parse). */
|
|
215
|
+
function populateNodeSource(node: MetaData): void {
|
|
216
|
+
if (_currentPath === undefined || _currentSourceId === undefined) return;
|
|
217
|
+
node.setSource({
|
|
218
|
+
format: "json",
|
|
219
|
+
files: [_currentSourceId],
|
|
220
|
+
jsonPath: _currentPath.toString(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
/**
|
|
200
225
|
* buildTree — the shared registry-driven tree-builder.
|
|
201
226
|
*
|
|
@@ -214,10 +239,16 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
214
239
|
const source = opts.sourceName;
|
|
215
240
|
_deferSuperResolution = opts.deferSuperResolution === true;
|
|
216
241
|
_currentErrors = errors;
|
|
242
|
+
// FR5a — start a fresh JSONPath stack rooted at "$"; sourceId is the
|
|
243
|
+
// source's id (from FileSource / InMemoryStringSource via opts.sourceName).
|
|
244
|
+
// Falls back to "<unknown>" when no name was supplied (e.g. ad-hoc parseJson
|
|
245
|
+
// calls from tests).
|
|
246
|
+
_currentPath = new JsonPathBuilder();
|
|
247
|
+
_currentSourceId = source ?? "<unknown>";
|
|
217
248
|
|
|
218
249
|
try {
|
|
219
250
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
220
|
-
throw new ParseError("Top-level metadata must be an object", {
|
|
251
|
+
throw new ParseError("Top-level metadata must be an object", { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() });
|
|
221
252
|
}
|
|
222
253
|
|
|
223
254
|
const topLevel = parsed as Record<string, unknown>;
|
|
@@ -226,12 +257,12 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
226
257
|
const wrapperKeys = Object.keys(topLevel).filter((k) => k !== JSON_KEY_SCHEMA);
|
|
227
258
|
|
|
228
259
|
if (wrapperKeys.length === 0) {
|
|
229
|
-
throw new ParseError("Top-level metadata object has no type wrapper key", {
|
|
260
|
+
throw new ParseError("Top-level metadata object has no type wrapper key", { code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() });
|
|
230
261
|
}
|
|
231
262
|
if (wrapperKeys.length > 1) {
|
|
232
263
|
throw new ParseError(
|
|
233
264
|
`Top-level metadata object must have exactly one wrapper key (found: ${wrapperKeys.join(", ")})`,
|
|
234
|
-
{
|
|
265
|
+
{ code: "ERR_TOP_LEVEL_NOT_OBJECT", source: errSource() },
|
|
235
266
|
);
|
|
236
267
|
}
|
|
237
268
|
|
|
@@ -239,9 +270,14 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
239
270
|
const rootData = topLevel[rootKey];
|
|
240
271
|
|
|
241
272
|
if (typeof rootData !== "object" || rootData === null || Array.isArray(rootData)) {
|
|
273
|
+
// The error context references the rootKey wrapper; push it so the
|
|
274
|
+
// envelope's jsonPath includes it (matches the legacy `path` slot).
|
|
275
|
+
_currentPath!.pushKey(rootKey);
|
|
276
|
+
const src = errSource();
|
|
277
|
+
_currentPath!.pop();
|
|
242
278
|
throw new ParseError(
|
|
243
279
|
`Top-level wrapper "${rootKey}" must contain an object`,
|
|
244
|
-
{
|
|
280
|
+
{ code: "ERR_TOP_LEVEL_NOT_OBJECT", source: src },
|
|
245
281
|
);
|
|
246
282
|
}
|
|
247
283
|
|
|
@@ -253,12 +289,22 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
253
289
|
const rootTypeCode = opts.registry.allSubTypesOf(rootType).length > 0
|
|
254
290
|
? "ERR_UNKNOWN_SUBTYPE" as const
|
|
255
291
|
: "ERR_UNKNOWN_TYPE" as const;
|
|
292
|
+
_currentPath!.pushKey(rootKey);
|
|
293
|
+
const src = errSource();
|
|
294
|
+
_currentPath!.pop();
|
|
256
295
|
throw new ParseError(
|
|
257
296
|
`Unknown root type "${rootType}.${rootSubType}" — not registered`,
|
|
258
|
-
{
|
|
297
|
+
{ code: rootTypeCode, source: src },
|
|
259
298
|
);
|
|
260
299
|
}
|
|
261
300
|
|
|
301
|
+
// FR5a — push the wrapper-key segment onto the JSONPath stack so all
|
|
302
|
+
// descendants emit jsonPath strings rooted at "$.<rootKey>". The merge-mode
|
|
303
|
+
// path keeps the existing root's source untouched; only NEW children get
|
|
304
|
+
// populated with the current source's id (correct: the existing root was
|
|
305
|
+
// already stamped from the file that created it).
|
|
306
|
+
_currentPath!.pushKey(rootKey);
|
|
307
|
+
|
|
262
308
|
if (opts.intoRoot !== undefined) {
|
|
263
309
|
// --- Merge mode: parse root's attrs/children into the existing root ---
|
|
264
310
|
// The JSON root's own package/super/reserved-keys are not re-applied to the
|
|
@@ -278,6 +324,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
278
324
|
source,
|
|
279
325
|
rootKey,
|
|
280
326
|
);
|
|
327
|
+
_currentPath!.pop();
|
|
281
328
|
return { root: opts.intoRoot, warnings, errors };
|
|
282
329
|
}
|
|
283
330
|
|
|
@@ -302,10 +349,13 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
302
349
|
source,
|
|
303
350
|
rootKey,
|
|
304
351
|
) as MetaRoot;
|
|
352
|
+
_currentPath!.pop();
|
|
305
353
|
return { root, warnings, errors };
|
|
306
354
|
} finally {
|
|
307
355
|
_deferSuperResolution = false;
|
|
308
356
|
_currentErrors = undefined;
|
|
357
|
+
_currentPath = undefined;
|
|
358
|
+
_currentSourceId = undefined;
|
|
309
359
|
}
|
|
310
360
|
}
|
|
311
361
|
|
|
@@ -341,10 +391,12 @@ function parseNodeFresh(
|
|
|
341
391
|
subType = SUBTYPE_BASE;
|
|
342
392
|
} else {
|
|
343
393
|
const msg = `Unknown type "${type}.${subType}" — not registered`;
|
|
344
|
-
errors.push(new ParseError(msg, {
|
|
394
|
+
errors.push(new ParseError(msg, { code: "ERR_UNKNOWN_TYPE", source: errSource() }));
|
|
345
395
|
const rawName = nodeData[RESERVED_KEY_NAME];
|
|
346
396
|
const name = typeof rawName === "string" ? rawName : "";
|
|
347
|
-
|
|
397
|
+
const stub = new MetaRoot(new TypeId(type, subType), name);
|
|
398
|
+
populateNodeSource(stub);
|
|
399
|
+
return stub;
|
|
348
400
|
}
|
|
349
401
|
}
|
|
350
402
|
|
|
@@ -355,6 +407,10 @@ function parseNodeFresh(
|
|
|
355
407
|
// --- Create the model ---
|
|
356
408
|
const def = registry.find(type, subType)!;
|
|
357
409
|
const model = def.factory(def.typeId, name);
|
|
410
|
+
// FR5a — stamp the source provenance envelope using the parser's current
|
|
411
|
+
// JSONPath stack + source id. setSource happens BEFORE freeze (the parser
|
|
412
|
+
// is the only caller during loading; freeze runs in the loader after).
|
|
413
|
+
populateNodeSource(model);
|
|
358
414
|
|
|
359
415
|
// --- Apply reserved keys (package, extends, abstract, isArray) ---
|
|
360
416
|
applyReservedKeys(model, nodeData, strict, source, path, warnings, inheritedContextPkg);
|
|
@@ -392,7 +448,7 @@ function parseNodeFresh(
|
|
|
392
448
|
} else {
|
|
393
449
|
throw new ParseError(
|
|
394
450
|
`the SuperClass '${model.superRef}' does not exist in file '${source ?? "<unknown>"}'`,
|
|
395
|
-
{
|
|
451
|
+
{ code: "ERR_UNRESOLVED_SUPER", source: errSource() },
|
|
396
452
|
);
|
|
397
453
|
}
|
|
398
454
|
} else if (model.superRef !== undefined && accumRoot === undefined) {
|
|
@@ -401,8 +457,6 @@ function parseNodeFresh(
|
|
|
401
457
|
`super on root node ('${model.superRef}') is not supported and will be ignored`,
|
|
402
458
|
strict,
|
|
403
459
|
warnings,
|
|
404
|
-
source,
|
|
405
|
-
path,
|
|
406
460
|
"ERR_UNRESOLVED_SUPER",
|
|
407
461
|
);
|
|
408
462
|
}
|
|
@@ -487,7 +541,7 @@ function createOrFindMetaData(
|
|
|
487
541
|
if (existing === undefined) {
|
|
488
542
|
throw new ParseError(
|
|
489
543
|
`Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`,
|
|
490
|
-
{
|
|
544
|
+
{ code: "ERR_OVERLAY_NO_TARGET", source: errSource() },
|
|
491
545
|
);
|
|
492
546
|
}
|
|
493
547
|
existing.setIsMerge(true);
|
|
@@ -524,7 +578,7 @@ function applyReservedKeys(
|
|
|
524
578
|
const rawPkg = nodeData[RESERVED_KEY_PACKAGE];
|
|
525
579
|
if (rawPkg !== undefined) {
|
|
526
580
|
if (typeof rawPkg !== "string") {
|
|
527
|
-
reportProblem(`"${RESERVED_KEY_PACKAGE}" must be a string at ${path}`, strict, warnings,
|
|
581
|
+
reportProblem(`"${RESERVED_KEY_PACKAGE}" must be a string at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
528
582
|
} else {
|
|
529
583
|
const expandedPkg = contextPkg !== undefined ? expandPackageForPath(contextPkg, rawPkg) : rawPkg;
|
|
530
584
|
model.setPackage(expandedPkg);
|
|
@@ -535,7 +589,7 @@ function applyReservedKeys(
|
|
|
535
589
|
const rawExtends = nodeData[RESERVED_KEY_EXTENDS];
|
|
536
590
|
if (rawExtends !== undefined) {
|
|
537
591
|
if (typeof rawExtends !== "string") {
|
|
538
|
-
reportProblem(`"${RESERVED_KEY_EXTENDS}" must be a string at ${path}`, strict, warnings,
|
|
592
|
+
reportProblem(`"${RESERVED_KEY_EXTENDS}" must be a string at ${path}`, strict, warnings, "ERR_UNRESOLVED_SUPER");
|
|
539
593
|
} else {
|
|
540
594
|
model.setSuper(rawExtends);
|
|
541
595
|
}
|
|
@@ -545,7 +599,7 @@ function applyReservedKeys(
|
|
|
545
599
|
const rawAbstract = nodeData[RESERVED_KEY_ABSTRACT];
|
|
546
600
|
if (rawAbstract !== undefined) {
|
|
547
601
|
if (typeof rawAbstract !== "boolean") {
|
|
548
|
-
reportProblem(`"${RESERVED_KEY_ABSTRACT}" must be a boolean at ${path}`, strict, warnings,
|
|
602
|
+
reportProblem(`"${RESERVED_KEY_ABSTRACT}" must be a boolean at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
549
603
|
} else {
|
|
550
604
|
model.setIsAbstract(rawAbstract);
|
|
551
605
|
}
|
|
@@ -555,7 +609,7 @@ function applyReservedKeys(
|
|
|
555
609
|
const rawIsArray = nodeData[RESERVED_KEY_IS_ARRAY];
|
|
556
610
|
if (rawIsArray !== undefined) {
|
|
557
611
|
if (typeof rawIsArray !== "boolean") {
|
|
558
|
-
reportProblem(`"${RESERVED_KEY_IS_ARRAY}" must be a boolean at ${path}`, strict, warnings,
|
|
612
|
+
reportProblem(`"${RESERVED_KEY_IS_ARRAY}" must be a boolean at ${path}`, strict, warnings, "ERR_BAD_ATTR_VALUE");
|
|
559
613
|
} else {
|
|
560
614
|
model.setIsArray(rawIsArray);
|
|
561
615
|
}
|
|
@@ -587,7 +641,7 @@ function applyInlineAttrsAndUnknownKeys(
|
|
|
587
641
|
model.name !== "" ? `${model.type}.${model.subType} '${model.name}'` : `${model.type}.${model.subType}`;
|
|
588
642
|
reportProblem(
|
|
589
643
|
`Unknown key '${key}' on ${displayName} at ${path} (must be reserved or ${ATTR_PREFIX}-prefixed)`,
|
|
590
|
-
strict, warnings,
|
|
644
|
+
strict, warnings, "ERR_UNKNOWN_ATTR",
|
|
591
645
|
);
|
|
592
646
|
continue;
|
|
593
647
|
}
|
|
@@ -608,14 +662,14 @@ function applyInlineAttrsAndUnknownKeys(
|
|
|
608
662
|
`Reserved structural key '${attrName}' must not be ${ATTR_PREFIX}-prefixed ` +
|
|
609
663
|
`on ${displayName} at ${path} (write it bare)`;
|
|
610
664
|
if (strict) {
|
|
611
|
-
throw new ParseError(msg, {
|
|
665
|
+
throw new ParseError(msg, { code: "ERR_RESERVED_ATTR", source: errSource() });
|
|
612
666
|
}
|
|
613
667
|
// Lax mode: route through the module-level errors sink so the loader
|
|
614
668
|
// sees this as a hard error (parity with attr-schema-validate's
|
|
615
669
|
// ERR_BAD_ATTR_VALUE direct pushes). Falls back to warnings only if
|
|
616
670
|
// _currentErrors isn't bound (unreachable when called from buildTree).
|
|
617
671
|
if (_currentErrors !== undefined) {
|
|
618
|
-
_currentErrors.push(new ParseError(msg, {
|
|
672
|
+
_currentErrors.push(new ParseError(msg, { code: "ERR_RESERVED_ATTR", source: errSource() }));
|
|
619
673
|
} else {
|
|
620
674
|
warnings.push(msg);
|
|
621
675
|
}
|
|
@@ -630,7 +684,7 @@ function applyInlineAttrsAndUnknownKeys(
|
|
|
630
684
|
} catch (err) {
|
|
631
685
|
reportProblem(
|
|
632
686
|
`Failed to convert attribute "${ATTR_PREFIX}${attrName}" at ${path}: ${(err as Error).message}`,
|
|
633
|
-
strict, warnings,
|
|
687
|
+
strict, warnings, "ERR_BAD_ATTR_VALUE",
|
|
634
688
|
);
|
|
635
689
|
}
|
|
636
690
|
}
|
|
@@ -703,16 +757,23 @@ function processChildren(
|
|
|
703
757
|
if (rawChildren === undefined) return;
|
|
704
758
|
|
|
705
759
|
if (!Array.isArray(rawChildren)) {
|
|
706
|
-
reportProblem(`"${RESERVED_KEY_CHILDREN}" must be an array at ${path}`, strict, warnings,
|
|
760
|
+
reportProblem(`"${RESERVED_KEY_CHILDREN}" must be an array at ${path}`, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
707
761
|
return;
|
|
708
762
|
}
|
|
709
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);
|
|
768
|
+
|
|
710
769
|
for (let i = 0; i < rawChildren.length; i++) {
|
|
711
770
|
const childEntry = rawChildren[i];
|
|
712
771
|
const childPath = `${path}.${RESERVED_KEY_CHILDREN}[${i}]`;
|
|
772
|
+
_currentPath?.pushIndex(i);
|
|
713
773
|
|
|
714
774
|
if (typeof childEntry !== "object" || childEntry === null || Array.isArray(childEntry)) {
|
|
715
|
-
reportProblem(`Child at ${childPath} must be an object`, strict, warnings,
|
|
775
|
+
reportProblem(`Child at ${childPath} must be an object`, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
776
|
+
_currentPath?.pop();
|
|
716
777
|
continue;
|
|
717
778
|
}
|
|
718
779
|
|
|
@@ -724,19 +785,23 @@ function processChildren(
|
|
|
724
785
|
childKeys.length === 0
|
|
725
786
|
? `Child at ${childPath} has no type wrapper key`
|
|
726
787
|
: `Child at ${childPath} has multiple keys (${childKeys.join(", ")}) — each child must have exactly one wrapper key`;
|
|
727
|
-
reportProblem(msg, strict, warnings,
|
|
788
|
+
reportProblem(msg, strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT");
|
|
789
|
+
_currentPath?.pop();
|
|
728
790
|
continue;
|
|
729
791
|
}
|
|
730
792
|
|
|
731
793
|
const childKey = childKeys[0]!;
|
|
732
794
|
const childData = childRecord[childKey];
|
|
733
795
|
const childNodePath = `${childPath}.${childKey}`;
|
|
796
|
+
_currentPath?.pushKey(childKey);
|
|
734
797
|
|
|
735
798
|
if (typeof childData !== "object" || childData === null || Array.isArray(childData)) {
|
|
736
799
|
reportProblem(
|
|
737
800
|
`Child wrapper "${childKey}" at ${childNodePath} must contain an object`,
|
|
738
|
-
strict, warnings,
|
|
801
|
+
strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT",
|
|
739
802
|
);
|
|
803
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
804
|
+
_currentPath?.pop(); // pop array index
|
|
740
805
|
continue;
|
|
741
806
|
}
|
|
742
807
|
|
|
@@ -758,9 +823,11 @@ function processChildren(
|
|
|
758
823
|
errors.push(
|
|
759
824
|
new ParseError(
|
|
760
825
|
`Unknown type "${childType}.${childSubType}" — not registered`,
|
|
761
|
-
{
|
|
826
|
+
{ code: childTypeCode, source: errSource() },
|
|
762
827
|
),
|
|
763
828
|
);
|
|
829
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
830
|
+
_currentPath?.pop(); // pop array index
|
|
764
831
|
continue; // skip this child
|
|
765
832
|
}
|
|
766
833
|
}
|
|
@@ -789,7 +856,10 @@ function processChildren(
|
|
|
789
856
|
parent.addChild(childModel);
|
|
790
857
|
}
|
|
791
858
|
}
|
|
859
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
860
|
+
_currentPath?.pop(); // pop array index
|
|
792
861
|
}
|
|
862
|
+
_currentPath?.pop(); // pop the "children" key
|
|
793
863
|
}
|
|
794
864
|
|
|
795
865
|
// ---------------------------------------------------------------------------
|
|
@@ -821,7 +891,7 @@ function parseAttrChild(
|
|
|
821
891
|
if (typeof attrName !== "string" || attrName === "") {
|
|
822
892
|
reportProblem(
|
|
823
893
|
`attr child at ${path} requires a non-empty "${RESERVED_KEY_NAME}" string`,
|
|
824
|
-
strict, warnings,
|
|
894
|
+
strict, warnings, "ERR_MISSING_REQUIRED_ATTR",
|
|
825
895
|
);
|
|
826
896
|
return;
|
|
827
897
|
}
|
|
@@ -829,7 +899,7 @@ function parseAttrChild(
|
|
|
829
899
|
if (attrValue === undefined) {
|
|
830
900
|
reportProblem(
|
|
831
901
|
`attr child "${attrName}" at ${path} is missing "${RESERVED_KEY_VALUE}"`,
|
|
832
|
-
strict, warnings,
|
|
902
|
+
strict, warnings, "ERR_MISSING_REQUIRED_ATTR",
|
|
833
903
|
);
|
|
834
904
|
return;
|
|
835
905
|
}
|
|
@@ -852,7 +922,7 @@ function parseAttrChild(
|
|
|
852
922
|
} catch (err) {
|
|
853
923
|
reportProblem(
|
|
854
924
|
`Failed to convert attr child "${attrName}" value at ${path}: ${(err as Error).message}`,
|
|
855
|
-
strict, warnings,
|
|
925
|
+
strict, warnings, "ERR_BAD_ATTR_VALUE",
|
|
856
926
|
);
|
|
857
927
|
return;
|
|
858
928
|
}
|
package/src/parser-json.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// the canonical object to the shared buildTree (parser-core.ts).
|
|
5
5
|
|
|
6
6
|
import { ParseError } from "./errors.js";
|
|
7
|
-
import { buildTree
|
|
7
|
+
import { buildTree } from "./parser-core.js";
|
|
8
8
|
import type { ParseOptions, ParseResult } from "./parser-core.js";
|
|
9
|
+
import type { ErrorSource } from "./source.js";
|
|
9
10
|
|
|
10
11
|
export type { ParseOptions, ParseResult } from "./parser-core.js";
|
|
11
12
|
|
|
@@ -18,9 +19,17 @@ export function parseJson(content: string, opts: ParseOptions): ParseResult {
|
|
|
18
19
|
try {
|
|
19
20
|
parsed = JSON.parse(normalizedContent);
|
|
20
21
|
} catch (err) {
|
|
22
|
+
// FR5a / ADR-0009 — pre-buildTree errors can't reach the parser's
|
|
23
|
+
// module-level JsonPathBuilder; build a minimal json-source envelope
|
|
24
|
+
// rooted at "$" so cross-port callers see a consistent shape.
|
|
25
|
+
const source: ErrorSource = {
|
|
26
|
+
format: "json",
|
|
27
|
+
files: [opts.sourceName ?? "<unknown>"],
|
|
28
|
+
jsonPath: "$",
|
|
29
|
+
};
|
|
21
30
|
throw new ParseError(
|
|
22
31
|
`Invalid JSON: ${(err as Error).message}`,
|
|
23
|
-
{
|
|
32
|
+
{ code: "ERR_MALFORMED_JSON", source },
|
|
24
33
|
);
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -36,14 +36,14 @@ export function validateSourceRoles(root: MetaData): ParseError[] {
|
|
|
36
36
|
errors.push(
|
|
37
37
|
new ParseError(
|
|
38
38
|
`object "${obj.name}" declares ${sources.length} source(s) but none has role "${SOURCE_ROLE_PRIMARY}"`,
|
|
39
|
-
{ code: "ERR_SOURCE_NO_PRIMARY" },
|
|
39
|
+
{ code: "ERR_SOURCE_NO_PRIMARY", source: obj.source },
|
|
40
40
|
),
|
|
41
41
|
);
|
|
42
42
|
} else if (primaryCount > 1) {
|
|
43
43
|
errors.push(
|
|
44
44
|
new ParseError(
|
|
45
45
|
`object "${obj.name}" declares ${primaryCount} sources with role "${SOURCE_ROLE_PRIMARY}"; exactly one is required`,
|
|
46
|
-
{ code: "ERR_SOURCE_MULTIPLE_PRIMARY" },
|
|
46
|
+
{ code: "ERR_SOURCE_MULTIPLE_PRIMARY", source: obj.source },
|
|
47
47
|
),
|
|
48
48
|
);
|
|
49
49
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/semantic-diff.ts
|
|
2
|
+
//
|
|
3
|
+
// FR5a / ADR-0009 — Cross-port-aligned semantic-equality compare for metadata
|
|
4
|
+
// trees. Returns `true` if the two inputs differ in any semantically-meaningful
|
|
5
|
+
// way (excluding `source`, which is loader output).
|
|
6
|
+
//
|
|
7
|
+
// Algorithm (ADR-0009 §semantic_diff):
|
|
8
|
+
// 1. Sort attrs lexicographically; compare attr-by-attr; values by canonical
|
|
9
|
+
// structural equality (key-order independent, whitespace-insensitive).
|
|
10
|
+
// 2. Children are compared as ordered sequences.
|
|
11
|
+
// 3. Reserved structural keys (name, package, extends, abstract, overlay,
|
|
12
|
+
// isArray, value) participate like attrs.
|
|
13
|
+
// 4. `source` excluded from the diff.
|
|
14
|
+
|
|
15
|
+
type Tree = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
const EXCLUDED = new Set(["source"]);
|
|
18
|
+
|
|
19
|
+
function isObject(v: unknown): v is Tree {
|
|
20
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function equal(a: unknown, b: unknown): boolean {
|
|
24
|
+
if (a === b) return true;
|
|
25
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
26
|
+
if (a.length !== b.length) return false;
|
|
27
|
+
for (let i = 0; i < a.length; i++) {
|
|
28
|
+
if (!equal(a[i], b[i])) return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (isObject(a) && isObject(b)) {
|
|
33
|
+
const aKeys = Object.keys(a).filter((k) => !EXCLUDED.has(k)).sort();
|
|
34
|
+
const bKeys = Object.keys(b).filter((k) => !EXCLUDED.has(k)).sort();
|
|
35
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
36
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
37
|
+
if (aKeys[i] !== bKeys[i]) return false;
|
|
38
|
+
if (!equal(a[aKeys[i]!], b[bKeys[i]!])) return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns `true` if the inputs differ in any semantically-meaningful way. */
|
|
46
|
+
export function semanticDiff(a: Tree, b: Tree): boolean {
|
|
47
|
+
return !equal(a, b);
|
|
48
|
+
}
|
package/src/shared/meta-data.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { DataType } from "../data-type.js";
|
|
|
5
5
|
import type { MetaAttr } from "../core/attr/meta-attr.js";
|
|
6
6
|
import { inferAttrSubType } from "../serializer-json.js";
|
|
7
7
|
import { attrClassFor } from "../attr-class-map.js";
|
|
8
|
+
import type { ErrorSource } from "../source.js";
|
|
8
9
|
|
|
9
10
|
export type AttrValue = string | number | boolean | string[] | AttrObject;
|
|
10
11
|
|
|
@@ -57,6 +58,14 @@ export abstract class MetaData {
|
|
|
57
58
|
// construction (for field/attr nodes). Read via MetaField/MetaAttr.dataType.
|
|
58
59
|
protected _dataType?: DataType;
|
|
59
60
|
|
|
61
|
+
// ADR-0009 provenance. Always populated; defaults to `{ format: "code" }`
|
|
62
|
+
// for programmatic construction (tests, factories, in-code builders). The
|
|
63
|
+
// JSON parser overwrites via setSource() during tree walk; future phases
|
|
64
|
+
// (overlay merge, super resolution) may overwrite with merged/resolved
|
|
65
|
+
// variants. Excluded from canonical JSON serialization — loader-derived
|
|
66
|
+
// state, not metadata.
|
|
67
|
+
private _source: ErrorSource = { format: "code" };
|
|
68
|
+
|
|
60
69
|
// Per-instance read cache: only populated once the node is frozen.
|
|
61
70
|
private readonly _cache = new Map<string, unknown>();
|
|
62
71
|
|
|
@@ -171,6 +180,25 @@ export abstract class MetaData {
|
|
|
171
180
|
this._dataType = dt;
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// source (ADR-0009 provenance)
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/** Provenance envelope for this node. Always populated. See ADR-0009. */
|
|
188
|
+
get source(): ErrorSource {
|
|
189
|
+
return this._source;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Loader-internal: assign provenance. Called by parser and merge phases as
|
|
194
|
+
* they build the tree. Honors the frozen-guard like other setters.
|
|
195
|
+
* @internal
|
|
196
|
+
*/
|
|
197
|
+
setSource(s: ErrorSource): void {
|
|
198
|
+
this._assertNotFrozen();
|
|
199
|
+
this._source = s;
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
// ---------------------------------------------------------------------------
|
|
175
203
|
// isAbstract
|
|
176
204
|
// ---------------------------------------------------------------------------
|