claude-dev-env 1.58.0 → 1.59.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 (52) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
  6. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
  7. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
  8. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  9. package/bin/install.mjs +100 -27
  10. package/bin/install.test.mjs +133 -1
  11. package/docs/CODE_RULES.md +3 -3
  12. package/hooks/blocking/code_rules_annotations_length.py +153 -0
  13. package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
  14. package/hooks/blocking/code_rules_duplicate_body.py +287 -0
  15. package/hooks/blocking/code_rules_enforcer.py +175 -21
  16. package/hooks/blocking/code_rules_magic_values.py +98 -0
  17. package/hooks/blocking/code_rules_shared.py +41 -0
  18. package/hooks/blocking/destructive_command_blocker.py +1027 -12
  19. package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
  20. package/hooks/blocking/subprocess_budget_completeness.py +380 -0
  21. package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
  22. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  23. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
  24. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
  25. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
  26. package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +622 -3
  28. package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
  29. package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
  30. package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
  31. package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
  32. package/hooks/hooks.json +15 -0
  33. package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
  34. package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
  35. package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
  36. package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
  37. package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
  38. package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
  39. package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
  40. package/package.json +1 -1
  41. package/rules/docstring-prose-matches-implementation.md +43 -0
  42. package/rules/hook-prose-matches-detector.md +26 -0
  43. package/rules/no-inline-destructive-literals.md +11 -0
  44. package/rules/workflow-substitution-slots.md +7 -0
  45. package/skills/autoconverge/SKILL.md +13 -2
  46. package/skills/autoconverge/reference/convergence.md +7 -3
  47. package/skills/autoconverge/reference/stop-conditions.md +7 -2
  48. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
  49. package/skills/autoconverge/workflow/converge.mjs +106 -36
  50. package/skills/pr-converge/scripts/check_convergence.py +195 -64
  51. package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
  52. package/skills/update/SKILL.md +37 -5
@@ -0,0 +1,242 @@
1
+ """Unit tests for workflow_substitution_slot_blocker PreToolUse hook."""
2
+
3
+ import importlib.util
4
+ import io
5
+ import json
6
+ import pathlib
7
+ import sys
8
+ from unittest import mock
9
+
10
+ _HOOK_DIR = pathlib.Path(__file__).parent
11
+ _HOOKS_ROOT = _HOOK_DIR.parent
12
+ for _each_root in (str(_HOOK_DIR), str(_HOOKS_ROOT)):
13
+ if _each_root not in sys.path:
14
+ sys.path.insert(0, _each_root)
15
+
16
+ hook_spec = importlib.util.spec_from_file_location(
17
+ "workflow_substitution_slot_blocker",
18
+ _HOOK_DIR / "workflow_substitution_slot_blocker.py",
19
+ )
20
+ assert hook_spec is not None
21
+ assert hook_spec.loader is not None
22
+ hook_module = importlib.util.module_from_spec(hook_spec)
23
+ hook_spec.loader.exec_module(hook_module)
24
+
25
+ content_has_violation = hook_module.content_has_violation
26
+ find_bare_index_segments = hook_module.find_bare_index_segments
27
+ find_bare_path_segments = hook_module.find_bare_path_segments
28
+ has_iteration_loop = hook_module.has_iteration_loop
29
+ written_content = hook_module.written_content
30
+
31
+
32
+ _VIOLATING_TEMPLATE = (
33
+ "For EACH candidate i, build a bible dir cand_i per the contract.\n"
34
+ " & ${PY} -c \"...Path(r'${args.work_dir}\\\\cand_i\\\\plate.svg')...\"\n"
35
+ " & ${PY} compose.py --out ${args.work_dir}\\\\cand_i\\\\sample.png "
36
+ "--glow <candidate glow_hex>\n"
37
+ 'Return: {key: "cand_i", name, sample_png}\n'
38
+ )
39
+
40
+ _FIXED_TEMPLATE = (
41
+ "For EACH candidate i, build a bible dir cand_<i> per the contract.\n"
42
+ " & ${PY} -c \"...Path(r'${args.work_dir}\\\\cand_<i>\\\\plate.svg')...\"\n"
43
+ " & ${PY} compose.py --out ${args.work_dir}\\\\cand_<i>\\\\sample.png "
44
+ "--glow <candidate glow_hex>\n"
45
+ 'Return: {key: "cand_<i>", name, sample_png}\n'
46
+ )
47
+
48
+
49
+ def test_detects_bare_index_in_path_segment() -> None:
50
+ assert find_bare_index_segments(
51
+ "render Path(r'${args.work_dir}\\\\cand_i\\\\plate.svg')"
52
+ ) == {"cand_i"}
53
+
54
+
55
+ def test_detects_quoted_key_when_token_also_appears_as_path_segment() -> None:
56
+ looped_path_and_key = "write ${work}\\\\cand_i\\\\plate.svg\n{key: \"cand_i\", name}"
57
+ assert "cand_i" in find_bare_index_segments(looped_path_and_key)
58
+
59
+
60
+ def test_quoted_key_alone_without_path_segment_is_not_detected() -> None:
61
+ assert find_bare_index_segments('{key: "metric_i", name}') == set()
62
+
63
+
64
+ def test_index_segments_equal_path_segments_for_looped_path_and_key() -> None:
65
+ looped_path_and_key = "write ${work}\\\\cand_i\\\\plate.svg\n{key: \"cand_i\", name}"
66
+ assert find_bare_index_segments(looped_path_and_key) == find_bare_path_segments(
67
+ looped_path_and_key
68
+ )
69
+
70
+
71
+ def test_index_segments_equal_path_segments_for_quoted_only_key() -> None:
72
+ quoted_only_key = '{key: "metric_i", name}'
73
+ assert find_bare_index_segments(quoted_only_key) == find_bare_path_segments(
74
+ quoted_only_key
75
+ )
76
+
77
+
78
+ def test_marked_substitution_slot_is_not_a_bare_segment() -> None:
79
+ assert (
80
+ find_bare_index_segments(
81
+ "render Path(r'${args.work_dir}\\\\cand_<i>\\\\plate.svg')"
82
+ )
83
+ == set()
84
+ )
85
+
86
+
87
+ def test_violating_template_is_flagged() -> None:
88
+ assert content_has_violation(_VIOLATING_TEMPLATE) is True
89
+
90
+
91
+ def test_fixed_template_passes() -> None:
92
+ assert content_has_violation(_FIXED_TEMPLATE) is False
93
+
94
+
95
+ def test_template_without_angle_convention_is_not_flagged() -> None:
96
+ no_convention = (
97
+ "For EACH candidate i, write to ${work}\\\\cand_i\\\\plate.svg and return.\n"
98
+ )
99
+ assert content_has_violation(no_convention) is False
100
+
101
+
102
+ def test_template_without_loop_is_not_flagged() -> None:
103
+ no_loop = "Write the plate to ${work}\\\\cand_i\\\\plate.svg using <glow_hex>.\n"
104
+ assert content_has_violation(no_loop) is False
105
+
106
+
107
+ def test_each_inside_an_ordinary_word_is_not_a_loop() -> None:
108
+ for each_word in ("reach", "teach", "breach", "bleach", "preach", "impeach"):
109
+ assert has_iteration_loop(each_word + " the end") is False
110
+
111
+
112
+ def test_standalone_lowercase_each_in_prose_is_not_a_loop() -> None:
113
+ assert has_iteration_loop("use each color once") is False
114
+
115
+
116
+ def test_standalone_each_keyword_is_a_loop() -> None:
117
+ assert has_iteration_loop("For EACH candidate i") is True
118
+
119
+
120
+ def test_lowercase_for_each_phrase_is_still_a_loop() -> None:
121
+ assert has_iteration_loop("for each candidate") is True
122
+
123
+
124
+ def test_benign_prose_each_with_fixed_literal_is_not_flagged() -> None:
125
+ benign_template = (
126
+ "Render each layer to <layer.svg>.\n"
127
+ "The protocol field is named 'tier_i' as a permanent identifier.\n"
128
+ )
129
+ assert content_has_violation(benign_template) is False
130
+
131
+
132
+ def test_quoted_permanent_identifier_key_is_not_flagged() -> None:
133
+ permanent_identifier_template = (
134
+ 'For EACH candidate, render <plate.svg>.\nReturn {key: "metric_i", value}'
135
+ )
136
+ assert content_has_violation(permanent_identifier_template) is False
137
+
138
+
139
+ def test_quoted_key_flagged_only_when_token_also_appears_as_path_segment() -> None:
140
+ looping_path_and_key = (
141
+ "For EACH candidate, write <plate.svg> to ${work}\\\\cand_i\\\\plate.svg.\n"
142
+ 'Return {key: "cand_i", name}\n'
143
+ )
144
+ assert content_has_violation(looping_path_and_key) is True
145
+
146
+
147
+ def test_written_content_reads_multiedit_new_strings() -> None:
148
+ multi_edit_input = {
149
+ "edits": [
150
+ {"old_string": "x", "new_string": "first ${work}\\\\cand_i\\\\plate.svg"},
151
+ {"old_string": "y", "new_string": "second <glow_hex>"},
152
+ ]
153
+ }
154
+ combined = written_content("MultiEdit", multi_edit_input)
155
+ assert "cand_i" in combined
156
+ assert "<glow_hex>" in combined
157
+
158
+
159
+ def _run_main_with_io(input_text: str) -> str:
160
+ with mock.patch("sys.stdin", io.StringIO(input_text)):
161
+ with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
162
+ try:
163
+ hook_module.main()
164
+ except SystemExit:
165
+ pass
166
+ return mock_stdout.getvalue()
167
+
168
+
169
+ def test_main_blocks_violating_workflow_write() -> None:
170
+ hook_input = {
171
+ "tool_name": "Write",
172
+ "tool_input": {
173
+ "file_path": "/repo/scripts/shared_palette_gate.workflow.js",
174
+ "content": _VIOLATING_TEMPLATE,
175
+ },
176
+ }
177
+ output_text = _run_main_with_io(json.dumps(hook_input))
178
+ payload = json.loads(output_text)
179
+ assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
180
+
181
+
182
+ def test_main_blocks_violating_workflow_edit() -> None:
183
+ hook_input = {
184
+ "tool_name": "Edit",
185
+ "tool_input": {
186
+ "file_path": "/repo/scripts/shared_palette_gate.workflow.js",
187
+ "new_string": _VIOLATING_TEMPLATE,
188
+ },
189
+ }
190
+ output_text = _run_main_with_io(json.dumps(hook_input))
191
+ payload = json.loads(output_text)
192
+ assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
193
+
194
+
195
+ def test_main_blocks_violating_workflow_multiedit() -> None:
196
+ hook_input = {
197
+ "tool_name": "MultiEdit",
198
+ "tool_input": {
199
+ "file_path": "/repo/scripts/shared_palette_gate.workflow.js",
200
+ "edits": [{"old_string": "placeholder", "new_string": _VIOLATING_TEMPLATE}],
201
+ },
202
+ }
203
+ output_text = _run_main_with_io(json.dumps(hook_input))
204
+ payload = json.loads(output_text)
205
+ assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
206
+
207
+
208
+ def test_main_passes_fixed_workflow_write() -> None:
209
+ hook_input = {
210
+ "tool_name": "Write",
211
+ "tool_input": {
212
+ "file_path": "/repo/scripts/shared_palette_gate.workflow.js",
213
+ "content": _FIXED_TEMPLATE,
214
+ },
215
+ }
216
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
217
+
218
+
219
+ def test_main_passes_non_workflow_path() -> None:
220
+ hook_input = {
221
+ "tool_name": "Write",
222
+ "tool_input": {
223
+ "file_path": "/repo/scripts/helper.js",
224
+ "content": _VIOLATING_TEMPLATE,
225
+ },
226
+ }
227
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
228
+
229
+
230
+ def test_main_passes_wrong_tool_name() -> None:
231
+ hook_input = {
232
+ "tool_name": "Bash",
233
+ "tool_input": {
234
+ "file_path": "/repo/scripts/x.workflow.js",
235
+ "command": "echo cand_i",
236
+ },
237
+ }
238
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
239
+
240
+
241
+ def test_main_passes_malformed_json() -> None:
242
+ assert _run_main_with_io("not valid json {{{") == ""
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: block bare per-iteration index tokens in .workflow.js templates.
3
+
4
+ Root cause: a `.workflow.js` agent-prompt block that loops over an index (for
5
+ example "For EACH candidate i, build a dir cand_i ...") sometimes writes the
6
+ per-iteration directory or output key as a bare token like `cand_i`. A bare
7
+ `_i`-suffixed token reads as a fixed literal rather than a substitution slot, so
8
+ an agent can plausibly create one literal directory named `cand_i` and overwrite
9
+ it across every iteration -- collapsing an N-iteration gate into a single run.
10
+
11
+ The established convention in these templates marks every per-call substitution
12
+ slot with angle brackets (`<plate.svg>`, `<object.svg>`, `<glow_hex>`). The fix
13
+ is to mark the index the same way: `cand_<i>`.
14
+
15
+ Detection strategy: act only on Write/Edit to a path ending in `.workflow.js`.
16
+ Within the written content, fire only when ALL of the following hold, so the
17
+ hook catches exactly the bare-literal shape and never a template that does not
18
+ use the substitution convention at all:
19
+
20
+ 1. the content uses the angle-bracket substitution convention somewhere
21
+ (a `<...>` slot), proving the author marks per-call values that way;
22
+ 2. the content establishes a per-iteration loop (an "each"/"EACH"/"for i"
23
+ style phrase, or an explicit `cand_0` enumeration);
24
+ 3. a bare `<word>_<i|j|k>` token appears as a per-iteration path segment
25
+ (adjacent to a path separator). A quoted structured-output key whose name
26
+ ends in `_i|_j|_k` (a permanent identifier with no per-iteration path) does
27
+ not fire on its own; only the per-iteration path shape triggers a block.
28
+
29
+ Fails OPEN (approves) on malformed input or a non-workflow path; the violation
30
+ shape is narrow enough that a false negative is preferable to blocking
31
+ unrelated edits.
32
+ """
33
+
34
+ import json
35
+ import re
36
+ import sys
37
+ from pathlib import Path
38
+
39
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
40
+ if _hooks_dir not in sys.path:
41
+ sys.path.insert(0, _hooks_dir)
42
+
43
+ from hooks_constants.workflow_substitution_slot_blocker_constants import ( # noqa: E402
44
+ CORRECTIVE_MESSAGE,
45
+ EDIT_TOOL_NAME,
46
+ MULTI_EDIT_TOOL_NAME,
47
+ WORKFLOW_FILE_SUFFIX,
48
+ WRITE_TOOL_NAME,
49
+ )
50
+
51
+ def multi_edit_new_strings(all_tool_input: dict[str, object]) -> str:
52
+ all_edits = all_tool_input.get("edits", [])
53
+ if not isinstance(all_edits, list):
54
+ return ""
55
+ all_new_strings = [
56
+ each_edit["new_string"]
57
+ for each_edit in all_edits
58
+ if isinstance(each_edit, dict) and isinstance(each_edit.get("new_string"), str)
59
+ ]
60
+ return "\n".join(all_new_strings)
61
+
62
+
63
+ def written_content(tool_name: str, all_tool_input: dict[str, object]) -> str:
64
+ if tool_name == WRITE_TOOL_NAME:
65
+ content = all_tool_input.get("content", "")
66
+ return content if isinstance(content, str) else ""
67
+ if tool_name == EDIT_TOOL_NAME:
68
+ new_string = all_tool_input.get("new_string", "")
69
+ return new_string if isinstance(new_string, str) else ""
70
+ if tool_name == MULTI_EDIT_TOOL_NAME:
71
+ return multi_edit_new_strings(all_tool_input)
72
+ return ""
73
+
74
+
75
+ def target_path(all_tool_input: dict[str, object]) -> str:
76
+ file_path = all_tool_input.get("file_path", "")
77
+ return file_path if isinstance(file_path, str) else ""
78
+
79
+
80
+ def uses_angle_slot_convention(content: str) -> bool:
81
+ angle_slot_pattern = re.compile(r"<[^<>\n]+>")
82
+ return bool(angle_slot_pattern.search(content))
83
+
84
+
85
+ def has_iteration_loop(content: str) -> bool:
86
+ loop_phrase_pattern = re.compile(
87
+ r"\b(?:for\s+each|each\s+candidate|for\s+[ijk]\b|candidate\s+[ijk]\b|cand_0)\b",
88
+ re.IGNORECASE,
89
+ )
90
+ uppercase_each_keyword_pattern = re.compile(r"\bEACH\b")
91
+ return bool(
92
+ loop_phrase_pattern.search(content)
93
+ or uppercase_each_keyword_pattern.search(content)
94
+ )
95
+
96
+
97
+ def find_bare_path_segments(content: str) -> set[str]:
98
+ loop_letters = "ijk"
99
+ path_context = re.compile(
100
+ r"(?:[\\/]\s*([A-Za-z][\w]*?_[" + loop_letters + r"])(?![\w>])"
101
+ r"|([A-Za-z][\w]*?_[" + loop_letters + r"])(?![\w>])\s*[\\/])"
102
+ )
103
+ all_path_segments: set[str] = set()
104
+ for each_match in path_context.finditer(content):
105
+ each_token = next(
106
+ (each_group for each_group in each_match.groups() if each_group),
107
+ "",
108
+ )
109
+ if each_token:
110
+ all_path_segments.add(each_token)
111
+ return all_path_segments
112
+
113
+
114
+ def find_bare_index_segments(content: str) -> set[str]:
115
+ return find_bare_path_segments(content)
116
+
117
+
118
+ def content_has_violation(content: str) -> bool:
119
+ if not uses_angle_slot_convention(content):
120
+ return False
121
+ if not has_iteration_loop(content):
122
+ return False
123
+ return bool(find_bare_index_segments(content))
124
+
125
+
126
+ def main() -> None:
127
+ try:
128
+ hook_input = json.load(sys.stdin)
129
+ except json.JSONDecodeError:
130
+ sys.exit(0)
131
+
132
+ tool_name = hook_input.get("tool_name", "")
133
+ if tool_name not in (WRITE_TOOL_NAME, EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME):
134
+ sys.exit(0)
135
+
136
+ all_tool_input = hook_input.get("tool_input", {})
137
+ if not isinstance(all_tool_input, dict):
138
+ sys.exit(0)
139
+
140
+ if not target_path(all_tool_input).endswith(WORKFLOW_FILE_SUFFIX):
141
+ sys.exit(0)
142
+
143
+ if not content_has_violation(written_content(tool_name, all_tool_input)):
144
+ sys.exit(0)
145
+
146
+ deny_payload = {
147
+ "hookSpecificOutput": {
148
+ "hookEventName": "PreToolUse",
149
+ "permissionDecision": "deny",
150
+ "permissionDecisionReason": CORRECTIVE_MESSAGE,
151
+ }
152
+ }
153
+ print(json.dumps(deny_payload))
154
+ sys.stdout.flush()
155
+ sys.exit(0)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
package/hooks/hooks.json CHANGED
@@ -44,12 +44,27 @@
44
44
  "type": "command",
45
45
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/state_description_blocker.py",
46
46
  "timeout": 10
47
+ },
48
+ {
49
+ "type": "command",
50
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/subprocess_budget_completeness.py",
51
+ "timeout": 10
52
+ },
53
+ {
54
+ "type": "command",
55
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/hook_prose_detector_consistency.py",
56
+ "timeout": 10
47
57
  }
48
58
  ]
49
59
  },
50
60
  {
51
61
  "matcher": "Write|Edit|MultiEdit",
52
62
  "hooks": [
63
+ {
64
+ "type": "command",
65
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/workflow_substitution_slot_blocker.py",
66
+ "timeout": 10
67
+ },
53
68
  {
54
69
  "type": "command",
55
70
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/open_questions_in_plans_blocker.py",
@@ -24,6 +24,8 @@ ALL_MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
24
24
  ADVISORY_LINE_THRESHOLD_SOFT = 400
25
25
  ADVISORY_LINE_THRESHOLD_HARD = 1000
26
26
 
27
+ DENY_REASON_ISSUE_PREVIEW_COUNT = 10
28
+
27
29
  ALL_BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_", "was_", "did_")
28
30
  UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
29
31
 
@@ -108,6 +110,20 @@ ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
108
110
  })
109
111
  ALL_UNION_TYPING_NAMES: frozenset[str] = frozenset({"Optional", "Union"})
110
112
  ALL_SELF_AND_CLS_PARAMETER_NAMES: frozenset[str] = frozenset({"self", "cls"})
113
+ ANNOTATION_BY_PYTEST_FIXTURE: dict[str, str] = {
114
+ "tmp_path": "Path",
115
+ "tmp_path_factory": "pytest.TempPathFactory",
116
+ "monkeypatch": "pytest.MonkeyPatch",
117
+ "capsys": "pytest.CaptureFixture[str]",
118
+ "capfd": "pytest.CaptureFixture[str]",
119
+ "caplog": "pytest.LogCaptureFixture",
120
+ "request": "pytest.FixtureRequest",
121
+ }
122
+ KNOWN_PYTEST_FIXTURE_ANNOTATION_MESSAGE_SUFFIX: str = (
123
+ "known pytest fixture parameter must carry its single documented type "
124
+ "(CODE_RULES §6; pytest builtin fixture reference "
125
+ "https://docs.pytest.org/en/stable/reference/fixtures.html)"
126
+ )
111
127
  ALL_LOOP_INDEX_LETTER_EXEMPTIONS: frozenset[str] = frozenset({"i", "j", "k", "_"})
112
128
  EACH_PREFIX = "each_"
113
129
  BARE_EACH_TOKEN = "each"
@@ -0,0 +1,25 @@
1
+ """Constants for the dead dataclass-field detector in ``code_rules_enforcer``.
2
+
3
+ Lives under the hooks-tree ``hooks_constants`` package so module-level
4
+ UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
5
+ requirement and share a home with the other hook-tree configuration.
6
+ """
7
+
8
+ ALL_DATACLASS_DECORATOR_NAMES: frozenset[str] = frozenset({"dataclass", "dataclasses"})
9
+ ATTRGETTER_FUNCTION_NAME: str = "attrgetter"
10
+ CLASSVAR_ANNOTATION_NAME: str = "ClassVar"
11
+ GETATTR_FUNCTION_NAME: str = "getattr"
12
+ GETATTR_NAME_ARGUMENT_MINIMUM: int = 2
13
+ ALL_REFLECTIVE_FIELD_CONSUMER_NAMES: frozenset[str] = frozenset(
14
+ {"asdict", "astuple", "fields", "replace", "vars"}
15
+ )
16
+ WHOLE_INSTANCE_DICT_ATTRIBUTE_NAME: str = "__dict__"
17
+ ALL_WHOLE_INSTANCE_STRINGIFY_NAMES: frozenset[str] = frozenset(
18
+ {"str", "repr", "format"}
19
+ )
20
+ MAX_DEAD_DATACLASS_FIELD_ISSUES: int = 25
21
+ DEAD_DATACLASS_FIELD_GUIDANCE: str = (
22
+ "field is assigned but never read in this file - remove the field and the code"
23
+ " that only exists to populate it, or read it where the value is needed"
24
+ " (CODE_RULES §9.8)"
25
+ )
@@ -0,0 +1,178 @@
1
+ """Segment-splitting and command-name constants for the destructive command blocker compound rm guard."""
2
+
3
+ ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset({"&&", "||", ";", "|&", "|", "&", "\n", "\r"})
4
+ ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
5
+ {
6
+ "timeout",
7
+ "nohup",
8
+ "nice",
9
+ "ionice",
10
+ "stdbuf",
11
+ "time",
12
+ "setsid",
13
+ "chrt",
14
+ "taskset",
15
+ }
16
+ )
17
+ ALL_LAUNCHERS_REQUIRING_A_POSITIONAL_VALUE: frozenset[str] = frozenset(
18
+ {
19
+ "timeout",
20
+ "chrt",
21
+ "taskset",
22
+ }
23
+ )
24
+ ALL_INTERPRETER_AND_WRAPPER_COMMANDS: frozenset[str] = frozenset(
25
+ {
26
+ "sh",
27
+ "bash",
28
+ "zsh",
29
+ "dash",
30
+ "ksh",
31
+ "tcsh",
32
+ "csh",
33
+ "fish",
34
+ "pwsh",
35
+ "powershell",
36
+ "cmd",
37
+ "eval",
38
+ "exec",
39
+ "source",
40
+ "sudo",
41
+ "su",
42
+ "env",
43
+ "xargs",
44
+ "awk",
45
+ "gawk",
46
+ "mawk",
47
+ "nawk",
48
+ "make",
49
+ "tclsh",
50
+ "expect",
51
+ "lua",
52
+ }
53
+ )
54
+ ALL_REMOTE_AND_PROGRAM_STRING_EXECUTORS: frozenset[str] = frozenset(
55
+ {
56
+ "ssh",
57
+ "python",
58
+ "python2",
59
+ "python3",
60
+ "perl",
61
+ "ruby",
62
+ "node",
63
+ "deno",
64
+ "bun",
65
+ "php",
66
+ }
67
+ )
68
+ ALL_STRING_ARGUMENT_EXECUTION_FLAGS: frozenset[str] = frozenset({"-c", "-e"})
69
+ ALL_BENIGN_COMPOUND_SEGMENT_COMMANDS: frozenset[str] = frozenset(
70
+ {
71
+ "echo",
72
+ "printf",
73
+ "gh",
74
+ "head",
75
+ "tail",
76
+ "cat",
77
+ "ls",
78
+ "grep",
79
+ "wc",
80
+ "sort",
81
+ "uniq",
82
+ "true",
83
+ "git",
84
+ }
85
+ )
86
+ OUTPUT_REDIRECTION_OPERATOR_PATTERN: str = r"(?:\d+|&)?>>?\|?(?!&[\d-])"
87
+ ALL_FILE_WRITING_OUTPUT_FLAGS_BY_BENIGN_PROGRAM: dict[str, frozenset[str]] = {
88
+ "sort": frozenset({"-o", "--output"}),
89
+ }
90
+ ALL_GIT_CONFIG_READ_ONLY_FLAGS: frozenset[str] = frozenset(
91
+ {"--get", "--get-all", "--get-regexp", "--list", "-l", "--get-urlmatch"}
92
+ )
93
+ ALL_GIT_REMOTE_READ_ONLY_VERBS: frozenset[str] = frozenset({"show", "get-url"})
94
+ ALL_GIT_FETCH_FORCE_FLAGS: frozenset[str] = frozenset({"-f", "--force"})
95
+ ALL_GH_HTTP_WRITE_METHOD_FLAGS: frozenset[str] = frozenset({"-X", "--method"})
96
+ ALL_GH_HTTP_WRITE_METHODS: frozenset[str] = frozenset({"POST", "PUT", "PATCH", "DELETE"})
97
+ GH_HTTP_READ_ONLY_METHOD: str = "GET"
98
+ GH_SHORT_METHOD_FLAG_PREFIX: str = "-X"
99
+ GH_LONG_METHOD_FLAG_EQUALS_PREFIX: str = "--method="
100
+ ALL_GH_API_REQUEST_BODY_FIELD_FLAGS: frozenset[str] = frozenset(
101
+ {"-f", "--raw-field", "-F", "--field", "--input"}
102
+ )
103
+ ALL_GH_API_GLUED_REQUEST_BODY_FIELD_FLAG_PREFIXES: tuple[str, ...] = (
104
+ "-f",
105
+ "-F",
106
+ "--raw-field=",
107
+ "--field=",
108
+ "--input=",
109
+ )
110
+ ALL_READ_ONLY_GIT_SUBCOMMANDS: frozenset[str] = frozenset(
111
+ {
112
+ "status",
113
+ "log",
114
+ "show",
115
+ "diff",
116
+ "rev-parse",
117
+ "rev-list",
118
+ "describe",
119
+ "config",
120
+ "remote",
121
+ "fetch",
122
+ "ls-files",
123
+ "ls-remote",
124
+ "ls-tree",
125
+ "cat-file",
126
+ "blame",
127
+ "shortlog",
128
+ "name-rev",
129
+ "for-each-ref",
130
+ "symbolic-ref",
131
+ "merge-base",
132
+ "count-objects",
133
+ "version",
134
+ "help",
135
+ }
136
+ )
137
+ ALL_READ_ONLY_GH_SUBCOMMANDS: frozenset[str] = frozenset(
138
+ {
139
+ "view",
140
+ "list",
141
+ "status",
142
+ "checks",
143
+ "diff",
144
+ "search",
145
+ "browse",
146
+ "api",
147
+ }
148
+ )
149
+ ALL_READ_ONLY_SUBCOMMANDS_BY_DISPATCHING_PROGRAM: dict[str, frozenset[str]] = {
150
+ "git": ALL_READ_ONLY_GIT_SUBCOMMANDS,
151
+ "gh": ALL_READ_ONLY_GH_SUBCOMMANDS,
152
+ }
153
+ ALL_READ_ONLY_SUBCOMMAND_POSITION_DEPTHS_BY_DISPATCHING_PROGRAM: dict[str, int] = {
154
+ "git": 1,
155
+ "gh": 2,
156
+ }
157
+ LAUNCHER_POSITIONAL_VALUE_SHAPE_PATTERN: str = (
158
+ r"^(?:0x[0-9A-Fa-f]+"
159
+ r"|[0-9]+(?:[.,][0-9]+)?[smhd]?"
160
+ r"|[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*)$"
161
+ )
162
+ ALL_LAUNCHER_OPTIONS_TAKING_SEPARATE_VALUE: frozenset[str] = frozenset(
163
+ {
164
+ "-s",
165
+ "--signal",
166
+ "-k",
167
+ "--kill-after",
168
+ "-n",
169
+ "-o",
170
+ "--output",
171
+ "-e",
172
+ "--error",
173
+ "-i",
174
+ "--input",
175
+ "--classdata",
176
+ }
177
+ )
178
+ ALL_SUBSHELL_GROUPING_CHARACTERS: str = "({"
@@ -0,0 +1,17 @@
1
+ """Constants for the cross-file duplicate-function-body scan in ``code_rules_enforcer``.
2
+
3
+ The scan flags a top-level function whose body is structurally identical to a
4
+ top-level function already defined in a sibling ``.py`` module in the same
5
+ directory. This catches the Reuse-before-create / DRY violation where a helper
6
+ is copy-pasted across several modules instead of imported from one shared home.
7
+ """
8
+
9
+ MINIMUM_DUPLICATE_BODY_STATEMENTS: int = 3
10
+ MAX_DUPLICATE_BODY_ISSUES: int = 25
11
+ DUNDER_INIT_FILENAME: str = "__init__.py"
12
+ PYTHON_SOURCE_SUFFIX: str = ".py"
13
+ DUPLICATE_BODY_GUIDANCE: str = (
14
+ "this function body is identical to one in a sibling module; "
15
+ "extract a single shared helper (for example in hooks_constants/) and "
16
+ "import it from both modules instead of copying it (Reuse before create / DRY)"
17
+ )