@sdd-method/author-cli 0.10.0 → 0.12.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/behaviour-store/archive.js +13 -0
- package/dist/lib/catalogue/build.js +45 -2
- package/dist/lib/catalogue/builders/adr-governs.js +39 -0
- package/dist/lib/catalogue/builders/adrs.js +11 -0
- package/dist/lib/catalogue/builders/capability-spec.js +1 -0
- package/dist/lib/catalogue/builders/contract-revisions.js +126 -0
- package/dist/lib/catalogue/builders/data-model.js +316 -0
- package/dist/lib/catalogue/builders/entities-mermaid.js +18 -85
- package/dist/lib/catalogue/builders/feature-grounding.js +1 -0
- package/dist/lib/catalogue/builders/features.js +23 -2
- package/dist/lib/catalogue/builders/integration-components.js +49 -5
- package/dist/lib/catalogue/builders/journey-requirement.js +144 -0
- package/dist/lib/catalogue/builders/quality-scores.js +17 -0
- package/dist/lib/catalogue/builders/requirements.js +168 -32
- package/dist/lib/catalogue/canonical-schema.yaml +845 -28
- package/dist/lib/catalogue/manifest-loader.js +13 -0
- package/dist/lib/catalogue/retired-catalogues.js +25 -0
- package/dist/lib/catalogue/rubric-coverage-map.yaml +108 -2
- package/dist/lib/catalogue/schema-compat.js +1 -1
- package/dist/lib/catalogue/schema-loader.js +3 -0
- package/dist/lib/catalogue/staleness-verdicts.js +75 -6
- package/dist/lib/intents/migrate.js +35 -9
- package/dist/lib/metamodel/tql-subset-parser.js +35 -2
- package/dist/lib/structure/catalogue.js +183 -0
- package/dist/lib/structure/compose.js +174 -0
- package/dist/lib/structure/effects.js +36 -0
- package/dist/lib/structure/model.js +212 -0
- package/dist/lib/structure/render.js +242 -0
- package/dist/lib/structure/view.js +301 -0
- package/dist/lib/validate/agreement-evidence.js +216 -0
- package/dist/lib/validate/behaviour-store.js +516 -28
- package/dist/lib/validate/dispatch.js +21 -0
- package/dist/lib/validate/feature-spec-pointers.js +175 -0
- package/dist/lib/validate/journey-impact.js +193 -0
- package/dist/lib/validate/structural-model.js +265 -0
- package/dist/maintain-version.json +2 -2
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
|
@@ -7,18 +7,28 @@
|
|
|
7
7
|
// requirements one row per `### Requirement:` in a living spec (stage living), keyed by the spec's
|
|
8
8
|
// directory name, which is the catalogue feature id; plus one per requirement an open,
|
|
9
9
|
// agreed change ADDS (stage agreed).
|
|
10
|
-
// requirement_lineage each archived change, in archive order, whose delta ADDED, MODIFIED or
|
|
11
|
-
// requirement
|
|
10
|
+
// requirement_lineage each archived change, in archive order, whose delta ADDED, MODIFIED, RENAMED or REMOVED a
|
|
11
|
+
// requirement, under any name a later RENAMED gave it. A change carrying
|
|
12
12
|
// baseline-evidence.md is a baseline. Statuses follow renames the same way.
|
|
13
|
+
// A requirement REMOVED by an archived change and not in a living spec now is kept in
|
|
14
|
+
// requirements as stage retired (schema 4.3.0), so its lineage ends in the REMOVED row;
|
|
15
|
+
// it carries no status.
|
|
13
16
|
// changes each change in the store, open or archived (M-ADR 0223 §4.2): its directory name, its
|
|
14
17
|
// undated name, kind, status (archived, or its agreement record's) and verdict.
|
|
15
18
|
// change_work_order per (change, repository): the record's work order, agreed or completed.
|
|
16
19
|
// change_work_item per (change, work item): the ids its proposal's `## Work Items` section lists, keyed by
|
|
17
20
|
// the undated name so the row survives archive.
|
|
18
|
-
//
|
|
21
|
+
// requirement_revisions one row per distinct text of a requirement (schema 6.0.0, M-ADR 0230 §4.7, Proposed): the
|
|
22
|
+
// living spec's is current, an agreed open change's ADDED or MODIFIED block intended, an
|
|
23
|
+
// archived change's block the living spec no longer holds history. Identified by its digest.
|
|
24
|
+
// requirement_assertions per (revision, repository): observed from a baseline's `**Observed in:**` line;
|
|
19
25
|
// declared-shipped from an archived change whose agreement record's verdict is PASS, through
|
|
20
|
-
// its work orders' `implements` lists;
|
|
21
|
-
//
|
|
26
|
+
// its work orders' `implements` lists; each about the revision that change's delta states, so
|
|
27
|
+
// evidence about one revision is never read as evidence about another. The latest wins, in
|
|
28
|
+
// archive order. An agreed change is not an assertion: it is an intended revision.
|
|
29
|
+
// change_effects per (change, element): each requirement a change's delta writes, from its base revision to
|
|
30
|
+
// its target, open and archived changes alike; and each structural element its structural
|
|
31
|
+
// deltas write (an archived change's from its effect record, structure/applied.yaml).
|
|
22
32
|
//
|
|
23
33
|
// Archive order is ../../behaviour-store/archive.ts's; same-day archives the history cannot order are a finding.
|
|
24
34
|
//
|
|
@@ -26,8 +36,25 @@
|
|
|
26
36
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
27
37
|
import { join } from "node:path";
|
|
28
38
|
import { parse as parseYaml } from "yaml";
|
|
29
|
-
import {
|
|
30
|
-
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
40
|
+
import { observedIn, orderArchives, parseRequirements, renamePairs, resolveImplements } from "../../behaviour-store/archive.js";
|
|
41
|
+
import { effectsOf, parseAppliedRecord } from "../../structure/effects.js";
|
|
42
|
+
import { APPLIED_FILE, loadChangeStructure, loadModel } from "../../structure/model.js";
|
|
43
|
+
import { viewOf } from "../../structure/view.js";
|
|
44
|
+
import { requirementBlock } from "../../validate/agreement-evidence.js";
|
|
45
|
+
export { observedIn, parseRequirements, resolveImplements };
|
|
46
|
+
/** `sha256:` of a requirement block, its lines trimmed and blank lines dropped (M-ADR 0230 §4.7). */
|
|
47
|
+
export function requirementDigest(spec, name) {
|
|
48
|
+
const block = requirementBlock(spec, name);
|
|
49
|
+
if (block === null)
|
|
50
|
+
return undefined;
|
|
51
|
+
const normal = block.split("\n").map((l) => l.trim()).filter((l) => l !== "").join("\n");
|
|
52
|
+
return `sha256:${createHash("sha256").update(normal, "utf8").digest("hex")}`;
|
|
53
|
+
}
|
|
54
|
+
/** A revision's id: <requirement_id>@<the digest's first 12 hex digits>. */
|
|
55
|
+
export function revisionId(requirementId, digest) {
|
|
56
|
+
return `${requirementId}@${digest.slice("sha256:".length, "sha256:".length + 12)}`;
|
|
57
|
+
}
|
|
31
58
|
const STORE = "openspec";
|
|
32
59
|
const AGREEMENTS = join("docs", "agreements");
|
|
33
60
|
async function isDir(p) {
|
|
@@ -62,8 +89,10 @@ async function deltaOf(changeDir) {
|
|
|
62
89
|
if (text === null)
|
|
63
90
|
continue;
|
|
64
91
|
for (const r of parseRequirements(text)) {
|
|
65
|
-
if (r.operation)
|
|
66
|
-
|
|
92
|
+
if (!r.operation)
|
|
93
|
+
continue;
|
|
94
|
+
const digest = r.operation === "ADDED" || r.operation === "MODIFIED" ? requirementDigest(text, r.name) : undefined;
|
|
95
|
+
out.push({ feature, name: r.name, operation: r.operation, file, scenarios: r.scenarios, ...(digest ? { digest } : {}) });
|
|
67
96
|
}
|
|
68
97
|
}
|
|
69
98
|
return out;
|
|
@@ -123,13 +152,24 @@ export function parseWorkItems(proposal) {
|
|
|
123
152
|
return ids;
|
|
124
153
|
}
|
|
125
154
|
const rel = (sddPath, p) => p.slice(sddPath.length + 1).split("\\").join("/");
|
|
155
|
+
/** One effect per (change, element): a change that renames and modifies a requirement records MODIFIED, as its lineage does. */
|
|
156
|
+
function dedupeEffects(rows) {
|
|
157
|
+
const byId = new Map();
|
|
158
|
+
for (const r of rows) {
|
|
159
|
+
const prior = byId.get(String(r["id"]));
|
|
160
|
+
byId.set(String(r["id"]), prior && r["operation"] === "RENAMED" ? prior : r);
|
|
161
|
+
}
|
|
162
|
+
return [...byId.values()];
|
|
163
|
+
}
|
|
126
164
|
export async function buildRequirements(inputs) {
|
|
127
165
|
const { sddPath, findings } = inputs;
|
|
128
|
-
const
|
|
166
|
+
const scoped = (change) => `${inputs.sddId}::${change}`;
|
|
167
|
+
const empty = { requirements: [], lineage: [], revisions: [], assertions: [], effects: [], changes: [], workOrders: [], workItems: [] };
|
|
129
168
|
const store = join(sddPath, STORE);
|
|
130
169
|
if (!(await isDir(store)))
|
|
131
170
|
return empty;
|
|
132
171
|
const requirements = new Map();
|
|
172
|
+
const livingDigest = new Map();
|
|
133
173
|
const unknownFeatures = new Set();
|
|
134
174
|
const known = (feature) => {
|
|
135
175
|
if (inputs.knownFeatureIds.has(feature))
|
|
@@ -150,17 +190,35 @@ export async function buildRequirements(inputs) {
|
|
|
150
190
|
continue;
|
|
151
191
|
}
|
|
152
192
|
requirements.set(id, { id, feature_id: feature, name: r.name, stage: "living", spec_path: rel(sddPath, file), scenario_count: r.scenarios, source_sdd_id: "", platform_sdd_id: "" });
|
|
193
|
+
const digest = requirementDigest(text, r.name);
|
|
194
|
+
if (digest)
|
|
195
|
+
livingDigest.set(id, digest);
|
|
153
196
|
}
|
|
154
197
|
}
|
|
155
198
|
const events = [];
|
|
199
|
+
const revisions = new Map();
|
|
200
|
+
const requirementEffects = [];
|
|
201
|
+
const structureEffects = [];
|
|
202
|
+
const effectRow = (changeId, kind, element, operation, base, target) => ({
|
|
203
|
+
id: `${changeId}__${kind}__${element}`,
|
|
204
|
+
change_id: changeId,
|
|
205
|
+
element_kind: kind,
|
|
206
|
+
element_id: element,
|
|
207
|
+
operation,
|
|
208
|
+
base_revision: base,
|
|
209
|
+
target_revision: target,
|
|
210
|
+
source_sdd_id: "",
|
|
211
|
+
platform_sdd_id: "",
|
|
212
|
+
});
|
|
156
213
|
const lineage = [];
|
|
157
214
|
const unknownRepos = new Set();
|
|
158
215
|
const changes = [];
|
|
159
216
|
const workOrders = [];
|
|
160
217
|
const workItems = [];
|
|
161
218
|
// The change itself, its work orders and its work items (M-ADR 0223 §4.2).
|
|
162
|
-
const recordChange = async (
|
|
219
|
+
const recordChange = async (localId, name, kind, status, archivedOn, changeDir, record) => {
|
|
163
220
|
const verdict = record?.verdict?.result === "PASS" || record?.verdict?.result === "FAIL" ? record.verdict.result : "";
|
|
221
|
+
const id = scoped(localId);
|
|
164
222
|
changes.push({ id, name, change_kind: kind, status, archived_on: archivedOn, verdict, source_sdd_id: "", platform_sdd_id: "" });
|
|
165
223
|
for (const wo of record?.work_orders ?? []) {
|
|
166
224
|
if (!wo.repo)
|
|
@@ -174,7 +232,7 @@ export async function buildRequirements(inputs) {
|
|
|
174
232
|
}
|
|
175
233
|
const proposal = await readText(join(changeDir, "proposal.md"));
|
|
176
234
|
for (const item of proposal === null ? [] : parseWorkItems(proposal)) {
|
|
177
|
-
workItems.push({ id: `${name}__${item}`, change_id: id, change_name: name, work_item_id: item, source_sdd_id: "", platform_sdd_id: "" });
|
|
235
|
+
workItems.push({ id: `${scoped(name)}__${item}`, change_id: id, change_name: name, work_item_id: item, source_sdd_id: "", platform_sdd_id: "" });
|
|
178
236
|
}
|
|
179
237
|
};
|
|
180
238
|
const pushEvent = (e) => {
|
|
@@ -182,7 +240,8 @@ export async function buildRequirements(inputs) {
|
|
|
182
240
|
unknownRepos.add(e.repo);
|
|
183
241
|
return;
|
|
184
242
|
}
|
|
185
|
-
|
|
243
|
+
// A retired requirement has no status: it is not where any repository's behaviour is.
|
|
244
|
+
if (requirements.has(e.requirementId) && requirements.get(e.requirementId)["stage"] !== "retired")
|
|
186
245
|
events.push(e);
|
|
187
246
|
};
|
|
188
247
|
// 2. Archived changes, in archive order. A requirement is named as it was when each change archived, so its
|
|
@@ -201,10 +260,20 @@ export async function buildRequirements(inputs) {
|
|
|
201
260
|
await recordChange(dir, change, kind, "archived", archivedOn, changeDir, await agreementRecord(sddPath, change));
|
|
202
261
|
const delta = await deltaOf(changeDir);
|
|
203
262
|
for (const d of delta) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
263
|
+
archived.push({
|
|
264
|
+
index,
|
|
265
|
+
feature: d.feature,
|
|
266
|
+
name: d.name,
|
|
267
|
+
lineage: { change_id: scoped(dir), change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" },
|
|
268
|
+
...(d.operation === "REMOVED" ? { removedIn: rel(sddPath, d.file) } : {}),
|
|
269
|
+
});
|
|
270
|
+
archived.push({ index, feature: d.feature, name: d.name, effect: { change: scoped(dir), operation: d.operation, ...(d.digest ? { digest: d.digest } : {}) } });
|
|
207
271
|
}
|
|
272
|
+
// Structural effects, from the effect record the method's archive wrote (M-ADR 0230 §4.6).
|
|
273
|
+
const applied = await readText(join(changeDir, "structure", APPLIED_FILE));
|
|
274
|
+
const rec = applied === null ? null : parseAppliedRecord(applied);
|
|
275
|
+
for (const e of rec?.effects ?? [])
|
|
276
|
+
structureEffects.push(effectRow(scoped(dir), "structure", e.element, e.operation, e.base ?? "", e.target ?? ""));
|
|
208
277
|
if (evidence !== null) {
|
|
209
278
|
const repos = observedIn(evidence);
|
|
210
279
|
if (repos.length === 0) {
|
|
@@ -212,8 +281,8 @@ export async function buildRequirements(inputs) {
|
|
|
212
281
|
}
|
|
213
282
|
for (const repo of repos)
|
|
214
283
|
for (const d of delta)
|
|
215
|
-
if (d.
|
|
216
|
-
archived.push({ index, feature: d.feature, name: d.name, event: { repo, status: "observed", basis: dir } });
|
|
284
|
+
if (d.digest)
|
|
285
|
+
archived.push({ index, feature: d.feature, name: d.name, event: { repo, status: "observed", basis: scoped(dir), digest: d.digest } });
|
|
217
286
|
continue;
|
|
218
287
|
}
|
|
219
288
|
const record = await agreementRecord(sddPath, change);
|
|
@@ -223,12 +292,17 @@ export async function buildRequirements(inputs) {
|
|
|
223
292
|
if (!wo.repo)
|
|
224
293
|
continue;
|
|
225
294
|
for (const name of wo.implements ?? []) {
|
|
226
|
-
const
|
|
227
|
-
|
|
295
|
+
const { matches, ambiguous } = resolveImplements(delta.filter((d) => d.operation !== "REMOVED"), name);
|
|
296
|
+
const match = matches;
|
|
297
|
+
if (ambiguous.length > 0) {
|
|
298
|
+
findings.add("divergence", `${AGREEMENTS}/${change}.yaml: work order for ${wo.repo} implements "${name}", a name ${ambiguous.length} features in the change share (${ambiguous.join(", ")}); qualify it as <feature>::<name>. No status is derived from it`);
|
|
299
|
+
}
|
|
300
|
+
else if (match.length === 0) {
|
|
228
301
|
findings.add("convention", `${AGREEMENTS}/${change}.yaml: work order for ${wo.repo} implements "${name}", which the change's delta does not name`);
|
|
229
302
|
}
|
|
230
303
|
for (const d of match)
|
|
231
|
-
|
|
304
|
+
if (d.digest)
|
|
305
|
+
archived.push({ index, feature: d.feature, name: d.name, event: { repo: wo.repo, status: "declared-shipped", basis: scoped(dir), digest: d.digest } });
|
|
232
306
|
}
|
|
233
307
|
}
|
|
234
308
|
}
|
|
@@ -240,11 +314,37 @@ export async function buildRequirements(inputs) {
|
|
|
240
314
|
n = r.to;
|
|
241
315
|
return `${feature}::${n}`;
|
|
242
316
|
};
|
|
317
|
+
// A requirement removed and not living now is retired: kept, so its lineage can end in the removal.
|
|
318
|
+
for (const a of archived) {
|
|
319
|
+
if (!a.removedIn)
|
|
320
|
+
continue;
|
|
321
|
+
const id = current(a.feature, a.name, a.index);
|
|
322
|
+
if (requirements.has(id) || !known(a.feature))
|
|
323
|
+
continue;
|
|
324
|
+
requirements.set(id, { id, feature_id: a.feature, name: id.slice(a.feature.length + 2), stage: "retired", spec_path: a.removedIn, scenario_count: 0, source_sdd_id: "", platform_sdd_id: "" });
|
|
325
|
+
}
|
|
243
326
|
// One lineage row per (requirement, change): a change that renames and modifies a requirement records MODIFIED,
|
|
244
327
|
// the behaviour change, and its delta still shows the rename.
|
|
245
328
|
const lineageById = new Map();
|
|
329
|
+
// Revisions in archive order: each ADDED or MODIFIED block is a revision its change introduced; the last one of a
|
|
330
|
+
// requirement is the base of the next change's effect on it.
|
|
331
|
+
const latest = new Map();
|
|
246
332
|
for (const a of archived) {
|
|
247
333
|
const requirementId = current(a.feature, a.name, a.index);
|
|
334
|
+
if (a.effect) {
|
|
335
|
+
if (!requirements.has(requirementId))
|
|
336
|
+
continue;
|
|
337
|
+
const base = latest.get(requirementId);
|
|
338
|
+
const target = a.effect.digest ? revisionId(requirementId, a.effect.digest) : undefined;
|
|
339
|
+
if (a.effect.digest && target) {
|
|
340
|
+
revisions.set(target, { id: target, requirement_id: requirementId, digest: a.effect.digest, state: "history", change_id: a.effect.change, source_sdd_id: "", platform_sdd_id: "" });
|
|
341
|
+
latest.set(requirementId, target);
|
|
342
|
+
}
|
|
343
|
+
if (a.effect.operation === "REMOVED")
|
|
344
|
+
latest.delete(requirementId);
|
|
345
|
+
requirementEffects.push(effectRow(a.effect.change, "requirement", requirementId, a.effect.operation, base ?? "", a.effect.operation === "RENAMED" ? base ?? "" : target ?? ""));
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
248
348
|
if (a.event)
|
|
249
349
|
pushEvent({ requirementId, ...a.event });
|
|
250
350
|
else if (a.lineage && requirements.has(requirementId)) {
|
|
@@ -255,6 +355,12 @@ export async function buildRequirements(inputs) {
|
|
|
255
355
|
}
|
|
256
356
|
}
|
|
257
357
|
lineage.push(...lineageById.values());
|
|
358
|
+
// The living text is the current revision, whichever change introduced it (none, for an edit no change made).
|
|
359
|
+
for (const [id, digest] of livingDigest) {
|
|
360
|
+
const rid = revisionId(id, digest);
|
|
361
|
+
const prior = revisions.get(rid);
|
|
362
|
+
revisions.set(rid, { id: rid, requirement_id: id, digest, state: "current", change_id: prior?.["change_id"] ?? "", source_sdd_id: "", platform_sdd_id: "" });
|
|
363
|
+
}
|
|
258
364
|
// 3. Open changes whose agreement record is agreed.
|
|
259
365
|
for (const dir of (await dirs(join(store, "changes"))).filter((d) => d !== "archive")) {
|
|
260
366
|
const record = await agreementRecord(sddPath, dir);
|
|
@@ -265,40 +371,70 @@ export async function buildRequirements(inputs) {
|
|
|
265
371
|
openStatus = "proposed";
|
|
266
372
|
}
|
|
267
373
|
await recordChange(dir, dir, openKind, openStatus, "", join(store, "changes", dir), record);
|
|
268
|
-
if (record?.status !== "agreed")
|
|
269
|
-
continue;
|
|
270
374
|
const delta = await deltaOf(join(store, "changes", dir));
|
|
375
|
+
const agreed = record?.status === "agreed";
|
|
271
376
|
for (const d of delta) {
|
|
272
377
|
const id = `${d.feature}::${d.name}`;
|
|
273
|
-
if (d.operation === "ADDED" && !requirements.has(id) && known(d.feature)) {
|
|
378
|
+
if (agreed && d.operation === "ADDED" && !requirements.has(id) && known(d.feature)) {
|
|
274
379
|
requirements.set(id, { id, feature_id: d.feature, name: d.name, stage: "agreed", spec_path: rel(sddPath, d.file), scenario_count: d.scenarios, source_sdd_id: "", platform_sdd_id: "" });
|
|
275
380
|
}
|
|
381
|
+
// An effect for every element the change writes: a requirement a change not yet agreed ADDs has no
|
|
382
|
+
// requirements row yet, and still has its effect (change_effects.element_id names no foreign key).
|
|
383
|
+
if (!requirements.has(id) && !(d.operation === "ADDED" && known(d.feature)))
|
|
384
|
+
continue;
|
|
385
|
+
const living = livingDigest.get(id);
|
|
386
|
+
const base = living ? revisionId(id, living) : "";
|
|
387
|
+
const target = d.digest ? revisionId(id, d.digest) : "";
|
|
388
|
+
// An agreed change's text is an intended revision; one not yet agreed has an effect and no revision row.
|
|
389
|
+
if (agreed && d.digest && !revisions.has(target)) {
|
|
390
|
+
revisions.set(target, { id: target, requirement_id: id, digest: d.digest, state: "intended", change_id: scoped(dir), source_sdd_id: "", platform_sdd_id: "" });
|
|
391
|
+
}
|
|
392
|
+
requirementEffects.push(effectRow(scoped(dir), "requirement", id, d.operation, d.operation === "ADDED" ? "" : base, d.operation === "REMOVED" ? "" : d.operation === "RENAMED" ? base : target));
|
|
276
393
|
}
|
|
394
|
+
if (!agreed)
|
|
395
|
+
continue;
|
|
277
396
|
for (const wo of record.work_orders ?? []) {
|
|
278
397
|
if (!wo.repo)
|
|
279
398
|
continue;
|
|
280
399
|
for (const name of wo.implements ?? []) {
|
|
281
|
-
|
|
282
|
-
|
|
400
|
+
const { ambiguous } = resolveImplements(delta.filter((x) => x.operation !== "REMOVED"), name);
|
|
401
|
+
if (ambiguous.length > 0) {
|
|
402
|
+
findings.add("divergence", `${AGREEMENTS}/${dir}.yaml: work order for ${wo.repo} implements "${name}", a name ${ambiguous.length} features in the change share (${ambiguous.join(", ")}); qualify it as <feature>::<name>`);
|
|
283
403
|
}
|
|
284
404
|
}
|
|
285
405
|
}
|
|
286
406
|
}
|
|
407
|
+
// Open changes' structural effects, against the model as it stands (M-ADR 0230 §4.4, §4.7).
|
|
408
|
+
{
|
|
409
|
+
const problems = [];
|
|
410
|
+
const files = await loadModel(sddPath, problems);
|
|
411
|
+
const view = viewOf(files ?? [], problems);
|
|
412
|
+
for (const dir of (await dirs(join(store, "changes"))).filter((d) => d !== "archive")) {
|
|
413
|
+
const s = await loadChangeStructure(sddPath, dir, problems);
|
|
414
|
+
for (const e of effectsOf(s, view))
|
|
415
|
+
structureEffects.push(effectRow(scoped(dir), "structure", e.element, e.operation, e.base ?? "", e.target ?? ""));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
287
418
|
for (const f of unknownFeatures)
|
|
288
419
|
findings.add("convention", `${STORE}/: a spec keyed "${f}" names no catalogue feature, so its requirements are not catalogued (M-ADR 0221 §4.7)`);
|
|
289
420
|
for (const r of unknownRepos)
|
|
290
|
-
findings.add("convention", `${STORE}/:
|
|
291
|
-
// Latest
|
|
292
|
-
const
|
|
421
|
+
findings.add("convention", `${STORE}/: an assertion names repository "${r}", which the catalogue does not hold; no assertion row`);
|
|
422
|
+
// Latest assertion per (revision, repository) wins.
|
|
423
|
+
const assertions = new Map();
|
|
293
424
|
for (const e of events) {
|
|
294
|
-
const
|
|
295
|
-
|
|
425
|
+
const rid = revisionId(e.requirementId, e.digest);
|
|
426
|
+
if (!revisions.has(rid))
|
|
427
|
+
continue;
|
|
428
|
+
const id = `${rid}__${e.repo}`;
|
|
429
|
+
assertions.set(id, { id, revision_id: rid, requirement_id: e.requirementId, repo_id: e.repo, kind: e.status, basis: e.basis, source_sdd_id: "", platform_sdd_id: "" });
|
|
296
430
|
}
|
|
297
431
|
const byId = (a, b) => String(a["id"]).localeCompare(String(b["id"]));
|
|
298
432
|
return {
|
|
299
433
|
requirements: [...requirements.values()].sort(byId),
|
|
300
434
|
lineage,
|
|
301
|
-
|
|
435
|
+
revisions: [...revisions.values()].sort(byId),
|
|
436
|
+
assertions: [...assertions.values()].sort(byId),
|
|
437
|
+
effects: [...dedupeEffects(requirementEffects), ...structureEffects].sort(byId),
|
|
302
438
|
changes: changes.sort(byId),
|
|
303
439
|
workOrders: workOrders.sort(byId),
|
|
304
440
|
workItems: workItems.sort(byId),
|