@sdd-method/author-cli 0.12.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/author-program.js +10 -2
- 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/apply.js +71 -0
- package/dist/lib/structure/archive.js +330 -0
- 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/extract.js +123 -0
- 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-version.json +2 -2
- package/dist/verbs/change.js +15 -0
- package/dist/verbs/migrate-structure.js +36 -0
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { archiveChange } from "../lib/structure/archive.js";
|
|
2
|
+
// `change archive <change>`: the method's archive operation (M-ADR 0230 §4.6). Behaviour and structure are
|
|
3
|
+
// prepared in a scratch worktree, validated as a whole, and published as one commit only if the store has not moved.
|
|
4
|
+
// Run it again after an interruption: it verifies an archive that already landed, and recovers a raw `openspec archive`.
|
|
5
|
+
export function registerChange(program) {
|
|
6
|
+
const change = program.command("change").description("Operate on a change in the behaviour store (M-ADR 0223, M-ADR 0230)");
|
|
7
|
+
change
|
|
8
|
+
.command("archive <change>")
|
|
9
|
+
.description("Archive a judged change: its behaviour and structure published as one guarded commit (M-ADR 0230 §4.6)")
|
|
10
|
+
.option("--sdd-path <path>", "the SDD whose openspec/ is the store (default: the current directory)", ".")
|
|
11
|
+
.action(async (name, opts) => {
|
|
12
|
+
process.exitCode = await archiveChange({ sddPath: opts.sddPath, change: name, write: (s) => process.stdout.write(s) });
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=change.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { extractStructure, writeExtraction } from "../lib/structure/extract.js";
|
|
2
|
+
// `migrate structure [--dry-run]`: extract an SDD's first structural model from its sources, with its report
|
|
3
|
+
// (M-ADR 0230 §6 step 1). Refuses an SDD that has a model; writes no source file.
|
|
4
|
+
export async function runMigrateStructure(sddPath, options, write) {
|
|
5
|
+
const x = await extractStructure(sddPath);
|
|
6
|
+
if (x.refused !== null) {
|
|
7
|
+
write(`[FAIL] migrate structure: ${x.refused}\n`);
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
const total = [...x.counts.values()].reduce((a, b) => a + b, 0);
|
|
11
|
+
write(`${options.dryRun ? "would extract" : "extracted"} ${total} element(s) in ${x.counts.size} catalogue(s), ${x.files.size} partition(s); ${x.loss.length + x.extraction.length} fact(s) the model could not carry\n`);
|
|
12
|
+
if (options.dryRun) {
|
|
13
|
+
write(x.report);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
for (const p of await writeExtraction(sddPath, x))
|
|
17
|
+
write(`wrote ${p}\n`);
|
|
18
|
+
write("[INFO] migrate structure: the model is now the record of structure, and the catalogue is built from it. Keep the original sources until it is reviewed against the code; removing openspec/structure/ withdraws it\n");
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
// Authored content, so the author's and sdd-cli's (M-ADR 0230 §6, at its acceptance). Attaches to the
|
|
22
|
+
// `migrate` command its program already registers.
|
|
23
|
+
export function registerMigrateStructure(program) {
|
|
24
|
+
const migrate = program.commands.find((c) => c.name() === "migrate");
|
|
25
|
+
if (!migrate)
|
|
26
|
+
throw new Error("registerMigrateStructure: register `migrate` first");
|
|
27
|
+
migrate
|
|
28
|
+
.command("structure")
|
|
29
|
+
.description("Extract the SDD's first structural model from its sources into openspec/structure/, with an extraction report; source files are untouched (M-ADR 0230)")
|
|
30
|
+
.option("--sdd-path <path>", "the SDD (default: the current directory)", ".")
|
|
31
|
+
.option("--dry-run", "write nothing; print the report", false)
|
|
32
|
+
.action(async (opts) => {
|
|
33
|
+
process.exitCode = await runMigrateStructure(opts.sddPath, { dryRun: opts.dryRun }, (s) => process.stdout.write(s));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=migrate-structure.js.map
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/author-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"sddCliVersion": "
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"sddCliVersion": "5.1.0",
|
|
5
5
|
"description": "Re-author one SDD's content — scaffold, plan, contracts, conformance, api-surface, snapshots, briefings, catalogue check — plus every read-only verify verb. Ships only the code those verbs reach; it cannot sync the baseline or rebuild the catalogues. Maintenance is in @sdd-method/maintain-cli, administration in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"type": "module",
|