@sdd-method/sdd-cli 0.123.0 → 1.0.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 (57) hide show
  1. package/dist/lib/catalogue/aggregate.d.ts +26 -2
  2. package/dist/lib/catalogue/aggregate.d.ts.map +1 -1
  3. package/dist/lib/catalogue/aggregate.js +220 -12
  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 +24 -5
  7. package/dist/lib/catalogue/build.js.map +1 -1
  8. package/dist/lib/catalogue/builders/c4-external-systems.d.ts.map +1 -1
  9. package/dist/lib/catalogue/builders/c4-external-systems.js +27 -19
  10. package/dist/lib/catalogue/builders/c4-external-systems.js.map +1 -1
  11. package/dist/lib/catalogue/builders/contracts.d.ts.map +1 -1
  12. package/dist/lib/catalogue/builders/contracts.js +12 -4
  13. package/dist/lib/catalogue/builders/contracts.js.map +1 -1
  14. package/dist/lib/catalogue/builders/external-system-exemption.d.ts +10 -0
  15. package/dist/lib/catalogue/builders/external-system-exemption.d.ts.map +1 -1
  16. package/dist/lib/catalogue/builders/external-system-exemption.js +19 -3
  17. package/dist/lib/catalogue/builders/external-system-exemption.js.map +1 -1
  18. package/dist/lib/catalogue/builders/products.d.ts.map +1 -1
  19. package/dist/lib/catalogue/builders/products.js +0 -6
  20. package/dist/lib/catalogue/builders/products.js.map +1 -1
  21. package/dist/lib/catalogue/builders/service-contract-consumes.d.ts +12 -0
  22. package/dist/lib/catalogue/builders/service-contract-consumes.d.ts.map +1 -1
  23. package/dist/lib/catalogue/builders/service-contract-consumes.js +14 -1
  24. package/dist/lib/catalogue/builders/service-contract-consumes.js.map +1 -1
  25. package/dist/lib/catalogue/canonical-schema.yaml +78 -17
  26. package/dist/lib/catalogue/contract-id.d.ts +11 -0
  27. package/dist/lib/catalogue/contract-id.d.ts.map +1 -0
  28. package/dist/lib/catalogue/contract-id.js +49 -0
  29. package/dist/lib/catalogue/contract-id.js.map +1 -0
  30. package/dist/lib/catalogue/enum-validator.d.ts +16 -0
  31. package/dist/lib/catalogue/enum-validator.d.ts.map +1 -0
  32. package/dist/lib/catalogue/enum-validator.js +46 -0
  33. package/dist/lib/catalogue/enum-validator.js.map +1 -0
  34. package/dist/lib/catalogue/ext-id.d.ts +25 -0
  35. package/dist/lib/catalogue/ext-id.d.ts.map +1 -0
  36. package/dist/lib/catalogue/ext-id.js +76 -0
  37. package/dist/lib/catalogue/ext-id.js.map +1 -0
  38. package/dist/lib/catalogue/index.d.ts +1 -0
  39. package/dist/lib/catalogue/index.d.ts.map +1 -1
  40. package/dist/lib/catalogue/index.js +1 -0
  41. package/dist/lib/catalogue/index.js.map +1 -1
  42. package/dist/lib/catalogue/migration-map.d.ts +27 -0
  43. package/dist/lib/catalogue/migration-map.d.ts.map +1 -0
  44. package/dist/lib/catalogue/migration-map.js +114 -0
  45. package/dist/lib/catalogue/migration-map.js.map +1 -0
  46. package/dist/lib/catalogue/reverse-product-quality.js +3 -3
  47. package/dist/lib/catalogue/reverse-product-quality.js.map +1 -1
  48. package/dist/lib/catalogue/schema-compat.d.ts +1 -1
  49. package/dist/lib/catalogue/schema-compat.js +1 -1
  50. package/dist/lib/catalogue/validate.d.ts +2 -0
  51. package/dist/lib/catalogue/validate.d.ts.map +1 -1
  52. package/dist/lib/catalogue/validate.js +22 -5
  53. package/dist/lib/catalogue/validate.js.map +1 -1
  54. package/dist/verbs/catalogue.d.ts.map +1 -1
  55. package/dist/verbs/catalogue.js +12 -1
  56. package/dist/verbs/catalogue.js.map +1 -1
  57. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { type CatalogueDef } from "./schema-loader.js";
1
+ import { type CanonicalSchema, type CatalogueDef } from "./schema-loader.js";
2
2
  import { parseCatalogueCsv } from "./csv-reader.js";
3
3
  import { type CatalogueRow } from "./csv-writer.js";
4
4
  import { FindingsLog } from "./findings.js";
@@ -31,6 +31,7 @@ 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
+ export declare function dedupExternalSystems(rowsByCatalogue: Map<string, CatalogueRow[]>, findings: FindingsLog): void;
34
35
  declare function stampSourceSddId(catalogue: CatalogueDef, rows: readonly Record<string, string>[], sourceSddId: string): CatalogueRow[];
35
36
  /**
36
37
  * Resolve cross-SDD `screen_feature` edges (schema 2.24.0, R3).
@@ -99,7 +100,9 @@ export interface AdjudicationSummary {
99
100
  * REFERENCE (§4.1), and even L1 declarations may name something the estate
100
101
  * owns (measured: 39% of one estate's dependency edges pointed at an
101
102
  * external-system row denoting its own platform). Identity is the declared
102
- * diagram entry id (§4.2), with the display-name slug as fallback.
103
+ * diagram entry id (§4.2) — at schema 3.0.0 the row id derives from it
104
+ * (M-ADR 0203 §4.2) — with the display name only as a unique-match
105
+ * fallback.
103
106
  *
104
107
  * Outcomes per candidate:
105
108
  * - resolves to an internal system of kind platform/subsystem/application:
@@ -119,6 +122,27 @@ export interface AdjudicationSummary {
119
122
  * Exported for testing.
120
123
  */
121
124
  export declare function adjudicateExternalSystems(rowsByCatalogue: Map<string, CatalogueRow[]>, findings: FindingsLog): AdjudicationSummary;
125
+ export interface ContractRekeySummary {
126
+ readonly rekeyed: number;
127
+ readonly deduped: number;
128
+ readonly ambiguous: number;
129
+ }
130
+ /**
131
+ * M-ADR 0203 §4.4's cross-SDD detector-and-re-key, cut window only.
132
+ *
133
+ * Every FK referencing `contracts.id` (schema-driven, so a future contract
134
+ * edge is covered without touching this) is scanned over the merged rows: a
135
+ * value that resolves to no contract, but whose `${value}-${kind}` form
136
+ * resolves via the legacy map built from the MERGED contracts rows, is a
137
+ * provably tool-authored 2.x id — regenerable, not drift. The value re-keys
138
+ * and the composite row id's embedded `__`-delimited component re-keys with
139
+ * it. A re-keyed row whose id then collides with an existing row is the
140
+ * same fact asserted under both eras — deduped, counted. An old id that was
141
+ * a 2.x cross-family collision maps to several contracts: loud and left
142
+ * dangling for FK validation to report (the author qualifies; never a
143
+ * guess). Exported for testing.
144
+ */
145
+ export declare function rekeyLegacyContractRefs(schema: CanonicalSchema, rowsByCatalogue: Map<string, CatalogueRow[]>, findings: FindingsLog): ContractRekeySummary;
122
146
  /**
123
147
  * M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4: the
124
148
  * `platform` kind is documented "exactly one per ecosystem after federation
@@ -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":"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"}
@@ -27,6 +27,7 @@ import { SCHEMA_COMPAT } from "./schema-compat.js";
27
27
  import { loadCanonicalSchema } from "./schema-loader.js";
28
28
  import { readCatalogueCsv, parseCatalogueCsv } from "./csv-reader.js";
29
29
  import { writeCatalogue } from "./csv-writer.js";
30
+ import { buildLegacyContractIdMap } from "./contract-id.js";
30
31
  import { validateForeignKeys } from "./fk-validator.js";
31
32
  import { FindingsLog } from "./findings.js";
32
33
  import { mergeEdgeLabel } from "./builders/edge-label-merge.js";
@@ -220,6 +221,19 @@ export async function runCatalogueAggregate(inputs, write) {
220
221
  `${adjudication.exempted} exempted, ${adjudication.unverified} unverified\n`);
221
222
  // 5g. Platform-kind uniqueness (M-ADR 0133 §4.2c open hygiene).
222
223
  checkPlatformKindUniqueness(rowsByCatalogue.get("c4_systems") ?? [], findings);
224
+ // 5h. M-ADR 0203 §4.4 cross-SDD re-key: the contract-id migration derives
225
+ // per-SDD, but contract-FK edges cross SDD boundaries — a federated SDD
226
+ // that owns no contracts has an empty map, so its references keep the 2.x
227
+ // kindless ids (measured on the first rehearsal probe: 170 dangling FKs
228
+ // over 6 old ids). This tier sees every contract row, so the old form is
229
+ // decidable here and nowhere earlier.
230
+ const contractRekey = rekeyLegacyContractRefs(schema, rowsByCatalogue, findings);
231
+ // Always emitted: an absent line is indistinguishable from a check that
232
+ // never ran (M-ADR 0200 §4.3 — a zero must be affirmable). Measured: the
233
+ // first all-zero estate correctly refused to report "0" for a line that
234
+ // was never printed.
235
+ write(` contract-id cut window (M-ADR 0203 §4.4): ${contractRekey.rekeyed} cross-SDD reference(s) re-keyed, ` +
236
+ `${contractRekey.deduped} duplicate fact(s) dropped, ${contractRekey.ambiguous} ambiguous left dangling\n`);
223
237
  // 6. Duplicate primary key check.
224
238
  const duplicates = findDuplicatePrimaryKeys(schema, rowsByCatalogue);
225
239
  if (duplicates.length > 0) {
@@ -335,21 +349,31 @@ function rederiveAppend(rowsByCatalogue, catalogueName, rederivedRows) {
335
349
  const added = rederivedRows.filter((r) => !existingIds.has(String(r["id"])));
336
350
  rowsByCatalogue.set(catalogueName, [...concatenated, ...added]);
337
351
  }
338
- function dedupExternalSystems(rowsByCatalogue, findings) {
352
+ export function dedupExternalSystems(rowsByCatalogue, findings) {
339
353
  const rows = rowsByCatalogue.get("c4_external_systems");
340
354
  if (rows === undefined || rows.length === 0)
341
355
  return;
342
356
  const byId = new Map();
357
+ const namesById = new Map();
343
358
  for (const row of rows) {
344
359
  const id = String(row["id"] ?? "");
345
360
  if (id === "")
346
361
  continue;
362
+ const name = String(row["name"] ?? "");
363
+ const names = namesById.get(id);
364
+ if (names) {
365
+ if (name !== "" && !names.includes(name))
366
+ names.push(name);
367
+ }
368
+ else {
369
+ namesById.set(id, name === "" ? [] : [name]);
370
+ }
347
371
  const existing = byId.get(id);
348
372
  if (existing === undefined) {
349
373
  byId.set(id, row);
350
374
  continue;
351
375
  }
352
- for (const attr of ["name", "technology", "description"]) {
376
+ for (const attr of ["technology", "description"]) {
353
377
  const a = String(existing[attr] ?? "");
354
378
  const b = String(row[attr] ?? "");
355
379
  if (a !== "" && b !== "" && a !== b) {
@@ -357,6 +381,47 @@ function dedupExternalSystems(rowsByCatalogue, findings) {
357
381
  }
358
382
  }
359
383
  }
384
+ // M-ADR 0203 §4.2's second fallout detector — the mirror image of the
385
+ // same-name split. With identity now the DECLARED entry id, a generic
386
+ // diagram-local mnemonic shared across SDDs merges DIFFERENT real-world
387
+ // systems into one row (measured before this shipped: 17 integration
388
+ // SDDs each declaring their provider under one shared mnemonic — 17
389
+ // providers, one surviving row). A merge whose display names corroborate
390
+ // (compact-equal or token-subset — benign label variance on one
391
+ // referent) stays the quiet dedup this catalogue is designed for; a
392
+ // merge whose names DON'T corroborate is likely N referents under one
393
+ // entry id, and every hidden referent is named. Rename the diagram entry
394
+ // ids apart — a diagram edit, the M-ADR 0189 §4.2 sanctioned opt-out.
395
+ const compactName = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
396
+ const nameTokens = (s) => new Set(s
397
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
398
+ .toLowerCase()
399
+ .split(/[^a-z0-9]+/)
400
+ .filter((t) => t !== ""));
401
+ const namesCorroborate = (a, b) => {
402
+ if (compactName(a) === compactName(b))
403
+ return true;
404
+ const ta = nameTokens(a);
405
+ const tb = nameTokens(b);
406
+ const subset = (x, y) => x.size > 0 && [...x].every((t) => y.has(t));
407
+ return subset(ta, tb) || subset(tb, ta);
408
+ };
409
+ for (const [id, names] of namesById) {
410
+ if (names.length < 2)
411
+ continue;
412
+ const anchor = names[0];
413
+ const strangers = names.slice(1).filter((n) => !namesCorroborate(anchor, n));
414
+ if (strangers.length === 0) {
415
+ // Corroborating label variance — keep the plain per-attr conflict
416
+ // shape the pre-3.0.0 dedup reported.
417
+ findings.add("convention", `c4_external_systems: id "${id}" has conflicting name across SDDs [${names.join(", ")}] — first-seen kept`);
418
+ continue;
419
+ }
420
+ findings.add("divergence", `c4_external_systems: declared id "${id}" merges ${names.length} rows whose display names do not corroborate ` +
421
+ `[${names.join(" | ")}] — likely DIFFERENT systems under one shared diagram entry id; ` +
422
+ `first-seen kept and ${names.length - 1} referent(s) are hidden by this merge. ` +
423
+ `Rename the diagram entry ids apart (M-ADR 0203 §4.2 / M-ADR 0189 §4.2)`);
424
+ }
360
425
  rowsByCatalogue.set("c4_external_systems", [...byId.values()]);
361
426
  }
362
427
  // Method-scoped ADRs (doc_path under docs/method/adr/) are the shared method
@@ -833,7 +898,9 @@ export function checkSuccessorReferences(products, contributorSddIds, findings)
833
898
  * REFERENCE (§4.1), and even L1 declarations may name something the estate
834
899
  * owns (measured: 39% of one estate's dependency edges pointed at an
835
900
  * external-system row denoting its own platform). Identity is the declared
836
- * diagram entry id (§4.2), with the display-name slug as fallback.
901
+ * diagram entry id (§4.2) — at schema 3.0.0 the row id derives from it
902
+ * (M-ADR 0203 §4.2) — with the display name only as a unique-match
903
+ * fallback.
837
904
  *
838
905
  * Outcomes per candidate:
839
906
  * - resolves to an internal system of kind platform/subsystem/application:
@@ -915,7 +982,11 @@ export function adjudicateExternalSystems(rowsByCatalogue, findings) {
915
982
  }
916
983
  }
917
984
  const resolveSystem = (ext) => {
918
- const declared = norm(String(ext["declared_id"] ?? ""));
985
+ // Schema 3.0.0: the row id itself derives from the declared entry id
986
+ // (M-ADR 0203 §4.2), so the id-slug IS declared identity — it backstops
987
+ // an empty declared_id cell, and a hit through it is a declared hit
988
+ // (corroboration-gated), never a label-keyed one.
989
+ const declared = norm(String(ext["declared_id"] ?? "")) || String(ext["id"]).replace(/^ext-/, "");
919
990
  if (declared !== "") {
920
991
  for (const cand of [declared, `${declared}-system`]) {
921
992
  if (systemIds.has(cand))
@@ -925,14 +996,26 @@ export function adjudicateExternalSystems(rowsByCatalogue, findings) {
925
996
  if (viaProduct !== undefined)
926
997
  return { id: viaProduct, method: "declared" };
927
998
  }
928
- const slug = String(ext["id"]).replace(/^ext-/, "");
929
- for (const cand of [slug, `${slug}-system`]) {
930
- if (systemIds.has(cand))
931
- return { id: cand, method: "name" };
999
+ // NAME evidence, per M-ADR 0189 §4.2's ratified fallback ("the
1000
+ // display-name slug remains only as a fallback for entries whose id
1001
+ // resolves to nothing"). Under 2.x the row id WAS the name slug, so
1002
+ // the id-slug path carried this; at 3.0.0 the slug must be derived
1003
+ // from the name itself or the fallback silently narrows — measured on
1004
+ // the first forward-estate probe: 35 of 38 internal platform edges
1005
+ // reclassified external because `System_Ext(platform, "Acme
1006
+ // Platform", …)` stopped resolving against the `acme-platform`
1007
+ // product. Every hit here is method "name" (label-keyed — the
1008
+ // conversion emits the §4.2 convention finding).
1009
+ const nameSlug = norm(String(ext["name"] ?? ""));
1010
+ if (nameSlug !== "") {
1011
+ for (const cand of [nameSlug, `${nameSlug}-system`]) {
1012
+ if (systemIds.has(cand))
1013
+ return { id: cand, method: "name" };
1014
+ }
1015
+ const viaNameProduct = systemByProduct.get(nameSlug);
1016
+ if (viaNameProduct !== undefined)
1017
+ return { id: viaNameProduct, method: "name" };
932
1018
  }
933
- const viaSlugProduct = systemByProduct.get(slug);
934
- if (viaSlugProduct !== undefined)
935
- return { id: viaSlugProduct, method: "name" };
936
1019
  const byName = systemsByName.get(compact(String(ext["name"] ?? "")));
937
1020
  if (byName !== undefined && byName.length === 1) {
938
1021
  return { id: byName[0], method: "name" };
@@ -965,6 +1048,8 @@ export function adjudicateExternalSystems(rowsByCatalogue, findings) {
965
1048
  if (declarerSystem !== undefined) {
966
1049
  const own = containerKeysBySystem.get(declarerSystem);
967
1050
  if (own !== undefined) {
1051
+ // The id-slug key duplicates the declared key at 3.0.0 (the row id
1052
+ // derives from the entry id) but backstops an empty declared_id.
968
1053
  for (const key of [
969
1054
  compact(String(ext["declared_id"] ?? "")),
970
1055
  compact(String(ext["id"]).replace(/^ext-/, "")),
@@ -977,7 +1062,8 @@ export function adjudicateExternalSystems(rowsByCatalogue, findings) {
977
1062
  }
978
1063
  }
979
1064
  const extName = String(ext["name"] ?? "");
980
- const declaredKey = compact(String(ext["declared_id"] ?? ""));
1065
+ const declaredKey = compact(String(ext["declared_id"] ?? "")) ||
1066
+ compact(String(ext["id"]).replace(/^ext-/, ""));
981
1067
  const byKey = declaredKey !== "" ? containersByKey.get(declaredKey) : undefined;
982
1068
  if (byKey !== undefined) {
983
1069
  const corroborating = byKey.filter((cid) => corroboratesLabel(extName, containerNameById.get(cid) ?? "", ""));
@@ -1367,6 +1453,35 @@ export function adjudicateExternalSystems(rowsByCatalogue, findings) {
1367
1453
  if (!existingIds.has(String(row["id"])))
1368
1454
  merged.push(row);
1369
1455
  }
1456
+ // M-ADR 0203 §4.2's detector for its own fallout: with row identity now
1457
+ // the DECLARED entry id, SDDs that declared different entry ids for one
1458
+ // real-world external system split into distinct rows — where the 2.x
1459
+ // name-slug ids silently merged them. Measured before this shipped: 5 old
1460
+ // ids deriving 2-3 conflicting new ids across one estate's SDDs. The
1461
+ // split must be loud, not latent: one display name held by several kept
1462
+ // declared identities is either one estate entity (converge the diagram
1463
+ // entry ids — the rename is a diagram edit, the sanctioned opt-out) or
1464
+ // genuinely several (rename the labels apart). Convention, not an error:
1465
+ // the aggregator cannot know which.
1466
+ const keptByName = new Map();
1467
+ for (const ext of keptExts) {
1468
+ const key = compact(String(ext["name"] ?? ""));
1469
+ if (key === "")
1470
+ continue;
1471
+ const ids = keptByName.get(key);
1472
+ if (ids)
1473
+ ids.push(String(ext["id"]));
1474
+ else
1475
+ keptByName.set(key, [String(ext["id"])]);
1476
+ }
1477
+ for (const [, ids] of [...keptByName.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
1478
+ if (ids.length < 2)
1479
+ continue;
1480
+ const sorted = [...ids].sort((a, b) => a.localeCompare(b));
1481
+ findings.add("convention", `c4_external_systems: one display name "${String(keptExts.find((e) => String(e["id"]) === sorted[0])?.["name"] ?? "")}" is held by ${sorted.length} declared identities [${sorted.join(", ")}] — ` +
1482
+ `either one estate entity split by divergent diagram entry ids (converge the entry ids) ` +
1483
+ `or several entities sharing a label (rename the labels apart) (M-ADR 0203 §4.2)`);
1484
+ }
1370
1485
  rowsByCatalogue.set("c4_external_systems", keptExts);
1371
1486
  rowsByCatalogue.set("system_external_dependency", keptDeps);
1372
1487
  rowsByCatalogue.set("system_system_dependency", merged);
@@ -1385,6 +1500,99 @@ export function adjudicateExternalSystems(rowsByCatalogue, findings) {
1385
1500
  systemFallbackEdges,
1386
1501
  };
1387
1502
  }
1503
+ /**
1504
+ * M-ADR 0203 §4.4's cross-SDD detector-and-re-key, cut window only.
1505
+ *
1506
+ * Every FK referencing `contracts.id` (schema-driven, so a future contract
1507
+ * edge is covered without touching this) is scanned over the merged rows: a
1508
+ * value that resolves to no contract, but whose `${value}-${kind}` form
1509
+ * resolves via the legacy map built from the MERGED contracts rows, is a
1510
+ * provably tool-authored 2.x id — regenerable, not drift. The value re-keys
1511
+ * and the composite row id's embedded `__`-delimited component re-keys with
1512
+ * it. A re-keyed row whose id then collides with an existing row is the
1513
+ * same fact asserted under both eras — deduped, counted. An old id that was
1514
+ * a 2.x cross-family collision maps to several contracts: loud and left
1515
+ * dangling for FK validation to report (the author qualifies; never a
1516
+ * guess). Exported for testing.
1517
+ */
1518
+ export function rekeyLegacyContractRefs(schema, rowsByCatalogue, findings) {
1519
+ const contracts = rowsByCatalogue.get("contracts") ?? [];
1520
+ const contractIds = new Set(contracts.map((r) => String(r["id"])));
1521
+ const legacyMap = buildLegacyContractIdMap(contracts);
1522
+ let rekeyed = 0;
1523
+ let deduped = 0;
1524
+ let ambiguous = 0;
1525
+ if (legacyMap.size === 0)
1526
+ return { rekeyed, deduped, ambiguous };
1527
+ for (const cat of schema.catalogues) {
1528
+ const fkCols = cat.foreignKeys
1529
+ .filter((fk) => fk.references === "contracts.id")
1530
+ .map((fk) => fk.column);
1531
+ if (fkCols.length === 0 || cat.name === "contracts")
1532
+ continue;
1533
+ const rows = rowsByCatalogue.get(cat.name) ?? [];
1534
+ if (rows.length === 0)
1535
+ continue;
1536
+ const perOldId = new Map();
1537
+ const ambiguousOld = new Map();
1538
+ const kept = [];
1539
+ const idsAfter = new Set(rows.map((r) => String(r["id"])));
1540
+ for (const row of rows) {
1541
+ let dropped = false;
1542
+ let current = row;
1543
+ for (const col of fkCols) {
1544
+ const value = String(current[col] ?? "");
1545
+ if (value === "" || contractIds.has(value))
1546
+ continue;
1547
+ const mapped = legacyMap.get(value);
1548
+ if (mapped === undefined)
1549
+ continue;
1550
+ if (mapped.length > 1) {
1551
+ ambiguousOld.set(value, mapped);
1552
+ continue;
1553
+ }
1554
+ const newVal = mapped[0];
1555
+ const oldRowId = String(current["id"] ?? "");
1556
+ const newRowId = oldRowId
1557
+ .split("__")
1558
+ .map((part) => (part === value ? newVal : part))
1559
+ .join("__");
1560
+ if (newRowId !== oldRowId && idsAfter.has(newRowId)) {
1561
+ // The same edge fact already exists under the new id (typically
1562
+ // the owning SDD's own re-keyed row) — one fact, two eras.
1563
+ deduped++;
1564
+ dropped = true;
1565
+ break;
1566
+ }
1567
+ const replaced = {
1568
+ ...current,
1569
+ [col]: newVal,
1570
+ };
1571
+ if (newRowId !== oldRowId) {
1572
+ idsAfter.delete(oldRowId);
1573
+ idsAfter.add(newRowId);
1574
+ replaced["id"] = newRowId;
1575
+ }
1576
+ current = replaced;
1577
+ perOldId.set(value, (perOldId.get(value) ?? 0) + 1);
1578
+ rekeyed++;
1579
+ }
1580
+ if (!dropped)
1581
+ kept.push(current);
1582
+ }
1583
+ rowsByCatalogue.set(cat.name, kept);
1584
+ for (const [oldId, count] of [...perOldId.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
1585
+ findings.add("inferred", `${cat.name}: ${count} cross-SDD reference(s) to "${oldId}" re-keyed to "${legacyMap.get(oldId)[0]}" — ` +
1586
+ `2.x kindless contract id migrated per the 3.0.0 map (regenerable, not drift; M-ADR 0203 §4.4)`);
1587
+ }
1588
+ for (const [oldId, targets] of [...ambiguousOld.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
1589
+ ambiguous++;
1590
+ findings.add("convention", `${cat.name}: references to "${oldId}" map to ${targets.length} contracts at 3.0.0 [${targets.join(", ")}] — ` +
1591
+ `a 2.x cross-family collision; the referencing SDD must qualify with the kind-suffixed id; left dangling (M-ADR 0203 §4.4)`);
1592
+ }
1593
+ }
1594
+ return { rekeyed, deduped, ambiguous };
1595
+ }
1388
1596
  /**
1389
1597
  * M-ADR 0133 §4.2c open hygiene, shipped with M-ADR 0189 §4.4: the
1390
1598
  * `platform` kind is documented "exactly one per ecosystem after federation