@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
|
@@ -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
|
|
@@ -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];
|
|
@@ -119,7 +119,7 @@ const num = (v) => {
|
|
|
119
119
|
return null;
|
|
120
120
|
};
|
|
121
121
|
/**
|
|
122
|
-
* The review's other assessed inputs (M-ADR 0230 §4.9
|
|
122
|
+
* The review's other assessed inputs (M-ADR 0230 §4.9): `inputs:` as a list of paths, or of `{path}`
|
|
123
123
|
* entries, each read at the review's corpus_ref. ';'-joined; null when the review names none.
|
|
124
124
|
*/
|
|
125
125
|
function inputPaths(v) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// change_work_order per (change, repository): the record's work order, agreed or completed.
|
|
19
19
|
// change_work_item per (change, work item): the ids its proposal's `## Work Items` section lists, keyed by
|
|
20
20
|
// the undated name so the row survives archive.
|
|
21
|
-
// requirement_revisions one row per distinct text of a requirement (schema 6.0.0, M-ADR 0230 §4.7
|
|
21
|
+
// requirement_revisions one row per distinct text of a requirement (schema 6.0.0, M-ADR 0230 §4.7): the
|
|
22
22
|
// living spec's is current, an agreed open change's ADDED or MODIFIED block intended, an
|
|
23
23
|
// archived change's block the living spec no longer holds history. Identified by its digest.
|
|
24
24
|
// requirement_assertions per (revision, repository): observed from a baseline's `**Observed in:**` line;
|
|
@@ -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;
|