claude-dev-env 1.23.1 → 1.25.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.
- package/docs/CODE_RULES.md +14 -1
- package/hooks/blocking/_gh_body_arg_utils.py +171 -13
- package/hooks/blocking/code-rules-enforcer.py +490 -15
- package/hooks/blocking/gh-body-arg-blocker.py +27 -21
- package/hooks/blocking/pr-description-enforcer.py +247 -11
- package/hooks/blocking/tdd-enforcer.py +208 -13
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +116 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +231 -0
- package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +144 -0
- package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +102 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +176 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +112 -0
- package/hooks/blocking/test_gh_body_arg_blocker.py +229 -2
- package/hooks/blocking/test_pr_description_enforcer.py +193 -3
- package/hooks/blocking/test_tdd_enforcer.py +249 -0
- package/hooks/validators/exempt_paths.py +99 -0
- package/hooks/validators/magic_value_checks.py +126 -26
- package/hooks/validators/test_magic_value_checks.py +356 -2
- package/package.json +1 -1
- package/rules/gh-body-file.md +11 -2
- package/skills/bugteam/SKILL.md +111 -59
- package/skills/searching-obsidian-vault/SKILL.md +131 -0
package/docs/CODE_RULES.md
CHANGED
|
@@ -43,13 +43,26 @@ These rules are automatically enforced by `code-rules-enforcer.py`. Violations b
|
|
|
43
43
|
|
|
44
44
|
| Rule | What's Checked |
|
|
45
45
|
|------|----------------|
|
|
46
|
-
| No NEW comments | `#` / `//` in new production code only (existing comments NEVER removed; shebangs, type
|
|
46
|
+
| No NEW comments | `#` / `//` in new production code only (existing comments NEVER removed; exempt markers: shebangs, `# type:`, `# noqa`, `# pylint:`, `# pragma:`, `// @ts-`, `// eslint-`, `// prettier-`, `/// `; docstrings and module docstrings are always allowed; all test files are exempt) |
|
|
47
47
|
| Imports at top | No `import` inside function bodies |
|
|
48
48
|
| Logging format args | No `log_*(f"...")` - use `log_*("...", arg)` |
|
|
49
49
|
| File line count | Advisory only — see [File length guidance](#65-file-length-guidance) |
|
|
50
50
|
| Magic values | No literals in production function bodies (0, 1, -1 exempt). **Test files exempt.** Includes string templates — if you strip the interpolations from an f-string and the remaining literal text is structural (paths, URLs, patterns), those fragments are magic values that belong in config |
|
|
51
51
|
| Constants location | No `UPPER_SNAKE =` outside `config/` in **production code**. **Test files may define local constants.** |
|
|
52
52
|
|
|
53
|
+
### Where UPPER_SNAKE is allowed
|
|
54
|
+
|
|
55
|
+
The "Constants location" rule is enforced at Write time. The hook exempts these path families where UPPER_SNAKE identifiers are either the canonical home or the native convention rather than misplaced scalar constants:
|
|
56
|
+
|
|
57
|
+
| Path pattern | Why it is exempt |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `config/*` | Canonical home for scalar constants. |
|
|
60
|
+
| `/migrations/` (Django migrations) | Migration files are self-contained by framework convention; their UPPER_SNAKE identifiers are operation names, not misplaced configuration. |
|
|
61
|
+
| `/workflow/`, `_tab.py`, `/states.py`, `/modules.py` (path normalized to forward slashes, matched as substrings) | Workflow state and module registries declare `StateDefinition` / `WorkflowModule` instances as module-level singletons using UPPER_SNAKE names. These are registry entries, not constants to hoist. |
|
|
62
|
+
| Test files (`test_*.py`, `*_test.py`, `*.spec.*`, `conftest.py`, paths under `/tests/`) | Test files may define local constants without using `config/`. |
|
|
63
|
+
|
|
64
|
+
Any production file outside these families that defines an UPPER_SNAKE at module scope is still flagged and must be moved to `config/`.
|
|
65
|
+
|
|
53
66
|
---
|
|
54
67
|
|
|
55
68
|
## 3. REUSE CONSTANTS (DRY CONFIG)
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"""Shared gh body-arg parsing utilities for blocking hooks."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
from typing import Iterator
|
|
7
|
+
|
|
3
8
|
body_file_flag: str = "--body-file"
|
|
4
9
|
body_file_flag_prefix: str = "--body-file="
|
|
10
|
+
body_file_short_flag: str = "-F"
|
|
11
|
+
body_file_short_flag_prefix: str = "-F="
|
|
5
12
|
|
|
6
13
|
all_body_flags: frozenset[str] = frozenset({"--body", "-b"})
|
|
7
14
|
all_body_flag_prefixes: tuple[str, ...] = ("--body=", "-b=")
|
|
@@ -25,24 +32,175 @@ all_value_flags: frozenset[str] = frozenset(
|
|
|
25
32
|
"-H",
|
|
26
33
|
"--repo",
|
|
27
34
|
"-R",
|
|
35
|
+
"--template",
|
|
36
|
+
"-T",
|
|
37
|
+
"--recover",
|
|
28
38
|
body_file_flag,
|
|
39
|
+
body_file_short_flag,
|
|
29
40
|
}
|
|
30
41
|
)
|
|
31
42
|
|
|
43
|
+
all_value_flag_equals_prefixes: tuple[str, ...] = tuple(
|
|
44
|
+
sorted((f"{each_flag}=" for each_flag in all_value_flags), key=len, reverse=True)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_all_equals_prefixes_for_skip: tuple[str, ...] = tuple(
|
|
48
|
+
sorted(
|
|
49
|
+
set(all_value_flag_equals_prefixes) | set(all_body_flag_prefixes),
|
|
50
|
+
key=len,
|
|
51
|
+
reverse=True,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
bash_continuation_marker: str = "\\"
|
|
56
|
+
powershell_continuation_marker: str = "`"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _count_trailing_run(text: str, marker_character: str) -> int:
|
|
60
|
+
trailing_run_length = 0
|
|
61
|
+
for each_character in reversed(text):
|
|
62
|
+
if each_character != marker_character:
|
|
63
|
+
break
|
|
64
|
+
trailing_run_length += 1
|
|
65
|
+
return trailing_run_length
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_bash_continuation(stripped_line: str) -> bool:
|
|
69
|
+
return _count_trailing_run(stripped_line, bash_continuation_marker) == 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_powershell_continuation(stripped_line: str) -> bool:
|
|
73
|
+
if _count_trailing_run(stripped_line, powershell_continuation_marker) != 1:
|
|
74
|
+
return False
|
|
75
|
+
if len(stripped_line) < 2:
|
|
76
|
+
return False
|
|
77
|
+
character_before_marker = stripped_line[-2]
|
|
78
|
+
return character_before_marker.isspace()
|
|
79
|
+
|
|
32
80
|
|
|
33
81
|
def get_logical_first_line(command: str) -> str:
|
|
34
|
-
|
|
82
|
+
logical_line = ""
|
|
35
83
|
for each_line in command.splitlines():
|
|
36
84
|
stripped_line = each_line.rstrip()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
if _is_bash_continuation(stripped_line) or _is_powershell_continuation(stripped_line):
|
|
86
|
+
logical_line += stripped_line[:-1].rstrip() + " "
|
|
87
|
+
continue
|
|
88
|
+
logical_line += each_line
|
|
89
|
+
break
|
|
90
|
+
return logical_line.strip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_flag_shaped(token: str) -> bool:
|
|
94
|
+
if len(token) < 2:
|
|
95
|
+
return False
|
|
96
|
+
if not token.startswith("-"):
|
|
97
|
+
return False
|
|
98
|
+
second_character = token[1]
|
|
99
|
+
if second_character == "-":
|
|
100
|
+
return len(token) > 2 and token[2].isalpha()
|
|
101
|
+
return second_character.isalpha()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _quoted_value_starts_split(value_token: str) -> bool:
|
|
105
|
+
if len(value_token) < 2:
|
|
106
|
+
return False
|
|
107
|
+
first_character = value_token[0]
|
|
108
|
+
if first_character not in {'"', "'"}:
|
|
109
|
+
return False
|
|
110
|
+
inside_quote = True
|
|
111
|
+
for each_character in value_token[1:]:
|
|
112
|
+
if each_character == first_character:
|
|
113
|
+
inside_quote = not inside_quote
|
|
114
|
+
return inside_quote
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def count_extra_tokens_to_skip_for_split_quoted_value(
|
|
118
|
+
remaining_tokens: list[str],
|
|
119
|
+
value_token: str,
|
|
120
|
+
) -> int | None:
|
|
121
|
+
if not _quoted_value_starts_split(value_token):
|
|
122
|
+
return 0
|
|
123
|
+
opening_quote = value_token[0]
|
|
124
|
+
extra_tokens_consumed = 0
|
|
125
|
+
for each_remaining_token in remaining_tokens:
|
|
126
|
+
extra_tokens_consumed += 1
|
|
127
|
+
if each_remaining_token.count(opening_quote) % 2 == 1:
|
|
128
|
+
return extra_tokens_consumed
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _match_equals_prefix_for_skip(token: str) -> str | None:
|
|
133
|
+
for each_prefix in _all_equals_prefixes_for_skip:
|
|
134
|
+
if token.startswith(each_prefix):
|
|
135
|
+
return each_prefix
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def iter_significant_tokens(
|
|
140
|
+
command: str,
|
|
141
|
+
pre_tokenized: tuple[str, list[str]] | None = None,
|
|
142
|
+
) -> Iterator[tuple[str, list[str]]]:
|
|
143
|
+
"""Yield (token, remaining_tokens) for every flag/positional after continuation join.
|
|
144
|
+
|
|
145
|
+
Joins bash/PowerShell continuations, tokenizes with shlex.split(posix=False),
|
|
146
|
+
then yields each flag and positional along with the remaining tokens. Values
|
|
147
|
+
of value-taking flags (including quoted values split across multiple
|
|
148
|
+
posix=False tokens) are SKIPPED from yield so that --body embedded in a
|
|
149
|
+
quoted --title value is never seen as a standalone flag. Equals-form value
|
|
150
|
+
flags whose quoted value may span multiple posix=False tokens are yielded
|
|
151
|
+
as-is and any trailing split-quote continuation tokens are skipped. A
|
|
152
|
+
value-taking flag whose next token is itself flag-shaped is treated as
|
|
153
|
+
value-missing: the flag is yielded but the flag-shaped follower is NOT
|
|
154
|
+
skipped (so a malformed --body-file --body "x" still yields --body).
|
|
155
|
+
|
|
156
|
+
When count_extra_tokens_to_skip_for_split_quoted_value returns None (opening
|
|
157
|
+
quote never closed among remaining tokens), raises ValueError so callers can
|
|
158
|
+
conservatively block -- the token stream is irrecoverably malformed.
|
|
159
|
+
|
|
160
|
+
If pre_tokenized is provided as (logical_line, raw_tokens), reuses those
|
|
161
|
+
instead of recomputing from command. The command argument is still required
|
|
162
|
+
for the public signature but is unused when pre_tokenized is given.
|
|
163
|
+
|
|
164
|
+
Raises ValueError if the logical line is unparseable by shlex, or if an
|
|
165
|
+
unclosed quoted value is detected in a value-taking flag.
|
|
166
|
+
"""
|
|
167
|
+
if pre_tokenized is not None:
|
|
168
|
+
logical_line, all_tokens = pre_tokenized
|
|
169
|
+
else:
|
|
170
|
+
logical_line = get_logical_first_line(command)
|
|
171
|
+
if not logical_line:
|
|
172
|
+
return
|
|
173
|
+
all_tokens = shlex.split(logical_line, posix=False)
|
|
174
|
+
token_index = 0
|
|
175
|
+
while token_index < len(all_tokens):
|
|
176
|
+
current_token = all_tokens[token_index]
|
|
177
|
+
remaining_tokens = all_tokens[token_index + 1:]
|
|
178
|
+
matched_equals_prefix = _match_equals_prefix_for_skip(current_token)
|
|
179
|
+
if matched_equals_prefix is not None:
|
|
180
|
+
value_token = current_token[len(matched_equals_prefix):]
|
|
181
|
+
split_value_extra_tokens = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
182
|
+
remaining_tokens,
|
|
183
|
+
value_token,
|
|
184
|
+
)
|
|
185
|
+
if split_value_extra_tokens is None:
|
|
186
|
+
raise ValueError("unclosed quoted value in equals-form flag")
|
|
187
|
+
yield current_token, remaining_tokens
|
|
188
|
+
token_index += 1 + split_value_extra_tokens
|
|
189
|
+
continue
|
|
190
|
+
if current_token in all_value_flags:
|
|
191
|
+
if not remaining_tokens or _is_flag_shaped(remaining_tokens[0]):
|
|
192
|
+
yield current_token, remaining_tokens
|
|
193
|
+
token_index += 1
|
|
194
|
+
continue
|
|
195
|
+
value_token = remaining_tokens[0]
|
|
196
|
+
split_value_extra_tokens = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
197
|
+
remaining_tokens[1:],
|
|
198
|
+
value_token,
|
|
199
|
+
)
|
|
200
|
+
if split_value_extra_tokens is None:
|
|
201
|
+
raise ValueError("unclosed quoted value in space-form flag")
|
|
202
|
+
yield current_token, remaining_tokens[1 + split_value_extra_tokens:]
|
|
203
|
+
token_index += 1 + 1 + split_value_extra_tokens
|
|
204
|
+
continue
|
|
205
|
+
yield current_token, remaining_tokens
|
|
206
|
+
token_index += 1
|