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
@@ -22,6 +22,9 @@ CONCERNS:
22
22
  - No deployment plan
23
23
  - Security considerations not addressed
24
24
 
25
+ BLOCKING_ISSUES:
26
+ - None
27
+
25
28
  RECOMMENDATIONS:
26
29
  - Add error handling middleware
27
30
  - Include deployment configuration
@@ -37,6 +40,7 @@ CONSENSUS: 85%
37
40
  expect(result.analysis).toContain('well-structured plan');
38
41
  expect(result.strengths).toContain('Clear project structure');
39
42
  expect(result.concerns).toContain('Missing error handling strategy');
43
+ expect(result.blockingIssues).toEqual([]);
40
44
  expect(result.recommendations).toContain('Add error handling middleware');
41
45
  expect(result.rawResponse).toBe(response);
42
46
  });
@@ -52,6 +56,9 @@ STRENGTHS:
52
56
  CONCERNS:
53
57
  - None
54
58
 
59
+ BLOCKING_ISSUES:
60
+ - None
61
+
55
62
  RECOMMENDATIONS:
56
63
  - None needed
57
64
 
@@ -62,6 +69,7 @@ CONSENSUS: 98%
62
69
 
63
70
  expect(result.score).toBe(98);
64
71
  expect(result.approved).toBe(true);
72
+ expect(result.blockingIssues).toEqual([]);
65
73
  });
66
74
 
67
75
  it('should handle missing sections gracefully', () => {
@@ -78,6 +86,7 @@ CONSENSUS: 70%
78
86
  expect(result.analysis).toBe('');
79
87
  expect(result.strengths).toEqual([]);
80
88
  expect(result.concerns).toEqual([]);
89
+ expect(result.blockingIssues).toEqual([]);
81
90
  });
82
91
 
83
92
  it('should handle missing score', () => {
@@ -110,6 +119,9 @@ CONCERNS:
110
119
  1. First concern
111
120
  2. Second concern
112
121
 
122
+ BLOCKING_ISSUES:
123
+ - None
124
+
113
125
  RECOMMENDATIONS:
114
126
  - Recommendation one
115
127
  - Recommendation two
@@ -121,6 +133,7 @@ CONSENSUS: 80%
121
133
 
122
134
  expect(result.strengths).toHaveLength(4);
123
135
  expect(result.concerns).toHaveLength(2);
136
+ expect(result.blockingIssues).toEqual([]);
124
137
  expect(result.recommendations).toHaveLength(2);
125
138
  });
126
139
 
@@ -142,4 +155,119 @@ CONSENSUS: 80%
142
155
  expect(parseConsensusResponse('CONSENSUS: 0%').score).toBe(0);
143
156
  expect(parseConsensusResponse('CONSENSUS: 100%').score).toBe(100);
144
157
  });
158
+
159
+ it('should parse blocking issues as separate field', () => {
160
+ const response = `
161
+ ANALYSIS: Good plan.
162
+
163
+ STRENGTHS:
164
+ - Solid architecture
165
+
166
+ CONCERNS:
167
+ - Consider adding caching
168
+
169
+ BLOCKING_ISSUES:
170
+ - Missing authentication flow
171
+ - No database migration strategy
172
+
173
+ RECOMMENDATIONS:
174
+ - Add caching layer
175
+
176
+ CONSENSUS: 72%
177
+ `;
178
+ const result = parseConsensusResponse(response);
179
+ expect(result.blockingIssues).toEqual([
180
+ 'Missing authentication flow',
181
+ 'No database migration strategy',
182
+ ]);
183
+ expect(result.concerns).toEqual(['Consider adding caching']);
184
+ });
185
+
186
+ it('should return empty blocking issues when "None"', () => {
187
+ const response = `
188
+ ANALYSIS: Good plan.
189
+
190
+ STRENGTHS:
191
+ - Solid
192
+
193
+ CONCERNS:
194
+ - Minor naming issues in the module structure
195
+
196
+ BLOCKING_ISSUES:
197
+ - None
198
+
199
+ RECOMMENDATIONS:
200
+ - Improve module names
201
+
202
+ CONSENSUS: 95%
203
+ `;
204
+ const result = parseConsensusResponse(response);
205
+ expect(result.blockingIssues).toEqual([]);
206
+ expect(result.approved).toBe(true);
207
+ });
208
+
209
+ it('should filter none-variant blocking issues like "No blocking issues found"', () => {
210
+ const response = `
211
+ ANALYSIS: Good plan.
212
+
213
+ STRENGTHS:
214
+ - Solid architecture
215
+
216
+ CONCERNS:
217
+ - Minor naming issues
218
+
219
+ BLOCKING_ISSUES:
220
+ - No blocking issues found
221
+
222
+ RECOMMENDATIONS:
223
+ - Improve naming
224
+
225
+ CONSENSUS: 92%
226
+ `;
227
+ const result = parseConsensusResponse(response);
228
+ expect(result.blockingIssues).toEqual([]);
229
+ });
230
+
231
+ it('should filter "None identified" from blocking issues', () => {
232
+ const response = `
233
+ ANALYSIS: Solid plan overall.
234
+
235
+ STRENGTHS:
236
+ - Good design choices
237
+
238
+ CONCERNS:
239
+ - Consider caching
240
+
241
+ BLOCKING_ISSUES:
242
+ - None identified
243
+
244
+ RECOMMENDATIONS:
245
+ - Add caching layer
246
+
247
+ CONSENSUS: 90%
248
+ `;
249
+ const result = parseConsensusResponse(response);
250
+ expect(result.blockingIssues).toEqual([]);
251
+ });
252
+
253
+ it('should handle missing BLOCKING_ISSUES section (backward compat)', () => {
254
+ // Old-format response without BLOCKING_ISSUES section
255
+ const response = `
256
+ ANALYSIS: Good plan.
257
+
258
+ STRENGTHS:
259
+ - Fine overall structure
260
+
261
+ CONCERNS:
262
+ - Some concern about performance
263
+
264
+ RECOMMENDATIONS:
265
+ - A recommendation for optimization
266
+
267
+ CONSENSUS: 90%
268
+ `;
269
+ const result = parseConsensusResponse(response);
270
+ expect(result.blockingIssues).toEqual([]);
271
+ expect(result.concerns).toHaveLength(1);
272
+ });
145
273
  });
@@ -104,18 +104,97 @@ describe('isSuspiciousProductName', () => {
104
104
  });
105
105
 
106
106
  describe('extractPricing', () => {
107
- it('extracts pricing tiers from markdown table', () => {
107
+ it('extracts pricing tiers from markdown table with provenance', () => {
108
108
  const docs = `## Pricing\n| Plan | Price |\n|---|---|\n| Free | Free |\n| Pro | $99/month minimum |\n| Enterprise | Custom pricing |`;
109
- const tiers = extractPricing(docs);
110
- expect(tiers).toBeDefined();
111
- expect(tiers!.length).toBe(3);
112
- expect(tiers![0].name).toContain('Free');
113
- expect(tiers![1].price).toBe('$99');
114
- expect(tiers![2].price).toBe('Custom');
109
+ const result = extractPricing(docs);
110
+ expect(result.source).toBe('docs');
111
+ expect(result.tiers.length).toBe(3);
112
+ expect(result.tiers[0].name).toContain('Free');
113
+ expect(result.tiers[1].price).toBe('$99');
114
+ expect(result.tiers[2].price).toBe('Custom');
115
+ expect(result.evidence).toBeDefined();
116
+ expect(result.evidence!.extractionMethod).toBe('known_plan_names');
117
+ expect(result.evidence!.matchedRows).toBe(3);
115
118
  });
116
119
 
117
- it('returns undefined when no pricing found', () => {
120
+ it('returns source none when no pricing found', () => {
118
121
  const docs = '# Product\nJust a product.';
119
- expect(extractPricing(docs)).toBeUndefined();
122
+ const result = extractPricing(docs);
123
+ expect(result.source).toBe('none');
124
+ expect(result.tiers).toEqual([]);
125
+ expect(result.evidence).toBeUndefined();
126
+ });
127
+
128
+ it('extracts Gateco-style pricing with emoji-prefixed rows and <br> tags', () => {
129
+ const docs = [
130
+ '## Pricing Overview',
131
+ '| Plan | Price | Users |',
132
+ '|---|---|---|',
133
+ '| Free | Free | Up to 5 |',
134
+ '| Pro | $99/month<br>minimum | Unlimited |',
135
+ '| Enterprise | Custom | Unlimited |',
136
+ ].join('\n');
137
+ const result = extractPricing(docs);
138
+ expect(result.source).toBe('docs');
139
+ expect(result.tiers.length).toBe(3);
140
+ expect(result.tiers[0].name).toBe('Free');
141
+ expect(result.tiers[0].price).toBe('Free');
142
+ expect(result.tiers[1].name).toBe('Pro');
143
+ expect(result.tiers[1].price).toBe('$99');
144
+ expect(result.tiers[2].name).toBe('Enterprise');
145
+ expect(result.tiers[2].price).toBe('Custom');
146
+ });
147
+
148
+ it('falls back to table_fallback for nonstandard plan names', () => {
149
+ const docs = [
150
+ '## Pricing',
151
+ '| Plan | Price |',
152
+ '|---|---|',
153
+ '| Hobby | Free |',
154
+ '| Scale | $49/mo |',
155
+ '| Organization | Custom |',
156
+ ].join('\n');
157
+ const result = extractPricing(docs);
158
+ expect(result.source).toBe('docs');
159
+ expect(result.tiers.length).toBe(3);
160
+ expect(result.evidence!.extractionMethod).toBe('table_fallback');
161
+ expect(result.tiers[0].name).toBe('Hobby');
162
+ expect(result.tiers[1].name).toBe('Scale');
163
+ expect(result.tiers[2].name).toBe('Organization');
164
+ });
165
+
166
+ it('detects price in 3rd column via header scan', () => {
167
+ const docs = [
168
+ '## Pricing',
169
+ '| Plan | Features | Price |',
170
+ '|---|---|---|',
171
+ '| Hobby | 5 projects | Free |',
172
+ '| Scale | Unlimited | $49/mo |',
173
+ ].join('\n');
174
+ const result = extractPricing(docs);
175
+ expect(result.source).toBe('docs');
176
+ expect(result.tiers.length).toBe(2);
177
+ expect(result.tiers[0].price).toBe('Free');
178
+ expect(result.tiers[1].price).toBe('$49');
179
+ });
180
+
181
+ it('skips separator rows and header-like rows in fallback', () => {
182
+ const docs = [
183
+ '## Pricing',
184
+ '| Plan | Price |',
185
+ '|---|---|',
186
+ '| Plan | $10 |', // header-like row, should skip
187
+ '| Hobby | Free |',
188
+ ].join('\n');
189
+ const result = extractPricing(docs);
190
+ // "Plan" row should be skipped by the /^(Plan|Tier|Name|Feature)/i filter
191
+ expect(result.tiers.every((t) => t.name !== 'Plan')).toBe(true);
192
+ });
193
+
194
+ it('returns empty tiers for pricing section with no table', () => {
195
+ const docs = '## Pricing\nContact us for pricing details.';
196
+ const result = extractPricing(docs);
197
+ expect(result.source).toBe('none');
198
+ expect(result.tiers).toEqual([]);
120
199
  });
121
200
  });
@@ -75,7 +75,7 @@ describe('validateWebsiteContextOrThrow', () => {
75
75
  });
76
76
 
77
77
  it('still throws when passed is false from validateWebsiteContext', () => {
78
- // Context with multiple blocking issues
78
+ // Context with multiple blocking issues (strategy absence is not a factor)
79
79
  const context = makeContext({
80
80
  productName: 'my-app',
81
81
  rawDocs: '',
@@ -87,6 +87,13 @@ describe('validateWebsiteContextOrThrow', () => {
87
87
  /Website generation blocked/
88
88
  );
89
89
  });
90
+
91
+ it('does not throw on missing strategy alone', () => {
92
+ // Strategy absence should never cause validation failure
93
+ const context = makeContext({ strategy: undefined });
94
+
95
+ expect(() => validateWebsiteContextOrThrow(context, 'gateco')).not.toThrow();
96
+ });
90
97
  });
91
98
 
92
99
  describe('validateWebsiteContext (soft mode)', () => {
@@ -168,7 +175,7 @@ describe('validateWebsiteContext (soft mode)', () => {
168
175
  expect(result.warnings.some((w) => /description/i.test(w))).toBe(true);
169
176
  });
170
177
 
171
- it('clamps score to 0 when everything is wrong', () => {
178
+ it('clamps score to floor when everything is wrong', () => {
172
179
  const context: WebsiteContentContext = {
173
180
  productName: 'my-app',
174
181
  features: [],
@@ -177,7 +184,16 @@ describe('validateWebsiteContext (soft mode)', () => {
177
184
  };
178
185
  const result = validateWebsiteContext(context, 'my-app');
179
186
 
180
- expect(result.contentScore).toBe(0);
187
+ // Score is low but no longer 0 since strategy penalty (-15) was removed
188
+ expect(result.contentScore).toBeLessThanOrEqual(15);
181
189
  expect(result.passed).toBe(false);
182
190
  });
191
+
192
+ it('does not emit strategy-related issues when strategy is undefined', () => {
193
+ const context = makeContext({ strategy: undefined });
194
+ const result = validateWebsiteContext(context, 'gateco');
195
+
196
+ // Strategy absence should not produce any issues or warnings about strategy
197
+ expect(result.issues.some((i) => /strategy/i.test(i))).toBe(false);
198
+ });
183
199
  });
@@ -9,6 +9,7 @@ import {
9
9
  generateWebsiteFooter,
10
10
  generateWebsiteNavigation,
11
11
  } from '../../src/generators/templates/website-components.js';
12
+ import { getWebsiteProjectFiles } from '../../src/generators/website.js';
12
13
  import type { WebsiteContentContext } from '../../src/generators/website-context.js';
13
14
  import type { WebsiteStrategyDocument } from '../../src/types/website-strategy.js';
14
15
 
@@ -132,6 +133,39 @@ describe('generateWebsiteFooter', () => {
132
133
  expect(footer).toContain('Resources');
133
134
  expect(footer).toContain('Legal');
134
135
  });
136
+
137
+ it('all default footer hrefs have corresponding generated pages', () => {
138
+ const footer = generateWebsiteFooter('deploy-ai', contextNoLogo);
139
+ const generatedFiles = getWebsiteProjectFiles('deploy-ai');
140
+
141
+ // Extract all href values from the default footer
142
+ const hrefPattern = /href:\s*'([^']+)'/g;
143
+ let match;
144
+ const hrefs: string[] = [];
145
+ while ((match = hrefPattern.exec(footer)) !== null) {
146
+ hrefs.push(match[1]);
147
+ }
148
+
149
+ // Build set of generated page routes from file paths
150
+ const pageRoutes = new Set<string>();
151
+ for (const file of generatedFiles) {
152
+ const pageMatch = file.match(/^src\/app(.*)\/page\.tsx$/);
153
+ if (pageMatch) {
154
+ pageRoutes.add(pageMatch[1] || '/');
155
+ }
156
+ }
157
+ // Also add root route
158
+ if (generatedFiles.includes('src/app/page.tsx')) {
159
+ pageRoutes.add('/');
160
+ }
161
+
162
+ // Every non-anchor internal href should correspond to a generated page
163
+ for (const href of hrefs) {
164
+ if (href.startsWith('/#')) continue; // Anchor links are fine
165
+ if (!href.startsWith('/')) continue; // External links are fine
166
+ expect(pageRoutes.has(href)).toBe(true);
167
+ }
168
+ });
135
169
  });
136
170
 
137
171
  describe('generateWebsiteNavigation', () => {
@@ -0,0 +1,308 @@
1
+ /**
2
+ * AI Website Content Generator tests — pricing guard, evidence detection,
3
+ * doc path extraction, and per-field AI fallback.
4
+ */
5
+
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { hasPricingEvidence } from '../../src/generators/website-content-ai.js';
8
+ import {
9
+ extractDocPathsFromText,
10
+ validateAndFilterDocPaths,
11
+ validateWebsiteContext,
12
+ } from '../../src/generators/website-context.js';
13
+ import type { WebsiteContentContext } from '../../src/generators/website-context.js';
14
+
15
+ describe('website-content-ai', () => {
16
+ describe('hasPricingEvidence', () => {
17
+ it('should detect dollar amounts in docs', () => {
18
+ expect(hasPricingEvidence('Plans start at $29/mo', '')).toBe(true);
19
+ });
20
+
21
+ it('should detect euro amounts', () => {
22
+ expect(hasPricingEvidence('Starting from €19/month', '')).toBe(true);
23
+ });
24
+
25
+ it('should detect pricing keywords', () => {
26
+ expect(hasPricingEvidence('Our pricing model includes three tiers', '')).toBe(true);
27
+ });
28
+
29
+ it('should detect subscription keywords', () => {
30
+ expect(hasPricingEvidence('Monthly subscription available', '')).toBe(true);
31
+ });
32
+
33
+ it('should detect free plan/tier', () => {
34
+ expect(hasPricingEvidence('We offer a free tier for developers', '')).toBe(true);
35
+ });
36
+
37
+ it('should detect evidence in specification', () => {
38
+ expect(hasPricingEvidence('', 'Pricing plan: $49/mo')).toBe(true);
39
+ });
40
+
41
+ it('should return false for docs without pricing info', () => {
42
+ expect(hasPricingEvidence(
43
+ 'Our product helps you build amazing software with AI.',
44
+ 'A task management tool for teams.',
45
+ )).toBe(false);
46
+ });
47
+
48
+ it('should return false for empty inputs', () => {
49
+ expect(hasPricingEvidence('', '')).toBe(false);
50
+ });
51
+ });
52
+
53
+ describe('pricing guard edge cases', () => {
54
+ it('should detect /yr pattern', () => {
55
+ expect(hasPricingEvidence('$299/yr for annual plans', '')).toBe(true);
56
+ });
57
+
58
+ it('should detect per month even without dollar sign', () => {
59
+ // Reason: "per month" is a strong pricing signal regardless of currency symbol
60
+ expect(hasPricingEvidence('10 per month for basic plan', '')).toBe(true);
61
+ expect(hasPricingEvidence('$10 per month for basic plan', '')).toBe(true);
62
+ });
63
+
64
+ it('should detect USD pattern', () => {
65
+ expect(hasPricingEvidence('USD 29 per month', '')).toBe(true);
66
+ });
67
+ });
68
+
69
+ describe('extractDocPathsFromText', () => {
70
+ it('should extract macOS paths with spaces (quoted)', () => {
71
+ const text = `Build a website for Gateco using '/Users/a b/Gateco/pricing.md'`;
72
+ const paths = extractDocPathsFromText(text);
73
+ expect(paths).toContain('/Users/a b/Gateco/pricing.md');
74
+ });
75
+
76
+ it('should extract macOS paths without quotes', () => {
77
+ const text = 'Build a website using /Users/me/Gateco/spec.md please';
78
+ const paths = extractDocPathsFromText(text);
79
+ expect(paths).toContain('/Users/me/Gateco/spec.md');
80
+ });
81
+
82
+ it('should extract Windows paths with backslashes (quoted)', () => {
83
+ const text = `Use "C:\\Users\\me\\Gateco\\pricing.md" for content`;
84
+ const paths = extractDocPathsFromText(text);
85
+ expect(paths).toContain('C:\\Users\\me\\Gateco\\pricing.md');
86
+ });
87
+
88
+ it('should extract Windows paths with forward slashes', () => {
89
+ const text = `Docs at 'D:/docs/spec.md'`;
90
+ const paths = extractDocPathsFromText(text);
91
+ expect(paths).toContain('D:/docs/spec.md');
92
+ });
93
+
94
+ it('should extract multiple paths from mixed text', () => {
95
+ const text = `Build with '/Users/me/spec.md' and "/Users/me/pricing.md"`;
96
+ const paths = extractDocPathsFromText(text);
97
+ expect(paths).toContain('/Users/me/spec.md');
98
+ expect(paths).toContain('/Users/me/pricing.md');
99
+ expect(paths).toHaveLength(2);
100
+ });
101
+
102
+ it('should deduplicate paths', () => {
103
+ const text = `Use '/Users/me/spec.md' and '/Users/me/spec.md' twice`;
104
+ const paths = extractDocPathsFromText(text);
105
+ expect(paths).toHaveLength(1);
106
+ });
107
+
108
+ it('should accept .mdx and .txt extensions', () => {
109
+ const text = `Read '/docs/spec.mdx' and '/docs/notes.txt'`;
110
+ const paths = extractDocPathsFromText(text);
111
+ expect(paths).toContain('/docs/spec.mdx');
112
+ expect(paths).toContain('/docs/notes.txt');
113
+ });
114
+
115
+ it('should reject .pdf and .docx extensions', () => {
116
+ const text = `Read '/docs/spec.pdf' and '/docs/notes.docx'`;
117
+ const paths = extractDocPathsFromText(text);
118
+ expect(paths).toHaveLength(0);
119
+ });
120
+
121
+ it('should return empty for text without paths', () => {
122
+ const text = 'Build a website for my project';
123
+ const paths = extractDocPathsFromText(text);
124
+ expect(paths).toHaveLength(0);
125
+ });
126
+
127
+ it('should handle paths at end of string', () => {
128
+ const text = 'Build from /Users/me/spec.md';
129
+ const paths = extractDocPathsFromText(text);
130
+ expect(paths).toContain('/Users/me/spec.md');
131
+ });
132
+ });
133
+
134
+ describe('validateAndFilterDocPaths', () => {
135
+ it('should reject unsupported extensions with log', async () => {
136
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
137
+ const result = await validateAndFilterDocPaths(['/tmp/doc.pdf']);
138
+ expect(result).toHaveLength(0);
139
+ expect(logSpy).toHaveBeenCalledWith(
140
+ expect.stringContaining('unsupported format'),
141
+ );
142
+ logSpy.mockRestore();
143
+ });
144
+
145
+ it('should skip non-existent files with log', async () => {
146
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
147
+ const result = await validateAndFilterDocPaths(['/tmp/nonexistent-abc-123.md']);
148
+ expect(result).toHaveLength(0);
149
+ expect(logSpy).toHaveBeenCalledWith(
150
+ expect.stringContaining('file not found'),
151
+ );
152
+ logSpy.mockRestore();
153
+ });
154
+
155
+ it('should accept allowed extensions for existing files', async () => {
156
+ // Reason: uses node:fs/promises, so we mock stat
157
+ const { promises: fsp } = await import('node:fs');
158
+ const statSpy = vi.spyOn(fsp, 'stat').mockResolvedValue({ size: 1024 } as any);
159
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
160
+
161
+ const result = await validateAndFilterDocPaths(['/tmp/test.md']);
162
+ expect(result).toEqual(['/tmp/test.md']);
163
+ expect(logSpy).toHaveBeenCalledWith(
164
+ expect.stringContaining('[doc-ingest] Read:'),
165
+ );
166
+
167
+ statSpy.mockRestore();
168
+ logSpy.mockRestore();
169
+ });
170
+
171
+ it('should skip oversized files', async () => {
172
+ const { promises: fsp } = await import('node:fs');
173
+ const statSpy = vi.spyOn(fsp, 'stat').mockResolvedValue({
174
+ size: 3 * 1024 * 1024, // 3MB > 2MB limit
175
+ } as any);
176
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
177
+
178
+ const result = await validateAndFilterDocPaths(['/tmp/huge.md']);
179
+ expect(result).toHaveLength(0);
180
+ expect(logSpy).toHaveBeenCalledWith(
181
+ expect.stringContaining('exceeds 2MB limit'),
182
+ );
183
+
184
+ statSpy.mockRestore();
185
+ logSpy.mockRestore();
186
+ });
187
+ });
188
+
189
+ describe('per-field AI fallback', () => {
190
+ it('should treat pricing: [] as missing (empty array)', () => {
191
+ // Reason: the fix changes `!context.pricing` to `context.pricing == null || context.pricing.length === 0`
192
+ const emptyPricing: any[] = [];
193
+ const isMissing = emptyPricing == null || emptyPricing.length === 0;
194
+ expect(isMissing).toBe(true);
195
+ });
196
+
197
+ it('should treat pricing: undefined as missing', () => {
198
+ const undefinedPricing = undefined;
199
+ const isMissing = undefinedPricing == null || (undefinedPricing as any)?.length === 0;
200
+ expect(isMissing).toBe(true);
201
+ });
202
+
203
+ it('should not treat populated pricing as missing', () => {
204
+ const pricing = [{ name: 'Pro', price: '$29', description: '', features: [], cta: 'Buy' }];
205
+ const isMissing = pricing == null || pricing.length === 0;
206
+ expect(isMissing).toBe(false);
207
+ });
208
+
209
+ it('should trigger fallback when only pricing is missing', () => {
210
+ // Reason: verifies per-field logic — tagline exists but pricing is empty
211
+ const context = {
212
+ tagline: 'Existing tagline',
213
+ description: 'Existing description',
214
+ features: [{ title: 'A', description: 'B' }],
215
+ pricing: [] as any[],
216
+ };
217
+ const pricingMissing = context.pricing == null || context.pricing.length === 0;
218
+ const hasMissingContent = !context.tagline || !context.description || context.features.length === 0 || pricingMissing;
219
+ expect(hasMissingContent).toBe(true);
220
+ expect(pricingMissing).toBe(true);
221
+ });
222
+
223
+ it('should not trigger fallback when all fields are present', () => {
224
+ const context = {
225
+ tagline: 'Tag',
226
+ description: 'Desc',
227
+ features: [{ title: 'A', description: 'B' }],
228
+ pricing: [{ name: 'Free', price: '$0', description: '', features: [], cta: 'Start' }],
229
+ };
230
+ const pricingMissing = context.pricing == null || context.pricing.length === 0;
231
+ const hasMissingContent = !context.tagline || !context.description || context.features.length === 0 || pricingMissing;
232
+ expect(hasMissingContent).toBe(false);
233
+ });
234
+ });
235
+
236
+ describe('validator provenance-based pricing check', () => {
237
+ it('should NOT warn about default pricing when source is docs', () => {
238
+ const context: WebsiteContentContext = {
239
+ productName: 'Gateco',
240
+ features: [{ title: 'Auth', description: 'Auth feature' }],
241
+ rawDocs: '# Gateco\nSome docs content here for minimum length requirement...',
242
+ pricing: [
243
+ { name: 'Free', price: 'Free', description: '', features: [], cta: 'Get started' },
244
+ { name: 'Pro', price: '$99', description: '', features: [], cta: 'Start free trial' },
245
+ { name: 'Enterprise', price: 'Custom', description: '', features: [], cta: 'Contact sales' },
246
+ ],
247
+ pricingDiagnostics: {
248
+ charsScanned: 5000,
249
+ foundPricingHeader: true,
250
+ extractionMethod: 'known_plan_names',
251
+ extractedTiers: [
252
+ { name: 'Free', price: 'Free' },
253
+ { name: 'Pro', price: '$99' },
254
+ { name: 'Enterprise', price: 'Custom' },
255
+ ],
256
+ source: 'docs',
257
+ },
258
+ };
259
+ const result = validateWebsiteContext(context, 'Gateco');
260
+ const defaultWarning = result.warnings.find((w) => w.includes('default values'));
261
+ expect(defaultWarning).toBeUndefined();
262
+ });
263
+
264
+ it('should NOT warn about default pricing when source is ai', () => {
265
+ const context: WebsiteContentContext = {
266
+ productName: 'TestApp',
267
+ features: [{ title: 'Feature', description: 'Desc' }],
268
+ rawDocs: '# TestApp\nSome docs content here for minimum length...',
269
+ pricing: [
270
+ { name: 'Starter', price: '$0', description: '', features: [], cta: 'Get started' },
271
+ { name: 'Pro', price: '$29', description: '', features: [], cta: 'Start' },
272
+ { name: 'Enterprise', price: 'Custom', description: '', features: [], cta: 'Contact' },
273
+ ],
274
+ pricingDiagnostics: {
275
+ charsScanned: 3000,
276
+ foundPricingHeader: false,
277
+ extractionMethod: 'ai',
278
+ extractedTiers: [
279
+ { name: 'Starter', price: '$0' },
280
+ { name: 'Pro', price: '$29' },
281
+ { name: 'Enterprise', price: 'Custom' },
282
+ ],
283
+ source: 'ai',
284
+ },
285
+ };
286
+ const result = validateWebsiteContext(context, 'TestApp');
287
+ const defaultWarning = result.warnings.find((w) => w.includes('default values'));
288
+ expect(defaultWarning).toBeUndefined();
289
+ });
290
+
291
+ it('should warn about default pricing when no diagnostics present', () => {
292
+ const context: WebsiteContentContext = {
293
+ productName: 'TestApp',
294
+ features: [{ title: 'Feature', description: 'Desc' }],
295
+ rawDocs: '# TestApp\nSome docs content here for minimum length...',
296
+ pricing: [
297
+ { name: 'Starter', price: '$0', description: '', features: [], cta: 'Get started' },
298
+ { name: 'Pro', price: '$29', description: '', features: [], cta: 'Start' },
299
+ { name: 'Enterprise', price: 'Custom', description: '', features: [], cta: 'Contact' },
300
+ ],
301
+ // No pricingDiagnostics — simulates legacy or missing provenance
302
+ };
303
+ const result = validateWebsiteContext(context, 'TestApp');
304
+ const defaultWarning = result.warnings.find((w) => w.includes('default values'));
305
+ expect(defaultWarning).toBeDefined();
306
+ });
307
+ });
308
+ });