@metaobjectsdev/metadata 0.6.0 → 0.7.0-rc.2

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.
Files changed (128) hide show
  1. package/README.md +54 -3
  2. package/dist/attr-schema-validate.js +7 -7
  3. package/dist/attr-schema-validate.js.map +1 -1
  4. package/dist/core/export-json.d.ts +6 -7
  5. package/dist/core/export-json.d.ts.map +1 -1
  6. package/dist/core/export-json.js +15 -17
  7. package/dist/core/export-json.js.map +1 -1
  8. package/dist/core/index.d.ts +4 -2
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +6 -2
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/core/parser-yaml.d.ts.map +1 -1
  13. package/dist/core/parser-yaml.js +36 -11
  14. package/dist/core/parser-yaml.js.map +1 -1
  15. package/dist/core/yaml-desugar.d.ts.map +1 -1
  16. package/dist/core/yaml-desugar.js +54 -5
  17. package/dist/core/yaml-desugar.js.map +1 -1
  18. package/dist/core/yaml-positions-walker.d.ts +21 -0
  19. package/dist/core/yaml-positions-walker.d.ts.map +1 -0
  20. package/dist/core/yaml-positions-walker.js +75 -0
  21. package/dist/core/yaml-positions-walker.js.map +1 -0
  22. package/dist/core/yaml-positions.d.ts +19 -0
  23. package/dist/core/yaml-positions.d.ts.map +1 -0
  24. package/dist/core/yaml-positions.js +60 -0
  25. package/dist/core/yaml-positions.js.map +1 -0
  26. package/dist/errors.d.ts +32 -9
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +44 -5
  29. package/dist/errors.js.map +1 -1
  30. package/dist/index.d.ts +6 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +9 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/json-path.d.ts +8 -0
  35. package/dist/json-path.d.ts.map +1 -0
  36. package/dist/json-path.js +39 -0
  37. package/dist/json-path.js.map +1 -0
  38. package/dist/loader/meta-data-loader.d.ts +47 -6
  39. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  40. package/dist/loader/meta-data-loader.js +126 -8
  41. package/dist/loader/meta-data-loader.js.map +1 -1
  42. package/dist/loader/meta-data-source.d.ts +6 -2
  43. package/dist/loader/meta-data-source.d.ts.map +1 -1
  44. package/dist/loader/meta-data-source.js +10 -6
  45. package/dist/loader/meta-data-source.js.map +1 -1
  46. package/dist/loader/shortcuts.d.ts +9 -0
  47. package/dist/loader/shortcuts.d.ts.map +1 -0
  48. package/dist/loader/shortcuts.js +19 -0
  49. package/dist/loader/shortcuts.js.map +1 -0
  50. package/dist/loader/sources/directory-source.d.ts +15 -0
  51. package/dist/loader/sources/directory-source.d.ts.map +1 -0
  52. package/dist/loader/sources/directory-source.js +80 -0
  53. package/dist/loader/sources/directory-source.js.map +1 -0
  54. package/dist/loader/sources/file-source.d.ts +12 -0
  55. package/dist/loader/sources/file-source.d.ts.map +1 -0
  56. package/dist/loader/sources/file-source.js +46 -0
  57. package/dist/loader/sources/file-source.js.map +1 -0
  58. package/dist/loader/sources/index.d.ts +5 -0
  59. package/dist/loader/sources/index.d.ts.map +1 -0
  60. package/dist/loader/sources/index.js +5 -0
  61. package/dist/loader/sources/index.js.map +1 -0
  62. package/dist/loader/sources/uri-source.d.ts +9 -0
  63. package/dist/loader/sources/uri-source.d.ts.map +1 -0
  64. package/dist/loader/sources/uri-source.js +42 -0
  65. package/dist/loader/sources/uri-source.js.map +1 -0
  66. package/dist/loader/validation-passes.d.ts.map +1 -1
  67. package/dist/loader/validation-passes.js +92 -28
  68. package/dist/loader/validation-passes.js.map +1 -1
  69. package/dist/naming.d.ts +15 -2
  70. package/dist/naming.d.ts.map +1 -1
  71. package/dist/naming.js +20 -6
  72. package/dist/naming.js.map +1 -1
  73. package/dist/parser-core.d.ts +17 -4
  74. package/dist/parser-core.d.ts.map +1 -1
  75. package/dist/parser-core.js +371 -44
  76. package/dist/parser-core.js.map +1 -1
  77. package/dist/parser-json.d.ts.map +1 -1
  78. package/dist/parser-json.js +10 -2
  79. package/dist/parser-json.js.map +1 -1
  80. package/dist/persistence/source/validate-source-roles.js +2 -2
  81. package/dist/persistence/source/validate-source-roles.js.map +1 -1
  82. package/dist/semantic-diff.d.ts +5 -0
  83. package/dist/semantic-diff.d.ts.map +1 -0
  84. package/dist/semantic-diff.js +49 -0
  85. package/dist/semantic-diff.js.map +1 -0
  86. package/dist/shared/meta-data.d.ts +10 -0
  87. package/dist/shared/meta-data.d.ts.map +1 -1
  88. package/dist/shared/meta-data.js +23 -0
  89. package/dist/shared/meta-data.js.map +1 -1
  90. package/dist/source.d.ts +96 -0
  91. package/dist/source.d.ts.map +1 -0
  92. package/dist/source.js +38 -0
  93. package/dist/source.js.map +1 -0
  94. package/dist/subtype-rules.js +1 -1
  95. package/dist/subtype-rules.js.map +1 -1
  96. package/dist/super-resolve.d.ts +2 -0
  97. package/dist/super-resolve.d.ts.map +1 -1
  98. package/dist/super-resolve.js +1 -1
  99. package/dist/super-resolve.js.map +1 -1
  100. package/package.json +1 -1
  101. package/src/attr-schema-validate.ts +7 -7
  102. package/src/core/export-json.ts +15 -18
  103. package/src/core/index.ts +8 -2
  104. package/src/core/parser-yaml.ts +38 -11
  105. package/src/core/yaml-desugar.ts +58 -4
  106. package/src/core/yaml-positions-walker.ts +101 -0
  107. package/src/core/yaml-positions.ts +80 -0
  108. package/src/errors.ts +57 -8
  109. package/src/index.ts +28 -3
  110. package/src/json-path.ts +46 -0
  111. package/src/loader/meta-data-loader.ts +168 -10
  112. package/src/loader/meta-data-source.ts +10 -6
  113. package/src/loader/shortcuts.ts +31 -0
  114. package/src/loader/sources/directory-source.ts +90 -0
  115. package/src/{core → loader/sources}/file-source.ts +3 -3
  116. package/src/loader/sources/index.ts +6 -0
  117. package/src/loader/sources/uri-source.ts +44 -0
  118. package/src/loader/validation-passes.ts +96 -29
  119. package/src/naming.ts +39 -7
  120. package/src/parser-core.ts +412 -46
  121. package/src/parser-json.ts +11 -2
  122. package/src/persistence/source/validate-source-roles.ts +2 -2
  123. package/src/semantic-diff.ts +48 -0
  124. package/src/shared/meta-data.ts +28 -0
  125. package/src/source.ts +99 -0
  126. package/src/subtype-rules.ts +1 -1
  127. package/src/super-resolve.ts +3 -1
  128. package/src/core/file-meta-data-loader.ts +0 -89
@@ -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, errOpts } from "./parser-core.js";
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
- { ...errOpts(opts.sourceName), code: "ERR_MALFORMED_JSON" },
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
+ }
@@ -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
  // ---------------------------------------------------------------------------
package/src/source.ts ADDED
@@ -0,0 +1,99 @@
1
+ // server/typescript/packages/metadata/src/source.ts
2
+ //
3
+ // FR5a — Loader error envelope + source-on-node (ADR-0009).
4
+ //
5
+ // Cross-port-aligned types: every metaobjects port emits the same envelope
6
+ // shape so a tool consuming errors from multiple language ports can compare
7
+ // them byte-identically.
8
+
9
+ /** Discriminated union over the provenance variants a metadata node or error
10
+ * can carry. See ADR-0009 §Decision for the canonical shape.
11
+ *
12
+ * FR5b note (finalized 2026-05-27, all four ports): buildTree-emitted errors
13
+ * from a YAML input emit `format: "yaml"` and carry the optional
14
+ * `yamlPosition` (line/col from the desugar's position map). The
15
+ * `yamlPosition` field is also declared optionally on the `format: "json"`
16
+ * variant for backward shape-compat with any FR5b-interim envelopes still
17
+ * serialized to disk; new YAML errors no longer use that variant. */
18
+ export type ErrorSource =
19
+ | { format: "json"; files: [string]; jsonPath: string;
20
+ yamlPosition?: { line: number; col: number } }
21
+ | { format: "yaml"; files: [string]; jsonPath: string;
22
+ yamlPosition?: { line: number; col: number } }
23
+ | { format: "merged"; files: string[]; jsonPath: string;
24
+ contributors: Contributor[] }
25
+ | { format: "resolved"; files: string[]; jsonPath?: string;
26
+ referrer?: string; target?: string }
27
+ | { format: "database"; dbLocation: { table: string; id: string };
28
+ jsonPath?: string }
29
+ | { format: "code"; caller?: string };
30
+
31
+ export interface Contributor {
32
+ file: string;
33
+ role: "overlay-base" | "overlay-extension" | "extends-base" | "extends-extension";
34
+ }
35
+
36
+ export interface NodeContext {
37
+ type?: string;
38
+ subtype?: string;
39
+ name?: string;
40
+ fqn?: string;
41
+ }
42
+
43
+ /** Envelope shape every loader error conforms to. */
44
+ export interface LoaderError {
45
+ // REQUIRED — conformance-enforced.
46
+ code: string;
47
+ message: string;
48
+ source: ErrorSource;
49
+ // RECOMMENDED — optional per ADR-0009 §What ports are NOT required to do.
50
+ suggestions?: string[];
51
+ fixture?: string;
52
+ node?: NodeContext;
53
+ }
54
+
55
+ /** Warning envelope — same shape as LoaderError but a `WARN_*` code. */
56
+ export interface LoaderWarning {
57
+ code: string;
58
+ message: string;
59
+ source: ErrorSource;
60
+ suggestions?: string[];
61
+ fixture?: string;
62
+ node?: NodeContext;
63
+ }
64
+
65
+ /** Canonical synthetic envelope for programmatic / test-constructed nodes.
66
+ * `caller` is an optional human label (e.g. "QueriesTest.makePost"). */
67
+ export function codeSource(caller?: string): ErrorSource {
68
+ return caller ? { format: "code", caller } : { format: "code" };
69
+ }
70
+
71
+ /** FR5d — build a `format: "resolved"` envelope from a referrer node's source
72
+ * envelope (typically a `format: "json"` or `format: "yaml"` parse-time
73
+ * source) plus the referrer FQN and unresolved target string.
74
+ *
75
+ * The resolved envelope carries:
76
+ * - `files`: the referrer's source files (so editors can jump to it),
77
+ * - `jsonPath`: the referrer's jsonPath when known (the location of the
78
+ * broken reference on disk),
79
+ * - `referrer`: the FQN of the metadata node that declared the broken
80
+ * reference (e.g. "myapp::content::Video"),
81
+ * - `target`: the unresolved reference string itself (e.g. "BaseEntity",
82
+ * "Program.weeks.invalid", "DoesNotExist").
83
+ *
84
+ * `referrerSource` may be any FR5a variant — we read its files/jsonPath
85
+ * best-effort and fall back to an empty `files: []` when the referrer's
86
+ * source is itself a `code`/`database` envelope. */
87
+ export function resolvedSource(
88
+ referrerSource: ErrorSource,
89
+ referrer: string,
90
+ target: string,
91
+ ): ErrorSource {
92
+ const files = "files" in referrerSource ? [...referrerSource.files] : [];
93
+ const jsonPath = "jsonPath" in referrerSource ? referrerSource.jsonPath : undefined;
94
+ const out: ErrorSource = { format: "resolved", files, referrer, target };
95
+ if (jsonPath !== undefined) {
96
+ (out as { jsonPath?: string }).jsonPath = jsonPath;
97
+ }
98
+ return out;
99
+ }
@@ -35,7 +35,7 @@ function walk(model: MetaData, errors: ParseError[], warnings: string[]): void {
35
35
  new ParseError(
36
36
  `value object '${model.fqn()}' must not have a primary identity ` +
37
37
  `(use subType: "entity" for records with identity)`,
38
- { code: "ERR_SUBTYPE_RULE_VIOLATION" },
38
+ { code: "ERR_SUBTYPE_RULE_VIOLATION", source: model.source },
39
39
  ),
40
40
  );
41
41
  } else if (
@@ -100,6 +100,8 @@ export interface DeferredSuperFailure {
100
100
  nodeFqn: string;
101
101
  /** The raw super ref string that didn't resolve. */
102
102
  ref: string;
103
+ /** ADR-0009 provenance envelope of the referencing node (FR5a). */
104
+ source: import("./source.js").ErrorSource;
103
105
  }
104
106
 
105
107
  /**
@@ -128,7 +130,7 @@ export function resolveDeferredSupers(root: MetaData): DeferredSuperFailure[] {
128
130
  // Frozen — ignore; the loader should resolve before freeze.
129
131
  }
130
132
  } else {
131
- failures.push({ nodeFqn: node.fqn(), ref: node.superRef });
133
+ failures.push({ nodeFqn: node.fqn(), ref: node.superRef, source: node.source });
132
134
  }
133
135
  });
134
136
  return failures;
@@ -1,89 +0,0 @@
1
- // FileMetaDataLoader — discovers file-backed MetaDataSources and runs the
2
- // MetaDataLoader pipeline over them. A UrlMetaDataLoader will slot in the
3
- // same way later.
4
-
5
- import type { Stats } from "node:fs";
6
- import { readdir, stat } from "node:fs/promises";
7
- import { join } from "node:path";
8
- import { MetaDataLoader, type LoadResult } from "../loader/meta-data-loader.js";
9
- import { FileSource } from "./file-source.js";
10
- import { parseYaml } from "./parser-yaml.js";
11
- import type { ParseOptions, ParseResult } from "../parser-core.js";
12
- import type { MetaDataSource } from "../loader/meta-data-source.js";
13
-
14
- /** Minimal glob matcher supporting `*` (any chars except `/`) and `**` (any chars). */
15
- function matchSimpleGlob(pattern: string, value: string): boolean {
16
- const regexStr = pattern
17
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
18
- .replace(/\*\*/g, "::DOUBLESTAR::")
19
- .replace(/\*/g, "[^/]*")
20
- .replace(/::DOUBLESTAR::/g, ".*");
21
- return new RegExp(`^${regexStr}$`).test(value);
22
- }
23
-
24
- export class FileMetaDataLoader extends MetaDataLoader {
25
- /** Adds YAML parsing on top of the base loader's JSON-only `parseSource`. */
26
- protected override parseSource(
27
- content: string,
28
- source: MetaDataSource,
29
- parseOpts: ParseOptions,
30
- ): ParseResult {
31
- if (source.format === "yaml") {
32
- return parseYaml(content, parseOpts);
33
- }
34
- return super.parseSource(content, source, parseOpts);
35
- }
36
-
37
- /**
38
- * Load every `.json` / `.yaml` / `.yml` file in a directory (non-recursive),
39
- * in deterministic ordinal filename order.
40
- * @param opts.exclude glob patterns (relative to dir) to skip — `*` / `**`.
41
- */
42
- async loadDirectory(dir: string, opts?: { exclude?: string[] }): Promise<LoadResult> {
43
- let entries: string[];
44
- try {
45
- // Sorted for deterministic multi-file load order: the overlay merge is
46
- // order-sensitive (last-writer-wins on attr conflicts), so the scan must
47
- // not depend on filesystem readdir order. Ordinal filename sort is
48
- // reproducible across the Java/Python/C# ports.
49
- entries = (await readdir(dir)).sort();
50
- } catch (err) {
51
- // Surface the I/O failure as a collected error via the empty-source path.
52
- const emptyResult = await this.load([]);
53
- return {
54
- ...emptyResult,
55
- errors: [
56
- new Error(`loadDirectory: cannot read ${dir}: ${(err as Error).message}`),
57
- ...emptyResult.errors,
58
- ],
59
- };
60
- }
61
-
62
- const excludes = opts?.exclude ?? [];
63
- const paths: string[] = [];
64
- for (const entry of entries) {
65
- const lower = entry.toLowerCase();
66
- if (!lower.endsWith(".json") && !lower.endsWith(".yaml") && !lower.endsWith(".yml")) {
67
- continue;
68
- }
69
- const filePath = join(dir, entry);
70
- let statResult: Stats;
71
- try {
72
- statResult = await stat(filePath);
73
- } catch {
74
- // Entry vanished between readdir and stat (TOCTOU) or is not accessible.
75
- // Skip it rather than breaking the no-throw contract of loadDirectory.
76
- continue;
77
- }
78
- if (!statResult.isFile()) continue;
79
- if (excludes.some((p) => matchSimpleGlob(p, entry))) continue;
80
- paths.push(filePath);
81
- }
82
- return this.loadFiles(paths);
83
- }
84
-
85
- /** Load an explicit list of file paths, in order. */
86
- async loadFiles(paths: string[]): Promise<LoadResult> {
87
- return this.load(paths.map((p) => new FileSource(p)));
88
- }
89
- }