claude-dev-env 2.8.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 (323) hide show
  1. package/CLAUDE.md +8 -2
  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 +62 -562
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +58 -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/CODE_RULES.md +4 -2
  44. package/docs/references/CLAUDE.md +3 -2
  45. package/docs/references/advisor-tool.md +44 -6
  46. package/docs/references/prose-style-enforcement.md +25 -0
  47. package/docs/references/team-advisor-skill.md +14 -8
  48. package/hooks/blocking/CLAUDE.md +6 -6
  49. package/hooks/blocking/_path_setup.py +9 -5
  50. package/hooks/blocking/code_rules_docstrings.py +124 -30
  51. package/hooks/blocking/code_rules_enforcer.py +161 -16
  52. package/hooks/blocking/code_rules_shared.py +40 -23
  53. package/hooks/blocking/config/CLAUDE.md +3 -5
  54. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  55. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  56. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  57. package/hooks/blocking/hedging_language_blocker.py +103 -20
  58. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  59. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  60. package/hooks/blocking/plain_language_blocker.py +139 -20
  61. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  62. package/hooks/blocking/state_description_blocker.py +7 -1
  63. package/hooks/blocking/tdd_enforcer.py +8 -0
  64. package/hooks/blocking/test__path_setup.py +28 -0
  65. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  66. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  67. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  68. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  69. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  70. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  71. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  72. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  73. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  74. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  75. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  76. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  77. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  78. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  79. package/hooks/blocking/test_state_description_blocker.py +45 -2
  80. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  81. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  82. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  83. package/hooks/hooks.json +15 -0
  84. package/hooks/hooks_constants/CLAUDE.md +14 -3
  85. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  86. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  87. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  88. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  89. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  90. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  91. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  92. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  93. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  94. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  95. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  96. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  97. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  98. package/hooks/hooks_constants/session_start_injector.py +163 -0
  99. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  100. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  101. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  102. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  103. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  104. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  105. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  106. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  107. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  108. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  109. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  110. package/hooks/observability/CLAUDE.md +2 -0
  111. package/hooks/observability/prose_matcher_advisory.py +237 -0
  112. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  113. package/hooks/session/CLAUDE.md +9 -1
  114. package/hooks/session/_path_setup.py +13 -0
  115. package/hooks/session/issue_tracker_session_starter.py +135 -0
  116. package/hooks/session/orchestrator_auto_starter.py +100 -0
  117. package/hooks/session/test__path_setup.py +28 -0
  118. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  119. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  120. package/hooks/session/test_working_style_prompt.py +47 -0
  121. package/hooks/session/untracked_repo_detector.py +1 -24
  122. package/hooks/session/working_style_prompt.py +36 -0
  123. package/hooks/validators/_path_setup.py +19 -0
  124. package/hooks/validators/run_all_validators.py +8 -13
  125. package/installable-surfaces.manifest.json +21 -0
  126. package/output-styles/CLAUDE.md +15 -0
  127. package/package.json +5 -2
  128. package/rules/CLAUDE.md +1 -0
  129. package/rules/code-standards.md +33 -7
  130. package/rules/durable-post-artifacts.md +2 -2
  131. package/rules/eli11-replies.md +7 -2
  132. package/rules/hedging-claims.md +4 -2
  133. package/rules/long-horizon-autonomy.md +3 -1
  134. package/rules/opus5-communication-contract.md +45 -0
  135. package/rules/plain-language.md +2 -2
  136. package/rules/research-mode.md +1 -1
  137. package/scripts/CLAUDE.md +13 -2
  138. package/scripts/Sync-RepoMain.ps1 +215 -0
  139. package/scripts/active_capability_references.py +218 -0
  140. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  141. package/scripts/claude_chain_runner.py +394 -6
  142. package/scripts/claude_chain_usage.py +1 -1
  143. package/scripts/codex_compat_materializer.py +105 -85
  144. package/scripts/dev_env_scripts_constants/CLAUDE.md +3 -1
  145. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  146. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  147. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  148. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
  149. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  150. package/scripts/grok_patch_artifacts.py +123 -0
  151. package/scripts/grok_run_ledger.py +318 -0
  152. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  153. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  154. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  155. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  156. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  157. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  158. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  159. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  160. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  161. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  162. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  163. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  164. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  165. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  166. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  167. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  168. package/scripts/spawn_grok_batch.py +556 -9
  169. package/scripts/test_active_capability_references.py +108 -0
  170. package/scripts/test_claude_chain_runner.py +414 -82
  171. package/scripts/test_claude_chain_usage.py +12 -12
  172. package/scripts/test_grok_patch_artifacts.py +82 -0
  173. package/scripts/test_grok_run_ledger.py +116 -0
  174. package/scripts/test_resolve_worker_spawn.py +2 -2
  175. package/scripts/test_spawn_grok_batch.py +295 -0
  176. package/scripts/test_verify_installable_package.py +208 -0
  177. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  178. package/scripts/verify_installable_package.py +612 -0
  179. package/settings.json +10 -0
  180. package/skills/CLAUDE.md +6 -2
  181. package/skills/_shared/CLAUDE.md +37 -4
  182. package/skills/_shared/advisor/CLAUDE.md +9 -0
  183. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  184. package/skills/_shared/advisor/scripts/README.md +9 -0
  185. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  186. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  187. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  188. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  189. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  190. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  191. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  192. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  193. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
  195. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  196. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  199. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  200. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  201. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  202. package/skills/_shared/pr-loop/state-schema.md +5 -0
  203. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  204. package/skills/autoconverge/reference/convergence.md +2 -1
  205. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  206. package/skills/beat-sheet/SKILL.md +54 -0
  207. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  208. package/skills/bugteam/CONSTRAINTS.md +4 -4
  209. package/skills/bugteam/EXAMPLES.md +1 -1
  210. package/skills/bugteam/reference/README.md +1 -1
  211. package/skills/e-code-review/SKILL.md +29 -3
  212. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  213. package/skills/e-code-review/reference/medium.md +15 -4
  214. package/skills/e-code-review/reference/runner-selection.md +40 -0
  215. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  216. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  217. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  218. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  219. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  220. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  221. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  222. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  223. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  224. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  225. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  226. package/skills/e-code-review/scripts/grok_code_review.py +230 -0
  227. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  228. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  229. package/skills/e-code-review/scripts/test_grok_code_review.py +289 -0
  230. package/skills/grok-spawn/SKILL.md +5 -0
  231. package/skills/orchestrator/SKILL.md +5 -0
  232. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  233. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  234. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  235. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  236. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  237. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  238. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  239. package/skills/pr-converge/scripts/conftest.py +35 -0
  240. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  241. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  242. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  243. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  244. package/skills/rebase/SKILL.md +15 -3
  245. package/skills/reviewer-gates/SKILL.md +2 -2
  246. package/skills/show/SKILL.md +51 -0
  247. package/skills/show/references/accessibility.md +7 -0
  248. package/skills/show/references/art.md +3 -0
  249. package/skills/show/references/charts.md +3 -0
  250. package/skills/show/references/core-design.md +14 -0
  251. package/skills/show/references/erds.md +3 -0
  252. package/skills/show/references/flowcharts.md +3 -0
  253. package/skills/show/references/host-and-html.md +3 -0
  254. package/skills/show/references/illustrative-diagrams.md +10 -0
  255. package/skills/show/references/interaction.md +3 -0
  256. package/skills/show/references/mockups.md +3 -0
  257. package/skills/show/references/quality-gates.md +7 -0
  258. package/skills/show/references/structural-diagrams.md +3 -0
  259. package/skills/show/references/subject-inventory.md +21 -0
  260. package/skills/show/references/svg-contract.md +22 -0
  261. package/skills/show/routing.yaml +30 -0
  262. package/skills/show/samples/pr1262-v2.svg +222 -0
  263. package/skills/show/scripts/README.md +6 -0
  264. package/skills/show/scripts/validate-artifact.py +91 -0
  265. package/skills/show/scripts/validate-package.py +18 -0
  266. package/skills/show/templates/html-widget.html +4 -0
  267. package/skills/show/templates/svg-base.svg +19 -0
  268. package/skills/show/tests/fixtures/css-var.svg +6 -0
  269. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  270. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  271. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  272. package/skills/show/tests/fixtures/invalid.svg +1 -0
  273. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  274. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  275. package/skills/show/tests/fixtures/valid.html +1 -0
  276. package/skills/show/tests/test_validate-artifact.py +74 -0
  277. package/skills/show/tests/test_validators.py +59 -0
  278. package/skills/show/workflows/create-visual.md +13 -0
  279. package/skills/show/workflows/review-visual.md +20 -0
  280. package/skills/split-pr/SKILL.md +85 -0
  281. package/skills/split-pr/reference/path-layers.md +16 -0
  282. package/skills/split-pr/reference/proposal-format.md +15 -0
  283. package/skills/split-pr/reference/split-further-loop.md +10 -0
  284. package/skills/split-pr/reference/splitting-principles.md +26 -0
  285. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  286. package/skills/split-pr/scripts/categorize_files.py +106 -0
  287. package/skills/split-pr/scripts/config/__init__.py +1 -0
  288. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  289. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  290. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  291. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  292. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  293. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  294. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  295. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  296. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  297. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  298. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  299. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  300. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  301. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  302. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  303. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  304. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  305. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  306. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  307. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  308. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  309. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  310. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  311. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  312. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  313. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  314. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  315. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  316. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  317. package/skills/split-pr/scripts/verify_plan.py +139 -0
  318. package/skills/task-build/reference/tool-routing.md +3 -0
  319. package/skills/team-advisor/SKILL.md +23 -44
  320. package/system-prompts/software-engineer.xml +17 -5
  321. package/commands/initialize.md +0 -90
  322. package/commands/stubcheck.md +0 -88
  323. package/skills/test_markdown_link_integrity.py +0 -107
@@ -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
+ }
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ """Grok medium-review discovery and verification orchestration.
3
+
4
+ Dispatches exactly eight named finder angles at one target head, binds each
5
+ finder to an isolated identity (worktree path, leader socket, advisor session),
6
+ deduplicates candidates, verifies them, and retains CONFIRMED plus unresolved
7
+ PLAUSIBLE findings with severity. Head drift rejects the whole batch.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+
14
+ from e_code_review_scripts_constants.grok_code_review_constants import (
15
+ ALL_MEDIUM_FINDER_ANGLES,
16
+ ALL_SEVERITIES,
17
+ ALL_VERIFICATION_VERDICTS,
18
+ MEDIUM_REVIEW_FINDER_COUNT,
19
+ MEDIUM_REVIEW_SCHEMA_VERSION,
20
+ VERDICT_CONFIRMED,
21
+ VERDICT_PLAUSIBLE,
22
+ VERDICT_REFUTED,
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FinderCandidate:
28
+ """One candidate finding from a discovery angle."""
29
+
30
+ angle: str
31
+ file_path: str
32
+ line_number: int
33
+ mechanism: str
34
+ scenario: str
35
+ worktree_path: str
36
+ leader_socket: str
37
+ advisor_session_id: str
38
+ reviewed_head: str
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class VerifiedFinding:
43
+ """A retained finding after verification."""
44
+
45
+ file_path: str
46
+ line_number: int
47
+ mechanism: str
48
+ scenario: str
49
+ verdict: str
50
+ severity: str
51
+ reviewed_head: str
52
+
53
+
54
+ @dataclass
55
+ class MediumReviewBatch:
56
+ """Result of one medium-review run at a single head."""
57
+
58
+ schema_version: str = MEDIUM_REVIEW_SCHEMA_VERSION
59
+ target_head: str = ""
60
+ diff_base: str = ""
61
+ all_finder_angles: tuple[str, ...] = ALL_MEDIUM_FINDER_ANGLES
62
+ all_retained_findings: list[VerifiedFinding] = field(default_factory=list)
63
+ is_rejected: bool = False
64
+ rejection_reason: str | None = None
65
+
66
+
67
+ def require_exact_finder_set(all_angles: tuple[str, ...] | list[str]) -> None:
68
+ """Require exactly the eight named finder angles.
69
+
70
+ Args:
71
+ all_angles: Angle names present in a run.
72
+
73
+ Raises:
74
+ ValueError: When the set is not exactly the eight angles.
75
+ """
76
+ angle_set = set(all_angles)
77
+ expected = set(ALL_MEDIUM_FINDER_ANGLES)
78
+ if angle_set != expected or len(all_angles) != MEDIUM_REVIEW_FINDER_COUNT:
79
+ raise ValueError(
80
+ f"finder angles must be exactly {MEDIUM_REVIEW_FINDER_COUNT}: "
81
+ f"got {sorted(angle_set)}"
82
+ )
83
+
84
+
85
+ def deduplicate_candidates(
86
+ all_candidates: list[FinderCandidate],
87
+ ) -> list[FinderCandidate]:
88
+ """Keep the most concrete scenario per file/line/mechanism.
89
+
90
+ Args:
91
+ all_candidates: Raw finder outputs.
92
+
93
+ Returns:
94
+ Deduplicated candidates preserving the longest scenario text.
95
+ """
96
+ best_by_key: dict[tuple[str, int, str], FinderCandidate] = {}
97
+ for each_candidate in all_candidates:
98
+ key = (
99
+ each_candidate.file_path,
100
+ each_candidate.line_number,
101
+ each_candidate.mechanism,
102
+ )
103
+ existing = best_by_key.get(key)
104
+ if existing is None or len(each_candidate.scenario) > len(existing.scenario):
105
+ best_by_key[key] = each_candidate
106
+ return list(best_by_key.values())
107
+
108
+
109
+ def retain_verified_findings(
110
+ *,
111
+ all_candidates: list[FinderCandidate],
112
+ verdict_by_key: dict[tuple[str, int, str], str],
113
+ severity_by_key: dict[tuple[str, int, str], str],
114
+ ) -> list[VerifiedFinding]:
115
+ """Retain CONFIRMED and unresolved PLAUSIBLE findings with severity.
116
+
117
+ Args:
118
+ all_candidates: Deduplicated candidates.
119
+ verdict_by_key: Verification verdict per file/line/mechanism.
120
+ severity_by_key: Severity per file/line/mechanism.
121
+
122
+ Returns:
123
+ Retained findings.
124
+
125
+ Raises:
126
+ ValueError: When a retained finding lacks a legal severity or verdict.
127
+ """
128
+ all_retained: list[VerifiedFinding] = []
129
+ for each_candidate in all_candidates:
130
+ key = (
131
+ each_candidate.file_path,
132
+ each_candidate.line_number,
133
+ each_candidate.mechanism,
134
+ )
135
+ verdict = verdict_by_key.get(key)
136
+ if verdict not in ALL_VERIFICATION_VERDICTS:
137
+ raise ValueError(f"missing or illegal verdict for {key}")
138
+ if verdict == VERDICT_REFUTED:
139
+ continue
140
+ if verdict not in {VERDICT_CONFIRMED, VERDICT_PLAUSIBLE}:
141
+ continue
142
+ severity = severity_by_key.get(key)
143
+ if severity not in ALL_SEVERITIES:
144
+ raise ValueError(f"missing or illegal severity for {key}")
145
+ all_retained.append(
146
+ VerifiedFinding(
147
+ file_path=each_candidate.file_path,
148
+ line_number=each_candidate.line_number,
149
+ mechanism=each_candidate.mechanism,
150
+ scenario=each_candidate.scenario,
151
+ verdict=verdict,
152
+ severity=severity,
153
+ reviewed_head=each_candidate.reviewed_head,
154
+ )
155
+ )
156
+ return all_retained
157
+
158
+
159
+ def run_medium_review(
160
+ *,
161
+ target_head: str,
162
+ diff_base: str,
163
+ all_finder_candidates: list[FinderCandidate],
164
+ verdict_by_key: dict[tuple[str, int, str], str],
165
+ severity_by_key: dict[tuple[str, int, str], str],
166
+ live_head: str,
167
+ is_any_advisor_blocked: bool = False,
168
+ ) -> MediumReviewBatch:
169
+ """Run discovery and verification for one medium-review head.
170
+
171
+ Args:
172
+ target_head: Reviewed head OID.
173
+ diff_base: Diff base OID.
174
+ all_finder_candidates: Outputs from the eight finder angles.
175
+ verdict_by_key: Verification results keyed by file/line/mechanism.
176
+ severity_by_key: Severities for retained findings.
177
+ live_head: Live head OID; must match target_head.
178
+ is_any_advisor_blocked: True when any finder ended advisor_blocked.
179
+
180
+ Raises:
181
+ ValueError: When finder angles are incomplete or duplicated.
182
+
183
+ Returns:
184
+ The medium-review batch (possibly rejected).
185
+ """
186
+ batch = MediumReviewBatch(target_head=target_head, diff_base=diff_base)
187
+ if is_any_advisor_blocked:
188
+ batch.is_rejected = True
189
+ batch.rejection_reason = "advisor_blocked"
190
+ return batch
191
+ if live_head != target_head:
192
+ batch.is_rejected = True
193
+ batch.rejection_reason = "head_drift"
194
+ return batch
195
+ for each_candidate in all_finder_candidates:
196
+ if each_candidate.reviewed_head != target_head:
197
+ batch.is_rejected = True
198
+ batch.rejection_reason = "finder_head_mismatch"
199
+ return batch
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
+ }
211
+ all_sockets = {each.leader_socket for each in all_finder_candidates}
212
+ all_sessions = {each.advisor_session_id for each in all_finder_candidates}
213
+ all_worktrees = {each.worktree_path for each in all_finder_candidates}
214
+ if (
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
219
+ ):
220
+ batch.is_rejected = True
221
+ batch.rejection_reason = "non_unique_finder_identity"
222
+ return batch
223
+ require_exact_finder_set(tuple(all_angles))
224
+ deduped = deduplicate_candidates(all_finder_candidates)
225
+ batch.all_retained_findings = retain_verified_findings(
226
+ all_candidates=deduped,
227
+ verdict_by_key=verdict_by_key,
228
+ severity_by_key=severity_by_key,
229
+ )
230
+ return batch
@@ -0,0 +1,180 @@
1
+ """Behavior tests for the effort evaluation harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
9
+ _CONSTANTS_PARENT = _SCRIPTS_DIRECTORY / "config"
10
+ for each_path in (_SCRIPTS_DIRECTORY, _CONSTANTS_PARENT):
11
+ path_text = str(each_path)
12
+ if path_text not in sys.path:
13
+ sys.path.insert(0, path_text)
14
+
15
+ from e_code_review_effort_constants import ( # noqa: E402
16
+ ALL_EFFORT_LEVELS,
17
+ ALL_FIXTURE_BANDS,
18
+ ALL_SKILL_EFFORT_LEVELS,
19
+ MINIMUM_QUALITY_HOLD_SCORE,
20
+ THINKING_ENABLED_DEFAULT,
21
+ WORKFLOW_FAMILY_E_CODE_REVIEW,
22
+ )
23
+ from effort_evaluation import ( # noqa: E402
24
+ build_synthetic_row,
25
+ evaluation_evidence_path,
26
+ fixtures_directory,
27
+ load_evaluation_evidence,
28
+ load_fixtures,
29
+ map_evaluation_effort_to_skill_level,
30
+ quality_holds,
31
+ recommend_effort_by_band,
32
+ resolve_skill_effort_for_band,
33
+ skill_defaults_from_recommendation,
34
+ validate_evaluation_row,
35
+ )
36
+
37
+
38
+ def test_fixtures_directory_points_at_frozen_json() -> None:
39
+ directory = fixtures_directory()
40
+ assert directory.is_dir()
41
+ assert directory.name == "fixtures"
42
+ assert (directory / "easy.json").is_file()
43
+
44
+
45
+ def test_loads_three_frozen_fixture_bands() -> None:
46
+ all_fixtures = load_fixtures()
47
+ assert len(all_fixtures) == len(ALL_FIXTURE_BANDS)
48
+ bands = {str(each_fixture["band"]) for each_fixture in all_fixtures}
49
+ assert bands == set(ALL_FIXTURE_BANDS)
50
+ for each_fixture in all_fixtures:
51
+ assert each_fixture["fixture_id"]
52
+ assert isinstance(each_fixture["seeded_findings"], list)
53
+ assert len(each_fixture["seeded_findings"]) >= 1
54
+
55
+
56
+ def test_validate_row_accepts_complete_row_and_rejects_thinking_off() -> None:
57
+ good_row = build_synthetic_row(
58
+ fixture_id="easy-comment-preservation",
59
+ fixture_band="easy",
60
+ effort="low",
61
+ quality_score=0.9,
62
+ finding_recall=1.0,
63
+ finding_precision=1.0,
64
+ visible_tokens=1200,
65
+ latency_ms=4000,
66
+ )
67
+ assert validate_evaluation_row(good_row) == []
68
+ assert good_row["thinking_enabled"] is THINKING_ENABLED_DEFAULT
69
+ bad_row = dict(good_row)
70
+ bad_row["thinking_enabled"] = False
71
+ problems = validate_evaluation_row(bad_row)
72
+ assert any("thinking_enabled" in each_problem for each_problem in problems)
73
+ bool_score_row = dict(good_row)
74
+ bool_score_row["quality_score"] = True
75
+ bool_problems = validate_evaluation_row(bool_score_row)
76
+ assert any("quality_score" in each_problem for each_problem in bool_problems)
77
+
78
+
79
+ def test_recommend_picks_lowest_effort_that_holds_quality() -> None:
80
+ all_rows = [
81
+ build_synthetic_row("easy-a", "easy", "low", 0.5, 0.5, 0.5, 100, 1000),
82
+ build_synthetic_row("easy-a", "easy", "medium", 0.95, 0.95, 0.95, 200, 2000),
83
+ build_synthetic_row("easy-a", "easy", "xhigh", 0.99, 0.99, 0.99, 400, 5000),
84
+ build_synthetic_row("medium-a", "medium", "high", 0.9, 0.9, 0.9, 300, 3000),
85
+ build_synthetic_row(
86
+ "demanding-a", "demanding", "max", 0.85, 0.85, 0.85, 800, 9000
87
+ ),
88
+ ]
89
+ for each_row in all_rows:
90
+ assert validate_evaluation_row(each_row) == []
91
+ recommendation = recommend_effort_by_band(all_rows)
92
+ by_band = recommendation["recommendation_by_band"]
93
+ assert isinstance(by_band, dict)
94
+ assert by_band["easy"]["recommended_effort"] == "medium"
95
+ assert by_band["easy"]["cited_row"]["effort"] == "medium"
96
+ assert by_band["medium"]["recommended_effort"] == "high"
97
+ assert by_band["demanding"]["recommended_effort"] == "max"
98
+ assert recommendation["thinking_enabled"] is True
99
+ assert recommendation["cost_latency_lever"] == "effort"
100
+ assert recommendation["defaults_unchanged"] is True
101
+
102
+
103
+ def test_recommend_blocks_band_without_holding_row() -> None:
104
+ all_rows = [
105
+ build_synthetic_row("easy-a", "easy", "low", 0.1, 0.1, 0.1, 50, 500),
106
+ ]
107
+ recommendation = recommend_effort_by_band(all_rows)
108
+ easy = recommendation["recommendation_by_band"]["easy"]
109
+ assert easy["recommended_effort"] is None
110
+ assert easy["cited_row"] is None
111
+ assert "quality floor" in str(easy["blocker"])
112
+
113
+
114
+ def test_quality_holds_uses_named_floor() -> None:
115
+ holding = build_synthetic_row(
116
+ "x", "easy", "low", MINIMUM_QUALITY_HOLD_SCORE, 1.0, 1.0, 1, 1
117
+ )
118
+ failing = build_synthetic_row(
119
+ "x", "easy", "low", MINIMUM_QUALITY_HOLD_SCORE - 0.01, 1.0, 1.0, 1, 1
120
+ )
121
+ assert quality_holds(holding) is True
122
+ assert quality_holds(failing) is False
123
+
124
+
125
+ def test_effort_levels_cover_cli_set() -> None:
126
+ assert "low" in ALL_EFFORT_LEVELS
127
+ assert "max" in ALL_EFFORT_LEVELS
128
+ assert len(ALL_EFFORT_LEVELS) == 5
129
+
130
+
131
+ def test_evidence_rows_validate_and_skill_defaults_cite_rows() -> None:
132
+ evidence_path = evaluation_evidence_path()
133
+ assert evidence_path.is_file()
134
+ assert evidence_path.name == "effort_defaults_evidence.json"
135
+ evidence = load_evaluation_evidence()
136
+ assert evidence["workflow_family"] == WORKFLOW_FAMILY_E_CODE_REVIEW
137
+ assert evidence["thinking_enabled"] is True
138
+ all_rows = evidence["all_rows"]
139
+ assert isinstance(all_rows, list)
140
+ for each_row in all_rows:
141
+ assert isinstance(each_row, dict)
142
+ assert validate_evaluation_row(each_row) == []
143
+ recommendation = recommend_effort_by_band(all_rows)
144
+ skill_defaults = skill_defaults_from_recommendation(recommendation)
145
+ assert skill_defaults["workflow_family"] == WORKFLOW_FAMILY_E_CODE_REVIEW
146
+ default_by_band = skill_defaults["default_by_band"]
147
+ assert isinstance(default_by_band, dict)
148
+ for each_band in ALL_FIXTURE_BANDS:
149
+ band_default = default_by_band[each_band]
150
+ assert band_default["skill_effort"] in ALL_SKILL_EFFORT_LEVELS
151
+ assert band_default["cited_row"] is not None
152
+ assert band_default["cited_row"]["thinking_enabled"] is True
153
+
154
+
155
+ def test_committed_skill_defaults_match_resolver() -> None:
156
+ assert resolve_skill_effort_for_band("easy") == "medium"
157
+ assert resolve_skill_effort_for_band("medium") == "xhigh"
158
+ assert resolve_skill_effort_for_band("demanding") == "xhigh"
159
+ evidence = load_evaluation_evidence()
160
+ committed = evidence["skill_defaults"]["default_by_band"]
161
+ for each_band in ALL_FIXTURE_BANDS:
162
+ assert committed[each_band]["skill_effort"] == resolve_skill_effort_for_band(
163
+ each_band
164
+ )
165
+ assert committed[each_band]["cited_row"]["effort"] is not None
166
+
167
+
168
+ def test_map_high_and_max_to_skill_xhigh() -> None:
169
+ assert map_evaluation_effort_to_skill_level("high") == "xhigh"
170
+ assert map_evaluation_effort_to_skill_level("max") == "xhigh"
171
+ assert map_evaluation_effort_to_skill_level("medium") == "medium"
172
+
173
+
174
+ def test_resolve_rejects_unknown_band() -> None:
175
+ try:
176
+ resolve_skill_effort_for_band("not-a-band")
177
+ except ValueError as error:
178
+ assert "unknown fixture band" in str(error)
179
+ else:
180
+ raise AssertionError("expected ValueError for unknown band")