@smartmemory/compose 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,593 @@
1
+ /**
2
+ * feature-scan.js — Scan feature folders and seed vision store.
3
+ *
4
+ * Scans docs/features/ (or custom path from config) and builds rich feature
5
+ * records from the artifacts found:
6
+ * - Status parsed from design.md / plan.md / report.md frontmatter
7
+ * - Description from first non-heading paragraph
8
+ * - Artifact completeness assessment (confidence score)
9
+ * - Related features extracted from document cross-references
10
+ * - Sub-package detection (top-level dirs with README.md)
11
+ *
12
+ * Routes: GET /api/features/scan, POST /api/features/seed.
13
+ */
14
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ import { getTargetRoot, resolveProjectPath } from './project-root.js';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Metadata extraction
22
+ // ---------------------------------------------------------------------------
23
+
24
+ const STATUS_RE = /^\*\*Status:\*\*\s*(.+)$/im;
25
+ const DATE_RE = /^\*\*Date:\*\*\s*(.+)$/im;
26
+ const FEATURE_ID_RE = /^\*\*Feature\s*ID:\*\*\s*`?([^`\n]+)`?/im;
27
+ const RELATED_DOC_RE = /\[.*?\]\(\.\.\/([\w-]+)\//g;
28
+
29
+ /**
30
+ * Parse markdown frontmatter-style metadata from a file.
31
+ * Looks for **Key:** Value patterns in the first 30 lines.
32
+ */
33
+ function parseMetadata(content) {
34
+ const meta = {};
35
+ const statusMatch = content.match(STATUS_RE);
36
+ if (statusMatch) meta.status = statusMatch[1].trim();
37
+
38
+ const dateMatch = content.match(DATE_RE);
39
+ if (dateMatch) meta.date = dateMatch[1].trim();
40
+
41
+ const idMatch = content.match(FEATURE_ID_RE);
42
+ if (idMatch) meta.featureId = idMatch[1].trim();
43
+
44
+ return meta;
45
+ }
46
+
47
+ /**
48
+ * Extract related feature codes from markdown cross-references.
49
+ * Matches patterns like [text](../FEATURE-CODE/file.md)
50
+ */
51
+ function parseRelatedFeatures(content) {
52
+ const related = new Set();
53
+ let match;
54
+ const re = new RegExp(RELATED_DOC_RE.source, 'g');
55
+ while ((match = re.exec(content)) !== null) {
56
+ related.add(match[1]);
57
+ }
58
+ return [...related];
59
+ }
60
+
61
+ /**
62
+ * Map free-text status strings to vision store status keys.
63
+ */
64
+ function normalizeStatus(raw) {
65
+ if (!raw) return null;
66
+ const lower = raw.toLowerCase().replace(/[^a-z_\s]/g, '').trim();
67
+ if (lower.includes('complete') || lower.includes('done') || lower.includes('shipped')) return 'complete';
68
+ if (lower.includes('in progress') || lower.includes('in_progress') || lower.includes('active')) return 'in_progress';
69
+ if (lower.includes('partial')) return 'in_progress';
70
+ if (lower.includes('blocked')) return 'blocked';
71
+ if (lower.includes('parked') || lower.includes('paused')) return 'parked';
72
+ if (lower.includes('killed') || lower.includes('cancelled') || lower.includes('superseded')) return 'killed';
73
+ if (lower.includes('review')) return 'review';
74
+ if (lower.includes('ready')) return 'ready';
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Infer lifecycle phase from which artifacts exist.
80
+ */
81
+ function inferPhase(artifacts) {
82
+ if (artifacts.includes('report.md')) return 'verification';
83
+ if (artifacts.includes('plan.md')) return 'planning';
84
+ if (artifacts.includes('blueprint.md')) return 'planning';
85
+ if (artifacts.includes('architecture.md')) return 'specification';
86
+ if (artifacts.includes('prd.md')) return 'specification';
87
+ if (artifacts.includes('design.md')) return 'planning';
88
+ return 'vision';
89
+ }
90
+
91
+ /**
92
+ * Compute a 0–3 confidence score based on artifact completeness.
93
+ * 0 = no artifacts, 1 = some exist, 2 = key artifacts present, 3 = full set
94
+ */
95
+ function computeConfidence(artifacts) {
96
+ if (artifacts.length === 0) return 0;
97
+ const key = ['design.md', 'plan.md'];
98
+ const hasKey = key.filter(k => artifacts.includes(k)).length;
99
+ if (artifacts.length >= 4 && hasKey === 2) return 3;
100
+ if (hasKey >= 1 && artifacts.length >= 2) return 2;
101
+ return 1;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Scan
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Scan feature folders and return structured feature data.
110
+ *
111
+ * Each subdirectory of the features path is a feature. Reads metadata from
112
+ * design.md, plan.md, report.md. Detects relationships from cross-references.
113
+ *
114
+ * @param {string} [featuresDir] — absolute path to features directory
115
+ * @returns {Array<Feature>}
116
+ */
117
+ export function scanFeatures(featuresDir) {
118
+ const dir = featuresDir || resolveProjectPath('features');
119
+ if (!fs.existsSync(dir)) return [];
120
+
121
+ let entries;
122
+ try {
123
+ entries = fs.readdirSync(dir, { withFileTypes: true });
124
+ } catch {
125
+ return [];
126
+ }
127
+
128
+ const features = [];
129
+
130
+ for (const entry of entries) {
131
+ if (!entry.isDirectory()) continue;
132
+ const featureDir = path.join(dir, entry.name);
133
+ const feature = {
134
+ name: entry.name,
135
+ description: '',
136
+ status: null,
137
+ date: null,
138
+ phase: 'planning',
139
+ confidence: 0,
140
+ artifacts: [],
141
+ relatedFeatures: [],
142
+ };
143
+
144
+ // List artifacts
145
+ try {
146
+ feature.artifacts = fs.readdirSync(featureDir)
147
+ .filter(f => f.endsWith('.md'))
148
+ .sort();
149
+ } catch { /* skip */ }
150
+
151
+ // Read metadata from docs in priority order
152
+ const docPriority = ['design.md', 'spec.md', 'plan.md', 'report.md', 'prd.md'];
153
+ let gotDescription = false;
154
+ const allRelated = new Set();
155
+
156
+ for (const docFile of docPriority) {
157
+ const filePath = path.join(featureDir, docFile);
158
+ if (!fs.existsSync(filePath)) continue;
159
+
160
+ try {
161
+ const raw = fs.readFileSync(filePath, 'utf-8');
162
+ const meta = parseMetadata(raw);
163
+
164
+ // First status found wins
165
+ if (!feature.status && meta.status) {
166
+ feature.status = normalizeStatus(meta.status);
167
+ }
168
+ if (!feature.date && meta.date) {
169
+ feature.date = meta.date;
170
+ }
171
+
172
+ // Description: first non-heading paragraph from first available doc
173
+ if (!gotDescription) {
174
+ const lines = raw.split('\n');
175
+ const descLines = [];
176
+ let pastHeading = false;
177
+ for (const line of lines) {
178
+ const trimmed = line.trim();
179
+ // Skip metadata lines like **Status:** etc.
180
+ if (trimmed.startsWith('**') && trimmed.includes(':**')) continue;
181
+ if (trimmed.startsWith('---')) continue;
182
+ if (trimmed.startsWith('>')) continue;
183
+ if (!pastHeading && trimmed.startsWith('#')) { pastHeading = true; continue; }
184
+ if (pastHeading && trimmed) { descLines.push(trimmed); }
185
+ if (descLines.length >= 3) break;
186
+ }
187
+ if (descLines.length) {
188
+ feature.description = descLines.join(' ');
189
+ gotDescription = true;
190
+ }
191
+ }
192
+
193
+ // Collect related features
194
+ for (const rel of parseRelatedFeatures(raw)) {
195
+ allRelated.add(rel);
196
+ }
197
+ } catch { /* skip */ }
198
+ }
199
+
200
+ // Remove self-references
201
+ allRelated.delete(feature.name);
202
+ feature.relatedFeatures = [...allRelated];
203
+
204
+ // Infer phase from artifacts
205
+ feature.phase = inferPhase(feature.artifacts);
206
+
207
+ // Compute confidence from artifact completeness
208
+ feature.confidence = computeConfidence(feature.artifacts);
209
+
210
+ // Default status if none found in docs
211
+ if (!feature.status) {
212
+ // If there's a report.md, likely complete
213
+ if (feature.artifacts.includes('report.md')) feature.status = 'complete';
214
+ else feature.status = 'planned';
215
+ }
216
+
217
+ features.push(feature);
218
+ }
219
+
220
+ return features;
221
+ }
222
+
223
+ /**
224
+ * Scan top-level directories that look like sub-packages.
225
+ * A sub-package has a README.md or setup.py/pyproject.toml at its root.
226
+ */
227
+ export function scanSubPackages() {
228
+ const root = getTargetRoot();
229
+ let entries;
230
+ try {
231
+ entries = fs.readdirSync(root, { withFileTypes: true });
232
+ } catch {
233
+ return [];
234
+ }
235
+
236
+ const packages = [];
237
+ const markers = ['README.md', 'pyproject.toml', 'setup.py', 'package.json', 'Cargo.toml', 'go.mod'];
238
+
239
+ for (const entry of entries) {
240
+ if (!entry.isDirectory()) continue;
241
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'docs') continue;
242
+
243
+ const dirPath = path.join(root, entry.name);
244
+ const hasMarker = markers.some(m => fs.existsSync(path.join(dirPath, m)));
245
+ if (!hasMarker) continue;
246
+
247
+ const pkg = { name: entry.name, type: 'package' };
248
+
249
+ // Try to read description from README
250
+ const readmePath = path.join(dirPath, 'README.md');
251
+ if (fs.existsSync(readmePath)) {
252
+ try {
253
+ const raw = fs.readFileSync(readmePath, 'utf-8');
254
+ const lines = raw.split('\n');
255
+ let pastHeading = false;
256
+ for (const line of lines) {
257
+ if (!pastHeading && line.startsWith('#')) { pastHeading = true; continue; }
258
+ if (pastHeading && line.trim()) {
259
+ pkg.description = line.trim().substring(0, 200);
260
+ break;
261
+ }
262
+ }
263
+ } catch { /* skip */ }
264
+ }
265
+
266
+ packages.push(pkg);
267
+ }
268
+
269
+ return packages;
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Roadmap graph import
274
+ // ---------------------------------------------------------------------------
275
+
276
+ /**
277
+ * Parse a roadmap-graph.html file (Cytoscape-based dependency graph).
278
+ * Extracts the `nodes` and `edges` JS arrays from the <script> block.
279
+ *
280
+ * @param {string} htmlPath — absolute path to roadmap-graph.html
281
+ * @returns {{ nodes: Array, edges: Array }}
282
+ */
283
+ export function parseRoadmapGraph(htmlPath) {
284
+ if (!fs.existsSync(htmlPath)) return { nodes: [], edges: [] };
285
+
286
+ const raw = fs.readFileSync(htmlPath, 'utf-8');
287
+
288
+ // Extract nodes array
289
+ const nodesMatch = raw.match(/const\s+nodes\s*=\s*\[([\s\S]*?)\];\s*\n/);
290
+ const edgesMatch = raw.match(/const\s+edges\s*=\s*\[([\s\S]*?)\];\s*\n/);
291
+
292
+ let nodes = [];
293
+ let edges = [];
294
+
295
+ if (nodesMatch) {
296
+ try {
297
+ // Wrap in array brackets and evaluate as JSON-ish JS
298
+ // The data uses single quotes and unquoted keys, so we need to eval
299
+ nodes = Function(`"use strict"; return [${nodesMatch[1]}]`)();
300
+ } catch (e) {
301
+ console.error('[feature-scan] Failed to parse roadmap-graph nodes:', e.message);
302
+ }
303
+ }
304
+
305
+ if (edgesMatch) {
306
+ try {
307
+ edges = Function(`"use strict"; return [${edgesMatch[1]}]`)();
308
+ } catch (e) {
309
+ console.error('[feature-scan] Failed to parse roadmap-graph edges:', e.message);
310
+ }
311
+ }
312
+
313
+ return { nodes, edges };
314
+ }
315
+
316
+ /**
317
+ * Find roadmap-graph.html in common locations within a project.
318
+ */
319
+ function findRoadmapGraph() {
320
+ const root = getTargetRoot();
321
+ const candidates = [
322
+ path.join(root, 'docs', 'roadmap-graph.html'),
323
+ path.join(root, 'roadmap-graph.html'),
324
+ ];
325
+
326
+ // Search one level of subdirs for docs/roadmap-graph.html
327
+ try {
328
+ const entries = fs.readdirSync(root, { withFileTypes: true });
329
+ for (const entry of entries) {
330
+ if (!entry.isDirectory()) continue;
331
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
332
+ candidates.push(path.join(root, entry.name, 'docs', 'roadmap-graph.html'));
333
+ candidates.push(path.join(root, entry.name, 'roadmap-graph.html'));
334
+ }
335
+ } catch { /* skip */ }
336
+
337
+ for (const p of candidates) {
338
+ if (fs.existsSync(p)) return p;
339
+ }
340
+ return null;
341
+ }
342
+
343
+ const GRAPH_STATUS_MAP = {
344
+ planned: 'planned',
345
+ parked: 'parked',
346
+ partial: 'in_progress',
347
+ open: 'in_progress',
348
+ complete: 'complete',
349
+ };
350
+
351
+ const GRAPH_EDGE_MAP = {
352
+ dep: 'blocks',
353
+ concurrent: 'supports',
354
+ };
355
+
356
+ /**
357
+ * Seed vision store from a roadmap-graph.html file.
358
+ * Creates items for nodes and connections for edges.
359
+ */
360
+ export function seedFromRoadmapGraph(store) {
361
+ const graphPath = findRoadmapGraph();
362
+ if (!graphPath) return { items: 0, connections: 0 };
363
+
364
+ const { nodes, edges } = parseRoadmapGraph(graphPath);
365
+ if (nodes.length === 0) return { items: 0, connections: 0 };
366
+
367
+ console.log(`[vision] Roadmap graph: ${nodes.length} nodes, ${edges.length} edges from ${graphPath}`);
368
+
369
+ const seeded = { items: 0, connections: 0 };
370
+ const idMap = new Map(); // graphNodeId → visionItemId
371
+
372
+ // Create/update items from nodes
373
+ for (const node of nodes) {
374
+ // Look for existing item by featureCode or title
375
+ let item = Array.from(store.items.values()).find(
376
+ i => i.lifecycle?.featureCode === node.id || i.title === node.id
377
+ );
378
+
379
+ const status = GRAPH_STATUS_MAP[node.status] || 'planned';
380
+ const description = [
381
+ node.name || '',
382
+ node.desc || '',
383
+ node.track ? `Track: ${node.track}` : '',
384
+ node.priority ? `Priority: ${node.priority}` : '',
385
+ ].filter(Boolean).join('\n');
386
+
387
+ if (!item) {
388
+ item = store.createItem({
389
+ type: 'feature',
390
+ title: node.id,
391
+ description,
392
+ status,
393
+ phase: 'planning',
394
+ confidence: status === 'complete' ? 3 : node.priority === 'high' ? 1 : 0,
395
+ });
396
+ try {
397
+ store.updateLifecycle(item.id, { featureCode: node.id });
398
+ } catch { /* skip */ }
399
+ item = store.items.get(item.id);
400
+ seeded.items++;
401
+ } else {
402
+ // Update description and status if richer
403
+ const updates = {};
404
+ if (description.length > (item.description || '').length) {
405
+ updates.description = description;
406
+ }
407
+ if (status !== item.status && status !== 'planned') {
408
+ updates.status = status;
409
+ }
410
+ if (Object.keys(updates).length > 0) {
411
+ store.updateItem(item.id, updates);
412
+ }
413
+ }
414
+
415
+ idMap.set(node.id, item.id);
416
+ }
417
+
418
+ // Create connections from edges
419
+ for (const edge of edges) {
420
+ const fromId = idMap.get(edge.source);
421
+ const toId = idMap.get(edge.target);
422
+ if (!fromId || !toId) continue;
423
+
424
+ const type = GRAPH_EDGE_MAP[edge.type] || 'informs';
425
+
426
+ // Check existing
427
+ const exists = Array.from(store.connections.values()).some(
428
+ c => (c.fromId === fromId && c.toId === toId) ||
429
+ (c.fromId === toId && c.toId === fromId)
430
+ );
431
+ if (exists) continue;
432
+
433
+ try {
434
+ store.createConnection({ fromId, toId, type });
435
+ seeded.connections++;
436
+ } catch { /* skip */ }
437
+ }
438
+
439
+ if (seeded.items || seeded.connections) {
440
+ console.log(`[vision] Roadmap graph: ${seeded.items} items, ${seeded.connections} connections seeded`);
441
+ }
442
+ return seeded;
443
+ }
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // Seed
447
+ // ---------------------------------------------------------------------------
448
+
449
+ /**
450
+ * Upsert feature folders into the vision store.
451
+ * Now uses parsed status, confidence, phase, and creates connections
452
+ * for related features.
453
+ *
454
+ * @param {Array} features — result of scanFeatures()
455
+ * @param {object} store — VisionStore instance
456
+ * @returns {{ features: number, updated: number, connections: number }}
457
+ */
458
+ export function seedFeatures(features, store) {
459
+ const seeded = { features: 0, updated: 0, connections: 0 };
460
+ const featureItemMap = new Map(); // featureCode → itemId
461
+
462
+ // First pass: create/update items
463
+ for (const feature of features) {
464
+ let featureItem = Array.from(store.items.values()).find(
465
+ i => i.lifecycle?.featureCode === feature.name
466
+ );
467
+
468
+ if (!featureItem) {
469
+ featureItem = store.createItem({
470
+ type: 'feature',
471
+ title: feature.name,
472
+ description: feature.description || '',
473
+ status: feature.status || 'planned',
474
+ phase: feature.phase || 'planning',
475
+ confidence: feature.confidence,
476
+ files: feature.artifacts.map(a => `docs/features/${feature.name}/${a}`),
477
+ });
478
+ try {
479
+ store.updateLifecycle(featureItem.id, { featureCode: feature.name, currentPhase: 'explore_design' });
480
+ } catch { /* lifecycle method may not exist */ }
481
+ featureItem = store.items.get(featureItem.id);
482
+ seeded.features++;
483
+ } else {
484
+ // Update with richer data if we have it
485
+ const updates = {};
486
+ if (feature.description && feature.description !== featureItem.description) {
487
+ updates.description = feature.description;
488
+ }
489
+ if (feature.status && feature.status !== featureItem.status) {
490
+ updates.status = feature.status;
491
+ }
492
+ if (feature.confidence > (featureItem.confidence || 0)) {
493
+ updates.confidence = feature.confidence;
494
+ }
495
+ const newFiles = feature.artifacts.map(a => `docs/features/${feature.name}/${a}`);
496
+ if (JSON.stringify(newFiles) !== JSON.stringify(featureItem.files || [])) {
497
+ updates.files = newFiles;
498
+ }
499
+ if (Object.keys(updates).length > 0) {
500
+ store.updateItem(featureItem.id, updates);
501
+ seeded.updated++;
502
+ }
503
+ }
504
+
505
+ featureItemMap.set(feature.name, featureItem.id);
506
+ }
507
+
508
+ // Second pass: create connections for related features
509
+ for (const feature of features) {
510
+ const fromId = featureItemMap.get(feature.name);
511
+ if (!fromId || !feature.relatedFeatures.length) continue;
512
+
513
+ for (const relatedName of feature.relatedFeatures) {
514
+ const toId = featureItemMap.get(relatedName);
515
+ if (!toId) continue;
516
+
517
+ // Check if connection already exists
518
+ const exists = Array.from(store.connections.values()).some(
519
+ c => (c.fromId === fromId && c.toId === toId) ||
520
+ (c.fromId === toId && c.toId === fromId)
521
+ );
522
+ if (exists) continue;
523
+
524
+ try {
525
+ store.createConnection({ fromId, toId, type: 'informs' });
526
+ seeded.connections++;
527
+ } catch { /* skip duplicate or invalid */ }
528
+ }
529
+ }
530
+
531
+ if (seeded.features || seeded.updated || seeded.connections) {
532
+ console.log(`[vision] Feature scan: ${seeded.features} new, ${seeded.updated} updated, ${seeded.connections} connections`);
533
+ }
534
+ return seeded;
535
+ }
536
+
537
+ /**
538
+ * Seed sub-packages as vision items (type: 'track').
539
+ */
540
+ export function seedSubPackages(packages, store) {
541
+ let created = 0;
542
+ for (const pkg of packages) {
543
+ // Check if already exists by title
544
+ const exists = Array.from(store.items.values()).some(
545
+ i => i.title === pkg.name && i.type === 'track'
546
+ );
547
+ if (exists) continue;
548
+
549
+ store.createItem({
550
+ type: 'track',
551
+ title: pkg.name,
552
+ description: pkg.description || '',
553
+ status: 'in_progress',
554
+ phase: 'implementation',
555
+ confidence: 2,
556
+ });
557
+ created++;
558
+ }
559
+ if (created) console.log(`[vision] Sub-package scan: ${created} new`);
560
+ return created;
561
+ }
562
+
563
+ // ---------------------------------------------------------------------------
564
+ // Route registration
565
+ // ---------------------------------------------------------------------------
566
+
567
+ /**
568
+ * Attach feature scan/seed REST routes to an Express app.
569
+ */
570
+ export function attachFeatureScanRoutes(app, { store, scheduleBroadcast }) {
571
+ app.get('/api/features/scan', (_req, res) => {
572
+ try {
573
+ const features = scanFeatures();
574
+ const packages = scanSubPackages();
575
+ res.json({ features, packages, count: features.length + packages.length });
576
+ } catch (err) {
577
+ res.status(500).json({ error: err.message });
578
+ }
579
+ });
580
+
581
+ app.post('/api/features/seed', (_req, res) => {
582
+ try {
583
+ const features = scanFeatures();
584
+ const seeded = seedFeatures(features, store);
585
+ const packages = scanSubPackages();
586
+ const pkgCount = seedSubPackages(packages, store);
587
+ scheduleBroadcast();
588
+ res.json({ ok: true, ...seeded, packages: pkgCount });
589
+ } catch (err) {
590
+ res.status(500).json({ error: err.message });
591
+ }
592
+ });
593
+ }