@sdd-method/sdd-cli 1.20.0 → 1.21.0

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 (27) hide show
  1. package/dist/lib/catalogue/aggregate.d.ts +40 -0
  2. package/dist/lib/catalogue/aggregate.d.ts.map +1 -1
  3. package/dist/lib/catalogue/aggregate.js +184 -4
  4. package/dist/lib/catalogue/aggregate.js.map +1 -1
  5. package/dist/lib/catalogue/build.d.ts.map +1 -1
  6. package/dist/lib/catalogue/build.js +17 -0
  7. package/dist/lib/catalogue/build.js.map +1 -1
  8. package/dist/lib/catalogue/builders/capability-spec.d.ts +14 -0
  9. package/dist/lib/catalogue/builders/capability-spec.d.ts.map +1 -1
  10. package/dist/lib/catalogue/builders/capability-spec.js +8 -5
  11. package/dist/lib/catalogue/builders/capability-spec.js.map +1 -1
  12. package/dist/lib/catalogue/builders/feature-grounding.d.ts +8 -0
  13. package/dist/lib/catalogue/builders/feature-grounding.d.ts.map +1 -1
  14. package/dist/lib/catalogue/builders/feature-grounding.js +6 -5
  15. package/dist/lib/catalogue/builders/feature-grounding.js.map +1 -1
  16. package/dist/lib/catalogue/ddl-generator.d.ts.map +1 -1
  17. package/dist/lib/catalogue/ddl-generator.js +22 -4
  18. package/dist/lib/catalogue/ddl-generator.js.map +1 -1
  19. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.d.ts +20 -0
  20. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.d.ts.map +1 -0
  21. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js +31 -0
  22. package/dist/lib/catalogue/feature-repo-zero-resolution-summary.js.map +1 -0
  23. package/dist/lib/catalogue/fk-validator.d.ts +20 -1
  24. package/dist/lib/catalogue/fk-validator.d.ts.map +1 -1
  25. package/dist/lib/catalogue/fk-validator.js +33 -3
  26. package/dist/lib/catalogue/fk-validator.js.map +1 -1
  27. package/package.json +1 -1
@@ -31,7 +31,31 @@ export interface AggregateResult {
31
31
  }
32
32
  export type Writer = (s: string) => void;
33
33
  export declare function runCatalogueAggregate(inputs: AggregateInputs, write: Writer): Promise<AggregateResult>;
34
+ interface DuplicatePk {
35
+ readonly catalogue: string;
36
+ readonly primaryKey: string;
37
+ readonly value: string;
38
+ readonly sources: readonly string[];
39
+ }
34
40
  export declare function dedupExternalSystems(rowsByCatalogue: Map<string, CatalogueRow[]>, findings: FindingsLog): void;
41
+ /**
42
+ * M-ADR 0208 §4.4: the catalogues whose organisation-tier key is
43
+ * `(platform_sdd_id, <pk>)`. Only `domains` — a domain is a bounded context
44
+ * OF a platform, so the pair is its natural key; every other catalogue's ids
45
+ * are already platform-qualified by construction or are method-scoped.
46
+ */
47
+ export declare const PLATFORM_SCOPED_AT_ORGANISATION: readonly string[];
48
+ export declare function findDuplicatePrimaryKeys(schema: CanonicalSchema, rowsByCatalogue: ReadonlyMap<string, readonly CatalogueRow[]>, platformScopedTargets?: readonly string[]): DuplicatePk[];
49
+ /**
50
+ * M-ADR 0208 §4.4 (F-D), organisation tier. For each platform-scoped
51
+ * catalogue: which pks are owned by more than one platform, and how many
52
+ * referencing rows carry such a pk with NO stamp of their own — those
53
+ * references resolve by scalar fallback (the FK check cannot fail them
54
+ * without inventing a platform) but they are AMBIGUOUS, and this says so.
55
+ * One line per scoped catalogue, always written; a coverage finding only
56
+ * when ambiguity exists. Fires zero on an estate where no id is shared.
57
+ */
58
+ export declare function checkPlatformScopedKeyAmbiguity(schema: CanonicalSchema, rowsByCatalogue: ReadonlyMap<string, readonly CatalogueRow[]>, platformScopedTargets: readonly string[], findings: FindingsLog, write: (s: string) => void): void;
35
59
  /**
36
60
  * Stamp `column` = `value` on every row that does not already carry one
37
61
  * (preserve-if-set — chained aggregation, M-ADR 0134 §4.5 step 4). Used for
@@ -150,6 +174,22 @@ export interface ContractRekeySummary {
150
174
  * guess). Exported for testing.
151
175
  */
152
176
  export declare function rekeyLegacyContractRefs(schema: CanonicalSchema, rowsByCatalogue: Map<string, CatalogueRow[]>, findings: FindingsLog): ContractRekeySummary;
177
+ /**
178
+ * Fill `platform_sdd_id` on rows the aggregator constructed after the node
179
+ * loader stamped its inputs (M-ADR 0208 §4.4). The node loader stamps every
180
+ * row it MERGES; rows built at this tier from those inputs — 0189 §4.4
181
+ * adjudication conversions into system_system_dependency / container_dependency,
182
+ * re-derived edges — set source_sdd_id explicitly and were emitted with an empty
183
+ * platform stamp under FK PASS (measured: 38 of 38 system_system_dependency rows
184
+ * and 2 of 208 container_dependency rows on the forward reference estate, the
185
+ * other 206 stamped by the loader; 26 of 26 and 13 of 194 on the reverse estate's
186
+ * platform tier). Resolution order per row: the §4.4 fill-rule platform recorded
187
+ * for its source_sdd_id; else the single stamp already observed on that source's
188
+ * rows anywhere in the aggregate (a source whose rows carry two different stamps
189
+ * is ambiguous and is NOT guessed). Whatever remains empty is reported as
190
+ * coverage — an empty stamp is unresolved, never a key value. Exported for testing.
191
+ */
192
+ export declare function fillDerivedPlatformStamps(schema: CanonicalSchema, rowsByCatalogue: Map<string, CatalogueRow[]>, platformBySource: ReadonlyMap<string, string>, findings: FindingsLog): void;
153
193
  /**
154
194
  * M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4, TIER-GATED by
155
195
  * M-ADR 0208 §4.2 — gated, not skipped: a skipped check fails toward success.
@@ -1 +1 @@
1
- {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../../../src/lib/catalogue/aggregate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../../../src/lib/catalogue/aggregate.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAuB,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClG,OAAO,EAAoB,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIpE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAmB5C,MAAM,WAAW,eAAe;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,gBAAgB,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;AAEzC,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,eAAe,CAAC,CA4nB1B;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAoDD,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAC5C,QAAQ,EAAE,WAAW,GACpB,IAAI,CAkFN;AA8MD;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,EAAE,SAAS,MAAM,EAAgB,CAAC;AAE9E,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,eAAe,EACvB,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC,EAC7D,qBAAqB,GAAE,SAAS,MAAM,EAAO,GAC5C,WAAW,EAAE,CAmDf;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,MAAM,EAAE,eAAe,EACvB,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC,EAC7D,qBAAqB,EAAE,SAAS,MAAM,EAAE,EACxC,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,GACzB,IAAI,CAyCN;AAkDD;;;;;;GAMG;AACH,iBAAS,WAAW,CAClB,SAAS,EAAE,YAAY,EACvB,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,IAAI,GACnB,YAAY,EAAE,CAOhB;AA2ND;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,CAC1C,cAAc,EAAE,SAAS,YAAY,EAAE,EACvC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,QAAQ,EAAE,WAAW,GACpB,YAAY,EAAE,CAiDhB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,EACtC,QAAQ,EAAE,WAAW,GACpB,IAAI,CAoBN;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,qGAAqG;IACrG,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,mFAAmF;IACnF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yGAAyG;IACzG,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+GAA+G;IAC/G,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,kEAAkE;IAClE,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,oFAAoF;IACpF,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,yBAAyB,CACvC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAC5C,QAAQ,EAAE,WAAW,GACpB,mBAAmB,CA6oBrB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,eAAe,EACvB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAC5C,QAAQ,EAAE,WAAW,GACpB,oBAAoB,CAgFtB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,eAAe,EACvB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,EAC5C,gBAAgB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7C,QAAQ,EAAE,WAAW,GACpB,IAAI,CAuDN;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,QAAQ,EAAE,WAAW,EACrB,IAAI,GAAE,UAAU,GAAG,cAA2B,GAC7C,IAAI,CAuCN;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,QAAQ,EAAE,WAAW,GACpB,IAAI,CAoBN;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gCAAgC,CAC9C,mBAAmB,EAAE,SAAS,YAAY,EAAE,EAC5C,kBAAkB,EAAE,SAAS,YAAY,EAAE,EAC3C,QAAQ,EAAE,WAAW,EACrB,YAAY,GAAE,SAAS,YAAY,EAAO,GACzC,YAAY,EAAE,CA2KhB;AAID,eAAO,MAAM,aAAa;;;CAAqC,CAAC"}
@@ -29,6 +29,7 @@ import { readCatalogueCsv, parseCatalogueCsv } from "./csv-reader.js";
29
29
  import { writeCatalogue } from "./csv-writer.js";
30
30
  import { buildLegacyContractIdMap } from "./contract-id.js";
31
31
  import { validateWithEngineToLog } from "./engine-validator.js";
32
+ import { PLATFORM_STAMP_COLUMN, isPlatformScopedFk } from "./fk-validator.js";
32
33
  import { FindingsLog } from "./findings.js";
33
34
  import { readLockFile } from "../lock-file.js";
34
35
  import { mergeEdgeLabel } from "./builders/edge-label-merge.js";
@@ -112,6 +113,14 @@ export async function runCatalogueAggregate(inputs, write) {
112
113
  rowsByCatalogue.set(cat.name, []);
113
114
  }
114
115
  perSiblingCounts.set(rootManifest.sddId, await mergeCatalogues(schema, rowsByCatalogue, join(sddPath, "catalogues"), rootManifest.sddId, rootIsOrganisation ? null : rootManifest.sddId));
116
+ // source_sdd_id -> the platform the §4.4 fill rule assigned it. Kept for the final
117
+ // pass (fillDerivedPlatformStamps): rows the aggregator itself constructs AFTER the
118
+ // node loader (0189 adjudication conversions, re-derived edges) set source_sdd_id
119
+ // explicitly and would otherwise leave platform_sdd_id empty under FK PASS —
120
+ // the same silent shape as the empty platform leg on cross_sdd_platform_capability.
121
+ const platformBySource = new Map();
122
+ if (!rootIsOrganisation)
123
+ platformBySource.set(rootManifest.sddId, rootManifest.sddId);
115
124
  for (const sib of aggManifest.siblings) {
116
125
  // M-ADR 0208 §4.4 fill rule, by pull path. An empty stamp is UNRESOLVED and is
117
126
  // reported below by checkPlatformKindUniqueness; it is never a key value.
@@ -141,6 +150,8 @@ export async function runCatalogueAggregate(inputs, write) {
141
150
  if (crossCheck.deprecatedPlatformField.has(sib.sddId)) {
142
151
  findings.add("convention", `sibling "${sib.sddId}" manifest uses deprecated key \`ecosystem_root\` — rename to \`platform_sdd_id\` (M-ADR 0208 §4.3; alias removed at 2.0.0)`);
143
152
  }
153
+ if (platformSddId !== null)
154
+ platformBySource.set(sib.sddId, platformSddId);
144
155
  perSiblingCounts.set(sib.sddId, await mergeCatalogues(schema, rowsByCatalogue, sib.cataloguesPath, sib.sddId, platformSddId));
145
156
  }
146
157
  // 5a. Derive cross_sdd_platform_capability — federated edge, schema 2.9.0.
@@ -424,6 +435,11 @@ export async function runCatalogueAggregate(inputs, write) {
424
435
  `${adjudication.containerConverted} container conversions (${adjudication.containerEdgesRewritten} edges → container_dependency, ` +
425
436
  `${adjudication.systemFallbackEdges} stuck edges → system_system_dependency at L1), ` +
426
437
  `${adjudication.exempted} exempted, ${adjudication.unverified} unverified\n`);
438
+ // 5f-bis. Rows constructed at THIS tier after the node loader (adjudication
439
+ // conversions, re-derived edges) carry source_sdd_id but no platform stamp.
440
+ // Fill them now that every derivation has run (M-ADR 0208 §4.4 as applied to
441
+ // aggregator-constructed rows); whatever stays empty is reported, never keyed on.
442
+ fillDerivedPlatformStamps(schema, rowsByCatalogue, platformBySource, findings);
427
443
  // 5g. Platform-kind uniqueness (M-ADR 0133 §4.2c open hygiene).
428
444
  checkPlatformKindUniqueness(rowsByCatalogue.get("c4_systems") ?? [], findings, rootIsOrganisation ? "organisation" : "platform");
429
445
  // 5h. M-ADR 0203 §4.4 cross-SDD re-key: the contract-id migration derives
@@ -439,8 +455,12 @@ export async function runCatalogueAggregate(inputs, write) {
439
455
  // was never printed.
440
456
  write(` contract-id cut window (M-ADR 0203 §4.4): ${contractRekey.rekeyed} cross-SDD reference(s) re-keyed, ` +
441
457
  `${contractRekey.deduped} duplicate fact(s) dropped, ${contractRekey.ambiguous} ambiguous left dangling\n`);
442
- // 6. Duplicate primary key check.
443
- const duplicates = findDuplicatePrimaryKeys(schema, rowsByCatalogue);
458
+ // 6. Duplicate primary key check. M-ADR 0208 §4.4 (F-D): at organisation
459
+ // tier the key of `domains` is (platform_sdd_id, id) — two platforms may
460
+ // each own `identity`. Everywhere else, and for every other catalogue,
461
+ // the scalar pk. An empty stamp is unresolved, never a key.
462
+ const platformScopedTargets = rootIsOrganisation ? PLATFORM_SCOPED_AT_ORGANISATION : [];
463
+ const duplicates = findDuplicatePrimaryKeys(schema, rowsByCatalogue, platformScopedTargets);
444
464
  if (duplicates.length > 0) {
445
465
  write(`\nDuplicate primary keys (${duplicates.length}) — aggregation fails:\n`);
446
466
  for (const d of duplicates) {
@@ -463,10 +483,16 @@ export async function runCatalogueAggregate(inputs, write) {
463
483
  });
464
484
  }
465
485
  // 7. Full FK validation, treating external as internal.
466
- const { fk } = await validateWithEngineToLog(schema, rowsByCatalogue, findings, { treatExternalAsInternal: true },
486
+ const { fk } = await validateWithEngineToLog(schema, rowsByCatalogue, findings, { treatExternalAsInternal: true, platformScopedTargets },
467
487
  // The aggregation tier has never emitted enum findings; the cutover
468
488
  // changes no tier's finding surface (M-ADR 0205 §4.4).
469
489
  { logEnumFindings: false });
490
+ // 7b. M-ADR 0208 §4.4 (F-D): report the domain-key state at organisation
491
+ // tier. Always emitted there — an absent line is indistinguishable from a
492
+ // check that never ran (M-ADR 0200 §4.3).
493
+ if (rootIsOrganisation) {
494
+ checkPlatformScopedKeyAmbiguity(schema, rowsByCatalogue, platformScopedTargets, findings, write);
495
+ }
470
496
  // 8. Emit aggregated catalogues + aggregation-manifest.json.
471
497
  const outDir = resolveOutDir(sddPath, inputs.outDir);
472
498
  const rowCounts = new Map();
@@ -815,11 +841,21 @@ async function enrichFederatedServiceMetadata(rowsByCatalogue, siblings, finding
815
841
  rowsByCatalogue.set("services", enriched);
816
842
  }
817
843
  }
818
- function findDuplicatePrimaryKeys(schema, rowsByCatalogue) {
844
+ /**
845
+ * M-ADR 0208 §4.4: the catalogues whose organisation-tier key is
846
+ * `(platform_sdd_id, <pk>)`. Only `domains` — a domain is a bounded context
847
+ * OF a platform, so the pair is its natural key; every other catalogue's ids
848
+ * are already platform-qualified by construction or are method-scoped.
849
+ */
850
+ export const PLATFORM_SCOPED_AT_ORGANISATION = ["domains"];
851
+ export function findDuplicatePrimaryKeys(schema, rowsByCatalogue, platformScopedTargets = []) {
819
852
  const out = [];
820
853
  for (const cat of schema.catalogues) {
821
854
  const rows = rowsByCatalogue.get(cat.name) ?? [];
855
+ // Scoped iff asked for AND the catalogue carries the stamp column.
856
+ const scoped = isPlatformScopedFk(cat, cat, { platformScopedTargets });
822
857
  const byKey = new Map();
858
+ const stampsByKey = new Map();
823
859
  for (const row of rows) {
824
860
  const pk = row[cat.primaryKey];
825
861
  if (pk === undefined || pk === null || pk === "")
@@ -829,8 +865,31 @@ function findDuplicatePrimaryKeys(schema, rowsByCatalogue) {
829
865
  const list = byKey.get(value) ?? [];
830
866
  list.push(sourceSdd || "<unknown>");
831
867
  byKey.set(value, list);
868
+ if (scoped) {
869
+ const stamps = stampsByKey.get(value) ?? [];
870
+ stamps.push(String(row[PLATFORM_STAMP_COLUMN] ?? "").trim());
871
+ stampsByKey.set(value, stamps);
872
+ }
832
873
  }
833
874
  for (const [value, sources] of byKey) {
875
+ if (sources.length > 1 && scoped) {
876
+ // n rows are distinct under (stamp, pk) iff every stamp is present
877
+ // and all differ. An EMPTY stamp is unresolved, never a key: it
878
+ // collides with everything (§4.4 — "two children contributing
879
+ // (∅, identity) must produce a finding, not a silent merge").
880
+ // Mirrors ddl-generator pkQuery's HAVING clause.
881
+ const stamps = stampsByKey.get(value) ?? [];
882
+ const distinctNonEmpty = new Set(stamps.filter((st) => st !== "")).size;
883
+ if (distinctNonEmpty === stamps.length)
884
+ continue;
885
+ out.push({
886
+ catalogue: cat.name,
887
+ primaryKey: `(${PLATFORM_STAMP_COLUMN}, ${cat.primaryKey})`,
888
+ value: `${value} [stamps: ${stamps.map((st) => st || "∅").join(", ")}] — an empty stamp is unresolved, never a key (M-ADR 0208 §4.4)`,
889
+ sources,
890
+ });
891
+ continue;
892
+ }
834
893
  if (sources.length > 1) {
835
894
  out.push({
836
895
  catalogue: cat.name,
@@ -843,6 +902,58 @@ function findDuplicatePrimaryKeys(schema, rowsByCatalogue) {
843
902
  }
844
903
  return out;
845
904
  }
905
+ /**
906
+ * M-ADR 0208 §4.4 (F-D), organisation tier. For each platform-scoped
907
+ * catalogue: which pks are owned by more than one platform, and how many
908
+ * referencing rows carry such a pk with NO stamp of their own — those
909
+ * references resolve by scalar fallback (the FK check cannot fail them
910
+ * without inventing a platform) but they are AMBIGUOUS, and this says so.
911
+ * One line per scoped catalogue, always written; a coverage finding only
912
+ * when ambiguity exists. Fires zero on an estate where no id is shared.
913
+ */
914
+ export function checkPlatformScopedKeyAmbiguity(schema, rowsByCatalogue, platformScopedTargets, findings, write) {
915
+ for (const target of schema.catalogues) {
916
+ if (!isPlatformScopedFk(target, target, { platformScopedTargets }))
917
+ continue;
918
+ const owners = new Map();
919
+ for (const row of rowsByCatalogue.get(target.name) ?? []) {
920
+ const pk = String(row[target.primaryKey] ?? "");
921
+ if (pk === "")
922
+ continue;
923
+ const set = owners.get(pk) ?? new Set();
924
+ set.add(String(row[PLATFORM_STAMP_COLUMN] ?? "").trim() || "∅");
925
+ owners.set(pk, set);
926
+ }
927
+ const shared = new Set([...owners].filter(([, o]) => o.size > 1).map(([pk]) => pk));
928
+ let ambiguous = 0;
929
+ const where = [];
930
+ for (const cat of schema.catalogues) {
931
+ for (const fk of cat.foreignKeys) {
932
+ if (fk.references.split(".")[0] !== target.name)
933
+ continue;
934
+ if (!isPlatformScopedFk(cat, target, { platformScopedTargets }))
935
+ continue;
936
+ let n = 0;
937
+ for (const row of rowsByCatalogue.get(cat.name) ?? []) {
938
+ const v = String(row[fk.column] ?? "");
939
+ if (v === "" || !shared.has(v))
940
+ continue;
941
+ if (String(row[PLATFORM_STAMP_COLUMN] ?? "").trim() === "")
942
+ n++;
943
+ }
944
+ if (n > 0) {
945
+ ambiguous += n;
946
+ where.push(`${cat.name}.${fk.column}=${n}`);
947
+ }
948
+ }
949
+ }
950
+ write(` ${target.name} key at organisation tier is (${PLATFORM_STAMP_COLUMN}, ${target.primaryKey}) (M-ADR 0208 §4.4): ` +
951
+ `${shared.size} id(s) owned by more than one platform, ${ambiguous} unstamped referencing row(s) ambiguous\n`);
952
+ if (ambiguous > 0) {
953
+ findings.add("coverage", `${target.name}: ${ambiguous} referencing row(s) with EMPTY ${PLATFORM_STAMP_COLUMN} name an id owned by more than one platform [${[...shared].sort().join(", ")}] (${where.join(", ")}) — resolved by scalar fallback, AMBIGUOUS until the row is stamped (M-ADR 0208 §4.4)`);
954
+ }
955
+ }
956
+ }
846
957
  async function mergeCatalogues(schema, rowsByCatalogue, cataloguesDir, sourceSddId,
847
958
  /** M-ADR 0208 §4.4: value to fill into `platform_sdd_id` when empty; null = do not fill. */
848
959
  platformSddId) {
@@ -1854,6 +1965,75 @@ export function rekeyLegacyContractRefs(schema, rowsByCatalogue, findings) {
1854
1965
  }
1855
1966
  return { rekeyed, deduped, ambiguous };
1856
1967
  }
1968
+ /**
1969
+ * Fill `platform_sdd_id` on rows the aggregator constructed after the node
1970
+ * loader stamped its inputs (M-ADR 0208 §4.4). The node loader stamps every
1971
+ * row it MERGES; rows built at this tier from those inputs — 0189 §4.4
1972
+ * adjudication conversions into system_system_dependency / container_dependency,
1973
+ * re-derived edges — set source_sdd_id explicitly and were emitted with an empty
1974
+ * platform stamp under FK PASS (measured: 38 of 38 system_system_dependency rows
1975
+ * and 2 of 208 container_dependency rows on the forward reference estate, the
1976
+ * other 206 stamped by the loader; 26 of 26 and 13 of 194 on the reverse estate's
1977
+ * platform tier). Resolution order per row: the §4.4 fill-rule platform recorded
1978
+ * for its source_sdd_id; else the single stamp already observed on that source's
1979
+ * rows anywhere in the aggregate (a source whose rows carry two different stamps
1980
+ * is ambiguous and is NOT guessed). Whatever remains empty is reported as
1981
+ * coverage — an empty stamp is unresolved, never a key value. Exported for testing.
1982
+ */
1983
+ export function fillDerivedPlatformStamps(schema, rowsByCatalogue, platformBySource, findings) {
1984
+ const stamped = schema.catalogues.filter((c) => c.columns.some((col) => col.name === "platform_sdd_id") &&
1985
+ c.columns.some((col) => col.name === "source_sdd_id"));
1986
+ const observed = new Map();
1987
+ for (const cat of stamped) {
1988
+ for (const r of rowsByCatalogue.get(cat.name) ?? []) {
1989
+ const src = String(r["source_sdd_id"] ?? "").trim();
1990
+ const pf = String(r["platform_sdd_id"] ?? "").trim();
1991
+ if (src === "" || pf === "")
1992
+ continue;
1993
+ if (!observed.has(src))
1994
+ observed.set(src, new Set());
1995
+ observed.get(src).add(pf);
1996
+ }
1997
+ }
1998
+ const resolve = (src) => {
1999
+ const ruled = platformBySource.get(src);
2000
+ if (ruled !== undefined)
2001
+ return ruled;
2002
+ const seen = observed.get(src);
2003
+ if (seen !== undefined && seen.size === 1)
2004
+ return [...seen][0];
2005
+ return null;
2006
+ };
2007
+ let filled = 0;
2008
+ const unresolved = [];
2009
+ for (const cat of stamped) {
2010
+ const rows = rowsByCatalogue.get(cat.name);
2011
+ if (rows === undefined || rows.length === 0)
2012
+ continue;
2013
+ let catEmpty = 0;
2014
+ const next = rows.map((r) => {
2015
+ if (String(r["platform_sdd_id"] ?? "").trim() !== "")
2016
+ return r;
2017
+ const src = String(r["source_sdd_id"] ?? "").trim();
2018
+ const value = src === "" ? null : resolve(src);
2019
+ if (value === null) {
2020
+ catEmpty++;
2021
+ return r;
2022
+ }
2023
+ filled++;
2024
+ return { ...r, platform_sdd_id: value };
2025
+ });
2026
+ rowsByCatalogue.set(cat.name, next);
2027
+ if (catEmpty > 0)
2028
+ unresolved.push(`${cat.name} ${catEmpty}`);
2029
+ }
2030
+ if (filled > 0) {
2031
+ findings.add("inferred", `platform_sdd_id: ${filled} aggregator-constructed row(s) stamped from their source_sdd_id after derivation (M-ADR 0208 §4.4) — the node loader stamps only the rows it merges`);
2032
+ }
2033
+ if (unresolved.length > 0) {
2034
+ findings.add("coverage", `platform_sdd_id: rows still EMPTY after the final pass [${unresolved.join(", ")}] — source unknown to the fill rule, or its rows carry more than one stamp; unresolved, not a key (M-ADR 0208 §4.4)`);
2035
+ }
2036
+ }
1857
2037
  /**
1858
2038
  * M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4, TIER-GATED by
1859
2039
  * M-ADR 0208 §4.2 — gated, not skipped: a skipped check fails toward success.