claude-dev-env 8.44.2 → 8.44.4
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 +48 -57
- package/docs/code-rules/README.md +19 -0
- package/docs/code-rules/comment-preservation.md +25 -0
- package/docs/code-rules/core-principles-and-config.md +49 -0
- package/docs/code-rules/design-and-structure.md +41 -0
- package/docs/code-rules/enforcement-surfaces.md +29 -0
- package/docs/code-rules/lint-enforced-rules.md +61 -0
- package/docs/code-rules/naming-and-types.md +59 -0
- package/docs/code-rules/tdd-and-proof.md +47 -0
- package/hooks/blocking/test_step_note_gate.py +25 -9
- package/package.json +1 -1
- package/scripts/tests/test_code_rules_index.py +145 -0
package/docs/CODE_RULES.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# Code Rules Reference
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
⚡ marks rules the hand-maintained `code_rules_enforcer.py` carries. The staged policy lint runs it over each changed file and returns the corrective detail, so this document lists those rules by name only. Session policy (question routing, task tracking) lives in `rules/*.md`; see [`code-standards.md`](../rules/code-standards.md).
|
|
6
|
-
|
|
7
|
-
---
|
|
3
|
+
Use this index for edits. [`.cursor/BUGBOT.md`](../../../.cursor/BUGBOT.md) points here. Open a linked guide when its trigger applies.
|
|
8
4
|
|
|
9
5
|
## COMMENT PRESERVATION
|
|
10
6
|
|
|
@@ -13,126 +9,121 @@ Do not add code comments. Preserve existing comments. Docstrings remain allowed.
|
|
|
13
9
|
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
10
|
Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
|
|
15
11
|
|
|
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
|
-
|
|
18
|
-
---
|
|
12
|
+
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`. Open [details](code-rules/comment-preservation.md) when editing comments.
|
|
19
13
|
|
|
20
14
|
## CORE PRINCIPLES
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
- **Centralized configuration** — every constant lives in ONE place (`config/`).
|
|
24
|
-
- **Reuse before create** — search first, import second, create last.
|
|
25
|
-
- **Encapsulation enables cleaner naming** — `isMaxLevel(level)` > `level >= MAXIMUM_LEVEL`.
|
|
26
|
-
- **Construction logic lives in the model** — path/URL building, formatting, and transformations belong on the model or service that owns the data; a string pattern built at two or more call sites moves to a method there.
|
|
16
|
+
Use clear names. Keep shared constants in `config/`. Search before adding helpers. Put construction and formatting with the data owner. Encapsulation enables cleaner naming, so prefer `isMaxLevel(level)` to `level >= MAXIMUM_LEVEL`. Session policy lives in [`code-standards.md`](../rules/code-standards.md).
|
|
27
17
|
|
|
28
|
-
|
|
18
|
+
Open [details](code-rules/core-principles-and-config.md) when adding shared values or construction logic.
|
|
29
19
|
|
|
30
20
|
## ⚡ LINT-ENFORCED RULES
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
22
|
+
Staged lint runs `code_rules_enforcer.py`. Keep imports at the top, parameterize logging, guard path insertion, use specific exceptions, and follow checks for values, types, docstrings, names, and tests. Magic values exempt 0, 1, and -1. Type escape hatches are allowed in boundary files. Stub bodies are allowed in abstract and Protocol classes.
|
|
35
23
|
|
|
36
|
-
|
|
24
|
+
- UPPER_SNAKE constants belong in `config/`. Exemptions include `config/*`, `/migrations/`, and test paths or names matching `test_`, `_test.`, `.spec.`, `conftest`, or `/tests/`.
|
|
25
|
+
- Workflow registries: a path that contains any of these substrings, `/workflow/`, `_tab.py`, `/states.py`, or `/modules.py`, is exempt; each matches independently as a substring.
|
|
37
26
|
|
|
38
|
-
|
|
27
|
+
Open [details](code-rules/lint-enforced-rules.md) when lint fires.
|
|
39
28
|
|
|
40
29
|
## 3. REUSE CONSTANTS / 4. CONFIG LOCATIONS
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
Search `config/` for an exact or semantic match. Put timing in `config/timing.py`, ports and thresholds in `config/constants.py`, and selectors in `config/selectors.py`.
|
|
43
32
|
|
|
44
|
-
|
|
33
|
+
Open [details](code-rules/core-principles-and-config.md) when placing a new value.
|
|
45
34
|
|
|
46
35
|
## 5. NO ABBREVIATIONS
|
|
47
36
|
|
|
48
|
-
|
|
37
|
+
Use `context`; `ctx` is banned. Allow `i`/`j`/`k` in loops and `e` for exceptions. Patterns: `each_*` loops; `is_`/`has_`/`should_`/`can_`/`was_`/`did_` booleans; `all_*` collections; `X_by_Y` maps; `from_path=`, `to=`, `into=` parameters. Banned names: `result`, `data`, `output`, `response`, `value`, `item`, `temp`. Banned prefixes: `handle`, `process`, `manage`, `do`. Name components for their roles. Use `ItemBlocked` or `RunFatal` for failure scope; these suffixes bypass the banned-noun check.
|
|
49
38
|
|
|
50
39
|
### Public compatibility definitions
|
|
51
40
|
|
|
52
|
-
The banned-noun check applies to public function definitions, parameters, and body bindings. Use clear names
|
|
41
|
+
The banned-noun check applies to public function definitions, parameters, and body bindings. Use clear names in each.
|
|
53
42
|
|
|
54
|
-
|
|
43
|
+
Open [details](code-rules/naming-and-types.md) when naming code.
|
|
55
44
|
|
|
56
45
|
## 6. COMPLETE TYPE HINTS
|
|
57
46
|
|
|
58
|
-
|
|
47
|
+
Type every parameter and return. Avoid `Any` and `# type: ignore`; remove ignores and use a typed boundary or precise annotation. Annotate known pytest fixtures and remove unused fixture parameters.
|
|
48
|
+
|
|
49
|
+
Open [details](code-rules/naming-and-types.md) when a type or fixture check fires.
|
|
59
50
|
|
|
60
51
|
## 6.5 FILE LENGTH GUIDANCE
|
|
61
52
|
|
|
62
|
-
|
|
53
|
+
File length is advisory: emit a stderr advisory at 400 lines and a stronger stderr advisory at 1000 lines. Split on cohesion after a readability check.
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
Open [details](code-rules/design-and-structure.md) when a length advisory fires.
|
|
65
56
|
|
|
66
57
|
## 7. RIGHT-SIZED ENGINEERING
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
Use functions without state and concrete classes with state. Avoid ABCs, factories, and DI frameworks for one implementation. Add abstractions for multiple implementations. Add optional parameters when callers vary them; require or inline fixed values. Remove unused parameters.
|
|
60
|
+
|
|
61
|
+
Open [details](code-rules/design-and-structure.md) when adding a class or parameter.
|
|
71
62
|
|
|
72
63
|
## 7.5 SOLID PRINCIPLES
|
|
73
64
|
|
|
74
|
-
|
|
65
|
+
Apply SRP throughout. Apply OCP, LSP, ISP, and DIP when two concrete implementations share a contract. Keep cohesive classes together.
|
|
75
66
|
|
|
76
|
-
|
|
67
|
+
Open [details](code-rules/design-and-structure.md) when extracting a responsibility.
|
|
77
68
|
|
|
78
69
|
## 8. TDD PROCESS
|
|
79
70
|
|
|
80
|
-
|
|
71
|
+
For a bug fix or new behavior, run RED with a failing test, GREEN with the smallest change, then REFACTOR when useful. A prototype adds tests before its pull request goes ready. A bug fix ships with a reproducing test.
|
|
81
72
|
|
|
82
|
-
|
|
73
|
+
**Proof of check.** Each pull request body names a repeatable test, run, screenshot, or measurement. A reviewer flags a body with no proof.
|
|
83
74
|
|
|
84
|
-
|
|
75
|
+
Open [details](code-rules/tdd-and-proof.md) when adding behavior or proof.
|
|
85
76
|
|
|
86
77
|
## 9. SELF-CONTAINED COMPONENTS
|
|
87
78
|
|
|
88
|
-
Components own their
|
|
79
|
+
Components own their state, modals, overlays, and toasts. Parents render `<Child />`.
|
|
80
|
+
|
|
81
|
+
Open [details](code-rules/design-and-structure.md) when splitting components.
|
|
89
82
|
|
|
90
83
|
## 9.5 NO THIN WRAPPER MODULES
|
|
91
84
|
|
|
92
|
-
|
|
85
|
+
Callers import the owning module directly. Keep re-exports in `__init__.py`; an imports-only non-`__init__.py` module is a thin wrapper.
|
|
86
|
+
|
|
87
|
+
Open [details](code-rules/design-and-structure.md) when moving modules.
|
|
93
88
|
|
|
94
89
|
## 9.6 NO BACKWARDS-COMPATIBILITY SHIMS
|
|
95
90
|
|
|
96
|
-
|
|
91
|
+
Remove renamed re-export aliases, old aliases, keep-alive wrappers, and tombstone markers. Update call sites in the same change when a symbol changes.
|
|
92
|
+
|
|
93
|
+
Open [details](code-rules/design-and-structure.md) when renaming symbols.
|
|
97
94
|
|
|
98
95
|
## 9.7 NO FALLBACK / BEST-EFFORT WRAPPERS
|
|
99
96
|
|
|
100
|
-
Never swallow a failure into a default unless the caller explicitly opted in at the boundary. Name the specific exception (`except KeyError:`) and propagate the rest
|
|
97
|
+
Never swallow a failure into a default unless the caller explicitly opted in at the boundary. Name the specific exception (`except KeyError:`) and propagate the rest. Collapsing every error class to `None` masks programming errors and makes debugging impossible.
|
|
101
98
|
|
|
102
|
-
|
|
99
|
+
Open [details](code-rules/design-and-structure.md) when handling batch failures.
|
|
103
100
|
|
|
104
101
|
## 9.8 REMOVE CODE YOU ORPHAN (Dead Code Elimination)
|
|
105
102
|
|
|
106
|
-
|
|
103
|
+
Remove orphaned variables, functions, parameters, branches, imports, and helpers. Check references and dynamic lookups first. Remove dead self-referential clusters. **When liveness is uncertain (public API, plugin hook, reflective dispatch), surface the ambiguity and never delete.**
|
|
104
|
+
|
|
105
|
+
Open [details](code-rules/design-and-structure.md) when removing consumers.
|
|
107
106
|
|
|
108
107
|
## 10. NO REDUNDANT DATA FETCHES
|
|
109
108
|
|
|
110
|
-
|
|
109
|
+
Use data already in hand; do not fetch it again.
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
Open [details](code-rules/design-and-structure.md) when reviewing fetches.
|
|
113
112
|
|
|
114
113
|
## 11. ENFORCEMENT SURFACES
|
|
115
114
|
|
|
116
|
-
|
|
115
|
+
Lint checks patterns; prompts carry judgment; rubrics cover cross-file concerns.
|
|
117
116
|
|
|
118
|
-
|
|
117
|
+
Open [details](code-rules/enforcement-surfaces.md) when routing a rule.
|
|
119
118
|
|
|
120
|
-
|
|
119
|
+
## 11.5 VALIDATION-PHASE PRECEDENCE
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
2. **Target classification filters within a lane.** The hook-infrastructure patterns and the ephemeral-path check decide whether a target is validated, and with which subset. Classification narrows a lane; it never adds a check the phase already excluded.
|
|
124
|
-
3. **Changed-line scope filters only the report.** `defer_scope_to_caller` and the changed-line set decide which found violations block. Scope filters findings after every check in the roster already ran; it adds or removes no check.
|
|
121
|
+
Phase selects checks; target classification narrows them; changed-line scope filters findings. Each axis narrows the previous one.
|
|
125
122
|
|
|
126
|
-
|
|
123
|
+
Open [details](code-rules/enforcement-surfaces.md) when routing a check.
|
|
127
124
|
|
|
128
125
|
## 11.6 LANE ASSIGNMENT IS BY SCOPE
|
|
129
126
|
|
|
130
127
|
Scope assigns the lane. A check that reads a file other than the target runs on the full gate. Every other check runs on both lanes.
|
|
131
128
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Three surfaces report on the roster, and each reports a specific thing:
|
|
135
|
-
|
|
136
|
-
- `hooks/validators/hook_timing_harness.py` builds a `Write` payload against a target that already holds content. `_contents_for_validation` returns `None` for that payload, so the harness times interpreter start and hook dispatch. Time an `Edit` payload against a file to measure the checks.
|
|
137
|
-
- `~/.claude/logs/hook-blocks.log` records the denials raised by fixtures in `test_code_rules_enforcer_*.py` and by the timing harness's default target.
|
|
138
|
-
- `hooks/validators/run_all_validators.py` stages the target under a temporary root and rebuilds the shortest path tail that carries every exemption signal. The walk starts at the target's own project root and skips a directory pytest generated for its own scratch tree, so the staged path reads the same wherever `--basetemp` places that tree.
|
|
129
|
+
Open [details](code-rules/enforcement-surfaces.md) when adding a check.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Code rules feature map
|
|
2
|
+
|
|
3
|
+
Open [the index](../CODE_RULES.md) for the rule core, then follow the family that matches the change.
|
|
4
|
+
|
|
5
|
+
- [Comment preservation](comment-preservation.md): comments, directives, and keep markers.
|
|
6
|
+
- [Core principles and config](core-principles-and-config.md): constants, paths, and repeated construction.
|
|
7
|
+
- [Lint-enforced rules](lint-enforced-rules.md): staged patterns, docstrings, imports, and logging.
|
|
8
|
+
- [Naming and types](naming-and-types.md): identifiers, annotations, and pytest fixtures.
|
|
9
|
+
- [Design and structure](design-and-structure.md): components, failure scope, and orphaned code.
|
|
10
|
+
- [TDD and proof](tdd-and-proof.md): tests, assertions, and review evidence.
|
|
11
|
+
- [Enforcement surfaces](enforcement-surfaces.md): check lanes, hook targets, and reporting.
|
|
12
|
+
|
|
13
|
+
## Feature entry contract
|
|
14
|
+
|
|
15
|
+
Each family has one agent-facing paragraph and four sections in order: Checks, When it fires, Proving it, and Gotchas. Checks lists each imported check id once across the map. Proving it names a breaking input, a pytest node or command, and an observable result for each id.
|
|
16
|
+
|
|
17
|
+
## Conventions
|
|
18
|
+
|
|
19
|
+
Keep rule headings and their short instructions in the index. Put patterns, exemptions, and procedures in the family file. Link each family back to its index section. Update the check map and its proof when the enforcer imports change.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Comment preservation
|
|
2
|
+
|
|
3
|
+
[Back to the index](../CODE_RULES.md#comment-preservation)
|
|
4
|
+
|
|
5
|
+
I keep comments attached to untouched code. When I change the code a comment describes, I remove that comment and put its meaning in names and structure.
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- check_comment_changes
|
|
10
|
+
|
|
11
|
+
## When it fires
|
|
12
|
+
|
|
13
|
+
The code-rules enforcer runs check_comment_changes on changed *.py, *.js, *.mjs, and *.ts lines during staged validation.
|
|
14
|
+
|
|
15
|
+
The rule applies to production and tests. Existing comments outside the changed code stay in place. Keep cleanup within the requested task.
|
|
16
|
+
|
|
17
|
+
## Proving it
|
|
18
|
+
|
|
19
|
+
Run the named pytest node or command with the described input. A breach produces a rule finding; advisory checks write to stderr.
|
|
20
|
+
|
|
21
|
+
- check_comment_changes: a new JavaScript comment on a changed line; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_javascript_comments.py::test_check_comment_changes_reports_javascript_comment_line` reports a named violation.
|
|
22
|
+
|
|
23
|
+
## Gotchas
|
|
24
|
+
|
|
25
|
+
A keep marker needs a configured prefix. A changed directive or task marker is removed. Docstrings remain allowed.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Core principles and config
|
|
2
|
+
|
|
3
|
+
[Back to the index](../CODE_RULES.md#core-principles)
|
|
4
|
+
|
|
5
|
+
I search for shared code and configuration before adding a constant or repeating construction logic.
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- check_config_duplicate_path_anchor
|
|
10
|
+
- check_constants_outside_config
|
|
11
|
+
- check_constants_outside_config_advisory
|
|
12
|
+
- check_fstring_structural_literals
|
|
13
|
+
- check_magic_values
|
|
14
|
+
- check_duplicated_format_patterns
|
|
15
|
+
- check_hardcoded_user_paths
|
|
16
|
+
- check_sys_path_insert_deduplication_guard
|
|
17
|
+
- check_inline_literal_collections
|
|
18
|
+
- check_inline_tuple_string_magic
|
|
19
|
+
- check_join_separator_string_magic
|
|
20
|
+
- check_string_literal_magic
|
|
21
|
+
- check_whitespace_indentation_magic
|
|
22
|
+
|
|
23
|
+
## When it fires
|
|
24
|
+
|
|
25
|
+
The code-rules enforcer runs constant, path, magic-value, and duplicate-format checks on changed *.py files. String and collection checks inspect production function bodies in *.py files.
|
|
26
|
+
|
|
27
|
+
Search config for the exact value and a semantic match. Add timing to config/timing.py, ports and thresholds to config/constants.py, and selectors to config/selectors.py. Keep construction and formatting with the owner of the data. Constants outside config are exempt in migrations, workflow registries, and test files. Workflow registry matching uses path substrings /workflow/, _tab.py, /states.py, and /modules.py. The test-file patterns include test_, _test., .spec., conftest, and /tests/.
|
|
28
|
+
|
|
29
|
+
## Proving it
|
|
30
|
+
|
|
31
|
+
Run the named pytest node or command with the described input. A breach produces a rule finding; advisory checks write to stderr.
|
|
32
|
+
|
|
33
|
+
- check_config_duplicate_path_anchor: two config constants pointing at the same path; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
34
|
+
- check_constants_outside_config: UPPER_SNAKE = 3 in a production module; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_config_path.py::test_should_produce_blocking_for_module_level_upper_snake_outside_config` reports a named violation.
|
|
35
|
+
- check_constants_outside_config_advisory: a function-local UPPER_SNAKE constant; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a stderr advisory.
|
|
36
|
+
- check_fstring_structural_literals: an f-string that embeds a URL path fragment; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_fstring_scan.py::test_should_flag_fstring_with_url_path` reports a named violation.
|
|
37
|
+
- check_magic_values: a production function comparing a value with 2; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py::test_check_magic_values_should_flag_literal_two_in_function_body` reports a named violation.
|
|
38
|
+
- check_duplicated_format_patterns: the same f-string skeleton at three call sites; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_split_optional_params.py::test_should_advise_when_fstring_skeleton_appears_three_or_more_times` reports a stderr advisory.
|
|
39
|
+
- check_hardcoded_user_paths: a fixed user-home path in source; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py::test_should_flag_windows_user_path_with_forward_slashes` reports a named violation.
|
|
40
|
+
- check_sys_path_insert_deduplication_guard: an unguarded module-level sys.path.insert call; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py::test_should_flag_unguarded_module_level_insert` reports a named violation.
|
|
41
|
+
- check_inline_literal_collections: a three-string set inside a function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_split_string_magic.py::test_check_inline_literal_collections_flags_three_string_set_in_function` reports a named violation.
|
|
42
|
+
- check_inline_tuple_string_magic: an inline tuple of two snake-case labels; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py::test_should_flag_inline_snake_case_tuple_pair_inside_function` reports a named violation.
|
|
43
|
+
- check_join_separator_string_magic: a literal delimiter passed to join in a function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py::test_should_flag_literal_delimiter_join_separator_in_function_body` reports a named violation.
|
|
44
|
+
- check_string_literal_magic: an environment-variable name literal in a function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_string_magic.py::test_should_flag_env_var_name_string_in_function_body` reports a named violation.
|
|
45
|
+
- check_whitespace_indentation_magic: a repeated literal indentation string in a function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_whitespace_indentation_magic.py` reports a named violation.
|
|
46
|
+
|
|
47
|
+
## Gotchas
|
|
48
|
+
|
|
49
|
+
Migration paths, config files, workflow registries, and tests have distinct constant exemptions. Each workflow substring matches independently.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Design and structure
|
|
2
|
+
|
|
3
|
+
[Back to the index](../CODE_RULES.md#7-right-sized-engineering)
|
|
4
|
+
|
|
5
|
+
I keep components cohesive and remove code whose entry-point path disappeared.
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- check_function_length
|
|
10
|
+
- check_blast_radius_declared
|
|
11
|
+
- check_duplicate_function_body_across_files
|
|
12
|
+
- check_unused_optional_parameters
|
|
13
|
+
- check_orphan_css_classes
|
|
14
|
+
- check_bare_except
|
|
15
|
+
- check_test_branching_in_production
|
|
16
|
+
- check_stub_implementations
|
|
17
|
+
- check_thin_wrapper_files
|
|
18
|
+
|
|
19
|
+
## When it fires
|
|
20
|
+
|
|
21
|
+
The code-rules enforcer checks structure, exception boundaries, and length in changed *.py files. It checks class names in changed *.css and component files. File-length advisories run at 400 and 1000 lines.
|
|
22
|
+
|
|
23
|
+
A member loop catches a declared ItemBlocked type inside the loop, records the member and reason, then continues. Escalations re-raise first, so a RunFatal passes through, and `except Exception` triggers the rule. The run report names each parked member. [Failure scope](../../rules/failure-blast-radius.md) gives the boundary shape. To remove orphaned code, run Serena find_referencing_symbols and search text for getattr and entry-point strings. Trace each reference to a live CLI command, route, public API, or test. Remove a dead self-referential cluster together. Keep uncertain symbols and surface the ambiguity. Follow the [dead-code procedure](../references/dead-code-elimination.md). Keep a component's state, modals, overlays, and toasts with that component. Re-exports belong in __init__.py. Update call sites when changing a symbol.
|
|
24
|
+
|
|
25
|
+
## Proving it
|
|
26
|
+
|
|
27
|
+
Run the named pytest node or command with the described input. A breach produces a rule finding; advisory checks write to stderr.
|
|
28
|
+
|
|
29
|
+
- check_function_length: a function exceeding the length threshold; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_function_length.py` reports a stderr advisory.
|
|
30
|
+
- check_blast_radius_declared: a raise inside a loop body with no declared handler; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_blast_radius.py::test_should_report_a_loop_raise_with_pending_blast_radius_declaration` reports an advisory. A RunFatal raise or an ItemBlocked raise inside a loop passes.
|
|
31
|
+
- check_duplicate_function_body_across_files: one function body copied into a sibling module; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_duplicate_body.py::test_should_flag_function_copied_from_sibling` reports a named violation.
|
|
32
|
+
- check_unused_optional_parameters: an optional parameter with one fixed value at every call site; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_split_optional_params.py::test_should_flag_optional_param_never_varied_in_file` reports a named violation.
|
|
33
|
+
- check_orphan_css_classes: a markup class with no matching CSS selector; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py::test_should_flag_class_with_no_matching_selector` reports a named violation.
|
|
34
|
+
- check_bare_except: a bare except clause; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_bare_except.py::test_should_flag_bare_except` reports a named violation.
|
|
35
|
+
- check_test_branching_in_production: a production branch on an environment testing flag; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_test_branching.py::test_should_flag_os_environ_get_testing_branch` reports a named violation.
|
|
36
|
+
- check_stub_implementations: a public function with a pass-only body; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_stub_implementations.py::test_should_flag_pass_only_function` reports a named violation.
|
|
37
|
+
- check_thin_wrapper_files: a module containing only imports and __all__; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py::test_should_flag_thin_wrapper_with_imports_and_all` reports a named violation.
|
|
38
|
+
|
|
39
|
+
## Gotchas
|
|
40
|
+
|
|
41
|
+
A length advisory asks for a cohesion review. Reflective dispatch, public APIs, and plugin hooks can hide references. Surface uncertain liveness and keep the code.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Enforcement surfaces
|
|
2
|
+
|
|
3
|
+
[Back to the index](../CODE_RULES.md#11-enforcement-surfaces)
|
|
4
|
+
|
|
5
|
+
I choose a check's lane from its scope and distinguish the roster from the findings shown to a caller.
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- check_unanchored_command_dispatch
|
|
10
|
+
- check_same_file_inline_duplicate_body
|
|
11
|
+
- check_zero_payload_function_alias
|
|
12
|
+
|
|
13
|
+
## When it fires
|
|
14
|
+
|
|
15
|
+
The blocking code-rules hook checks command dispatch in hooks/blocking/*.py and hook-infrastructure targets in hooks/**/*.py. Staged lint runs on changed *.py, *.js, *.mjs, and *.ts files; the full gate runs the wider roster.
|
|
16
|
+
|
|
17
|
+
Lint reports patterns; prompt context carries judgment about SRP, right-sized design, research on ambiguous intent, BDD discovery, and docstring prose. Audit rubrics A to Q cover cross-file concerns. Rules with pending checks live in rules/*.md and name their promotion path. Category O6 audits whether [docstring enumerations](../../rules/docstring-prose-matches-implementation.md) match behavior. Category O9 audits [illustrative docstrings](../../rules/plain-illustrative-docstrings.md). A summary line, a :: example or doctest, and short prose support that check. Validation uses three axes: EDIT_LANE_PHASE or FULL_GATE_PHASE chooses the roster; target classification narrows that roster; defer_scope_to_caller and changed lines filter findings. validate_content_for_phase requires an explicit phase keyword. validation_phase_constants.py owns the phase names and both roster sets. A check reading another file runs on the full gate; other checks run on both lanes. Hook-infrastructure edit targets run check_same_file_inline_duplicate_body, check_zero_payload_function_alias, and check_unanchored_command_dispatch. The full gate runs its complete roster. The timing harness uses an unchanged Write payload to time dispatch; use Edit to time checks. The hook denial log records test denials. run_all_validators.py stages a target under a temporary root and preserves its shortest exemption-bearing path tail. It skips pytest's own scratch directory when finding the project root.
|
|
18
|
+
|
|
19
|
+
## Proving it
|
|
20
|
+
|
|
21
|
+
Run the named pytest node or command with the described input. A breach produces a rule finding; advisory checks write to stderr.
|
|
22
|
+
|
|
23
|
+
- check_unanchored_command_dispatch: a command regex that matches a verb inside another command; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py` reports a named violation.
|
|
24
|
+
- check_same_file_inline_duplicate_body: a helper body copied inline into a caller; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py::test_should_flag_helper_whose_body_is_inlined_in_another_function` reports a named violation.
|
|
25
|
+
- check_zero_payload_function_alias: a pass-through alias forwarding the same arguments; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py::test_should_flag_pass_through_alias_forwarding_same_parameters` reports a named violation.
|
|
26
|
+
|
|
27
|
+
## Gotchas
|
|
28
|
+
|
|
29
|
+
Phase chooses the roster before target classification. Changed-line filtering only narrows reports. A Write payload with unchanged content can time dispatch without timing checks.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Lint-enforced rules
|
|
2
|
+
|
|
3
|
+
[Back to the index](../CODE_RULES.md#-lint-enforced-rules)
|
|
4
|
+
|
|
5
|
+
I use the staged enforcer findings to locate the breached pattern and the relevant fixture.
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- check_class_docstring_names_public_methods
|
|
10
|
+
- check_docstring_args_match_signature
|
|
11
|
+
- check_docstring_documents_unreferenced_parameter
|
|
12
|
+
- check_docstring_format
|
|
13
|
+
- check_docstring_names_undefined_constant
|
|
14
|
+
- check_docstring_prose_wall_without_illustration
|
|
15
|
+
- check_docstring_runon_sentence
|
|
16
|
+
- check_module_docstring_names_public_checks
|
|
17
|
+
- check_module_docstring_scope_omits_data_schema_constants
|
|
18
|
+
- check_imports_at_top
|
|
19
|
+
- check_js_bare_flag_return_directive
|
|
20
|
+
- check_js_resume_task_enumeration_coverage
|
|
21
|
+
- check_js_returns_object_schemaless_branch
|
|
22
|
+
- check_js_sibling_return_object_key_drift
|
|
23
|
+
- check_library_print
|
|
24
|
+
- check_logging_adjacent_string_literals
|
|
25
|
+
- check_logging_fstrings
|
|
26
|
+
- check_naive_datetime_construction
|
|
27
|
+
- check_windows_api_none
|
|
28
|
+
|
|
29
|
+
## When it fires
|
|
30
|
+
|
|
31
|
+
The code-rules enforcer runs on changed *.py, *.js, *.mjs, and *.ts files. Docstring, import, logging, and datetime checks use *.py. JavaScript return checks use *.js and *.mjs.
|
|
32
|
+
|
|
33
|
+
The roster covers imports at top, logging format arguments, hardcoded home paths, guarded path insertion, bare and broad exception handlers, docstring formats and contents, JavaScript return objects, and platform calls. Public functions with parameters use Google-style Args entries that match the signature. Long prose in a docstring calls for an illustration. Command classifiers anchor a multiword expression at the command start or tokenize its first word.
|
|
34
|
+
|
|
35
|
+
## Proving it
|
|
36
|
+
|
|
37
|
+
Run the named pytest node or command with the described input. A breach produces a rule finding; advisory checks write to stderr.
|
|
38
|
+
|
|
39
|
+
- check_class_docstring_names_public_methods: a class summary omitting its public methods; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py::test_should_flag_single_line_docstring_omitting_two_public_methods` reports a named violation.
|
|
40
|
+
- check_docstring_args_match_signature: an Args entry absent from the signature; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py::test_should_flag_documented_arg_not_in_signature` reports a named violation.
|
|
41
|
+
- check_docstring_documents_unreferenced_parameter: a documented parameter unused in the body; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py` reports a named violation.
|
|
42
|
+
- check_docstring_format: a public function with parameters and no Args section; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_docstring_format.py::test_should_flag_public_function_with_params_missing_args_section` reports a named violation.
|
|
43
|
+
- check_docstring_names_undefined_constant: a docstring naming an undefined constant; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_docstring_undefined_constant.py::test_flags_docstring_naming_constant_the_module_never_defines` reports a named violation.
|
|
44
|
+
- check_docstring_prose_wall_without_illustration: a prose-wall docstring with no example; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py::test_should_flag_prose_wall_with_no_illustration` reports a named violation.
|
|
45
|
+
- check_docstring_runon_sentence: a long run-on docstring sentence; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py::test_should_flag_run_lifecycle_module_docstring_wall` reports a named violation.
|
|
46
|
+
- check_module_docstring_names_public_checks: a module summary omitting a public check; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py::test_should_flag_module_docstring_omitting_a_public_check` reports a named violation.
|
|
47
|
+
- check_module_docstring_scope_omits_data_schema_constants: a module summary listing schema constants as behavior; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py` reports a named violation.
|
|
48
|
+
- check_imports_at_top: an ordinary import inside a function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py::test_should_flag_runtime_import_inside_function_even_if_file_uses_type_checking` reports a named violation.
|
|
49
|
+
- check_js_bare_flag_return_directive: a JavaScript return directive with an unexplained bare flag; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
50
|
+
- check_js_resume_task_enumeration_coverage: a resume task branch missing an enumerated state; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
51
|
+
- check_js_returns_object_schemaless_branch: sibling JavaScript return paths with an ad hoc object; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
52
|
+
- check_js_sibling_return_object_key_drift: sibling return objects with different keys; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
53
|
+
- check_library_print: a print call in library code; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
54
|
+
- check_logging_adjacent_string_literals: adjacent string literals in a logger call; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
55
|
+
- check_logging_fstrings: an f-string passed to logger.info; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_logger_fstring.py::test_should_flag_logger_info_fstring` reports a named violation.
|
|
56
|
+
- check_naive_datetime_construction: datetime.fromtimestamp without a timezone; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_naive_datetime.py::test_flags_fromtimestamp_without_timezone` reports a named violation.
|
|
57
|
+
- check_windows_api_none: a Windows API call passed None for a required argument; `python -m pytest packages/claude-dev-env/hooks/blocking` reports a named violation.
|
|
58
|
+
|
|
59
|
+
## Gotchas
|
|
60
|
+
|
|
61
|
+
Some checks report only changed lines. Import order and unused imports belong to ruff F401 and I001. Advisory findings write to stderr.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Naming and types
|
|
2
|
+
|
|
3
|
+
[Back to the index](../CODE_RULES.md#5-no-abbreviations)
|
|
4
|
+
|
|
5
|
+
I name identifiers for their role and type the boundary of each public function.
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- check_known_pytest_fixture_annotations
|
|
10
|
+
- check_parameter_annotations
|
|
11
|
+
- check_return_annotations
|
|
12
|
+
- check_unused_known_pytest_fixture_parameters
|
|
13
|
+
- check_banned_identifiers
|
|
14
|
+
- check_banned_noun_word_boundary
|
|
15
|
+
- check_banned_prefixes
|
|
16
|
+
- check_boolean_naming
|
|
17
|
+
- check_js_banned_identifiers
|
|
18
|
+
- check_js_boolean_naming
|
|
19
|
+
- check_collection_prefix
|
|
20
|
+
- check_loop_variable_naming
|
|
21
|
+
- check_polarity_name_contradiction
|
|
22
|
+
- check_referenced_underscore_loop_variable
|
|
23
|
+
- check_stuttering_collection_prefix
|
|
24
|
+
- check_boundary_types
|
|
25
|
+
- check_type_escape_hatches
|
|
26
|
+
- check_typed_dict_encode_decode
|
|
27
|
+
|
|
28
|
+
## When it fires
|
|
29
|
+
|
|
30
|
+
The code-rules enforcer checks names in changed *.py, *.js, *.mjs, and *.ts declarations. Annotation checks skip test files. Pytest fixture annotation and unused-fixture checks run on test_*.py, *_test.py, and tests/ paths. Boolean names cover bool-typed parameters. JavaScript `@param {boolean}` names take camelCase prefixes. A test_* function needs a return annotation. A bare `object` parameter whose body reads an attribute fails the type check.
|
|
31
|
+
|
|
32
|
+
Known pytest fixture parameters are tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys, capfd, caplog, request, and tmp_path_factory with their documented injected types. A collectable test drops a known fixture parameter it never reads, augments, or deletes. Reads inside nested functions and comprehensions count. Ordinary test parameters remain exempt. For typed data, keep _encode_* and _decode_* companions in the same module. The banned identifiers include ctx, cfg, msg, btn, idx, cnt, tmp, elem, and val. Public function names avoid handle_, process_, manage_, and do_.
|
|
33
|
+
|
|
34
|
+
## Proving it
|
|
35
|
+
|
|
36
|
+
Run the named pytest node or command with the described input. A breach produces a rule finding; advisory checks write to stderr.
|
|
37
|
+
|
|
38
|
+
- check_known_pytest_fixture_annotations: an untyped tmp_path parameter in a test; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_annotations.py::test_should_flag_unannotated_known_fixture_in_test_file` reports a named violation.
|
|
39
|
+
- check_parameter_annotations: a public parameter without an annotation; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_annotations.py::test_should_flag_parameter_without_annotation` reports a named violation.
|
|
40
|
+
- check_return_annotations: a function without a return annotation; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_annotations.py::test_should_flag_function_without_return_annotation` reports a named violation.
|
|
41
|
+
- check_unused_known_pytest_fixture_parameters: a test declaring tmp_path and never reading it; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_annotations.py::test_should_flag_unused_known_fixture_parameter_in_test_file` reports a named violation.
|
|
42
|
+
- check_banned_identifiers: a changed ctx binding; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_banned_identifier.py` reports a named violation.
|
|
43
|
+
- check_banned_noun_word_boundary: a public function binding a banned noun; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py` reports a named violation.
|
|
44
|
+
- check_banned_prefixes: a function named handle_event; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py::test_should_flag_handle_prefixed_function` reports a named violation.
|
|
45
|
+
- check_boolean_naming: a boolean assignment named enabled; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_naming_pattern.py::test_should_flag_boolean_assignment_without_is_prefix` reports a named violation.
|
|
46
|
+
- check_js_banned_identifiers: a JavaScript declaration named ctx; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_naming_pattern.py` reports a named violation.
|
|
47
|
+
- check_js_boolean_naming: a boolean JavaScript declaration named enabled; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_naming_pattern.py` reports a named violation.
|
|
48
|
+
- check_collection_prefix: a list parameter named users; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_collection_prefix.py` reports a named violation.
|
|
49
|
+
- check_loop_variable_naming: a loop variable named user; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_split_naming.py::test_check_loop_variable_naming_flags_missing_each_prefix` reports a named violation.
|
|
50
|
+
- check_polarity_name_contradiction: an is_allowed binding assigned from is_forbidden; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py::test_should_flag_allowed_target_assigned_from_forbidden_callee` reports a named violation.
|
|
51
|
+
- check_referenced_underscore_loop_variable: a loop variable named _user that its body reads; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py::test_should_flag_referenced_underscore_loop_variable_in_conftest` reports a named violation.
|
|
52
|
+
- check_stuttering_collection_prefix: a function named all_all_process; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_split_naming.py::test_stuttering_collection_prefix_flags_function_name_loop1_1` reports a named violation.
|
|
53
|
+
- check_boundary_types: Any as a direct parameter annotation; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_boundary_types.py::test_should_flag_any_as_direct_param_annotation` reports a named violation.
|
|
54
|
+
- check_type_escape_hatches: a cast call in production code; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py::test_should_flag_cast_call_in_production` reports a named violation.
|
|
55
|
+
- check_typed_dict_encode_decode: a TypedDict with no encode and decode companion; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py::test_should_flag_typed_dict_without_encode_or_decode` reports a named violation.
|
|
56
|
+
|
|
57
|
+
## Gotchas
|
|
58
|
+
|
|
59
|
+
Known fixture names need an injected type even in test files. Fixture functions can request another fixture for setup order. Local nested helpers are outside the unused-fixture check.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# TDD and proof
|
|
2
|
+
|
|
3
|
+
[Back to the index](../CODE_RULES.md#8-tdd-process)
|
|
4
|
+
|
|
5
|
+
I write a failing behavior test, make it pass, then refactor when the change benefits from cleanup.
|
|
6
|
+
|
|
7
|
+
## Checks
|
|
8
|
+
|
|
9
|
+
- check_e2e_test_naming
|
|
10
|
+
- check_public_function_missing_paired_test
|
|
11
|
+
- check_test_file_omits_module_public_function
|
|
12
|
+
- check_constant_equality_tests
|
|
13
|
+
- check_existence_check_tests
|
|
14
|
+
- check_flag_gated_scenario_test_naming
|
|
15
|
+
- check_skip_decorators_in_tests
|
|
16
|
+
- check_stale_test_name_target
|
|
17
|
+
- check_vacuous_cleanup_assertion_tests
|
|
18
|
+
- check_tests_use_isolated_filesystem_paths
|
|
19
|
+
- check_dead_test_module_constant
|
|
20
|
+
- check_unused_test_helper_parameter
|
|
21
|
+
|
|
22
|
+
## When it fires
|
|
23
|
+
|
|
24
|
+
The code-rules enforcer runs test assertion, paired-test, isolation, and layout checks on test_*.py, *_test.py, and tests/ paths. Paired-test checks also read linked *.py production modules.
|
|
25
|
+
|
|
26
|
+
The TDD loop is the default for bug fixes and new behavior. A prototype may precede its tests and adds them before review readiness. Hook lint does not check the order. A bug fix includes a reproducing test. Proof can be a named test, run, screenshot, or measurement.
|
|
27
|
+
|
|
28
|
+
## Proving it
|
|
29
|
+
|
|
30
|
+
Run the named pytest node or command with the described input. A breach produces a rule finding; advisory checks write to stderr.
|
|
31
|
+
|
|
32
|
+
- check_e2e_test_naming: an end-to-end test with an unmarked name; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py` reports a named violation.
|
|
33
|
+
- check_public_function_missing_paired_test: a new public function absent from its established suite; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_paired_test.py::test_flags_public_function_absent_from_established_suite` reports a named violation.
|
|
34
|
+
- check_test_file_omits_module_public_function: an established test module omitting a public function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_paired_test.py::test_flags_module_public_function_when_test_suite_omits_it` reports a named violation.
|
|
35
|
+
- check_constant_equality_tests: a test asserting a constant equals its literal; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_constant_equality.py::test_should_flag_test_asserting_constant_equals_literal` reports a named violation.
|
|
36
|
+
- check_existence_check_tests: a test asserting only that a function exists; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_existence_checks.py::test_should_flag_test_with_only_callable_assertion` reports a named violation.
|
|
37
|
+
- check_flag_gated_scenario_test_naming: a flag-gated scenario whose name omits its flag; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py::test_should_advise_when_scenario_test_omits_flag_its_siblings_patch` reports a stderr advisory.
|
|
38
|
+
- check_skip_decorators_in_tests: a skipped pytest test function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_skip_decorators.py::test_should_flag_pytest_mark_skip_on_test_function` reports a named violation.
|
|
39
|
+
- check_stale_test_name_target: a test name still naming a removed function; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_stale_test_name.py::test_flags_renamed_away_target_in_test_name` reports a named violation.
|
|
40
|
+
- check_vacuous_cleanup_assertion_tests: a cleanup test with no created temporary file; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py::test_should_flag_glob_emptiness_cleanup_test_without_temp_creation` reports a named violation.
|
|
41
|
+
- check_tests_use_isolated_filesystem_paths: a test writing to a shared user-home path; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_tests_isolate_home_temp.py` reports a named violation.
|
|
42
|
+
- check_dead_test_module_constant: an unused private constant in a test module; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_test_layout.py` reports a named violation.
|
|
43
|
+
- check_unused_test_helper_parameter: a test helper parameter its body never reads; `python -m pytest packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_test_layout.py` reports a named violation.
|
|
44
|
+
|
|
45
|
+
## Gotchas
|
|
46
|
+
|
|
47
|
+
A cleanup test needs setup that creates something to clean. A renamed function needs updated test names. A pull request body names repeatable proof.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import json
|
|
3
|
-
import
|
|
3
|
+
from collections.abc import Callable
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
@@ -160,6 +160,24 @@ def test_should_allow_every_parallel_call_under_one_note(
|
|
|
160
160
|
assert (first_exit_code, second_exit_code) == (0, 0)
|
|
161
161
|
|
|
162
162
|
|
|
163
|
+
class HarnessWritesDuringFirstSleep:
|
|
164
|
+
"""Stand-in for the time module that runs the harness write on the first poll sleep."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, write_late_lines: Callable[[], None]) -> None:
|
|
167
|
+
self.write_late_lines = write_late_lines
|
|
168
|
+
self.elapsed_seconds = 0.0
|
|
169
|
+
self.sleep_count = 0
|
|
170
|
+
|
|
171
|
+
def monotonic(self) -> float:
|
|
172
|
+
return self.elapsed_seconds
|
|
173
|
+
|
|
174
|
+
def sleep(self, seconds: float) -> None:
|
|
175
|
+
if self.sleep_count == 0:
|
|
176
|
+
self.write_late_lines()
|
|
177
|
+
self.sleep_count += 1
|
|
178
|
+
self.elapsed_seconds += seconds
|
|
179
|
+
|
|
180
|
+
|
|
163
181
|
def test_should_see_a_note_the_harness_writes_after_the_hook_starts(
|
|
164
182
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
165
183
|
) -> None:
|
|
@@ -174,12 +192,11 @@ def test_should_see_a_note_the_harness_writes_after_the_hook_starts(
|
|
|
174
192
|
with transcript.open("a", encoding="utf-8") as transcript_file:
|
|
175
193
|
transcript_file.write(late_lines)
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
harness_clock = HarnessWritesDuringFirstSleep(append_late_lines)
|
|
196
|
+
monkeypatch.setattr(step_note_gate, "time", harness_clock)
|
|
179
197
|
exit_code, _ = run_gate(monkeypatch, capsys, transcript, "call_1")
|
|
180
|
-
writer.join()
|
|
181
198
|
|
|
182
|
-
assert exit_code == 0
|
|
199
|
+
assert (exit_code, harness_clock.sleep_count) == (0, 1)
|
|
183
200
|
|
|
184
201
|
|
|
185
202
|
def test_should_block_a_bare_call_the_harness_writes_after_the_hook_starts(
|
|
@@ -192,12 +209,11 @@ def test_should_block_a_bare_call_the_harness_writes_after_the_hook_starts(
|
|
|
192
209
|
with transcript.open("a", encoding="utf-8") as transcript_file:
|
|
193
210
|
transcript_file.write(json.dumps(call_entry("m1", "call_1")) + "\n")
|
|
194
211
|
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
harness_clock = HarnessWritesDuringFirstSleep(append_late_call)
|
|
213
|
+
monkeypatch.setattr(step_note_gate, "time", harness_clock)
|
|
197
214
|
exit_code, _ = run_gate(monkeypatch, capsys, transcript, "call_1")
|
|
198
|
-
writer.join()
|
|
199
215
|
|
|
200
|
-
assert exit_code == 2
|
|
216
|
+
assert (exit_code, harness_clock.sleep_count) == (2, 1)
|
|
201
217
|
|
|
202
218
|
|
|
203
219
|
def test_should_allow_a_call_that_never_reaches_the_transcript(
|
package/package.json
CHANGED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Keep the code-rules index and its feature map in sync with enforcement."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
13
|
+
_REPOSITORY_ROOT = _PACKAGE_ROOT.parent.parent
|
|
14
|
+
_INDEX_PATH = _PACKAGE_ROOT / "docs" / "CODE_RULES.md"
|
|
15
|
+
_FAMILY_DIRECTORY = _PACKAGE_ROOT / "docs" / "code-rules"
|
|
16
|
+
_ENFORCER_PATH = _PACKAGE_ROOT / "hooks" / "blocking" / "code_rules_enforcer.py"
|
|
17
|
+
_CITATION_ROOTS = (
|
|
18
|
+
_PACKAGE_ROOT / "hooks",
|
|
19
|
+
_PACKAGE_ROOT / "scripts",
|
|
20
|
+
_PACKAGE_ROOT / "docs",
|
|
21
|
+
_PACKAGE_ROOT / "rules",
|
|
22
|
+
_PACKAGE_ROOT / ".agents",
|
|
23
|
+
_REPOSITORY_ROOT / ".cursor",
|
|
24
|
+
)
|
|
25
|
+
_TEXT_SUFFIXES = frozenset({".md", ".py", ".js", ".mjs", ".ts", ".json", ".xml"})
|
|
26
|
+
_MARKDOWN_LINK = re.compile(r"(?<!!)\[[^]]+\]\(([^)\s]+)(?:\s+[^)]*)?\)")
|
|
27
|
+
_INDEX_ANCHOR = re.compile(r"CODE_RULES\.md#([^\s)\]<>\"`]+)", re.IGNORECASE)
|
|
28
|
+
_NUMBERED_CITATION = re.compile(
|
|
29
|
+
r"(?:CODE_RULES\s*\u00a7\s*|\bsection\s+)(\d+(?:\.\d+)?)", re.IGNORECASE
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _heading_slugs(markdown_text: str) -> set[str]:
|
|
34
|
+
all_slugs: set[str] = set()
|
|
35
|
+
slug_counts: Counter[str] = Counter()
|
|
36
|
+
for each_heading in re.findall(r"^#{1,6}\s+(.+)$", markdown_text, re.MULTILINE):
|
|
37
|
+
heading_words = re.sub(r"[^\w\- ]", "", each_heading.lower())
|
|
38
|
+
base_slug = re.sub(r"\s", "-", heading_words)
|
|
39
|
+
count = slug_counts[base_slug]
|
|
40
|
+
all_slugs.add(base_slug if count == 0 else f"{base_slug}-{count}")
|
|
41
|
+
slug_counts[base_slug] += 1
|
|
42
|
+
return all_slugs
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _index_numbers(index_text: str) -> set[str]:
|
|
46
|
+
all_numbers: set[str] = set()
|
|
47
|
+
for each_heading in re.findall(r"^#{1,6}\s+(.+)$", index_text, re.MULTILINE):
|
|
48
|
+
all_numbers.update(
|
|
49
|
+
re.findall(r"(?<!\d)(\d+(?:\.\d+)?)(?=\.?\s|$)", each_heading)
|
|
50
|
+
)
|
|
51
|
+
return all_numbers
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _assert_citations_resolve(
|
|
55
|
+
index_text: str, all_citations: list[tuple[Path, str]]
|
|
56
|
+
) -> None:
|
|
57
|
+
valid_slugs = _heading_slugs(index_text)
|
|
58
|
+
valid_numbers = _index_numbers(index_text)
|
|
59
|
+
for each_path, each_text in all_citations:
|
|
60
|
+
for each_anchor in _INDEX_ANCHOR.findall(each_text):
|
|
61
|
+
assert each_anchor in valid_slugs, f"{each_path}: dead CODE_RULES anchor #{each_anchor}"
|
|
62
|
+
for each_number in _NUMBERED_CITATION.findall(each_text):
|
|
63
|
+
normalized_number = ".".join(
|
|
64
|
+
str(int(each_part)) for each_part in each_number.split(".")
|
|
65
|
+
)
|
|
66
|
+
assert normalized_number in valid_numbers, (
|
|
67
|
+
f"{each_path}: dead CODE_RULES section {each_number}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_should_assign_each_enforcer_check_to_one_family() -> None:
|
|
72
|
+
enforcer_tree = ast.parse(_ENFORCER_PATH.read_text(encoding="utf-8"))
|
|
73
|
+
imported_checks = Counter(
|
|
74
|
+
each_name.name
|
|
75
|
+
for each_import in enforcer_tree.body
|
|
76
|
+
if isinstance(each_import, ast.ImportFrom)
|
|
77
|
+
for each_name in each_import.names
|
|
78
|
+
if each_name.name.startswith("check_")
|
|
79
|
+
)
|
|
80
|
+
documented_checks: Counter[str] = Counter()
|
|
81
|
+
for each_family_path in _FAMILY_DIRECTORY.glob("*.md"):
|
|
82
|
+
if each_family_path.name == "README.md":
|
|
83
|
+
continue
|
|
84
|
+
family_text = each_family_path.read_text(encoding="utf-8")
|
|
85
|
+
assert re.findall(r"^## (.+)$", family_text, re.MULTILINE) == [
|
|
86
|
+
"Checks",
|
|
87
|
+
"When it fires",
|
|
88
|
+
"Proving it",
|
|
89
|
+
"Gotchas",
|
|
90
|
+
], each_family_path
|
|
91
|
+
checks_match = re.search(
|
|
92
|
+
r"^## Checks\s*\n(.*?)(?=^## |\Z)", family_text, re.MULTILINE | re.DOTALL
|
|
93
|
+
)
|
|
94
|
+
assert checks_match is not None, each_family_path
|
|
95
|
+
family_checks = re.findall(
|
|
96
|
+
r"^- (check_\w+)\s*$", checks_match.group(1), re.MULTILINE
|
|
97
|
+
)
|
|
98
|
+
proof_checks = re.findall(r"^- (check_\w+):", family_text, re.MULTILINE)
|
|
99
|
+
assert Counter(proof_checks) == Counter(family_checks), each_family_path
|
|
100
|
+
documented_checks.update(family_checks)
|
|
101
|
+
assert documented_checks == imported_checks
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _collect_citation_texts() -> list[tuple[Path, str]]:
|
|
105
|
+
all_citations: list[tuple[Path, str]] = []
|
|
106
|
+
for each_root in _CITATION_ROOTS:
|
|
107
|
+
all_candidate_paths = [
|
|
108
|
+
each_path
|
|
109
|
+
for each_path in each_root.rglob("*")
|
|
110
|
+
if each_path.is_file() and each_path.suffix in _TEXT_SUFFIXES
|
|
111
|
+
]
|
|
112
|
+
all_citations.extend(
|
|
113
|
+
(each_path, each_path.read_text(encoding="utf-8"))
|
|
114
|
+
for each_path in all_candidate_paths
|
|
115
|
+
)
|
|
116
|
+
return all_citations
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_should_resolve_code_rules_anchors_and_sections() -> None:
|
|
120
|
+
index_text = _INDEX_PATH.read_text(encoding="utf-8")
|
|
121
|
+
all_citations = _collect_citation_texts()
|
|
122
|
+
injected_anchor = os.environ.get("CODE_RULES_INDEX_TEST_ANCHOR")
|
|
123
|
+
if injected_anchor:
|
|
124
|
+
all_citations.append((_INDEX_PATH, "CODE_RULES.md" + "#" + injected_anchor))
|
|
125
|
+
_assert_citations_resolve(index_text, all_citations)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _assert_destination_resolves(family_path: Path, destination: str) -> None:
|
|
129
|
+
if "://" in destination or destination.startswith("#"):
|
|
130
|
+
return
|
|
131
|
+
path_text, _, anchor_text = destination.partition("#")
|
|
132
|
+
destination_path = family_path.parent / path_text
|
|
133
|
+
assert destination_path.is_file(), f"{family_path}: {destination}"
|
|
134
|
+
if anchor_text:
|
|
135
|
+
destination_text = destination_path.read_text(encoding="utf-8")
|
|
136
|
+
assert anchor_text in _heading_slugs(destination_text), (
|
|
137
|
+
f"{family_path}: {destination}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_should_resolve_every_family_relative_link() -> None:
|
|
142
|
+
for each_family_path in _FAMILY_DIRECTORY.glob("*.md"):
|
|
143
|
+
family_text = each_family_path.read_text(encoding="utf-8")
|
|
144
|
+
for each_destination in _MARKDOWN_LINK.findall(family_text):
|
|
145
|
+
_assert_destination_resolves(each_family_path, each_destination)
|