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