claude-dev-env 1.68.0 → 1.69.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/_shared/CLAUDE.md +13 -0
- package/_shared/pr-loop/CLAUDE.md +24 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +30 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +21 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +32 -0
- package/agents/CLAUDE.md +29 -0
- package/audit-rubrics/CLAUDE.md +41 -0
- package/audit-rubrics/category_rubrics/CLAUDE.md +36 -0
- package/audit-rubrics/prompts/CLAUDE.md +36 -0
- package/bin/CLAUDE.md +28 -0
- package/commands/CLAUDE.md +25 -0
- package/docs/CLAUDE.md +28 -0
- package/docs/references/CLAUDE.md +13 -0
- package/hooks/CLAUDE.md +31 -0
- package/hooks/advisory/CLAUDE.md +16 -0
- package/hooks/blocking/CLAUDE.md +107 -0
- package/hooks/blocking/code_rules_constants_config.py +7 -4
- package/hooks/blocking/code_rules_docstrings.py +97 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/config/CLAUDE.md +22 -0
- package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +262 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +36 -0
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +9 -0
- package/hooks/diagnostic/CLAUDE.md +43 -0
- package/hooks/diagnostic/migrations/CLAUDE.md +16 -0
- package/hooks/diagnostic/queries/CLAUDE.md +19 -0
- package/hooks/git-hooks/CLAUDE.md +28 -0
- package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +21 -0
- package/hooks/hooks_constants/CLAUDE.md +60 -0
- package/hooks/hooks_constants/blocking_check_limits.py +2 -0
- package/hooks/lifecycle/CLAUDE.md +18 -0
- package/hooks/observability/CLAUDE.md +16 -0
- package/hooks/session/CLAUDE.md +21 -0
- package/hooks/validation/CLAUDE.md +19 -0
- package/hooks/validators/CLAUDE.md +49 -0
- package/hooks/workflow/CLAUDE.md +22 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +46 -0
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/scripts/CLAUDE.md +34 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +14 -0
- package/scripts/sync_to_cursor/CLAUDE.md +23 -0
- package/scripts/tests/CLAUDE.md +18 -0
- package/skills/CLAUDE.md +66 -0
- package/skills/_shared/CLAUDE.md +11 -0
- package/skills/_shared/pr-loop/CLAUDE.md +27 -0
- package/skills/_shared/pr-loop/prompts/CLAUDE.md +9 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +23 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +19 -0
- package/skills/anthropic-plan/CLAUDE.md +34 -0
- package/skills/anthropic-plan/SKILL.md +1 -1
- package/skills/anthropic-plan/scripts/CLAUDE.md +11 -0
- package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +16 -0
- package/skills/anthropic-plan/templates/CLAUDE.md +13 -0
- package/skills/anthropic-plan/workflow/CLAUDE.md +14 -0
- package/skills/auditing-claude-config/CLAUDE.md +20 -0
- package/skills/autoconverge/CLAUDE.md +30 -0
- package/skills/autoconverge/reference/CLAUDE.md +12 -0
- package/skills/autoconverge/workflow/CLAUDE.md +23 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +16 -0
- package/skills/bdd-protocol/CLAUDE.md +26 -0
- package/skills/bdd-protocol/references/CLAUDE.md +10 -0
- package/skills/bg-agent/CLAUDE.md +17 -0
- package/skills/bugteam/CLAUDE.md +30 -0
- package/skills/bugteam/reference/CLAUDE.md +22 -0
- package/skills/bugteam/reference/obstacles/CLAUDE.md +24 -0
- package/skills/bugteam/scripts/CLAUDE.md +36 -0
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +20 -0
- package/skills/caveman/CLAUDE.md +15 -0
- package/skills/code/CLAUDE.md +17 -0
- package/skills/copilot-review/CLAUDE.md +17 -0
- package/skills/deep-research/CLAUDE.md +17 -0
- package/skills/doc-gist/CLAUDE.md +25 -0
- package/skills/doc-gist/references/CLAUDE.md +9 -0
- package/skills/doc-gist/references/examples/CLAUDE.md +25 -0
- package/skills/doc-gist/scripts/CLAUDE.md +27 -0
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +10 -0
- package/skills/everything-search/CLAUDE.md +17 -0
- package/skills/findbugs/CLAUDE.md +20 -0
- package/skills/fixbugs/CLAUDE.md +19 -0
- package/skills/fresh-branch/CLAUDE.md +15 -0
- package/skills/gh-paginate/CLAUDE.md +18 -0
- package/skills/gotcha/CLAUDE.md +33 -0
- package/skills/implement/CLAUDE.md +27 -0
- package/skills/implement/scripts/CLAUDE.md +22 -0
- package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +22 -0
- package/skills/logifix/CLAUDE.md +36 -0
- package/skills/logifix/scripts/CLAUDE.md +16 -0
- package/skills/monitor-open-prs/CLAUDE.md +34 -0
- package/skills/monitor-open-prs/scripts/CLAUDE.md +17 -0
- package/skills/pr-consistency-audit/CLAUDE.md +34 -0
- package/skills/pr-consistency-audit/reference/CLAUDE.md +16 -0
- package/skills/pr-converge/CLAUDE.md +29 -0
- package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +26 -0
- package/skills/pr-converge/reference/CLAUDE.md +27 -0
- package/skills/pr-converge/reference/obstacles/CLAUDE.md +23 -0
- package/skills/pr-converge/scripts/CLAUDE.md +36 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +17 -0
- package/skills/pr-converge/workflows/CLAUDE.md +16 -0
- package/skills/pr-review-responder/CLAUDE.md +35 -0
- package/skills/pre-compact/CLAUDE.md +24 -0
- package/skills/qbug/CLAUDE.md +40 -0
- package/skills/rebase/CLAUDE.md +32 -0
- package/skills/recall/CLAUDE.md +30 -0
- package/skills/refine/CLAUDE.md +44 -0
- package/skills/refine/templates/CLAUDE.md +17 -0
- package/skills/remember/CLAUDE.md +31 -0
- package/skills/research-mode/CLAUDE.md +35 -0
- package/skills/session-log/CLAUDE.md +31 -0
- package/skills/session-tidy/CLAUDE.md +36 -0
- package/skills/skill-builder/CLAUDE.md +45 -0
- package/skills/skill-builder/references/CLAUDE.md +19 -0
- package/skills/skill-builder/templates/CLAUDE.md +14 -0
- package/skills/skill-builder/workflows/CLAUDE.md +17 -0
- package/skills/structure-prompt/CLAUDE.md +42 -0
- package/skills/structure-prompt/reference/CLAUDE.md +28 -0
- package/skills/task-build/CLAUDE.md +28 -0
- package/skills/update/CLAUDE.md +38 -0
- package/skills/verified-build/CLAUDE.md +33 -0
- package/system-prompts/CLAUDE.md +17 -0
|
@@ -26,9 +26,11 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
|
|
|
26
26
|
ALL_DOCSTRING_MULTIPLE_CONDITION_JOINING_PHRASES,
|
|
27
27
|
DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT,
|
|
28
28
|
DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT,
|
|
29
|
+
MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES,
|
|
29
30
|
MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES,
|
|
30
31
|
MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES,
|
|
31
32
|
MAX_DOCSTRING_FORMAT_ISSUES,
|
|
33
|
+
MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH,
|
|
32
34
|
)
|
|
33
35
|
from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
34
36
|
ALL_DOCSTRING_ARGS_SECTION_HEADERS,
|
|
@@ -462,3 +464,98 @@ def check_docstring_fallback_branch_coverage(content: str, file_path: str) -> li
|
|
|
462
464
|
if len(issues) >= MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES:
|
|
463
465
|
break
|
|
464
466
|
return issues[:MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES]
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _class_docstring_summary_is_single_line(docstring_text: str) -> bool:
|
|
470
|
+
stripped_text = docstring_text.strip()
|
|
471
|
+
if not stripped_text:
|
|
472
|
+
return False
|
|
473
|
+
summary_line, separator, _remainder = stripped_text.partition("\n")
|
|
474
|
+
if separator and stripped_text[len(summary_line):].strip():
|
|
475
|
+
return False
|
|
476
|
+
return bool(summary_line.strip())
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _public_method_names(class_node: ast.ClassDef) -> list[str]:
|
|
480
|
+
deduplicated_names: dict[str, None] = {}
|
|
481
|
+
for each_statement in class_node.body:
|
|
482
|
+
if not isinstance(each_statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
483
|
+
continue
|
|
484
|
+
if _function_is_private_or_dunder(each_statement.name):
|
|
485
|
+
continue
|
|
486
|
+
deduplicated_names[each_statement.name] = None
|
|
487
|
+
return list(deduplicated_names)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _name_tokens(method_name: str) -> list[str]:
|
|
491
|
+
return [each_token for each_token in method_name.split("_") if each_token]
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _docstring_mentions_method(docstring_text: str, method_name: str) -> bool:
|
|
495
|
+
lowered_docstring = docstring_text.lower()
|
|
496
|
+
if method_name.lower() in lowered_docstring:
|
|
497
|
+
return True
|
|
498
|
+
return all(
|
|
499
|
+
each_token.lower() in lowered_docstring for each_token in _name_tokens(method_name)
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _unmentioned_public_methods(class_node: ast.ClassDef, docstring_text: str) -> list[str]:
|
|
504
|
+
return [
|
|
505
|
+
each_name
|
|
506
|
+
for each_name in _public_method_names(class_node)
|
|
507
|
+
if not _docstring_mentions_method(docstring_text, each_name)
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def check_class_docstring_names_public_methods(
|
|
512
|
+
content: str, file_path: str
|
|
513
|
+
) -> list[str]:
|
|
514
|
+
"""Flag a one-line class docstring that omits two or more public methods.
|
|
515
|
+
|
|
516
|
+
A class whose docstring is a single summary line names one responsibility,
|
|
517
|
+
so a reader trusts that line to describe the whole class. When the class
|
|
518
|
+
later gains a second public entry point — the drift pattern where a
|
|
519
|
+
coffee-break reporter grows a regular-pace method — the terse summary keeps
|
|
520
|
+
describing only the original feature. Each public method whose name (or all
|
|
521
|
+
of its underscore-separated tokens) appears nowhere in the summary counts as
|
|
522
|
+
omitted; a class with two or more omitted public methods is reported so the
|
|
523
|
+
summary is widened to name the broader surface. Classes with a multi-line
|
|
524
|
+
docstring body are left to the audit lane, since their prose can carry the
|
|
525
|
+
enumeration without naming each method by name.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
content: The source text to inspect.
|
|
529
|
+
file_path: The path the source will be written to, used for exemptions.
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
One issue per class whose single-line docstring omits two or more of its
|
|
533
|
+
public methods, capped at the module limit.
|
|
534
|
+
"""
|
|
535
|
+
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
536
|
+
return []
|
|
537
|
+
try:
|
|
538
|
+
parsed_tree = ast.parse(content)
|
|
539
|
+
except SyntaxError:
|
|
540
|
+
return []
|
|
541
|
+
issues: list[str] = []
|
|
542
|
+
for each_node in _walk_skipping_type_checking_blocks(parsed_tree):
|
|
543
|
+
if not isinstance(each_node, ast.ClassDef):
|
|
544
|
+
continue
|
|
545
|
+
class_docstring = ast.get_docstring(each_node) or ""
|
|
546
|
+
if not _class_docstring_summary_is_single_line(class_docstring):
|
|
547
|
+
continue
|
|
548
|
+
public_names = _public_method_names(each_node)
|
|
549
|
+
if len(public_names) < MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH:
|
|
550
|
+
continue
|
|
551
|
+
unmentioned_names = _unmentioned_public_methods(each_node, class_docstring)
|
|
552
|
+
if len(unmentioned_names) < MINIMUM_PUBLIC_METHODS_FOR_CLASS_DOCSTRING_BREADTH:
|
|
553
|
+
continue
|
|
554
|
+
issues.append(
|
|
555
|
+
f"Line {each_node.lineno}: {each_node.name} one-line docstring omits "
|
|
556
|
+
f"public method(s) {', '.join(unmentioned_names)} — widen the summary "
|
|
557
|
+
"so it names the class's full public surface"
|
|
558
|
+
)
|
|
559
|
+
if len(issues) >= MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES:
|
|
560
|
+
break
|
|
561
|
+
return issues[:MAX_CLASS_DOCSTRING_PUBLIC_METHOD_ISSUES]
|
|
@@ -65,6 +65,7 @@ from code_rules_dead_module_constant import ( # noqa: E402
|
|
|
65
65
|
check_dead_module_constants,
|
|
66
66
|
)
|
|
67
67
|
from code_rules_docstrings import ( # noqa: E402
|
|
68
|
+
check_class_docstring_names_public_methods,
|
|
68
69
|
check_docstring_args_match_signature,
|
|
69
70
|
check_docstring_fallback_branch_coverage,
|
|
70
71
|
check_docstring_format,
|
|
@@ -250,6 +251,9 @@ def validate_content(
|
|
|
250
251
|
all_issues.extend(check_docstring_format(effective_content, file_path))
|
|
251
252
|
all_issues.extend(check_docstring_args_match_signature(effective_content, file_path))
|
|
252
253
|
all_issues.extend(check_docstring_fallback_branch_coverage(effective_content, file_path))
|
|
254
|
+
all_issues.extend(
|
|
255
|
+
check_class_docstring_names_public_methods(effective_content, file_path)
|
|
256
|
+
)
|
|
253
257
|
all_issues.extend(
|
|
254
258
|
check_boolean_naming(
|
|
255
259
|
effective_content,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# hooks/blocking/config
|
|
2
|
+
|
|
3
|
+
A Python package that holds shared constants for the verified-commit gate family. Three modules in `blocking/` import from here:
|
|
4
|
+
|
|
5
|
+
- `verification_verdict_store.py`
|
|
6
|
+
- `verified_commit_gate.py`
|
|
7
|
+
- `verifier_verdict_minter.py`
|
|
8
|
+
|
|
9
|
+
## Key files
|
|
10
|
+
|
|
11
|
+
| File | Contents |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
|
|
14
|
+
| `verified_commit_constants.py` | All tunables for the gate: directory names, regex patterns for detecting verdict paths and obfuscation attempts, timeout values, git subcommand sets, bypass marker, and corrective messages |
|
|
15
|
+
|
|
16
|
+
## Key constants in `verified_commit_constants.py`
|
|
17
|
+
|
|
18
|
+
- `VERIFICATION_BYPASS_MARKER` — the `# verify-skip` comment that exempts a single commit/push from the gate
|
|
19
|
+
- `MINTING_AGENT_TYPE` — `"code-verifier"`, the agent type whose SubagentStop hook mints verdicts
|
|
20
|
+
- `VERDICT_DIRECTORY_NAME` — `"verification"`, the directory under `~/.claude/` that holds verdict JSON files
|
|
21
|
+
- `DOCS_ONLY_EXTENSIONS` — extensions (`.md`, `.txt`, images) whose changes are mechanically exempt from the gate
|
|
22
|
+
- `CORRECTIVE_MESSAGE` / `VERDICT_DIRECTORY_GUARD_MESSAGE` — user-facing block messages
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Tests for check_class_docstring_names_public_methods — class prose breadth.
|
|
2
|
+
|
|
3
|
+
A class whose docstring is a single summary line names one responsibility. When
|
|
4
|
+
the class exposes a second public entry point the summary never names, the prose
|
|
5
|
+
under-describes the class — the same drift the os_update_workflow break reporter
|
|
6
|
+
hit when it grew a regular-pace method beside its coffee-break method.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import ModuleType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_enforcer_module() -> ModuleType:
|
|
17
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
18
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
19
|
+
assert spec is not None
|
|
20
|
+
assert spec.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(module)
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_class_docstring_names_public_methods(content: str, file_path: str) -> list[str]:
|
|
30
|
+
return code_rules_enforcer.check_class_docstring_names_public_methods(content, file_path)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
34
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
PRODUCTION_FILE_PATH = "/project/src/break_reporter.py"
|
|
38
|
+
TEST_FILE_PATH = "/project/src/test_break_reporter.py"
|
|
39
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _narrow_class_with_widened_surface() -> str:
|
|
43
|
+
return (
|
|
44
|
+
"class ConsoleBreakReporter:\n"
|
|
45
|
+
' """Run a coffee break with operator visibility: announce, then count down."""\n'
|
|
46
|
+
"\n"
|
|
47
|
+
" async def pause_then_resume(self, seconds: float) -> None:\n"
|
|
48
|
+
" await self._sleep(seconds)\n"
|
|
49
|
+
"\n"
|
|
50
|
+
" async def stretch_then_resume(self, seconds: float) -> None:\n"
|
|
51
|
+
" await self._sleep(seconds)\n"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_should_flag_single_line_docstring_omitting_two_public_methods() -> None:
|
|
56
|
+
issues = check_class_docstring_names_public_methods(
|
|
57
|
+
_narrow_class_with_widened_surface(), PRODUCTION_FILE_PATH
|
|
58
|
+
)
|
|
59
|
+
assert any("pause_then_resume" in each for each in issues), (
|
|
60
|
+
f"Expected omitted-method flag, got: {issues!r}"
|
|
61
|
+
)
|
|
62
|
+
assert any("stretch_then_resume" in each for each in issues)
|
|
63
|
+
assert len(issues) == 1
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_should_not_flag_when_summary_names_every_public_method() -> None:
|
|
67
|
+
source = (
|
|
68
|
+
"class ConsoleBreakReporter:\n"
|
|
69
|
+
' """Announce a pause then resume, or stretch then resume, with a countdown."""\n'
|
|
70
|
+
"\n"
|
|
71
|
+
" async def pause_then_resume(self, seconds: float) -> None:\n"
|
|
72
|
+
" await self._sleep(seconds)\n"
|
|
73
|
+
"\n"
|
|
74
|
+
" async def stretch_then_resume(self, seconds: float) -> None:\n"
|
|
75
|
+
" await self._sleep(seconds)\n"
|
|
76
|
+
)
|
|
77
|
+
issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
|
|
78
|
+
assert issues == [], f"Summary naming every method must not flag, got: {issues!r}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_should_not_flag_when_only_one_public_method_is_omitted() -> None:
|
|
82
|
+
source = (
|
|
83
|
+
"class ConsoleBreakReporter:\n"
|
|
84
|
+
' """Pause then resume the submission run with an operator countdown."""\n'
|
|
85
|
+
"\n"
|
|
86
|
+
" async def pause_then_resume(self, seconds: float) -> None:\n"
|
|
87
|
+
" await self._sleep(seconds)\n"
|
|
88
|
+
"\n"
|
|
89
|
+
" async def stretch_then_resume(self, seconds: float) -> None:\n"
|
|
90
|
+
" await self._sleep(seconds)\n"
|
|
91
|
+
)
|
|
92
|
+
issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
|
|
93
|
+
assert issues == [], f"A single omitted method must not flag, got: {issues!r}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_should_not_flag_multi_line_docstring_body() -> None:
|
|
97
|
+
source = (
|
|
98
|
+
"class ConsoleBreakReporter:\n"
|
|
99
|
+
' """Run a coffee break with operator visibility.\n'
|
|
100
|
+
"\n"
|
|
101
|
+
" Also paces the regular between-theme waits through the same seam.\n"
|
|
102
|
+
' """\n'
|
|
103
|
+
"\n"
|
|
104
|
+
" async def pause_then_resume(self, seconds: float) -> None:\n"
|
|
105
|
+
" await self._sleep(seconds)\n"
|
|
106
|
+
"\n"
|
|
107
|
+
" async def stretch_then_resume(self, seconds: float) -> None:\n"
|
|
108
|
+
" await self._sleep(seconds)\n"
|
|
109
|
+
)
|
|
110
|
+
issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
|
|
111
|
+
assert issues == [], f"Multi-line docstrings go to the audit lane, got: {issues!r}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_should_not_flag_class_with_single_public_method() -> None:
|
|
115
|
+
source = (
|
|
116
|
+
"class ConsoleBreakReporter:\n"
|
|
117
|
+
' """Run a coffee break with operator visibility."""\n'
|
|
118
|
+
"\n"
|
|
119
|
+
" async def pause_then_resume(self, seconds: float) -> None:\n"
|
|
120
|
+
" await self._sleep(seconds)\n"
|
|
121
|
+
)
|
|
122
|
+
issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
|
|
123
|
+
assert issues == [], f"A one-method class must not flag, got: {issues!r}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_should_skip_private_methods_when_counting_surface() -> None:
|
|
127
|
+
source = (
|
|
128
|
+
"class ConsoleBreakReporter:\n"
|
|
129
|
+
' """Run a coffee break with operator visibility."""\n'
|
|
130
|
+
"\n"
|
|
131
|
+
" async def pause_then_resume(self, seconds: float) -> None:\n"
|
|
132
|
+
" await self._sleep(seconds)\n"
|
|
133
|
+
"\n"
|
|
134
|
+
" async def _sleep(self, seconds: float) -> None:\n"
|
|
135
|
+
" await self._clock.sleep(seconds)\n"
|
|
136
|
+
"\n"
|
|
137
|
+
" def __init__(self) -> None:\n"
|
|
138
|
+
" self._clock = None\n"
|
|
139
|
+
)
|
|
140
|
+
issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
|
|
141
|
+
assert issues == [], f"Private and dunder methods are not public surface, got: {issues!r}"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_should_skip_class_without_docstring() -> None:
|
|
145
|
+
source = (
|
|
146
|
+
"class ConsoleBreakReporter:\n"
|
|
147
|
+
" async def pause_then_resume(self, seconds: float) -> None:\n"
|
|
148
|
+
" await self._sleep(seconds)\n"
|
|
149
|
+
"\n"
|
|
150
|
+
" async def stretch_then_resume(self, seconds: float) -> None:\n"
|
|
151
|
+
" await self._sleep(seconds)\n"
|
|
152
|
+
)
|
|
153
|
+
issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
|
|
154
|
+
assert issues == [], f"No-docstring classes are out of scope, got: {issues!r}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_should_skip_test_file() -> None:
|
|
158
|
+
issues = check_class_docstring_names_public_methods(
|
|
159
|
+
_narrow_class_with_widened_surface(), TEST_FILE_PATH
|
|
160
|
+
)
|
|
161
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
165
|
+
issues = check_class_docstring_names_public_methods(
|
|
166
|
+
_narrow_class_with_widened_surface(), HOOK_INFRASTRUCTURE_PATH
|
|
167
|
+
)
|
|
168
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
172
|
+
issues = check_class_docstring_names_public_methods("class Broken(\n", PRODUCTION_FILE_PATH)
|
|
173
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _real_break_reporter_drift() -> str:
|
|
177
|
+
return (
|
|
178
|
+
"class ConsoleBreakReporter:\n"
|
|
179
|
+
' """Run a coffee-break with operator visibility: announce, then count down."""\n'
|
|
180
|
+
"\n"
|
|
181
|
+
" async def announce_and_pause(self, nominal_break_seconds: float) -> None:\n"
|
|
182
|
+
" await self._announce(nominal_break_seconds)\n"
|
|
183
|
+
"\n"
|
|
184
|
+
" async def announce_and_pause_exact(self, break_seconds: float) -> None:\n"
|
|
185
|
+
" await self._announce(break_seconds)\n"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_should_flag_real_break_reporter_widened_surface() -> None:
|
|
190
|
+
issues = check_class_docstring_names_public_methods(
|
|
191
|
+
_real_break_reporter_drift(), PRODUCTION_FILE_PATH
|
|
192
|
+
)
|
|
193
|
+
assert any("announce_and_pause_exact" in each for each in issues), (
|
|
194
|
+
f"Expected the regular-pace method to flag, got: {issues!r}"
|
|
195
|
+
)
|
|
196
|
+
assert any("announce_and_pause" in each for each in issues)
|
|
197
|
+
assert len(issues) == 1
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _overload_class_omitting_the_only_method() -> str:
|
|
201
|
+
return (
|
|
202
|
+
"from typing import overload\n"
|
|
203
|
+
"\n"
|
|
204
|
+
"class PayloadTransformer:\n"
|
|
205
|
+
' """Hold a fixed payload value for later use."""\n'
|
|
206
|
+
"\n"
|
|
207
|
+
" @overload\n"
|
|
208
|
+
" def transform(self, payload: str) -> dict[str, str]: ...\n"
|
|
209
|
+
"\n"
|
|
210
|
+
" @overload\n"
|
|
211
|
+
" def transform(self, payload: bytes) -> dict[str, str]: ...\n"
|
|
212
|
+
"\n"
|
|
213
|
+
" def transform(self, payload: str | bytes) -> dict[str, str]:\n"
|
|
214
|
+
" return self._normalize(payload)\n"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_should_not_flag_single_overloaded_method_below_breadth_threshold() -> None:
|
|
219
|
+
issues = check_class_docstring_names_public_methods(
|
|
220
|
+
_overload_class_omitting_the_only_method(), PRODUCTION_FILE_PATH
|
|
221
|
+
)
|
|
222
|
+
assert issues == [], f"One overloaded public method must not flag, got: {issues!r}"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_should_not_repeat_an_overloaded_name_in_the_issue_message() -> None:
|
|
226
|
+
source = (
|
|
227
|
+
"from typing import overload\n"
|
|
228
|
+
"\n"
|
|
229
|
+
"class PayloadTransformer:\n"
|
|
230
|
+
' """Hold a fixed payload value for later use."""\n'
|
|
231
|
+
"\n"
|
|
232
|
+
" @overload\n"
|
|
233
|
+
" def transform(self, payload: str) -> dict[str, str]: ...\n"
|
|
234
|
+
"\n"
|
|
235
|
+
" @overload\n"
|
|
236
|
+
" def transform(self, payload: bytes) -> dict[str, str]: ...\n"
|
|
237
|
+
"\n"
|
|
238
|
+
" def transform(self, payload: str | bytes) -> dict[str, str]:\n"
|
|
239
|
+
" return self._normalize(payload)\n"
|
|
240
|
+
"\n"
|
|
241
|
+
" def reset(self) -> None:\n"
|
|
242
|
+
" self._payload = None\n"
|
|
243
|
+
)
|
|
244
|
+
issues = check_class_docstring_names_public_methods(source, PRODUCTION_FILE_PATH)
|
|
245
|
+
assert len(issues) == 1, f"Expected a single drift issue, got: {issues!r}"
|
|
246
|
+
only_issue = issues[0]
|
|
247
|
+
assert only_issue.count("transform") == 1, (
|
|
248
|
+
f"Overloaded name must appear once in the message, got: {only_issue!r}"
|
|
249
|
+
)
|
|
250
|
+
assert "reset" in only_issue
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_validate_content_surfaces_class_docstring_breadth_drift() -> None:
|
|
254
|
+
issues = validate_content(
|
|
255
|
+
_narrow_class_with_widened_surface(), PRODUCTION_FILE_PATH, old_content=""
|
|
256
|
+
)
|
|
257
|
+
matching_issues = [
|
|
258
|
+
each for each in issues if "pause_then_resume" in each and "public method" in each
|
|
259
|
+
]
|
|
260
|
+
assert matching_issues, (
|
|
261
|
+
f"Expected validate_content to surface the class-breadth drift, got: {issues!r}"
|
|
262
|
+
)
|
|
@@ -56,6 +56,12 @@ def _check(source: str, file_path: str) -> list[str]:
|
|
|
56
56
|
return code_rules_enforcer.check_dead_module_constants(source, file_path)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def _check_edit(fragment: str, full_file_content: str, file_path: str) -> list[str]:
|
|
60
|
+
return code_rules_enforcer.check_dead_module_constants(
|
|
61
|
+
fragment, file_path, full_file_content
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
59
65
|
def _build_constants_package(
|
|
60
66
|
workflow_directory: Path,
|
|
61
67
|
constants_body: str,
|
|
@@ -94,6 +100,36 @@ def test_flags_constant_imported_by_no_module_in_the_tree(neutral_root: Path) ->
|
|
|
94
100
|
), f"Imported constants must not be flagged, got: {issues}"
|
|
95
101
|
|
|
96
102
|
|
|
103
|
+
def test_flags_a_dead_constant_added_by_an_edit_to_an_existing_module(
|
|
104
|
+
neutral_root: Path,
|
|
105
|
+
) -> None:
|
|
106
|
+
consumer_body = (
|
|
107
|
+
"from report_constants.render_report_constants import (\n"
|
|
108
|
+
" MEDIUM_CODE,\n"
|
|
109
|
+
" MEDIUM_TERMINAL,\n"
|
|
110
|
+
")\n"
|
|
111
|
+
"\n"
|
|
112
|
+
"def panel_class(medium: str) -> str:\n"
|
|
113
|
+
" if medium == MEDIUM_TERMINAL:\n"
|
|
114
|
+
" return 'terminal'\n"
|
|
115
|
+
" return 'code-panel' if medium == MEDIUM_CODE else 'text-panel'\n"
|
|
116
|
+
)
|
|
117
|
+
prior_body = 'MEDIUM_TERMINAL = "terminal"\nMEDIUM_CODE = "code"\n'
|
|
118
|
+
constants_path = _build_constants_package(
|
|
119
|
+
neutral_root / "workflow", prior_body, consumer_body
|
|
120
|
+
)
|
|
121
|
+
edit_fragment = 'MEDIUM_CODE = "code"\nMEDIUM_TEXT = "text"\n'
|
|
122
|
+
post_edit_body = prior_body + 'MEDIUM_TEXT = "text"\n'
|
|
123
|
+
issues = _check_edit(edit_fragment, post_edit_body, str(constants_path))
|
|
124
|
+
assert any("MEDIUM_TEXT" in each_issue for each_issue in issues), (
|
|
125
|
+
f"An Edit that inserts a dead constant must be flagged, got: {issues}"
|
|
126
|
+
)
|
|
127
|
+
assert not any(
|
|
128
|
+
"MEDIUM_TERMINAL" in each_issue or "MEDIUM_CODE" in each_issue
|
|
129
|
+
for each_issue in issues
|
|
130
|
+
), f"Imported constants must not be flagged on an edit, got: {issues}"
|
|
131
|
+
|
|
132
|
+
|
|
97
133
|
def test_does_not_flag_constant_imported_one_directory_up(neutral_root: Path) -> None:
|
|
98
134
|
consumer_uses_text = (
|
|
99
135
|
"from report_constants.render_report_constants import (\n"
|
|
@@ -32,6 +32,7 @@ TYPESCRIPT_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example.ts"
|
|
|
32
32
|
TOP_LEVEL_CONFIG_FILE_PATH = "config/timing.py"
|
|
33
33
|
NESTED_CONFIG_FILE_PATH = "packages/claude-dev-env/hooks/config/example_constants.py"
|
|
34
34
|
BACKSLASH_CONFIG_FILE_PATH = "packages\\claude-dev-env\\hooks\\config\\example_constants.py"
|
|
35
|
+
WORKFLOW_REGISTRY_FILE_PATH = "packages/claude-dev-env/hooks/blocking/workflow/app_info/states.py"
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def test_should_flag_constant_used_by_only_one_function() -> None:
|
|
@@ -114,6 +115,14 @@ def test_should_exempt_test_files() -> None:
|
|
|
114
115
|
assert issues == [], f"Expected test file exemption, got: {issues}"
|
|
115
116
|
|
|
116
117
|
|
|
118
|
+
def test_should_exempt_workflow_registry_files() -> None:
|
|
119
|
+
source = "UPPER = 1\n\ndef lonely_caller():\n return UPPER\n"
|
|
120
|
+
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
121
|
+
source, WORKFLOW_REGISTRY_FILE_PATH
|
|
122
|
+
)
|
|
123
|
+
assert issues == [], f"Expected workflow-registry file exemption, got: {issues}"
|
|
124
|
+
|
|
125
|
+
|
|
117
126
|
def test_should_flag_constant_used_only_in_decorator_of_one_function() -> None:
|
|
118
127
|
source = (
|
|
119
128
|
"TIMEOUT = 5.0\n"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# hooks/diagnostic
|
|
2
|
+
|
|
3
|
+
Hooks and scripts that collect, store, and query hook-firing records. The pipeline reads session JSONL transcripts, extracts hook attachment records, and writes them as rows into a Neon (Postgres) `hook_events` table.
|
|
4
|
+
|
|
5
|
+
## Subdirectories
|
|
6
|
+
|
|
7
|
+
| Directory | Role |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `migrations/` | SQL migration files for the `hook_events` schema |
|
|
10
|
+
| `queries/` | Parameterized SQL queries for inspecting blocked commands |
|
|
11
|
+
|
|
12
|
+
## Key files
|
|
13
|
+
|
|
14
|
+
| File | What it does |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `hook_log_init.py` | One-time setup: creates the Neon schema (runs `schema.sql`), then verifies read-write parity with a sentinel round-trip |
|
|
17
|
+
| `hook_log_extractor.py` | Stop hook — reads per-session JSONL transcripts and ingests new `hook_*` attachment records into the `hook_events` table; idempotent via a UNIQUE constraint on `(source_jsonl_path, source_line_number)` |
|
|
18
|
+
| `hook_log_stop_wrapper.py` | Thin wrapper that invokes `hook_log_extractor.py` from the Stop lifecycle event |
|
|
19
|
+
| `schema.sql` | DDL for the `hook_events` table, `blocked_commands` view, and supporting indexes |
|
|
20
|
+
| `requirements-hook-logs.txt` | Runtime dependencies (`psycopg`) for the extractor |
|
|
21
|
+
| `requirements-hook-logs-dev.txt` | Dev/test dependencies |
|
|
22
|
+
| `test_hook_log_extractor.py` | Tests for the extractor |
|
|
23
|
+
| `test_hook_log_init.py` | Tests for the schema-init script |
|
|
24
|
+
| `test_hook_log_stop_wrapper.py` | Tests for the Stop wrapper |
|
|
25
|
+
|
|
26
|
+
## Schema overview (`schema.sql`)
|
|
27
|
+
|
|
28
|
+
The `hook_events` table captures one row per hook firing:
|
|
29
|
+
|
|
30
|
+
- `hook_event`, `hook_name`, `hook_category` — what fired
|
|
31
|
+
- `outcome` — `allowed`, `blocked`, or `ask`
|
|
32
|
+
- `tool_name`, `command_excerpt` — what tool was called
|
|
33
|
+
- `session_id`, `git_branch`, `cwd` — context
|
|
34
|
+
- `duration_ms`, `exit_code` — timing and result
|
|
35
|
+
- `source_jsonl_path`, `source_line_number` — idempotency key
|
|
36
|
+
|
|
37
|
+
The `blocked_commands` view filters to `outcome = 'blocked'`.
|
|
38
|
+
|
|
39
|
+
## Conventions
|
|
40
|
+
|
|
41
|
+
- The extractor exits 0 even when Neon is unreachable (offline-graceful); it logs to `OFFLINE_WARNING_LOG` and does not block session end.
|
|
42
|
+
- Constants for the extractor (table name, offset state file, timeout) live in `hooks_constants/hook_log_extractor_constants.py`.
|
|
43
|
+
- Tests run with `python -m pytest diagnostic/test_hook_log_*.py`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# hooks/diagnostic/migrations
|
|
2
|
+
|
|
3
|
+
SQL migration files for the `hook_events` Neon schema. Each file applies a schema change to the `hook_events` table or its indexes.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | What it does |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `2026-04-25-drop-themes-hook-events.sql` | Drops the `themes` hook-events table variant |
|
|
10
|
+
| `README.md` | Notes on the migration approach |
|
|
11
|
+
|
|
12
|
+
## Conventions
|
|
13
|
+
|
|
14
|
+
- Run migrations manually against the Neon database using `psql` or the Neon console.
|
|
15
|
+
- The baseline schema lives in `diagnostic/schema.sql`.
|
|
16
|
+
- File names follow `YYYY-MM-DD-<description>.sql` for chronological ordering.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# hooks/diagnostic/queries
|
|
2
|
+
|
|
3
|
+
Parameterized SQL queries for inspecting the `hook_events` Neon table. Run these directly against the Neon database to analyze hook-firing patterns.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | What it returns |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `block_details_for_hook.sql` | Full details for all blocked events matching a given hook name |
|
|
10
|
+
| `blocks_by_category.sql` | Count of blocks grouped by hook category |
|
|
11
|
+
| `blocks_by_tool.sql` | Count of blocks grouped by tool name |
|
|
12
|
+
| `blocks_last_7_days.sql` | All blocked events from the last 7 days |
|
|
13
|
+
| `top_blockers_last_24_hours.sql` | Hook names with the most blocks in the last 24 hours |
|
|
14
|
+
| `top_blockers_overall.sql` | Hook names with the most blocks across all time |
|
|
15
|
+
|
|
16
|
+
## Conventions
|
|
17
|
+
|
|
18
|
+
- Queries target the `hook_events` table and `blocked_commands` view defined in `diagnostic/schema.sql`.
|
|
19
|
+
- Run with `psql $DATABASE_URL -f <query>.sql` or paste into the Neon console.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# hooks/git-hooks
|
|
2
|
+
|
|
3
|
+
Native git hooks that run outside the Claude Code lifecycle — invoked directly by git at commit and push time. The installer copies these scripts into the user's shared git-hooks directory (`core.hooksPath`).
|
|
4
|
+
|
|
5
|
+
## Key files
|
|
6
|
+
|
|
7
|
+
| File | Git hook | What it does |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `pre_commit.py` | `pre-commit` | Runs the CODE_RULES gate (`precommit_code_rules_gate.py`) over staged changes; exits 1 when any staged file has a blocking violation |
|
|
10
|
+
| `pre_push.py` | `pre-push` | Runs the verified-commit gate check before a push reaches the remote |
|
|
11
|
+
| `post_commit.py` | `post-commit` | Runs after a commit lands; performs any post-commit bookkeeping |
|
|
12
|
+
| `gate_utils.py` | — | Shared helpers: resolves the gate script path, checks that the path is a safe regular file |
|
|
13
|
+
| `test_config.py` | — | Test configuration helpers |
|
|
14
|
+
| `test_gate_utils.py` | — | Tests for `gate_utils.py` |
|
|
15
|
+
| `test_pre_commit.py` | — | Tests for `pre_commit.py` |
|
|
16
|
+
| `test_pre_push.py` | — | Tests for `pre_push.py` |
|
|
17
|
+
|
|
18
|
+
## Subdirectory
|
|
19
|
+
|
|
20
|
+
| Directory | Role |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `git_hooks_constants/` | Shared constants imported by the git-hook scripts |
|
|
23
|
+
|
|
24
|
+
## Conventions
|
|
25
|
+
|
|
26
|
+
- The installer strips the `_` and `.py` suffix when copying into the live git-hooks path (e.g. `pre_commit.py` becomes `pre-commit`).
|
|
27
|
+
- Constants (exit codes, argument names, error messages) live in `git_hooks_constants/` and are imported at the top of each script.
|
|
28
|
+
- Run tests with `python -m pytest git-hooks/test_<name>.py`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# hooks/git-hooks/git_hooks_constants
|
|
2
|
+
|
|
3
|
+
Shared constants imported by the git-hook scripts in `git-hooks/`. Centralizes exit codes, argument names, and error messages so every tunable lives in one place.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Contents |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `__init__.py` | Exports all constants; marks this as a package so `from git_hooks_constants import ...` resolves |
|
|
10
|
+
|
|
11
|
+
## Key constants (defined in `__init__.py`)
|
|
12
|
+
|
|
13
|
+
- `GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE` — exit code when the gate script cannot be found or launched
|
|
14
|
+
- `GATE_SCRIPT_NOT_FOUND_MESSAGE` — error message when the gate script path does not exist
|
|
15
|
+
- `INVOKE_GATE_FAILURE_MESSAGE` — error message when the gate subprocess fails to start
|
|
16
|
+
- `STAGED_SCOPE_ARGUMENT` — CLI argument passed to the gate script to scope it to staged changes
|
|
17
|
+
|
|
18
|
+
## Conventions
|
|
19
|
+
|
|
20
|
+
- Import with `from git_hooks_constants import <CONSTANT>` from within the `git-hooks/` directory.
|
|
21
|
+
- Add new constants here rather than inline in the hook scripts.
|