popeye-cli 2.2.0 → 2.7.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 (323) hide show
  1. package/dist/adapters/gemini.d.ts +14 -0
  2. package/dist/adapters/gemini.d.ts.map +1 -1
  3. package/dist/adapters/gemini.js +41 -6
  4. package/dist/adapters/gemini.js.map +1 -1
  5. package/dist/adapters/grok.d.ts +14 -0
  6. package/dist/adapters/grok.d.ts.map +1 -1
  7. package/dist/adapters/grok.js +42 -6
  8. package/dist/adapters/grok.js.map +1 -1
  9. package/dist/adapters/openai.d.ts +10 -0
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +44 -5
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/cli/commands/create.js +1 -1
  14. package/dist/cli/commands/create.js.map +1 -1
  15. package/dist/cli/interactive.d.ts.map +1 -1
  16. package/dist/cli/interactive.js +324 -20
  17. package/dist/cli/interactive.js.map +1 -1
  18. package/dist/generators/all.d.ts.map +1 -1
  19. package/dist/generators/all.js +3 -2
  20. package/dist/generators/all.js.map +1 -1
  21. package/dist/generators/doc-parser.d.ts +21 -6
  22. package/dist/generators/doc-parser.d.ts.map +1 -1
  23. package/dist/generators/doc-parser.js +55 -4
  24. package/dist/generators/doc-parser.js.map +1 -1
  25. package/dist/generators/templates/fullstack.js +1 -1
  26. package/dist/generators/templates/website-components.js +1 -1
  27. package/dist/generators/templates/website-components.js.map +1 -1
  28. package/dist/generators/templates/website-config.d.ts +4 -1
  29. package/dist/generators/templates/website-config.d.ts.map +1 -1
  30. package/dist/generators/templates/website-config.js +17 -11
  31. package/dist/generators/templates/website-config.js.map +1 -1
  32. package/dist/generators/templates/website-conversion.js +1 -1
  33. package/dist/generators/templates/website-conversion.js.map +1 -1
  34. package/dist/generators/templates/website-landing.js +1 -1
  35. package/dist/generators/templates/website-landing.js.map +1 -1
  36. package/dist/generators/templates/website-layout.d.ts +36 -4
  37. package/dist/generators/templates/website-layout.d.ts.map +1 -1
  38. package/dist/generators/templates/website-layout.js +466 -23
  39. package/dist/generators/templates/website-layout.js.map +1 -1
  40. package/dist/generators/templates/website-pricing.js +1 -1
  41. package/dist/generators/templates/website-pricing.js.map +1 -1
  42. package/dist/generators/templates/website-sections.js +1 -1
  43. package/dist/generators/templates/website-sections.js.map +1 -1
  44. package/dist/generators/templates/website-seo.d.ts.map +1 -1
  45. package/dist/generators/templates/website-seo.js +4 -1
  46. package/dist/generators/templates/website-seo.js.map +1 -1
  47. package/dist/generators/templates/website.d.ts +1 -1
  48. package/dist/generators/templates/website.d.ts.map +1 -1
  49. package/dist/generators/templates/website.js +1 -1
  50. package/dist/generators/templates/website.js.map +1 -1
  51. package/dist/generators/website-content-ai.d.ts +52 -0
  52. package/dist/generators/website-content-ai.d.ts.map +1 -0
  53. package/dist/generators/website-content-ai.js +141 -0
  54. package/dist/generators/website-content-ai.js.map +1 -0
  55. package/dist/generators/website-content-scanner.d.ts +1 -1
  56. package/dist/generators/website-content-scanner.d.ts.map +1 -1
  57. package/dist/generators/website-content-scanner.js +98 -1
  58. package/dist/generators/website-content-scanner.js.map +1 -1
  59. package/dist/generators/website-context.d.ts +34 -1
  60. package/dist/generators/website-context.d.ts.map +1 -1
  61. package/dist/generators/website-context.js +131 -9
  62. package/dist/generators/website-context.js.map +1 -1
  63. package/dist/generators/website-debug.d.ts +12 -0
  64. package/dist/generators/website-debug.d.ts.map +1 -1
  65. package/dist/generators/website-debug.js +16 -0
  66. package/dist/generators/website-debug.js.map +1 -1
  67. package/dist/generators/website.d.ts.map +1 -1
  68. package/dist/generators/website.js +26 -4
  69. package/dist/generators/website.js.map +1 -1
  70. package/dist/pipeline/auto-recovery.d.ts +56 -0
  71. package/dist/pipeline/auto-recovery.d.ts.map +1 -0
  72. package/dist/pipeline/auto-recovery.js +185 -0
  73. package/dist/pipeline/auto-recovery.js.map +1 -0
  74. package/dist/pipeline/change-request.d.ts +39 -0
  75. package/dist/pipeline/change-request.d.ts.map +1 -1
  76. package/dist/pipeline/change-request.js +40 -1
  77. package/dist/pipeline/change-request.js.map +1 -1
  78. package/dist/pipeline/check-runner.d.ts +30 -1
  79. package/dist/pipeline/check-runner.d.ts.map +1 -1
  80. package/dist/pipeline/check-runner.js +122 -1
  81. package/dist/pipeline/check-runner.js.map +1 -1
  82. package/dist/pipeline/command-resolver.d.ts.map +1 -1
  83. package/dist/pipeline/command-resolver.js +33 -2
  84. package/dist/pipeline/command-resolver.js.map +1 -1
  85. package/dist/pipeline/consensus/arbitrator-query.d.ts +22 -0
  86. package/dist/pipeline/consensus/arbitrator-query.d.ts.map +1 -0
  87. package/dist/pipeline/consensus/arbitrator-query.js +70 -0
  88. package/dist/pipeline/consensus/arbitrator-query.js.map +1 -0
  89. package/dist/pipeline/consensus/consensus-runner.d.ts +131 -7
  90. package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -1
  91. package/dist/pipeline/consensus/consensus-runner.js +809 -35
  92. package/dist/pipeline/consensus/consensus-runner.js.map +1 -1
  93. package/dist/pipeline/cr-lifecycle.d.ts +42 -0
  94. package/dist/pipeline/cr-lifecycle.d.ts.map +1 -0
  95. package/dist/pipeline/cr-lifecycle.js +89 -0
  96. package/dist/pipeline/cr-lifecycle.js.map +1 -0
  97. package/dist/pipeline/gate-engine.d.ts +1 -0
  98. package/dist/pipeline/gate-engine.d.ts.map +1 -1
  99. package/dist/pipeline/gate-engine.js +26 -7
  100. package/dist/pipeline/gate-engine.js.map +1 -1
  101. package/dist/pipeline/orchestrator.d.ts +1 -1
  102. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  103. package/dist/pipeline/orchestrator.js +306 -16
  104. package/dist/pipeline/orchestrator.js.map +1 -1
  105. package/dist/pipeline/packets/consensus-packet-builder.d.ts +15 -4
  106. package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -1
  107. package/dist/pipeline/packets/consensus-packet-builder.js +29 -17
  108. package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -1
  109. package/dist/pipeline/phases/architecture.d.ts.map +1 -1
  110. package/dist/pipeline/phases/architecture.js +5 -3
  111. package/dist/pipeline/phases/architecture.js.map +1 -1
  112. package/dist/pipeline/phases/audit.d.ts.map +1 -1
  113. package/dist/pipeline/phases/audit.js +5 -3
  114. package/dist/pipeline/phases/audit.js.map +1 -1
  115. package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -1
  116. package/dist/pipeline/phases/consensus-architecture.js +10 -1
  117. package/dist/pipeline/phases/consensus-architecture.js.map +1 -1
  118. package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -1
  119. package/dist/pipeline/phases/consensus-master-plan.js +10 -3
  120. package/dist/pipeline/phases/consensus-master-plan.js.map +1 -1
  121. package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -1
  122. package/dist/pipeline/phases/consensus-role-plans.js +10 -1
  123. package/dist/pipeline/phases/consensus-role-plans.js.map +1 -1
  124. package/dist/pipeline/phases/done.d.ts.map +1 -1
  125. package/dist/pipeline/phases/done.js +9 -4
  126. package/dist/pipeline/phases/done.js.map +1 -1
  127. package/dist/pipeline/phases/intake.d.ts.map +1 -1
  128. package/dist/pipeline/phases/intake.js +7 -3
  129. package/dist/pipeline/phases/intake.js.map +1 -1
  130. package/dist/pipeline/phases/phase-context.d.ts +2 -0
  131. package/dist/pipeline/phases/phase-context.d.ts.map +1 -1
  132. package/dist/pipeline/phases/phase-context.js +3 -1
  133. package/dist/pipeline/phases/phase-context.js.map +1 -1
  134. package/dist/pipeline/phases/production-gate.d.ts.map +1 -1
  135. package/dist/pipeline/phases/production-gate.js +28 -3
  136. package/dist/pipeline/phases/production-gate.js.map +1 -1
  137. package/dist/pipeline/phases/qa-validation.d.ts.map +1 -1
  138. package/dist/pipeline/phases/qa-validation.js +38 -5
  139. package/dist/pipeline/phases/qa-validation.js.map +1 -1
  140. package/dist/pipeline/phases/recovery-loop.d.ts +2 -0
  141. package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -1
  142. package/dist/pipeline/phases/recovery-loop.js +200 -6
  143. package/dist/pipeline/phases/recovery-loop.js.map +1 -1
  144. package/dist/pipeline/phases/review.d.ts.map +1 -1
  145. package/dist/pipeline/phases/review.js +58 -28
  146. package/dist/pipeline/phases/review.js.map +1 -1
  147. package/dist/pipeline/phases/role-planning.d.ts.map +1 -1
  148. package/dist/pipeline/phases/role-planning.js +18 -2
  149. package/dist/pipeline/phases/role-planning.js.map +1 -1
  150. package/dist/pipeline/phases/stuck.d.ts.map +1 -1
  151. package/dist/pipeline/phases/stuck.js +10 -0
  152. package/dist/pipeline/phases/stuck.js.map +1 -1
  153. package/dist/pipeline/repo-snapshot.d.ts.map +1 -1
  154. package/dist/pipeline/repo-snapshot.js +3 -0
  155. package/dist/pipeline/repo-snapshot.js.map +1 -1
  156. package/dist/pipeline/role-execution-adapter.d.ts +2 -1
  157. package/dist/pipeline/role-execution-adapter.d.ts.map +1 -1
  158. package/dist/pipeline/role-execution-adapter.js +22 -7
  159. package/dist/pipeline/role-execution-adapter.js.map +1 -1
  160. package/dist/pipeline/skill-loader.d.ts +19 -0
  161. package/dist/pipeline/skill-loader.d.ts.map +1 -1
  162. package/dist/pipeline/skill-loader.js +22 -0
  163. package/dist/pipeline/skill-loader.js.map +1 -1
  164. package/dist/pipeline/skills/coverage-gate.d.ts +44 -0
  165. package/dist/pipeline/skills/coverage-gate.d.ts.map +1 -0
  166. package/dist/pipeline/skills/coverage-gate.js +143 -0
  167. package/dist/pipeline/skills/coverage-gate.js.map +1 -0
  168. package/dist/pipeline/skills/usage-registry.d.ts +48 -0
  169. package/dist/pipeline/skills/usage-registry.d.ts.map +1 -0
  170. package/dist/pipeline/skills/usage-registry.js +55 -0
  171. package/dist/pipeline/skills/usage-registry.js.map +1 -0
  172. package/dist/pipeline/strategy-context.d.ts +20 -0
  173. package/dist/pipeline/strategy-context.d.ts.map +1 -0
  174. package/dist/pipeline/strategy-context.js +55 -0
  175. package/dist/pipeline/strategy-context.js.map +1 -0
  176. package/dist/pipeline/type-defs/artifacts.d.ts +25 -5
  177. package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -1
  178. package/dist/pipeline/type-defs/artifacts.js +4 -0
  179. package/dist/pipeline/type-defs/artifacts.js.map +1 -1
  180. package/dist/pipeline/type-defs/audit.d.ts +25 -13
  181. package/dist/pipeline/type-defs/audit.d.ts.map +1 -1
  182. package/dist/pipeline/type-defs/checks.d.ts +18 -8
  183. package/dist/pipeline/type-defs/checks.d.ts.map +1 -1
  184. package/dist/pipeline/type-defs/checks.js +4 -0
  185. package/dist/pipeline/type-defs/checks.js.map +1 -1
  186. package/dist/pipeline/type-defs/packets.d.ts +104 -18
  187. package/dist/pipeline/type-defs/packets.d.ts.map +1 -1
  188. package/dist/pipeline/type-defs/packets.js +17 -1
  189. package/dist/pipeline/type-defs/packets.js.map +1 -1
  190. package/dist/pipeline/type-defs/state.d.ts +160 -16
  191. package/dist/pipeline/type-defs/state.d.ts.map +1 -1
  192. package/dist/pipeline/type-defs/state.js +26 -1
  193. package/dist/pipeline/type-defs/state.js.map +1 -1
  194. package/dist/shared/text-utils.d.ts +23 -0
  195. package/dist/shared/text-utils.d.ts.map +1 -0
  196. package/dist/shared/text-utils.js +66 -0
  197. package/dist/shared/text-utils.js.map +1 -0
  198. package/dist/shared/website-strategy-format.d.ts +18 -0
  199. package/dist/shared/website-strategy-format.d.ts.map +1 -0
  200. package/dist/shared/website-strategy-format.js +47 -0
  201. package/dist/shared/website-strategy-format.js.map +1 -0
  202. package/dist/state/index.d.ts +2 -0
  203. package/dist/state/index.d.ts.map +1 -1
  204. package/dist/state/index.js +57 -8
  205. package/dist/state/index.js.map +1 -1
  206. package/dist/types/consensus.d.ts +1 -0
  207. package/dist/types/consensus.d.ts.map +1 -1
  208. package/dist/types/consensus.js.map +1 -1
  209. package/dist/types/website-strategy.d.ts +1 -1
  210. package/dist/types/workflow.d.ts +447 -0
  211. package/dist/types/workflow.d.ts.map +1 -1
  212. package/dist/types/workflow.js +3 -0
  213. package/dist/types/workflow.js.map +1 -1
  214. package/dist/upgrade/handlers.d.ts.map +1 -1
  215. package/dist/upgrade/handlers.js +6 -3
  216. package/dist/upgrade/handlers.js.map +1 -1
  217. package/dist/workflow/consensus.d.ts.map +1 -1
  218. package/dist/workflow/consensus.js +1 -0
  219. package/dist/workflow/consensus.js.map +1 -1
  220. package/dist/workflow/website-strategy.d.ts.map +1 -1
  221. package/dist/workflow/website-strategy.js +2 -29
  222. package/dist/workflow/website-strategy.js.map +1 -1
  223. package/dist/workflow/website-updater.d.ts.map +1 -1
  224. package/dist/workflow/website-updater.js +3 -2
  225. package/dist/workflow/website-updater.js.map +1 -1
  226. package/package.json +1 -1
  227. package/src/adapters/gemini.ts +51 -6
  228. package/src/adapters/grok.ts +51 -6
  229. package/src/adapters/openai.ts +53 -5
  230. package/src/cli/commands/create.ts +1 -1
  231. package/src/cli/interactive.ts +333 -19
  232. package/src/generators/all.ts +3 -2
  233. package/src/generators/doc-parser.ts +75 -15
  234. package/src/generators/templates/fullstack.ts +1 -1
  235. package/src/generators/templates/website-components.ts +1 -1
  236. package/src/generators/templates/website-config.ts +23 -11
  237. package/src/generators/templates/website-conversion.ts +1 -1
  238. package/src/generators/templates/website-landing.ts +1 -1
  239. package/src/generators/templates/website-layout.ts +491 -23
  240. package/src/generators/templates/website-pricing.ts +1 -1
  241. package/src/generators/templates/website-sections.ts +1 -1
  242. package/src/generators/templates/website-seo.ts +4 -1
  243. package/src/generators/templates/website.ts +3 -0
  244. package/src/generators/website-content-ai.ts +186 -0
  245. package/src/generators/website-content-scanner.ts +113 -1
  246. package/src/generators/website-context.ts +151 -12
  247. package/src/generators/website-debug.ts +26 -0
  248. package/src/generators/website.ts +28 -3
  249. package/src/pipeline/auto-recovery.ts +283 -0
  250. package/src/pipeline/change-request.ts +63 -1
  251. package/src/pipeline/check-runner.ts +141 -2
  252. package/src/pipeline/command-resolver.ts +34 -2
  253. package/src/pipeline/consensus/arbitrator-query.ts +101 -0
  254. package/src/pipeline/consensus/consensus-runner.ts +1099 -42
  255. package/src/pipeline/cr-lifecycle.ts +103 -0
  256. package/src/pipeline/gate-engine.ts +35 -7
  257. package/src/pipeline/orchestrator.ts +361 -16
  258. package/src/pipeline/packets/consensus-packet-builder.ts +44 -18
  259. package/src/pipeline/phases/architecture.ts +6 -3
  260. package/src/pipeline/phases/audit.ts +6 -3
  261. package/src/pipeline/phases/consensus-architecture.ts +10 -1
  262. package/src/pipeline/phases/consensus-master-plan.ts +10 -3
  263. package/src/pipeline/phases/consensus-role-plans.ts +10 -1
  264. package/src/pipeline/phases/done.ts +15 -4
  265. package/src/pipeline/phases/intake.ts +7 -3
  266. package/src/pipeline/phases/phase-context.ts +6 -1
  267. package/src/pipeline/phases/production-gate.ts +41 -3
  268. package/src/pipeline/phases/qa-validation.ts +51 -5
  269. package/src/pipeline/phases/recovery-loop.ts +229 -7
  270. package/src/pipeline/phases/review.ts +73 -30
  271. package/src/pipeline/phases/role-planning.ts +21 -2
  272. package/src/pipeline/phases/stuck.ts +10 -0
  273. package/src/pipeline/repo-snapshot.ts +3 -0
  274. package/src/pipeline/role-execution-adapter.ts +30 -4
  275. package/src/pipeline/skill-loader.ts +33 -0
  276. package/src/pipeline/skills/coverage-gate.ts +199 -0
  277. package/src/pipeline/skills/usage-registry.ts +87 -0
  278. package/src/pipeline/strategy-context.ts +60 -0
  279. package/src/pipeline/type-defs/artifacts.ts +4 -0
  280. package/src/pipeline/type-defs/checks.ts +4 -0
  281. package/src/pipeline/type-defs/packets.ts +18 -1
  282. package/src/pipeline/type-defs/state.ts +26 -1
  283. package/src/shared/text-utils.ts +70 -0
  284. package/src/shared/website-strategy-format.ts +56 -0
  285. package/src/state/index.ts +60 -8
  286. package/src/types/consensus.ts +1 -0
  287. package/src/types/workflow.ts +6 -0
  288. package/src/upgrade/handlers.ts +9 -3
  289. package/src/workflow/consensus.ts +1 -0
  290. package/src/workflow/website-strategy.ts +2 -36
  291. package/src/workflow/website-updater.ts +4 -2
  292. package/tests/adapters/gemini.test.ts +165 -0
  293. package/tests/adapters/grok.test.ts +137 -0
  294. package/tests/adapters/openai.test.ts +128 -0
  295. package/tests/generators/doc-parser.test.ts +88 -9
  296. package/tests/generators/quality-gate.test.ts +19 -3
  297. package/tests/generators/website-components.test.ts +34 -0
  298. package/tests/generators/website-content-ai.test.ts +308 -0
  299. package/tests/generators/website-content-scanner.test.ts +86 -0
  300. package/tests/generators/website-context.test.ts +3 -2
  301. package/tests/integration/smokestack-scaffold.test.ts +385 -0
  302. package/tests/pipeline/auto-recovery.test.ts +337 -0
  303. package/tests/pipeline/change-request.test.ts +70 -0
  304. package/tests/pipeline/command-resolver.test.ts +42 -0
  305. package/tests/pipeline/consensus/arbitrator-query.test.ts +107 -0
  306. package/tests/pipeline/consensus-runner.test.ts +1333 -10
  307. package/tests/pipeline/consensus-scoring.test.ts +602 -18
  308. package/tests/pipeline/gate-engine.test.ts +34 -0
  309. package/tests/pipeline/install-check.test.ts +261 -0
  310. package/tests/pipeline/orchestrator.test.ts +1506 -15
  311. package/tests/pipeline/packets/builders.test.ts +29 -6
  312. package/tests/pipeline/phases/role-planning.strategy.test.ts +204 -0
  313. package/tests/pipeline/pipeline-persistence.test.ts +230 -0
  314. package/tests/pipeline/recovery-loop-guidance.test.ts +280 -0
  315. package/tests/pipeline/role-execution-adapter.test.ts +88 -0
  316. package/tests/pipeline/skills/coverage-gate.test.ts +370 -0
  317. package/tests/pipeline/skills/usage-registry.test.ts +114 -0
  318. package/tests/pipeline/strategy-context.test.ts +148 -0
  319. package/tests/shared/text-utils.test.ts +155 -0
  320. package/tests/state/progress-analysis.test.ts +375 -0
  321. package/tests/upgrade/handlers.test.ts +33 -2
  322. package/tests/workflow/consensus.test.ts +6 -0
  323. package/tsconfig.json +1 -1
@@ -178,4 +178,90 @@ export default function Page() {
178
178
  expect(result.filesScanned).toBe(0);
179
179
  expect(result.issues).toHaveLength(0);
180
180
  });
181
+
182
+ it('detects "coming soon" text as error', async () => {
183
+ await fs.writeFile(
184
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
185
+ `export default function Page() {
186
+ return <p>Documentation coming soon...</p>;
187
+ }
188
+ `,
189
+ );
190
+
191
+ const result = await scanGeneratedContent(tmpDir);
192
+
193
+ const comingSoonIssue = result.issues.find((i) => /coming soon/i.test(i.message));
194
+ expect(comingSoonIssue).toBeDefined();
195
+ expect(comingSoonIssue!.severity).toBe('error');
196
+ });
197
+
198
+ it('detects internal link to missing route as error', async () => {
199
+ // Create a page at /src/app/page.tsx (route: /)
200
+ await fs.writeFile(
201
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
202
+ `export default function Home() {
203
+ return <a href="/nonexistent">Broken Link</a>;
204
+ }
205
+ `,
206
+ );
207
+
208
+ const result = await scanGeneratedContent(tmpDir);
209
+
210
+ const linkIssue = result.issues.find((i) => /internal link.*nonexistent/i.test(i.message));
211
+ expect(linkIssue).toBeDefined();
212
+ expect(linkIssue!.severity).toBe('error');
213
+ });
214
+
215
+ it('passes for internal link to existing route', async () => {
216
+ // Create pages for / and /pricing
217
+ await fs.writeFile(
218
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
219
+ `export default function Home() {
220
+ return <a href="/pricing">Pricing</a>;
221
+ }
222
+ `,
223
+ );
224
+ await fs.mkdir(path.join(tmpDir, 'src', 'app', 'pricing'), { recursive: true });
225
+ await fs.writeFile(
226
+ path.join(tmpDir, 'src', 'app', 'pricing', 'page.tsx'),
227
+ `export default function Pricing() { return <div>Pricing</div>; }
228
+ `,
229
+ );
230
+
231
+ const result = await scanGeneratedContent(tmpDir);
232
+
233
+ const linkIssue = result.issues.find((i) => /internal link/i.test(i.message));
234
+ expect(linkIssue).toBeUndefined();
235
+ });
236
+
237
+ it('allows anchor links (/#features)', async () => {
238
+ await fs.writeFile(
239
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
240
+ `export default function Home() {
241
+ return <a href="/#features">Features</a>;
242
+ }
243
+ `,
244
+ );
245
+
246
+ const result = await scanGeneratedContent(tmpDir);
247
+
248
+ const linkIssue = result.issues.find((i) => /internal link/i.test(i.message));
249
+ expect(linkIssue).toBeUndefined();
250
+ });
251
+
252
+ it('ignores external links in internal link scan', async () => {
253
+ await fs.writeFile(
254
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
255
+ `export default function Home() {
256
+ return <div>No internal links here, just text</div>;
257
+ }
258
+ `,
259
+ );
260
+
261
+ const result = await scanGeneratedContent(tmpDir);
262
+
263
+ // No internal link issues should be reported
264
+ const linkIssue = result.issues.find((i) => /internal link/i.test(i.message));
265
+ expect(linkIssue).toBeUndefined();
266
+ });
181
267
  });
@@ -291,9 +291,10 @@ describe('validateWebsiteContextOrThrow', () => {
291
291
  expect(result.issues).toEqual([]);
292
292
  });
293
293
 
294
- it('fails when strategy is missing', () => {
294
+ it('does not fail when strategy is missing (strategy validated at injection time)', () => {
295
295
  const ctx: WebsiteContentContext = { ...baseContext, strategy: undefined };
296
- expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('strategy missing');
296
+ // Strategy absence is no longer a validation error — it's injected later by the pipeline
297
+ expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).not.toThrow();
297
298
  });
298
299
 
299
300
  it('fails when features are empty', () => {
@@ -0,0 +1,385 @@
1
+ /**
2
+ * SmokeStack E2E Scaffold Test
3
+ *
4
+ * Verifies that generateAllProject() produces a correct "all" (monorepo) project.
5
+ * Uses TEST.md requirements as acceptance criteria.
6
+ * Scaffold generation is 100% template-based (no API keys needed).
7
+ */
8
+
9
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
10
+ import { generateAllProject, validateAllProject } from '../../src/generators/all.js';
11
+ import type { GenerationResult } from '../../src/generators/python.js';
12
+ import os from 'node:os';
13
+ import fs from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import { execSync } from 'node:child_process';
16
+
17
+ let tmpDir: string;
18
+ let result: GenerationResult;
19
+ let projectDir: string;
20
+
21
+ beforeAll(async () => {
22
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-smokestack-'));
23
+ const spec = {
24
+ idea: 'Build an "all" monorepo called Popeye SmokeStack with 4 parts: backend (FastAPI), frontend (React+Vite+Tailwind), website (static marketing), and database with migrations. DB: create a messages table with id, text, created_at.',
25
+ name: 'popeye-smokestack',
26
+ language: 'all' as const,
27
+ openaiModel: 'gpt-4.1',
28
+ };
29
+ result = await generateAllProject(spec, tmpDir);
30
+ projectDir = result.projectDir;
31
+ }, 30_000);
32
+
33
+ afterAll(async () => {
34
+ if (tmpDir) await fs.rm(tmpDir, { recursive: true, force: true });
35
+ });
36
+
37
+ // --- Helper ---
38
+
39
+ async function exists(relativePath: string): Promise<boolean> {
40
+ try {
41
+ await fs.access(path.join(projectDir, relativePath));
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ async function readText(relativePath: string): Promise<string> {
49
+ return fs.readFile(path.join(projectDir, relativePath), 'utf-8');
50
+ }
51
+
52
+ async function parseJson(relativePath: string): Promise<unknown> {
53
+ const text = await readText(relativePath);
54
+ return JSON.parse(text);
55
+ }
56
+
57
+ async function listDir(relativePath: string): Promise<string[]> {
58
+ const entries = await fs.readdir(path.join(projectDir, relativePath));
59
+ return entries;
60
+ }
61
+
62
+ async function findFiles(dir: string, ext: string): Promise<string[]> {
63
+ const results: string[] = [];
64
+ async function walk(current: string): Promise<void> {
65
+ const entries = await fs.readdir(current, { withFileTypes: true });
66
+ for (const entry of entries) {
67
+ const fullPath = path.join(current, entry.name);
68
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
69
+ await walk(fullPath);
70
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
71
+ results.push(fullPath);
72
+ }
73
+ }
74
+ }
75
+ await walk(path.join(projectDir, dir));
76
+ return results;
77
+ }
78
+
79
+ // =====================================================================
80
+ // 1. Generation succeeds
81
+ // =====================================================================
82
+ describe('1. Generation succeeds', () => {
83
+ it('result.success is true', () => {
84
+ expect(result.success).toBe(true);
85
+ });
86
+
87
+ it('created more than 50 files', () => {
88
+ expect(result.filesCreated.length).toBeGreaterThan(50);
89
+ });
90
+
91
+ it('projectDir contains "popeye-smokestack"', () => {
92
+ expect(result.projectDir).toContain('popeye-smokestack');
93
+ });
94
+ });
95
+
96
+ // =====================================================================
97
+ // 2. Required structure exists (from TEST.md)
98
+ // =====================================================================
99
+ describe('2. Required structure exists', () => {
100
+ it('apps/backend/ directory exists', async () => {
101
+ expect(await exists('apps/backend')).toBe(true);
102
+ });
103
+
104
+ it('apps/frontend/ directory exists', async () => {
105
+ expect(await exists('apps/frontend')).toBe(true);
106
+ });
107
+
108
+ it('apps/website/ directory exists', async () => {
109
+ expect(await exists('apps/website')).toBe(true);
110
+ });
111
+
112
+ it('docker-compose.yml exists', async () => {
113
+ expect(await exists('docker-compose.yml')).toBe(true);
114
+ });
115
+
116
+ it('README.md exists', async () => {
117
+ expect(await exists('README.md')).toBe(true);
118
+ });
119
+
120
+ it('.env.example or env template exists', async () => {
121
+ const backendEnv = await exists('apps/backend/.env.example');
122
+ const frontendEnv = await exists('apps/frontend/.env.example');
123
+ expect(backendEnv || frontendEnv).toBe(true);
124
+ });
125
+
126
+ it('packages/ directory exists', async () => {
127
+ expect(await exists('packages')).toBe(true);
128
+ });
129
+
130
+ it('.popeye/workspace.json exists', async () => {
131
+ expect(await exists('.popeye/workspace.json')).toBe(true);
132
+ });
133
+
134
+ it('Backend: pyproject.toml exists', async () => {
135
+ expect(await exists('apps/backend/pyproject.toml')).toBe(true);
136
+ });
137
+
138
+ it('Frontend: package.json exists', async () => {
139
+ expect(await exists('apps/frontend/package.json')).toBe(true);
140
+ });
141
+
142
+ it('Website: package.json exists', async () => {
143
+ expect(await exists('apps/website/package.json')).toBe(true);
144
+ });
145
+
146
+ it('DB: migration files exist', async () => {
147
+ const alembicDir = await exists('apps/backend/alembic.ini');
148
+ const migrationsDir = await exists('apps/backend/migrations');
149
+ expect(alembicDir || migrationsDir).toBe(true);
150
+ });
151
+
152
+ it('DB: models.py exists', async () => {
153
+ // models.py under apps/backend/src/popeye_smokestack/database/
154
+ const pyFiles = await findFiles('apps/backend', '.py');
155
+ const hasModels = pyFiles.some((f) => f.includes('models.py'));
156
+ expect(hasModels).toBe(true);
157
+ });
158
+ });
159
+
160
+ // =====================================================================
161
+ // 3. No unplanned app directories
162
+ // =====================================================================
163
+ describe('3. No unplanned app directories', () => {
164
+ it('apps/ contains ONLY backend, frontend, website', async () => {
165
+ const entries = await listDir('apps');
166
+ const allowed = new Set(['backend', 'frontend', 'website']);
167
+ const extra = entries.filter((e) => !allowed.has(e));
168
+ expect(extra).toEqual([]);
169
+ });
170
+ });
171
+
172
+ // =====================================================================
173
+ // 4. Content alignment with TEST.md
174
+ // =====================================================================
175
+ describe('4. Content alignment', () => {
176
+ it('Root package.json contains project name', async () => {
177
+ const pkg = (await parseJson('package.json')) as Record<string, unknown>;
178
+ expect(JSON.stringify(pkg)).toContain('popeye-smokestack');
179
+ });
180
+
181
+ it('Backend main.py imports FastAPI', async () => {
182
+ const pyFiles = await findFiles('apps/backend', '.py');
183
+ const mainFile = pyFiles.find((f) => f.endsWith('main.py'));
184
+ expect(mainFile).toBeDefined();
185
+ const content = await fs.readFile(mainFile!, 'utf-8');
186
+ expect(content).toContain('FastAPI');
187
+ });
188
+
189
+ it('Backend has CORS setup', async () => {
190
+ const pyFiles = await findFiles('apps/backend', '.py');
191
+ const mainFile = pyFiles.find((f) => f.endsWith('main.py'));
192
+ expect(mainFile).toBeDefined();
193
+ const content = await fs.readFile(mainFile!, 'utf-8');
194
+ expect(content.toLowerCase()).toMatch(/cors/i);
195
+ });
196
+
197
+ it('Backend has health endpoint pattern', async () => {
198
+ const pyFiles = await findFiles('apps/backend', '.py');
199
+ let foundHealth = false;
200
+ for (const f of pyFiles) {
201
+ const content = await fs.readFile(f, 'utf-8');
202
+ if (content.includes('health') || content.includes('/health')) {
203
+ foundHealth = true;
204
+ break;
205
+ }
206
+ }
207
+ expect(foundHealth).toBe(true);
208
+ });
209
+
210
+ it('Frontend package.json has react dependency', async () => {
211
+ const pkg = (await parseJson('apps/frontend/package.json')) as Record<string, unknown>;
212
+ const allDeps = JSON.stringify(pkg);
213
+ expect(allDeps).toContain('react');
214
+ });
215
+
216
+ it('Frontend has vite dependency or vite.config', async () => {
217
+ const pkg = (await parseJson('apps/frontend/package.json')) as Record<string, unknown>;
218
+ const hasViteDep = JSON.stringify(pkg).includes('vite');
219
+ const hasViteConfig = await exists('apps/frontend/vite.config.ts');
220
+ expect(hasViteDep || hasViteConfig).toBe(true);
221
+ });
222
+
223
+ it('Frontend has tailwindcss dependency or tailwind.config', async () => {
224
+ const pkg = (await parseJson('apps/frontend/package.json')) as Record<string, unknown>;
225
+ const hasTailwindDep = JSON.stringify(pkg).includes('tailwindcss');
226
+ const hasTailwindConfig =
227
+ (await exists('apps/frontend/tailwind.config.ts')) ||
228
+ (await exists('apps/frontend/tailwind.config.js'));
229
+ expect(hasTailwindDep || hasTailwindConfig).toBe(true);
230
+ });
231
+
232
+ it('Website package.json has next dependency', async () => {
233
+ const pkg = (await parseJson('apps/website/package.json')) as Record<string, unknown>;
234
+ const allDeps = JSON.stringify(pkg);
235
+ expect(allDeps).toContain('next');
236
+ });
237
+
238
+ it('Docker compose has postgres service', async () => {
239
+ const content = await readText('docker-compose.yml');
240
+ expect(content.toLowerCase()).toContain('postgres');
241
+ });
242
+
243
+ it('Env template has DATABASE_URL', async () => {
244
+ const content = await readText('apps/backend/.env.example');
245
+ expect(content).toContain('DATABASE_URL');
246
+ });
247
+ });
248
+
249
+ // =====================================================================
250
+ // 5. Syntax validation (fast, no install)
251
+ // =====================================================================
252
+ describe('5. Syntax validation', () => {
253
+ it('All package.json files are valid JSON', async () => {
254
+ const pkgFiles = [
255
+ 'package.json',
256
+ 'apps/frontend/package.json',
257
+ 'apps/website/package.json',
258
+ 'packages/design-tokens/package.json',
259
+ 'packages/ui/package.json',
260
+ ];
261
+ for (const f of pkgFiles) {
262
+ const text = await readText(f);
263
+ expect(() => JSON.parse(text), `Invalid JSON in ${f}`).not.toThrow();
264
+ }
265
+ });
266
+
267
+ it('All tsconfig.json files are valid JSON', async () => {
268
+ const tsconfigPaths = [
269
+ 'apps/frontend/tsconfig.json',
270
+ 'apps/website/tsconfig.json',
271
+ ];
272
+ for (const f of tsconfigPaths) {
273
+ if (await exists(f)) {
274
+ const text = await readText(f);
275
+ expect(() => JSON.parse(text), `Invalid JSON in ${f}`).not.toThrow();
276
+ }
277
+ }
278
+ });
279
+
280
+ it('docker-compose.yml has valid basic YAML structure', async () => {
281
+ const content = await readText('docker-compose.yml');
282
+ // Basic YAML structure check: must have services key
283
+ expect(content).toMatch(/^services:/m);
284
+ });
285
+
286
+ it('.popeye/workspace.json is valid JSON', async () => {
287
+ const text = await readText('.popeye/workspace.json');
288
+ expect(() => JSON.parse(text)).not.toThrow();
289
+ });
290
+ });
291
+
292
+ // =====================================================================
293
+ // 6. Build / Compilation verification
294
+ // =====================================================================
295
+ describe('6. Build verification', () => {
296
+ describe('Backend (Python syntax)', () => {
297
+ it('All .py files compile syntactically', async () => {
298
+ const pyFiles = await findFiles('apps/backend', '.py');
299
+ expect(pyFiles.length).toBeGreaterThan(0);
300
+
301
+ const failures: string[] = [];
302
+ for (const f of pyFiles) {
303
+ try {
304
+ execSync(
305
+ `python3 -c "compile(open('${f}').read(), '${f}', 'exec')"`,
306
+ { timeout: 10_000, stdio: 'pipe' }
307
+ );
308
+ } catch (e: unknown) {
309
+ const msg = e instanceof Error ? e.message : String(e);
310
+ failures.push(`${path.relative(projectDir, f)}: ${msg.split('\n')[0]}`);
311
+ }
312
+ }
313
+ expect(failures, `Python syntax errors:\n${failures.join('\n')}`).toEqual([]);
314
+ });
315
+ });
316
+
317
+ // Workspace install: run npm install from root so all workspace packages resolve together
318
+ describe('Workspace npm install', () => {
319
+ it('npm install from root succeeds', async () => {
320
+ execSync('npm install --ignore-scripts', {
321
+ cwd: projectDir,
322
+ timeout: 180_000,
323
+ stdio: 'pipe',
324
+ });
325
+ const feModules = await exists('apps/frontend/node_modules');
326
+ const wsModules = await exists('apps/website/node_modules');
327
+ // npm workspaces hoists to root; app dirs may or may not have node_modules
328
+ const rootModules = await exists('node_modules');
329
+ expect(rootModules || feModules || wsModules).toBe(true);
330
+ }, 200_000);
331
+ });
332
+
333
+ describe('Frontend (React+Vite+Tailwind)', () => {
334
+ it('TypeScript compiles (tsc --noEmit)', async () => {
335
+ const frontendDir = path.join(projectDir, 'apps', 'frontend');
336
+ try {
337
+ execSync('npx tsc --noEmit', {
338
+ cwd: frontendDir,
339
+ timeout: 60_000,
340
+ stdio: 'pipe',
341
+ });
342
+ } catch (e: unknown) {
343
+ const stderr =
344
+ e && typeof e === 'object' && 'stderr' in e ? String((e as { stderr: unknown }).stderr) : '';
345
+ const stdout =
346
+ e && typeof e === 'object' && 'stdout' in e ? String((e as { stdout: unknown }).stdout) : '';
347
+ throw new Error(`Frontend tsc failed:\n${stdout}\n${stderr}`);
348
+ }
349
+ }, 70_000);
350
+ });
351
+
352
+ describe('Website (Next.js)', () => {
353
+ it('TypeScript compiles (tsc --noEmit)', async () => {
354
+ const websiteDir = path.join(projectDir, 'apps', 'website');
355
+ try {
356
+ execSync('npx tsc --noEmit', {
357
+ cwd: websiteDir,
358
+ timeout: 60_000,
359
+ stdio: 'pipe',
360
+ });
361
+ } catch (e: unknown) {
362
+ const stderr =
363
+ e && typeof e === 'object' && 'stderr' in e ? String((e as { stderr: unknown }).stderr) : '';
364
+ const stdout =
365
+ e && typeof e === 'object' && 'stdout' in e ? String((e as { stdout: unknown }).stdout) : '';
366
+ throw new Error(`Website tsc failed:\n${stdout}\n${stderr}`);
367
+ }
368
+ }, 70_000);
369
+ });
370
+ });
371
+
372
+ // =====================================================================
373
+ // 7. Existing validateAllProject() (defense-in-depth)
374
+ // =====================================================================
375
+ describe('7. validateAllProject()', () => {
376
+ it('validation passes', async () => {
377
+ const validation = await validateAllProject(projectDir);
378
+ expect(validation.valid).toBe(true);
379
+ });
380
+
381
+ it('missingFiles is empty', async () => {
382
+ const validation = await validateAllProject(projectDir);
383
+ expect(validation.missingFiles).toEqual([]);
384
+ });
385
+ });