@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,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* roadmap-gen.js — Generate ROADMAP.md from feature.json files.
|
|
3
|
+
*
|
|
4
|
+
* feature.json is the source of truth. ROADMAP.md is a rendered view.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { listFeatures } from './feature-json.js';
|
|
10
|
+
|
|
11
|
+
const STATUS_ORDER = ['IN_PROGRESS', 'PARTIAL', 'PLANNED', 'COMPLETE', 'SUPERSEDED', 'PARKED'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compute the aggregate status for a phase based on its features.
|
|
15
|
+
*/
|
|
16
|
+
function phaseStatus(features) {
|
|
17
|
+
const statuses = new Set(features.map(f => f.status));
|
|
18
|
+
if (statuses.size === 1) return [...statuses][0];
|
|
19
|
+
if (statuses.has('IN_PROGRESS') || statuses.has('PARTIAL')) return 'PARTIAL';
|
|
20
|
+
if (statuses.has('PLANNED') && statuses.has('COMPLETE')) return 'PARTIAL';
|
|
21
|
+
return 'PLANNED';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate ROADMAP.md content from feature.json files.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} cwd - Project root
|
|
28
|
+
* @param {object} [opts]
|
|
29
|
+
* @param {string} [opts.featuresDir] - Relative path to features dir
|
|
30
|
+
* @param {string} [opts.projectName] - Project name for header
|
|
31
|
+
* @param {string} [opts.projectDescription] - Project description for header
|
|
32
|
+
* @returns {string} - Generated ROADMAP.md content
|
|
33
|
+
*/
|
|
34
|
+
export function generateRoadmap(cwd, opts = {}) {
|
|
35
|
+
const featuresDir = opts.featuresDir ?? 'docs/features';
|
|
36
|
+
const features = listFeatures(cwd, featuresDir);
|
|
37
|
+
|
|
38
|
+
// Read existing ROADMAP.md to preserve header/preamble
|
|
39
|
+
const preamble = readPreamble(cwd, opts);
|
|
40
|
+
|
|
41
|
+
// Group by phase
|
|
42
|
+
const phases = new Map();
|
|
43
|
+
const ungrouped = [];
|
|
44
|
+
|
|
45
|
+
for (const f of features) {
|
|
46
|
+
const phase = f.phase ?? null;
|
|
47
|
+
if (!phase) {
|
|
48
|
+
ungrouped.push(f);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (!phases.has(phase)) phases.set(phase, []);
|
|
52
|
+
phases.get(phase).push(f);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sections = [preamble.trimEnd()];
|
|
56
|
+
|
|
57
|
+
// Sort phases by the minimum position of their features (preserves ROADMAP order)
|
|
58
|
+
const sortedPhases = [...phases.entries()].sort((a, b) => {
|
|
59
|
+
const minA = Math.min(...a[1].map(f => f.position ?? 999));
|
|
60
|
+
const minB = Math.min(...b[1].map(f => f.position ?? 999));
|
|
61
|
+
return minA - minB;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Render each phase
|
|
65
|
+
for (const [phase, phaseFeatures] of sortedPhases) {
|
|
66
|
+
const status = phaseStatus(phaseFeatures);
|
|
67
|
+
sections.push(renderPhase(phase, status, phaseFeatures));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Render ungrouped features
|
|
71
|
+
if (ungrouped.length > 0) {
|
|
72
|
+
sections.push(renderPhase('Features', phaseStatus(ungrouped), ungrouped));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Key documents section
|
|
76
|
+
const keyDocs = buildKeyDocs(features, featuresDir);
|
|
77
|
+
if (keyDocs) sections.push(keyDocs);
|
|
78
|
+
|
|
79
|
+
return sections.join('\n\n---\n\n') + '\n';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read the preamble (everything before the first ## Phase/Feature section)
|
|
84
|
+
* from an existing ROADMAP.md, or generate a default one.
|
|
85
|
+
*/
|
|
86
|
+
function readPreamble(cwd, opts) {
|
|
87
|
+
const roadmapPath = join(cwd, 'ROADMAP.md');
|
|
88
|
+
if (existsSync(roadmapPath)) {
|
|
89
|
+
const text = readFileSync(roadmapPath, 'utf-8');
|
|
90
|
+
// Find the first ## heading that looks like a phase/feature section
|
|
91
|
+
const match = text.match(/^(---\s*\n\s*)?(?=## )/m);
|
|
92
|
+
if (match) {
|
|
93
|
+
const idx = text.indexOf(match[0]);
|
|
94
|
+
const pre = text.slice(0, idx).trimEnd();
|
|
95
|
+
if (pre.length > 0) return pre;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Default preamble
|
|
100
|
+
const name = opts.projectName ?? 'Project';
|
|
101
|
+
const desc = opts.projectDescription ?? '';
|
|
102
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
103
|
+
return `# ${name} Roadmap
|
|
104
|
+
|
|
105
|
+
${desc ? desc + '\n\n' : ''}<!-- Generated from feature.json — do not edit manually -->
|
|
106
|
+
<!-- Run: compose roadmap generate -->
|
|
107
|
+
|
|
108
|
+
**Last updated:** ${today}
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Roadmap Conventions
|
|
113
|
+
|
|
114
|
+
- **Status:** \`PLANNED\` | \`IN_PROGRESS\` | \`PARTIAL\` | \`COMPLETE\` | \`SUPERSEDED\` | \`PARKED\`
|
|
115
|
+
- Items are numbered sequentially. Never reuse a number.
|
|
116
|
+
- Cross-reference stable IDs (e.g. \`FEAT-1\`) not section headings.`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Render a phase section with its feature table.
|
|
121
|
+
*/
|
|
122
|
+
function renderPhase(phaseName, status, features) {
|
|
123
|
+
const lines = [`## ${phaseName} — ${status}`, ''];
|
|
124
|
+
|
|
125
|
+
// Phase description from the first feature's phaseDescription if available
|
|
126
|
+
const desc = features[0]?.phaseDescription;
|
|
127
|
+
if (desc) {
|
|
128
|
+
lines.push(desc, '');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Determine table columns based on whether features have sub-items
|
|
132
|
+
const hasSubItems = features.some(f => f.items && f.items.length > 0);
|
|
133
|
+
|
|
134
|
+
if (hasSubItems) {
|
|
135
|
+
// Expanded: one row per sub-item
|
|
136
|
+
lines.push('| # | Feature | Item | Status |');
|
|
137
|
+
lines.push('|---|---------|------|--------|');
|
|
138
|
+
for (const f of features) {
|
|
139
|
+
if (f.items && f.items.length > 0) {
|
|
140
|
+
for (const item of f.items) {
|
|
141
|
+
const num = item.position ?? '—';
|
|
142
|
+
const desc = item.description ?? '';
|
|
143
|
+
const st = item.status ?? f.status;
|
|
144
|
+
lines.push(`| ${num} | ${f.code} | ${desc} | ${st} |`);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const num = f.position ?? '—';
|
|
148
|
+
lines.push(`| ${num} | ${f.code} | ${f.description} | ${f.status} |`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// Simple: one row per feature
|
|
153
|
+
lines.push('| # | Feature | Description | Status |');
|
|
154
|
+
lines.push('|---|---------|-------------|--------|');
|
|
155
|
+
for (const f of features) {
|
|
156
|
+
const num = f.position ?? '—';
|
|
157
|
+
const desc = f.description.length > 80 ? f.description.slice(0, 77) + '...' : f.description;
|
|
158
|
+
lines.push(`| ${num} | ${f.code} | ${desc} | ${f.status} |`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Exit criteria
|
|
163
|
+
const exit = features[0]?.phaseExit;
|
|
164
|
+
if (exit) {
|
|
165
|
+
lines.push('', `**Exit:** ${exit}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Links to design docs
|
|
169
|
+
const docsLinks = features
|
|
170
|
+
.filter(f => f.designDoc)
|
|
171
|
+
.map(f => `See \`${f.designDoc}\` for ${f.code} design.`);
|
|
172
|
+
if (docsLinks.length > 0) {
|
|
173
|
+
lines.push('', docsLinks.join('\n'));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return lines.join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Build a Key Documents section from features that have design docs.
|
|
181
|
+
*/
|
|
182
|
+
function buildKeyDocs(features, featuresDir) {
|
|
183
|
+
const docs = features
|
|
184
|
+
.filter(f => existsSync || f.designDoc) // always include if designDoc is set
|
|
185
|
+
.filter(f => f.designDoc)
|
|
186
|
+
.map(f => `| \`${f.designDoc}\` | ${f.code} design |`);
|
|
187
|
+
|
|
188
|
+
if (docs.length === 0) return null;
|
|
189
|
+
|
|
190
|
+
return [
|
|
191
|
+
'## Key Documents',
|
|
192
|
+
'',
|
|
193
|
+
'| Document | What it is |',
|
|
194
|
+
'|---|---|',
|
|
195
|
+
...docs,
|
|
196
|
+
].join('\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Write the generated ROADMAP.md to disk.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} cwd - Project root
|
|
203
|
+
* @param {object} [opts]
|
|
204
|
+
*/
|
|
205
|
+
export function writeRoadmap(cwd, opts = {}) {
|
|
206
|
+
const content = generateRoadmap(cwd, opts);
|
|
207
|
+
const roadmapPath = join(cwd, 'ROADMAP.md');
|
|
208
|
+
writeFileSync(roadmapPath, content);
|
|
209
|
+
return roadmapPath;
|
|
210
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* roadmap-parser.js — Parse ROADMAP.md into structured feature entries.
|
|
3
|
+
*
|
|
4
|
+
* Extracts feature codes, descriptions, statuses, and phase membership
|
|
5
|
+
* from the markdown table format used by Compose roadmaps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const SKIP_STATUSES = new Set(['COMPLETE', 'SUPERSEDED', 'PARKED']);
|
|
9
|
+
|
|
10
|
+
const PHASE_HEADING_RE = /^##\s+(.+?)(?:\s+—\s+(.+))?$/;
|
|
11
|
+
const MILESTONE_HEADING_RE = /^###\s+(.+?)(?:\s*:\s*(.+))?$/;
|
|
12
|
+
const TABLE_ROW_RE = /^\|(.+)\|$/;
|
|
13
|
+
const FEATURE_CODE_RE = /^[A-Z][\w-]*-\d+/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{ code: string, description: string, status: string, phaseId: string, position: number }} FeatureEntry
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse ROADMAP.md text into an ordered list of feature entries.
|
|
21
|
+
* Anonymous rows (code === '—' or no code) are included for dependency chain
|
|
22
|
+
* purposes but can be filtered out for build lists.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} text - Raw ROADMAP.md content
|
|
25
|
+
* @returns {FeatureEntry[]}
|
|
26
|
+
*/
|
|
27
|
+
export function parseRoadmap(text) {
|
|
28
|
+
const lines = text.split('\n');
|
|
29
|
+
const entries = [];
|
|
30
|
+
let currentPhaseId = '';
|
|
31
|
+
let currentPhaseStatus = '';
|
|
32
|
+
let position = 0;
|
|
33
|
+
let inTable = false;
|
|
34
|
+
let columnLayout = null; // { codeCol, descCol, statusCol }
|
|
35
|
+
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
|
|
39
|
+
// Phase heading: ## Phase 0: Bootstrap — COMPLETE
|
|
40
|
+
const phaseMatch = trimmed.match(PHASE_HEADING_RE);
|
|
41
|
+
if (phaseMatch) {
|
|
42
|
+
currentPhaseId = phaseMatch[1].trim();
|
|
43
|
+
currentPhaseStatus = phaseMatch[2]?.trim() ?? '';
|
|
44
|
+
inTable = false;
|
|
45
|
+
columnLayout = null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Milestone heading: ### Milestone 1: Stratum Engine Complete
|
|
50
|
+
const milestoneMatch = trimmed.match(MILESTONE_HEADING_RE);
|
|
51
|
+
if (milestoneMatch) {
|
|
52
|
+
// Nest under parent phase
|
|
53
|
+
const milestoneLabel = milestoneMatch[1].trim();
|
|
54
|
+
if (currentPhaseId) {
|
|
55
|
+
currentPhaseId = `${currentPhaseId} > ${milestoneLabel}`;
|
|
56
|
+
} else {
|
|
57
|
+
currentPhaseId = milestoneLabel;
|
|
58
|
+
}
|
|
59
|
+
inTable = false;
|
|
60
|
+
columnLayout = null;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Table row
|
|
65
|
+
if (!TABLE_ROW_RE.test(trimmed)) {
|
|
66
|
+
if (inTable) {
|
|
67
|
+
inTable = false;
|
|
68
|
+
columnLayout = null;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const cells = trimmed.split('|').slice(1, -1).map(c => c.trim());
|
|
74
|
+
|
|
75
|
+
// Skip separator rows (|---|---|---|)
|
|
76
|
+
if (cells.every(c => /^[-:]+$/.test(c))) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Detect header row
|
|
81
|
+
if (!inTable) {
|
|
82
|
+
inTable = true;
|
|
83
|
+
columnLayout = detectColumnLayout(cells);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!columnLayout) continue;
|
|
88
|
+
|
|
89
|
+
// Parse data row
|
|
90
|
+
const code = cells[columnLayout.codeCol] ?? '—';
|
|
91
|
+
const desc = cells[columnLayout.descCol] ?? '';
|
|
92
|
+
let status = cells[columnLayout.statusCol] ?? '';
|
|
93
|
+
|
|
94
|
+
// Clean up status (strip bold markers, etc.)
|
|
95
|
+
status = status.replace(/\*\*/g, '').trim();
|
|
96
|
+
|
|
97
|
+
// If the entire phase is COMPLETE, override individual statuses
|
|
98
|
+
if (SKIP_STATUSES.has(currentPhaseStatus)) {
|
|
99
|
+
status = currentPhaseStatus;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isAnonymous = code === '—' || code === '-' || !FEATURE_CODE_RE.test(code);
|
|
103
|
+
|
|
104
|
+
entries.push({
|
|
105
|
+
code: isAnonymous ? `_anon_${position}` : code,
|
|
106
|
+
description: desc.replace(/\*\*/g, '').trim(),
|
|
107
|
+
status: status || 'PLANNED',
|
|
108
|
+
phaseId: currentPhaseId,
|
|
109
|
+
position: position++,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return entries;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Detect the column layout of a table header row.
|
|
118
|
+
*
|
|
119
|
+
* Supported layouts:
|
|
120
|
+
* 4-col: # | Feature | Item | Status → code=Feature, desc=Item
|
|
121
|
+
* 4-col: ID | Item | Location | Status → code=ID, desc=Item
|
|
122
|
+
* 3-col: ID | Feature | Status → code=ID, desc=Feature
|
|
123
|
+
* 3-col: ID | Item | Status → code=ID, desc=Item
|
|
124
|
+
* 3-col: # | Item | Status → anonymous (# is a row number, not a code)
|
|
125
|
+
*/
|
|
126
|
+
function detectColumnLayout(headerCells) {
|
|
127
|
+
const lower = headerCells.map(c => c.toLowerCase());
|
|
128
|
+
|
|
129
|
+
// 4+ columns: look for a Feature column as the code source
|
|
130
|
+
if (lower.length >= 4) {
|
|
131
|
+
const featureIdx = lower.findIndex(c => c === 'feature');
|
|
132
|
+
if (featureIdx !== -1) {
|
|
133
|
+
return {
|
|
134
|
+
codeCol: featureIdx,
|
|
135
|
+
descCol: featureIdx + 1,
|
|
136
|
+
statusCol: lower.length - 1,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// No "feature" column — use first col as code, second as desc
|
|
140
|
+
return {
|
|
141
|
+
codeCol: 0,
|
|
142
|
+
descCol: 1,
|
|
143
|
+
statusCol: lower.length - 1,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3 columns: check if first column is "id" (contains feature codes)
|
|
148
|
+
if (lower.length === 3 && lower[0] === 'id') {
|
|
149
|
+
return {
|
|
150
|
+
codeCol: 0,
|
|
151
|
+
descCol: 1,
|
|
152
|
+
statusCol: 2,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3-column fallback: # | Item | Status (row-number tables, anonymous)
|
|
157
|
+
return {
|
|
158
|
+
codeCol: -1, // will yield undefined → anonymous
|
|
159
|
+
descCol: Math.min(1, lower.length - 2),
|
|
160
|
+
statusCol: lower.length - 1,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Return only features that should be built:
|
|
166
|
+
* - has a real feature code (not anonymous)
|
|
167
|
+
* - status is PLANNED or IN_PROGRESS or PARTIAL
|
|
168
|
+
*
|
|
169
|
+
* @param {FeatureEntry[]} entries
|
|
170
|
+
* @returns {FeatureEntry[]}
|
|
171
|
+
*/
|
|
172
|
+
export function filterBuildable(entries) {
|
|
173
|
+
return entries.filter(e =>
|
|
174
|
+
!e.code.startsWith('_anon_') && !SKIP_STATUSES.has(e.status)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolvePort } from './resolve-port.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Probe whether the Compose server is reachable.
|
|
5
|
+
* @param {number} [port] - Server port (default: resolvePort())
|
|
6
|
+
* @param {number} [timeoutMs=500] - Timeout in ms
|
|
7
|
+
* @returns {Promise<boolean>} true if server responds 2xx to GET /api/health
|
|
8
|
+
*/
|
|
9
|
+
export async function probeServer(port, timeoutMs = 500) {
|
|
10
|
+
const p = port ?? resolvePort();
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`http://localhost:${p}/api/health`, {
|
|
15
|
+
signal: controller.signal,
|
|
16
|
+
});
|
|
17
|
+
return res.ok;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
} finally {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/lib/staleness.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* staleness.js — Artifact staleness detection for COMP-CTX (item 101).
|
|
3
|
+
*
|
|
4
|
+
* Artifacts embed a phase marker in their first 5 lines:
|
|
5
|
+
* <!-- phase: explore_design -->
|
|
6
|
+
*
|
|
7
|
+
* If the feature's current phase is past the artifact's written phase,
|
|
8
|
+
* the artifact is flagged stale.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
// Canonical phase order — earlier index = earlier phase
|
|
15
|
+
const PHASE_ORDER = [
|
|
16
|
+
'explore_design',
|
|
17
|
+
'blueprint',
|
|
18
|
+
'plan',
|
|
19
|
+
'build',
|
|
20
|
+
'ship',
|
|
21
|
+
'done',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Return numeric index of phase (lower = earlier).
|
|
26
|
+
* Unknown phases return -1 so they're never considered stale.
|
|
27
|
+
*/
|
|
28
|
+
function phaseIndex(phase) {
|
|
29
|
+
return PHASE_ORDER.indexOf(phase);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract the <!-- phase: <name> --> marker from the first 5 lines of text.
|
|
34
|
+
* Returns the phase name or null if not found.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} content
|
|
37
|
+
* @returns {string|null}
|
|
38
|
+
*/
|
|
39
|
+
export function extractPhaseMarker(content) {
|
|
40
|
+
const lines = content.split('\n').slice(0, 5);
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const m = line.match(/<!--\s*phase:\s*([\w_-]+)\s*-->/);
|
|
43
|
+
if (m) return m[1];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check staleness of tracked artifacts in a feature folder.
|
|
50
|
+
*
|
|
51
|
+
* Scans for design.md, blueprint.md, plan.md in featureDir.
|
|
52
|
+
* For each file that exists and has a phase marker, compares its
|
|
53
|
+
* phase to currentPhase. If currentPhase is strictly later in
|
|
54
|
+
* PHASE_ORDER, the artifact is stale.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} featureDir - Absolute path to the feature folder
|
|
57
|
+
* @param {string} currentPhase - Feature's current phase name
|
|
58
|
+
* @returns {Array<{ file: string, writtenPhase: string, currentPhase: string, stale: boolean }>}
|
|
59
|
+
*/
|
|
60
|
+
export function checkStaleness(featureDir, currentPhase) {
|
|
61
|
+
const TRACKED = ['design.md', 'blueprint.md', 'plan.md'];
|
|
62
|
+
const results = [];
|
|
63
|
+
|
|
64
|
+
const currentIdx = phaseIndex(currentPhase);
|
|
65
|
+
|
|
66
|
+
for (const filename of TRACKED) {
|
|
67
|
+
const filePath = join(featureDir, filename);
|
|
68
|
+
if (!existsSync(filePath)) continue;
|
|
69
|
+
|
|
70
|
+
let content;
|
|
71
|
+
try {
|
|
72
|
+
content = readFileSync(filePath, 'utf-8');
|
|
73
|
+
} catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const writtenPhase = extractPhaseMarker(content);
|
|
78
|
+
if (!writtenPhase) continue;
|
|
79
|
+
|
|
80
|
+
const writtenIdx = phaseIndex(writtenPhase);
|
|
81
|
+
const stale = writtenIdx !== -1 && currentIdx !== -1 && currentIdx > writtenIdx;
|
|
82
|
+
|
|
83
|
+
results.push({ file: filename, writtenPhase, currentPhase, stale });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return results;
|
|
87
|
+
}
|