cyber-sdd 0.7.0 → 0.7.2
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/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
//#region skills/concept-index/scripts/concept-index.mts
|
|
6
|
-
const BEGIN_MARKER = "<!-- BEGIN generated: by-concept (project-spec/concept-index) -->";
|
|
7
|
-
const END_MARKER = "<!-- END generated: by-concept -->";
|
|
8
|
-
const ANCHOR_HEADING = "## Invariants";
|
|
9
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
10
|
-
"node_modules",
|
|
11
|
-
".git",
|
|
12
|
-
"dist",
|
|
13
|
-
".turbo",
|
|
14
|
-
".next",
|
|
15
|
-
"coverage"
|
|
16
|
-
]);
|
|
17
|
-
function parseFrontmatter(text) {
|
|
18
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
19
|
-
if (!m) return null;
|
|
20
|
-
const fm = {
|
|
21
|
-
concepts: [],
|
|
22
|
-
model: false
|
|
23
|
-
};
|
|
24
|
-
const lines = m[1].split("\n").map((l) => l.replace(/\r$/, ""));
|
|
25
|
-
for (let i = 0; i < lines.length; i++) {
|
|
26
|
-
const line = lines[i];
|
|
27
|
-
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
28
|
-
if (line.length - line.trimStart().length !== 0) continue;
|
|
29
|
-
const [key, ...rest] = line.trim().split(":");
|
|
30
|
-
const value = rest.join(":").trim();
|
|
31
|
-
if (key === "spec-type") fm.specType = unquote(value);
|
|
32
|
-
else if (key === "model") fm.model = unquote(value) === "true";
|
|
33
|
-
else if (key === "concept") {
|
|
34
|
-
if (value === "" || value === "|" || value === ">") for (let j = i + 1; j < lines.length; j++) {
|
|
35
|
-
const item = lines[j];
|
|
36
|
-
if (item.trim() === "") continue;
|
|
37
|
-
if (item.length - item.trimStart().length === 0) break;
|
|
38
|
-
const dash = /^\s*-\s+(.*)$/.exec(item);
|
|
39
|
-
if (dash) fm.concepts.push(unquote(dash[1].trim()));
|
|
40
|
-
}
|
|
41
|
-
else fm.concepts.push(...parseScalarOrFlow(value));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return fm;
|
|
45
|
-
}
|
|
46
|
-
function parseScalarOrFlow(value) {
|
|
47
|
-
const v = value.trim();
|
|
48
|
-
if (v.startsWith("[") && v.endsWith("]")) return v.slice(1, -1).split(",").map((s) => unquote(s.trim())).filter((s) => s.length > 0);
|
|
49
|
-
const single = unquote(v);
|
|
50
|
-
return single.length > 0 ? [single] : [];
|
|
51
|
-
}
|
|
52
|
-
function unquote(v) {
|
|
53
|
-
return v.replace(/^["']|["']$/g, "");
|
|
54
|
-
}
|
|
55
|
-
function facetKind(relPath, specType) {
|
|
56
|
-
const p = relPath.replace(/\\/g, "/");
|
|
57
|
-
if (p.startsWith("design/")) return "rule";
|
|
58
|
-
if (p.startsWith("workflows/")) return "workflow";
|
|
59
|
-
if (specType === "reference") return "reference";
|
|
60
|
-
if (specType === "behavioral") return "behavior";
|
|
61
|
-
return "index";
|
|
62
|
-
}
|
|
63
|
-
function displayPath(relPath) {
|
|
64
|
-
const p = relPath.replace(/\\/g, "/");
|
|
65
|
-
return p.endsWith("/README.md") ? p.slice(0, -9) : p;
|
|
66
|
-
}
|
|
67
|
-
function scanProjectSpec(specDir) {
|
|
68
|
-
const records = [];
|
|
69
|
-
walk(specDir, specDir, records);
|
|
70
|
-
return records;
|
|
71
|
-
}
|
|
72
|
-
function walk(dir, specDir, out) {
|
|
73
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
74
|
-
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
75
|
-
const full = join(dir, entry.name);
|
|
76
|
-
if (entry.isDirectory()) walk(full, specDir, out);
|
|
77
|
-
else if (entry.name.endsWith(".md")) {
|
|
78
|
-
const fm = parseFrontmatter(readFileSync(full, "utf8"));
|
|
79
|
-
if (!fm || fm.concepts.length === 0) continue;
|
|
80
|
-
const relPath = relative(specDir, full).replace(/\\/g, "/");
|
|
81
|
-
out.push({
|
|
82
|
-
relPath,
|
|
83
|
-
display: displayPath(relPath),
|
|
84
|
-
concepts: fm.concepts,
|
|
85
|
-
facet: facetKind(relPath, fm.specType)
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function groupByConcept(records) {
|
|
91
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
92
|
-
for (const rec of records) for (const concept of rec.concepts) {
|
|
93
|
-
const list = grouped.get(concept) ?? [];
|
|
94
|
-
list.push(rec);
|
|
95
|
-
grouped.set(concept, list);
|
|
96
|
-
}
|
|
97
|
-
return new Map([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, [...v].sort((x, y) => x.display.localeCompare(y.display))]));
|
|
98
|
-
}
|
|
99
|
-
function renderTable(grouped) {
|
|
100
|
-
return [
|
|
101
|
-
"| Concept | Facets |",
|
|
102
|
-
"|---|---|",
|
|
103
|
-
...[...grouped.entries()].map(([concept, nodes]) => {
|
|
104
|
-
return `| \`${concept}\` | ${nodes.map((n) => `\`${n.display}\` (${n.facet})`).join(" · ")} |`;
|
|
105
|
-
})
|
|
106
|
-
].join("\n");
|
|
107
|
-
}
|
|
108
|
-
function renderSection(grouped) {
|
|
109
|
-
return [
|
|
110
|
-
BEGIN_MARKER,
|
|
111
|
-
"",
|
|
112
|
-
"## By concept",
|
|
113
|
-
"",
|
|
114
|
-
"> Generated from `concept:` frontmatter by `project-spec/concept-index` — do not edit by hand.",
|
|
115
|
-
"",
|
|
116
|
-
renderTable(grouped),
|
|
117
|
-
"",
|
|
118
|
-
END_MARKER
|
|
119
|
-
].join("\n");
|
|
120
|
-
}
|
|
121
|
-
function applySection(specText, section) {
|
|
122
|
-
const begin = specText.indexOf(BEGIN_MARKER);
|
|
123
|
-
const end = specText.indexOf(END_MARKER);
|
|
124
|
-
if (begin !== -1 && end !== -1 && end > begin) {
|
|
125
|
-
const before = specText.slice(0, begin);
|
|
126
|
-
const after = specText.slice(end + 34);
|
|
127
|
-
return before + section + after;
|
|
128
|
-
}
|
|
129
|
-
const anchor = specText.indexOf(ANCHOR_HEADING);
|
|
130
|
-
if (anchor !== -1) return specText.slice(0, anchor) + section + "\n\n" + specText.slice(anchor);
|
|
131
|
-
return `${specText.replace(/\s*$/, "")}\n\n${section}\n`;
|
|
132
|
-
}
|
|
133
|
-
function extractSection(specText) {
|
|
134
|
-
const begin = specText.indexOf(BEGIN_MARKER);
|
|
135
|
-
const end = specText.indexOf(END_MARKER);
|
|
136
|
-
if (begin === -1 || end === -1 || end < begin) return null;
|
|
137
|
-
return specText.slice(begin, end + 34);
|
|
138
|
-
}
|
|
139
|
-
function parseArgs(argv) {
|
|
140
|
-
let specDir = ".";
|
|
141
|
-
let mode = "print";
|
|
142
|
-
for (let i = 0; i < argv.length; i++) {
|
|
143
|
-
const a = argv[i];
|
|
144
|
-
if (a === "--spec-dir") specDir = argv[++i] ?? ".";
|
|
145
|
-
else if (a === "--write") mode = "write";
|
|
146
|
-
else if (a === "--check") mode = "check";
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
specDir,
|
|
150
|
-
mode
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
function main(argv) {
|
|
154
|
-
const { specDir, mode } = parseArgs(argv);
|
|
155
|
-
const section = renderSection(groupByConcept(scanProjectSpec(specDir)));
|
|
156
|
-
const specPath = join(specDir, "spec.md");
|
|
157
|
-
if (mode === "print") {
|
|
158
|
-
process.stdout.write(`${section}\n`);
|
|
159
|
-
return 0;
|
|
160
|
-
}
|
|
161
|
-
if (!existsSync(specPath)) {
|
|
162
|
-
process.stderr.write(`concept-index: no spec.md at ${specPath}\n`);
|
|
163
|
-
return 2;
|
|
164
|
-
}
|
|
165
|
-
const specText = readFileSync(specPath, "utf8");
|
|
166
|
-
if (mode === "check") {
|
|
167
|
-
if (extractSection(specText) === section) {
|
|
168
|
-
process.stdout.write("concept-index: no drift\n");
|
|
169
|
-
return 0;
|
|
170
|
-
}
|
|
171
|
-
process.stderr.write("concept-index: drift — spec.md by-concept block is stale; run --write\n");
|
|
172
|
-
return 1;
|
|
173
|
-
}
|
|
174
|
-
const next = applySection(specText, section);
|
|
175
|
-
if (next !== specText) writeFileSync(specPath, next);
|
|
176
|
-
process.stdout.write(`concept-index: ${next === specText ? "unchanged" : "updated"} ${specPath}\n`);
|
|
177
|
-
return 0;
|
|
178
|
-
}
|
|
179
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
180
|
-
//#endregion
|
|
181
|
-
export { BEGIN_MARKER, END_MARKER, applySection, extractSection, facetKind, groupByConcept, main, parseFrontmatter, parseScalarOrFlow, renderSection, renderTable, scanProjectSpec };
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// concept-index — project-spec/concept-index's concrete engine. Scans one project-spec for every node's
|
|
3
|
-
// `concept:` frontmatter and renders the by-concept view (concept → its nodes across every folder),
|
|
4
|
-
// which re-unifies a cross-cutting concern the capability folder tree scatters
|
|
5
|
-
// (the concept axis).
|
|
6
|
-
//
|
|
7
|
-
// Pure derivation from the `concept:` tags: rendering twice is byte-identical, and the generated
|
|
8
|
-
// block in the root spec.md is the only thing a --write touches. Frontmatter only — no node body
|
|
9
|
-
// reaches the output. No dependencies (the repo's node-≥23.6 / no-deps convention). Pure functions
|
|
10
|
-
// are exported for node:test; running the file directly drives the CLI.
|
|
11
|
-
|
|
12
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
13
|
-
import { join, relative } from 'node:path'
|
|
14
|
-
import { pathToFileURL } from 'node:url'
|
|
15
|
-
|
|
16
|
-
export const BEGIN_MARKER = '<!-- BEGIN generated: by-concept (project-spec/concept-index) -->'
|
|
17
|
-
export const END_MARKER = '<!-- END generated: by-concept -->'
|
|
18
|
-
// Where the block is inserted when the markers are absent: just before this heading.
|
|
19
|
-
const ANCHOR_HEADING = '## Invariants'
|
|
20
|
-
|
|
21
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.turbo', '.next', 'coverage'])
|
|
22
|
-
|
|
23
|
-
export type FacetKind = 'rule' | 'workflow' | 'reference' | 'behavior' | 'index'
|
|
24
|
-
|
|
25
|
-
export interface NodeRecord {
|
|
26
|
-
/** Path relative to the spec directory (POSIX). */
|
|
27
|
-
relPath: string
|
|
28
|
-
/** Display form — a README.md node shows its folder, a design doc shows the file. */
|
|
29
|
-
display: string
|
|
30
|
-
concepts: string[]
|
|
31
|
-
facet: FacetKind
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface NodeFrontmatter {
|
|
35
|
-
concepts: string[]
|
|
36
|
-
specType?: string
|
|
37
|
-
model: boolean
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ── Frontmatter parse (a minimal YAML subset — only the node classification schema) ──
|
|
41
|
-
// Reads the leading `---` … `---` block for `concept` (scalar | flow list | block list),
|
|
42
|
-
// `spec-type`, and `model`. Returns null when there is no frontmatter block.
|
|
43
|
-
export function parseFrontmatter(text: string): NodeFrontmatter | null {
|
|
44
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text)
|
|
45
|
-
if (!m) return null
|
|
46
|
-
const fm: NodeFrontmatter = { concepts: [], model: false }
|
|
47
|
-
const lines = m[1].split('\n').map((l) => l.replace(/\r$/, ''))
|
|
48
|
-
for (let i = 0; i < lines.length; i++) {
|
|
49
|
-
const line = lines[i]
|
|
50
|
-
if (line.trim() === '' || line.trim().startsWith('#')) continue
|
|
51
|
-
if (line.length - line.trimStart().length !== 0) continue // only top-level keys
|
|
52
|
-
const [key, ...rest] = line.trim().split(':')
|
|
53
|
-
const value = rest.join(':').trim()
|
|
54
|
-
if (key === 'spec-type') fm.specType = unquote(value)
|
|
55
|
-
else if (key === 'model') fm.model = unquote(value) === 'true'
|
|
56
|
-
else if (key === 'concept') {
|
|
57
|
-
if (value === '' || value === '|' || value === '>') {
|
|
58
|
-
// block list on following more-indented `- item` lines
|
|
59
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
60
|
-
const item = lines[j]
|
|
61
|
-
if (item.trim() === '') continue
|
|
62
|
-
if (item.length - item.trimStart().length === 0) break
|
|
63
|
-
const dash = /^\s*-\s+(.*)$/.exec(item)
|
|
64
|
-
if (dash) fm.concepts.push(unquote(dash[1].trim()))
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
fm.concepts.push(...parseScalarOrFlow(value))
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return fm
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// `resolution` | `[governance, resolution]` → string[]
|
|
75
|
-
export function parseScalarOrFlow(value: string): string[] {
|
|
76
|
-
const v = value.trim()
|
|
77
|
-
if (v.startsWith('[') && v.endsWith(']')) {
|
|
78
|
-
return v
|
|
79
|
-
.slice(1, -1)
|
|
80
|
-
.split(',')
|
|
81
|
-
.map((s) => unquote(s.trim()))
|
|
82
|
-
.filter((s) => s.length > 0)
|
|
83
|
-
}
|
|
84
|
-
const single = unquote(v)
|
|
85
|
-
return single.length > 0 ? [single] : []
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function unquote(v: string): string {
|
|
89
|
-
return v.replace(/^["']|["']$/g, '')
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ── Facet kind — where this node's facet sits, mechanically ──
|
|
93
|
-
export function facetKind(relPath: string, specType: string | undefined): FacetKind {
|
|
94
|
-
const p = relPath.replace(/\\/g, '/')
|
|
95
|
-
if (p.startsWith('design/')) return 'rule'
|
|
96
|
-
if (p.startsWith('workflows/')) return 'workflow'
|
|
97
|
-
if (specType === 'reference') return 'reference'
|
|
98
|
-
if (specType === 'behavioral') return 'behavior'
|
|
99
|
-
return 'index'
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function displayPath(relPath: string): string {
|
|
103
|
-
const p = relPath.replace(/\\/g, '/')
|
|
104
|
-
return p.endsWith('/README.md') ? p.slice(0, -'README.md'.length) : p
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── Scan the project-spec — every *.md node carrying a concept tag ──
|
|
108
|
-
export function scanProjectSpec(specDir: string): NodeRecord[] {
|
|
109
|
-
const records: NodeRecord[] = []
|
|
110
|
-
walk(specDir, specDir, records)
|
|
111
|
-
return records
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function walk(dir: string, specDir: string, out: NodeRecord[]): void {
|
|
115
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
116
|
-
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name)) continue
|
|
117
|
-
const full = join(dir, entry.name)
|
|
118
|
-
if (entry.isDirectory()) {
|
|
119
|
-
walk(full, specDir, out)
|
|
120
|
-
} else if (entry.name.endsWith('.md')) {
|
|
121
|
-
const fm = parseFrontmatter(readFileSync(full, 'utf8'))
|
|
122
|
-
if (!fm || fm.concepts.length === 0) continue
|
|
123
|
-
const relPath = relative(specDir, full).replace(/\\/g, '/')
|
|
124
|
-
out.push({
|
|
125
|
-
relPath,
|
|
126
|
-
display: displayPath(relPath),
|
|
127
|
-
concepts: fm.concepts,
|
|
128
|
-
facet: facetKind(relPath, fm.specType),
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── Group + render ──
|
|
135
|
-
export function groupByConcept(records: NodeRecord[]): Map<string, NodeRecord[]> {
|
|
136
|
-
const grouped = new Map<string, NodeRecord[]>()
|
|
137
|
-
for (const rec of records) {
|
|
138
|
-
for (const concept of rec.concepts) {
|
|
139
|
-
const list = grouped.get(concept) ?? []
|
|
140
|
-
list.push(rec)
|
|
141
|
-
grouped.set(concept, list)
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// stable order: concepts alphabetical, nodes by display path
|
|
145
|
-
return new Map(
|
|
146
|
-
[...grouped.entries()]
|
|
147
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
148
|
-
.map(([k, v]) => [k, [...v].sort((x, y) => x.display.localeCompare(y.display))]),
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function renderTable(grouped: Map<string, NodeRecord[]>): string {
|
|
153
|
-
const rows = [...grouped.entries()].map(([concept, nodes]) => {
|
|
154
|
-
const facets = nodes.map((n) => `\`${n.display}\` (${n.facet})`).join(' · ')
|
|
155
|
-
return `| \`${concept}\` | ${facets} |`
|
|
156
|
-
})
|
|
157
|
-
return ['| Concept | Facets |', '|---|---|', ...rows].join('\n')
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// The full generated section, markers included.
|
|
161
|
-
export function renderSection(grouped: Map<string, NodeRecord[]>): string {
|
|
162
|
-
return [
|
|
163
|
-
BEGIN_MARKER,
|
|
164
|
-
'',
|
|
165
|
-
'## By concept',
|
|
166
|
-
'',
|
|
167
|
-
'> Generated from `concept:` frontmatter by `project-spec/concept-index` — do not edit by hand.',
|
|
168
|
-
'',
|
|
169
|
-
renderTable(grouped),
|
|
170
|
-
'',
|
|
171
|
-
END_MARKER,
|
|
172
|
-
].join('\n')
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ── spec.md block maintenance ──
|
|
176
|
-
// Replace the BEGIN…END block with `section`; insert at the anchor (or append) when absent.
|
|
177
|
-
export function applySection(specText: string, section: string): string {
|
|
178
|
-
const begin = specText.indexOf(BEGIN_MARKER)
|
|
179
|
-
const end = specText.indexOf(END_MARKER)
|
|
180
|
-
if (begin !== -1 && end !== -1 && end > begin) {
|
|
181
|
-
const before = specText.slice(0, begin)
|
|
182
|
-
const after = specText.slice(end + END_MARKER.length)
|
|
183
|
-
return before + section + after
|
|
184
|
-
}
|
|
185
|
-
const anchor = specText.indexOf(ANCHOR_HEADING)
|
|
186
|
-
if (anchor !== -1) {
|
|
187
|
-
return specText.slice(0, anchor) + section + '\n\n' + specText.slice(anchor)
|
|
188
|
-
}
|
|
189
|
-
const trimmed = specText.replace(/\s*$/, '')
|
|
190
|
-
return `${trimmed}\n\n${section}\n`
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Extract the current block (BEGIN…END inclusive), or null when absent.
|
|
194
|
-
export function extractSection(specText: string): string | null {
|
|
195
|
-
const begin = specText.indexOf(BEGIN_MARKER)
|
|
196
|
-
const end = specText.indexOf(END_MARKER)
|
|
197
|
-
if (begin === -1 || end === -1 || end < begin) return null
|
|
198
|
-
return specText.slice(begin, end + END_MARKER.length)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── CLI ──
|
|
202
|
-
function parseArgs(argv: string[]): { specDir: string; mode: 'print' | 'write' | 'check' } {
|
|
203
|
-
let specDir = '.'
|
|
204
|
-
let mode: 'print' | 'write' | 'check' = 'print'
|
|
205
|
-
for (let i = 0; i < argv.length; i++) {
|
|
206
|
-
const a = argv[i]
|
|
207
|
-
if (a === '--spec-dir') specDir = argv[++i] ?? '.'
|
|
208
|
-
else if (a === '--write') mode = 'write'
|
|
209
|
-
else if (a === '--check') mode = 'check'
|
|
210
|
-
}
|
|
211
|
-
return { specDir, mode }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function main(argv: string[]): number {
|
|
215
|
-
const { specDir, mode } = parseArgs(argv)
|
|
216
|
-
const grouped = groupByConcept(scanProjectSpec(specDir))
|
|
217
|
-
const section = renderSection(grouped)
|
|
218
|
-
const specPath = join(specDir, 'spec.md')
|
|
219
|
-
if (mode === 'print') {
|
|
220
|
-
process.stdout.write(`${section}\n`)
|
|
221
|
-
return 0
|
|
222
|
-
}
|
|
223
|
-
if (!existsSync(specPath)) {
|
|
224
|
-
process.stderr.write(`concept-index: no spec.md at ${specPath}\n`)
|
|
225
|
-
return 2
|
|
226
|
-
}
|
|
227
|
-
const specText = readFileSync(specPath, 'utf8')
|
|
228
|
-
if (mode === 'check') {
|
|
229
|
-
const current = extractSection(specText)
|
|
230
|
-
if (current === section) {
|
|
231
|
-
process.stdout.write('concept-index: no drift\n')
|
|
232
|
-
return 0
|
|
233
|
-
}
|
|
234
|
-
process.stderr.write('concept-index: drift — spec.md by-concept block is stale; run --write\n')
|
|
235
|
-
return 1
|
|
236
|
-
}
|
|
237
|
-
// write
|
|
238
|
-
const next = applySection(specText, section)
|
|
239
|
-
if (next !== specText) writeFileSync(specPath, next)
|
|
240
|
-
process.stdout.write(`concept-index: ${next === specText ? 'unchanged' : 'updated'} ${specPath}\n`)
|
|
241
|
-
return 0
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
245
|
-
process.exit(main(process.argv.slice(2)))
|
|
246
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
//#region skills/discover-plans/scripts/discover-plans.mts
|
|
6
|
-
const TODO_STATUSES = /* @__PURE__ */ new Set([
|
|
7
|
-
"pending",
|
|
8
|
-
"in_progress",
|
|
9
|
-
"completed"
|
|
10
|
-
]);
|
|
11
|
-
/** The default mission dispatch status — the meaning of an absent top-level `status`. */
|
|
12
|
-
const DEFAULT_PLAN_STATUS = "active";
|
|
13
|
-
function parsePlanFrontmatter(text) {
|
|
14
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
15
|
-
if (!m) return null;
|
|
16
|
-
const fm = {
|
|
17
|
-
name: "",
|
|
18
|
-
status: DEFAULT_PLAN_STATUS,
|
|
19
|
-
total: 0,
|
|
20
|
-
completed: 0,
|
|
21
|
-
inProgress: 0,
|
|
22
|
-
pending: 0
|
|
23
|
-
};
|
|
24
|
-
let inTodos = false;
|
|
25
|
-
for (const raw of m[1].split("\n")) {
|
|
26
|
-
const line = raw.replace(/\r$/, "");
|
|
27
|
-
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
28
|
-
const indent = line.length - line.trimStart().length;
|
|
29
|
-
const trimmed = line.trim();
|
|
30
|
-
if (indent === 0) {
|
|
31
|
-
inTodos = false;
|
|
32
|
-
const [key, ...rest] = trimmed.split(":");
|
|
33
|
-
const value = rest.join(":").trim();
|
|
34
|
-
if (key === "name") fm.name = unquote(value);
|
|
35
|
-
else if (key === "status") {
|
|
36
|
-
const v = unquote(value);
|
|
37
|
-
if (v !== "") fm.status = v;
|
|
38
|
-
} else if (key === "todos") inTodos = true;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (inTodos) {
|
|
42
|
-
const sm = /^(?:-\s+)?status:\s*(.+)$/.exec(trimmed);
|
|
43
|
-
if (sm) {
|
|
44
|
-
const status = unquote(sm[1].trim());
|
|
45
|
-
fm.total++;
|
|
46
|
-
if (status === "completed") fm.completed++;
|
|
47
|
-
else if (status === "in_progress") fm.inProgress++;
|
|
48
|
-
else if (status === "pending") fm.pending++;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return fm;
|
|
53
|
-
}
|
|
54
|
-
function unquote(v) {
|
|
55
|
-
return v.replace(/^["']|["']$/g, "");
|
|
56
|
-
}
|
|
57
|
-
function nextLead(text) {
|
|
58
|
-
const lines = text.split("\n");
|
|
59
|
-
let i = lines.findIndex((l) => /^##\s+NEXT\b/i.test(l.trim()));
|
|
60
|
-
if (i === -1) return "";
|
|
61
|
-
for (i += 1; i < lines.length; i++) {
|
|
62
|
-
const l = lines[i].replace(/\r$/, "").trim();
|
|
63
|
-
if (l === "") continue;
|
|
64
|
-
if (l.startsWith("#")) return "";
|
|
65
|
-
const cleaned = l.replace(/^[-*]\s+/, "").replace(/\*\*/g, "").trim();
|
|
66
|
-
return cleaned.length > 200 ? `${cleaned.slice(0, 197)}...` : cleaned;
|
|
67
|
-
}
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
70
|
-
function discoverPlanFiles(root) {
|
|
71
|
-
const dir = join(root, ".agents", "plans");
|
|
72
|
-
if (!existsSync(dir)) return [];
|
|
73
|
-
let entries;
|
|
74
|
-
try {
|
|
75
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
76
|
-
} catch {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".plan.md")).map((e) => e.name).sort();
|
|
80
|
-
}
|
|
81
|
-
function collectPlans(root) {
|
|
82
|
-
const out = [];
|
|
83
|
-
for (const name of discoverPlanFiles(root)) {
|
|
84
|
-
let text;
|
|
85
|
-
try {
|
|
86
|
-
text = readFileSync(join(root, ".agents", "plans", name), "utf8");
|
|
87
|
-
} catch {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const fm = parsePlanFrontmatter(text);
|
|
91
|
-
if (!fm) continue;
|
|
92
|
-
out.push({
|
|
93
|
-
cr: name.replace(/\.plan\.md$/, ""),
|
|
94
|
-
name: fm.name,
|
|
95
|
-
total: fm.total,
|
|
96
|
-
completed: fm.completed,
|
|
97
|
-
inProgress: fm.inProgress,
|
|
98
|
-
status: fm.status,
|
|
99
|
-
next: nextLead(text)
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return out.sort((a, b) => a.cr < b.cr ? -1 : a.cr > b.cr ? 1 : 0);
|
|
103
|
-
}
|
|
104
|
-
function filterByStatus(plans, status) {
|
|
105
|
-
return plans.filter((p) => p.status === status);
|
|
106
|
-
}
|
|
107
|
-
const COLUMNS = [
|
|
108
|
-
"cr",
|
|
109
|
-
"name",
|
|
110
|
-
"total",
|
|
111
|
-
"completed",
|
|
112
|
-
"inProgress",
|
|
113
|
-
"status",
|
|
114
|
-
"next"
|
|
115
|
-
];
|
|
116
|
-
function toonField(v) {
|
|
117
|
-
if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
|
|
118
|
-
return v;
|
|
119
|
-
}
|
|
120
|
-
function toToon(plans) {
|
|
121
|
-
return [`plans[${plans.length}]{${COLUMNS.join(",")}}:`, ...plans.map((p) => ` ${COLUMNS.map((c) => toonField(String(p[c]))).join(",")}`)].join("\n");
|
|
122
|
-
}
|
|
123
|
-
function main(argv) {
|
|
124
|
-
const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
|
|
125
|
-
const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
|
|
126
|
-
const statusFilter = argv.includes("--status") ? argv[argv.indexOf("--status") + 1] : void 0;
|
|
127
|
-
let plans = collectPlans(root);
|
|
128
|
-
if (statusFilter !== void 0) plans = filterByStatus(plans, statusFilter);
|
|
129
|
-
const out = format === "json" ? JSON.stringify(plans, null, 2) : toToon(plans);
|
|
130
|
-
process.stdout.write(`${out}\n`);
|
|
131
|
-
return 0;
|
|
132
|
-
}
|
|
133
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
134
|
-
//#endregion
|
|
135
|
-
export { DEFAULT_PLAN_STATUS, TODO_STATUSES, collectPlans, filterByStatus, main, nextLead, parsePlanFrontmatter, toToon };
|