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
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Skill Coverage Gate tests — assertSkillCoverage with conditional roles.
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import {
7
+ assertSkillCoverage,
8
+ ROLE_REQUIRED_USAGE,
9
+ PHASE_ORDER,
10
+ } from '../../../src/pipeline/skills/coverage-gate.js';
11
+ import type { SkillUsageEvent } from '../../../src/pipeline/skills/usage-registry.js';
12
+ import type { PipelineState, PipelineRole, PipelinePhase } from '../../../src/pipeline/types.js';
13
+ import { createDefaultPipelineState } from '../../../src/pipeline/types.js';
14
+
15
+ /** Helper to create a usage event */
16
+ function makeEvent(
17
+ role: PipelineRole,
18
+ phase: string = 'ROLE_PLANNING',
19
+ ): SkillUsageEvent {
20
+ return {
21
+ role,
22
+ phase: phase as SkillUsageEvent['phase'],
23
+ used_as: 'system_prompt',
24
+ skill_source: 'defaults',
25
+ timestamp: new Date().toISOString(),
26
+ };
27
+ }
28
+
29
+ /** Helper to create pipeline state with overrides */
30
+ function makePipeline(overrides: Partial<PipelineState> = {}): PipelineState {
31
+ return {
32
+ ...createDefaultPipelineState(),
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ describe('assertSkillCoverage', () => {
38
+ describe('expected use — all active roles have usage', () => {
39
+ it('should pass when all active roles have recorded usage', () => {
40
+ const activeRoles: PipelineRole[] = [
41
+ 'DISPATCHER', 'ARCHITECT', 'DB_EXPERT', 'BACKEND_PROGRAMMER',
42
+ ];
43
+ const events: SkillUsageEvent[] = [
44
+ makeEvent('ARCHITECT', 'ARCHITECTURE'),
45
+ makeEvent('DB_EXPERT', 'ROLE_PLANNING'),
46
+ makeEvent('BACKEND_PROGRAMMER', 'ROLE_PLANNING'),
47
+ ];
48
+ const pipeline = makePipeline({ activeRoles });
49
+
50
+ const result = assertSkillCoverage(activeRoles, events, pipeline);
51
+
52
+ expect(result.pass).toBe(true);
53
+ expect(result.missing).toHaveLength(0);
54
+ expect(result.covered).toContain('DISPATCHER');
55
+ expect(result.covered).toContain('ARCHITECT');
56
+ });
57
+ });
58
+
59
+ describe('fail case — active role missing usage', () => {
60
+ it('should fail when active UI_UX_SPECIALIST has no usage events', () => {
61
+ const activeRoles: PipelineRole[] = [
62
+ 'DISPATCHER', 'ARCHITECT', 'UI_UX_SPECIALIST',
63
+ ];
64
+ const events: SkillUsageEvent[] = [
65
+ makeEvent('ARCHITECT', 'ARCHITECTURE'),
66
+ // UI_UX_SPECIALIST has no events
67
+ ];
68
+ const pipeline = makePipeline({ activeRoles });
69
+
70
+ const result = assertSkillCoverage(activeRoles, events, pipeline);
71
+
72
+ expect(result.pass).toBe(false);
73
+ expect(result.missing).toHaveLength(1);
74
+ expect(result.missing[0].role).toBe('UI_UX_SPECIALIST');
75
+ expect(result.missing[0].reason).toContain('No skill usage recorded');
76
+ });
77
+ });
78
+
79
+ describe('DISPATCHER — meta-only role always passes', () => {
80
+ it('should mark DISPATCHER as covered without any events', () => {
81
+ const result = assertSkillCoverage(
82
+ ['DISPATCHER'],
83
+ [],
84
+ makePipeline({ activeRoles: ['DISPATCHER'] }),
85
+ );
86
+
87
+ expect(result.pass).toBe(true);
88
+ expect(result.covered).toContain('DISPATCHER');
89
+ });
90
+ });
91
+
92
+ describe('DEBUGGER — conditional on recovery', () => {
93
+ it('should pass when recoveryCount=0 (not required)', () => {
94
+ const result = assertSkillCoverage(
95
+ ['DISPATCHER', 'DEBUGGER'],
96
+ [],
97
+ makePipeline({
98
+ activeRoles: ['DISPATCHER', 'DEBUGGER'],
99
+ recoveryCount: 0,
100
+ }),
101
+ );
102
+
103
+ expect(result.pass).toBe(true);
104
+ expect(result.covered).toContain('DEBUGGER');
105
+ });
106
+
107
+ it('should fail when recoveryCount>0 and no usage events', () => {
108
+ const result = assertSkillCoverage(
109
+ ['DISPATCHER', 'DEBUGGER'],
110
+ [],
111
+ makePipeline({
112
+ activeRoles: ['DISPATCHER', 'DEBUGGER'],
113
+ recoveryCount: 1,
114
+ }),
115
+ );
116
+
117
+ expect(result.pass).toBe(false);
118
+ expect(result.missing).toHaveLength(1);
119
+ expect(result.missing[0].role).toBe('DEBUGGER');
120
+ });
121
+
122
+ it('should pass when recoveryCount>0 and usage recorded', () => {
123
+ const result = assertSkillCoverage(
124
+ ['DISPATCHER', 'DEBUGGER'],
125
+ [makeEvent('DEBUGGER', 'RECOVERY_LOOP')],
126
+ makePipeline({
127
+ activeRoles: ['DISPATCHER', 'DEBUGGER'],
128
+ recoveryCount: 2,
129
+ }),
130
+ );
131
+
132
+ expect(result.pass).toBe(true);
133
+ expect(result.covered).toContain('DEBUGGER');
134
+ });
135
+ });
136
+
137
+ describe('ARBITRATOR — conditional on arbitration', () => {
138
+ it('should pass when no arbitration artifacts exist', () => {
139
+ const result = assertSkillCoverage(
140
+ ['DISPATCHER', 'ARBITRATOR'],
141
+ [],
142
+ makePipeline({
143
+ activeRoles: ['DISPATCHER', 'ARBITRATOR'],
144
+ artifacts: [],
145
+ }),
146
+ );
147
+
148
+ expect(result.pass).toBe(true);
149
+ expect(result.covered).toContain('ARBITRATOR');
150
+ });
151
+
152
+ it('should fail when arbitration artifacts exist but no usage', () => {
153
+ const result = assertSkillCoverage(
154
+ ['DISPATCHER', 'ARBITRATOR'],
155
+ [],
156
+ makePipeline({
157
+ activeRoles: ['DISPATCHER', 'ARBITRATOR'],
158
+ artifacts: [{
159
+ id: 'arb-1',
160
+ type: 'arbitration',
161
+ phase: 'CONSENSUS_MASTER_PLAN',
162
+ version: 1,
163
+ path: 'docs/arbitration.json',
164
+ sha256: 'abc',
165
+ timestamp: new Date().toISOString(),
166
+ immutable: true,
167
+ content_type: 'json',
168
+ group_id: 'g1',
169
+ }],
170
+ }),
171
+ );
172
+
173
+ expect(result.pass).toBe(false);
174
+ expect(result.missing[0].role).toBe('ARBITRATOR');
175
+ });
176
+ });
177
+
178
+ describe('JOURNALIST — conditional on journal triggers', () => {
179
+ it('should pass when no journal-triggering phases completed', () => {
180
+ const result = assertSkillCoverage(
181
+ ['DISPATCHER', 'JOURNALIST'],
182
+ [],
183
+ makePipeline({
184
+ activeRoles: ['DISPATCHER', 'JOURNALIST'],
185
+ gateResults: {},
186
+ }),
187
+ );
188
+
189
+ expect(result.pass).toBe(true);
190
+ });
191
+
192
+ it('should fail when journal phase completed but no usage', () => {
193
+ const result = assertSkillCoverage(
194
+ ['DISPATCHER', 'JOURNALIST'],
195
+ [],
196
+ makePipeline({
197
+ activeRoles: ['DISPATCHER', 'JOURNALIST'],
198
+ gateResults: {
199
+ CONSENSUS_MASTER_PLAN: {
200
+ phase: 'CONSENSUS_MASTER_PLAN',
201
+ pass: true,
202
+ blockers: [],
203
+ missingArtifacts: [],
204
+ failedChecks: [],
205
+ timestamp: new Date().toISOString(),
206
+ },
207
+ },
208
+ }),
209
+ );
210
+
211
+ expect(result.pass).toBe(false);
212
+ expect(result.missing[0].role).toBe('JOURNALIST');
213
+ });
214
+ });
215
+
216
+ describe('ROLE_REQUIRED_USAGE configuration', () => {
217
+ it('should have entries for all 16 pipeline roles', () => {
218
+ const expectedRoles: PipelineRole[] = [
219
+ 'DISPATCHER', 'ARCHITECT', 'DB_EXPERT', 'BACKEND_PROGRAMMER',
220
+ 'FRONTEND_PROGRAMMER', 'WEBSITE_PROGRAMMER', 'UI_UX_SPECIALIST',
221
+ 'MARKETING_EXPERT', 'SOCIAL_EXPERT', 'QA_TESTER', 'REVIEWER',
222
+ 'ARBITRATOR', 'DEBUGGER', 'AUDITOR', 'JOURNALIST', 'RELEASE_MANAGER',
223
+ ];
224
+
225
+ for (const role of expectedRoles) {
226
+ expect(ROLE_REQUIRED_USAGE[role]).toBeDefined();
227
+ }
228
+ });
229
+ });
230
+
231
+ // ─── v2.4.5: Phase-aware deferral ────────────────────────
232
+
233
+ describe('phase-aware deferral (v2.4.5)', () => {
234
+ it('should defer AUDITOR at CONSENSUS_ROLE_PLANS', () => {
235
+ // AUDITOR requires AUDIT phase, which comes after CONSENSUS_ROLE_PLANS
236
+ const activeRoles: PipelineRole[] = ['DISPATCHER', 'AUDITOR'];
237
+ const pipeline = makePipeline({ activeRoles });
238
+
239
+ const result = assertSkillCoverage(activeRoles, [], pipeline, 'CONSENSUS_ROLE_PLANS');
240
+
241
+ expect(result.pass).toBe(true);
242
+ expect(result.deferred).toContain('AUDITOR');
243
+ expect(result.missing).toHaveLength(0);
244
+ });
245
+
246
+ it('should check AUDITOR at PRODUCTION_GATE (AUDIT <= PRODUCTION_GATE)', () => {
247
+ // AUDIT comes before PRODUCTION_GATE in PHASE_ORDER, so AUDITOR is checked
248
+ const activeRoles: PipelineRole[] = ['DISPATCHER', 'AUDITOR'];
249
+ const pipeline = makePipeline({ activeRoles });
250
+
251
+ const result = assertSkillCoverage(activeRoles, [], pipeline, 'PRODUCTION_GATE');
252
+
253
+ expect(result.pass).toBe(false);
254
+ expect(result.missing).toHaveLength(1);
255
+ expect(result.missing[0].role).toBe('AUDITOR');
256
+ expect(result.deferred).not.toContain('AUDITOR');
257
+ });
258
+
259
+ it('should pass AUDITOR at PRODUCTION_GATE with events', () => {
260
+ const activeRoles: PipelineRole[] = ['DISPATCHER', 'AUDITOR'];
261
+ const events = [makeEvent('AUDITOR', 'AUDIT')];
262
+ const pipeline = makePipeline({ activeRoles });
263
+
264
+ const result = assertSkillCoverage(activeRoles, events, pipeline, 'PRODUCTION_GATE');
265
+
266
+ expect(result.pass).toBe(true);
267
+ expect(result.covered).toContain('AUDITOR');
268
+ expect(result.deferred).toHaveLength(0);
269
+ });
270
+
271
+ it('should defer RELEASE_MANAGER at PRODUCTION_GATE (DONE > PRODUCTION_GATE)', () => {
272
+ const activeRoles: PipelineRole[] = ['DISPATCHER', 'RELEASE_MANAGER'];
273
+ const pipeline = makePipeline({ activeRoles });
274
+
275
+ const result = assertSkillCoverage(activeRoles, [], pipeline, 'PRODUCTION_GATE');
276
+
277
+ expect(result.pass).toBe(true);
278
+ expect(result.deferred).toContain('RELEASE_MANAGER');
279
+ });
280
+
281
+ it('should check QA_TESTER at CONSENSUS_ROLE_PLANS (ROLE_PLANNING <= CONSENSUS_ROLE_PLANS)', () => {
282
+ // QA_TESTER has phases [ROLE_PLANNING, QA_VALIDATION].
283
+ // ROLE_PLANNING comes before CONSENSUS_ROLE_PLANS, so QA_TESTER is checked.
284
+ const activeRoles: PipelineRole[] = ['DISPATCHER', 'QA_TESTER'];
285
+ const events = [makeEvent('QA_TESTER', 'ROLE_PLANNING')];
286
+ const pipeline = makePipeline({ activeRoles });
287
+
288
+ const result = assertSkillCoverage(activeRoles, events, pipeline, 'CONSENSUS_ROLE_PLANS');
289
+
290
+ expect(result.pass).toBe(true);
291
+ expect(result.covered).toContain('QA_TESTER');
292
+ expect(result.deferred).not.toContain('QA_TESTER');
293
+ });
294
+
295
+ it('should pass full production scenario at CONSENSUS_ROLE_PLANS', () => {
296
+ // Simulate real pipeline: all SUPPORT_ROLES + website impl roles active.
297
+ // Provide events only for roles through ROLE_PLANNING phase.
298
+ const activeRoles: PipelineRole[] = [
299
+ 'DISPATCHER', 'ARCHITECT', 'QA_TESTER', 'REVIEWER', 'ARBITRATOR',
300
+ 'DEBUGGER', 'AUDITOR', 'JOURNALIST', 'RELEASE_MANAGER',
301
+ 'DB_EXPERT', 'BACKEND_PROGRAMMER', 'WEBSITE_PROGRAMMER',
302
+ ];
303
+ const events: SkillUsageEvent[] = [
304
+ makeEvent('ARCHITECT', 'ARCHITECTURE'),
305
+ makeEvent('DB_EXPERT', 'ROLE_PLANNING'),
306
+ makeEvent('BACKEND_PROGRAMMER', 'ROLE_PLANNING'),
307
+ makeEvent('WEBSITE_PROGRAMMER', 'ROLE_PLANNING'),
308
+ makeEvent('QA_TESTER', 'ROLE_PLANNING'),
309
+ ];
310
+ const pipeline = makePipeline({
311
+ activeRoles,
312
+ recoveryCount: 0,
313
+ artifacts: [],
314
+ gateResults: {},
315
+ });
316
+
317
+ const result = assertSkillCoverage(activeRoles, events, pipeline, 'CONSENSUS_ROLE_PLANS');
318
+
319
+ expect(result.pass).toBe(true);
320
+ // AUDITOR and RELEASE_MANAGER should be deferred
321
+ expect(result.deferred).toContain('AUDITOR');
322
+ expect(result.deferred).toContain('RELEASE_MANAGER');
323
+ // REVIEWER should also be deferred (REVIEW phase > CONSENSUS_ROLE_PLANS)
324
+ expect(result.deferred).toContain('REVIEWER');
325
+ // Conditional roles with no trigger are covered (not deferred)
326
+ expect(result.covered).toContain('ARBITRATOR');
327
+ expect(result.covered).toContain('DEBUGGER');
328
+ expect(result.covered).toContain('JOURNALIST');
329
+ });
330
+
331
+ it('should fall back to strict mode with unknown currentPhase', () => {
332
+ // An unknown phase not in PHASE_ORDER gets indexOf = -1, same as strict mode
333
+ const activeRoles: PipelineRole[] = ['DISPATCHER', 'AUDITOR'];
334
+ const pipeline = makePipeline({ activeRoles });
335
+
336
+ const result = assertSkillCoverage(
337
+ activeRoles, [], pipeline, 'SOME_NEW_PHASE' as PipelinePhase,
338
+ );
339
+
340
+ // Strict mode: AUDITOR is checked (not deferred) and fails
341
+ expect(result.pass).toBe(false);
342
+ expect(result.missing).toHaveLength(1);
343
+ expect(result.missing[0].role).toBe('AUDITOR');
344
+ expect(result.deferred).toHaveLength(0);
345
+ });
346
+
347
+ it('should use strict mode when currentPhase is omitted (backward compat)', () => {
348
+ const activeRoles: PipelineRole[] = ['DISPATCHER', 'AUDITOR'];
349
+ const pipeline = makePipeline({ activeRoles });
350
+
351
+ // No currentPhase param — same as original behavior
352
+ const result = assertSkillCoverage(activeRoles, [], pipeline);
353
+
354
+ expect(result.pass).toBe(false);
355
+ expect(result.missing).toHaveLength(1);
356
+ expect(result.missing[0].role).toBe('AUDITOR');
357
+ expect(result.deferred).toHaveLength(0);
358
+ });
359
+
360
+ it('PHASE_ORDER should include all key phases', () => {
361
+ const keyPhases: PipelinePhase[] = [
362
+ 'INTAKE', 'CONSENSUS_ROLE_PLANS', 'PRODUCTION_GATE', 'DONE', 'STUCK',
363
+ 'AUDIT', 'REVIEW', 'ARCHITECTURE', 'ROLE_PLANNING',
364
+ ];
365
+ for (const phase of keyPhases) {
366
+ expect(PHASE_ORDER).toContain(phase);
367
+ }
368
+ });
369
+ });
370
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * SkillUsageRegistry tests — recording, querying, and array reference sharing.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { SkillUsageRegistry } from '../../../src/pipeline/skills/usage-registry.js';
7
+ import type { SkillUsageEvent } from '../../../src/pipeline/skills/usage-registry.js';
8
+
9
+ describe('SkillUsageRegistry', () => {
10
+ let events: SkillUsageEvent[];
11
+ let registry: SkillUsageRegistry;
12
+
13
+ beforeEach(() => {
14
+ events = [];
15
+ registry = new SkillUsageRegistry(events);
16
+ });
17
+
18
+ describe('record', () => {
19
+ it('should record a usage event with all fields', () => {
20
+ registry.record('ARCHITECT', 'ARCHITECTURE', 'system_prompt', 'defaults', '1.0');
21
+
22
+ const recorded = registry.getEvents();
23
+ expect(recorded).toHaveLength(1);
24
+ expect(recorded[0].role).toBe('ARCHITECT');
25
+ expect(recorded[0].phase).toBe('ARCHITECTURE');
26
+ expect(recorded[0].used_as).toBe('system_prompt');
27
+ expect(recorded[0].skill_source).toBe('defaults');
28
+ expect(recorded[0].skill_version).toBe('1.0');
29
+ expect(recorded[0].timestamp).toBeTruthy();
30
+ });
31
+
32
+ it('should record event without optional version', () => {
33
+ registry.record('REVIEWER', 'REVIEW', 'review_prompt', 'project_override');
34
+
35
+ const recorded = registry.getEvents();
36
+ expect(recorded).toHaveLength(1);
37
+ expect(recorded[0].skill_version).toBeUndefined();
38
+ });
39
+
40
+ it('should push into the shared array reference', () => {
41
+ registry.record('AUDITOR', 'AUDIT', 'system_prompt', 'defaults');
42
+
43
+ // The underlying events array should be mutated directly
44
+ expect(events).toHaveLength(1);
45
+ expect(events[0].role).toBe('AUDITOR');
46
+ });
47
+ });
48
+
49
+ describe('getEvents', () => {
50
+ it('should return a copy of events', () => {
51
+ registry.record('ARCHITECT', 'ARCHITECTURE', 'system_prompt', 'defaults');
52
+ const copy = registry.getEvents();
53
+
54
+ // Mutating the copy should not affect the original
55
+ copy.push({
56
+ role: 'DEBUGGER',
57
+ phase: 'RECOVERY_LOOP',
58
+ used_as: 'system_prompt',
59
+ skill_source: 'defaults',
60
+ timestamp: new Date().toISOString(),
61
+ });
62
+
63
+ expect(registry.getEvents()).toHaveLength(1);
64
+ });
65
+ });
66
+
67
+ describe('getEventsForRole', () => {
68
+ it('should filter events by role', () => {
69
+ registry.record('ARCHITECT', 'ARCHITECTURE', 'system_prompt', 'defaults');
70
+ registry.record('REVIEWER', 'REVIEW', 'review_prompt', 'defaults');
71
+ registry.record('ARCHITECT', 'CONSENSUS_ARCHITECTURE', 'other', 'defaults');
72
+
73
+ const architectEvents = registry.getEventsForRole('ARCHITECT');
74
+ expect(architectEvents).toHaveLength(2);
75
+ expect(architectEvents.every((e) => e.role === 'ARCHITECT')).toBe(true);
76
+ });
77
+
78
+ it('should return empty array for role with no events', () => {
79
+ registry.record('ARCHITECT', 'ARCHITECTURE', 'system_prompt', 'defaults');
80
+
81
+ const debuggerEvents = registry.getEventsForRole('DEBUGGER');
82
+ expect(debuggerEvents).toHaveLength(0);
83
+ });
84
+ });
85
+
86
+ describe('hasUsage', () => {
87
+ it('should return true for role with recorded events', () => {
88
+ registry.record('AUDITOR', 'AUDIT', 'system_prompt', 'defaults');
89
+
90
+ expect(registry.hasUsage('AUDITOR')).toBe(true);
91
+ });
92
+
93
+ it('should return false for role with no events', () => {
94
+ expect(registry.hasUsage('DEBUGGER')).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe('state serialization', () => {
99
+ it('should round-trip through JSON correctly', () => {
100
+ registry.record('ARCHITECT', 'ARCHITECTURE', 'system_prompt', 'defaults', '1.0');
101
+ registry.record('REVIEWER', 'REVIEW', 'review_prompt', 'project_override', '2.0');
102
+
103
+ // Simulate state serialization
104
+ const serialized = JSON.stringify(events);
105
+ const deserialized = JSON.parse(serialized) as SkillUsageEvent[];
106
+
107
+ // Create new registry from deserialized data
108
+ const newRegistry = new SkillUsageRegistry(deserialized);
109
+ expect(newRegistry.getEvents()).toHaveLength(2);
110
+ expect(newRegistry.hasUsage('ARCHITECT')).toBe(true);
111
+ expect(newRegistry.hasUsage('REVIEWER')).toBe(true);
112
+ });
113
+ });
114
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Strategy context loader tests — loadStrategyForRole, STRATEGY_ROLES.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { STRATEGY_ROLES, loadStrategyForRole } from '../../src/pipeline/strategy-context.js';
9
+
10
+ const TEST_DIR = join(process.cwd(), 'tmp-strategy-context-test');
11
+
12
+ function makeValidStrategy() {
13
+ return {
14
+ icp: { primaryPersona: 'developers', painPoints: ['slow builds'], goals: ['fast CI'], objections: ['cost'] },
15
+ positioning: { category: 'DevTools', differentiators: ['speed'], valueProposition: 'Build faster', proofPoints: ['10x'] },
16
+ messaging: { headline: 'Build Faster', subheadline: 'CI that works', elevatorPitch: 'Fast CI', longDescription: 'A fast CI platform' },
17
+ seoStrategy: { primaryKeywords: ['CI'], secondaryKeywords: ['build'], longTailKeywords: ['fast CI'], titleTemplates: {}, metaDescriptions: {} },
18
+ siteArchitecture: {
19
+ pages: [{ path: '/', title: 'Home', purpose: 'Landing', pageType: 'landing', sections: ['hero'], seoKeywords: ['CI'], conversionGoal: 'signup' }],
20
+ navigation: [{ label: 'Home', href: '/' }],
21
+ footerSections: [{ title: 'Links', links: [{ label: 'Home', href: '/' }] }],
22
+ },
23
+ conversionStrategy: {
24
+ primaryCta: { text: 'Get Started', href: '/signup' },
25
+ secondaryCta: { text: 'Learn More', href: '/docs' },
26
+ trustSignals: ['SOC2'],
27
+ socialProof: ['1000+ teams'],
28
+ leadCapture: 'webhook',
29
+ },
30
+ competitiveContext: { category: 'CI/CD', competitors: ['CircleCI'], differentiators: ['speed'] },
31
+ };
32
+ }
33
+
34
+ beforeEach(() => {
35
+ mkdirSync(join(TEST_DIR, '.popeye'), { recursive: true });
36
+ });
37
+
38
+ afterEach(() => {
39
+ if (existsSync(TEST_DIR)) {
40
+ rmSync(TEST_DIR, { recursive: true, force: true });
41
+ }
42
+ });
43
+
44
+ describe('STRATEGY_ROLES', () => {
45
+ it('includes exactly WEBSITE_PROGRAMMER, MARKETING_EXPERT, SOCIAL_EXPERT', () => {
46
+ expect(STRATEGY_ROLES).toContain('WEBSITE_PROGRAMMER');
47
+ expect(STRATEGY_ROLES).toContain('MARKETING_EXPERT');
48
+ expect(STRATEGY_ROLES).toContain('SOCIAL_EXPERT');
49
+ expect(STRATEGY_ROLES).toHaveLength(3);
50
+ });
51
+ });
52
+
53
+ describe('loadStrategyForRole', () => {
54
+ it('returns undefined when no strategy file exists', () => {
55
+ const result = loadStrategyForRole(TEST_DIR);
56
+ expect(result).toBeUndefined();
57
+ });
58
+
59
+ it('returns formatted string when valid .popeye/website-strategy.json exists', () => {
60
+ const strategy = makeValidStrategy();
61
+ writeFileSync(
62
+ join(TEST_DIR, '.popeye', 'website-strategy.json'),
63
+ JSON.stringify(strategy),
64
+ );
65
+
66
+ const result = loadStrategyForRole(TEST_DIR);
67
+ expect(result).toBeDefined();
68
+ expect(result).toContain('Target Customer');
69
+ expect(result).toContain('developers');
70
+ expect(result).toContain('Build Faster');
71
+ expect(result).toContain('SEO Keywords');
72
+ expect(result).toContain('Conversion Strategy');
73
+ });
74
+
75
+ it('handles nested strategy field in JSON', () => {
76
+ const strategy = makeValidStrategy();
77
+ writeFileSync(
78
+ join(TEST_DIR, '.popeye', 'website-strategy.json'),
79
+ JSON.stringify({ strategy, metadata: { inputHash: 'abc', generatedAt: '2025-01-01', version: 1 } }),
80
+ );
81
+
82
+ const result = loadStrategyForRole(TEST_DIR);
83
+ expect(result).toBeDefined();
84
+ expect(result).toContain('developers');
85
+ });
86
+
87
+ it('returns undefined for malformed JSON', () => {
88
+ writeFileSync(
89
+ join(TEST_DIR, '.popeye', 'website-strategy.json'),
90
+ '{ invalid json!!!',
91
+ );
92
+
93
+ const result = loadStrategyForRole(TEST_DIR);
94
+ expect(result).toBeUndefined();
95
+ });
96
+
97
+ it('returns undefined for JSON that fails schema validation', () => {
98
+ writeFileSync(
99
+ join(TEST_DIR, '.popeye', 'website-strategy.json'),
100
+ JSON.stringify({ icp: { primaryPersona: '' } }),
101
+ );
102
+
103
+ const result = loadStrategyForRole(TEST_DIR);
104
+ expect(result).toBeUndefined();
105
+ });
106
+
107
+ it('falls back to .popeye/website-strategy.md when JSON absent', () => {
108
+ const mdContent = '# Strategy\n\nTarget: developers\nKeywords: CI, build';
109
+ writeFileSync(
110
+ join(TEST_DIR, '.popeye', 'website-strategy.md'),
111
+ mdContent,
112
+ );
113
+
114
+ const result = loadStrategyForRole(TEST_DIR);
115
+ expect(result).toBe(mdContent);
116
+ });
117
+
118
+ it('prefers JSON over MD when both exist', () => {
119
+ const strategy = makeValidStrategy();
120
+ writeFileSync(
121
+ join(TEST_DIR, '.popeye', 'website-strategy.json'),
122
+ JSON.stringify(strategy),
123
+ );
124
+ writeFileSync(
125
+ join(TEST_DIR, '.popeye', 'website-strategy.md'),
126
+ '# Fallback MD',
127
+ );
128
+
129
+ const result = loadStrategyForRole(TEST_DIR);
130
+ // JSON is checked first, should get formatted strategy not raw MD
131
+ expect(result).toContain('Target Customer');
132
+ expect(result).not.toContain('Fallback MD');
133
+ });
134
+
135
+ it('falls back to MD when JSON is malformed', () => {
136
+ writeFileSync(
137
+ join(TEST_DIR, '.popeye', 'website-strategy.json'),
138
+ '{ bad json }',
139
+ );
140
+ writeFileSync(
141
+ join(TEST_DIR, '.popeye', 'website-strategy.md'),
142
+ '# Good MD Strategy',
143
+ );
144
+
145
+ const result = loadStrategyForRole(TEST_DIR);
146
+ expect(result).toBe('# Good MD Strategy');
147
+ });
148
+ });