claude-dev-env 8.44.3 → 8.44.5

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.
@@ -1,10 +1,6 @@
1
1
  # Code Rules Reference
2
2
 
3
- The canonical review-criteria instruction set for every AI agent that audits pull requests in this repository, loaded on demand. [`.cursor/BUGBOT.md`](../../../.cursor/BUGBOT.md) is the checked-in pointer file Cursor BugBot reads; it points here.
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
- - **Self-documenting code.** Names carry the meaning in place of comments.
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
- 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
-
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
- 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.
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
- Before writing ANY constant: search `config/` for the exact value → semantic match → add to the existing config file → create new (rare). Locations: timeouts/delays/retries → `config/timing.py`; ports/URLs/thresholds → `config/constants.py`; CSS selectors → `config/selectors.py`.
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
- Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` for exception. Naming patterns: loop vars `each_*`; booleans `is_/has_/should_/can_/was_/did_`; collections `all_*`; maps `X_by_Y`; preposition params (`from_path=`, `to=`, `into=`). Banned names: `result`, `data`, `output`, `response`, `value`, `item`, `temp`. Banned prefixes: `handle`, `process`, `manage`, `do`. Name components for what they are: `Overlay`, `Validator`, `InvoicePreview`. The failure-blast-radius rule requires `ItemBlocked` or `RunFatal`. Names ending with either bypass the banned-noun word check.
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 instead of a directive marker.
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
- ALL parameters typed, ALL returns typed. No `Any`. Avoid `# type: ignore`; remove it and use a typed boundary or the type. Prefer fixing the type over an ignore when an annotation is available.
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
- Advisory only, never blocking: emit a stderr advisory at >= 400 lines and a stronger stderr advisory at >= 1000 (pylint / SonarQube defaults). Split on cohesion (SRP, "Large Class" smell), not line count — run the readability rubric when an advisory fires.
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
- **Simple > Clever. Functions > Classes. Concrete > Abstract.**
69
- Never: ABC for single impl, DI frameworks, factory for single type. Always: functions when no state, concrete classes, simple imports.
70
- Parameters follow YAGNI: add an optional parameter when a caller varies the value; when every call site passes the same value, make it required or inline the constant. Remove parameters no caller passes and no body reads.
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
- **SRP always applies** — one reason to change per function/class/module. **OCP, LSP, ISP, DIP apply only where two or more concrete implementations already share a contract**; with a single concretion §7 wins (concrete classes, direct imports, YAGNI — introduce the abstraction at the commit that adds the second concretion). Misapplication signals: interface/ABC with exactly one implementation, SRP-splitting a cohesive class by size alone, abstract factories for one product, DI containers where every injected type has one concretion.
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
- 1. **RED** — a failing test. 2. **GREEN** — minimum code to pass. 3. **REFACTOR** — only if valuable.
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
- This loop is the default for a bug fix and for new behavior, and the TDD skill (`pstack:tdd`) carries the procedure. No hook or lint checks the order. A prototype may run ahead of its tests and adds them before the pull request goes ready. A bug fix ships with a test that reproduces the bug.
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
- **Proof of check.** Each pull request body states how the change was checked: a test, a run, a screenshot, or a measurement, named so a reviewer can repeat it. A reviewer flags a pull request whose body names no proof.
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 complete feature (state, modals, overlays, toasts). Parents just render `<Child />`.
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
- A non-`__init__.py` module whose body is only imports (optionally `__all__`) is indirection without payload — callers import the module. `__init__.py` is the canonical re-export surface and is exempt.
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
- Removed code is removed: no renamed re-export aliases, no `_old_*` aliases, no keep-alive wrapper modules, no tombstone comment markers. When a symbol's name or signature changes, update the call sites in the same commit. Git history records change; the codebase records what exists.
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 — collapsing every error class to `None` masks programming errors and makes debugging impossible.
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
- **A per-member boundary records each member outcome.** In a batch loop, a `try`/`except` inside the loop body catches a declared `*ItemBlocked` type, records the failure with its reason, and continues to the next member — the failure reaches the run report by name. The boundary preserves the blast radius: escalations re-raise first so a `*RunFatal` passes through directly, while `except Exception` triggers the rule. Types, boundary shape, and the parked-member report: [`rules/failure-blast-radius.md`](../rules/failure-blast-radius.md).
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
- An edit that deletes or rewrites code also removes everything it makes dead: unread variables, uncalled functions, unpassed parameters, dead branches, unused imports, helper files whose only consumer that edit deleted. Prove unreachability first: Serena `find_referencing_symbols` plus a text search for dynamic lookups (`getattr`, entry-point names). A symbol is live only when a reference chain reaches a live entry point (CLI command, route, public API, test); a self-referential dead cluster is removed together in the same commit. **When liveness is uncertain (public API, plugin hook, reflective dispatch), do NOT delete — surface the ambiguity via AskUserQuestion.** Source links: [`references/dead-code-elimination.md`](references/dead-code-elimination.md).
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
- If you already have the data, don't fetch it again.
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
- **Lint** reports pattern-matchable violations. The staged policy lint runs `code_rules_enforcer.py` over each changed file, and CI runs it against the merge base. **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, research-first action on ambiguous intent, BDD discovery, docstring-prose-matches-implementation). **Audit rubrics** (`packages/claude-dev-env/audit-rubrics/` categories A to Q) cover cross-file architectural concerns. Rules with documented-but-pending check coverage live in `~/.claude/rules/*.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit. The diagram-first docstring standard (a summary line, then a `::` example or doctest, then a couple of short prose lines) lives in `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`, enforced by the `check_docstring_runon_sentence` and `check_docstring_prose_wall_without_illustration` enforcer checks and Category O9 audit.
115
+ Lint checks patterns; prompts carry judgment; rubrics cover cross-file concerns.
117
116
 
118
- ## 11.5 VALIDATION-PHASE PRECEDENCE
117
+ Open [details](code-rules/enforcement-surfaces.md) when routing a rule.
119
118
 
120
- A staged policy lint run of `code_rules_enforcer.py` selects what it checks and reports along three independent axes. Each axis filters a narrower scope than the one before it; none widens what the axis before it already decided.
119
+ ## 11.5 VALIDATION-PHASE PRECEDENCE
121
120
 
122
- 1. **Phase selects the roster.** `EDIT_LANE_PHASE` or `FULL_GATE_PHASE` decides which checks exist in the lane at all. `validate_content_for_phase` takes `phase` keyword-only with no default, so every caller names its lane explicitly.
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
- `hooks/hooks_constants/validation_phase_constants.py` is the single source for all three axes: the phase names (`EDIT_LANE_PHASE`, `FULL_GATE_PHASE`), the full-gate-only roster (`ALL_FULL_GATE_ONLY_CHECK_NAMES`), and the hook-infrastructure edit-lane roster (`ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES`).
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
- Hook-infrastructure targets run three checks in the edit lane — `check_same_file_inline_duplicate_body`, `check_zero_payload_function_alias`, `check_unanchored_command_dispatch` — and the whole roster on the full gate. `ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES` holds that set.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.44.3",
3
+ "version": "8.44.5",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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)
@@ -28,6 +28,7 @@ MAXIMUM_ENTRY_BYTES = 1_500
28
28
  MAXIMUM_ALWAYS_ON_BYTES = 12_000
29
29
  POINTER_ENTRY_NAMES = frozenset({"skill-pointers.md"})
30
30
  CODEX_VERBATIM_ENTRY_NAMES = frozenset({"question-presentation.md"})
31
+ INDEX_SHAPE_EXEMPT_ENTRY_NAMES = POINTER_ENTRY_NAMES | CODEX_VERBATIM_ENTRY_NAMES
31
32
  CODEX_MATERIALIZED_GUIDE_NAMES = frozenset(
32
33
  Path(each_path).name for each_path in codex_instruction_rule_relative_paths
33
34
  )
@@ -93,7 +94,7 @@ def test_each_entry_states_when_it_applies() -> None:
93
94
  without_when_line = [
94
95
  each_path.name
95
96
  for each_path in _entry_paths()
96
- if each_path.name not in POINTER_ENTRY_NAMES | CODEX_VERBATIM_ENTRY_NAMES
97
+ if each_path.name not in INDEX_SHAPE_EXEMPT_ENTRY_NAMES
97
98
  and not WHEN_LINE_PATTERN.search(_entry_text(each_path))
98
99
  ]
99
100
  assert without_when_line == []
@@ -114,7 +115,7 @@ def test_each_entry_links_a_full_text_guide_that_exists() -> None:
114
115
  all_problems = [
115
116
  each_problem
116
117
  for each_path in _entry_paths()
117
- if each_path.name not in POINTER_ENTRY_NAMES | CODEX_VERBATIM_ENTRY_NAMES
118
+ if each_path.name not in INDEX_SHAPE_EXEMPT_ENTRY_NAMES
118
119
  for each_problem in _full_text_link_problems(each_path)
119
120
  ]
120
121
  assert all_problems == []