@sdd-method/author-cli 0.8.0 → 0.10.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/author-program.js +2 -0
- package/dist/lib/behaviour-store/archive.js +28 -1
- package/dist/lib/behaviour-store/schema.js +85 -0
- package/dist/lib/catalogue/build.js +3 -0
- package/dist/lib/catalogue/builders/requirements.js +110 -11
- package/dist/lib/catalogue/canonical-schema.yaml +138 -2
- package/dist/lib/catalogue/rubric-coverage-map.yaml +28 -0
- package/dist/lib/intents/format.js +77 -0
- package/dist/lib/intents/migrate.js +644 -0
- package/dist/lib/repo-kind.js +1 -9
- package/dist/lib/scaffold/archetype-behaviour.js +6 -4
- package/dist/lib/scaffold/behaviour-store.js +60 -0
- package/dist/lib/validate/archetype-citations.js +37 -2
- package/dist/lib/validate/behaviour-store.js +137 -6
- package/dist/lib/validate/dispatch.js +7 -0
- package/dist/lib/validate/intents.js +76 -0
- package/dist/maintain-version.json +2 -2
- package/dist/verbs/migrate.js +44 -0
- package/dist/verbs/scaffold.js +29 -0
- package/dist/verify-version.json +2 -2
- package/package.json +2 -2
package/dist/author-program.js
CHANGED
|
@@ -31,6 +31,7 @@ const SPEC_URL = "docs/method/cli/sdd-cli-specification.md (in the method distri
|
|
|
31
31
|
/** The verbs the author adds to verify, as `<verb>` or `<verb> <subcommand>` (M-ADR 0220 §4.2). Asserted by test. */
|
|
32
32
|
export const AUTHOR_ONLY_SURFACE = [
|
|
33
33
|
"scaffold domain",
|
|
34
|
+
"scaffold behaviour-store",
|
|
34
35
|
"plan",
|
|
35
36
|
"contracts attest",
|
|
36
37
|
"conformance",
|
|
@@ -46,6 +47,7 @@ export const AUTHOR_ONLY_SURFACE = [
|
|
|
46
47
|
"context pull",
|
|
47
48
|
"catalogue check",
|
|
48
49
|
"migrate vocabulary",
|
|
50
|
+
"migrate intents",
|
|
49
51
|
];
|
|
50
52
|
/** The full surface: verify's, plus the author's own. */
|
|
51
53
|
export const AUTHOR_SURFACE = [...VERIFY_SURFACE, ...AUTHOR_ONLY_SURFACE];
|
|
@@ -64,6 +64,33 @@ export function renamedFrom(text) {
|
|
|
64
64
|
}
|
|
65
65
|
return out;
|
|
66
66
|
}
|
|
67
|
+
/** The FROM -> TO pairs of a RENAMED section, in file order. */
|
|
68
|
+
export function renamePairs(text) {
|
|
69
|
+
const out = [];
|
|
70
|
+
let op = null;
|
|
71
|
+
let from = null;
|
|
72
|
+
for (const line of text.split("\n")) {
|
|
73
|
+
const section = /^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements\b/.exec(line);
|
|
74
|
+
if (section) {
|
|
75
|
+
op = section[1];
|
|
76
|
+
from = null;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (op !== "RENAMED")
|
|
80
|
+
continue;
|
|
81
|
+
const f = /^\s*-\s*FROM:\s*`?###\s+Requirement:\s*(.+?)`?\s*$/.exec(line);
|
|
82
|
+
if (f) {
|
|
83
|
+
from = f[1].trim();
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const t = /^\s*-\s*TO:\s*`?###\s+Requirement:\s*(.+?)`?\s*$/.exec(line);
|
|
87
|
+
if (t && from !== null) {
|
|
88
|
+
out.push({ from, to: t[1].trim() });
|
|
89
|
+
from = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
67
94
|
/** Each archive directory's position in the SDD's history: the index of the first commit that added a file under it. */
|
|
68
95
|
async function archiveHistory(sddPath, archiveRel) {
|
|
69
96
|
let stdout;
|
|
@@ -100,7 +127,7 @@ export async function orderArchives(sddPath, archiveRel, names) {
|
|
|
100
127
|
if (date(a) === date(b) && position(a) === position(b))
|
|
101
128
|
ties.push([a, b]);
|
|
102
129
|
}
|
|
103
|
-
return { order, ties };
|
|
130
|
+
return { order, ties, history: history !== null };
|
|
104
131
|
}
|
|
105
132
|
/** The repositories a baseline was observed in: its evidence file's `**Observed in:** repo[@sha], repo[@sha]` line. */
|
|
106
133
|
export function observedIn(evidence) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The method's OpenSpec change schema (M-ADR 0223 §4.2) and its copy in a behaviour store.
|
|
2
|
+
//
|
|
3
|
+
// The method ships the schema inert, at docs/method/foundations/openspec/sdd-change/, because
|
|
4
|
+
// sync writes an SDD's managed paths and a store is created only when its SDD first needs one
|
|
5
|
+
// (M-ADR 0221 §4.2): a managed path at openspec/ would create a store in every SDD.
|
|
6
|
+
// OpenSpec reads a project schema only from <store>/openspec/schemas/<name>/, so the store
|
|
7
|
+
// holds a copy. The copy is method-managed: sync refreshes it, scaffold writes it, and the
|
|
8
|
+
// behaviour-store validator fails when it differs. Adopter guidance goes in the store's
|
|
9
|
+
// config.yaml (`context:`, per-artifact `rules:`), never in the copy.
|
|
10
|
+
import { cp, mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
11
|
+
import { join, relative } from "node:path";
|
|
12
|
+
import { parseDocument } from "yaml";
|
|
13
|
+
export const SCHEMA_NAME = "sdd-change";
|
|
14
|
+
export const SHIPPED_SCHEMA_DIR = "docs/method/foundations/openspec/sdd-change";
|
|
15
|
+
export const SHIPPED_CONFIG_TEMPLATE = "docs/method/foundations/openspec/config-template.yaml";
|
|
16
|
+
export const STORE_SCHEMA_DIR = `openspec/schemas/${SCHEMA_NAME}`;
|
|
17
|
+
export const STORE_CONFIG = "openspec/config.yaml";
|
|
18
|
+
async function files(dir) {
|
|
19
|
+
const out = [];
|
|
20
|
+
async function walk(d) {
|
|
21
|
+
for (const e of await readdir(d, { withFileTypes: true })) {
|
|
22
|
+
const p = join(d, e.name);
|
|
23
|
+
if (e.isDirectory())
|
|
24
|
+
await walk(p);
|
|
25
|
+
else if (e.isFile())
|
|
26
|
+
out.push(relative(dir, p));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await walk(dir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return out.sort();
|
|
36
|
+
}
|
|
37
|
+
async function read(p) {
|
|
38
|
+
try {
|
|
39
|
+
return await readFile(p);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The schema the store's config.yaml selects: its name, null without a config, or "" without a `schema:` key. */
|
|
46
|
+
export async function storeSchemaName(repo) {
|
|
47
|
+
const text = await read(join(repo, STORE_CONFIG));
|
|
48
|
+
if (text === null)
|
|
49
|
+
return null;
|
|
50
|
+
const v = parseDocument(text.toString("utf8")).toJS()?.schema;
|
|
51
|
+
return typeof v === "string" ? v : "";
|
|
52
|
+
}
|
|
53
|
+
/** Compare the store's copy of the schema with the one the SDD's method bundle shipped, file by file. */
|
|
54
|
+
export async function compareStoreSchema(repo) {
|
|
55
|
+
const shipped = await files(join(repo, SHIPPED_SCHEMA_DIR));
|
|
56
|
+
if (shipped === null || shipped.length === 0)
|
|
57
|
+
return { shipped: false, missing: [], differing: [], extra: [] };
|
|
58
|
+
const store = (await files(join(repo, STORE_SCHEMA_DIR))) ?? [];
|
|
59
|
+
const missing = shipped.filter((f) => !store.includes(f));
|
|
60
|
+
const extra = store.filter((f) => !shipped.includes(f));
|
|
61
|
+
const differing = [];
|
|
62
|
+
for (const f of shipped.filter((x) => store.includes(x))) {
|
|
63
|
+
const a = await read(join(repo, SHIPPED_SCHEMA_DIR, f));
|
|
64
|
+
const b = await read(join(repo, STORE_SCHEMA_DIR, f));
|
|
65
|
+
if (a === null || b === null || !a.equals(b))
|
|
66
|
+
differing.push(f);
|
|
67
|
+
}
|
|
68
|
+
return { shipped: true, missing, differing, extra };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Replace the store's copy of the schema with the shipped one. Returns what changed, as a
|
|
72
|
+
* comparison taken before the write; a store already current returns empty lists.
|
|
73
|
+
*/
|
|
74
|
+
export async function writeStoreSchema(repo) {
|
|
75
|
+
const before = await compareStoreSchema(repo);
|
|
76
|
+
if (!before.shipped)
|
|
77
|
+
return before;
|
|
78
|
+
if (before.missing.length + before.differing.length + before.extra.length === 0)
|
|
79
|
+
return before;
|
|
80
|
+
await rm(join(repo, STORE_SCHEMA_DIR), { recursive: true, force: true });
|
|
81
|
+
await mkdir(join(repo, STORE_SCHEMA_DIR), { recursive: true });
|
|
82
|
+
await cp(join(repo, SHIPPED_SCHEMA_DIR), join(repo, STORE_SCHEMA_DIR), { recursive: true });
|
|
83
|
+
return before;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -1167,6 +1167,9 @@ async function collectRows(schema, manifest, findings, sddPath, snapshotStore) {
|
|
|
1167
1167
|
out.set("requirements", r.requirements);
|
|
1168
1168
|
out.set("requirement_lineage", r.lineage);
|
|
1169
1169
|
out.set("requirement_status", r.status);
|
|
1170
|
+
out.set("changes", r.changes);
|
|
1171
|
+
out.set("change_work_order", r.workOrders);
|
|
1172
|
+
out.set("change_work_item", r.workItems);
|
|
1170
1173
|
}
|
|
1171
1174
|
// 15. C4 systems + containers + components (L1 + L2 + L3). The L3 walker
|
|
1172
1175
|
// is the forward-method authoring path reading
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
// directory name, which is the catalogue feature id; plus one per requirement an open,
|
|
9
9
|
// agreed change ADDS (stage agreed).
|
|
10
10
|
// requirement_lineage each archived change, in archive order, whose delta ADDED, MODIFIED or RENAMED a
|
|
11
|
-
// requirement that is current now
|
|
11
|
+
// requirement that is current now, under any name a later RENAMED gave it. A change carrying
|
|
12
|
+
// baseline-evidence.md is a baseline. Statuses follow renames the same way.
|
|
13
|
+
// changes each change in the store, open or archived (M-ADR 0223 §4.2): its directory name, its
|
|
14
|
+
// undated name, kind, status (archived, or its agreement record's) and verdict.
|
|
15
|
+
// change_work_order per (change, repository): the record's work order, agreed or completed.
|
|
16
|
+
// change_work_item per (change, work item): the ids its proposal's `## Work Items` section lists, keyed by
|
|
17
|
+
// the undated name so the row survives archive.
|
|
12
18
|
// requirement_status per (requirement, repository): observed from a baseline's `**Observed in:**` line;
|
|
13
19
|
// declared-shipped from an archived change whose agreement record's verdict is PASS, through
|
|
14
20
|
// its work orders' `implements` lists; agreed from an open change whose record is agreed.
|
|
@@ -20,7 +26,7 @@
|
|
|
20
26
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
21
27
|
import { join } from "node:path";
|
|
22
28
|
import { parse as parseYaml } from "yaml";
|
|
23
|
-
import { observedIn, orderArchives, parseRequirements } from "../../behaviour-store/archive.js";
|
|
29
|
+
import { observedIn, orderArchives, parseRequirements, renamePairs } from "../../behaviour-store/archive.js";
|
|
24
30
|
export { observedIn, parseRequirements };
|
|
25
31
|
const STORE = "openspec";
|
|
26
32
|
const AGREEMENTS = join("docs", "agreements");
|
|
@@ -62,6 +68,17 @@ async function deltaOf(changeDir) {
|
|
|
62
68
|
}
|
|
63
69
|
return out;
|
|
64
70
|
}
|
|
71
|
+
async function renamesOf(changeDir) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const feature of await dirs(join(changeDir, "specs"))) {
|
|
74
|
+
const text = await readText(join(changeDir, "specs", feature, "spec.md"));
|
|
75
|
+
if (text === null)
|
|
76
|
+
continue;
|
|
77
|
+
for (const r of renamePairs(text))
|
|
78
|
+
out.push({ feature, ...r });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
65
82
|
async function agreementRecord(sddPath, change) {
|
|
66
83
|
const text = await readText(join(sddPath, AGREEMENTS, `${change}.yaml`));
|
|
67
84
|
if (text === null)
|
|
@@ -82,10 +99,33 @@ export async function archiveOrder(sddPath, names, findings) {
|
|
|
82
99
|
}
|
|
83
100
|
return order;
|
|
84
101
|
}
|
|
102
|
+
const CHANGE_STATUSES = new Set(["proposed", "agreed", "superseded", "withdrawn"]);
|
|
103
|
+
/**
|
|
104
|
+
* The work-item ids a proposal's `## Work Items` section lists (M-ADR 0223 §4.2): one per bullet, the backticked
|
|
105
|
+
* id if there is one, else the bullet's first word. HTML comments (the template's guidance) are ignored.
|
|
106
|
+
*/
|
|
107
|
+
export function parseWorkItems(proposal) {
|
|
108
|
+
const text = proposal.replace(/<!--[\s\S]*?-->/g, "");
|
|
109
|
+
const m = /^##\s+Work Items\s*$/im.exec(text);
|
|
110
|
+
if (!m)
|
|
111
|
+
return [];
|
|
112
|
+
const rest = text.slice(m.index + m[0].length);
|
|
113
|
+
const end = /^##\s/m.exec(rest);
|
|
114
|
+
const ids = [];
|
|
115
|
+
for (const line of (end ? rest.slice(0, end.index) : rest).split("\n")) {
|
|
116
|
+
const b = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
117
|
+
if (!b)
|
|
118
|
+
continue;
|
|
119
|
+
const id = (/`([^`]+)`/.exec(b[1])?.[1] ?? b[1].split(/\s+/)[0] ?? "").replace(/[:,;]+$/, "").trim();
|
|
120
|
+
if (id && !ids.includes(id))
|
|
121
|
+
ids.push(id);
|
|
122
|
+
}
|
|
123
|
+
return ids;
|
|
124
|
+
}
|
|
85
125
|
const rel = (sddPath, p) => p.slice(sddPath.length + 1).split("\\").join("/");
|
|
86
126
|
export async function buildRequirements(inputs) {
|
|
87
127
|
const { sddPath, findings } = inputs;
|
|
88
|
-
const empty = { requirements: [], lineage: [], status: [] };
|
|
128
|
+
const empty = { requirements: [], lineage: [], status: [], changes: [], workOrders: [], workItems: [] };
|
|
89
129
|
const store = join(sddPath, STORE);
|
|
90
130
|
if (!(await isDir(store)))
|
|
91
131
|
return empty;
|
|
@@ -115,6 +155,28 @@ export async function buildRequirements(inputs) {
|
|
|
115
155
|
const events = [];
|
|
116
156
|
const lineage = [];
|
|
117
157
|
const unknownRepos = new Set();
|
|
158
|
+
const changes = [];
|
|
159
|
+
const workOrders = [];
|
|
160
|
+
const workItems = [];
|
|
161
|
+
// The change itself, its work orders and its work items (M-ADR 0223 §4.2).
|
|
162
|
+
const recordChange = async (id, name, kind, status, archivedOn, changeDir, record) => {
|
|
163
|
+
const verdict = record?.verdict?.result === "PASS" || record?.verdict?.result === "FAIL" ? record.verdict.result : "";
|
|
164
|
+
changes.push({ id, name, change_kind: kind, status, archived_on: archivedOn, verdict, source_sdd_id: "", platform_sdd_id: "" });
|
|
165
|
+
for (const wo of record?.work_orders ?? []) {
|
|
166
|
+
if (!wo.repo)
|
|
167
|
+
continue;
|
|
168
|
+
if (!inputs.knownRepoIds.has(wo.repo)) {
|
|
169
|
+
unknownRepos.add(wo.repo);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const woStatus = wo.status === "agreed" || wo.status === "completed" ? wo.status : verdict === "PASS" && status === "archived" ? "completed" : "agreed";
|
|
173
|
+
workOrders.push({ id: `${id}__${wo.repo}`, change_id: id, repo_id: wo.repo, status: woStatus, source_sdd_id: "", platform_sdd_id: "" });
|
|
174
|
+
}
|
|
175
|
+
const proposal = await readText(join(changeDir, "proposal.md"));
|
|
176
|
+
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: "" });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
118
180
|
const pushEvent = (e) => {
|
|
119
181
|
if (!inputs.knownRepoIds.has(e.repo)) {
|
|
120
182
|
unknownRepos.add(e.repo);
|
|
@@ -123,21 +185,25 @@ export async function buildRequirements(inputs) {
|
|
|
123
185
|
if (requirements.has(e.requirementId))
|
|
124
186
|
events.push(e);
|
|
125
187
|
};
|
|
126
|
-
// 2. Archived changes, in archive order.
|
|
127
|
-
|
|
188
|
+
// 2. Archived changes, in archive order. A requirement is named as it was when each change archived, so its
|
|
189
|
+
// rows are carried forward through every later RENAMED (and one in the same change) to the name it has now;
|
|
190
|
+
// otherwise a rename would read as a requirement born at the rename, with no baseline.
|
|
191
|
+
const archived = [];
|
|
192
|
+
const renames = [];
|
|
193
|
+
const archives = await archiveOrder(sddPath, await dirs(join(store, "changes", "archive")), findings);
|
|
194
|
+
for (const [index, dir] of archives.entries()) {
|
|
128
195
|
const changeDir = join(store, "changes", "archive", dir);
|
|
196
|
+
renames.push(await renamesOf(changeDir));
|
|
129
197
|
const change = dir.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
130
198
|
const archivedOn = /^(\d{4}-\d{2}-\d{2})-/.exec(dir)?.[1] ?? "";
|
|
131
199
|
const evidence = await readText(join(changeDir, "baseline-evidence.md"));
|
|
132
200
|
const kind = evidence !== null ? "baseline" : "change";
|
|
201
|
+
await recordChange(dir, change, kind, "archived", archivedOn, changeDir, await agreementRecord(sddPath, change));
|
|
133
202
|
const delta = await deltaOf(changeDir);
|
|
134
203
|
for (const d of delta) {
|
|
135
204
|
if (d.operation === "REMOVED")
|
|
136
205
|
continue;
|
|
137
|
-
|
|
138
|
-
if (!requirements.has(requirementId))
|
|
139
|
-
continue;
|
|
140
|
-
lineage.push({ id: `${requirementId}__${dir}`, requirement_id: requirementId, change_id: dir, change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" });
|
|
206
|
+
archived.push({ index, feature: d.feature, name: d.name, lineage: { change_id: dir, change_kind: kind, operation: d.operation, archived_on: archivedOn, source_sdd_id: "", platform_sdd_id: "" } });
|
|
141
207
|
}
|
|
142
208
|
if (evidence !== null) {
|
|
143
209
|
const repos = observedIn(evidence);
|
|
@@ -147,7 +213,7 @@ export async function buildRequirements(inputs) {
|
|
|
147
213
|
for (const repo of repos)
|
|
148
214
|
for (const d of delta)
|
|
149
215
|
if (d.operation !== "REMOVED")
|
|
150
|
-
|
|
216
|
+
archived.push({ index, feature: d.feature, name: d.name, event: { repo, status: "observed", basis: dir } });
|
|
151
217
|
continue;
|
|
152
218
|
}
|
|
153
219
|
const record = await agreementRecord(sddPath, change);
|
|
@@ -162,13 +228,43 @@ export async function buildRequirements(inputs) {
|
|
|
162
228
|
findings.add("convention", `${AGREEMENTS}/${change}.yaml: work order for ${wo.repo} implements "${name}", which the change's delta does not name`);
|
|
163
229
|
}
|
|
164
230
|
for (const d of match)
|
|
165
|
-
|
|
231
|
+
archived.push({ index, feature: d.feature, name: d.name, event: { repo: wo.repo, status: "declared-shipped", basis: dir } });
|
|
166
232
|
}
|
|
167
233
|
}
|
|
168
234
|
}
|
|
235
|
+
const current = (feature, name, from) => {
|
|
236
|
+
let n = name;
|
|
237
|
+
for (let j = from; j < renames.length; j++)
|
|
238
|
+
for (const r of renames[j])
|
|
239
|
+
if (r.feature === feature && r.from === n)
|
|
240
|
+
n = r.to;
|
|
241
|
+
return `${feature}::${n}`;
|
|
242
|
+
};
|
|
243
|
+
// One lineage row per (requirement, change): a change that renames and modifies a requirement records MODIFIED,
|
|
244
|
+
// the behaviour change, and its delta still shows the rename.
|
|
245
|
+
const lineageById = new Map();
|
|
246
|
+
for (const a of archived) {
|
|
247
|
+
const requirementId = current(a.feature, a.name, a.index);
|
|
248
|
+
if (a.event)
|
|
249
|
+
pushEvent({ requirementId, ...a.event });
|
|
250
|
+
else if (a.lineage && requirements.has(requirementId)) {
|
|
251
|
+
const id = `${requirementId}__${a.lineage["change_id"]}`;
|
|
252
|
+
if (lineageById.get(id)?.["operation"] === "MODIFIED")
|
|
253
|
+
continue;
|
|
254
|
+
lineageById.set(id, { id, requirement_id: requirementId, ...a.lineage });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
lineage.push(...lineageById.values());
|
|
169
258
|
// 3. Open changes whose agreement record is agreed.
|
|
170
259
|
for (const dir of (await dirs(join(store, "changes"))).filter((d) => d !== "archive")) {
|
|
171
260
|
const record = await agreementRecord(sddPath, dir);
|
|
261
|
+
const openKind = (await readText(join(store, "changes", dir, "baseline-evidence.md"))) !== null ? "baseline" : "change";
|
|
262
|
+
let openStatus = record?.status ?? "proposed";
|
|
263
|
+
if (!CHANGE_STATUSES.has(openStatus)) {
|
|
264
|
+
findings.add("convention", `${AGREEMENTS}/${dir}.yaml: status "${openStatus}" is not a change status; catalogued as proposed`);
|
|
265
|
+
openStatus = "proposed";
|
|
266
|
+
}
|
|
267
|
+
await recordChange(dir, dir, openKind, openStatus, "", join(store, "changes", dir), record);
|
|
172
268
|
if (record?.status !== "agreed")
|
|
173
269
|
continue;
|
|
174
270
|
const delta = await deltaOf(join(store, "changes", dir));
|
|
@@ -203,6 +299,9 @@ export async function buildRequirements(inputs) {
|
|
|
203
299
|
requirements: [...requirements.values()].sort(byId),
|
|
204
300
|
lineage,
|
|
205
301
|
status: [...status.values()].sort(byId),
|
|
302
|
+
changes: changes.sort(byId),
|
|
303
|
+
workOrders: workOrders.sort(byId),
|
|
304
|
+
workItems: workItems.sort(byId),
|
|
206
305
|
};
|
|
207
306
|
}
|
|
208
307
|
//# sourceMappingURL=requirements.js.map
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# Emitted by sdd-cli's YAML emitter with no line folding; the three header scalars are quoted for the
|
|
8
8
|
# textual readers downstream. The one deliberate difference from the authored form is schema_status:
|
|
9
9
|
# the authored form says canonical; a projection never does.
|
|
10
|
-
schema_version: "4.
|
|
10
|
+
schema_version: "4.2.0"
|
|
11
11
|
schema_owner: "sdd-method"
|
|
12
12
|
schema_status: "generated"
|
|
13
13
|
enums:
|
|
@@ -643,6 +643,31 @@ enums:
|
|
|
643
643
|
- value: observed
|
|
644
644
|
description: Independent evidence from this repository's code matches (a baseline's characterization tests or a reverse snapshot).
|
|
645
645
|
description: A requirement's status in one implementing repository (M-ADR 0221 §4.5).
|
|
646
|
+
change_status:
|
|
647
|
+
values:
|
|
648
|
+
- value: proposed
|
|
649
|
+
description: Open, with a proposed agreement record or none yet.
|
|
650
|
+
- value: agreed
|
|
651
|
+
description: "Open, with an agreed record: being built."
|
|
652
|
+
- value: superseded
|
|
653
|
+
description: Open, its record superseded by another change.
|
|
654
|
+
- value: withdrawn
|
|
655
|
+
description: Open, its record withdrawn.
|
|
656
|
+
- value: archived
|
|
657
|
+
description: Archived into the living specs.
|
|
658
|
+
description: Where a change stands (M-ADR 0222 §4.6, M-ADR 0223 §4.6).
|
|
659
|
+
judge_verdict:
|
|
660
|
+
values:
|
|
661
|
+
- value: PASS
|
|
662
|
+
- value: FAIL
|
|
663
|
+
description: The judge's verdict on a change (M-ADR 0223 §4.5).
|
|
664
|
+
work_order_status:
|
|
665
|
+
values:
|
|
666
|
+
- value: agreed
|
|
667
|
+
description: Agreed; the repository's work is not yet judged.
|
|
668
|
+
- value: completed
|
|
669
|
+
description: Marked completed by a passing judge.
|
|
670
|
+
description: A work order's status in its change's agreement record (M-ADR 0223 §4.2, §4.6).
|
|
646
671
|
catalogues:
|
|
647
672
|
domains:
|
|
648
673
|
primary_key: id
|
|
@@ -4200,7 +4225,7 @@ catalogues:
|
|
|
4200
4225
|
- name: change_id
|
|
4201
4226
|
type: string
|
|
4202
4227
|
nullable: false
|
|
4203
|
-
description: The archived change's directory name (its archive date prefix included).
|
|
4228
|
+
description: The archived change's directory name (its archive date prefix included); a changes.id.
|
|
4204
4229
|
- name: change_kind
|
|
4205
4230
|
type: string
|
|
4206
4231
|
nullable: false
|
|
@@ -4226,6 +4251,9 @@ catalogues:
|
|
|
4226
4251
|
- column: requirement_id
|
|
4227
4252
|
references: requirements.id
|
|
4228
4253
|
nullable: false
|
|
4254
|
+
- column: change_id
|
|
4255
|
+
references: changes.id
|
|
4256
|
+
nullable: false
|
|
4229
4257
|
column_enums:
|
|
4230
4258
|
change_kind: enums.requirement_change_kind
|
|
4231
4259
|
operation: enums.requirement_operation
|
|
@@ -4269,3 +4297,111 @@ catalogues:
|
|
|
4269
4297
|
nullable: false
|
|
4270
4298
|
column_enums:
|
|
4271
4299
|
status: enums.requirement_status_value
|
|
4300
|
+
changes:
|
|
4301
|
+
primary_key: id
|
|
4302
|
+
columns:
|
|
4303
|
+
- name: id
|
|
4304
|
+
type: string
|
|
4305
|
+
nullable: false
|
|
4306
|
+
description: "The change's directory name: the open change's name, or the archived change's directory (its archive date prefix included). requirement_lineage.change_id references it."
|
|
4307
|
+
- name: name
|
|
4308
|
+
type: string
|
|
4309
|
+
nullable: false
|
|
4310
|
+
description: "The change's name without the archive date prefix: the same before and after archive, so a work item that cites the change keeps its link."
|
|
4311
|
+
- name: change_kind
|
|
4312
|
+
type: string
|
|
4313
|
+
nullable: false
|
|
4314
|
+
description: "Bound to requirement_change_kind: baseline (it carries baseline-evidence.md) or change."
|
|
4315
|
+
- name: status
|
|
4316
|
+
type: string
|
|
4317
|
+
nullable: false
|
|
4318
|
+
description: "Bound to change_status: archived for an archived change; otherwise the agreement record's status, or proposed when there is no record yet."
|
|
4319
|
+
- name: archived_on
|
|
4320
|
+
type: string
|
|
4321
|
+
nullable: true
|
|
4322
|
+
description: The archive date (YYYY-MM-DD) from the archived change's directory name; empty for an open change.
|
|
4323
|
+
- name: verdict
|
|
4324
|
+
type: string
|
|
4325
|
+
nullable: true
|
|
4326
|
+
description: "Bound to judge_verdict: the agreement record's verdict result (M-ADR 0223 §4.6). Empty for a baseline, and for a change not yet judged."
|
|
4327
|
+
- name: source_sdd_id
|
|
4328
|
+
type: string
|
|
4329
|
+
nullable: true
|
|
4330
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4331
|
+
- name: platform_sdd_id
|
|
4332
|
+
type: string
|
|
4333
|
+
nullable: true
|
|
4334
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4335
|
+
description: "A change in the SDD's behaviour store (M-ADR 0223 §4.2), open or archived, including baseline changes. Derived, never authored: from the store's change directories and their agreement records (M-ADR 0222 §4.6)."
|
|
4336
|
+
column_enums:
|
|
4337
|
+
change_kind: enums.requirement_change_kind
|
|
4338
|
+
status: enums.change_status
|
|
4339
|
+
verdict: enums.judge_verdict
|
|
4340
|
+
change_work_order:
|
|
4341
|
+
primary_key: id
|
|
4342
|
+
columns:
|
|
4343
|
+
- name: id
|
|
4344
|
+
type: string
|
|
4345
|
+
nullable: false
|
|
4346
|
+
description: <change_id>__<repo_id>.
|
|
4347
|
+
- name: change_id
|
|
4348
|
+
type: string
|
|
4349
|
+
nullable: false
|
|
4350
|
+
- name: repo_id
|
|
4351
|
+
type: string
|
|
4352
|
+
nullable: false
|
|
4353
|
+
description: The repository the work order is for.
|
|
4354
|
+
- name: status
|
|
4355
|
+
type: string
|
|
4356
|
+
nullable: false
|
|
4357
|
+
description: "Bound to work_order_status: the record's status for the work order; a judge PASS marks it completed (M-ADR 0223 §4.6)."
|
|
4358
|
+
- name: source_sdd_id
|
|
4359
|
+
type: string
|
|
4360
|
+
nullable: true
|
|
4361
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4362
|
+
- name: platform_sdd_id
|
|
4363
|
+
type: string
|
|
4364
|
+
nullable: true
|
|
4365
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4366
|
+
description: "Edge: a change's work order for one implementing repository (M-ADR 0223 §4.2), from the change's agreement record. Derived, never authored."
|
|
4367
|
+
foreign_keys:
|
|
4368
|
+
- column: change_id
|
|
4369
|
+
references: changes.id
|
|
4370
|
+
nullable: false
|
|
4371
|
+
- column: repo_id
|
|
4372
|
+
references: repositories.id
|
|
4373
|
+
nullable: false
|
|
4374
|
+
column_enums:
|
|
4375
|
+
status: enums.work_order_status
|
|
4376
|
+
change_work_item:
|
|
4377
|
+
primary_key: id
|
|
4378
|
+
columns:
|
|
4379
|
+
- name: id
|
|
4380
|
+
type: string
|
|
4381
|
+
nullable: false
|
|
4382
|
+
description: "<change_name>__<work_item_id>: keyed by the change's undated name, so the row is the same before and after archive."
|
|
4383
|
+
- name: change_id
|
|
4384
|
+
type: string
|
|
4385
|
+
nullable: false
|
|
4386
|
+
description: "The change as it stands now: its open name, or its archive directory."
|
|
4387
|
+
- name: change_name
|
|
4388
|
+
type: string
|
|
4389
|
+
nullable: false
|
|
4390
|
+
description: The change's undated name.
|
|
4391
|
+
- name: work_item_id
|
|
4392
|
+
type: string
|
|
4393
|
+
nullable: false
|
|
4394
|
+
description: The work item's id in the adopter's work-item tool, as the proposal's Work Items section gives it.
|
|
4395
|
+
- name: source_sdd_id
|
|
4396
|
+
type: string
|
|
4397
|
+
nullable: true
|
|
4398
|
+
description: Originating SDD (stamped at aggregation; empty in a per-SDD build).
|
|
4399
|
+
- name: platform_sdd_id
|
|
4400
|
+
type: string
|
|
4401
|
+
nullable: true
|
|
4402
|
+
description: The platform SDD the row belongs to (stamped at aggregation).
|
|
4403
|
+
description: "Annotation: a work item a change's proposal says it delivers (M-ADR 0223 §4.2), giving the roll-up from work item to changes to requirements. The work item's id is the adopter's work-item tool's (M-ADR 0035), so it has no foreign key."
|
|
4404
|
+
foreign_keys:
|
|
4405
|
+
- column: change_id
|
|
4406
|
+
references: changes.id
|
|
4407
|
+
nullable: false
|
|
@@ -905,3 +905,31 @@ coverage:
|
|
|
905
905
|
kind: enum
|
|
906
906
|
scoring: not-applicable
|
|
907
907
|
reason: closed vocabulary, not an artefact
|
|
908
|
+
changes:
|
|
909
|
+
kind: entity
|
|
910
|
+
scoring: not-applicable
|
|
911
|
+
reason: projected from the behaviour store, whose change gate and judge hold its quality (M-ADR 0222, M-ADR 0223)
|
|
912
|
+
change_work_order:
|
|
913
|
+
kind: edge
|
|
914
|
+
scoring: derived-edge
|
|
915
|
+
class: realisation
|
|
916
|
+
impact_flow: both
|
|
917
|
+
reason: derived from agreement records and verdicts
|
|
918
|
+
change_work_item:
|
|
919
|
+
kind: annotation
|
|
920
|
+
scoring: derived-edge
|
|
921
|
+
class: traceability
|
|
922
|
+
impact_flow: none
|
|
923
|
+
reason: derived from each change's proposal
|
|
924
|
+
change_status:
|
|
925
|
+
kind: enum
|
|
926
|
+
scoring: not-applicable
|
|
927
|
+
reason: closed vocabulary, not an artefact
|
|
928
|
+
judge_verdict:
|
|
929
|
+
kind: enum
|
|
930
|
+
scoring: not-applicable
|
|
931
|
+
reason: closed vocabulary, not an artefact
|
|
932
|
+
work_order_status:
|
|
933
|
+
kind: enum
|
|
934
|
+
scoring: not-applicable
|
|
935
|
+
reason: closed vocabulary, not an artefact
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The intent artefact's shape (M-ADR 0227 §4.1), shared by the migration that writes intents and the check
|
|
2
|
+
// that reads them. The section names and header lines are the shipped template's
|
|
3
|
+
// (docs/method/methods/templates/intent-template.md), so a change there is a change here.
|
|
4
|
+
export const INTENT_DIRS = ["content/method/intents", "content/product/intents"];
|
|
5
|
+
export const INTENT_SECTIONS = [
|
|
6
|
+
"Problem",
|
|
7
|
+
"Proposed outcome",
|
|
8
|
+
"Affected users and systems",
|
|
9
|
+
"Constraints",
|
|
10
|
+
"Open questions",
|
|
11
|
+
"Decision to resolve in exploration",
|
|
12
|
+
"ADR requirement",
|
|
13
|
+
"Where it went",
|
|
14
|
+
];
|
|
15
|
+
/** Header fields of the retired Idea and the idea map: planning state, which belongs in the work item (§4.3). */
|
|
16
|
+
export const PLANNING_FIELDS = [
|
|
17
|
+
"Status",
|
|
18
|
+
"Priority",
|
|
19
|
+
"Blocked by",
|
|
20
|
+
"Enables",
|
|
21
|
+
"Parent Idea",
|
|
22
|
+
"Child Ideas",
|
|
23
|
+
"Idea ID",
|
|
24
|
+
"Epic ID",
|
|
25
|
+
"Theme",
|
|
26
|
+
];
|
|
27
|
+
/** A "Where it went" line naming a path: "- `<path>` (<sdd-id>; <source>)". */
|
|
28
|
+
export const WHERE_PATH_LINE = /^-\s+`([^`]+)`\s+\(([^;)]+);\s*([^)]+)\)\s*$/;
|
|
29
|
+
/** "## 3. Why Explore This (Revised)" -> "why explore this". */
|
|
30
|
+
export function normaliseHeading(h) {
|
|
31
|
+
return h
|
|
32
|
+
.replace(/^\s*\d+[A-Za-z]?(\.\d+)*\.?\s*/, "")
|
|
33
|
+
.replace(/\((revised|updated)\)\s*$/i, "")
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
export function parseMarkdown(text) {
|
|
38
|
+
const lines = text.split("\n");
|
|
39
|
+
let title = "";
|
|
40
|
+
const fields = new Map();
|
|
41
|
+
const sections = new Map();
|
|
42
|
+
const headings = [];
|
|
43
|
+
let current = null;
|
|
44
|
+
let buf = [];
|
|
45
|
+
const flush = () => {
|
|
46
|
+
if (current !== null) {
|
|
47
|
+
const prev = sections.get(current);
|
|
48
|
+
const body = buf.join("\n").trim();
|
|
49
|
+
sections.set(current, prev ? `${prev}\n\n${body}` : body);
|
|
50
|
+
}
|
|
51
|
+
buf = [];
|
|
52
|
+
};
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const h1 = /^#\s+(.+?)\s*$/.exec(line);
|
|
55
|
+
if (h1 && !title && current === null) {
|
|
56
|
+
title = h1[1];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const h2 = /^##\s+(.+?)\s*$/.exec(line);
|
|
60
|
+
if (h2) {
|
|
61
|
+
flush();
|
|
62
|
+
headings.push(h2[1]);
|
|
63
|
+
current = normaliseHeading(h2[1]);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (current === null) {
|
|
67
|
+
const f = /^\*\*([^*:]+):\*\*\s*(.*?)\s*$/.exec(line);
|
|
68
|
+
if (f)
|
|
69
|
+
fields.set(f[1].trim(), f[2]);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
buf.push(line);
|
|
73
|
+
}
|
|
74
|
+
flush();
|
|
75
|
+
return { title, fields, sections, headings };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=format.js.map
|