@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,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qa-scoping.js — COMP-QA: Diff-Aware QA Scoping (items 113-116)
|
|
3
|
+
*
|
|
4
|
+
* Analyzes git diff output (via context.filesChanged) to identify which
|
|
5
|
+
* routes/pages are affected by a change set. v1 is file-analysis only —
|
|
6
|
+
* no actual Playwright execution.
|
|
7
|
+
*
|
|
8
|
+
* Exports:
|
|
9
|
+
* mapFilesToRoutes(filesChanged, config?)
|
|
10
|
+
* classifyRoutes(routes, allKnownRoutes)
|
|
11
|
+
* detectDevServer(timeout?)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join, posix } from 'node:path';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Ports to probe when searching for a running dev server. */
|
|
22
|
+
const DEV_SERVER_PORTS = [3000, 3001, 4000, 5173, 8080];
|
|
23
|
+
|
|
24
|
+
/** Files that are docs/config only — no route mapping needed. */
|
|
25
|
+
const DOCS_CONFIG_EXTENSIONS = new Set([
|
|
26
|
+
'.md', '.mdx', '.txt', '.rst',
|
|
27
|
+
'.json', '.yaml', '.yml', '.toml', '.ini', '.env',
|
|
28
|
+
'.gitignore', '.npmrc', '.editorconfig',
|
|
29
|
+
'.prettierrc', '.eslintrc',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Routes.yaml config loader
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load explicit route mappings from .compose/routes.yaml or compose.routes.yaml.
|
|
38
|
+
* Returns null if no config file is found or it cannot be parsed.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} [cwd] Project root. Defaults to process.cwd().
|
|
41
|
+
* @returns {{ mappings: Array<{ pattern: string, routes: string[] }> } | null}
|
|
42
|
+
*/
|
|
43
|
+
export function loadRoutesConfig(cwd = process.cwd()) {
|
|
44
|
+
const candidates = [
|
|
45
|
+
join(cwd, '.compose', 'routes.yaml'),
|
|
46
|
+
join(cwd, 'compose.routes.yaml'),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (!existsSync(candidate)) continue;
|
|
51
|
+
try {
|
|
52
|
+
const raw = readFileSync(candidate, 'utf-8');
|
|
53
|
+
// Minimal YAML parser for the routes.yaml shape — avoids a yaml dep.
|
|
54
|
+
const parsed = parseRoutesYaml(raw);
|
|
55
|
+
if (parsed?.mappings && Array.isArray(parsed.mappings)) {
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Malformed config — fall through to heuristics
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Minimal parser for the routes.yaml shape.
|
|
67
|
+
* Only handles the documented format — not a full YAML parser.
|
|
68
|
+
*
|
|
69
|
+
* Format:
|
|
70
|
+
* mappings:
|
|
71
|
+
* - pattern: "src/pages/auth/*"
|
|
72
|
+
* routes: ["/login", "/signup"]
|
|
73
|
+
* - pattern: "src/api/users*"
|
|
74
|
+
* routes: ["/api/users", "/api/users/:id"]
|
|
75
|
+
*
|
|
76
|
+
* @param {string} raw Raw YAML content
|
|
77
|
+
* @returns {{ mappings: Array<{ pattern: string, routes: string[] }> }}
|
|
78
|
+
*/
|
|
79
|
+
export function parseRoutesYaml(raw) {
|
|
80
|
+
const lines = raw.split('\n');
|
|
81
|
+
const mappings = [];
|
|
82
|
+
let current = null;
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
87
|
+
|
|
88
|
+
// Start of a new list item under mappings:
|
|
89
|
+
if (trimmed.startsWith('- pattern:')) {
|
|
90
|
+
if (current) mappings.push(current);
|
|
91
|
+
const pattern = trimmed.replace(/^- pattern:\s*/, '').replace(/^["']|["']$/g, '');
|
|
92
|
+
current = { pattern, routes: [] };
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (trimmed.startsWith('pattern:') && current) {
|
|
97
|
+
current.pattern = trimmed.replace(/^pattern:\s*/, '').replace(/^["']|["']$/g, '');
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (trimmed.startsWith('routes:') && current) {
|
|
102
|
+
// Inline array form: routes: ["/a", "/b"]
|
|
103
|
+
const inline = trimmed.replace(/^routes:\s*/, '').trim();
|
|
104
|
+
if (inline.startsWith('[')) {
|
|
105
|
+
const items = inline.slice(1, inline.lastIndexOf(']'));
|
|
106
|
+
current.routes = items.split(',')
|
|
107
|
+
.map(r => r.trim().replace(/^["']|["']$/g, ''))
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// List item under routes:
|
|
114
|
+
if (trimmed.startsWith('- /') && current) {
|
|
115
|
+
const route = trimmed.replace(/^-\s*/, '').replace(/^["']|["']$/g, '');
|
|
116
|
+
current.routes.push(route);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (trimmed.startsWith('- "') || trimmed.startsWith("- '")) {
|
|
120
|
+
if (current) {
|
|
121
|
+
const route = trimmed.replace(/^-\s*/, '').replace(/^["']|["']$/g, '');
|
|
122
|
+
current.routes.push(route);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (current) mappings.push(current);
|
|
128
|
+
return { mappings };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Framework detection
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Detect the frontend/server framework from file patterns in the changed set.
|
|
137
|
+
*
|
|
138
|
+
* @param {string[]} files Changed file paths (relative to project root)
|
|
139
|
+
* @returns {'nextjs' | 'express' | 'react-router' | 'spa' | 'unknown'}
|
|
140
|
+
*/
|
|
141
|
+
export function detectFramework(files) {
|
|
142
|
+
for (const f of files) {
|
|
143
|
+
const norm = f.replace(/\\/g, '/');
|
|
144
|
+
if (/^(src\/)?app\//.test(norm) && /\.(jsx?|tsx?|mdx?)$/.test(norm)) return 'nextjs';
|
|
145
|
+
if (/^(src\/)?pages\//.test(norm)) return 'nextjs';
|
|
146
|
+
// React Router: check filename pattern BEFORE routes/ directory
|
|
147
|
+
// so src/routes/AuthRoute.tsx resolves as react-router, not express.
|
|
148
|
+
if (/Route\.(jsx?|tsx?)$/.test(norm)) return 'react-router';
|
|
149
|
+
if (/routes?\.(jsx?|tsx?)$/.test(norm)) return 'react-router';
|
|
150
|
+
// Express only if it's a routes/ dir with .js files (backend convention)
|
|
151
|
+
if (/^(src\/)?routes?\//.test(norm) && /\.(js|ts|mjs|cjs)$/.test(norm) && !/\.(jsx|tsx)$/.test(norm)) return 'express';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Fallback: check for react/SPA indicators
|
|
155
|
+
for (const f of files) {
|
|
156
|
+
const norm = f.replace(/\\/g, '/');
|
|
157
|
+
if (/\.(jsx?|tsx?)$/.test(norm)) return 'spa';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return 'unknown';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Route derivation helpers
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Convert a Next.js pages/ or app/ file path to a URL route.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} file e.g. "pages/users/[id].tsx" or "app/users/[id]/page.tsx"
|
|
171
|
+
* @returns {string} URL route, e.g. "/users/[id]"
|
|
172
|
+
*/
|
|
173
|
+
function nextjsFileToRoute(file) {
|
|
174
|
+
const norm = file.replace(/\\/g, '/');
|
|
175
|
+
|
|
176
|
+
// app/ directory: strip app/ prefix, remove /page.tsx, /route.tsx, /layout.tsx etc.
|
|
177
|
+
const appMatch = norm.match(/(?:src\/)?app\/(.+?)(?:\/(?:page|route|layout|loading|error|not-found))?\.(?:jsx?|tsx?|mdx?)$/);
|
|
178
|
+
if (appMatch) {
|
|
179
|
+
const segments = appMatch[1]
|
|
180
|
+
.split('/')
|
|
181
|
+
.filter(s => !s.startsWith('(') || !s.endsWith(')')); // strip route groups like (auth)
|
|
182
|
+
return '/' + segments.join('/');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// pages/ directory
|
|
186
|
+
const pagesMatch = norm.match(/(?:src\/)?pages\/(.+)\.(?:jsx?|tsx?|mdx?)$/);
|
|
187
|
+
if (pagesMatch) {
|
|
188
|
+
const slug = pagesMatch[1];
|
|
189
|
+
// Strip trailing /index or bare "index"
|
|
190
|
+
const clean = slug.replace(/(?:^|\/)index$/, '') || '';
|
|
191
|
+
if (!clean) return '/';
|
|
192
|
+
return clean.startsWith('/') ? clean : '/' + clean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convert an Express routes/ file path to a mount path hint.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} file e.g. "routes/users.js" or "src/routes/auth/login.js"
|
|
202
|
+
* @returns {string} Mount hint, e.g. "/users" or "/auth/login"
|
|
203
|
+
*/
|
|
204
|
+
function expressFileToRoute(file) {
|
|
205
|
+
const norm = file.replace(/\\/g, '/');
|
|
206
|
+
const match = norm.match(/(?:src\/)?routes?\/(.+)\.(?:jsx?|tsx?|mjs?)$/);
|
|
207
|
+
if (!match) return null;
|
|
208
|
+
const slug = match[1].replace(/(?:^|\/)index$/, '');
|
|
209
|
+
if (!slug) return '/';
|
|
210
|
+
return '/' + slug;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extract a route hint from a React Router route component filename.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} file e.g. "src/UserRoute.tsx" or "src/routes/AuthRoute.tsx"
|
|
217
|
+
* @returns {string} Hint like "/user" or "/auth"
|
|
218
|
+
*/
|
|
219
|
+
function reactRouterFileToRoute(file) {
|
|
220
|
+
const norm = file.replace(/\\/g, '/');
|
|
221
|
+
const base = norm.split('/').pop() ?? '';
|
|
222
|
+
const name = base.replace(/Route\.(jsx?|tsx?)$/, '').replace(/routes?\.(jsx?|tsx?)$/, '');
|
|
223
|
+
if (!name) return null;
|
|
224
|
+
// camelCase or PascalCase → kebab-case path segment
|
|
225
|
+
const kebab = name
|
|
226
|
+
.replace(/([A-Z])/g, '-$1')
|
|
227
|
+
.toLowerCase()
|
|
228
|
+
.replace(/^-/, '');
|
|
229
|
+
return '/' + kebab;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Glob pattern matching
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Test whether a file path matches a glob-style pattern.
|
|
238
|
+
* Supports * (any chars within a segment) and ** (any path).
|
|
239
|
+
*
|
|
240
|
+
* @param {string} file
|
|
241
|
+
* @param {string} pattern
|
|
242
|
+
* @returns {boolean}
|
|
243
|
+
*/
|
|
244
|
+
export function matchesGlob(file, pattern) {
|
|
245
|
+
const norm = file.replace(/\\/g, '/');
|
|
246
|
+
const pat = pattern.replace(/\\/g, '/');
|
|
247
|
+
|
|
248
|
+
// Convert glob to regex
|
|
249
|
+
const escaped = pat
|
|
250
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex specials except * and ?
|
|
251
|
+
.replace(/\\\*/g, '§STAR§') // temporarily replace escaped *
|
|
252
|
+
.replace(/\*\*/g, '§GLOBSTAR§') // ** before *
|
|
253
|
+
.replace(/\*/g, '[^/]*') // * = any within segment
|
|
254
|
+
.replace(/§GLOBSTAR§/g, '.*') // ** = any path
|
|
255
|
+
.replace(/§STAR§/g, '\\*'); // restore literal *
|
|
256
|
+
|
|
257
|
+
const re = new RegExp(`^${escaped}$`);
|
|
258
|
+
return re.test(norm);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Main export: mapFilesToRoutes
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Map a set of changed files to affected routes/pages.
|
|
267
|
+
*
|
|
268
|
+
* @param {string[]} filesChanged Changed file paths (relative to project root)
|
|
269
|
+
* @param {object} [config] Optional config override
|
|
270
|
+
* @param {string} [config.cwd] Project root for loading routes.yaml
|
|
271
|
+
* @param {object} [config.routes] Pre-loaded routes config (skips disk read)
|
|
272
|
+
* @returns {{
|
|
273
|
+
* affectedRoutes: string[],
|
|
274
|
+
* unmappedFiles: string[],
|
|
275
|
+
* framework: string,
|
|
276
|
+
* docsOnly: boolean,
|
|
277
|
+
* }}
|
|
278
|
+
*/
|
|
279
|
+
export function mapFilesToRoutes(filesChanged, config = {}) {
|
|
280
|
+
const files = filesChanged ?? [];
|
|
281
|
+
|
|
282
|
+
// Check if all files are docs/config only
|
|
283
|
+
const docsOnly = files.length > 0 && files.every(f => {
|
|
284
|
+
const ext = '.' + f.split('.').pop();
|
|
285
|
+
return DOCS_CONFIG_EXTENSIONS.has(ext) || f.startsWith('docs/') || f.startsWith('.compose/');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (docsOnly) {
|
|
289
|
+
return { affectedRoutes: [], unmappedFiles: files, framework: 'unknown', docsOnly: true };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Load explicit routes config
|
|
293
|
+
const routesConfig = config.routes ?? loadRoutesConfig(config.cwd ?? process.cwd());
|
|
294
|
+
|
|
295
|
+
const affectedRoutes = new Set();
|
|
296
|
+
const unmappedFiles = [];
|
|
297
|
+
|
|
298
|
+
// Check explicit mappings first
|
|
299
|
+
if (routesConfig?.mappings?.length > 0) {
|
|
300
|
+
for (const file of files) {
|
|
301
|
+
let matched = false;
|
|
302
|
+
for (const mapping of routesConfig.mappings) {
|
|
303
|
+
if (matchesGlob(file, mapping.pattern)) {
|
|
304
|
+
for (const route of mapping.routes ?? []) {
|
|
305
|
+
affectedRoutes.add(route);
|
|
306
|
+
}
|
|
307
|
+
matched = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!matched) unmappedFiles.push(file);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
affectedRoutes: [...affectedRoutes],
|
|
315
|
+
unmappedFiles,
|
|
316
|
+
framework: 'explicit',
|
|
317
|
+
docsOnly: false,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Heuristic framework detection
|
|
322
|
+
const framework = detectFramework(files);
|
|
323
|
+
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
let route = null;
|
|
326
|
+
|
|
327
|
+
if (framework === 'nextjs') {
|
|
328
|
+
route = nextjsFileToRoute(file);
|
|
329
|
+
} else if (framework === 'express') {
|
|
330
|
+
route = expressFileToRoute(file);
|
|
331
|
+
} else if (framework === 'react-router') {
|
|
332
|
+
route = reactRouterFileToRoute(file);
|
|
333
|
+
}
|
|
334
|
+
// spa / unknown: mark as unmapped
|
|
335
|
+
|
|
336
|
+
if (route) {
|
|
337
|
+
affectedRoutes.add(route);
|
|
338
|
+
} else {
|
|
339
|
+
unmappedFiles.push(file);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
affectedRoutes: [...affectedRoutes],
|
|
345
|
+
unmappedFiles,
|
|
346
|
+
framework,
|
|
347
|
+
docsOnly: false,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// Export: classifyRoutes
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Classify routes as affected (directly changed) vs adjacent (share parent path).
|
|
357
|
+
*
|
|
358
|
+
* Adjacent routes share the same path prefix as any affected route.
|
|
359
|
+
* E.g. if /users/123 is affected, /users is adjacent.
|
|
360
|
+
*
|
|
361
|
+
* @param {string[]} affectedRoutes Routes directly changed
|
|
362
|
+
* @param {string[]} allKnownRoutes Full set of known routes in the app
|
|
363
|
+
* @returns {{ affected: string[], adjacent: string[] }}
|
|
364
|
+
*/
|
|
365
|
+
export function classifyRoutes(affectedRoutes, allKnownRoutes) {
|
|
366
|
+
const affectedSet = new Set(affectedRoutes);
|
|
367
|
+
const adjacent = new Set();
|
|
368
|
+
|
|
369
|
+
for (const affected of affectedRoutes) {
|
|
370
|
+
// Build all parent paths of the affected route
|
|
371
|
+
const segments = affected.split('/').filter(Boolean);
|
|
372
|
+
for (let depth = 1; depth < segments.length; depth++) {
|
|
373
|
+
const parentPath = '/' + segments.slice(0, depth).join('/');
|
|
374
|
+
for (const known of allKnownRoutes) {
|
|
375
|
+
if (known === parentPath && !affectedSet.has(known)) {
|
|
376
|
+
adjacent.add(known);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Also find siblings (same parent prefix, different leaf)
|
|
382
|
+
const parentPrefix = affected.substring(0, affected.lastIndexOf('/')) || '/';
|
|
383
|
+
for (const known of allKnownRoutes) {
|
|
384
|
+
if (!affectedSet.has(known) && known !== affected) {
|
|
385
|
+
const knownParent = known.substring(0, known.lastIndexOf('/')) || '/';
|
|
386
|
+
if (knownParent === parentPrefix) {
|
|
387
|
+
adjacent.add(known);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
affected: [...affectedSet],
|
|
395
|
+
adjacent: [...adjacent],
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Export: detectDevServer
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Probe common dev server ports and return the first that responds.
|
|
405
|
+
* Detection only — never starts a server.
|
|
406
|
+
*
|
|
407
|
+
* @param {number} [timeout=5000] Per-port timeout in milliseconds
|
|
408
|
+
* @returns {Promise<{ url: string, port: number } | null>}
|
|
409
|
+
*/
|
|
410
|
+
export async function detectDevServer(timeout = 5000) {
|
|
411
|
+
for (const port of DEV_SERVER_PORTS) {
|
|
412
|
+
const url = `http://localhost:${port}`;
|
|
413
|
+
try {
|
|
414
|
+
const result = await probePort(url, timeout);
|
|
415
|
+
if (result) return { url, port };
|
|
416
|
+
} catch {
|
|
417
|
+
// Port not responding — try next
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Probe a single URL with a timeout.
|
|
425
|
+
* Returns true if the server responds (any HTTP status), false on error/timeout.
|
|
426
|
+
*
|
|
427
|
+
* @param {string} url
|
|
428
|
+
* @param {number} timeout Milliseconds
|
|
429
|
+
* @returns {Promise<boolean>}
|
|
430
|
+
*/
|
|
431
|
+
async function probePort(url, timeout) {
|
|
432
|
+
const controller = new AbortController();
|
|
433
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
await fetch(url, {
|
|
437
|
+
method: 'GET',
|
|
438
|
+
signal: controller.signal,
|
|
439
|
+
// Ignore redirect chains — we just want any response
|
|
440
|
+
});
|
|
441
|
+
return true;
|
|
442
|
+
} catch (err) {
|
|
443
|
+
// AbortError = timeout, ECONNREFUSED = nothing listening
|
|
444
|
+
return false;
|
|
445
|
+
} finally {
|
|
446
|
+
clearTimeout(timer);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Utility: isDocsOnlyDiff
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Returns true if all changed files are documentation or config — no code paths
|
|
456
|
+
* that would map to routes.
|
|
457
|
+
*
|
|
458
|
+
* @param {string[]} filesChanged
|
|
459
|
+
* @returns {boolean}
|
|
460
|
+
*/
|
|
461
|
+
export function isDocsOnlyDiff(filesChanged) {
|
|
462
|
+
if (!filesChanged?.length) return false;
|
|
463
|
+
return filesChanged.every(f => {
|
|
464
|
+
const norm = f.replace(/\\/g, '/');
|
|
465
|
+
const dotParts = norm.split('.');
|
|
466
|
+
const ext = dotParts.length > 1 ? '.' + dotParts.pop() : '';
|
|
467
|
+
return (
|
|
468
|
+
DOCS_CONFIG_EXTENSIONS.has(ext) ||
|
|
469
|
+
norm.startsWith('docs/') ||
|
|
470
|
+
norm.startsWith('.compose/') ||
|
|
471
|
+
norm.startsWith('.github/')
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* questionnaire.js — Interactive pre-flight for compose new.
|
|
3
|
+
*
|
|
4
|
+
* Asks the user questions to refine intent before launching the pipeline.
|
|
5
|
+
* Supports: single-line input, multi-choice, yes/no, free-form notes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
|
|
12
|
+
function createRL(opts = {}) {
|
|
13
|
+
return createInterface({
|
|
14
|
+
input: opts.input ?? process.stdin,
|
|
15
|
+
output: opts.output ?? process.stdout,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ask(rl, question) {
|
|
20
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Question types
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
async function askText(rl, prompt, defaultVal = '') {
|
|
28
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : '';
|
|
29
|
+
const answer = await ask(rl, ` ${prompt}${suffix}: `);
|
|
30
|
+
return answer.trim() || defaultVal;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function askChoice(rl, prompt, options, defaultVal = '') {
|
|
34
|
+
const defaultIdx = defaultVal ? options.indexOf(defaultVal) : -1;
|
|
35
|
+
const defaultNum = defaultIdx >= 0 ? defaultIdx + 1 : 1;
|
|
36
|
+
console.log(` ${prompt}`);
|
|
37
|
+
for (let i = 0; i < options.length; i++) {
|
|
38
|
+
const marker = i === defaultNum - 1 ? ' *' : '';
|
|
39
|
+
console.log(` ${i + 1}. ${options[i]}${marker}`);
|
|
40
|
+
}
|
|
41
|
+
const answer = await ask(rl, ` Choice [${defaultNum}]: `);
|
|
42
|
+
const idx = parseInt(answer.trim(), 10) - 1;
|
|
43
|
+
return options[idx >= 0 && idx < options.length ? idx : defaultNum - 1];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function askYesNo(rl, prompt, defaultVal = true) {
|
|
47
|
+
const hint = defaultVal ? '[Y/n]' : '[y/N]';
|
|
48
|
+
const answer = await ask(rl, ` ${prompt} ${hint}: `);
|
|
49
|
+
const a = answer.trim().toLowerCase();
|
|
50
|
+
if (!a) return defaultVal;
|
|
51
|
+
return a === 'y' || a === 'yes';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function askMultiline(rl, prompt) {
|
|
55
|
+
console.log(` ${prompt} (blank line to finish):`);
|
|
56
|
+
const lines = [];
|
|
57
|
+
while (true) {
|
|
58
|
+
const line = await ask(rl, ' > ');
|
|
59
|
+
if (!line.trim()) break;
|
|
60
|
+
lines.push(line);
|
|
61
|
+
}
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Main questionnaire
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run the interactive questionnaire.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} name - Project name
|
|
73
|
+
* @param {string} intent - Initial intent from CLI
|
|
74
|
+
* @param {object} [opts]
|
|
75
|
+
* @param {boolean} [opts.hasExistingContent] - Whether the project dir has existing files
|
|
76
|
+
* @returns {Promise<{ enrichedIntent: string, options: object }>}
|
|
77
|
+
*/
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Persistence — save/load previous answers
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
function loadPrevious(cwd) {
|
|
83
|
+
const p = join(cwd, '.compose', 'questionnaire.json');
|
|
84
|
+
if (!existsSync(p)) return {};
|
|
85
|
+
try { return JSON.parse(readFileSync(p, 'utf-8')); } catch { return {}; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function savePrevious(cwd, answers) {
|
|
89
|
+
const dir = join(cwd, '.compose');
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
writeFileSync(join(dir, 'questionnaire.json'), JSON.stringify(answers, null, 2) + '\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Main questionnaire
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export async function runQuestionnaire(name, intent, opts = {}) {
|
|
99
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
100
|
+
const prev = loadPrevious(cwd);
|
|
101
|
+
const rl = createRL();
|
|
102
|
+
|
|
103
|
+
console.log(`\n Setting up: ${name}`);
|
|
104
|
+
console.log(` Intent: ${intent}\n`);
|
|
105
|
+
if (Object.keys(prev).length > 0) {
|
|
106
|
+
console.log(' (Previous answers loaded as defaults — press Enter to keep)\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// 1. Refine intent
|
|
111
|
+
const refined = await askText(rl, 'Refine the description (or press Enter to keep)', prev.refined ?? intent);
|
|
112
|
+
|
|
113
|
+
// 2. Project type
|
|
114
|
+
const projectType = await askChoice(rl, 'What kind of project?', [
|
|
115
|
+
'CLI tool',
|
|
116
|
+
'Web API / server',
|
|
117
|
+
'Library / SDK',
|
|
118
|
+
'Full-stack app',
|
|
119
|
+
'Other',
|
|
120
|
+
], prev.projectType);
|
|
121
|
+
|
|
122
|
+
// 3. Language/runtime
|
|
123
|
+
const language = await askChoice(rl, 'Primary language/runtime?', [
|
|
124
|
+
'Node.js (JavaScript)',
|
|
125
|
+
'Node.js (TypeScript)',
|
|
126
|
+
'Python',
|
|
127
|
+
'Go',
|
|
128
|
+
'Rust',
|
|
129
|
+
'Other',
|
|
130
|
+
], prev.language);
|
|
131
|
+
|
|
132
|
+
// 4. Complexity
|
|
133
|
+
const complexity = await askChoice(rl, 'Scope?', [
|
|
134
|
+
'Small (1-3 features, single module)',
|
|
135
|
+
'Medium (3-8 features, multiple modules)',
|
|
136
|
+
'Large (8+ features, multi-component)',
|
|
137
|
+
], prev.complexity);
|
|
138
|
+
|
|
139
|
+
// 5. Research
|
|
140
|
+
const doResearch = await askYesNo(rl, 'Research prior art before brainstorming?', prev.doResearch ?? true);
|
|
141
|
+
|
|
142
|
+
// 6. Additional context
|
|
143
|
+
const hasNotes = await askYesNo(rl, 'Any additional context or constraints to add?', prev.notes ? true : false);
|
|
144
|
+
let notes = '';
|
|
145
|
+
if (hasNotes) {
|
|
146
|
+
if (prev.notes) console.log(` Previous notes: ${prev.notes.split('\n')[0]}...`);
|
|
147
|
+
notes = await askMultiline(rl, 'Type your notes (or blank to keep previous)');
|
|
148
|
+
if (!notes && prev.notes) notes = prev.notes;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 7. Review agents
|
|
152
|
+
const reviewAgent = await askChoice(rl, 'Who should review designs?', [
|
|
153
|
+
'Human (gate prompt)',
|
|
154
|
+
'Codex (automated review)',
|
|
155
|
+
'Skip review',
|
|
156
|
+
], prev.reviewAgent);
|
|
157
|
+
|
|
158
|
+
// 8. Confirm
|
|
159
|
+
console.log('\n Summary:');
|
|
160
|
+
console.log(` Project: ${name}`);
|
|
161
|
+
console.log(` Type: ${projectType}`);
|
|
162
|
+
console.log(` Language: ${language}`);
|
|
163
|
+
console.log(` Scope: ${complexity}`);
|
|
164
|
+
console.log(` Research: ${doResearch ? 'yes' : 'skip'}`);
|
|
165
|
+
console.log(` Review: ${reviewAgent}`);
|
|
166
|
+
if (notes) console.log(` Notes: ${notes.split('\n')[0]}...`);
|
|
167
|
+
|
|
168
|
+
const proceed = await askYesNo(rl, '\n Launch kickoff?', true);
|
|
169
|
+
if (!proceed) {
|
|
170
|
+
console.log(' Aborted.');
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Save answers for next run
|
|
175
|
+
savePrevious(cwd, { refined, projectType, language, complexity, doResearch, notes, reviewAgent });
|
|
176
|
+
|
|
177
|
+
// Build enriched intent
|
|
178
|
+
const parts = [refined];
|
|
179
|
+
parts.push(`\n## Project Constraints`);
|
|
180
|
+
parts.push(`- Type: ${projectType}`);
|
|
181
|
+
parts.push(`- Language/Runtime: ${language}`);
|
|
182
|
+
parts.push(`- Scope: ${complexity}`);
|
|
183
|
+
if (notes) parts.push(`\n## Additional Context\n${notes}`);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
enrichedIntent: parts.join('\n'),
|
|
187
|
+
options: {
|
|
188
|
+
projectType,
|
|
189
|
+
language,
|
|
190
|
+
complexity,
|
|
191
|
+
doResearch,
|
|
192
|
+
reviewAgent,
|
|
193
|
+
notes,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
} finally {
|
|
198
|
+
rl.close();
|
|
199
|
+
}
|
|
200
|
+
}
|