claude-dev-env 2.28.1 → 2.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/AGENTS.md +0 -31
- package/.agents/agents/clean-coder.md +1 -1
- package/.agents/agents/pr-description-writer.md +21 -6
- package/.agents/agents/reference/README.md +6 -0
- package/.agents/agents/reference/pr-description-illustrative-voice.md +45 -0
- package/.agents/agents/reference/pr-description-verification.md +19 -0
- package/.agents/agents/test_agent_frontmatter.py +3 -1
- package/.agents/agents/test_pr_description_writer_style.py +47 -0
- package/.agents/skills/AGENTS.md +0 -70
- package/.agents/skills/_shared/AGENTS.md +0 -44
- package/.agents/skills/_shared/advisor/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -57
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -8
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -34
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -23
- package/.agents/skills/autoconverge/AGENTS.md +0 -35
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -15
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -25
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -15
- package/.agents/skills/everything-search/AGENTS.md +0 -16
- package/.agents/skills/fresh-branch/AGENTS.md +0 -13
- package/.agents/skills/grok-spawn/AGENTS.md +0 -27
- package/.agents/skills/orchestrator/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/SKILL.md +44 -53
- package/.agents/skills/orchestrator/reference/.claude/CLAUDE.md +1 -0
- package/.agents/skills/orchestrator/reference/AGENTS.md +1 -0
- package/.agents/skills/orchestrator/reference/consult-the-orchestrator.md +70 -0
- package/.agents/skills/orchestrator/reference/executor-consult-block.md +62 -0
- package/.agents/skills/orchestrator/reference/host-detect.md +15 -0
- package/.agents/skills/orchestrator/test_orchestrator_skill_contract.py +62 -0
- package/.agents/skills/orchestrator-refresh/SKILL.md +16 -34
- package/.agents/skills/rebase/AGENTS.md +0 -31
- package/.agents/skills/session-log/AGENTS.md +0 -31
- package/.agents/skills/session-tidy/AGENTS.md +0 -35
- package/.agents/skills/skill-builder/AGENTS.md +0 -48
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -24
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -13
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -18
- package/.agents/skills/task-build/AGENTS.md +0 -28
- package/.agents/skills/update/AGENTS.md +0 -37
- package/.agents/skills-archived/AGENTS.md +0 -44
- package/.agents/skills-archived/anthropic-plan/AGENTS.md +0 -33
- package/.agents/skills-archived/anthropic-plan/scripts/AGENTS.md +0 -10
- package/.agents/skills-archived/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +0 -15
- package/.agents/skills-archived/anthropic-plan/templates/AGENTS.md +0 -12
- package/.agents/skills-archived/anthropic-plan/workflow/AGENTS.md +0 -13
- package/.agents/skills-archived/auditing-claude-config/AGENTS.md +0 -20
- package/.agents/skills-archived/bugteam/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/reference/AGENTS.md +0 -19
- package/.agents/skills-archived/bugteam/reference/obstacles/AGENTS.md +0 -23
- package/.agents/skills-archived/bugteam/scripts/AGENTS.md +0 -29
- package/.agents/skills-archived/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/AGENTS.md +0 -45
- package/.agents/skills-archived/codex-review/reference/AGENTS.md +0 -14
- package/.agents/skills-archived/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/codex-review/test_skill_scaffold.py +13 -3
- package/.agents/skills-archived/copilot-review/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/AGENTS.md +0 -31
- package/.agents/skills-archived/pr-converge/pr_converge_skill_constants/AGENTS.md +0 -25
- package/.agents/skills-archived/pr-converge/reference/AGENTS.md +0 -27
- package/.agents/skills-archived/pr-converge/reference/obstacles/AGENTS.md +0 -22
- package/.agents/skills-archived/pr-converge/scripts/AGENTS.md +0 -45
- package/.agents/skills-archived/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/.agents/skills-archived/pr-converge/workflows/AGENTS.md +0 -15
- package/.agents/skills-archived/recall/AGENTS.md +0 -29
- package/.agents/skills-archived/remember/AGENTS.md +0 -30
- package/AGENTS.md +0 -112
- package/_shared/AGENTS.md +0 -16
- package/_shared/advisor/AGENTS.md +0 -21
- package/_shared/advisor/advisor-protocol.md +4 -1
- package/_shared/advisor/reference/consult-format.md +4 -2
- package/_shared/pr-loop/AGENTS.md +0 -27
- package/_shared/pr-loop/code-rules-gate.md +1 -1
- package/_shared/pr-loop/scripts/AGENTS.md +0 -49
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +0 -41
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +8 -8
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +1 -1
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/AGENTS.md +0 -17
- package/_shared/pr-loop/scripts/pr_converge_skill_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +0 -25
- package/_shared/pr-loop/scripts/tests/AGENTS.md +0 -43
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +15 -15
- package/_shared/process-tree/AGENTS.md +0 -40
- package/audit-rubrics/AGENTS.md +0 -42
- package/audit-rubrics/category_rubrics/AGENTS.md +0 -36
- package/audit-rubrics/prompts/AGENTS.md +0 -36
- package/bin/AGENTS.md +0 -118
- package/bin/install.test.mjs +1 -1
- package/commands/AGENTS.md +0 -14
- package/docs/AGENTS.md +0 -31
- package/docs/CODE_RULES.md +10 -0
- package/docs/references/AGENTS.md +0 -16
- package/hooks/AGENTS.md +0 -27
- package/hooks/advisory/AGENTS.md +0 -15
- package/hooks/blocking/AGENTS.md +0 -112
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +0 -27
- package/hooks/blocking/code_rules_enforcer.py +433 -313
- package/hooks/blocking/config/AGENTS.md +0 -9
- package/hooks/blocking/inventory_intent_records/AGENTS.md +0 -25
- package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +0 -25
- package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +0 -23
- package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +0 -29
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +117 -22
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +3 -3
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +117 -38
- package/hooks/blocking/test_code_rules_enforcer_output_snapshot.py +121 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +17 -15
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +4 -2
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_stage.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -2
- package/hooks/blocking/test_code_rules_validation_phase.py +281 -0
- package/hooks/blocking/test_fable_spawn_gate.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +26 -11
- package/hooks/git-hooks/AGENTS.md +0 -31
- package/hooks/git-hooks/git_hooks_constants/AGENTS.md +0 -20
- package/hooks/hooks_constants/AGENTS.md +0 -99
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +21 -0
- package/hooks/hooks_constants/validation_phase_constants.py +34 -0
- package/hooks/lifecycle/AGENTS.md +0 -17
- package/hooks/observability/AGENTS.md +0 -19
- package/hooks/session/AGENTS.md +0 -37
- package/hooks/validation/AGENTS.md +0 -19
- package/hooks/validators/AGENTS.md +0 -51
- package/hooks/validators/README.md +1 -1
- package/hooks/validators/config/hook_timing_harness_constants.py +16 -0
- package/hooks/validators/hook_timing_harness.py +287 -0
- package/hooks/validators/test_hook_timing_harness.py +251 -0
- package/hooks/workflow/AGENTS.md +0 -15
- package/output-styles/AGENTS.md +0 -14
- package/package.json +1 -1
- package/rules/AGENTS.md +0 -55
- package/scripts/AGENTS.md +0 -55
- package/scripts/dev_env_scripts_constants/AGENTS.md +0 -20
- package/scripts/sync_to_cursor/AGENTS.md +0 -22
- package/scripts/tests/AGENTS.md +0 -34
- package/system-prompts/AGENTS.md +0 -24
|
@@ -1,10 +1 @@
|
|
|
1
|
-
# hooks/blocking/config
|
|
2
1
|
|
|
3
|
-
A Python package that holds shared constants for blocking hooks. Opinionated prose-style gates import from here.
|
|
4
|
-
|
|
5
|
-
## Key files
|
|
6
|
-
|
|
7
|
-
| File | Contents |
|
|
8
|
-
|---|---|
|
|
9
|
-
| `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
|
|
10
|
-
| `prose_style_enforcement_constants.py` | `CLAUDE_PROSE_STYLE_ENFORCEMENT` opt-in (default off) for opinionated prose gates |
|
|
@@ -1,26 +1 @@
|
|
|
1
|
-
# inventory_intent_records
|
|
2
1
|
|
|
3
|
-
The shared pending-intent store that lets `package_inventory_stale_blocker.py`
|
|
4
|
-
and `claude_md_orphan_file_blocker.py` add a new file and its inventory row in
|
|
5
|
-
one change, in either order. When one blocker denies, it records a note; the
|
|
6
|
-
sibling blocker reads that note to allow the matching second write.
|
|
7
|
-
|
|
8
|
-
## Modules
|
|
9
|
-
|
|
10
|
-
| File | Purpose |
|
|
11
|
-
|---|---|
|
|
12
|
-
| `records.py` | Read, record, peek, and consume the per-session file and row intents; a missing or corrupt store reads as no notes |
|
|
13
|
-
| `__init__.py` | Package marker |
|
|
14
|
-
|
|
15
|
-
## Subdirectories
|
|
16
|
-
|
|
17
|
-
| Entry | Description |
|
|
18
|
-
|---|---|
|
|
19
|
-
| `config/` | The records-file name shape, the freshness window, the list keys, and the note field names (`intent_records_constants.py`) |
|
|
20
|
-
| `tests/` | pytest suite for the records store |
|
|
21
|
-
|
|
22
|
-
## Running tests
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
python -m pytest packages/claude-dev-env/hooks/blocking/inventory_intent_records/tests/
|
|
26
|
-
```
|
|
@@ -1,26 +1 @@
|
|
|
1
|
-
# package_inventory_stale_blocker_parts
|
|
2
1
|
|
|
3
|
-
The concern modules `package_inventory_stale_blocker.py` wires together to block a
|
|
4
|
-
new production file its package inventory omits. The entry hook imports them and
|
|
5
|
-
re-exports their surface for the test suite.
|
|
6
|
-
|
|
7
|
-
## Modules
|
|
8
|
-
|
|
9
|
-
| File | Purpose |
|
|
10
|
-
|---|---|
|
|
11
|
-
| `inventory_detection.py` | Surveys a directory's `README.md`/`CLAUDE.md`/`SKILL.md`, collects the filenames they name, and reports whether a maintained inventory omits the file being written |
|
|
12
|
-
| `decision.py` | Builds the deny payload naming the omitted file and closing with the retry hint, and emits the decision JSON |
|
|
13
|
-
| `__init__.py` | Package marker |
|
|
14
|
-
|
|
15
|
-
## Subdirectories
|
|
16
|
-
|
|
17
|
-
| Entry | Description |
|
|
18
|
-
|---|---|
|
|
19
|
-
| `config/` | The retry hint the deny reason closes with and the inventory-name join separator (`inventory_blocker_constants.py`) |
|
|
20
|
-
| `tests/` | pytest suite with one test module per module above |
|
|
21
|
-
|
|
22
|
-
## Running tests
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
python -m pytest packages/claude-dev-env/hooks/blocking/package_inventory_stale_blocker_parts/tests/
|
|
26
|
-
```
|
|
@@ -1,24 +1 @@
|
|
|
1
|
-
# pii_prevention_blocker_parts
|
|
2
1
|
|
|
3
|
-
The concern modules `pii_prevention_blocker.py` wires together. The entry hook
|
|
4
|
-
imports them and re-exports their surface for the test suite.
|
|
5
|
-
|
|
6
|
-
## Modules
|
|
7
|
-
|
|
8
|
-
| File | Purpose |
|
|
9
|
-
|---|---|
|
|
10
|
-
| `repository_exemption.py` | Resolves a commit's origin owner/repo slug, decides whether the repository skips the staged PII scan, and resolves the exact values that repository allows past the scan |
|
|
11
|
-
| `repository_resolution.py` | Reads the repository a commit command targets — composing multiple `-C` values and a leading `cd`/`pushd` — and builds the deny reason naming the path when the repository root does not resolve |
|
|
12
|
-
| `__init__.py` | Package marker |
|
|
13
|
-
|
|
14
|
-
## Subdirectories
|
|
15
|
-
|
|
16
|
-
| Entry | Description |
|
|
17
|
-
|---|---|
|
|
18
|
-
| `config/` | The repository-resolution deny-message template and the session-cwd label (`repository_resolution_constants.py`) |
|
|
19
|
-
|
|
20
|
-
## Running tests
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
python -m pytest packages/claude-dev-env/hooks/blocking/tests/
|
|
24
|
-
```
|
|
@@ -1,30 +1 @@
|
|
|
1
|
-
# tdd_enforcer_parts
|
|
2
1
|
|
|
3
|
-
The concern modules `tdd_enforcer.py` wires together to run the TDD gate. Each
|
|
4
|
-
module owns one concern; the entry hook imports them and re-exports their
|
|
5
|
-
surface for the test suite.
|
|
6
|
-
|
|
7
|
-
## Modules
|
|
8
|
-
|
|
9
|
-
| File | Purpose |
|
|
10
|
-
|---|---|
|
|
11
|
-
| `path_classification.py` | Classifies a write target (docs, tests, `.claude` trees) the gate skips, and extracts the written text from a Write, Edit, or MultiEdit payload |
|
|
12
|
-
| `content_analysis.py` | Decides whether a payload is constants-only, and whether an Edit or MultiEdit merely removes or reorders imports |
|
|
13
|
-
| `candidate_paths.py` | Resolves the candidate test files whose freshness can satisfy the gate for a production file |
|
|
14
|
-
| `freshness.py` | Checks whether a candidate test was modified within the window and holds a real test function |
|
|
15
|
-
| `git_tracking.py` | Detects a write that restores a git-tracked file absent on disk, so a remove-then-Write rewrite is exempt from the gate |
|
|
16
|
-
| `decisions.py` | Builds the deny reason and writes the allow or deny decision JSON to stdout |
|
|
17
|
-
| `__init__.py` | Package marker |
|
|
18
|
-
|
|
19
|
-
## Subdirectories
|
|
20
|
-
|
|
21
|
-
| Entry | Description |
|
|
22
|
-
|---|---|
|
|
23
|
-
| `config/` | The freshness window, the ancestor-walk limit, the git-tracking command tokens, the source-file extension sets, and the join separator (`tdd_enforcer_constants.py`) |
|
|
24
|
-
| `tests/` | pytest suite with one test module per module above |
|
|
25
|
-
|
|
26
|
-
## Running tests
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
python -m pytest packages/claude-dev-env/hooks/blocking/tdd_enforcer_parts/tests/
|
|
30
|
-
```
|
|
@@ -28,7 +28,7 @@ assert hook_spec.loader is not None
|
|
|
28
28
|
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
29
29
|
hook_spec.loader.exec_module(hook_module)
|
|
30
30
|
check_banned_noun_word_boundary = hook_module.check_banned_noun_word_boundary
|
|
31
|
-
validate_content = hook_module.
|
|
31
|
+
validate_content = hook_module.validate_content_for_full_gate
|
|
32
32
|
|
|
33
33
|
from code_rules_banned_identifiers import ( # noqa: E402
|
|
34
34
|
_find_banned_noun_word,
|
|
@@ -31,7 +31,7 @@ def check_class_docstring_names_public_methods(content: str, file_path: str) ->
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
34
|
-
return code_rules_enforcer.
|
|
34
|
+
return code_rules_enforcer.validate_content_for_full_gate(content, file_path, old_content)
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
PRODUCTION_FILE_PATH = "/project/src/break_reporter.py"
|
|
@@ -1,25 +1,49 @@
|
|
|
1
|
-
"""Meta-test asserting every check_* function is
|
|
1
|
+
"""Meta-test asserting every check_* function is reachable from the full gate.
|
|
2
2
|
|
|
3
3
|
The per-check test modules each prove one ``check_*`` function flags the right
|
|
4
4
|
violation, but none proves the enforcer actually calls that function. A refactor
|
|
5
|
-
that drops a dispatch line from ``
|
|
6
|
-
|
|
5
|
+
that drops a dispatch line from ``validate_content_for_full_gate`` or from one
|
|
6
|
+
of its extension-issue helper functions leaves every per-check test green
|
|
7
|
+
while the check stops firing at Write/Edit time — the precise failure mode
|
|
7
8
|
that would let a dead module-level constant (the ``MEDIUM_TEXT`` class) or an
|
|
8
9
|
orphan CSS class slip past the gate again.
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
::
|
|
12
|
+
|
|
13
|
+
validate_content_for_full_gate -> _python_extension_issues -> _python_magic_value_and_constant_issues
|
|
14
|
+
|
|
|
15
|
+
v
|
|
16
|
+
references check_magic_values
|
|
17
|
+
-> check_magic_values counts as reachable
|
|
18
|
+
|
|
19
|
+
check_unanchored_command_dispatch: called only from
|
|
20
|
+
_hook_infrastructure_blocking_issues, which the full gate's call graph
|
|
21
|
+
never reaches -> stays in KNOWN_UNDISPATCHED_CHECKS
|
|
22
|
+
|
|
23
|
+
This module walks the enforcer's real call graph from
|
|
24
|
+
``validate_content_for_full_gate``, following every direct function call into
|
|
25
|
+
the functions the enforcer module defines, and collects every
|
|
26
|
+
``check_*``/``advise_*`` name each reached function body references — a direct
|
|
27
|
+
call or a bare reference passed on as a callback (the
|
|
28
|
+
``_fragment_or_deferred_check(check_magic_values, ...)`` shape). A name that
|
|
29
|
+
surfaces only in an import statement or a docstring is not a reference inside
|
|
30
|
+
a reached function body, so it does not count; this is what keeps the check
|
|
31
|
+
from going vacuous the way a whole-module substring scan would.
|
|
32
|
+
|
|
33
|
+
A check that is intentionally not wired must be listed in
|
|
34
|
+
``KNOWN_UNDISPATCHED_CHECKS`` with a reason in this docstring.
|
|
35
|
+
``check_unanchored_command_dispatch`` is listed there: it guards a
|
|
36
|
+
``hooks/blocking`` command classifier, and the whole
|
|
37
|
+
``validate_content_for_full_gate`` verdict stays off hook-infrastructure
|
|
38
|
+
files, so the enforcer dispatches it from
|
|
16
39
|
``_hook_infrastructure_blocking_issues`` instead. The companion
|
|
17
|
-
``test_code_rules_enforcer_cap_meta.py`` guards the payload-cap convention;
|
|
18
|
-
module guards the wiring.
|
|
40
|
+
``test_code_rules_enforcer_cap_meta.py`` guards the payload-cap convention;
|
|
41
|
+
this module guards the wiring.
|
|
19
42
|
"""
|
|
20
43
|
|
|
21
44
|
from __future__ import annotations
|
|
22
45
|
|
|
46
|
+
import ast
|
|
23
47
|
import importlib.util
|
|
24
48
|
import inspect
|
|
25
49
|
import pathlib
|
|
@@ -42,6 +66,8 @@ KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset(
|
|
|
42
66
|
{"check_unanchored_command_dispatch"}
|
|
43
67
|
)
|
|
44
68
|
|
|
69
|
+
DISPATCH_ENTRY_POINT_NAME = "validate_content_for_full_gate"
|
|
70
|
+
|
|
45
71
|
|
|
46
72
|
def _all_check_function_names() -> list[str]:
|
|
47
73
|
return [
|
|
@@ -52,22 +78,91 @@ def _all_check_function_names() -> list[str]:
|
|
|
52
78
|
]
|
|
53
79
|
|
|
54
80
|
|
|
55
|
-
def
|
|
56
|
-
|
|
81
|
+
def _module_level_function_defs(module_tree: ast.Module) -> dict[str, ast.FunctionDef]:
|
|
82
|
+
"""Map each module-level function's name to its definition node."""
|
|
83
|
+
return {
|
|
84
|
+
each_node.name: each_node
|
|
85
|
+
for each_node in module_tree.body
|
|
86
|
+
if isinstance(each_node, ast.FunctionDef)
|
|
87
|
+
}
|
|
57
88
|
|
|
58
89
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
def _local_call_targets(function_node: ast.FunctionDef) -> set[str]:
|
|
91
|
+
"""Return the plain-name targets one function directly calls."""
|
|
92
|
+
return {
|
|
93
|
+
each_call.func.id
|
|
94
|
+
for each_call in ast.walk(function_node)
|
|
95
|
+
if isinstance(each_call, ast.Call) and isinstance(each_call.func, ast.Name)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _unvisited_local_calls(
|
|
100
|
+
function_name: str,
|
|
101
|
+
function_defs_by_name: dict[str, ast.FunctionDef],
|
|
102
|
+
visited: set[str],
|
|
103
|
+
) -> list[str]:
|
|
104
|
+
"""Return *function_name*'s call targets not already visited by the walk."""
|
|
105
|
+
function_node = function_defs_by_name.get(function_name)
|
|
106
|
+
if function_node is None:
|
|
107
|
+
return []
|
|
108
|
+
return [each for each in _local_call_targets(function_node) if each not in visited]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _reachable_function_names(
|
|
112
|
+
start_name: str, function_defs_by_name: dict[str, ast.FunctionDef]
|
|
113
|
+
) -> set[str]:
|
|
114
|
+
"""Return every function name the call graph reaches, starting from start_name."""
|
|
115
|
+
visited: set[str] = set()
|
|
116
|
+
pending = [start_name]
|
|
117
|
+
while pending:
|
|
118
|
+
each_pending_name = pending.pop()
|
|
119
|
+
if each_pending_name in visited:
|
|
120
|
+
continue
|
|
121
|
+
visited.add(each_pending_name)
|
|
122
|
+
pending.extend(_unvisited_local_calls(each_pending_name, function_defs_by_name, visited))
|
|
123
|
+
return visited
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _referenced_names(function_node: ast.FunctionDef) -> set[str]:
|
|
127
|
+
"""Return every loaded identifier one function body references.
|
|
128
|
+
|
|
129
|
+
A callback-style dispatch (``_fragment_or_deferred_check(check_magic_values,
|
|
130
|
+
...)``) passes the check function by bare name rather than calling it
|
|
131
|
+
directly, so this collects every loaded ``Name``, not only call targets.
|
|
132
|
+
"""
|
|
133
|
+
return {
|
|
134
|
+
each_name.id
|
|
135
|
+
for each_name in ast.walk(function_node)
|
|
136
|
+
if isinstance(each_name, ast.Name) and isinstance(each_name.ctx, ast.Load)
|
|
64
137
|
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _reachable_referenced_names(
|
|
141
|
+
start_name: str, function_defs_by_name: dict[str, ast.FunctionDef]
|
|
142
|
+
) -> set[str]:
|
|
143
|
+
"""Return every identifier referenced by a function the call graph reaches."""
|
|
144
|
+
reachable_function_names = _reachable_function_names(start_name, function_defs_by_name)
|
|
145
|
+
referenced_names: set[str] = set()
|
|
146
|
+
for each_function_name in reachable_function_names:
|
|
147
|
+
function_node = function_defs_by_name.get(each_function_name)
|
|
148
|
+
if function_node is not None:
|
|
149
|
+
referenced_names.update(_referenced_names(function_node))
|
|
150
|
+
return referenced_names
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_every_check_function_is_reachable_from_the_full_gate() -> None:
|
|
154
|
+
module_tree = ast.parse(inspect.getsource(_hook_module))
|
|
155
|
+
function_defs_by_name = _module_level_function_defs(module_tree)
|
|
156
|
+
reachable_names = _reachable_referenced_names(DISPATCH_ENTRY_POINT_NAME, function_defs_by_name)
|
|
157
|
+
all_check_names = set(_all_check_function_names())
|
|
158
|
+
undispatched_check_names = all_check_names - reachable_names
|
|
65
159
|
unexpected_undispatched = undispatched_check_names - KNOWN_UNDISPATCHED_CHECKS
|
|
66
160
|
assert unexpected_undispatched == set(), (
|
|
67
|
-
f"check_* functions are imported but never
|
|
68
|
-
f"{sorted(unexpected_undispatched)}.
|
|
69
|
-
f"
|
|
70
|
-
f"
|
|
161
|
+
f"check_* functions are imported but never reachable from "
|
|
162
|
+
f"validate_content_for_full_gate's call graph: {sorted(unexpected_undispatched)}. "
|
|
163
|
+
f"Wire each into validate_content_for_phase or one of its extension-issue "
|
|
164
|
+
f"helpers so the check fires at Write/Edit time, or list it in "
|
|
165
|
+
f"KNOWN_UNDISPATCHED_CHECKS with a reason in the test header docstring."
|
|
71
166
|
)
|
|
72
167
|
|
|
73
168
|
|
|
@@ -30,7 +30,7 @@ def check_docstring_args_match_signature(content: str, file_path: str) -> list[s
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
33
|
-
return code_rules_enforcer.
|
|
33
|
+
return code_rules_enforcer.validate_content_for_full_gate(content, file_path, old_content)
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
PRODUCTION_FILE_PATH = "/project/src/services.py"
|
|
@@ -39,7 +39,7 @@ def check_docstring_prose_wall_without_illustration(content: str, file_path: str
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
42
|
-
return code_rules_enforcer.
|
|
42
|
+
return code_rules_enforcer.validate_content_for_full_gate(content, file_path, old_content)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
PRODUCTION_FILE_PATH = "/project/src/tally.py"
|
|
@@ -42,7 +42,7 @@ def check_docstring_runon_sentence(content: str, file_path: str) -> list[str]:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
45
|
-
return code_rules_enforcer.
|
|
45
|
+
return code_rules_enforcer.validate_content_for_full_gate(content, file_path, old_content)
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
PRODUCTION_FILE_PATH = "/project/src/run_lifecycle.py"
|
|
@@ -32,7 +32,7 @@ def check_documents_unreferenced_parameter(content: str, file_path: str) -> list
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
35
|
-
return code_rules_enforcer.
|
|
35
|
+
return code_rules_enforcer.validate_content_for_full_gate(content, file_path, old_content)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
PRODUCTION_FILE_PATH = "/project/src/per_theme_loop.py"
|
|
@@ -17,7 +17,7 @@ assert hook_spec.loader is not None
|
|
|
17
17
|
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
18
18
|
hook_spec.loader.exec_module(hook_module)
|
|
19
19
|
check_fstring_structural_literals = hook_module.check_fstring_structural_literals
|
|
20
|
-
validate_content = hook_module.
|
|
20
|
+
validate_content = hook_module.validate_content_for_full_gate
|
|
21
21
|
|
|
22
22
|
PRODUCTION_FILE_PATH = "packages/claude-dev-env/example.py"
|
|
23
23
|
TEST_FILE_PATH = "packages/claude-dev-env/test_example.py"
|
|
@@ -25,7 +25,7 @@ from code_rules_boolean_mustcheck import ( # noqa: E402
|
|
|
25
25
|
check_ignored_must_check_return as _check_ignored_must_check_return,
|
|
26
26
|
)
|
|
27
27
|
from code_rules_enforcer import ( # noqa: E402
|
|
28
|
-
|
|
28
|
+
validate_content_for_full_gate as _validate_content,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
code_rules_enforcer = SimpleNamespace(
|
|
@@ -23,7 +23,7 @@ assert hook_spec.loader is not None
|
|
|
23
23
|
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
24
24
|
hook_spec.loader.exec_module(hook_module)
|
|
25
25
|
check_inline_tuple_string_magic = hook_module.check_inline_tuple_string_magic
|
|
26
|
-
validate_content = hook_module.
|
|
26
|
+
validate_content = hook_module.validate_content_for_full_gate
|
|
27
27
|
|
|
28
28
|
PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
|
|
29
29
|
TEST_FILE_PATH = "packages/app/services/test_loader.py"
|
|
@@ -77,7 +77,7 @@ def test_should_grandfather_unchanged_join_separator_in_wide_edit() -> None:
|
|
|
77
77
|
" # keep the join separator the same\n"
|
|
78
78
|
" return ', '.join(str(each_path) for each_path in all_paths)\n"
|
|
79
79
|
)
|
|
80
|
-
issues = code_rules_enforcer.
|
|
80
|
+
issues = code_rules_enforcer.validate_content_for_full_gate(
|
|
81
81
|
proposed_fragment,
|
|
82
82
|
PRODUCTION_FILE_PATH,
|
|
83
83
|
prior_fragment,
|
|
@@ -97,7 +97,7 @@ def test_should_block_new_join_separator_in_wide_edit() -> None:
|
|
|
97
97
|
"def render(all_paths: list) -> str:\n"
|
|
98
98
|
" return ', '.join(all_paths)\n"
|
|
99
99
|
)
|
|
100
|
-
issues = code_rules_enforcer.
|
|
100
|
+
issues = code_rules_enforcer.validate_content_for_full_gate(
|
|
101
101
|
proposed_fragment,
|
|
102
102
|
PRODUCTION_FILE_PATH,
|
|
103
103
|
prior_fragment,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Enforcer-dispatch tests for the JS returns-object schema-less branch check.
|
|
2
2
|
|
|
3
|
-
These drive ``
|
|
3
|
+
These drive ``validate_content_for_full_gate`` end-to-end on a ``.mjs`` payload, so they prove
|
|
4
4
|
the check is wired into the JavaScript branch of the enforcer, not just callable
|
|
5
5
|
in isolation. The drift: a ``function`` whose JSDoc ``@returns {Promise<object>}``
|
|
6
6
|
promises a structured object while one branch returns the agent helper with an
|
|
@@ -62,11 +62,11 @@ def _has_returns_object_finding(all_issues: list[str]) -> bool:
|
|
|
62
62
|
|
|
63
63
|
def test_enforcer_reports_schema_less_branch_under_returns_object() -> None:
|
|
64
64
|
drift_source = _mixed_schema_mjs()
|
|
65
|
-
all_issues = code_rules_enforcer.
|
|
65
|
+
all_issues = code_rules_enforcer.validate_content_for_full_gate(drift_source, _MJS_PATH, drift_source)
|
|
66
66
|
assert _has_returns_object_finding(all_issues)
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def test_enforcer_accepts_every_branch_with_a_schema() -> None:
|
|
70
70
|
clean_source = _all_schema_mjs()
|
|
71
|
-
all_issues = code_rules_enforcer.
|
|
71
|
+
all_issues = code_rules_enforcer.validate_content_for_full_gate(clean_source, _MJS_PATH, clean_source)
|
|
72
72
|
assert not _has_returns_object_finding(all_issues)
|
|
@@ -31,7 +31,7 @@ def check_module_docstring_names_public_checks(content: str, file_path: str) ->
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
34
|
-
return code_rules_enforcer.
|
|
34
|
+
return code_rules_enforcer.validate_content_for_full_gate(content, file_path, old_content)
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/code_rules_test_assertions.py"
|
|
@@ -18,7 +18,7 @@ assert _hook_spec.loader is not None
|
|
|
18
18
|
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
19
19
|
_hook_spec.loader.exec_module(_hook_module)
|
|
20
20
|
check_boolean_naming = _hook_module.check_boolean_naming
|
|
21
|
-
validate_content = _hook_module.
|
|
21
|
+
validate_content = _hook_module.validate_content_for_full_gate
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
PRODUCTION_FILE_PATH = "src/app/feature.py"
|
|
@@ -20,7 +20,7 @@ if _HOOKS_DIRECTORY not in sys.path:
|
|
|
20
20
|
|
|
21
21
|
from code_rules_enforcer import ( # noqa: E402
|
|
22
22
|
main,
|
|
23
|
-
validate_content,
|
|
23
|
+
validate_content_for_full_gate as validate_content,
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
|
|
@@ -165,47 +165,126 @@ def _validate_narrow_edit(narrow_edit_fixture: NarrowEditFixture) -> list[str]:
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
_SCOPE_CARRYING_ATTRIBUTE_NAMES = frozenset({"all_changed_lines", "defer_scope_to_caller"})
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _assignment_target_names(target: ast.expr) -> list[str]:
|
|
172
|
+
"""Return the plain names one assignment target binds, Name or Tuple-of-Name."""
|
|
173
|
+
if isinstance(target, ast.Name):
|
|
174
|
+
return [target.id]
|
|
175
|
+
if isinstance(target, ast.Tuple):
|
|
176
|
+
return [each.id for each in target.elts if isinstance(each, ast.Name)]
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _scope_aliases_for_pair(target_names: list[str], all_values: list[ast.expr]) -> dict[str, str]:
|
|
181
|
+
"""Return the local-name-to-scope-attribute map one assignment pair contributes."""
|
|
182
|
+
aliases: dict[str, str] = {}
|
|
183
|
+
for each_name, each_value in zip(target_names, all_values):
|
|
184
|
+
if not isinstance(each_value, ast.Attribute):
|
|
173
185
|
continue
|
|
174
|
-
if not
|
|
186
|
+
if each_value.attr not in _SCOPE_CARRYING_ATTRIBUTE_NAMES:
|
|
175
187
|
continue
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
aliases[each_name] = each_value.attr
|
|
189
|
+
return aliases
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _context_alias_map(function_node: ast.AST) -> dict[str, str]:
|
|
193
|
+
"""Map each local name one function binds from ``context.<scope attribute>``.
|
|
194
|
+
|
|
195
|
+
A dispatch helper unpacks ``context.all_changed_lines`` and
|
|
196
|
+
``context.defer_scope_to_caller`` into a short local name (``changed``,
|
|
197
|
+
``defer``) before passing it on, so a call carrying that local name still
|
|
198
|
+
carries scope, even though the local name spells out none of it.
|
|
199
|
+
"""
|
|
200
|
+
aliases: dict[str, str] = {}
|
|
201
|
+
for each_node in ast.walk(function_node):
|
|
202
|
+
if not isinstance(each_node, ast.Assign) or len(each_node.targets) != 1:
|
|
189
203
|
continue
|
|
190
|
-
|
|
204
|
+
target_names = _assignment_target_names(each_node.targets[0])
|
|
205
|
+
if not target_names:
|
|
191
206
|
continue
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
value = each_node.value
|
|
208
|
+
all_values = value.elts if isinstance(value, ast.Tuple) else [value]
|
|
209
|
+
aliases.update(_scope_aliases_for_pair(target_names, all_values))
|
|
210
|
+
return aliases
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _scope_names_for_reference(reference: ast.AST, aliases: dict[str, str]) -> set[str]:
|
|
214
|
+
"""Return the scope-carrying attribute name one AST reference resolves to, if any."""
|
|
215
|
+
if isinstance(reference, ast.Name) and reference.id in aliases:
|
|
216
|
+
return {aliases[reference.id]}
|
|
217
|
+
if isinstance(reference, ast.Attribute) and reference.attr in _SCOPE_CARRYING_ATTRIBUTE_NAMES:
|
|
218
|
+
return {reference.attr}
|
|
219
|
+
return set()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _referenced_scope_attribute_names(node: ast.AST, aliases: dict[str, str]) -> set[str]:
|
|
223
|
+
"""Return every scope-carrying attribute name *node* references, direct or aliased."""
|
|
224
|
+
found: set[str] = set()
|
|
225
|
+
for each_reference in ast.walk(node):
|
|
226
|
+
found.update(_scope_names_for_reference(each_reference, aliases))
|
|
227
|
+
return found
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _fragment_deferred_rule_name(call_node: ast.Call, aliases: dict[str, str]) -> str | None:
|
|
231
|
+
"""Return the rule name one ``_fragment_or_deferred_check`` call selects, if scoped."""
|
|
232
|
+
argument_scope_names = {
|
|
233
|
+
each_scope_name
|
|
234
|
+
for each_argument in call_node.args[1:]
|
|
235
|
+
for each_scope_name in _referenced_scope_attribute_names(each_argument, aliases)
|
|
236
|
+
}
|
|
237
|
+
if "defer_scope_to_caller" not in argument_scope_names:
|
|
238
|
+
return None
|
|
239
|
+
if not call_node.args or not isinstance(call_node.args[0], ast.Name):
|
|
240
|
+
return None
|
|
241
|
+
return call_node.args[0].id
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _check_call_rule_name(call_node: ast.Call, aliases: dict[str, str]) -> str | None:
|
|
245
|
+
"""Return the rule name one direct ``check_*`` call selects, if it carries scope."""
|
|
246
|
+
func_name = call_node.func.id # type: ignore[attr-defined] # caller already narrowed func to ast.Name
|
|
247
|
+
if not func_name.startswith("check_"):
|
|
248
|
+
return None
|
|
249
|
+
all_call_arguments = (*call_node.args, *(each_keyword.value for each_keyword in call_node.keywords))
|
|
250
|
+
argument_scope_names = {
|
|
251
|
+
each_scope_name
|
|
252
|
+
for each_argument in all_call_arguments
|
|
253
|
+
for each_scope_name in _referenced_scope_attribute_names(each_argument, aliases)
|
|
254
|
+
}
|
|
255
|
+
return func_name if argument_scope_names else None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _rule_names_from_function(function_node: ast.FunctionDef) -> set[str]:
|
|
259
|
+
"""Return the check_* names *function_node* calls with scope-carrying arguments."""
|
|
260
|
+
aliases = _context_alias_map(function_node)
|
|
261
|
+
rule_names: set[str] = set()
|
|
262
|
+
for each_call in ast.walk(function_node):
|
|
263
|
+
if not isinstance(each_call, ast.Call) or not isinstance(each_call.func, ast.Name):
|
|
264
|
+
continue
|
|
265
|
+
if each_call.func.id == "_fragment_or_deferred_check":
|
|
266
|
+
rule_name = _fragment_deferred_rule_name(each_call, aliases)
|
|
267
|
+
else:
|
|
268
|
+
rule_name = _check_call_rule_name(each_call, aliases)
|
|
269
|
+
if rule_name is not None:
|
|
270
|
+
rule_names.add(rule_name)
|
|
271
|
+
return rule_names
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _scope_aware_rule_names_from_source() -> frozenset[str]:
|
|
275
|
+
"""Return every check_* name dispatched anywhere in the enforcer with scope.
|
|
276
|
+
|
|
277
|
+
Walks every function in the enforcer module rather than only
|
|
278
|
+
``validate_content``, since the extension-issue helper functions hold the
|
|
279
|
+
actual dispatch calls today.
|
|
280
|
+
"""
|
|
281
|
+
enforcer_module = inspect.getmodule(validate_content)
|
|
282
|
+
assert enforcer_module is not None
|
|
283
|
+
module_tree = ast.parse(inspect.getsource(enforcer_module))
|
|
284
|
+
all_rule_names: set[str] = set()
|
|
285
|
+
for each_function in ast.walk(module_tree):
|
|
286
|
+
if isinstance(each_function, ast.FunctionDef):
|
|
287
|
+
all_rule_names.update(_rule_names_from_function(each_function))
|
|
209
288
|
return frozenset(all_rule_names)
|
|
210
289
|
|
|
211
290
|
|