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.
- package/CHANGELOG.md +3645 -0
- package/README.md +46 -52
- package/agents/pan-conductor.md +2 -3
- package/agents/pan-counterfactual.md +1 -2
- package/agents/pan-debugger.md +14 -3
- package/agents/pan-design-checker.md +1 -2
- package/agents/pan-designer.md +2 -3
- package/agents/pan-document_code.md +1 -2
- package/agents/pan-executor.md +25 -0
- package/agents/pan-hardener.md +4 -3
- package/agents/pan-integration-checker.md +1 -2
- package/agents/pan-knowledge.md +1 -2
- package/agents/pan-meta-reviewer.md +3 -3
- package/agents/pan-optimizer.md +4 -4
- package/agents/pan-phase-researcher.md +80 -475
- package/agents/pan-plan-checker.md +40 -3
- package/agents/pan-planner.md +21 -7
- package/agents/pan-previewer.md +3 -2
- package/agents/pan-project-researcher.md +72 -493
- package/agents/pan-release.md +1 -1
- package/agents/pan-reviewer.md +14 -3
- package/agents/pan-roadmapper.md +12 -7
- package/agents/pan-verifier.md +9 -4
- package/bin/install-lib.cjs +134 -14
- package/bin/install.js +59 -12
- package/commands/pan/army.md +1 -1
- package/commands/pan/autodev.md +76 -0
- package/commands/pan/cost.md +21 -1
- package/commands/pan/exec-phase.md +3 -4
- package/commands/pan/focus-auto.md +14 -7
- package/commands/pan/focus-design.md +4 -5
- package/commands/pan/focus-docs.md +24 -0
- package/commands/pan/learn.md +13 -6
- package/commands/pan/milestone-audit.md +8 -1
- package/commands/pan/milestone-done.md +10 -5
- package/commands/pan/milestone-new.md +2 -2
- package/commands/pan/new-project.md +15 -12
- package/commands/pan/plan-phase.md +15 -9
- package/commands/pan/preview.md +1 -1
- package/commands/pan/quick.md +1 -1
- package/commands/pan/review-deep.md +11 -5
- package/commands/pan/settings.md +6 -0
- package/commands/pan/verify-phase.md +6 -2
- package/docs/AGENTS.md +917 -0
- package/docs/ARCHITECTURE.md +1054 -0
- package/docs/CLI-REFERENCE.md +4582 -0
- package/docs/COMPARISON.md +116 -0
- package/docs/DEVELOPMENT.md +490 -0
- package/docs/ECOSYSTEM-REVIEW-2026-06.md +107 -0
- package/docs/ECOSYSTEM-REVIEW-2026-08.md +349 -0
- package/docs/EXAMPLES.md +584 -0
- package/docs/FAQ.md +254 -0
- package/docs/FIELD-HARVEST-2026-07.md +88 -0
- package/docs/FIELD-REPORT-army-2026-06.md +121 -0
- package/docs/HOOKS.md +638 -0
- package/docs/IMPROVEMENT-TODO.md +246 -0
- package/docs/INTERNALS.md +485 -0
- package/docs/MIGRATION-v2-to-v3.md +258 -0
- package/docs/SECURITY-HARDENING.md +230 -0
- package/docs/SKILLS-ENHANCEMENT-PLAN-V2.md +122 -0
- package/docs/SKILLS-FULL-TEXT.md +9685 -0
- package/docs/SKILLS-REFERENCE.md +1037 -0
- package/docs/TROUBLESHOOTING.md +1084 -0
- package/docs/USER-GUIDE.md +1437 -0
- package/docs/archive/SKILLS-ENHANCEMENT-PLAN.md +230 -0
- package/docs/audits/AUDIT-2026-08-e2e-ROADMAP.md +117 -0
- package/docs/audits/AUDIT-2026-08-e2e-VERIFICATION.md +693 -0
- package/docs/audits/AUDIT-2026-08-e2e.md +1546 -0
- package/docs/audits/OUTSTANDING-2026-08.md +275 -0
- package/docs/audits/STABILITY-TEST-2026-08.md +640 -0
- package/docs/branding/README-brand-draft.md +119 -0
- package/docs/branding/handoff/IMPLEMENTATION.md +205 -0
- package/docs/branding/handoff/README-brand-draft.md +119 -0
- package/docs/branding/handoff/image-prompts.md +46 -0
- package/docs/branding/image-prompts.md +46 -0
- package/docs/branding/nano-banana-prompts.txt +60 -0
- package/docs/context-monitor.md +13 -0
- package/docs/decisions/ADR-0001-strategic-feature-prioritization.md +83 -0
- package/docs/decisions/ADR-0002-copilot-cli-runtime.md +71 -0
- package/docs/decisions/ADR-0003-smart-execution.md +84 -0
- package/docs/decisions/ADR-0004-copilot-cli-interaction.md +89 -0
- package/docs/decisions/ADR-0005-command-naming.md +80 -0
- package/docs/decisions/ADR-0006-focus-commands.md +101 -0
- package/docs/decisions/ADR-0007-ship-v1.md +73 -0
- package/docs/decisions/ADR-0008-map-codebase-mermaid-togaf.md +80 -0
- package/docs/decisions/ADR-0009-production-deployment-checklist.md +68 -0
- package/docs/decisions/ADR-0010-standards-integration.md +79 -0
- package/docs/decisions/ADR-0011-help-commands-rewrite.md +64 -0
- package/docs/decisions/ADR-0012-contributors-attribution-index.md +42 -0
- package/docs/decisions/ADR-0013-strategic-feature-additions.md +53 -0
- package/docs/decisions/ADR-0014-internal-cleanup-code-quality.md +53 -0
- package/docs/decisions/ADR-0015-focus-auto-runner.md +296 -0
- package/docs/decisions/ADR-0016-enhanced-doc-sync.md +54 -0
- package/docs/decisions/ADR-0017-enhanced-focus-design.md +69 -0
- package/docs/decisions/ADR-0018-user-reality-testing.md +84 -0
- package/docs/decisions/ADR-0019-end-to-end-user-acceptance-testing.md +113 -0
- package/docs/decisions/ADR-0020-ai-drift-prevention.md +55 -0
- package/docs/decisions/ADR-0021-codebase-mapper-v2.md +76 -0
- package/docs/decisions/ADR-0022-lifecycle-completeness.md +63 -0
- package/docs/decisions/ADR-0023-opus-4-7-adoption.md +89 -0
- package/docs/decisions/ADR-0024-spec-b-v2-completion.md +137 -0
- package/docs/decisions/ADR-0025-behavioral-guardrails-layer.md +129 -0
- package/docs/decisions/ADR-0026-self-improvement-loop.md +171 -0
- package/docs/decisions/ADR-0027-doc-code-link-graph.md +164 -0
- package/docs/decisions/ADR-0028-unified-agents-skills-target.md +46 -0
- package/docs/decisions/ADR-0029-experiment-evaluator.md +76 -0
- package/docs/decisions/ADR-0030-planning-concurrency.md +37 -0
- package/docs/decisions/ADR-0031-focus-auto-backlog-source.md +47 -0
- package/docs/decisions/ADR-0032-squad-model.md +64 -0
- package/docs/decisions/ADR-0033-army-campaign.md +40 -0
- package/docs/decisions/ADR-0034-scheduled-campaigns.md +57 -0
- package/docs/decisions/ADR-0035-army-hud-dashboard.md +40 -0
- package/docs/decisions/ADR-0036-agentic-memory-architecture.md +106 -0
- package/docs/decisions/ADR-0037-pin-security-agents-to-opus.md +51 -0
- package/docs/decisions/ADR-0038-skill-aligned-decomposition.md +41 -0
- package/docs/decisions/ADR-0039-worktree-as-project-variant.md +37 -0
- package/docs/decisions/ADR-0040-memory-injection-defense.md +55 -0
- package/docs/decisions/ADR-0041-mcp-dual-era-bridge.md +47 -0
- package/docs/decisions/ADR-0042-design-parity-both-flows.md +45 -0
- package/docs/decisions/ADR-0043-addressable-planning-roots.md +56 -0
- package/docs/decisions/ADR-0044-bound-the-cached-context.md +104 -0
- package/docs/decisions/ADR-0045-agent-plugins-bundle.md +76 -0
- package/docs/decisions/ADR-0046-subagent-frontmatter-decisions.md +39 -0
- package/docs/decisions/ADR-0047-pan-harness.md +76 -0
- package/docs/decisions/ADR-0048-one-core-per-project.md +43 -0
- package/docs/decisions/ADR-0049-machine-verdicts-and-findings-ledger.md +116 -0
- package/docs/specs/ai_drift_prevention_featureai.md +286 -0
- package/docs/specs/codebase_mapper_v2_featureai.md +930 -0
- package/docs/specs/command_naming_optimization_featureai.md +930 -0
- package/docs/specs/comp_analysis_markdown_v1.md +149 -0
- package/docs/specs/continuous_autonomous_execution_featureai.md +304 -0
- package/docs/specs/contributors_attribution_index_featureai.md +476 -0
- package/docs/specs/copilot_cli_interaction_featureai.md +868 -0
- package/docs/specs/copilot_cli_runtime_featureai.md +647 -0
- package/docs/specs/custom-skills-assessment.md +461 -0
- package/docs/specs/deployment_audit_findings_featureai.md +243 -0
- package/docs/specs/doc_code_link_graph_featureai.md +253 -0
- package/docs/specs/documentation_alignment_cleanup_featureai.md +69 -0
- package/docs/specs/e2e_visual_testing_featureai.md +615 -0
- package/docs/specs/end_to_end_user_acceptance_testing_featureai.md +810 -0
- package/docs/specs/evidence-loop-plan.md +76 -0
- package/docs/specs/evidence_loop_featureai.md +497 -0
- package/docs/specs/focus-auto-optimize-category.md +449 -0
- package/docs/specs/focus-auto-optimize-superplan.md +188 -0
- package/docs/specs/focus_auto_runner_featureai.md +1102 -0
- package/docs/specs/focus_design_v2_featureai.md +197 -0
- package/docs/specs/focus_sync_v2_featureai.md +202 -0
- package/docs/specs/git_integration_overhaul_featureai.md +326 -0
- package/docs/specs/glm_zai_provider_featureai.md +200 -0
- package/docs/specs/googlecli_adoption_featureai.md +661 -0
- package/docs/specs/help_commands_rewrite_featureai.md +860 -0
- package/docs/specs/industry_analysis_strategic_additions_featureai.md +231 -0
- package/docs/specs/industry_comparison_and_feature_gaps_featureai.md +674 -0
- package/docs/specs/internal_cleanup_code_quality_featureai.md +486 -0
- package/docs/specs/investigations/README.md +31 -0
- package/docs/specs/investigations/digests/2026-09-28.md +47 -0
- package/docs/specs/investigations/digests/2026-10-08.md +62 -0
- package/docs/specs/investigations/digests/2026-10-10.md +61 -0
- package/docs/specs/investigations/features/debugger-red-loop-first.md +55 -0
- package/docs/specs/investigations/features/harness-cross-host-behaviour.md +93 -0
- package/docs/specs/investigations/features/optimize-apply-regression-gate.md +66 -0
- package/docs/specs/investigations/features/reviewer-smell-baseline.md +57 -0
- package/docs/specs/investigations/features/trace-failure-capture.md +69 -0
- package/docs/specs/lifecycle_completeness_featureai.md +754 -0
- package/docs/specs/local-first-ci.md +42 -0
- package/docs/specs/map_codebase_mermaid_togaf_featureai.md +805 -0
- package/docs/specs/market-delta-2026-09-superplan.md +310 -0
- package/docs/specs/market-ideas-2026-09.md +131 -0
- package/docs/specs/market-ideas-2026-10.md +428 -0
- package/docs/specs/market-ideas-ledger.md +177 -0
- package/docs/specs/memory-optimization-2026-10.md +285 -0
- package/docs/specs/multi-track-planning-roots.md +143 -0
- package/docs/specs/multi_model_routing_featureai.md +687 -0
- package/docs/specs/multi_model_routing_superplan.md +197 -0
- package/docs/specs/opus_47_existing_enhancements_featureai.md +247 -0
- package/docs/specs/opus_47_extended_features_featureai.md +363 -0
- package/docs/specs/opus_47_extended_features_v2_featureai.md +397 -0
- package/docs/specs/pan_focus_commands_featureai.md +2336 -0
- package/docs/specs/pan_zcode_mcp_bridge_featureai.md +120 -0
- package/docs/specs/product-excellence-superplan.md +315 -0
- package/docs/specs/product_excellence_program_2026_q4_featureai.md +365 -0
- package/docs/specs/production_deployment_readiness_featureai.md +433 -0
- package/docs/specs/production_readiness_ship_v1_featureai.md +698 -0
- package/docs/specs/real_test_enforcement_featureai.md +213 -0
- package/docs/specs/reality-check-2026-09-superplan.md +362 -0
- package/docs/specs/right-sized-ceremony.md +74 -0
- package/docs/specs/roadmap_and_competition_2026_q4_featureai.md +390 -0
- package/docs/specs/self_improvement_loop_featureai.md +875 -0
- package/docs/specs/skill-aligned-decomposition.md +155 -0
- package/docs/specs/standards_integration_featureai.md +833 -0
- package/docs/specs/testing-system-redesign-2026-09.md +267 -0
- package/docs/specs/user_reality_testing_system_featureai.md +788 -0
- package/docs/specs/watch-triggers.json +22 -0
- package/docs/specs/workflow-integration-redesign.md +742 -0
- package/docs/specs/workflow_state_consistency_featureai.md +183 -0
- package/hooks/dist/pan-context-monitor.js +9 -2
- package/hooks/dist/pan-cost-logger.js +5 -0
- package/hooks/dist/pan-guard.js +28 -5
- package/hooks/dist/pan-stop-guard.js +9 -9
- package/hooks/dist/pan-trace-logger.js +5 -0
- package/package.json +10 -2
- package/pan-wizard-core/bin/lib/agents-md.cjs +57 -8
- package/pan-wizard-core/bin/lib/autodev-report.cjs +180 -0
- package/pan-wizard-core/bin/lib/autodev.cjs +642 -0
- package/pan-wizard-core/bin/lib/ci.cjs +143 -0
- package/pan-wizard-core/bin/lib/commands.cjs +1 -1
- package/pan-wizard-core/bin/lib/config.cjs +41 -5
- package/pan-wizard-core/bin/lib/constants.cjs +27 -1
- package/pan-wizard-core/bin/lib/core.cjs +31 -14
- package/pan-wizard-core/bin/lib/cost.cjs +160 -4
- package/pan-wizard-core/bin/lib/cross-family.cjs +164 -0
- package/pan-wizard-core/bin/lib/debug-cases.cjs +104 -0
- package/pan-wizard-core/bin/lib/doc-lint.cjs +5 -3
- package/pan-wizard-core/bin/lib/failure-class.cjs +81 -0
- package/pan-wizard-core/bin/lib/findings.cjs +148 -9
- package/pan-wizard-core/bin/lib/focus.cjs +1 -1
- package/pan-wizard-core/bin/lib/git.cjs +1 -1
- package/pan-wizard-core/bin/lib/hosts.cjs +88 -0
- package/pan-wizard-core/bin/lib/init.cjs +7 -2
- package/pan-wizard-core/bin/lib/knowledge.cjs +2 -0
- package/pan-wizard-core/bin/lib/merge.cjs +50 -0
- package/pan-wizard-core/bin/lib/mutate.cjs +268 -0
- package/pan-wizard-core/bin/lib/next-action.cjs +112 -0
- package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
- package/pan-wizard-core/bin/lib/phase-gate.cjs +15 -9
- package/pan-wizard-core/bin/lib/phase-remove.cjs +91 -11
- package/pan-wizard-core/bin/lib/phase.cjs +85 -13
- package/pan-wizard-core/bin/lib/plan-lint.cjs +310 -0
- package/pan-wizard-core/bin/lib/preview.cjs +26 -4
- package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
- package/pan-wizard-core/bin/lib/research-lint.cjs +268 -0
- package/pan-wizard-core/bin/lib/review-deep.cjs +14 -22
- package/pan-wizard-core/bin/lib/review-vote.cjs +105 -0
- package/pan-wizard-core/bin/lib/roadmap.cjs +1 -0
- package/pan-wizard-core/bin/lib/runner.cjs +2 -16
- package/pan-wizard-core/bin/lib/sizing.cjs +356 -0
- package/pan-wizard-core/bin/lib/squads.cjs +1 -1
- package/pan-wizard-core/bin/lib/state-write.cjs +247 -0
- package/pan-wizard-core/bin/lib/test-gate.cjs +433 -0
- package/pan-wizard-core/bin/lib/uat.cjs +149 -0
- package/pan-wizard-core/bin/lib/usage.cjs +104 -0
- package/pan-wizard-core/bin/lib/verdict.cjs +26 -3
- package/pan-wizard-core/bin/lib/verify-plan-set.cjs +30 -1
- package/pan-wizard-core/bin/lib/verify-preflight.cjs +2 -2
- package/pan-wizard-core/bin/lib/verify-red.cjs +174 -0
- package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
- package/pan-wizard-core/bin/lib/verify-scope.cjs +143 -2
- package/pan-wizard-core/bin/lib/verify.cjs +86 -31
- package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
- package/pan-wizard-core/bin/pan-tools.cjs +257 -28
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
- package/pan-wizard-core/mcp/orchestrator.cjs +4 -1
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/design-methodology.md +1 -1
- package/pan-wizard-core/references/guardrails.md +1 -1
- package/pan-wizard-core/references/handoff-decisions.md +1 -1
- package/pan-wizard-core/references/model-profiles.md +2 -3
- package/pan-wizard-core/references/questioning.md +24 -0
- package/pan-wizard-core/references/ui-brand.md +4 -6
- package/pan-wizard-core/references/verdict-contract.md +2 -1
- package/pan-wizard-core/templates/config.json +10 -2
- package/pan-wizard-core/templates/debug.md +1 -0
- package/pan-wizard-core/templates/design.md +5 -4
- package/pan-wizard-core/templates/discovery.md +2 -2
- package/pan-wizard-core/templates/phase-prompt.md +7 -0
- package/pan-wizard-core/templates/requirements.md +9 -0
- package/pan-wizard-core/templates/research.md +10 -0
- package/pan-wizard-core/templates/summary-complex.md +5 -1
- package/pan-wizard-core/templates/summary-minimal.md +4 -0
- package/pan-wizard-core/templates/summary-standard.md +5 -1
- package/pan-wizard-core/templates/summary.md +8 -1
- package/pan-wizard-core/workflows/add-phase.md +4 -6
- package/pan-wizard-core/workflows/assumptions.md +19 -1
- package/pan-wizard-core/workflows/diagnose-issues.md +2 -0
- package/pan-wizard-core/workflows/discuss-phase.md +4 -2
- package/pan-wizard-core/workflows/exec-lean.md +69 -0
- package/pan-wizard-core/workflows/exec-phase.md +79 -15
- package/pan-wizard-core/workflows/execute-plan.md +3 -1
- package/{commands/pan → pan-wizard-core/workflows}/focus-doc-audit.md +13 -29
- package/{commands/pan → pan-wizard-core/workflows}/focus-drift-walking.md +3 -18
- package/{commands/pan → pan-wizard-core/workflows}/focus-exec.md +8 -23
- package/{commands/pan → pan-wizard-core/workflows}/focus-plan.md +6 -20
- package/{commands/pan → pan-wizard-core/workflows}/focus-scan.md +3 -16
- package/{commands/pan → pan-wizard-core/workflows}/focus-sync.md +7 -21
- package/pan-wizard-core/workflows/health.md +6 -0
- package/pan-wizard-core/workflows/help.md +114 -77
- package/pan-wizard-core/workflows/insert-phase.md +4 -6
- package/pan-wizard-core/workflows/learn.md +3 -3
- package/pan-wizard-core/workflows/milestone-audit.md +2 -2
- package/pan-wizard-core/workflows/milestone-new.md +38 -95
- package/pan-wizard-core/workflows/new-project.md +96 -258
- package/pan-wizard-core/workflows/optimize.md +107 -5
- package/pan-wizard-core/workflows/phase-tests.md +10 -0
- package/pan-wizard-core/workflows/plan-phase.md +106 -45
- package/pan-wizard-core/workflows/profile.md +50 -0
- package/pan-wizard-core/workflows/progress.md +10 -0
- package/pan-wizard-core/workflows/quick.md +34 -43
- package/pan-wizard-core/workflows/remove-phase.md +11 -1
- package/pan-wizard-core/workflows/research-phase.md +118 -40
- package/pan-wizard-core/workflows/resume-project.md +18 -2
- package/pan-wizard-core/workflows/settings.md +33 -19
- package/pan-wizard-core/workflows/todo-check.md +6 -2
- package/pan-wizard-core/workflows/transition.md +22 -4
- package/pan-wizard-core/workflows/uat-walkthrough.md +90 -0
- package/pan-wizard-core/workflows/update.md +1 -1
- package/pan-wizard-core/workflows/verify-phase.md +39 -46
- package/scripts/build-agent-plugin.js +35 -15
- package/scripts/build-help.cjs +106 -0
- package/scripts/build-plugin.js +10 -1
- package/scripts/deprecate-old-versions.js +70 -3
- package/scripts/generate-skills-docs.py +1 -1
- package/scripts/git-hooks/pre-push +27 -0
- package/scripts/install-git-hooks.js +7 -5
- package/scripts/install-pre-push.cjs +38 -0
- package/scripts/land.cjs +151 -0
- package/scripts/local-ci.cjs +501 -0
- package/scripts/local-schedule.cjs +277 -0
- package/scripts/mutation-probe.cjs +7 -80
- package/scripts/prompt-lint.cjs +32 -0
- package/scripts/prompt-rules.cjs +156 -0
- package/scripts/run-tests.cjs +7 -1
- package/scripts/test-quality-lint.cjs +3 -3
- package/scripts/test-surface.cjs +175 -6
- package/agents/pan-research-synthesizer.md +0 -246
- package/commands/pan/audit-deployment.md +0 -386
- package/commands/pan/dashboard.md +0 -22
- package/commands/pan/discord.md +0 -19
- package/commands/pan/mcp-bridge.md +0 -143
- package/commands/pan/milestone-cleanup.md +0 -19
- package/commands/pan/milestone-gaps.md +0 -35
- package/commands/pan/optimize.md +0 -115
- package/commands/pan/profile.md +0 -76
- package/commands/pan/research-phase.md +0 -189
- package/pan-wizard-core/bin/lib/bridge.cjs +0 -269
- package/pan-wizard-core/templates/research-project/architecture.md +0 -204
- package/pan-wizard-core/templates/research-project/features.md +0 -147
- package/pan-wizard-core/templates/research-project/pitfalls.md +0 -200
- package/pan-wizard-core/templates/research-project/stack.md +0 -120
- package/pan-wizard-core/templates/research-project/summary.md +0 -170
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Research claims with provenance and freshness (program P3).
|
|
4
|
+
*
|
|
5
|
+
* A research file lists the claims a plan will rest on in a `## Claims` table:
|
|
6
|
+
*
|
|
7
|
+
* | Claim | Type | Source | Checked |
|
|
8
|
+
* | jose 6 supports EdDSA | version | https://github.com/panva/jose/releases | 2026-10-12 |
|
|
9
|
+
*
|
|
10
|
+
* `research lint <phase|path>` reads it. A claim with no source, or with no date (or
|
|
11
|
+
* a date that is not YYYY-MM-DD, or is in the future), is an error: nobody can tell
|
|
12
|
+
* where it came from or when it was true. A claim older than the freshness window for
|
|
13
|
+
* its type is `stale`, not an error: what a version or a price was two months ago
|
|
14
|
+
* says little about today, while a practice lasts for years. The plan-checker weighs
|
|
15
|
+
* stale claims against what the plans rest on.
|
|
16
|
+
*
|
|
17
|
+
* Research answers questions (2026-10-25: research that surveyed a domain never
|
|
18
|
+
* finished and grew the scope). The orchestrator writes at most five open questions
|
|
19
|
+
* that block building; one researcher answers them in a table
|
|
20
|
+
*
|
|
21
|
+
* | # | Question | Answer | Source | Confidence | Decision it settles |
|
|
22
|
+
*
|
|
23
|
+
* `research check <questions> <research> [--market]` holds it to that: at most five
|
|
24
|
+
* questions, every question a row with an answer and a source, or an answer marked
|
|
25
|
+
* `unknown` that names the assumption the plan uses; in market mode, at most five
|
|
26
|
+
* comparable products. Without a `## Claims` table, `research lint` reads the
|
|
27
|
+
* answered rows of that table as claims, with the date in the source cell.
|
|
28
|
+
*/
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const { output, error, toLf, toPosix, findPhaseInternal } = require('./core.cjs');
|
|
32
|
+
const { sectionBody } = require('./verify-scope.cjs');
|
|
33
|
+
|
|
34
|
+
// Days a claim of each type stays fresh. Versions, prices and security advisories
|
|
35
|
+
// move monthly; an API surface quarterly; an engineering practice yearly.
|
|
36
|
+
const FRESHNESS_DAYS = Object.freeze({ version: 30, pricing: 30, security: 30, api: 90, general: 180, practice: 365 });
|
|
37
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
38
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
39
|
+
// A template placeholder (`[Official docs URL]`, `<url>`) or a stand-in is no source.
|
|
40
|
+
const PLACEHOLDER_RE = /^(\[.*\]|<.*>|-+|n\/?a|none|tbd|todo|\?+|unknown)$/i;
|
|
41
|
+
|
|
42
|
+
function utcDay(s) {
|
|
43
|
+
if (!DATE_RE.test(s)) return null;
|
|
44
|
+
const t = Date.parse(s + 'T00:00:00Z');
|
|
45
|
+
return Number.isNaN(t) ? null : t;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The research file a phase number or a path names, or `{error, reason}`. */
|
|
49
|
+
function resolveResearch(cwd, target) {
|
|
50
|
+
const asPath = path.resolve(cwd, String(target));
|
|
51
|
+
if (/\.md$/i.test(String(target))) {
|
|
52
|
+
if (!fs.existsSync(asPath)) return { error: 'file_not_found', reason: `no file at ${target}` };
|
|
53
|
+
return { abs: asPath };
|
|
54
|
+
}
|
|
55
|
+
const info = findPhaseInternal(cwd, target);
|
|
56
|
+
if (!info || !info.found) return { error: 'phase_not_found', reason: `no phase directory for phase ${target}` };
|
|
57
|
+
const dir = path.join(cwd, info.directory);
|
|
58
|
+
let files = [];
|
|
59
|
+
try { files = fs.readdirSync(dir); } catch { /* read as empty */ }
|
|
60
|
+
const file = files.filter((f) => /-research\.md$/i.test(f) || /^research\.md$/i.test(f)).sort()[0];
|
|
61
|
+
if (!file) return { error: 'no_research', reason: `phase ${target} has no research file` };
|
|
62
|
+
return { abs: path.join(dir, file) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Table rows of the `## Claims` section as arrays of cells, header and rule dropped. */
|
|
66
|
+
function claimRows(body) {
|
|
67
|
+
const rows = body.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('|'))
|
|
68
|
+
.map((l) => l.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim()));
|
|
69
|
+
if (!rows.length) return { header: null, rows: [] };
|
|
70
|
+
const header = rows[0].map((c) => c.toLowerCase());
|
|
71
|
+
return { header, rows: rows.slice(1).filter((r) => !r.every((c) => /^:?-+:?$/.test(c) || c === '')) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} cwd
|
|
76
|
+
* @param {string} target a phase number, or a path to a research markdown file
|
|
77
|
+
* @param {{now?: string}} [opts] `now` (YYYY-MM-DD) fixes "today" for a reproducible lint
|
|
78
|
+
*/
|
|
79
|
+
function lintResearch(cwd, target, opts = {}) {
|
|
80
|
+
const at = resolveResearch(cwd, target);
|
|
81
|
+
if (at.error) return at;
|
|
82
|
+
const rel = path.relative(path.resolve(cwd), at.abs);
|
|
83
|
+
const file = rel && !rel.startsWith('..') ? toPosix(rel) : toPosix(at.abs);
|
|
84
|
+
const now = opts.now ? utcDay(opts.now) : utcDay(new Date().toISOString().slice(0, 10));
|
|
85
|
+
if (now == null) return { error: 'invalid_now', reason: '--now must be YYYY-MM-DD' };
|
|
86
|
+
|
|
87
|
+
const text = toLf(fs.readFileSync(at.abs, 'utf-8'));
|
|
88
|
+
const base = { file, ok: true, claims: 0, errors: [], warnings: [], stale: [], windows: FRESHNESS_DAYS };
|
|
89
|
+
const body = sectionBody(text, 'Claims');
|
|
90
|
+
let header;
|
|
91
|
+
let rows;
|
|
92
|
+
if (body == null) {
|
|
93
|
+
// A question-driven research file: its answered rows are the claims.
|
|
94
|
+
const answers = answerTable(text);
|
|
95
|
+
if (!answers) {
|
|
96
|
+
return { ...base, ok: false, errors: [{ code: 'claims_missing', detail: 'no `## Claims` table (claim · type · source · checked) and no answers table (# · question · answer · source)' }] };
|
|
97
|
+
}
|
|
98
|
+
header = ['claim', 'type', 'source', 'checked'];
|
|
99
|
+
rows = answers.rows.filter((r) => !isUnknown(r.answer)).map((r) => {
|
|
100
|
+
const date = (r.source.match(/\d{4}-\d{2}-\d{2}/) || [''])[0];
|
|
101
|
+
return [r.answer, 'general', r.source, date];
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
({ header, rows } = claimRows(body));
|
|
105
|
+
}
|
|
106
|
+
const col = (name, fallback) => {
|
|
107
|
+
const i = header ? header.findIndex((h) => h.startsWith(name)) : -1;
|
|
108
|
+
return i === -1 ? fallback : i;
|
|
109
|
+
};
|
|
110
|
+
const C = { claim: col('claim', 0), type: col('type', 1), source: col('source', 2), checked: col('checked', 3) };
|
|
111
|
+
const errors = [];
|
|
112
|
+
const warnings = [];
|
|
113
|
+
const stale = [];
|
|
114
|
+
rows.forEach((r, i) => {
|
|
115
|
+
const row = i + 1;
|
|
116
|
+
const claim = r[C.claim] || '';
|
|
117
|
+
const rawType = (r[C.type] || '').toLowerCase();
|
|
118
|
+
const source = r[C.source] || '';
|
|
119
|
+
const checked = r[C.checked] || '';
|
|
120
|
+
let type = rawType;
|
|
121
|
+
if (!Object.prototype.hasOwnProperty.call(FRESHNESS_DAYS, rawType)) {
|
|
122
|
+
warnings.push({ code: 'claim_unknown_type', row, claim, detail: `type "${rawType}" is not one of ${Object.keys(FRESHNESS_DAYS).join(', ')}; read as general` });
|
|
123
|
+
type = 'general';
|
|
124
|
+
}
|
|
125
|
+
if (!source || PLACEHOLDER_RE.test(source)) errors.push({ code: 'claim_no_source', row, claim, detail: 'no source' });
|
|
126
|
+
const day = utcDay(checked);
|
|
127
|
+
if (day == null) {
|
|
128
|
+
errors.push({ code: 'claim_no_date', row, claim, detail: checked ? `"${checked}" is not a YYYY-MM-DD date` : 'no checked date' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (day > now) { errors.push({ code: 'claim_future_date', row, claim, detail: `checked ${checked}, after today` }); return; }
|
|
132
|
+
const age = Math.round((now - day) / DAY_MS);
|
|
133
|
+
if (age > FRESHNESS_DAYS[type]) stale.push({ row, claim, type, checked, age_days: age, window_days: FRESHNESS_DAYS[type] });
|
|
134
|
+
});
|
|
135
|
+
return { ...base, ok: errors.length === 0, claims: rows.length, errors, warnings, stale };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const MAX_QUESTIONS = 5;
|
|
139
|
+
const MAX_PRODUCTS = 5;
|
|
140
|
+
|
|
141
|
+
/** Every markdown table in `text` as arrays of cells (rule rows dropped); a non-table line ends a table. */
|
|
142
|
+
function tables(text) {
|
|
143
|
+
const out = [];
|
|
144
|
+
let cur = null;
|
|
145
|
+
for (const line of String(text).split('\n')) {
|
|
146
|
+
const t = line.trim();
|
|
147
|
+
if (!t.startsWith('|')) { cur = null; continue; }
|
|
148
|
+
const cells = t.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
149
|
+
if (cells.every((c) => /^:?-+:?$/.test(c) || c === '')) continue;
|
|
150
|
+
if (!cur) { cur = []; out.push(cur); }
|
|
151
|
+
cur.push(cells);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The answers table (`# · Question · Answer · Source …`), rows as objects, or null. */
|
|
157
|
+
function answerTable(text) {
|
|
158
|
+
for (const t of tables(text)) {
|
|
159
|
+
const h = t[0].map((c) => c.toLowerCase());
|
|
160
|
+
const qi = h.findIndex((c) => c.startsWith('question'));
|
|
161
|
+
const ai = h.findIndex((c) => c.startsWith('answer'));
|
|
162
|
+
const si = h.findIndex((c) => c.startsWith('source'));
|
|
163
|
+
if (qi === -1 || ai === -1 || si === -1) continue;
|
|
164
|
+
const ni = h.findIndex((c) => c === '#' || c === 'no' || c === 'n');
|
|
165
|
+
const ci = h.findIndex((c) => c.startsWith('confidence'));
|
|
166
|
+
const di = h.findIndex((c) => c.startsWith('decision'));
|
|
167
|
+
return {
|
|
168
|
+
rows: t.slice(1).map((r, i) => ({
|
|
169
|
+
n: ni === -1 ? i + 1 : parseInt(String(r[ni] || '').replace(/\D/g, ''), 10),
|
|
170
|
+
question: r[qi] || '', answer: r[ai] || '', source: r[si] || '',
|
|
171
|
+
confidence: ci === -1 ? '' : (r[ci] || ''), decision: di === -1 ? '' : (r[di] || ''),
|
|
172
|
+
})),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Rows of the comparable-products table (first header cell `Product`), or null. */
|
|
179
|
+
function productTable(text) {
|
|
180
|
+
const t = tables(text).find((tb) => /^products?$/i.test(tb[0][0] || ''));
|
|
181
|
+
return t ? t.slice(1) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const isUnknown = (answer) => /^\W*unknown\b/i.test(answer);
|
|
185
|
+
const hasAssumption = (cell) => /assum\w*\s*[:\u2014\u2013-]?\s*\S/i.test(cell);
|
|
186
|
+
|
|
187
|
+
/** Numbered questions in a questions file: lines `1. …` or `1) …`, or `| 1 | … |` table rows. */
|
|
188
|
+
function readQuestions(text) {
|
|
189
|
+
const qs = [];
|
|
190
|
+
for (const line of String(text).split('\n')) {
|
|
191
|
+
const m = line.match(/^\s*(\d+)[.)]\s+(\S.*)$/) || line.match(/^\s*\|\s*(\d+)\s*\|\s*([^|]*\S[^|]*)\|/);
|
|
192
|
+
if (m) qs.push({ n: parseInt(m[1], 10), text: m[2].trim() });
|
|
193
|
+
}
|
|
194
|
+
return qs;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} cwd
|
|
199
|
+
* @param {string} questionsPath the questions file the orchestrator wrote
|
|
200
|
+
* @param {string} researchPath the researcher's answers
|
|
201
|
+
* @param {{market?: boolean}} [opts] market mode also requires the products table
|
|
202
|
+
*/
|
|
203
|
+
function checkResearch(cwd, questionsPath, researchPath, opts = {}) {
|
|
204
|
+
const qAbs = path.resolve(cwd, String(questionsPath));
|
|
205
|
+
const rAbs = path.resolve(cwd, String(researchPath));
|
|
206
|
+
if (!fs.existsSync(qAbs)) return { error: 'file_not_found', reason: `no questions file at ${questionsPath}` };
|
|
207
|
+
if (!fs.existsSync(rAbs)) return { error: 'file_not_found', reason: `no research file at ${researchPath}` };
|
|
208
|
+
const questions = readQuestions(toLf(fs.readFileSync(qAbs, 'utf-8')));
|
|
209
|
+
const text = toLf(fs.readFileSync(rAbs, 'utf-8'));
|
|
210
|
+
const errors = [];
|
|
211
|
+
const warnings = [];
|
|
212
|
+
if (questions.length === 0) errors.push({ code: 'no_questions', detail: 'the questions file lists no numbered question: with nothing blocking the build, no research runs' });
|
|
213
|
+
if (questions.length > MAX_QUESTIONS) errors.push({ code: 'too_many_questions', detail: `${questions.length} questions; at most ${MAX_QUESTIONS}, the ones that block building` });
|
|
214
|
+
const table = answerTable(text);
|
|
215
|
+
let answered = 0;
|
|
216
|
+
let unknown = 0;
|
|
217
|
+
if (!table) {
|
|
218
|
+
errors.push({ code: 'answers_missing', detail: 'no answers table (| # | Question | Answer | Source | Confidence | Decision it settles |)' });
|
|
219
|
+
} else {
|
|
220
|
+
for (const q of questions) {
|
|
221
|
+
const row = table.rows.find((r) => r.n === q.n);
|
|
222
|
+
if (!row || !row.answer || (PLACEHOLDER_RE.test(row.answer) && !isUnknown(row.answer))) {
|
|
223
|
+
errors.push({ code: 'question_unanswered', question: q.n, detail: `question ${q.n} has no answer row` });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (isUnknown(row.answer)) {
|
|
227
|
+
if (hasAssumption(`${row.answer} ${row.decision}`)) unknown++;
|
|
228
|
+
else errors.push({ code: 'unknown_without_assumption', question: q.n, detail: `question ${q.n} is unknown but names no assumption the plan should use` });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (!row.source || PLACEHOLDER_RE.test(row.source)) {
|
|
232
|
+
errors.push({ code: 'answer_no_source', question: q.n, detail: `question ${q.n} has an answer with no source (URL or doc, and date)` });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (!/^(high|medium|low)\b/i.test(row.confidence)) warnings.push({ code: 'answer_no_confidence', question: q.n, detail: `question ${q.n} gives no HIGH, MEDIUM or LOW confidence` });
|
|
236
|
+
answered++;
|
|
237
|
+
}
|
|
238
|
+
const known = new Set(questions.map((q) => q.n));
|
|
239
|
+
for (const r of table.rows) {
|
|
240
|
+
if (!known.has(r.n)) warnings.push({ code: 'extra_row', row: r.n, detail: `row ${r.n} answers no listed question: new scope goes under Ideas, not into the answers` });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const products = productTable(text);
|
|
244
|
+
if (products && products.length > MAX_PRODUCTS) errors.push({ code: 'too_many_products', detail: `${products.length} comparable products; at most ${MAX_PRODUCTS}` });
|
|
245
|
+
if (opts.market && !products) errors.push({ code: 'products_missing', detail: 'market mode: no table whose first column is Product' });
|
|
246
|
+
const rel = (abs) => { const r = path.relative(path.resolve(cwd), abs); return r && !r.startsWith('..') ? toPosix(r) : toPosix(abs); };
|
|
247
|
+
return {
|
|
248
|
+
questions_file: rel(qAbs), research_file: rel(rAbs), ok: errors.length === 0,
|
|
249
|
+
questions: questions.length, answered, unknown, products: products ? products.length : 0,
|
|
250
|
+
limits: { questions: MAX_QUESTIONS, products: MAX_PRODUCTS }, errors, warnings,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function cmdResearchCheck(cwd, questionsPath, researchPath, opts, raw) {
|
|
255
|
+
if (!questionsPath || !researchPath) error('Usage: research check <questions file> <research file> [--market]');
|
|
256
|
+
const r = checkResearch(cwd, questionsPath, researchPath, opts);
|
|
257
|
+
if (r.error) { output(r, raw); return; }
|
|
258
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function cmdResearchLint(cwd, target, opts, raw) {
|
|
262
|
+
if (!target) error('Usage: research lint <phase|path> [--now YYYY-MM-DD]');
|
|
263
|
+
const r = lintResearch(cwd, target, opts);
|
|
264
|
+
if (r.error) { output(r, raw); return; }
|
|
265
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { FRESHNESS_DAYS, MAX_QUESTIONS, MAX_PRODUCTS, lintResearch, cmdResearchLint, checkResearch, cmdResearchCheck };
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
* - writeDeepReview(cwd, phaseNum, payload) — serialize the merged output
|
|
15
15
|
* to .planning/reviews/<N>/deep-review.md
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* It used to publish each merge to a `review-handoff` bus channel that nothing read;
|
|
18
|
+
* deep-review.md is the record (program L7).
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
const fs = require('fs');
|
|
21
22
|
const path = require('path');
|
|
22
23
|
const { output, error, safeReadFile, toPosix, escapeTableCell } = require('./core.cjs');
|
|
23
24
|
const { planningPath } = require('./utils.cjs');
|
|
24
|
-
const { publish } = require('./bus.cjs');
|
|
25
25
|
const { findVerdictBlocks, validateVerdict } = require('./verdict.cjs');
|
|
26
26
|
|
|
27
27
|
const REVIEWS_DIR = 'reviews';
|
|
@@ -126,14 +126,16 @@ function findingsFromVerdictBlock(content, source) {
|
|
|
126
126
|
* @param {Array|string} reviewer - reviewer findings array OR markdown content
|
|
127
127
|
* @param {Array|string} hardener - hardener findings array OR markdown content
|
|
128
128
|
* @param {Array|string} [meta] - optional meta-reviewer findings
|
|
129
|
+
* @param {Array|string} [cross] - optional cross-family lane findings (R4)
|
|
129
130
|
* @returns {Object} Merged payload
|
|
130
131
|
*/
|
|
131
|
-
function mergeReviews(reviewer, hardener, meta) {
|
|
132
|
+
function mergeReviews(reviewer, hardener, meta, cross) {
|
|
132
133
|
const r = Array.isArray(reviewer) ? reviewer : parseReviewFindings(reviewer || '', 'reviewer');
|
|
133
134
|
const h = Array.isArray(hardener) ? hardener : parseReviewFindings(hardener || '', 'hardener');
|
|
134
135
|
const m = Array.isArray(meta) ? meta : parseReviewFindings(meta || '', 'meta-reviewer');
|
|
136
|
+
const x = Array.isArray(cross) ? cross : parseReviewFindings(cross || '', 'cross-family');
|
|
135
137
|
|
|
136
|
-
const findings = [...r, ...h, ...m].sort((a, b) => {
|
|
138
|
+
const findings = [...r, ...h, ...m, ...x].sort((a, b) => {
|
|
137
139
|
const wa = SEVERITY_WEIGHT[a.severity] ?? 0;
|
|
138
140
|
const wb = SEVERITY_WEIGHT[b.severity] ?? 0;
|
|
139
141
|
if (wa !== wb) return wb - wa;
|
|
@@ -174,6 +176,7 @@ function mergeReviews(reviewer, hardener, meta) {
|
|
|
174
176
|
reviewer: r.length,
|
|
175
177
|
hardener: h.length,
|
|
176
178
|
meta_reviewer: m.length,
|
|
179
|
+
cross_family: x.length,
|
|
177
180
|
},
|
|
178
181
|
by_severity: SEVERITIES.reduce((acc, s) => { acc[s] = findings.filter(f => f.severity === s).length; return acc; }, {}),
|
|
179
182
|
};
|
|
@@ -263,19 +266,6 @@ function writeDeepReview(cwd, phaseNum, payload, opts) {
|
|
|
263
266
|
return { error: `Failed to write ${file}: ${e.message}` };
|
|
264
267
|
}
|
|
265
268
|
|
|
266
|
-
// Audit trail on the review-handoff bus channel (best-effort).
|
|
267
|
-
if (opts?.audit_channel !== false) {
|
|
268
|
-
try {
|
|
269
|
-
publish(cwd, 'review-handoff', {
|
|
270
|
-
phase: phaseNum,
|
|
271
|
-
verdict: payload.verdict,
|
|
272
|
-
finding_count: payload.coverage.total,
|
|
273
|
-
conflict_count: payload.conflicts.length,
|
|
274
|
-
file: toPosix(path.relative(cwd, file)),
|
|
275
|
-
}, { source: 'pan-meta-reviewer' });
|
|
276
|
-
} catch { /* non-blocking */ }
|
|
277
|
-
}
|
|
278
|
-
|
|
279
269
|
return { written: true, file: toPosix(path.relative(cwd, file)) };
|
|
280
270
|
}
|
|
281
271
|
|
|
@@ -290,15 +280,16 @@ function resolveAgainstCwd(cwd, p) {
|
|
|
290
280
|
}
|
|
291
281
|
|
|
292
282
|
function cmdReviewDeepMerge(cwd, phaseNum, opts, raw) {
|
|
293
|
-
if (!phaseNum) error('Usage: review-deep merge <phase> --reviewer-file X --hardener-file Y [--meta-file Z]');
|
|
283
|
+
if (!phaseNum) error('Usage: review-deep merge <phase> --reviewer-file X --hardener-file Y [--meta-file Z] [--cross-file W]');
|
|
294
284
|
const reviewerContent = opts.reviewerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.reviewerFile)) : '';
|
|
295
285
|
const hardenerContent = opts.hardenerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.hardenerFile)) : '';
|
|
296
286
|
const metaContent = opts.metaFile ? safeReadFile(resolveAgainstCwd(cwd, opts.metaFile)) : '';
|
|
297
|
-
|
|
287
|
+
const crossContent = opts.crossFile ? safeReadFile(resolveAgainstCwd(cwd, opts.crossFile)) : '';
|
|
288
|
+
if (!reviewerContent && !hardenerContent && !metaContent && !crossContent) {
|
|
298
289
|
output({ error: 'No input files provided or readable' }, raw);
|
|
299
290
|
return;
|
|
300
291
|
}
|
|
301
|
-
const payload = mergeReviews(reviewerContent, hardenerContent, metaContent);
|
|
292
|
+
const payload = mergeReviews(reviewerContent, hardenerContent, metaContent, crossContent);
|
|
302
293
|
const result = writeDeepReview(cwd, phaseNum, payload);
|
|
303
294
|
if (result.error) { output(result, raw); return; }
|
|
304
295
|
output({ ...result, verdict: payload.verdict, coverage: payload.coverage, conflicts: payload.conflicts.length }, raw);
|
|
@@ -306,11 +297,12 @@ function cmdReviewDeepMerge(cwd, phaseNum, opts, raw) {
|
|
|
306
297
|
|
|
307
298
|
function cmdReviewDeepAnalyze(cwd, phaseNum, opts, raw) {
|
|
308
299
|
// Returns the merged payload WITHOUT writing a file. Useful for piping.
|
|
309
|
-
if (!phaseNum) error('Usage: review-deep analyze <phase> --reviewer-file X --hardener-file Y [--meta-file Z]');
|
|
300
|
+
if (!phaseNum) error('Usage: review-deep analyze <phase> --reviewer-file X --hardener-file Y [--meta-file Z] [--cross-file W]');
|
|
310
301
|
const reviewerContent = opts.reviewerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.reviewerFile)) : '';
|
|
311
302
|
const hardenerContent = opts.hardenerFile ? safeReadFile(resolveAgainstCwd(cwd, opts.hardenerFile)) : '';
|
|
312
303
|
const metaContent = opts.metaFile ? safeReadFile(resolveAgainstCwd(cwd, opts.metaFile)) : '';
|
|
313
|
-
|
|
304
|
+
const crossContent = opts.crossFile ? safeReadFile(resolveAgainstCwd(cwd, opts.crossFile)) : '';
|
|
305
|
+
output(mergeReviews(reviewerContent, hardenerContent, metaContent, crossContent), raw);
|
|
314
306
|
}
|
|
315
307
|
|
|
316
308
|
module.exports = {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Review lenses and k-pass voting (program R2).
|
|
4
|
+
*
|
|
5
|
+
* With `workflow.review_mode: lenses`, exec-phase runs the reviewer once per lens
|
|
6
|
+
* (LENSES) instead of once, and `review vote` merges the passes in code. A finding
|
|
7
|
+
* survives only when a quorum of passes reported it (the validator: a finding one
|
|
8
|
+
* pass alone raised is a likely false alarm), at the highest severity any pass gave
|
|
9
|
+
* it. The verdict is recomputed from what survived, never taken from one pass. The
|
|
10
|
+
* merged report carries a normal `pan-verdict` block, so `findings record` reads it
|
|
11
|
+
* like any single review.
|
|
12
|
+
*
|
|
13
|
+
* Opt-in until its evidence gate decides: a paired corpus run must show lenses
|
|
14
|
+
* catching more without raising false alarms (docs/specs/product-excellence-superplan.md).
|
|
15
|
+
*/
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { output, error } = require('./core.cjs');
|
|
19
|
+
const V = require('./verdict.cjs');
|
|
20
|
+
const { sameFinding } = require('./findings.cjs');
|
|
21
|
+
|
|
22
|
+
const LENSES = Object.freeze(['correctness', 'security', 'maintainability']);
|
|
23
|
+
const SEVERITY_RANK = Object.fromEntries(V.SEVERITIES.map((s, i) => [s, V.SEVERITIES.length - i]));
|
|
24
|
+
const FENCE = '```';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Merge reviewer passes.
|
|
28
|
+
* @param {string[]} texts - each pass's full report (with its pan-verdict block)
|
|
29
|
+
* @param {{phase?: string, quorum?: number}} [opts]
|
|
30
|
+
* @returns {{ok:true, passes, quorum, verdict, dropped, text}|{ok:false, error, reason}}
|
|
31
|
+
*/
|
|
32
|
+
function voteVerdicts(texts, opts = {}) {
|
|
33
|
+
const parsed = [];
|
|
34
|
+
for (let i = 0; i < texts.length; i++) {
|
|
35
|
+
const p = V.parseVerdictText(String(texts[i] || ''), { agent: 'pan-reviewer', phase: opts.phase });
|
|
36
|
+
if (!p.ok) return { ok: false, error: 'unreadable_pass', reason: `pass ${i + 1} has no readable pan-verdict block (${p.error})` };
|
|
37
|
+
parsed.push(p.verdict);
|
|
38
|
+
}
|
|
39
|
+
if (parsed.length === 0) return { ok: false, error: 'no_passes', reason: 'review vote needs at least one pass' };
|
|
40
|
+
const quorum = Number.isInteger(opts.quorum) && opts.quorum > 0 ? opts.quorum : Math.ceil(parsed.length / 2);
|
|
41
|
+
|
|
42
|
+
// Cluster findings across passes; a pass counts once per cluster.
|
|
43
|
+
const clusters = [];
|
|
44
|
+
parsed.forEach((v, pass) => {
|
|
45
|
+
for (const f of v.findings) {
|
|
46
|
+
const c = clusters.find((x) => sameFinding(x.finding, f));
|
|
47
|
+
if (c) {
|
|
48
|
+
c.passes.add(pass);
|
|
49
|
+
if ((SEVERITY_RANK[f.severity] || 0) > (SEVERITY_RANK[c.finding.severity] || 0)) c.finding = { ...c.finding, severity: f.severity };
|
|
50
|
+
} else {
|
|
51
|
+
clusters.push({ finding: { ...f }, passes: new Set([pass]) });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const kept = clusters.filter((c) => c.passes.size >= quorum).map((c) => ({ ...c.finding, votes: c.passes.size }));
|
|
56
|
+
const dropped = clusters.filter((c) => c.passes.size < quorum).map((c) => ({ ...c.finding, votes: c.passes.size }));
|
|
57
|
+
|
|
58
|
+
const counted = kept.filter((f) => !f.pre_existing);
|
|
59
|
+
const errors = counted.some((f) => f.severity === 'critical' || f.severity === 'high');
|
|
60
|
+
const warnings = counted.some((f) => f.severity === 'medium' || f.severity === 'low');
|
|
61
|
+
const verdictWord = errors ? 'NEEDS_FIXES' : (warnings ? 'PASS_WITH_WARNINGS' : 'PASS');
|
|
62
|
+
|
|
63
|
+
const declined = [];
|
|
64
|
+
for (const v of parsed) for (const d of v.declined || []) if (!declined.some((x) => x.behaviour === d.behaviour)) declined.push(d);
|
|
65
|
+
const notChecked = [];
|
|
66
|
+
for (const v of parsed) for (const n of v.not_checked || []) if (!notChecked.some((x) => x.check === n.check)) notChecked.push(n);
|
|
67
|
+
|
|
68
|
+
const verdict = {
|
|
69
|
+
contract: V.VERDICT_CONTRACT, agent: 'pan-reviewer', phase: opts.phase || parsed[0].phase || null,
|
|
70
|
+
verdict: verdictWord, outcome: errors ? 'fail' : 'pass',
|
|
71
|
+
findings: kept.map(({ votes, ...f }) => ({ ...f, votes })),
|
|
72
|
+
...(declined.length ? { declined } : {}),
|
|
73
|
+
...(notChecked.length ? { not_checked: notChecked } : {}),
|
|
74
|
+
};
|
|
75
|
+
const lines = [
|
|
76
|
+
`## Code Review — Phase ${verdict.phase || '?'} (${parsed.length} passes, quorum ${quorum})`,
|
|
77
|
+
'',
|
|
78
|
+
`### Verdict`,
|
|
79
|
+
verdictWord,
|
|
80
|
+
'',
|
|
81
|
+
`Kept ${kept.length} finding(s) at least ${quorum} pass(es) reported; dropped ${dropped.length} only fewer passes raised.`,
|
|
82
|
+
...kept.map((f) => `- ${f.severity.toUpperCase()} ${f.where || '(no location)'}: ${f.summary} (${f.votes}/${parsed.length})`),
|
|
83
|
+
...(dropped.length ? ['', 'Dropped (below quorum):', ...dropped.map((f) => `- ${f.severity} ${f.where || '(no location)'}: ${f.summary} (${f.votes}/${parsed.length})`)] : []),
|
|
84
|
+
'',
|
|
85
|
+
`${FENCE}pan-verdict`,
|
|
86
|
+
JSON.stringify(verdict),
|
|
87
|
+
FENCE,
|
|
88
|
+
'',
|
|
89
|
+
];
|
|
90
|
+
return { ok: true, passes: parsed.length, quorum, verdict, dropped, text: lines.join('\n') };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cmdReviewVote(cwd, opts, raw) {
|
|
94
|
+
const files = opts.files || [];
|
|
95
|
+
if (!files.length) error('Usage: review vote [--phase N] [--quorum K] [--out <file>] <pass-file>...');
|
|
96
|
+
const texts = files.map((f) => {
|
|
97
|
+
try { return fs.readFileSync(path.isAbsolute(f) ? f : path.join(cwd, f), 'utf8'); } catch { return ''; }
|
|
98
|
+
});
|
|
99
|
+
const r = voteVerdicts(texts, { phase: opts.phase, quorum: opts.quorum ? Number(opts.quorum) : undefined });
|
|
100
|
+
if (!r.ok) { output(r, raw, 'error', 1); return; }
|
|
101
|
+
if (opts.out) fs.writeFileSync(path.isAbsolute(opts.out) ? opts.out : path.join(cwd, opts.out), r.text);
|
|
102
|
+
output({ ok: true, passes: r.passes, quorum: r.quorum, kept: r.verdict.findings.length, dropped: r.dropped.length, verdict: r.verdict.verdict, out: opts.out || null }, raw, r.verdict.verdict);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { LENSES, voteVerdicts, cmdReviewVote };
|
|
@@ -55,22 +55,8 @@ const { getExperimentManifest, PAN_EXPERIMENTS_ROOT_DEFAULT } = require('./exper
|
|
|
55
55
|
// claude into `--output-format json` so the trailing usage envelope can be
|
|
56
56
|
// parsed for cost/token metrics. Other runtimes are unchanged — token
|
|
57
57
|
// metering for codex/gemini/opencode is deferred (no equivalent flag).
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
bin: 'claude',
|
|
61
|
-
buildArgs: (prompt, opts) => {
|
|
62
|
-
const args = ['-p', '--dangerously-skip-permissions'];
|
|
63
|
-
if (opts && opts.captureMetrics) args.push('--output-format', 'json');
|
|
64
|
-
args.push(prompt);
|
|
65
|
-
return args;
|
|
66
|
-
},
|
|
67
|
-
shell: 'win32',
|
|
68
|
-
},
|
|
69
|
-
codex: { bin: 'codex', buildArgs: (prompt) => ['exec', prompt], shell: 'win32' },
|
|
70
|
-
gemini: { bin: 'gemini', buildArgs: (prompt) => ['-p', '--yolo', prompt], shell: 'win32' },
|
|
71
|
-
opencode: { bin: 'opencode', buildArgs: (prompt) => [prompt], shell: 'win32' },
|
|
72
|
-
copilot: null,
|
|
73
|
-
});
|
|
58
|
+
// The adapters live in hosts.cjs, the one host table (program A0).
|
|
59
|
+
const RUNTIME_RUNNERS = require('./hosts.cjs').runnerTable();
|
|
74
60
|
|
|
75
61
|
// ── Stop reasons (enum-ish) ─────────────────────────────────────────────────
|
|
76
62
|
|