claude-dev-env 2.9.0 → 2.10.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 (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -0,0 +1,362 @@
1
+ """Machine-readable Opus effort evaluation for e-code-review.
2
+
3
+ Freezes easy / medium / demanding fixtures, validates evaluation rows, and
4
+ publishes a recommendation that cites completed rows. Live paid runs are
5
+ optional — offline tests feed synthetic rows through the same functions::
6
+
7
+ load fixtures → validate rows → recommend lowest effort that holds quality
8
+
9
+ Thinking stays enabled; effort is the only planned cost and latency lever.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Mapping, MutableMapping, Sequence
18
+
19
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
20
+ if str(_SCRIPTS_DIRECTORY / "config") not in sys.path:
21
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY / "config"))
22
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
23
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
24
+
25
+ from e_code_review_effort_constants import (
26
+ ALL_EFFORT_LEVELS,
27
+ ALL_EFFORT_RANK_BY_NAME,
28
+ ALL_FIXTURE_BANDS,
29
+ ALL_REQUIRED_ROW_KEYS,
30
+ ALL_SCORE_ROW_KEYS,
31
+ ALL_SKILL_EFFORT_FOR_EVALUATION_EFFORT,
32
+ ALL_SKILL_EFFORT_LEVELS,
33
+ COST_LATENCY_LEVER,
34
+ EVALUATION_EVIDENCE_FILENAME,
35
+ EVALUATION_SCHEMA_VERSION,
36
+ FIXTURES_DIRECTORY_NAME,
37
+ JSON_SUFFIX,
38
+ LATENCY_MS_ROW_KEY,
39
+ MINIMUM_QUALITY_HOLD_SCORE,
40
+ THINKING_ENABLED_DEFAULT,
41
+ VISIBLE_TOKENS_ROW_KEY,
42
+ WORKFLOW_FAMILY_E_CODE_REVIEW,
43
+ )
44
+
45
+
46
+ def fixtures_directory() -> Path:
47
+ """Return the frozen fixture directory next to this module.
48
+
49
+ Returns:
50
+ Absolute path to the fixtures directory.
51
+ """
52
+ return _SCRIPTS_DIRECTORY / FIXTURES_DIRECTORY_NAME
53
+
54
+
55
+ def load_fixtures() -> list[dict[str, object]]:
56
+ """Load every JSON fixture under the fixtures directory.
57
+
58
+ Returns:
59
+ Fixture dicts sorted by band then fixture_id.
60
+
61
+ Raises:
62
+ ValueError: When a fixture file is not a JSON object.
63
+ OSError: When a fixture file cannot be read.
64
+ json.JSONDecodeError: When a fixture file is not valid JSON.
65
+ """
66
+ directory = fixtures_directory()
67
+ all_fixtures: list[dict[str, object]] = []
68
+ for each_path in sorted(directory.glob(f"*{JSON_SUFFIX}")):
69
+ loaded = json.loads(each_path.read_text(encoding="utf-8"))
70
+ if not isinstance(loaded, dict):
71
+ raise ValueError(f"Fixture must be an object: {each_path}")
72
+ all_fixtures.append(loaded)
73
+ all_fixtures.sort(
74
+ key=lambda each_fixture: (
75
+ str(each_fixture.get("band", "")),
76
+ str(each_fixture.get("fixture_id", "")),
77
+ )
78
+ )
79
+ return all_fixtures
80
+
81
+
82
+ def validate_evaluation_row(all_row_fields: Mapping[str, object]) -> list[str]:
83
+ """Return human-readable problems for one evaluation row.
84
+
85
+ Args:
86
+ all_row_fields: Candidate evaluation row mapping.
87
+
88
+ Returns:
89
+ Problem strings; empty when the row is valid.
90
+ """
91
+ all_problems: list[str] = []
92
+ for each_key in ALL_REQUIRED_ROW_KEYS:
93
+ if each_key not in all_row_fields:
94
+ all_problems.append(f"missing key: {each_key}")
95
+ if all_problems:
96
+ return all_problems
97
+ effort = all_row_fields["effort"]
98
+ if effort not in ALL_EFFORT_LEVELS:
99
+ all_problems.append(f"unknown effort: {effort!r}")
100
+ fixture_band = all_row_fields["fixture_band"]
101
+ if fixture_band not in ALL_FIXTURE_BANDS:
102
+ all_problems.append(f"unknown fixture_band: {fixture_band!r}")
103
+ if all_row_fields["thinking_enabled"] is not True:
104
+ all_problems.append("thinking_enabled must be true on Opus paths")
105
+ for each_score_key in ALL_SCORE_ROW_KEYS:
106
+ score_amount = all_row_fields[each_score_key]
107
+ if isinstance(score_amount, bool) or not isinstance(score_amount, (int, float)):
108
+ all_problems.append(f"{each_score_key} must be numeric")
109
+ elif not 0.0 <= float(score_amount) <= 1.0:
110
+ all_problems.append(f"{each_score_key} out of range [0, 1]")
111
+ for each_count_key in (VISIBLE_TOKENS_ROW_KEY, LATENCY_MS_ROW_KEY):
112
+ count_amount = all_row_fields[each_count_key]
113
+ if (
114
+ isinstance(count_amount, bool)
115
+ or not isinstance(count_amount, (int, float))
116
+ or float(count_amount) < 0
117
+ ):
118
+ all_problems.append(f"{each_count_key} must be a non-negative number")
119
+ return all_problems
120
+
121
+
122
+ def quality_holds(
123
+ all_row_fields: Mapping[str, object],
124
+ minimum_quality: float = MINIMUM_QUALITY_HOLD_SCORE,
125
+ ) -> bool:
126
+ """Return True when quality, recall, and precision all meet the floor.
127
+
128
+ Args:
129
+ all_row_fields: Evaluation row mapping.
130
+ minimum_quality: Floor for quality, recall, and precision.
131
+
132
+ Returns:
133
+ Whether the row holds quality at the floor.
134
+ """
135
+ return all(
136
+ float(all_row_fields[each_score_key]) >= minimum_quality
137
+ for each_score_key in ALL_SCORE_ROW_KEYS
138
+ )
139
+
140
+
141
+ def _lowest_holding_row(
142
+ all_band_rows: Sequence[Mapping[str, object]],
143
+ ) -> Mapping[str, object]:
144
+ return min(
145
+ all_band_rows,
146
+ key=lambda each_row: ALL_EFFORT_RANK_BY_NAME[str(each_row["effort"])],
147
+ )
148
+
149
+
150
+ def recommend_effort_by_band(
151
+ all_rows: Sequence[Mapping[str, object]],
152
+ minimum_quality: float = MINIMUM_QUALITY_HOLD_SCORE,
153
+ ) -> dict[str, object]:
154
+ """Pick the lowest effort that holds quality for each fixture band.
155
+
156
+ Every recommendation entry cites the evaluation row that justified it.
157
+ Bands with no holding row stay null with an explicit blocker note.
158
+
159
+ Args:
160
+ all_rows: Completed evaluation rows (already validated by the caller).
161
+ minimum_quality: Floor for quality, recall, and precision.
162
+
163
+ Returns:
164
+ Machine-readable recommendation document.
165
+ """
166
+ recommendation_by_band: MutableMapping[str, object] = {}
167
+ for each_band in ALL_FIXTURE_BANDS:
168
+ all_holding_rows = [
169
+ each_row
170
+ for each_row in all_rows
171
+ if each_row["fixture_band"] == each_band
172
+ and quality_holds(each_row, minimum_quality)
173
+ ]
174
+ if not all_holding_rows:
175
+ recommendation_by_band[each_band] = {
176
+ "recommended_effort": None,
177
+ "cited_row": None,
178
+ "blocker": "no completed row meets the quality floor",
179
+ }
180
+ continue
181
+ chosen_row = _lowest_holding_row(all_holding_rows)
182
+ recommendation_by_band[each_band] = {
183
+ "recommended_effort": chosen_row["effort"],
184
+ "cited_row": dict(chosen_row),
185
+ "blocker": None,
186
+ }
187
+ return {
188
+ "schema_version": EVALUATION_SCHEMA_VERSION,
189
+ "thinking_enabled": THINKING_ENABLED_DEFAULT,
190
+ "cost_latency_lever": COST_LATENCY_LEVER,
191
+ "defaults_unchanged": True,
192
+ "minimum_quality": minimum_quality,
193
+ "recommendation_by_band": dict(recommendation_by_band),
194
+ }
195
+
196
+
197
+ def build_synthetic_row(
198
+ fixture_id: str,
199
+ fixture_band: str,
200
+ effort: str,
201
+ quality_score: float,
202
+ finding_recall: float,
203
+ finding_precision: float,
204
+ visible_tokens: int,
205
+ latency_ms: int,
206
+ ) -> dict[str, object]:
207
+ """Build one evaluation row with thinking enabled.
208
+
209
+ Args:
210
+ fixture_id: Fixture identifier.
211
+ fixture_band: Band name (easy / medium / demanding).
212
+ effort: Effort level name.
213
+ quality_score: Overall quality in [0, 1].
214
+ finding_recall: Recall in [0, 1].
215
+ finding_precision: Precision in [0, 1].
216
+ visible_tokens: Visible token count.
217
+ latency_ms: Latency in milliseconds.
218
+
219
+ Returns:
220
+ Complete evaluation row mapping.
221
+ """
222
+ return {
223
+ "fixture_id": fixture_id,
224
+ "fixture_band": fixture_band,
225
+ "effort": effort,
226
+ "quality_score": quality_score,
227
+ "finding_recall": finding_recall,
228
+ "finding_precision": finding_precision,
229
+ VISIBLE_TOKENS_ROW_KEY: visible_tokens,
230
+ LATENCY_MS_ROW_KEY: latency_ms,
231
+ "thinking_enabled": THINKING_ENABLED_DEFAULT,
232
+ }
233
+
234
+
235
+ def evaluation_evidence_path() -> Path:
236
+ """Return the committed evaluation evidence file path.
237
+
238
+ Returns:
239
+ Path to ``effort_defaults_evidence.json`` beside this module.
240
+ """
241
+ return _SCRIPTS_DIRECTORY / EVALUATION_EVIDENCE_FILENAME
242
+
243
+
244
+ def load_evaluation_evidence() -> dict[str, object]:
245
+ """Load the committed evaluation evidence document.
246
+
247
+ Returns:
248
+ Parsed evidence mapping (rows, recommendation, skill defaults).
249
+
250
+ Raises:
251
+ OSError: When the evidence file cannot be read.
252
+ json.JSONDecodeError: When the evidence file is not valid JSON.
253
+ """
254
+ return json.loads(evaluation_evidence_path().read_text(encoding="utf-8"))
255
+
256
+
257
+ def map_evaluation_effort_to_skill_level(evaluation_effort: str) -> str:
258
+ """Map a full evaluation effort name to an e-code-review skill level.
259
+
260
+ Args:
261
+ evaluation_effort: One of low / medium / high / xhigh / max.
262
+
263
+ Returns:
264
+ One of low / medium / xhigh.
265
+
266
+ Raises:
267
+ ValueError: When the evaluation effort is unknown.
268
+ """
269
+ skill_effort = ALL_SKILL_EFFORT_FOR_EVALUATION_EFFORT.get(evaluation_effort)
270
+ if skill_effort is None:
271
+ raise ValueError(f"unknown evaluation effort: {evaluation_effort!r}")
272
+ return skill_effort
273
+
274
+
275
+ def skill_defaults_from_recommendation(
276
+ all_recommendation_fields: Mapping[str, object],
277
+ ) -> dict[str, object]:
278
+ """Build e-code-review skill defaults that each cite a recommendation row.
279
+
280
+ Args:
281
+ all_recommendation_fields: Output of ``recommend_effort_by_band``.
282
+
283
+ Returns:
284
+ Skill-family defaults document with cited rows per band.
285
+
286
+ Raises:
287
+ TypeError: When recommendation structure is not mapping-shaped.
288
+ ValueError: When a mapped skill effort leaves the skill surface.
289
+ """
290
+ recommendation_by_band = all_recommendation_fields["recommendation_by_band"]
291
+ if not isinstance(recommendation_by_band, Mapping):
292
+ raise TypeError("recommendation_by_band must be a mapping")
293
+ default_by_band: MutableMapping[str, object] = {}
294
+ for each_band in ALL_FIXTURE_BANDS:
295
+ band_entry = recommendation_by_band[each_band]
296
+ if not isinstance(band_entry, Mapping):
297
+ raise TypeError(f"band entry for {each_band} must be a mapping")
298
+ evaluation_effort = band_entry.get("recommended_effort")
299
+ cited_row = band_entry.get("cited_row")
300
+ if evaluation_effort is None or cited_row is None:
301
+ default_by_band[each_band] = {
302
+ "skill_effort": None,
303
+ "cited_row": None,
304
+ "blocker": band_entry.get("blocker"),
305
+ }
306
+ continue
307
+ skill_effort = map_evaluation_effort_to_skill_level(str(evaluation_effort))
308
+ if skill_effort not in ALL_SKILL_EFFORT_LEVELS:
309
+ raise ValueError(f"skill effort out of surface: {skill_effort!r}")
310
+ default_by_band[each_band] = {
311
+ "skill_effort": skill_effort,
312
+ "evaluation_effort": evaluation_effort,
313
+ "cited_row": dict(cited_row) if isinstance(cited_row, Mapping) else cited_row,
314
+ "blocker": None,
315
+ }
316
+ return {
317
+ "workflow_family": WORKFLOW_FAMILY_E_CODE_REVIEW,
318
+ "thinking_enabled": THINKING_ENABLED_DEFAULT,
319
+ "cost_latency_lever": COST_LATENCY_LEVER,
320
+ "skill_levels": list(ALL_SKILL_EFFORT_LEVELS),
321
+ "default_by_band": dict(default_by_band),
322
+ }
323
+
324
+
325
+ def resolve_skill_effort_for_band(fixture_band: str) -> str:
326
+ """Return the evaluation-backed skill effort for one fixture band.
327
+
328
+ Args:
329
+ fixture_band: easy / medium / demanding.
330
+
331
+ Returns:
332
+ Skill effort level (low / medium / xhigh).
333
+
334
+ Raises:
335
+ ValueError: When the band is unknown or has no holding recommendation.
336
+ KeyError: When the evidence document lacks the band.
337
+ """
338
+ if fixture_band not in ALL_FIXTURE_BANDS:
339
+ raise ValueError(f"unknown fixture band: {fixture_band!r}")
340
+ evidence = load_evaluation_evidence()
341
+ skill_defaults = evidence["skill_defaults"]
342
+ if not isinstance(skill_defaults, Mapping):
343
+ raise TypeError("skill_defaults must be a mapping")
344
+ default_by_band = skill_defaults["default_by_band"]
345
+ if not isinstance(default_by_band, Mapping):
346
+ raise TypeError("default_by_band must be a mapping")
347
+ band_default = default_by_band[fixture_band]
348
+ if not isinstance(band_default, Mapping):
349
+ raise TypeError(f"default for {fixture_band} must be a mapping")
350
+ skill_effort = band_default.get("skill_effort")
351
+ if skill_effort is None:
352
+ raise ValueError(
353
+ f"no evaluation-backed skill effort for band {fixture_band!r}: "
354
+ f"{band_default.get('blocker')}"
355
+ )
356
+ skill_effort_name = str(skill_effort)
357
+ if skill_effort_name not in ALL_SKILL_EFFORT_LEVELS:
358
+ raise ValueError(
359
+ f"skill effort out of surface for band {fixture_band!r}: "
360
+ f"{skill_effort_name!r}"
361
+ )
362
+ return skill_effort_name
@@ -0,0 +1,140 @@
1
+ """Collect every real review finding; filter severity only later.
2
+
3
+ ::
4
+
5
+ collection = collect_findings(all_seeded)
6
+ ok: low and nit findings remain in the collection record
7
+ filtered = filter_findings_by_severity(collection, minimum_severity="medium")
8
+ ok: consumer stage drops lower severities; collection is unchanged
9
+
10
+ Collection never drops a real finding by severity. A later consumer stage
11
+ reads the complete collection and may filter for action or display.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+
18
+ from e_code_review_scripts_constants.finding_pipeline_constants import (
19
+ ALL_COLLECTION_SEVERITIES,
20
+ COLLECTION_STAGE_NAME,
21
+ FILTER_STAGE_NAME,
22
+ FINDING_FIELD_CATEGORY,
23
+ FINDING_FIELD_EVIDENCE,
24
+ FINDING_FIELD_FILE,
25
+ FINDING_FIELD_LINE,
26
+ FINDING_FIELD_SEVERITY,
27
+ ALL_SEVERITY_RANK_BY_TOKEN,
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class CollectedFinding:
33
+ """One real finding retained during the collection stage."""
34
+
35
+ file_path: str
36
+ line_number: int
37
+ severity: str
38
+ category: str
39
+ evidence: str
40
+
41
+ def as_mapping(self) -> dict[str, str | int]:
42
+ """Return the stable field map every retained finding keeps.
43
+
44
+ Returns:
45
+ Mapping with file, line, severity, category, and evidence.
46
+ """
47
+ return {
48
+ FINDING_FIELD_FILE: self.file_path,
49
+ FINDING_FIELD_LINE: self.line_number,
50
+ FINDING_FIELD_SEVERITY: self.severity,
51
+ FINDING_FIELD_CATEGORY: self.category,
52
+ FINDING_FIELD_EVIDENCE: self.evidence,
53
+ }
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class FindingCollection:
58
+ """Unfiltered collection record of every real finding."""
59
+
60
+ stage_name: str
61
+ all_findings: tuple[CollectedFinding, ...]
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class SeverityFilterView:
66
+ """Severity-filtered view produced by a separate consumer stage."""
67
+
68
+ stage_name: str
69
+ minimum_severity: str
70
+ all_findings: tuple[CollectedFinding, ...]
71
+
72
+
73
+ def collect_findings(
74
+ all_findings: list[CollectedFinding] | tuple[CollectedFinding, ...],
75
+ ) -> FindingCollection:
76
+ """Retain every real finding with its file, line, evidence, and category.
77
+
78
+ Args:
79
+ all_findings: Seeded real findings of any legal severity.
80
+
81
+ Returns:
82
+ Collection stage record holding every input finding.
83
+
84
+ Raises:
85
+ ValueError: When a finding lacks a required field or legal severity.
86
+ """
87
+ all_retained: list[CollectedFinding] = []
88
+ for each_finding in all_findings:
89
+ if not each_finding.file_path:
90
+ raise ValueError("finding requires a non-empty file path")
91
+ if each_finding.line_number < 1:
92
+ raise ValueError("finding requires a positive line number")
93
+ if not each_finding.category:
94
+ raise ValueError("finding requires a non-empty category")
95
+ if not each_finding.evidence:
96
+ raise ValueError("finding requires non-empty evidence")
97
+ if each_finding.severity not in ALL_COLLECTION_SEVERITIES:
98
+ raise ValueError(
99
+ f"unknown collection severity: {each_finding.severity!r}"
100
+ )
101
+ all_retained.append(each_finding)
102
+ return FindingCollection(
103
+ stage_name=COLLECTION_STAGE_NAME,
104
+ all_findings=tuple(all_retained),
105
+ )
106
+
107
+
108
+ def filter_findings_by_severity(
109
+ collection: FindingCollection,
110
+ *,
111
+ minimum_severity: str,
112
+ ) -> SeverityFilterView:
113
+ """Filter a complete collection by severity in a separate consumer stage.
114
+
115
+ Args:
116
+ collection: Unfiltered collection record from ``collect_findings``.
117
+ minimum_severity: Lowest severity to keep (inclusive).
118
+
119
+ Returns:
120
+ Filter-stage view. The input collection record is not mutated.
121
+
122
+ Raises:
123
+ ValueError: When ``minimum_severity`` or a finding severity is unknown.
124
+ """
125
+ if minimum_severity not in ALL_SEVERITY_RANK_BY_TOKEN:
126
+ raise ValueError(f"unknown minimum severity: {minimum_severity!r}")
127
+ minimum_rank = ALL_SEVERITY_RANK_BY_TOKEN[minimum_severity]
128
+ all_kept: list[CollectedFinding] = []
129
+ for each_finding in collection.all_findings:
130
+ if each_finding.severity not in ALL_SEVERITY_RANK_BY_TOKEN:
131
+ raise ValueError(
132
+ f"unknown collection severity: {each_finding.severity!r}"
133
+ )
134
+ if ALL_SEVERITY_RANK_BY_TOKEN[each_finding.severity] >= minimum_rank:
135
+ all_kept.append(each_finding)
136
+ return SeverityFilterView(
137
+ stage_name=FILTER_STAGE_NAME,
138
+ minimum_severity=minimum_severity,
139
+ all_findings=tuple(all_kept),
140
+ )
@@ -0,0 +1,26 @@
1
+ {
2
+ "fixture_id": "demanding-multi-surface",
3
+ "band": "demanding",
4
+ "description": "Cross-file review with three seeded findings across hooks and tests.",
5
+ "target_summary": "Hook change drifts docstring, drops a paired test path, and uses a banned identifier.",
6
+ "seeded_findings": [
7
+ {
8
+ "file": "hooks/blocking/sample_gate.py",
9
+ "line": 88,
10
+ "category": "docstring-vs-impl",
11
+ "severity": "high"
12
+ },
13
+ {
14
+ "file": "hooks/blocking/test_sample_gate.py",
15
+ "line": 1,
16
+ "category": "paired-test-coverage",
17
+ "severity": "blocker"
18
+ },
19
+ {
20
+ "file": "hooks/blocking/sample_gate.py",
21
+ "line": 15,
22
+ "category": "banned-identifier",
23
+ "severity": "medium"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "fixture_id": "easy-comment-preservation",
3
+ "band": "easy",
4
+ "description": "Single-file comment-preservation miss with one seeded finding.",
5
+ "target_summary": "Production helper drops an existing inline comment on an unchanged line.",
6
+ "seeded_findings": [
7
+ {
8
+ "file": "sample_helper.py",
9
+ "line": 12,
10
+ "category": "comment-preservation",
11
+ "severity": "medium"
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "fixture_id": "medium-constants-and-types",
3
+ "band": "medium",
4
+ "description": "Two seeded findings: bare type-ignore and a magic timeout literal.",
5
+ "target_summary": "Service module adds a bare type ignore and a hard-coded timeout.",
6
+ "seeded_findings": [
7
+ {
8
+ "file": "order_service.py",
9
+ "line": 24,
10
+ "category": "type-ignore-justification",
11
+ "severity": "high"
12
+ },
13
+ {
14
+ "file": "order_service.py",
15
+ "line": 41,
16
+ "category": "magic-value",
17
+ "severity": "medium"
18
+ }
19
+ ]
20
+ }
@@ -197,21 +197,30 @@ def run_medium_review(
197
197
  batch.is_rejected = True
198
198
  batch.rejection_reason = "finder_head_mismatch"
199
199
  return batch
200
- all_angles = [each.angle for each in all_finder_candidates]
200
+ all_angles = sorted({each.angle for each in all_finder_candidates})
201
+ finder_count = len(all_angles)
202
+ all_identities = {
203
+ (
204
+ each.angle,
205
+ each.leader_socket,
206
+ each.advisor_session_id,
207
+ each.worktree_path,
208
+ )
209
+ for each in all_finder_candidates
210
+ }
201
211
  all_sockets = {each.leader_socket for each in all_finder_candidates}
202
212
  all_sessions = {each.advisor_session_id for each in all_finder_candidates}
203
213
  all_worktrees = {each.worktree_path for each in all_finder_candidates}
204
214
  if (
205
- len(all_sockets) != len(all_finder_candidates)
206
- or len(all_sessions) != len(all_finder_candidates)
207
- or len(all_worktrees) != len(all_finder_candidates)
215
+ len(all_identities) != finder_count
216
+ or len(all_sockets) != finder_count
217
+ or len(all_sessions) != finder_count
218
+ or len(all_worktrees) != finder_count
208
219
  ):
209
220
  batch.is_rejected = True
210
221
  batch.rejection_reason = "non_unique_finder_identity"
211
222
  return batch
212
- require_exact_finder_set(tuple(sorted(set(all_angles))))
213
- if len(set(all_angles)) != MEDIUM_REVIEW_FINDER_COUNT:
214
- raise ValueError("each finder angle must run exactly once")
223
+ require_exact_finder_set(tuple(all_angles))
215
224
  deduped = deduplicate_candidates(all_finder_candidates)
216
225
  batch.all_retained_findings = retain_verified_findings(
217
226
  all_candidates=deduped,