claude-dev-env 8.25.0 → 8.26.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 +2 -2
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -82
- package/hooks/blocking/code_rules_comments.py +14 -23
- package/hooks/blocking/code_rules_enforcer.py +0 -6
- package/hooks/blocking/code_rules_imports_logging.py +1 -120
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +9 -12
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +0 -10
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +0 -16
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/blocking_check_limits.py +0 -13
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -8
- package/hooks/hooks_constants/local_identity.py +7 -120
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +0 -6
- package/hooks/hooks_constants/test_local_identity.py +7 -43
- package/hooks/hooks_constants/thread_spawn_pace_hook_constants.py +45 -0
- package/hooks/hooks_constants/usage_pace_constants.py +49 -0
- package/hooks/routing/conftest.py +10 -0
- package/hooks/routing/test_thread_spawn_pace_hook.py +139 -0
- package/hooks/routing/test_usage_pace.py +142 -0
- package/hooks/routing/thread_spawn_pace_hook.py +220 -0
- package/hooks/routing/usage_pace.py +345 -0
- package/package.json +1 -1
- package/rules/shell-invocation.md +1 -1
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +0 -4
- package/scripts/repository_checks/config/constants.py +0 -5
- package/scripts/test_resolve_worker_spawn.py +0 -28
- package/docs/nas-ssh-invocation.md +0 -107
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +0 -266
- package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +0 -87
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +0 -134
- package/rules/nas-ssh-invocation.md +0 -29
package/docs/CODE_RULES.md
CHANGED
|
@@ -13,7 +13,7 @@ Do not add code comments. Preserve existing comments. Docstrings remain allowed.
|
|
|
13
13
|
When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
|
|
14
14
|
Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
|
|
15
15
|
|
|
16
|
-
A keep marker is the one comment that may be added and kept: a comment that opens with
|
|
16
|
+
A keep marker is the one comment that may be added and kept: a comment that opens with a prefix the repository lists under `comment_keep_markers` in `.claude/policy-lint.json`.
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
@@ -31,7 +31,7 @@ A keep marker is the one comment that may be added and kept: a comment that open
|
|
|
31
31
|
|
|
32
32
|
The staged policy lint reports each of these through `code_rules_enforcer.py` and names the specific breach; exact patterns and exemption lists live in that module:
|
|
33
33
|
|
|
34
|
-
no new comments · imports at top · logging format args (`log_*("...", arg)`) · no
|
|
34
|
+
no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`; `/migrations/`; Workflow registries: path contains any of these substrings — `/workflow/`, `_tab.py`, `/states.py`, or `/modules.py`, each matching independently as a substring, so `pkg/states.py` qualifies while a top-level `states.py` follows the standard `config/` rule; test files — path or filename matches `test_`, `_test.`, `.spec.`, `conftest`, or `/tests/`) · no hardcoded user home paths · guarded `sys.path.insert` · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`, a parameter typed bare `object` whose body reads `param.attribute`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost) · JavaScript/TypeScript boolean declarations (`const`/`let`/`var` bound to a boolean literal or negation) and `@param {boolean}` JSDoc names prefixed `is`/`has`/`should`/`can`/`was`/`did` (camelCase forms) · banned identifiers as `.mjs`/`.js` declaration names (`result`, `data`, `ctx`, `msg`, …), scoped to changed lines · in test files, banned identifiers fire on changed lines, and pytest-collectable `test_*` functions need a return annotation · unused module-level imports and unsorted import blocks belong to ruff (F401, isort I001) · a `hooks/blocking/` command classifier anchors its multi-word command regex to the command start (`^`/`\A`) or tokenizes the first word (`shlex.split`), never matching a command as a bare substring
|
|
35
35
|
|
|
36
36
|
Test files follow the comment policy above; other test-specific exemptions are listed here. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks.
|
|
37
37
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Boolean naming-prefix
|
|
1
|
+
"""Boolean naming-prefix checks."""
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import sys
|
|
@@ -21,12 +21,8 @@ from code_rules_shared import ( # noqa: E402
|
|
|
21
21
|
is_workflow_registry_file,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
25
|
-
MAX_IGNORED_MUST_CHECK_RETURN_ISSUES,
|
|
26
|
-
)
|
|
27
24
|
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
28
25
|
ALL_BOOLEAN_NAME_PREFIXES,
|
|
29
|
-
ALL_MUST_CHECK_RETURN_FUNCTION_NAMES,
|
|
30
26
|
ALL_SELF_AND_CLS_PARAMETER_NAMES,
|
|
31
27
|
UPPER_SNAKE_CONSTANT_PATTERN,
|
|
32
28
|
)
|
|
@@ -271,80 +267,3 @@ def _called_terminal_name(call_node: ast.Call) -> str | None:
|
|
|
271
267
|
if isinstance(callee, ast.Attribute):
|
|
272
268
|
return callee.attr
|
|
273
269
|
return None
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def check_ignored_must_check_return(
|
|
277
|
-
content: str,
|
|
278
|
-
file_path: str,
|
|
279
|
-
all_changed_lines: set[int] | None = None,
|
|
280
|
-
defer_scope_to_caller: bool = False,
|
|
281
|
-
) -> list[str]:
|
|
282
|
-
"""Flag bare-expression calls whose discarded return is the only failure signal.
|
|
283
|
-
|
|
284
|
-
Functions in ``ALL_MUST_CHECK_RETURN_FUNCTION_NAMES`` report success or failure
|
|
285
|
-
solely through their return value. A bare-statement call discards that value,
|
|
286
|
-
so the caller silently proceeds on failure. Bare ``ast.Expr`` calls are flagged,
|
|
287
|
-
including a bare ``await``-wrapped call (``await find_and_click(...)`` as a
|
|
288
|
-
statement); an assigned or branched-on call is exempt.
|
|
289
|
-
|
|
290
|
-
The caller passes the reconstructed full file as *content* so ``ast.parse``
|
|
291
|
-
sees a complete module rather than an Edit's ``new_string`` fragment, which is
|
|
292
|
-
rarely valid standalone Python (a bare ``await find_and_click(...)`` line is a
|
|
293
|
-
SyntaxError on its own). Findings are then scoped to *all_changed_lines* so an
|
|
294
|
-
Edit blocks on the discarded return it just introduced while a pre-existing
|
|
295
|
-
violation on an untouched line does not block the edit.
|
|
296
|
-
|
|
297
|
-
Args:
|
|
298
|
-
content: The source text to inspect — the reconstructed full file on an
|
|
299
|
-
Edit so the parse succeeds.
|
|
300
|
-
file_path: The path the source will be written to, used for exemptions.
|
|
301
|
-
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
302
|
-
None to treat the whole file as in scope. When provided, a violation
|
|
303
|
-
blocks only when the bare call's line intersects the changed lines.
|
|
304
|
-
defer_scope_to_caller: When True, return every violation so the
|
|
305
|
-
commit/push gate's ``split_violations_by_scope`` can scope by added
|
|
306
|
-
line.
|
|
307
|
-
|
|
308
|
-
Returns:
|
|
309
|
-
One issue per discarded must-check return, scoped to the changed lines
|
|
310
|
-
unless *defer_scope_to_caller* is True or *all_changed_lines* is None. When
|
|
311
|
-
*defer_scope_to_caller* is True every violation is returned uncapped so the
|
|
312
|
-
gate can scope by added line and apply its own ceiling; otherwise the
|
|
313
|
-
terminal result is capped at the module limit.
|
|
314
|
-
"""
|
|
315
|
-
if is_test_file(file_path):
|
|
316
|
-
return []
|
|
317
|
-
try:
|
|
318
|
-
tree = ast.parse(content)
|
|
319
|
-
except SyntaxError:
|
|
320
|
-
return []
|
|
321
|
-
all_violations_in_walk_order: list[tuple[range, str]] = []
|
|
322
|
-
for each_node in ast.walk(tree):
|
|
323
|
-
if not isinstance(each_node, ast.Expr):
|
|
324
|
-
continue
|
|
325
|
-
expression_value = each_node.value
|
|
326
|
-
call_node = (
|
|
327
|
-
expression_value.value
|
|
328
|
-
if isinstance(expression_value, ast.Await)
|
|
329
|
-
else expression_value
|
|
330
|
-
)
|
|
331
|
-
if not isinstance(call_node, ast.Call):
|
|
332
|
-
continue
|
|
333
|
-
called_name = _called_terminal_name(call_node)
|
|
334
|
-
if called_name is None or called_name not in ALL_MUST_CHECK_RETURN_FUNCTION_NAMES:
|
|
335
|
-
continue
|
|
336
|
-
end_line_number = each_node.end_lineno or each_node.lineno
|
|
337
|
-
line_span = range(each_node.lineno, end_line_number + 1)
|
|
338
|
-
message = (
|
|
339
|
-
f"Line {each_node.lineno}: return value of {called_name}() is discarded - "
|
|
340
|
-
"assign and check it (the boolean/outcome is the only failure signal)"
|
|
341
|
-
)
|
|
342
|
-
all_violations_in_walk_order.append((line_span, message))
|
|
343
|
-
scoped_issues = _scope_violations_to_changed_lines(
|
|
344
|
-
all_violations_in_walk_order,
|
|
345
|
-
all_changed_lines,
|
|
346
|
-
defer_scope_to_caller,
|
|
347
|
-
)
|
|
348
|
-
if defer_scope_to_caller:
|
|
349
|
-
return scoped_issues
|
|
350
|
-
return scoped_issues[:MAX_IGNORED_MUST_CHECK_RETURN_ISSUES]
|
|
@@ -36,7 +36,6 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
36
36
|
REPOSITORY_LINT_SETTINGS_RELATIVE_PATH,
|
|
37
37
|
REPOSITORY_ROOT_MARKER_NAME,
|
|
38
38
|
SETTINGS_TEXT_ENCODING,
|
|
39
|
-
STEALTH_KEEP_COMMENT_MARKER,
|
|
40
39
|
)
|
|
41
40
|
_javascript_comment_scanner = importlib.import_module("javascript_comment_scanner")
|
|
42
41
|
extract_javascript_comment_occurrences = (
|
|
@@ -180,26 +179,22 @@ def comment_keep_markers(file_path: str) -> tuple[str, ...]:
|
|
|
180
179
|
::
|
|
181
180
|
|
|
182
181
|
repo/.claude/policy-lint.json {"comment_keep_markers": ["VENDOR: Keep"]}
|
|
183
|
-
repo/app/main.py ("
|
|
184
|
-
no settings file (
|
|
182
|
+
repo/app/main.py ("VENDOR: Keep",)
|
|
183
|
+
no settings file ()
|
|
185
184
|
|
|
186
185
|
The repository that holds the file names its own markers. A missing or
|
|
187
|
-
unreadable settings file
|
|
186
|
+
unreadable settings file names none.
|
|
188
187
|
|
|
189
188
|
Args:
|
|
190
189
|
file_path: The path of the file under check.
|
|
191
190
|
|
|
192
191
|
Returns:
|
|
193
|
-
The
|
|
194
|
-
adds.
|
|
192
|
+
The markers the file's repository names.
|
|
195
193
|
"""
|
|
196
194
|
for each_directory in Path(file_path).resolve().parents:
|
|
197
195
|
if (each_directory / REPOSITORY_ROOT_MARKER_NAME).exists():
|
|
198
|
-
return (
|
|
199
|
-
|
|
200
|
-
*_repository_keep_markers(each_directory),
|
|
201
|
-
)
|
|
202
|
-
return (STEALTH_KEEP_COMMENT_MARKER,)
|
|
196
|
+
return _repository_keep_markers(each_directory)
|
|
197
|
+
return ()
|
|
203
198
|
|
|
204
199
|
|
|
205
200
|
def _is_keep_occurrence(comment_text: str, all_keep_markers: tuple[str, ...]) -> bool:
|
|
@@ -230,10 +225,9 @@ def check_comment_changes(old_content: str, new_content: str, file_path: str) ->
|
|
|
230
225
|
"""Check for comment additions or removals between old and new content.
|
|
231
226
|
|
|
232
227
|
Inline and standalone comments are blocking findings when added, except
|
|
233
|
-
keep-marker occurrences (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
line.
|
|
228
|
+
keep-marker occurrences (a marker the file's repository names in
|
|
229
|
+
``.claude/policy-lint.json``), which are excluded entirely so the marker
|
|
230
|
+
can be added and can persist across edits to its line.
|
|
237
231
|
Existing comments can be removed when the touched code no longer needs them.
|
|
238
232
|
|
|
239
233
|
When the file is Python and either *old_content* or *new_content* cannot
|
|
@@ -431,14 +425,11 @@ def _is_exempt_python_comment(comment_token: tokenize.TokenInfo) -> bool:
|
|
|
431
425
|
token-anchored directive body never legitimately carries a ``#``
|
|
432
426
|
(noqa codes, pylint symbols, and pragma directives contain none), so
|
|
433
427
|
any inner ``#`` reliably marks chained prose. Free-form markers
|
|
434
|
-
(``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
annotation text by convention, and ``STEALTH: Keep`` is the
|
|
440
|
-
acknowledged-bypass marker the CDP runtime-usage scanner reads
|
|
441
|
-
from the flagged line together with its reason.
|
|
428
|
+
(``type:``, ``TODO``, ``FIXME``, ``HACK``, ``XXX``) accept any
|
|
429
|
+
trailing prose: ``# type:`` participates in the documented
|
|
430
|
+
justification convention enforced by ``check_type_escape_hatches``
|
|
431
|
+
(which requires a trailing reason), and the TODO-family markers carry
|
|
432
|
+
annotation text by convention.
|
|
442
433
|
"""
|
|
443
434
|
comment_string = comment_token.string
|
|
444
435
|
if comment_string.startswith("#!") and comment_token.start == (1, 0):
|
|
@@ -46,7 +46,6 @@ from code_rules_blast_radius import ( # noqa: E402
|
|
|
46
46
|
)
|
|
47
47
|
from code_rules_boolean_mustcheck import ( # noqa: E402
|
|
48
48
|
check_boolean_naming,
|
|
49
|
-
check_ignored_must_check_return,
|
|
50
49
|
)
|
|
51
50
|
from code_rules_command_dispatch import ( # noqa: E402
|
|
52
51
|
check_unanchored_command_dispatch,
|
|
@@ -86,7 +85,6 @@ from code_rules_imports_logging import ( # noqa: E402
|
|
|
86
85
|
check_library_print,
|
|
87
86
|
check_logging_adjacent_string_literals,
|
|
88
87
|
check_logging_fstrings,
|
|
89
|
-
check_logging_printf_tokens,
|
|
90
88
|
check_naive_datetime_construction,
|
|
91
89
|
check_windows_api_none,
|
|
92
90
|
)
|
|
@@ -329,7 +327,6 @@ def _python_comment_and_logging_issues(context: _ValidationContext) -> list[str]
|
|
|
329
327
|
all_issues.extend(check_comment_changes(old_content, content, file_path))
|
|
330
328
|
all_issues.extend(check_imports_at_top(content))
|
|
331
329
|
all_issues.extend(check_logging_fstrings(content))
|
|
332
|
-
all_issues.extend(check_logging_printf_tokens(content, file_path))
|
|
333
330
|
all_issues.extend(check_logging_adjacent_string_literals(content, file_path))
|
|
334
331
|
all_issues.extend(check_windows_api_none(content))
|
|
335
332
|
all_issues.extend(check_naive_datetime_construction(content, file_path))
|
|
@@ -450,9 +447,6 @@ def _python_boolean_and_test_assertion_issues(context: _ValidationContext) -> li
|
|
|
450
447
|
)
|
|
451
448
|
changed, defer = context.all_changed_lines, context.defer_scope_to_caller
|
|
452
449
|
all_issues = check_boolean_naming(effective_content, file_path, changed, defer)
|
|
453
|
-
all_issues.extend(
|
|
454
|
-
check_ignored_must_check_return(effective_content, file_path, changed, defer)
|
|
455
|
-
)
|
|
456
450
|
all_issues.extend(check_skip_decorators_in_tests(content, file_path))
|
|
457
451
|
all_issues.extend(
|
|
458
452
|
check_tests_use_isolated_filesystem_paths(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Import and logging convention checks: check_imports_at_top, check_logging_fstrings,
|
|
1
|
+
"""Import and logging convention checks: check_imports_at_top, check_logging_fstrings, check_logging_adjacent_string_literals, check_windows_api_none, check_naive_datetime_construction, check_e2e_test_naming, check_js_resume_task_enumeration_coverage, check_js_returns_object_schemaless_branch, check_js_sibling_return_object_key_drift, check_js_bare_flag_return_directive, and check_library_print."""
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import re
|
|
@@ -25,7 +25,6 @@ from code_rules_shared import ( # noqa: E402
|
|
|
25
25
|
|
|
26
26
|
from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
27
27
|
ALL_ALWAYS_NAIVE_DATETIME_CONSTRUCTORS,
|
|
28
|
-
ALL_FORMAT_LOGGER_FUNCTION_NAMES,
|
|
29
28
|
DATETIME_CLASS_ATTRIBUTE_NAME,
|
|
30
29
|
FROMTIMESTAMP_POSITIONAL_TIMEZONE_ARGUMENT_COUNT,
|
|
31
30
|
MAX_E2E_TEST_NAMING_ISSUES,
|
|
@@ -35,7 +34,6 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
35
34
|
MAX_JS_SIBLING_RETURN_OBJECT_KEY_DRIFT_ISSUES,
|
|
36
35
|
MAX_LOGGING_ADJACENT_LITERAL_ISSUES,
|
|
37
36
|
MAX_LOGGING_FSTRING_ISSUES,
|
|
38
|
-
MAX_LOGGING_PRINTF_TOKEN_ISSUES,
|
|
39
37
|
MAX_NAIVE_DATETIME_ISSUES,
|
|
40
38
|
MAX_WINDOWS_API_NONE_ISSUES,
|
|
41
39
|
MINIMUM_RESUME_TASK_ENUMERATION_ITEMS,
|
|
@@ -87,9 +85,7 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
87
85
|
JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
|
|
88
86
|
LOGGING_FSTRING_PATTERN,
|
|
89
87
|
LOGGING_HELPER_FUNCTION_NAME_PATTERN,
|
|
90
|
-
LOGGING_PRINTF_TOKEN_PATTERN,
|
|
91
88
|
LOGGING_RECEIVER_NAME_PATTERN,
|
|
92
|
-
MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT,
|
|
93
89
|
NOT_INSIDE_TYPE_CHECKING_BLOCK,
|
|
94
90
|
RESUME_TASK_ENUMERATION_PATTERN,
|
|
95
91
|
RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
|
|
@@ -280,121 +276,6 @@ def check_logging_fstrings(content: str) -> list[str]:
|
|
|
280
276
|
return issues
|
|
281
277
|
|
|
282
278
|
|
|
283
|
-
def _format_logger_names_imported(tree: ast.Module) -> set[str]:
|
|
284
|
-
"""Return the local names bound to automation_logging log helpers.
|
|
285
|
-
|
|
286
|
-
Scans every ``from ... import ...`` statement for an import whose module
|
|
287
|
-
path contains ``automation_logging`` and collects the local binding of each
|
|
288
|
-
imported ``log_*`` helper (the alias when ``as`` is present, otherwise the
|
|
289
|
-
imported name). Only these names identify a str.format-logger call; a
|
|
290
|
-
``log_*`` helper from any other module is not collected, because a
|
|
291
|
-
``%``-style logger formats its tokens correctly.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
tree: The parsed module to scan for logger imports.
|
|
295
|
-
|
|
296
|
-
Returns:
|
|
297
|
-
The set of local names bound to automation_logging log helpers.
|
|
298
|
-
"""
|
|
299
|
-
bound_names: set[str] = set()
|
|
300
|
-
for each_node in ast.walk(tree):
|
|
301
|
-
if not isinstance(each_node, ast.ImportFrom):
|
|
302
|
-
continue
|
|
303
|
-
if each_node.module is None or "automation_logging" not in each_node.module:
|
|
304
|
-
continue
|
|
305
|
-
for each_alias in each_node.names:
|
|
306
|
-
if each_alias.name in ALL_FORMAT_LOGGER_FUNCTION_NAMES:
|
|
307
|
-
bound_names.add(each_alias.asname or each_alias.name)
|
|
308
|
-
return bound_names
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def _printf_token_log_call_line(
|
|
312
|
-
node: ast.AST, all_format_logger_names: set[str]
|
|
313
|
-
) -> int | None:
|
|
314
|
-
"""Return the line of a format-logger call carrying a printf token, else None.
|
|
315
|
-
|
|
316
|
-
Args:
|
|
317
|
-
node: The AST node to inspect.
|
|
318
|
-
all_format_logger_names: Local names bound to automation_logging log
|
|
319
|
-
helpers.
|
|
320
|
-
|
|
321
|
-
Returns:
|
|
322
|
-
The 1-based line number when ``node`` is a bare-name call to a format
|
|
323
|
-
logger that has at least one format argument after the message and
|
|
324
|
-
whose first string-literal argument carries a printf token; otherwise
|
|
325
|
-
None. A call with only the message and no format arguments never runs
|
|
326
|
-
``.format(*args)``, so its token prints intact and is not flagged.
|
|
327
|
-
"""
|
|
328
|
-
if not isinstance(node, ast.Call):
|
|
329
|
-
return None
|
|
330
|
-
function_reference = node.func
|
|
331
|
-
if (
|
|
332
|
-
not isinstance(function_reference, ast.Name)
|
|
333
|
-
or function_reference.id not in all_format_logger_names
|
|
334
|
-
):
|
|
335
|
-
return None
|
|
336
|
-
if len(node.args) < MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT:
|
|
337
|
-
return None
|
|
338
|
-
message_argument = node.args[0]
|
|
339
|
-
if not isinstance(message_argument, ast.Constant) or not isinstance(
|
|
340
|
-
message_argument.value, str
|
|
341
|
-
):
|
|
342
|
-
return None
|
|
343
|
-
if not LOGGING_PRINTF_TOKEN_PATTERN.search(message_argument.value):
|
|
344
|
-
return None
|
|
345
|
-
return node.lineno
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def check_logging_printf_tokens(content: str, file_path: str) -> list[str]:
|
|
349
|
-
"""Flag printf tokens in a str.format-logger (automation_logging) message.
|
|
350
|
-
|
|
351
|
-
The ``shared_utils.automation_logging`` helpers (``log_info``, ``log_error``,
|
|
352
|
-
...) format with ``str.format`` (``{}`` placeholders) only when format
|
|
353
|
-
arguments follow the message (``message.format(*args) if args else
|
|
354
|
-
message``), so a printf-style token such as ``%s`` in the message literal is
|
|
355
|
-
never substituted: the format arguments are dropped and the literal token
|
|
356
|
-
prints. The check fires only in a file that imports one of those helpers
|
|
357
|
-
from an ``automation_logging`` module, and only for a bare-name call to such
|
|
358
|
-
a helper that passes at least one format argument after the message and
|
|
359
|
-
whose first argument is a string literal carrying a token. A call with only
|
|
360
|
-
the message and no format arguments never runs ``.format``, so its token
|
|
361
|
-
prints intact and is left alone. An attribute call (``logger.info``) or a
|
|
362
|
-
``log_*`` helper from any other module formats ``%``-tokens correctly and is
|
|
363
|
-
left alone. Test files are exempt so a test may exercise the malformed shape.
|
|
364
|
-
|
|
365
|
-
Args:
|
|
366
|
-
content: The Python source under validation.
|
|
367
|
-
file_path: The destination path, used to skip test files and non-Python.
|
|
368
|
-
|
|
369
|
-
Returns:
|
|
370
|
-
One issue line per offending call, capped at the configured maximum.
|
|
371
|
-
"""
|
|
372
|
-
if is_test_file(file_path):
|
|
373
|
-
return []
|
|
374
|
-
if get_file_extension(file_path) not in ALL_PYTHON_EXTENSIONS:
|
|
375
|
-
return []
|
|
376
|
-
try:
|
|
377
|
-
tree = ast.parse(content)
|
|
378
|
-
except SyntaxError:
|
|
379
|
-
return []
|
|
380
|
-
all_format_logger_names = _format_logger_names_imported(tree)
|
|
381
|
-
if not all_format_logger_names:
|
|
382
|
-
return []
|
|
383
|
-
issues: list[str] = []
|
|
384
|
-
for each_node in ast.walk(tree):
|
|
385
|
-
offending_line = _printf_token_log_call_line(each_node, all_format_logger_names)
|
|
386
|
-
if offending_line is None:
|
|
387
|
-
continue
|
|
388
|
-
issues.append(
|
|
389
|
-
f"Line {offending_line}: printf token in a str.format logger "
|
|
390
|
-
"message - the automation_logging helpers format with str.format; "
|
|
391
|
-
"use {} placeholders (the %-arguments are silently dropped)"
|
|
392
|
-
)
|
|
393
|
-
if len(issues) >= MAX_LOGGING_PRINTF_TOKEN_ISSUES:
|
|
394
|
-
break
|
|
395
|
-
return issues
|
|
396
|
-
|
|
397
|
-
|
|
398
279
|
def _logging_call_receiver_name(receiver_node: ast.expr) -> str | None:
|
|
399
280
|
"""Return the trailing identifier of a logging-call receiver expression, else None.
|
|
400
281
|
|
|
@@ -134,18 +134,15 @@ def test_check_comment_changes_allows_comment_removal_without_advisory(
|
|
|
134
134
|
assert captured.err == ""
|
|
135
135
|
|
|
136
136
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
new_content = "x = 2 # STEALTH: Keep -- reason\n"
|
|
147
|
-
issues = code_rules_enforcer.check_comment_changes(old_content, new_content, "foo.py")
|
|
148
|
-
assert issues == []
|
|
137
|
+
def test_check_comment_changes_flags_stealth_keep_in_a_repository_that_names_no_marker(
|
|
138
|
+
tmp_path: Path,
|
|
139
|
+
) -> None:
|
|
140
|
+
file_path = _repository_file(tmp_path, None)
|
|
141
|
+
issues = code_rules_enforcer.check_comment_changes(
|
|
142
|
+
"x = 1\n", "x = 1 # STEALTH: Keep -- reason\n", file_path
|
|
143
|
+
)
|
|
144
|
+
assert len(issues) == 1
|
|
145
|
+
assert "comment added" in issues[0]
|
|
149
146
|
|
|
150
147
|
|
|
151
148
|
@pytest.mark.parametrize(
|
|
@@ -156,16 +156,6 @@ def test_should_exempt_fixme_with_trailing_prose() -> None:
|
|
|
156
156
|
assert _is_exempt_python_comment(token) is True
|
|
157
157
|
|
|
158
158
|
|
|
159
|
-
def test_should_exempt_stealth_keep_with_trailing_reason() -> None:
|
|
160
|
-
token = _build_comment_token("# STEALTH: Keep bypass reviewed in incident 4021")
|
|
161
|
-
assert _is_exempt_python_comment(token) is True
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def test_should_flag_stealth_marker_missing_required_space_after_colon() -> None:
|
|
165
|
-
token = _build_comment_token("# STEALTH:Keep bypass reviewed in incident 4021")
|
|
166
|
-
assert _is_exempt_python_comment(token) is False
|
|
167
|
-
|
|
168
|
-
|
|
169
159
|
def test_should_exempt_shebang_at_line_one_column_zero() -> None:
|
|
170
160
|
token = tokenize.TokenInfo(
|
|
171
161
|
type=tokenize.COMMENT,
|
|
@@ -49,7 +49,6 @@ ALL_SCOPE_AWARE_RULE_NAMES = frozenset(
|
|
|
49
49
|
"check_docstring_runon_sentence",
|
|
50
50
|
"check_docstring_prose_wall_without_illustration",
|
|
51
51
|
"check_boolean_naming",
|
|
52
|
-
"check_ignored_must_check_return",
|
|
53
52
|
"check_tests_use_isolated_filesystem_paths",
|
|
54
53
|
"check_return_annotations",
|
|
55
54
|
"check_function_length",
|
|
@@ -70,7 +69,6 @@ NARROW_EDIT_ACCEPTED_RULE_NAMES = frozenset(
|
|
|
70
69
|
"check_join_separator_string_magic",
|
|
71
70
|
"check_banned_noun_word_boundary",
|
|
72
71
|
"check_boolean_naming",
|
|
73
|
-
"check_ignored_must_check_return",
|
|
74
72
|
}
|
|
75
73
|
)
|
|
76
74
|
|
|
@@ -80,7 +78,6 @@ EXPECTED_RULE_TEXT_BY_NAME = {
|
|
|
80
78
|
"check_join_separator_string_magic": "string separator",
|
|
81
79
|
"check_banned_noun_word_boundary": "Identifier",
|
|
82
80
|
"check_boolean_naming": "Boolean",
|
|
83
|
-
"check_ignored_must_check_return": "return value",
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
|
|
@@ -139,19 +136,6 @@ ALL_REQUIRED_RULE_FIXTURES = (
|
|
|
139
136
|
expected_marker="ready",
|
|
140
137
|
rule_name="check_boolean_naming",
|
|
141
138
|
),
|
|
142
|
-
NarrowEditFixture(
|
|
143
|
-
old_fragment=(
|
|
144
|
-
"def submit_form() -> None:\n"
|
|
145
|
-
" if find_and_click('#submit'):\n"
|
|
146
|
-
" return\n"
|
|
147
|
-
),
|
|
148
|
-
new_fragment=(
|
|
149
|
-
"def submit_form() -> None:\n"
|
|
150
|
-
" find_and_click('#submit')\n"
|
|
151
|
-
),
|
|
152
|
-
expected_marker="find_and_click",
|
|
153
|
-
rule_name="check_ignored_must_check_return",
|
|
154
|
-
),
|
|
155
139
|
)
|
|
156
140
|
|
|
157
141
|
|
package/hooks/hooks.json
CHANGED
|
@@ -72,6 +72,16 @@
|
|
|
72
72
|
}
|
|
73
73
|
]
|
|
74
74
|
},
|
|
75
|
+
{
|
|
76
|
+
"matcher": "mcp__hearthbot__start_thread_session",
|
|
77
|
+
"hooks": [
|
|
78
|
+
{
|
|
79
|
+
"type": "command",
|
|
80
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/routing/thread_spawn_pace_hook.py",
|
|
81
|
+
"timeout": 30
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
75
85
|
{
|
|
76
86
|
"matcher": "*",
|
|
77
87
|
"hooks": [
|
|
@@ -22,12 +22,10 @@ MAX_DOCSTRING_FORMAT_ISSUES: int = 5
|
|
|
22
22
|
MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES: int = 5
|
|
23
23
|
MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES: int = 5
|
|
24
24
|
MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH: int = 2
|
|
25
|
-
MAX_IGNORED_MUST_CHECK_RETURN_ISSUES: int = 5
|
|
26
25
|
MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
|
|
27
26
|
MAX_THIN_WRAPPER_ISSUES: int = 1
|
|
28
27
|
MAX_ZERO_PAYLOAD_ALIAS_ISSUES: int = 3
|
|
29
28
|
MAX_LOGGING_FSTRING_ISSUES: int = 3
|
|
30
|
-
MAX_LOGGING_PRINTF_TOKEN_ISSUES: int = 3
|
|
31
29
|
MAX_LOGGING_ADJACENT_LITERAL_ISSUES: int = 3
|
|
32
30
|
MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES: int = 3
|
|
33
31
|
MAX_WINDOWS_API_NONE_ISSUES: int = 3
|
|
@@ -204,17 +202,6 @@ DOCSTRING_REFERENCE_MARKER_WINDOW: int = 2
|
|
|
204
202
|
ALL_GENERIC_CHECK_NAME_TOKENS: frozenset[str] = frozenset(
|
|
205
203
|
{"check", "checks", "test", "tests", "in", "for", "and", "the"}
|
|
206
204
|
)
|
|
207
|
-
ALL_FORMAT_LOGGER_FUNCTION_NAMES: frozenset[str] = frozenset(
|
|
208
|
-
{
|
|
209
|
-
"log_debug",
|
|
210
|
-
"log_info",
|
|
211
|
-
"log_ok",
|
|
212
|
-
"log_error",
|
|
213
|
-
"log_warning",
|
|
214
|
-
"log_batch",
|
|
215
|
-
"log_background",
|
|
216
|
-
}
|
|
217
|
-
)
|
|
218
205
|
DOCSTRING_RUNON_SENTENCE_WORD_LIMIT: int = 30
|
|
219
206
|
MAX_DOCSTRING_RUNON_SENTENCE_ISSUES: int = 5
|
|
220
207
|
ALL_DOCSTRING_RUNON_JOINER_MARKERS: tuple[str, ...] = ("—", " -- ", ";")
|
|
@@ -72,8 +72,6 @@ ALL_POLARITY_ANTONYM_TOKEN_PAIRS: tuple[tuple[str, str], ...] = (
|
|
|
72
72
|
)
|
|
73
73
|
POLARITY_TOKEN_BOUNDARY_PATTERN: str = r"(?:^|_)%s(?:_|$)"
|
|
74
74
|
|
|
75
|
-
ALL_MUST_CHECK_RETURN_FUNCTION_NAMES: frozenset[str] = frozenset({"find_and_click", "write_outcome"})
|
|
76
|
-
|
|
77
75
|
DOCSTRING_ARG_ENTRY_PATTERN: re.Pattern[str] = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*[:(]")
|
|
78
76
|
DOCSTRING_PLURAL_FAMILY_STOP_PATTERN: re.Pattern[str] = re.compile(
|
|
79
77
|
r"\bthe\s+([a-z][a-z]+)\s+stops\b"
|
|
@@ -135,7 +133,6 @@ ALL_TOKEN_ANCHORED_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
|
|
|
135
133
|
"pragma:",
|
|
136
134
|
)
|
|
137
135
|
ALL_TOKEN_ANCHORED_DIRECTIVE_BOUNDARY_CHARACTERS: frozenset[str] = frozenset({":"})
|
|
138
|
-
STEALTH_KEEP_COMMENT_MARKER: str = "STEALTH: Keep"
|
|
139
136
|
REPOSITORY_LINT_SETTINGS_RELATIVE_PATH: str = ".claude/policy-lint.json"
|
|
140
137
|
COMMENT_KEEP_MARKERS_KEY: str = "comment_keep_markers"
|
|
141
138
|
REPOSITORY_ROOT_MARKER_NAME: str = ".git"
|
|
@@ -146,7 +143,6 @@ ALL_FREE_FORM_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
|
|
|
146
143
|
"FIXME",
|
|
147
144
|
"HACK",
|
|
148
145
|
"XXX",
|
|
149
|
-
STEALTH_KEEP_COMMENT_MARKER,
|
|
150
146
|
)
|
|
151
147
|
CHAINED_INLINE_COMMENT_PATTERN = re.compile(r"#")
|
|
152
148
|
ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES: tuple[str, ...] = (
|
|
@@ -211,14 +207,10 @@ LOGGING_FSTRING_PATTERN = re.compile(
|
|
|
211
207
|
r'|(?:logger|logging|log)\.(?:debug|info|warning|error|critical|exception))'
|
|
212
208
|
r'\s*\(\s*(?:[rR][fF]|[fF][rR]?)["\']'
|
|
213
209
|
)
|
|
214
|
-
LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
215
|
-
r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
|
|
216
|
-
)
|
|
217
210
|
ADJACENT_STRING_LITERAL_PATTERN: re.Pattern[str] = re.compile(
|
|
218
211
|
r'(?<!["\'])[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')'
|
|
219
212
|
r'\s*[rRbBfFuU]{0,2}(?:"(?:[^"\\\n]|\\.)*"|\'(?:[^\'\\\n]|\\.)*\')(?!["\'])'
|
|
220
213
|
)
|
|
221
|
-
MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
|
|
222
214
|
ALL_LOGGING_CALL_METHOD_NAMES: frozenset[str] = frozenset(
|
|
223
215
|
{"debug", "info", "warning", "error", "critical", "exception"}
|
|
224
216
|
)
|