pan-wizard 3.35.0 → 3.37.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 (339) hide show
  1. package/CHANGELOG.md +3645 -0
  2. package/README.md +46 -52
  3. package/agents/pan-conductor.md +2 -3
  4. package/agents/pan-counterfactual.md +1 -2
  5. package/agents/pan-debugger.md +14 -3
  6. package/agents/pan-design-checker.md +1 -2
  7. package/agents/pan-designer.md +2 -3
  8. package/agents/pan-document_code.md +1 -2
  9. package/agents/pan-executor.md +25 -0
  10. package/agents/pan-hardener.md +4 -3
  11. package/agents/pan-integration-checker.md +1 -2
  12. package/agents/pan-knowledge.md +1 -2
  13. package/agents/pan-meta-reviewer.md +3 -3
  14. package/agents/pan-optimizer.md +4 -4
  15. package/agents/pan-phase-researcher.md +80 -475
  16. package/agents/pan-plan-checker.md +40 -3
  17. package/agents/pan-planner.md +21 -7
  18. package/agents/pan-previewer.md +3 -2
  19. package/agents/pan-project-researcher.md +72 -493
  20. package/agents/pan-release.md +1 -1
  21. package/agents/pan-reviewer.md +14 -3
  22. package/agents/pan-roadmapper.md +12 -7
  23. package/agents/pan-verifier.md +9 -4
  24. package/bin/install-lib.cjs +134 -14
  25. package/bin/install.js +59 -12
  26. package/commands/pan/army.md +1 -1
  27. package/commands/pan/autodev.md +76 -0
  28. package/commands/pan/cost.md +21 -1
  29. package/commands/pan/exec-phase.md +3 -4
  30. package/commands/pan/focus-auto.md +14 -7
  31. package/commands/pan/focus-design.md +4 -5
  32. package/commands/pan/focus-docs.md +24 -0
  33. package/commands/pan/learn.md +13 -6
  34. package/commands/pan/milestone-audit.md +8 -1
  35. package/commands/pan/milestone-done.md +10 -5
  36. package/commands/pan/milestone-new.md +2 -2
  37. package/commands/pan/new-project.md +15 -12
  38. package/commands/pan/plan-phase.md +15 -9
  39. package/commands/pan/preview.md +1 -1
  40. package/commands/pan/quick.md +1 -1
  41. package/commands/pan/review-deep.md +11 -5
  42. package/commands/pan/settings.md +6 -0
  43. package/commands/pan/verify-phase.md +6 -2
  44. package/docs/AGENTS.md +917 -0
  45. package/docs/ARCHITECTURE.md +1054 -0
  46. package/docs/CLI-REFERENCE.md +4582 -0
  47. package/docs/COMPARISON.md +116 -0
  48. package/docs/DEVELOPMENT.md +490 -0
  49. package/docs/ECOSYSTEM-REVIEW-2026-06.md +107 -0
  50. package/docs/ECOSYSTEM-REVIEW-2026-08.md +349 -0
  51. package/docs/EXAMPLES.md +584 -0
  52. package/docs/FAQ.md +254 -0
  53. package/docs/FIELD-HARVEST-2026-07.md +88 -0
  54. package/docs/FIELD-REPORT-army-2026-06.md +121 -0
  55. package/docs/HOOKS.md +638 -0
  56. package/docs/IMPROVEMENT-TODO.md +246 -0
  57. package/docs/INTERNALS.md +485 -0
  58. package/docs/MIGRATION-v2-to-v3.md +258 -0
  59. package/docs/SECURITY-HARDENING.md +230 -0
  60. package/docs/SKILLS-ENHANCEMENT-PLAN-V2.md +122 -0
  61. package/docs/SKILLS-FULL-TEXT.md +9685 -0
  62. package/docs/SKILLS-REFERENCE.md +1037 -0
  63. package/docs/TROUBLESHOOTING.md +1084 -0
  64. package/docs/USER-GUIDE.md +1437 -0
  65. package/docs/archive/SKILLS-ENHANCEMENT-PLAN.md +230 -0
  66. package/docs/audits/AUDIT-2026-08-e2e-ROADMAP.md +117 -0
  67. package/docs/audits/AUDIT-2026-08-e2e-VERIFICATION.md +693 -0
  68. package/docs/audits/AUDIT-2026-08-e2e.md +1546 -0
  69. package/docs/audits/OUTSTANDING-2026-08.md +275 -0
  70. package/docs/audits/STABILITY-TEST-2026-08.md +640 -0
  71. package/docs/branding/README-brand-draft.md +119 -0
  72. package/docs/branding/handoff/IMPLEMENTATION.md +205 -0
  73. package/docs/branding/handoff/README-brand-draft.md +119 -0
  74. package/docs/branding/handoff/image-prompts.md +46 -0
  75. package/docs/branding/image-prompts.md +46 -0
  76. package/docs/branding/nano-banana-prompts.txt +60 -0
  77. package/docs/context-monitor.md +13 -0
  78. package/docs/decisions/ADR-0001-strategic-feature-prioritization.md +83 -0
  79. package/docs/decisions/ADR-0002-copilot-cli-runtime.md +71 -0
  80. package/docs/decisions/ADR-0003-smart-execution.md +84 -0
  81. package/docs/decisions/ADR-0004-copilot-cli-interaction.md +89 -0
  82. package/docs/decisions/ADR-0005-command-naming.md +80 -0
  83. package/docs/decisions/ADR-0006-focus-commands.md +101 -0
  84. package/docs/decisions/ADR-0007-ship-v1.md +73 -0
  85. package/docs/decisions/ADR-0008-map-codebase-mermaid-togaf.md +80 -0
  86. package/docs/decisions/ADR-0009-production-deployment-checklist.md +68 -0
  87. package/docs/decisions/ADR-0010-standards-integration.md +79 -0
  88. package/docs/decisions/ADR-0011-help-commands-rewrite.md +64 -0
  89. package/docs/decisions/ADR-0012-contributors-attribution-index.md +42 -0
  90. package/docs/decisions/ADR-0013-strategic-feature-additions.md +53 -0
  91. package/docs/decisions/ADR-0014-internal-cleanup-code-quality.md +53 -0
  92. package/docs/decisions/ADR-0015-focus-auto-runner.md +296 -0
  93. package/docs/decisions/ADR-0016-enhanced-doc-sync.md +54 -0
  94. package/docs/decisions/ADR-0017-enhanced-focus-design.md +69 -0
  95. package/docs/decisions/ADR-0018-user-reality-testing.md +84 -0
  96. package/docs/decisions/ADR-0019-end-to-end-user-acceptance-testing.md +113 -0
  97. package/docs/decisions/ADR-0020-ai-drift-prevention.md +55 -0
  98. package/docs/decisions/ADR-0021-codebase-mapper-v2.md +76 -0
  99. package/docs/decisions/ADR-0022-lifecycle-completeness.md +63 -0
  100. package/docs/decisions/ADR-0023-opus-4-7-adoption.md +89 -0
  101. package/docs/decisions/ADR-0024-spec-b-v2-completion.md +137 -0
  102. package/docs/decisions/ADR-0025-behavioral-guardrails-layer.md +129 -0
  103. package/docs/decisions/ADR-0026-self-improvement-loop.md +171 -0
  104. package/docs/decisions/ADR-0027-doc-code-link-graph.md +164 -0
  105. package/docs/decisions/ADR-0028-unified-agents-skills-target.md +46 -0
  106. package/docs/decisions/ADR-0029-experiment-evaluator.md +76 -0
  107. package/docs/decisions/ADR-0030-planning-concurrency.md +37 -0
  108. package/docs/decisions/ADR-0031-focus-auto-backlog-source.md +47 -0
  109. package/docs/decisions/ADR-0032-squad-model.md +64 -0
  110. package/docs/decisions/ADR-0033-army-campaign.md +40 -0
  111. package/docs/decisions/ADR-0034-scheduled-campaigns.md +57 -0
  112. package/docs/decisions/ADR-0035-army-hud-dashboard.md +40 -0
  113. package/docs/decisions/ADR-0036-agentic-memory-architecture.md +106 -0
  114. package/docs/decisions/ADR-0037-pin-security-agents-to-opus.md +51 -0
  115. package/docs/decisions/ADR-0038-skill-aligned-decomposition.md +41 -0
  116. package/docs/decisions/ADR-0039-worktree-as-project-variant.md +37 -0
  117. package/docs/decisions/ADR-0040-memory-injection-defense.md +55 -0
  118. package/docs/decisions/ADR-0041-mcp-dual-era-bridge.md +47 -0
  119. package/docs/decisions/ADR-0042-design-parity-both-flows.md +45 -0
  120. package/docs/decisions/ADR-0043-addressable-planning-roots.md +56 -0
  121. package/docs/decisions/ADR-0044-bound-the-cached-context.md +104 -0
  122. package/docs/decisions/ADR-0045-agent-plugins-bundle.md +76 -0
  123. package/docs/decisions/ADR-0046-subagent-frontmatter-decisions.md +39 -0
  124. package/docs/decisions/ADR-0047-pan-harness.md +76 -0
  125. package/docs/decisions/ADR-0048-one-core-per-project.md +43 -0
  126. package/docs/decisions/ADR-0049-machine-verdicts-and-findings-ledger.md +116 -0
  127. package/docs/specs/ai_drift_prevention_featureai.md +286 -0
  128. package/docs/specs/codebase_mapper_v2_featureai.md +930 -0
  129. package/docs/specs/command_naming_optimization_featureai.md +930 -0
  130. package/docs/specs/comp_analysis_markdown_v1.md +149 -0
  131. package/docs/specs/continuous_autonomous_execution_featureai.md +304 -0
  132. package/docs/specs/contributors_attribution_index_featureai.md +476 -0
  133. package/docs/specs/copilot_cli_interaction_featureai.md +868 -0
  134. package/docs/specs/copilot_cli_runtime_featureai.md +647 -0
  135. package/docs/specs/custom-skills-assessment.md +461 -0
  136. package/docs/specs/deployment_audit_findings_featureai.md +243 -0
  137. package/docs/specs/doc_code_link_graph_featureai.md +253 -0
  138. package/docs/specs/documentation_alignment_cleanup_featureai.md +69 -0
  139. package/docs/specs/e2e_visual_testing_featureai.md +615 -0
  140. package/docs/specs/end_to_end_user_acceptance_testing_featureai.md +810 -0
  141. package/docs/specs/evidence-loop-plan.md +76 -0
  142. package/docs/specs/evidence_loop_featureai.md +497 -0
  143. package/docs/specs/focus-auto-optimize-category.md +449 -0
  144. package/docs/specs/focus-auto-optimize-superplan.md +188 -0
  145. package/docs/specs/focus_auto_runner_featureai.md +1102 -0
  146. package/docs/specs/focus_design_v2_featureai.md +197 -0
  147. package/docs/specs/focus_sync_v2_featureai.md +202 -0
  148. package/docs/specs/git_integration_overhaul_featureai.md +326 -0
  149. package/docs/specs/glm_zai_provider_featureai.md +200 -0
  150. package/docs/specs/googlecli_adoption_featureai.md +661 -0
  151. package/docs/specs/help_commands_rewrite_featureai.md +860 -0
  152. package/docs/specs/industry_analysis_strategic_additions_featureai.md +231 -0
  153. package/docs/specs/industry_comparison_and_feature_gaps_featureai.md +674 -0
  154. package/docs/specs/internal_cleanup_code_quality_featureai.md +486 -0
  155. package/docs/specs/investigations/README.md +31 -0
  156. package/docs/specs/investigations/digests/2026-09-28.md +47 -0
  157. package/docs/specs/investigations/digests/2026-10-08.md +62 -0
  158. package/docs/specs/investigations/digests/2026-10-10.md +61 -0
  159. package/docs/specs/investigations/features/debugger-red-loop-first.md +55 -0
  160. package/docs/specs/investigations/features/harness-cross-host-behaviour.md +93 -0
  161. package/docs/specs/investigations/features/optimize-apply-regression-gate.md +66 -0
  162. package/docs/specs/investigations/features/reviewer-smell-baseline.md +57 -0
  163. package/docs/specs/investigations/features/trace-failure-capture.md +69 -0
  164. package/docs/specs/lifecycle_completeness_featureai.md +754 -0
  165. package/docs/specs/local-first-ci.md +42 -0
  166. package/docs/specs/map_codebase_mermaid_togaf_featureai.md +805 -0
  167. package/docs/specs/market-delta-2026-09-superplan.md +310 -0
  168. package/docs/specs/market-ideas-2026-09.md +131 -0
  169. package/docs/specs/market-ideas-2026-10.md +428 -0
  170. package/docs/specs/market-ideas-ledger.md +177 -0
  171. package/docs/specs/memory-optimization-2026-10.md +285 -0
  172. package/docs/specs/multi-track-planning-roots.md +143 -0
  173. package/docs/specs/multi_model_routing_featureai.md +687 -0
  174. package/docs/specs/multi_model_routing_superplan.md +197 -0
  175. package/docs/specs/opus_47_existing_enhancements_featureai.md +247 -0
  176. package/docs/specs/opus_47_extended_features_featureai.md +363 -0
  177. package/docs/specs/opus_47_extended_features_v2_featureai.md +397 -0
  178. package/docs/specs/pan_focus_commands_featureai.md +2336 -0
  179. package/docs/specs/pan_zcode_mcp_bridge_featureai.md +120 -0
  180. package/docs/specs/product-excellence-superplan.md +315 -0
  181. package/docs/specs/product_excellence_program_2026_q4_featureai.md +365 -0
  182. package/docs/specs/production_deployment_readiness_featureai.md +433 -0
  183. package/docs/specs/production_readiness_ship_v1_featureai.md +698 -0
  184. package/docs/specs/real_test_enforcement_featureai.md +213 -0
  185. package/docs/specs/reality-check-2026-09-superplan.md +362 -0
  186. package/docs/specs/right-sized-ceremony.md +74 -0
  187. package/docs/specs/roadmap_and_competition_2026_q4_featureai.md +390 -0
  188. package/docs/specs/self_improvement_loop_featureai.md +875 -0
  189. package/docs/specs/skill-aligned-decomposition.md +155 -0
  190. package/docs/specs/standards_integration_featureai.md +833 -0
  191. package/docs/specs/testing-system-redesign-2026-09.md +267 -0
  192. package/docs/specs/user_reality_testing_system_featureai.md +788 -0
  193. package/docs/specs/watch-triggers.json +22 -0
  194. package/docs/specs/workflow-integration-redesign.md +742 -0
  195. package/docs/specs/workflow_state_consistency_featureai.md +183 -0
  196. package/hooks/dist/pan-context-monitor.js +9 -2
  197. package/hooks/dist/pan-cost-logger.js +5 -0
  198. package/hooks/dist/pan-guard.js +28 -5
  199. package/hooks/dist/pan-stop-guard.js +9 -9
  200. package/hooks/dist/pan-trace-logger.js +5 -0
  201. package/package.json +10 -2
  202. package/pan-wizard-core/bin/lib/agents-md.cjs +57 -8
  203. package/pan-wizard-core/bin/lib/autodev-report.cjs +180 -0
  204. package/pan-wizard-core/bin/lib/autodev.cjs +642 -0
  205. package/pan-wizard-core/bin/lib/ci.cjs +143 -0
  206. package/pan-wizard-core/bin/lib/commands.cjs +1 -1
  207. package/pan-wizard-core/bin/lib/config.cjs +41 -5
  208. package/pan-wizard-core/bin/lib/constants.cjs +27 -1
  209. package/pan-wizard-core/bin/lib/core.cjs +31 -14
  210. package/pan-wizard-core/bin/lib/cost.cjs +160 -4
  211. package/pan-wizard-core/bin/lib/cross-family.cjs +164 -0
  212. package/pan-wizard-core/bin/lib/debug-cases.cjs +104 -0
  213. package/pan-wizard-core/bin/lib/doc-lint.cjs +5 -3
  214. package/pan-wizard-core/bin/lib/failure-class.cjs +81 -0
  215. package/pan-wizard-core/bin/lib/findings.cjs +148 -9
  216. package/pan-wizard-core/bin/lib/focus.cjs +1 -1
  217. package/pan-wizard-core/bin/lib/git.cjs +1 -1
  218. package/pan-wizard-core/bin/lib/hosts.cjs +88 -0
  219. package/pan-wizard-core/bin/lib/init.cjs +7 -2
  220. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -0
  221. package/pan-wizard-core/bin/lib/merge.cjs +50 -0
  222. package/pan-wizard-core/bin/lib/mutate.cjs +268 -0
  223. package/pan-wizard-core/bin/lib/next-action.cjs +112 -0
  224. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  225. package/pan-wizard-core/bin/lib/phase-gate.cjs +15 -9
  226. package/pan-wizard-core/bin/lib/phase-remove.cjs +91 -11
  227. package/pan-wizard-core/bin/lib/phase.cjs +85 -13
  228. package/pan-wizard-core/bin/lib/plan-lint.cjs +310 -0
  229. package/pan-wizard-core/bin/lib/preview.cjs +26 -4
  230. package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
  231. package/pan-wizard-core/bin/lib/research-lint.cjs +268 -0
  232. package/pan-wizard-core/bin/lib/review-deep.cjs +14 -22
  233. package/pan-wizard-core/bin/lib/review-vote.cjs +105 -0
  234. package/pan-wizard-core/bin/lib/roadmap.cjs +1 -0
  235. package/pan-wizard-core/bin/lib/runner.cjs +2 -16
  236. package/pan-wizard-core/bin/lib/sizing.cjs +356 -0
  237. package/pan-wizard-core/bin/lib/squads.cjs +1 -1
  238. package/pan-wizard-core/bin/lib/state-write.cjs +247 -0
  239. package/pan-wizard-core/bin/lib/test-gate.cjs +433 -0
  240. package/pan-wizard-core/bin/lib/uat.cjs +149 -0
  241. package/pan-wizard-core/bin/lib/usage.cjs +104 -0
  242. package/pan-wizard-core/bin/lib/verdict.cjs +26 -3
  243. package/pan-wizard-core/bin/lib/verify-plan-set.cjs +30 -1
  244. package/pan-wizard-core/bin/lib/verify-preflight.cjs +2 -2
  245. package/pan-wizard-core/bin/lib/verify-red.cjs +174 -0
  246. package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
  247. package/pan-wizard-core/bin/lib/verify-scope.cjs +143 -2
  248. package/pan-wizard-core/bin/lib/verify.cjs +86 -31
  249. package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
  250. package/pan-wizard-core/bin/pan-tools.cjs +257 -28
  251. package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
  252. package/pan-wizard-core/mcp/orchestrator.cjs +4 -1
  253. package/pan-wizard-core/references/continuation-format.md +3 -3
  254. package/pan-wizard-core/references/design-methodology.md +1 -1
  255. package/pan-wizard-core/references/guardrails.md +1 -1
  256. package/pan-wizard-core/references/handoff-decisions.md +1 -1
  257. package/pan-wizard-core/references/model-profiles.md +2 -3
  258. package/pan-wizard-core/references/questioning.md +24 -0
  259. package/pan-wizard-core/references/ui-brand.md +4 -6
  260. package/pan-wizard-core/references/verdict-contract.md +2 -1
  261. package/pan-wizard-core/templates/config.json +10 -2
  262. package/pan-wizard-core/templates/debug.md +1 -0
  263. package/pan-wizard-core/templates/design.md +5 -4
  264. package/pan-wizard-core/templates/discovery.md +2 -2
  265. package/pan-wizard-core/templates/phase-prompt.md +7 -0
  266. package/pan-wizard-core/templates/requirements.md +9 -0
  267. package/pan-wizard-core/templates/research.md +10 -0
  268. package/pan-wizard-core/templates/summary-complex.md +5 -1
  269. package/pan-wizard-core/templates/summary-minimal.md +4 -0
  270. package/pan-wizard-core/templates/summary-standard.md +5 -1
  271. package/pan-wizard-core/templates/summary.md +8 -1
  272. package/pan-wizard-core/workflows/add-phase.md +4 -6
  273. package/pan-wizard-core/workflows/assumptions.md +19 -1
  274. package/pan-wizard-core/workflows/diagnose-issues.md +2 -0
  275. package/pan-wizard-core/workflows/discuss-phase.md +4 -2
  276. package/pan-wizard-core/workflows/exec-lean.md +69 -0
  277. package/pan-wizard-core/workflows/exec-phase.md +79 -15
  278. package/pan-wizard-core/workflows/execute-plan.md +3 -1
  279. package/{commands/pan → pan-wizard-core/workflows}/focus-doc-audit.md +13 -29
  280. package/{commands/pan → pan-wizard-core/workflows}/focus-drift-walking.md +3 -18
  281. package/{commands/pan → pan-wizard-core/workflows}/focus-exec.md +8 -23
  282. package/{commands/pan → pan-wizard-core/workflows}/focus-plan.md +6 -20
  283. package/{commands/pan → pan-wizard-core/workflows}/focus-scan.md +3 -16
  284. package/{commands/pan → pan-wizard-core/workflows}/focus-sync.md +7 -21
  285. package/pan-wizard-core/workflows/health.md +6 -0
  286. package/pan-wizard-core/workflows/help.md +114 -77
  287. package/pan-wizard-core/workflows/insert-phase.md +4 -6
  288. package/pan-wizard-core/workflows/learn.md +3 -3
  289. package/pan-wizard-core/workflows/milestone-audit.md +2 -2
  290. package/pan-wizard-core/workflows/milestone-new.md +38 -95
  291. package/pan-wizard-core/workflows/new-project.md +96 -258
  292. package/pan-wizard-core/workflows/optimize.md +107 -5
  293. package/pan-wizard-core/workflows/phase-tests.md +10 -0
  294. package/pan-wizard-core/workflows/plan-phase.md +106 -45
  295. package/pan-wizard-core/workflows/profile.md +50 -0
  296. package/pan-wizard-core/workflows/progress.md +10 -0
  297. package/pan-wizard-core/workflows/quick.md +34 -43
  298. package/pan-wizard-core/workflows/remove-phase.md +11 -1
  299. package/pan-wizard-core/workflows/research-phase.md +118 -40
  300. package/pan-wizard-core/workflows/resume-project.md +18 -2
  301. package/pan-wizard-core/workflows/settings.md +33 -19
  302. package/pan-wizard-core/workflows/todo-check.md +6 -2
  303. package/pan-wizard-core/workflows/transition.md +22 -4
  304. package/pan-wizard-core/workflows/uat-walkthrough.md +90 -0
  305. package/pan-wizard-core/workflows/update.md +1 -1
  306. package/pan-wizard-core/workflows/verify-phase.md +39 -46
  307. package/scripts/build-agent-plugin.js +35 -15
  308. package/scripts/build-help.cjs +106 -0
  309. package/scripts/build-plugin.js +10 -1
  310. package/scripts/deprecate-old-versions.js +70 -3
  311. package/scripts/generate-skills-docs.py +1 -1
  312. package/scripts/git-hooks/pre-push +27 -0
  313. package/scripts/install-git-hooks.js +7 -5
  314. package/scripts/install-pre-push.cjs +38 -0
  315. package/scripts/land.cjs +151 -0
  316. package/scripts/local-ci.cjs +501 -0
  317. package/scripts/local-schedule.cjs +277 -0
  318. package/scripts/mutation-probe.cjs +7 -80
  319. package/scripts/prompt-lint.cjs +32 -0
  320. package/scripts/prompt-rules.cjs +156 -0
  321. package/scripts/run-tests.cjs +7 -1
  322. package/scripts/test-quality-lint.cjs +3 -3
  323. package/scripts/test-surface.cjs +175 -6
  324. package/agents/pan-research-synthesizer.md +0 -246
  325. package/commands/pan/audit-deployment.md +0 -386
  326. package/commands/pan/dashboard.md +0 -22
  327. package/commands/pan/discord.md +0 -19
  328. package/commands/pan/mcp-bridge.md +0 -143
  329. package/commands/pan/milestone-cleanup.md +0 -19
  330. package/commands/pan/milestone-gaps.md +0 -35
  331. package/commands/pan/optimize.md +0 -115
  332. package/commands/pan/profile.md +0 -76
  333. package/commands/pan/research-phase.md +0 -189
  334. package/pan-wizard-core/bin/lib/bridge.cjs +0 -269
  335. package/pan-wizard-core/templates/research-project/architecture.md +0 -204
  336. package/pan-wizard-core/templates/research-project/features.md +0 -147
  337. package/pan-wizard-core/templates/research-project/pitfalls.md +0 -200
  338. package/pan-wizard-core/templates/research-project/stack.md +0 -120
  339. package/pan-wizard-core/templates/research-project/summary.md +0 -170
@@ -0,0 +1,433 @@
1
+ 'use strict';
2
+ /**
3
+ * The engine-run test gate (program V2): the test suite is run by pan-tools, in any
4
+ * language, and its result is a record on disk, not a sentence a model wrote.
5
+ *
6
+ * Until now the verifier ran `npm test` itself, read the counts with grep and wrote
7
+ * `test_gate:` into its verification. That only worked for Node's own runner, and the
8
+ * result was the model's word. Here:
9
+ *
10
+ * command `workflow.test_command` from config.json, else detected: package.json's
11
+ * `test` script, pytest, `go test ./...`, cargo, Maven or Gradle, `make test`
12
+ * run `pan-tools test run --phase N [--baseline]` runs it and writes
13
+ * `{NN}-test-baseline.json` (before execution) or `{NN}-test-run.json`
14
+ * parse the runner's own output, by its shape: Node's runner (spec, TAP, JUnit),
15
+ * jest, pytest, go, cargo, and JUnit XML reports (Maven, Gradle, any runner
16
+ * that writes them), into counts and the ids of the failing tests
17
+ * classify passed · failed · skipped. A suite that exits non-zero and reports no
18
+ * failing test did not run, and that is `failed`, never `skipped`. With a
19
+ * baseline, a failure the baseline already had is pre-existing, not a
20
+ * regression; any new one fails the gate. `skipped` means there was nothing
21
+ * to run (no command, or a command that ran no tests)
22
+ *
23
+ * The verifier copies `test_gate` from the record, reconcile reports a verification
24
+ * that disagrees with it, and the verdict recorder takes a failed run as a code fact
25
+ * that lowers a pass (V9's two-key verdict).
26
+ */
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const { spawnSync } = require('child_process');
30
+ const { findPhaseInternal, execGit, output, error, toPosix, safeReadFile } = require('./core.cjs');
31
+ const { planningPath } = require('./utils.cjs');
32
+
33
+ const RECORD_VERSION = 1;
34
+ const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
35
+ const OUTPUT_TAIL = 4000;
36
+ const MAX_IDS = 200;
37
+ // npm init's placeholder script: a test script that is not a test suite.
38
+ const NPM_PLACEHOLDER_RE = /no test specified/i;
39
+
40
+ const stripAnsi = (s) => String(s || '').replace(/\u001b\[[0-9;]*[A-Za-z]/g, '');
41
+ const toLf = (s) => String(s || '').replace(/\r\n?/g, '\n');
42
+ // A file in a test id, relative to the project and with forward slashes, whatever OS
43
+ // wrote the output and whatever OS reads it: ids must match between a baseline and a run.
44
+ const posixRel = (cwd, file) => {
45
+ const f = String(file || '').replace(/\\/g, '/').replace(/\/{2,}/g, '/');
46
+ if (!f) return '';
47
+ const root = cwd ? String(cwd).replace(/\\/g, '/').replace(/\/+$/, '') : '';
48
+ if (root && f.toLowerCase().startsWith(`${root.toLowerCase()}/`)) return f.slice(root.length + 1);
49
+ return f.replace(/^\.\//, '');
50
+ };
51
+ const xmlText = (s) => String(s || '').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&#10;/g, '\n').replace(/&amp;/g, '&');
52
+ const num = (m) => (m ? Number(m) : 0);
53
+
54
+ // ─── Detection ───────────────────────────────────────────────────────────────
55
+
56
+ /** config.json as written (loadConfig flattens the workflow section away). */
57
+ function rawConfig(cwd) {
58
+ try { return JSON.parse(fs.readFileSync(planningPath(cwd, 'config.json'), 'utf8')); } catch { return {}; }
59
+ }
60
+
61
+ /**
62
+ * The project's test command.
63
+ * @returns {{command: string|null, source: string|null, reason?: string}}
64
+ */
65
+ function detectTestCommand(cwd, config = rawConfig(cwd)) {
66
+ const configured = config && config.workflow && typeof config.workflow.test_command === 'string' ? config.workflow.test_command.trim() : '';
67
+ if (configured) return { command: configured, source: 'config' };
68
+ const has = (f) => fs.existsSync(path.join(cwd, f));
69
+ const read = (f) => safeReadFile(path.join(cwd, f)) || '';
70
+ if (has('package.json')) {
71
+ let pkg = null;
72
+ try { pkg = JSON.parse(read('package.json')); } catch { /* unreadable reads as absent */ }
73
+ const script = pkg && pkg.scripts && typeof pkg.scripts.test === 'string' ? pkg.scripts.test : '';
74
+ if (script && !NPM_PLACEHOLDER_RE.test(script)) return { command: 'npm test', source: 'package.json' };
75
+ }
76
+ if (has('pytest.ini') || has('conftest.py') || /\[tool\.pytest/.test(read('pyproject.toml')) || /\[tool:pytest\]/.test(read('setup.cfg')) || /\[pytest\]/.test(read('tox.ini'))) {
77
+ return { command: 'python -m pytest', source: 'pytest' };
78
+ }
79
+ if (has('go.mod')) return { command: 'go test ./...', source: 'go.mod' };
80
+ if (has('Cargo.toml')) return { command: 'cargo test', source: 'Cargo.toml' };
81
+ if (has('pom.xml')) return { command: 'mvn -q test', source: 'pom.xml' };
82
+ if (has('build.gradle') || has('build.gradle.kts')) return { command: has('gradlew') ? './gradlew test' : 'gradle test', source: 'gradle' };
83
+ if (/^test\s*:/m.test(read('Makefile'))) return { command: 'make test', source: 'Makefile' };
84
+ return { command: null, source: null, reason: 'no test command: set workflow.test_command, or add a test script (package.json, pytest, go.mod, Cargo.toml, Maven, Gradle, Makefile)' };
85
+ }
86
+
87
+ /**
88
+ * The command that runs one test file (V4, V5), or null with a reason. A configured
89
+ * `workflow.test_file_command` with a `{file}` placeholder wins; otherwise it is derived
90
+ * from the suite's runner: node's runner, jest, vitest, mocha, pytest, or go (the file's
91
+ * package). Cargo, Maven, Gradle and make cannot target one file this way.
92
+ */
93
+ function fileCommand(cwd, file, config = rawConfig(cwd)) {
94
+ const quoted = JSON.stringify(String(file).replace(/\\/g, '/'));
95
+ const configured = config && config.workflow && typeof config.workflow.test_file_command === 'string' ? config.workflow.test_file_command.trim() : '';
96
+ if (configured) return configured.includes('{file}') ? { command: configured.split('{file}').join(quoted) } : { command: null, reason: 'workflow.test_file_command has no {file} placeholder' };
97
+ const suite = detectTestCommand(cwd, config);
98
+ let script = suite.command || '';
99
+ if (script === 'npm test') {
100
+ try { script = JSON.parse(safeReadFile(path.join(cwd, 'package.json')) || '{}').scripts.test || ''; } catch { script = ''; }
101
+ }
102
+ if (/\bnode\b[^&|;]*--test\b/.test(script)) return { command: `node --test ${quoted}` };
103
+ if (/\bjest\b/.test(script)) return { command: `npx --no-install jest ${quoted}` };
104
+ if (/\bvitest\b/.test(script)) return { command: `npx --no-install vitest run ${quoted}` };
105
+ if (/\bmocha\b/.test(script)) return { command: `npx --no-install mocha ${quoted}` };
106
+ if (/pytest/.test(script)) return { command: `python -m pytest ${quoted}` };
107
+ if (/^go test\b/.test(script)) return { command: `go test ./${path.posix.dirname(String(file).replace(/\\/g, '/'))}` };
108
+ return { command: null, reason: suite.command ? `no per-file command for "${suite.command}": set workflow.test_file_command with a {file} placeholder` : (suite.reason || 'no test command') };
109
+ }
110
+
111
+ /**
112
+ * Run a shell command with the gate's environment; `{exit, timed_out, output}`.
113
+ * NODE_TEST_CONTEXT is how node's runner marks its own children: a `node --test` that
114
+ * inherits it skips every file and exits 0, so a gate run from inside a test process
115
+ * would pass a suite that never ran.
116
+ */
117
+ function runCommand(cwd, command, timeoutMs = DEFAULT_TIMEOUT_MS) {
118
+ const env = { ...process.env, CI: process.env.CI || '1', FORCE_COLOR: '0', NO_COLOR: '1' };
119
+ delete env.NODE_TEST_CONTEXT;
120
+ const r = spawnSync(command, { cwd, shell: true, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024, env });
121
+ const timedOut = !!(r.error && r.error.code === 'ETIMEDOUT');
122
+ return { exit: typeof r.status === 'number' ? r.status : (timedOut ? null : 1), timed_out: timedOut, output: `${r.stdout || ''}\n${r.stderr || ''}` };
123
+ }
124
+
125
+ // ─── Parsers (each tested on real emitted output: tests/fixtures/test-runs/) ─
126
+
127
+ function result(runner, counts, failing, extra = {}) {
128
+ const ids = [...new Set(failing)].slice(0, MAX_IDS);
129
+ return { runner, parsed: true, total: counts.total ?? null, passed: counts.passed ?? null, failed: counts.failed ?? null, skipped: counts.skipped ?? null, failing: ids, ...extra };
130
+ }
131
+
132
+ /** Node's built-in runner, spec reporter. */
133
+ function parseNodeSpec(text, cwd) {
134
+ const count = (k) => { const m = text.match(new RegExp(`^ℹ ${k} (\\d+)`, 'm')); return m ? Number(m[1]) : null; };
135
+ const failing = [];
136
+ const at = text.indexOf('✖ failing tests:');
137
+ if (at !== -1) {
138
+ let file = '';
139
+ for (const line of text.slice(at).split('\n').slice(1)) {
140
+ const loc = line.match(/^test at (.+?):\d+:\d+$/);
141
+ if (loc) { file = posixRel(cwd, loc[1]); continue; }
142
+ const name = line.match(/^✖ (.+?) \([\d.]+m?s\)$/);
143
+ if (name) failing.push(file ? `${file} > ${name[1]}` : name[1]);
144
+ }
145
+ }
146
+ return result('node', { total: count('tests'), passed: count('pass'), failed: (count('fail') || 0) + (count('cancelled') || 0), skipped: (count('skipped') || 0) + (count('todo') || 0) }, failing);
147
+ }
148
+
149
+ /** Node's built-in runner, TAP reporter (and any TAP 13 producer with node's summary). */
150
+ function parseTap(text, cwd) {
151
+ const count = (k) => { const m = text.match(new RegExp(`^# ${k} (\\d+)`, 'm')); return m ? Number(m[1]) : null; };
152
+ const lines = text.split('\n');
153
+ const failing = [];
154
+ for (let i = 0; i < lines.length; i++) {
155
+ const m = lines[i].match(/^(\s*)not ok \d+ - (.+?)(?: # .*)?$/);
156
+ if (!m) continue;
157
+ // A suite is reported `not ok` when a child fails; only leaf tests are failures.
158
+ let type = 'test';
159
+ let file = '';
160
+ for (let j = i + 1; j < lines.length && !/^\s*\.\.\.\s*$/.test(lines[j]); j++) {
161
+ const t = lines[j].match(/^\s*type: '(\w+)'/);
162
+ if (t) type = t[1];
163
+ const loc = lines[j].match(/^\s*location: '(.+?):\d+:\d+'/);
164
+ if (loc) file = posixRel(cwd, loc[1].replace(/\\\\/g, '\\'));
165
+ if (/^\s*(not )?ok \d+/.test(lines[j])) break;
166
+ }
167
+ if (type === 'suite') continue;
168
+ failing.push(file ? `${file} > ${m[2]}` : m[2]);
169
+ }
170
+ return result('node-tap', { total: count('tests'), passed: count('pass'), failed: (count('fail') || 0) + (count('cancelled') || 0), skipped: (count('skipped') || 0) + (count('todo') || 0) }, failing);
171
+ }
172
+
173
+ /** jest (Windows and POSIX glyphs; ANSI already stripped). */
174
+ function parseJest(text) {
175
+ const testsLine = (text.match(/^Tests:\s+(.+)$/m) || [])[1] || '';
176
+ const pick = (line, k) => { const m = line.match(new RegExp(`(\\d+) ${k}`)); return m ? Number(m[1]) : 0; };
177
+ const failing = [];
178
+ let file = '';
179
+ let crashes = 0;
180
+ for (const line of text.split('\n')) {
181
+ const f = line.match(/^(?:FAIL|PASS)\s+(\S+)/);
182
+ if (f) { file = toPosix(f[1]); continue; }
183
+ const b = line.match(/^\s+● (.+)$/);
184
+ if (!b) continue;
185
+ if (/^Test suite failed to run$/.test(b[1].trim())) { crashes++; failing.push(`${file} (suite failed to run)`); continue; }
186
+ failing.push(`${file} > ${b[1].trim()}`);
187
+ }
188
+ // A suite that failed to run counts as one failure: its tests never reached the total.
189
+ return result('jest', { total: pick(testsLine, 'total'), passed: pick(testsLine, 'passed'), failed: pick(testsLine, 'failed') + crashes, skipped: pick(testsLine, 'skipped') + pick(testsLine, 'todo') }, failing, crashes ? { crashed: true } : {});
190
+ }
191
+
192
+ /** pytest. */
193
+ function parsePytest(text) {
194
+ const summary = [...text.matchAll(/^=+ (.+?) in [\d.]+s(?: \([^)]*\))? =+$/gm)].pop();
195
+ const s = summary ? summary[1] : '';
196
+ const pick = (k) => { const m = s.match(new RegExp(`(\\d+) ${k}\\b`)); return m ? Number(m[1]) : 0; };
197
+ const failed = pick('failed');
198
+ const errors = pick('errors') + pick('error');
199
+ const passed = pick('passed');
200
+ const skipped = pick('skipped') + pick('xfailed') + pick('deselected');
201
+ const failing = [...text.matchAll(/^(?:FAILED|ERROR) (\S+)/gm)].map((m) => m[1]);
202
+ const crashed = /error(s)? during collection/.test(text) || (errors > 0 && failed === 0 && passed === 0);
203
+ return result('pytest', { total: passed + failed + errors + skipped + pick('xpassed'), passed, failed: failed + errors, skipped }, failing, crashed ? { crashed: true } : {});
204
+ }
205
+
206
+ /** go test (plain or -v). */
207
+ function parseGo(text) {
208
+ const failing = [];
209
+ let pending = [];
210
+ let passCount = 0;
211
+ let failCount = 0;
212
+ let skipCount = 0;
213
+ let crashed = false;
214
+ let verbose = false;
215
+ for (const line of text.split('\n')) {
216
+ const t = line.match(/^\s*--- (FAIL|PASS|SKIP): (\S+)/);
217
+ if (t) {
218
+ if (t[1] === 'FAIL') { pending.push(t[2]); failCount++; } else if (t[1] === 'PASS') { passCount++; verbose = true; } else skipCount++;
219
+ continue;
220
+ }
221
+ const pkg = line.match(/^(ok|FAIL)\s+(\S+)(?:\s+\[([^\]]+)\])?/);
222
+ if (pkg && pkg[2] !== '') {
223
+ if (pkg[1] === 'FAIL' && pkg[3]) { crashed = true; failing.push(`${pkg[2]} [${pkg[3]}]`); }
224
+ if (pkg[1] === 'FAIL') failing.push(...pending.map((n) => `${pkg[2]} > ${n}`));
225
+ pending = [];
226
+ }
227
+ }
228
+ failing.push(...pending);
229
+ return result('go', { total: verbose ? passCount + failCount + skipCount : null, passed: verbose ? passCount : null, failed: failCount, skipped: skipCount }, failing, crashed ? { crashed: true } : {});
230
+ }
231
+
232
+ /** cargo test (every `test result:` line summed). */
233
+ function parseCargo(text) {
234
+ const totals = { passed: 0, failed: 0, skipped: 0 };
235
+ for (const m of text.matchAll(/^test result: \w+\. (\d+) passed; (\d+) failed; (\d+) ignored;/gm)) {
236
+ totals.passed += Number(m[1]); totals.failed += Number(m[2]); totals.skipped += Number(m[3]);
237
+ }
238
+ const failing = [...text.matchAll(/^test (\S+) \.\.\. FAILED$/gm)].map((m) => m[1]);
239
+ return result('cargo', { total: totals.passed + totals.failed + totals.skipped, ...totals }, failing);
240
+ }
241
+
242
+ /** JUnit XML: one report, or several concatenated (Maven, Gradle, pytest --junitxml, node --test-reporter=junit). */
243
+ function parseJUnit(text, cwd) {
244
+ let total = 0; let failed = 0; let skipped = 0;
245
+ const failing = [];
246
+ for (const m of text.matchAll(/<testcase\b([^>]*?)(\/>|>([\s\S]*?)<\/testcase>)/g)) {
247
+ total++;
248
+ const attr = (k) => { const a = m[1].match(new RegExp(`\\b${k}="([^"]*)"`)); return a ? xmlText(a[1]) : ''; };
249
+ const body = m[3] || '';
250
+ if (/<skipped\b/.test(body)) { skipped++; continue; }
251
+ if (/<(failure|error)\b/.test(body) || /\bfailure="/.test(m[1])) {
252
+ failed++;
253
+ const where = attr('file') ? posixRel(cwd, attr('file')) : attr('classname');
254
+ failing.push(where ? `${where} > ${attr('name')}` : attr('name'));
255
+ }
256
+ }
257
+ return result('junit', { total, passed: total - failed - skipped, failed, skipped }, failing);
258
+ }
259
+
260
+ /**
261
+ * Parse a runner's output by its shape. `parsed: false` when no known shape is found.
262
+ * @param {string} raw - stdout and stderr together
263
+ * @param {string} [cwd] - for making absolute file paths in ids relative
264
+ */
265
+ function parseTestOutput(raw, cwd) {
266
+ const text = toLf(stripAnsi(raw));
267
+ if (/<testcase\b/.test(text)) return parseJUnit(text, cwd);
268
+ if (/^# tests \d+/m.test(text) && /^\s*(not )?ok \d+/m.test(text)) return parseTap(text, cwd);
269
+ if (/^ℹ tests \d+/m.test(text)) return parseNodeSpec(text, cwd);
270
+ if (/^Tests:\s+.*\d+ total/m.test(text)) return parseJest(text);
271
+ if (/^=+ .*\b(passed|failed|errors?|skipped|no tests ran)\b.* in [\d.]+s.*=+$/m.test(text)) return parsePytest(text);
272
+ if (/^test result: /m.test(text)) return parseCargo(text);
273
+ if (/^(ok|FAIL)\s+\S+/m.test(text) || /^\s*--- (FAIL|PASS):/m.test(text)) return parseGo(text);
274
+ return { runner: null, parsed: false, total: null, passed: null, failed: null, skipped: null, failing: [] };
275
+ }
276
+
277
+ /** JUnit XML reports a Maven or Gradle run leaves behind, read after the run. */
278
+ function junitReports(cwd) {
279
+ const dirs = ['target/surefire-reports', 'target/failsafe-reports', 'build/test-results/test'];
280
+ const out = [];
281
+ for (const d of dirs) {
282
+ let names = [];
283
+ try { names = fs.readdirSync(path.join(cwd, d)).filter((n) => /\.xml$/i.test(n)); } catch { continue; }
284
+ for (const n of names) out.push(safeReadFile(path.join(cwd, d, n)) || '');
285
+ }
286
+ return out.join('\n');
287
+ }
288
+
289
+ // ─── Classification ──────────────────────────────────────────────────────────
290
+
291
+ /**
292
+ * passed · failed · skipped, from one run and an optional baseline. Pure.
293
+ * @param {{command: string|null, exit: number|null, timed_out?: boolean, parse: Object, reason?: string}} run
294
+ * @param {Object|null} baseline - an earlier record of the same command
295
+ */
296
+ function classifyRun(run, baseline) {
297
+ if (!run.command) return { status: 'skipped', reason: run.reason || 'no test command', regressions: [], preexisting: [] };
298
+ const p = run.parse || { parsed: false, failing: [] };
299
+ if (run.timed_out) return { status: 'failed', reason: 'the suite timed out', regressions: [], preexisting: [] };
300
+ if (run.exit === 0) {
301
+ if (p.parsed && p.failed > 0) return { status: 'failed', reason: `the runner exited 0 but reported ${p.failed} failure(s)`, regressions: p.failing, preexisting: [] };
302
+ if (p.parsed && p.total === 0) return { status: 'skipped', reason: 'the test command ran no tests', regressions: [], preexisting: [] };
303
+ return { status: 'passed', reason: null, regressions: [], preexisting: [] };
304
+ }
305
+ // pytest's "no tests collected" exit code.
306
+ if (run.exit === 5 && p.runner === 'pytest' && !p.failing.length) return { status: 'skipped', reason: 'the test command ran no tests', regressions: [], preexisting: [] };
307
+ if (!p.parsed || !p.failing.length || p.crashed) {
308
+ return { status: 'failed', reason: `suite did not run: exit ${run.exit}${p.parsed ? '' : ', no test result in its output'}`, regressions: p.failing, preexisting: [] };
309
+ }
310
+ const base = baseline && baseline.command === run.command && Array.isArray(baseline.failing) && baseline.parsed !== false ? new Set(baseline.failing) : null;
311
+ if (!base) return { status: 'failed', reason: `${p.failing.length} failing test(s)`, regressions: p.failing, preexisting: [] };
312
+ const regressions = p.failing.filter((id) => !base.has(id));
313
+ const preexisting = p.failing.filter((id) => base.has(id));
314
+ if (regressions.length) return { status: 'failed', reason: `${regressions.length} regression(s): failing now, passing at the baseline`, regressions, preexisting };
315
+ return { status: 'passed', reason: `${preexisting.length} failure(s) already failing at the baseline (not regressions)`, regressions: [], preexisting };
316
+ }
317
+
318
+ // ─── Running ─────────────────────────────────────────────────────────────────
319
+
320
+ function headOf(cwd) {
321
+ const r = execGit(cwd, ['rev-parse', 'HEAD']);
322
+ return r.exitCode === 0 ? r.stdout.trim() : null;
323
+ }
324
+
325
+ /** Record paths for a phase. */
326
+ function recordPaths(cwd, info) {
327
+ const dir = path.join(cwd, info.directory);
328
+ const prefix = String(info.phase_number).split('.').map((p, i) => (i === 0 ? p.padStart(2, '0') : p)).join('.');
329
+ return { dir, run: path.join(dir, `${prefix}-test-run.json`), baseline: path.join(dir, `${prefix}-test-baseline.json`) };
330
+ }
331
+
332
+ function readRecord(file) {
333
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
334
+ }
335
+
336
+ /**
337
+ * Run the project's test command and classify the result.
338
+ * @param {string} cwd
339
+ * @param {{command?: string, timeoutMs?: number, baseline?: Object|null, kind?: 'run'|'baseline', phase?: string}} [opts]
340
+ * @returns {Object} the record
341
+ */
342
+ function runTests(cwd, opts = {}) {
343
+ const detected = opts.command ? { command: opts.command, source: 'flag' } : detectTestCommand(cwd);
344
+ const started = Date.now();
345
+ const rec = {
346
+ version: RECORD_VERSION, kind: opts.kind || 'run', phase: opts.phase || null,
347
+ command: detected.command, source: detected.source, head: headOf(cwd), started_at: new Date(started).toISOString(),
348
+ };
349
+ if (!detected.command) {
350
+ const c = classifyRun({ command: null, reason: detected.reason });
351
+ return { ...rec, exit: null, duration_ms: 0, runner: null, parsed: false, total: null, passed: null, failed: null, skipped: null, failing: [], status: c.status, reason: c.reason, regressions: [], preexisting_failures: [] };
352
+ }
353
+ const r = runCommand(cwd, detected.command, opts.timeoutMs || DEFAULT_TIMEOUT_MS);
354
+ const raw = r.output;
355
+ const timedOut = r.timed_out;
356
+ let parse = parseTestOutput(raw, cwd);
357
+ if (!parse.parsed && /^(mvn|gradle|\.\/gradlew)\b/.test(detected.command)) {
358
+ const reports = junitReports(cwd);
359
+ if (reports) parse = parseJUnit(toLf(reports), cwd);
360
+ }
361
+ const exit = r.exit;
362
+ const c = classifyRun({ command: detected.command, exit, timed_out: timedOut, parse }, opts.baseline || null);
363
+ return {
364
+ ...rec, exit, timed_out: timedOut || undefined, duration_ms: Date.now() - started,
365
+ runner: parse.runner, parsed: parse.parsed, total: parse.total, passed: parse.passed, failed: parse.failed, skipped: parse.skipped,
366
+ failing: parse.failing, status: c.status, reason: c.reason, regressions: c.regressions, preexisting_failures: c.preexisting,
367
+ baseline: opts.baseline ? { head: opts.baseline.head, status: opts.baseline.status, failing: (opts.baseline.failing || []).length } : undefined,
368
+ output_tail: toLf(stripAnsi(raw)).slice(-OUTPUT_TAIL),
369
+ };
370
+ }
371
+
372
+ /** The phase's latest test-run record, or null. */
373
+ function readTestRun(cwd, phase) {
374
+ const info = findPhaseInternal(cwd, phase);
375
+ if (!info) return null;
376
+ return readRecord(recordPaths(cwd, info).run);
377
+ }
378
+
379
+ /**
380
+ * The code fact a phase's test run gives the verdict recorder (V9): a failed run lowers a
381
+ * pass. None when the phase has no run record.
382
+ */
383
+ function testRunFacts(cwd, phase) {
384
+ const r = readTestRun(cwd, phase);
385
+ if (!r || !r.status) return [];
386
+ if (r.status === 'failed') return [{ fact: 'test_run', ok: false, summary: `The engine's test run failed: ${r.reason || 'see the test-run record'}` }];
387
+ if (r.status === 'passed') return [{ fact: 'test_run', ok: true, summary: 'the engine\'s test run passed' }];
388
+ return [];
389
+ }
390
+
391
+ /**
392
+ * `pan-tools test run [--phase N] [--baseline] [--command "<cmd>"] [--timeout <ms>]`.
393
+ * With --phase, the record is written to the phase directory; a run (not a baseline)
394
+ * is compared with the phase's baseline when there is one.
395
+ */
396
+ function cmdTestRun(cwd, opts, raw) {
397
+ let paths = null;
398
+ let baseline = null;
399
+ if (opts.phase) {
400
+ const info = findPhaseInternal(cwd, opts.phase);
401
+ if (!info) error(`phase ${opts.phase} not found`);
402
+ paths = recordPaths(cwd, info);
403
+ if (!opts.baseline) baseline = readRecord(paths.baseline);
404
+ // A baseline is the phase's starting point. A resumed run or a gaps-only round that
405
+ // re-baselined would excuse the regressions of the first round, so the first one is
406
+ // kept unless --rebaseline asks otherwise.
407
+ const existing = opts.baseline && !opts.rebaseline ? readRecord(paths.baseline) : null;
408
+ if (existing) {
409
+ output({ ...existing, kept_existing: true, record: toPosix(path.relative(cwd, paths.baseline)) }, raw, existing.status);
410
+ return;
411
+ }
412
+ }
413
+ const timeoutMs = opts.timeout ? Number(opts.timeout) : undefined;
414
+ const rec = runTests(cwd, { command: opts.command, timeoutMs, baseline, kind: opts.baseline ? 'baseline' : 'run', phase: opts.phase || null });
415
+ if (paths) {
416
+ const file = opts.baseline ? paths.baseline : paths.run;
417
+ fs.writeFileSync(file, JSON.stringify(rec, null, 2) + '\n');
418
+ rec.record = toPosix(path.relative(cwd, file));
419
+ }
420
+ output(rec, raw, rec.status);
421
+ }
422
+
423
+ /** `pan-tools test detect`: the command the gate would run, and why. */
424
+ function cmdTestDetect(cwd, raw) {
425
+ const d = detectTestCommand(cwd);
426
+ output(d, raw, d.command || '');
427
+ }
428
+
429
+ module.exports = {
430
+ detectTestCommand, parseTestOutput, parseNodeSpec, parseTap, parseJest, parsePytest, parseGo, parseCargo, parseJUnit,
431
+ classifyRun, runTests, readTestRun, testRunFacts, recordPaths, cmdTestRun, cmdTestDetect, stripAnsi,
432
+ fileCommand, runCommand, rawConfig,
433
+ };
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+ /**
3
+ * UAT through the engine (program L8): the walkthrough is a portable markdown workflow
4
+ * (workflows/uat-walkthrough.md, `/pan:verify-phase N --uat`), and what it records goes
5
+ * through these verbs, so every runtime leaves the same `{phase}-uat.md`:
6
+ *
7
+ * uat record <phase> --test N --result pass|issue|skipped [--reported "…"] [--severity S] [--reason "…"]
8
+ * sets one test's result, then rebuilds `## Summary` and `## Gaps` (the YAML list
9
+ * diagnose-issues and `plan-phase --gaps` read). Diagnosis fields a gap already
10
+ * carries (root_cause, artifacts, missing, debug_session) survive a re-record.
11
+ * The session is `complete` when no test is pending.
12
+ * uat status <phase>
13
+ * counts and the gaps, for the walkthrough to decide whether to diagnose.
14
+ *
15
+ * The file shape is templates/uat.md.
16
+ */
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const { findPhaseInternal, output, error, toLf, dominantEol, withEol } = require('./core.cjs');
20
+
21
+ const RESULTS = Object.freeze(['pass', 'issue', 'skipped']);
22
+ const SEVERITIES = Object.freeze(['blocker', 'major', 'minor', 'cosmetic']);
23
+ const KEEP_GAP_FIELDS = Object.freeze(['root_cause', 'artifacts', 'missing', 'debug_session']);
24
+
25
+ const unquote = (s) => String(s || '').trim().replace(/^"(.*)"$/, '$1');
26
+ const quote = (s) => `"${String(s || '').replace(/"/g, "'")}"`;
27
+
28
+ function uatPath(cwd, phase) {
29
+ const info = findPhaseInternal(cwd, phase);
30
+ if (!info || !info.found) return null;
31
+ const dir = path.join(cwd, info.directory);
32
+ let files = [];
33
+ try { files = fs.readdirSync(dir).filter((f) => /-uat\.md$/i.test(f)); } catch { /* none */ }
34
+ return files.length ? path.join(dir, files[0]) : null;
35
+ }
36
+
37
+ /** Parse a uat.md: frontmatter status, the tests, and the gaps' extra fields by test number. */
38
+ function parseUat(raw) {
39
+ const text = toLf(String(raw || ''));
40
+ const status = ((/^---\n[\s\S]*?^status:\s*(.+)$/m.exec(text) || [])[1] || '').trim();
41
+ const tests = [];
42
+ const testsSec = (/^## Tests\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(text) || [])[1] || '';
43
+ for (const block of testsSec.split(/^(?=### \d+\.)/m)) {
44
+ const h = /^### (\d+)\.\s*(.+)$/m.exec(block);
45
+ if (!h) continue;
46
+ const field = (k) => ((new RegExp(`^${k}:\\s*(.*)$`, 'm').exec(block) || [])[1] || '').trim();
47
+ let result = field('result').replace(/^\[|\]$/g, '') || 'pending';
48
+ tests.push({ number: Number(h[1]), name: h[2].trim(), expected: field('expected'), result, reported: unquote(field('reported')) || null, severity: field('severity') || null, reason: field('reason') || null });
49
+ }
50
+ const gapsSec = (/^## Gaps\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(text) || [])[1] || '';
51
+ const gapExtras = new Map();
52
+ for (const item of gapsSec.split(/^(?=- truth:)/m)) {
53
+ const n = /^\s+test:\s*(\d+)/m.exec(item);
54
+ if (!n) continue;
55
+ const extras = {};
56
+ for (const k of KEEP_GAP_FIELDS) {
57
+ const m = new RegExp(`^\\s+${k}:\\s*(.*)$`, 'm').exec(item);
58
+ if (m && m[1].trim() && m[1].trim() !== '""' && m[1].trim() !== '[]') extras[k] = m[1].trim();
59
+ }
60
+ gapExtras.set(Number(n[1]), extras);
61
+ }
62
+ return { status, tests, gapExtras };
63
+ }
64
+
65
+ function renderTest(t) {
66
+ const lines = [`### ${t.number}. ${t.name}`, `expected: ${t.expected}`, `result: ${t.result === 'pending' ? '[pending]' : t.result}`];
67
+ if (t.result === 'issue') {
68
+ lines.push(`reported: ${quote(t.reported)}`);
69
+ lines.push(`severity: ${t.severity || 'major'}`);
70
+ }
71
+ if (t.result === 'skipped' && t.reason) lines.push(`reason: ${t.reason}`);
72
+ return lines.join('\n');
73
+ }
74
+
75
+ function summaryOf(tests) {
76
+ const n = (r) => tests.filter((t) => t.result === r).length;
77
+ return { total: tests.length, passed: n('pass'), issues: n('issue'), pending: n('pending'), skipped: n('skipped') };
78
+ }
79
+
80
+ function gapsOf(tests, gapExtras) {
81
+ return tests.filter((t) => t.result === 'issue').map((t) => ({
82
+ truth: t.expected, status: 'failed', reason: `User reported: ${t.reported || ''}`.trim(), severity: t.severity || 'major', test: t.number,
83
+ ...(gapExtras.get(t.number) || {}),
84
+ }));
85
+ }
86
+
87
+ function renderGaps(gaps) {
88
+ return gaps.map((g) => {
89
+ const lines = [`- truth: ${quote(g.truth)}`, ` status: ${g.status}`, ` reason: ${quote(g.reason)}`, ` severity: ${g.severity}`, ` test: ${g.test}`];
90
+ for (const k of KEEP_GAP_FIELDS) if (g[k]) lines.push(` ${k}: ${g[k]}`);
91
+ return lines.join('\n');
92
+ }).join('\n');
93
+ }
94
+
95
+ /** Set one test's result and rebuild the summary and the gaps. */
96
+ function recordUatResult(cwd, phase, input) {
97
+ const file = uatPath(cwd, phase);
98
+ if (!file) return { error: 'uat_not_found', reason: `no {phase}-uat.md in phase ${phase} (create one with scaffold uat)` };
99
+ if (!RESULTS.includes(input.result)) return { error: 'invalid_result', reason: `result is one of ${RESULTS.join(', ')}` };
100
+ if (input.severity && !SEVERITIES.includes(input.severity)) return { error: 'invalid_severity', reason: `severity is one of ${SEVERITIES.join(', ')}` };
101
+ const raw = fs.readFileSync(file, 'utf8');
102
+ const eol = dominantEol(raw);
103
+ let text = toLf(raw);
104
+ const u = parseUat(text);
105
+ const t = u.tests.find((x) => x.number === Number(input.test));
106
+ if (!t) return { error: 'test_not_found', reason: `uat.md has no test ${input.test}` };
107
+ t.result = input.result;
108
+ t.reported = input.result === 'issue' ? (input.reported || t.reported || '') : null;
109
+ t.severity = input.result === 'issue' ? (input.severity || t.severity || 'major') : null;
110
+ t.reason = input.result === 'skipped' ? (input.reason || t.reason || null) : null;
111
+
112
+ const testsBody = '\n' + u.tests.map(renderTest).join('\n\n') + '\n\n';
113
+ text = text.replace(/^## Tests\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, `## Tests\n${testsBody}`);
114
+ const s = summaryOf(u.tests);
115
+ const summary = `## Summary\n\ntotal: ${s.total}\npassed: ${s.passed}\nissues: ${s.issues}\npending: ${s.pending}\nskipped: ${s.skipped}\n\n`;
116
+ text = /^## Summary\s*$/m.test(text) ? text.replace(/^## Summary\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, summary) : text + `\n${summary}`;
117
+ const gaps = gapsOf(u.tests, u.gapExtras);
118
+ const gapsBlock = `## Gaps\n\n<!-- YAML format for plan-phase --gaps consumption -->\n${gaps.length ? renderGaps(gaps) + '\n' : ''}`;
119
+ text = /^## Gaps\s*$/m.test(text) ? text.replace(/^## Gaps\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, gapsBlock) : text + `\n${gapsBlock}`;
120
+ const now = new Date().toISOString();
121
+ text = text.replace(/^updated:.*$/m, `updated: ${now}`);
122
+ if (s.pending === 0) text = text.replace(/^status:\s*testing\s*$/m, 'status: complete');
123
+ fs.writeFileSync(file, withEol(text, eol));
124
+ return { recorded: true, test: t.number, result: t.result, ...s, complete: s.pending === 0, gaps: gaps.length };
125
+ }
126
+
127
+ function uatStatus(cwd, phase) {
128
+ const file = uatPath(cwd, phase);
129
+ if (!file) return { error: 'uat_not_found', reason: `no {phase}-uat.md in phase ${phase}` };
130
+ const u = parseUat(fs.readFileSync(file, 'utf8'));
131
+ const s = summaryOf(u.tests);
132
+ return { file: path.basename(file), status: u.status, ...s, complete: s.pending === 0, gaps: gapsOf(u.tests, u.gapExtras) };
133
+ }
134
+
135
+ function cmdUatRecord(cwd, phase, opts, raw) {
136
+ if (!phase || !opts.test || !opts.result) error('Usage: uat record <phase> --test N --result pass|issue|skipped [--reported "…"] [--severity S] [--reason "…"]');
137
+ const r = recordUatResult(cwd, phase, opts);
138
+ if (r.error) { output(r, raw, 'error', 1); return; }
139
+ output(r, raw, `${r.test}: ${r.result}`);
140
+ }
141
+
142
+ function cmdUatStatus(cwd, phase, raw) {
143
+ if (!phase) error('Usage: uat status <phase>');
144
+ const r = uatStatus(cwd, phase);
145
+ if (r.error) { output(r, raw, 'error', 1); return; }
146
+ output(r, raw, `${r.passed} passed, ${r.issues} issues, ${r.pending} pending`);
147
+ }
148
+
149
+ module.exports = { parseUat, recordUatResult, uatStatus, cmdUatRecord, cmdUatStatus };