@sdd-method/author-cli 0.12.1 → 0.14.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 (46) hide show
  1. package/README.md +1 -1
  2. package/dist/author-program.js +10 -2
  3. package/dist/lib/catalogue/build.js +72 -23
  4. package/dist/lib/catalogue/builders/adr-governs.js +1 -1
  5. package/dist/lib/catalogue/builders/adrs.js +2 -2
  6. package/dist/lib/catalogue/builders/c4-containers.js +6 -2
  7. package/dist/lib/catalogue/builders/contract-revisions.js +45 -7
  8. package/dist/lib/catalogue/builders/contracts.js +4 -11
  9. package/dist/lib/catalogue/builders/data-model.js +1 -1
  10. package/dist/lib/catalogue/builders/entities-manifest.js +10 -4
  11. package/dist/lib/catalogue/builders/entity-archetype.js +121 -0
  12. package/dist/lib/catalogue/builders/entity-merge.js +90 -0
  13. package/dist/lib/catalogue/builders/features.js +1 -1
  14. package/dist/lib/catalogue/builders/glossary-xref.js +11 -5
  15. package/dist/lib/catalogue/builders/journey-requirement.js +5 -5
  16. package/dist/lib/catalogue/builders/quality-scores.js +1 -1
  17. package/dist/lib/catalogue/builders/requirements.js +1 -1
  18. package/dist/lib/catalogue/builders/service-contract-consumes.js +2 -2
  19. package/dist/lib/catalogue/builders/service-entity-tables.js +1 -1
  20. package/dist/lib/catalogue/canonical-schema.yaml +50 -30
  21. package/dist/lib/catalogue/contract-id.js +49 -12
  22. package/dist/lib/catalogue/manifest-loader.js +1 -1
  23. package/dist/lib/catalogue/retired-catalogues.js +1 -1
  24. package/dist/lib/catalogue/rubric-coverage-map.yaml +7 -3
  25. package/dist/lib/catalogue/schema-compat.js +1 -1
  26. package/dist/lib/catalogue/staleness-verdicts.js +3 -3
  27. package/dist/lib/structure/apply.js +71 -0
  28. package/dist/lib/structure/archive.js +330 -0
  29. package/dist/lib/structure/catalogue.js +31 -10
  30. package/dist/lib/structure/compose.js +1 -1
  31. package/dist/lib/structure/effects.js +1 -1
  32. package/dist/lib/structure/extract.js +123 -0
  33. package/dist/lib/structure/intended.js +194 -0
  34. package/dist/lib/structure/model.js +1 -1
  35. package/dist/lib/structure/render.js +1 -1
  36. package/dist/lib/structure/view.js +8 -3
  37. package/dist/lib/validate/agreement-evidence.js +2 -2
  38. package/dist/lib/validate/behaviour-store.js +49 -12
  39. package/dist/lib/validate/dispatch.js +2 -2
  40. package/dist/lib/validate/journey-impact.js +7 -7
  41. package/dist/lib/validate/structural-model.js +48 -35
  42. package/dist/maintain-version.json +2 -2
  43. package/dist/verbs/change.js +15 -0
  44. package/dist/verbs/migrate-structure.js +36 -0
  45. package/dist/verify-version.json +2 -2
  46. package/package.json +2 -2
package/README.md CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  `sdd-author` is for the people and agents who own one SDD's content. It re-authors that content and **cannot move the baseline or rebuild the model** (M-ADR 0220).
4
4
 
5
- Verbs: everything in `@sdd-method/verify-cli`, plus `scaffold domain`, `plan`, `contracts attest`, `conformance`, `api-surface`, the `snapshot` verbs, `render screen`, `generate-briefing`, `context pull` and `catalogue check`.
5
+ Verbs: everything in `@sdd-method/verify-cli`, plus `scaffold domain`, `plan`, `contracts attest`, `conformance`, `api-surface`, the `snapshot` verbs, `render screen`, `generate-briefing`, `context pull`, `catalogue check`, and the structural model's `migrate structure` and `change archive`.
6
6
 
7
7
  `catalogue check` reports whether the committed catalogues are what the committed specifications build to, and writes nothing. Rebuilding them (`catalogue build`), `sync` and `pack build` are in `@sdd-method/maintain-cli`; `init` and the administrator's verbs are in `@sdd-method/sdd-cli`. They are not present in this package, and invoking one fails with exit 1.
@@ -5,8 +5,8 @@
5
5
  // (0220 §4.1): both carry the whole verify surface, and the author adds what 0220 §4.2 gives it —
6
6
  // new artefacts and plans (`scaffold`, `plan`), the evidence and findings it triages (`contracts`,
7
7
  // `conformance`, `api-surface`, the `snapshot` verbs), the projections maintain also holds
8
- // (`render screen`, `generate-briefing`, `context pull`), and `catalogue check`, which reports a
9
- // stale model and writes nothing.
8
+ // (`render screen`, `generate-briefing`, `context pull`), `catalogue check`, which reports a
9
+ // stale model and writes nothing, and the structural model it authors (`migrate structure`, `change archive`).
10
10
  //
11
11
  // Not here, by design: `sync`, `catalogue build | aggregate | graph | diff` and `pack build`
12
12
  // (maintain's), and `init`, `metamodel roundtrip` and `catalogue migration-map` (sdd-cli's,
@@ -18,11 +18,13 @@ import { getPackageVersion } from "./lib/package-version.js";
18
18
  import { registerVerifySurface, rejectOutsideSurface, VERIFY_SURFACE } from "./verify-program.js";
19
19
  import { registerApiSurface } from "./verbs/api-surface.js";
20
20
  import { registerCatalogueCheck } from "./verbs/catalogue-check.js";
21
+ import { registerChange } from "./verbs/change.js";
21
22
  import { registerConformance } from "./verbs/conformance.js";
22
23
  import { registerContext } from "./verbs/context.js";
23
24
  import { registerContracts } from "./verbs/contracts.js";
24
25
  import { registerGenerateBriefing } from "./verbs/generate-briefing.js";
25
26
  import { registerMigrate } from "./verbs/migrate.js";
27
+ import { registerMigrateStructure } from "./verbs/migrate-structure.js";
26
28
  import { registerPlan } from "./verbs/plan.js";
27
29
  import { registerRender } from "./verbs/render.js";
28
30
  import { registerScaffold } from "./verbs/scaffold.js";
@@ -48,6 +50,8 @@ export const AUTHOR_ONLY_SURFACE = [
48
50
  "catalogue check",
49
51
  "migrate vocabulary",
50
52
  "migrate intents",
53
+ "migrate structure",
54
+ "change archive",
51
55
  ];
52
56
  /** The full surface: verify's, plus the author's own. */
53
57
  export const AUTHOR_SURFACE = [...VERIFY_SURFACE, ...AUTHOR_ONLY_SURFACE];
@@ -88,6 +92,7 @@ export function buildAuthorProgram() {
88
92
  registerVerifySurface(program);
89
93
  registerScaffold(program);
90
94
  registerMigrate(program);
95
+ registerMigrateStructure(program);
91
96
  registerPlan(program);
92
97
  registerContracts(program);
93
98
  registerConformance(program);
@@ -96,6 +101,9 @@ export function buildAuthorProgram() {
96
101
  registerRender(program);
97
102
  registerGenerateBriefing(program);
98
103
  registerContext(program);
104
+ // The structural model is authored content: its first extraction, and a judged change's archive into it (M-ADR 0230
105
+ // §4.6, §6).
106
+ registerChange(program);
99
107
  const catalogue = program.commands.find((c) => c.name() === "catalogue");
100
108
  catalogue.description("Validate and query per-SDD catalogues (M-ADR 0134), and check them for staleness — never rebuilds");
101
109
  registerCatalogueCheck(catalogue);
@@ -17,7 +17,10 @@
17
17
  * smoke test that the infrastructure works end-to-end on any SDD repo.
18
18
  */
19
19
  import { overlayModel } from "../structure/catalogue.js";
20
+ import { agreedOrder } from "../structure/compose.js";
21
+ import { Additions, intendedWrites, loadOpenChanges, splitDangling } from "../structure/intended.js";
20
22
  import { buildDataModelElements, readDataModelSources } from "./builders/data-model.js";
23
+ import { enrichEntityArchetype } from "./builders/entity-archetype.js";
21
24
  import { removeRetiredCatalogues } from "./retired-catalogues.js";
22
25
  import { loadModel } from "../structure/model.js";
23
26
  import { viewOf } from "../structure/view.js";
@@ -41,9 +44,11 @@ import { buildEvidenceTargetRepos } from "./builders/evidence-target-repo.js";
41
44
  import { buildRepoDependencies } from "./builders/repo-dependency.js";
42
45
  import { buildRequirements } from "./builders/requirements.js";
43
46
  import { deriveAdrGoverns } from "./builders/adr-governs.js";
44
- import { buildContractRevisions } from "./builders/contract-revisions.js";
47
+ import { contractRevisionsAndDeprecations } from "./builders/contract-revisions.js";
45
48
  import { buildJourneyRequirement } from "./builders/journey-requirement.js";
46
49
  import { buildDomains, buildProducts, buildApplicationDataPlaneBindings, buildApplicationDataPlaneDeviations, buildApplicationConformanceCoverage, buildImplementations, buildRepositories, buildServices, buildCapabilities, buildCapabilityService, buildCapabilityAdr, buildCapabilityFeature, buildCapabilityContract, buildAdrs, buildAdrSupersedes, buildFeatures, buildFeatureGroups, buildFeatureGrounding, buildCapabilitySpec, buildContracts, buildDeclaredExternalContracts, buildRepoEdges, deriveFeatureRepoFromServices, buildRepoCapabilityExemption, buildServiceCapabilityExemption, buildEvents, buildServiceEvent, buildCapabilityEvent, buildFeatureEvent, buildServiceContract, buildServiceContractConsumes, buildFeatureContract, buildDomainModels, buildCapabilityDomainModel, buildFeatureDomainModel, buildFeatureSubscriptionChannels, buildProductPlatformDependencies, buildGlossaryItems, backfillGlossaryRelations, buildGlossaryXref, buildC4Systems, buildC4Containers, buildC4Components, buildC4ExternalSystems, buildExternalSystemExemption, buildContainerDependency, reportEntryIdReferentCollisions, buildSystemExternalDependency, buildPersonaC4System, buildPersonaContainer, buildPersonas, buildQualityScores, buildScaffolds, buildScreens, buildUserJourneys, buildPersonaJourney, buildPersonaFeature, buildJourneyFeature, buildProductEdges, buildIntegrations, entitySlug, buildEntitiesFromManifest, buildEntitiesFromMermaid, buildServiceEntityFromTables, enrichEntityPersistenceKind, buildC4ContainerEntity, buildC4SystemEntity, } from "./builders/index.js";
50
+ import { mergeEntities } from "./builders/entity-merge.js";
51
+ import { loadAliasMap } from "./builders/service-entity-tables.js";
47
52
  import { buildIntegrationComponents } from "./builders/integration-components.js";
48
53
  import { loadReverseProductQuality } from "./reverse-product-quality.js";
49
54
  import { loadTraceabilityManifests } from "./traceability-loader.js";
@@ -77,12 +82,17 @@ export async function runCatalogueBuild(inputs, write) {
77
82
  }
78
83
  const collected = await collectRows(schema, manifest, findings, sddPath, inputs.snapshotStore);
79
84
  // 3 (then). Where the SDD has a structural model, it is the record of structure: every structure catalogue is built from
80
- // its current view, and what the sources say otherwise is reported (M-ADR 0230 §4.3, Proposed).
85
+ // its current view, and what the sources say otherwise is reported (M-ADR 0230 §4.3).
86
+ // An agreed open change's elements, stated by the sources ahead of its archive, are intended, not divergent (§4.7).
81
87
  const modelProblems = [];
82
88
  const modelFiles = await loadModel(sddPath, modelProblems);
83
- const rowsByCatalogue = modelFiles === null ? collected : overlayModel(schema, viewOf(modelFiles, modelProblems), collected, findings);
89
+ const { open, newContracts } = await loadOpenChanges(sddPath, []);
90
+ const intendedRows = [];
91
+ const rowsByCatalogue = modelFiles === null
92
+ ? collected
93
+ : overlayModel(schema, viewOf(modelFiles, modelProblems), collected, findings, intendedWrites(open, agreedOrder(open)), intendedRows);
84
94
  for (const p of modelProblems)
85
- findings.add("convention", `${p} (M-ADR 0230, Proposed)`);
95
+ findings.add("convention", `${p} (M-ADR 0230)`);
86
96
  // 3a. Normalise every keyed catalogue to PK-unique rows. Multi-source edge
87
97
  // tables (feature_service, feature_repo) can have the same fact asserted by
88
98
  // more than one walker; each walker's own seen-set only dedupes intra-walker.
@@ -139,7 +149,13 @@ export async function runCatalogueBuild(inputs, write) {
139
149
  // SDD profile so profile-external FKs (schema 2.23.0, e.g. an app/integration
140
150
  // capability's platform-resident domain_id per M-ADR 0136) resolve at
141
151
  // aggregation instead of failing this local build.
142
- const { fk, enums } = await validateWithEngineToLog(schema, rowsByCatalogue, findings, { profile: manifest.profile });
152
+ const { fk: engineFk, enums } = await validateWithEngineToLog(schema, rowsByCatalogue, findings, { profile: manifest.profile });
153
+ // 5a. A row an open change owns (its change_id, or a contract its record agrees as new) resolves against what that
154
+ // change adds; any other row naming an addition is a current reference to an intended element, and still dangles
155
+ // (M-ADR 0230 §4.7, §4.11 proof 3).
156
+ const additions = new Additions(open, newContracts, rowsByCatalogue.get("contracts") ?? []);
157
+ const split = splitDangling(schema, rowsByCatalogue, engineFk.internalFailures, additions, manifest.sddId);
158
+ const fk = { ...engineFk, internalPass: split.dangling.length === 0, internalFailures: split.dangling.map((d) => d.failure) };
143
159
  // 5b. Bound-enum validation (M-ADR 0203 §4.1): every column_enums-bound
144
160
  // column hard-fails on an out-of-set value, like a dangling internal FK.
145
161
  // The canonical builder cannot emit a non-canonical c4_containers.type
@@ -171,10 +187,24 @@ export async function runCatalogueBuild(inputs, write) {
171
187
  }
172
188
  else {
173
189
  write(`\nFK validation (internal): FAIL (${fk.internalFailures.length} dangling)\n`);
174
- for (const f of fk.internalFailures) {
175
- write(` ${f.catalogue}.${f.column}: row pk=${f.referencingRow} → ${f.target}=${f.value || "<empty>"}\n`);
190
+ for (const { failure: f, addedBy } of split.dangling) {
191
+ write(` ${f.catalogue}.${f.column}: row pk=${f.referencingRow} → ${f.target}=${f.value || "<empty>"}` +
192
+ (addedBy === undefined
193
+ ? ""
194
+ : ` (open change ${addedBy} adds it, and nothing ties this row to that change: a current reference to an intended element)`) +
195
+ `\n`);
176
196
  }
177
197
  }
198
+ if (split.intended.length > 0) {
199
+ write(`Intended references: ${split.intended.length}, each made by an open change's own row to what that change adds\n`);
200
+ for (const { failure: f, change } of split.intended) {
201
+ write(` ${f.catalogue}.${f.column}: row pk=${f.referencingRow} → ${f.target}=${f.value} (${change})\n`);
202
+ }
203
+ }
204
+ if (intendedRows.length > 0) {
205
+ write(`Intended elements: ${intendedRows.length} source row(s) state what an agreed open change adds or modifies, ` +
206
+ `catalogued once it archives: ${[...intendedRows].sort().join(", ")}\n`);
207
+ }
178
208
  if (enums.pass) {
179
209
  write(`Enum validation (column_enums): PASS (${validatedPopulation(rowsByCatalogue)})\n`);
180
210
  }
@@ -895,8 +925,8 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
895
925
  findings,
896
926
  knownServiceIds,
897
927
  aliases: serviceContractAliases,
898
- // M-ADR 0203 §4.4 cut window: alias targets authored as 2.x kindless
899
- // contract ids re-key against this build's kind-suffixed rows.
928
+ // M-ADR 0203 §4.4 cut window: alias targets authored as 2.x kindless contract ids, or with a pre-7.0.0
929
+ // unsplit stem, re-key against this build's rows.
900
930
  legacyContractIdMap: buildLegacyContractIdMap(out.get("contracts") ?? []),
901
931
  });
902
932
  out.set("service_contract", [...exposesRows, ...consumesRows]);
@@ -1192,11 +1222,17 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
1192
1222
  out.set("changes", r.changes);
1193
1223
  out.set("change_work_order", r.workOrders);
1194
1224
  out.set("change_work_item", r.workItems);
1195
- // M-ADR 0230 §4.2 (Proposed): which view each decision governs, now the store's changes are known.
1225
+ // M-ADR 0230 §4.2: which view each decision governs, now the store's changes are known.
1196
1226
  out.set("adrs", deriveAdrGoverns(out.get("adrs") ?? [], out.get("adr_supersedes") ?? [], r.changes, findings));
1197
- // M-ADR 0230 §4.2 (Proposed): each contract's revisions by digest, active beside agreed.
1198
- out.set("contract_revisions", await buildContractRevisions({ sddPath, findings, contracts: out.get("contracts") ?? [], changes: r.changes }));
1199
- // M-ADR 0229 §4.4 (Proposed): the requirements each journey cites, from its tagged end-to-end files or its
1227
+ // M-ADR 0230 §4.2: each contract's revisions by digest, active beside agreed.
1228
+ const revisions = await contractRevisionsAndDeprecations({ sddPath, findings, contracts: out.get("contracts") ?? [], changes: r.changes });
1229
+ out.set("contract_revisions", revisions.rows);
1230
+ // A version an archived change deprecated (agreement record 1.3.0) is deprecated in contracts too, whether or
1231
+ // not its file is in this SDD.
1232
+ const deprecated = revisions.deprecated;
1233
+ if (deprecated.size > 0)
1234
+ out.set("contracts", (out.get("contracts") ?? []).map((c) => (deprecated.has(String(c["id"])) ? { ...c, status: "deprecated" } : c)));
1235
+ // M-ADR 0229 §4.4: the requirements each journey cites, from its tagged end-to-end files or its
1200
1236
  // journey feature's living spec.
1201
1237
  out.set("journey_requirement", await buildJourneyRequirement({
1202
1238
  sddPath,
@@ -1229,6 +1265,8 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
1229
1265
  findings,
1230
1266
  knownSystemIds,
1231
1267
  knownServiceIds,
1268
+ // A malformed alias file is reported by the service_entity walker; not twice.
1269
+ serviceIdByDisplayLower: (await loadAliasMap(sddPath, new FindingsLog())).byDisplayLower,
1232
1270
  knownDomainIds,
1233
1271
  containerIdByEntryIdOut: containerIdByEntryId,
1234
1272
  containerNameByEntryIdOut: containerNameByEntryId,
@@ -1343,12 +1381,14 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
1343
1381
  // c4_containers.service_id.
1344
1382
  // - c4-system-entity: aggregation roll-up over c4_container_entity ⋈
1345
1383
  // c4_containers.system_id with container_count.
1384
+ const platformWideEntityIds = new Set();
1346
1385
  const entitiesFromManifest = await buildEntitiesFromManifest({
1347
1386
  sddPath,
1348
1387
  findings,
1349
1388
  profile: manifest.profile,
1350
1389
  knownDomainIds,
1351
1390
  knownGlossaryIds,
1391
+ platformWideIdsOut: platformWideEntityIds,
1352
1392
  });
1353
1393
  const entitiesFromMermaid = await buildEntitiesFromMermaid({
1354
1394
  sddPath,
@@ -1361,13 +1401,8 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
1361
1401
  knownGlossaryIds,
1362
1402
  });
1363
1403
  // Forward wins on attribute conflicts (matches per-adopter authoring intent
1364
- // per M-ADR 0159 §4.4). Reverse rows seed the map; forward overwrites.
1365
- const entitiesById = new Map();
1366
- for (const r of entitiesFromMermaid)
1367
- entitiesById.set(String(r["id"]), r);
1368
- for (const r of entitiesFromManifest)
1369
- entitiesById.set(String(r["id"]), r);
1370
- const entityRows = [...entitiesById.values()];
1404
+ // per M-ADR 0159 §4.4), field by field; a platform-wide entry naming one domain entity is that entity.
1405
+ const entityRows = mergeEntities(entitiesFromMermaid, entitiesFromManifest, findings, platformWideEntityIds);
1371
1406
  out.set("entities", entityRows);
1372
1407
  // Per M-ADR 0159 §4.9: now that entities.csv exists, append entity_glossary
1373
1408
  // rows for the reverse-method side. The forward-method emission via
@@ -1391,7 +1426,9 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
1391
1426
  domain_id: domainId,
1392
1427
  entity_name: name,
1393
1428
  glossary_item_id: glossaryId,
1394
- status: "matched_direct",
1429
+ // A manifest entry folded into this entity keeps the alignment it was authored with; a data-model slug match
1430
+ // is direct.
1431
+ status: /(?:^|;)status=aligned(?:;|$)/.test(String(e["notes"] ?? "")) ? "aligned" : "matched_direct",
1395
1432
  source_sdd_id: "",
1396
1433
  });
1397
1434
  }
@@ -1423,8 +1460,20 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
1423
1460
  entityIdBySlug,
1424
1461
  entityRows,
1425
1462
  });
1426
- out.set("entities", enrichedEntityRows);
1427
- // 17a. The domain models' own content (M-ADR 0230 §4.3, §4.11 proof 7, Proposed; schema 6.0.0): each entity's
1463
+ // Schema 7.1.0: the PPT archetype, from the capability specs' Entity Classification tables, in an SDD without a
1464
+ // structural model; with one, the model holds it (M-ADR 0230 §4.3).
1465
+ const hasModel = await stat(join(sddPath, "openspec", "structure")).then(() => true, () => false);
1466
+ out.set("entities", hasModel
1467
+ ? enrichedEntityRows
1468
+ : await enrichEntityArchetype({
1469
+ sddPath,
1470
+ findings,
1471
+ capabilities: (out.get("capabilities") ?? []).map((r) => ({ spec_path: String(r["spec_path"] ?? ""), domain_id: String(r["domain_id"] ?? "") })),
1472
+ entityIdByDomainSlug,
1473
+ entityIdBySlug,
1474
+ entityRows: enrichedEntityRows,
1475
+ }));
1476
+ // 17a. The domain models' own content (M-ADR 0230 §4.3, §4.11 proof 7; schema 6.0.0): each entity's
1428
1477
  // attributes, the relationships with both ends' cardinality and roles, and the keys as invariants, from the same
1429
1478
  // parse the entities came from.
1430
1479
  const dataModel = buildDataModelElements(await readDataModelSources(sddPath, domainModels.map((r) => ({ id: String(r["id"]), domainId: String(r["domain_id"]), docPath: String(r["doc_path"]) }))), enrichedEntityRows, findings);
@@ -18,7 +18,7 @@ export function deriveAdrGoverns(adrs, supersedes, changes, findings) {
18
18
  }
19
19
  const state = changeState(change);
20
20
  if (state === "absent")
21
- findings.add("convention", `${String(a["doc_path"])}: effective on archive of ${change}, which is no change in this store; the decision governs nothing until it is (M-ADR 0230 §4.2, Proposed)`);
21
+ findings.add("convention", `${String(a["doc_path"])}: effective on archive of ${change}, which is no change in this store; the decision governs nothing until it is (M-ADR 0230 §4.2)`);
22
22
  accepted.set(String(a["id"]), state === "archived" ? "current" : state === "open" ? "intended" : "none");
23
23
  }
24
24
  const successors = new Map();
@@ -19,7 +19,7 @@
19
19
  // - **Date:** YYYY-MM-DD
20
20
  // - **Supersedes:** raw text (captured into supersedes_raw for the
21
21
  // adr_supersedes builder to parse)
22
- // - **Effective:** `on acceptance` (the default) or `on archive of <change>` (M-ADR 0230 §4.2, Proposed):
22
+ // - **Effective:** `on acceptance` (the default) or `on archive of <change>` (M-ADR 0230 §4.2):
23
23
  // effective_change holds the change; governs is derived once the store's changes are known (adr-governs.ts)
24
24
  //
25
25
  // ID canonicalisation: filename stem `ADR XXXX[: Title]` → `ADR-{0-padded}`.
@@ -85,7 +85,7 @@ export async function buildAdrs(inputs) {
85
85
  const effectiveRaw = (extractField(text, "Effective") || "").trim();
86
86
  const onArchive = /^on archive of\s+`?([A-Za-z0-9][A-Za-z0-9._-]*)`?\s*\.?$/i.exec(effectiveRaw);
87
87
  if (effectiveRaw !== "" && !onArchive && !/^on acceptance\.?$/i.test(effectiveRaw)) {
88
- inputs.findings.add("convention", `${relPath}: **Effective:** is "${effectiveRaw}", neither "on acceptance" nor "on archive of <change>"; read as on acceptance (M-ADR 0230 §4.2, Proposed)`);
88
+ inputs.findings.add("convention", `${relPath}: **Effective:** is "${effectiveRaw}", neither "on acceptance" nor "on archive of <change>"; read as on acceptance (M-ADR 0230 §4.2)`);
89
89
  }
90
90
  const effectiveChange = onArchive?.[1] ?? "";
91
91
  if (scope === "domain" && !inputs.knownDomainIds.has(domainId)) {
@@ -11,7 +11,8 @@
11
11
  // - domain_id (nullable, denormalised) derived from the L2 file path:
12
12
  // docs/domains/<slug>/... → <slug>; platform-wide L2 → empty.
13
13
  // - service_id (optional) — opportunistic match on the C4 entry's id
14
- // or a slug of its display name to a known service id.
14
+ // or a slug of its display name to a known service id, then on its
15
+ // display name through service-name-aliases.yaml.
15
16
  //
16
17
  // We only consider entries inside System_Boundary or Container_Boundary
17
18
  // blocks — mirrors the python reference and excludes Person/System_Ext
@@ -90,11 +91,14 @@ export async function buildC4Containers(inputs) {
90
91
  continue;
91
92
  }
92
93
  seen.add(id);
94
+ const aliased = inputs.serviceIdByDisplayLower?.get(entry.name.toLowerCase()) ?? "";
93
95
  const serviceId = inputs.knownServiceIds.has(entry.entryId)
94
96
  ? entry.entryId
95
97
  : inputs.knownServiceIds.has(slug)
96
98
  ? slug
97
- : "";
99
+ : inputs.knownServiceIds.has(aliased)
100
+ ? aliased
101
+ : "";
98
102
  const domainId = file.domainId !== "" && inputs.knownDomainIds.has(file.domainId)
99
103
  ? file.domainId
100
104
  : "";
@@ -1,9 +1,12 @@
1
- // contract_revisions: a contract version's content revisions, by digest (M-ADR 0230 §4.2, Proposed; schema 6.0.0).
1
+ // contract_revisions: a contract version's content revisions, by digest (M-ADR 0230 §4.2; schema 6.0.0).
2
2
  // active the contract file's current content (deprecated where the contract is)
3
3
  // agreed a revision an agreed open change agrees (agreement record 1.2.0, contracts[].agrees)
4
4
  // retired a revision an archived change agreed that the file no longer holds
5
+ // deprecated the active revision, where an archived change's record deprecates the version (record 1.3.0,
6
+ // contracts[].deprecates: {sunset}), with that sunset and that change; the latest archive wins
5
7
  // An archived change that agreed the current content is the active revision's change. A compatible amendment is a
6
- // new revision at the same path, so an agreed revision and the active one coexist.
8
+ // new revision at the same path, so an agreed revision and the active one coexist. A deprecation an open change
9
+ // states is intended, not current, and moves no row until the change archives.
7
10
  //
8
11
  // A record names a contract by the file in its own repository (`file:`, or a `files:` list), which the catalogue holds
9
12
  // under the SDD's mount of that repository; so a record's file matches the contract whose spec_path ends with it, and
@@ -16,11 +19,12 @@ import { join } from "node:path";
16
19
  import { parse as parseYaml } from "yaml";
17
20
  const AGREEMENTS = join("docs", "agreements");
18
21
  const DIGEST = /^sha256:[0-9a-f]{64}$/;
22
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
19
23
  export function contentDigest(bytes) {
20
24
  return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
21
25
  }
22
26
  const revisionId = (contract, digest) => `${contract}@${digest.slice("sha256:".length, "sha256:".length + 12)}`;
23
- /** Each (file, relies_on, agrees) a record's contracts list pins. */
27
+ /** Each (file, relies_on, agrees, deprecates) a record's contracts list pins. */
24
28
  function pins(record, where, findings) {
25
29
  const r = record?.agreement_record;
26
30
  if (!Array.isArray(r?.contracts))
@@ -44,16 +48,32 @@ function pins(record, where, findings) {
44
48
  }
45
49
  return d;
46
50
  };
51
+ // `deprecates: {sunset}` deprecates the version at each of the entry's files.
52
+ let deprecates;
53
+ if (c["deprecates"] !== undefined) {
54
+ const sunset = c["deprecates"]?.sunset;
55
+ if (typeof sunset === "string" && DATE.test(sunset))
56
+ deprecates = sunset;
57
+ else
58
+ findings.add("convention", `${where}: contracts[${i}].deprecates is {sunset: YYYY-MM-DD}; it deprecates nothing as written`);
59
+ }
47
60
  for (const file of files) {
48
61
  const reliesOn = digestFor("relies_on", file);
49
62
  const agrees = digestFor("agrees", file);
50
- if (reliesOn || agrees)
51
- out.push({ file, ...(reliesOn ? { reliesOn } : {}), ...(agrees ? { agrees } : {}) });
63
+ if (reliesOn || agrees || deprecates)
64
+ out.push({ file, ...(reliesOn ? { reliesOn } : {}), ...(agrees ? { agrees } : {}), ...(deprecates ? { deprecates } : {}) });
52
65
  }
53
66
  }
54
67
  return out;
55
68
  }
56
69
  export async function buildContractRevisions(inputs) {
70
+ return (await contractRevisionsAndDeprecations(inputs)).rows;
71
+ }
72
+ /**
73
+ * The revision rows, and every contract an archived change deprecated. The second is kept apart because a contract
74
+ * declared outside this SDD has no file here and so no revision row, yet its version is still deprecated.
75
+ */
76
+ export async function contractRevisionsAndDeprecations(inputs) {
57
77
  const { sddPath, findings } = inputs;
58
78
  const rows = new Map();
59
79
  const current = new Map(); // contract id -> current digest
@@ -78,6 +98,8 @@ export async function buildContractRevisions(inputs) {
78
98
  catch {
79
99
  names = [];
80
100
  }
101
+ // contract id -> the latest archived deprecation of its version
102
+ const deprecations = new Map();
81
103
  for (const name of names) {
82
104
  const change = name.slice(0, -".yaml".length);
83
105
  const where = join(AGREEMENTS, name);
@@ -103,8 +125,14 @@ export async function buildContractRevisions(inputs) {
103
125
  }
104
126
  const contract = matches[0];
105
127
  const now = current.get(contract);
128
+ if (p.deprecates && archived) {
129
+ const archivedOn = String(archived["archived_on"] ?? "");
130
+ const prior = deprecations.get(contract);
131
+ if (!prior || `${archivedOn}\u0000${changeId}` > `${prior.archivedOn}\u0000${prior.changeId}`)
132
+ deprecations.set(contract, { sunset: p.deprecates, changeId, archivedOn });
133
+ }
106
134
  if (state === "agreed" && p.reliesOn && now && p.reliesOn !== now) {
107
- findings.add("divergence", `${where}: relies on revision ${p.reliesOn.slice(0, 19)}… of ${contract}, which has moved to ${now.slice(0, 19)}…; the change relies on a revision no longer published, so it is re-agreed (M-ADR 0230 §4.2, Proposed)`);
135
+ findings.add("divergence", `${where}: relies on revision ${p.reliesOn.slice(0, 19)}… of ${contract}, which has moved to ${now.slice(0, 19)}…; the change relies on a revision no longer published, so it is re-agreed (M-ADR 0230 §4.2)`);
108
136
  }
109
137
  if (!p.agrees)
110
138
  continue;
@@ -121,6 +149,16 @@ export async function buildContractRevisions(inputs) {
121
149
  rows.set(rid, { id: rid, contract_id: contract, digest: p.agrees, state: "retired", sunset_date: "", change_id: changeId, source_sdd_id: "", platform_sdd_id: "" });
122
150
  }
123
151
  }
124
- return [...rows.values()].sort((a, b) => String(a["id"]).localeCompare(String(b["id"])));
152
+ const deprecated = new Map();
153
+ for (const [contract, d] of deprecations) {
154
+ deprecated.set(contract, { sunset: d.sunset, changeId: d.changeId });
155
+ // A contract whose file is outside this SDD has no revision row to mark.
156
+ const now = current.get(contract);
157
+ const rid = now ? revisionId(contract, now) : undefined;
158
+ const row = rid ? rows.get(rid) : undefined;
159
+ if (rid && row)
160
+ rows.set(rid, { ...row, state: "deprecated", sunset_date: d.sunset, change_id: d.changeId });
161
+ }
162
+ return { rows: [...rows.values()].sort((a, b) => String(a["id"]).localeCompare(String(b["id"]))), deprecated };
125
163
  }
126
164
  //# sourceMappingURL=contract-revisions.js.map
@@ -18,7 +18,7 @@
18
18
  import { readdir, readFile } from "node:fs/promises";
19
19
  import { extname, join, relative, sep } from "node:path";
20
20
  import { parse as parseYaml } from "yaml";
21
- import { contractIdWithKind } from "../contract-id.js";
21
+ import { contractIdWithKind, contractKindlessSlug } from "../contract-id.js";
22
22
  const CONTRACTS_DIR_REL = "contracts";
23
23
  const DOCS_DOMAINS_DIR_REL = join("docs", "domains");
24
24
  const KIND_SPECS = [
@@ -117,8 +117,8 @@ export async function buildContracts(inputs) {
117
117
  // this format exists to end. A remaining collision is same-kind and
118
118
  // still reported.
119
119
  const kindless = entry.domain !== ""
120
- ? slugify(`${entry.domain}-${stem}`)
121
- : slugify(stem);
120
+ ? contractKindlessSlug(entry.domain, stem)
121
+ : contractKindlessSlug("", stem);
122
122
  const slug = contractIdWithKind(kindless, entry.kind);
123
123
  if (seen.has(slug)) {
124
124
  inputs.findings.add("divergence", `${entry.relPath}: contract id "${slug}" already emitted by an earlier file`);
@@ -311,13 +311,6 @@ function domainSegmentCanonical(relPath) {
311
311
  return "";
312
312
  return parts[2] ?? "";
313
313
  }
314
- function slugify(s) {
315
- return s
316
- .toLowerCase()
317
- .replace(/^_+/, "") // turn _common into common
318
- .replace(/[^a-z0-9]+/g, "-")
319
- .replace(/^-+|-+$/g, "");
320
- }
321
314
  const DECLARED_KINDS = {
322
315
  openapi: "rest",
323
316
  proto: "rpc",
@@ -366,7 +359,7 @@ export function buildDeclaredExternalContracts(inputs) {
366
359
  if (inputs.discoveredIdByPath.has(declared) || localBasenames.has(base))
367
360
  continue;
368
361
  const stem = stemOf(base);
369
- const kindless = m.domain !== "" ? slugify(`${m.domain}-${stem}`) : slugify(stem);
362
+ const kindless = contractKindlessSlug(m.domain, stem);
370
363
  const slug = contractIdWithKind(kindless, kind);
371
364
  if (seenDeclaredPaths.has(declared)) {
372
365
  // Same path, same contract: legitimate cross-capability sharing.
@@ -1,4 +1,4 @@
1
- // A domain model's content, parsed whole (M-ADR 0230 §4.3, §4.11 proof 7, Proposed; schema 6.0.0). One parser per
1
+ // A domain model's content, parsed whole (M-ADR 0230 §4.3, §4.11 proof 7; schema 6.0.0). One parser per
2
2
  // notation, shared by every reader of the file: the entities builder takes its entities from here, and this module
3
3
  // builds entity_attributes, entity_relationships and domain_invariants from the same parse.
4
4
  //
@@ -24,8 +24,8 @@
24
24
  // - For each entities[] item with non-empty name, emit a row.
25
25
  // - Within-model dedup by id; if attributes diverge, fire [finding] divergence
26
26
  // on first re-encounter.
27
- // - `status: superseded` entries carry extra `superseded_by` + `note` keys.
28
- // Ingested as ordinary rows; superseded_by preserved in notes for future
27
+ // - `status: superseded` entries emit no entity: a superseded term is history, and glossary_xref carries
28
+ // its supersedes edge (schema 7.0.0). Formerly ingested as ordinary rows; superseded_by preserved in notes for future
29
29
  // Phase 5 promotion to an entity_supersedes edge.
30
30
  // - `glossary_term` resolved best-effort against glossary_items (exact term
31
31
  // then entitySlug equality); leave empty and fire orphan on miss.
@@ -34,8 +34,8 @@ import { join } from "node:path";
34
34
  import { parse as parseYaml } from "yaml";
35
35
  import { entitySlug } from "./entity-slug.js";
36
36
  const MANIFEST_REL_PATH = join("docs", "platform", "reference", "glossary-alignment-manifest.yaml");
37
- const NON_CANONICAL_DOMAINS = new Set(["platform-wide", "", "platform_wide"]);
38
- const PLATFORM_WIDE_PK_PREFIX = "_platform-wide";
37
+ export const NON_CANONICAL_DOMAINS = new Set(["platform-wide", "", "platform_wide"]);
38
+ export const PLATFORM_WIDE_PK_PREFIX = "_platform-wide";
39
39
  export async function buildEntitiesFromManifest(inputs) {
40
40
  // Profile gate per M-ADR 0159 §4.2 amendment: only platform-profile SDDs
41
41
  // read the manifest. Sibling profiles (application-product, integration-
@@ -93,6 +93,10 @@ export async function buildEntitiesFromManifest(inputs) {
93
93
  inputs.findings.add("convention", `${MANIFEST_REL_PATH}: model ${model.path ?? "(unknown)"} has an entities[] entry with empty name — skipped`);
94
94
  continue;
95
95
  }
96
+ // A superseded entry is history, not an element: the glossary keeps its supersedes edge (glossary_xref), and
97
+ // the entity it names is its successor's.
98
+ if (String(entity.status ?? "").trim() === "superseded")
99
+ continue; // as glossary_xref reads it
96
100
  const slug = entitySlug(name);
97
101
  const id = domainId
98
102
  ? `${domainId}-${slug}`
@@ -128,6 +132,8 @@ export async function buildEntitiesFromManifest(inputs) {
128
132
  }
129
133
  byId.set(id, row);
130
134
  rows.push(row);
135
+ if (NON_CANONICAL_DOMAINS.has(rawDomain))
136
+ inputs.platformWideIdsOut?.add(id);
131
137
  }
132
138
  }
133
139
  return rows;
@@ -0,0 +1,121 @@
1
+ // Catalogue schema 7.1.0: backfill `entities.ppt_archetype` from the capability specs' `## Entity Classification`
2
+ // tables (M-ADR 0230 §4.3, at its acceptance).
3
+ //
4
+ // The archetype is a structure fact. Where the SDD has a structural model the model holds it, and the capability spec
5
+ // template drops its table, so this reads the tables only in an SDD without a model: that build is what `migrate
6
+ // structure` carries into a first model.
7
+ //
8
+ // Authored cells vary: "Party (Green)", "**Thing — audit record**", "Blue/Description", "Moment", "PartyRole". A cell's
9
+ // archetype is the first one it names, colour labels and qualifiers aside. Moment alone is moment-interval, and
10
+ // PartyRole is a role. A cell naming more than two is a list of options, not a classification, and names none.
11
+ // What cannot be classified is reported, once per build, never guessed.
12
+ import { readFile } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+ import { collectTableLines, detectColumn, entitySlug, splitRow } from "./data-store-mapping.js";
15
+ export const PPT_ARCHETYPES = ["party", "place", "thing", "role", "moment-interval", "description"];
16
+ const RE_HEADING = /^## Entity Classification\b/;
17
+ const RE_TOKEN = /\b(?:moment[\s-]*interval|moment|party[\s-]*role|party|place|thing|role|description)/g;
18
+ const EXAMPLES = 5;
19
+ /** The archetypes a cell names, in order, each once. */
20
+ export function archetypesNamed(cell) {
21
+ const out = [];
22
+ for (const m of cell.toLowerCase().matchAll(RE_TOKEN)) {
23
+ const t = m[0].replace(/[\s-]+/g, "");
24
+ const a = t.startsWith("moment") ? "moment-interval" : t === "partyrole" ? "role" : t;
25
+ if (!out.includes(a))
26
+ out.push(a);
27
+ }
28
+ return out;
29
+ }
30
+ /** A cell's archetype: the first it names, or null when it names none or more than two. */
31
+ export function archetypeOf(cell) {
32
+ const named = archetypesNamed(cell);
33
+ return named.length === 0 || named.length > 2 ? null : named[0];
34
+ }
35
+ /** The entity a row's first cell names: markup and a trailing qualifier stripped. */
36
+ function entityName(cell) {
37
+ return cell
38
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
39
+ .replace(/[*`_]/g, "")
40
+ .split(/\s+[—–-]\s+/)[0]
41
+ .replace(/\s*\([^)]*\)\s*$/, "")
42
+ .trim();
43
+ }
44
+ function sample(xs) {
45
+ return xs.slice(0, EXAMPLES).join(", ") + (xs.length > EXAMPLES ? `, and ${xs.length - EXAMPLES} more` : "");
46
+ }
47
+ export async function enrichEntityArchetype(inputs) {
48
+ // A spec in the entity's own domain outranks one that reaches it by the cross-domain fallback; otherwise the first wins.
49
+ const chosen = new Map();
50
+ const several = [];
51
+ const none = [];
52
+ const unresolved = [];
53
+ const disagree = new Set();
54
+ const specs = [...new Map(inputs.capabilities.filter((c) => c.spec_path !== "").map((c) => [c.spec_path, c])).values()].sort((a, b) => a.spec_path.localeCompare(b.spec_path));
55
+ for (const cap of specs) {
56
+ let text;
57
+ try {
58
+ text = await readFile(join(inputs.sddPath, cap.spec_path), "utf8");
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ const lines = text.split(/\r?\n/);
64
+ const at = lines.findIndex((l) => RE_HEADING.test(l));
65
+ if (at === -1)
66
+ continue;
67
+ const table = collectTableLines(lines, at + 1);
68
+ if (table === null || table.length < 3)
69
+ continue;
70
+ const headers = splitRow(table[0]);
71
+ const col = detectColumn(headers, (h) => h.includes("archetype") || h.includes("ppt"));
72
+ if (col === -1)
73
+ continue;
74
+ const entityCol = Math.max(0, detectColumn(headers, (h) => h.includes("entity") || h.includes("aggregate")));
75
+ for (const line of table.slice(2)) {
76
+ const cells = splitRow(line);
77
+ const name = entityName(cells[entityCol] ?? "");
78
+ // a separator, the template's placeholder row, or a header repeated inside the table
79
+ if (name === "" || /^[-:]+$/.test(name) || name.startsWith("{") || name.toLowerCase() === (headers[entityCol] ?? "").toLowerCase())
80
+ continue;
81
+ const where = `${cap.spec_path}: ${name}`;
82
+ const cell = cells[col] ?? "";
83
+ const named = archetypesNamed(cell);
84
+ const archetype = archetypeOf(cell);
85
+ if (archetype === null) {
86
+ none.push(`${where} (${JSON.stringify(cell)})`);
87
+ continue;
88
+ }
89
+ if (named.length > 1)
90
+ several.push(`${where} (${JSON.stringify(cell)} read as ${archetype})`);
91
+ const slug = entitySlug(name);
92
+ const localId = inputs.entityIdByDomainSlug.get(`${cap.domain_id}|${slug}`);
93
+ const id = localId ?? inputs.entityIdBySlug.get(slug);
94
+ if (id === undefined) {
95
+ unresolved.push(where);
96
+ continue;
97
+ }
98
+ const mine = { archetype, spec: cap.spec_path, local: localId !== undefined };
99
+ const prior = chosen.get(id);
100
+ if (prior !== undefined && prior.archetype !== archetype) {
101
+ const [kept, other] = mine.local && !prior.local ? [mine, prior] : [prior, mine];
102
+ disagree.add(`${id} (${kept.archetype} in ${kept.spec}, catalogued; ${other.archetype} in ${other.spec})`);
103
+ }
104
+ if (prior === undefined || (mine.local && !prior.local))
105
+ chosen.set(id, mine);
106
+ }
107
+ }
108
+ const report = (n, what, items) => {
109
+ if (n > 0)
110
+ inputs.findings.add("coverage", `entities.ppt_archetype: ${n} ${what}: ${sample([...items].sort())}`);
111
+ };
112
+ report(none.length, "Entity Classification row(s) name no archetype, so none is catalogued", none);
113
+ report(several.length, "Entity Classification row(s) name two archetypes; the first is catalogued", several);
114
+ report(unresolved.length, "Entity Classification row(s) name no entity of this SDD", unresolved);
115
+ report(disagree.size, "entities classified differently by two specs; the entity's own domain's spec, else the first, is catalogued", disagree);
116
+ return inputs.entityRows.map((r) => {
117
+ const c = chosen.get(String(r["id"]));
118
+ return c === undefined || String(r["ppt_archetype"] ?? "") !== "" ? r : { ...r, ppt_archetype: c.archetype };
119
+ });
120
+ }
121
+ //# sourceMappingURL=entity-archetype.js.map