claude-dev-env 1.94.0 → 2.0.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 (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. package/hooks/validators/verify_paths.py +0 -57
@@ -0,0 +1,326 @@
1
+ """Validate the resolved file set and emit a diff-scoped violation report.
2
+
3
+ ``run_gate`` reads each eligible code file, runs the enforcer's
4
+ ``validate_content`` plus the wrapper plumb-through check, partitions the result
5
+ into blocking versus advisory by touched line, prints the report, and always
6
+ names how many files it inspected.
7
+ """
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ from pr_loop_shared_constants.code_rules_gate_constants import INSPECTED_COUNT_MESSAGE
13
+ from pr_loop_shared_constants.terminology_sweep_constants import (
14
+ TERMINOLOGY_SWEEP_GATE_HEADER,
15
+ )
16
+
17
+ from code_rules_gate_parts.added_line_maps import _resolved_under_root
18
+ from code_rules_gate_parts.enforcer_loading import ValidateContentCallable
19
+ from code_rules_gate_parts.git_blob_readers import (
20
+ read_prior_committed_content,
21
+ read_staged_content,
22
+ staged_blob_exists,
23
+ )
24
+ from code_rules_gate_parts.violation_scoping import split_violations_by_scope
25
+ from code_rules_gate_parts.wrapper_plumb_check import (
26
+ check_wrapper_plumb_through,
27
+ is_code_path,
28
+ )
29
+
30
+ PartitionedViolations = tuple[dict[Path, list[str]], dict[Path, list[str]], int]
31
+
32
+
33
+ def _path_is_eligible_for_validation(
34
+ resolved_path: Path,
35
+ repository_root: Path,
36
+ should_read_staged_content: bool,
37
+ ) -> bool:
38
+ """Decide whether *resolved_path* should be validated by the gate.
39
+
40
+ Args:
41
+ resolved_path: A resolved candidate path under *repository_root*.
42
+ repository_root: Repository root used to compute the relative path.
43
+ should_read_staged_content: When True, require staged-index presence;
44
+ when False, require working-tree presence.
45
+
46
+ Returns:
47
+ True when the path carries a code extension and exists in the source
48
+ the gate will read; False otherwise.
49
+ """
50
+ if not is_code_path(resolved_path):
51
+ return False
52
+ if should_read_staged_content:
53
+ resolved_root = repository_root.resolve()
54
+ relative_posix = str(resolved_path.relative_to(resolved_root)).replace("\\", "/")
55
+ return staged_blob_exists(resolved_root, relative_posix)
56
+ return resolved_path.is_file()
57
+
58
+
59
+ def _eligible_resolved_paths(
60
+ all_file_paths: list[Path],
61
+ repository_root: Path,
62
+ should_read_staged_content: bool,
63
+ ) -> list[Path]:
64
+ """Return the resolved code files under the repo the gate will validate."""
65
+ resolved_root = repository_root.resolve()
66
+ all_eligible: list[Path] = []
67
+ for each_path in sorted(set(all_file_paths)):
68
+ each_resolved = _resolved_under_root(each_path, resolved_root)
69
+ if each_resolved is None:
70
+ continue
71
+ if _path_is_eligible_for_validation(
72
+ each_resolved, repository_root, should_read_staged_content
73
+ ):
74
+ all_eligible.append(each_resolved)
75
+ return all_eligible
76
+
77
+
78
+ def _file_content_for_validation(
79
+ resolved_path: Path,
80
+ resolved_root: Path,
81
+ relative_posix: str,
82
+ should_read_staged_content: bool,
83
+ ) -> str | None:
84
+ """Return the content the gate validates for one file, or None if unreadable."""
85
+ if should_read_staged_content:
86
+ return read_staged_content(resolved_root, relative_posix)
87
+ try:
88
+ return resolved_path.read_text(encoding="utf-8")
89
+ except (OSError, UnicodeDecodeError):
90
+ return None
91
+
92
+
93
+ def _scoped_violations_for_file(
94
+ validate_content: ValidateContentCallable,
95
+ resolved_path: Path,
96
+ repository_root: Path,
97
+ all_added_lines_for_file: set[int] | None,
98
+ should_read_staged_content: bool = False,
99
+ ) -> tuple[list[str], list[str]] | None:
100
+ """Validate one resolved file and partition its violations by diff scope."""
101
+ resolved_root = repository_root.resolve()
102
+ relative_posix = str(resolved_path.relative_to(resolved_root)).replace("\\", "/")
103
+ content = _file_content_for_validation(
104
+ resolved_path, resolved_root, relative_posix, should_read_staged_content
105
+ )
106
+ if content is None:
107
+ sys.stderr.write(f"code_rules_gate: skip unreadable {resolved_path}\n")
108
+ return None
109
+ prior_content = read_prior_committed_content(resolved_root, relative_posix)
110
+ issues = validate_content(
111
+ content,
112
+ relative_posix,
113
+ prior_content,
114
+ defer_scope_to_caller=True,
115
+ sibling_directory=resolved_path.parent,
116
+ )
117
+ issues.extend(check_wrapper_plumb_through(content, relative_posix))
118
+ if not issues:
119
+ return [], []
120
+ return split_violations_by_scope(issues, all_added_lines_for_file)
121
+
122
+
123
+ def _added_lines_for(
124
+ all_added_lines_by_path: dict[Path, set[int]] | None, each_resolved: Path
125
+ ) -> set[int] | None:
126
+ """Return the added-line set for one path, or None for whole-file scope."""
127
+ if all_added_lines_by_path is None:
128
+ return None
129
+ return all_added_lines_by_path.get(each_resolved)
130
+
131
+
132
+ def _partition_over_eligible_paths(
133
+ validate_content: ValidateContentCallable,
134
+ all_eligible_paths: list[Path],
135
+ repository_root: Path,
136
+ all_added_lines_by_path: dict[Path, set[int]] | None,
137
+ should_read_staged_content: bool,
138
+ ) -> PartitionedViolations:
139
+ """Validate each already-resolved eligible file and partition the results."""
140
+ blocking_by_file: dict[Path, list[str]] = {}
141
+ advisory_by_file: dict[Path, list[str]] = {}
142
+ skipped_unreadable_count = 0
143
+ for each_resolved in all_eligible_paths:
144
+ scoped_violations = _scoped_violations_for_file(
145
+ validate_content,
146
+ each_resolved,
147
+ repository_root,
148
+ _added_lines_for(all_added_lines_by_path, each_resolved),
149
+ should_read_staged_content,
150
+ )
151
+ if scoped_violations is None:
152
+ skipped_unreadable_count += 1
153
+ continue
154
+ blocking, advisory = scoped_violations
155
+ if blocking:
156
+ blocking_by_file[each_resolved] = blocking
157
+ if advisory:
158
+ advisory_by_file[each_resolved] = advisory
159
+ return blocking_by_file, advisory_by_file, skipped_unreadable_count
160
+
161
+
162
+ def _collect_partitioned_violations(
163
+ validate_content: ValidateContentCallable,
164
+ all_file_paths: list[Path],
165
+ repository_root: Path,
166
+ all_added_lines_by_path: dict[Path, set[int]] | None,
167
+ should_read_staged_content: bool = False,
168
+ ) -> PartitionedViolations:
169
+ """Validate every eligible file and partition results, counting read skips."""
170
+ all_eligible_paths = _eligible_resolved_paths(
171
+ all_file_paths, repository_root, should_read_staged_content
172
+ )
173
+ return _partition_over_eligible_paths(
174
+ validate_content,
175
+ all_eligible_paths,
176
+ repository_root,
177
+ all_added_lines_by_path,
178
+ should_read_staged_content,
179
+ )
180
+
181
+
182
+ def _blocking_header(blocking_count: int, is_whole_file_scope: bool) -> str:
183
+ """Return the blocking-section header for the current scope."""
184
+ if is_whole_file_scope:
185
+ return f"code_rules_gate: {blocking_count} violation(s) reported."
186
+ return f"code_rules_gate: {blocking_count} violation(s) introduced on changed lines:"
187
+
188
+
189
+ def print_violation_section(
190
+ header_message: str,
191
+ all_violations_by_file: dict[Path, list[str]],
192
+ repository_root: Path,
193
+ ) -> None:
194
+ """Print a labeled block of violations grouped by relative path.
195
+
196
+ Args:
197
+ header_message: Section header to write to stderr.
198
+ all_violations_by_file: Mapping from absolute file path to its violation
199
+ strings.
200
+ repository_root: Repository root used to compute relative paths.
201
+ """
202
+ sys.stderr.write(header_message + "\n")
203
+ resolved_root = repository_root.resolve()
204
+ for each_path in sorted(all_violations_by_file.keys()):
205
+ relative = each_path.relative_to(resolved_root)
206
+ sys.stderr.write(f"{relative}:\n")
207
+ for each_issue in all_violations_by_file[each_path]:
208
+ sys.stderr.write(f" {each_issue}\n")
209
+
210
+
211
+ def _report_advisory_section(
212
+ advisory_count: int,
213
+ blocking_count: int,
214
+ all_advisory_by_file: dict[Path, list[str]],
215
+ repository_root: Path,
216
+ ) -> None:
217
+ """Print the advisory violation section when any advisory violation exists."""
218
+ if not advisory_count:
219
+ return
220
+ if blocking_count:
221
+ sys.stderr.write("\n")
222
+ print_violation_section(
223
+ (
224
+ f"code_rules_gate: {advisory_count} pre-existing violation(s) "
225
+ "in touched files (advisory, not blocking):"
226
+ ),
227
+ all_advisory_by_file,
228
+ repository_root,
229
+ )
230
+
231
+
232
+ def _report_partitioned_violations(
233
+ partitions: PartitionedViolations,
234
+ repository_root: Path,
235
+ is_whole_file_scope: bool,
236
+ ) -> int:
237
+ """Print the blocking and advisory sections and return the gate exit code."""
238
+ all_blocking_by_file, all_advisory_by_file, skipped_unreadable_count = partitions
239
+ blocking_count = sum(len(each) for each in all_blocking_by_file.values())
240
+ advisory_count = sum(len(each) for each in all_advisory_by_file.values())
241
+ if blocking_count:
242
+ print_violation_section(
243
+ _blocking_header(blocking_count, is_whole_file_scope),
244
+ all_blocking_by_file,
245
+ repository_root,
246
+ )
247
+ _report_advisory_section(advisory_count, blocking_count, all_advisory_by_file, repository_root)
248
+ if skipped_unreadable_count:
249
+ sys.stderr.write(
250
+ f"code_rules_gate: {skipped_unreadable_count} file(s) skipped due to "
251
+ "read errors; gate cannot vouch for those files.\n"
252
+ )
253
+ if blocking_count or skipped_unreadable_count:
254
+ return 1
255
+ return 0
256
+
257
+
258
+ def _report_inspected_count(inspected_count: int) -> None:
259
+ """Write the inspected-file count to standard error."""
260
+ sys.stderr.write(INSPECTED_COUNT_MESSAGE.format(inspected_count=inspected_count) + "\n")
261
+
262
+
263
+ def _validate_and_count(
264
+ validate_content: ValidateContentCallable,
265
+ all_file_paths: list[Path],
266
+ repository_root: Path,
267
+ all_added_lines_by_path: dict[Path, set[int]] | None,
268
+ should_read_staged_content: bool,
269
+ ) -> PartitionedViolations:
270
+ """Validate the eligible files, report the inspected count, return partitions."""
271
+ all_eligible_paths = _eligible_resolved_paths(
272
+ all_file_paths, repository_root, should_read_staged_content
273
+ )
274
+ blocking_by_file, advisory_by_file, skipped_count = _partition_over_eligible_paths(
275
+ validate_content,
276
+ all_eligible_paths,
277
+ repository_root,
278
+ all_added_lines_by_path,
279
+ should_read_staged_content,
280
+ )
281
+ _report_inspected_count(len(all_eligible_paths) - skipped_count)
282
+ return blocking_by_file, advisory_by_file, skipped_count
283
+
284
+
285
+ def run_gate(
286
+ validate_content: ValidateContentCallable,
287
+ all_file_paths: list[Path],
288
+ repository_root: Path,
289
+ all_added_lines_by_path: dict[Path, set[int]] | None = None,
290
+ should_read_staged_content: bool = False,
291
+ ) -> int:
292
+ """Run the gate over *all_file_paths* and emit a partitioned report.
293
+
294
+ Args:
295
+ validate_content: The enforcer ``validate_content`` callable.
296
+ all_file_paths: File paths to inspect.
297
+ repository_root: Repository root for resolving relative paths.
298
+ all_added_lines_by_path: Per-file added-line maps, or None.
299
+ should_read_staged_content: When True, validate staged blobs.
300
+
301
+ Returns:
302
+ Zero when clean; non-zero on a blocking violation or a skipped file.
303
+ """
304
+ partitions = _validate_and_count(
305
+ validate_content,
306
+ all_file_paths,
307
+ repository_root,
308
+ all_added_lines_by_path,
309
+ should_read_staged_content,
310
+ )
311
+ return _report_partitioned_violations(
312
+ partitions, repository_root, all_added_lines_by_path is None
313
+ )
314
+
315
+
316
+ def _report_terminology_findings(all_findings: list[str]) -> None:
317
+ """Print the terminology-sweep findings, when any, to standard error.
318
+
319
+ Args:
320
+ all_findings: The near-miss findings from the staged terminology sweep.
321
+ """
322
+ if not all_findings:
323
+ return
324
+ sys.stderr.write(TERMINOLOGY_SWEEP_GATE_HEADER.format(finding_count=len(all_findings)) + "\n")
325
+ for each_finding in all_findings:
326
+ sys.stderr.write(f" {each_finding}\n")
@@ -0,0 +1,85 @@
1
+ """Read the committed or staged content of one file from git.
2
+
3
+ The gate validates staged content against the file's prior committed content, so
4
+ these readers return the HEAD blob, the staged blob, and a staged-presence
5
+ probe. A missing or non-Unicode blob resolves to an empty string, None, or
6
+ False so the caller fails closed rather than crashing.
7
+ """
8
+
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ from terminology_sweep import repository_environment
13
+
14
+
15
+ def read_prior_committed_content(repository_root: Path, relative_path_posix: str) -> str:
16
+ """Return the HEAD-committed content for *relative_path_posix*.
17
+
18
+ Args:
19
+ repository_root: Repository root used as the ``git -C`` target.
20
+ relative_path_posix: Repository-relative POSIX path to read.
21
+
22
+ Returns:
23
+ The committed content at HEAD, or an empty string when the path is not
24
+ tracked or ``git show`` returns non-zero.
25
+ """
26
+ completed = subprocess.run(
27
+ ["git", "show", f"HEAD:{relative_path_posix}"],
28
+ cwd=str(repository_root),
29
+ capture_output=True,
30
+ text=True,
31
+ encoding="utf-8",
32
+ errors="replace",
33
+ check=False,
34
+ env=repository_environment(),
35
+ )
36
+ if completed.returncode != 0:
37
+ return ""
38
+ return completed.stdout
39
+
40
+
41
+ def read_staged_content(repository_root: Path, relative_path_posix: str) -> str | None:
42
+ """Return the staged-blob content for *relative_path_posix*.
43
+
44
+ Args:
45
+ repository_root: Repository root used as the ``git -C`` target.
46
+ relative_path_posix: Repository-relative POSIX path to read.
47
+
48
+ Returns:
49
+ The staged blob content, or None when the path is not staged, when
50
+ ``git show`` returns non-zero, or when the bytes are not Unicode.
51
+ """
52
+ completed = subprocess.run(
53
+ ["git", "show", f":{relative_path_posix}"],
54
+ cwd=str(repository_root),
55
+ capture_output=True,
56
+ check=False,
57
+ env=repository_environment(),
58
+ )
59
+ if completed.returncode != 0:
60
+ return None
61
+ try:
62
+ return completed.stdout.decode(encoding="utf-8")
63
+ except UnicodeDecodeError:
64
+ return None
65
+
66
+
67
+ def staged_blob_exists(repository_root: Path, relative_path_posix: str) -> bool:
68
+ """Report whether *relative_path_posix* is present in the staged index.
69
+
70
+ Args:
71
+ repository_root: Repository root used as the ``git -C`` target.
72
+ relative_path_posix: Repository-relative POSIX path to probe.
73
+
74
+ Returns:
75
+ True when the path's blob exists in the index; False when absent, such
76
+ as a staged deletion.
77
+ """
78
+ completed = subprocess.run(
79
+ ["git", "cat-file", "-e", f":{relative_path_posix}"],
80
+ cwd=str(repository_root),
81
+ capture_output=True,
82
+ check=False,
83
+ env=repository_environment(),
84
+ )
85
+ return completed.returncode == 0