@sdd-method/maintain-cli 0.13.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.
- package/dist/lib/catalogue/aggregate.js +5 -5
- package/dist/lib/catalogue/build.js +14 -11
- package/dist/lib/catalogue/builders/c4-containers.js +6 -2
- package/dist/lib/catalogue/builders/contracts.js +4 -11
- package/dist/lib/catalogue/builders/entities-manifest.js +10 -4
- 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/service-contract-consumes.js +2 -2
- package/dist/lib/catalogue/builders/service-entity-tables.js +1 -1
- package/dist/lib/catalogue/canonical-schema.yaml +10 -10
- package/dist/lib/catalogue/contract-id.js +49 -12
- package/dist/lib/catalogue/manifest-loader.js +1 -1
- package/dist/lib/catalogue/schema-compat.js +1 -1
- package/dist/lib/validate/dispatch.js +1 -1
- package/dist/lib/validate/journey-impact.js +7 -7
- package/dist/maintain-version.json +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
|
@@ -344,7 +344,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
344
344
|
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
345
|
}
|
|
346
346
|
}
|
|
347
|
-
// M-ADR 0229 §4.4
|
|
347
|
+
// M-ADR 0229 §4.4: journey_requirement re-derived over the merged rows. A journey is often catalogued
|
|
348
348
|
// in an application SDD and the requirements its steps cite in the platform's behaviour store, so neither member
|
|
349
349
|
// holds both join inputs; each reports the tag as external. Each contributor's tagged end-to-end files (its own
|
|
350
350
|
// manifest's journeys.test_paths) and its journey features' living specs are read again here against the MERGED
|
|
@@ -400,7 +400,7 @@ export async function runCatalogueAggregate(inputs, write) {
|
|
|
400
400
|
rederiveAppend(rowsByCatalogue, "journey_requirement", rederived);
|
|
401
401
|
const added = (rowsByCatalogue.get("journey_requirement") ?? []).length - before;
|
|
402
402
|
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
|
|
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; ${contributors.length} contributor(s) read)`);
|
|
404
404
|
}
|
|
405
405
|
for (const u of unresolved)
|
|
406
406
|
findings.add("orphan", `journey_requirement: unresolved against the merged catalogue: ${u}`);
|
|
@@ -2106,12 +2106,12 @@ export function rekeyLegacyContractRefs(schema, rowsByCatalogue, findings) {
|
|
|
2106
2106
|
rowsByCatalogue.set(cat.name, kept);
|
|
2107
2107
|
for (const [oldId, count] of [...perOldId.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
2108
2108
|
findings.add("inferred", `${cat.name}: ${count} cross-SDD reference(s) to "${oldId}" re-keyed to "${legacyMap.get(oldId)[0]}" — ` +
|
|
2109
|
-
`2.x kindless
|
|
2109
|
+
`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
2110
|
}
|
|
2111
2111
|
for (const [oldId, targets] of [...ambiguousOld.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
2112
2112
|
ambiguous++;
|
|
2113
|
-
findings.add("convention", `${cat.name}: references to "${oldId}" map to ${targets.length} contracts
|
|
2114
|
-
`a
|
|
2113
|
+
findings.add("convention", `${cat.name}: references to "${oldId}" map to ${targets.length} current contracts [${targets.join(", ")}] — ` +
|
|
2114
|
+
`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
2115
|
}
|
|
2116
2116
|
}
|
|
2117
2117
|
return { rekeyed, deduped, ambiguous };
|
|
@@ -44,6 +44,8 @@ import { deriveAdrGoverns } from "./builders/adr-governs.js";
|
|
|
44
44
|
import { buildContractRevisions } from "./builders/contract-revisions.js";
|
|
45
45
|
import { buildJourneyRequirement } from "./builders/journey-requirement.js";
|
|
46
46
|
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";
|
|
47
|
+
import { mergeEntities } from "./builders/entity-merge.js";
|
|
48
|
+
import { loadAliasMap } from "./builders/service-entity-tables.js";
|
|
47
49
|
import { buildIntegrationComponents } from "./builders/integration-components.js";
|
|
48
50
|
import { loadReverseProductQuality } from "./reverse-product-quality.js";
|
|
49
51
|
import { loadTraceabilityManifests } from "./traceability-loader.js";
|
|
@@ -895,8 +897,8 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
895
897
|
findings,
|
|
896
898
|
knownServiceIds,
|
|
897
899
|
aliases: serviceContractAliases,
|
|
898
|
-
// M-ADR 0203 §4.4 cut window: alias targets authored as 2.x kindless
|
|
899
|
-
//
|
|
900
|
+
// M-ADR 0203 §4.4 cut window: alias targets authored as 2.x kindless contract ids, or with a pre-7.0.0
|
|
901
|
+
// unsplit stem, re-key against this build's rows.
|
|
900
902
|
legacyContractIdMap: buildLegacyContractIdMap(out.get("contracts") ?? []),
|
|
901
903
|
});
|
|
902
904
|
out.set("service_contract", [...exposesRows, ...consumesRows]);
|
|
@@ -1196,7 +1198,7 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
1196
1198
|
out.set("adrs", deriveAdrGoverns(out.get("adrs") ?? [], out.get("adr_supersedes") ?? [], r.changes, findings));
|
|
1197
1199
|
// M-ADR 0230 §4.2 (Proposed): each contract's revisions by digest, active beside agreed.
|
|
1198
1200
|
out.set("contract_revisions", await buildContractRevisions({ sddPath, findings, contracts: out.get("contracts") ?? [], changes: r.changes }));
|
|
1199
|
-
// M-ADR 0229 §4.4
|
|
1201
|
+
// M-ADR 0229 §4.4: the requirements each journey cites, from its tagged end-to-end files or its
|
|
1200
1202
|
// journey feature's living spec.
|
|
1201
1203
|
out.set("journey_requirement", await buildJourneyRequirement({
|
|
1202
1204
|
sddPath,
|
|
@@ -1229,6 +1231,8 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
1229
1231
|
findings,
|
|
1230
1232
|
knownSystemIds,
|
|
1231
1233
|
knownServiceIds,
|
|
1234
|
+
// A malformed alias file is reported by the service_entity walker; not twice.
|
|
1235
|
+
serviceIdByDisplayLower: (await loadAliasMap(sddPath, new FindingsLog())).byDisplayLower,
|
|
1232
1236
|
knownDomainIds,
|
|
1233
1237
|
containerIdByEntryIdOut: containerIdByEntryId,
|
|
1234
1238
|
containerNameByEntryIdOut: containerNameByEntryId,
|
|
@@ -1343,12 +1347,14 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
1343
1347
|
// c4_containers.service_id.
|
|
1344
1348
|
// - c4-system-entity: aggregation roll-up over c4_container_entity ⋈
|
|
1345
1349
|
// c4_containers.system_id with container_count.
|
|
1350
|
+
const platformWideEntityIds = new Set();
|
|
1346
1351
|
const entitiesFromManifest = await buildEntitiesFromManifest({
|
|
1347
1352
|
sddPath,
|
|
1348
1353
|
findings,
|
|
1349
1354
|
profile: manifest.profile,
|
|
1350
1355
|
knownDomainIds,
|
|
1351
1356
|
knownGlossaryIds,
|
|
1357
|
+
platformWideIdsOut: platformWideEntityIds,
|
|
1352
1358
|
});
|
|
1353
1359
|
const entitiesFromMermaid = await buildEntitiesFromMermaid({
|
|
1354
1360
|
sddPath,
|
|
@@ -1361,13 +1367,8 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
1361
1367
|
knownGlossaryIds,
|
|
1362
1368
|
});
|
|
1363
1369
|
// 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()];
|
|
1370
|
+
// per M-ADR 0159 §4.4), field by field; a platform-wide entry naming one domain entity is that entity.
|
|
1371
|
+
const entityRows = mergeEntities(entitiesFromMermaid, entitiesFromManifest, findings, platformWideEntityIds);
|
|
1371
1372
|
out.set("entities", entityRows);
|
|
1372
1373
|
// Per M-ADR 0159 §4.9: now that entities.csv exists, append entity_glossary
|
|
1373
1374
|
// rows for the reverse-method side. The forward-method emission via
|
|
@@ -1391,7 +1392,9 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
1391
1392
|
domain_id: domainId,
|
|
1392
1393
|
entity_name: name,
|
|
1393
1394
|
glossary_item_id: glossaryId,
|
|
1394
|
-
|
|
1395
|
+
// A manifest entry folded into this entity keeps the alignment it was authored with; a data-model slug match
|
|
1396
|
+
// is direct.
|
|
1397
|
+
status: /(?:^|;)status=aligned(?:;|$)/.test(String(e["notes"] ?? "")) ? "aligned" : "matched_direct",
|
|
1395
1398
|
source_sdd_id: "",
|
|
1396
1399
|
});
|
|
1397
1400
|
}
|
|
@@ -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
|
: "";
|
|
@@ -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.
|
|
@@ -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;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// entities: the data-model walker's rows merged with the glossary-alignment manifest's (M-ADR 0159 §4.4).
|
|
2
|
+
//
|
|
3
|
+
// Field by field: a manifest value wins where it is non-empty, and the data model's value stands where the
|
|
4
|
+
// manifest is silent, so source_domain_model_id and the columns= note survive a manifest entry naming the
|
|
5
|
+
// same entity. Notes from both are kept.
|
|
6
|
+
//
|
|
7
|
+
// A platform-wide manifest entry (`_platform-wide-<slug>`, M-ADR 0159 §4.2) whose slug names exactly one
|
|
8
|
+
// domain entity (`<domain>-<slug>`) IS that entity: it fills the domain entity's empty fields and emits no row
|
|
9
|
+
// of its own, so one entity is one element. Its glossary term moves with it, and the entity_glossary step
|
|
10
|
+
// derives the edge from the domain entity. An entry matching no domain entity, or several, stays platform-wide
|
|
11
|
+
// and is reported.
|
|
12
|
+
import { PLATFORM_WIDE_PK_PREFIX } from "./entities-manifest.js";
|
|
13
|
+
const MANIFEST_REL_PATH = "glossary-alignment-manifest.yaml";
|
|
14
|
+
function filled(v) {
|
|
15
|
+
return v !== undefined && v !== null && String(v) !== "";
|
|
16
|
+
}
|
|
17
|
+
function joinNotes(a, b) {
|
|
18
|
+
const parts = [];
|
|
19
|
+
for (const v of [a, b]) {
|
|
20
|
+
for (const p of String(v ?? "").split(";"))
|
|
21
|
+
if (p !== "" && !parts.includes(p))
|
|
22
|
+
parts.push(p);
|
|
23
|
+
}
|
|
24
|
+
return parts.join(";");
|
|
25
|
+
}
|
|
26
|
+
/** `over`'s non-empty values on top of `base`; notes from both. */
|
|
27
|
+
function overlay(base, over) {
|
|
28
|
+
const out = { ...base };
|
|
29
|
+
for (const [k, v] of Object.entries(over)) {
|
|
30
|
+
if (k === "notes")
|
|
31
|
+
out[k] = joinNotes(base[k], v);
|
|
32
|
+
else if (filled(v))
|
|
33
|
+
out[k] = v;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/** `extra`'s values only where `base` is empty; notes from both. */
|
|
38
|
+
function fillEmpty(base, extra) {
|
|
39
|
+
const out = { ...base };
|
|
40
|
+
for (const [k, v] of Object.entries(extra)) {
|
|
41
|
+
if (k === "id" || k === "name" || k === "domain_id")
|
|
42
|
+
continue;
|
|
43
|
+
if (k === "notes")
|
|
44
|
+
out[k] = joinNotes(base[k], v);
|
|
45
|
+
else if (!filled(base[k]) && filled(v))
|
|
46
|
+
out[k] = v;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
export function mergeEntities(fromDataModels, fromManifest, findings,
|
|
51
|
+
/** The sentinel ids from a platform-wide domain hint (entities-manifest's out-set): the only ones that fold. */
|
|
52
|
+
platformWideIds) {
|
|
53
|
+
const byId = new Map();
|
|
54
|
+
for (const r of fromDataModels)
|
|
55
|
+
byId.set(String(r["id"]), r);
|
|
56
|
+
for (const r of fromManifest) {
|
|
57
|
+
const id = String(r["id"]);
|
|
58
|
+
const existing = byId.get(id);
|
|
59
|
+
byId.set(id, existing ? overlay(existing, r) : r);
|
|
60
|
+
}
|
|
61
|
+
const prefix = `${PLATFORM_WIDE_PK_PREFIX}-`;
|
|
62
|
+
for (const [id, row] of [...byId]) {
|
|
63
|
+
if (!id.startsWith(prefix) || !platformWideIds.has(id))
|
|
64
|
+
continue;
|
|
65
|
+
const slug = id.slice(prefix.length);
|
|
66
|
+
const matches = [...byId.values()].filter((e) => {
|
|
67
|
+
const dom = String(e["domain_id"] ?? "");
|
|
68
|
+
return dom !== "" && String(e["id"]) === `${dom}-${slug}`;
|
|
69
|
+
});
|
|
70
|
+
if (matches.length === 1) {
|
|
71
|
+
const target = matches[0];
|
|
72
|
+
const own = String(target["glossary_term"] ?? "");
|
|
73
|
+
const theirs = String(row["glossary_term"] ?? "");
|
|
74
|
+
if (own !== "" && theirs !== "" && own !== theirs) {
|
|
75
|
+
findings.add("divergence", `${MANIFEST_REL_PATH}: platform-wide entity ${row["name"]} names glossary term ${theirs}, but ${target["id"]} ` +
|
|
76
|
+
`already names ${own} — kept ${own}`);
|
|
77
|
+
}
|
|
78
|
+
byId.set(String(target["id"]), fillEmpty(target, row));
|
|
79
|
+
byId.delete(id);
|
|
80
|
+
}
|
|
81
|
+
else if (matches.length === 0) {
|
|
82
|
+
findings.add("coverage", `${MANIFEST_REL_PATH}: platform-wide entity ${row["name"]} names no domain entity — kept as ${id}`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
findings.add("divergence", `${MANIFEST_REL_PATH}: platform-wide entity ${row["name"]} names ${matches.length} domain entities (${matches.map((m) => m["id"]).join(", ")}) — kept as ${id}; give it a domain`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...byId.values()];
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=entity-merge.js.map
|
|
@@ -158,7 +158,7 @@ export function featureStatus(text) {
|
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
160
|
* A feature spec's kind (schema 4.4.0): the value of its `**Kind:**` line, lower-cased, else `feature`. An
|
|
161
|
-
* obligation (M-ADR 0226, Proposed) and a journey feature (M-ADR 0229 §4.2
|
|
161
|
+
* obligation (M-ADR 0226, Proposed) and a journey feature (M-ADR 0229 §4.2) say so; any other value is written as given, so
|
|
162
162
|
* the catalogue's vocabulary gate names the spec rather than a default hiding the mistake.
|
|
163
163
|
*/
|
|
164
164
|
export function featureKind(text) {
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
// 4. Forward-method supersedes (new).
|
|
28
28
|
// models[].entities[] with status: superseded + superseded_by
|
|
29
29
|
// One row per supersedes mapping after resolving both endpoints'
|
|
30
|
-
// glossary_term against glossary_items.csv
|
|
31
|
-
//
|
|
30
|
+
// glossary_term against glossary_items.csv: the successor (superseded_by)
|
|
31
|
+
// is the source and supersedes the superseded term, the target. Orphan
|
|
32
|
+
// finding when an endpoint doesn't resolve.
|
|
32
33
|
//
|
|
33
34
|
// 5. Forward-method entity-mapping (new, writes to entity_glossary).
|
|
34
35
|
// models[].entities[] with status ∈ {aligned, matched_direct} and
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
import { readFile } from "node:fs/promises";
|
|
41
42
|
import { join } from "node:path";
|
|
42
43
|
import { parse as parseYaml } from "yaml";
|
|
44
|
+
import { NON_CANONICAL_DOMAINS } from "./entities-manifest.js";
|
|
43
45
|
const MANIFEST_REL_PATH = join("docs", "platform", "reference", "glossary-alignment-manifest.yaml");
|
|
44
46
|
export async function buildGlossaryXref(inputs) {
|
|
45
47
|
// Profile gate per M-ADR 0158 §amendment: the manifest is platform-owned;
|
|
@@ -259,14 +261,18 @@ export async function buildGlossaryXref(inputs) {
|
|
|
259
261
|
inputs.findings.add("orphan", `${MANIFEST_REL_PATH}: supersedes edge can't emit — target "${supersededBy}" (→ glossary "${targetEntity?.glossaryTerm ?? "?"}") doesn't resolve to a glossary item`);
|
|
260
262
|
continue;
|
|
261
263
|
}
|
|
262
|
-
|
|
264
|
+
// The successor supersedes the superseded term: `source supersedes target`, as adr_supersedes reads.
|
|
265
|
+
emitXref(targetId, sourceId, "supersedes");
|
|
263
266
|
continue;
|
|
264
267
|
}
|
|
265
268
|
if ((status === "aligned" || status === "matched_direct") && glossaryTerm !== "") {
|
|
266
269
|
// Entity-mapping walker writes to entity_glossary.csv.
|
|
267
270
|
if (domain === "" || !inputs.knownDomainIds.has(domain)) {
|
|
268
|
-
//
|
|
269
|
-
//
|
|
271
|
+
// A platform-wide entry is the domain entity its slug names (entity-merge.ts), and the entity_glossary
|
|
272
|
+
// edge is derived from that entity after the merge; the merge reports one it cannot place. Any other
|
|
273
|
+
// domain not in this SDD's domains.csv is skipped with a coverage finding.
|
|
274
|
+
if (NON_CANONICAL_DOMAINS.has(domain))
|
|
275
|
+
continue;
|
|
270
276
|
inputs.findings.add("coverage", `${MANIFEST_REL_PATH}: entity ${name} domain "${domain}" not in known domains — entity_glossary row skipped`);
|
|
271
277
|
continue;
|
|
272
278
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// journey_requirement: the requirements a user journey cites for its steps (M-ADR 0229 §4.4
|
|
1
|
+
// journey_requirement: the requirements a user journey cites for its steps (M-ADR 0229 §4.4; schema 4.4.0).
|
|
2
2
|
//
|
|
3
3
|
// Derived, never authored, from either master (§4.2):
|
|
4
4
|
// tag option A: the end-to-end `.feature` files under the manifest's `journeys.test_paths`. A file tagged
|
|
@@ -64,13 +64,13 @@ export async function buildJourneyRequirement(inputs) {
|
|
|
64
64
|
for (const m of text.matchAll(REQ_TAG)) {
|
|
65
65
|
const quiet = atAggregate && inputs.reportedFeatureIds.has(m[1]);
|
|
66
66
|
if (!knownFeatures.has(m[1])) {
|
|
67
|
-
findings.add("external", `${where}: @req:${m[1]}/${m[2]} names a feature this catalogue does not hold; resolves at aggregation (M-ADR 0229 §4.4
|
|
67
|
+
findings.add("external", `${where}: @req:${m[1]}/${m[2]} names a feature this catalogue does not hold; resolves at aggregation (M-ADR 0229 §4.4)`);
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
70
|
const ids = bySlug.get(`${m[1]}/${m[2]}`) ?? [];
|
|
71
71
|
if (ids.length !== 1) {
|
|
72
72
|
if (!quiet)
|
|
73
|
-
findings.add("orphan", `${where}: @req:${m[1]}/${m[2]} ${ids.length === 0 ? "names no requirement of that feature" : `matches ${ids.length} requirements of that feature`}; no journey_requirement row (M-ADR 0229 §4.4
|
|
73
|
+
findings.add("orphan", `${where}: @req:${m[1]}/${m[2]} ${ids.length === 0 ? "names no requirement of that feature" : `matches ${ids.length} requirements of that feature`}; no journey_requirement row (M-ADR 0229 §4.4)`);
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
76
76
|
const id = `${journeyId}__${ids[0]}`;
|
|
@@ -95,7 +95,7 @@ export async function buildJourneyRequirement(inputs) {
|
|
|
95
95
|
continue;
|
|
96
96
|
if (journeys.length > 1) {
|
|
97
97
|
if (!atAggregate)
|
|
98
|
-
findings.add("divergence", `${where}: tagged with ${journeys.length} journeys (${journeys.join(", ")}); one file is one journey's master (M-ADR 0229 §4.2
|
|
98
|
+
findings.add("divergence", `${where}: tagged with ${journeys.length} journeys (${journeys.join(", ")}); one file is one journey's master (M-ADR 0229 §4.2)`);
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
101
101
|
const journeyId = journeys[0];
|
|
@@ -129,7 +129,7 @@ export async function buildJourneyRequirement(inputs) {
|
|
|
129
129
|
const journeys = [...new Set([...spec.matchAll(JOURNEY_TAG)].map((m) => m[1]))];
|
|
130
130
|
if (journeys.length !== 1) {
|
|
131
131
|
if (!atAggregate)
|
|
132
|
-
findings.add(journeys.length === 0 ? "orphan" : "divergence", `${where}: a journey feature's living spec ${journeys.length === 0 ? "names no journey (@journey:<journey-id>)" : `names ${journeys.length} journeys`}; one journey feature masters one journey (M-ADR 0229 §4.2
|
|
132
|
+
findings.add(journeys.length === 0 ? "orphan" : "divergence", `${where}: a journey feature's living spec ${journeys.length === 0 ? "names no journey (@journey:<journey-id>)" : `names ${journeys.length} journeys`}; one journey feature masters one journey (M-ADR 0229 §4.2)`);
|
|
133
133
|
continue;
|
|
134
134
|
}
|
|
135
135
|
const journeyId = journeys[0];
|
|
@@ -59,13 +59,13 @@ export async function buildServiceContractConsumes(inputs) {
|
|
|
59
59
|
const mapped = inputs.legacyContractIdMap?.get(contractId);
|
|
60
60
|
if (mapped !== undefined && mapped.length === 1) {
|
|
61
61
|
inputs.findings.add("inferred", `service_contract: alias target "${contractId}" (${serviceId} ← "${iface.interfaceText}") re-keyed to ` +
|
|
62
|
-
`"${mapped[0]}" — 2.x kindless
|
|
62
|
+
`"${mapped[0]}" — 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); ` +
|
|
63
63
|
`update .sdd/service-contract-aliases.yaml to retire this finding`);
|
|
64
64
|
contractId = mapped[0];
|
|
65
65
|
}
|
|
66
66
|
else if (mapped !== undefined && mapped.length > 1) {
|
|
67
67
|
inputs.findings.add("convention", `service_contract: alias target "${contractId}" (${serviceId} ← "${iface.interfaceText}") maps to ` +
|
|
68
|
-
`${mapped.length} contracts
|
|
68
|
+
`${mapped.length} current contracts [${mapped.join(", ")}] — a cross-family collision under the earlier form; ` +
|
|
69
69
|
`qualify the alias with the kind-suffixed id; edge dropped`);
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
@@ -128,7 +128,7 @@ export async function buildServiceEntityFromTables(inputs) {
|
|
|
128
128
|
}
|
|
129
129
|
return [...rowsByPk.values()];
|
|
130
130
|
}
|
|
131
|
-
async function loadAliasMap(sddPath, findings) {
|
|
131
|
+
export async function loadAliasMap(sddPath, findings) {
|
|
132
132
|
const empty = { byDisplayLower: new Map() };
|
|
133
133
|
const path = join(sddPath, ALIAS_REL_PATH);
|
|
134
134
|
let text;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# Emitted by sdd-cli's YAML emitter with no line folding; the three header scalars are quoted for the
|
|
8
8
|
# textual readers downstream. The one deliberate difference from the authored form is schema_status:
|
|
9
9
|
# the authored form says canonical; a projection never does.
|
|
10
|
-
schema_version: "
|
|
10
|
+
schema_version: "7.0.0"
|
|
11
11
|
schema_owner: "sdd-method"
|
|
12
12
|
schema_status: "generated"
|
|
13
13
|
enums:
|
|
@@ -685,15 +685,15 @@ enums:
|
|
|
685
685
|
- value: obligation
|
|
686
686
|
description: Behaviour every feature in a population depends on, owned as a feature of its own (M-ADR 0226, Proposed).
|
|
687
687
|
- value: journey
|
|
688
|
-
description: A user journey's joint behaviour, held in the behaviour store (M-ADR 0229 §4.2, option B
|
|
689
|
-
description: What a catalogued feature is (M-ADR 0226 and M-ADR 0229
|
|
688
|
+
description: A user journey's joint behaviour, held in the behaviour store (M-ADR 0229 §4.2, option B).
|
|
689
|
+
description: What a catalogued feature is (M-ADR 0226, Proposed, and M-ADR 0229).
|
|
690
690
|
journey_requirement_basis:
|
|
691
691
|
values:
|
|
692
692
|
- value: tag
|
|
693
693
|
description: A @req tag in the journey's end-to-end test file (option A).
|
|
694
694
|
- value: citation
|
|
695
695
|
description: A citation in the journey feature's living spec (option B).
|
|
696
|
-
description: Where a journey's citation of a requirement was read (M-ADR 0229 §4.4
|
|
696
|
+
description: Where a journey's citation of a requirement was read (M-ADR 0229 §4.4).
|
|
697
697
|
source_of_truth:
|
|
698
698
|
values:
|
|
699
699
|
- value: structure
|
|
@@ -1318,7 +1318,7 @@ catalogues:
|
|
|
1318
1318
|
- name: relation
|
|
1319
1319
|
type: string
|
|
1320
1320
|
nullable: false
|
|
1321
|
-
description: alias-of | conflicts-with | references | supersedes (extended in schema 2.12.0 per M-ADR 0158).
|
|
1321
|
+
description: "alias-of | conflicts-with | references | supersedes (extended in schema 2.12.0 per M-ADR 0158). Read as source, relation, target: for supersedes the successor is the source and the superseded term the target."
|
|
1322
1322
|
description: "Edge: cross-domain glossary relationships from glossary-alignment-manifest.yaml. Per M-ADR 0158, four walkers ingest into this catalogue: (a) forward declared pair-form {source, target} entries under aliases/conflicts, (b) reverse-method cross_domain_terms (emits references edges per referencer→owner), (c) reverse-method multi-domain conflicts (pair-wise expansion across domains[]), (d) forward-method supersedes (status: superseded entries with superseded_by back-link)."
|
|
1323
1323
|
natural_key:
|
|
1324
1324
|
- source_id
|
|
@@ -1646,7 +1646,7 @@ catalogues:
|
|
|
1646
1646
|
- name: kind
|
|
1647
1647
|
type: string
|
|
1648
1648
|
nullable: false
|
|
1649
|
-
description: "Bound to feature_kind: feature (the default), obligation (M-ADR 0226, Proposed) or journey (M-ADR 0229 §4.2
|
|
1649
|
+
description: "Bound to feature_kind: feature (the default), obligation (M-ADR 0226, Proposed) or journey (M-ADR 0229 §4.2), from the feature spec's **Kind:** line."
|
|
1650
1650
|
- name: source_sdd_id
|
|
1651
1651
|
type: string
|
|
1652
1652
|
nullable: true
|
|
@@ -1760,7 +1760,7 @@ catalogues:
|
|
|
1760
1760
|
- name: id
|
|
1761
1761
|
type: string
|
|
1762
1762
|
nullable: false
|
|
1763
|
-
description: "{domain}-{stem}-{kind} — the kind component joined the id at schema 3.0.0 (M-ADR 0203 §4.4), ending the kindless collision class where one service published as two contract families (openapi + proto) minted one id (measured cost: 1 row + 2 edges permanently unrepresentable, plus legitimate shared-contract citation flagged as divergence). Mechanical migration: new id = old id + '-' + kind; `sdd-cli catalogue migration-map` emits the pairs, and authored alias targets (M-ADR 0147) re-key automatically during the cut window."
|
|
1763
|
+
description: "{domain}-{stem}-{kind} — the kind component joined the id at schema 3.0.0 (M-ADR 0203 §4.4), ending the kindless collision class where one service published as two contract families (openapi + proto) minted one id (measured cost: 1 row + 2 edges permanently unrepresentable, plus legitimate shared-contract citation flagged as divergence). Mechanical migration: new id = old id + '-' + kind; `sdd-cli catalogue migration-map` emits the pairs, and authored alias targets (M-ADR 0147) re-key automatically during the cut window. From schema 7.0.0 the stem is split at its CamelCase boundaries as an entity name is (`OrderPlaced` → `order-placed`), and references to the unsplit form re-key the same way, from id, kind and spec_path."
|
|
1764
1764
|
- name: name
|
|
1765
1765
|
type: string
|
|
1766
1766
|
nullable: false
|
|
@@ -1946,7 +1946,7 @@ catalogues:
|
|
|
1946
1946
|
- name: service_id
|
|
1947
1947
|
type: string
|
|
1948
1948
|
nullable: true
|
|
1949
|
-
description: Matched by container name
|
|
1949
|
+
description: Matched to a service id by the container's diagram id or display-name slug, then by its display name through docs/platform/reference/service-name-aliases.yaml.
|
|
1950
1950
|
- name: type
|
|
1951
1951
|
type: string
|
|
1952
1952
|
nullable: true
|
|
@@ -2211,7 +2211,7 @@ catalogues:
|
|
|
2211
2211
|
- name: id
|
|
2212
2212
|
type: string
|
|
2213
2213
|
nullable: false
|
|
2214
|
-
description: "Slugged PK: {domain_id}-{entitySlug(name)}.
|
|
2214
|
+
description: "Slugged PK: {domain_id}-{entitySlug(name)}. A glossary-manifest entry with domain=platform-wide is the domain entity its slug names when exactly one does, and adds no row of its own; one naming none, or several, is kept as `_platform-wide-{entitySlug(name)}` with a finding. A superseded manifest entry is no entity: glossary_xref holds its supersedes edge."
|
|
2215
2215
|
- name: name
|
|
2216
2216
|
type: string
|
|
2217
2217
|
nullable: false
|
|
@@ -4904,7 +4904,7 @@ catalogues:
|
|
|
4904
4904
|
nullable: true
|
|
4905
4905
|
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4906
4906
|
source_of_truth: execution
|
|
4907
|
-
description: "Edge: a requirement a user journey cites for one of its steps (M-ADR 0229 §4.4
|
|
4907
|
+
description: "Edge: a requirement a user journey cites for one of its steps (M-ADR 0229 §4.4). Derived, never authored: from the @req tags in the journey's end-to-end test file, or from the citations in its journey feature's living spec. A journey with no row is untraced."
|
|
4908
4908
|
foreign_keys:
|
|
4909
4909
|
- column: journey_id
|
|
4910
4910
|
references: user_journeys.id
|
|
@@ -13,26 +13,46 @@
|
|
|
13
13
|
// `sdd-cli catalogue migration-map`; the alias-file re-key (M-ADR 0147 targets
|
|
14
14
|
// are authored copies of tool-minted ids — regenerable, not drift, the same
|
|
15
15
|
// rule as external_system_exemption) consults the in-build form of it.
|
|
16
|
+
/** A contract id's kindless slug (catalogue schema 7.0.0): CamelCase split, lower-cased, non-alphanumerics to `-`. */
|
|
17
|
+
export function contractSlug(s) {
|
|
18
|
+
return legacySlug(s.replace(/([A-Z]{2,})([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2"));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A contract id's kindless slug from its domain segment and file stem: only the stem is CamelCase-split, so a
|
|
22
|
+
* domain directory keeps the slug it always had.
|
|
23
|
+
*/
|
|
24
|
+
export function contractKindlessSlug(domain, stem) {
|
|
25
|
+
if (domain === "")
|
|
26
|
+
return contractSlug(stem);
|
|
27
|
+
return [legacySlug(domain), contractSlug(stem)].filter((p) => p !== "").join("-");
|
|
28
|
+
}
|
|
29
|
+
/** The pre-7.0.0 slug: the same, without the CamelCase split. */
|
|
30
|
+
function legacySlug(s) {
|
|
31
|
+
return s
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/^_+/, "") // turn _common into common
|
|
34
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
35
|
+
.replace(/^-+|-+$/g, "");
|
|
36
|
+
}
|
|
37
|
+
function stemOfPath(specPath) {
|
|
38
|
+
const base = specPath.split(/[\\/]/).pop() ?? "";
|
|
39
|
+
return base.replace(/\.[^.]+$/, "");
|
|
40
|
+
}
|
|
16
41
|
/** The 3.0.0 contract id: the kindless slug with its kind appended. */
|
|
17
42
|
export function contractIdWithKind(kindlessSlug, kind) {
|
|
18
43
|
return kindlessSlug === "" ? "" : `${kindlessSlug}-${kind}`;
|
|
19
44
|
}
|
|
20
45
|
/**
|
|
21
|
-
* legacy
|
|
22
|
-
*
|
|
23
|
-
* under
|
|
24
|
-
*
|
|
46
|
+
* legacy id → the current ids it maps to, from built rows: the 2.x kindless id (→ 3.0.0), and the pre-7.0.0
|
|
47
|
+
* unsplit-stem id, with and without its kind. More than one target means the id was a measured collision
|
|
48
|
+
* under the old form (two families sharing one slug) — the consumer decides which family is meant, loudly,
|
|
49
|
+
* never by guess.
|
|
25
50
|
*/
|
|
26
51
|
export function buildLegacyContractIdMap(rows) {
|
|
27
52
|
const map = new Map();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (id === "" || kind === "" || !id.endsWith(`-${kind}`))
|
|
32
|
-
continue;
|
|
33
|
-
const legacy = id.slice(0, -(kind.length + 1));
|
|
34
|
-
if (legacy === "")
|
|
35
|
-
continue;
|
|
53
|
+
const add = (legacy, id) => {
|
|
54
|
+
if (legacy === "" || legacy === id)
|
|
55
|
+
return;
|
|
36
56
|
const ids = map.get(legacy);
|
|
37
57
|
if (ids) {
|
|
38
58
|
if (!ids.includes(id))
|
|
@@ -41,6 +61,23 @@ export function buildLegacyContractIdMap(rows) {
|
|
|
41
61
|
else {
|
|
42
62
|
map.set(legacy, [id]);
|
|
43
63
|
}
|
|
64
|
+
};
|
|
65
|
+
for (const row of rows) {
|
|
66
|
+
const id = String(row["id"] ?? "");
|
|
67
|
+
const kind = String(row["kind"] ?? "");
|
|
68
|
+
if (id === "" || kind === "" || !id.endsWith(`-${kind}`))
|
|
69
|
+
continue;
|
|
70
|
+
const kindless = id.slice(0, -(kind.length + 1));
|
|
71
|
+
add(kindless, id);
|
|
72
|
+
// 7.0.0: the stem's segment as it read before the CamelCase split.
|
|
73
|
+
const stem = stemOfPath(String(row["spec_path"] ?? ""));
|
|
74
|
+
const split = contractSlug(stem);
|
|
75
|
+
const unsplit = legacySlug(stem);
|
|
76
|
+
if (split === "" || split === unsplit || !kindless.endsWith(split))
|
|
77
|
+
continue;
|
|
78
|
+
const oldKindless = kindless.slice(0, -split.length) + unsplit;
|
|
79
|
+
add(oldKindless, id);
|
|
80
|
+
add(`${oldKindless}-${kind}`, id);
|
|
44
81
|
}
|
|
45
82
|
for (const ids of map.values())
|
|
46
83
|
ids.sort((a, b) => a.localeCompare(b));
|
|
@@ -218,7 +218,7 @@ const ManifestSchema = z.object({
|
|
|
218
218
|
// Shared-library trait (sdd-manifest-schema 1.12.0, M-ADR 0217 §4.4). Profile-agnostic,
|
|
219
219
|
// like `exemplar`; absent is the unmarked norm, no finding.
|
|
220
220
|
shared_library: z.boolean().optional(),
|
|
221
|
-
// Where this estate's journey behaviour is mastered (sdd-manifest-schema 1.13.0, M-ADR 0229 §4.2
|
|
221
|
+
// Where this estate's journey behaviour is mastered (sdd-manifest-schema 1.13.0, M-ADR 0229 §4.2).
|
|
222
222
|
// `master` is declared at the root, and journey-impact treats a member naming its platform as inheriting it.
|
|
223
223
|
// `test_paths` is never inherited: it is relative to this SDD, so an SDD that holds end-to-end files declares
|
|
224
224
|
// its own block, with the master and its paths. Unknown values fail at load.
|
|
@@ -37,7 +37,7 @@ const BEHAVIOUR_STORE_VALIDATORS = [
|
|
|
37
37
|
source: { kind: "native", run: async (ctx) => checkFeatureSpecPointers(ctx.targetRepo) },
|
|
38
38
|
build: () => ({ status: "run", args: [] }),
|
|
39
39
|
},
|
|
40
|
-
// The journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5
|
|
40
|
+
// The journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5). Report only; [N/A] without journeys.
|
|
41
41
|
{
|
|
42
42
|
name: "journey-impact",
|
|
43
43
|
source: { kind: "native", run: async (ctx) => checkJourneyImpact(ctx.targetRepo) },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// journey-impact: the journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5
|
|
2
|
-
// Report only (M-ADR 0229 §4.5
|
|
1
|
+
// journey-impact: the journeys a change can break, and whether its proposal names them (M-ADR 0229 §4.5).
|
|
2
|
+
// Report only (M-ADR 0229 §4.5): every finding is a [WARN] and the check exits 0. Gate 0 carries this report; nothing
|
|
3
3
|
// blocks on it, and the judge does not fail a change for it.
|
|
4
4
|
//
|
|
5
5
|
// For each open change in the behaviour store (openspec/changes/<change>/, not the archive):
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// with no reason is reported too.
|
|
15
15
|
//
|
|
16
16
|
// Also reported: a journey citing a requirement of a feature its journey_feature rows do not name (one record or
|
|
17
|
-
// the other is wrong, M-ADR 0229 §4.4
|
|
18
|
-
// platform it inherits from (an unresolved root, M-ADR 0229 §4.2
|
|
17
|
+
// the other is wrong, M-ADR 0229 §4.4); and an SDD with catalogued journeys whose manifest declares no journey master and that names no
|
|
18
|
+
// platform it inherits from (an unresolved root, M-ADR 0229 §4.2). A member that names its platform inherits the
|
|
19
19
|
// root's declaration, which is checked at the root.
|
|
20
20
|
//
|
|
21
21
|
// Reads the aggregated catalogue (catalogues-aggregated/*.csv) where this SDD has one, since a root's changes can
|
|
@@ -100,7 +100,7 @@ export async function checkJourneyImpact(repo) {
|
|
|
100
100
|
else if (platform)
|
|
101
101
|
lines.push(`journey master: not declared here; inherited from platform ${platform}, where it is checked`);
|
|
102
102
|
else
|
|
103
|
-
warnings.push(`.sdd/sdd-manifest.yaml: ${journeys.length} catalogued journey(s) and no journey master (journeys.master: test-file or store), and no platform to inherit one from: unresolved (M-ADR 0229 §4.2
|
|
103
|
+
warnings.push(`.sdd/sdd-manifest.yaml: ${journeys.length} catalogued journey(s) and no journey master (journeys.master: test-file or store), and no platform to inherit one from: unresolved (M-ADR 0229 §4.2)`);
|
|
104
104
|
const jr = await csv(repo, "journey_requirement", dir);
|
|
105
105
|
const jf = (await csv(repo, "journey_feature", dir)) ?? [];
|
|
106
106
|
const featureKinds = new Map(((await csv(repo, "features", dir)) ?? []).map((f) => [f["id"], f["kind"] ?? ""]));
|
|
@@ -137,7 +137,7 @@ export async function checkJourneyImpact(repo) {
|
|
|
137
137
|
if (touched.get(r["journey_id"])?.has(feature) || mismatched.has(key))
|
|
138
138
|
continue;
|
|
139
139
|
mismatched.add(key);
|
|
140
|
-
warnings.push(`journey ${r["journey_id"]} cites requirements of ${feature}, which its journey_feature rows do not name: the journey's related features or its tags are wrong (M-ADR 0229 §4.4
|
|
140
|
+
warnings.push(`journey ${r["journey_id"]} cites requirements of ${feature}, which its journey_feature rows do not name: the journey's related features or its tags are wrong (M-ADR 0229 §4.4)`);
|
|
141
141
|
}
|
|
142
142
|
const untracedCount = journeys.filter((j) => !traced.has(j["id"])).length;
|
|
143
143
|
lines.push(`journeys: ${journeys.length} catalogued, ${journeys.length - untracedCount} traced to requirements, ${untracedCount} untraced`);
|
|
@@ -203,6 +203,6 @@ export async function checkJourneyImpact(repo) {
|
|
|
203
203
|
const body = lines.map((l) => `${l}\n`).join("");
|
|
204
204
|
if (warnings.length === 0)
|
|
205
205
|
return { exitCode: 0, stdout: `journey-impact:\n${body}[PASS] journey-impact: every listed journey is named\n` };
|
|
206
|
-
return { exitCode: 0, stdout: `journey-impact:\n${body}${warnings.map((w) => `[WARN] ${w}\n`).join("")}[WARN] journey-impact: ${warnings.length} finding(s), report only (M-ADR 0229 §4.5
|
|
206
|
+
return { exitCode: 0, stdout: `journey-impact:\n${body}${warnings.map((w) => `[WARN] ${w}\n`).join("")}[WARN] journey-impact: ${warnings.length} finding(s), report only (M-ADR 0229 §4.5)\n` };
|
|
207
207
|
}
|
|
208
208
|
//# sourceMappingURL=journey-impact.js.map
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/maintain-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"sddCliVersion": "
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"sddCliVersion": "5.0.0",
|
|
5
5
|
"description": "Bring an SDD repository current and rebuild its derived model — sync, briefing, catalogue build and aggregate, pack build, context pull — plus every read-only verify verb. Ships only the code those verbs reach; it cannot author specification content. Authoring is in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|