@sdd-method/verify-cli 5.5.1 → 5.7.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/builders/feature-tree-walker.js +311 -0
- package/dist/lib/catalogue/builders/features.js +351 -0
- package/dist/lib/catalogue/canonical-schema.yaml +19 -1
- package/dist/lib/catalogue/coverage-predicates.js +19 -2
- package/dist/lib/catalogue/coverage-render.js +10 -1
- package/dist/lib/catalogue/impact-run.js +54 -2
- package/dist/lib/catalogue/rubric-coverage-map.yaml +4 -0
- package/dist/lib/derived-specs/index.js +501 -0
- package/dist/lib/derived-specs/source-path.js +275 -0
- package/dist/lib/structure/catalogue.js +13 -0
- package/dist/lib/validate/agreement-evidence.js +14 -3
- package/dist/lib/validate/behaviour-store.js +20 -2
- package/dist/lib/validate/carve-citations.js +64 -5
- package/dist/lib/validate/derived-specs.js +300 -0
- package/dist/lib/validate/dispatch.js +8 -1
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/catalogue-verify.js +2 -0
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// derived-specs (M-ADR 0232): the living specs an executable source owns are what its pinned generator derives from it,
|
|
2
|
+
// with no hand edit and no stale derivation; every requirement and scenario names the declared field behind it
|
|
3
|
+
// (M-ADR 0211); and every derived feature is a catalogue feature. Where the pinned generator cannot run on this
|
|
4
|
+
// machine, digests are compared instead, and the report says so, so a weaker check never reads as the full one.
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
8
|
+
import { parseCatalogueCsv } from "../catalogue/csv-reader.js";
|
|
9
|
+
import { DERIVED_CONFIG, DERIVED_LOCK, generate, generatorIdentity, inputStates, livingSpecs, loadDerivedSources, openDerivations, requirementsWithoutScenario, loadLock, parseSource, pathWithin, resolvePath, resolveSourcePath, renderPath, sha256, specBlocks, } from "../derived-specs/index.js";
|
|
10
|
+
const POSITIVE_SUPPLY = new Set(["supported", "partial"]);
|
|
11
|
+
async function csvRows(path) {
|
|
12
|
+
return readFile(path, "utf8")
|
|
13
|
+
.then((t) => parseCatalogueCsv(t).rows.map((r) => Object.fromEntries(Object.entries(r).map(([k, v]) => [k, String(v ?? "")]))))
|
|
14
|
+
.catch(() => null);
|
|
15
|
+
}
|
|
16
|
+
async function supplyContext(repo, platformRoot) {
|
|
17
|
+
const m = await readFile(join(repo, ".sdd", "sdd-manifest.yaml"), "utf8").then((t) => parseYaml(t)).catch(() => null);
|
|
18
|
+
if (m?.["profile"] !== "integration-product")
|
|
19
|
+
return null;
|
|
20
|
+
const sddId = String(m["sdd_id"] ?? "");
|
|
21
|
+
let registry = null;
|
|
22
|
+
let registryFrom = "no data-item registry here (the platform's catalogue was not found)";
|
|
23
|
+
for (const [root, label] of [[repo, "this repository's catalogue"], [platformRoot, "the platform's catalogue"]]) {
|
|
24
|
+
if (root === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
const rows = await csvRows(join(root, "catalogues", "data_items.csv"));
|
|
27
|
+
if (rows !== null && rows.length > 0) {
|
|
28
|
+
registry = new Map(rows.map((r) => [r["id"], r["category"]]));
|
|
29
|
+
registryFrom = label;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const supply = (await csvRows(join(repo, "catalogues", "integration_data_item.csv"))) ?? [];
|
|
34
|
+
const supplied = new Set(supply.filter((r) => POSITIVE_SUPPLY.has(r["verdict"] ?? "")).map((r) => r["data_item_id"]));
|
|
35
|
+
return { sddId, registry, registryFrom, supplied };
|
|
36
|
+
}
|
|
37
|
+
export async function checkDerivedSpecs(repo, platformRoot) {
|
|
38
|
+
let sources;
|
|
39
|
+
try {
|
|
40
|
+
sources = await loadDerivedSources(repo);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return { exitCode: 1, stdout: `[FAIL] ${e.message}\n` };
|
|
44
|
+
}
|
|
45
|
+
if (sources === null || sources.length === 0) {
|
|
46
|
+
return { exitCode: 0, stdout: `[N/A] no derived sources (${DERIVED_CONFIG}) in this repository\n` };
|
|
47
|
+
}
|
|
48
|
+
const lock = new Map((await loadLock(repo)).map((l) => [l.id, l]));
|
|
49
|
+
const living = await livingSpecs(repo);
|
|
50
|
+
const catalogueFeatures = await readFile(join(repo, "catalogues", "features.csv"), "utf8")
|
|
51
|
+
.then((t) => new Set(parseCatalogueCsv(t).rows.map((r) => String(r["id"]))))
|
|
52
|
+
.catch(() => null);
|
|
53
|
+
const failures = [];
|
|
54
|
+
const warnings = [];
|
|
55
|
+
const lines = [];
|
|
56
|
+
const owner = new Map();
|
|
57
|
+
const supply = await supplyContext(repo, platformRoot);
|
|
58
|
+
for (const s of sources) {
|
|
59
|
+
const where = `derived source '${s.id}' (${s.source})`;
|
|
60
|
+
const sourceText = await readFile(join(repo, s.source), "utf8").catch(() => null);
|
|
61
|
+
if (sourceText === null) {
|
|
62
|
+
failures.push(`${where}: the source file does not exist`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const rec = lock.get(s.id);
|
|
66
|
+
if (rec === undefined) {
|
|
67
|
+
const pending = await openDerivations(repo, s.id);
|
|
68
|
+
if (pending.length > 0)
|
|
69
|
+
warnings.push(`${where}: first derivation pending in ${pending.map((c) => `'${c}'`).join(", ")}; no derivation record until it archives (M-ADR 0232 §4.7)`);
|
|
70
|
+
else
|
|
71
|
+
failures.push(`${where}: no derivation record in ${DERIVED_LOCK}, and no open change derives it; derive it through a baseline (M-ADR 0232 §4.7)`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const sp of rec.specs) {
|
|
75
|
+
const prior = owner.get(sp.feature);
|
|
76
|
+
if (prior !== undefined)
|
|
77
|
+
failures.push(`feature '${sp.feature}' is owned by two derived sources ('${prior}' and '${s.id}')`);
|
|
78
|
+
owner.set(sp.feature, s.id);
|
|
79
|
+
}
|
|
80
|
+
// What the specs were derived with must be what is pinned now: the generator and every declared input. A pin that moved
|
|
81
|
+
// without regeneration is a stale derivation, named by its cause, never read as a hand edit. It is pending, a warning,
|
|
82
|
+
// while an open change carries the source's regeneration (derived/<id>); otherwise it fails.
|
|
83
|
+
const drift = [];
|
|
84
|
+
if (rec.generator?.identity !== s.generator.identity || rec.generator?.version !== s.generator.version) {
|
|
85
|
+
drift.push(`the generator pin is ${s.generator.identity} ${s.generator.version}, but the specs were derived with ${rec.generator?.identity ?? "?"} ${rec.generator?.version ?? "?"}`);
|
|
86
|
+
}
|
|
87
|
+
const recordedInputs = new Map((rec.inputs ?? []).map((x) => [x.id, x.sha256]));
|
|
88
|
+
for (const x of s.inputs) {
|
|
89
|
+
const was = recordedInputs.get(x.id);
|
|
90
|
+
if (was === undefined)
|
|
91
|
+
drift.push(`input '${x.id}' is pinned at ${x.sha256.slice(0, 12)}, but the specs were derived without it`);
|
|
92
|
+
else if (was !== x.sha256)
|
|
93
|
+
drift.push(`input '${x.id}' is pinned at ${x.sha256.slice(0, 12)}, but the specs were derived with ${was.slice(0, 12)}`);
|
|
94
|
+
}
|
|
95
|
+
for (const id of recordedInputs.keys()) {
|
|
96
|
+
if (!s.inputs.some((x) => x.id === id))
|
|
97
|
+
drift.push(`input '${id}' is recorded but no longer declared`);
|
|
98
|
+
}
|
|
99
|
+
if (drift.length > 0) {
|
|
100
|
+
const pending = await openDerivations(repo, s.id);
|
|
101
|
+
for (const d of drift) {
|
|
102
|
+
if (pending.length > 0)
|
|
103
|
+
warnings.push(`${where}: ${d}; regeneration pending in ${pending.map((c) => `'${c}'`).join(", ")} (M-ADR 0232 §4.3)`);
|
|
104
|
+
else
|
|
105
|
+
failures.push(`${where}: ${d} (a stale derivation, not a hand edit); regenerate through a change (M-ADR 0232 §4.3)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Full check: regenerate with the pinned generator from the pinned inputs, and compare. Fallback: compare digests,
|
|
109
|
+
// and say why, so a weaker check never reads as the full one.
|
|
110
|
+
const ident = await generatorIdentity(repo, s.generator, s.inputs);
|
|
111
|
+
let why = null;
|
|
112
|
+
if (ident === null)
|
|
113
|
+
why = "the generator is not available here";
|
|
114
|
+
else if (ident.identity !== s.generator.identity || ident.version !== s.generator.version)
|
|
115
|
+
why = `the installed generator is ${ident.identity} ${ident.version}, not the pinned ${s.generator.version}`;
|
|
116
|
+
for (const st of await inputStates(repo, s.inputs)) {
|
|
117
|
+
if (why !== null)
|
|
118
|
+
break;
|
|
119
|
+
if (st.digest === null)
|
|
120
|
+
why = `input '${st.input.id}' is not available here (${st.location})`;
|
|
121
|
+
else if (st.digest !== st.input.sha256)
|
|
122
|
+
why = `input '${st.input.id}' at ${st.location} is ${st.digest.slice(0, 12)}, not the pinned ${st.input.sha256.slice(0, 12)}`;
|
|
123
|
+
}
|
|
124
|
+
if (why === null && drift.length > 0)
|
|
125
|
+
why = "a pin moved since the derivation was recorded";
|
|
126
|
+
if (why === null) {
|
|
127
|
+
let derived;
|
|
128
|
+
try {
|
|
129
|
+
derived = await generate(repo, s, s.source);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
failures.push(`${where}: the generator failed: ${e.message}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const recorded = new Set(rec.specs.map((x) => x.feature));
|
|
136
|
+
for (const [feature, text] of derived) {
|
|
137
|
+
if (!recorded.has(feature))
|
|
138
|
+
failures.push(`${where}: the generator emits '${feature}', which the derivation record does not own; regenerate through a change`);
|
|
139
|
+
else if (living.get(feature) !== text)
|
|
140
|
+
failures.push(`${where}: openspec/specs/${feature}/spec.md differs from what the source derives (a hand edit, or the source changed without regeneration)`);
|
|
141
|
+
}
|
|
142
|
+
for (const f of recorded)
|
|
143
|
+
if (!derived.has(f))
|
|
144
|
+
failures.push(`${where}: '${f}' is recorded as derived but the generator no longer emits it`);
|
|
145
|
+
const fromInputs = s.inputs.length > 0 ? ` from ${s.inputs.map((x) => `${x.id} ${x.sha256.slice(0, 12)}`).join(", ")}` : "";
|
|
146
|
+
lines.push(` ${s.id}: regenerated with ${ident.identity} ${ident.version} (${ident.commit})${fromInputs} and compared: ${derived.size} living spec(s)`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (sha256(sourceText) !== rec.source_sha256)
|
|
150
|
+
failures.push(`${where}: the source changed since its derivation was recorded (a stale derivation); regenerate through a change`);
|
|
151
|
+
for (const sp of rec.specs) {
|
|
152
|
+
const t = living.get(sp.feature);
|
|
153
|
+
if (t === undefined)
|
|
154
|
+
failures.push(`${where}: recorded living spec '${sp.feature}' is missing`);
|
|
155
|
+
else if (sha256(t) !== sp.sha256)
|
|
156
|
+
failures.push(`${where}: openspec/specs/${sp.feature}/spec.md differs from its recorded digest (a hand edit)`);
|
|
157
|
+
}
|
|
158
|
+
lines.push(` ${s.id}: digests compared, NOT regenerated (${why}): ${rec.specs.length} living spec(s)`);
|
|
159
|
+
}
|
|
160
|
+
// M-ADR 0211: every requirement and scenario names the declared field behind it, and each one resolves.
|
|
161
|
+
let doc;
|
|
162
|
+
try {
|
|
163
|
+
doc = parseSource(sourceText);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
failures.push(`${where}: the source does not parse: ${e.message}`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// M-ADR 0232 §4.1: the features map says which part of the source each feature specifies.
|
|
170
|
+
if (s.features.size === 0) {
|
|
171
|
+
failures.push(`${where}: declares no features map (features: {<feature-id>: [<path>, ...]}, M-ADR 0232 §4.1)`);
|
|
172
|
+
}
|
|
173
|
+
const mapped = [];
|
|
174
|
+
for (const path of s.shared) {
|
|
175
|
+
const why = resolvePath(doc, path, { keys: s.keys, positions: false });
|
|
176
|
+
if (why !== null)
|
|
177
|
+
failures.push(`${where}: shared path '${path}' does not resolve: ${why}`);
|
|
178
|
+
}
|
|
179
|
+
for (const [feature, paths] of s.features) {
|
|
180
|
+
for (const path of paths) {
|
|
181
|
+
const why = resolvePath(doc, path, { keys: s.keys, positions: false });
|
|
182
|
+
if (why !== null)
|
|
183
|
+
failures.push(`${where}: features map: '${feature}' path '${path}' does not resolve: ${why}`);
|
|
184
|
+
for (const o of mapped) {
|
|
185
|
+
if (o.feature !== feature && (pathWithin(path, o.path) || pathWithin(o.path, path))) {
|
|
186
|
+
failures.push(`${where}: features map: '${path}' (${feature}) and '${o.path}' (${o.feature}) map one part of the source to two features`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
mapped.push({ feature, path });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (s.features.size > 0) {
|
|
193
|
+
for (const sp of rec.specs) {
|
|
194
|
+
if (!s.features.has(sp.feature))
|
|
195
|
+
failures.push(`${where}: derived feature '${sp.feature}' is not in the features map`);
|
|
196
|
+
}
|
|
197
|
+
// A mapped feature not derived yet is a visible gap, not a failure: features are derived incrementally.
|
|
198
|
+
const derivedNow = new Set(rec.specs.map((x) => x.feature));
|
|
199
|
+
const pending = [...s.features.keys()].filter((f) => !derivedNow.has(f)).sort();
|
|
200
|
+
if (pending.length > 0)
|
|
201
|
+
lines.push(` ${s.id}: gap: mapped but not derived yet: ${pending.map((f) => `'${f}'`).join(", ")} (visible, not a failure)`);
|
|
202
|
+
if (doc instanceof Map) {
|
|
203
|
+
const gaps = [...doc.keys()].map((k) => (typeof k === "string" ? renderPath([{ kind: "key", name: k }]) : `[${String(k)}]`)).filter((k) => ![...mapped.map((m) => m.path), ...s.shared].some((p) => pathWithin(p, k) || pathWithin(k, p)));
|
|
204
|
+
if (gaps.length > 0)
|
|
205
|
+
lines.push(` ${s.id}: gap: no feature maps ${gaps.map((g) => `'${g}'`).join(", ")} (visible, not a failure)`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
let blocks = 0;
|
|
209
|
+
let named = 0; // Data item lines across this source's supply features
|
|
210
|
+
const observes = new Map();
|
|
211
|
+
for (const sp of rec.specs) {
|
|
212
|
+
const t = living.get(sp.feature);
|
|
213
|
+
if (t === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
const featurePaths = s.features.get(sp.feature) ?? [];
|
|
216
|
+
const supplyFeature = supply !== null;
|
|
217
|
+
const slug = supply !== null && sp.feature.startsWith(`${supply.sddId}-`) ? sp.feature.slice(supply.sddId.length + 1) : null;
|
|
218
|
+
if (supplyFeature && slug === null) {
|
|
219
|
+
failures.push(`derived feature '${sp.feature}' is not a supply feature named '${supply.sddId}-<category>' (M-ADR 0143 as amended)`);
|
|
220
|
+
}
|
|
221
|
+
let items = 0;
|
|
222
|
+
for (const bare of requirementsWithoutScenario(t)) {
|
|
223
|
+
failures.push(`openspec/specs/${sp.feature}/spec.md: requirement '${bare}' has no scenario; OpenSpec's strict validation refuses it (a generator defect: ${s.generator.identity} ${s.generator.version})`);
|
|
224
|
+
}
|
|
225
|
+
for (const b of specBlocks(t)) {
|
|
226
|
+
blocks++;
|
|
227
|
+
if (b.sources.length === 0) {
|
|
228
|
+
failures.push(`openspec/specs/${sp.feature}/spec.md: ${b.kind} '${b.name}' names no Source: (a derived spec asserts only what its source declares, M-ADR 0211)`);
|
|
229
|
+
}
|
|
230
|
+
for (const path of b.sources) {
|
|
231
|
+
const why = resolveSourcePath(doc, path, s.keys);
|
|
232
|
+
if (why !== null)
|
|
233
|
+
failures.push(`openspec/specs/${sp.feature}/spec.md: ${b.kind} '${b.name}': Source '${path}' does not resolve in ${s.source}: ${why}`);
|
|
234
|
+
else if (featurePaths.length > 0 && !featurePaths.some((fp) => pathWithin(path, fp)) && !s.shared.some((sp) => pathWithin(path, sp))) {
|
|
235
|
+
failures.push(`openspec/specs/${sp.feature}/spec.md: ${b.kind} '${b.name}': Source '${path}' is outside the paths the features map gives '${sp.feature}'`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Observed via: a path the block is observed through, owned by another feature (a dependency, never an assertion).
|
|
239
|
+
for (const path of b.via) {
|
|
240
|
+
const why = resolveSourcePath(doc, path, s.keys);
|
|
241
|
+
if (why !== null) {
|
|
242
|
+
failures.push(`openspec/specs/${sp.feature}/spec.md: ${b.kind} '${b.name}': Observed via '${path}' does not resolve in ${s.source}: ${why}`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const other = [...s.features].find(([, ps]) => ps.some((fp) => pathWithin(path, fp)))?.[0] ?? "(no feature: unmapped)";
|
|
246
|
+
if (other === sp.feature)
|
|
247
|
+
continue;
|
|
248
|
+
const set = observes.get(sp.feature) ?? new Set();
|
|
249
|
+
set.add(other);
|
|
250
|
+
observes.set(sp.feature, set);
|
|
251
|
+
}
|
|
252
|
+
if (!supplyFeature || slug === null)
|
|
253
|
+
continue;
|
|
254
|
+
if (b.kind === "requirement" && b.dataItems.length === 0 && slug !== "operation") {
|
|
255
|
+
failures.push(`openspec/specs/${sp.feature}/spec.md: requirement '${b.name}' names no Data item: (a supply feature's requirement names the item it supplies, M-ADR 0232 §4.4)`);
|
|
256
|
+
}
|
|
257
|
+
for (const di of b.dataItems) {
|
|
258
|
+
items++;
|
|
259
|
+
const at = `openspec/specs/${sp.feature}/spec.md: ${b.kind} '${b.name}': Data item '${di}'`;
|
|
260
|
+
if (supply.registry !== null) {
|
|
261
|
+
const cat = supply.registry.get(di);
|
|
262
|
+
if (cat === undefined)
|
|
263
|
+
failures.push(`${at} is not a data item in ${supply.registryFrom}`);
|
|
264
|
+
else if (cat !== slug)
|
|
265
|
+
failures.push(`${at} is in category '${cat}', not the feature's '${slug}'`);
|
|
266
|
+
}
|
|
267
|
+
if (!supply.supplied.has(di))
|
|
268
|
+
failures.push(`${at} is not supplied by this integration (no supported or partial integration_data_item row)`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
named += items;
|
|
272
|
+
if (supplyFeature && slug !== null && slug !== "operation" && items === 0) {
|
|
273
|
+
failures.push(`supply feature '${sp.feature}' supplies no data item (only the operation feature may, M-ADR 0232 §4.4)`);
|
|
274
|
+
}
|
|
275
|
+
if (catalogueFeatures !== null && !catalogueFeatures.has(sp.feature)) {
|
|
276
|
+
failures.push(`derived feature '${sp.feature}' is not a feature in catalogues/features.csv (add it through a structural change first, M-ADR 0232 §4.7)`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
lines.push(` ${s.id}: ${blocks} requirement and scenario block(s) checked for Source:`);
|
|
280
|
+
for (const [f, others] of [...observes].sort(([a], [b]) => a.localeCompare(b))) {
|
|
281
|
+
lines.push(` ${s.id}: '${f}' is observed via ${[...others].sort().map((o) => `'${o}'`).join(", ")} (a dependency, not an assertion)`);
|
|
282
|
+
}
|
|
283
|
+
// A data item's existence and category are read from the platform's registry. Where none was found, those checks
|
|
284
|
+
// did not run: that is unresolved, never a pass, so it fails while any item is named (pilot finding for M-ADR 0232).
|
|
285
|
+
if (supply !== null && supply.registry === null && named > 0) {
|
|
286
|
+
failures.push(`${s.id}: ${named} Data item line(s) whose existence and category are unresolved: no data-item registry was found (${supply.registryFrom}); run with --platform-root <the platform root's directory> so its catalogues/data_items.csv is read (M-ADR 0232 §4.4)`);
|
|
287
|
+
}
|
|
288
|
+
if (supply !== null) {
|
|
289
|
+
lines.push(supply.registry !== null
|
|
290
|
+
? ` ${s.id}: data items checked against ${supply.registryFrom} and this integration's supply`
|
|
291
|
+
: ` ${s.id}: data items checked against this integration's supply only; existence and category NOT checked (${supply.registryFrom})`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const body = lines.join("\n") + "\n" + warnings.map((w) => `[WARN] ${w}\n`).join("");
|
|
295
|
+
if (failures.length > 0) {
|
|
296
|
+
return { exitCode: 1, stdout: body + failures.map((f) => `[FAIL] ${f}\n`).join("") + `[FAIL] derived specs: ${failures.length} problem(s)\n` };
|
|
297
|
+
}
|
|
298
|
+
return { exitCode: 0, stdout: body + `[PASS] derived specs: every owned living spec is what its source derives\n` };
|
|
299
|
+
}
|
|
300
|
+
//# sourceMappingURL=derived-specs.js.map
|
|
@@ -2,6 +2,7 @@ import { checkArchetypeCitations } from "./archetype-citations.js";
|
|
|
2
2
|
import { checkBehaviourStore, checkBehaviourStoreOpenspec } from "./behaviour-store.js";
|
|
3
3
|
import { checkBundleCoherence } from "./bundle-coherence.js";
|
|
4
4
|
import { checkCarveCitations } from "./carve-citations.js";
|
|
5
|
+
import { checkDerivedSpecs } from "./derived-specs.js";
|
|
5
6
|
import { checkFeatureSpecPointers } from "./feature-spec-pointers.js";
|
|
6
7
|
import { checkIntents } from "./intents.js";
|
|
7
8
|
import { checkJourneyImpact } from "./journey-impact.js";
|
|
@@ -29,7 +30,13 @@ const BEHAVIOUR_STORE_VALIDATORS = [
|
|
|
29
30
|
// Behaviour carved into another SDD keeps one home at a time (the carving guide); without citations it reports [N/A].
|
|
30
31
|
{
|
|
31
32
|
name: "carve-citations",
|
|
32
|
-
source: { kind: "native", run: async (ctx) => checkCarveCitations(ctx.targetRepo) },
|
|
33
|
+
source: { kind: "native", run: async (ctx) => checkCarveCitations(ctx.targetRepo, process.env.SDD_SNAPSHOT_STORE ?? "") },
|
|
34
|
+
build: () => ({ status: "run", args: [] }),
|
|
35
|
+
},
|
|
36
|
+
// Living specs derived from an executable source (M-ADR 0232); [N/A] where the SDD declares none.
|
|
37
|
+
{
|
|
38
|
+
name: "derived-specs",
|
|
39
|
+
source: { kind: "native", run: async (ctx) => checkDerivedSpecs(ctx.targetRepo, ctx.platformRoot) },
|
|
33
40
|
build: () => ({ status: "run", args: [] }),
|
|
34
41
|
},
|
|
35
42
|
// Intents (M-ADR 0227) can sit in an SDD of any profile; without any the check reports [N/A].
|
|
@@ -59,6 +59,7 @@ export function registerCatalogueImpact(catalogue) {
|
|
|
59
59
|
.option("--sdd-path <path>", "Path to the SDD repo root (defaults to cwd)")
|
|
60
60
|
.option("--catalogues-dir <dir>", "Catalogues directory relative to the SDD root (default: catalogues, or catalogues-aggregated at --tier aggregate)")
|
|
61
61
|
.option("--tier <tier>", "Tier, explicit never inferred (member | aggregate; default member)")
|
|
62
|
+
.option("--consumers", "Report the node's consumers instead: only dependency relationships, in the consumer direction, each with a shortest path", false)
|
|
62
63
|
.action(async (opts) => {
|
|
63
64
|
if (opts.tier !== undefined &&
|
|
64
65
|
opts.tier !== "member" &&
|
|
@@ -75,6 +76,7 @@ export function registerCatalogueImpact(catalogue) {
|
|
|
75
76
|
...(opts.tier !== undefined
|
|
76
77
|
? { tier: opts.tier }
|
|
77
78
|
: {}),
|
|
79
|
+
...(opts.consumers ? { consumers: true } : {}),
|
|
78
80
|
}, (s) => process.stdout.write(s));
|
|
79
81
|
if (result.exitCode !== 0)
|
|
80
82
|
process.exit(result.exitCode);
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/verify-cli",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"sddCliVersion": "5.
|
|
3
|
+
"version": "5.7.0",
|
|
4
|
+
"sddCliVersion": "5.7.0",
|
|
5
5
|
"description": "Verify an SDD repository without changing it — validate, list, and read-only catalogue and pack checks. Ships only the code those verbs reach; it cannot write to the repository it is pointed at. Authoring and maintenance are in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|