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.
Files changed (25) hide show
  1. package/docs/CODE_RULES.md +14 -1
  2. package/hooks/blocking/_gh_body_arg_utils.py +171 -13
  3. package/hooks/blocking/code-rules-enforcer.py +490 -15
  4. package/hooks/blocking/gh-body-arg-blocker.py +27 -21
  5. package/hooks/blocking/pr-description-enforcer.py +247 -11
  6. package/hooks/blocking/tdd-enforcer.py +208 -13
  7. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +116 -0
  8. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +231 -0
  9. package/hooks/blocking/test_code_rules_enforcer_conftest_anchor.py +51 -0
  10. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +55 -0
  11. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +144 -0
  12. package/hooks/blocking/test_code_rules_enforcer_logger_fstring.py +102 -0
  13. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +76 -0
  14. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +176 -0
  15. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +112 -0
  16. package/hooks/blocking/test_gh_body_arg_blocker.py +229 -2
  17. package/hooks/blocking/test_pr_description_enforcer.py +193 -3
  18. package/hooks/blocking/test_tdd_enforcer.py +249 -0
  19. package/hooks/validators/exempt_paths.py +99 -0
  20. package/hooks/validators/magic_value_checks.py +126 -26
  21. package/hooks/validators/test_magic_value_checks.py +356 -2
  22. package/package.json +1 -1
  23. package/rules/gh-body-file.md +11 -2
  24. package/skills/bugteam/SKILL.md +111 -59
  25. package/skills/searching-obsidian-vault/SKILL.md +131 -0
@@ -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:, noqa, eslint, docstrings, module docstrings, and all test files exempt) |
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
- logical = ""
82
+ logical_line = ""
35
83
  for each_line in command.splitlines():
36
84
  stripped_line = each_line.rstrip()
37
- is_backslash_continuation = (
38
- stripped_line.endswith("\\") and stripped_line.count("\\") % 2 == 1
39
- )
40
- is_powershell_backtick_continuation = (
41
- stripped_line.endswith("`") and stripped_line.count("`") % 2 == 1
42
- )
43
- if is_backslash_continuation or is_powershell_backtick_continuation:
44
- logical += stripped_line[:-1].rstrip() + " "
45
- else:
46
- logical += each_line
47
- break
48
- return logical.strip()
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