cyber-sdd 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/agents/sdd-scanner.md +1 -1
  4. package/package.json +11 -13
  5. package/plugin.json +55 -0
  6. package/skills/align-spec/SKILL.md +1 -1
  7. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  8. package/skills/blast-estimate/README.md +3 -3
  9. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  10. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  11. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  12. package/skills/check-plan-safety/SKILL.md +2 -3
  13. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  14. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  15. package/skills/check-project-specs/SKILL.md +1 -1
  16. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  17. package/skills/check-retired-terms/SKILL.md +2 -3
  18. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  19. package/skills/check-scenario-overlap/SKILL.md +1 -2
  20. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  21. package/skills/check-spec-references/SKILL.md +2 -2
  22. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  23. package/skills/check-spec-structure/SKILL.md +1 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  25. package/skills/collision-ladder/README.md +2 -2
  26. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  27. package/skills/combat-log-governance/SKILL.md +1 -1
  28. package/skills/concept-index/SKILL.md +1 -1
  29. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  30. package/skills/discover-plans/README.md +3 -3
  31. package/skills/discover-plans/SKILL.md +1 -1
  32. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  33. package/skills/discover-specs/README.md +3 -3
  34. package/skills/discover-specs/SKILL.md +1 -1
  35. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  36. package/skills/doctrine-loop/SKILL.md +1 -1
  37. package/skills/gate-validation-governance/SKILL.md +1 -1
  38. package/skills/init/README.md +2 -2
  39. package/skills/init/scripts/wire-statusline.mjs +168 -12
  40. package/skills/manage-ignore/README.md +3 -3
  41. package/skills/manage-ignore/SKILL.md +1 -2
  42. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  43. package/skills/manage-scenario-bridge/README.md +3 -3
  44. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  45. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  46. package/skills/manage-spec-anchors/README.md +3 -3
  47. package/skills/manage-spec-anchors/SKILL.md +1 -1
  48. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  49. package/skills/mission-graph/README.md +2 -2
  50. package/skills/mission-graph/SKILL.md +1 -1
  51. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  52. package/skills/pause-mission/SKILL.md +1 -1
  53. package/skills/place-node/SKILL.md +2 -2
  54. package/skills/place-node/scripts/place-node.mjs +117 -12
  55. package/skills/plan-retirement/README.md +2 -2
  56. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  57. package/skills/resolve-governances/README.md +3 -3
  58. package/skills/resolve-governances/SKILL.md +1 -1
  59. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  60. package/skills/resolve-tracking/SKILL.md +1 -1
  61. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  62. package/skills/scaffold-project-spec/SKILL.md +1 -1
  63. package/skills/spec-gate/README.md +12 -9
  64. package/skills/spec-gate/SKILL.md +2 -2
  65. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  66. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  67. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  68. package/skills/spec-producer-governance/SKILL.md +2 -2
  69. package/skills/start-mission/SKILL.md +1 -1
  70. package/skills/suite-format-governance/README.md +1 -1
  71. package/skills/suite-format-governance/SKILL.md +1 -1
  72. package/skills/touch-set-correction/README.md +5 -5
  73. package/skills/touch-set-correction/SKILL.md +1 -1
  74. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  75. package/skills/verify-scenarios/README.md +6 -6
  76. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  77. package/.plugin/pins.json +0 -3
  78. package/.plugin/plugin.json +0 -19
  79. package/dist/align-spec/scripts/align-spec.js +0 -102
  80. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  81. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  82. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  83. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  84. package/dist/concept-index/scripts/concept-index.js +0 -181
  85. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  86. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  87. package/dist/spec-gate/scripts/check-suite.js +0 -317
  88. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  89. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  90. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  91. package/skills/align-spec/scripts/align-spec.mts +0 -188
  92. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  93. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  94. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  95. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  96. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  97. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  98. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  99. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  100. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  102. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  103. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  104. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  105. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  106. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  108. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  109. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  110. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  111. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  112. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  113. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  114. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  115. package/skills/concept-index/scripts/concept-index.mts +0 -246
  116. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  117. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  118. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  119. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  120. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  121. package/skills/init/scripts/wire-statusline.mts +0 -279
  122. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  123. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  124. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  126. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  128. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  129. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  130. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  131. package/skills/place-node/scripts/place-node.mts +0 -158
  132. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  133. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  134. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  135. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  136. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  137. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  138. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  139. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  140. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  141. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  142. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  143. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  144. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  145. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  146. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  147. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  148. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  149. 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 };