@sdd-method/maintain-cli 0.10.0 → 0.11.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.
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { checkArchetypeCitations } from "./archetype-citations.js";
|
|
2
2
|
import { checkBehaviourStore, checkBehaviourStoreOpenspec } from "./behaviour-store.js";
|
|
3
3
|
import { checkBundleCoherence } from "./bundle-coherence.js";
|
|
4
|
+
import { checkIntents } from "./intents.js";
|
|
4
5
|
import { checkYamlParse } from "./yaml-parse.js";
|
|
5
6
|
// The behaviour store (M-ADR 0221-0223) can sit in an SDD of any profile, so every profile carries
|
|
6
7
|
// its two checks; without a store both report [N/A], an empty population, not a pass.
|
|
@@ -21,6 +22,12 @@ const BEHAVIOUR_STORE_VALIDATORS = [
|
|
|
21
22
|
source: { kind: "native", run: async (ctx) => checkArchetypeCitations(ctx.targetRepo) },
|
|
22
23
|
build: () => ({ status: "run", args: [] }),
|
|
23
24
|
},
|
|
25
|
+
// Intents (M-ADR 0227) can sit in an SDD of any profile; without any the check reports [N/A].
|
|
26
|
+
{
|
|
27
|
+
name: "intents",
|
|
28
|
+
source: { kind: "native", run: async (ctx) => checkIntents(ctx.targetRepo) },
|
|
29
|
+
build: () => ({ status: "run", args: [] }),
|
|
30
|
+
},
|
|
24
31
|
];
|
|
25
32
|
// Validators that accept --app-root / --repo-root flags work fine from the
|
|
26
33
|
// bundled location because they override their internal ROOT_DIR via the
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// intents: every intent file (M-ADR 0227 §4.1) has the template's shape, carries no planning state, names its
|
|
2
|
+
// work item, and every path its "Where it went" names in this SDD exists.
|
|
3
|
+
//
|
|
4
|
+
// Read from content/method/intents/ and content/product/intents/. A path line in "Where it went" is
|
|
5
|
+
// "- `<path>` (<sdd-id>; <source>)"; only paths labelled with this SDD's id are checked here, since another
|
|
6
|
+
// SDD's paths resolve in that SDD. Commit and plan lines are records, not paths, and are not checked. An
|
|
7
|
+
// intent with nothing yet in "Where it went" is captured but not explored: counted, not failed. With no
|
|
8
|
+
// intents the check reports [N/A], an empty population, not a pass.
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
import { parseDocument } from "yaml";
|
|
13
|
+
import { INTENT_DIRS, INTENT_SECTIONS, PLANNING_FIELDS, WHERE_PATH_LINE, parseMarkdown } from "../intents/format.js";
|
|
14
|
+
async function sddId(repo) {
|
|
15
|
+
try {
|
|
16
|
+
const m = parseDocument(await readFile(join(repo, ".sdd/sdd-manifest.yaml"), "utf8")).toJS();
|
|
17
|
+
if (typeof m?.sdd_id === "string" && m.sdd_id)
|
|
18
|
+
return m.sdd_id;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// fall through
|
|
22
|
+
}
|
|
23
|
+
return basename(repo);
|
|
24
|
+
}
|
|
25
|
+
export async function checkIntents(repo) {
|
|
26
|
+
const files = [];
|
|
27
|
+
for (const d of INTENT_DIRS) {
|
|
28
|
+
try {
|
|
29
|
+
for (const e of await readdir(join(repo, d), { withFileTypes: true })) {
|
|
30
|
+
if (e.isFile() && e.name.endsWith(".md") && e.name.toLowerCase() !== "readme.md")
|
|
31
|
+
files.push(`${d}/${e.name}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// no such directory
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (files.length === 0)
|
|
39
|
+
return { exitCode: 0, stdout: "[N/A] no intents (content/{method,product}/intents/*.md) in this repository\n" };
|
|
40
|
+
const self = await sddId(repo);
|
|
41
|
+
const problems = [];
|
|
42
|
+
let unexplored = 0;
|
|
43
|
+
let pathsChecked = 0;
|
|
44
|
+
for (const rel of files.sort()) {
|
|
45
|
+
const text = await readFile(join(repo, rel), "utf8");
|
|
46
|
+
const doc = parseMarkdown(text);
|
|
47
|
+
if (!/^Intent:\s*\S/.test(doc.title))
|
|
48
|
+
problems.push(`${rel}: no "# Intent: <title>" line`);
|
|
49
|
+
for (const s of INTENT_SECTIONS)
|
|
50
|
+
if (!doc.sections.has(s.toLowerCase()))
|
|
51
|
+
problems.push(`${rel}: no "## ${s}" section`);
|
|
52
|
+
const wi = doc.fields.get("Work item")?.trim() ?? "";
|
|
53
|
+
if (!wi || /^<.*>$/.test(wi))
|
|
54
|
+
problems.push(`${rel}: no work item named (**Work item:**)`);
|
|
55
|
+
for (const f of PLANNING_FIELDS)
|
|
56
|
+
if (doc.fields.has(f))
|
|
57
|
+
problems.push(`${rel}: planning field **${f}:** belongs in the work item (M-ADR 0227 §4.3)`);
|
|
58
|
+
const where = (doc.sections.get("where it went") ?? "").split("\n").map((l) => l.trim()).filter((l) => l.startsWith("- "));
|
|
59
|
+
if (where.length === 0)
|
|
60
|
+
unexplored++;
|
|
61
|
+
for (const line of where) {
|
|
62
|
+
const m = WHERE_PATH_LINE.exec(line);
|
|
63
|
+
if (!m || m[2].trim() !== self)
|
|
64
|
+
continue;
|
|
65
|
+
pathsChecked++;
|
|
66
|
+
const p = m[1].replace(/[#?].*$/, "");
|
|
67
|
+
if (!existsSync(join(repo, p)))
|
|
68
|
+
problems.push(`${rel}: "Where it went" names ${p}, which does not exist`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const head = `intents: ${files.length} intent(s); ${pathsChecked} "Where it went" path(s) in this SDD checked; ${unexplored} not yet explored\n`;
|
|
72
|
+
if (problems.length === 0)
|
|
73
|
+
return { exitCode: 0, stdout: `${head}[PASS] intents: ok\n` };
|
|
74
|
+
return { exitCode: 1, stdout: `${head}${problems.map((p) => `[FAIL] ${p}\n`).join("")}[FAIL] intents: ${problems.length} problem(s)\n` };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=intents.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": "2.
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"sddCliVersion": "2.5.0",
|
|
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",
|