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.
Files changed (780) hide show
  1. package/LICENSE +21 -0
  2. package/PORTING.md +134 -0
  3. package/README.md +207 -0
  4. package/UPSTREAM.json +64 -0
  5. package/agents/Explore.md +15 -0
  6. package/agents/a11y-review.md +118 -0
  7. package/agents/adr-author.md +70 -0
  8. package/agents/ai-provenance-review.md +120 -0
  9. package/agents/angular-reactivity-review.md +95 -0
  10. package/agents/arch-review.md +135 -0
  11. package/agents/architect.md +78 -0
  12. package/agents/autoship-batch-proposer.md +69 -0
  13. package/agents/claude-setup-review.md +136 -0
  14. package/agents/codebase-recon.md +184 -0
  15. package/agents/component-architecture-review.md +119 -0
  16. package/agents/concurrency-review.md +109 -0
  17. package/agents/correctness-review.md +290 -0
  18. package/agents/data-flow-tracer.md +120 -0
  19. package/agents/doc-review.md +165 -0
  20. package/agents/domain-review.md +136 -0
  21. package/agents/general-purpose.md +10 -0
  22. package/agents/gherkin-quality-critic.md +113 -0
  23. package/agents/js-fp-review.md +114 -0
  24. package/agents/mutation-kill.md +684 -0
  25. package/agents/naming-review.md +142 -0
  26. package/agents/orchestrator.md +339 -0
  27. package/agents/performance-review.md +105 -0
  28. package/agents/plan-review-acceptance.md +115 -0
  29. package/agents/plan-review-design.md +90 -0
  30. package/agents/plan-review-parallelization.md +84 -0
  31. package/agents/plan-review-strategic.md +96 -0
  32. package/agents/plan-review-ux.md +110 -0
  33. package/agents/platform-engineer.md +64 -0
  34. package/agents/product-manager.md +68 -0
  35. package/agents/progress-guardian.md +79 -0
  36. package/agents/qa-engineer.md +289 -0
  37. package/agents/quality-reviewer.md +132 -0
  38. package/agents/react-reactivity-review.md +102 -0
  39. package/agents/refactor-opportunity-review.md +128 -0
  40. package/agents/security-engineer.md +60 -0
  41. package/agents/security-review.md +218 -0
  42. package/agents/session-analysis.md +95 -0
  43. package/agents/software-engineer.md +105 -0
  44. package/agents/spec-compliance-review.md +100 -0
  45. package/agents/spec-reviewer.md +114 -0
  46. package/agents/structure-review.md +146 -0
  47. package/agents/tech-writer.md +84 -0
  48. package/agents/test-review.md +246 -0
  49. package/agents/test-smell-review.md +188 -0
  50. package/agents/token-efficiency-review.md +139 -0
  51. package/agents/ui-ux-designer.md +54 -0
  52. package/agents/vue-reactivity-review.md +95 -0
  53. package/bin/__pycache__/claudecpython-314.pyc +0 -0
  54. package/bin/claude +258 -0
  55. package/docs/upstream/.pages +1 -0
  56. package/docs/upstream/CHANGELOG.md +2586 -0
  57. package/docs/upstream/README.md +155 -0
  58. package/docs/upstream/agent-architecture.md +214 -0
  59. package/docs/upstream/agent_info.md +187 -0
  60. package/docs/upstream/artifact-migration.md +124 -0
  61. package/docs/upstream/code-intelligence-nudge.md +149 -0
  62. package/docs/upstream/code-review-process.md +294 -0
  63. package/docs/upstream/concurrent-use.md +73 -0
  64. package/docs/upstream/context-management.md +111 -0
  65. package/docs/upstream/developer-notes.md +280 -0
  66. package/docs/upstream/diagrams/architecture-overview.svg +101 -0
  67. package/docs/upstream/diagrams/review-dispatch.svg +139 -0
  68. package/docs/upstream/diagrams/team-agents.svg +128 -0
  69. package/docs/upstream/diagrams/test-improve-flow.svg +166 -0
  70. package/docs/upstream/diagrams/workflow-linear.svg +66 -0
  71. package/docs/upstream/diagrams/workflow-three-phase.svg +200 -0
  72. package/docs/upstream/eval-maintenance.md +95 -0
  73. package/docs/upstream/eval-running-guide.md +147 -0
  74. package/docs/upstream/eval-system.md +291 -0
  75. package/docs/upstream/session-review-oss-complements.md +75 -0
  76. package/docs/upstream/session-review.md +212 -0
  77. package/docs/upstream/skills.md +188 -0
  78. package/docs/upstream/team-structure.md +21 -0
  79. package/docs/upstream/telemetry-ci-access.md +129 -0
  80. package/docs/upstream/telemetry-repo-security.md +120 -0
  81. package/docs/upstream/test-evaluation.md +277 -0
  82. package/docs/upstream/test-improve.md +154 -0
  83. package/docs/upstream/triage-workflow.md +282 -0
  84. package/docs/upstream/workflows.md +289 -0
  85. package/extensions/dev-team/index.ts +539 -0
  86. package/extensions/dev-team/lib/agents.ts +272 -0
  87. package/extensions/dev-team/lib/ai-credits.ts +92 -0
  88. package/extensions/dev-team/lib/autocompact.ts +81 -0
  89. package/extensions/dev-team/lib/child-run.ts +102 -0
  90. package/extensions/dev-team/lib/config.ts +236 -0
  91. package/extensions/dev-team/lib/gh-command.ts +103 -0
  92. package/extensions/dev-team/lib/github-style.ts +307 -0
  93. package/extensions/dev-team/lib/hooks.ts +350 -0
  94. package/extensions/dev-team/lib/metrics.ts +115 -0
  95. package/extensions/dev-team/lib/safe-read.ts +49 -0
  96. package/extensions/dev-team/lib/session-files.ts +57 -0
  97. package/extensions/dev-team/lib/session-spend.ts +123 -0
  98. package/extensions/dev-team/lib/shell-scan.ts +205 -0
  99. package/extensions/dev-team/lib/skills.ts +213 -0
  100. package/extensions/dev-team/lib/subagent-render.ts +245 -0
  101. package/extensions/dev-team/lib/subagent-types.ts +164 -0
  102. package/extensions/dev-team/lib/subagent.ts +596 -0
  103. package/extensions/dev-team/lib/terminal-text.ts +54 -0
  104. package/extensions/dev-team/lib/tools-misc.ts +152 -0
  105. package/extensions/dev-team/lib/transcript.ts +110 -0
  106. package/extensions/dev-team/lib/trust.ts +52 -0
  107. package/extensions/dev-team/lib/usage-breakdown.ts +176 -0
  108. package/extensions/dev-team/lib/usage-chart.ts +153 -0
  109. package/extensions/dev-team/lib/usage-command.ts +107 -0
  110. package/extensions/dev-team/lib/usage-history.ts +203 -0
  111. package/extensions/dev-team/lib/usage-render.ts +225 -0
  112. package/extensions/dev-team/lib/usage-split-bar.ts +127 -0
  113. package/extensions/dev-team/lib/usage-state.ts +116 -0
  114. package/extensions/dev-team/lib/usage-text.ts +159 -0
  115. package/extensions/dev-team/lib/usage-view.ts +109 -0
  116. package/hooks/__pycache__/refactor_test_freeze_guard.cpython-314.pyc +0 -0
  117. package/hooks/agent_dispatch_ledger.py +190 -0
  118. package/hooks/autocompact_setup_nudge.py +99 -0
  119. package/hooks/bash_retry_guard.py +228 -0
  120. package/hooks/boundary_events_write_guard.py +352 -0
  121. package/hooks/code_intelligence_nudge.py +293 -0
  122. package/hooks/code_intelligence_turn_mark.py +317 -0
  123. package/hooks/codegraph_bootstrap.py +139 -0
  124. package/hooks/contract_version_guard.py +362 -0
  125. package/hooks/cost_meter.py +106 -0
  126. package/hooks/destructive-commands.json +62 -0
  127. package/hooks/destructive_guard.py +477 -0
  128. package/hooks/eval_compliance_check.py +440 -0
  129. package/hooks/guards.json +17 -0
  130. package/hooks/hooks.json +323 -0
  131. package/hooks/internal_double_gate.py +296 -0
  132. package/hooks/js_fp_review.py +212 -0
  133. package/hooks/knowledge_index.py +119 -0
  134. package/hooks/lib/__pycache__/artifact_paths.cpython-314.pyc +0 -0
  135. package/hooks/lib/__pycache__/atomic_state.cpython-314.pyc +0 -0
  136. package/hooks/lib/__pycache__/autocompact_config.cpython-314.pyc +0 -0
  137. package/hooks/lib/__pycache__/boundary_events.cpython-314.pyc +0 -0
  138. package/hooks/lib/__pycache__/doc_classification.cpython-314.pyc +0 -0
  139. package/hooks/lib/__pycache__/gh_pr_create_detect.cpython-314.pyc +0 -0
  140. package/hooks/lib/__pycache__/git_safe_diff.cpython-314.pyc +0 -0
  141. package/hooks/lib/__pycache__/instrument_log.cpython-314.pyc +0 -0
  142. package/hooks/lib/__pycache__/metrics_query.cpython-314.pyc +0 -0
  143. package/hooks/lib/__pycache__/plugin_version.cpython-314.pyc +0 -0
  144. package/hooks/lib/__pycache__/pre_commit_doc_classifier.cpython-314.pyc +0 -0
  145. package/hooks/lib/__pycache__/review_agent_registry.cpython-314.pyc +0 -0
  146. package/hooks/lib/__pycache__/review_gate_corroboration.cpython-314.pyc +0 -0
  147. package/hooks/lib/__pycache__/review_gate_hash.cpython-314.pyc +0 -0
  148. package/hooks/lib/__pycache__/review_verdicts.cpython-314.pyc +0 -0
  149. package/hooks/lib/__pycache__/stdin_json.cpython-314.pyc +0 -0
  150. package/hooks/lib/__pycache__/stryker_invocation.cpython-314.pyc +0 -0
  151. package/hooks/lib/__pycache__/telemetry_consent.cpython-314.pyc +0 -0
  152. package/hooks/lib/__pycache__/test_file_classify.cpython-314.pyc +0 -0
  153. package/hooks/lib/__pycache__/token_efficiency_limits.cpython-314.pyc +0 -0
  154. package/hooks/lib/__pycache__/verify_guard_state.cpython-314.pyc +0 -0
  155. package/hooks/lib/__pycache__/xunit_v3_operator_gate.cpython-314.pyc +0 -0
  156. package/hooks/lib/agent_skill_hints.py +74 -0
  157. package/hooks/lib/artifact_paths.py +263 -0
  158. package/hooks/lib/atomic_state.py +557 -0
  159. package/hooks/lib/autocompact_config.py +103 -0
  160. package/hooks/lib/autoship_log.py +106 -0
  161. package/hooks/lib/banned_scripts_policy.py +51 -0
  162. package/hooks/lib/boundary_events.py +436 -0
  163. package/hooks/lib/build_knowledge_index.py +504 -0
  164. package/hooks/lib/build_skills_index.py +361 -0
  165. package/hooks/lib/build_state.py +116 -0
  166. package/hooks/lib/classify_ship_outcome.py +126 -0
  167. package/hooks/lib/config_changelog_schema.py +115 -0
  168. package/hooks/lib/cost_meter.py +955 -0
  169. package/hooks/lib/doc_classification.py +116 -0
  170. package/hooks/lib/gh_pr_create_detect.py +136 -0
  171. package/hooks/lib/git_safe_diff.py +123 -0
  172. package/hooks/lib/instrument_log.py +66 -0
  173. package/hooks/lib/iteration_journal_gate.py +197 -0
  174. package/hooks/lib/knowledge_index_paths.py +88 -0
  175. package/hooks/lib/mcp_json_repowise.py +177 -0
  176. package/hooks/lib/metrics_query.py +202 -0
  177. package/hooks/lib/minimal_yaml.py +434 -0
  178. package/hooks/lib/plugin_version.py +142 -0
  179. package/hooks/lib/pre_commit_detect.py +537 -0
  180. package/hooks/lib/pre_commit_doc_classifier.py +126 -0
  181. package/hooks/lib/pricing.py +118 -0
  182. package/hooks/lib/report_pdf.py +371 -0
  183. package/hooks/lib/review_agent_registry.py +142 -0
  184. package/hooks/lib/review_dispatch_ledger.py +101 -0
  185. package/hooks/lib/review_gate_corroboration.py +521 -0
  186. package/hooks/lib/review_gate_hash.py +252 -0
  187. package/hooks/lib/review_gate_normalized_hash.py +1115 -0
  188. package/hooks/lib/review_verdicts.py +301 -0
  189. package/hooks/lib/run_report.py +160 -0
  190. package/hooks/lib/skill_categories.yaml +125 -0
  191. package/hooks/lib/stdin_json.py +57 -0
  192. package/hooks/lib/stryker_invocation.py +102 -0
  193. package/hooks/lib/telemetry_consent.py +41 -0
  194. package/hooks/lib/telemetry_report.py +108 -0
  195. package/hooks/lib/test_file_classify.py +160 -0
  196. package/hooks/lib/token_efficiency_limits.py +51 -0
  197. package/hooks/lib/turn_identity.py +77 -0
  198. package/hooks/lib/verify_guard_state.py +110 -0
  199. package/hooks/lib/workflow_state.py +206 -0
  200. package/hooks/lib/xunit_v3_operator_gate.py +596 -0
  201. package/hooks/mcp_json_repowise_nudge.py +74 -0
  202. package/hooks/mutation_adapters/__init__.py +7 -0
  203. package/hooks/mutation_adapters/__pycache__/__init__.cpython-314.pyc +0 -0
  204. package/hooks/mutation_adapters/__pycache__/lib.cpython-314.pyc +0 -0
  205. package/hooks/mutation_adapters/__pycache__/mutmut.cpython-314.pyc +0 -0
  206. package/hooks/mutation_adapters/__pycache__/pitest.cpython-314.pyc +0 -0
  207. package/hooks/mutation_adapters/__pycache__/stryker.cpython-314.pyc +0 -0
  208. package/hooks/mutation_adapters/__pycache__/stryker_net.cpython-314.pyc +0 -0
  209. package/hooks/mutation_adapters/lib.py +478 -0
  210. package/hooks/mutation_adapters/mutmut.py +188 -0
  211. package/hooks/mutation_adapters/pitest.py +266 -0
  212. package/hooks/mutation_adapters/stryker.py +157 -0
  213. package/hooks/mutation_adapters/stryker_net.py +264 -0
  214. package/hooks/mutation_gate.py +193 -0
  215. package/hooks/mutation_testing_smoke_gate.py +371 -0
  216. package/hooks/pending_review_notify.py +121 -0
  217. package/hooks/phase_marker.py +138 -0
  218. package/hooks/post_compact_state_reinject.py +180 -0
  219. package/hooks/post_format.py +115 -0
  220. package/hooks/pre_commit_knowledge_index.py +128 -0
  221. package/hooks/pre_commit_review.py +66 -0
  222. package/hooks/pre_pr_review.py +694 -0
  223. package/hooks/pre_tool_guard.py +405 -0
  224. package/hooks/py.sh +73 -0
  225. package/hooks/refactor-bash-write-patterns.json +29 -0
  226. package/hooks/refactor_test_bash_guard.py +253 -0
  227. package/hooks/refactor_test_freeze_guard.py +139 -0
  228. package/hooks/refactor_test_revert_guard.py +186 -0
  229. package/hooks/repo_review_nudge.py +287 -0
  230. package/hooks/review_verdict_recorder.py +464 -0
  231. package/hooks/scan_bash_command_for_banned_scripts.py +428 -0
  232. package/hooks/scan_worktree_for_banned_scripts.py +238 -0
  233. package/hooks/session_learning_trigger.py +248 -0
  234. package/hooks/skills_index.py +126 -0
  235. package/hooks/stryker_xunit_shim_guard.py +571 -0
  236. package/hooks/subagent_completion_guard.py +309 -0
  237. package/hooks/subagent_skill_context.py +139 -0
  238. package/hooks/task_completion_metrics.py +216 -0
  239. package/hooks/tdd_guard.py +229 -0
  240. package/hooks/telemetry.py +341 -0
  241. package/hooks/token_efficiency_review.py +194 -0
  242. package/hooks/verify_guard.py +183 -0
  243. package/hooks/verify_guard_edit_marker.py +73 -0
  244. package/hooks/version_check.py +173 -0
  245. package/knowledge/accepted-risks-schema.md +98 -0
  246. package/knowledge/adr-decision-criteria.md +64 -0
  247. package/knowledge/adversarial-review-protocol.md +139 -0
  248. package/knowledge/agent-registry.md +228 -0
  249. package/knowledge/agent-review-methodology.md +80 -0
  250. package/knowledge/ai-friendly-repo-guidelines.md +67 -0
  251. package/knowledge/architecture-assessment.md +96 -0
  252. package/knowledge/artifact-lifecycle.md +57 -0
  253. package/knowledge/cd-maturity-model.md +82 -0
  254. package/knowledge/cd-test-architecture.md +190 -0
  255. package/knowledge/ci-cd-file-scope.md +24 -0
  256. package/knowledge/codegraph-vs-graphify.md +192 -0
  257. package/knowledge/component-test-patterns.md +139 -0
  258. package/knowledge/database-change-management.md +80 -0
  259. package/knowledge/database-test-patterns.md +79 -0
  260. package/knowledge/decision-defaults.md +88 -0
  261. package/knowledge/dependency-breaking-techniques.md +116 -0
  262. package/knowledge/deployment-pipeline.md +86 -0
  263. package/knowledge/design-smells.md +122 -0
  264. package/knowledge/directory-enumeration.md +38 -0
  265. package/knowledge/domain-modeling.md +123 -0
  266. package/knowledge/evidence-bundle.md +90 -0
  267. package/knowledge/exploratory-testing-field-guide.md +122 -0
  268. package/knowledge/failure-routing.md +28 -0
  269. package/knowledge/fixture-construction.md +56 -0
  270. package/knowledge/frontend-component-architecture.md +139 -0
  271. package/knowledge/gherkin-quality-review-dispatch.md +135 -0
  272. package/knowledge/index.json +6766 -0
  273. package/knowledge/internal-collaborator-doubling.md +101 -0
  274. package/knowledge/legacy-test-strategy.md +71 -0
  275. package/knowledge/long-run-waiting.md +66 -0
  276. package/knowledge/microservice-testing.md +71 -0
  277. package/knowledge/model-pricing.json +23 -0
  278. package/knowledge/mutation-score-formulas.md +60 -0
  279. package/knowledge/object-calisthenics.md +147 -0
  280. package/knowledge/oracle-provenance.md +94 -0
  281. package/knowledge/orchestrator-script-implementation.md +185 -0
  282. package/knowledge/owasp-detection.md +148 -0
  283. package/knowledge/plan-review-rubric.md +56 -0
  284. package/knowledge/proxy-connectivity.md +62 -0
  285. package/knowledge/reactive-effect-patterns.md +73 -0
  286. package/knowledge/recon-inventory-excludes.txt +32 -0
  287. package/knowledge/references/bdd-value-guide.md +61 -0
  288. package/knowledge/references/csharp-http-client-testing.md +264 -0
  289. package/knowledge/release-strategies.md +74 -0
  290. package/knowledge/report-output-location.md +117 -0
  291. package/knowledge/report-pdf-integration.md +63 -0
  292. package/knowledge/report-print.css +129 -0
  293. package/knowledge/report-template.md +114 -0
  294. package/knowledge/report-to-pdf.md +69 -0
  295. package/knowledge/request-processing-flow.md +63 -0
  296. package/knowledge/result-verification.md +52 -0
  297. package/knowledge/review-agent-output-contract.md +121 -0
  298. package/knowledge/review-lens-classification.md +113 -0
  299. package/knowledge/review-rubric.md +62 -0
  300. package/knowledge/review-template.md +104 -0
  301. package/knowledge/rule-fixtures/A02.insecure-random-js/negative.js +1 -0
  302. package/knowledge/rule-fixtures/A02.insecure-random-js/positive.js +1 -0
  303. package/knowledge/rule-fixtures/A02.weak-hashing-md5/negative.py +1 -0
  304. package/knowledge/rule-fixtures/A02.weak-hashing-md5/positive.py +1 -0
  305. package/knowledge/rule-fixtures/A03.command-injection/negative.js +1 -0
  306. package/knowledge/rule-fixtures/A03.command-injection/positive.js +1 -0
  307. package/knowledge/rule-fixtures/A03.sql-injection/negative.js +1 -0
  308. package/knowledge/rule-fixtures/A03.sql-injection/positive.js +1 -0
  309. package/knowledge/rule-fixtures/A03.xss-innerhtml/negative.js +1 -0
  310. package/knowledge/rule-fixtures/A03.xss-innerhtml/positive.js +1 -0
  311. package/knowledge/rule-fixtures/A05.cors-wildcard/negative.js +1 -0
  312. package/knowledge/rule-fixtures/A05.cors-wildcard/positive.js +1 -0
  313. package/knowledge/rule-fixtures/A05.default-credentials/negative.js +1 -0
  314. package/knowledge/rule-fixtures/A05.default-credentials/positive.js +1 -0
  315. package/knowledge/rule-fixtures/A07.jwt-alg-none/negative.js +1 -0
  316. package/knowledge/rule-fixtures/A07.jwt-alg-none/positive.js +1 -0
  317. package/knowledge/rule-fixtures/A08.binary-formatter/negative.cs +1 -0
  318. package/knowledge/rule-fixtures/A08.binary-formatter/positive.cs +1 -0
  319. package/knowledge/rule-fixtures/A08.js-eval/negative.js +1 -0
  320. package/knowledge/rule-fixtures/A08.js-eval/positive.js +1 -0
  321. package/knowledge/rule-fixtures/A08.object-input-stream/negative.java +1 -0
  322. package/knowledge/rule-fixtures/A08.object-input-stream/positive.java +1 -0
  323. package/knowledge/schemas/disposition-register-v1.json +65 -0
  324. package/knowledge/schemas/recon-envelope-v1.json +198 -0
  325. package/knowledge/schemas/unified-finding-v1.json +72 -0
  326. package/knowledge/security-primitives-contract.md +301 -0
  327. package/knowledge/security-review-rule-map.yaml +107 -0
  328. package/knowledge/skills-registry.md +72 -0
  329. package/knowledge/task-size-classifier.md +103 -0
  330. package/knowledge/telemetry-schema.md +881 -0
  331. package/knowledge/test-automation-maturity.md +56 -0
  332. package/knowledge/test-automation-principles.md +71 -0
  333. package/knowledge/test-cadence-tradeoffs.md +68 -0
  334. package/knowledge/test-doubles.md +105 -0
  335. package/knowledge/test-file-indicators.md +22 -0
  336. package/knowledge/test-layer-gates.md +35 -0
  337. package/knowledge/test-matrix-examples/django-batch.md +24 -0
  338. package/knowledge/test-matrix-examples/dotnet-grpc-fronting-api.md +90 -0
  339. package/knowledge/test-matrix-examples/dotnet-http-consumer.md +131 -0
  340. package/knowledge/test-matrix-examples/react-node-spa.md +24 -0
  341. package/knowledge/test-matrix-examples/spring-boot-service.md +25 -0
  342. package/knowledge/test-matrix-examples/ssr-htmx.md +24 -0
  343. package/knowledge/test-organization.md +70 -0
  344. package/knowledge/test-pyramid.md +84 -0
  345. package/knowledge/test-refactoring.md +67 -0
  346. package/knowledge/test-review-division-of-labor.md +85 -0
  347. package/knowledge/test-smells.md +80 -0
  348. package/knowledge/test-stack-profiles/bdd-frameworks.md +235 -0
  349. package/knowledge/test-stack-profiles/django.md +13 -0
  350. package/knowledge/test-stack-profiles/dotnet.md +18 -0
  351. package/knowledge/test-stack-profiles/go.md +16 -0
  352. package/knowledge/test-stack-profiles/node.md +16 -0
  353. package/knowledge/test-stack-profiles/react.md +12 -0
  354. package/knowledge/test-stack-profiles/spring-boot.md +16 -0
  355. package/knowledge/test-stack-profiles/ssr-htmx.md +14 -0
  356. package/knowledge/test-stack-profiles/vue.md +12 -0
  357. package/knowledge/test-strategy.md +70 -0
  358. package/knowledge/testability-patterns.md +240 -0
  359. package/knowledge/testing-quadrants.md +44 -0
  360. package/knowledge/testing-techniques/approval.md +15 -0
  361. package/knowledge/testing-techniques/chaos.md +17 -0
  362. package/knowledge/testing-techniques/fuzz.md +15 -0
  363. package/knowledge/testing-techniques/property-based.md +15 -0
  364. package/knowledge/testing-techniques/schema-validation.md +15 -0
  365. package/knowledge/testing-techniques/screenshot.md +15 -0
  366. package/knowledge/three-phase-workflow.md +198 -0
  367. package/knowledge/value-patterns.md +55 -0
  368. package/knowledge/verification-mode.md +116 -0
  369. package/knowledge/virtual-service-libraries.md +75 -0
  370. package/knowledge/wave-consolidation-guidance.md +21 -0
  371. package/overrides/agents/Explore.md +15 -0
  372. package/overrides/agents/general-purpose.md +10 -0
  373. package/overrides/notes/autoship.md +6 -0
  374. package/overrides/notes/issues-from-assessment.md +3 -0
  375. package/overrides/notes/issues-from-plan.md +3 -0
  376. package/overrides/notes/mutation-night-watch.md +3 -0
  377. package/overrides/notes/mutation-testing.md +3 -0
  378. package/overrides/notes/pr.md +7 -0
  379. package/overrides/notes/project-init.md +6 -0
  380. package/overrides/notes/setup.md +13 -0
  381. package/overrides/notes/specs.md +3 -0
  382. package/overrides/skills/headless-run/SKILL.md +45 -0
  383. package/overrides/skills/upgrade/SKILL.md +30 -0
  384. package/overrides/skills/version/SKILL.md +25 -0
  385. package/package.json +36 -0
  386. package/scripts/authoring_digest.py +93 -0
  387. package/scripts/autoship_discover.py +121 -0
  388. package/scripts/autoship_group.py +409 -0
  389. package/scripts/autoship_proposals.py +494 -0
  390. package/scripts/autoship_queue.py +291 -0
  391. package/scripts/autoship_reclaim.py +495 -0
  392. package/scripts/build_jobs.py +108 -0
  393. package/scripts/build_rollback_point.py +240 -0
  394. package/scripts/build_slice_scope.py +157 -0
  395. package/scripts/build_wave.py +109 -0
  396. package/scripts/build_wave_reconcile.py +252 -0
  397. package/scripts/build_worktree_baseref.py +113 -0
  398. package/scripts/check_agent_scope.py +117 -0
  399. package/scripts/check_agent_tool_mapping.py +213 -0
  400. package/scripts/check_review_agent_mcp_tools.py +317 -0
  401. package/scripts/check_security_assessment_mcp_tools.py +165 -0
  402. package/scripts/checkpoint_abort.py +502 -0
  403. package/scripts/claude_setup_review.py +438 -0
  404. package/scripts/codebase_recon.py +556 -0
  405. package/scripts/coverage_config.py +623 -0
  406. package/scripts/coverage_delta_steering.py +330 -0
  407. package/scripts/coverage_discovery_dotnet.py +315 -0
  408. package/scripts/coverage_discovery_java.py +742 -0
  409. package/scripts/coverage_discovery_js.py +546 -0
  410. package/scripts/coverage_gap_ranking.py +556 -0
  411. package/scripts/coverage_readiness.py +455 -0
  412. package/scripts/coverage_report_parse.py +521 -0
  413. package/scripts/detect_bdd_convention.py +252 -0
  414. package/scripts/eval_ablation.py +376 -0
  415. package/scripts/gherkin_analysis_coverage_gate.py +306 -0
  416. package/scripts/gherkin_cross_feature_duplicate_titles_gate.py +173 -0
  417. package/scripts/gherkin_effectiveness_rollup.py +238 -0
  418. package/scripts/gherkin_failure_path_gate.py +206 -0
  419. package/scripts/gherkin_feature_merge.py +720 -0
  420. package/scripts/gherkin_stub_gate.py +163 -0
  421. package/scripts/gherkin_stub_merge.py +479 -0
  422. package/scripts/git_origin_host.py +88 -0
  423. package/scripts/install-java-static-analysis.py +110 -0
  424. package/scripts/issue_deps.py +74 -0
  425. package/scripts/lib/_bdd_markers.py +28 -0
  426. package/scripts/lib/_gherkin_text.py +93 -0
  427. package/scripts/lib/_vendored_tree.py +70 -0
  428. package/scripts/lib/autoship_state.py +397 -0
  429. package/scripts/lib/claude_md_guard.py +226 -0
  430. package/scripts/lib/deterministic_recon.py +446 -0
  431. package/scripts/lib/mcp_tool_grants.py +211 -0
  432. package/scripts/lib/plan_parse.py +386 -0
  433. package/scripts/lib/review_result.py +84 -0
  434. package/scripts/lib/review_roster.py +86 -0
  435. package/scripts/lib/session_log/__init__.py +34 -0
  436. package/scripts/lib/session_log/__pycache__/__init__.cpython-314.pyc +0 -0
  437. package/scripts/lib/session_log/__pycache__/records.cpython-314.pyc +0 -0
  438. package/scripts/lib/session_log/classify.py +231 -0
  439. package/scripts/lib/session_log/corrections.py +194 -0
  440. package/scripts/lib/session_log/discovery.py +108 -0
  441. package/scripts/lib/session_log/records.py +218 -0
  442. package/scripts/lib/session_log/redact.py +76 -0
  443. package/scripts/lib/session_log/signals.py +373 -0
  444. package/scripts/lib/session_report_downstream.py +614 -0
  445. package/scripts/lib/session_report_maintainer.py +1273 -0
  446. package/scripts/lib/session_report_shared.py +262 -0
  447. package/scripts/lib/settings_hook_guard.py +157 -0
  448. package/scripts/lib/slug.py +33 -0
  449. package/scripts/lib/stub_extractors/__init__.py +82 -0
  450. package/scripts/lib/stub_extractors/_common.py +328 -0
  451. package/scripts/lib/stub_extractors/csharp.py +19 -0
  452. package/scripts/lib/stub_extractors/go.py +173 -0
  453. package/scripts/lib/stub_extractors/java.py +18 -0
  454. package/scripts/lib/stub_extractors/jsts.py +126 -0
  455. package/scripts/mutation_stack_sections.py +149 -0
  456. package/scripts/mutation_yield_steering.py +345 -0
  457. package/scripts/orchestrator.py +895 -0
  458. package/scripts/plan_gherkin_export.py +227 -0
  459. package/scripts/plan_waves.py +208 -0
  460. package/scripts/pr_close_keyword_lint.py +108 -0
  461. package/scripts/progress_guardian.py +888 -0
  462. package/scripts/recon_inventory.py +273 -0
  463. package/scripts/review_findings_log.py +93 -0
  464. package/scripts/run_invariants.py +124 -0
  465. package/scripts/select_lenses.py +640 -0
  466. package/scripts/session_report.py +486 -0
  467. package/scripts/set_autocompact_env.py +221 -0
  468. package/scripts/ship_resume_guard.py +135 -0
  469. package/scripts/ship_review_gate.py +63 -0
  470. package/scripts/specs_convention_marker.py +103 -0
  471. package/scripts/test_improve_resume.py +277 -0
  472. package/scripts/test_review_mechanics.py +958 -0
  473. package/scripts/token_efficiency_review.py +322 -0
  474. package/scripts/verdict_scope.py +285 -0
  475. package/scripts/verify_gherkin_quality_critic_isolation.py +296 -0
  476. package/scripts/verify_tier.py +157 -0
  477. package/skills/adr-tools/SKILL.md +118 -0
  478. package/skills/agent-readiness/SKILL.md +105 -0
  479. package/skills/agent-readiness/ai_friendly_analyzers.py +326 -0
  480. package/skills/agent-readiness/scanner.py +441 -0
  481. package/skills/agent-readiness/scorecard.yaml +88 -0
  482. package/skills/api-design/SKILL.md +115 -0
  483. package/skills/apply-fixes/SKILL.md +171 -0
  484. package/skills/apply-test-doubles/SKILL.md +321 -0
  485. package/skills/artifact-lifecycle/SKILL.md +127 -0
  486. package/skills/autoship/SKILL.md +1124 -0
  487. package/skills/benchmark/SKILL.md +105 -0
  488. package/skills/branch-workflow/SKILL.md +89 -0
  489. package/skills/browse/SKILL.md +184 -0
  490. package/skills/browser-testing/SKILL.md +62 -0
  491. package/skills/browser-testing/references/playwright-patterns.md +216 -0
  492. package/skills/build/SKILL.md +422 -0
  493. package/skills/build/references/static-self-heal.md +245 -0
  494. package/skills/careful/SKILL.md +72 -0
  495. package/skills/cd-test-architecture/SKILL.md +371 -0
  496. package/skills/ci-debugging/SKILL.md +105 -0
  497. package/skills/co-evolution-audit/SKILL.md +269 -0
  498. package/skills/code-review/SKILL.md +1015 -0
  499. package/skills/code-review/examples/aggregated-sample.json +56 -0
  500. package/skills/code-review/examples/sample-report.md +41 -0
  501. package/skills/code-review/output-format.md +478 -0
  502. package/skills/code-review/scripts/activation.py +86 -0
  503. package/skills/code-review/scripts/change_impact.py +357 -0
  504. package/skills/code-review/scripts/change_shape.py +372 -0
  505. package/skills/code-review/scripts/change_size.py +212 -0
  506. package/skills/code-review/scripts/changed_file_list.py +141 -0
  507. package/skills/code-review/scripts/closing_pass.py +187 -0
  508. package/skills/code-review/scripts/consolidate.py +277 -0
  509. package/skills/code-review/scripts/contract_failure_report.py +185 -0
  510. package/skills/code-review/scripts/dispatch_reconcile.py +66 -0
  511. package/skills/code-review/scripts/dispatch_waves.py +164 -0
  512. package/skills/code-review/scripts/finding_signature.py +446 -0
  513. package/skills/code-review/scripts/ledger.py +283 -0
  514. package/skills/code-review/scripts/partition.py +169 -0
  515. package/skills/code-review/scripts/render_tiered_findings.py +274 -0
  516. package/skills/code-review/scripts/repo_invariants.py +1066 -0
  517. package/skills/code-review/scripts/review_context_pack.py +306 -0
  518. package/skills/code-review/scripts/review_round_log.py +345 -0
  519. package/skills/code-review/scripts/review_value_coverage.py +297 -0
  520. package/skills/code-review/scripts/validate_review_output.py +467 -0
  521. package/skills/code-review/sliced-mode.md +205 -0
  522. package/skills/competitive-analysis/SKILL.md +191 -0
  523. package/skills/context-loading-protocol/SKILL.md +157 -0
  524. package/skills/continue/SKILL.md +90 -0
  525. package/skills/cost-report/SKILL.md +178 -0
  526. package/skills/coverage-baseline/SKILL.md +335 -0
  527. package/skills/coverage-baseline/references/multi-project-discovery.md +202 -0
  528. package/skills/coverage-delta/SKILL.md +181 -0
  529. package/skills/coverage-delta/references/mutation-gate.md +70 -0
  530. package/skills/design-doc/SKILL.md +95 -0
  531. package/skills/design-interrogation/SKILL.md +89 -0
  532. package/skills/design-it-twice/SKILL.md +91 -0
  533. package/skills/docker-image-audit/SKILL.md +108 -0
  534. package/skills/docker-image-audit/references/install-guide.md +64 -0
  535. package/skills/docker-image-audit/references/report-template.md +73 -0
  536. package/skills/docker-image-create/SKILL.md +185 -0
  537. package/skills/domain-analysis/SKILL.md +183 -0
  538. package/skills/domain-driven-design/SKILL.md +194 -0
  539. package/skills/exploratory-testing/SKILL.md +108 -0
  540. package/skills/explore/SKILL.md +51 -0
  541. package/skills/farley-score/SKILL.md +165 -0
  542. package/skills/feature-file-validation/SKILL.md +78 -0
  543. package/skills/feature-file-validation/references/validation-rules.md +115 -0
  544. package/skills/feedback-learning/SKILL.md +414 -0
  545. package/skills/fix/SKILL.md +450 -0
  546. package/skills/freeze/SKILL.md +68 -0
  547. package/skills/frontend-architecture/SKILL.md +113 -0
  548. package/skills/gherkin-derive/SKILL.md +630 -0
  549. package/skills/gherkin-public/SKILL.md +266 -0
  550. package/skills/governance-compliance/SKILL.md +150 -0
  551. package/skills/guard/SKILL.md +75 -0
  552. package/skills/handoff/SKILL.md +139 -0
  553. package/skills/handoff/references/summary-templates.md +242 -0
  554. package/skills/harness-audit/SKILL.md +751 -0
  555. package/skills/harness-audit/scripts/lesson_validate.py +386 -0
  556. package/skills/harness-audit/scripts/redundancy_criterion.py +188 -0
  557. package/skills/headless-run/SKILL.md +45 -0
  558. package/skills/headless-run/scripts/isolated_dispatch.py +381 -0
  559. package/skills/help/SKILL.md +72 -0
  560. package/skills/hexagonal-architecture/SKILL.md +85 -0
  561. package/skills/human-oversight-protocol/SKILL.md +224 -0
  562. package/skills/issues-from-assessment/SKILL.md +223 -0
  563. package/skills/issues-from-plan/SKILL.md +133 -0
  564. package/skills/legacy-code/SKILL.md +132 -0
  565. package/skills/mermaid-diagramming/SKILL.md +120 -0
  566. package/skills/mutation-night-watch/SKILL.md +154 -0
  567. package/skills/mutation-night-watch/references/scheduling.md +135 -0
  568. package/skills/mutation-testing/SKILL.md +396 -0
  569. package/skills/mutation-testing/references/languages/csharp-stryker-net.md +676 -0
  570. package/skills/mutation-testing/references/languages/go-go-mutesting.md +95 -0
  571. package/skills/mutation-testing/references/languages/java-pitest.md +77 -0
  572. package/skills/mutation-testing/references/languages/javascript-stryker.md +188 -0
  573. package/skills/mutation-testing/references/languages/python-mutmut.md +97 -0
  574. package/skills/mutation-testing/references/time-estimation.md +34 -0
  575. package/skills/mutation-testing/references/tool-detection.md +15 -0
  576. package/skills/mutation-testing/references/workflow-callers.md +23 -0
  577. package/skills/mutation-testing/scripts/__pycache__/xunit_v3_feature_detector.cpython-314.pyc +0 -0
  578. package/skills/mutation-testing/scripts/csharp_stryker_net_slice_runner.py +635 -0
  579. package/skills/mutation-testing/scripts/csharp_stryker_net_status_loop.py +525 -0
  580. package/skills/mutation-testing/scripts/csharp_stryker_net_wrapper.py +681 -0
  581. package/skills/mutation-testing/scripts/mutation_baseline_reuse.py +292 -0
  582. package/skills/mutation-testing/scripts/mutation_exclude_policy.py +268 -0
  583. package/skills/mutation-testing/scripts/mutation_feasibility_gate.py +463 -0
  584. package/skills/mutation-testing/scripts/mutation_kill_headless.py +331 -0
  585. package/skills/mutation-testing/scripts/mutation_kill_insert.py +199 -0
  586. package/skills/mutation-testing/scripts/mutation_kill_insert_python.py +150 -0
  587. package/skills/mutation-testing/scripts/mutation_kill_loop.py +869 -0
  588. package/skills/mutation-testing/scripts/mutation_kill_loop_python.py +949 -0
  589. package/skills/mutation-testing/scripts/mutation_kill_retry.py +592 -0
  590. package/skills/mutation-testing/scripts/mutation_kill_shared.py +620 -0
  591. package/skills/mutation-testing/scripts/mutation_nightwatch.py +462 -0
  592. package/skills/mutation-testing/scripts/mutation_nightwatch_stacks.py +425 -0
  593. package/skills/mutation-testing/scripts/mutation_report.py +743 -0
  594. package/skills/mutation-testing/scripts/mutation_report_cli.py +175 -0
  595. package/skills/mutation-testing/scripts/mutation_safety_gate.py +69 -0
  596. package/skills/mutation-testing/scripts/stryker_shard_pipeline.py +847 -0
  597. package/skills/mutation-testing/scripts/stryker_shard_setup.py +440 -0
  598. package/skills/mutation-testing/scripts/stryker_timeout_retry.py +142 -0
  599. package/skills/mutation-testing/scripts/xunit_v3_feature_detector.py +341 -0
  600. package/skills/performance-benchmark/SKILL.md +174 -0
  601. package/skills/performance-benchmark/examples/report-format.md +43 -0
  602. package/skills/performance-benchmark/references/benchmark-script.md +169 -0
  603. package/skills/performance-metrics/SKILL.md +265 -0
  604. package/skills/plan/SKILL.md +199 -0
  605. package/skills/plan/references/gherkin-persistence.md +43 -0
  606. package/skills/plan/references/plan-template.md +182 -0
  607. package/skills/pr/SKILL.md +289 -0
  608. package/skills/pr/scripts/gate_retry_state.py +368 -0
  609. package/skills/project-init/README.md +141 -0
  610. package/skills/project-init/SKILL.md +1197 -0
  611. package/skills/project-init/evals/evals.json +200 -0
  612. package/skills/project-init/references/capability-tools.md +55 -0
  613. package/skills/project-init/references/configs.md +221 -0
  614. package/skills/property-based-testing/SKILL.md +121 -0
  615. package/skills/property-based-testing/fixtures/invariant_fixture.py +15 -0
  616. package/skills/property-based-testing/fixtures/js-roundtrip/README.md +42 -0
  617. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/LICENSE +21 -0
  618. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/README.md +263 -0
  619. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/fast-check.d.ts +5165 -0
  620. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/fast-check.js +12147 -0
  621. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/package.json +3 -0
  622. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/cjs/types57/fast-check.d.ts +5165 -0
  623. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/fast-check.d.ts +5165 -0
  624. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/fast-check.js +12011 -0
  625. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/rolldown-runtime-D7D4PA-g.js +13 -0
  626. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/lib/types57/fast-check.d.ts +5165 -0
  627. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/fast-check/package.json +94 -0
  628. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/LICENSE +21 -0
  629. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/README.md +168 -0
  630. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/RandomGenerator-DcXj09Ch.d.ts +14 -0
  631. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformBigInt.d.ts +15 -0
  632. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformBigInt.js +38 -0
  633. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat32.d.ts +15 -0
  634. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat32.js +18 -0
  635. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat64.d.ts +15 -0
  636. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformFloat64.js +22 -0
  637. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformInt.d.ts +15 -0
  638. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/distribution/uniformInt.js +134 -0
  639. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/RandomGenerator-DcXj09Ch.d.ts +14 -0
  640. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.d.ts +15 -0
  641. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformBigInt.js +37 -0
  642. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.d.ts +15 -0
  643. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat32.js +17 -0
  644. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.d.ts +15 -0
  645. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformFloat64.js +21 -0
  646. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformInt.d.ts +15 -0
  647. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/distribution/uniformInt.js +133 -0
  648. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/congruential32.d.ts +7 -0
  649. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/congruential32.js +44 -0
  650. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/mersenne.d.ts +7 -0
  651. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/mersenne.js +90 -0
  652. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.d.ts +7 -0
  653. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xoroshiro128plus.js +80 -0
  654. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xorshift128plus.d.ts +7 -0
  655. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/generator/xorshift128plus.js +78 -0
  656. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/package.json +3 -0
  657. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.d.ts +16 -0
  658. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/JumpableRandomGenerator.js +0 -0
  659. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/RandomGenerator.d.ts +2 -0
  660. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/types/RandomGenerator.js +0 -0
  661. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/generateN.d.ts +6 -0
  662. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/generateN.js +8 -0
  663. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/purify.d.ts +12 -0
  664. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/purify.js +9 -0
  665. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/skipN.d.ts +6 -0
  666. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/esm/utils/skipN.js +6 -0
  667. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/congruential32.d.ts +7 -0
  668. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/congruential32.js +46 -0
  669. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/mersenne.d.ts +7 -0
  670. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/mersenne.js +92 -0
  671. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xoroshiro128plus.d.ts +7 -0
  672. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xoroshiro128plus.js +82 -0
  673. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xorshift128plus.d.ts +7 -0
  674. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/generator/xorshift128plus.js +80 -0
  675. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/JumpableRandomGenerator.d.ts +16 -0
  676. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/JumpableRandomGenerator.js +0 -0
  677. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/RandomGenerator.d.ts +2 -0
  678. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/types/RandomGenerator.js +0 -0
  679. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/generateN.d.ts +6 -0
  680. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/generateN.js +9 -0
  681. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/purify.d.ts +12 -0
  682. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/purify.js +10 -0
  683. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/skipN.d.ts +6 -0
  684. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/lib/utils/skipN.js +7 -0
  685. package/skills/property-based-testing/fixtures/js-roundtrip/node_modules/pure-rand/package.json +133 -0
  686. package/skills/property-based-testing/fixtures/js-roundtrip/package-lock.json +1179 -0
  687. package/skills/property-based-testing/fixtures/js-roundtrip/package.json +14 -0
  688. package/skills/property-based-testing/fixtures/js-roundtrip/roundtrip.js +29 -0
  689. package/skills/property-based-testing/fixtures/js-roundtrip/roundtrip.properties.test.js +16 -0
  690. package/skills/property-based-testing/fixtures/no_property_fixture.py +10 -0
  691. package/skills/property-based-testing/fixtures/roundtrip_fixture.py +16 -0
  692. package/skills/property-based-testing/references/languages/javascript.md +54 -0
  693. package/skills/property-based-testing/scripts/detect_and_dispatch.py +80 -0
  694. package/skills/property-based-testing/scripts/hypothesis_scaffold.py +276 -0
  695. package/skills/proxy-resilience/SKILL.md +84 -0
  696. package/skills/quality-gate-pipeline/SKILL.md +184 -0
  697. package/skills/quality-targets-converge/SKILL.md +254 -0
  698. package/skills/repo-review/SKILL.md +159 -0
  699. package/skills/report-pdf/SKILL.md +66 -0
  700. package/skills/review/SKILL.md +47 -0
  701. package/skills/review-agent/SKILL.md +152 -0
  702. package/skills/review-summary/SKILL.md +73 -0
  703. package/skills/run-report/SKILL.md +70 -0
  704. package/skills/semantic-duplication-scan/SKILL.md +337 -0
  705. package/skills/semantic-scan/SKILL.md +53 -0
  706. package/skills/semgrep-analyze/SKILL.md +139 -0
  707. package/skills/setup/SKILL.md +1122 -0
  708. package/skills/ship/SKILL.md +240 -0
  709. package/skills/source-verification/SKILL.md +210 -0
  710. package/skills/source-verification/scripts/claim_extractor.py +155 -0
  711. package/skills/specs/.size-baseline.json +4 -0
  712. package/skills/specs/SKILL.md +243 -0
  713. package/skills/specs/references/completeness-checklist.md +83 -0
  714. package/skills/specs/references/extraction.md +58 -0
  715. package/skills/specs/references/glossary.md +59 -0
  716. package/skills/specs/references/persistence.md +115 -0
  717. package/skills/specs/references/predictability-check.md +77 -0
  718. package/skills/static-analysis-integration/SKILL.md +235 -0
  719. package/skills/static-analysis-integration/adapters/_envelope.py +26 -0
  720. package/skills/static-analysis-integration/adapters/jscpd-adapter.py +66 -0
  721. package/skills/static-analysis-integration/adapters/lizard-adapter.py +81 -0
  722. package/skills/static-analysis-integration/adapters/mypy-adapter.py +50 -0
  723. package/skills/static-analysis-integration/adapters/mypy-src-layout.py +93 -0
  724. package/skills/static-analysis-integration/adapters/security-review-adapter.py +212 -0
  725. package/skills/static-analysis-integration/maintenance.md +23 -0
  726. package/skills/static-analysis-integration/references/language-setup.md +228 -0
  727. package/skills/static-analysis-integration/references/sarif-parser.md +124 -0
  728. package/skills/static-analysis-integration/references/security-review-adapter.md +118 -0
  729. package/skills/static-analysis-integration/references/tool-configs.md +617 -0
  730. package/skills/static-analysis-integration/rulesets/pmd-quickstart.xml +24 -0
  731. package/skills/stryker-xunit-v2-shim/SKILL.md +274 -0
  732. package/skills/stryker-xunit-v2-shim/references/shim-howto.md +256 -0
  733. package/skills/stryker-xunit-v2-shim/scripts/generate_shim.py +143 -0
  734. package/skills/systematic-debugging/SKILL.md +130 -0
  735. package/skills/telemetry/SKILL.md +75 -0
  736. package/skills/test-audit-disable/SKILL.md +129 -0
  737. package/skills/test-design/SKILL.md +177 -0
  738. package/skills/test-design/scripts/__pycache__/internal_double_detector.cpython-314.pyc +0 -0
  739. package/skills/test-design/scripts/internal_double_detector.py +631 -0
  740. package/skills/test-design-advisor/SKILL.md +166 -0
  741. package/skills/test-driven-development/SKILL.md +169 -0
  742. package/skills/test-health/SKILL.md +262 -0
  743. package/skills/test-improve/SKILL.md +239 -0
  744. package/skills/test-improve/references/phase-0-approach-contract.md +228 -0
  745. package/skills/test-improve/references/phase-1-analyze.md +131 -0
  746. package/skills/test-improve/references/phase-2-baseline.md +121 -0
  747. package/skills/test-improve/references/phase-3-derive-gherkin.md +53 -0
  748. package/skills/test-improve/references/phase-4-plan-fixes.md +34 -0
  749. package/skills/test-improve/references/phase-5-improve.md +215 -0
  750. package/skills/test-improve/references/phase-6-refactor-decision.md +45 -0
  751. package/skills/test-improve/references/phase-7-refactor.md +44 -0
  752. package/skills/test-improve/references/phase-8-validate.md +66 -0
  753. package/skills/test-improve/references/phase-9-close-out-prompt.md +11 -0
  754. package/skills/test-improve/references/phase-9-report.md +62 -0
  755. package/skills/test-improve/references/review-loop.md +92 -0
  756. package/skills/test-improve/templates/executive-summary.md +123 -0
  757. package/skills/threat-modeling/SKILL.md +108 -0
  758. package/skills/triage/SKILL.md +211 -0
  759. package/skills/ubiquitous-language/SKILL.md +192 -0
  760. package/skills/ubiquitous-language/scripts/collect_domain_signals.py +300 -0
  761. package/skills/unfreeze/SKILL.md +37 -0
  762. package/skills/upgrade/SKILL.md +31 -0
  763. package/skills/upgrade/scripts/check_version_drift.py +113 -0
  764. package/skills/upgrade/scripts/enable_autoupdate.py +149 -0
  765. package/skills/version/SKILL.md +25 -0
  766. package/sync/__pycache__/sync_upstream.cpython-314.pyc +0 -0
  767. package/sync/sync_upstream.py +293 -0
  768. package/templates/ACCEPTED-RISKS.md.tmpl +46 -0
  769. package/templates/agents/agent-template.md +151 -0
  770. package/templates/agents/angular-testing.md +66 -0
  771. package/templates/agents/csharp-quality.md +63 -0
  772. package/templates/agents/esm-enforcer.md +52 -0
  773. package/templates/agents/front-end-testing.md +65 -0
  774. package/templates/agents/go-quality.md +65 -0
  775. package/templates/agents/python-quality.md +62 -0
  776. package/templates/agents/react-testing.md +61 -0
  777. package/templates/agents/ts-enforcer.md +60 -0
  778. package/templates/agents/twelve-factor-audit.md +49 -0
  779. package/tools/entropy-check.py +250 -0
  780. 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")