@metaobjectsdev/metadata 1.0.2 → 1.0.4-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/core/parser-yaml.d.ts.map +1 -1
  2. package/dist/core/parser-yaml.js +4 -0
  3. package/dist/core/parser-yaml.js.map +1 -1
  4. package/dist/core/relationship/derive-m2m-fields.d.ts +3 -1
  5. package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -1
  6. package/dist/core/relationship/derive-m2m-fields.js +110 -15
  7. package/dist/core/relationship/derive-m2m-fields.js.map +1 -1
  8. package/dist/core/relationship/find-reference.d.ts +21 -5
  9. package/dist/core/relationship/find-reference.d.ts.map +1 -1
  10. package/dist/core/relationship/find-reference.js +29 -10
  11. package/dist/core/relationship/find-reference.js.map +1 -1
  12. package/dist/core/relationship/relationship-definition.embedded.js +8 -8
  13. package/dist/core/relationship/relationship-definition.embedded.js.map +1 -1
  14. package/dist/core/relationship/resolve-relationship-reference.d.ts +27 -0
  15. package/dist/core/relationship/resolve-relationship-reference.d.ts.map +1 -0
  16. package/dist/core/relationship/resolve-relationship-reference.js +84 -0
  17. package/dist/core/relationship/resolve-relationship-reference.js.map +1 -0
  18. package/dist/errors.d.ts +1 -1
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +31 -0
  21. package/dist/errors.js.map +1 -1
  22. package/dist/index.d.ts +8 -5
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +9 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/json-path.d.ts +14 -0
  27. package/dist/json-path.d.ts.map +1 -1
  28. package/dist/json-path.js +13 -0
  29. package/dist/json-path.js.map +1 -1
  30. package/dist/library/embedded-library.generated.d.ts +2 -0
  31. package/dist/library/embedded-library.generated.d.ts.map +1 -1
  32. package/dist/library/embedded-library.generated.js +11 -1
  33. package/dist/library/embedded-library.generated.js.map +1 -1
  34. package/dist/library/index.d.ts +2 -0
  35. package/dist/library/index.d.ts.map +1 -0
  36. package/dist/library/index.js +24 -0
  37. package/dist/library/index.js.map +1 -0
  38. package/dist/library/library-sources.d.ts +99 -6
  39. package/dist/library/library-sources.d.ts.map +1 -1
  40. package/dist/library/library-sources.js +165 -43
  41. package/dist/library/library-sources.js.map +1 -1
  42. package/dist/loader/meta-data-loader.d.ts +44 -21
  43. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  44. package/dist/loader/meta-data-loader.js +118 -88
  45. package/dist/loader/meta-data-loader.js.map +1 -1
  46. package/dist/loader/sources/file-source.d.ts +7 -1
  47. package/dist/loader/sources/file-source.d.ts.map +1 -1
  48. package/dist/loader/sources/file-source.js +3 -2
  49. package/dist/loader/sources/file-source.js.map +1 -1
  50. package/dist/loader/validation-passes.d.ts +2 -0
  51. package/dist/loader/validation-passes.d.ts.map +1 -1
  52. package/dist/loader/validation-passes.js +162 -22
  53. package/dist/loader/validation-passes.js.map +1 -1
  54. package/dist/naming.d.ts +7 -0
  55. package/dist/naming.d.ts.map +1 -1
  56. package/dist/naming.js +10 -0
  57. package/dist/naming.js.map +1 -1
  58. package/dist/parser-core.d.ts +103 -0
  59. package/dist/parser-core.d.ts.map +1 -1
  60. package/dist/parser-core.js +202 -18
  61. package/dist/parser-core.js.map +1 -1
  62. package/dist/scope.d.ts +16 -0
  63. package/dist/scope.d.ts.map +1 -0
  64. package/dist/scope.js +78 -0
  65. package/dist/scope.js.map +1 -0
  66. package/dist/serializer-json.d.ts +1 -0
  67. package/dist/serializer-json.d.ts.map +1 -1
  68. package/dist/serializer-json.js +43 -1
  69. package/dist/serializer-json.js.map +1 -1
  70. package/dist/vocabulary-rewrite-yaml.d.ts +2 -0
  71. package/dist/vocabulary-rewrite-yaml.d.ts.map +1 -0
  72. package/dist/vocabulary-rewrite-yaml.js +22 -0
  73. package/dist/vocabulary-rewrite-yaml.js.map +1 -0
  74. package/package.json +7 -7
  75. package/src/core/parser-yaml.ts +4 -0
  76. package/src/core/relationship/derive-m2m-fields.ts +115 -15
  77. package/src/core/relationship/find-reference.ts +34 -11
  78. package/src/core/relationship/relationship-definition.embedded.ts +8 -8
  79. package/src/core/relationship/resolve-relationship-reference.ts +99 -0
  80. package/src/errors.ts +31 -0
  81. package/src/index.ts +18 -5
  82. package/src/json-path.ts +16 -1
  83. package/src/library/embedded-library.generated.ts +12 -1
  84. package/src/library/index.ts +23 -0
  85. package/src/library/library-sources.ts +197 -47
  86. package/src/loader/meta-data-loader.ts +154 -102
  87. package/src/loader/sources/file-source.ts +10 -2
  88. package/src/loader/validation-passes.ts +181 -23
  89. package/src/naming.ts +11 -0
  90. package/src/parser-core.ts +319 -22
  91. package/src/scope.ts +97 -0
  92. package/src/serializer-json.ts +46 -1
  93. package/src/vocabulary-rewrite-yaml.ts +21 -0
package/src/naming.ts CHANGED
@@ -30,6 +30,17 @@ export function stripPackage(name: string | undefined): string {
30
30
  return idx === -1 ? name : name.slice(idx + PACKAGE_SEPARATOR.length);
31
31
  }
32
32
 
33
+ /**
34
+ * The package half of a resolution key — the complement of `stripPackage`
35
+ * (`"a::b::C"` → `"a::b"`; a root-level `"C"` → `""`). A resolution key is
36
+ * `package::name` for a packaged node and the bare name otherwise, so the text
37
+ * before the LAST separator is exactly the node's package.
38
+ */
39
+ export function packageOfResolutionKey(fqn: string): string {
40
+ const idx = fqn.lastIndexOf(PACKAGE_SEPARATOR);
41
+ return idx === -1 ? "" : fqn.slice(0, idx);
42
+ }
43
+
33
44
  export function toSnakeCase(s: string): string {
34
45
  return s
35
46
  .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
@@ -39,7 +39,7 @@ import {
39
39
  extendsTargetCompatible,
40
40
  EXTENDS_TARGET_MISMATCH_RULE,
41
41
  } from "./super-resolve.js";
42
- import { JsonPathBuilder } from "./json-path.js";
42
+ import { JsonPathBuilder, type Segment as JsonPathSegment } from "./json-path.js";
43
43
  import { getYamlPosition, type YamlPosition } from "./core/yaml-positions.js";
44
44
  import {
45
45
  TYPE_ATTR,
@@ -100,6 +100,64 @@ export interface ParseOptions {
100
100
  * desugar attached one, the optional `yamlPosition`.
101
101
  */
102
102
  sourceFormat?: "json" | "yaml";
103
+ /**
104
+ * ADR-0055 — if true, `overlay: true` declarations are QUEUED as they are met
105
+ * rather than applied during the walk, and handed back as
106
+ * {@link ParseResult.pendingOverlays} for the loader to apply once every
107
+ * source has been parsed. Exactly the shape `deferSuperResolution` uses for
108
+ * `extends`, and for the same reason: the target may be declared in a source
109
+ * parsed later, or later in this same document.
110
+ *
111
+ * When absent, `buildTree` drains its own queue before returning — so a
112
+ * standalone `parseJson`/`parseYaml` call is order-independent within its one
113
+ * document. There is no eager path: queue-then-drain is the only door.
114
+ */
115
+ deferOverlays?: boolean;
116
+ }
117
+
118
+ /**
119
+ * ADR-0055 — one `overlay: true` declaration met during the walk and deferred.
120
+ *
121
+ * A deferred super hangs on its node as `model.superRef`; a deferred overlay has
122
+ * NO node — nothing was created — so everything the parser would have used has
123
+ * to travel out with it, including the module-level walk state needed to report
124
+ * an error against the declaration's own location long after the walk unwound.
125
+ */
126
+ export interface PendingOverlay {
127
+ /** Wrapper-key type and subType. The lookup is by (type, name); subType is
128
+ * carried for diagnostics only — it is never consulted when matching. */
129
+ readonly type: string;
130
+ readonly subType: string;
131
+ readonly name: string;
132
+ /** The declaration body, untouched. `parseNodeInto` consumes it at
133
+ * application time; for YAML input it still carries the desugar's
134
+ * position-by-key map, so nested children keep correct positions. */
135
+ readonly nodeData: Record<string, unknown>;
136
+ /** The node the target is sought under — the accumulating root for a
137
+ * top-level overlay, the enclosing PLAIN node for a nested one. A live
138
+ * reference: the tree is mutated in place and nodes are never replaced. */
139
+ readonly parent: MetaData;
140
+ /** Accumulating root, for super resolution of anything the overlay adds. */
141
+ readonly accumRoot: MetaData;
142
+ /** Effective context package at the declaration site — needed for the
143
+ * package-qualified root lookup and for package inheritance of new children. */
144
+ readonly inheritedContextPkg: string;
145
+ /** `opts.sourceName` as passed to buildTree (may be undefined). */
146
+ readonly sourceName: string | undefined;
147
+ /** The resolved source id used in envelopes (`sourceName ?? "<unknown>"`). */
148
+ readonly sourceId: string;
149
+ /** The `path` string parseNodeInto receives for diagnostics. */
150
+ readonly path: string;
151
+ /** ADR-0009 parse-time envelope of the declaration itself — the `files` and
152
+ * `jsonPath` the eventual resolved error carries. */
153
+ readonly errorSource: ErrorSource;
154
+ /** JSONPath stack at queue time, so the module-level builder can be
155
+ * re-seeded before re-entering the walk. */
156
+ readonly pathSegments: readonly JsonPathSegment[];
157
+ /** FR5b — source format discriminant at queue time. */
158
+ readonly format: "json" | "yaml";
159
+ /** FR5b — the declaration's own YAML position, when the desugar had one. */
160
+ readonly yamlPosition?: YamlPosition;
103
161
  }
104
162
 
105
163
  export interface ParseResult {
@@ -114,6 +172,15 @@ export interface ParseResult {
114
172
  * `code` + `source` and are surfaced unchanged. Defaults to `[]`.
115
173
  */
116
174
  envelopeWarnings: LoaderWarning[];
175
+ /**
176
+ * ADR-0055 — `overlay: true` declarations queued during this parse, in
177
+ * encounter order. Empty unless {@link ParseOptions.deferOverlays} was set;
178
+ * when it was not, buildTree already drained them. Because sources are parsed
179
+ * sequentially and the walk is pre-order, encounter order IS "source order,
180
+ * then declaration order within a source" — no sort is needed and stability
181
+ * is by construction.
182
+ */
183
+ pendingOverlays: PendingOverlay[];
117
184
  }
118
185
 
119
186
  // ---------------------------------------------------------------------------
@@ -346,8 +413,14 @@ function splitTypeKey(key: string, registry: TypeRegistry): SplitKey {
346
413
  * - Absolute path (::foo::bar) → prepended with base: "acme" + "::foo" → "acme::foo::bar"
347
414
  * - Relative parent (..) → handled in super resolution, not here
348
415
  * - No leading :: → used as-is
416
+ *
417
+ * Exported so `declaredTopLevelKeys` (meta-data-loader.ts) — the structural
418
+ * pre-parse walk that must produce the SAME resolution key `rootChildResolutionKey`
419
+ * below computes — reuses this rather than reimplementing it. A second copy is
420
+ * exactly how the two silently disagreed on a relative (`::`-prefixed) `package`
421
+ * before task 17's fix-round-1 caught it against a real fixture.
349
422
  */
350
- function expandPackageForPath(basePkg: string, pkgPath: string): string {
423
+ export function expandPackageForPath(basePkg: string, pkgPath: string): string {
351
424
  if (basePkg.trim() === "" || !pkgPath.startsWith(PACKAGE_SEPARATOR)) {
352
425
  return pkgPath;
353
426
  }
@@ -415,6 +488,10 @@ let _currentSourceId: string | undefined;
415
488
  let _currentFormat: "json" | "yaml" = "json";
416
489
  let _currentYamlPosition: YamlPosition | undefined;
417
490
 
491
+ // ADR-0055 — sink for overlay declarations deferred out of the walk. Set at
492
+ // buildTree entry; same synchronous-buildTree reentrancy argument as the others.
493
+ let _pendingOverlays: PendingOverlay[] | undefined;
494
+
418
495
  /** FR5a/FR5b — stamp the source-provenance envelope on a freshly-created
419
496
  * node. No-op when invoked outside buildTree's setup (defensive — the
420
497
  * module-level state will always be populated during a normal parse).
@@ -457,6 +534,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
457
534
  const warnings: string[] = [];
458
535
  const errors: ParseError[] = [];
459
536
  const envelopeWarnings: LoaderWarning[] = [];
537
+ const pendingOverlays: PendingOverlay[] = [];
460
538
  const strict = opts.strict ?? false;
461
539
  const source = opts.sourceName;
462
540
  _deferSuperResolution = opts.deferSuperResolution === true;
@@ -465,6 +543,25 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
465
543
  // emit envelope warnings without threading another parameter through the
466
544
  // entire walk. Safe because buildTree is fully synchronous.
467
545
  _currentEnvelopeWarnings = envelopeWarnings;
546
+ // ADR-0055 — overlay declarations are queued here as the walk meets them.
547
+ _pendingOverlays = pendingOverlays;
548
+
549
+ /** ADR-0055 — a caller that is not deferring gets its own queue drained here,
550
+ * so "queue, then apply" is the only path through the parser and a single
551
+ * document is order-independent on its own. */
552
+ const finishParse = (r: MetaRoot): ParseResult => {
553
+ if (opts.deferOverlays !== true && pendingOverlays.length > 0) {
554
+ const drained = applyPendingOverlays(pendingOverlays, {
555
+ registry: opts.registry,
556
+ strict,
557
+ });
558
+ errors.push(...drained.errors);
559
+ warnings.push(...drained.warnings);
560
+ envelopeWarnings.push(...drained.envelopeWarnings);
561
+ pendingOverlays.length = 0;
562
+ }
563
+ return { root: r, warnings, errors, envelopeWarnings, pendingOverlays };
564
+ };
468
565
  // FR5a — start a fresh JSONPath stack rooted at "$"; sourceId is the
469
566
  // source's id (from FileSource / InMemoryStringSource via opts.sourceName).
470
567
  // Falls back to "<unknown>" when no name was supplied (e.g. ad-hoc parseJson
@@ -596,7 +693,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
596
693
  rootKey,
597
694
  );
598
695
  _currentPath!.pop();
599
- return { root: opts.intoRoot, warnings, errors, envelopeWarnings };
696
+ return finishParse(opts.intoRoot);
600
697
  }
601
698
 
602
699
  // --- Fresh root mode: create a new root from the JSON ---
@@ -621,7 +718,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
621
718
  rootKey,
622
719
  ) as MetaRoot;
623
720
  _currentPath!.pop();
624
- return { root, warnings, errors, envelopeWarnings };
721
+ return finishParse(root);
625
722
  } finally {
626
723
  _deferSuperResolution = false;
627
724
  _currentErrors = undefined;
@@ -630,6 +727,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
630
727
  _currentSourceId = undefined;
631
728
  _currentFormat = "json";
632
729
  _currentYamlPosition = undefined;
730
+ _pendingOverlays = undefined;
633
731
  }
634
732
  }
635
733
 
@@ -847,7 +945,18 @@ function parseNodeInto(
847
945
  // "merged"` envelope. Last-writer-wins is preserved for non-conflicting
848
946
  // cases (one side unset, same value, etc.) — those carry through to the
849
947
  // existing applyInlineAttrsAndUnknownKeys logic below.
850
- if (fr5cActive && preMergeAttrSnapshot !== undefined) {
948
+ //
949
+ // FR-043 Amendment 2 — `overlay: true` LICENSES the override. The conflict error
950
+ // exists to catch two files that collided without knowing about each other; the flag
951
+ // is the author saying "I know about the other declaration and I mean to change it".
952
+ // The loader already treats the flag specially (find-or-throw versus create-or-find),
953
+ // so honouring it here makes it mean ONE thing rather than two. An unmarked
954
+ // redeclaration still merges and still errors, which is the case FR5c was written for.
955
+ if (
956
+ fr5cActive &&
957
+ preMergeAttrSnapshot !== undefined &&
958
+ nodeData[RESERVED_KEY_OVERLAY] !== true
959
+ ) {
851
960
  detectAttrMergeConflicts(
852
961
  target,
853
962
  nodeData,
@@ -939,7 +1048,9 @@ function parseNodeInto(
939
1048
  * non-empty value. If so, emit ERR_MERGE_CONFLICT with a `format: "merged"`
940
1049
  * envelope naming both contributors. The merge itself proceeds (existing
941
1050
  * last-writer-wins) so the loader sees one canonical tree; the error
942
- * surfaces the conflict so a consumer can fix the metadata. */
1051
+ * surfaces the conflict so a consumer can fix the metadata.
1052
+ *
1053
+ * NOT called for an `overlay: true` declaration — see the call site. */
943
1054
  function detectAttrMergeConflicts(
944
1055
  target: MetaData,
945
1056
  nodeData: Record<string, unknown>,
@@ -1070,6 +1181,100 @@ function createOrFindMetaData(
1070
1181
  // merge pair (mirrors the Java parser, which searches root children by
1071
1182
  // "pkg::name"). Nested children stay bare-name matched — they are scoped
1072
1183
  // by their parent, and packages don't disambiguate siblings inside a node.
1184
+ if (isOverlayNode) {
1185
+ // ADR-0055 — an overlay is ALWAYS queued, never applied during the walk, and
1186
+ // deliberately not conditioned on whether its target happens to exist yet.
1187
+ //
1188
+ // Applying it when the base is already present and queueing only on a miss is
1189
+ // the "retry-on-miss" variant the ADR rejected: it would leave output
1190
+ // dependent on whether a base had been parsed yet, which is the fragility
1191
+ // being removed. G1 — every plain declaration, from every source, precedes
1192
+ // every overlay — only holds if the queue is unconditional.
1193
+ //
1194
+ // This node is the OUTERMOST overlay on this branch and we do NOT descend
1195
+ // into it, so its whole subtree — nested overlays included — rides along and
1196
+ // is applied as one unit (G3). Returning undefined is what keeps the caller
1197
+ // from addChild-ing a node that was never created; the overlay contributes
1198
+ // nothing until the drain.
1199
+ {
1200
+ if (_pendingOverlays !== undefined) {
1201
+ _pendingOverlays.push({
1202
+ type,
1203
+ subType,
1204
+ name,
1205
+ nodeData,
1206
+ parent,
1207
+ accumRoot,
1208
+ inheritedContextPkg,
1209
+ sourceName: source,
1210
+ sourceId: _currentSourceId ?? "<unknown>",
1211
+ path,
1212
+ errorSource: errSource(),
1213
+ pathSegments: _currentPath?.snapshot() ?? [],
1214
+ format: _currentFormat,
1215
+ ...(_currentYamlPosition !== undefined ? { yamlPosition: _currentYamlPosition } : {}),
1216
+ });
1217
+ return undefined;
1218
+ }
1219
+ // No queue means we are already INSIDE applyPendingOverlays, applying a
1220
+ // queued unit. A nested overlay within that unit resolves find-or-fail
1221
+ // right here (§2.2): by now its parent is complete, and there is no later
1222
+ // pass left to defer to. This is also the door a caller outside any
1223
+ // buildTree run would take.
1224
+ const nested = findOverlayTarget(parent, type, name, nodeData, inheritedContextPkg);
1225
+ if (nested === undefined) {
1226
+ errors.push(
1227
+ new ParseError(overlayNoTargetMessage(type, name), {
1228
+ code: "ERR_OVERLAY_NO_TARGET",
1229
+ source: errSource(),
1230
+ }),
1231
+ );
1232
+ return undefined;
1233
+ }
1234
+ nested.setIsMerge(true);
1235
+ parseNodeInto(nodeData, nested, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
1236
+ return nested;
1237
+ }
1238
+ }
1239
+
1240
+ // Only the non-overlay path needs the target here; a queued overlay resolves
1241
+ // its own target at application time, against the completed tree.
1242
+ const existing = findOverlayTarget(parent, type, name, nodeData, inheritedContextPkg);
1243
+
1244
+ // Default: no operator → silently reuse existing or create new.
1245
+ if (existing !== undefined) {
1246
+ parseNodeInto(nodeData, existing, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
1247
+ return existing;
1248
+ }
1249
+
1250
+ // Not found (or unnamed) → create new
1251
+ return parseNodeFresh(type, subType, nodeData, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path, parent.type, parent);
1252
+ }
1253
+
1254
+ // ---------------------------------------------------------------------------
1255
+ // ADR-0055 — deferred overlay application.
1256
+ // ---------------------------------------------------------------------------
1257
+
1258
+ /** The node an `overlay: true` declaration re-opens, or undefined.
1259
+ *
1260
+ * ADR-0039 sanctioned own read: an overlay targets the AUTHORED declaration
1261
+ * layer (an own child), never the resolved/inherited view.
1262
+ *
1263
+ * ROOT-LEVEL lookups are PACKAGE-QUALIFIED: two files declaring the same
1264
+ * (type, name) under different packages are DISTINCT root nodes, never a merge
1265
+ * pair (mirrors the Java parser, which searches root children by "pkg::name").
1266
+ * Nested children stay bare-name matched — they are scoped by their parent, and
1267
+ * packages do not disambiguate siblings inside a node.
1268
+ *
1269
+ * One implementation, shared by the walk and by {@link applyPendingOverlays},
1270
+ * so "what does this overlay target" cannot drift between the two. */
1271
+ function findOverlayTarget(
1272
+ parent: MetaData,
1273
+ type: string,
1274
+ name: string,
1275
+ nodeData: Record<string, unknown>,
1276
+ inheritedContextPkg: string,
1277
+ ): MetaData | undefined {
1073
1278
  let existing = name !== "" ? parent.ownChildByTypeAndName(type, name) : undefined;
1074
1279
  if (existing !== undefined && parent instanceof MetaRoot) {
1075
1280
  const candidateKey = rootChildResolutionKey(nodeData, inheritedContextPkg, name);
@@ -1081,27 +1286,119 @@ function createOrFindMetaData(
1081
1286
  .find((c) => c.type === type && c.name === name && c.resolutionKey() === candidateKey);
1082
1287
  }
1083
1288
  }
1289
+ return existing;
1290
+ }
1084
1291
 
1085
- if (isOverlayNode) {
1086
- if (existing === undefined) {
1087
- throw new ParseError(
1088
- `Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`,
1089
- { code: "ERR_OVERLAY_NO_TARGET", source: errSource() },
1090
- );
1091
- }
1092
- existing.setIsMerge(true);
1093
- parseNodeInto(nodeData, existing, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
1094
- return existing;
1292
+ /** The one wording for a missing overlay target, shared by the defensive
1293
+ * in-walk path and the deferred pass, so the two cannot drift. */
1294
+ function overlayNoTargetMessage(type: string, name: string): string {
1295
+ return `Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`;
1296
+ }
1297
+
1298
+ /** The declaration's own address, for the resolved envelope's `referrer`. */
1299
+ function overlayReferrer(item: PendingOverlay): string {
1300
+ if (item.parent instanceof MetaRoot) {
1301
+ return rootChildResolutionKey(item.nodeData, item.inheritedContextPkg, item.name);
1095
1302
  }
1303
+ // ADR-0029 addressing — a nested overlay is named relative to its parent.
1304
+ return `${item.parent.name}.${item.name}`;
1305
+ }
1096
1306
 
1097
- // Default: no operator → silently reuse existing or create new.
1098
- if (existing !== undefined) {
1099
- parseNodeInto(nodeData, existing, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
1100
- return existing;
1307
+ /**
1308
+ * ADR-0055 apply queued `overlay: true` declarations against the complete tree.
1309
+ *
1310
+ * Called once, after every source has been parsed and BEFORE deferred super
1311
+ * resolution (G5), with every source's queue concatenated in parse order — which
1312
+ * is already "source order, then declaration order within a source" (G2).
1313
+ *
1314
+ * Each element is applied independently: a missing target is recorded and the
1315
+ * element skipped, so one bad overlay no longer takes its whole source down with
1316
+ * it (the eager throw aborted the entire document, losing every sibling
1317
+ * declaration and cascading into ERR_UNRESOLVED_SUPER).
1318
+ */
1319
+ export function applyPendingOverlays(
1320
+ pending: readonly PendingOverlay[],
1321
+ opts: { registry: TypeRegistry; strict?: boolean },
1322
+ ): { errors: ParseError[]; warnings: string[]; envelopeWarnings: LoaderWarning[] } {
1323
+ const errors: ParseError[] = [];
1324
+ const warnings: string[] = [];
1325
+ const envelopeWarnings: LoaderWarning[] = [];
1326
+ const strict = opts.strict ?? false;
1327
+
1328
+ for (const item of pending) {
1329
+ try {
1330
+ // Re-enter the walk state this declaration was queued under. The walk that
1331
+ // built it has unwound, so without this anything constructed now would be
1332
+ // stamped with the wrong provenance and an error would name the wrong
1333
+ // location (or none at all).
1334
+ _currentPath = JsonPathBuilder.fromSegments(item.pathSegments);
1335
+ _currentSourceId = item.sourceId;
1336
+ _currentFormat = item.format;
1337
+ _currentYamlPosition = item.yamlPosition;
1338
+ _currentErrors = errors;
1339
+ _currentEnvelopeWarnings = envelopeWarnings;
1340
+ // Anything the overlay contributes may `extends` a node in any source; the
1341
+ // loader resolves every ref after this pass.
1342
+ _deferSuperResolution = true;
1343
+
1344
+ const target = findOverlayTarget(
1345
+ item.parent,
1346
+ item.type,
1347
+ item.name,
1348
+ item.nodeData,
1349
+ item.inheritedContextPkg,
1350
+ );
1351
+ if (target === undefined) {
1352
+ errors.push(
1353
+ new ParseError(overlayNoTargetMessage(item.type, item.name), {
1354
+ code: "ERR_OVERLAY_NO_TARGET",
1355
+ // ADR-0009 FR5d — a reference that did not resolve, reported with the
1356
+ // declaration's own files/jsonPath.
1357
+ source: resolvedSource(
1358
+ item.errorSource,
1359
+ overlayReferrer(item),
1360
+ `${item.type}:${item.name}`,
1361
+ ),
1362
+ }),
1363
+ );
1364
+ continue; // parseNodeInto was never entered — no partial state to unwind
1365
+ }
1366
+ target.setIsMerge(true);
1367
+ parseNodeInto(
1368
+ item.nodeData,
1369
+ target,
1370
+ item.accumRoot,
1371
+ item.inheritedContextPkg,
1372
+ opts.registry,
1373
+ warnings,
1374
+ errors,
1375
+ strict,
1376
+ item.sourceName,
1377
+ item.path,
1378
+ );
1379
+ } catch (err) {
1380
+ // Per-element, as the loader already does per-source: a strict-mode
1381
+ // reportProblem or a registry error must not abandon the remaining queue.
1382
+ errors.push(
1383
+ err instanceof ParseError
1384
+ ? err
1385
+ : new ParseError(
1386
+ `Failed to apply overlay for [${item.type}:${item.name}]: ${String(err)}`,
1387
+ { code: "ERR_UNKNOWN", source: item.errorSource },
1388
+ ),
1389
+ );
1390
+ } finally {
1391
+ _currentPath = undefined;
1392
+ _currentSourceId = undefined;
1393
+ _currentFormat = "json";
1394
+ _currentYamlPosition = undefined;
1395
+ _currentErrors = undefined;
1396
+ _currentEnvelopeWarnings = undefined;
1397
+ _deferSuperResolution = false;
1398
+ }
1101
1399
  }
1102
1400
 
1103
- // Not found (or unnamed) → create new
1104
- return parseNodeFresh(type, subType, nodeData, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path, parent.type, parent);
1401
+ return { errors, warnings, envelopeWarnings };
1105
1402
  }
1106
1403
 
1107
1404
  // ---------------------------------------------------------------------------
package/src/scope.ts ADDED
@@ -0,0 +1,97 @@
1
+ // server/typescript/packages/metadata/src/scope.ts
2
+ //
3
+ // FR-023 §4.3 — the scope-pattern grammar. Moved here from
4
+ // `@metaobjectsdev/sdk` so `codegen-ts`'s publisher generator
5
+ // (`sharedModelFile()`) can select its exports with the same patterns
6
+ // without taking a dependency on sdk. `@metaobjectsdev/sdk` re-exports
7
+ // `compileScope` / `matchesScope` / `Scope` / `CompiledScope` from here
8
+ // unchanged, so existing importers of the scope API from sdk keep working.
9
+ //
10
+ // A pure, no-I/O module deciding whether a fully-qualified node name falls
11
+ // inside a consumer's declared `include`/`exclude` scope. Source resolution
12
+ // and discovery (later phase-1 tasks) build on this; a cross-language
13
+ // conformance corpus pins its semantics, so exact pattern behavior matters.
14
+ //
15
+ // Uses no `node:` imports — stays browser-safe like the rest of the root
16
+ // entry (see `test/browser-safety.test.ts`).
17
+ import { PACKAGE_SEPARATOR } from "./shared/structural.js";
18
+ import { ParseError } from "./errors.js";
19
+ import { codeSource } from "./source.js";
20
+
21
+ /** A consumer-side output filter over fully-qualified node names. */
22
+ export interface Scope {
23
+ /** Absent or empty means "everything". */
24
+ readonly include?: readonly string[];
25
+ /** Applied after `include`. */
26
+ readonly exclude?: readonly string[];
27
+ }
28
+
29
+ export interface CompiledScope {
30
+ readonly include: readonly RegExp[];
31
+ readonly exclude: readonly RegExp[];
32
+ }
33
+
34
+ /** One package segment: any run of characters containing no separator char. */
35
+ const SEGMENT = "[^:]+";
36
+ /** One or more segments, separator-joined — the `**` expansion. */
37
+ const SEGMENTS = `${SEGMENT}(?:${PACKAGE_SEPARATOR}${SEGMENT})*`;
38
+
39
+ function escapeLiteral(text: string): string {
40
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
41
+ }
42
+
43
+ /** Compile one segment. `**` spans segments; `*` never crosses a separator. */
44
+ function compileSegment(segment: string, pattern: string): string {
45
+ if (segment.length === 0) {
46
+ throw new ParseError(`empty segment in scope pattern "${pattern}"`, {
47
+ code: "ERR_SCOPE_PATTERN_INVALID",
48
+ source: codeSource("compileSegment"),
49
+ });
50
+ }
51
+ // A segment surviving the split on the two-character PACKAGE_SEPARATOR
52
+ // ("::") can still contain a lone ":" when the pattern has an odd colon
53
+ // run — e.g. "acme:::Order".split("::") => ["acme", ":Order"]. SEGMENT
54
+ // ([^:]+) already excludes ":" from a well-formed segment, so a leftover
55
+ // ":" here means the separator was malformed, not that ":" is meant
56
+ // literally. Left unchecked, escapeLiteral treats it as a literal
57
+ // character and compiles a regex requiring three colons in a row — which
58
+ // no legal "::"-joined fully-qualified name can ever contain, so the
59
+ // pattern silently matches nothing instead of failing loud.
60
+ if (segment.includes(":")) {
61
+ throw new ParseError(
62
+ `scope pattern "${pattern}" has a malformed separator (an odd run of ":") — segments are joined by "::", never a single ":"`,
63
+ { code: "ERR_SCOPE_PATTERN_INVALID", source: codeSource("compileSegment") },
64
+ );
65
+ }
66
+ if (segment === "**") return `(?:${SEGMENTS})`;
67
+ // `*` inside a segment matches any characters except the separator char.
68
+ return segment.split("*").map(escapeLiteral).join("[^:]*");
69
+ }
70
+
71
+ export function compilePattern(pattern: string): RegExp {
72
+ if (pattern.length === 0) {
73
+ throw new ParseError(`scope pattern must not be empty`, {
74
+ code: "ERR_SCOPE_PATTERN_INVALID",
75
+ source: codeSource("compilePattern"),
76
+ });
77
+ }
78
+ const body = pattern
79
+ .split(PACKAGE_SEPARATOR)
80
+ .map((segment) => compileSegment(segment, pattern))
81
+ .join(PACKAGE_SEPARATOR);
82
+ return new RegExp(`^${body}$`);
83
+ }
84
+
85
+ export function compileScope(scope: Scope): CompiledScope {
86
+ return {
87
+ include: (scope.include ?? []).map(compilePattern),
88
+ exclude: (scope.exclude ?? []).map(compilePattern),
89
+ };
90
+ }
91
+
92
+ /** True when `fqn` is inside the scope. An empty `include` means everything. */
93
+ export function matchesScope(fqn: string, compiled: CompiledScope): boolean {
94
+ const included = compiled.include.length === 0 || compiled.include.some((re) => re.test(fqn));
95
+ if (!included) return false;
96
+ return !compiled.exclude.some((re) => re.test(fqn));
97
+ }
@@ -32,7 +32,8 @@ import {
32
32
  DEFAULT_SOURCE_KIND,
33
33
  PHYSICAL_NAME_ATTR_BY_KIND,
34
34
  } from "./persistence/source/source-constants.js";
35
- import { TYPE_SOURCE } from "./shared/base-types.js";
35
+ import { TYPE_METADATA, TYPE_SOURCE, SUBTYPE_ROOT } from "./shared/base-types.js";
36
+ import { packageOfResolutionKey } from "./naming.js";
36
37
 
37
38
  const SOURCE_RDB_FUSED_KEY = `${TYPE_SOURCE}${TYPE_SUBTYPE_SEPARATOR}${SOURCE_SUBTYPE_RDB}`;
38
39
 
@@ -301,3 +302,47 @@ function sortAttrValue(value: unknown): unknown {
301
302
  }
302
303
  return value;
303
304
  }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // serializeSharedDocument — the FR-023 shared-model artifact form
308
+ //
309
+ // One canonical-JSON `metadata.root` document holding top-level nodes from any
310
+ // number of packages: NO root `package`, and every top-level node carries its own
311
+ // explicit `package` (a root-level child may name its package — ADR-0029's
312
+ // addressing model — so the document re-loads to the same resolution keys in
313
+ // every port). Each node is its canonicalSerialize form: raw own-layer, `extends`
314
+ // preserved (not flattened), attribute keys alphabetized, the FR-016 physical-name
315
+ // rewrite applied. Top-level nodes are sorted by resolution key; each node's
316
+ // children keep their authored order. Body key order: name, package, then the
317
+ // canonical rest. Byte-identical to Python's `serialize_shared_document`.
318
+ // ---------------------------------------------------------------------------
319
+
320
+ export function serializeSharedDocument(nodes: readonly MetaData[]): string {
321
+ const sorted = [...nodes].sort((a, b) => {
322
+ const ka = a.resolutionKey();
323
+ const kb = b.resolutionKey();
324
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
325
+ });
326
+ const children = sorted.map((node) => {
327
+ const pkg = packageOfResolutionKey(node.resolutionKey());
328
+ if (pkg === "") {
329
+ throw new Error(
330
+ `serializeSharedDocument: ${node.resolutionKey()} has no package; a shared document carries only packaged nodes`,
331
+ );
332
+ }
333
+ const parsed = JSON.parse(canonicalSerialize(node)) as Record<string, Record<string, unknown>>;
334
+ const [fused, body] = Object.entries(parsed)[0]!;
335
+ // The node's own `package` (if it declared one) is replaced by the RESOLVED
336
+ // one: a node inheriting its file's root package declares none of its own.
337
+ const ordered: Record<string, unknown> = {
338
+ [RESERVED_KEY_NAME]: body[RESERVED_KEY_NAME],
339
+ [RESERVED_KEY_PACKAGE]: pkg,
340
+ };
341
+ for (const [key, value] of Object.entries(body)) {
342
+ if (key !== RESERVED_KEY_NAME && key !== RESERVED_KEY_PACKAGE) ordered[key] = value;
343
+ }
344
+ return { [fused]: ordered };
345
+ });
346
+ const doc = { [fusedKey(TYPE_METADATA, SUBTYPE_ROOT)]: { [RESERVED_KEY_CHILDREN]: children } };
347
+ return JSON.stringify(doc, null, 2) + "\n";
348
+ }
@@ -0,0 +1,21 @@
1
+ // @metaobjectsdev/metadata/vocabulary-rewrite-yaml — the YAML arm of `meta upgrade`.
2
+ //
3
+ // The implementation stays under `core/`, beside the canonical-JSON rewriter it mirrors.
4
+ // This file exists so the SUBPATH NAME mirrors the layout under `src/`, which is what
5
+ // `tsconfig.scripts.json`'s `@metaobjectsdev/metadata/*` → `src/*` mapping requires.
6
+ //
7
+ // Nesting is NOT the problem — the `*` matches across `/`, which is why
8
+ // `@metaobjectsdev/codegen-ts/templates/entity-file` resolves to
9
+ // `src/templates/entity-file.ts` perfectly well. The problem was a subpath NAMED
10
+ // `vocabulary-rewrite-yaml` whose source sat at `src/core/vocabulary-rewrite-yaml.ts`:
11
+ // the substitution yields `src/vocabulary-rewrite-yaml`, and that is not where the module
12
+ // is. Renaming the export to `./core/vocabulary-rewrite-yaml` would also have satisfied
13
+ // the rule and was the cheaper edit, but this package is published and that subpath is
14
+ // public API, so the name stays and the layout moves to meet it.
15
+ //
16
+ // When the mapping misses, tsc falls through to `node_modules` and reads `dist/` — present
17
+ // for anyone who has built, absent on a fresh CI checkout where the `gates` lane runs
18
+ // `bun install` and never builds. `./library` shipped that way and took the lane down;
19
+ // this one had not been imported from anywhere `scripts/` typechecks, so it was latent.
20
+ // `cli/test/subpath-resolves-under-scripts-paths.test.ts` now gates the class.
21
+ export { rewriteYamlDocument, type YamlRewriteResult } from "./core/vocabulary-rewrite-yaml.js";