@garygentry/feature-forge 0.3.2 → 0.3.5
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/README.md +1 -1
- package/adapters/claude/.feature-forge-bundle.json +1 -1
- package/adapters/claude/agents/forge-verifier.md +3 -1
- package/adapters/claude/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/claude/references/epic-state-schema.json +50 -0
- package/adapters/claude/references/forge-config-schema.json +20 -2
- package/adapters/claude/references/forge-decisions-schema.json +33 -0
- package/adapters/claude/references/pipeline-state-schema.json +34 -2
- package/adapters/claude/references/ralph-loop-contract.md +6 -3
- package/adapters/claude/references/shared-conventions.md +15 -4
- package/adapters/claude/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/scripts/epic-manifest.py +82 -4
- package/adapters/claude/scripts/fix-sweep.py +1180 -0
- package/adapters/claude/scripts/forge-session.py +1151 -32
- package/adapters/claude/skills/forge/SKILL.md +5 -5
- package/adapters/claude/skills/forge/references/pipeline-state-schema.json +34 -2
- package/adapters/claude/skills/forge/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-0-epic/references/edit-mode.md +5 -1
- package/adapters/claude/skills/forge-0-epic/references/epic-manifest-subcommands.md +5 -0
- package/adapters/claude/skills/forge-0-epic/references/pipeline-state-schema.json +34 -2
- package/adapters/claude/skills/forge-0-epic/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-0-epic/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-1-prd/SKILL.md +3 -1
- package/adapters/claude/skills/forge-1-prd/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-1-prd/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-2-tech/SKILL.md +5 -1
- package/adapters/claude/skills/forge-2-tech/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-2-tech/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-3-specs/SKILL.md +5 -1
- package/adapters/claude/skills/forge-3-specs/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-3-specs/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-4-backlog/SKILL.md +41 -3
- package/adapters/claude/skills/forge-4-backlog/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-4-backlog/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-5-loop/SKILL.md +36 -36
- package/adapters/claude/skills/forge-5-loop/references/agent-selection.md +16 -0
- package/adapters/claude/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/claude/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/claude/skills/forge-5-loop/references/result-reporting.md +40 -11
- package/adapters/claude/skills/forge-5-loop/references/runner-contract.md +22 -4
- package/adapters/claude/skills/forge-5-loop/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-5-loop/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-6-docs/SKILL.md +29 -6
- package/adapters/claude/skills/forge-6-docs/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-6-docs/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-fix/SKILL.md +34 -0
- package/adapters/claude/skills/forge-fix/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-fix/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-guide/references/forge-config-schema.json +20 -2
- package/adapters/claude/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/claude/skills/forge-guide/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-verify/SKILL.md +10 -11
- package/adapters/claude/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/claude/skills/forge-verify/references/findings-template.md +30 -0
- package/adapters/claude/skills/forge-verify/references/shared-conventions.md +15 -4
- package/adapters/claude/skills/forge-verify/references/stage-exit-protocol.md +55 -11
- package/adapters/claude/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/claude/skills/forge-verify/references/verification-checklists/specs.md +43 -1
- package/adapters/codex/.feature-forge-bundle.json +1 -1
- package/adapters/codex/agents/forge-verifier.toml +3 -1
- package/adapters/codex/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/codex/references/epic-state-schema.json +50 -0
- package/adapters/codex/references/forge-config-schema.json +20 -2
- package/adapters/codex/references/forge-decisions-schema.json +33 -0
- package/adapters/codex/references/pipeline-state-schema.json +34 -2
- package/adapters/codex/references/process-overview.md +2 -2
- package/adapters/codex/references/ralph-loop-contract.md +6 -3
- package/adapters/codex/references/shared-conventions.md +44 -33
- package/adapters/codex/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/scripts/epic-manifest.py +82 -4
- package/adapters/codex/scripts/fix-sweep.py +1180 -0
- package/adapters/codex/scripts/forge-session.py +1151 -32
- package/adapters/codex/skills/forge/SKILL.md +8 -8
- package/adapters/codex/skills/forge/references/pipeline-state-schema.json +34 -2
- package/adapters/codex/skills/forge/references/process-overview.md +2 -2
- package/adapters/codex/skills/forge/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/codex/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/codex/skills/forge-0-epic/references/pipeline-state-schema.json +34 -2
- package/adapters/codex/skills/forge-0-epic/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-0-epic/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-1-prd/SKILL.md +3 -1
- package/adapters/codex/skills/forge-1-prd/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-1-prd/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-2-tech/SKILL.md +5 -1
- package/adapters/codex/skills/forge-2-tech/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-2-tech/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-3-specs/SKILL.md +5 -1
- package/adapters/codex/skills/forge-3-specs/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-3-specs/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-4-backlog/SKILL.md +41 -3
- package/adapters/codex/skills/forge-4-backlog/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-4-backlog/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-5-loop/SKILL.md +36 -36
- package/adapters/codex/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/codex/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/codex/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/codex/skills/forge-5-loop/references/result-reporting.md +40 -11
- package/adapters/codex/skills/forge-5-loop/references/runner-contract.md +26 -8
- package/adapters/codex/skills/forge-5-loop/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-5-loop/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-6-docs/SKILL.md +29 -6
- package/adapters/codex/skills/forge-6-docs/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-6-docs/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-fix/SKILL.md +34 -0
- package/adapters/codex/skills/forge-fix/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-fix/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-guide/SKILL.md +1 -1
- package/adapters/codex/skills/forge-guide/references/forge-config-schema.json +20 -2
- package/adapters/codex/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/codex/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/codex/skills/forge-guide/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-init/SKILL.md +1 -1
- package/adapters/codex/skills/forge-verify/SKILL.md +11 -12
- package/adapters/codex/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/codex/skills/forge-verify/references/findings-template.md +32 -2
- package/adapters/codex/skills/forge-verify/references/shared-conventions.md +44 -33
- package/adapters/codex/skills/forge-verify/references/stage-exit-protocol.md +64 -20
- package/adapters/codex/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/epic.md +1 -1
- package/adapters/codex/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/codex/skills/forge-verify/references/verification-checklists/specs.md +43 -1
- package/adapters/copilot/.feature-forge-bundle.json +1 -1
- package/adapters/copilot/agents/forge-verifier.md +3 -1
- package/adapters/copilot/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/copilot/references/epic-state-schema.json +50 -0
- package/adapters/copilot/references/forge-config-schema.json +20 -2
- package/adapters/copilot/references/forge-decisions-schema.json +33 -0
- package/adapters/copilot/references/pipeline-state-schema.json +34 -2
- package/adapters/copilot/references/process-overview.md +2 -2
- package/adapters/copilot/references/ralph-loop-contract.md +6 -3
- package/adapters/copilot/references/shared-conventions.md +44 -33
- package/adapters/copilot/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/scripts/epic-manifest.py +82 -4
- package/adapters/copilot/scripts/fix-sweep.py +1180 -0
- package/adapters/copilot/scripts/forge-session.py +1151 -32
- package/adapters/copilot/skills/forge/forge.md +8 -8
- package/adapters/copilot/skills/forge/references/pipeline-state-schema.json +34 -2
- package/adapters/copilot/skills/forge/references/process-overview.md +2 -2
- package/adapters/copilot/skills/forge/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/copilot/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/copilot/skills/forge-0-epic/references/pipeline-state-schema.json +34 -2
- package/adapters/copilot/skills/forge-0-epic/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-0-epic/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-1-prd/forge-1-prd.md +3 -1
- package/adapters/copilot/skills/forge-1-prd/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-1-prd/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-2-tech/forge-2-tech.md +5 -1
- package/adapters/copilot/skills/forge-2-tech/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-2-tech/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-3-specs/forge-3-specs.md +5 -1
- package/adapters/copilot/skills/forge-3-specs/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-3-specs/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-4-backlog/forge-4-backlog.md +41 -3
- package/adapters/copilot/skills/forge-4-backlog/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-4-backlog/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-5-loop/forge-5-loop.md +36 -36
- package/adapters/copilot/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/copilot/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/copilot/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/copilot/skills/forge-5-loop/references/result-reporting.md +40 -11
- package/adapters/copilot/skills/forge-5-loop/references/runner-contract.md +26 -8
- package/adapters/copilot/skills/forge-5-loop/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-5-loop/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-6-docs/forge-6-docs.md +29 -6
- package/adapters/copilot/skills/forge-6-docs/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-6-docs/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-fix/forge-fix.md +34 -0
- package/adapters/copilot/skills/forge-fix/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-fix/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-guide/forge-guide.md +1 -1
- package/adapters/copilot/skills/forge-guide/references/forge-config-schema.json +20 -2
- package/adapters/copilot/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/copilot/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/copilot/skills/forge-guide/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-init/forge-init.md +1 -1
- package/adapters/copilot/skills/forge-verify/forge-verify.md +11 -12
- package/adapters/copilot/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/copilot/skills/forge-verify/references/findings-template.md +32 -2
- package/adapters/copilot/skills/forge-verify/references/shared-conventions.md +44 -33
- package/adapters/copilot/skills/forge-verify/references/stage-exit-protocol.md +64 -20
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/epic.md +1 -1
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/copilot/skills/forge-verify/references/verification-checklists/specs.md +43 -1
- package/adapters/cursor/.feature-forge-bundle.json +1 -1
- package/adapters/cursor/agents/forge-verifier.mdc +3 -1
- package/adapters/cursor/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/cursor/references/epic-state-schema.json +50 -0
- package/adapters/cursor/references/forge-config-schema.json +20 -2
- package/adapters/cursor/references/forge-decisions-schema.json +33 -0
- package/adapters/cursor/references/pipeline-state-schema.json +34 -2
- package/adapters/cursor/references/process-overview.md +2 -2
- package/adapters/cursor/references/ralph-loop-contract.md +6 -3
- package/adapters/cursor/references/shared-conventions.md +44 -33
- package/adapters/cursor/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/scripts/epic-manifest.py +82 -4
- package/adapters/cursor/scripts/fix-sweep.py +1180 -0
- package/adapters/cursor/scripts/forge-session.py +1151 -32
- package/adapters/cursor/skills/forge/forge.mdc +8 -8
- package/adapters/cursor/skills/forge/references/pipeline-state-schema.json +34 -2
- package/adapters/cursor/skills/forge/references/process-overview.md +2 -2
- package/adapters/cursor/skills/forge/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/cursor/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/cursor/skills/forge-0-epic/references/pipeline-state-schema.json +34 -2
- package/adapters/cursor/skills/forge-0-epic/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-0-epic/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-1-prd/forge-1-prd.mdc +3 -1
- package/adapters/cursor/skills/forge-1-prd/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-1-prd/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-2-tech/forge-2-tech.mdc +5 -1
- package/adapters/cursor/skills/forge-2-tech/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-2-tech/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-3-specs/forge-3-specs.mdc +5 -1
- package/adapters/cursor/skills/forge-3-specs/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-3-specs/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-4-backlog/forge-4-backlog.mdc +41 -3
- package/adapters/cursor/skills/forge-4-backlog/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-4-backlog/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-5-loop/forge-5-loop.mdc +36 -36
- package/adapters/cursor/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/cursor/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/cursor/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/cursor/skills/forge-5-loop/references/result-reporting.md +40 -11
- package/adapters/cursor/skills/forge-5-loop/references/runner-contract.md +26 -8
- package/adapters/cursor/skills/forge-5-loop/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-5-loop/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-6-docs/forge-6-docs.mdc +29 -6
- package/adapters/cursor/skills/forge-6-docs/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-6-docs/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-fix/forge-fix.mdc +34 -0
- package/adapters/cursor/skills/forge-fix/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-fix/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-guide/forge-guide.mdc +1 -1
- package/adapters/cursor/skills/forge-guide/references/forge-config-schema.json +20 -2
- package/adapters/cursor/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/cursor/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/cursor/skills/forge-guide/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-init/forge-init.mdc +1 -1
- package/adapters/cursor/skills/forge-verify/forge-verify.mdc +11 -12
- package/adapters/cursor/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/cursor/skills/forge-verify/references/findings-template.md +32 -2
- package/adapters/cursor/skills/forge-verify/references/shared-conventions.md +44 -33
- package/adapters/cursor/skills/forge-verify/references/stage-exit-protocol.md +64 -20
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/epic.md +1 -1
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/cursor/skills/forge-verify/references/verification-checklists/specs.md +43 -1
- package/adapters/gemini/.feature-forge-bundle.json +1 -1
- package/adapters/gemini/agents/forge-verifier.md +3 -1
- package/adapters/gemini/gemini-extension.json +1 -1
- package/adapters/gemini/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/gemini/references/epic-state-schema.json +50 -0
- package/adapters/gemini/references/forge-config-schema.json +20 -2
- package/adapters/gemini/references/forge-decisions-schema.json +33 -0
- package/adapters/gemini/references/pipeline-state-schema.json +34 -2
- package/adapters/gemini/references/process-overview.md +2 -2
- package/adapters/gemini/references/ralph-loop-contract.md +6 -3
- package/adapters/gemini/references/shared-conventions.md +44 -33
- package/adapters/gemini/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/scripts/epic-manifest.py +82 -4
- package/adapters/gemini/scripts/fix-sweep.py +1180 -0
- package/adapters/gemini/scripts/forge-session.py +1151 -32
- package/adapters/gemini/skills/forge/forge.md +8 -8
- package/adapters/gemini/skills/forge/references/pipeline-state-schema.json +34 -2
- package/adapters/gemini/skills/forge/references/process-overview.md +2 -2
- package/adapters/gemini/skills/forge/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-0-epic/references/edit-mode.md +14 -10
- package/adapters/gemini/skills/forge-0-epic/references/epic-manifest-subcommands.md +8 -3
- package/adapters/gemini/skills/forge-0-epic/references/pipeline-state-schema.json +34 -2
- package/adapters/gemini/skills/forge-0-epic/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-0-epic/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-1-prd/forge-1-prd.md +3 -1
- package/adapters/gemini/skills/forge-1-prd/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-1-prd/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-2-tech/forge-2-tech.md +5 -1
- package/adapters/gemini/skills/forge-2-tech/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-2-tech/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-3-specs/forge-3-specs.md +5 -1
- package/adapters/gemini/skills/forge-3-specs/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-3-specs/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-4-backlog/forge-4-backlog.md +41 -3
- package/adapters/gemini/skills/forge-4-backlog/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-4-backlog/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-5-loop/forge-5-loop.md +36 -36
- package/adapters/gemini/skills/forge-5-loop/references/agent-selection.md +17 -1
- package/adapters/gemini/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/gemini/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/gemini/skills/forge-5-loop/references/result-reporting.md +40 -11
- package/adapters/gemini/skills/forge-5-loop/references/runner-contract.md +26 -8
- package/adapters/gemini/skills/forge-5-loop/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-5-loop/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-6-docs/forge-6-docs.md +29 -6
- package/adapters/gemini/skills/forge-6-docs/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-6-docs/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-fix/forge-fix.md +34 -0
- package/adapters/gemini/skills/forge-fix/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-fix/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-guide/forge-guide.md +1 -1
- package/adapters/gemini/skills/forge-guide/references/forge-config-schema.json +20 -2
- package/adapters/gemini/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/gemini/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/gemini/skills/forge-guide/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-init/forge-init.md +1 -1
- package/adapters/gemini/skills/forge-verify/forge-verify.md +11 -12
- package/adapters/gemini/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/gemini/skills/forge-verify/references/findings-template.md +32 -2
- package/adapters/gemini/skills/forge-verify/references/shared-conventions.md +44 -33
- package/adapters/gemini/skills/forge-verify/references/stage-exit-protocol.md +64 -20
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/epic.md +1 -1
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/gemini/skills/forge-verify/references/verification-checklists/specs.md +43 -1
- package/adapters/pi/.feature-forge-bundle.json +1 -1
- package/adapters/pi/agents/forge-verifier.md +3 -1
- package/adapters/pi/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/pi/references/epic-state-schema.json +50 -0
- package/adapters/pi/references/forge-config-schema.json +20 -2
- package/adapters/pi/references/forge-decisions-schema.json +33 -0
- package/adapters/pi/references/pipeline-state-schema.json +34 -2
- package/adapters/pi/references/process-overview.md +2 -2
- package/adapters/pi/references/ralph-loop-contract.md +6 -3
- package/adapters/pi/references/shared-conventions.md +33 -22
- package/adapters/pi/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/scripts/epic-manifest.py +82 -4
- package/adapters/pi/scripts/fix-sweep.py +1180 -0
- package/adapters/pi/scripts/forge-session.py +1151 -32
- package/adapters/pi/skills/forge/SKILL.md +5 -5
- package/adapters/pi/skills/forge/references/pipeline-state-schema.json +34 -2
- package/adapters/pi/skills/forge/references/process-overview.md +2 -2
- package/adapters/pi/skills/forge/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-0-epic/references/edit-mode.md +8 -4
- package/adapters/pi/skills/forge-0-epic/references/epic-manifest-subcommands.md +6 -1
- package/adapters/pi/skills/forge-0-epic/references/pipeline-state-schema.json +34 -2
- package/adapters/pi/skills/forge-0-epic/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-0-epic/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-1-prd/SKILL.md +3 -1
- package/adapters/pi/skills/forge-1-prd/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-1-prd/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-2-tech/SKILL.md +5 -1
- package/adapters/pi/skills/forge-2-tech/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-2-tech/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-3-specs/SKILL.md +5 -1
- package/adapters/pi/skills/forge-3-specs/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-3-specs/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-4-backlog/SKILL.md +41 -3
- package/adapters/pi/skills/forge-4-backlog/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-4-backlog/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-5-loop/SKILL.md +36 -36
- package/adapters/pi/skills/forge-5-loop/references/agent-selection.md +16 -0
- package/adapters/pi/skills/forge-5-loop/references/ralph-loop-contract.md +6 -3
- package/adapters/pi/skills/forge-5-loop/references/recovery-procedure.md +349 -0
- package/adapters/pi/skills/forge-5-loop/references/result-reporting.md +40 -11
- package/adapters/pi/skills/forge-5-loop/references/runner-contract.md +25 -7
- package/adapters/pi/skills/forge-5-loop/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-5-loop/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-6-docs/SKILL.md +29 -6
- package/adapters/pi/skills/forge-6-docs/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-6-docs/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-fix/SKILL.md +34 -0
- package/adapters/pi/skills/forge-fix/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-fix/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-guide/references/forge-config-schema.json +20 -2
- package/adapters/pi/skills/forge-guide/references/process-overview.md +2 -2
- package/adapters/pi/skills/forge-guide/references/ralph-loop-contract.md +6 -3
- package/adapters/pi/skills/forge-guide/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-verify/SKILL.md +10 -11
- package/adapters/pi/skills/forge-verify/references/decisions/single-writer-threat-model.md +53 -0
- package/adapters/pi/skills/forge-verify/references/findings-template.md +31 -1
- package/adapters/pi/skills/forge-verify/references/shared-conventions.md +33 -22
- package/adapters/pi/skills/forge-verify/references/stage-exit-protocol.md +63 -19
- package/adapters/pi/skills/forge-verify/references/verification-checklists/backlog.md +73 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/epic.md +1 -1
- package/adapters/pi/skills/forge-verify/references/verification-checklists/impl.md +85 -0
- package/adapters/pi/skills/forge-verify/references/verification-checklists/specs.md +43 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/rauf.d.ts +3 -3
- package/dist/rauf.js +2 -2
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Sweep a fix delta for surviving occurrences of corrected text (REQ-SWEEP-01..03).
|
|
3
|
+
|
|
4
|
+
Two deterministic, model-free subcommands used by the forge-fix pass:
|
|
5
|
+
|
|
6
|
+
``sweep`` extracts the removed lines of ``git diff HEAD`` as needles and
|
|
7
|
+
reports every surviving normalized occurrence across the
|
|
8
|
+
repository's tracked and untracked files.
|
|
9
|
+
``plan-coverage`` asserts that a verification findings document's Fix Execution
|
|
10
|
+
Plan covers every finding it reports, naming omissions.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python3 fix-sweep.py sweep [--repo-root DIR] [--exclude PREFIX]...
|
|
14
|
+
[--min-chars N] [--json]
|
|
15
|
+
python3 fix-sweep.py plan-coverage FINDINGS_DOC [--json]
|
|
16
|
+
|
|
17
|
+
Exit codes:
|
|
18
|
+
0 = sweep found no survivors (or was skipped: no git delta);
|
|
19
|
+
plan-coverage fully covered, or not applicable
|
|
20
|
+
1 = sweep reported one or more survivors;
|
|
21
|
+
plan-coverage found uncovered findings and/or a claimed-total mismatch
|
|
22
|
+
2 = usage or environment error (bad flag, unreadable document, git failure
|
|
23
|
+
inside a valid repository)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import bisect
|
|
30
|
+
import json
|
|
31
|
+
import re
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path, PurePosixPath
|
|
35
|
+
from typing import Final, TypedDict
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
# Module-level constants.
|
|
40
|
+
#
|
|
41
|
+
# The first four are the sweep's shared vocabulary, carried here rather than
|
|
42
|
+
# imported: a standalone, import-free single-file script has no shared module to
|
|
43
|
+
# import from (the same reason epic-manifest.py and forge-session.py keep
|
|
44
|
+
# byte-identical copies of KNOWN_VERIFY_STATUSES).
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
#: Minimum normalized length for a removed line to become a needle (REQ-SWEEP-02).
|
|
48
|
+
#: Also the --min-chars default.
|
|
49
|
+
MIN_NEEDLE_CHARS: Final[int] = 24
|
|
50
|
+
|
|
51
|
+
#: Path segment excluding findings documents from the corpus — unconditional.
|
|
52
|
+
#: Findings documents quote corrected claims by design: they are audit records,
|
|
53
|
+
#: not survivors.
|
|
54
|
+
VERIFICATION_SEGMENT: Final[str] = ".verification"
|
|
55
|
+
|
|
56
|
+
#: Drift-gated regenerated tree, excluded ONLY when the gate is detectably
|
|
57
|
+
#: present at DRIFT_GATE_SENTINEL.
|
|
58
|
+
DRIFT_GATED_PREFIX: Final[str] = "adapters/"
|
|
59
|
+
|
|
60
|
+
#: Repo-relative sentinel whose presence proves the drift gate exists, and so
|
|
61
|
+
#: gates the DRIFT_GATED_PREFIX exclusion.
|
|
62
|
+
DRIFT_GATE_SENTINEL: Final[str] = "scripts/build-adapters.py"
|
|
63
|
+
|
|
64
|
+
#: Label reported in SweepReport["excludes"] for the VERIFICATION_SEGMENT rule.
|
|
65
|
+
#: The rule matches a path SEGMENT; the label is the human-facing prefix form.
|
|
66
|
+
VERIFICATION_EXCLUDE_LABEL: Final[str] = ".verification/"
|
|
67
|
+
|
|
68
|
+
#: Wall-clock bound on every git subprocess. The sweep runs inside a fix pass; a
|
|
69
|
+
#: hung git must fail the pass loudly (exit 2), never hang it.
|
|
70
|
+
GIT_TIMEOUT_SECONDS: Final[int] = 30
|
|
71
|
+
|
|
72
|
+
#: run_git() return code when git could not be executed at all (binary missing,
|
|
73
|
+
#: OSError) or exceeded GIT_TIMEOUT_SECONDS. Callers classify it — a probe treats
|
|
74
|
+
#: it as the skip path, a corpus call raises UsageError.
|
|
75
|
+
GIT_UNAVAILABLE: Final[int] = -1
|
|
76
|
+
|
|
77
|
+
#: Non-alphanumeric run -> single space. Sole regex of normalize(); the `+`
|
|
78
|
+
#: quantifier is what collapses runs, so no second pass is needed.
|
|
79
|
+
_NON_ALNUM: Final = re.compile(r"[^a-z0-9]+")
|
|
80
|
+
|
|
81
|
+
#: Unified-diff hunk header. Group 1 is the a-side start line — the only field the
|
|
82
|
+
#: needle line numbering needs. `,count` is omitted by git when it is 1.
|
|
83
|
+
HUNK_RE: Final = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
|
84
|
+
|
|
85
|
+
#: Findings-document `## ` heading; the negative lookahead keeps `###`/`####`
|
|
86
|
+
#: out. This block is the authoritative form of the findings-document anchors.
|
|
87
|
+
H2_RE: Final = re.compile(r"^##(?!#)\s*(.*?)\s*$")
|
|
88
|
+
|
|
89
|
+
#: Findings-document `### ` heading — the sub-section scope for step counting.
|
|
90
|
+
H3_RE: Final = re.compile(r"^###(?!#)\s*(.*?)\s*$")
|
|
91
|
+
|
|
92
|
+
#: Finding heading `### V-NNN: {title}`; group 1 is the finding id.
|
|
93
|
+
FINDING_RE: Final = re.compile(r"^### (V-\d{3}):")
|
|
94
|
+
|
|
95
|
+
#: Execution-step heading `#### Step {N}: {title}`.
|
|
96
|
+
STEP_RE: Final = re.compile(r"^#### Step \d+:")
|
|
97
|
+
|
|
98
|
+
#: The `- **Addresses:** …` coverage field of one execution step.
|
|
99
|
+
ADDRESSES_RE: Final = re.compile(r"^\s*-\s*\*\*Addresses:\*\*")
|
|
100
|
+
|
|
101
|
+
#: Any finding id, used as a findall over an Addresses field.
|
|
102
|
+
FINDING_ID_RE: Final = re.compile(r"V-\d{3}")
|
|
103
|
+
|
|
104
|
+
#: The `## Summary` claimed total; group 1 is the claimed count.
|
|
105
|
+
TOTAL_FINDINGS_RE: Final = re.compile(r"Total findings:\s*(\d+)")
|
|
106
|
+
|
|
107
|
+
#: Fenced-code delimiter. Lines inside a fence are skipped by the findings parser
|
|
108
|
+
#: so the template's fenced `### V-001:` examples cannot fabricate findings.
|
|
109
|
+
FENCE_RE: Final = re.compile(r"^\s*(?:```|~~~)")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
+
# Types and errors — the JSON-boundary shapes and the one exception type.
|
|
114
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Needle(TypedDict):
|
|
118
|
+
"""One removed line surviving extraction filters, in normalized form.
|
|
119
|
+
|
|
120
|
+
Keys:
|
|
121
|
+
file: Repo-relative path the line was removed from (diff's a-side path).
|
|
122
|
+
line: 1-based line number in the PRE-fix file (from the @@ hunk header's
|
|
123
|
+
a-side start, plus offset within the hunk's removed run).
|
|
124
|
+
normalized: normalize(original) — the matching key.
|
|
125
|
+
original: The removed line's raw text, verbatim (REQ-OBS-01/REQ-SEC-01:
|
|
126
|
+
echoed without elision; it is already in git history).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
file: str
|
|
130
|
+
line: int
|
|
131
|
+
normalized: str
|
|
132
|
+
original: str
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class NormalizedFile(TypedDict):
|
|
136
|
+
"""A corpus file prepared for substring matching.
|
|
137
|
+
|
|
138
|
+
Keys:
|
|
139
|
+
path: Repo-relative POSIX path.
|
|
140
|
+
blob: normalize() applied to the full file content — one string, so a
|
|
141
|
+
match spanning the file's original line breaks still lands (the F-5
|
|
142
|
+
whitespace-reflow success criterion).
|
|
143
|
+
line_starts: For each character offset in `blob`, enough structure to map
|
|
144
|
+
a match offset back to the 1-based line number in the ORIGINAL file.
|
|
145
|
+
Concretely: a sorted list of (blob_offset, original_line) pairs; the
|
|
146
|
+
match's line is the last pair whose blob_offset <= match offset
|
|
147
|
+
(bisect).
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
path: str
|
|
151
|
+
blob: str
|
|
152
|
+
line_starts: list[tuple[int, int]]
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SweepHit(TypedDict):
|
|
156
|
+
"""One surviving occurrence of corrected text (REQ-OBS-01).
|
|
157
|
+
|
|
158
|
+
Keys:
|
|
159
|
+
file: Repo-relative path of the surviving occurrence.
|
|
160
|
+
line: 1-based line in the CURRENT working-tree file where the match
|
|
161
|
+
begins (mapped through NormalizedFile.line_starts).
|
|
162
|
+
needle: The matched needle's ORIGINAL removed text, verbatim
|
|
163
|
+
(REQ-SEC-01: no elision — it is already in git history).
|
|
164
|
+
excerpt: The original text of the matched region in the corpus file
|
|
165
|
+
(the line(s) overlapping the match span), verbatim.
|
|
166
|
+
sourceFile: Needle provenance — file the text was removed from.
|
|
167
|
+
sourceLine: Needle provenance — pre-fix line number of the removal.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
file: str
|
|
171
|
+
line: int
|
|
172
|
+
needle: str
|
|
173
|
+
excerpt: str
|
|
174
|
+
sourceFile: str
|
|
175
|
+
sourceLine: int
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class DroppedNeedles(TypedDict):
|
|
179
|
+
"""Filter counters for the evidence archive.
|
|
180
|
+
|
|
181
|
+
Keys:
|
|
182
|
+
belowFloor: Count of raw needles dropped because normalize(original) was
|
|
183
|
+
shorter than MIN_NEEDLE_CHARS (filter 1). A needle counted
|
|
184
|
+
here is never tested for reflow.
|
|
185
|
+
reflowSuppressed: Count of raw needles dropped because their normalized
|
|
186
|
+
text appears in the delta's normalized added text (filter 2).
|
|
187
|
+
|
|
188
|
+
Invariant: belowFloor + reflowSuppressed + len(needles) equals the raw
|
|
189
|
+
removed-line count extracted from the delta.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
belowFloor: int
|
|
193
|
+
reflowSuppressed: int
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class SweepReport(TypedDict):
|
|
197
|
+
"""Top-level `sweep --json` payload.
|
|
198
|
+
|
|
199
|
+
Keys:
|
|
200
|
+
skipped: True iff no delta was available (REQ-SWEEP-07).
|
|
201
|
+
reason: None when not skipped; "not-a-git-repo" | "no-head" when skipped.
|
|
202
|
+
baseline: Always "HEAD" when the sweep ran; None when skipped.
|
|
203
|
+
needles: Surviving needles after both extraction filters.
|
|
204
|
+
droppedNeedles: Filter counters.
|
|
205
|
+
excludes: The exclusion prefixes/segments actually applied this run —
|
|
206
|
+
[".verification/"] always; plus "adapters/" when gated;
|
|
207
|
+
plus any --exclude values, in the order applied.
|
|
208
|
+
filesScanned: Count of corpus files read and matched (decode-skipped
|
|
209
|
+
files are not counted).
|
|
210
|
+
hits: All survivors, ordered by (file, line) for determinism.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
skipped: bool
|
|
214
|
+
reason: str | None
|
|
215
|
+
baseline: str | None
|
|
216
|
+
needles: list[Needle]
|
|
217
|
+
droppedNeedles: DroppedNeedles
|
|
218
|
+
excludes: list[str]
|
|
219
|
+
filesScanned: int
|
|
220
|
+
hits: list[SweepHit]
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class PlanCoverageReport(TypedDict):
|
|
224
|
+
"""Top-level `plan-coverage --json` payload (REQ-CARD-01, REQ-CARD-04).
|
|
225
|
+
|
|
226
|
+
Keys:
|
|
227
|
+
applicable: False when the document has no `## Findings` section or no
|
|
228
|
+
`## Fix Execution Plan` section — exit 0, nothing asserted
|
|
229
|
+
(REQ-CARD-04 analog at the fix-pass level).
|
|
230
|
+
findings: Every V-NNN id found as a `### V-NNN:` heading under
|
|
231
|
+
`## Findings`, in document order.
|
|
232
|
+
steps: Count of `#### Step {N}:` entries under `### Execution Steps`.
|
|
233
|
+
covered: Findings ids appearing in >=1 step's `**Addresses:**` field.
|
|
234
|
+
uncovered: Findings ids appearing in NO step's Addresses field —
|
|
235
|
+
omissions BY NAME, never a count delta.
|
|
236
|
+
claimedTotal: The N parsed from `## Summary`'s `Total findings: {N}`
|
|
237
|
+
line; None when no such line exists.
|
|
238
|
+
actualTotal: len(findings) — re-derived, never trusted from prose.
|
|
239
|
+
totalMismatch: True iff claimedTotal is not None and differs from
|
|
240
|
+
actualTotal. False whenever claimedTotal is None.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
applicable: bool
|
|
244
|
+
findings: list[str]
|
|
245
|
+
steps: int
|
|
246
|
+
covered: list[str]
|
|
247
|
+
uncovered: list[str]
|
|
248
|
+
claimedTotal: int | None
|
|
249
|
+
actualTotal: int
|
|
250
|
+
totalMismatch: bool
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class UsageError(Exception):
|
|
254
|
+
"""A caller/environment error that maps to exit 2.
|
|
255
|
+
|
|
256
|
+
Raised for: an unreadable findings document, a git invocation that fails
|
|
257
|
+
inside a valid repository (timeout, non-zero exit on diff/ls-files), a
|
|
258
|
+
repository with no working tree (a bare repo: `rev-parse --git-dir`
|
|
259
|
+
succeeds while `rev-parse --show-toplevel` fails), or invalid flag
|
|
260
|
+
combinations. The message is printed as `Error: {msg}` on stderr; stdout
|
|
261
|
+
stays empty (the exit-2 convention).
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
266
|
+
# Normalization — one contract, applied to needles and corpus alike.
|
|
267
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def normalize(text: str) -> str:
|
|
271
|
+
"""Normalize text for sweep matching.
|
|
272
|
+
|
|
273
|
+
Lowercases, maps every non-alphanumeric character to a space, collapses
|
|
274
|
+
whitespace runs to a single space, and strips. Two texts that differ only
|
|
275
|
+
in case, punctuation, or line-wrapping normalize identically — the
|
|
276
|
+
reflowed-prose recall target of REQ-SWEEP-02.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
text: Raw text (a diff line or file content).
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The normalized form; possibly the empty string.
|
|
283
|
+
"""
|
|
284
|
+
return _NON_ALNUM.sub(" ", text.lower()).strip()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
288
|
+
# Bounded git helper — deliberately not shared with forge-session.py.
|
|
289
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def run_git(args: list[str], repo_root: Path) -> tuple[int, str, str]:
|
|
293
|
+
"""Run one bounded, read-only git command with cwd set to `repo_root`.
|
|
294
|
+
|
|
295
|
+
The helper NEVER classifies: it reports what happened and lets the caller
|
|
296
|
+
decide whether the outcome is the skip path or a UsageError. A git
|
|
297
|
+
binary that cannot be executed at all, or one that exceeds
|
|
298
|
+
GIT_TIMEOUT_SECONDS, yields GIT_UNAVAILABLE rather than raising — the probe
|
|
299
|
+
calls in resolve_repo_root() must treat a missing git as "not a repo"
|
|
300
|
+
(REQ-SWEEP-07), while list_corpus_paths() must treat it as exit 2.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
args: git arguments after the program name, e.g. ["rev-parse", "HEAD"].
|
|
304
|
+
repo_root: Directory used as the subprocess cwd. Every git invocation in
|
|
305
|
+
this script runs from the repository top level so that paths in
|
|
306
|
+
output are repo-relative.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
(returncode, stdout, stderr). returncode is GIT_UNAVAILABLE (-1) when
|
|
310
|
+
git could not be run or timed out; stdout is "" in that case and stderr
|
|
311
|
+
carries a short diagnostic.
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
proc = subprocess.run(
|
|
315
|
+
["git", *args],
|
|
316
|
+
cwd=repo_root,
|
|
317
|
+
capture_output=True,
|
|
318
|
+
text=True,
|
|
319
|
+
timeout=GIT_TIMEOUT_SECONDS,
|
|
320
|
+
)
|
|
321
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
322
|
+
return GIT_UNAVAILABLE, "", str(exc)
|
|
323
|
+
return proc.returncode, proc.stdout, proc.stderr
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _first_line(text: str) -> str:
|
|
327
|
+
"""Return the first non-empty-stripped line of `text`, or "".
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
text: Captured stderr from a git invocation.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
The first line, stripped; the empty string when there is none.
|
|
334
|
+
"""
|
|
335
|
+
stripped = text.strip()
|
|
336
|
+
return stripped.splitlines()[0] if stripped else ""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
340
|
+
# sweep pipeline.
|
|
341
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def resolve_repo_root(start_dir: Path) -> tuple[Path | None, str | None]:
|
|
345
|
+
"""Resolve the repository top level and detect the two skip conditions.
|
|
346
|
+
|
|
347
|
+
Probes in order: `rev-parse --git-dir` (is this a repository at all?),
|
|
348
|
+
`rev-parse --show-toplevel` (where is the working tree?), and
|
|
349
|
+
`rev-parse HEAD` (is there a baseline to diff against?). The top level —
|
|
350
|
+
not `start_dir` — becomes the cwd of every later git call, so a sweep
|
|
351
|
+
launched from a subdirectory still enumerates the whole corpus rather than
|
|
352
|
+
the subtree `git ls-files` would default to.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
start_dir: The --repo-root value (default: the process cwd).
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
(repo_root, None) when the sweep can run, or (None, reason) where reason
|
|
359
|
+
is "not-a-git-repo" or "no-head" — the two values SweepReport["reason"]
|
|
360
|
+
admits.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
UsageError: The repository exists but has no working tree (a bare repo:
|
|
364
|
+
--git-dir succeeds while --show-toplevel fails), or --show-toplevel
|
|
365
|
+
fails for any other reason. Not a skip — an operational failure
|
|
366
|
+
(exit 2), and forge-fix never runs in a bare repo.
|
|
367
|
+
"""
|
|
368
|
+
rc, _out, _err = run_git(["rev-parse", "--git-dir"], start_dir)
|
|
369
|
+
if rc != 0:
|
|
370
|
+
return None, "not-a-git-repo"
|
|
371
|
+
|
|
372
|
+
rc, out, _err = run_git(["rev-parse", "--show-toplevel"], start_dir)
|
|
373
|
+
if rc != 0 or not out.strip():
|
|
374
|
+
raise UsageError(
|
|
375
|
+
f"repository has no working tree (bare repo): {start_dir}"
|
|
376
|
+
)
|
|
377
|
+
repo_root = Path(out.strip())
|
|
378
|
+
|
|
379
|
+
rc, _out, _err = run_git(["rev-parse", "HEAD"], repo_root)
|
|
380
|
+
if rc != 0:
|
|
381
|
+
return None, "no-head"
|
|
382
|
+
return repo_root, None
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _diff_path(raw: str) -> str | None:
|
|
386
|
+
"""Decode one `---`/`+++` diff header path.
|
|
387
|
+
|
|
388
|
+
A path git C-quoted (embedded quote, backslash, or control character) is
|
|
389
|
+
unquoted best-effort by stripping the surrounding double quotes; a
|
|
390
|
+
mis-decoded path affects only provenance reporting, never matching.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
raw: The header text after the `--- ` / `+++ ` prefix.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
The repo-relative path with its `a/` or `b/` prefix stripped, or None
|
|
397
|
+
for `/dev/null`.
|
|
398
|
+
"""
|
|
399
|
+
value = raw.strip()
|
|
400
|
+
if value.startswith('"') and value.endswith('"') and len(value) >= 2:
|
|
401
|
+
value = value[1:-1]
|
|
402
|
+
if value == "/dev/null":
|
|
403
|
+
return None
|
|
404
|
+
if value.startswith("a/") or value.startswith("b/"):
|
|
405
|
+
value = value[2:]
|
|
406
|
+
return value
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def extract_needles(diff_text: str) -> tuple[list[Needle], dict[str, list[str]]]:
|
|
410
|
+
"""Parse a unified diff into raw needles and per-file added lines.
|
|
411
|
+
|
|
412
|
+
`--unified=0` means every diff body line is a change: hunk headers delimit
|
|
413
|
+
the changed runs exactly, so a-side line numbers are computable without
|
|
414
|
+
context-line bookkeeping.
|
|
415
|
+
|
|
416
|
+
Parse contract:
|
|
417
|
+
* `diff --git …` resets file state and clears the in-hunk flag.
|
|
418
|
+
* `--- a/{path}` sets the a-side path; `+++ b/{path}` sets the b-side path.
|
|
419
|
+
A leading `a/`/`b/` prefix is stripped; `/dev/null` maps to None.
|
|
420
|
+
* `@@ -a[,b] +c[,d] @@` sets the a-side counter to `a` and raises the
|
|
421
|
+
in-hunk flag.
|
|
422
|
+
* ONLY while in-hunk: a line starting with `-` is a removed line whose
|
|
423
|
+
content is the text after the prefix, at the current a-side counter,
|
|
424
|
+
which then advances by one; a line starting with `+` is an added line,
|
|
425
|
+
appended to added_by_file[b_path] (it does not move the a-side counter);
|
|
426
|
+
anything else (` `, `\\`) is ignored.
|
|
427
|
+
Gating on the in-hunk flag is what keeps a removed line whose content is
|
|
428
|
+
literally `--` (rendered `---`) from being mistaken for a file header —
|
|
429
|
+
headers only ever appear before the first `@@` of a file.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
diff_text: stdout of `git diff HEAD --unified=0 --no-color`.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
(raw_needles, added_by_file) where raw_needles are Needle dicts in
|
|
436
|
+
document order with `normalized` already computed, and added_by_file maps
|
|
437
|
+
each b-side path to its normalized non-empty added lines in order — the
|
|
438
|
+
input to reflow suppression.
|
|
439
|
+
"""
|
|
440
|
+
raw_needles: list[Needle] = []
|
|
441
|
+
added_by_file: dict[str, list[str]] = {}
|
|
442
|
+
a_path: str | None = None
|
|
443
|
+
b_path: str | None = None
|
|
444
|
+
a_line = 0
|
|
445
|
+
in_hunk = False
|
|
446
|
+
|
|
447
|
+
for line in diff_text.splitlines():
|
|
448
|
+
if line.startswith("diff --git "):
|
|
449
|
+
a_path = None
|
|
450
|
+
b_path = None
|
|
451
|
+
in_hunk = False
|
|
452
|
+
continue
|
|
453
|
+
if not in_hunk:
|
|
454
|
+
if line.startswith("--- "):
|
|
455
|
+
a_path = _diff_path(line[4:])
|
|
456
|
+
continue
|
|
457
|
+
if line.startswith("+++ "):
|
|
458
|
+
b_path = _diff_path(line[4:])
|
|
459
|
+
continue
|
|
460
|
+
hunk = HUNK_RE.match(line)
|
|
461
|
+
if hunk:
|
|
462
|
+
a_line = int(hunk.group(1))
|
|
463
|
+
in_hunk = True
|
|
464
|
+
continue
|
|
465
|
+
if not in_hunk:
|
|
466
|
+
continue
|
|
467
|
+
if line.startswith("-"):
|
|
468
|
+
content = line[1:]
|
|
469
|
+
raw_needles.append(
|
|
470
|
+
Needle(
|
|
471
|
+
file=a_path or "",
|
|
472
|
+
line=a_line,
|
|
473
|
+
normalized=normalize(content),
|
|
474
|
+
original=content,
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
a_line += 1
|
|
478
|
+
elif line.startswith("+"):
|
|
479
|
+
piece = normalize(line[1:])
|
|
480
|
+
if piece:
|
|
481
|
+
added_by_file.setdefault(b_path or "", []).append(piece)
|
|
482
|
+
return raw_needles, added_by_file
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def filter_needles(
|
|
486
|
+
raw: list[Needle],
|
|
487
|
+
added_by_file: dict[str, list[str]],
|
|
488
|
+
min_chars: int,
|
|
489
|
+
) -> tuple[list[Needle], DroppedNeedles]:
|
|
490
|
+
"""Apply the two extraction filters, in order, with counters.
|
|
491
|
+
|
|
492
|
+
1. Length floor: `len(needle["normalized"]) < min_chars` -> dropped,
|
|
493
|
+
counted in `belowFloor`. A line that normalizes to "" is dropped here.
|
|
494
|
+
2. Reflow/move suppression: the needle's normalized text appearing as a
|
|
495
|
+
substring of the delta's added text -> dropped, counted in
|
|
496
|
+
`reflowSuppressed`. Text merely moved or re-wrapped was not corrected;
|
|
497
|
+
sweeping it would flag every reflow as a survivor.
|
|
498
|
+
|
|
499
|
+
The order matters for the counters: a below-floor needle is counted ONCE,
|
|
500
|
+
in `belowFloor`, and never tested for reflow.
|
|
501
|
+
|
|
502
|
+
The added text is built once per run: per file, its normalized added lines
|
|
503
|
+
joined with a single space (in diff order); then those per-file strings
|
|
504
|
+
joined with a single space, in the order files appeared in the diff —
|
|
505
|
+
concatenated per file, then joined delta-wide. Joining rather than
|
|
506
|
+
testing lines individually is deliberate — a corrected sentence re-wrapped
|
|
507
|
+
across new line breaks must still suppress.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
raw: Needles in extraction order.
|
|
511
|
+
added_by_file: Normalized added lines per b-side path.
|
|
512
|
+
min_chars: The --min-chars value; MIN_NEEDLE_CHARS by default.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
(surviving needles in extraction order, the DroppedNeedles counters).
|
|
516
|
+
"""
|
|
517
|
+
added_text = " ".join(" ".join(lines) for lines in added_by_file.values())
|
|
518
|
+
survivors: list[Needle] = []
|
|
519
|
+
below_floor = 0
|
|
520
|
+
reflow_suppressed = 0
|
|
521
|
+
|
|
522
|
+
for needle in raw:
|
|
523
|
+
if len(needle["normalized"]) < min_chars:
|
|
524
|
+
below_floor += 1
|
|
525
|
+
continue
|
|
526
|
+
if needle["normalized"] in added_text:
|
|
527
|
+
reflow_suppressed += 1
|
|
528
|
+
continue
|
|
529
|
+
survivors.append(needle)
|
|
530
|
+
|
|
531
|
+
counters = DroppedNeedles(
|
|
532
|
+
belowFloor=below_floor, reflowSuppressed=reflow_suppressed
|
|
533
|
+
)
|
|
534
|
+
return survivors, counters
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def list_corpus_paths(repo_root: Path) -> list[str]:
|
|
538
|
+
"""Enumerate candidate corpus paths: tracked plus untracked-not-ignored.
|
|
539
|
+
|
|
540
|
+
Runs `git ls-files -z --cached --others --exclude-standard` — NUL-terminated
|
|
541
|
+
so paths containing spaces, quotes, or newlines survive intact and no quoting
|
|
542
|
+
mode can mangle them. During an unresolved merge, `--cached` lists
|
|
543
|
+
a conflicted path once per stage; entries are de-duplicated preserving first
|
|
544
|
+
appearance, then sorted lexicographically so the scan order — and therefore
|
|
545
|
+
every tie-break in the output — is deterministic.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
repo_root: Repository top level; also the subprocess cwd.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Sorted, unique, repo-relative POSIX paths.
|
|
552
|
+
|
|
553
|
+
Raises:
|
|
554
|
+
UsageError: ls-files exited non-zero or could not be run.
|
|
555
|
+
"""
|
|
556
|
+
rc, out, err = run_git(
|
|
557
|
+
["ls-files", "-z", "--cached", "--others", "--exclude-standard"], repo_root
|
|
558
|
+
)
|
|
559
|
+
if rc != 0:
|
|
560
|
+
raise UsageError(f"git ls-files failed ({rc}): {_first_line(err)}")
|
|
561
|
+
seen: dict[str, None] = {}
|
|
562
|
+
for entry in out.split("\0"):
|
|
563
|
+
if entry:
|
|
564
|
+
seen.setdefault(entry, None)
|
|
565
|
+
return sorted(seen)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def applicable_excludes(repo_root: Path, user_excludes: list[str]) -> list[str]:
|
|
569
|
+
"""Compute the exclusion labels actually in force this run.
|
|
570
|
+
|
|
571
|
+
Order, matching SweepReport["excludes"]:
|
|
572
|
+
1. VERIFICATION_EXCLUDE_LABEL — always.
|
|
573
|
+
2. DRIFT_GATED_PREFIX — ONLY when (repo_root / DRIFT_GATE_SENTINEL) is an
|
|
574
|
+
existing file. REQ-SWEEP-03 defines a CLASS (trees a mechanical drift
|
|
575
|
+
gate already keeps fresh); `adapters/` is only this repository's
|
|
576
|
+
instance, and a consumer repo's ungated `adapters/` must be swept, not
|
|
577
|
+
silently dropped.
|
|
578
|
+
3. Each --exclude value, in the order given.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
repo_root: Repository top level.
|
|
582
|
+
user_excludes: Raw --exclude values.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
The labels, in application order, for the payload and for is_excluded().
|
|
586
|
+
"""
|
|
587
|
+
excludes = [VERIFICATION_EXCLUDE_LABEL]
|
|
588
|
+
if (repo_root / DRIFT_GATE_SENTINEL).is_file():
|
|
589
|
+
excludes.append(DRIFT_GATED_PREFIX)
|
|
590
|
+
excludes.extend(user_excludes)
|
|
591
|
+
return excludes
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def is_excluded(path: str, excludes: list[str], user_excludes: list[str]) -> bool:
|
|
595
|
+
"""Decide whether one repo-relative path is out of corpus.
|
|
596
|
+
|
|
597
|
+
Rules, in evaluation order:
|
|
598
|
+
1. VERIFICATION_SEGMENT appears as a path SEGMENT — at any depth,
|
|
599
|
+
unconditionally. Segment matching, not prefix matching: findings
|
|
600
|
+
documents live at `{featureDir}/.verification/…`, never at the root.
|
|
601
|
+
2. The path starts with DRIFT_GATED_PREFIX and that label is present in
|
|
602
|
+
`excludes` (i.e. the gate sentinel was found).
|
|
603
|
+
3. The path starts with any of `user_excludes`, compared as a plain string
|
|
604
|
+
prefix against the repo-relative POSIX path.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
path: Repo-relative POSIX path from list_corpus_paths().
|
|
608
|
+
excludes: Labels from applicable_excludes() (used for rule 2's gate).
|
|
609
|
+
user_excludes: The --exclude values (rule 3).
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
True when the path must not be read or matched.
|
|
613
|
+
"""
|
|
614
|
+
if VERIFICATION_SEGMENT in PurePosixPath(path).parts:
|
|
615
|
+
return True
|
|
616
|
+
if DRIFT_GATED_PREFIX in excludes and path.startswith(DRIFT_GATED_PREFIX):
|
|
617
|
+
return True
|
|
618
|
+
return any(path.startswith(prefix) for prefix in user_excludes)
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def build_normalized_file(path: str, content: str) -> NormalizedFile:
|
|
622
|
+
"""Normalize a file into one blob plus a blob-offset -> line-number map.
|
|
623
|
+
|
|
624
|
+
The blob is behaviorally `normalize(content)`: each original line is
|
|
625
|
+
normalized independently and the non-empty results are joined with a single
|
|
626
|
+
space. That is identical to normalizing the whole content, because the line
|
|
627
|
+
break between two lines is itself a non-alphanumeric run that collapses to
|
|
628
|
+
one space — and building it line-wise is what makes `line_starts` free.
|
|
629
|
+
|
|
630
|
+
Algorithm, per original line (1-based) of `content.splitlines()`:
|
|
631
|
+
* piece = normalize(raw_line).
|
|
632
|
+
* If piece is empty (blank line, or punctuation only), record
|
|
633
|
+
(current_offset, line_number) as a zero-width entry and continue. The
|
|
634
|
+
entry keeps the map total over lines while contributing no blob text;
|
|
635
|
+
because it is recorded BEFORE the following line's entry and carries a
|
|
636
|
+
strictly smaller-or-equal offset, the bisect in line_for_offset() can
|
|
637
|
+
never attribute a match to it (no match can begin at a separator).
|
|
638
|
+
* Otherwise: if the blob is non-empty, append a single space separator and
|
|
639
|
+
advance the offset by 1; then record (current_offset, line_number) —
|
|
640
|
+
pointing at the piece's FIRST character — and append the piece,
|
|
641
|
+
advancing the offset by len(piece).
|
|
642
|
+
|
|
643
|
+
`line_starts` is therefore sorted by offset by construction, which is what
|
|
644
|
+
line_for_offset()'s bisect requires.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
path: Repo-relative POSIX path (stored on the result).
|
|
648
|
+
content: The file's working-tree text (read post-fix,
|
|
649
|
+
so just-corrected sites read as corrected, not as survivors).
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
The NormalizedFile. The ORIGINAL lines are deliberately not stored on
|
|
653
|
+
it — the TypedDict's three keys are fixed — so the caller keeps
|
|
654
|
+
`content.splitlines()` alongside it for excerpt rendering.
|
|
655
|
+
"""
|
|
656
|
+
pieces: list[str] = []
|
|
657
|
+
line_starts: list[tuple[int, int]] = []
|
|
658
|
+
offset = 0
|
|
659
|
+
|
|
660
|
+
for line_number, raw_line in enumerate(content.splitlines(), start=1):
|
|
661
|
+
piece = normalize(raw_line)
|
|
662
|
+
if not piece:
|
|
663
|
+
line_starts.append((offset, line_number))
|
|
664
|
+
continue
|
|
665
|
+
if pieces:
|
|
666
|
+
pieces.append(" ")
|
|
667
|
+
offset += 1
|
|
668
|
+
line_starts.append((offset, line_number))
|
|
669
|
+
pieces.append(piece)
|
|
670
|
+
offset += len(piece)
|
|
671
|
+
|
|
672
|
+
return NormalizedFile(path=path, blob="".join(pieces), line_starts=line_starts)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def line_for_offset(nf: NormalizedFile, offset: int) -> int:
|
|
676
|
+
"""Map a blob offset back to a 1-based line number in the original file.
|
|
677
|
+
|
|
678
|
+
Uses `bisect.bisect_right` over the offsets of `nf["line_starts"]`, taking
|
|
679
|
+
the entry at index-1: the LAST pair whose blob_offset <= offset. When a
|
|
680
|
+
zero-width (blank-line) entry ties with the following real entry,
|
|
681
|
+
bisect_right selects the later — the line whose text actually begins there.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
nf: A NormalizedFile from build_normalized_file().
|
|
685
|
+
offset: A blob offset, expected to be a match start or end.
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
The 1-based original line number; 1 for an empty map (empty file, which
|
|
689
|
+
cannot produce a match anyway).
|
|
690
|
+
"""
|
|
691
|
+
starts = nf["line_starts"]
|
|
692
|
+
if not starts:
|
|
693
|
+
return 1
|
|
694
|
+
offsets = [entry[0] for entry in starts]
|
|
695
|
+
index = bisect.bisect_right(offsets, offset)
|
|
696
|
+
if index == 0:
|
|
697
|
+
return starts[0][1]
|
|
698
|
+
return starts[index - 1][1]
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def dedupe_needles(needles: list[Needle]) -> list[Needle]:
|
|
702
|
+
"""Pick one representative per distinct normalized text, first extracted wins.
|
|
703
|
+
|
|
704
|
+
Duplicate needles stay distinct in the payload, but a corpus hit
|
|
705
|
+
reports the FIRST extracted needle matching it. Deduplicating here makes
|
|
706
|
+
that deterministic and removes redundant str.find() calls.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
needles: Surviving needles in extraction order.
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
Representatives in extraction order.
|
|
713
|
+
"""
|
|
714
|
+
seen: set[str] = set()
|
|
715
|
+
representatives: list[Needle] = []
|
|
716
|
+
for needle in needles:
|
|
717
|
+
if needle["normalized"] in seen:
|
|
718
|
+
continue
|
|
719
|
+
seen.add(needle["normalized"])
|
|
720
|
+
representatives.append(needle)
|
|
721
|
+
return representatives
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def scan_file(
|
|
725
|
+
nf: NormalizedFile,
|
|
726
|
+
original_lines: list[str],
|
|
727
|
+
representatives: list[Needle],
|
|
728
|
+
) -> list[SweepHit]:
|
|
729
|
+
"""Search one prepared corpus file for every representative needle.
|
|
730
|
+
|
|
731
|
+
One hit per (file, needle), at the FIRST match offset: disposition
|
|
732
|
+
is recorded per file + needle, so reporting every occurrence in a file would
|
|
733
|
+
generate hits the disposition vocabulary cannot address separately.
|
|
734
|
+
|
|
735
|
+
The file a needle was removed from is scanned like any other — self-file
|
|
736
|
+
hits count: a surviving duplicate two sections below the
|
|
737
|
+
corrected site is the F-5 self-contradiction and must be reported. The
|
|
738
|
+
corrected site itself does not match because content is read post-fix.
|
|
739
|
+
|
|
740
|
+
Args:
|
|
741
|
+
nf: The prepared file from build_normalized_file().
|
|
742
|
+
original_lines: `content.splitlines()` for the same file.
|
|
743
|
+
representatives: Output of dedupe_needles().
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
Hits for this file, in representative order; the caller sorts globally.
|
|
747
|
+
"""
|
|
748
|
+
hits: list[SweepHit] = []
|
|
749
|
+
blob = nf["blob"]
|
|
750
|
+
for needle in representatives:
|
|
751
|
+
target = needle["normalized"]
|
|
752
|
+
if not target:
|
|
753
|
+
continue
|
|
754
|
+
offset = blob.find(target)
|
|
755
|
+
if offset < 0:
|
|
756
|
+
continue
|
|
757
|
+
line = line_for_offset(nf, offset)
|
|
758
|
+
end = line_for_offset(nf, offset + len(target) - 1)
|
|
759
|
+
excerpt = "\n".join(original_lines[line - 1 : end])
|
|
760
|
+
hits.append(
|
|
761
|
+
SweepHit(
|
|
762
|
+
file=nf["path"],
|
|
763
|
+
line=line,
|
|
764
|
+
needle=needle["original"],
|
|
765
|
+
excerpt=excerpt,
|
|
766
|
+
sourceFile=needle["file"],
|
|
767
|
+
sourceLine=needle["line"],
|
|
768
|
+
)
|
|
769
|
+
)
|
|
770
|
+
return hits
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def run_sweep(
|
|
774
|
+
start_dir: Path,
|
|
775
|
+
user_excludes: list[str],
|
|
776
|
+
min_chars: int,
|
|
777
|
+
) -> SweepReport:
|
|
778
|
+
"""Execute the whole sweep and return the payload.
|
|
779
|
+
|
|
780
|
+
Steps: resolve_repo_root() -> skip payload or continue; run the diff;
|
|
781
|
+
extract_needles(); filter_needles(); applicable_excludes();
|
|
782
|
+
list_corpus_paths(); for each non-excluded readable path
|
|
783
|
+
build_normalized_file() + scan_file(); sort hits; assemble.
|
|
784
|
+
|
|
785
|
+
Args:
|
|
786
|
+
start_dir: The --repo-root value.
|
|
787
|
+
user_excludes: --exclude values, in order.
|
|
788
|
+
min_chars: --min-chars value.
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
A fully populated SweepReport. Never partially populated: the skip
|
|
792
|
+
shape fills every key.
|
|
793
|
+
|
|
794
|
+
Raises:
|
|
795
|
+
UsageError: Any git failure inside a valid repository, or a bare repo
|
|
796
|
+
— exit 2, and the fix pass closes on its failure outcome.
|
|
797
|
+
"""
|
|
798
|
+
repo_root, reason = resolve_repo_root(start_dir)
|
|
799
|
+
if repo_root is None:
|
|
800
|
+
return SweepReport(
|
|
801
|
+
skipped=True,
|
|
802
|
+
reason=reason,
|
|
803
|
+
baseline=None,
|
|
804
|
+
needles=[],
|
|
805
|
+
droppedNeedles=DroppedNeedles(belowFloor=0, reflowSuppressed=0),
|
|
806
|
+
excludes=[],
|
|
807
|
+
filesScanned=0,
|
|
808
|
+
hits=[],
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
rc, diff_text, err = run_git(
|
|
812
|
+
["-c", "core.quotePath=false", "diff", "HEAD", "--unified=0", "--no-color"],
|
|
813
|
+
repo_root,
|
|
814
|
+
)
|
|
815
|
+
if rc != 0:
|
|
816
|
+
raise UsageError(f"git diff failed ({rc}): {_first_line(err)}")
|
|
817
|
+
|
|
818
|
+
raw_needles, added_by_file = extract_needles(diff_text)
|
|
819
|
+
needles, dropped = filter_needles(raw_needles, added_by_file, min_chars)
|
|
820
|
+
excludes = applicable_excludes(repo_root, user_excludes)
|
|
821
|
+
representatives = dedupe_needles(needles)
|
|
822
|
+
rep_order = {
|
|
823
|
+
(needle["file"], needle["line"]): index
|
|
824
|
+
for index, needle in enumerate(representatives)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
files_scanned = 0
|
|
828
|
+
hits: list[SweepHit] = []
|
|
829
|
+
for path in list_corpus_paths(repo_root):
|
|
830
|
+
if is_excluded(path, excludes, user_excludes):
|
|
831
|
+
continue
|
|
832
|
+
try:
|
|
833
|
+
content = (repo_root / path).read_text(encoding="utf-8")
|
|
834
|
+
except (OSError, UnicodeDecodeError):
|
|
835
|
+
continue
|
|
836
|
+
files_scanned += 1
|
|
837
|
+
nf = build_normalized_file(path, content)
|
|
838
|
+
hits.extend(scan_file(nf, content.splitlines(), representatives))
|
|
839
|
+
|
|
840
|
+
hits.sort(
|
|
841
|
+
key=lambda hit: (
|
|
842
|
+
hit["file"],
|
|
843
|
+
hit["line"],
|
|
844
|
+
rep_order.get((hit["sourceFile"], hit["sourceLine"]), 0),
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
return SweepReport(
|
|
849
|
+
skipped=False,
|
|
850
|
+
reason=None,
|
|
851
|
+
baseline="HEAD",
|
|
852
|
+
needles=needles,
|
|
853
|
+
droppedNeedles=dropped,
|
|
854
|
+
excludes=excludes,
|
|
855
|
+
filesScanned=files_scanned,
|
|
856
|
+
hits=hits,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
861
|
+
# plan-coverage.
|
|
862
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def parse_findings_doc(text: str) -> PlanCoverageReport:
|
|
866
|
+
"""Parse a findings document into the coverage payload.
|
|
867
|
+
|
|
868
|
+
State carried per line: `h2` (the current `## ` heading text, reset by every
|
|
869
|
+
`## ` line), `h3` (the current `### ` heading text, reset by every `## ` and
|
|
870
|
+
every `### ` line), and `in_fence` (toggled by a line matching FENCE_RE).
|
|
871
|
+
Lines inside a fence are skipped entirely — the findings template ships
|
|
872
|
+
fenced markdown examples containing `### V-001:` and `**Addresses:**`
|
|
873
|
+
literals, and counting those would fabricate findings or coverage.
|
|
874
|
+
|
|
875
|
+
Scoped recognitions (each ONLY under its stated scope):
|
|
876
|
+
* `### V-NNN:` while h2 == "Findings" -> append the id to `findings` in
|
|
877
|
+
document order (duplicates ignored: first wins).
|
|
878
|
+
* `#### Step N:` while h2 == "Fix Execution Plan" and
|
|
879
|
+
h3 == "Execution Steps" -> increment `steps`.
|
|
880
|
+
* `- **Addresses:** …` in the same scope -> every FINDING_ID_RE match on
|
|
881
|
+
that line joins the covered set. Scope is the SECTION, not an enclosing
|
|
882
|
+
step heading: a mis-numbered `#### Step` heading must not silently drop
|
|
883
|
+
the coverage it declares.
|
|
884
|
+
* `Total findings: N` while h2 == "Summary" -> the first such match sets
|
|
885
|
+
`claimedTotal`; later ones are ignored.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
text: The full document text.
|
|
889
|
+
|
|
890
|
+
Returns:
|
|
891
|
+
A fully populated PlanCoverageReport.
|
|
892
|
+
"""
|
|
893
|
+
h2: str | None = None
|
|
894
|
+
h3: str | None = None
|
|
895
|
+
in_fence = False
|
|
896
|
+
has_findings = False
|
|
897
|
+
has_plan = False
|
|
898
|
+
findings: list[str] = []
|
|
899
|
+
covered_ids: set[str] = set()
|
|
900
|
+
steps = 0
|
|
901
|
+
claimed_total: int | None = None
|
|
902
|
+
|
|
903
|
+
for line in text.splitlines():
|
|
904
|
+
if FENCE_RE.match(line):
|
|
905
|
+
in_fence = not in_fence
|
|
906
|
+
continue
|
|
907
|
+
if in_fence:
|
|
908
|
+
continue
|
|
909
|
+
|
|
910
|
+
heading2 = H2_RE.match(line)
|
|
911
|
+
if heading2:
|
|
912
|
+
h2 = heading2.group(1)
|
|
913
|
+
h3 = None
|
|
914
|
+
if h2 == "Findings":
|
|
915
|
+
has_findings = True
|
|
916
|
+
elif h2 == "Fix Execution Plan":
|
|
917
|
+
has_plan = True
|
|
918
|
+
continue
|
|
919
|
+
|
|
920
|
+
heading3 = H3_RE.match(line)
|
|
921
|
+
if heading3:
|
|
922
|
+
h3 = heading3.group(1)
|
|
923
|
+
finding = FINDING_RE.match(line)
|
|
924
|
+
if finding and h2 == "Findings":
|
|
925
|
+
finding_id = finding.group(1)
|
|
926
|
+
if finding_id not in findings:
|
|
927
|
+
findings.append(finding_id)
|
|
928
|
+
continue
|
|
929
|
+
|
|
930
|
+
in_steps = h2 == "Fix Execution Plan" and h3 == "Execution Steps"
|
|
931
|
+
if in_steps and STEP_RE.match(line):
|
|
932
|
+
steps += 1
|
|
933
|
+
continue
|
|
934
|
+
if in_steps and ADDRESSES_RE.match(line):
|
|
935
|
+
covered_ids.update(FINDING_ID_RE.findall(line))
|
|
936
|
+
continue
|
|
937
|
+
if h2 == "Summary" and claimed_total is None:
|
|
938
|
+
total = TOTAL_FINDINGS_RE.search(line)
|
|
939
|
+
if total:
|
|
940
|
+
claimed_total = int(total.group(1))
|
|
941
|
+
|
|
942
|
+
if not (has_findings and has_plan):
|
|
943
|
+
return PlanCoverageReport(
|
|
944
|
+
applicable=False,
|
|
945
|
+
findings=[],
|
|
946
|
+
steps=0,
|
|
947
|
+
covered=[],
|
|
948
|
+
uncovered=[],
|
|
949
|
+
claimedTotal=None,
|
|
950
|
+
actualTotal=0,
|
|
951
|
+
totalMismatch=False,
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
covered = [fid for fid in findings if fid in covered_ids]
|
|
955
|
+
uncovered = [fid for fid in findings if fid not in covered_ids]
|
|
956
|
+
actual_total = len(findings)
|
|
957
|
+
return PlanCoverageReport(
|
|
958
|
+
applicable=True,
|
|
959
|
+
findings=findings,
|
|
960
|
+
steps=steps,
|
|
961
|
+
covered=covered,
|
|
962
|
+
uncovered=uncovered,
|
|
963
|
+
claimedTotal=claimed_total,
|
|
964
|
+
actualTotal=actual_total,
|
|
965
|
+
totalMismatch=claimed_total is not None and claimed_total != actual_total,
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def run_plan_coverage(doc_path: Path) -> PlanCoverageReport:
|
|
970
|
+
"""Read and parse a findings document.
|
|
971
|
+
|
|
972
|
+
Args:
|
|
973
|
+
doc_path: The FINDINGS_DOC argument.
|
|
974
|
+
|
|
975
|
+
Returns:
|
|
976
|
+
The PlanCoverageReport from parse_findings_doc().
|
|
977
|
+
|
|
978
|
+
Raises:
|
|
979
|
+
UsageError: The path does not exist, is a directory, is unreadable, or
|
|
980
|
+
is not valid UTF-8 — exit 2. An unreadable path is an error,
|
|
981
|
+
while a readable-but-unrecognizable document is `applicable: false`.
|
|
982
|
+
"""
|
|
983
|
+
try:
|
|
984
|
+
text = doc_path.read_text(encoding="utf-8")
|
|
985
|
+
except OSError as exc:
|
|
986
|
+
detail = exc.strerror or str(exc)
|
|
987
|
+
raise UsageError(
|
|
988
|
+
f"cannot read findings document: {doc_path} ({detail})"
|
|
989
|
+
) from exc
|
|
990
|
+
except UnicodeDecodeError as exc:
|
|
991
|
+
raise UsageError(
|
|
992
|
+
f"cannot read findings document: {doc_path} (not valid UTF-8)"
|
|
993
|
+
) from exc
|
|
994
|
+
return parse_findings_doc(text)
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
998
|
+
# Rendering.
|
|
999
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def render_sweep(report: SweepReport, json_output: bool) -> None:
|
|
1003
|
+
"""Render a SweepReport to stdout.
|
|
1004
|
+
|
|
1005
|
+
With `--json`, stdout carries exactly one JSON object and no human lines.
|
|
1006
|
+
Otherwise the report is rendered in the check-spec-purity.py reporting
|
|
1007
|
+
style: a verdict line, then one indented row per hit. A skip always
|
|
1008
|
+
produces a visible line, so it is never silent on any surface
|
|
1009
|
+
(REQ-SWEEP-07).
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
report: The payload from run_sweep().
|
|
1013
|
+
json_output: True when --json was passed.
|
|
1014
|
+
"""
|
|
1015
|
+
if json_output:
|
|
1016
|
+
print(json.dumps(report, indent=2))
|
|
1017
|
+
return
|
|
1018
|
+
|
|
1019
|
+
if report["skipped"]:
|
|
1020
|
+
print(f"sweep: SKIPPED — no git delta ({report['reason']})")
|
|
1021
|
+
return
|
|
1022
|
+
|
|
1023
|
+
needle_count = len(report["needles"])
|
|
1024
|
+
files = report["filesScanned"]
|
|
1025
|
+
hits = report["hits"]
|
|
1026
|
+
if not hits:
|
|
1027
|
+
dropped = report["droppedNeedles"]
|
|
1028
|
+
print(
|
|
1029
|
+
f"sweep: PASS — 0 survivor(s) in {files} file(s) "
|
|
1030
|
+
f"({needle_count} needle(s), {dropped['belowFloor']} below floor, "
|
|
1031
|
+
f"{dropped['reflowSuppressed']} reflowed)."
|
|
1032
|
+
)
|
|
1033
|
+
return
|
|
1034
|
+
|
|
1035
|
+
print(
|
|
1036
|
+
f"sweep: FAIL — {len(hits)} survivor(s) in {files} file(s) "
|
|
1037
|
+
f"({needle_count} needle(s)):"
|
|
1038
|
+
)
|
|
1039
|
+
for hit in hits:
|
|
1040
|
+
text = hit["needle"].strip()
|
|
1041
|
+
print(
|
|
1042
|
+
f" {hit['file']}:{hit['line']}: survivor of \"{text}\" "
|
|
1043
|
+
f"(removed at {hit['sourceFile']}:{hit['sourceLine']})"
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def render_plan_coverage(report: PlanCoverageReport, json_output: bool) -> None:
|
|
1048
|
+
"""Render a PlanCoverageReport to stdout.
|
|
1049
|
+
|
|
1050
|
+
Both FAIL lines are printed when a document is both uncovered and
|
|
1051
|
+
mismatched.
|
|
1052
|
+
|
|
1053
|
+
Args:
|
|
1054
|
+
report: The payload from run_plan_coverage().
|
|
1055
|
+
json_output: True when --json was passed.
|
|
1056
|
+
"""
|
|
1057
|
+
if json_output:
|
|
1058
|
+
print(json.dumps(report, indent=2))
|
|
1059
|
+
return
|
|
1060
|
+
|
|
1061
|
+
if not report["applicable"]:
|
|
1062
|
+
print(
|
|
1063
|
+
"plan-coverage: NOT APPLICABLE — no `## Findings` and/or "
|
|
1064
|
+
"`## Fix Execution Plan` section."
|
|
1065
|
+
)
|
|
1066
|
+
return
|
|
1067
|
+
|
|
1068
|
+
uncovered = report["uncovered"]
|
|
1069
|
+
if not uncovered and not report["totalMismatch"]:
|
|
1070
|
+
print(
|
|
1071
|
+
f"plan-coverage: PASS — {len(report['findings'])} finding(s), "
|
|
1072
|
+
f"{report['steps']} step(s), all covered."
|
|
1073
|
+
)
|
|
1074
|
+
return
|
|
1075
|
+
|
|
1076
|
+
if uncovered:
|
|
1077
|
+
print(f"plan-coverage: FAIL — {len(uncovered)} uncovered finding(s):")
|
|
1078
|
+
for finding_id in uncovered:
|
|
1079
|
+
print(
|
|
1080
|
+
f" {finding_id}: named in no execution step's "
|
|
1081
|
+
f"**Addresses:** field"
|
|
1082
|
+
)
|
|
1083
|
+
if report["totalMismatch"]:
|
|
1084
|
+
print(
|
|
1085
|
+
f"plan-coverage: FAIL — claimed {report['claimedTotal']}, actual "
|
|
1086
|
+
f"{report['actualTotal']} (`## Summary` total disagrees with "
|
|
1087
|
+
f"`### V-NNN:` count)"
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1092
|
+
# CLI.
|
|
1093
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
1097
|
+
"""Build the parser with one subparser per subcommand (epic-manifest.py idiom).
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
The configured parser. `--json` is stored as `json_output` on both
|
|
1101
|
+
subcommands so `main()` can read it uniformly.
|
|
1102
|
+
"""
|
|
1103
|
+
parser = argparse.ArgumentParser(prog="fix-sweep.py", description=__doc__)
|
|
1104
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
1105
|
+
|
|
1106
|
+
p_sweep = sub.add_parser("sweep", help="Report survivors of corrected text")
|
|
1107
|
+
p_sweep.add_argument(
|
|
1108
|
+
"--repo-root",
|
|
1109
|
+
default=".",
|
|
1110
|
+
help="Directory inside the repository to sweep (default: cwd). The "
|
|
1111
|
+
"repository top level is resolved from it.",
|
|
1112
|
+
)
|
|
1113
|
+
p_sweep.add_argument(
|
|
1114
|
+
"--exclude",
|
|
1115
|
+
action="append",
|
|
1116
|
+
default=[],
|
|
1117
|
+
metavar="PREFIX",
|
|
1118
|
+
help="Additional repo-relative path prefix to exclude (repeatable).",
|
|
1119
|
+
)
|
|
1120
|
+
p_sweep.add_argument(
|
|
1121
|
+
"--min-chars",
|
|
1122
|
+
type=int,
|
|
1123
|
+
default=MIN_NEEDLE_CHARS,
|
|
1124
|
+
metavar="N",
|
|
1125
|
+
help=f"Minimum normalized needle length (default: {MIN_NEEDLE_CHARS}).",
|
|
1126
|
+
)
|
|
1127
|
+
p_sweep.add_argument(
|
|
1128
|
+
"--json", action="store_true", dest="json_output", help="Output as JSON"
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
p_plan = sub.add_parser(
|
|
1132
|
+
"plan-coverage", help="Assert Fix Execution Plan coverage of the findings"
|
|
1133
|
+
)
|
|
1134
|
+
p_plan.add_argument(
|
|
1135
|
+
"findings_doc",
|
|
1136
|
+
metavar="FINDINGS_DOC",
|
|
1137
|
+
help="Path to a verification findings document",
|
|
1138
|
+
)
|
|
1139
|
+
p_plan.add_argument(
|
|
1140
|
+
"--json", action="store_true", dest="json_output", help="Output as JSON"
|
|
1141
|
+
)
|
|
1142
|
+
return parser
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def main() -> int:
|
|
1146
|
+
"""Parse arguments, dispatch, and map exceptions to exit codes.
|
|
1147
|
+
|
|
1148
|
+
Returns:
|
|
1149
|
+
0, 1, or 2 per the exit-code table in the module docstring.
|
|
1150
|
+
"""
|
|
1151
|
+
args = _build_parser().parse_args()
|
|
1152
|
+
try:
|
|
1153
|
+
if args.cmd == "sweep":
|
|
1154
|
+
if args.min_chars < 1:
|
|
1155
|
+
raise UsageError("--min-chars must be >= 1")
|
|
1156
|
+
for prefix in args.exclude:
|
|
1157
|
+
if not prefix.strip():
|
|
1158
|
+
raise UsageError("--exclude requires a non-empty path prefix")
|
|
1159
|
+
report = run_sweep(
|
|
1160
|
+
start_dir=Path(args.repo_root),
|
|
1161
|
+
user_excludes=list(args.exclude),
|
|
1162
|
+
min_chars=args.min_chars,
|
|
1163
|
+
)
|
|
1164
|
+
render_sweep(report, args.json_output)
|
|
1165
|
+
return 1 if report["hits"] else 0
|
|
1166
|
+
if args.cmd == "plan-coverage":
|
|
1167
|
+
plan = run_plan_coverage(Path(args.findings_doc))
|
|
1168
|
+
render_plan_coverage(plan, args.json_output)
|
|
1169
|
+
return 1 if (plan["uncovered"] or plan["totalMismatch"]) else 0
|
|
1170
|
+
raise UsageError(f"unknown command: {args.cmd}")
|
|
1171
|
+
except UsageError as exc:
|
|
1172
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
1173
|
+
return 2
|
|
1174
|
+
except OSError as exc:
|
|
1175
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
1176
|
+
return 2
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
if __name__ == "__main__":
|
|
1180
|
+
sys.exit(main())
|