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
|
@@ -278,9 +278,9 @@ function cmdPhasePlanIndex(cwd, phase, raw, opts = {}) {
|
|
|
278
278
|
const planFiles = filterPlanFiles(phaseFiles);
|
|
279
279
|
const summaryFiles = filterSummaryFiles(phaseFiles);
|
|
280
280
|
|
|
281
|
-
const { plans, waves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
|
|
281
|
+
const { plans, waves, subWaves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
|
|
282
282
|
|
|
283
|
-
const result = { phase: normalized, plans, waves, incomplete, has_checkpoints: hasCheckpoints };
|
|
283
|
+
const result = { phase: normalized, plans, waves, sub_waves: subWaves, incomplete, has_checkpoints: hasCheckpoints };
|
|
284
284
|
// `--failed <ids>` (market-ideas M28): the plans that must not run after those
|
|
285
285
|
// failed, because a dependency, direct or transitive, is among them.
|
|
286
286
|
if (opts.failed) {
|
|
@@ -336,6 +336,35 @@ function blockedPlans(plans, failedIds) {
|
|
|
336
336
|
* @param {string[]} summaryFiles - List of summary filenames
|
|
337
337
|
* @returns {{plans: Array, waves: Object, incomplete: string[], hasCheckpoints: boolean}}
|
|
338
338
|
*/
|
|
339
|
+
/**
|
|
340
|
+
* Split one wave's plans into sub-waves so no two plans in a sub-wave modify the same
|
|
341
|
+
* file (program A3). Greedy, in plan order: each plan joins the first sub-wave it does
|
|
342
|
+
* not overlap. A plan that declares no files may touch anything, so it runs alone.
|
|
343
|
+
* @param {Array<{id: string, files_modified?: string[]}>} plans
|
|
344
|
+
* @returns {string[][]} plan ids per sub-wave
|
|
345
|
+
*/
|
|
346
|
+
function partitionWave(plans) {
|
|
347
|
+
const subs = [];
|
|
348
|
+
const norm = (p) => String(p).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
349
|
+
for (const p of plans) {
|
|
350
|
+
const files = (p.files_modified || []).map(norm);
|
|
351
|
+
const solo = files.length === 0;
|
|
352
|
+
let placed = false;
|
|
353
|
+
if (!solo) {
|
|
354
|
+
for (const sub of subs) {
|
|
355
|
+
if (sub.solo) continue;
|
|
356
|
+
if (files.some((x) => sub.files.has(x))) continue;
|
|
357
|
+
sub.ids.push(p.id);
|
|
358
|
+
for (const x of files) sub.files.add(x);
|
|
359
|
+
placed = true;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!placed) subs.push({ ids: [p.id], files: new Set(files), solo });
|
|
364
|
+
}
|
|
365
|
+
return subs.map((x) => x.ids);
|
|
366
|
+
}
|
|
367
|
+
|
|
339
368
|
function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
|
|
340
369
|
const completedPlanIds = new Set(
|
|
341
370
|
summaryFiles.map(summaryFile => summaryFile.replace('-summary.md', '').replace('summary.md', ''))
|
|
@@ -401,7 +430,14 @@ function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
|
|
|
401
430
|
waves[waveKey].push(planId);
|
|
402
431
|
}
|
|
403
432
|
|
|
404
|
-
|
|
433
|
+
// A3: within a wave, plans that share a file run in different sub-waves.
|
|
434
|
+
const subWaves = {};
|
|
435
|
+
for (const [w, ids] of Object.entries(waves)) {
|
|
436
|
+
subWaves[w] = partitionWave(ids.map((id) => plans.find((p) => p.id === id)));
|
|
437
|
+
subWaves[w].forEach((sub, i) => { for (const id of sub) plans.find((p) => p.id === id).sub_wave = i; });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { plans, waves, subWaves, incomplete, hasCheckpoints };
|
|
405
441
|
}
|
|
406
442
|
|
|
407
443
|
/**
|
|
@@ -430,6 +466,30 @@ function appendPhaseChecklistEntry(content, phaseNum, name) {
|
|
|
430
466
|
return lines.join('\n');
|
|
431
467
|
}
|
|
432
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Add an inserted phase's "## Phases" checklist entry after its base phase's entry and
|
|
471
|
+
* that phase's existing decimals, so the list stays in numeric order (appending put 3.1
|
|
472
|
+
* after Phase 4; found by H7's property test). Falls back to appending when the base
|
|
473
|
+
* phase has no checklist entry.
|
|
474
|
+
*
|
|
475
|
+
* @param {string} content - Full roadmap.md content
|
|
476
|
+
* @param {string} basePhase - The integer phase inserted after, without leading zeros
|
|
477
|
+
* @param {string} phaseNum - The inserted phase's number as it should appear
|
|
478
|
+
* @param {string} name - Phase name/description
|
|
479
|
+
* @returns {string} Updated content
|
|
480
|
+
*/
|
|
481
|
+
function insertPhaseChecklistEntry(content, basePhase, phaseNum, name) {
|
|
482
|
+
const lines = content.split('\n');
|
|
483
|
+
const own = new RegExp(`^\\s*- \\[[ x]\\]\\s*(?:\\*\\*)?Phase\\s+0*${escapeRegex(basePhase)}(?:\\.\\d+)*:`, 'i');
|
|
484
|
+
let lastIdx = -1;
|
|
485
|
+
for (let i = 0; i < lines.length; i++) {
|
|
486
|
+
if (own.test(lines[i])) lastIdx = i;
|
|
487
|
+
}
|
|
488
|
+
if (lastIdx === -1) return appendPhaseChecklistEntry(content, phaseNum, name);
|
|
489
|
+
lines.splice(lastIdx + 1, 0, `- [ ] **Phase ${phaseNum}: ${name}**`);
|
|
490
|
+
return lines.join('\n');
|
|
491
|
+
}
|
|
492
|
+
|
|
433
493
|
/**
|
|
434
494
|
* Append a new phase to the end of the roadmap and create its directory.
|
|
435
495
|
* @param {string} cwd - Working directory path
|
|
@@ -581,20 +641,24 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
581
641
|
}
|
|
582
642
|
|
|
583
643
|
const headerIdx = content.indexOf(headerMatch[0]);
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
644
|
+
const bodyStart = headerIdx + headerMatch[0].length;
|
|
645
|
+
const afterHeader = content.slice(bodyStart);
|
|
646
|
+
// The new section goes before the next phase that is not one of this phase's decimals:
|
|
647
|
+
// after 06.1 and 06.2, not straight after Phase 6, or the roadmap (and `roadmap analyze`,
|
|
648
|
+
// which lists phases in roadmap order) would read 6, 6.3, 6.2, 6.1 (found by H7's
|
|
649
|
+
// property test).
|
|
650
|
+
const ownDecimal = new RegExp(`^#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}\\.\\d`, 'i');
|
|
651
|
+
const headingRe = /\n(#{2,4}\s+Phase\s+\d[^\n]*)/gi;
|
|
652
|
+
let insertIdx = content.length;
|
|
653
|
+
let heading;
|
|
654
|
+
while ((heading = headingRe.exec(afterHeader)) !== null) {
|
|
655
|
+
if (!ownDecimal.test(heading[1])) { insertIdx = bodyStart + heading.index; break; }
|
|
592
656
|
}
|
|
593
657
|
|
|
594
658
|
// Insert the detail section, then the matching checklist entry — same reason as
|
|
595
659
|
// cmdPhaseAdd: without it the inserted phase never appears in the "## Phases" list.
|
|
596
660
|
const withEntry = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
|
|
597
|
-
const updatedContent =
|
|
661
|
+
const updatedContent = insertPhaseChecklistEntry(withEntry, unpadded, decimalPhase, description);
|
|
598
662
|
try {
|
|
599
663
|
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
|
600
664
|
} catch (e) {
|
|
@@ -713,8 +777,14 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
|
|
|
713
777
|
const reqIds = roadmapRequirementIds(roadmapContent, phaseNum);
|
|
714
778
|
if (reqIds.length === 0) return;
|
|
715
779
|
let reqContent = reqReadContent;
|
|
780
|
+
// V8: a requirement with EARS criteria is ticked only when its mapped tests pass;
|
|
781
|
+
// a legacy one (no criteria) keeps the old tick. Held ones are reported.
|
|
782
|
+
const decisions = require('./requirements-lint.cjs').tickDecisions(cwd, reqIds);
|
|
783
|
+
const held = [];
|
|
716
784
|
|
|
717
785
|
for (const reqId of reqIds) {
|
|
786
|
+
const d = decisions.get(reqId);
|
|
787
|
+
if (d && !d.allowed) { held.push({ id: reqId, status: d.status }); continue; }
|
|
718
788
|
const reqEscaped = escapeRegex(reqId);
|
|
719
789
|
reqContent = reqContent.replace(
|
|
720
790
|
new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'),
|
|
@@ -731,7 +801,7 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
|
|
|
731
801
|
} catch (e) {
|
|
732
802
|
return { updated: false, error: 'Failed to write requirements.md: ' + e.message };
|
|
733
803
|
}
|
|
734
|
-
return { updated: true };
|
|
804
|
+
return { updated: true, held };
|
|
735
805
|
} catch {
|
|
736
806
|
// requirements.md not found or unreadable — skip
|
|
737
807
|
return { updated: false };
|
|
@@ -999,6 +1069,7 @@ function classifyPlanTier(frontmatter, config) {
|
|
|
999
1069
|
}
|
|
1000
1070
|
|
|
1001
1071
|
module.exports = {
|
|
1072
|
+
partitionWave,
|
|
1002
1073
|
cmdPhasesList,
|
|
1003
1074
|
cmdPhaseNextDecimal,
|
|
1004
1075
|
cmdFindPhase,
|
|
@@ -1016,6 +1087,7 @@ module.exports = {
|
|
|
1016
1087
|
renumberIntegerPhases,
|
|
1017
1088
|
updateRoadmapAfterRemoval,
|
|
1018
1089
|
markPhaseCompleteInRoadmap,
|
|
1090
|
+
markRequirementsCompleteForPhase,
|
|
1019
1091
|
updateStateAfterPhaseComplete,
|
|
1020
1092
|
classifyPlanTier,
|
|
1021
1093
|
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Plan intent checked in code: the parts of a plan check that are reading, not
|
|
4
|
+
* judgement, so the plan-checker is not spent on them.
|
|
5
|
+
*
|
|
6
|
+
* `verify review-focus <phase>` (program P4, M41). A plan names, under
|
|
7
|
+
* `## Review Focus`, at most five inputs or failure modes its requirements imply and
|
|
8
|
+
* no test exercises yet, each pinned to the task that owns the code and the test that
|
|
9
|
+
* task adds. A plan with none says so on one `None — <reason>` line. The reviewer gets
|
|
10
|
+
* the section through `review scope` (R1).
|
|
11
|
+
*
|
|
12
|
+
* `verify decisions <phase>` (program P2). The phase design records its decisions with
|
|
13
|
+
* ids (`DD-N`), and only the ones that pass the three-question test (hard to reverse,
|
|
14
|
+
* a reasonable engineer could choose otherwise, more than one task depends on it).
|
|
15
|
+
* Plans cite the ids they implement. A plan citing an id the design does not record
|
|
16
|
+
* relies on a decision nobody approved; a recorded decision no plan cites is approved
|
|
17
|
+
* design nobody builds. A design from before ids is `legacy`: a warning, never a refusal.
|
|
18
|
+
*/
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { output, error, toLf } = require('./core.cjs');
|
|
22
|
+
const { getPlanId } = require('./constants.cjs');
|
|
23
|
+
const { phaseFiles } = require('./verify-plan-set.cjs');
|
|
24
|
+
const { sectionBody } = require('./verify-scope.cjs');
|
|
25
|
+
|
|
26
|
+
const REVIEW_FOCUS_MAX = 5;
|
|
27
|
+
const DECISION_ID_RE = /\bDD-\d+\b/g;
|
|
28
|
+
|
|
29
|
+
function readText(file) {
|
|
30
|
+
try { return toLf(fs.readFileSync(file, 'utf-8')); } catch { return ''; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The phase's plans as `{plan, file, text}`, or `{error, reason}`. */
|
|
34
|
+
function phasePlans(cwd, phase) {
|
|
35
|
+
const pf = phaseFiles(cwd, phase);
|
|
36
|
+
if (pf.error) return pf;
|
|
37
|
+
return { pf, plans: pf.planFiles.map((file) => ({ plan: getPlanId(file), file, text: readText(path.join(pf.dir, file)) })) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Review Focus (P4) ───────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Text with HTML comments removed: repeated until nothing changes, since removing
|
|
44
|
+
* one can join the text around it into another, and an unclosed `<!--` hides the
|
|
45
|
+
* rest. A placeholder comment is not a Review Focus item.
|
|
46
|
+
*/
|
|
47
|
+
function stripComments(s) {
|
|
48
|
+
let prev;
|
|
49
|
+
do { prev = s; s = s.replace(/<!--[\s\S]*?-->/g, ''); } while (s !== prev);
|
|
50
|
+
const open = s.indexOf('<!--');
|
|
51
|
+
return open === -1 ? s : s.slice(0, open);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Check one plan's `## Review Focus`. */
|
|
55
|
+
function reviewFocusOf(p) {
|
|
56
|
+
const body = sectionBody(p.text, 'Review Focus');
|
|
57
|
+
const out = { plan: p.plan, file: p.file, status: 'ok', items: 0 };
|
|
58
|
+
const errors = [];
|
|
59
|
+
if (body == null) {
|
|
60
|
+
errors.push({ code: 'review_focus_missing', plan: p.plan, detail: 'no `## Review Focus` section' });
|
|
61
|
+
return { ...out, status: 'missing', errors };
|
|
62
|
+
}
|
|
63
|
+
const text = stripComments(body).trim();
|
|
64
|
+
const items = text.split('\n').map((l) => l.match(/^\s*(?:[-*]|\d+[.)])\s+(.*\S)/)).filter(Boolean).map((m) => m[1]);
|
|
65
|
+
if (!items.length) {
|
|
66
|
+
// `None — <reason>`: the reason is what tells the checker the planner looked.
|
|
67
|
+
const none = text.match(/^none\b[\s—–:-]*(.*)$/im);
|
|
68
|
+
if (none && none[1].trim().split(/\s+/).length >= 3) return { ...out, status: 'none', errors };
|
|
69
|
+
errors.push({ code: 'review_focus_empty', plan: p.plan, detail: 'no items, and no `None — <reason>` line' });
|
|
70
|
+
return { ...out, status: 'invalid', errors };
|
|
71
|
+
}
|
|
72
|
+
out.items = items.length;
|
|
73
|
+
if (items.length > REVIEW_FOCUS_MAX) {
|
|
74
|
+
errors.push({ code: 'review_focus_too_many', plan: p.plan, detail: `${items.length} items; at most ${REVIEW_FOCUS_MAX}` });
|
|
75
|
+
}
|
|
76
|
+
const taskCount = (p.text.match(/<task\b/g) || []).length;
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
const short = item.slice(0, 80);
|
|
79
|
+
const task = item.match(/\bTask\s+(\d+)\b/i);
|
|
80
|
+
if (!task) errors.push({ code: 'review_focus_no_task', plan: p.plan, detail: `names no owning task: ${short}` });
|
|
81
|
+
else if (Number(task[1]) < 1 || Number(task[1]) > taskCount) errors.push({ code: 'review_focus_unknown_task', plan: p.plan, detail: `Task ${task[1]} is not one of the plan's ${taskCount} tasks: ${short}` });
|
|
82
|
+
if (!/\btests?\b/i.test(item)) errors.push({ code: 'review_focus_no_test', plan: p.plan, detail: `names no test: ${short}` });
|
|
83
|
+
}
|
|
84
|
+
return { ...out, status: errors.length ? 'invalid' : 'ok', errors };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @returns {{phase, ok, plans: Array<{plan, file, status, items}>, errors}} or `{error, reason}`
|
|
89
|
+
*/
|
|
90
|
+
function checkReviewFocus(cwd, phase) {
|
|
91
|
+
const pp = phasePlans(cwd, phase);
|
|
92
|
+
if (pp.error) return pp;
|
|
93
|
+
const results = pp.plans.map(reviewFocusOf);
|
|
94
|
+
const errors = results.flatMap((r) => r.errors);
|
|
95
|
+
return { phase: pp.pf.info.phase_number, ok: errors.length === 0, plans: results.map(({ errors: _e, ...r }) => r), errors };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Decision spine (P2) ─────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/** Decision ids a design records: the first cell of a table row, or the head of a list item. */
|
|
101
|
+
function recordedDecisions(text) {
|
|
102
|
+
const ids = [];
|
|
103
|
+
for (const line of text.split('\n')) {
|
|
104
|
+
const m = line.match(/^\s*\|\s*\**(DD-\d+)\**\s*\|/) || line.match(/^\s*(?:[-*]|\d+[.)])\s+\**(DD-\d+)\**\b/) || line.match(/^#{2,6}\s+(DD-\d+)\b/);
|
|
105
|
+
if (m) ids.push(m[1]);
|
|
106
|
+
}
|
|
107
|
+
return ids;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @returns {{phase, ok, status: 'checked'|'no_design'|'legacy', design, decisions, cited, errors, warnings}} or `{error, reason}`
|
|
112
|
+
*/
|
|
113
|
+
function checkDecisions(cwd, phase) {
|
|
114
|
+
const pp = phasePlans(cwd, phase);
|
|
115
|
+
if (pp.error) return pp;
|
|
116
|
+
const base = { phase: pp.pf.info.phase_number, ok: true, design: null, decisions: [], cited: {}, errors: [], warnings: [] };
|
|
117
|
+
let designFile = null;
|
|
118
|
+
try { designFile = fs.readdirSync(pp.pf.dir).filter((f) => /-design\.md$/i.test(f)).sort()[0] || null; } catch { /* phaseFiles read it */ }
|
|
119
|
+
if (!designFile) return { ...base, status: 'no_design' };
|
|
120
|
+
const recorded = recordedDecisions(readText(path.join(pp.pf.dir, designFile)));
|
|
121
|
+
const design = path.posix.join(pp.pf.info.directory.replace(/\\/g, '/'), designFile);
|
|
122
|
+
if (!recorded.length) {
|
|
123
|
+
return { ...base, status: 'legacy', design, warnings: [{ code: 'decisions_without_ids', detail: `${designFile} records no DD-N decision ids; plans cannot cite them` }] };
|
|
124
|
+
}
|
|
125
|
+
const errors = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (const id of recorded) {
|
|
128
|
+
if (seen.has(id)) errors.push({ code: 'decision_duplicate', id, detail: `${id} is recorded more than once in ${designFile}` });
|
|
129
|
+
seen.add(id);
|
|
130
|
+
}
|
|
131
|
+
const cited = {};
|
|
132
|
+
for (const p of pp.plans) {
|
|
133
|
+
for (const id of new Set(p.text.match(DECISION_ID_RE) || [])) {
|
|
134
|
+
if (!seen.has(id)) errors.push({ code: 'decision_unrecorded', plan: p.plan, id, detail: `${p.plan} cites ${id}, which ${designFile} does not record` });
|
|
135
|
+
else (cited[id] = cited[id] || []).push(p.plan);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const decisions = [...seen].sort((a, b) => Number(a.slice(3)) - Number(b.slice(3)));
|
|
139
|
+
for (const id of decisions) {
|
|
140
|
+
if (!cited[id]) errors.push({ code: 'decision_uncited', id, detail: `${id} is recorded in ${designFile} and cited by no plan` });
|
|
141
|
+
}
|
|
142
|
+
return { ...base, ok: errors.length === 0, status: 'checked', design, decisions, cited, errors };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── CLI ─────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
// ─── Gate baselines (M48) ─────────────────────────────────────────────────────
|
|
148
|
+
//
|
|
149
|
+
// A plan's `<verify>` often names a grep or count gate. One that already passes
|
|
150
|
+
// before any work proves nothing about the task; one whose pattern cannot compile
|
|
151
|
+
// can never match (gsd-core v1.18.0 "gate baselines"). The simple grep forms are
|
|
152
|
+
// interpreted here, in Node, never through a shell: the same on every platform,
|
|
153
|
+
// and nothing a plan writes is executed. Anything else is listed as not checkable.
|
|
154
|
+
|
|
155
|
+
const GATE_TOOLS = [['git', 'grep'], ['grep'], ['rg']];
|
|
156
|
+
const GATE_FLAGS = new Set(['r', 'R', 'i', 'c', 'E', 'F', 'w', 'q', 'l', 'n', 's', 'H', 'h']);
|
|
157
|
+
const REGRESSION_GUARD = /regression guard|already passes|must still pass/i;
|
|
158
|
+
|
|
159
|
+
/** Split a command line on spaces, honouring quotes. Null on shell syntax. */
|
|
160
|
+
function shellWords(cmd) {
|
|
161
|
+
const words = [];
|
|
162
|
+
let cur = '';
|
|
163
|
+
let quote = null;
|
|
164
|
+
let started = false;
|
|
165
|
+
for (const ch of cmd) {
|
|
166
|
+
if (quote) {
|
|
167
|
+
if (ch === quote) quote = null; else cur += ch;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (ch === '"' || ch === "'") { quote = ch; started = true; continue; }
|
|
171
|
+
if (/[|;&<>`$()\\]/.test(ch)) return null;
|
|
172
|
+
if (/\s/.test(ch)) { if (started) { words.push(cur); cur = ''; started = false; } continue; }
|
|
173
|
+
cur += ch; started = true;
|
|
174
|
+
}
|
|
175
|
+
if (quote) return null;
|
|
176
|
+
if (started) words.push(cur);
|
|
177
|
+
return words;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse a simple grep gate: `grep|rg|git grep [flags] PATTERN [paths...]`.
|
|
182
|
+
* @returns {{tool, flags, pattern, paths}|null} null when the command is not one
|
|
183
|
+
*/
|
|
184
|
+
function parseGateCommand(cmd) {
|
|
185
|
+
const words = shellWords(String(cmd || '').trim());
|
|
186
|
+
if (!words || !words.length) return null;
|
|
187
|
+
const tool = GATE_TOOLS.find((t) => t.every((w, i) => words[i] === w));
|
|
188
|
+
if (!tool) return null;
|
|
189
|
+
const flags = {};
|
|
190
|
+
let pattern = null;
|
|
191
|
+
const paths = [];
|
|
192
|
+
let flagsDone = false;
|
|
193
|
+
for (const w of words.slice(tool.length)) {
|
|
194
|
+
if (!flagsDone && w === '--') { flagsDone = true; continue; }
|
|
195
|
+
if (!flagsDone && /^-[A-Za-z]+$/.test(w)) {
|
|
196
|
+
for (const f of w.slice(1)) { if (!GATE_FLAGS.has(f)) return null; flags[f] = true; }
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (!flagsDone && w.startsWith('-')) return null;
|
|
200
|
+
if (pattern === null) pattern = w; else paths.push(w);
|
|
201
|
+
}
|
|
202
|
+
if (pattern === null) return null;
|
|
203
|
+
return { tool: tool.join(' '), flags, pattern, paths };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A JS RegExp for a grep pattern: -F literal, -E extended, else basic (BRE). Null when it cannot compile. */
|
|
207
|
+
function gateRegex(g) {
|
|
208
|
+
let src = g.pattern;
|
|
209
|
+
if (g.flags.F) src = src.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
210
|
+
else if (!g.flags.E && g.tool !== 'rg') {
|
|
211
|
+
// BRE: \( \) \{ \} \| \+ \? are the operators; their bare forms are literals.
|
|
212
|
+
src = src.replace(/\\([(){}|+?])|([(){}|+?])/g, (m, esc, bare) => (esc ? esc : '\\' + bare));
|
|
213
|
+
}
|
|
214
|
+
if (g.flags.w) src = `\\b(?:${src})\\b`;
|
|
215
|
+
try { return new RegExp(src, g.flags.i ? 'i' : ''); } catch { return null; }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function filesUnder(root, rel, recursive) {
|
|
219
|
+
const abs = path.join(root, rel);
|
|
220
|
+
let st;
|
|
221
|
+
try { st = fs.statSync(abs); } catch { return null; }
|
|
222
|
+
if (st.isFile()) return [abs];
|
|
223
|
+
if (!st.isDirectory() || !recursive) return [];
|
|
224
|
+
const out = [];
|
|
225
|
+
const walk = (d) => {
|
|
226
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
227
|
+
if (e.name === '.git' || e.name === 'node_modules') continue;
|
|
228
|
+
const p = path.join(d, e.name);
|
|
229
|
+
if (e.isDirectory()) walk(p); else if (e.isFile()) out.push(p);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
walk(abs);
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Run a parsed gate against the tree as it is: passes_now | fails_now | target_missing | invalid. */
|
|
237
|
+
function runGate(cwd, g) {
|
|
238
|
+
const re = gateRegex(g);
|
|
239
|
+
if (!re) return 'invalid';
|
|
240
|
+
const recursive = g.tool !== 'grep' || g.flags.r || g.flags.R;
|
|
241
|
+
const targets = g.paths.length ? g.paths : ['.'];
|
|
242
|
+
let anyTarget = false;
|
|
243
|
+
for (const t of targets) {
|
|
244
|
+
const files = filesUnder(cwd, t, recursive);
|
|
245
|
+
if (files === null) continue;
|
|
246
|
+
anyTarget = true;
|
|
247
|
+
for (const f of files) {
|
|
248
|
+
let text;
|
|
249
|
+
try { text = toLf(fs.readFileSync(f, 'utf-8')); } catch { continue; }
|
|
250
|
+
if (text.split('\n').some((line) => re.test(line))) return 'passes_now';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return anyTarget ? 'fails_now' : 'target_missing';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The gate commands in a plan: backticked spans inside each task's <verify>. */
|
|
257
|
+
function gatesOf(text) {
|
|
258
|
+
const out = [];
|
|
259
|
+
const taskRe = /<task\b[^>]*>([\s\S]*?)<\/task>/g;
|
|
260
|
+
let t;
|
|
261
|
+
while ((t = taskRe.exec(text)) !== null) {
|
|
262
|
+
const name = ((t[1].match(/<name>([\s\S]*?)<\/name>/) || [])[1] || 'unnamed').trim();
|
|
263
|
+
const verify = (t[1].match(/<verify>([\s\S]*?)<\/verify>/) || [])[1] || '';
|
|
264
|
+
for (const m of verify.matchAll(/`([^`]+)`/g)) out.push({ task: name, command: m[1].trim(), guard: REGRESSION_GUARD.test(verify) });
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function checkGates(cwd, phase) {
|
|
270
|
+
const pp = phasePlans(cwd, phase);
|
|
271
|
+
if (pp.error) return pp;
|
|
272
|
+
const gates = [];
|
|
273
|
+
const errors = [];
|
|
274
|
+
for (const p of pp.plans) {
|
|
275
|
+
for (const g of gatesOf(p.text)) {
|
|
276
|
+
const parsed = parseGateCommand(g.command);
|
|
277
|
+
const status = parsed ? runGate(cwd, parsed) : 'not_checkable';
|
|
278
|
+
gates.push({ plan: p.plan, task: g.task, command: g.command, status, ...(g.guard ? { regression_guard: true } : {}) });
|
|
279
|
+
if (status === 'passes_now' && !g.guard) errors.push({ code: 'gate_vacuous', plan: p.plan, task: g.task, command: g.command, detail: 'passes before any work, so it cannot show the task did anything; check for what the task adds, or mark it a regression guard' });
|
|
280
|
+
if (status === 'invalid') errors.push({ code: 'gate_invalid', plan: p.plan, task: g.task, command: g.command, detail: 'the pattern does not compile, so the gate can never match as written' });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { phase: pp.pf.info.phase_number, ok: errors.length === 0, gates, errors };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function cmdVerifyGates(cwd, phase, raw) {
|
|
287
|
+
if (!phase) error('Usage: verify gates <phase>');
|
|
288
|
+
const r = checkGates(cwd, phase);
|
|
289
|
+
if (r.error) { output(r, raw); return; }
|
|
290
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function cmdVerifyReviewFocus(cwd, phase, raw) {
|
|
294
|
+
if (!phase) error('Usage: verify review-focus <phase>');
|
|
295
|
+
const r = checkReviewFocus(cwd, phase);
|
|
296
|
+
if (r.error) { output(r, raw); return; }
|
|
297
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function cmdVerifyDecisions(cwd, phase, raw) {
|
|
301
|
+
if (!phase) error('Usage: verify decisions <phase>');
|
|
302
|
+
const r = checkDecisions(cwd, phase);
|
|
303
|
+
if (r.error) { output(r, raw); return; }
|
|
304
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
REVIEW_FOCUS_MAX, checkReviewFocus, checkDecisions, recordedDecisions, cmdVerifyReviewFocus, cmdVerifyDecisions,
|
|
309
|
+
parseGateCommand, checkGates, cmdVerifyGates,
|
|
310
|
+
};
|
|
@@ -152,6 +152,9 @@ function buildPhasePreview(cwd, phaseNum) {
|
|
|
152
152
|
risk_signals,
|
|
153
153
|
risk_score,
|
|
154
154
|
plans: planContents.map(p => ({ file: p.file, bytes: Buffer.byteLength(p.content, 'utf-8') })),
|
|
155
|
+
// p10/p50/p90 dollars for this phase's plans, from the project's own cost ledger
|
|
156
|
+
// (P6); this phase's rows are left out of its own samples.
|
|
157
|
+
cost_forecast: require('./cost.cjs').costForecast(cwd, { plans: planFiles.length, excludePhase: phaseInfo.phase_number || phaseNum }),
|
|
155
158
|
};
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -368,7 +371,7 @@ function buildMilestoneETA(cwd) {
|
|
|
368
371
|
const remaining = phaseList.filter(p => !p.completed);
|
|
369
372
|
|
|
370
373
|
// Sample phase durations from summary frontmatter or commit metadata.
|
|
371
|
-
const durations = sampleCompletedPhaseDurations(cwd, completed);
|
|
374
|
+
const { durations, sources } = sampleCompletedPhaseDurations(cwd, completed, { withSources: true });
|
|
372
375
|
const avg = durations.length > 0
|
|
373
376
|
? durations.reduce((a, b) => a + b, 0) / durations.length
|
|
374
377
|
: 5.0; // default 5 days per phase when no history
|
|
@@ -405,6 +408,9 @@ function buildMilestoneETA(cwd) {
|
|
|
405
408
|
sample_size: durations.length,
|
|
406
409
|
eta_date: etaDate,
|
|
407
410
|
confidence_pct: confidence,
|
|
411
|
+
// L6b: where the average came from — dated summaries, the phases' plan commits, or
|
|
412
|
+
// the 5-day default when there is no history at all.
|
|
413
|
+
eta_source: durations.length === 0 ? 'default' : (sources.every((s) => s === 'git') ? 'git' : (sources.every((s) => s === 'summary') ? 'summary' : 'mixed')),
|
|
408
414
|
bottleneck,
|
|
409
415
|
};
|
|
410
416
|
}
|
|
@@ -417,8 +423,9 @@ function findPhaseDir(dirs, num) {
|
|
|
417
423
|
return dirs.find(d => d.startsWith(`${num}-`) || d.startsWith(`${padded}-`));
|
|
418
424
|
}
|
|
419
425
|
|
|
420
|
-
function sampleCompletedPhaseDurations(cwd, completedPhases) {
|
|
426
|
+
function sampleCompletedPhaseDurations(cwd, completedPhases, opts = {}) {
|
|
421
427
|
const durations = [];
|
|
428
|
+
const sources = [];
|
|
422
429
|
const phasesRoot = phasesPath(cwd);
|
|
423
430
|
let dirs = [];
|
|
424
431
|
try { dirs = fs.readdirSync(phasesRoot); } catch { return durations; }
|
|
@@ -438,10 +445,25 @@ function sampleCompletedPhaseDurations(cwd, completedPhases) {
|
|
|
438
445
|
const completed_at = fm.completed || fm.finished || fm.done;
|
|
439
446
|
if (started && completed_at) {
|
|
440
447
|
const d = daysBetween(started, completed_at);
|
|
441
|
-
if (d && d > 0 && d < 60) durations.push(d);
|
|
448
|
+
if (d && d > 0 && d < 60) { durations.push(d); sources.push('summary'); continue; }
|
|
442
449
|
}
|
|
450
|
+
// No dated summary (most have none): time the phase by its plan commits, oldest to newest.
|
|
451
|
+
const g = phaseCommitSpanDays(cwd, p.num);
|
|
452
|
+
if (g && g > 0 && g < 60) { durations.push(g); sources.push('git'); }
|
|
443
453
|
}
|
|
444
|
-
return durations;
|
|
454
|
+
return opts.withSources ? { durations, sources } : durations;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Days between a phase's oldest and newest plan commit ({type}({phase}-{plan}): …), or null. */
|
|
458
|
+
function phaseCommitSpanDays(cwd, phaseNum) {
|
|
459
|
+
const n = String(phaseNum);
|
|
460
|
+
const padded = n.includes('.') ? n.split('.').map((s, i) => (i === 0 ? s.padStart(2, '0') : s)).join('.') : n.padStart(2, '0');
|
|
461
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
462
|
+
const r = require('./core.cjs').execGit(cwd, ['log', '--format=%ct', '-E', `--grep=^[a-z]+\\((${esc(padded)}|${esc(n)})-[0-9]+\\):`]);
|
|
463
|
+
if (!r || r.exitCode !== 0 || !r.stdout) return null;
|
|
464
|
+
const times = r.stdout.split('\n').map(Number).filter(Number.isFinite);
|
|
465
|
+
if (times.length < 2) return null;
|
|
466
|
+
return Math.round(((Math.max(...times) - Math.min(...times)) / 86400) * 10) / 10;
|
|
445
467
|
}
|
|
446
468
|
|
|
447
469
|
function findBottleneckCandidate(cwd, remainingPhases) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Requirements with EARS criteria, a requirements lint and a truth→test trace matrix
|
|
4
|
+
* (program V8).
|
|
5
|
+
*
|
|
6
|
+
* A v1 requirement (a checkbox line `- [ ] **ID**: …`) carries its acceptance criteria
|
|
7
|
+
* as indented sub-bullets in an EARS form, and the tests that check them as `Test:`
|
|
8
|
+
* sub-bullets:
|
|
9
|
+
*
|
|
10
|
+
* - [ ] **AUTH-01**: User can sign in
|
|
11
|
+
* - WHEN a user submits a correct email and password THE SYSTEM SHALL start a session
|
|
12
|
+
* - Test: tests/auth.test.js
|
|
13
|
+
*
|
|
14
|
+
* `verify requirements` fails a v1 requirement with no criterion (`req_no_scenario`).
|
|
15
|
+
* `verify traceability` maps each requirement to its tests and runs them: `passed`, `failing`,
|
|
16
|
+
* or `not_checked` when a truth has no test (an untested truth is never read as met).
|
|
17
|
+
* `phase complete` ticks a requirement that has criteria only when its trace passes; a
|
|
18
|
+
* legacy requirement with no criteria keeps the old tick, so existing projects work.
|
|
19
|
+
*/
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { output, toLf } = require('./core.cjs');
|
|
23
|
+
const { planningPath } = require('./utils.cjs');
|
|
24
|
+
const { REQUIREMENTS_FILE } = require('./constants.cjs');
|
|
25
|
+
const TG = require('./test-gate.cjs');
|
|
26
|
+
|
|
27
|
+
// EARS forms: ubiquitous (THE <system> SHALL), event (WHEN … SHALL), state (WHILE …),
|
|
28
|
+
// optional feature (WHERE …), unwanted behaviour (IF … THEN … SHALL).
|
|
29
|
+
const EARS_RE = /^(?:(?:WHEN|WHILE|WHERE)\b[\s\S]*\bSHALL\b|IF\b[\s\S]*\bTHEN\b[\s\S]*\bSHALL\b|THE\s+\S[\s\S]*\bSHALL\b)/i;
|
|
30
|
+
const REQ_RE = /^- (\[[ xX]\] )?\*\*([A-Z][A-Z0-9]*-\d+)\*\*:?\s*(.*)$/;
|
|
31
|
+
const DEFAULT_TEST_TIMEOUT_MS = 3 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
const isEars = (s) => EARS_RE.test(String(s || '').trim());
|
|
34
|
+
|
|
35
|
+
/** The requirements in a requirements.md, with criteria and mapped tests. */
|
|
36
|
+
function parseRequirements(raw) {
|
|
37
|
+
const lines = toLf(String(raw || '')).split('\n');
|
|
38
|
+
const out = [];
|
|
39
|
+
let cur = null;
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const m = REQ_RE.exec(line);
|
|
42
|
+
if (m) {
|
|
43
|
+
cur = { id: m[2], v1: !!m[1], checked: !!m[1] && /x/i.test(m[1]), text: m[3].trim(), criteria: [], tests: [] };
|
|
44
|
+
out.push(cur);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!cur) continue;
|
|
48
|
+
const sub = /^\s{2,}- (.*)$/.exec(line);
|
|
49
|
+
if (!sub) { if (line.trim() !== '') cur = null; continue; }
|
|
50
|
+
const body = sub[1].trim();
|
|
51
|
+
const t = /^tests?:\s*(.+)$/i.exec(body);
|
|
52
|
+
if (t) { for (const p of t[1].split(',')) { const file = p.trim().replace(/`/g, '').split('#')[0].trim(); if (file) cur.tests.push(file); } continue; }
|
|
53
|
+
if (isEars(body)) cur.criteria.push(body);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readRequirements(cwd) {
|
|
59
|
+
try { return fs.readFileSync(path.join(planningPath(cwd), REQUIREMENTS_FILE), 'utf-8'); } catch { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function lintRequirements(cwd) {
|
|
63
|
+
const text = readRequirements(cwd);
|
|
64
|
+
if (text === null) return { error: 'requirements_not_found', reason: '.planning/requirements.md is missing' };
|
|
65
|
+
const reqs = parseRequirements(text).filter((r) => r.v1);
|
|
66
|
+
const errors = reqs.filter((r) => r.criteria.length === 0).map((r) => ({ code: 'req_no_scenario', id: r.id, detail: 'no acceptance criterion in an EARS form (WHEN/WHILE/WHERE/IF … THE SYSTEM SHALL …)' }));
|
|
67
|
+
return { ok: errors.length === 0, requirements: reqs.map((r) => ({ id: r.id, criteria: r.criteria.length, tests: r.tests.length })), errors };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Run a requirement's mapped tests: passed | failing | not_checked | legacy. Pure over the cache. */
|
|
71
|
+
function traceOf(cwd, req, cache, opts = {}) {
|
|
72
|
+
if (req.criteria.length === 0) return { id: req.id, status: 'legacy', tests: req.tests };
|
|
73
|
+
if (req.tests.length === 0) return { id: req.id, status: 'not_checked', tests: [], detail: 'no test is mapped to this requirement' };
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const file of req.tests) {
|
|
76
|
+
if (!cache.has(file)) {
|
|
77
|
+
if (!fs.existsSync(path.join(cwd, file))) cache.set(file, { file, result: 'missing' });
|
|
78
|
+
else {
|
|
79
|
+
const fc = TG.fileCommand(cwd, file);
|
|
80
|
+
if (!fc.command) cache.set(file, { file, result: 'not_checked', detail: fc.reason });
|
|
81
|
+
else cache.set(file, { file, result: TG.runCommand(cwd, fc.command, opts.timeoutMs || DEFAULT_TEST_TIMEOUT_MS).exit === 0 ? 'passed' : 'failing' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
results.push(cache.get(file));
|
|
85
|
+
}
|
|
86
|
+
const status = results.some((r) => r.result === 'failing' || r.result === 'missing') ? 'failing'
|
|
87
|
+
: results.every((r) => r.result === 'passed') ? 'passed' : 'not_checked';
|
|
88
|
+
return { id: req.id, status, tests: results };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function traceMatrix(cwd, opts = {}) {
|
|
92
|
+
const text = readRequirements(cwd);
|
|
93
|
+
if (text === null) return { error: 'requirements_not_found', reason: '.planning/requirements.md is missing' };
|
|
94
|
+
let reqs = parseRequirements(text).filter((r) => r.v1);
|
|
95
|
+
if (opts.ids && opts.ids.length) reqs = reqs.filter((r) => opts.ids.includes(r.id));
|
|
96
|
+
const cache = new Map();
|
|
97
|
+
const requirements = reqs.map((r) => traceOf(cwd, r, cache, opts));
|
|
98
|
+
const count = (s) => requirements.filter((r) => r.status === s).length;
|
|
99
|
+
return { requirements, passed: count('passed'), failing: count('failing'), not_checked: count('not_checked'), legacy: count('legacy') };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether `phase complete` may tick each id. Legacy requirements may; the rest need a passing trace. */
|
|
103
|
+
function tickDecisions(cwd, ids) {
|
|
104
|
+
const text = readRequirements(cwd);
|
|
105
|
+
if (text === null) return new Map(ids.map((id) => [id, { allowed: true, status: 'unknown' }]));
|
|
106
|
+
const byId = new Map(parseRequirements(text).map((r) => [r.id, r]));
|
|
107
|
+
const cache = new Map();
|
|
108
|
+
const out = new Map();
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
const r = byId.get(id);
|
|
111
|
+
if (!r) { out.set(id, { allowed: true, status: 'unknown' }); continue; }
|
|
112
|
+
const t = traceOf(cwd, r, cache);
|
|
113
|
+
out.set(id, { allowed: t.status === 'legacy' || t.status === 'passed', status: t.status });
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cmdVerifyRequirements(cwd, raw) {
|
|
119
|
+
const r = lintRequirements(cwd);
|
|
120
|
+
if (r.error) { output(r, raw, 'error', 1); return; }
|
|
121
|
+
output(r, raw, r.ok ? 'valid' : 'invalid', r.ok ? 0 : 1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function cmdVerifyTrace(cwd, ids, raw) {
|
|
125
|
+
const r = traceMatrix(cwd, { ids });
|
|
126
|
+
if (r.error) { output(r, raw, 'error', 1); return; }
|
|
127
|
+
output(r, raw, r.failing ? 'failing' : (r.not_checked ? 'not_checked' : 'passed'), r.failing ? 1 : 0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { isEars, parseRequirements, lintRequirements, traceMatrix, tickDecisions, cmdVerifyRequirements, cmdVerifyTrace };
|