@sdd-method/maintain-cli 0.14.0 → 0.15.1
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 +45 -8
- package/dist/lib/catalogue/build.js +58 -12
- package/dist/lib/catalogue/builders/adr-governs.js +1 -1
- package/dist/lib/catalogue/builders/adrs.js +2 -2
- package/dist/lib/catalogue/builders/contract-revisions.js +45 -7
- package/dist/lib/catalogue/builders/data-model.js +1 -1
- package/dist/lib/catalogue/builders/entity-archetype.js +121 -0
- package/dist/lib/catalogue/builders/quality-scores.js +1 -1
- package/dist/lib/catalogue/builders/requirements.js +1 -1
- package/dist/lib/catalogue/canonical-schema.yaml +41 -21
- package/dist/lib/catalogue/retired-catalogues.js +1 -1
- package/dist/lib/catalogue/rubric-coverage-map.yaml +7 -3
- 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 +199 -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 +1 -1
- 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
|
@@ -121,7 +121,12 @@ function naturalKey(cat, e) {
|
|
|
121
121
|
* catalogue of another class, against its built rows), no cycle through a self-reference, natural keys unique, and
|
|
122
122
|
* domain invariants naming their entity's own attributes and relationships.
|
|
123
123
|
*/
|
|
124
|
-
export function validateView(schema, view, built, problems, unresolved) {
|
|
124
|
+
export function validateView(schema, view, built, problems, unresolved, addedBy = () => undefined) {
|
|
125
|
+
// A reference to an open change's addition, from outside that change and its dependants (§4.11 proof 3).
|
|
126
|
+
const outside = (catalogue, id) => {
|
|
127
|
+
const by = addedBy(catalogue, id);
|
|
128
|
+
return by === undefined ? "" : `; open change ${by} adds it, and only that change and those depending on it can name it`;
|
|
129
|
+
};
|
|
125
130
|
for (const [catalogue, byId] of view) {
|
|
126
131
|
const cat = catalogueOf(schema, catalogue);
|
|
127
132
|
for (const e of byId.values()) {
|
|
@@ -133,14 +138,14 @@ export function validateView(schema, view, built, problems, unresolved) {
|
|
|
133
138
|
for (const r of referencesOf(schema, e)) {
|
|
134
139
|
if (r.structural) {
|
|
135
140
|
if (!find(view, r.catalogue, r.targetColumn, r.value))
|
|
136
|
-
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no ${r.catalogue} element in this view (${elementKey(r.catalogue, r.value)})`);
|
|
141
|
+
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no ${r.catalogue} element in this view (${elementKey(r.catalogue, r.value)})${r.targetColumn === "id" ? outside(r.catalogue, r.value) : ""}`);
|
|
137
142
|
}
|
|
138
143
|
else {
|
|
139
144
|
const rows = built(r.catalogue, r.targetColumn);
|
|
140
145
|
if (rows === null)
|
|
141
146
|
unresolved.add(r.catalogue);
|
|
142
147
|
else if (!rows.has(r.value))
|
|
143
|
-
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no row of ${r.catalogue} as built`);
|
|
148
|
+
problems.push(`${e.file}: ${elementKey(catalogue, e.id)}: ${r.column} ${JSON.stringify(r.value)} names no row of ${r.catalogue} as built${outside(r.catalogue, r.value)}`);
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// agreed content the change's proposal, deltas, Gate 0, design, tasks (checkboxes ignored), the test plan's
|
|
5
5
|
// scenario-to-test rows (its Before and After cells ignored) and its structural deltas with their
|
|
6
|
-
// base revisions (M-ADR 0230 §4.4
|
|
6
|
+
// base revisions (M-ADR 0230 §4.4; report only until it is accepted) are what was approved. Each is compared
|
|
7
7
|
// with its state at the record's pinned commit: the latest amendment's commit, else sdd.commit.
|
|
8
8
|
// requirement a requirement's block in a spec: its heading to the next requirement or section heading. The
|
|
9
9
|
// stale check compares the blocks a change targets, not the whole file.
|
|
@@ -16,7 +16,7 @@ export function boundFile(rel) {
|
|
|
16
16
|
return /^(proposal|design|gate-0|tasks|test-plan)\.md$/.test(rel) || /^specs\/.+\/spec\.md$/.test(rel) || structuralFile(rel);
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* A change's structural artifact (M-ADR 0230 §4.4
|
|
19
|
+
* A change's structural artifact (M-ADR 0230 §4.4): a partition delta or the no-op claim, both agreed
|
|
20
20
|
* content, so a delta's base revisions are pinned with it. The effect record (structure/applied.yaml) is written by
|
|
21
21
|
* the archive, after agreement, and is not.
|
|
22
22
|
*/
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
// check is vacuous, and says so.
|
|
17
17
|
// agreed content: the change's proposal, deltas, Gate 0, design, tasks (checkboxes ignored), test
|
|
18
18
|
// plan (its execution results ignored) and structural deltas equal their state at that commit
|
|
19
|
-
// (M-ADR 0222 §4.6; a structural delta's change
|
|
19
|
+
// (M-ADR 0222 §4.6; a structural delta's change fails like the rest, a [WARN] only with ENFORCED false).
|
|
20
20
|
// kinds: on a record held to 1.1.0, each test-plan row's Kind (changed or carried) is the one computed
|
|
21
|
-
// from the deltas and the living spec,
|
|
22
|
-
//
|
|
21
|
+
// from the deltas and the living spec, or repaired (declared, on a carried scenario); a row
|
|
22
|
+
// naming no scenario of the deltas fails; and a filled Before, read as whole-word results, is a
|
|
23
|
+
// fail for changed, a pass for carried, and a fail citing the test double's correcting commit
|
|
24
|
+
// for repaired (M-ADR 0223 §4.4).
|
|
23
25
|
// 4. verdicts: every archived change except a baseline has a verdict.md whose verdict line agrees with
|
|
24
26
|
// its record's PASS (M-ADR 0223 §4.6). A record held to 1.1.0 (by its own history, or by the
|
|
25
27
|
// schema the SDD shipped when the change archived) also needs: an agreed record; archived
|
|
@@ -186,6 +188,16 @@ export function parseAgreementRecord(file, text) {
|
|
|
186
188
|
noLongerImplements.push({ repo: e.repo, requirement: e.requirement });
|
|
187
189
|
}
|
|
188
190
|
}
|
|
191
|
+
// Record 1.3.0 (M-ADR 0230 §4.2): a contracts entry may deprecate its version, stating the sunset. A
|
|
192
|
+
// deprecation that names no date states no runway, so it is refused rather than read as a deprecation without one.
|
|
193
|
+
for (const [i, x] of (Array.isArray(r.contracts) ? r.contracts : []).entries()) {
|
|
194
|
+
const d = x?.deprecates;
|
|
195
|
+
if (d === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
const sunset = d?.sunset;
|
|
198
|
+
if (typeof sunset !== "string" || !isCalendarDate(sunset))
|
|
199
|
+
problems.push(`${file}: contracts[${i}].deprecates must be {sunset: YYYY-MM-DD}, a calendar date as a string; found ${JSON.stringify(d)}`);
|
|
200
|
+
}
|
|
189
201
|
if (problems.length > 0)
|
|
190
202
|
return { problems };
|
|
191
203
|
return {
|
|
@@ -205,6 +217,13 @@ export function parseAgreementRecord(file, text) {
|
|
|
205
217
|
problems,
|
|
206
218
|
};
|
|
207
219
|
}
|
|
220
|
+
/** A YYYY-MM-DD string naming a day that exists. */
|
|
221
|
+
function isCalendarDate(s) {
|
|
222
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(s))
|
|
223
|
+
return false;
|
|
224
|
+
const t = new Date(`${s}T00:00:00Z`);
|
|
225
|
+
return !Number.isNaN(t.getTime()) && t.toISOString().slice(0, 10) === s;
|
|
226
|
+
}
|
|
208
227
|
/** Every pinned commit in a record: any key named `commit` or ending `_commit`, and each value under verdict.merged and current. */
|
|
209
228
|
function commitFields(r) {
|
|
210
229
|
const out = [];
|
|
@@ -489,7 +508,7 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
489
508
|
if (a === null || b === null || normaliseBound(f, a) !== normaliseBound(f, b)) {
|
|
490
509
|
const what = `${dir}: AGREED CONTENT CHANGED: ${f} ${a === null ? "was added" : b === null ? "was removed" : "differs"} since ${base}, with no amendment recording a commit and an approver; amend the agreement or renew it (M-ADR 0222 §4.6)`;
|
|
491
510
|
if (structuralFile(f) && !STRUCTURE_ENFORCED)
|
|
492
|
-
warnings.push(`${what}; a structural delta,
|
|
511
|
+
warnings.push(`${what}; a structural delta, reported only because ENFORCED is off`);
|
|
493
512
|
else
|
|
494
513
|
failures.push(what);
|
|
495
514
|
}
|
|
@@ -497,14 +516,14 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
497
516
|
}
|
|
498
517
|
}
|
|
499
518
|
}
|
|
500
|
-
// A test plan's kinds (M-ADR 0223 §4.4 as amended): changed scenarios go red then green, carried ones stay green.
|
|
519
|
+
// A test plan's kinds (M-ADR 0223 §4.4 as amended): changed and repaired scenarios go red then green, carried ones stay green.
|
|
501
520
|
if (versionAtLeast(record?.schemaVersion, "1.1.0") && record?.status === "agreed") {
|
|
502
521
|
const plan = await readText(join(repo, dir, "test-plan.md"));
|
|
503
522
|
if (plan !== null) {
|
|
504
523
|
const header = testPlanHeader(plan);
|
|
505
524
|
const col = (name) => header.indexOf(name);
|
|
506
525
|
if (col("kind") < 0)
|
|
507
|
-
failures.push(`${dir}/test-plan.md: the scenario table has no Kind column (changed | carried)`);
|
|
526
|
+
failures.push(`${dir}/test-plan.md: the scenario table has no Kind column (changed | carried | repaired)`);
|
|
508
527
|
else {
|
|
509
528
|
for (const row of testPlanMapping(plan)) {
|
|
510
529
|
const cell = row[col("requirement")] ?? "";
|
|
@@ -533,13 +552,31 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
533
552
|
continue;
|
|
534
553
|
}
|
|
535
554
|
kindsChecked++;
|
|
536
|
-
|
|
555
|
+
// `repaired` is declared, never computed: a carried scenario whose earlier pass was false-green.
|
|
556
|
+
const repaired = kind === "repaired";
|
|
557
|
+
if (repaired && expected === "changed")
|
|
558
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is changed against the living spec, so it cannot be repaired: repaired is a scenario carried as the living spec states it, whose code did not meet it`);
|
|
559
|
+
else if (!repaired && kind !== expected)
|
|
537
560
|
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is ${expected} against the living spec, and the plan says ${JSON.stringify(kind)}`);
|
|
538
561
|
const before = col("before") >= 0 ? (row[col("before")] ?? "") : "";
|
|
539
|
-
if (before !== ""
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
562
|
+
if (before !== "") {
|
|
563
|
+
// The cell's results, as whole words: "PASS at x; FAIL at y" holds both, and "setup required" or "bypass" holds neither.
|
|
564
|
+
const pass = /\b(?:pass(?:es|ed)?|green)\b/i.test(before);
|
|
565
|
+
const fail = /\b(?:fail(?:s|ed)?|red)\b/i.test(before);
|
|
566
|
+
const said = `the scenario table's Before cell says ${JSON.stringify(before)}`;
|
|
567
|
+
if (repaired) {
|
|
568
|
+
if (!fail)
|
|
569
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is repaired, so its test fails before the code; ${said}`);
|
|
570
|
+
else if (!/\b[0-9a-f]{7,40}\b/i.test(before))
|
|
571
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is repaired, so its Before cell cites the commit that corrects the test double, with its own red run; ${said}`);
|
|
572
|
+
}
|
|
573
|
+
else if (expected === "changed" && (!fail || pass)) {
|
|
574
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is changed, so its test fails before the code${pass && fail ? ", and a cell holding both results is not a result" : ""}; ${said}`);
|
|
575
|
+
}
|
|
576
|
+
else if (expected === "carried" && (!pass || fail)) {
|
|
577
|
+
failures.push(`${dir}/test-plan.md: "${req}" / "${scen}" is carried, so its test passes before the code as regression${fail ? "; if its earlier pass was false-green, declare it repaired and cite the commit that corrects the test double" : ""}; ${said}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
543
580
|
}
|
|
544
581
|
}
|
|
545
582
|
}
|
|
@@ -640,7 +677,7 @@ export async function checkBehaviourStore(repo, options = {}) {
|
|
|
640
677
|
if (a === null || b === null || normaliseBound(f, a) !== normaliseBound(f, b)) {
|
|
641
678
|
const what = `${dir}: AGREED CONTENT CHANGED: ${f} ${a === null ? "was added" : b === null ? "was removed" : "differs"} from what was agreed at ${base}, with no amendment recording a commit and an approver (M-ADR 0222 §4.6)`;
|
|
642
679
|
if (structuralFile(f) && !STRUCTURE_ENFORCED)
|
|
643
|
-
warnings.push(`${what}; a structural delta,
|
|
680
|
+
warnings.push(`${what}; a structural delta, reported only because ENFORCED is off`);
|
|
644
681
|
else
|
|
645
682
|
failures.push(what);
|
|
646
683
|
}
|
|
@@ -43,7 +43,7 @@ const BEHAVIOUR_STORE_VALIDATORS = [
|
|
|
43
43
|
source: { kind: "native", run: async (ctx) => checkJourneyImpact(ctx.targetRepo) },
|
|
44
44
|
build: () => ({ status: "run", args: [] }),
|
|
45
45
|
},
|
|
46
|
-
// The structural model and each open change's structural artifact (M-ADR 0230
|
|
46
|
+
// The structural model and each open change's structural artifact (M-ADR 0230). Report only; [N/A] without a model.
|
|
47
47
|
{
|
|
48
48
|
name: "structural-model",
|
|
49
49
|
source: { kind: "native", run: async (ctx) => checkStructuralModel(ctx.targetRepo) },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// structural-model: the structural model and each open change's structural artifact (M-ADR 0230 §4.3, §4.4
|
|
1
|
+
// structural-model: the structural model and each open change's structural artifact (M-ADR 0230 §4.3, §4.4;
|
|
2
2
|
// docs/method/methods/guides/structural-model-format.md).
|
|
3
3
|
//
|
|
4
4
|
// The model (openspec/structure/<partition>.yaml), as the current view:
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
// (a platform SDD: its domains and `platform`; an application or integration SDD: its sdd_id)
|
|
7
7
|
// elements a `structure` catalogue, its `structure` columns only, an id held once, required columns,
|
|
8
8
|
// value types and vocabularies (the catalogue metamodel's)
|
|
9
|
-
// references resolved in the view; into a catalogue of another class, against that catalogue as built
|
|
9
|
+
// references resolved in the view; into a catalogue of another class, against that catalogue as built, less the
|
|
10
|
+
// contracts open changes' records agree as new (lib/structure/intended.ts): those are in their change's
|
|
11
|
+
// view, its dependants' and the intended one, so a current reference to one is refused (§4.11 proof 3)
|
|
10
12
|
// hierarchies no cycle through a self-reference; natural keys unique; domain invariants naming their entity's own
|
|
11
13
|
// attributes and relationships
|
|
12
|
-
// views
|
|
13
|
-
// hand (its body no longer matches the digest in its header)
|
|
14
|
+
// views docs/structure/ absent where the model is admitted, and each generated view under it missing, stale
|
|
15
|
+
// (rendered from an earlier model) or edited by hand (its body no longer matches the digest in its header)
|
|
14
16
|
// Each open change (openspec/changes/<change>/structure/):
|
|
15
17
|
// artifact where the model exists, a change has partition deltas or none.yaml, never both
|
|
16
18
|
// deltas well formed; ADDED ids new; MODIFIED and REMOVED ids held by the delta's partition; a base
|
|
@@ -29,20 +31,22 @@
|
|
|
29
31
|
// archived a change archived with structural deltas and no effect record (a raw `openspec archive`): its
|
|
30
32
|
// structure is not applied, so its transition is not shipped
|
|
31
33
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
+
// Enforced (the release that accepts M-ADR 0230): every finding is a [FAIL] and the check
|
|
35
|
+
// exits 1; with ENFORCED false it reports them as [WARN] and exits 0. Without openspec/structure/ and with no change
|
|
36
|
+
// carrying a structure/ directory it reports [N/A], so an SDD without a model is unaffected.
|
|
34
37
|
import { readdir, readFile } from "node:fs/promises";
|
|
35
38
|
import { join } from "node:path";
|
|
36
39
|
import { parseDocument } from "yaml";
|
|
37
40
|
import { parseCatalogueCsv } from "../catalogue/csv-reader.js";
|
|
38
41
|
import { loadCanonicalSchema } from "../catalogue/schema-loader.js";
|
|
39
42
|
import { APPLIED_FILE, elementKey, loadChangeStructure, loadModel, STRUCTURE_DIR } from "../structure/model.js";
|
|
40
|
-
import { agreedOrder, attribute, checkDependencies, checkOverlap, intended, viewAt } from "../structure/compose.js";
|
|
43
|
+
import { agreedOrder, attribute, checkDependencies, checkOverlap, closure, intended, viewAt } from "../structure/compose.js";
|
|
41
44
|
import { describeState, renderViews, VIEW_PATHS, viewState } from "../structure/render.js";
|
|
45
|
+
import { Additions, CONTRACTS, readAgreement } from "../structure/intended.js";
|
|
42
46
|
import { validateDeltas, validateView, viewOf } from "../structure/view.js";
|
|
43
47
|
const VIEW_COUNT = Object.keys(VIEW_PATHS).length;
|
|
44
|
-
/**
|
|
45
|
-
export const ENFORCED =
|
|
48
|
+
/** Findings FAIL (from M-ADR 0230's acceptance); false reports them only, as while M-ADR 0230 was Proposed. Also binds a change's structural deltas as agreed content in the behaviour-store check. */
|
|
49
|
+
export const ENFORCED = true;
|
|
46
50
|
const CHANGES_DIR = join("openspec", "changes");
|
|
47
51
|
async function readText(p) {
|
|
48
52
|
try {
|
|
@@ -60,19 +64,6 @@ async function subdirs(p) {
|
|
|
60
64
|
return [];
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
|
-
/** The change's agreement record status (docs/agreements/<change>.yaml), or `unrecorded`. */
|
|
64
|
-
async function agreementStatus(repo, change) {
|
|
65
|
-
const text = await readText(join(repo, "docs", "agreements", `${change}.yaml`));
|
|
66
|
-
if (text === null)
|
|
67
|
-
return "unrecorded";
|
|
68
|
-
try {
|
|
69
|
-
const status = String(parseDocument(text).toJS()?.agreement_record?.status ?? "");
|
|
70
|
-
return ["proposed", "agreed", "withdrawn", "superseded"].find((s) => s === status) ?? "unrecorded";
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return "unrecorded";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
67
|
/** Names of archived changes: openspec/changes/archive/<YYYY-MM-DD>-<name>. */
|
|
77
68
|
async function archivedNames(repo) {
|
|
78
69
|
return new Set((await subdirs(join(repo, CHANGES_DIR, "archive"))).map((d) => d.replace(/^\d{4}-\d{2}-\d{2}-/, "")));
|
|
@@ -91,8 +82,22 @@ function builtLookup(repo) {
|
|
|
91
82
|
const text = await readText(join(repo, "catalogues", `${catalogue}.csv`));
|
|
92
83
|
cache.set(catalogue, text === null ? null : parseCatalogueCsv(text).rows);
|
|
93
84
|
};
|
|
85
|
+
fn.rows = (catalogue) => cache.get(catalogue) ?? [];
|
|
94
86
|
return fn;
|
|
95
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The built catalogues as one view sees them. A contract an open change's record agrees as new is built from the
|
|
90
|
+
* change's own sources, so it is in no view but that change's, its dependants' and, once agreed, the intended one.
|
|
91
|
+
*/
|
|
92
|
+
function builtIn(built, all, mine) {
|
|
93
|
+
return (catalogue, column) => {
|
|
94
|
+
const rows = built(catalogue, column);
|
|
95
|
+
if (rows === null || catalogue !== CONTRACTS || column !== "id")
|
|
96
|
+
return rows;
|
|
97
|
+
const out = new Set([...rows].filter((id) => !all.has(id) || mine.has(id)));
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
96
101
|
export async function checkStructuralModel(repo, options = {}) {
|
|
97
102
|
const enforced = options.enforced ?? ENFORCED;
|
|
98
103
|
const problems = [];
|
|
@@ -123,6 +128,16 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
123
128
|
for (const c of schema.catalogues)
|
|
124
129
|
if (c.sourceOfTruth !== "structure")
|
|
125
130
|
await built.load(c.name);
|
|
131
|
+
const open = new Map();
|
|
132
|
+
const newContracts = new Map();
|
|
133
|
+
for (const s of structures) {
|
|
134
|
+
const agreement = await readAgreement(repo, s.change);
|
|
135
|
+
open.set(s.change, { structure: s, status: agreement.status });
|
|
136
|
+
newContracts.set(s.change, agreement.newContracts);
|
|
137
|
+
}
|
|
138
|
+
const additions = new Additions(open, newContracts, built.rows(CONTRACTS));
|
|
139
|
+
const allNew = additions.contracts(open.keys());
|
|
140
|
+
const addedBy = (catalogue, id) => additions.addedBy(catalogue, id);
|
|
126
141
|
let allowed = null;
|
|
127
142
|
if (profile === "platform-profile") {
|
|
128
143
|
allowed = new Set(["platform", ...(view.get("domains")?.keys() ?? []), ...(built("domains", "id") ?? [])]);
|
|
@@ -143,14 +158,14 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
143
158
|
// The current view.
|
|
144
159
|
const unresolved = new Set();
|
|
145
160
|
const currentProblems = [];
|
|
146
|
-
validateView(schema, view, built, currentProblems, unresolved);
|
|
161
|
+
validateView(schema, view, builtIn(built, allNew, new Set()), currentProblems, unresolved, addedBy);
|
|
147
162
|
problems.push(...currentProblems);
|
|
148
163
|
const elementCount = [...view.values()].reduce((n, m) => n + m.size, 0);
|
|
149
164
|
lines.push(files === null ? `model: none (${STRUCTURE_DIR}/ absent)` : `model: ${files.length} partition file(s), ${elementCount} element(s) in ${view.size} catalogue(s)`);
|
|
150
165
|
if (unresolved.size > 0)
|
|
151
166
|
lines.push(`references into ${[...unresolved].sort().join(", ")}: unresolved (no built catalogue)`);
|
|
152
|
-
// The generated views, against a render of the current view.
|
|
153
|
-
//
|
|
167
|
+
// The generated views, against a render of the current view. Where a model is admitted they are required (M-ADR
|
|
168
|
+
// 0230 §4.3): an SDD reviews an extraction before writing it (§6 step 1), so none is mid-adoption with a model.
|
|
154
169
|
if (files !== null && (await subdirs(join(repo, "docs"))).includes("structure")) {
|
|
155
170
|
let current = 0;
|
|
156
171
|
for (const [path, rendered] of renderViews(view)) {
|
|
@@ -163,14 +178,15 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
163
178
|
lines.push(`generated views: ${current} of ${VIEW_COUNT} current`);
|
|
164
179
|
}
|
|
165
180
|
else if (files !== null) {
|
|
166
|
-
|
|
181
|
+
problems.push("docs/structure/: the generated views are not rendered; where a structural model is admitted they are required. `structure render` writes them (sdd-maintain or sdd-cli)");
|
|
182
|
+
lines.push(`generated views: 0 of ${VIEW_COUNT} (no docs/structure/)`);
|
|
167
183
|
}
|
|
168
184
|
// Open changes.
|
|
169
|
-
const
|
|
185
|
+
const addedByChange = new Map();
|
|
170
186
|
for (const s of structures)
|
|
171
187
|
for (const d of s.deltas)
|
|
172
188
|
for (const e of d.added)
|
|
173
|
-
|
|
189
|
+
addedByChange.set(elementKey(e.catalogue, e.id), s.change);
|
|
174
190
|
const features = new Set(view.get("features")?.keys() ?? []);
|
|
175
191
|
let withDeltas = 0;
|
|
176
192
|
let noOps = 0;
|
|
@@ -190,7 +206,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
190
206
|
}
|
|
191
207
|
if (s.noOp && s.deltas.length > 0)
|
|
192
208
|
problems.push(`${s.noOp.file}: a no-op claim beside ${s.deltas.length} partition delta(s); state one or the other`);
|
|
193
|
-
const elsewhere = new Map([...
|
|
209
|
+
const elsewhere = new Map([...addedByChange].filter(([, c]) => c !== s.change));
|
|
194
210
|
validateDeltas(schema, view, s.deltas, elsewhere, problems);
|
|
195
211
|
// A behaviour delta for a feature the model does not hold needs that feature added here.
|
|
196
212
|
const adds = new Set(s.deltas.flatMap((d) => d.added.filter((e) => e.catalogue === "features").map((e) => e.id)));
|
|
@@ -207,9 +223,6 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
207
223
|
}
|
|
208
224
|
lines.push(`changes: ${changes.length} open, ${withDeltas} with structural deltas, ${noOps} claiming none, ${without} without the artifact`);
|
|
209
225
|
// Composition (M-ADR 0230 §4.5): the three views and the three checks.
|
|
210
|
-
const open = new Map();
|
|
211
|
-
for (const s of structures)
|
|
212
|
-
open.set(s.change, { structure: s, status: await agreementStatus(repo, s.change) });
|
|
213
226
|
const archived = await archivedNames(repo);
|
|
214
227
|
checkOverlap(open, problems);
|
|
215
228
|
checkDependencies(open, archived, problems);
|
|
@@ -219,7 +232,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
219
232
|
if (oc.structure.deltas.length === 0)
|
|
220
233
|
continue;
|
|
221
234
|
const at = [];
|
|
222
|
-
validateView(schema, viewAt(view, name, open), built, at, new Set());
|
|
235
|
+
validateView(schema, viewAt(view, name, open), builtIn(built, allNew, additions.contracts(closure(name, open))), at, new Set(), addedBy);
|
|
223
236
|
for (const p of at) {
|
|
224
237
|
if (known.has(p))
|
|
225
238
|
continue;
|
|
@@ -231,7 +244,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
231
244
|
const agreed = agreedOrder(open);
|
|
232
245
|
if (agreed.length > 0) {
|
|
233
246
|
const inIntended = [];
|
|
234
|
-
validateView(schema, intended(view, open), built, inIntended, new Set());
|
|
247
|
+
validateView(schema, intended(view, open), builtIn(built, allNew, additions.contracts(agreed)), inIntended, new Set(), addedBy);
|
|
235
248
|
for (const p of inIntended) {
|
|
236
249
|
if (known.has(p) || atAgreed.has(p))
|
|
237
250
|
continue;
|
|
@@ -259,7 +272,7 @@ export async function checkStructuralModel(repo, options = {}) {
|
|
|
259
272
|
if (problems.length === 0)
|
|
260
273
|
return { exitCode: 0, stdout: `structural-model:\n${body}[PASS] structural-model: ok\n` };
|
|
261
274
|
const tag = enforced ? "FAIL" : "WARN";
|
|
262
|
-
const tail = enforced ? `[FAIL] structural-model: ${problems.length} finding(s)\n` : `[WARN] structural-model: ${problems.length} finding(s), report only (M-ADR 0230
|
|
275
|
+
const tail = enforced ? `[FAIL] structural-model: ${problems.length} finding(s)\n` : `[WARN] structural-model: ${problems.length} finding(s), report only (M-ADR 0230)\n`;
|
|
263
276
|
return { exitCode: enforced ? 1 : 0, stdout: `structural-model:\n${body}${problems.map((p) => `[${tag}] ${p}\n`).join("")}${tail}` };
|
|
264
277
|
}
|
|
265
278
|
//# sourceMappingURL=structural-model.js.map
|
package/dist/maintain-program.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// (M-ADR 0218 §4.2–4.3): the method payload, lock, CI block and briefing (`sync`,
|
|
8
8
|
// `generate-briefing`), the derived model (`catalogue build | aggregate | graph | diff`), an
|
|
9
9
|
// output the caller names (`pack build`, `render screen`), and a code repository's briefing
|
|
10
|
-
// (`context pull`)
|
|
10
|
+
// (`context pull`), and the structural model's generated views (`structure render`, with `structure show` beside
|
|
11
|
+
// them). Plus the whole verify surface, because the personas nest.
|
|
11
12
|
//
|
|
12
13
|
// ⚠ As with sdd-verify, the boundary is ABSENCE, not a menu: the package ships only the import
|
|
13
14
|
// closure of maintain-bin.js, so authoring verbs (init, scaffold, plan, conformance,
|
|
@@ -23,6 +24,7 @@ import { registerContext } from "./verbs/context.js";
|
|
|
23
24
|
import { registerGenerateBriefing } from "./verbs/generate-briefing.js";
|
|
24
25
|
import { registerPackBuild } from "./verbs/pack-build.js";
|
|
25
26
|
import { registerRender } from "./verbs/render.js";
|
|
27
|
+
import { registerStructure } from "./verbs/structure.js";
|
|
26
28
|
import { registerSync } from "./verbs/sync.js";
|
|
27
29
|
const SPEC_URL = "docs/method/cli/sdd-cli-specification.md (in the method distribution)";
|
|
28
30
|
/** The verbs maintain adds to verify, as `<verb>` or `<verb> <subcommand>`. Asserted by test. */
|
|
@@ -37,6 +39,8 @@ export const MAINTAIN_ONLY_SURFACE = [
|
|
|
37
39
|
"catalogue diff",
|
|
38
40
|
"pack build",
|
|
39
41
|
"render screen",
|
|
42
|
+
"structure show",
|
|
43
|
+
"structure render",
|
|
40
44
|
];
|
|
41
45
|
/** The full surface: verify's, plus maintain's own. */
|
|
42
46
|
export const MAINTAIN_SURFACE = [...VERIFY_SURFACE, ...MAINTAIN_ONLY_SURFACE];
|
|
@@ -79,6 +83,8 @@ export function buildMaintainProgram() {
|
|
|
79
83
|
registerGenerateBriefing(program);
|
|
80
84
|
registerContext(program);
|
|
81
85
|
registerRender(program);
|
|
86
|
+
// The structural model's generated views are derived (M-ADR 0230 §4.3); show is the read beside them.
|
|
87
|
+
registerStructure(program);
|
|
82
88
|
const catalogue = program.commands.find((c) => c.name() === "catalogue");
|
|
83
89
|
catalogue.description("Build, aggregate, validate and query per-SDD catalogues (M-ADR 0134) — the derived model");
|
|
84
90
|
registerCatalogueBuild(catalogue);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { stringify } from "yaml";
|
|
4
|
+
import { elementKey, loadModel, STRUCTURE_DIR } from "../lib/structure/model.js";
|
|
5
|
+
import { describeState, renderViews, viewState } from "../lib/structure/render.js";
|
|
6
|
+
import { revisionOf, viewOf } from "../lib/structure/view.js";
|
|
7
|
+
// `structure show <catalogue>/<id>`: one element of the structural model and its revision, the value a change's
|
|
8
|
+
// `base:` records for it (M-ADR 0230 §4.4). Read only.
|
|
9
|
+
export async function runStructureShow(sddPath, ref, write) {
|
|
10
|
+
const slash = ref.indexOf("/");
|
|
11
|
+
if (slash <= 0 || slash === ref.length - 1) {
|
|
12
|
+
write(`[FAIL] structure show: name an element as <catalogue>/<id>, not ${JSON.stringify(ref)}\n`);
|
|
13
|
+
return 2;
|
|
14
|
+
}
|
|
15
|
+
const catalogue = ref.slice(0, slash);
|
|
16
|
+
const id = ref.slice(slash + 1);
|
|
17
|
+
const problems = [];
|
|
18
|
+
const files = await loadModel(sddPath, problems);
|
|
19
|
+
if (files === null) {
|
|
20
|
+
write(`[FAIL] structure show: no structural model (${STRUCTURE_DIR}/) in ${sddPath}\n`);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const e = viewOf(files, problems).get(catalogue)?.get(id);
|
|
24
|
+
if (!e) {
|
|
25
|
+
write(`[FAIL] structure show: the model holds no ${elementKey(catalogue, id)}\n`);
|
|
26
|
+
for (const p of problems)
|
|
27
|
+
write(` ${p}\n`);
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
write(`# ${elementKey(catalogue, id)} in ${e.file} (partition ${e.partition})\n`);
|
|
31
|
+
write(`revision: ${revisionOf(e)}\n`);
|
|
32
|
+
write(stringify({ id: e.id, ...e.columns }));
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
// `structure render [--check]`: write the generated views of the current view (M-ADR 0230 §4.3), or, with
|
|
36
|
+
// --check, write nothing and report each view that is missing, stale or edited by hand (exit 1 if any is).
|
|
37
|
+
export async function runStructureRender(sddPath, options, write) {
|
|
38
|
+
const problems = [];
|
|
39
|
+
const files = await loadModel(sddPath, problems);
|
|
40
|
+
if (files === null) {
|
|
41
|
+
write(`[FAIL] structure render: no structural model (${STRUCTURE_DIR}/) in ${sddPath}\n`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
const view = viewOf(files, problems);
|
|
45
|
+
if (problems.length > 0) {
|
|
46
|
+
write(`[FAIL] structure render: the model does not load cleanly; run \`sdd-cli validate --validator structural-model\`\n`);
|
|
47
|
+
for (const p of problems)
|
|
48
|
+
write(` ${p}\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
let off = 0;
|
|
52
|
+
for (const [path, rendered] of renderViews(view)) {
|
|
53
|
+
const abs = join(sddPath, path);
|
|
54
|
+
const text = await readFile(abs, "utf8").catch(() => null);
|
|
55
|
+
const state = viewState(rendered, text);
|
|
56
|
+
if (options.check) {
|
|
57
|
+
if (state !== "current") {
|
|
58
|
+
off++;
|
|
59
|
+
write(`[WARN] ${describeState(path, state)}\n`);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (state === "hand-edited")
|
|
64
|
+
write(`[WARN] ${describeState(path, state)}; the edit is overwritten\n`);
|
|
65
|
+
if (state !== "current") {
|
|
66
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
67
|
+
await writeFile(abs, rendered, "utf8");
|
|
68
|
+
}
|
|
69
|
+
write(`${state === "current" ? "unchanged" : "wrote"} ${path}\n`);
|
|
70
|
+
}
|
|
71
|
+
if (options.check)
|
|
72
|
+
write(off === 0 ? "[PASS] structure render --check: every view is current\n" : `[FAIL] structure render --check: ${off} view(s) not current\n`);
|
|
73
|
+
return options.check && off > 0 ? 1 : 0;
|
|
74
|
+
}
|
|
75
|
+
// Read the model and write its generated views: maintain's and sdd-cli's (M-ADR 0230 §4.3, at its acceptance).
|
|
76
|
+
// Extraction is the author's (migrate-structure.ts), registered apart so this module's closure carries no extractor.
|
|
77
|
+
export function registerStructure(program) {
|
|
78
|
+
const structure = program.command("structure").description("Read the structural model, and write its generated views (M-ADR 0230)");
|
|
79
|
+
structure
|
|
80
|
+
.command("show <element>")
|
|
81
|
+
.description("Print one element of the structural model, <catalogue>/<id>, with its revision")
|
|
82
|
+
.option("--sdd-path <path>", "the SDD (default: the current directory)", ".")
|
|
83
|
+
.action(async (element, opts) => {
|
|
84
|
+
process.exitCode = await runStructureShow(opts.sddPath, element, (s) => process.stdout.write(s));
|
|
85
|
+
});
|
|
86
|
+
structure
|
|
87
|
+
.command("render")
|
|
88
|
+
.description("Write the generated views of the structural model under docs/structure/; --check reports instead")
|
|
89
|
+
.option("--sdd-path <path>", "the SDD (default: the current directory)", ".")
|
|
90
|
+
.option("--check", "write nothing; report each view that is missing, stale or edited by hand", false)
|
|
91
|
+
.action(async (opts) => {
|
|
92
|
+
process.exitCode = await runStructureRender(opts.sddPath, { check: opts.check }, (s) => process.stdout.write(s));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=structure.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": "5.
|
|
3
|
+
"version": "0.15.1",
|
|
4
|
+
"sddCliVersion": "5.1.1",
|
|
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",
|