@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.
- package/LICENSE +21 -0
- package/README.md +1014 -0
- package/bin/compose.js +1515 -0
- package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
- package/dist/assets/arc-SxJ2J1sh.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
- package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
- package/dist/assets/channel-DGElom1e.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
- package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
- package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
- package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
- package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
- package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
- package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
- package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
- package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
- package/dist/assets/clone-DUJKJXd7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
- package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
- package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
- package/dist/assets/graph-D0Cfv00Y.js +1 -0
- package/dist/assets/index-CUd6pFGF.css +1 -0
- package/dist/assets/index-DReRlzZI.js +1144 -0
- package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
- package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
- package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-Bj72wOEB.js +1 -0
- package/dist/assets/linear-BRFo114D.js +1 -0
- package/dist/assets/min-GCHnKlJS.js +1 -0
- package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
- package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
- package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
- package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
- package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
- package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
- package/dist/index.html +30 -0
- package/lib/agent-chains.js +65 -0
- package/lib/agent-string.js +86 -0
- package/lib/budget-ledger.js +86 -0
- package/lib/build-all.js +162 -0
- package/lib/build-dag.js +120 -0
- package/lib/build-stream-writer.js +190 -0
- package/lib/build.js +2997 -0
- package/lib/capability-checker.js +53 -0
- package/lib/cert-inject.js +38 -0
- package/lib/cli-progress.js +483 -0
- package/lib/constants.js +69 -0
- package/lib/cross-layer-audit.js +84 -0
- package/lib/debug-discipline.js +173 -0
- package/lib/feature-json.js +106 -0
- package/lib/gate-prompt.js +291 -0
- package/lib/gate-tiers.js +194 -0
- package/lib/health-history.js +119 -0
- package/lib/health-score.js +227 -0
- package/lib/ideabox.js +570 -0
- package/lib/import.js +244 -0
- package/lib/migrate-roadmap.js +94 -0
- package/lib/model-pricing.js +67 -0
- package/lib/new.js +413 -0
- package/lib/pipeline-cli.js +489 -0
- package/lib/plan-parser.js +103 -0
- package/lib/qa-scoping.js +474 -0
- package/lib/questionnaire.js +200 -0
- package/lib/resolve-port.js +7 -0
- package/lib/result-normalizer.js +349 -0
- package/lib/review-lenses.js +166 -0
- package/lib/roadmap-gen.js +210 -0
- package/lib/roadmap-parser.js +176 -0
- package/lib/server-probe.js +23 -0
- package/lib/staleness.js +87 -0
- package/lib/step-prompt.js +260 -0
- package/lib/step-validator.js +49 -0
- package/lib/stratum-mcp-client.js +365 -0
- package/lib/team-flag.js +46 -0
- package/lib/test-bootstrap.js +401 -0
- package/lib/triage.js +274 -0
- package/lib/vision-writer.js +391 -0
- package/package.json +111 -0
- package/pipelines/bug-fix.stratum.yaml +230 -0
- package/pipelines/build.stratum.yaml +498 -0
- package/pipelines/content.stratum.yaml +112 -0
- package/pipelines/coverage-sweep.stratum.yaml +52 -0
- package/pipelines/refactor.stratum.yaml +169 -0
- package/pipelines/research.stratum.yaml +88 -0
- package/pipelines/review-fix.stratum.yaml +109 -0
- package/presets/team-feature.stratum.yaml +105 -0
- package/presets/team-research.stratum.yaml +108 -0
- package/presets/team-review.stratum.yaml +106 -0
- package/scripts/agent-activity-hook.sh +31 -0
- package/scripts/agent-error-hook.sh +28 -0
- package/scripts/analyze-orphans.mjs +50 -0
- package/scripts/find-orphans.mjs +26 -0
- package/scripts/fix-phases.mjs +49 -0
- package/scripts/generate-stratum-spec.mjs +137 -0
- package/scripts/import-roadmap.mjs +116 -0
- package/scripts/phase-audit.mjs +33 -0
- package/scripts/run-pipeline.mjs +314 -0
- package/scripts/session-end-hook.sh +18 -0
- package/scripts/session-start-hook.sh +38 -0
- package/scripts/vision-hook.sh +104 -0
- package/scripts/vision-track.mjs +554 -0
- package/scripts/wire-all-orphans.mjs +108 -0
- package/scripts/wire-orphans.mjs +164 -0
- package/server/activity-routes.js +123 -0
- package/server/agent-health.js +197 -0
- package/server/agent-hooks.js +102 -0
- package/server/agent-mcp.js +10 -0
- package/server/agent-registry.js +95 -0
- package/server/agent-server.js +290 -0
- package/server/agent-spawn.js +251 -0
- package/server/agent-templates.js +77 -0
- package/server/artifact-manager.js +247 -0
- package/server/artifact-templates/architecture.md +28 -0
- package/server/artifact-templates/blueprint.md +21 -0
- package/server/artifact-templates/design.md +36 -0
- package/server/artifact-templates/plan.md +25 -0
- package/server/artifact-templates/prd.md +43 -0
- package/server/artifact-templates/report.md +40 -0
- package/server/block-tracker.js +90 -0
- package/server/build-stream-bridge.js +502 -0
- package/server/coalescing-buffer.js +46 -0
- package/server/compose-mcp-tools.js +479 -0
- package/server/compose-mcp.js +324 -0
- package/server/connectors/agent-connector.js +78 -0
- package/server/connectors/claude-sdk-connector.js +198 -0
- package/server/connectors/codex-connector.js +240 -0
- package/server/connectors/connector-discovery.js +18 -0
- package/server/connectors/connector-runtime.js +13 -0
- package/server/connectors/opencode-connector.js +200 -0
- package/server/design-routes.js +540 -0
- package/server/design-session.js +161 -0
- package/server/feature-scan.js +593 -0
- package/server/file-watcher.js +284 -0
- package/server/find-root.js +29 -0
- package/server/graph-export.js +343 -0
- package/server/ideabox-cache.js +77 -0
- package/server/ideabox-routes.js +294 -0
- package/server/index.js +156 -0
- package/server/model-tiers.js +49 -0
- package/server/pipeline-routes.js +288 -0
- package/server/policy-evaluator.js +36 -0
- package/server/project-root.js +122 -0
- package/server/security.js +23 -0
- package/server/session-manager.js +403 -0
- package/server/session-routes.js +190 -0
- package/server/session-store.js +107 -0
- package/server/settings-routes.js +35 -0
- package/server/settings-store.js +234 -0
- package/server/stratum-api.js +102 -0
- package/server/stratum-client.js +192 -0
- package/server/stratum-sync.js +193 -0
- package/server/summarizer.js +139 -0
- package/server/supervisor.js +196 -0
- package/server/vision-routes.js +668 -0
- package/server/vision-server.js +393 -0
- package/server/vision-store.js +360 -0
- package/server/vision-utils.js +179 -0
- package/server/worktree-gc.js +137 -0
- 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
|
+
}
|