pan-wizard 3.36.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 +36 -48
- 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 +1 -2
- package/agents/pan-document_code.md +1 -2
- package/agents/pan-hardener.md +2 -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 +79 -488
- package/agents/pan-plan-checker.md +12 -2
- package/agents/pan-planner.md +10 -6
- package/agents/pan-previewer.md +1 -2
- package/agents/pan-project-researcher.md +70 -499
- package/agents/pan-release.md +1 -1
- package/agents/pan-reviewer.md +9 -3
- package/agents/pan-roadmapper.md +12 -7
- package/agents/pan-verifier.md +7 -2
- 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/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/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 +31 -2
- package/pan-wizard-core/bin/lib/constants.cjs +27 -1
- package/pan-wizard-core/bin/lib/core.cjs +22 -10
- package/pan-wizard-core/bin/lib/cost.cjs +14 -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/failure-class.cjs +81 -0
- package/pan-wizard-core/bin/lib/findings.cjs +34 -4
- package/pan-wizard-core/bin/lib/focus.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 +16 -5
- 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.cjs +48 -4
- package/pan-wizard-core/bin/lib/plan-lint.cjs +150 -1
- package/pan-wizard-core/bin/lib/preview.cjs +23 -4
- package/pan-wizard-core/bin/lib/requirements-lint.cjs +130 -0
- package/pan-wizard-core/bin/lib/research-lint.cjs +150 -3
- 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/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/uat.cjs +149 -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 +37 -1
- package/pan-wizard-core/bin/lib/verify-retro.cjs +3 -2
- package/pan-wizard-core/bin/lib/verify.cjs +59 -4
- package/pan-wizard-core/bin/lib/worktree.cjs +70 -0
- package/pan-wizard-core/bin/pan-tools.cjs +168 -27
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +2 -2
- package/pan-wizard-core/references/continuation-format.md +3 -3
- package/pan-wizard-core/references/guardrails.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 +5 -1
- package/pan-wizard-core/templates/debug.md +1 -0
- package/pan-wizard-core/templates/discovery.md +2 -2
- package/pan-wizard-core/templates/requirements.md +9 -0
- 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 +36 -8
- 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 +31 -53
- 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 +86 -43
- 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/research-phase.md +118 -40
- package/pan-wizard-core/workflows/resume-project.md +18 -2
- package/pan-wizard-core/workflows/settings.md +21 -24
- 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 +2 -2
- package/scripts/build-agent-plugin.js +35 -15
- 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/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 +9 -2
- 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 -210
- package/pan-wizard-core/templates/research-project/features.md +0 -153
- package/pan-wizard-core/templates/research-project/pitfalls.md +0 -206
- package/pan-wizard-core/templates/research-project/stack.md +0 -126
- package/pan-wizard-core/templates/research-project/summary.md +0 -170
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* UAT through the engine (program L8): the walkthrough is a portable markdown workflow
|
|
4
|
+
* (workflows/uat-walkthrough.md, `/pan:verify-phase N --uat`), and what it records goes
|
|
5
|
+
* through these verbs, so every runtime leaves the same `{phase}-uat.md`:
|
|
6
|
+
*
|
|
7
|
+
* uat record <phase> --test N --result pass|issue|skipped [--reported "…"] [--severity S] [--reason "…"]
|
|
8
|
+
* sets one test's result, then rebuilds `## Summary` and `## Gaps` (the YAML list
|
|
9
|
+
* diagnose-issues and `plan-phase --gaps` read). Diagnosis fields a gap already
|
|
10
|
+
* carries (root_cause, artifacts, missing, debug_session) survive a re-record.
|
|
11
|
+
* The session is `complete` when no test is pending.
|
|
12
|
+
* uat status <phase>
|
|
13
|
+
* counts and the gaps, for the walkthrough to decide whether to diagnose.
|
|
14
|
+
*
|
|
15
|
+
* The file shape is templates/uat.md.
|
|
16
|
+
*/
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { findPhaseInternal, output, error, toLf, dominantEol, withEol } = require('./core.cjs');
|
|
20
|
+
|
|
21
|
+
const RESULTS = Object.freeze(['pass', 'issue', 'skipped']);
|
|
22
|
+
const SEVERITIES = Object.freeze(['blocker', 'major', 'minor', 'cosmetic']);
|
|
23
|
+
const KEEP_GAP_FIELDS = Object.freeze(['root_cause', 'artifacts', 'missing', 'debug_session']);
|
|
24
|
+
|
|
25
|
+
const unquote = (s) => String(s || '').trim().replace(/^"(.*)"$/, '$1');
|
|
26
|
+
const quote = (s) => `"${String(s || '').replace(/"/g, "'")}"`;
|
|
27
|
+
|
|
28
|
+
function uatPath(cwd, phase) {
|
|
29
|
+
const info = findPhaseInternal(cwd, phase);
|
|
30
|
+
if (!info || !info.found) return null;
|
|
31
|
+
const dir = path.join(cwd, info.directory);
|
|
32
|
+
let files = [];
|
|
33
|
+
try { files = fs.readdirSync(dir).filter((f) => /-uat\.md$/i.test(f)); } catch { /* none */ }
|
|
34
|
+
return files.length ? path.join(dir, files[0]) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse a uat.md: frontmatter status, the tests, and the gaps' extra fields by test number. */
|
|
38
|
+
function parseUat(raw) {
|
|
39
|
+
const text = toLf(String(raw || ''));
|
|
40
|
+
const status = ((/^---\n[\s\S]*?^status:\s*(.+)$/m.exec(text) || [])[1] || '').trim();
|
|
41
|
+
const tests = [];
|
|
42
|
+
const testsSec = (/^## Tests\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(text) || [])[1] || '';
|
|
43
|
+
for (const block of testsSec.split(/^(?=### \d+\.)/m)) {
|
|
44
|
+
const h = /^### (\d+)\.\s*(.+)$/m.exec(block);
|
|
45
|
+
if (!h) continue;
|
|
46
|
+
const field = (k) => ((new RegExp(`^${k}:\\s*(.*)$`, 'm').exec(block) || [])[1] || '').trim();
|
|
47
|
+
let result = field('result').replace(/^\[|\]$/g, '') || 'pending';
|
|
48
|
+
tests.push({ number: Number(h[1]), name: h[2].trim(), expected: field('expected'), result, reported: unquote(field('reported')) || null, severity: field('severity') || null, reason: field('reason') || null });
|
|
49
|
+
}
|
|
50
|
+
const gapsSec = (/^## Gaps\s*$([\s\S]*?)(?=^## |(?![\s\S]))/m.exec(text) || [])[1] || '';
|
|
51
|
+
const gapExtras = new Map();
|
|
52
|
+
for (const item of gapsSec.split(/^(?=- truth:)/m)) {
|
|
53
|
+
const n = /^\s+test:\s*(\d+)/m.exec(item);
|
|
54
|
+
if (!n) continue;
|
|
55
|
+
const extras = {};
|
|
56
|
+
for (const k of KEEP_GAP_FIELDS) {
|
|
57
|
+
const m = new RegExp(`^\\s+${k}:\\s*(.*)$`, 'm').exec(item);
|
|
58
|
+
if (m && m[1].trim() && m[1].trim() !== '""' && m[1].trim() !== '[]') extras[k] = m[1].trim();
|
|
59
|
+
}
|
|
60
|
+
gapExtras.set(Number(n[1]), extras);
|
|
61
|
+
}
|
|
62
|
+
return { status, tests, gapExtras };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderTest(t) {
|
|
66
|
+
const lines = [`### ${t.number}. ${t.name}`, `expected: ${t.expected}`, `result: ${t.result === 'pending' ? '[pending]' : t.result}`];
|
|
67
|
+
if (t.result === 'issue') {
|
|
68
|
+
lines.push(`reported: ${quote(t.reported)}`);
|
|
69
|
+
lines.push(`severity: ${t.severity || 'major'}`);
|
|
70
|
+
}
|
|
71
|
+
if (t.result === 'skipped' && t.reason) lines.push(`reason: ${t.reason}`);
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function summaryOf(tests) {
|
|
76
|
+
const n = (r) => tests.filter((t) => t.result === r).length;
|
|
77
|
+
return { total: tests.length, passed: n('pass'), issues: n('issue'), pending: n('pending'), skipped: n('skipped') };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function gapsOf(tests, gapExtras) {
|
|
81
|
+
return tests.filter((t) => t.result === 'issue').map((t) => ({
|
|
82
|
+
truth: t.expected, status: 'failed', reason: `User reported: ${t.reported || ''}`.trim(), severity: t.severity || 'major', test: t.number,
|
|
83
|
+
...(gapExtras.get(t.number) || {}),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderGaps(gaps) {
|
|
88
|
+
return gaps.map((g) => {
|
|
89
|
+
const lines = [`- truth: ${quote(g.truth)}`, ` status: ${g.status}`, ` reason: ${quote(g.reason)}`, ` severity: ${g.severity}`, ` test: ${g.test}`];
|
|
90
|
+
for (const k of KEEP_GAP_FIELDS) if (g[k]) lines.push(` ${k}: ${g[k]}`);
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}).join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Set one test's result and rebuild the summary and the gaps. */
|
|
96
|
+
function recordUatResult(cwd, phase, input) {
|
|
97
|
+
const file = uatPath(cwd, phase);
|
|
98
|
+
if (!file) return { error: 'uat_not_found', reason: `no {phase}-uat.md in phase ${phase} (create one with scaffold uat)` };
|
|
99
|
+
if (!RESULTS.includes(input.result)) return { error: 'invalid_result', reason: `result is one of ${RESULTS.join(', ')}` };
|
|
100
|
+
if (input.severity && !SEVERITIES.includes(input.severity)) return { error: 'invalid_severity', reason: `severity is one of ${SEVERITIES.join(', ')}` };
|
|
101
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
102
|
+
const eol = dominantEol(raw);
|
|
103
|
+
let text = toLf(raw);
|
|
104
|
+
const u = parseUat(text);
|
|
105
|
+
const t = u.tests.find((x) => x.number === Number(input.test));
|
|
106
|
+
if (!t) return { error: 'test_not_found', reason: `uat.md has no test ${input.test}` };
|
|
107
|
+
t.result = input.result;
|
|
108
|
+
t.reported = input.result === 'issue' ? (input.reported || t.reported || '') : null;
|
|
109
|
+
t.severity = input.result === 'issue' ? (input.severity || t.severity || 'major') : null;
|
|
110
|
+
t.reason = input.result === 'skipped' ? (input.reason || t.reason || null) : null;
|
|
111
|
+
|
|
112
|
+
const testsBody = '\n' + u.tests.map(renderTest).join('\n\n') + '\n\n';
|
|
113
|
+
text = text.replace(/^## Tests\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, `## Tests\n${testsBody}`);
|
|
114
|
+
const s = summaryOf(u.tests);
|
|
115
|
+
const summary = `## Summary\n\ntotal: ${s.total}\npassed: ${s.passed}\nissues: ${s.issues}\npending: ${s.pending}\nskipped: ${s.skipped}\n\n`;
|
|
116
|
+
text = /^## Summary\s*$/m.test(text) ? text.replace(/^## Summary\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, summary) : text + `\n${summary}`;
|
|
117
|
+
const gaps = gapsOf(u.tests, u.gapExtras);
|
|
118
|
+
const gapsBlock = `## Gaps\n\n<!-- YAML format for plan-phase --gaps consumption -->\n${gaps.length ? renderGaps(gaps) + '\n' : ''}`;
|
|
119
|
+
text = /^## Gaps\s*$/m.test(text) ? text.replace(/^## Gaps\s*$[\s\S]*?(?=^## |(?![\s\S]))/m, gapsBlock) : text + `\n${gapsBlock}`;
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
text = text.replace(/^updated:.*$/m, `updated: ${now}`);
|
|
122
|
+
if (s.pending === 0) text = text.replace(/^status:\s*testing\s*$/m, 'status: complete');
|
|
123
|
+
fs.writeFileSync(file, withEol(text, eol));
|
|
124
|
+
return { recorded: true, test: t.number, result: t.result, ...s, complete: s.pending === 0, gaps: gaps.length };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function uatStatus(cwd, phase) {
|
|
128
|
+
const file = uatPath(cwd, phase);
|
|
129
|
+
if (!file) return { error: 'uat_not_found', reason: `no {phase}-uat.md in phase ${phase}` };
|
|
130
|
+
const u = parseUat(fs.readFileSync(file, 'utf8'));
|
|
131
|
+
const s = summaryOf(u.tests);
|
|
132
|
+
return { file: path.basename(file), status: u.status, ...s, complete: s.pending === 0, gaps: gapsOf(u.tests, u.gapExtras) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cmdUatRecord(cwd, phase, opts, raw) {
|
|
136
|
+
if (!phase || !opts.test || !opts.result) error('Usage: uat record <phase> --test N --result pass|issue|skipped [--reported "…"] [--severity S] [--reason "…"]');
|
|
137
|
+
const r = recordUatResult(cwd, phase, opts);
|
|
138
|
+
if (r.error) { output(r, raw, 'error', 1); return; }
|
|
139
|
+
output(r, raw, `${r.test}: ${r.result}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cmdUatStatus(cwd, phase, raw) {
|
|
143
|
+
if (!phase) error('Usage: uat status <phase>');
|
|
144
|
+
const r = uatStatus(cwd, phase);
|
|
145
|
+
if (r.error) { output(r, raw, 'error', 1); return; }
|
|
146
|
+
output(r, raw, `${r.passed} passed, ${r.issues} issues, ${r.pending} pending`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { parseUat, recordUatResult, uatStatus, cmdUatRecord, cmdUatStatus };
|
|
@@ -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 = { phaseFiles, 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
|
|
@@ -127,6 +127,42 @@ function checkRed(cwd, phase, opts = {}) {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
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
|
+
|
|
130
166
|
/** `pan-tools verify red <phase> [--timeout <ms>]`: exit 1 when a test is not_red or not_green. */
|
|
131
167
|
function cmdVerifyRed(cwd, phase, raw, opts = {}) {
|
|
132
168
|
if (!phase) error('Usage: verify red <phase> [--timeout <ms>]');
|
|
@@ -135,4 +171,4 @@ function cmdVerifyRed(cwd, phase, raw, opts = {}) {
|
|
|
135
171
|
output(r, raw, !r.checked ? 'not_checked' : (r.ok ? 'caught' : 'not_caught'), r.ok ? 0 : 1);
|
|
136
172
|
}
|
|
137
173
|
|
|
138
|
-
module.exports = { checkRed, cmdVerifyRed, phaseBase, withWorktree };
|
|
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
|
}
|
|
@@ -168,6 +168,36 @@ function checkCheckpointTask(type, attrs, body, errors, warnings) {
|
|
|
168
168
|
return { name: label, type, auto_select: autoMatch ? autoMatch[1] : null };
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// The elements a plan nests. A file that ends with one of them still open was cut
|
|
172
|
+
// off: runtimes cap tool output, and a long plan has been truncated mid-tag while
|
|
173
|
+
// the write reported success (market-ideas M47; gsd-core v1.18.0).
|
|
174
|
+
const PLAN_TAGS = ['tasks', 'task', 'name', 'files', 'action', 'verify', 'done'];
|
|
175
|
+
const TRUNCATION_MARKER = /\[\s*\.{0,3}\s*(?:output\s+)?truncated\b|<!--\s*(?:continue|continued|truncated)\b|\(output truncated\)|\bcontinued in next (?:message|write)\b/i;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Signs that a plan file was cut off. Pure; tags inside fenced code blocks do not count.
|
|
179
|
+
* @param {string} content - plan file content
|
|
180
|
+
* @returns {string[]} one message per problem, empty when the file is whole
|
|
181
|
+
*/
|
|
182
|
+
function detectPlanTruncation(content) {
|
|
183
|
+
const text = String(content || '').replace(/\r\n/g, '\n').replace(/^(`{3,}|~{3,})[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, '');
|
|
184
|
+
const out = [];
|
|
185
|
+
const open = [];
|
|
186
|
+
const tagRe = new RegExp(`<(/?)(${PLAN_TAGS.join('|')})\\b[^>]*?(/?)>`, 'g');
|
|
187
|
+
let m;
|
|
188
|
+
while ((m = tagRe.exec(text)) !== null) {
|
|
189
|
+
const [, closing, tag, selfClosing] = m;
|
|
190
|
+
if (selfClosing) continue;
|
|
191
|
+
if (!closing) { open.push(tag); continue; }
|
|
192
|
+
const at = open.lastIndexOf(tag);
|
|
193
|
+
if (at >= 0) open.splice(at);
|
|
194
|
+
}
|
|
195
|
+
if (open.length) out.push(`Plan looks truncated: the file ends with ${open.map((t) => `<${t}>`).join(' > ')} still open`);
|
|
196
|
+
const marker = text.match(TRUNCATION_MARKER);
|
|
197
|
+
if (marker) out.push(`Plan looks truncated: it contains a continuation marker ("${marker[0].trim()}")`);
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
171
201
|
/**
|
|
172
202
|
* Validate a plan.md structure: required frontmatter, task elements, and consistency.
|
|
173
203
|
* @param {string} cwd - Working directory path
|
|
@@ -180,11 +210,19 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
180
210
|
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
181
211
|
const content = safeReadFile(fullPath);
|
|
182
212
|
if (!content) { output({ error: 'File not found', path: filePath }, raw); return; }
|
|
213
|
+
const r = planStructure(content);
|
|
214
|
+
output(r, raw, r.valid ? 'valid' : 'invalid');
|
|
215
|
+
}
|
|
183
216
|
|
|
217
|
+
/** A plan's structure check (cmdVerifyPlanStructure without the output). Pure. */
|
|
218
|
+
function planStructure(content) {
|
|
184
219
|
const fm = extractFrontmatter(content);
|
|
185
220
|
const errors = [];
|
|
186
221
|
const warnings = [];
|
|
187
222
|
|
|
223
|
+
// A cut-off plan first: its task count and fields would mislead (M47).
|
|
224
|
+
errors.push(...detectPlanTruncation(content));
|
|
225
|
+
|
|
188
226
|
// Check required frontmatter fields
|
|
189
227
|
const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
|
|
190
228
|
for (const field of required) {
|
|
@@ -246,14 +284,14 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
246
284
|
errors.push('Has checkpoint tasks but autonomous is not false');
|
|
247
285
|
}
|
|
248
286
|
|
|
249
|
-
|
|
287
|
+
return {
|
|
250
288
|
valid: errors.length === 0,
|
|
251
289
|
errors,
|
|
252
290
|
warnings,
|
|
253
291
|
task_count: tasks.length,
|
|
254
292
|
tasks,
|
|
255
293
|
frontmatter_fields: Object.keys(fm),
|
|
256
|
-
}
|
|
294
|
+
};
|
|
257
295
|
}
|
|
258
296
|
|
|
259
297
|
/**
|
|
@@ -584,6 +622,16 @@ function reconcilePhase(cwd, phaseNum) {
|
|
|
584
622
|
contradictions.push(`verification status "${status}" but the engine's test run failed${why}`);
|
|
585
623
|
}
|
|
586
624
|
}
|
|
625
|
+
// V5G: in gate mode, a mutation score below the floor means the tests do not pin the
|
|
626
|
+
// code, so a passed verification is a contradiction until they do.
|
|
627
|
+
const mutFile = files.find((f) => /-mutation\.json$/.test(f));
|
|
628
|
+
if (claimsPass && mutFile) {
|
|
629
|
+
let mut = null;
|
|
630
|
+
try { mut = JSON.parse(safeReadFile(path.join(dir, mutFile)) || 'null'); } catch {}
|
|
631
|
+
if (mut && mut.mode === 'gate' && mut.gate && mut.gate.failed) {
|
|
632
|
+
contradictions.push(`verification status "${status}" but the mutation score ${mut.score} is below the floor ${mut.gate.floor} (workflow.mutation_gate: gate): ${mut.survived} of ${mut.sampled} mutants survived`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
587
635
|
return {
|
|
588
636
|
phase: String(phaseNum), found: true, verification_status: status, claims_pass: claimsPass,
|
|
589
637
|
test_run: testRun ? { status: testRun.status, head: testRun.head || null, regressions: (testRun.regressions || []).length } : undefined,
|
|
@@ -1473,9 +1521,13 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1473
1521
|
}
|
|
1474
1522
|
|
|
1475
1523
|
// Perform repairs if requested and there are repairable issues
|
|
1524
|
+
// L6a: health asks per repair class. Without --repair the result lists the classes;
|
|
1525
|
+
// --only limits a repair to the ones the user chose.
|
|
1526
|
+
const only = Array.isArray(options.only) && options.only.length ? new Set(options.only) : null;
|
|
1527
|
+
const chosen = only ? repairs.filter((r) => only.has(r)) : repairs;
|
|
1476
1528
|
let repairActions = [];
|
|
1477
|
-
if (options.repair &&
|
|
1478
|
-
repairActions = repairIssues(cwd,
|
|
1529
|
+
if (options.repair && chosen.length > 0) {
|
|
1530
|
+
repairActions = repairIssues(cwd, chosen);
|
|
1479
1531
|
}
|
|
1480
1532
|
|
|
1481
1533
|
// Check 10 (optional): full validation — run tests and build
|
|
@@ -1548,6 +1600,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
|
|
1548
1600
|
warnings,
|
|
1549
1601
|
info,
|
|
1550
1602
|
repairable_count: repairableCount,
|
|
1603
|
+
repairs_available: repairs.length > 0 ? [...new Set(repairs)] : undefined,
|
|
1551
1604
|
repairs_performed: repairActions.length > 0 ? repairActions : undefined,
|
|
1552
1605
|
};
|
|
1553
1606
|
if (options.full) {
|
|
@@ -1623,8 +1676,10 @@ function runFullBuildCheck(cwd) {
|
|
|
1623
1676
|
// ─── Deployment validation — extracted to verify-deploy.cjs (re-exported below)
|
|
1624
1677
|
|
|
1625
1678
|
module.exports = {
|
|
1679
|
+
planStructure,
|
|
1626
1680
|
cmdVerifySummary,
|
|
1627
1681
|
cmdVerifyPlanStructure,
|
|
1682
|
+
detectPlanTruncation,
|
|
1628
1683
|
cmdVerifyPhaseCompleteness,
|
|
1629
1684
|
cmdVerifyReferences,
|
|
1630
1685
|
cmdVerifyCommits,
|
|
@@ -208,7 +208,77 @@ function cmdWorktreeCleanup(cwd, raw, opts) {
|
|
|
208
208
|
output(r, raw, lines.length ? lines.join('\n') : 'nothing to clean');
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// ─── Shadow-merge preflight (program A3) ────────────────────────────────────
|
|
212
|
+
// Branches that each pass the oracle can still break it together. Before merging a
|
|
213
|
+
// sub-wave back, merge every branch onto the base in a throwaway worktree, in order,
|
|
214
|
+
// and run the test command there. A conflict is named; a red result is bisected to
|
|
215
|
+
// "each passes alone" (break_together) or the branches that fail on their own.
|
|
216
|
+
|
|
217
|
+
const GIT_IDENTITY = ['-c', 'user.name=PAN preflight', '-c', 'user.email=preflight@pan.local', '-c', 'commit.gpgsign=false'];
|
|
218
|
+
|
|
219
|
+
function runOracle(dir, test) {
|
|
220
|
+
const { spawnSync } = require('child_process');
|
|
221
|
+
const r = spawnSync(test, { cwd: dir, shell: true, encoding: 'utf8', timeout: 20 * 60 * 1000 });
|
|
222
|
+
return r.status === 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {string} cwd
|
|
227
|
+
* @param {{base?: string, branches: string[], test?: string}} opts
|
|
228
|
+
* @returns {{ok: boolean, verdict: 'clean'|'conflict'|'break_together'|'branch_fails'|'base_failing'|'no_test', ...}}
|
|
229
|
+
*/
|
|
230
|
+
function shadowMergePreflight(cwd, opts = {}) {
|
|
231
|
+
if (!isGitRepo(cwd)) return { error: 'not_git', reason: 'the preflight needs a git repository' };
|
|
232
|
+
const base = opts.base || 'HEAD';
|
|
233
|
+
const branches = (opts.branches || []).filter(Boolean);
|
|
234
|
+
let test = opts.test;
|
|
235
|
+
if (!test) {
|
|
236
|
+
try { test = require('./test-gate.cjs').detectTestCommand(cwd).command; } catch { test = null; }
|
|
237
|
+
}
|
|
238
|
+
if (!test) return { ok: false, verdict: 'no_test', base, branches, reason: 'no test command: pass --test or set one for the project' };
|
|
239
|
+
const os = require('os');
|
|
240
|
+
const fsx = require('fs');
|
|
241
|
+
const dir = fsx.mkdtempSync(path.join(os.tmpdir(), 'pan-preflight-'));
|
|
242
|
+
fsx.rmSync(dir, { recursive: true, force: true });
|
|
243
|
+
const add = execGit(cwd, ['worktree', 'add', '--detach', dir, base]);
|
|
244
|
+
if (add.exitCode !== 0) return { error: 'worktree_failed', reason: add.stderr.trim() };
|
|
245
|
+
const g = (args) => execGit(dir, [...GIT_IDENTITY, ...args]);
|
|
246
|
+
const result = { base, branches, test, merged: [], alone: {} };
|
|
247
|
+
try {
|
|
248
|
+
if (!runOracle(dir, test)) return { ...result, ok: false, verdict: 'base_failing', reason: 'the oracle already fails on the base: a merge cannot be judged' };
|
|
249
|
+
for (const b of branches) {
|
|
250
|
+
const m = g(['merge', '--no-ff', '--no-edit', b]);
|
|
251
|
+
if (m.exitCode !== 0) {
|
|
252
|
+
const files = g(['diff', '--name-only', '--diff-filter=U']).stdout.split('\n').map((x) => x.trim()).filter(Boolean);
|
|
253
|
+
g(['merge', '--abort']);
|
|
254
|
+
return { ...result, ok: false, verdict: 'conflict', conflict: { branch: b, files, after: [...result.merged] } };
|
|
255
|
+
}
|
|
256
|
+
result.merged.push(b);
|
|
257
|
+
}
|
|
258
|
+
if (runOracle(dir, test)) return { ...result, ok: true, verdict: 'clean' };
|
|
259
|
+
for (const b of branches) {
|
|
260
|
+
g(['reset', '--hard', '-q', base]);
|
|
261
|
+
result.alone[b] = g(['merge', '--no-ff', '--no-edit', b]).exitCode === 0 && runOracle(dir, test);
|
|
262
|
+
}
|
|
263
|
+
const failing = branches.filter((b) => !result.alone[b]);
|
|
264
|
+
return failing.length
|
|
265
|
+
? { ...result, ok: false, verdict: 'branch_fails', failing }
|
|
266
|
+
: { ...result, ok: false, verdict: 'break_together', reason: 'each branch passes alone; merged together they fail' };
|
|
267
|
+
} finally {
|
|
268
|
+
execGit(cwd, ['worktree', 'remove', '--force', dir]);
|
|
269
|
+
try { fsx.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
270
|
+
execGit(cwd, ['worktree', 'prune']);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function cmdWorktreePreflight(cwd, opts, raw) {
|
|
275
|
+
const r = shadowMergePreflight(cwd, opts);
|
|
276
|
+
output(r, raw, r.verdict || r.error);
|
|
277
|
+
}
|
|
278
|
+
|
|
211
279
|
module.exports = {
|
|
280
|
+
shadowMergePreflight,
|
|
281
|
+
cmdWorktreePreflight,
|
|
212
282
|
ARMY_BRANCH_PREFIX,
|
|
213
283
|
createTaskWorktree,
|
|
214
284
|
removeTaskWorktree,
|