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,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Local usage census (program L9): `pan-tools usage`.
|
|
4
|
+
*
|
|
5
|
+
* Which of the installed commands and agents this project has used, from its own
|
|
6
|
+
* files only: the cost ledger (`.planning/metrics/tokens.jsonl`: `command` and `agent`
|
|
7
|
+
* per row) and the trace sessions (`.planning/optimization/traces/<sess>/session.json`:
|
|
8
|
+
* `command`, `ended_at`). It reports what was used (calls, last use), what was never
|
|
9
|
+
* used, and commands that were started but never finished: every trace session for
|
|
10
|
+
* the command stopped without ending, the newest more than UNFINISHED_AFTER_MS ago.
|
|
11
|
+
* Nothing leaves the machine.
|
|
12
|
+
*/
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { output } = require('./core.cjs');
|
|
16
|
+
const { readRecords } = require('./cost.cjs');
|
|
17
|
+
const { planningPath } = require('./utils.cjs');
|
|
18
|
+
|
|
19
|
+
// A trace session still open after this long is not "running", it stopped.
|
|
20
|
+
const UNFINISHED_AFTER_MS = 6 * 60 * 60 * 1000;
|
|
21
|
+
// The install this engine ships in: <runtime dir>/pan-wizard-core/bin/lib → <runtime dir>.
|
|
22
|
+
const DEFAULT_INVENTORY = path.resolve(__dirname, '..', '..', '..');
|
|
23
|
+
|
|
24
|
+
/** A command name as the inventory spells it: `/pan:quick`, `pan-quick`, `pan:quick` → `quick`. */
|
|
25
|
+
function commandName(s) {
|
|
26
|
+
return String(s || '').trim().replace(/^\//, '').replace(/^pan[:-]/, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The installed commands and agents: commands/pan/*.md or skills/pan-*, and agents/pan-*. */
|
|
30
|
+
function readInventory(root) {
|
|
31
|
+
const list = (d, re) => { try { return fs.readdirSync(d).filter((f) => re.test(f)); } catch { return []; } };
|
|
32
|
+
const commands = new Set(list(path.join(root, 'commands', 'pan'), /\.md$/).map((f) => f.replace(/\.md$/, '')));
|
|
33
|
+
for (const d of list(path.join(root, 'skills'), /^pan-/)) commands.add(commandName(d));
|
|
34
|
+
const agents = new Set(list(path.join(root, 'agents'), /^pan-.*\.(md|toml)$/).map((f) => f.replace(/\.(md|toml)$/, '')));
|
|
35
|
+
return { commands: [...commands].sort(), agents: [...agents].sort() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readSessions(cwd) {
|
|
39
|
+
const dir = planningPath(cwd, 'optimization', 'traces');
|
|
40
|
+
let names = [];
|
|
41
|
+
try { names = fs.readdirSync(dir).filter((n) => n.startsWith('sess_')); } catch { return []; }
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const n of names) {
|
|
44
|
+
try { out.push(JSON.parse(fs.readFileSync(path.join(dir, n, 'session.json'), 'utf-8'))); } catch { /* unreadable session */ }
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} cwd
|
|
51
|
+
* @param {{inventory?: string, now?: string}} [opts]
|
|
52
|
+
*/
|
|
53
|
+
function usageCensus(cwd, opts = {}) {
|
|
54
|
+
const inventory = readInventory(opts.inventory || DEFAULT_INVENTORY);
|
|
55
|
+
const now = opts.now ? Date.parse(opts.now) : Date.now();
|
|
56
|
+
const cmds = new Map(inventory.commands.map((n) => [n, { name: n, calls: 0, last_used: null, sessions: 0, finished: 0, _lastStart: null }]));
|
|
57
|
+
const agents = new Map(inventory.agents.map((n) => [n, { name: n, calls: 0, last_used: null }]));
|
|
58
|
+
const later = (a, b) => (!a || (b && b > a) ? b : a);
|
|
59
|
+
|
|
60
|
+
const rows = readRecords(cwd);
|
|
61
|
+
for (const r of rows) {
|
|
62
|
+
const c = cmds.get(commandName(r.command));
|
|
63
|
+
if (c) { c.calls++; c.last_used = later(c.last_used, r.ts || null); }
|
|
64
|
+
const a = agents.get(String(r.agent || ''));
|
|
65
|
+
if (a) { a.calls++; a.last_used = later(a.last_used, r.ts || null); }
|
|
66
|
+
}
|
|
67
|
+
const sessions = readSessions(cwd);
|
|
68
|
+
for (const s of sessions) {
|
|
69
|
+
const c = cmds.get(commandName(s.command));
|
|
70
|
+
if (!c) continue;
|
|
71
|
+
c.sessions++;
|
|
72
|
+
if (s.ended_at) c.finished++;
|
|
73
|
+
c._lastStart = later(c._lastStart, s.started_at || null);
|
|
74
|
+
c.last_used = later(c.last_used, s.ended_at || s.started_at || null);
|
|
75
|
+
}
|
|
76
|
+
const byUse = (a, b) => b.calls - a.calls || String(b.last_used).localeCompare(String(a.last_used)) || a.name.localeCompare(b.name);
|
|
77
|
+
const pub = ({ _lastStart, ...rest }) => rest;
|
|
78
|
+
const cmdList = [...cmds.values()];
|
|
79
|
+
const used = cmdList.filter((c) => c.calls > 0 || c.sessions > 0);
|
|
80
|
+
const unfinished = used.filter((c) => c.sessions > 0 && c.finished === 0 && c._lastStart && now - Date.parse(c._lastStart) > UNFINISHED_AFTER_MS);
|
|
81
|
+
const agentList = [...agents.values()];
|
|
82
|
+
return {
|
|
83
|
+
local_only: true,
|
|
84
|
+
commands: {
|
|
85
|
+
used: used.sort(byUse).map(pub),
|
|
86
|
+
never_used: cmdList.filter((c) => c.calls === 0 && c.sessions === 0).map((c) => c.name).sort(),
|
|
87
|
+
unfinished: unfinished.sort(byUse).map(pub),
|
|
88
|
+
installed: inventory.commands.length,
|
|
89
|
+
},
|
|
90
|
+
agents: {
|
|
91
|
+
used: agentList.filter((a) => a.calls > 0).sort(byUse),
|
|
92
|
+
never_used: agentList.filter((a) => a.calls === 0).map((a) => a.name).sort(),
|
|
93
|
+
installed: inventory.agents.length,
|
|
94
|
+
},
|
|
95
|
+
sources: { cost_rows: rows.length, trace_sessions: sessions.length },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cmdUsage(cwd, raw) {
|
|
100
|
+
const r = usageCensus(cwd);
|
|
101
|
+
output(r, raw, `${r.commands.used.length} of ${r.commands.installed} commands used, ${r.commands.unfinished.length} unfinished; ${r.agents.used.length} of ${r.agents.installed} agents used`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { usageCensus, readInventory, commandName, cmdUsage, UNFINISHED_AFTER_MS };
|
|
@@ -36,7 +36,7 @@ const KNOWN_VERDICTS = Object.freeze({
|
|
|
36
36
|
'pan-design-checker': Object.freeze({ PASS: 'pass', GAPS: 'fail' }),
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80 });
|
|
39
|
+
const LIMITS = Object.freeze({ findings: 100, where: 200, summary: 300, verdict: 40, score: 40, not_checked: 20, check: 80, declined: 20 });
|
|
40
40
|
// What the verifier's test gate can report (verify-phase.md, run_test_suite).
|
|
41
41
|
const TEST_GATE_VALUES = Object.freeze(['passed', 'failed', 'skipped']);
|
|
42
42
|
const RESOLUTION_STATUSES = Object.freeze(['resolved', 'still_present', 'cannot_tell']);
|
|
@@ -192,12 +192,15 @@ function validateVerdict(obj, opts = {}) {
|
|
|
192
192
|
warnings.push(`summary_truncated: #${i}`);
|
|
193
193
|
}
|
|
194
194
|
const where = f.where == null || cleanText(f.where) === '' ? null : truncate(cleanText(f.where).replace(/\\/g, '/'), LIMITS.where);
|
|
195
|
-
|
|
195
|
+
// pre_existing (M52): the finding is in code the change did not touch. It is
|
|
196
|
+
// recorded as deferred debt, never a blocker; findings.cjs files it.
|
|
197
|
+
findings.push({ class: cls, severity, where, summary, ...(f.pre_existing === true ? { pre_existing: true } : {}) });
|
|
196
198
|
});
|
|
197
199
|
|
|
198
200
|
const notChecked = normaliseNotChecked(obj.not_checked, warnings);
|
|
199
201
|
const resolutions = normaliseResolutions(obj.resolutions, warnings);
|
|
200
|
-
|
|
202
|
+
const declined = normaliseDeclined(obj.declined, warnings);
|
|
203
|
+
return { ok: true, verdict: { contract, agent, outcome, verdict, phase, score, findings, not_checked: notChecked, resolutions, ...(declined.length ? { declined } : {}) }, warnings };
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
/**
|
|
@@ -234,6 +237,26 @@ function normaliseResolutions(value, warnings) {
|
|
|
234
237
|
return out;
|
|
235
238
|
}
|
|
236
239
|
|
|
240
|
+
/**
|
|
241
|
+
* The behaviours a judge set aside, each `{behaviour, reason}` (MI-118, program R2).
|
|
242
|
+
* A review that names what it declined to judge says where its silence is not a pass.
|
|
243
|
+
* Accepts bare strings; drops entries that name nothing.
|
|
244
|
+
*/
|
|
245
|
+
function normaliseDeclined(value, warnings) {
|
|
246
|
+
if (value == null) return [];
|
|
247
|
+
if (!Array.isArray(value)) { warnings.push('declined_not_an_array'); return []; }
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const entry of value.slice(0, LIMITS.declined)) {
|
|
250
|
+
const obj = typeof entry === 'string' ? { behaviour: entry } : (entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null);
|
|
251
|
+
const behaviour = obj ? truncate(cleanText(obj.behaviour ?? obj.behavior ?? ''), LIMITS.summary) : '';
|
|
252
|
+
if (!behaviour) continue;
|
|
253
|
+
const reason = obj.reason == null || cleanText(obj.reason) === '' ? null : truncate(cleanText(obj.reason), LIMITS.summary);
|
|
254
|
+
out.push({ behaviour, reason });
|
|
255
|
+
}
|
|
256
|
+
if (value.length > LIMITS.declined) warnings.push(`declined_truncated: ${value.length} reported, ${LIMITS.declined} kept`);
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
237
260
|
/**
|
|
238
261
|
* The checks a judge could not run, each `{check, reason}` (market-ideas M23).
|
|
239
262
|
* A verdict that carries them says what it did not see: a `pass` with tests that
|
|
@@ -176,9 +176,38 @@ function checkPlanSet(cwd, phase) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// 5. Property tasks (V10): a requirement whose EARS criterion is a validation rule
|
|
180
|
+
// (IF … THEN … SHALL, or a claim over any / every / all inputs, or a "never") is a
|
|
181
|
+
// claim over many inputs, which one example test cannot show. The plan that lists it
|
|
182
|
+
// must carry a property-test task: property="true" on the task, or "property test" /
|
|
183
|
+
// "property-based" in its name or action.
|
|
184
|
+
let reqText = '';
|
|
185
|
+
try { reqText = fs.readFileSync(planningPath(cwd, 'requirements.md'), 'utf-8'); } catch { /* none */ }
|
|
186
|
+
if (reqText && required.length) {
|
|
187
|
+
const RL = require('./requirements-lint.cjs');
|
|
188
|
+
const byId = new Map(RL.parseRequirements(reqText).map((r) => [key(r.id), r]));
|
|
189
|
+
for (const id of required) {
|
|
190
|
+
const r = byId.get(key(id));
|
|
191
|
+
if (!r || !r.criteria.some(isValidationRule)) continue;
|
|
192
|
+
const covering = plans.filter((p) => p.requirements.map(key).includes(key(id)));
|
|
193
|
+
const hasProperty = covering.some((p) => {
|
|
194
|
+
let text = '';
|
|
195
|
+
try { text = toLf(fs.readFileSync(path.join(pf.dir, p.file), 'utf-8')); } catch { /* unreadable */ }
|
|
196
|
+
return /<task\b[^>]*\bproperty=["']?true/i.test(text) || /property[- ]based|property test/i.test(text);
|
|
197
|
+
});
|
|
198
|
+
if (!hasProperty) errors.push({ code: 'missing_property_task', requirement: id, detail: `${id} states a validation rule ("${r.criteria.find(isValidationRule).slice(0, 80)}"): a plan covering it needs a property-test task, which checks the rule over generated inputs with the project's own property library` });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
179
202
|
return { phase: phaseNumber, ok: errors.length === 0, errors, warnings, plans: plans.length, plans_sha: plansSha(pf.dir, pf.planFiles) };
|
|
180
203
|
}
|
|
181
204
|
|
|
205
|
+
/** A validation-rule criterion (V10): unwanted-behaviour EARS, or a claim over all inputs. */
|
|
206
|
+
function isValidationRule(criterion) {
|
|
207
|
+
const c = String(criterion || '');
|
|
208
|
+
return /^IF\b[\s\S]*\bTHEN\b/i.test(c) || /\b(any|every|all|each)\s+\w+/i.test(c) || /\bnever\b/i.test(c);
|
|
209
|
+
}
|
|
210
|
+
|
|
182
211
|
/**
|
|
183
212
|
* Whether the plans exec-phase is about to run are the plans that passed a plan check.
|
|
184
213
|
* @returns {{status: 'ok'|'stale'|'failed'|'missing'|'legacy'|'disabled', reason?: string, plans_sha?: string, checked_sha?: string, attempt?: number}}
|
|
@@ -227,4 +256,4 @@ function cmdVerifyPlanSet(cwd, phase, raw) {
|
|
|
227
256
|
output(result, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
228
257
|
}
|
|
229
258
|
|
|
230
|
-
module.exports = { checkPlanSet, plansSha, plansShaFor, planCheckStatus, planDrift, cmdVerifyPlanSet, PLAN_CHECKER };
|
|
259
|
+
module.exports = { isValidationRule, phaseFiles, checkPlanSet, plansSha, plansShaFor, planCheckStatus, planDrift, cmdVerifyPlanSet, PLAN_CHECKER };
|
|
@@ -103,11 +103,11 @@ function cmdPreflight(cwd, target, raw) {
|
|
|
103
103
|
checks.push({ name: 'batch_exists', passed: true, detail: files[files.length - 1] });
|
|
104
104
|
} else {
|
|
105
105
|
checks.push({ name: 'batch_exists', passed: false, detail: 'no batch file found' });
|
|
106
|
-
blockers.push('No batch file — run /pan:focus-plan first');
|
|
106
|
+
blockers.push('No batch file — run /pan:focus-auto --step plan first');
|
|
107
107
|
}
|
|
108
108
|
} catch {
|
|
109
109
|
checks.push({ name: 'batch_exists', passed: false, detail: 'focus/ directory not found' });
|
|
110
|
-
blockers.push('No focus/ directory — run /pan:focus-scan first');
|
|
110
|
+
blockers.push('No focus/ directory — run /pan:focus-auto --step scan first');
|
|
111
111
|
}
|
|
112
112
|
} else {
|
|
113
113
|
// target is a phase number — check the phase directory exists
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* RED and catch proof (program V4): does each test this phase wrote actually test what
|
|
4
|
+
* the phase built?
|
|
5
|
+
*
|
|
6
|
+
* A test that passes before the phase's code exists cannot have caught anything: it is
|
|
7
|
+
* tautological, or it tests something the phase did not change. So, independent of the
|
|
8
|
+
* order the commits landed in (Meta's "weak catch"):
|
|
9
|
+
*
|
|
10
|
+
* base the parent of the phase's oldest plan commit (`{type}({phase}-{plan}):`)
|
|
11
|
+
* tests the test files the phase's plan commits added or changed, as they are now
|
|
12
|
+
* red in a throwaway worktree at the base, with the test file copied in from HEAD,
|
|
13
|
+
* the test must FAIL (a crash on a missing module counts: the code is not there)
|
|
14
|
+
* green at HEAD it must PASS
|
|
15
|
+
*
|
|
16
|
+
* caught red at the base, green at HEAD
|
|
17
|
+
* not_red passes without the phase's code: it cannot have caught a regression
|
|
18
|
+
* not_green fails at HEAD: the test gate reports that, not this check
|
|
19
|
+
* not_checked no command can run that one file (cargo, Maven, Gradle, make), with why
|
|
20
|
+
*
|
|
21
|
+
* Each file runs on its own (`workflow.test_file_command`, else derived from the suite's
|
|
22
|
+
* runner: test-gate.cjs `fileCommand`). The worktree links the project's node_modules,
|
|
23
|
+
* which git does not track, so a JS runner resolves the same at the base as at HEAD.
|
|
24
|
+
*/
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const { execGit, findPhaseInternal, output, error } = require('./core.cjs');
|
|
29
|
+
const { committedFiles } = require('./verify-scope.cjs');
|
|
30
|
+
const { isTestFile } = require('./verify-integrity.cjs');
|
|
31
|
+
const TG = require('./test-gate.cjs');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_FILE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The commit the phase started from: the parent of its oldest plan commit.
|
|
37
|
+
* @returns {{base: string, files: Set<string>, commits: number}|{base: null, reason: string}}
|
|
38
|
+
*/
|
|
39
|
+
function phaseBase(cwd, phase) {
|
|
40
|
+
const c = committedFiles(cwd, phase);
|
|
41
|
+
if (!c.git) return { base: null, reason: 'git is unavailable' };
|
|
42
|
+
if (!c.commits) return { base: null, reason: 'the phase has no plan commits (subjects like feat(03-01): …)' };
|
|
43
|
+
const oldest = c.hashes[c.hashes.length - 1];
|
|
44
|
+
const r = execGit(cwd, ['rev-parse', '--verify', '-q', `${oldest}^`]);
|
|
45
|
+
if (r.exitCode !== 0 || !r.stdout) return { base: null, reason: 'the phase starts at the first commit of the repository: there is no base to run against' };
|
|
46
|
+
return { base: r.stdout.trim(), files: c.files, commits: c.commits };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Remove a directory link (a junction or a symlink) without touching what it points to. */
|
|
50
|
+
function unlinkDir(p) {
|
|
51
|
+
try { fs.unlinkSync(p); } catch { try { fs.rmdirSync(p); } catch { /* already gone */ } }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run `fn(dir)` in a detached worktree at `rev`, then remove the worktree. The project's
|
|
56
|
+
* node_modules is linked in (and unlinked before git removes the worktree, so the removal
|
|
57
|
+
* can never reach the real one).
|
|
58
|
+
* @returns {{value?: *, error?: string}}
|
|
59
|
+
*/
|
|
60
|
+
function withWorktree(cwd, rev, fn) {
|
|
61
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-wt-'));
|
|
62
|
+
const dir = path.join(root, 'wt');
|
|
63
|
+
const add = execGit(cwd, ['worktree', 'add', '--detach', '-q', dir, rev]);
|
|
64
|
+
if (add.exitCode !== 0) {
|
|
65
|
+
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
66
|
+
return { error: `git worktree add failed: ${add.stderr || add.stdout}` };
|
|
67
|
+
}
|
|
68
|
+
const link = path.join(dir, 'node_modules');
|
|
69
|
+
let linked = false;
|
|
70
|
+
try {
|
|
71
|
+
const modules = path.join(cwd, 'node_modules');
|
|
72
|
+
if (fs.existsSync(modules) && !fs.existsSync(link)) {
|
|
73
|
+
try { fs.symlinkSync(modules, link, 'junction'); linked = true; } catch { /* runs without it */ }
|
|
74
|
+
}
|
|
75
|
+
return { value: fn(dir) };
|
|
76
|
+
} finally {
|
|
77
|
+
if (linked) unlinkDir(link);
|
|
78
|
+
execGit(cwd, ['worktree', 'remove', '--force', dir]);
|
|
79
|
+
execGit(cwd, ['worktree', 'prune']);
|
|
80
|
+
try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tail = (s) => TG.stripAnsi(String(s || '')).replace(/\r\n?/g, '\n').trim().split('\n').slice(-6).join('\n');
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The RED/catch proof for one phase.
|
|
88
|
+
* @param {string} cwd
|
|
89
|
+
* @param {string} phase
|
|
90
|
+
* @param {{timeoutMs?: number}} [opts]
|
|
91
|
+
* @returns {Object} `{phase, checked, ok, base, files: [{file, verdict, command, base_exit, head_exit, detail}], reason?}` or `{error, reason}`
|
|
92
|
+
*/
|
|
93
|
+
function checkRed(cwd, phase, opts = {}) {
|
|
94
|
+
const info = findPhaseInternal(cwd, phase);
|
|
95
|
+
if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
96
|
+
const phaseNumber = info.phase_number || String(phase);
|
|
97
|
+
const b = phaseBase(cwd, phaseNumber);
|
|
98
|
+
if (!b.base) return { phase: phaseNumber, checked: false, ok: true, files: [], reason: b.reason };
|
|
99
|
+
const tests = [...b.files].filter((f) => isTestFile(f) && fs.existsSync(path.join(cwd, f))).sort();
|
|
100
|
+
if (!tests.length) return { phase: phaseNumber, checked: false, ok: true, base: b.base, files: [], reason: 'the phase changed no test file' };
|
|
101
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_FILE_TIMEOUT_MS;
|
|
102
|
+
const files = [];
|
|
103
|
+
const wt = withWorktree(cwd, b.base, (dir) => {
|
|
104
|
+
for (const file of tests) {
|
|
105
|
+
const fc = TG.fileCommand(cwd, file);
|
|
106
|
+
if (!fc.command) { files.push({ file, verdict: 'not_checked', detail: fc.reason }); continue; }
|
|
107
|
+
const dest = path.join(dir, file);
|
|
108
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
109
|
+
fs.copyFileSync(path.join(cwd, file), dest);
|
|
110
|
+
const atBase = TG.runCommand(dir, fc.command, timeoutMs);
|
|
111
|
+
const atHead = TG.runCommand(cwd, fc.command, timeoutMs);
|
|
112
|
+
const red = atBase.exit !== 0;
|
|
113
|
+
const green = atHead.exit === 0;
|
|
114
|
+
const verdict = red && green ? 'caught' : (!red ? 'not_red' : 'not_green');
|
|
115
|
+
const detail = verdict === 'not_red'
|
|
116
|
+
? 'passes without the phase\'s code: it is tautological, or tests something the phase did not change'
|
|
117
|
+
: verdict === 'not_green' ? `fails at HEAD: ${tail(atHead.output)}` : null;
|
|
118
|
+
files.push({ file, verdict, command: fc.command, base_exit: atBase.exit, head_exit: atHead.exit, ...(detail ? { detail } : {}) });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
if (wt.error) return { phase: phaseNumber, checked: false, ok: true, base: b.base, files: [], reason: wt.error };
|
|
122
|
+
const ran = files.filter((f) => f.verdict !== 'not_checked');
|
|
123
|
+
return {
|
|
124
|
+
phase: phaseNumber, checked: ran.length > 0, ok: ran.every((f) => f.verdict === 'caught'), base: b.base,
|
|
125
|
+
caught: ran.filter((f) => f.verdict === 'caught').length, files,
|
|
126
|
+
...(ran.length ? {} : { reason: 'no changed test file could be run on its own' }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Prove a review finding with a catching test (program R5): the inverse of RED. The
|
|
132
|
+
* test shows the defect the phase introduced when it PASSES at the phase's base (the
|
|
133
|
+
* behaviour was right before) and FAILS at HEAD (the phase broke it).
|
|
134
|
+
*
|
|
135
|
+
* confirmed passes at the base, fails at HEAD
|
|
136
|
+
* fails_at_base fails before the phase too: not specific to the change
|
|
137
|
+
* passes_at_head the defect it claims is not there, or the test does not reach it
|
|
138
|
+
* not_checked no base, no command for the file, or the worktree failed (with why)
|
|
139
|
+
*
|
|
140
|
+
* @returns {{status, test, base?, command?, base_exit?, head_exit?, detail?}}
|
|
141
|
+
*/
|
|
142
|
+
function proveFinding(cwd, phase, testFile, opts = {}) {
|
|
143
|
+
const test = String(testFile || '').split(path.sep).join('/').split('\\').join('/');
|
|
144
|
+
if (!test || !fs.existsSync(path.join(cwd, test))) return { status: 'not_checked', test, detail: 'the test file does not exist' };
|
|
145
|
+
const info = findPhaseInternal(cwd, phase);
|
|
146
|
+
const b = phaseBase(cwd, (info && info.phase_number) || String(phase));
|
|
147
|
+
if (!b.base) return { status: 'not_checked', test, detail: b.reason };
|
|
148
|
+
const fc = TG.fileCommand(cwd, test);
|
|
149
|
+
if (!fc.command) return { status: 'not_checked', test, detail: fc.reason };
|
|
150
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_FILE_TIMEOUT_MS;
|
|
151
|
+
let atBase = null;
|
|
152
|
+
const wt = withWorktree(cwd, b.base, (dir) => {
|
|
153
|
+
const dest = path.join(dir, test);
|
|
154
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
155
|
+
fs.copyFileSync(path.join(cwd, test), dest);
|
|
156
|
+
atBase = TG.runCommand(dir, fc.command, timeoutMs);
|
|
157
|
+
});
|
|
158
|
+
if (wt.error) return { status: 'not_checked', test, base: b.base, detail: wt.error };
|
|
159
|
+
const atHead = TG.runCommand(cwd, fc.command, timeoutMs);
|
|
160
|
+
const status = atBase.exit !== 0 ? 'fails_at_base' : (atHead.exit === 0 ? 'passes_at_head' : 'confirmed');
|
|
161
|
+
const detail = status === 'fails_at_base' ? `fails before the phase too: ${tail(atBase.output)}`
|
|
162
|
+
: status === 'passes_at_head' ? 'passes at HEAD: it does not show the defect the finding claims' : null;
|
|
163
|
+
return { status, test, base: b.base, command: fc.command, base_exit: atBase.exit, head_exit: atHead.exit, ...(detail ? { detail } : {}) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `pan-tools verify red <phase> [--timeout <ms>]`: exit 1 when a test is not_red or not_green. */
|
|
167
|
+
function cmdVerifyRed(cwd, phase, raw, opts = {}) {
|
|
168
|
+
if (!phase) error('Usage: verify red <phase> [--timeout <ms>]');
|
|
169
|
+
const r = checkRed(cwd, phase, { timeoutMs: opts.timeout ? Number(opts.timeout) : undefined });
|
|
170
|
+
if (r.error) error(r.reason);
|
|
171
|
+
output(r, raw, !r.checked ? 'not_checked' : (r.ok ? 'caught' : 'not_caught'), r.ok ? 0 : 1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { checkRed, cmdVerifyRed, phaseBase, withWorktree, proveFinding };
|
|
@@ -38,9 +38,10 @@ function collectVerificationStats(phasesDir) {
|
|
|
38
38
|
// Extract gap descriptions from ## Gaps section
|
|
39
39
|
const gapsMatch = content.match(/## Gaps[\s\S]*?(?=\n## |$)/);
|
|
40
40
|
if (gapsMatch) {
|
|
41
|
-
|
|
41
|
+
// Bullets and numbered items both (L6b: numbered gap lists were dropped).
|
|
42
|
+
const lines = gapsMatch[0].split('\n').filter(l => l.match(/^(?:[-*]|\d+[.)])\s+/));
|
|
42
43
|
for (const line of lines) {
|
|
43
|
-
const desc = line.replace(/^[-*]\s+/, '').trim();
|
|
44
|
+
const desc = line.replace(/^(?:[-*]|\d+[.)])\s+/, '').trim();
|
|
44
45
|
if (desc) stats.gap_patterns.push(desc);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { safeReadFile, execGit, findPhaseInternal, toPosix, output, escapeRegex } = require('./core.cjs');
|
|
20
|
-
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
20
|
+
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
|
|
21
21
|
const { isPlanFile, isSummaryFile, isVerificationFile } = require('./constants.cjs');
|
|
22
22
|
const { planningRel } = require('./utils.cjs');
|
|
23
23
|
|
|
@@ -151,6 +151,147 @@ function scopePhase(cwd, phase) {
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// ─── Review scope (program R1) ───────────────────────────────────────────────
|
|
155
|
+
// The reviewer used to get the summaries' key-files: what the executor said it touched.
|
|
156
|
+
// A file the executor changed and did not list was never reviewed. The review scope is
|
|
157
|
+
// the diff instead (the phase's plan commits, plus work not yet committed), with the
|
|
158
|
+
// intent the code is judged against: each plan's objective, must_haves and Review Focus,
|
|
159
|
+
// and the text of the requirements the plans claim.
|
|
160
|
+
|
|
161
|
+
/** Files modified, staged or untracked in the working tree, relative to cwd. */
|
|
162
|
+
function workingTreeFiles(cwd) {
|
|
163
|
+
// Porcelain v2 with -z: each entry starts with its type (`1`, `2`, `u`, `?`), never a
|
|
164
|
+
// space, and paths come unquoted. v1 starts an unstaged edit with a space (" M a.js"),
|
|
165
|
+
// which execGit's trim() cut off the first line, and the path lost its first letter.
|
|
166
|
+
const r = execGit(cwd, ['status', '--porcelain=v2', '-z', '--untracked-files=all', '--', '.']);
|
|
167
|
+
if (r.exitCode !== 0) return { git: false, uncommitted: [], untracked: [] };
|
|
168
|
+
const top = execGit(cwd, ['rev-parse', '--show-prefix']);
|
|
169
|
+
const prefix = top.exitCode === 0 ? normalisePath(top.stdout.trim()) : '';
|
|
170
|
+
const rel = (p) => { const n = normalisePath(p); return prefix && n.startsWith(prefix) ? n.slice(prefix.length) : n; };
|
|
171
|
+
const uncommitted = [];
|
|
172
|
+
const untracked = [];
|
|
173
|
+
const entries = r.stdout.split('\0');
|
|
174
|
+
for (let i = 0; i < entries.length; i++) {
|
|
175
|
+
const e = entries[i];
|
|
176
|
+
if (!e) continue;
|
|
177
|
+
const f = e.split(' ');
|
|
178
|
+
if (e[0] === '?') untracked.push(rel(e.slice(2)));
|
|
179
|
+
else if (e[0] === '1') { if (!f[1].includes('D')) uncommitted.push(rel(f.slice(8).join(' '))); }
|
|
180
|
+
else if (e[0] === '2') { if (!f[1].includes('D')) uncommitted.push(rel(f.slice(9).join(' '))); i++; } // the next entry is the old path
|
|
181
|
+
else if (e[0] === 'u') uncommitted.push(rel(f.slice(10).join(' ')));
|
|
182
|
+
}
|
|
183
|
+
return { git: true, uncommitted, untracked };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A markdown section's body, from its `## <title>` heading to the next heading of the same or higher level. */
|
|
187
|
+
function sectionBody(text, title) {
|
|
188
|
+
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
189
|
+
const re = new RegExp(`^(#{1,6})\\s+${escapeRegex(title)}\\s*$`, 'i');
|
|
190
|
+
const at = lines.findIndex((l) => re.test(l.trim()));
|
|
191
|
+
if (at === -1) return null;
|
|
192
|
+
const level = lines[at].trim().match(/^#+/)[0].length;
|
|
193
|
+
const out = [];
|
|
194
|
+
for (let i = at + 1; i < lines.length; i++) {
|
|
195
|
+
const h = lines[i].match(/^(#{1,6})\s/);
|
|
196
|
+
if (h && h[1].length <= level) break;
|
|
197
|
+
out.push(lines[i]);
|
|
198
|
+
}
|
|
199
|
+
const body = out.join('\n').trim();
|
|
200
|
+
return body || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The text between `<tag>` and `</tag>`, trimmed, or null. */
|
|
204
|
+
function tagBody(text, tag) {
|
|
205
|
+
const m = String(text || '').match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
|
|
206
|
+
return m && m[1].trim() ? m[1].trim() : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* What a reviewer of one phase reads: the diff and the intent.
|
|
211
|
+
* @returns {Object} `{phase, directory, base, files: [{path, sources, listed}], unlisted, excluded,
|
|
212
|
+
* intent: {plans: [{plan, objective, must_haves, review_focus, requirements}], requirements: [{id, text}], missing_requirements}, git, commits}`
|
|
213
|
+
* or `{error, reason}`. `sources` is any of commit · summary · uncommitted · untracked;
|
|
214
|
+
* `listed` is whether a summary's key-files names it; `unlisted` lists the files that are not.
|
|
215
|
+
*/
|
|
216
|
+
function reviewScope(cwd, phase) {
|
|
217
|
+
const info = findPhaseInternal(cwd, phase);
|
|
218
|
+
if (!info) return { error: 'phase_not_found', reason: `no phase directory for phase ${phase}` };
|
|
219
|
+
const dir = path.join(cwd, info.directory);
|
|
220
|
+
const sources = new Map();
|
|
221
|
+
const note = (file, source) => {
|
|
222
|
+
if (!file) return;
|
|
223
|
+
if (!sources.has(file)) sources.set(file, new Set());
|
|
224
|
+
sources.get(file).add(source);
|
|
225
|
+
};
|
|
226
|
+
const listed = new Set();
|
|
227
|
+
for (const f of info.summaries.filter(isSummaryFile)) {
|
|
228
|
+
const fm = extractFrontmatter(safeReadFile(path.join(dir, f)) || '');
|
|
229
|
+
const kf = fm['key-files'] || fm.key_files || {};
|
|
230
|
+
for (const p of [...asList(kf.created), ...asList(kf.modified)]) { const n = normalisePath(p); if (n) { listed.add(n); note(n, 'summary'); } }
|
|
231
|
+
}
|
|
232
|
+
const git = committedFiles(cwd, info.phase_number);
|
|
233
|
+
for (const p of git.files) note(p, 'commit');
|
|
234
|
+
const wt = workingTreeFiles(cwd);
|
|
235
|
+
for (const p of wt.uncommitted) note(p, 'uncommitted');
|
|
236
|
+
for (const p of wt.untracked) note(p, 'untracked');
|
|
237
|
+
|
|
238
|
+
const planningPrefix = normalisePath(planningRel());
|
|
239
|
+
const files = [];
|
|
240
|
+
const excluded = [];
|
|
241
|
+
for (const file of [...sources.keys()].sort()) {
|
|
242
|
+
const why = exclusionReason(file, planningPrefix);
|
|
243
|
+
if (why) { excluded.push({ path: file, reason: why }); continue; }
|
|
244
|
+
// A file deleted by the phase has nothing left to read.
|
|
245
|
+
if (!fs.existsSync(path.join(cwd, file))) continue;
|
|
246
|
+
files.push({ path: file, sources: [...sources.get(file)].sort(), listed: listed.has(file) });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const plans = [];
|
|
250
|
+
const reqIds = new Set();
|
|
251
|
+
for (const f of info.plans.filter(isPlanFile).sort()) {
|
|
252
|
+
const text = safeReadFile(path.join(dir, f)) || '';
|
|
253
|
+
const fm = extractFrontmatter(text);
|
|
254
|
+
const requirements = asList(fm.requirements).map((s) => s.trim()).filter(Boolean);
|
|
255
|
+
for (const id of requirements) reqIds.add(id);
|
|
256
|
+
const mh = fm.must_haves && typeof fm.must_haves === 'object' ? fm.must_haves : {};
|
|
257
|
+
plans.push({
|
|
258
|
+
plan: f,
|
|
259
|
+
objective: tagBody(text, 'objective'),
|
|
260
|
+
must_haves: { truths: asList(mh.truths), artifacts: parseMustHavesBlock(text, 'artifacts'), key_links: parseMustHavesBlock(text, 'key_links') },
|
|
261
|
+
review_focus: sectionBody(text, 'Review Focus'),
|
|
262
|
+
requirements,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const { requirementLines } = require('./roadmap.cjs');
|
|
266
|
+
const reqText = safeReadFile(path.join(cwd, planningRel(), 'requirements.md')) || '';
|
|
267
|
+
const ids = [...reqIds].sort();
|
|
268
|
+
const requirements = ids.map((id) => ({ id, text: requirementLines(reqText, [id]).lines.join('\n').trim() || null }));
|
|
269
|
+
|
|
270
|
+
let base = null;
|
|
271
|
+
if (git.hashes.length) {
|
|
272
|
+
const r = execGit(cwd, ['rev-parse', '--verify', '-q', `${git.hashes[git.hashes.length - 1]}^`]);
|
|
273
|
+
if (r.exitCode === 0 && r.stdout) base = r.stdout.trim();
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
phase: info.phase_number,
|
|
277
|
+
directory: info.directory,
|
|
278
|
+
base,
|
|
279
|
+
files,
|
|
280
|
+
unlisted: files.filter((f) => !f.listed).map((f) => f.path),
|
|
281
|
+
excluded,
|
|
282
|
+
intent: { plans, requirements, missing_requirements: requirements.filter((r) => !r.text).map((r) => r.id) },
|
|
283
|
+
git: git.git,
|
|
284
|
+
commits: git.commits,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** `pan-tools review scope <phase>`: the JSON scope; `--raw` prints the file paths, one per line. */
|
|
289
|
+
function cmdReviewScope(cwd, phase, raw) {
|
|
290
|
+
if (!phase) { output({ error: 'phase_required', reason: 'Usage: review scope <phase>' }, raw); return; }
|
|
291
|
+
const r = reviewScope(cwd, phase);
|
|
292
|
+
output(r, raw, r.error ? undefined : r.files.map((f) => f.path).join('\n'));
|
|
293
|
+
}
|
|
294
|
+
|
|
154
295
|
function cmdVerifyScope(cwd, phase, raw) {
|
|
155
296
|
if (!phase) { output({ error: 'phase_required', reason: 'Usage: verify scope <phase>' }, raw); return; }
|
|
156
297
|
const r = scopePhase(cwd, phase);
|
|
@@ -224,4 +365,4 @@ function cmdVerifyStale(cwd, phase, raw) {
|
|
|
224
365
|
output(r, raw, r.error ? undefined : r.state);
|
|
225
366
|
}
|
|
226
367
|
|
|
227
|
-
module.exports = { scopePhase, cmdVerifyScope, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };
|
|
368
|
+
module.exports = { scopePhase, cmdVerifyScope, reviewScope, cmdReviewScope, workingTreeFiles, sectionBody, verificationFreshness, cmdVerifyStale, coveredFiles, committedFiles, LOCKFILES, TEST_PATH_RE, TEST_FILE_RE };
|