@metaobjectsdev/metadata 1.0.1 → 1.0.3

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 (50) 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/errors.d.ts +1 -1
  5. package/dist/errors.d.ts.map +1 -1
  6. package/dist/errors.js +21 -0
  7. package/dist/errors.js.map +1 -1
  8. package/dist/index.d.ts +6 -4
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +7 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/json-path.d.ts +14 -0
  13. package/dist/json-path.d.ts.map +1 -1
  14. package/dist/json-path.js +13 -0
  15. package/dist/json-path.js.map +1 -1
  16. package/dist/loader/meta-data-loader.d.ts +44 -21
  17. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  18. package/dist/loader/meta-data-loader.js +112 -87
  19. package/dist/loader/meta-data-loader.js.map +1 -1
  20. package/dist/loader/sources/file-source.d.ts +7 -1
  21. package/dist/loader/sources/file-source.d.ts.map +1 -1
  22. package/dist/loader/sources/file-source.js +3 -2
  23. package/dist/loader/sources/file-source.js.map +1 -1
  24. package/dist/naming.d.ts +7 -0
  25. package/dist/naming.d.ts.map +1 -1
  26. package/dist/naming.js +10 -0
  27. package/dist/naming.js.map +1 -1
  28. package/dist/parser-core.d.ts +103 -0
  29. package/dist/parser-core.d.ts.map +1 -1
  30. package/dist/parser-core.js +189 -16
  31. package/dist/parser-core.js.map +1 -1
  32. package/dist/scope.d.ts +16 -0
  33. package/dist/scope.d.ts.map +1 -0
  34. package/dist/scope.js +78 -0
  35. package/dist/scope.js.map +1 -0
  36. package/dist/serializer-json.d.ts +1 -0
  37. package/dist/serializer-json.d.ts.map +1 -1
  38. package/dist/serializer-json.js +43 -1
  39. package/dist/serializer-json.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/core/parser-yaml.ts +4 -0
  42. package/src/errors.ts +21 -0
  43. package/src/index.ts +12 -4
  44. package/src/json-path.ts +16 -1
  45. package/src/loader/meta-data-loader.ts +147 -101
  46. package/src/loader/sources/file-source.ts +10 -2
  47. package/src/naming.ts +11 -0
  48. package/src/parser-core.ts +304 -20
  49. package/src/scope.ts +97 -0
  50. package/src/serializer-json.ts +46 -1
@@ -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
 
@@ -1070,6 +1168,100 @@ function createOrFindMetaData(
1070
1168
  // merge pair (mirrors the Java parser, which searches root children by
1071
1169
  // "pkg::name"). Nested children stay bare-name matched — they are scoped
1072
1170
  // by their parent, and packages don't disambiguate siblings inside a node.
1171
+ if (isOverlayNode) {
1172
+ // ADR-0055 — an overlay is ALWAYS queued, never applied during the walk, and
1173
+ // deliberately not conditioned on whether its target happens to exist yet.
1174
+ //
1175
+ // Applying it when the base is already present and queueing only on a miss is
1176
+ // the "retry-on-miss" variant the ADR rejected: it would leave output
1177
+ // dependent on whether a base had been parsed yet, which is the fragility
1178
+ // being removed. G1 — every plain declaration, from every source, precedes
1179
+ // every overlay — only holds if the queue is unconditional.
1180
+ //
1181
+ // This node is the OUTERMOST overlay on this branch and we do NOT descend
1182
+ // into it, so its whole subtree — nested overlays included — rides along and
1183
+ // is applied as one unit (G3). Returning undefined is what keeps the caller
1184
+ // from addChild-ing a node that was never created; the overlay contributes
1185
+ // nothing until the drain.
1186
+ {
1187
+ if (_pendingOverlays !== undefined) {
1188
+ _pendingOverlays.push({
1189
+ type,
1190
+ subType,
1191
+ name,
1192
+ nodeData,
1193
+ parent,
1194
+ accumRoot,
1195
+ inheritedContextPkg,
1196
+ sourceName: source,
1197
+ sourceId: _currentSourceId ?? "<unknown>",
1198
+ path,
1199
+ errorSource: errSource(),
1200
+ pathSegments: _currentPath?.snapshot() ?? [],
1201
+ format: _currentFormat,
1202
+ ...(_currentYamlPosition !== undefined ? { yamlPosition: _currentYamlPosition } : {}),
1203
+ });
1204
+ return undefined;
1205
+ }
1206
+ // No queue means we are already INSIDE applyPendingOverlays, applying a
1207
+ // queued unit. A nested overlay within that unit resolves find-or-fail
1208
+ // right here (§2.2): by now its parent is complete, and there is no later
1209
+ // pass left to defer to. This is also the door a caller outside any
1210
+ // buildTree run would take.
1211
+ const nested = findOverlayTarget(parent, type, name, nodeData, inheritedContextPkg);
1212
+ if (nested === undefined) {
1213
+ errors.push(
1214
+ new ParseError(overlayNoTargetMessage(type, name), {
1215
+ code: "ERR_OVERLAY_NO_TARGET",
1216
+ source: errSource(),
1217
+ }),
1218
+ );
1219
+ return undefined;
1220
+ }
1221
+ nested.setIsMerge(true);
1222
+ parseNodeInto(nodeData, nested, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
1223
+ return nested;
1224
+ }
1225
+ }
1226
+
1227
+ // Only the non-overlay path needs the target here; a queued overlay resolves
1228
+ // its own target at application time, against the completed tree.
1229
+ const existing = findOverlayTarget(parent, type, name, nodeData, inheritedContextPkg);
1230
+
1231
+ // Default: no operator → silently reuse existing or create new.
1232
+ if (existing !== undefined) {
1233
+ parseNodeInto(nodeData, existing, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
1234
+ return existing;
1235
+ }
1236
+
1237
+ // Not found (or unnamed) → create new
1238
+ return parseNodeFresh(type, subType, nodeData, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path, parent.type, parent);
1239
+ }
1240
+
1241
+ // ---------------------------------------------------------------------------
1242
+ // ADR-0055 — deferred overlay application.
1243
+ // ---------------------------------------------------------------------------
1244
+
1245
+ /** The node an `overlay: true` declaration re-opens, or undefined.
1246
+ *
1247
+ * ADR-0039 sanctioned own read: an overlay targets the AUTHORED declaration
1248
+ * layer (an own child), never the resolved/inherited view.
1249
+ *
1250
+ * ROOT-LEVEL lookups are PACKAGE-QUALIFIED: two files declaring the same
1251
+ * (type, name) under different packages are DISTINCT root nodes, never a merge
1252
+ * pair (mirrors the Java parser, which searches root children by "pkg::name").
1253
+ * Nested children stay bare-name matched — they are scoped by their parent, and
1254
+ * packages do not disambiguate siblings inside a node.
1255
+ *
1256
+ * One implementation, shared by the walk and by {@link applyPendingOverlays},
1257
+ * so "what does this overlay target" cannot drift between the two. */
1258
+ function findOverlayTarget(
1259
+ parent: MetaData,
1260
+ type: string,
1261
+ name: string,
1262
+ nodeData: Record<string, unknown>,
1263
+ inheritedContextPkg: string,
1264
+ ): MetaData | undefined {
1073
1265
  let existing = name !== "" ? parent.ownChildByTypeAndName(type, name) : undefined;
1074
1266
  if (existing !== undefined && parent instanceof MetaRoot) {
1075
1267
  const candidateKey = rootChildResolutionKey(nodeData, inheritedContextPkg, name);
@@ -1081,27 +1273,119 @@ function createOrFindMetaData(
1081
1273
  .find((c) => c.type === type && c.name === name && c.resolutionKey() === candidateKey);
1082
1274
  }
1083
1275
  }
1276
+ return existing;
1277
+ }
1084
1278
 
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;
1279
+ /** The one wording for a missing overlay target, shared by the defensive
1280
+ * in-walk path and the deferred pass, so the two cannot drift. */
1281
+ function overlayNoTargetMessage(type: string, name: string): string {
1282
+ return `Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`;
1283
+ }
1284
+
1285
+ /** The declaration's own address, for the resolved envelope's `referrer`. */
1286
+ function overlayReferrer(item: PendingOverlay): string {
1287
+ if (item.parent instanceof MetaRoot) {
1288
+ return rootChildResolutionKey(item.nodeData, item.inheritedContextPkg, item.name);
1095
1289
  }
1290
+ // ADR-0029 addressing — a nested overlay is named relative to its parent.
1291
+ return `${item.parent.name}.${item.name}`;
1292
+ }
1096
1293
 
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;
1294
+ /**
1295
+ * ADR-0055 apply queued `overlay: true` declarations against the complete tree.
1296
+ *
1297
+ * Called once, after every source has been parsed and BEFORE deferred super
1298
+ * resolution (G5), with every source's queue concatenated in parse order — which
1299
+ * is already "source order, then declaration order within a source" (G2).
1300
+ *
1301
+ * Each element is applied independently: a missing target is recorded and the
1302
+ * element skipped, so one bad overlay no longer takes its whole source down with
1303
+ * it (the eager throw aborted the entire document, losing every sibling
1304
+ * declaration and cascading into ERR_UNRESOLVED_SUPER).
1305
+ */
1306
+ export function applyPendingOverlays(
1307
+ pending: readonly PendingOverlay[],
1308
+ opts: { registry: TypeRegistry; strict?: boolean },
1309
+ ): { errors: ParseError[]; warnings: string[]; envelopeWarnings: LoaderWarning[] } {
1310
+ const errors: ParseError[] = [];
1311
+ const warnings: string[] = [];
1312
+ const envelopeWarnings: LoaderWarning[] = [];
1313
+ const strict = opts.strict ?? false;
1314
+
1315
+ for (const item of pending) {
1316
+ try {
1317
+ // Re-enter the walk state this declaration was queued under. The walk that
1318
+ // built it has unwound, so without this anything constructed now would be
1319
+ // stamped with the wrong provenance and an error would name the wrong
1320
+ // location (or none at all).
1321
+ _currentPath = JsonPathBuilder.fromSegments(item.pathSegments);
1322
+ _currentSourceId = item.sourceId;
1323
+ _currentFormat = item.format;
1324
+ _currentYamlPosition = item.yamlPosition;
1325
+ _currentErrors = errors;
1326
+ _currentEnvelopeWarnings = envelopeWarnings;
1327
+ // Anything the overlay contributes may `extends` a node in any source; the
1328
+ // loader resolves every ref after this pass.
1329
+ _deferSuperResolution = true;
1330
+
1331
+ const target = findOverlayTarget(
1332
+ item.parent,
1333
+ item.type,
1334
+ item.name,
1335
+ item.nodeData,
1336
+ item.inheritedContextPkg,
1337
+ );
1338
+ if (target === undefined) {
1339
+ errors.push(
1340
+ new ParseError(overlayNoTargetMessage(item.type, item.name), {
1341
+ code: "ERR_OVERLAY_NO_TARGET",
1342
+ // ADR-0009 FR5d — a reference that did not resolve, reported with the
1343
+ // declaration's own files/jsonPath.
1344
+ source: resolvedSource(
1345
+ item.errorSource,
1346
+ overlayReferrer(item),
1347
+ `${item.type}:${item.name}`,
1348
+ ),
1349
+ }),
1350
+ );
1351
+ continue; // parseNodeInto was never entered — no partial state to unwind
1352
+ }
1353
+ target.setIsMerge(true);
1354
+ parseNodeInto(
1355
+ item.nodeData,
1356
+ target,
1357
+ item.accumRoot,
1358
+ item.inheritedContextPkg,
1359
+ opts.registry,
1360
+ warnings,
1361
+ errors,
1362
+ strict,
1363
+ item.sourceName,
1364
+ item.path,
1365
+ );
1366
+ } catch (err) {
1367
+ // Per-element, as the loader already does per-source: a strict-mode
1368
+ // reportProblem or a registry error must not abandon the remaining queue.
1369
+ errors.push(
1370
+ err instanceof ParseError
1371
+ ? err
1372
+ : new ParseError(
1373
+ `Failed to apply overlay for [${item.type}:${item.name}]: ${String(err)}`,
1374
+ { code: "ERR_UNKNOWN", source: item.errorSource },
1375
+ ),
1376
+ );
1377
+ } finally {
1378
+ _currentPath = undefined;
1379
+ _currentSourceId = undefined;
1380
+ _currentFormat = "json";
1381
+ _currentYamlPosition = undefined;
1382
+ _currentErrors = undefined;
1383
+ _currentEnvelopeWarnings = undefined;
1384
+ _deferSuperResolution = false;
1385
+ }
1101
1386
  }
1102
1387
 
1103
- // Not found (or unnamed) → create new
1104
- return parseNodeFresh(type, subType, nodeData, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path, parent.type, parent);
1388
+ return { errors, warnings, envelopeWarnings };
1105
1389
  }
1106
1390
 
1107
1391
  // ---------------------------------------------------------------------------
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
+ }