@smartmemory/compose 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1014 -0
  3. package/bin/compose.js +1515 -0
  4. package/dist/assets/_baseUniq-CQwX6VLz.js +1 -0
  5. package/dist/assets/arc-SxJ2J1sh.js +1 -0
  6. package/dist/assets/architectureDiagram-Q4EWVU46-BykunY1F.js +36 -0
  7. package/dist/assets/blockDiagram-DXYQGD6D-ohAKBOUw.js +132 -0
  8. package/dist/assets/c4Diagram-AHTNJAMY-DBDC3ENB.js +10 -0
  9. package/dist/assets/channel-DGElom1e.js +1 -0
  10. package/dist/assets/chunk-4BX2VUAB-Cv93Z7uM.js +1 -0
  11. package/dist/assets/chunk-4TB4RGXK-DE0WBDkj.js +206 -0
  12. package/dist/assets/chunk-55IACEB6-CE1EXenG.js +1 -0
  13. package/dist/assets/chunk-EDXVE4YY-DA7Ana6H.js +1 -0
  14. package/dist/assets/chunk-FMBD7UC4-CTDIPA3p.js +15 -0
  15. package/dist/assets/chunk-OYMX7WX6-uGBaPaTX.js +231 -0
  16. package/dist/assets/chunk-QZHKN3VN-CYlnXuUO.js +1 -0
  17. package/dist/assets/chunk-YZCP3GAM-ojGkzcZK.js +1 -0
  18. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +1 -0
  19. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +1 -0
  20. package/dist/assets/clone-DUJKJXd7.js +1 -0
  21. package/dist/assets/cose-bilkent-S5V4N54A-Bktn9hL-.js +1 -0
  22. package/dist/assets/dagre-KV5264BT-DFaSzuRF.js +4 -0
  23. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  24. package/dist/assets/diagram-5BDNPKRD-DnfmDzEm.js +10 -0
  25. package/dist/assets/diagram-G4DWMVQ6-Bm8W9YnG.js +24 -0
  26. package/dist/assets/diagram-MMDJMWI5-B5-TSKvp.js +43 -0
  27. package/dist/assets/diagram-TYMM5635-ls4rqlky.js +24 -0
  28. package/dist/assets/erDiagram-SMLLAGMA-giG6WO-r.js +85 -0
  29. package/dist/assets/flowDiagram-DWJPFMVM-XvlUuz-7.js +162 -0
  30. package/dist/assets/ganttDiagram-T4ZO3ILL-hLBV57oV.js +292 -0
  31. package/dist/assets/gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js +106 -0
  32. package/dist/assets/graph-D0Cfv00Y.js +1 -0
  33. package/dist/assets/index-CUd6pFGF.css +1 -0
  34. package/dist/assets/index-DReRlzZI.js +1144 -0
  35. package/dist/assets/infoDiagram-42DDH7IO-DbqRsOo3.js +2 -0
  36. package/dist/assets/init-Gi6I4Gst.js +1 -0
  37. package/dist/assets/ishikawaDiagram-UXIWVN3A-DnCdx7zb.js +70 -0
  38. package/dist/assets/journeyDiagram-VCZTEJTY-CfD7eNcP.js +139 -0
  39. package/dist/assets/kanban-definition-6JOO6SKY-BYaO9-mK.js +89 -0
  40. package/dist/assets/katex-DkKDou_j.js +257 -0
  41. package/dist/assets/layout-Bj72wOEB.js +1 -0
  42. package/dist/assets/linear-BRFo114D.js +1 -0
  43. package/dist/assets/min-GCHnKlJS.js +1 -0
  44. package/dist/assets/mindmap-definition-QFDTVHPH-n0PMebY4.js +96 -0
  45. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  46. package/dist/assets/pieDiagram-DEJITSTG-pN4CljHF.js +30 -0
  47. package/dist/assets/quadrantDiagram-34T5L4WZ-DNoAy8-D.js +7 -0
  48. package/dist/assets/requirementDiagram-MS252O5E-BhtY05PT.js +84 -0
  49. package/dist/assets/sankeyDiagram-XADWPNL6-B6AD-16A.js +10 -0
  50. package/dist/assets/sequenceDiagram-FGHM5R23-DShHM-uk.js +157 -0
  51. package/dist/assets/stateDiagram-FHFEXIEX-DMxn7HTo.js +1 -0
  52. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +1 -0
  53. package/dist/assets/timeline-definition-GMOUNBTQ-Cdu6uq52.js +120 -0
  54. package/dist/assets/vennDiagram-DHZGUBPP-CpK29iRe.js +34 -0
  55. package/dist/assets/wardley-RL74JXVD-BQgSkdcO.js +162 -0
  56. package/dist/assets/wardleyDiagram-NUSXRM2D-DJHYev6O.js +20 -0
  57. package/dist/assets/xychartDiagram-5P7HB3ND-1d75pbaO.js +7 -0
  58. package/dist/index.html +30 -0
  59. package/lib/agent-chains.js +65 -0
  60. package/lib/agent-string.js +86 -0
  61. package/lib/budget-ledger.js +86 -0
  62. package/lib/build-all.js +162 -0
  63. package/lib/build-dag.js +120 -0
  64. package/lib/build-stream-writer.js +190 -0
  65. package/lib/build.js +2997 -0
  66. package/lib/capability-checker.js +53 -0
  67. package/lib/cert-inject.js +38 -0
  68. package/lib/cli-progress.js +483 -0
  69. package/lib/constants.js +69 -0
  70. package/lib/cross-layer-audit.js +84 -0
  71. package/lib/debug-discipline.js +173 -0
  72. package/lib/feature-json.js +106 -0
  73. package/lib/gate-prompt.js +291 -0
  74. package/lib/gate-tiers.js +194 -0
  75. package/lib/health-history.js +119 -0
  76. package/lib/health-score.js +227 -0
  77. package/lib/ideabox.js +570 -0
  78. package/lib/import.js +244 -0
  79. package/lib/migrate-roadmap.js +94 -0
  80. package/lib/model-pricing.js +67 -0
  81. package/lib/new.js +413 -0
  82. package/lib/pipeline-cli.js +489 -0
  83. package/lib/plan-parser.js +103 -0
  84. package/lib/qa-scoping.js +474 -0
  85. package/lib/questionnaire.js +200 -0
  86. package/lib/resolve-port.js +7 -0
  87. package/lib/result-normalizer.js +349 -0
  88. package/lib/review-lenses.js +166 -0
  89. package/lib/roadmap-gen.js +210 -0
  90. package/lib/roadmap-parser.js +176 -0
  91. package/lib/server-probe.js +23 -0
  92. package/lib/staleness.js +87 -0
  93. package/lib/step-prompt.js +260 -0
  94. package/lib/step-validator.js +49 -0
  95. package/lib/stratum-mcp-client.js +365 -0
  96. package/lib/team-flag.js +46 -0
  97. package/lib/test-bootstrap.js +401 -0
  98. package/lib/triage.js +274 -0
  99. package/lib/vision-writer.js +391 -0
  100. package/package.json +111 -0
  101. package/pipelines/bug-fix.stratum.yaml +230 -0
  102. package/pipelines/build.stratum.yaml +498 -0
  103. package/pipelines/content.stratum.yaml +112 -0
  104. package/pipelines/coverage-sweep.stratum.yaml +52 -0
  105. package/pipelines/refactor.stratum.yaml +169 -0
  106. package/pipelines/research.stratum.yaml +88 -0
  107. package/pipelines/review-fix.stratum.yaml +109 -0
  108. package/presets/team-feature.stratum.yaml +105 -0
  109. package/presets/team-research.stratum.yaml +108 -0
  110. package/presets/team-review.stratum.yaml +106 -0
  111. package/scripts/agent-activity-hook.sh +31 -0
  112. package/scripts/agent-error-hook.sh +28 -0
  113. package/scripts/analyze-orphans.mjs +50 -0
  114. package/scripts/find-orphans.mjs +26 -0
  115. package/scripts/fix-phases.mjs +49 -0
  116. package/scripts/generate-stratum-spec.mjs +137 -0
  117. package/scripts/import-roadmap.mjs +116 -0
  118. package/scripts/phase-audit.mjs +33 -0
  119. package/scripts/run-pipeline.mjs +314 -0
  120. package/scripts/session-end-hook.sh +18 -0
  121. package/scripts/session-start-hook.sh +38 -0
  122. package/scripts/vision-hook.sh +104 -0
  123. package/scripts/vision-track.mjs +554 -0
  124. package/scripts/wire-all-orphans.mjs +108 -0
  125. package/scripts/wire-orphans.mjs +164 -0
  126. package/server/activity-routes.js +123 -0
  127. package/server/agent-health.js +197 -0
  128. package/server/agent-hooks.js +102 -0
  129. package/server/agent-mcp.js +10 -0
  130. package/server/agent-registry.js +95 -0
  131. package/server/agent-server.js +290 -0
  132. package/server/agent-spawn.js +251 -0
  133. package/server/agent-templates.js +77 -0
  134. package/server/artifact-manager.js +247 -0
  135. package/server/artifact-templates/architecture.md +28 -0
  136. package/server/artifact-templates/blueprint.md +21 -0
  137. package/server/artifact-templates/design.md +36 -0
  138. package/server/artifact-templates/plan.md +25 -0
  139. package/server/artifact-templates/prd.md +43 -0
  140. package/server/artifact-templates/report.md +40 -0
  141. package/server/block-tracker.js +90 -0
  142. package/server/build-stream-bridge.js +502 -0
  143. package/server/coalescing-buffer.js +46 -0
  144. package/server/compose-mcp-tools.js +479 -0
  145. package/server/compose-mcp.js +324 -0
  146. package/server/connectors/agent-connector.js +78 -0
  147. package/server/connectors/claude-sdk-connector.js +198 -0
  148. package/server/connectors/codex-connector.js +240 -0
  149. package/server/connectors/connector-discovery.js +18 -0
  150. package/server/connectors/connector-runtime.js +13 -0
  151. package/server/connectors/opencode-connector.js +200 -0
  152. package/server/design-routes.js +540 -0
  153. package/server/design-session.js +161 -0
  154. package/server/feature-scan.js +593 -0
  155. package/server/file-watcher.js +284 -0
  156. package/server/find-root.js +29 -0
  157. package/server/graph-export.js +343 -0
  158. package/server/ideabox-cache.js +77 -0
  159. package/server/ideabox-routes.js +294 -0
  160. package/server/index.js +156 -0
  161. package/server/model-tiers.js +49 -0
  162. package/server/pipeline-routes.js +288 -0
  163. package/server/policy-evaluator.js +36 -0
  164. package/server/project-root.js +122 -0
  165. package/server/security.js +23 -0
  166. package/server/session-manager.js +403 -0
  167. package/server/session-routes.js +190 -0
  168. package/server/session-store.js +107 -0
  169. package/server/settings-routes.js +35 -0
  170. package/server/settings-store.js +234 -0
  171. package/server/stratum-api.js +102 -0
  172. package/server/stratum-client.js +192 -0
  173. package/server/stratum-sync.js +193 -0
  174. package/server/summarizer.js +139 -0
  175. package/server/supervisor.js +196 -0
  176. package/server/vision-routes.js +668 -0
  177. package/server/vision-server.js +393 -0
  178. package/server/vision-store.js +360 -0
  179. package/server/vision-utils.js +179 -0
  180. package/server/worktree-gc.js +137 -0
  181. package/templates/ROADMAP.md +46 -0
@@ -0,0 +1,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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Canonical port resolution for the Compose server.
3
+ * Single source of truth: COMPOSE_PORT > PORT > 3001.
4
+ */
5
+ export function resolvePort() {
6
+ return Number(process.env.COMPOSE_PORT) || Number(process.env.PORT) || 3001;
7
+ }