pi-dev-team 0.2.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/LICENSE +21 -0
- package/PORTING.md +134 -0
- package/README.md +207 -0
- package/UPSTREAM.json +64 -0
- package/agents/Explore.md +15 -0
- package/agents/a11y-review.md +118 -0
- package/agents/adr-author.md +70 -0
- package/agents/ai-provenance-review.md +120 -0
- package/agents/angular-reactivity-review.md +95 -0
- package/agents/arch-review.md +135 -0
- package/agents/architect.md +78 -0
- package/agents/autoship-batch-proposer.md +69 -0
- package/agents/claude-setup-review.md +136 -0
- package/agents/codebase-recon.md +184 -0
- package/agents/component-architecture-review.md +119 -0
- package/agents/concurrency-review.md +109 -0
- package/agents/correctness-review.md +290 -0
- package/agents/data-flow-tracer.md +120 -0
- package/agents/doc-review.md +165 -0
- package/agents/domain-review.md +136 -0
- package/agents/general-purpose.md +10 -0
- package/agents/gherkin-quality-critic.md +113 -0
- package/agents/js-fp-review.md +114 -0
- package/agents/mutation-kill.md +684 -0
- package/agents/naming-review.md +142 -0
- package/agents/orchestrator.md +339 -0
- package/agents/performance-review.md +105 -0
- package/agents/plan-review-acceptance.md +115 -0
- package/agents/plan-review-design.md +90 -0
- package/agents/plan-review-parallelization.md +84 -0
- package/agents/plan-review-strategic.md +96 -0
- package/agents/plan-review-ux.md +110 -0
- package/agents/platform-engineer.md +64 -0
- package/agents/product-manager.md +68 -0
- package/agents/progress-guardian.md +79 -0
- package/agents/qa-engineer.md +289 -0
- package/agents/quality-reviewer.md +132 -0
- package/agents/react-reactivity-review.md +102 -0
- package/agents/refactor-opportunity-review.md +128 -0
- package/agents/security-engineer.md +60 -0
- package/agents/security-review.md +218 -0
- package/agents/session-analysis.md +95 -0
- package/agents/software-engineer.md +105 -0
- package/agents/spec-compliance-review.md +100 -0
- package/agents/spec-reviewer.md +114 -0
- package/agents/structure-review.md +146 -0
- package/agents/tech-writer.md +84 -0
- package/agents/test-review.md +246 -0
- package/agents/test-smell-review.md +188 -0
- package/agents/token-efficiency-review.md +139 -0
- package/agents/ui-ux-designer.md +54 -0
- package/agents/vue-reactivity-review.md +95 -0
- package/bin/__pycache__/claudecpython-314.pyc +0 -0
- package/bin/claude +258 -0
- package/docs/upstream/.pages +1 -0
- package/docs/upstream/CHANGELOG.md +2586 -0
- package/docs/upstream/README.md +155 -0
- package/docs/upstream/agent-architecture.md +214 -0
- package/docs/upstream/agent_info.md +187 -0
- package/docs/upstream/artifact-migration.md +124 -0
- package/docs/upstream/code-intelligence-nudge.md +149 -0
- package/docs/upstream/code-review-process.md +294 -0
- package/docs/upstream/concurrent-use.md +73 -0
- package/docs/upstream/context-management.md +111 -0
- package/docs/upstream/developer-notes.md +280 -0
- package/docs/upstream/diagrams/architecture-overview.svg +101 -0
- package/docs/upstream/diagrams/review-dispatch.svg +139 -0
- package/docs/upstream/diagrams/team-agents.svg +128 -0
- package/docs/upstream/diagrams/test-improve-flow.svg +166 -0
- package/docs/upstream/diagrams/workflow-linear.svg +66 -0
- package/docs/upstream/diagrams/workflow-three-phase.svg +200 -0
- package/docs/upstream/eval-maintenance.md +95 -0
- package/docs/upstream/eval-running-guide.md +147 -0
- package/docs/upstream/eval-system.md +291 -0
- package/docs/upstream/session-review-oss-complements.md +75 -0
- package/docs/upstream/session-review.md +212 -0
- package/docs/upstream/skills.md +188 -0
- package/docs/upstream/team-structure.md +21 -0
- package/docs/upstream/telemetry-ci-access.md +129 -0
- package/docs/upstream/telemetry-repo-security.md +120 -0
- package/docs/upstream/test-evaluation.md +277 -0
- package/docs/upstream/test-improve.md +154 -0
- package/docs/upstream/triage-workflow.md +282 -0
- package/docs/upstream/workflows.md +289 -0
- package/extensions/dev-team/index.ts +539 -0
- package/extensions/dev-team/lib/agents.ts +272 -0
- package/extensions/dev-team/lib/ai-credits.ts +92 -0
- package/extensions/dev-team/lib/autocompact.ts +81 -0
- package/extensions/dev-team/lib/child-run.ts +102 -0
- package/extensions/dev-team/lib/config.ts +236 -0
- package/extensions/dev-team/lib/gh-command.ts +103 -0
- package/extensions/dev-team/lib/github-style.ts +307 -0
- package/extensions/dev-team/lib/hooks.ts +350 -0
- package/extensions/dev-team/lib/metrics.ts +115 -0
- package/extensions/dev-team/lib/safe-read.ts +49 -0
- package/extensions/dev-team/lib/session-files.ts +57 -0
- package/extensions/dev-team/lib/session-spend.ts +123 -0
- package/extensions/dev-team/lib/shell-scan.ts +205 -0
- package/extensions/dev-team/lib/skills.ts +213 -0
- package/extensions/dev-team/lib/subagent-render.ts +245 -0
- package/extensions/dev-team/lib/subagent-types.ts +164 -0
- package/extensions/dev-team/lib/subagent.ts +596 -0
- package/extensions/dev-team/lib/terminal-text.ts +54 -0
- package/extensions/dev-team/lib/tools-misc.ts +152 -0
- package/extensions/dev-team/lib/transcript.ts +110 -0
- package/extensions/dev-team/lib/trust.ts +52 -0
- package/extensions/dev-team/lib/usage-breakdown.ts +176 -0
- package/extensions/dev-team/lib/usage-chart.ts +153 -0
- package/extensions/dev-team/lib/usage-command.ts +107 -0
- package/extensions/dev-team/lib/usage-history.ts +203 -0
- package/extensions/dev-team/lib/usage-render.ts +225 -0
- package/extensions/dev-team/lib/usage-split-bar.ts +127 -0
- package/extensions/dev-team/lib/usage-state.ts +116 -0
- package/extensions/dev-team/lib/usage-text.ts +159 -0
- package/extensions/dev-team/lib/usage-view.ts +109 -0
- package/hooks/__pycache__/refactor_test_freeze_guard.cpython-314.pyc +0 -0
- package/hooks/agent_dispatch_ledger.py +190 -0
- package/hooks/autocompact_setup_nudge.py +99 -0
- package/hooks/bash_retry_guard.py +228 -0
- package/hooks/boundary_events_write_guard.py +352 -0
- package/hooks/code_intelligence_nudge.py +293 -0
- package/hooks/code_intelligence_turn_mark.py +317 -0
- package/hooks/codegraph_bootstrap.py +139 -0
- package/hooks/contract_version_guard.py +362 -0
- package/hooks/cost_meter.py +106 -0
- package/hooks/destructive-commands.json +62 -0
- package/hooks/destructive_guard.py +477 -0
- package/hooks/eval_compliance_check.py +440 -0
- package/hooks/guards.json +17 -0
- package/hooks/hooks.json +323 -0
- package/hooks/internal_double_gate.py +296 -0
- package/hooks/js_fp_review.py +212 -0
- package/hooks/knowledge_index.py +119 -0
- package/hooks/lib/__pycache__/artifact_paths.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/atomic_state.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/autocompact_config.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/boundary_events.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/doc_classification.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/gh_pr_create_detect.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/git_safe_diff.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/instrument_log.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/metrics_query.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/plugin_version.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/pre_commit_doc_classifier.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/review_agent_registry.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/review_gate_corroboration.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/review_gate_hash.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/review_verdicts.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/stdin_json.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/stryker_invocation.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/telemetry_consent.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/test_file_classify.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/token_efficiency_limits.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/verify_guard_state.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/xunit_v3_operator_gate.cpython-314.pyc +0 -0
- package/hooks/lib/agent_skill_hints.py +74 -0
- package/hooks/lib/artifact_paths.py +263 -0
- package/hooks/lib/atomic_state.py +557 -0
- package/hooks/lib/autocompact_config.py +103 -0
- package/hooks/lib/autoship_log.py +106 -0
- package/hooks/lib/banned_scripts_policy.py +51 -0
- package/hooks/lib/boundary_events.py +436 -0
- package/hooks/lib/build_knowledge_index.py +504 -0
- package/hooks/lib/build_skills_index.py +361 -0
- package/hooks/lib/build_state.py +116 -0
- package/hooks/lib/classify_ship_outcome.py +126 -0
- package/hooks/lib/config_changelog_schema.py +115 -0
- package/hooks/lib/cost_meter.py +955 -0
- package/hooks/lib/doc_classification.py +116 -0
- package/hooks/lib/gh_pr_create_detect.py +136 -0
- package/hooks/lib/git_safe_diff.py +123 -0
- package/hooks/lib/instrument_log.py +66 -0
- package/hooks/lib/iteration_journal_gate.py +197 -0
- package/hooks/lib/knowledge_index_paths.py +88 -0
- package/hooks/lib/mcp_json_repowise.py +177 -0
- package/hooks/lib/metrics_query.py +202 -0
- package/hooks/lib/minimal_yaml.py +434 -0
- package/hooks/lib/plugin_version.py +142 -0
- package/hooks/lib/pre_commit_detect.py +537 -0
- package/hooks/lib/pre_commit_doc_classifier.py +126 -0
- package/hooks/lib/pricing.py +118 -0
- package/hooks/lib/report_pdf.py +371 -0
- package/hooks/lib/review_agent_registry.py +142 -0
- package/hooks/lib/review_dispatch_ledger.py +101 -0
- package/hooks/lib/review_gate_corroboration.py +521 -0
- package/hooks/lib/review_gate_hash.py +252 -0
- package/hooks/lib/review_gate_normalized_hash.py +1115 -0
- package/hooks/lib/review_verdicts.py +301 -0
- package/hooks/lib/run_report.py +160 -0
- package/hooks/lib/skill_categories.yaml +125 -0
- package/hooks/lib/stdin_json.py +57 -0
- package/hooks/lib/stryker_invocation.py +102 -0
- package/hooks/lib/telemetry_consent.py +41 -0
- package/hooks/lib/telemetry_report.py +108 -0
- package/hooks/lib/test_file_classify.py +160 -0
- package/hooks/lib/token_efficiency_limits.py +51 -0
- package/hooks/lib/turn_identity.py +77 -0
- package/hooks/lib/verify_guard_state.py +110 -0
- package/hooks/lib/workflow_state.py +206 -0
- package/hooks/lib/xunit_v3_operator_gate.py +596 -0
- package/hooks/mcp_json_repowise_nudge.py +74 -0
- package/hooks/mutation_adapters/__init__.py +7 -0
- package/hooks/mutation_adapters/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/mutation_adapters/__pycache__/lib.cpython-314.pyc +0 -0
- package/hooks/mutation_adapters/__pycache__/mutmut.cpython-314.pyc +0 -0
- package/hooks/mutation_adapters/__pycache__/pitest.cpython-314.pyc +0 -0
- package/hooks/mutation_adapters/__pycache__/stryker.cpython-314.pyc +0 -0
- package/hooks/mutation_adapters/__pycache__/stryker_net.cpython-314.pyc +0 -0
- package/hooks/mutation_adapters/lib.py +478 -0
- package/hooks/mutation_adapters/mutmut.py +188 -0
- package/hooks/mutation_adapters/pitest.py +266 -0
- package/hooks/mutation_adapters/stryker.py +157 -0
- package/hooks/mutation_adapters/stryker_net.py +264 -0
- package/hooks/mutation_gate.py +193 -0
- package/hooks/mutation_testing_smoke_gate.py +371 -0
- package/hooks/pending_review_notify.py +121 -0
- package/hooks/phase_marker.py +138 -0
- package/hooks/post_compact_state_reinject.py +180 -0
- package/hooks/post_format.py +115 -0
- package/hooks/pre_commit_knowledge_index.py +128 -0
- package/hooks/pre_commit_review.py +66 -0
- package/hooks/pre_pr_review.py +694 -0
- package/hooks/pre_tool_guard.py +405 -0
- package/hooks/py.sh +73 -0
- package/hooks/refactor-bash-write-patterns.json +29 -0
- package/hooks/refactor_test_bash_guard.py +253 -0
- package/hooks/refactor_test_freeze_guard.py +139 -0
- package/hooks/refactor_test_revert_guard.py +186 -0
- package/hooks/repo_review_nudge.py +287 -0
- package/hooks/review_verdict_recorder.py +464 -0
- package/hooks/scan_bash_command_for_banned_scripts.py +428 -0
- package/hooks/scan_worktree_for_banned_scripts.py +238 -0
- package/hooks/session_learning_trigger.py +248 -0
- package/hooks/skills_index.py +126 -0
- package/hooks/stryker_xunit_shim_guard.py +571 -0
- package/hooks/subagent_completion_guard.py +309 -0
- package/hooks/subagent_skill_context.py +139 -0
- package/hooks/task_completion_metrics.py +216 -0
- package/hooks/tdd_guard.py +229 -0
- package/hooks/telemetry.py +341 -0
- package/hooks/token_efficiency_review.py +194 -0
- package/hooks/verify_guard.py +183 -0
- package/hooks/verify_guard_edit_marker.py +73 -0
- package/hooks/version_check.py +173 -0
- package/knowledge/accepted-risks-schema.md +98 -0
- package/knowledge/adr-decision-criteria.md +64 -0
- package/knowledge/adversarial-review-protocol.md +139 -0
- package/knowledge/agent-registry.md +228 -0
- package/knowledge/agent-review-methodology.md +80 -0
- package/knowledge/ai-friendly-repo-guidelines.md +67 -0
- package/knowledge/architecture-assessment.md +96 -0
- package/knowledge/artifact-lifecycle.md +57 -0
- package/knowledge/cd-maturity-model.md +82 -0
- package/knowledge/cd-test-architecture.md +190 -0
- package/knowledge/ci-cd-file-scope.md +24 -0
- package/knowledge/codegraph-vs-graphify.md +192 -0
- package/knowledge/component-test-patterns.md +139 -0
- package/knowledge/database-change-management.md +80 -0
- package/knowledge/database-test-patterns.md +79 -0
- package/knowledge/decision-defaults.md +88 -0
- package/knowledge/dependency-breaking-techniques.md +116 -0
- package/knowledge/deployment-pipeline.md +86 -0
- package/knowledge/design-smells.md +122 -0
- package/knowledge/directory-enumeration.md +38 -0
- package/knowledge/domain-modeling.md +123 -0
- package/knowledge/evidence-bundle.md +90 -0
- package/knowledge/exploratory-testing-field-guide.md +122 -0
- package/knowledge/failure-routing.md +28 -0
- package/knowledge/fixture-construction.md +56 -0
- package/knowledge/frontend-component-architecture.md +139 -0
- package/knowledge/gherkin-quality-review-dispatch.md +135 -0
- package/knowledge/index.json +6766 -0
- package/knowledge/internal-collaborator-doubling.md +101 -0
- package/knowledge/legacy-test-strategy.md +71 -0
- package/knowledge/long-run-waiting.md +66 -0
- package/knowledge/microservice-testing.md +71 -0
- package/knowledge/model-pricing.json +23 -0
- package/knowledge/mutation-score-formulas.md +60 -0
- package/knowledge/object-calisthenics.md +147 -0
- package/knowledge/oracle-provenance.md +94 -0
- package/knowledge/orchestrator-script-implementation.md +185 -0
- package/knowledge/owasp-detection.md +148 -0
- package/knowledge/plan-review-rubric.md +56 -0
- package/knowledge/proxy-connectivity.md +62 -0
- package/knowledge/reactive-effect-patterns.md +73 -0
- package/knowledge/recon-inventory-excludes.txt +32 -0
- package/knowledge/references/bdd-value-guide.md +61 -0
- package/knowledge/references/csharp-http-client-testing.md +264 -0
- package/knowledge/release-strategies.md +74 -0
- package/knowledge/report-output-location.md +117 -0
- package/knowledge/report-pdf-integration.md +63 -0
- package/knowledge/report-print.css +129 -0
- package/knowledge/report-template.md +114 -0
- package/knowledge/report-to-pdf.md +69 -0
- package/knowledge/request-processing-flow.md +63 -0
- package/knowledge/result-verification.md +52 -0
- package/knowledge/review-agent-output-contract.md +121 -0
- package/knowledge/review-lens-classification.md +113 -0
- package/knowledge/review-rubric.md +62 -0
- package/knowledge/review-template.md +104 -0
- package/knowledge/rule-fixtures/A02.insecure-random-js/negative.js +1 -0
- package/knowledge/rule-fixtures/A02.insecure-random-js/positive.js +1 -0
- package/knowledge/rule-fixtures/A02.weak-hashing-md5/negative.py +1 -0
- package/knowledge/rule-fixtures/A02.weak-hashing-md5/positive.py +1 -0
- package/knowledge/rule-fixtures/A03.command-injection/negative.js +1 -0
- package/knowledge/rule-fixtures/A03.command-injection/positive.js +1 -0
- package/knowledge/rule-fixtures/A03.sql-injection/negative.js +1 -0
- package/knowledge/rule-fixtures/A03.sql-injection/positive.js +1 -0
- package/knowledge/rule-fixtures/A03.xss-innerhtml/negative.js +1 -0
- package/knowledge/rule-fixtures/A03.xss-innerhtml/positive.js +1 -0
- package/knowledge/rule-fixtures/A05.cors-wildcard/negative.js +1 -0
- package/knowledge/rule-fixtures/A05.cors-wildcard/positive.js +1 -0
- package/knowledge/rule-fixtures/A05.default-credentials/negative.js +1 -0
- package/knowledge/rule-fixtures/A05.default-credentials/positive.js +1 -0
- package/knowledge/rule-fixtures/A07.jwt-alg-none/negative.js +1 -0
- package/knowledge/rule-fixtures/A07.jwt-alg-none/positive.js +1 -0
- package/knowledge/rule-fixtures/A08.binary-formatter/negative.cs +1 -0
- package/knowledge/rule-fixtures/A08.binary-formatter/positive.cs +1 -0
- package/knowledge/rule-fixtures/A08.js-eval/negative.js +1 -0
- package/knowledge/rule-fixtures/A08.js-eval/positive.js +1 -0
- package/knowledge/rule-fixtures/A08.object-input-stream/negative.java +1 -0
- package/knowledge/rule-fixtures/A08.object-input-stream/positive.java +1 -0
- package/knowledge/schemas/disposition-register-v1.json +65 -0
- package/knowledge/schemas/recon-envelope-v1.json +198 -0
- package/knowledge/schemas/unified-finding-v1.json +72 -0
- package/knowledge/security-primitives-contract.md +301 -0
- package/knowledge/security-review-rule-map.yaml +107 -0
- package/knowledge/skills-registry.md +72 -0
- package/knowledge/task-size-classifier.md +103 -0
- package/knowledge/telemetry-schema.md +881 -0
- package/knowledge/test-automation-maturity.md +56 -0
- package/knowledge/test-automation-principles.md +71 -0
- package/knowledge/test-cadence-tradeoffs.md +68 -0
- package/knowledge/test-doubles.md +105 -0
- package/knowledge/test-file-indicators.md +22 -0
- package/knowledge/test-layer-gates.md +35 -0
- package/knowledge/test-matrix-examples/django-batch.md +24 -0
- package/knowledge/test-matrix-examples/dotnet-grpc-fronting-api.md +90 -0
- package/knowledge/test-matrix-examples/dotnet-http-consumer.md +131 -0
- package/knowledge/test-matrix-examples/react-node-spa.md +24 -0
- package/knowledge/test-matrix-examples/spring-boot-service.md +25 -0
- package/knowledge/test-matrix-examples/ssr-htmx.md +24 -0
- package/knowledge/test-organization.md +70 -0
- package/knowledge/test-pyramid.md +84 -0
- package/knowledge/test-refactoring.md +67 -0
- package/knowledge/test-review-division-of-labor.md +85 -0
- package/knowledge/test-smells.md +80 -0
- package/knowledge/test-stack-profiles/bdd-frameworks.md +235 -0
- package/knowledge/test-stack-profiles/django.md +13 -0
- package/knowledge/test-stack-profiles/dotnet.md +18 -0
- package/knowledge/test-stack-profiles/go.md +16 -0
- package/knowledge/test-stack-profiles/node.md +16 -0
- package/knowledge/test-stack-profiles/react.md +12 -0
- package/knowledge/test-stack-profiles/spring-boot.md +16 -0
- package/knowledge/test-stack-profiles/ssr-htmx.md +14 -0
- package/knowledge/test-stack-profiles/vue.md +12 -0
- package/knowledge/test-strategy.md +70 -0
- package/knowledge/testability-patterns.md +240 -0
- package/knowledge/testing-quadrants.md +44 -0
- package/knowledge/testing-techniques/approval.md +15 -0
- package/knowledge/testing-techniques/chaos.md +17 -0
- package/knowledge/testing-techniques/fuzz.md +15 -0
- package/knowledge/testing-techniques/property-based.md +15 -0
- package/knowledge/testing-techniques/schema-validation.md +15 -0
- package/knowledge/testing-techniques/screenshot.md +15 -0
- package/knowledge/three-phase-workflow.md +198 -0
- package/knowledge/value-patterns.md +55 -0
- package/knowledge/verification-mode.md +116 -0
- package/knowledge/virtual-service-libraries.md +75 -0
- package/knowledge/wave-consolidation-guidance.md +21 -0
- package/overrides/agents/Explore.md +15 -0
- package/overrides/agents/general-purpose.md +10 -0
- package/overrides/notes/autoship.md +6 -0
- package/overrides/notes/issues-from-assessment.md +3 -0
- package/overrides/notes/issues-from-plan.md +3 -0
- package/overrides/notes/mutation-night-watch.md +3 -0
- package/overrides/notes/mutation-testing.md +3 -0
- package/overrides/notes/pr.md +7 -0
- package/overrides/notes/project-init.md +6 -0
- package/overrides/notes/setup.md +13 -0
- package/overrides/notes/specs.md +3 -0
- package/overrides/skills/headless-run/SKILL.md +45 -0
- package/overrides/skills/upgrade/SKILL.md +30 -0
- package/overrides/skills/version/SKILL.md +25 -0
- package/package.json +36 -0
- package/scripts/authoring_digest.py +93 -0
- package/scripts/autoship_discover.py +121 -0
- package/scripts/autoship_group.py +409 -0
- package/scripts/autoship_proposals.py +494 -0
- package/scripts/autoship_queue.py +291 -0
- package/scripts/autoship_reclaim.py +495 -0
- package/scripts/build_jobs.py +108 -0
- package/scripts/build_rollback_point.py +240 -0
- package/scripts/build_slice_scope.py +157 -0
- package/scripts/build_wave.py +109 -0
- package/scripts/build_wave_reconcile.py +252 -0
- package/scripts/build_worktree_baseref.py +113 -0
- package/scripts/check_agent_scope.py +117 -0
- package/scripts/check_agent_tool_mapping.py +213 -0
- package/scripts/check_review_agent_mcp_tools.py +317 -0
- package/scripts/check_security_assessment_mcp_tools.py +165 -0
- package/scripts/checkpoint_abort.py +502 -0
- package/scripts/claude_setup_review.py +438 -0
- package/scripts/codebase_recon.py +556 -0
- package/scripts/coverage_config.py +623 -0
- package/scripts/coverage_delta_steering.py +330 -0
- package/scripts/coverage_discovery_dotnet.py +315 -0
- package/scripts/coverage_discovery_java.py +742 -0
- package/scripts/coverage_discovery_js.py +546 -0
- package/scripts/coverage_gap_ranking.py +556 -0
- package/scripts/coverage_readiness.py +455 -0
- package/scripts/coverage_report_parse.py +521 -0
- package/scripts/detect_bdd_convention.py +252 -0
- package/scripts/eval_ablation.py +376 -0
- package/scripts/gherkin_analysis_coverage_gate.py +306 -0
- package/scripts/gherkin_cross_feature_duplicate_titles_gate.py +173 -0
- package/scripts/gherkin_effectiveness_rollup.py +238 -0
- package/scripts/gherkin_failure_path_gate.py +206 -0
- package/scripts/gherkin_feature_merge.py +720 -0
- package/scripts/gherkin_stub_gate.py +163 -0
- package/scripts/gherkin_stub_merge.py +479 -0
- package/scripts/git_origin_host.py +88 -0
- package/scripts/install-java-static-analysis.py +110 -0
- package/scripts/issue_deps.py +74 -0
- package/scripts/lib/_bdd_markers.py +28 -0
- package/scripts/lib/_gherkin_text.py +93 -0
- package/scripts/lib/_vendored_tree.py +70 -0
- package/scripts/lib/autoship_state.py +397 -0
- package/scripts/lib/claude_md_guard.py +226 -0
- package/scripts/lib/deterministic_recon.py +446 -0
- package/scripts/lib/mcp_tool_grants.py +211 -0
- package/scripts/lib/plan_parse.py +386 -0
- package/scripts/lib/review_result.py +84 -0
- package/scripts/lib/review_roster.py +86 -0
- package/scripts/lib/session_log/__init__.py +34 -0
- package/scripts/lib/session_log/__pycache__/__init__.cpython-314.pyc +0 -0
- package/scripts/lib/session_log/__pycache__/records.cpython-314.pyc +0 -0
- package/scripts/lib/session_log/classify.py +231 -0
- package/scripts/lib/session_log/corrections.py +194 -0
- package/scripts/lib/session_log/discovery.py +108 -0
- package/scripts/lib/session_log/records.py +218 -0
- package/scripts/lib/session_log/redact.py +76 -0
- package/scripts/lib/session_log/signals.py +373 -0
- package/scripts/lib/session_report_downstream.py +614 -0
- package/scripts/lib/session_report_maintainer.py +1273 -0
- package/scripts/lib/session_report_shared.py +262 -0
- package/scripts/lib/settings_hook_guard.py +157 -0
- package/scripts/lib/slug.py +33 -0
- package/scripts/lib/stub_extractors/__init__.py +82 -0
- package/scripts/lib/stub_extractors/_common.py +328 -0
- package/scripts/lib/stub_extractors/csharp.py +19 -0
- package/scripts/lib/stub_extractors/go.py +173 -0
- package/scripts/lib/stub_extractors/java.py +18 -0
- package/scripts/lib/stub_extractors/jsts.py +126 -0
- package/scripts/mutation_stack_sections.py +149 -0
- package/scripts/mutation_yield_steering.py +345 -0
- package/scripts/orchestrator.py +895 -0
- package/scripts/plan_gherkin_export.py +227 -0
- package/scripts/plan_waves.py +208 -0
- package/scripts/pr_close_keyword_lint.py +108 -0
- package/scripts/progress_guardian.py +888 -0
- package/scripts/recon_inventory.py +273 -0
- package/scripts/review_findings_log.py +93 -0
- package/scripts/run_invariants.py +124 -0
- package/scripts/select_lenses.py +640 -0
- package/scripts/session_report.py +486 -0
- package/scripts/set_autocompact_env.py +221 -0
- package/scripts/ship_resume_guard.py +135 -0
- package/scripts/ship_review_gate.py +63 -0
- package/scripts/specs_convention_marker.py +103 -0
- package/scripts/test_improve_resume.py +277 -0
- package/scripts/test_review_mechanics.py +958 -0
- package/scripts/token_efficiency_review.py +322 -0
- package/scripts/verdict_scope.py +285 -0
- package/scripts/verify_gherkin_quality_critic_isolation.py +296 -0
- package/scripts/verify_tier.py +157 -0
- package/skills/adr-tools/SKILL.md +118 -0
- package/skills/agent-readiness/SKILL.md +105 -0
- package/skills/agent-readiness/ai_friendly_analyzers.py +326 -0
- package/skills/agent-readiness/scanner.py +441 -0
- package/skills/agent-readiness/scorecard.yaml +88 -0
- package/skills/api-design/SKILL.md +115 -0
- package/skills/apply-fixes/SKILL.md +171 -0
- package/skills/apply-test-doubles/SKILL.md +321 -0
- package/skills/artifact-lifecycle/SKILL.md +127 -0
- package/skills/autoship/SKILL.md +1124 -0
- package/skills/benchmark/SKILL.md +105 -0
- package/skills/branch-workflow/SKILL.md +89 -0
- package/skills/browse/SKILL.md +184 -0
- package/skills/browser-testing/SKILL.md +62 -0
- package/skills/browser-testing/references/playwright-patterns.md +216 -0
- package/skills/build/SKILL.md +422 -0
- package/skills/build/references/static-self-heal.md +245 -0
- package/skills/careful/SKILL.md +72 -0
- package/skills/cd-test-architecture/SKILL.md +371 -0
- package/skills/ci-debugging/SKILL.md +105 -0
- package/skills/co-evolution-audit/SKILL.md +269 -0
- package/skills/code-review/SKILL.md +1015 -0
- package/skills/code-review/examples/aggregated-sample.json +56 -0
- package/skills/code-review/examples/sample-report.md +41 -0
- package/skills/code-review/output-format.md +478 -0
- package/skills/code-review/scripts/activation.py +86 -0
- package/skills/code-review/scripts/change_impact.py +357 -0
- package/skills/code-review/scripts/change_shape.py +372 -0
- package/skills/code-review/scripts/change_size.py +212 -0
- package/skills/code-review/scripts/changed_file_list.py +141 -0
- package/skills/code-review/scripts/closing_pass.py +187 -0
- package/skills/code-review/scripts/consolidate.py +277 -0
- package/skills/code-review/scripts/contract_failure_report.py +185 -0
- package/skills/code-review/scripts/dispatch_reconcile.py +66 -0
- package/skills/code-review/scripts/dispatch_waves.py +164 -0
- package/skills/code-review/scripts/finding_signature.py +446 -0
- package/skills/code-review/scripts/ledger.py +283 -0
- package/skills/code-review/scripts/partition.py +169 -0
- package/skills/code-review/scripts/render_tiered_findings.py +274 -0
- package/skills/code-review/scripts/repo_invariants.py +1066 -0
- package/skills/code-review/scripts/review_context_pack.py +306 -0
- package/skills/code-review/scripts/review_round_log.py +345 -0
- package/skills/code-review/scripts/review_value_coverage.py +297 -0
- package/skills/code-review/scripts/validate_review_output.py +467 -0
- package/skills/code-review/sliced-mode.md +205 -0
- package/skills/competitive-analysis/SKILL.md +191 -0
- package/skills/context-loading-protocol/SKILL.md +157 -0
- package/skills/continue/SKILL.md +90 -0
- package/skills/cost-report/SKILL.md +178 -0
- package/skills/coverage-baseline/SKILL.md +335 -0
- package/skills/coverage-baseline/references/multi-project-discovery.md +202 -0
- package/skills/coverage-delta/SKILL.md +181 -0
- package/skills/coverage-delta/references/mutation-gate.md +70 -0
- package/skills/design-doc/SKILL.md +95 -0
- package/skills/design-interrogation/SKILL.md +89 -0
- package/skills/design-it-twice/SKILL.md +91 -0
- package/skills/docker-image-audit/SKILL.md +108 -0
- package/skills/docker-image-audit/references/install-guide.md +64 -0
- package/skills/docker-image-audit/references/report-template.md +73 -0
- package/skills/docker-image-create/SKILL.md +185 -0
- package/skills/domain-analysis/SKILL.md +183 -0
- package/skills/domain-driven-design/SKILL.md +194 -0
- package/skills/exploratory-testing/SKILL.md +108 -0
- package/skills/explore/SKILL.md +51 -0
- package/skills/farley-score/SKILL.md +165 -0
- package/skills/feature-file-validation/SKILL.md +78 -0
- package/skills/feature-file-validation/references/validation-rules.md +115 -0
- package/skills/feedback-learning/SKILL.md +414 -0
- package/skills/fix/SKILL.md +450 -0
- package/skills/freeze/SKILL.md +68 -0
- package/skills/frontend-architecture/SKILL.md +113 -0
- package/skills/gherkin-derive/SKILL.md +630 -0
- package/skills/gherkin-public/SKILL.md +266 -0
- package/skills/governance-compliance/SKILL.md +150 -0
- package/skills/guard/SKILL.md +75 -0
- package/skills/handoff/SKILL.md +139 -0
- package/skills/handoff/references/summary-templates.md +242 -0
- package/skills/harness-audit/SKILL.md +751 -0
- package/skills/harness-audit/scripts/lesson_validate.py +386 -0
- package/skills/harness-audit/scripts/redundancy_criterion.py +188 -0
- package/skills/headless-run/SKILL.md +45 -0
- package/skills/headless-run/scripts/isolated_dispatch.py +381 -0
- package/skills/help/SKILL.md +72 -0
- package/skills/hexagonal-architecture/SKILL.md +85 -0
- package/skills/human-oversight-protocol/SKILL.md +224 -0
- package/skills/issues-from-assessment/SKILL.md +223 -0
- package/skills/issues-from-plan/SKILL.md +133 -0
- package/skills/legacy-code/SKILL.md +132 -0
- package/skills/mermaid-diagramming/SKILL.md +120 -0
- package/skills/mutation-night-watch/SKILL.md +154 -0
- package/skills/mutation-night-watch/references/scheduling.md +135 -0
- package/skills/mutation-testing/SKILL.md +396 -0
- package/skills/mutation-testing/references/languages/csharp-stryker-net.md +676 -0
- package/skills/mutation-testing/references/languages/go-go-mutesting.md +95 -0
- package/skills/mutation-testing/references/languages/java-pitest.md +77 -0
- package/skills/mutation-testing/references/languages/javascript-stryker.md +188 -0
- package/skills/mutation-testing/references/languages/python-mutmut.md +97 -0
- package/skills/mutation-testing/references/time-estimation.md +34 -0
- package/skills/mutation-testing/references/tool-detection.md +15 -0
- package/skills/mutation-testing/references/workflow-callers.md +23 -0
- package/skills/mutation-testing/scripts/__pycache__/xunit_v3_feature_detector.cpython-314.pyc +0 -0
- package/skills/mutation-testing/scripts/csharp_stryker_net_slice_runner.py +635 -0
- package/skills/mutation-testing/scripts/csharp_stryker_net_status_loop.py +525 -0
- package/skills/mutation-testing/scripts/csharp_stryker_net_wrapper.py +681 -0
- package/skills/mutation-testing/scripts/mutation_baseline_reuse.py +292 -0
- package/skills/mutation-testing/scripts/mutation_exclude_policy.py +268 -0
- package/skills/mutation-testing/scripts/mutation_feasibility_gate.py +463 -0
- package/skills/mutation-testing/scripts/mutation_kill_headless.py +331 -0
- package/skills/mutation-testing/scripts/mutation_kill_insert.py +199 -0
- package/skills/mutation-testing/scripts/mutation_kill_insert_python.py +150 -0
- package/skills/mutation-testing/scripts/mutation_kill_loop.py +869 -0
- package/skills/mutation-testing/scripts/mutation_kill_loop_python.py +949 -0
- package/skills/mutation-testing/scripts/mutation_kill_retry.py +592 -0
- package/skills/mutation-testing/scripts/mutation_kill_shared.py +620 -0
- package/skills/mutation-testing/scripts/mutation_nightwatch.py +462 -0
- package/skills/mutation-testing/scripts/mutation_nightwatch_stacks.py +425 -0
- package/skills/mutation-testing/scripts/mutation_report.py +743 -0
- package/skills/mutation-testing/scripts/mutation_report_cli.py +175 -0
- package/skills/mutation-testing/scripts/mutation_safety_gate.py +69 -0
- package/skills/mutation-testing/scripts/stryker_shard_pipeline.py +847 -0
- package/skills/mutation-testing/scripts/stryker_shard_setup.py +440 -0
- package/skills/mutation-testing/scripts/stryker_timeout_retry.py +142 -0
- package/skills/mutation-testing/scripts/xunit_v3_feature_detector.py +341 -0
- package/skills/performance-benchmark/SKILL.md +174 -0
- package/skills/performance-benchmark/examples/report-format.md +43 -0
- package/skills/performance-benchmark/references/benchmark-script.md +169 -0
- package/skills/performance-metrics/SKILL.md +265 -0
- package/skills/plan/SKILL.md +199 -0
- package/skills/plan/references/gherkin-persistence.md +43 -0
- package/skills/plan/references/plan-template.md +182 -0
- package/skills/pr/SKILL.md +289 -0
- package/skills/pr/scripts/gate_retry_state.py +368 -0
- package/skills/project-init/README.md +141 -0
- package/skills/project-init/SKILL.md +1197 -0
- package/skills/project-init/evals/evals.json +200 -0
- package/skills/project-init/references/capability-tools.md +55 -0
- package/skills/project-init/references/configs.md +221 -0
- package/skills/property-based-testing/SKILL.md +121 -0
- package/skills/property-based-testing/fixtures/invariant_fixture.py +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/README.md +42 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/LICENSE +21 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/README.md +263 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/fast-check.d.ts +5165 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/fast-check.js +12147 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/package.json +3 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/types57/fast-check.d.ts +5165 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/fast-check.d.ts +5165 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/fast-check.js +12011 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/rolldown-runtime-D7D4PA-g.js +13 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/types57/fast-check.d.ts +5165 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/package.json +94 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/LICENSE +21 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/README.md +168 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/RandomGenerator-DcXj09Ch.d.ts +14 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformBigInt.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformBigInt.js +38 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat32.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat32.js +18 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat64.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat64.js +22 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformInt.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformInt.js +134 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/RandomGenerator-DcXj09Ch.d.ts +14 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.js +37 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.js +17 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.js +21 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformInt.d.ts +15 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformInt.js +133 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/congruential32.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/congruential32.js +44 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/mersenne.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/mersenne.js +90 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.js +80 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xorshift128plus.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xorshift128plus.js +78 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/package.json +3 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.d.ts +16 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.js +0 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/RandomGenerator.d.ts +2 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/RandomGenerator.js +0 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/generateN.d.ts +6 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/generateN.js +8 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/purify.d.ts +12 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/purify.js +9 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/skipN.d.ts +6 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/skipN.js +6 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/congruential32.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/congruential32.js +46 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/mersenne.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/mersenne.js +92 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xoroshiro128plus.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xoroshiro128plus.js +82 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xorshift128plus.d.ts +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xorshift128plus.js +80 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/JumpableRandomGenerator.d.ts +16 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/JumpableRandomGenerator.js +0 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/RandomGenerator.d.ts +2 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/RandomGenerator.js +0 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/generateN.d.ts +6 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/generateN.js +9 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/purify.d.ts +12 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/purify.js +10 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/skipN.d.ts +6 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/skipN.js +7 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/package.json +133 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/package-lock.json +1179 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/package.json +14 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/roundtrip.js +29 -0
- package/skills/property-based-testing/fixtures/js-roundtrip/roundtrip.properties.test.js +16 -0
- package/skills/property-based-testing/fixtures/no_property_fixture.py +10 -0
- package/skills/property-based-testing/fixtures/roundtrip_fixture.py +16 -0
- package/skills/property-based-testing/references/languages/javascript.md +54 -0
- package/skills/property-based-testing/scripts/detect_and_dispatch.py +80 -0
- package/skills/property-based-testing/scripts/hypothesis_scaffold.py +276 -0
- package/skills/proxy-resilience/SKILL.md +84 -0
- package/skills/quality-gate-pipeline/SKILL.md +184 -0
- package/skills/quality-targets-converge/SKILL.md +254 -0
- package/skills/repo-review/SKILL.md +159 -0
- package/skills/report-pdf/SKILL.md +66 -0
- package/skills/review/SKILL.md +47 -0
- package/skills/review-agent/SKILL.md +152 -0
- package/skills/review-summary/SKILL.md +73 -0
- package/skills/run-report/SKILL.md +70 -0
- package/skills/semantic-duplication-scan/SKILL.md +337 -0
- package/skills/semantic-scan/SKILL.md +53 -0
- package/skills/semgrep-analyze/SKILL.md +139 -0
- package/skills/setup/SKILL.md +1122 -0
- package/skills/ship/SKILL.md +240 -0
- package/skills/source-verification/SKILL.md +210 -0
- package/skills/source-verification/scripts/claim_extractor.py +155 -0
- package/skills/specs/.size-baseline.json +4 -0
- package/skills/specs/SKILL.md +243 -0
- package/skills/specs/references/completeness-checklist.md +83 -0
- package/skills/specs/references/extraction.md +58 -0
- package/skills/specs/references/glossary.md +59 -0
- package/skills/specs/references/persistence.md +115 -0
- package/skills/specs/references/predictability-check.md +77 -0
- package/skills/static-analysis-integration/SKILL.md +235 -0
- package/skills/static-analysis-integration/adapters/_envelope.py +26 -0
- package/skills/static-analysis-integration/adapters/jscpd-adapter.py +66 -0
- package/skills/static-analysis-integration/adapters/lizard-adapter.py +81 -0
- package/skills/static-analysis-integration/adapters/mypy-adapter.py +50 -0
- package/skills/static-analysis-integration/adapters/mypy-src-layout.py +93 -0
- package/skills/static-analysis-integration/adapters/security-review-adapter.py +212 -0
- package/skills/static-analysis-integration/maintenance.md +23 -0
- package/skills/static-analysis-integration/references/language-setup.md +228 -0
- package/skills/static-analysis-integration/references/sarif-parser.md +124 -0
- package/skills/static-analysis-integration/references/security-review-adapter.md +118 -0
- package/skills/static-analysis-integration/references/tool-configs.md +617 -0
- package/skills/static-analysis-integration/rulesets/pmd-quickstart.xml +24 -0
- package/skills/stryker-xunit-v2-shim/SKILL.md +274 -0
- package/skills/stryker-xunit-v2-shim/references/shim-howto.md +256 -0
- package/skills/stryker-xunit-v2-shim/scripts/generate_shim.py +143 -0
- package/skills/systematic-debugging/SKILL.md +130 -0
- package/skills/telemetry/SKILL.md +75 -0
- package/skills/test-audit-disable/SKILL.md +129 -0
- package/skills/test-design/SKILL.md +177 -0
- package/skills/test-design/scripts/__pycache__/internal_double_detector.cpython-314.pyc +0 -0
- package/skills/test-design/scripts/internal_double_detector.py +631 -0
- package/skills/test-design-advisor/SKILL.md +166 -0
- package/skills/test-driven-development/SKILL.md +169 -0
- package/skills/test-health/SKILL.md +262 -0
- package/skills/test-improve/SKILL.md +239 -0
- package/skills/test-improve/references/phase-0-approach-contract.md +228 -0
- package/skills/test-improve/references/phase-1-analyze.md +131 -0
- package/skills/test-improve/references/phase-2-baseline.md +121 -0
- package/skills/test-improve/references/phase-3-derive-gherkin.md +53 -0
- package/skills/test-improve/references/phase-4-plan-fixes.md +34 -0
- package/skills/test-improve/references/phase-5-improve.md +215 -0
- package/skills/test-improve/references/phase-6-refactor-decision.md +45 -0
- package/skills/test-improve/references/phase-7-refactor.md +44 -0
- package/skills/test-improve/references/phase-8-validate.md +66 -0
- package/skills/test-improve/references/phase-9-close-out-prompt.md +11 -0
- package/skills/test-improve/references/phase-9-report.md +62 -0
- package/skills/test-improve/references/review-loop.md +92 -0
- package/skills/test-improve/templates/executive-summary.md +123 -0
- package/skills/threat-modeling/SKILL.md +108 -0
- package/skills/triage/SKILL.md +211 -0
- package/skills/ubiquitous-language/SKILL.md +192 -0
- package/skills/ubiquitous-language/scripts/collect_domain_signals.py +300 -0
- package/skills/unfreeze/SKILL.md +37 -0
- package/skills/upgrade/SKILL.md +31 -0
- package/skills/upgrade/scripts/check_version_drift.py +113 -0
- package/skills/upgrade/scripts/enable_autoupdate.py +149 -0
- package/skills/version/SKILL.md +25 -0
- package/sync/__pycache__/sync_upstream.cpython-314.pyc +0 -0
- package/sync/sync_upstream.py +293 -0
- package/templates/ACCEPTED-RISKS.md.tmpl +46 -0
- package/templates/agents/agent-template.md +151 -0
- package/templates/agents/angular-testing.md +66 -0
- package/templates/agents/csharp-quality.md +63 -0
- package/templates/agents/esm-enforcer.md +52 -0
- package/templates/agents/front-end-testing.md +65 -0
- package/templates/agents/go-quality.md +65 -0
- package/templates/agents/python-quality.md +62 -0
- package/templates/agents/react-testing.md +61 -0
- package/templates/agents/ts-enforcer.md +60 -0
- package/templates/agents/twelve-factor-audit.md +49 -0
- package/tools/entropy-check.py +250 -0
- package/tools/model-hash-verify.py +213 -0
|
@@ -0,0 +1,1115 @@
|
|
|
1
|
+
"""hooks/lib/review_gate_normalized_hash.py — normalization-invariant gate
|
|
2
|
+
hash for cosmetic-delta carry-forward (#1627).
|
|
3
|
+
|
|
4
|
+
`review_gate_hash()` hashes the raw staged patch bytes, so ANY re-stage after
|
|
5
|
+
the corroborating dispatches — a whitespace fix, a markdown edit alongside
|
|
6
|
+
code — voids the evidence and forces fresh dispatches purely to re-satisfy
|
|
7
|
+
the gate. Those dispatches review nothing new; they exist only to feed the
|
|
8
|
+
ledger. #1623 §2 records 4 of the last ~5 sessions hitting
|
|
9
|
+
`dispatch-evidence-different-content`/`stale` blocks this way.
|
|
10
|
+
|
|
11
|
+
This module computes a SECOND hash, invariant under changes that provably
|
|
12
|
+
cannot alter behavior, so `pre_commit_review.py` can carry corroboration
|
|
13
|
+
forward across such a re-stage — **without** any exemption event, hook
|
|
14
|
+
bypass, or self-asserted claim.
|
|
15
|
+
|
|
16
|
+
## Why this does not reopen #1461
|
|
17
|
+
|
|
18
|
+
The exemption is a **property of content, recomputed by the hook itself at
|
|
19
|
+
gate time** from `git diff --cached`. It is not a claim written by the gated
|
|
20
|
+
party. Contrast the doc-only exemption, which already needed hook-side
|
|
21
|
+
re-derivation precisely because the ledger event is a self-assertion
|
|
22
|
+
(`pre_commit_doc_classifier.py`'s docstring) — here the re-derivation IS the
|
|
23
|
+
mechanism; there is no assertion at all. Ledger events still originate only
|
|
24
|
+
from genuine PreToolUse dispatch recording, and the forgery surface named in
|
|
25
|
+
`agent_dispatch_ledger.py`'s KNOWN RESIDUAL GAP is unchanged in kind.
|
|
26
|
+
|
|
27
|
+
Kept as a sibling module rather than folded into `review_gate_hash.py`,
|
|
28
|
+
following the `review_gate_corroboration.py` precedent named in the issue:
|
|
29
|
+
`review_gate_hash.py` is deliberately a minimal pure hash function with no
|
|
30
|
+
doc-classification knowledge, and this module's normalization needs exactly
|
|
31
|
+
that knowledge.
|
|
32
|
+
|
|
33
|
+
## What v1 normalizes — and what it deliberately does not
|
|
34
|
+
|
|
35
|
+
**Dropped:** hunks whose file is doc-classified, reusing
|
|
36
|
+
`pre_commit_doc_classifier.is_doc_only_changeset` per file — the same STRICT
|
|
37
|
+
predicate the doc-only exemption uses, including its "functional Claude-config
|
|
38
|
+
markdown is never documentation" carve-out. A "cosmetic" edit to `agents/`,
|
|
39
|
+
`skills/`, `.claude/`, `CLAUDE.md`, or any other enforcement machinery can
|
|
40
|
+
therefore never ride the carry-forward.
|
|
41
|
+
|
|
42
|
+
**Collapsed:** leading/trailing whitespace on a changed line — but only when
|
|
43
|
+
BOTH of the following hold. Each is a place where the obvious rule is
|
|
44
|
+
unsound, so each is a hard precondition, not a refinement:
|
|
45
|
+
|
|
46
|
+
1. **The file's language does not make indentation significant.** In Python,
|
|
47
|
+
YAML, Haskell, and friends, dedenting a line moves it out of its block:
|
|
48
|
+
|
|
49
|
+
for x in items: for x in items:
|
|
50
|
+
total += x total += x
|
|
51
|
+
return total return total # <- different behavior
|
|
52
|
+
|
|
53
|
+
Both `return total` lines strip to the same text. Treating that as
|
|
54
|
+
cosmetic would let a genuine control-flow change carry corroboration
|
|
55
|
+
forward. Files with an indentation-significant extension
|
|
56
|
+
(`_INDENT_SIGNIFICANT_EXTENSIONS`) therefore get **byte-exact**
|
|
57
|
+
comparison — no whitespace normalization at all. The default for an
|
|
58
|
+
unknown extension is also byte-exact: an extension this module has never
|
|
59
|
+
heard of is not one it can prove is brace-delimited.
|
|
60
|
+
|
|
61
|
+
2. **The line carries no quote character** (`"`, `'`, or a backtick).
|
|
62
|
+
Whitespace inside a string literal is data. A multi-line string's
|
|
63
|
+
indentation is part of its value, and `"a b"` vs `"a b"` is a behavior
|
|
64
|
+
change that would otherwise read as cosmetic. Without language awareness
|
|
65
|
+
the only sound language-agnostic rule is to keep every quote-bearing line
|
|
66
|
+
out of the cosmetic bucket entirely.
|
|
67
|
+
|
|
68
|
+
Interior whitespace is NEVER collapsed under any circumstances. All of this
|
|
69
|
+
errs closed: a Python reindent, or an indentation fix on a quoted line,
|
|
70
|
+
simply doesn't get carry-forward — costing one re-dispatch rather than
|
|
71
|
+
weakening the gate.
|
|
72
|
+
|
|
73
|
+
## Unquoted multi-line literal bodies (#1638, #1660, #1661, #1667)
|
|
74
|
+
|
|
75
|
+
The quote-character rule above is what keeps string DATA out of the cosmetic
|
|
76
|
+
bucket — but it only sees quote characters, and a whole family of multi-line
|
|
77
|
+
literal forms carries a data body with no quote character on its interior
|
|
78
|
+
lines: Ruby's `<<~SQL`, PHP's `<<<TXT`, Perl's `<<EOF`, Lua's `[[ ]]`,
|
|
79
|
+
PostgreSQL's `$$ $$`, Go raw strings and JS/TS template literals, C++'s
|
|
80
|
+
`R"( )"`, and PHP's inline-HTML region between `?>` and `<?`.
|
|
81
|
+
`_heredoc_body_marks` tracks each language's grammars per hunk side
|
|
82
|
+
(`_HEREDOC_GRAMMARS`, extension-keyed so PHP's three-angle-bracket form can
|
|
83
|
+
never cross-match Ruby/Perl's two-angle-bracket form) and forces every line
|
|
84
|
+
between an opener and its closer to byte-exact comparison, bypassing
|
|
85
|
+
`_canonical_line`'s collapsible/quote-char rules entirely.
|
|
86
|
+
|
|
87
|
+
Failing closed here means three things, all load-bearing:
|
|
88
|
+
|
|
89
|
+
- **An opener with no closer inside the visible hunk side marks every
|
|
90
|
+
remaining line, not just up to end-of-context.** The true close may be
|
|
91
|
+
outside what this hunk shows; the safe assumption is that it hasn't
|
|
92
|
+
closed yet, so nothing after it is eligible for whitespace collapsing.
|
|
93
|
+
- **An opener could be invisible entirely** — above the first line this
|
|
94
|
+
function ever sees. `normalized_gate_hash` narrows that gap at the source
|
|
95
|
+
by requesting `--unified=100000` context, so for any realistically-sized
|
|
96
|
+
file every hunk spans start-to-end.
|
|
97
|
+
- **That context request is a magic number, so it is checked rather than
|
|
98
|
+
trusted (#1662).** A file longer than the window still yields a hunk that
|
|
99
|
+
does not start at line 1, and an opener above it is invisible — the exact
|
|
100
|
+
hazard the grammars close, reopened silently, and failing OPEN rather than
|
|
101
|
+
closed. `normalize_patch` therefore reads the hunk's declared start lines
|
|
102
|
+
and drops any grammared file's non-line-1 hunk to byte-exact comparison
|
|
103
|
+
instead of believing the context flag did its job.
|
|
104
|
+
|
|
105
|
+
The same call is bounded in both directions (#1663): an explicit subprocess
|
|
106
|
+
timeout on the `git diff`, and a cap on the patch size accepted for
|
|
107
|
+
processing. Whole-file context means a one-line edit to a large tracked file
|
|
108
|
+
(a `.json` lockfile, a generated fixture) otherwise produces and canonicalizes
|
|
109
|
+
that entire file, several in-memory copies deep, synchronously inside a
|
|
110
|
+
PreToolUse hook on every commit.
|
|
111
|
+
|
|
112
|
+
## What the canonical form must contain to bind a subject (#1631 review)
|
|
113
|
+
|
|
114
|
+
The first draft of this module compared, per file, the flat list of removed
|
|
115
|
+
lines against the flat list of added lines, and parsed the patch by
|
|
116
|
+
dispatching on line prefixes. An adversarial pass found five distinct
|
|
117
|
+
collisions where behaviorally different changesets normalized identically,
|
|
118
|
+
each of which let unreviewed content ride a carry-forward. All five are
|
|
119
|
+
closed here, and each fix is a hard requirement rather than a refinement:
|
|
120
|
+
|
|
121
|
+
1. **Hunks are consumed by their declared line counts.** Dispatching on
|
|
122
|
+
`--- `/`+++ ` prefixes while inside a hunk is parser confusion: a REMOVED
|
|
123
|
+
source line beginning `-- ` (a SQL/Lua/Haskell comment) renders as
|
|
124
|
+
`--- ...`, was read as a file header, and silently terminated the hunk —
|
|
125
|
+
so every following line in it, including injected code, vanished from the
|
|
126
|
+
digest. An added line beginning `++ ` did the same via `+++ `. A malformed
|
|
127
|
+
or over-running hunk is a parse failure, and fails closed.
|
|
128
|
+
|
|
129
|
+
2. **The canonical form is per HUNK, comparing each hunk's whole old side
|
|
130
|
+
against its whole new side, context lines included.** Flat per-file
|
|
131
|
+
removed/added lists carry no position, so inserting the same line at two
|
|
132
|
+
different places in a file — `+audit()` before vs. after a call — produced
|
|
133
|
+
one digest. Context lines are what make an insertion point part of the
|
|
134
|
+
subject. A hunk whose two sides canonicalize identically is the true
|
|
135
|
+
definition of a cosmetic hunk, and only then is it dropped.
|
|
136
|
+
|
|
137
|
+
3. **The collapse is per hunk, never across hunks.** Whole-file
|
|
138
|
+
removed-equals-added treated a line MOVED between two hunks (a
|
|
139
|
+
`lock.acquire()` relocated across a function) as formatting, because the
|
|
140
|
+
file's removed and added lists matched.
|
|
141
|
+
|
|
142
|
+
4. **Changes carried entirely by patch metadata are recorded, not skipped.**
|
|
143
|
+
A mode flip (`chmod +x`), a rename, a binary-file replacement, and an
|
|
144
|
+
empty new file all produce a diff with NO hunk body. They were therefore
|
|
145
|
+
invisible: staging one on top of an already-corroborated changeset left
|
|
146
|
+
the digest untouched. The earlier rationale — "a mode change is real, but
|
|
147
|
+
it is carried by the raw-hash lens, which is still evaluated first and
|
|
148
|
+
still authoritative" — does not hold, because this lens runs precisely
|
|
149
|
+
when the raw lens has already rejected. Binary files bind their `index`
|
|
150
|
+
blob SHAs, which is the only content signal a textual diff exposes.
|
|
151
|
+
|
|
152
|
+
5. **An empty canonical form is never a digest.** `sha256("")` is a CONSTANT
|
|
153
|
+
shared by every fully-cosmetic changeset AND by every dispatch recorded
|
|
154
|
+
while the index was clean — so two review dispatches made before anything
|
|
155
|
+
was staged stamped exactly the value a later mode-only or rename-only
|
|
156
|
+
stage recomputes, satisfying the `>= 2` floor with evidence from agents
|
|
157
|
+
that reviewed nothing. `normalized_gate_hash` returns `None` for an empty
|
|
158
|
+
normalization, exactly as it does for a git failure and for the same
|
|
159
|
+
reason: this lens only means something when two parties independently
|
|
160
|
+
arrive at the same NON-TRIVIAL value.
|
|
161
|
+
|
|
162
|
+
Fixes 2 and 3 cost some invariance — a whitespace fix close enough to a
|
|
163
|
+
reviewed change that git merges their hunks now shifts the digest, and the
|
|
164
|
+
carry-forward is lost. That is the correct direction for this trade: a lost
|
|
165
|
+
carry-forward costs one re-dispatch, a spurious one admits unreviewed code.
|
|
166
|
+
|
|
167
|
+
**Deferred to v2:** comment-only stripping. It needs language awareness, and
|
|
168
|
+
string literals containing comment markers are a known trap. Gated on the
|
|
169
|
+
measured residual gate-block rate from #1624's recidivism metric.
|
|
170
|
+
|
|
171
|
+
## Fail-closed
|
|
172
|
+
|
|
173
|
+
Any parse or normalization error returns `None`. `pre_commit_review.py` treats
|
|
174
|
+
`None` as "this lens is not decisive" and falls through to today's exact
|
|
175
|
+
behavior — never to a pass. Same posture as every other read-side check in
|
|
176
|
+
the gate path.
|
|
177
|
+
|
|
178
|
+
Stdlib only. See ADR 0014 / ADR 0015.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
from __future__ import annotations
|
|
182
|
+
|
|
183
|
+
import hashlib
|
|
184
|
+
import re
|
|
185
|
+
import subprocess
|
|
186
|
+
import sys
|
|
187
|
+
from collections.abc import Callable
|
|
188
|
+
from pathlib import Path
|
|
189
|
+
from types import MappingProxyType
|
|
190
|
+
from typing import NamedTuple
|
|
191
|
+
|
|
192
|
+
_LIB_DIR = Path(__file__).resolve().parent
|
|
193
|
+
if str(_LIB_DIR) not in sys.path:
|
|
194
|
+
sys.path.insert(0, str(_LIB_DIR))
|
|
195
|
+
|
|
196
|
+
from git_safe_diff import run_safe_git_diff
|
|
197
|
+
from pre_commit_doc_classifier import is_doc_only_changeset
|
|
198
|
+
|
|
199
|
+
#: Characters whose presence on a changed line disqualifies it from
|
|
200
|
+
#: strip-equality. See the module docstring: string literals are behavior.
|
|
201
|
+
_QUOTE_CHARS = ("\"", "'", "`")
|
|
202
|
+
|
|
203
|
+
#: Extensions of languages where leading whitespace carries meaning, so a
|
|
204
|
+
#: changed line's indentation can never be normalized away. Deliberately
|
|
205
|
+
#: over-inclusive: an extension listed here in error costs a re-dispatch; one
|
|
206
|
+
#: missing lets a control-flow change look cosmetic. Unknown extensions get
|
|
207
|
+
#: the same byte-exact treatment (see `_whitespace_collapsible`) — this set
|
|
208
|
+
#: exists to document the known cases, not to define the safe default.
|
|
209
|
+
_INDENT_SIGNIFICANT_EXTENSIONS = frozenset(
|
|
210
|
+
{
|
|
211
|
+
".py", ".pyi", ".pyx",
|
|
212
|
+
".yaml", ".yml",
|
|
213
|
+
".hs", ".lhs", ".elm", ".nim", ".cr",
|
|
214
|
+
".fs", ".fsx", ".fsi",
|
|
215
|
+
".coffee", ".pug", ".jade", ".haml", ".slim",
|
|
216
|
+
".sass", ".styl",
|
|
217
|
+
".md", ".mdx", ".markdown", ".rst", ".adoc",
|
|
218
|
+
".txt",
|
|
219
|
+
# Markup whose rendered output preserves whitespace inside `<pre>`
|
|
220
|
+
# and `<textarea>`. Not brace-delimited in any case (#1631 review).
|
|
221
|
+
".html", ".htm", ".vue", ".svelte",
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
#: Extensions whose languages are brace/keyword-delimited, where leading and
|
|
226
|
+
#: trailing whitespace on a line is formatting only. Only these opt IN to
|
|
227
|
+
#: whitespace collapsing; everything else is compared byte-exactly.
|
|
228
|
+
_WHITESPACE_INSIGNIFICANT_EXTENSIONS = frozenset(
|
|
229
|
+
{
|
|
230
|
+
".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts",
|
|
231
|
+
".java", ".kt", ".kts", ".scala", ".groovy",
|
|
232
|
+
".c", ".h", ".cc", ".cpp", ".hpp", ".cxx", ".hxx",
|
|
233
|
+
".cs", ".go", ".rs", ".swift", ".m", ".mm",
|
|
234
|
+
".php", ".rb", ".pl", ".pm", ".lua", ".dart",
|
|
235
|
+
".css", ".scss", ".less",
|
|
236
|
+
# `.xml` is deliberately ABSENT (#1660): element text and CDATA
|
|
237
|
+
# sections preserve whitespace across most of a document, and no
|
|
238
|
+
# per-delimiter grammar would mark meaningfully fewer lines than
|
|
239
|
+
# "all of them". Falling back to the byte-exact default is both
|
|
240
|
+
# cheaper and safer — it costs one re-dispatch, never a weakened
|
|
241
|
+
# gate. See `_HEREDOC_GRAMMARS`' KNOWN RESIDUAL note.
|
|
242
|
+
".json", ".jsonc",
|
|
243
|
+
".sql", ".proto", ".tf", ".hcl",
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
#: Shared by `_HEREDOC_GRAMMARS` below — see that mapping's own comment for
|
|
248
|
+
#: the grammar contract. Ruby's `<<~`/`<<-`/bare, HCL's `<<-`/bare, and Perl's
|
|
249
|
+
#: `<<~`/bare all share this exact closer rule.
|
|
250
|
+
def _bareish_heredoc_close(line: str, ident: str, modifier: str) -> bool:
|
|
251
|
+
"""Shared closer for grammars whose only modifier axis is "must the
|
|
252
|
+
closing line sit in column 0". No modifier means the opener demands an
|
|
253
|
+
unindented closer (`line == ident`); `~`/`-` means the closer may be
|
|
254
|
+
indented (`line.lstrip() == ident`).
|
|
255
|
+
|
|
256
|
+
Leading whitespace only — never `.strip()`. A real heredoc terminator
|
|
257
|
+
(Ruby, Perl, HCL) is the identifier and nothing else on the line; text
|
|
258
|
+
AFTER it, including trailing whitespace, means the line is not a
|
|
259
|
+
terminator at all. Stripping trailing whitespace here would false-close
|
|
260
|
+
on a body line like `" SQL "` (trailing spaces) and dump every
|
|
261
|
+
subsequent body line back into whitespace collapsing — the "more body,
|
|
262
|
+
never fewer" rule this grammar exists to uphold, breached by the exact
|
|
263
|
+
kind of over-eager closer match that rule warns against."""
|
|
264
|
+
return line == ident if modifier == "" else line.lstrip() == ident
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _line_terminated(predicate):
|
|
268
|
+
"""Adapt a whole-line closer predicate to the positional closer protocol.
|
|
269
|
+
|
|
270
|
+
A heredoc terminator is a whole LINE, never a delimiter embedded in one.
|
|
271
|
+
Returning `len(line)` on a match means "this region consumed the entire
|
|
272
|
+
line", which is what stops the scanner re-scanning a terminator line for
|
|
273
|
+
a fresh opener — sound only because every predicate used here requires
|
|
274
|
+
the whole (stripped) line to equal the identifier, so a closing line can
|
|
275
|
+
never also carry an opener. `from_pos > 0` only happens when an INLINE
|
|
276
|
+
region already closed earlier on this line, and a heredoc terminator
|
|
277
|
+
cannot begin mid-line, so that case correctly never matches.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
def close(line: str, ident: str, modifier: str, from_pos: int):
|
|
281
|
+
if from_pos:
|
|
282
|
+
return None
|
|
283
|
+
return len(line) if predicate(line, ident, modifier) else None
|
|
284
|
+
|
|
285
|
+
return close
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _delimited(build_close):
|
|
289
|
+
"""Positional closer for an INLINE multi-line literal — one whose closing
|
|
290
|
+
delimiter is embedded in a line rather than being the whole line (Go/JS
|
|
291
|
+
backticks, Lua long brackets, SQL dollar-quoting, C++ raw strings, PHP's
|
|
292
|
+
inline-HTML region).
|
|
293
|
+
|
|
294
|
+
Returns the index just past the delimiter so the scanner can resume
|
|
295
|
+
hunting for a fresh opener on the remainder of the SAME line. That
|
|
296
|
+
resumption is what makes these forms safe to mix with heredocs: an inline
|
|
297
|
+
form genuinely can close and re-open on one line, the case the pre-#1660
|
|
298
|
+
scanner could not represent (and whose absence its own docstring flagged
|
|
299
|
+
as the thing a "grammar with a looser closer" would need).
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def close(line: str, ident: str, modifier: str, from_pos: int):
|
|
303
|
+
needle = build_close(ident, modifier)
|
|
304
|
+
index = line.find(needle, from_pos)
|
|
305
|
+
return None if index < 0 else index + len(needle)
|
|
306
|
+
|
|
307
|
+
return close
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class _Grammar(NamedTuple):
|
|
311
|
+
"""One unquoted multi-line-literal form for one language family.
|
|
312
|
+
|
|
313
|
+
`open_re` matches an opener anywhere on a line. Two OPTIONAL named groups
|
|
314
|
+
carry what a closer needs: `ident` (the delimiter identifier — a heredoc
|
|
315
|
+
tag, a Lua bracket level, a SQL dollar tag, a C++ raw-string delimiter)
|
|
316
|
+
and `mod` (the modifier axis, e.g. Ruby's `~`/`-`). NAMED, not positional
|
|
317
|
+
(#1665): the old convention lived only in a comment, and forced PHP's
|
|
318
|
+
regex to carry a phantom empty capture group purely to keep `group(2)`
|
|
319
|
+
aligned across grammars.
|
|
320
|
+
|
|
321
|
+
`close(line, ident, mod, from_pos)` returns the index just past this
|
|
322
|
+
region's terminator in `line`, or `None` if it does not terminate there.
|
|
323
|
+
|
|
324
|
+
`inline` records where the body starts: `False` for heredocs (body starts
|
|
325
|
+
on the line AFTER the opener, so an opener never closes itself), `True`
|
|
326
|
+
for inline spans (body starts mid-line, and the region may open and close
|
|
327
|
+
on one line).
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
open_re: re.Pattern
|
|
331
|
+
close: Callable[[str, str, str, int], int | None]
|
|
332
|
+
inline: bool
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _opener_parts(match) -> tuple[str, str]:
|
|
336
|
+
"""`(ident, modifier)` from an opener match, tolerating grammars that
|
|
337
|
+
declare only some of the optional named groups.
|
|
338
|
+
|
|
339
|
+
`qident` is the QUOTED-delimiter alternative, kept a separate group so the
|
|
340
|
+
unquoted alternative can keep its `[A-Za-z_]` first-character rule — which
|
|
341
|
+
is what stops Ruby/Perl's shovel operator (`x << 2`) reading as a heredoc
|
|
342
|
+
opener — while a quoted delimiter may legally start with a digit
|
|
343
|
+
(`<<~'1SQL'`) or be empty (Perl's blank-line-terminated `<<""`). Both are
|
|
344
|
+
#1667. An empty `qident` is a real match, so the `is not None` test must
|
|
345
|
+
not be shortened to a truthiness test.
|
|
346
|
+
"""
|
|
347
|
+
groups = match.groupdict()
|
|
348
|
+
quoted = groups.get("qident")
|
|
349
|
+
ident = quoted if quoted is not None else (groups.get("ident") or "")
|
|
350
|
+
return ident, (groups.get("mod") or "")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
#: Ruby's `<<~SQL`/`<<-SQL`/`<<SQL`, plus its backtick command form
|
|
354
|
+
#: ``<<`EOC` `` (#1667). The alternation keeps the digit-permissive delimiter
|
|
355
|
+
#: behind a required quote — see `_opener_parts`.
|
|
356
|
+
_RUBY_HEREDOC = _Grammar(
|
|
357
|
+
re.compile(
|
|
358
|
+
r"<<(?P<mod>[~-]?)"
|
|
359
|
+
r"(?:(?P<q>['\"`])(?P<qident>[A-Za-z0-9_]+)(?P=q)"
|
|
360
|
+
r"|(?P<ident>[A-Za-z_][A-Za-z0-9_]*))"
|
|
361
|
+
),
|
|
362
|
+
_line_terminated(_bareish_heredoc_close),
|
|
363
|
+
inline=False,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
#: Perl's `<<EOF`/`<<~EOF`, its spaced-quoted (`print << "EOF";`) and
|
|
367
|
+
#: backslash-quoted (`print <<\EOF;`) forms, and its empty-delimiter
|
|
368
|
+
#: blank-line-terminated form `print <<"";` (#1667 — `qident` uses `*`, not
|
|
369
|
+
#: `+`, and `_bareish_heredoc_close` then closes on a genuinely empty line).
|
|
370
|
+
_PERL_HEREDOC = _Grammar(
|
|
371
|
+
re.compile(
|
|
372
|
+
r"<<(?P<mod>~?)(?:\s+(?=['\"]))?"
|
|
373
|
+
r"(?:(?P<q>['\"])(?P<qident>[A-Za-z0-9_]*)(?P=q)"
|
|
374
|
+
r"|\\?(?P<ident>[A-Za-z_][A-Za-z0-9_]*))"
|
|
375
|
+
),
|
|
376
|
+
_line_terminated(_bareish_heredoc_close),
|
|
377
|
+
inline=False,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
#: PHP's `<<<TXT` heredoc and `<<<'TXT'` nowdoc. The closer is `.lstrip()`
|
|
381
|
+
#: (never `.strip()` — see `_bareish_heredoc_close`) plus `.rstrip(";,)")`,
|
|
382
|
+
#: since a real PHP terminator may be trailed by a statement terminator, an
|
|
383
|
+
#: array-element comma, or a call-argument close-paren. Known, accepted
|
|
384
|
+
#: residual: that makes PHP's close check looser than the "more body, never
|
|
385
|
+
#: fewer" rule for trailing PUNCTUATION — a body line reading exactly `TXT)`
|
|
386
|
+
#: false-closes. Distinguishing it needs the real PHP closer grammar, not a
|
|
387
|
+
#: regex tweak.
|
|
388
|
+
_PHP_HEREDOC = _Grammar(
|
|
389
|
+
re.compile(r"<<<\s*(?P<q>['\"]?)(?P<ident>[A-Za-z_][A-Za-z0-9_]*)(?P=q)"),
|
|
390
|
+
_line_terminated(lambda line, ident, _mod: line.lstrip().rstrip(";,)") == ident),
|
|
391
|
+
inline=False,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
#: PHP's inline-HTML region: everything between a `?>` and the next `<?` is
|
|
395
|
+
#: emitted verbatim, so reindenting it changes program OUTPUT (#1660).
|
|
396
|
+
_PHP_INLINE_HTML = _Grammar(
|
|
397
|
+
re.compile(r"\?>"),
|
|
398
|
+
_delimited(lambda _ident, _mod: "<?"),
|
|
399
|
+
inline=True,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
#: Lua long brackets — `[[ ... ]]`, `[==[ ... ]==]` (#1660). `ident` is the
|
|
403
|
+
#: `=` run, so the closer is level-matched and a `]]` at a different level
|
|
404
|
+
#: cannot close it. Also covers long comments (`--[[ ... ]]`), which share
|
|
405
|
+
#: the bracket grammar.
|
|
406
|
+
_LUA_LONG_BRACKET = _Grammar(
|
|
407
|
+
re.compile(r"\[(?P<ident>=*)\["),
|
|
408
|
+
_delimited(lambda ident, _mod: f"]{ident}]"),
|
|
409
|
+
inline=True,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
#: PostgreSQL dollar-quoting — `$$ ... $$`, `$tag$ ... $tag$` (#1660). `ident`
|
|
413
|
+
#: is the tag (absent for the bare `$$` form), so the closer is tag-matched.
|
|
414
|
+
_SQL_DOLLAR_QUOTE = _Grammar(
|
|
415
|
+
re.compile(r"\$(?P<ident>[A-Za-z_][A-Za-z0-9_]*)?\$"),
|
|
416
|
+
_delimited(lambda ident, _mod: f"${ident}$"),
|
|
417
|
+
inline=True,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
#: Go raw strings and JS/TS template literals — both backtick-delimited
|
|
421
|
+
#: (#1661). The OPENER line is already byte-exact under `_canonical_line`'s
|
|
422
|
+
#: quote-character rule (a backtick is in `_QUOTE_CHARS`); the gap this
|
|
423
|
+
#: closes is the INTERIOR lines, which routinely carry no quote character at
|
|
424
|
+
#: all. Over-matching is safe and expected here: a lone backtick in a comment
|
|
425
|
+
#: opens a region that never closes, and an unclosed region marks the rest of
|
|
426
|
+
#: the hunk byte-exact — a lost carry-forward, never a weakened gate.
|
|
427
|
+
_BACKTICK_RAW_STRING = _Grammar(
|
|
428
|
+
re.compile(r"`"),
|
|
429
|
+
_delimited(lambda _ident, _mod: "`"),
|
|
430
|
+
inline=True,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
#: C++ raw string literals — `R"(...)"`, `R"delim(...)delim"` (#1661).
|
|
434
|
+
_CPP_RAW_STRING = _Grammar(
|
|
435
|
+
re.compile(r'R"(?P<ident>[^()\\\s]{0,16})\('),
|
|
436
|
+
_delimited(lambda ident, _mod: f'){ident}"'),
|
|
437
|
+
inline=True,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
#: Shared tuples, so the `is`-identity aliases below stay identity-equal.
|
|
441
|
+
_RUBY_FORMS = (_RUBY_HEREDOC,)
|
|
442
|
+
_PERL_FORMS = (_PERL_HEREDOC,)
|
|
443
|
+
_PHP_FORMS = (_PHP_HEREDOC, _PHP_INLINE_HTML)
|
|
444
|
+
_LUA_FORMS = (_LUA_LONG_BRACKET,)
|
|
445
|
+
_SQL_FORMS = (_SQL_DOLLAR_QUOTE,)
|
|
446
|
+
_BACKTICK_FORMS = (_BACKTICK_RAW_STRING,)
|
|
447
|
+
_CPP_FORMS = (_CPP_RAW_STRING,)
|
|
448
|
+
|
|
449
|
+
#: Per-language unquoted multi-line-literal grammars (#1638, extended by
|
|
450
|
+
#: #1660/#1661/#1667). The quote-character rule in `_canonical_line` is what
|
|
451
|
+
#: keeps string data out of the cosmetic bucket, and it does not see an
|
|
452
|
+
#: UNQUOTED body — Ruby's `<<~SQL`, PHP's `<<<TXT`, Perl's `<<EOF`, Lua's
|
|
453
|
+
#: `[[`, SQL's `$$`, Go/JS backticks, C++'s `R"(`. Reindenting a line inside
|
|
454
|
+
#: one is a data change this module would otherwise read as formatting.
|
|
455
|
+
#:
|
|
456
|
+
#: Deliberately erring toward MORE lines counting as body, never fewer — see
|
|
457
|
+
#: `_heredoc_body_marks`'s docstring for why the closing-match direction
|
|
458
|
+
#: matters (an early false close drops real body content back into whitespace
|
|
459
|
+
#: collapsing, the hazard this exists to close).
|
|
460
|
+
#:
|
|
461
|
+
#: Keyed by extension, not by one cross-language regex, so PHP's `<<<` and
|
|
462
|
+
#: Ruby/Perl's `<<` can never cross-match. `.tf`/`.hcl` alias `.rb`'s forms
|
|
463
|
+
#: rather than `.pl`'s: Terraform/HCL heredocs support the bare and dash
|
|
464
|
+
#: (`<<-EOT`) forms but not the squiggly one, and only `.rb`'s regex captures
|
|
465
|
+
#: a dash modifier — `.pl`'s does not, so it would silently fail to match
|
|
466
|
+
#: `<<-EOT` at all.
|
|
467
|
+
#:
|
|
468
|
+
#: A `MappingProxyType` (#1665): this table is safety-relevant, and it was
|
|
469
|
+
#: previously a plain dict mutated after construction, so any importer could
|
|
470
|
+
#: rewrite a language's grammar at runtime. Its sibling language tables
|
|
471
|
+
#: (`_INDENT_SIGNIFICANT_EXTENSIONS`, `_WHITESPACE_INSIGNIFICANT_EXTENSIONS`)
|
|
472
|
+
#: are already `frozenset` for the same reason.
|
|
473
|
+
#:
|
|
474
|
+
#: KNOWN RESIDUAL (restored per #1660 — #1638 replaced the original residual
|
|
475
|
+
#: note with the grammar dict, leaving this class recorded nowhere):
|
|
476
|
+
#: - A `.php` file's leading inline-HTML region, before the file's FIRST
|
|
477
|
+
#: `<?php`, is not marked: `_PHP_INLINE_HTML` needs a `?>` to open, and
|
|
478
|
+
#: the file itself opens in HTML mode with no such token. Reaching it also
|
|
479
|
+
#: requires a hunk starting at line 1, which the #1662 guard below already
|
|
480
|
+
#: constrains.
|
|
481
|
+
#: - `.cs` verbatim strings (`@"..."`) and `.py`-style triple-quoted forms in
|
|
482
|
+
#: other whitespace-insignificant languages have no grammar. Triple-quoted
|
|
483
|
+
#: forms are covered incidentally — their delimiter IS a quote character,
|
|
484
|
+
#: so `_canonical_line` keeps those lines byte-exact — but only on lines
|
|
485
|
+
#: that carry the quote, never on interior lines.
|
|
486
|
+
#: - `.xml` was DROPPED from `_WHITESPACE_INSIGNIFICANT_EXTENSIONS` instead
|
|
487
|
+
#: of being given a grammar: element text and CDATA sections are
|
|
488
|
+
#: whitespace-preserving over most of a document, so the fail-closed option
|
|
489
|
+
#: #1660 offers is both cheaper and safer than a grammar that would have to
|
|
490
|
+
#: mark nearly every line anyway.
|
|
491
|
+
#:
|
|
492
|
+
#: Ruby/Perl's bare `<<` is also their shift/append ("shovel") operator
|
|
493
|
+
#: (`arr << item`, `x << 2`), but every real style guide writes that with
|
|
494
|
+
#: spaces around it. A heredoc opener has no space before an UNQUOTED
|
|
495
|
+
#: delimiter, so the identifier group starting immediately after `<<` (no
|
|
496
|
+
#: `\s*`) structurally cannot match the idiomatic spaced operator form.
|
|
497
|
+
#: Verified directly: `arr << item` and `x << 2` do not match; only the
|
|
498
|
+
#: unidiomatic no-space `arr<<item` would, which costs a rare false-positive
|
|
499
|
+
#: carry-forward loss. This is also exactly why #1667's digit-permissive
|
|
500
|
+
#: delimiter sits behind a REQUIRED quote: allowing `[0-9]` to lead an
|
|
501
|
+
#: unquoted delimiter would make `x << 2` a heredoc opener. Perl's grammar
|
|
502
|
+
#: carries two further exceptions, both in the SAFE (over-detection, never
|
|
503
|
+
#: under-detection) direction:
|
|
504
|
+
#: - A space IS legal (and not deprecated) before a QUOTED delimiter
|
|
505
|
+
#: (`print << "EOF";`), so `.pl`'s regex allows whitespace only when
|
|
506
|
+
#: immediately followed by a quote character — never bare — keeping the
|
|
507
|
+
#: shovel-operator distinction intact while still matching this real
|
|
508
|
+
#: heredoc form.
|
|
509
|
+
#: - A backslash-quoted delimiter (`print <<\EOF;`, perlop's no-interpolation
|
|
510
|
+
#: shorthand for `<<'EOF'`) is matched by an optional `\` before the
|
|
511
|
+
#: unquoted alternative. Over-matching an opener costs at most a spurious
|
|
512
|
+
#: carry-forward loss (marking replaces a line with itself, byte-exact,
|
|
513
|
+
#: never creating a collision); missing one is the unsafe direction this
|
|
514
|
+
#: whole grammar exists to close.
|
|
515
|
+
_HEREDOC_GRAMMARS = MappingProxyType(
|
|
516
|
+
{
|
|
517
|
+
".rb": _RUBY_FORMS,
|
|
518
|
+
".tf": _RUBY_FORMS,
|
|
519
|
+
".hcl": _RUBY_FORMS,
|
|
520
|
+
".pl": _PERL_FORMS,
|
|
521
|
+
".pm": _PERL_FORMS,
|
|
522
|
+
".php": _PHP_FORMS,
|
|
523
|
+
".lua": _LUA_FORMS,
|
|
524
|
+
".sql": _SQL_FORMS,
|
|
525
|
+
".go": _BACKTICK_FORMS,
|
|
526
|
+
".js": _BACKTICK_FORMS,
|
|
527
|
+
".jsx": _BACKTICK_FORMS,
|
|
528
|
+
".mjs": _BACKTICK_FORMS,
|
|
529
|
+
".cjs": _BACKTICK_FORMS,
|
|
530
|
+
".ts": _BACKTICK_FORMS,
|
|
531
|
+
".tsx": _BACKTICK_FORMS,
|
|
532
|
+
".mts": _BACKTICK_FORMS,
|
|
533
|
+
".cts": _BACKTICK_FORMS,
|
|
534
|
+
".cc": _CPP_FORMS,
|
|
535
|
+
".cpp": _CPP_FORMS,
|
|
536
|
+
".cxx": _CPP_FORMS,
|
|
537
|
+
".hpp": _CPP_FORMS,
|
|
538
|
+
".hxx": _CPP_FORMS,
|
|
539
|
+
".h": _CPP_FORMS,
|
|
540
|
+
}
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _heredoc_body_marks(lines: list, suffix: str) -> list:
|
|
546
|
+
"""Return one bool per entry in `lines`: True where the line is inside an
|
|
547
|
+
unquoted heredoc body (or is an unresolved opener's tail — see below),
|
|
548
|
+
and must therefore be compared byte-exact regardless of the
|
|
549
|
+
collapsible/quote-char rules `_canonical_line` otherwise applies.
|
|
550
|
+
|
|
551
|
+
Tracks a FIFO queue of pending `(ident, modifier)` pairs, not a single
|
|
552
|
+
one: stacked openers on one line are ordinary idioms (Ruby
|
|
553
|
+
`assert_equal(<<~A, <<~B)`, Perl `print <<A, <<B;`), and their bodies
|
|
554
|
+
close in the same order the openers appeared. Marking only the first of
|
|
555
|
+
several openers — losing the rest to ordinary whitespace collapsing —
|
|
556
|
+
is exactly the hazard this function exists to close, so every opener
|
|
557
|
+
found while the queue is empty is enqueued, not just the first match on
|
|
558
|
+
the line.
|
|
559
|
+
|
|
560
|
+
Fails closed on an unmatched opener: if a heredoc opens within `lines`
|
|
561
|
+
but no closing delimiter is found before `lines` ends, every remaining
|
|
562
|
+
line is marked True. The true close may lie outside what this hunk's
|
|
563
|
+
side shows; the safe assumption is that it has not closed yet.
|
|
564
|
+
`normalized_gate_hash` additionally requests full-file diff context (see
|
|
565
|
+
its docstring) so that in real use an opener earlier in the same file is
|
|
566
|
+
always part of the same hunk as any body line it covers — this function
|
|
567
|
+
only needs to reason within one hunk side, never across hunks.
|
|
568
|
+
|
|
569
|
+
A line that closes a heredoc is not re-scanned for a new opener of its
|
|
570
|
+
own, because `_line_terminated` reports that the terminator consumed the
|
|
571
|
+
whole line. That is sound only for grammars whose closer IS the entire
|
|
572
|
+
(stripped) line. INLINE grammars (#1660/#1661 — Lua long brackets, SQL
|
|
573
|
+
dollar-quoting, Go/JS backticks, C++ raw strings, PHP inline HTML) have a
|
|
574
|
+
genuinely looser closer, so `_delimited` reports the position just past
|
|
575
|
+
the delimiter and the scan resumes there — the "fresh opener on the
|
|
576
|
+
closer's line" case this docstring previously recorded as a hazard a
|
|
577
|
+
looser grammar would reopen.
|
|
578
|
+
|
|
579
|
+
An opener line itself is never marked — evident intent, and it usually
|
|
580
|
+
carries no body content (`sql = <<~SQL`) — only lines strictly after it,
|
|
581
|
+
through and including whatever line closes it. For inline forms the
|
|
582
|
+
opener line is additionally already byte-exact under `_canonical_line`'s
|
|
583
|
+
quote-character rule wherever the delimiter is a quote character.
|
|
584
|
+
"""
|
|
585
|
+
grammars = _HEREDOC_GRAMMARS.get(suffix)
|
|
586
|
+
if not grammars:
|
|
587
|
+
return [False] * len(lines)
|
|
588
|
+
marks = [False] * len(lines)
|
|
589
|
+
# FIFO of (ident, modifier, close); bodies close in the order opened.
|
|
590
|
+
pending: list = []
|
|
591
|
+
for index, line in enumerate(lines):
|
|
592
|
+
position = 0
|
|
593
|
+
if pending:
|
|
594
|
+
marks[index] = True
|
|
595
|
+
while pending:
|
|
596
|
+
ident, modifier, close = pending[0]
|
|
597
|
+
end = close(line, ident, modifier, position)
|
|
598
|
+
if end is None:
|
|
599
|
+
break
|
|
600
|
+
pending.pop(0)
|
|
601
|
+
position = end
|
|
602
|
+
if pending:
|
|
603
|
+
continue
|
|
604
|
+
_enqueue_openers(line, position, grammars, pending)
|
|
605
|
+
return marks
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _enqueue_openers(line: str, position: int, grammars, pending: list) -> None:
|
|
609
|
+
"""Enqueue every region opened at or after `position` on `line`.
|
|
610
|
+
|
|
611
|
+
Openers are consumed in POSITIONAL order across ALL of a language's
|
|
612
|
+
grammars, not one grammar at a time, so a line mixing two forms (PHP's
|
|
613
|
+
`<<<TXT` heredoc and its `?>` inline-HTML region) enqueues them in the
|
|
614
|
+
order their bodies actually close. Scanning grammar-by-grammar would
|
|
615
|
+
invert that order and pair a body with the wrong closer.
|
|
616
|
+
|
|
617
|
+
An INLINE region that closes on its own line spills nothing into the
|
|
618
|
+
following lines, so the scan simply resumes past it. One that does NOT
|
|
619
|
+
close consumes the rest of the line by definition — no further opener can
|
|
620
|
+
start inside it — so the scan stops. A heredoc opener never consumes its
|
|
621
|
+
own line: stacked openers (`assert_equal(<<~A, <<~B)`) are ordinary
|
|
622
|
+
idioms, and all of them must be enqueued.
|
|
623
|
+
"""
|
|
624
|
+
while position <= len(line):
|
|
625
|
+
best = None
|
|
626
|
+
for grammar in grammars:
|
|
627
|
+
match = grammar.open_re.search(line, position)
|
|
628
|
+
if match is None:
|
|
629
|
+
continue
|
|
630
|
+
if best is None or match.start() < best[0].start():
|
|
631
|
+
best = (match, grammar)
|
|
632
|
+
if best is None:
|
|
633
|
+
return
|
|
634
|
+
match, grammar = best
|
|
635
|
+
ident, modifier = _opener_parts(match)
|
|
636
|
+
# Never let a zero-width match stall the scan.
|
|
637
|
+
advanced = max(match.end(), match.start() + 1)
|
|
638
|
+
if grammar.inline:
|
|
639
|
+
closed_at = grammar.close(line, ident, modifier, match.end())
|
|
640
|
+
if closed_at is not None:
|
|
641
|
+
position = max(closed_at, advanced)
|
|
642
|
+
continue
|
|
643
|
+
pending.append((ident, modifier, grammar.close))
|
|
644
|
+
return
|
|
645
|
+
pending.append((ident, modifier, grammar.close))
|
|
646
|
+
position = advanced
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
#: Field/record separators for the canonical serialization. Chosen from the
|
|
650
|
+
#: ASCII separator block because they are vanishingly rare in source, but NOT
|
|
651
|
+
#: relied on for unambiguity: every field is length-prefixed by `_encode`, so
|
|
652
|
+
#: a file that genuinely contains these bytes cannot forge a field boundary.
|
|
653
|
+
#: git treats any NUL-free blob as text, so "no source line contains \x1f" is
|
|
654
|
+
#: an assumption about content an attacker supplies (#1631 review).
|
|
655
|
+
_FIELD_SEP = "\x1f"
|
|
656
|
+
_RECORD_SEP = "\x1e"
|
|
657
|
+
|
|
658
|
+
#: Bounds on the one caller that asks git for whole-file context (#1663).
|
|
659
|
+
#: Both are fail-closed: exceeding either returns `None`, which every caller
|
|
660
|
+
#: already treats as "this lens is not decisive", never as a pass.
|
|
661
|
+
#: `run_safe_git_diff` had no timeout at all, so a wedged or pathologically
|
|
662
|
+
#: slow `git diff` hung the `pre_commit_review.py` PreToolUse hook
|
|
663
|
+
#: indefinitely.
|
|
664
|
+
_GIT_DIFF_TIMEOUT_SECONDS = 30.0
|
|
665
|
+
_MAX_PATCH_BYTES = 8 * 1024 * 1024
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _encode(parts) -> str:
|
|
669
|
+
"""Length-prefix each field so the canonical form is unambiguous.
|
|
670
|
+
|
|
671
|
+
Without this, two different changesets could serialize identically by
|
|
672
|
+
embedding a separator byte in a source line — the standard delimiter-
|
|
673
|
+
injection hazard, and a real one here because the digest's whole job is
|
|
674
|
+
to be hard to collide with.
|
|
675
|
+
"""
|
|
676
|
+
return _FIELD_SEP.join(f"{len(part)}:{part}" for part in parts)
|
|
677
|
+
|
|
678
|
+
#: Patch-metadata prefixes that carry a real change with no hunk body — a
|
|
679
|
+
#: mode flip, a rename, a copy, an empty new or deleted file. See fix 4 in
|
|
680
|
+
#: the module docstring: omitting these makes such a change invisible to the
|
|
681
|
+
#: digest, so it could be staged on top of already-corroborated content.
|
|
682
|
+
_STRUCTURAL_PREFIXES = (
|
|
683
|
+
"old mode ",
|
|
684
|
+
"new mode ",
|
|
685
|
+
"new file mode ",
|
|
686
|
+
"deleted file mode ",
|
|
687
|
+
"rename from ",
|
|
688
|
+
"rename to ",
|
|
689
|
+
"copy from ",
|
|
690
|
+
"copy to ",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
#: `@@ -old_start[,old_count] +new_start[,new_count] @@[ section heading]`.
|
|
694
|
+
#: Start lines are captured but deliberately unused — they shift whenever an
|
|
695
|
+
#: earlier hunk gains or loses a line, while the counts are what bound the
|
|
696
|
+
#: body. The leading anchor rejects the combined-diff `@@@` form, which this
|
|
697
|
+
#: module never asks git to produce.
|
|
698
|
+
_HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _is_documentation(path: str) -> bool:
|
|
702
|
+
"""Per-file documentation predicate, delegating to the gate's own STRICT
|
|
703
|
+
classifier. A single-element list is exactly the "is this one file
|
|
704
|
+
provably documentation" question, so no second implementation exists."""
|
|
705
|
+
return is_doc_only_changeset([path])
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def _suffix(path: str) -> str | None:
|
|
709
|
+
"""Lowercased extension including the dot, or None for an extensionless
|
|
710
|
+
file — shared by `_whitespace_collapsible` and the heredoc grammar
|
|
711
|
+
lookup so the two never disagree about what a path's extension is."""
|
|
712
|
+
name = str(path or "").replace("\\", "/").rsplit("/", 1)[-1].lower()
|
|
713
|
+
if "." not in name:
|
|
714
|
+
return None
|
|
715
|
+
return "." + name.rsplit(".", 1)[-1]
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def _whitespace_collapsible(path: str) -> bool:
|
|
719
|
+
"""True only for extensions this module can prove are brace/keyword-
|
|
720
|
+
delimited. Everything else — including every unknown extension and every
|
|
721
|
+
extensionless file — is compared byte-exactly. Fail-closed by default:
|
|
722
|
+
the safe answer to "is this language's indentation meaningless?" is no."""
|
|
723
|
+
suffix = _suffix(path)
|
|
724
|
+
if suffix is None or suffix in _INDENT_SIGNIFICANT_EXTENSIONS:
|
|
725
|
+
return False
|
|
726
|
+
return suffix in _WHITESPACE_INSIGNIFICANT_EXTENSIONS
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _canonical_line(line: str, collapsible: bool) -> str:
|
|
730
|
+
"""The form of a changed line the normalized hash is computed over.
|
|
731
|
+
|
|
732
|
+
Strips leading/trailing whitespace ONLY when the file's language allows
|
|
733
|
+
it AND the line carries no quote character. Both conditions are load
|
|
734
|
+
bearing — see the module docstring.
|
|
735
|
+
"""
|
|
736
|
+
if not collapsible:
|
|
737
|
+
return line
|
|
738
|
+
if any(q in line for q in _QUOTE_CHARS):
|
|
739
|
+
return line
|
|
740
|
+
return line.strip()
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _target_path(header: str) -> str | None:
|
|
744
|
+
"""Extract the new-side path from a `+++ ` header. `/dev/null` (a pure
|
|
745
|
+
delete) has no new-side path; the old-side name is used by the caller."""
|
|
746
|
+
target = header[4:].strip()
|
|
747
|
+
if target == "/dev/null":
|
|
748
|
+
return None
|
|
749
|
+
return target.removeprefix("b/")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _source_path(header: str) -> str | None:
|
|
753
|
+
source = header[4:].strip()
|
|
754
|
+
if source == "/dev/null":
|
|
755
|
+
return None
|
|
756
|
+
return source.removeprefix("a/")
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _structural_marker(raw: str) -> str | None:
|
|
760
|
+
"""A patch-metadata line that carries a real change with no hunk body.
|
|
761
|
+
|
|
762
|
+
Everything matched here alters the committed tree without producing a
|
|
763
|
+
single `+`/`-` line, so omitting it makes the change invisible to this
|
|
764
|
+
digest — see fix 4 in the module docstring. `index` is deliberately NOT
|
|
765
|
+
here: its blob SHAs move with every content edit, which would defeat the
|
|
766
|
+
invariance this module exists for. It is picked up only for binary files
|
|
767
|
+
(`_FileSection.binary`), where it is the sole content signal available.
|
|
768
|
+
"""
|
|
769
|
+
for prefix in _STRUCTURAL_PREFIXES:
|
|
770
|
+
if raw.startswith(prefix):
|
|
771
|
+
return raw.strip()
|
|
772
|
+
return None
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class _FileSection:
|
|
776
|
+
"""Accumulator for one `diff --git` section."""
|
|
777
|
+
|
|
778
|
+
__slots__ = ("binary", "fallback", "hunks", "index_line", "old_path", "path", "structural")
|
|
779
|
+
|
|
780
|
+
def __init__(self, fallback: str) -> None:
|
|
781
|
+
self.path: str | None = None
|
|
782
|
+
self.old_path: str | None = None
|
|
783
|
+
self.fallback = fallback
|
|
784
|
+
self.structural: list = []
|
|
785
|
+
self.hunks: list = []
|
|
786
|
+
self.binary = False
|
|
787
|
+
self.index_line: str | None = None
|
|
788
|
+
|
|
789
|
+
@property
|
|
790
|
+
def name(self) -> str:
|
|
791
|
+
"""The section's identity for sorting and doc classification."""
|
|
792
|
+
return self.path or self.old_path or self.fallback
|
|
793
|
+
|
|
794
|
+
@property
|
|
795
|
+
def has_real_path(self) -> bool:
|
|
796
|
+
"""True when a `---`/`+++` header supplied a genuine path.
|
|
797
|
+
|
|
798
|
+
A section identified only by its `diff --git` remainder (a pure
|
|
799
|
+
rename, mode flip, or binary swap) is never doc-classified away: the
|
|
800
|
+
fallback is not a path this module can hand to the STRICT classifier
|
|
801
|
+
with confidence, and the safe answer to "may this be dropped?" is no.
|
|
802
|
+
"""
|
|
803
|
+
return self.path is not None or self.old_path is not None
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _parse_hunk_header(raw: str) -> tuple[int, int, int, int] | None:
|
|
807
|
+
"""`(old_count, new_count, old_start, new_start)` from an `@@ -a,b +c,d @@`
|
|
808
|
+
header.
|
|
809
|
+
|
|
810
|
+
An omitted count means 1 (`@@ -1 +1 @@`). Returns `None` for anything
|
|
811
|
+
that is not a well-formed unified-diff hunk header, including the
|
|
812
|
+
combined `@@@` form, which this module never asks git to produce.
|
|
813
|
+
|
|
814
|
+
The START lines were previously parsed and discarded, with a comment
|
|
815
|
+
explaining that they shift whenever an earlier hunk gains or loses a line
|
|
816
|
+
while the counts are what bound the body. That reasoning went stale in
|
|
817
|
+
#1638: the heredoc grammars made "does this hunk begin at the top of the
|
|
818
|
+
file" a question the digest depends on, because an opener above a hunk is
|
|
819
|
+
only visible when the hunk reaches back to line 1. #1662 makes them
|
|
820
|
+
load-bearing again — see `normalize_patch`.
|
|
821
|
+
"""
|
|
822
|
+
match = _HUNK_HEADER_RE.match(raw)
|
|
823
|
+
if match is None:
|
|
824
|
+
return None
|
|
825
|
+
old_start = int(match.group(1))
|
|
826
|
+
old_count = int(match.group(2)) if match.group(2) is not None else 1
|
|
827
|
+
new_start = int(match.group(3))
|
|
828
|
+
new_count = int(match.group(4)) if match.group(4) is not None else 1
|
|
829
|
+
return old_count, new_count, old_start, new_start
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def normalize_patch(patch_text: str) -> str | None:
|
|
833
|
+
"""Reduce a unified diff to its behavior-bearing content.
|
|
834
|
+
|
|
835
|
+
Returns a canonical string, or `None` when the patch cannot be parsed
|
|
836
|
+
(fail-closed — the caller must not treat `None` as "no changes"). An
|
|
837
|
+
EMPTY string means "parsed fine, nothing behavior-bearing left"; it is a
|
|
838
|
+
legitimate value here, and `normalized_gate_hash` is what refuses to turn
|
|
839
|
+
it into a digest (fix 5 in the module docstring).
|
|
840
|
+
|
|
841
|
+
Hunk bodies are consumed by the line counts in their own `@@` headers,
|
|
842
|
+
never by dispatching on line prefixes, so a removed line that happens to
|
|
843
|
+
start with `-- ` cannot masquerade as a file header. Each hunk is reduced
|
|
844
|
+
to its canonicalized old side and new side, context lines included, so an
|
|
845
|
+
insertion's position is part of the subject; a hunk whose two sides come
|
|
846
|
+
out identical was purely formatting and is dropped. Files are emitted in
|
|
847
|
+
sorted order so a re-stage that reorders git's own file output cannot
|
|
848
|
+
change the digest.
|
|
849
|
+
|
|
850
|
+
Split on literal `"\n"` only (#1904 item 15) — NOT `str.splitlines()`,
|
|
851
|
+
which also breaks on the full Unicode line-boundary set (form feed,
|
|
852
|
+
U+2028 LINE SEPARATOR, U+0085 NEL, etc.). git's own `@@ -a,b +c,d @@`
|
|
853
|
+
hunk-header line counts are computed over literal `\n` bytes only, so a
|
|
854
|
+
staged line containing one of those other boundary characters would make
|
|
855
|
+
`splitlines()` produce more "lines" than the hunk header accounts for,
|
|
856
|
+
desyncing this parser's line model from git's and corrupting every
|
|
857
|
+
subsequent line's old/new-side attribution for the rest of the hunk.
|
|
858
|
+
"""
|
|
859
|
+
if patch_text is None:
|
|
860
|
+
return None
|
|
861
|
+
|
|
862
|
+
sections: list = []
|
|
863
|
+
current: _FileSection | None = None
|
|
864
|
+
old_side: list = []
|
|
865
|
+
new_side: list = []
|
|
866
|
+
old_remaining = 0
|
|
867
|
+
new_remaining = 0
|
|
868
|
+
old_start = 0
|
|
869
|
+
new_start = 0
|
|
870
|
+
in_hunk = False
|
|
871
|
+
|
|
872
|
+
def close_hunk() -> None:
|
|
873
|
+
nonlocal old_side, new_side, in_hunk
|
|
874
|
+
if in_hunk and current is not None:
|
|
875
|
+
current.hunks.append((old_side, new_side, old_start, new_start))
|
|
876
|
+
old_side, new_side = [], []
|
|
877
|
+
in_hunk = False
|
|
878
|
+
|
|
879
|
+
try:
|
|
880
|
+
# `"".split("\n")` is `[""]`, not `[]` (unlike `"".splitlines()`) —
|
|
881
|
+
# guard the genuinely-empty-input case explicitly so it still yields
|
|
882
|
+
# zero iterations, matching the pre-existing "parsed fine, nothing
|
|
883
|
+
# behavior-bearing left" contract (fix 5 in the module docstring)
|
|
884
|
+
# rather than one spurious iteration over a fake empty line.
|
|
885
|
+
for raw in (patch_text.split("\n") if patch_text else []):
|
|
886
|
+
if in_hunk and (old_remaining > 0 or new_remaining > 0):
|
|
887
|
+
# Inside a hunk body: the declared counts, not the line's
|
|
888
|
+
# prefix, decide where the body ends.
|
|
889
|
+
if raw.startswith("\\"):
|
|
890
|
+
# "" — annotates the preceding
|
|
891
|
+
# line and consumes no budget on either side.
|
|
892
|
+
continue
|
|
893
|
+
if raw.startswith("+"):
|
|
894
|
+
new_side.append(raw[1:])
|
|
895
|
+
new_remaining -= 1
|
|
896
|
+
elif raw.startswith("-"):
|
|
897
|
+
old_side.append(raw[1:])
|
|
898
|
+
old_remaining -= 1
|
|
899
|
+
elif raw.startswith(" ") or raw == "":
|
|
900
|
+
# Context. An empty line is a context line whose trailing
|
|
901
|
+
# space some tools strip.
|
|
902
|
+
body = raw[1:] if raw else ""
|
|
903
|
+
old_side.append(body)
|
|
904
|
+
new_side.append(body)
|
|
905
|
+
old_remaining -= 1
|
|
906
|
+
new_remaining -= 1
|
|
907
|
+
else:
|
|
908
|
+
# A hunk body cannot contain anything else. Rather than
|
|
909
|
+
# guess, fail closed.
|
|
910
|
+
return None
|
|
911
|
+
if old_remaining <= 0 and new_remaining <= 0:
|
|
912
|
+
close_hunk()
|
|
913
|
+
continue
|
|
914
|
+
|
|
915
|
+
if raw.startswith("diff --git "):
|
|
916
|
+
close_hunk()
|
|
917
|
+
current = _FileSection(raw[len("diff --git ") :].strip())
|
|
918
|
+
sections.append(current)
|
|
919
|
+
continue
|
|
920
|
+
|
|
921
|
+
if current is None:
|
|
922
|
+
# Diff output that never opened a section. Not something git
|
|
923
|
+
# produces for the invocation this module makes; fail closed
|
|
924
|
+
# rather than digest a shape we do not understand.
|
|
925
|
+
return None
|
|
926
|
+
|
|
927
|
+
if raw.startswith("--- "):
|
|
928
|
+
close_hunk()
|
|
929
|
+
current.old_path = _source_path(raw)
|
|
930
|
+
elif raw.startswith("+++ "):
|
|
931
|
+
close_hunk()
|
|
932
|
+
current.path = _target_path(raw)
|
|
933
|
+
elif raw.startswith("@@"):
|
|
934
|
+
close_hunk()
|
|
935
|
+
counts = _parse_hunk_header(raw)
|
|
936
|
+
if counts is None:
|
|
937
|
+
return None
|
|
938
|
+
old_remaining, new_remaining, old_start, new_start = counts
|
|
939
|
+
in_hunk = True
|
|
940
|
+
if old_remaining <= 0 and new_remaining <= 0:
|
|
941
|
+
# A degenerate `@@ -0,0 +0,0 @@`: nothing to consume.
|
|
942
|
+
close_hunk()
|
|
943
|
+
elif raw.startswith("index "):
|
|
944
|
+
current.index_line = raw.strip()
|
|
945
|
+
elif raw.startswith(("Binary files ", "GIT binary patch")):
|
|
946
|
+
current.binary = True
|
|
947
|
+
else:
|
|
948
|
+
marker = _structural_marker(raw)
|
|
949
|
+
if marker is not None:
|
|
950
|
+
current.structural.append(marker)
|
|
951
|
+
close_hunk()
|
|
952
|
+
except Exception: # noqa: BLE001 - fail closed, see module docstring
|
|
953
|
+
return None
|
|
954
|
+
|
|
955
|
+
records = []
|
|
956
|
+
try:
|
|
957
|
+
for section in sorted(sections, key=lambda s: s.name):
|
|
958
|
+
name = section.name
|
|
959
|
+
if section.has_real_path and _is_documentation(name):
|
|
960
|
+
continue
|
|
961
|
+
collapsible = _whitespace_collapsible(name)
|
|
962
|
+
suffix = _suffix(name)
|
|
963
|
+
parts = list(section.structural)
|
|
964
|
+
if section.binary:
|
|
965
|
+
# The blob SHAs are the only content signal a textual diff
|
|
966
|
+
# exposes for a binary file, and without them any binary
|
|
967
|
+
# replacement is invisible to this digest.
|
|
968
|
+
parts.append(section.index_line or "binary")
|
|
969
|
+
grammared = suffix in _HEREDOC_GRAMMARS
|
|
970
|
+
for old_lines, new_lines, hunk_old_start, hunk_new_start in section.hunks:
|
|
971
|
+
if grammared and (hunk_old_start > 1 or hunk_new_start > 1):
|
|
972
|
+
# #1662: `_heredoc_body_marks` can only see an opener that
|
|
973
|
+
# is inside the hunk it is given. `normalized_gate_hash`
|
|
974
|
+
# asks for `--unified=100000` so that in practice every
|
|
975
|
+
# hunk spans the whole file — but that is a magic number,
|
|
976
|
+
# not a guarantee. A file longer than that context window
|
|
977
|
+
# still yields a hunk starting partway down, and an opener
|
|
978
|
+
# ABOVE it is invisible: the exact hazard the grammars
|
|
979
|
+
# exist to close, silently reopened, and failing OPEN
|
|
980
|
+
# rather than closed. Rather than trust the number, detect
|
|
981
|
+
# the condition it is supposed to make impossible and fall
|
|
982
|
+
# back to byte-exact for this hunk. Costs a carry-forward
|
|
983
|
+
# on files past the window; costs nothing otherwise, since
|
|
984
|
+
# a whole-file hunk starts at line 1 by construction.
|
|
985
|
+
# A pure add/delete reports start 0 on its empty side, so
|
|
986
|
+
# `> 1` (not `!= 1`) is what keeps those on the fast path.
|
|
987
|
+
old_canon = list(old_lines)
|
|
988
|
+
new_canon = list(new_lines)
|
|
989
|
+
if old_canon == new_canon:
|
|
990
|
+
continue
|
|
991
|
+
parts.append("\n".join(old_canon))
|
|
992
|
+
parts.append("\n".join(new_canon))
|
|
993
|
+
continue
|
|
994
|
+
old_heredoc = _heredoc_body_marks(old_lines, suffix)
|
|
995
|
+
new_heredoc = _heredoc_body_marks(new_lines, suffix)
|
|
996
|
+
old_canon = [
|
|
997
|
+
ln if marked else _canonical_line(ln, collapsible)
|
|
998
|
+
for ln, marked in zip(old_lines, old_heredoc)
|
|
999
|
+
]
|
|
1000
|
+
new_canon = [
|
|
1001
|
+
ln if marked else _canonical_line(ln, collapsible)
|
|
1002
|
+
for ln, marked in zip(new_lines, new_heredoc)
|
|
1003
|
+
]
|
|
1004
|
+
if old_canon == new_canon:
|
|
1005
|
+
# This hunk's two sides canonicalize identically — its
|
|
1006
|
+
# whole delta was formatting.
|
|
1007
|
+
continue
|
|
1008
|
+
parts.append("\n".join(old_canon))
|
|
1009
|
+
parts.append("\n".join(new_canon))
|
|
1010
|
+
if not parts:
|
|
1011
|
+
continue
|
|
1012
|
+
records.append(_encode([name] + parts))
|
|
1013
|
+
except Exception: # noqa: BLE001 - fail closed, see module docstring
|
|
1014
|
+
return None
|
|
1015
|
+
return _RECORD_SEP.join(_encode([record]) for record in records)
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def normalized_gate_hash(cwd=None, target: str = "--cached") -> str | None:
|
|
1019
|
+
"""sha256 of the normalized staged patch, or `None` on any failure.
|
|
1020
|
+
|
|
1021
|
+
Uses the same `git_safe_diff.run_safe_git_diff` invocation and the same
|
|
1022
|
+
`--no-color --no-ext-diff --no-textconv` pins as `review_gate_hash()`.
|
|
1023
|
+
Those flags are not optional here either: a `diff.external` driver would
|
|
1024
|
+
otherwise collapse this function's input to empty for every changeset,
|
|
1025
|
+
turning the normalized hash into a constant — the same subject-binding
|
|
1026
|
+
bypass `review_gate_hash()`'s docstring documents at length.
|
|
1027
|
+
|
|
1028
|
+
Also passes `--unified=100000` (#1638): `_heredoc_body_marks` reasons
|
|
1029
|
+
only within one hunk side, never across hunks, so a heredoc opener more
|
|
1030
|
+
than the default 3 lines of context above a changed body line would
|
|
1031
|
+
otherwise be invisible to it — the exact "opened outside the visible
|
|
1032
|
+
hunk" hazard the heredoc handling has to fail closed against. Requesting
|
|
1033
|
+
enough context to cover any realistically-sized file makes each file's
|
|
1034
|
+
changes arrive as one hunk spanning start to end, so an opener anywhere
|
|
1035
|
+
earlier in the file is always part of the same hunk as any body line it
|
|
1036
|
+
covers. This is strictly safer than the default, never less: wider
|
|
1037
|
+
context only ever MERGES hunks, and a merged hunk whose two sides still
|
|
1038
|
+
canonicalize identically was purely formatting regardless of how many
|
|
1039
|
+
original hunks it absorbed (see the "costs some invariance, never
|
|
1040
|
+
safety" trade already accepted for fixes 2-3 in the module docstring).
|
|
1041
|
+
|
|
1042
|
+
`target` mirrors `review_gate_hash()`'s own `target` parameter:
|
|
1043
|
+
`--cached` for an ordinary staged commit. (The `HEAD` shape this
|
|
1044
|
+
mirrored, for the `git commit -a`/pathspec form (#1476), was
|
|
1045
|
+
`review_gate_hash.py`'s `working_tree_gate_hash()` — deleted in #1904
|
|
1046
|
+
once `pre_commit_review.py`, its sole caller, was retired to a no-op.)
|
|
1047
|
+
|
|
1048
|
+
Returns `None` — never a digest of empty input — when git fails, AND
|
|
1049
|
+
when the changeset normalizes to nothing at all. Both are the same
|
|
1050
|
+
hazard: `sha256("")` is a CONSTANT, shared by every broken-git
|
|
1051
|
+
invocation, every fully-cosmetic changeset, and every dispatch recorded
|
|
1052
|
+
while the index was still clean. Two review dispatches made before
|
|
1053
|
+
anything was staged would otherwise stamp precisely the value a later
|
|
1054
|
+
mode-only or rename-only stage recomputes, clearing the `>= 2` floor with
|
|
1055
|
+
evidence from agents that reviewed nothing. The whole point of this lens
|
|
1056
|
+
is that two parties independently arrive at the same NON-TRIVIAL value.
|
|
1057
|
+
|
|
1058
|
+
A changeset with nothing behavior-bearing left therefore gets no
|
|
1059
|
+
carry-forward. It loses nothing real: a wholly doc-classified changeset
|
|
1060
|
+
is already served by the doc-only exemption lens, which re-derives its
|
|
1061
|
+
own predicate against the actual staged files.
|
|
1062
|
+
"""
|
|
1063
|
+
try:
|
|
1064
|
+
completed = run_safe_git_diff(
|
|
1065
|
+
["--no-color", "--no-ext-diff", "--no-textconv", "--unified=100000"],
|
|
1066
|
+
cwd=cwd,
|
|
1067
|
+
text=False,
|
|
1068
|
+
target=target,
|
|
1069
|
+
timeout=_GIT_DIFF_TIMEOUT_SECONDS,
|
|
1070
|
+
)
|
|
1071
|
+
except (FileNotFoundError, OSError, subprocess.SubprocessError):
|
|
1072
|
+
# `SubprocessError` covers `TimeoutExpired`, which is NOT an `OSError`
|
|
1073
|
+
# subclass. Fail closed, matching every other failure path here.
|
|
1074
|
+
return None
|
|
1075
|
+
|
|
1076
|
+
if completed.returncode != 0:
|
|
1077
|
+
return None
|
|
1078
|
+
|
|
1079
|
+
if len(completed.stdout) > _MAX_PATCH_BYTES:
|
|
1080
|
+
# #1663: `--unified=100000` turns a one-line edit to a large tracked
|
|
1081
|
+
# file into a whole-file diff, and `normalize_patch` holds several
|
|
1082
|
+
# full in-memory copies of it (split lines, per-hunk old/new sides,
|
|
1083
|
+
# canonicalized sides, joined strings, encoded records). `.json` is a
|
|
1084
|
+
# whitespace-insignificant extension, so an ordinary lockfile touch
|
|
1085
|
+
# hits this path on every commit. Refusing above the cap costs a
|
|
1086
|
+
# carry-forward on very large files; processing an unbounded payload
|
|
1087
|
+
# synchronously inside a PreToolUse hook costs the commit.
|
|
1088
|
+
return None
|
|
1089
|
+
|
|
1090
|
+
try:
|
|
1091
|
+
patch_text = completed.stdout.decode("utf-8", errors="replace")
|
|
1092
|
+
except Exception: # noqa: BLE001 - fail closed
|
|
1093
|
+
return None
|
|
1094
|
+
|
|
1095
|
+
normalized = normalize_patch(patch_text)
|
|
1096
|
+
if not normalized:
|
|
1097
|
+
# Covers both the parse failure (`None`) and the degenerate
|
|
1098
|
+
# empty-canonical-form case (`""`) — see the docstring above.
|
|
1099
|
+
return None
|
|
1100
|
+
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _main() -> int:
|
|
1104
|
+
value = normalized_gate_hash()
|
|
1105
|
+
if value is None:
|
|
1106
|
+
return 1
|
|
1107
|
+
print(value)
|
|
1108
|
+
return 0
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
if __name__ == "__main__":
|
|
1112
|
+
raise SystemExit(_main())
|
|
1113
|
+
|
|
1114
|
+
|
|
1115
|
+
__all__ = ("normalize_patch", "normalized_gate_hash")
|