@sdd-method/maintain-cli 0.13.1 → 0.15.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.
- package/README.md +1 -1
- package/dist/lib/catalogue/aggregate.js +50 -13
- package/dist/lib/catalogue/build.js +72 -23
- package/dist/lib/catalogue/builders/adr-governs.js +1 -1
- package/dist/lib/catalogue/builders/adrs.js +2 -2
- package/dist/lib/catalogue/builders/c4-containers.js +6 -2
- package/dist/lib/catalogue/builders/contract-revisions.js +45 -7
- package/dist/lib/catalogue/builders/contracts.js +4 -11
- package/dist/lib/catalogue/builders/data-model.js +1 -1
- package/dist/lib/catalogue/builders/entities-manifest.js +10 -4
- package/dist/lib/catalogue/builders/entity-archetype.js +121 -0
- package/dist/lib/catalogue/builders/entity-merge.js +90 -0
- package/dist/lib/catalogue/builders/features.js +1 -1
- package/dist/lib/catalogue/builders/glossary-xref.js +11 -5
- package/dist/lib/catalogue/builders/journey-requirement.js +5 -5
- package/dist/lib/catalogue/builders/quality-scores.js +1 -1
- package/dist/lib/catalogue/builders/requirements.js +1 -1
- package/dist/lib/catalogue/builders/service-contract-consumes.js +2 -2
- package/dist/lib/catalogue/builders/service-entity-tables.js +1 -1
- package/dist/lib/catalogue/canonical-schema.yaml +50 -30
- package/dist/lib/catalogue/contract-id.js +49 -12
- package/dist/lib/catalogue/manifest-loader.js +1 -1
- package/dist/lib/catalogue/retired-catalogues.js +1 -1
- package/dist/lib/catalogue/rubric-coverage-map.yaml +7 -3
- package/dist/lib/catalogue/schema-compat.js +1 -1
- package/dist/lib/catalogue/staleness-verdicts.js +3 -3
- package/dist/lib/structure/catalogue.js +31 -10
- package/dist/lib/structure/compose.js +1 -1
- package/dist/lib/structure/effects.js +1 -1
- package/dist/lib/structure/intended.js +194 -0
- package/dist/lib/structure/model.js +1 -1
- package/dist/lib/structure/render.js +1 -1
- package/dist/lib/structure/view.js +8 -3
- package/dist/lib/validate/agreement-evidence.js +2 -2
- package/dist/lib/validate/behaviour-store.js +49 -12
- package/dist/lib/validate/dispatch.js +2 -2
- package/dist/lib/validate/journey-impact.js +7 -7
- package/dist/lib/validate/structural-model.js +48 -35
- package/dist/maintain-program.js +7 -1
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/structure.js +95 -0
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
`sdd-maintain` brings an SDD repository current and rebuilds its derived model, and **cannot author specification content** (M-ADR 0218).
|
|
4
4
|
|
|
5
|
-
Verbs: everything in `@sdd-method/verify-cli`, plus `sync`, `generate-briefing`, `context pull`, `catalogue build`, `catalogue aggregate`, `catalogue graph`, `catalogue diff`, `pack build`, `render screen`.
|
|
5
|
+
Verbs: everything in `@sdd-method/verify-cli`, plus `sync`, `generate-briefing`, `context pull`, `catalogue build`, `catalogue aggregate`, `catalogue graph`, `catalogue diff`, `pack build`, `render screen`, `structure show`, `structure render`.
|
|
6
6
|
|
|
7
7
|
Each writes only derived or method-managed content, and two runs on the same commit write the same bytes. Authoring verbs — `init`, `scaffold`, `plan`, `conformance` and the rest — are not present in it, and are in `@sdd-method/sdd-cli`. Invoking one fails with exit 1.
|
|
@@ -32,6 +32,7 @@ import { buildLegacyContractIdMap } from "./contract-id.js";
|
|
|
32
32
|
import { validatedPopulation, validateWithEngineToLog } from "./engine-validator.js";
|
|
33
33
|
import { PLATFORM_STAMP_COLUMN, isPlatformScopedFk } from "./fk-validator.js";
|
|
34
34
|
import { FindingsLog } from "./findings.js";
|
|
35
|
+
import { STRUCTURE_DIR } from "../structure/model.js";
|
|
35
36
|
import { readLockFile } from "../lock-file.js";
|
|
36
37
|
import { mergeEdgeLabel } from "./builders/edge-label-merge.js";
|
|
37
38
|
import { isAsyncContractType } from "./builders/product-platform-dependencies.js";
|
|
@@ -189,6 +190,14 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
189
190
|
// `sdd-loyalty-platform`). Kept separate from sddRootsById on purpose: the
|
|
190
191
|
// try-every-root pass must iterate each root ONCE, or a root reachable under two
|
|
191
192
|
// names would count as two hits and report a false ambiguity.
|
|
193
|
+
// M-ADR 0230: the SDDs here that hold a structural model. A structure-sourced row is that model's
|
|
194
|
+
// to state, so re-derivation never adds one the model leaves out (see rederiveAppend).
|
|
195
|
+
const modelSdds = new Set();
|
|
196
|
+
for (const [id, root] of sddRootsById) {
|
|
197
|
+
if (await stat(join(root, STRUCTURE_DIR)).then(() => true, () => false))
|
|
198
|
+
modelSdds.add(id);
|
|
199
|
+
}
|
|
200
|
+
const modelled = { schema, rowsByCatalogue, modelSdds, findings };
|
|
192
201
|
const sddRootAliases = new Map(sddRootsById);
|
|
193
202
|
for (const root of sddRootsById.values()) {
|
|
194
203
|
sddRootAliases.set(basename(root), root);
|
|
@@ -338,13 +347,13 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
349
|
const before = (rowsByCatalogue.get("feature_repo") ?? []).length;
|
|
341
|
-
rederiveAppend(rowsByCatalogue, "feature_repo", rederivedFeatureRepo);
|
|
350
|
+
rederiveAppend(rowsByCatalogue, "feature_repo", rederivedFeatureRepo, modelled);
|
|
342
351
|
const added = (rowsByCatalogue.get("feature_repo") ?? []).length - before;
|
|
343
352
|
if (added > 0) {
|
|
344
353
|
findings.add("coverage", `feature_repo: ${added} row(s) re-derived at aggregate tier for features that had NO anchor at member tier — evidence tokens naming a repo registered by another contributor resolve only against the merged repositories; already-anchored features are not widened (M-ADR 0198 §4.1 as amended; ${contributors.length} contributor doc trees scanned)`);
|
|
345
354
|
}
|
|
346
355
|
}
|
|
347
|
-
// M-ADR 0229 §4.4
|
|
356
|
+
// M-ADR 0229 §4.4: journey_requirement re-derived over the merged rows. A journey is often catalogued
|
|
348
357
|
// in an application SDD and the requirements its steps cite in the platform's behaviour store, so neither member
|
|
349
358
|
// holds both join inputs; each reports the tag as external. Each contributor's tagged end-to-end files (its own
|
|
350
359
|
// manifest's journeys.test_paths) and its journey features' living specs are read again here against the MERGED
|
|
@@ -397,10 +406,10 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
397
406
|
}
|
|
398
407
|
}
|
|
399
408
|
const before = (rowsByCatalogue.get("journey_requirement") ?? []).length;
|
|
400
|
-
rederiveAppend(rowsByCatalogue, "journey_requirement", rederived);
|
|
409
|
+
rederiveAppend(rowsByCatalogue, "journey_requirement", rederived, modelled);
|
|
401
410
|
const added = (rowsByCatalogue.get("journey_requirement") ?? []).length - before;
|
|
402
411
|
if (added > 0) {
|
|
403
|
-
findings.add("coverage", `journey_requirement: ${added} row(s) derived at aggregate tier, where a journey and the requirements it cites are catalogued in different SDDs (M-ADR 0229 §4.4
|
|
412
|
+
findings.add("coverage", `journey_requirement: ${added} row(s) derived at aggregate tier, where a journey and the requirements it cites are catalogued in different SDDs (M-ADR 0229 §4.4; ${contributors.length} contributor(s) read)`);
|
|
404
413
|
}
|
|
405
414
|
for (const u of unresolved)
|
|
406
415
|
findings.add("orphan", `journey_requirement: unresolved against the merged catalogue: ${u}`);
|
|
@@ -416,8 +425,8 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
416
425
|
capabilityFeatureRows: rowsByCatalogue.get("capability_feature") ?? [],
|
|
417
426
|
featureRepoRows: rowsByCatalogue.get("feature_repo") ?? [],
|
|
418
427
|
});
|
|
419
|
-
rederiveAppend(rowsByCatalogue, "repo_capability", repoEdges.repoCapability);
|
|
420
|
-
rederiveAppend(rowsByCatalogue, "repo_contract", repoEdges.repoContract);
|
|
428
|
+
rederiveAppend(rowsByCatalogue, "repo_capability", repoEdges.repoCapability, modelled);
|
|
429
|
+
rederiveAppend(rowsByCatalogue, "repo_contract", repoEdges.repoContract, modelled);
|
|
421
430
|
const featureContractRows = buildFeatureContract({
|
|
422
431
|
findings,
|
|
423
432
|
capabilityFeatureRows: rowsByCatalogue.get("capability_feature") ?? [],
|
|
@@ -425,7 +434,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
425
434
|
featureServiceRows: rowsByCatalogue.get("feature_service") ?? [],
|
|
426
435
|
serviceContractRows: rowsByCatalogue.get("service_contract") ?? [],
|
|
427
436
|
});
|
|
428
|
-
rederiveAppend(rowsByCatalogue, "feature_contract", featureContractRows);
|
|
437
|
+
rederiveAppend(rowsByCatalogue, "feature_contract", featureContractRows, modelled);
|
|
429
438
|
const capabilityEventRows = buildCapabilityEvent({
|
|
430
439
|
findings,
|
|
431
440
|
manifests: [],
|
|
@@ -434,7 +443,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
434
443
|
capabilityServiceRows: rowsByCatalogue.get("capability_service") ?? [],
|
|
435
444
|
serviceEventRows: rowsByCatalogue.get("service_event") ?? [],
|
|
436
445
|
});
|
|
437
|
-
rederiveAppend(rowsByCatalogue, "capability_event", capabilityEventRows);
|
|
446
|
+
rederiveAppend(rowsByCatalogue, "capability_event", capabilityEventRows, modelled);
|
|
438
447
|
// 5a-iii. Archetype data-plane expectations overlay (M-ADR 0191 §4.3) — the
|
|
439
448
|
// deviation-visible WARN + conformance surface, run at AGGREGATION tier where
|
|
440
449
|
// the reference-architecture bundle's `expected_bindings` resolve (the
|
|
@@ -687,14 +696,42 @@ function dedupExternalExemptions(rowsByCatalogue, findings) {
|
|
|
687
696
|
}
|
|
688
697
|
rowsByCatalogue.set("external_system_exemption", [...byId.values()].sort((a, b) => String(a["id"]).localeCompare(String(b["id"]))));
|
|
689
698
|
}
|
|
699
|
+
// M-ADR 0230: in a structure-sourced catalogue, the column naming the row's subject, and the
|
|
700
|
+
// node catalogue it names. The subject's SDD is the one whose model states the row.
|
|
701
|
+
const STRUCTURE_SUBJECT = {
|
|
702
|
+
capability_event: { column: "capability_id", catalogue: "capabilities" },
|
|
703
|
+
};
|
|
690
704
|
// M-ADR 0198 §4.2/§4.3: append a re-derived catalogue's rows to whatever concatenation
|
|
691
705
|
// already produced for it, keeping the concatenated row on any primary-key collision. One
|
|
692
706
|
// shared mechanism for every re-derived catalogue, so adding a fifth is a call to this
|
|
693
707
|
// function with that catalogue's builder output, not new merge logic.
|
|
694
|
-
|
|
708
|
+
// M-ADR 0230: a structure-sourced catalogue is stated by the structural model where its
|
|
709
|
+
// subject's SDD holds one, and that SDD's own build already put the model's rows here; a
|
|
710
|
+
// re-derived row the model leaves out is reported, never added. A row whose subject's SDD this
|
|
711
|
+
// aggregation cannot see (an organisation sibling's member) is added as before.
|
|
712
|
+
function rederiveAppend(rowsByCatalogue, catalogueName, rederivedRows, modelled) {
|
|
695
713
|
const concatenated = rowsByCatalogue.get(catalogueName) ?? [];
|
|
696
714
|
const existingIds = new Set(concatenated.map((r) => String(r["id"])));
|
|
697
|
-
|
|
715
|
+
let added = rederivedRows.filter((r) => !existingIds.has(String(r["id"])));
|
|
716
|
+
const def = modelled.schema.catalogues.find((c) => c.name === catalogueName);
|
|
717
|
+
if (def?.sourceOfTruth === "structure") {
|
|
718
|
+
const subject = STRUCTURE_SUBJECT[catalogueName];
|
|
719
|
+
if (subject === undefined)
|
|
720
|
+
throw new Error(`rederiveAppend: ${catalogueName} is structure-sourced and names no subject column`);
|
|
721
|
+
const sddOf = new Map((modelled.rowsByCatalogue.get(subject.catalogue) ?? []).map((r) => [String(r["id"]), String(r["source_sdd_id"] ?? "")]));
|
|
722
|
+
const left = [];
|
|
723
|
+
added = added.filter((r) => {
|
|
724
|
+
const sdd = sddOf.get(String(r[subject.column] ?? "")) ?? "";
|
|
725
|
+
if (!modelled.modelSdds.has(sdd))
|
|
726
|
+
return true;
|
|
727
|
+
left.push(`${String(r["id"])} (${sdd})`);
|
|
728
|
+
return false;
|
|
729
|
+
});
|
|
730
|
+
if (left.length > 0) {
|
|
731
|
+
left.sort();
|
|
732
|
+
modelled.findings.add("divergence", `${catalogueName}: ${left.length} row(s) derived at aggregate tier left out: the ${subject.column.replace(/_id$/, "")}'s SDD holds a structural model, which states them (M-ADR 0230): ${left.slice(0, 5).join(", ")}${left.length > 5 ? `, and ${left.length - 5} more` : ""}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
698
735
|
rowsByCatalogue.set(catalogueName, [...concatenated, ...added]);
|
|
699
736
|
}
|
|
700
737
|
export function dedupExternalSystems(rowsByCatalogue, findings) {
|
|
@@ -2106,12 +2143,12 @@ export function rekeyLegacyContractRefs(schema, rowsByCatalogue, findings) {
|
|
|
2106
2143
|
rowsByCatalogue.set(cat.name, kept);
|
|
2107
2144
|
for (const [oldId, count] of [...perOldId.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
2108
2145
|
findings.add("inferred", `${cat.name}: ${count} cross-SDD reference(s) to "${oldId}" re-keyed to "${legacyMap.get(oldId)[0]}" — ` +
|
|
2109
|
-
`2.x kindless
|
|
2146
|
+
`an earlier-form contract id (2.x kindless, or a pre-7.0.0 unsplit stem) migrated per the contract-id map (regenerable, not drift; M-ADR 0203 §4.4)`);
|
|
2110
2147
|
}
|
|
2111
2148
|
for (const [oldId, targets] of [...ambiguousOld.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
2112
2149
|
ambiguous++;
|
|
2113
|
-
findings.add("convention", `${cat.name}: references to "${oldId}" map to ${targets.length} contracts
|
|
2114
|
-
`a
|
|
2150
|
+
findings.add("convention", `${cat.name}: references to "${oldId}" map to ${targets.length} current contracts [${targets.join(", ")}] — ` +
|
|
2151
|
+
`a cross-family collision under the earlier form; the referencing SDD must qualify with the current id; left dangling (M-ADR 0203 §4.4)`);
|
|
2115
2152
|
}
|
|
2116
2153
|
}
|
|
2117
2154
|
return { rekeyed, deduped, ambiguous };
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
175
|
-
write(` ${f.catalogue}.${f.column}: row pk=${f.referencingRow} → ${f.target}=${f.value || "<empty>"}
|
|
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
|
-
//
|
|
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
|
|
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
|
|
1198
|
-
|
|
1199
|
-
|
|
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)
|
|
1365
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1427
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
?
|
|
121
|
-
:
|
|
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 =
|
|
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
|
|
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
|
|
28
|
-
//
|
|
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;
|