claude-dev-env 1.83.0 → 1.85.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/pr-loop/audit-contract.md +24 -12
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -4
- package/agents/clean-coder.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +16 -8
- package/bin/install.test.mjs +17 -6
- package/commands/CLAUDE.md +0 -1
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +0 -4
- package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
- package/hooks/blocking/code_rules_constants_config.py +5 -0
- package/hooks/blocking/code_rules_docstrings.py +133 -19
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_imports_logging.py +10 -6
- package/hooks/blocking/code_rules_magic_values.py +5 -0
- package/hooks/blocking/code_rules_naming_collection.py +15 -8
- package/hooks/blocking/code_rules_paired_test.py +3 -2
- package/hooks/blocking/code_rules_shared.py +34 -0
- package/hooks/blocking/code_rules_string_magic.py +1 -1
- package/hooks/blocking/code_rules_unused_imports.py +2 -2
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +24 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
- package/hooks/blocking/test_code_rules_magic_values.py +54 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
- package/hooks/hooks_constants/CLAUDE.md +0 -3
- package/hooks/hooks_constants/blocking_check_limits.py +4 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
- package/hooks/validation/post_tool_use_dispatcher.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
- package/hooks/workflow/CLAUDE.md +2 -8
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -1
- package/rules/docstring-prose-matches-implementation.md +1 -1
- package/rules/env-var-table-code-drift.md +1 -1
- package/rules/package-inventory-stale-entry.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/plain-illustrative-docstrings.md +35 -1
- package/skills/CLAUDE.md +6 -1
- package/skills/_shared/pr-loop/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
- package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
- package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
- package/skills/autoconverge/SKILL.md +82 -58
- package/skills/autoconverge/reference/closing-report.md +6 -6
- package/skills/autoconverge/reference/convergence.md +17 -15
- package/skills/autoconverge/reference/gotchas.md +6 -3
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
- package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
- package/skills/autoconverge/workflow/converge.mjs +71 -74
- package/skills/autoconverge/workflow/render_report.py +7 -11
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +25 -72
- package/skills/bugteam/reference/CLAUDE.md +1 -3
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -7
- package/skills/bugteam/scripts/CLAUDE.md +0 -6
- package/skills/bugteam/scripts/README.md +0 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
- package/skills/code/SKILL.md +2 -0
- package/skills/copilot-review/CLAUDE.md +1 -1
- package/skills/copilot-review/SKILL.md +25 -23
- package/skills/findbugs/CLAUDE.md +2 -2
- package/skills/findbugs/SKILL.md +22 -83
- package/skills/fixbugs/SKILL.md +2 -4
- package/skills/log-audit/CLAUDE.md +20 -0
- package/skills/log-audit/SKILL.md +68 -0
- package/skills/log-audit/reference/CLAUDE.md +9 -0
- package/skills/log-audit/reference/charter.md +52 -0
- package/skills/log-audit/scripts/CLAUDE.md +27 -0
- package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
- package/skills/log-audit/scripts/collect_log_window.py +199 -0
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
- package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
- package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
- package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
- package/skills/monitor-open-prs/SKILL.md +2 -2
- package/skills/post-audit-findings/SKILL.md +84 -0
- package/skills/pr-converge/CLAUDE.md +2 -0
- package/skills/pr-converge/SKILL.md +99 -60
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +16 -19
- package/skills/pr-converge/reference/examples.md +5 -5
- package/skills/pr-converge/reference/fix-protocol.md +16 -43
- package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +24 -45
- package/skills/pr-converge/reference/state-schema.md +27 -0
- package/skills/pr-converge/scripts/README.md +3 -5
- package/skills/pr-fix-protocol/SKILL.md +70 -0
- package/skills/pr-loop-lifecycle/SKILL.md +73 -0
- package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
- package/skills/pr-scope-resolve/SKILL.md +48 -0
- package/skills/qbug/CLAUDE.md +4 -4
- package/skills/qbug/SKILL.md +46 -144
- package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
- package/skills/reviewer-gates/SKILL.md +96 -0
- package/skills/session-log/CLAUDE.md +7 -7
- package/skills/session-log/SKILL.md +27 -44
- package/skills/test_markdown_link_integrity.py +103 -0
- package/commands/doc-gist.md +0 -16
- package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
- package/hooks/blocking/conftest.py +0 -30
- package/hooks/blocking/md_path_exemptions.py +0 -224
- package/hooks/blocking/md_to_html_blocker.py +0 -155
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
- package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
- package/hooks/hooks_constants/html_companion_constants.py +0 -20
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
- package/hooks/workflow/doc_gist_auto_publish.py +0 -144
- package/hooks/workflow/md_to_html_companion.py +0 -358
- package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
- package/hooks/workflow/test_md_to_html_companion.py +0 -613
- package/skills/bugteam/reference/audit-contract.md +0 -163
- package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
- package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
- package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
- package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
- package/skills/doc-gist/CLAUDE.md +0 -25
- package/skills/doc-gist/SKILL.md +0 -97
- package/skills/doc-gist/references/CLAUDE.md +0 -9
- package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
- package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
- package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
- package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
- package/skills/doc-gist/references/examples/05-design-system.html +0 -629
- package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
- package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
- package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
- package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
- package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
- package/skills/doc-gist/references/examples/11-status-report.html +0 -528
- package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
- package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
- package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
- package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
- package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
- package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
- package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
- package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
- package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
- package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
- package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
- package/skills/doc-gist/references/examples/README.md +0 -5
- package/skills/doc-gist/scripts/CLAUDE.md +0 -27
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
- package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
- package/skills/doc-gist/scripts/gist_upload.py +0 -177
- package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
- /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Behavioural checks for the show-don't-tell narrative gate.
|
|
2
|
+
|
|
3
|
+
A readable summary earns its place by painting a concrete scene: a sample
|
|
4
|
+
input, an annotated outcome, an OK/FLAG contrast a reader pictures at once. A
|
|
5
|
+
summary that runs many short sentences with no worked example tells without
|
|
6
|
+
showing. The gate flags that wall in a top-level, class, or callable summary,
|
|
7
|
+
and stays quiet the moment the narrative carries a '::' listing or a '>>>'
|
|
8
|
+
doctest.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from types import ModuleType
|
|
17
|
+
|
|
18
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
19
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
20
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_enforcer_module() -> ModuleType:
|
|
24
|
+
enforcer_module_name = "code_rules_enforcer"
|
|
25
|
+
enforcer_path = Path(__file__).parent / (enforcer_module_name + ".py")
|
|
26
|
+
spec = importlib.util.spec_from_file_location(enforcer_module_name, enforcer_path)
|
|
27
|
+
assert spec is not None
|
|
28
|
+
assert spec.loader is not None
|
|
29
|
+
loaded_module = importlib.util.module_from_spec(spec)
|
|
30
|
+
spec.loader.exec_module(loaded_module)
|
|
31
|
+
return loaded_module
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_docstring_prose_wall_without_illustration(content: str, file_path: str) -> list[str]:
|
|
38
|
+
return code_rules_enforcer.check_docstring_prose_wall_without_illustration(content, file_path)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
42
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
PRODUCTION_FILE_PATH = "/project/src/tally.py"
|
|
46
|
+
SUITE_FILE_PATH = "/project/src/test_tally.py"
|
|
47
|
+
INFRA_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fixture_top_level_wall() -> str:
|
|
51
|
+
return (
|
|
52
|
+
'"""Assemble the nightly voyage tally from the harbor scans.\n'
|
|
53
|
+
"\n"
|
|
54
|
+
"A scan names one vessel and where it dropped anchor.\n"
|
|
55
|
+
"The tally walks the scans in arrival order and keeps that order.\n"
|
|
56
|
+
"A calm voyage ends well for every vessel it carried.\n"
|
|
57
|
+
"A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
58
|
+
"A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
59
|
+
"The tally groups the vessels by their final port for the harbor.\n"
|
|
60
|
+
"The harbor reads the tally and sees every arrival at a glance.\n"
|
|
61
|
+
'"""\n'
|
|
62
|
+
"\n"
|
|
63
|
+
"def emit_tally() -> None:\n"
|
|
64
|
+
" return None\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _fixture_callable_wall() -> str:
|
|
69
|
+
return (
|
|
70
|
+
"def emit_tally() -> None:\n"
|
|
71
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
72
|
+
"\n"
|
|
73
|
+
" A scan names one vessel and where it dropped anchor.\n"
|
|
74
|
+
" The tally walks the scans in arrival order and keeps that order.\n"
|
|
75
|
+
" A calm voyage ends well for every vessel it carried.\n"
|
|
76
|
+
" A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
77
|
+
" A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
78
|
+
" The tally groups the vessels by their final port for the reader.\n"
|
|
79
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
80
|
+
' """\n'
|
|
81
|
+
" return None\n"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _fixture_wall_with_listing() -> str:
|
|
86
|
+
return (
|
|
87
|
+
"def emit_tally() -> None:\n"
|
|
88
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
89
|
+
"\n"
|
|
90
|
+
" A scan names one vessel and where it dropped anchor::\n"
|
|
91
|
+
"\n"
|
|
92
|
+
" vessel 42 -- halted -> marked mid-voyage\n"
|
|
93
|
+
" OK: a calm voyage ends well for every vessel\n"
|
|
94
|
+
" FLAG: a wrecked voyage reads the same as a calm one\n"
|
|
95
|
+
"\n"
|
|
96
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
97
|
+
' """\n'
|
|
98
|
+
" return None\n"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _fixture_wall_with_doctest() -> str:
|
|
103
|
+
return (
|
|
104
|
+
"def emit_tally() -> None:\n"
|
|
105
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
106
|
+
"\n"
|
|
107
|
+
" A calm voyage ends well for every vessel it carried.\n"
|
|
108
|
+
" A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
109
|
+
" A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
110
|
+
"\n"
|
|
111
|
+
" >>> emit_tally()\n"
|
|
112
|
+
" vessel 42 halted\n"
|
|
113
|
+
" vessel 43 calm\n"
|
|
114
|
+
"\n"
|
|
115
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
116
|
+
' """\n'
|
|
117
|
+
" return None\n"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _fixture_short_summary() -> str:
|
|
122
|
+
return (
|
|
123
|
+
"def emit_tally() -> None:\n"
|
|
124
|
+
' """Post the voyage tally to the harbor board.\n'
|
|
125
|
+
"\n"
|
|
126
|
+
" A scan names one vessel and where it dropped anchor.\n"
|
|
127
|
+
" A calm voyage ends well for every vessel it carried.\n"
|
|
128
|
+
" A halted voyage marks the vessel it was near when it stopped.\n"
|
|
129
|
+
" The reader reads the whole tally at a single glance.\n"
|
|
130
|
+
' """\n'
|
|
131
|
+
" return None\n"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_should_flag_prose_wall_with_no_illustration() -> None:
|
|
136
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
137
|
+
_fixture_top_level_wall(), PRODUCTION_FILE_PATH
|
|
138
|
+
)
|
|
139
|
+
assert any("worked example" in each for each in issues), (
|
|
140
|
+
f"A top-level wall must flag, got: {issues!r}"
|
|
141
|
+
)
|
|
142
|
+
assert any("module" in each for each in issues)
|
|
143
|
+
assert len(issues) == 1
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_should_flag_function_prose_wall() -> None:
|
|
147
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
148
|
+
_fixture_callable_wall(), PRODUCTION_FILE_PATH
|
|
149
|
+
)
|
|
150
|
+
assert any("emit_tally" in each for each in issues), (
|
|
151
|
+
f"A callable wall must flag, got: {issues!r}"
|
|
152
|
+
)
|
|
153
|
+
assert len(issues) == 1
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_should_not_flag_wall_with_literal_block() -> None:
|
|
157
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
158
|
+
_fixture_wall_with_listing(), PRODUCTION_FILE_PATH
|
|
159
|
+
)
|
|
160
|
+
assert issues == [], f"A '::' listing must pass, got: {issues!r}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_should_not_flag_wall_with_doctest() -> None:
|
|
164
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
165
|
+
_fixture_wall_with_doctest(), PRODUCTION_FILE_PATH
|
|
166
|
+
)
|
|
167
|
+
assert issues == [], f"A '>>>' doctest must pass, got: {issues!r}"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_should_not_flag_short_narrative() -> None:
|
|
171
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
172
|
+
_fixture_short_summary(), PRODUCTION_FILE_PATH
|
|
173
|
+
)
|
|
174
|
+
assert issues == [], f"A summary at the sentence limit must pass, got: {issues!r}"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_should_skip_test_file() -> None:
|
|
178
|
+
issues = check_docstring_prose_wall_without_illustration(_fixture_top_level_wall(), SUITE_FILE_PATH)
|
|
179
|
+
assert issues == [], f"A suite path stays exempt, got: {issues!r}"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
183
|
+
issues = check_docstring_prose_wall_without_illustration(
|
|
184
|
+
_fixture_top_level_wall(), INFRA_PATH
|
|
185
|
+
)
|
|
186
|
+
assert issues == [], f"An infra path stays exempt, got: {issues!r}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
190
|
+
issues = check_docstring_prose_wall_without_illustration("def broken(\n", PRODUCTION_FILE_PATH)
|
|
191
|
+
assert issues == [], f"A syntax error yields no issues, got: {issues!r}"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_validate_content_surfaces_prose_wall() -> None:
|
|
195
|
+
issues = validate_content(_fixture_top_level_wall(), PRODUCTION_FILE_PATH, old_content="")
|
|
196
|
+
matching_issues = [each for each in issues if "worked example" in each]
|
|
197
|
+
assert matching_issues, f"validate_content must surface the wall, got: {issues!r}"
|
|
@@ -252,6 +252,33 @@ def test_should_flag_spaced_double_hyphen_joiner() -> None:
|
|
|
252
252
|
assert len(issues) == 1
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
def _diagram_first_literal_block() -> str:
|
|
256
|
+
return (
|
|
257
|
+
"def emit_tally() -> None:\n"
|
|
258
|
+
' """Post the voyage tally so the reader sees how the run ended.\n'
|
|
259
|
+
"\n"
|
|
260
|
+
" A calm voyage versus a halted one::\n"
|
|
261
|
+
"\n"
|
|
262
|
+
" vessel 42 -- halted -> marked mid-voyage while it sailed\n"
|
|
263
|
+
" calm — every vessel it carried shows a clean recorded outcome\n"
|
|
264
|
+
" OK: a calm run and a sunk run read differently in the log\n"
|
|
265
|
+
" FLAG: a sunk run looks identical to a calm finished run here\n"
|
|
266
|
+
" The reader reads the final outcome at a glance.\n"
|
|
267
|
+
' """\n'
|
|
268
|
+
" return None\n"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_should_not_flag_diagram_first_literal_block() -> None:
|
|
273
|
+
issues = check_docstring_runon_sentence(
|
|
274
|
+
_diagram_first_literal_block(), PRODUCTION_FILE_PATH
|
|
275
|
+
)
|
|
276
|
+
assert issues == [], (
|
|
277
|
+
"A '::' listing's arrow, em-dash, and OK/FLAG rows sit outside the "
|
|
278
|
+
f"narrative and must not inflate the run-on count, got: {issues!r}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
255
282
|
def test_docstring_names_every_inspected_section_header() -> None:
|
|
256
283
|
docstring = code_rules_enforcer.check_docstring_runon_sentence.__doc__
|
|
257
284
|
assert docstring is not None
|
|
@@ -181,6 +181,30 @@ def test_utcfromtimestamp_remediation_uses_generic_placeholder() -> None:
|
|
|
181
181
|
assert "stamp, tz=" not in issues[0]
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
def test_utcnow_remediation_is_import_agnostic() -> None:
|
|
185
|
+
content = (
|
|
186
|
+
"import datetime\n"
|
|
187
|
+
"def stamp() -> object:\n"
|
|
188
|
+
" return datetime.datetime.utcnow()\n"
|
|
189
|
+
)
|
|
190
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
191
|
+
assert len(issues) == 1
|
|
192
|
+
assert "use datetime.now(" not in issues[0]
|
|
193
|
+
assert "on the same datetime class" in issues[0]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_utcfromtimestamp_remediation_is_import_agnostic() -> None:
|
|
197
|
+
content = (
|
|
198
|
+
"import datetime\n"
|
|
199
|
+
"def build(stamp: float) -> object:\n"
|
|
200
|
+
" return datetime.datetime.utcfromtimestamp(stamp)\n"
|
|
201
|
+
)
|
|
202
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
203
|
+
assert len(issues) == 1
|
|
204
|
+
assert "use datetime.fromtimestamp(" not in issues[0]
|
|
205
|
+
assert "on the same datetime class" in issues[0]
|
|
206
|
+
|
|
207
|
+
|
|
184
208
|
def test_remediation_does_not_mandate_astimezone() -> None:
|
|
185
209
|
content = (
|
|
186
210
|
"from datetime import datetime\n"
|
|
@@ -177,6 +177,39 @@ def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
|
|
|
177
177
|
)
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def test_scan_function_body_constants_skips_docstring_contrast_line() -> None:
|
|
181
|
+
source = (
|
|
182
|
+
"def demo(label):\n"
|
|
183
|
+
' """Flag a boolean assignment whose target and callee clash.\n'
|
|
184
|
+
"\n"
|
|
185
|
+
" ::\n"
|
|
186
|
+
"\n"
|
|
187
|
+
" FLAG: ready = make_stale(...)\n"
|
|
188
|
+
' """\n'
|
|
189
|
+
" return label\n"
|
|
190
|
+
)
|
|
191
|
+
advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
|
|
192
|
+
assert advisory_issues == [], (
|
|
193
|
+
f"Expected no function-local constant issue on an illustrative row, got: {advisory_issues}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_scan_function_body_constants_still_flags_real_function_body_constant() -> None:
|
|
198
|
+
source = (
|
|
199
|
+
"def demo(label):\n"
|
|
200
|
+
' """Flag a boolean assignment whose target and callee clash.\n'
|
|
201
|
+
"\n"
|
|
202
|
+
" FLAG: ready = make_stale(...)\n"
|
|
203
|
+
' """\n'
|
|
204
|
+
" SOME_CONST = 3\n"
|
|
205
|
+
" return SOME_CONST\n"
|
|
206
|
+
)
|
|
207
|
+
advisory_issues = code_rules_enforcer._scan_function_body_constants(source)
|
|
208
|
+
assert any("SOME_CONST" in issue for issue in advisory_issues), (
|
|
209
|
+
f"Expected the real function-body constant SOME_CONST to stay flagged, got: {advisory_issues}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
180
213
|
def test_check_constants_outside_config_flags_annotated_assignment() -> None:
|
|
181
214
|
source = "TEXT_FILE_ENCODING: str = 'utf-8'\n"
|
|
182
215
|
issues = code_rules_enforcer.check_constants_outside_config(
|
|
@@ -18,14 +18,37 @@ from code_rules_enforcer import ( # noqa: E402
|
|
|
18
18
|
)
|
|
19
19
|
from code_rules_shared import ( # noqa: E402
|
|
20
20
|
changed_line_numbers,
|
|
21
|
+
docstring_line_numbers,
|
|
21
22
|
)
|
|
22
23
|
|
|
23
24
|
code_rules_enforcer = SimpleNamespace(
|
|
24
25
|
changed_line_numbers=changed_line_numbers,
|
|
26
|
+
docstring_line_numbers=docstring_line_numbers,
|
|
25
27
|
prior_and_post_edit_content=prior_and_post_edit_content,
|
|
26
28
|
)
|
|
27
29
|
|
|
28
30
|
|
|
31
|
+
def test_docstring_line_numbers_spans_a_multi_line_function_docstring() -> None:
|
|
32
|
+
source = (
|
|
33
|
+
"def describe():\n"
|
|
34
|
+
' """First row.\n'
|
|
35
|
+
"\n"
|
|
36
|
+
" Third row.\n"
|
|
37
|
+
' """\n'
|
|
38
|
+
" return 1\n"
|
|
39
|
+
)
|
|
40
|
+
assert code_rules_enforcer.docstring_line_numbers(source) == {2, 3, 4, 5}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_docstring_line_numbers_excludes_real_code_lines() -> None:
|
|
44
|
+
source = "def compute():\n counter = 41\n return counter\n"
|
|
45
|
+
assert code_rules_enforcer.docstring_line_numbers(source) == set()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_docstring_line_numbers_returns_empty_set_on_syntax_error() -> None:
|
|
49
|
+
assert code_rules_enforcer.docstring_line_numbers("def broken(:\n") == set()
|
|
50
|
+
|
|
51
|
+
|
|
29
52
|
def test_readable_prior_yields_consistent_prior_and_reconstruction(tmp_path) -> None:
|
|
30
53
|
"""When the prior reads cleanly, the helper returns the same prior content it
|
|
31
54
|
reconstructed the post-edit view from, so the two never diverge across two
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Behavior tests for the code_rules_magic_values check module.
|
|
2
|
+
|
|
3
|
+
A diagram-first docstring may carry a bare number on an illustrative row
|
|
4
|
+
(``at line 2``). That number is prose, not a code literal, so the magic-value
|
|
5
|
+
check skips docstring rows while still flagging a real magic number in an
|
|
6
|
+
actual code statement.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_BLOCKING_DIRECTORY = str(Path(__file__).resolve().parent)
|
|
15
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
16
|
+
if _BLOCKING_DIRECTORY not in sys.path:
|
|
17
|
+
sys.path.insert(0, _BLOCKING_DIRECTORY)
|
|
18
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
19
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
20
|
+
|
|
21
|
+
from code_rules_magic_values import check_magic_values # noqa: E402
|
|
22
|
+
|
|
23
|
+
PRODUCTION_FILE_PATH = "packages/app/rendering.py"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_magic_value_check_skips_bare_number_inside_docstring_illustration() -> None:
|
|
27
|
+
source = (
|
|
28
|
+
"def summarize(record):\n"
|
|
29
|
+
' """Summarize how a run ended.\n'
|
|
30
|
+
"\n"
|
|
31
|
+
" A finished run shows a clean outcome::\n"
|
|
32
|
+
"\n"
|
|
33
|
+
" marked in-flight at row 2\n"
|
|
34
|
+
' """\n'
|
|
35
|
+
" return record\n"
|
|
36
|
+
)
|
|
37
|
+
issues = check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
38
|
+
assert issues == [], f"Expected no magic-value issue on an illustrative row, got: {issues}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_magic_value_check_still_flags_real_magic_number_in_code_statement() -> None:
|
|
42
|
+
source = (
|
|
43
|
+
"def summarize(record):\n"
|
|
44
|
+
' """Summarize how a run ended.\n'
|
|
45
|
+
"\n"
|
|
46
|
+
" marked in-flight at row 2\n"
|
|
47
|
+
' """\n'
|
|
48
|
+
" threshold = record.score * 42\n"
|
|
49
|
+
" return threshold\n"
|
|
50
|
+
)
|
|
51
|
+
issues = check_magic_values(source, PRODUCTION_FILE_PATH)
|
|
52
|
+
assert any(issue.endswith("Magic value 42 - extract to named constant") for issue in issues), (
|
|
53
|
+
f"Expected the real magic number 42 to stay flagged, got: {issues}"
|
|
54
|
+
)
|
|
@@ -142,12 +142,6 @@ def test_path_exempts_existing_session_env_cleanup_definition_site() -> None:
|
|
|
142
142
|
assert path_is_exempt("packages/claude-dev-env/hooks/session/session_env_cleanup.py")
|
|
143
143
|
|
|
144
144
|
|
|
145
|
-
def test_path_exempts_existing_md_to_html_test_support_definition_site() -> None:
|
|
146
|
-
assert path_is_exempt(
|
|
147
|
-
"packages/claude-dev-env/hooks/blocking/_md_to_html_blocker_test_support.py"
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
|
|
151
145
|
def test_path_exempts_existing_teardown_worktrees_definition_site() -> None:
|
|
152
146
|
assert path_is_exempt(
|
|
153
147
|
"packages/claude-dev-env/skills/_shared/pr-loop/scripts/teardown_worktrees.py"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Unit tests: persistent-agent state keys leave both pr-converge hooks unchanged.
|
|
2
|
+
|
|
3
|
+
pr-converge-state.json carries `agents_session_id` and `persistent_agents`
|
|
4
|
+
(the persistent per-step agent map with a `fix_executor` entry). Neither the
|
|
5
|
+
enforcer (`pr_converge_bugteam_enforcer`) nor the tracker
|
|
6
|
+
(`pr_converge_bugteam_skill_tracker`) reads these keys, so their allow/block
|
|
7
|
+
decisions and stamped fields match a state file without them, and the tracker
|
|
8
|
+
preserves the keys unchanged on its stamp write.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import pathlib
|
|
17
|
+
import sys
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
from typing import Any
|
|
20
|
+
from unittest import mock
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
_BLOCKING_DIR = pathlib.Path(__file__).parent
|
|
25
|
+
_HOOKS_TREE = _BLOCKING_DIR.parent
|
|
26
|
+
_LIFECYCLE_DIR = _HOOKS_TREE / "lifecycle"
|
|
27
|
+
for each_path in (str(_BLOCKING_DIR), str(_HOOKS_TREE)):
|
|
28
|
+
if each_path not in sys.path:
|
|
29
|
+
sys.path.insert(0, each_path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_hook_module(module_name: str, module_path: pathlib.Path) -> ModuleType:
|
|
33
|
+
hook_spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
34
|
+
assert hook_spec is not None
|
|
35
|
+
assert hook_spec.loader is not None
|
|
36
|
+
loaded_module = importlib.util.module_from_spec(hook_spec)
|
|
37
|
+
hook_spec.loader.exec_module(loaded_module)
|
|
38
|
+
return loaded_module
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
enforcer_module = _load_hook_module(
|
|
42
|
+
"pr_converge_bugteam_enforcer_state_tolerance_target",
|
|
43
|
+
_BLOCKING_DIR / "pr_converge_bugteam_enforcer.py",
|
|
44
|
+
)
|
|
45
|
+
tracker_module = _load_hook_module(
|
|
46
|
+
"pr_converge_bugteam_skill_tracker_state_tolerance_target",
|
|
47
|
+
_LIFECYCLE_DIR / "pr_converge_bugteam_skill_tracker.py",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from hooks_constants.pr_converge_bugteam_enforcer_constants import (
|
|
51
|
+
BUGTEAM_PHASE,
|
|
52
|
+
CLAUDE_JOB_DIR_ENV_VAR,
|
|
53
|
+
PR_CONVERGE_STATE_FILENAME,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_HEAD_SHA_CURRENT = "abc123def456abc123def456abc123def456abcd"
|
|
57
|
+
_TICK_CURRENT = 7
|
|
58
|
+
_AUDIT_PROMPT = "Run the bugteam audit and report findings against the A-J categories."
|
|
59
|
+
_AGENTS_SESSION_ID = "session-0123456789abcdef"
|
|
60
|
+
_FIX_EXECUTOR_AGENT_ID = "prc-fix-42"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _persistent_agent_state_keys() -> dict[str, Any]:
|
|
64
|
+
return {
|
|
65
|
+
"agents_session_id": _AGENTS_SESSION_ID,
|
|
66
|
+
"persistent_agents": {
|
|
67
|
+
"fix_executor": {
|
|
68
|
+
"agent_id": _FIX_EXECUTOR_AGENT_ID,
|
|
69
|
+
"created_tick": 2,
|
|
70
|
+
"last_used_tick": 6,
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _bugteam_phase_state(**overrides: Any) -> dict[str, Any]:
|
|
77
|
+
baseline_state: dict[str, Any] = {
|
|
78
|
+
"phase": BUGTEAM_PHASE,
|
|
79
|
+
"current_head": _HEAD_SHA_CURRENT,
|
|
80
|
+
"tick_count": _TICK_CURRENT,
|
|
81
|
+
"bugteam_skill_invoked_at_head": None,
|
|
82
|
+
"bugteam_skill_invoked_at_tick": None,
|
|
83
|
+
}
|
|
84
|
+
baseline_state.update(_persistent_agent_state_keys())
|
|
85
|
+
baseline_state.update(overrides)
|
|
86
|
+
return baseline_state
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _write_state(state_directory: pathlib.Path, state: dict[str, Any]) -> pathlib.Path:
|
|
90
|
+
state_path = state_directory / PR_CONVERGE_STATE_FILENAME
|
|
91
|
+
state_path.write_text(json.dumps(state), encoding="utf-8")
|
|
92
|
+
return state_path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_state(state_directory: pathlib.Path) -> dict[str, Any]:
|
|
96
|
+
state_path = state_directory / PR_CONVERGE_STATE_FILENAME
|
|
97
|
+
return json.loads(state_path.read_text(encoding="utf-8"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _clean_coder_audit_payload() -> dict[str, Any]:
|
|
101
|
+
return {
|
|
102
|
+
"tool_name": "Agent",
|
|
103
|
+
"tool_input": {
|
|
104
|
+
"subagent_type": "clean-coder",
|
|
105
|
+
"prompt": _AUDIT_PROMPT,
|
|
106
|
+
"description": "Audit the PR",
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _bugteam_skill_payload() -> dict[str, Any]:
|
|
112
|
+
return {
|
|
113
|
+
"tool_name": "Skill",
|
|
114
|
+
"tool_input": {"skill": "bugteam", "args": "https://github.com/o/r/pull/1"},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run_enforcer_main_with_io(input_text: str) -> str:
|
|
119
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
120
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO) as captured_stdout:
|
|
121
|
+
try:
|
|
122
|
+
enforcer_module.main()
|
|
123
|
+
except SystemExit:
|
|
124
|
+
pass
|
|
125
|
+
return captured_stdout.getvalue()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _run_tracker_main_with_io(input_text: str) -> None:
|
|
129
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
130
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO):
|
|
131
|
+
with mock.patch("sys.stderr", new_callable=io.StringIO):
|
|
132
|
+
try:
|
|
133
|
+
tracker_module.main()
|
|
134
|
+
except SystemExit:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.fixture()
|
|
139
|
+
def claude_job_directory(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
|
|
140
|
+
monkeypatch.setenv(CLAUDE_JOB_DIR_ENV_VAR, str(tmp_path))
|
|
141
|
+
return tmp_path
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_should_block_enforcer_when_persistent_agent_keys_present_without_skill(
|
|
145
|
+
claude_job_directory: pathlib.Path,
|
|
146
|
+
) -> None:
|
|
147
|
+
_write_state(claude_job_directory, _bugteam_phase_state())
|
|
148
|
+
captured_output = _run_enforcer_main_with_io(json.dumps(_clean_coder_audit_payload()))
|
|
149
|
+
deny_payload = json.loads(captured_output)
|
|
150
|
+
assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_allow_enforcer_when_persistent_agent_keys_present_with_skill_recorded(
|
|
154
|
+
claude_job_directory: pathlib.Path,
|
|
155
|
+
) -> None:
|
|
156
|
+
_write_state(
|
|
157
|
+
claude_job_directory,
|
|
158
|
+
_bugteam_phase_state(
|
|
159
|
+
bugteam_skill_invoked_at_head=_HEAD_SHA_CURRENT,
|
|
160
|
+
bugteam_skill_invoked_at_tick=_TICK_CURRENT,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
captured_output = _run_enforcer_main_with_io(json.dumps(_clean_coder_audit_payload()))
|
|
164
|
+
assert captured_output == ""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_should_stamp_tracker_fields_when_persistent_agent_keys_present(
|
|
168
|
+
claude_job_directory: pathlib.Path,
|
|
169
|
+
) -> None:
|
|
170
|
+
_write_state(claude_job_directory, _bugteam_phase_state())
|
|
171
|
+
_run_tracker_main_with_io(json.dumps(_bugteam_skill_payload()))
|
|
172
|
+
updated_state = _read_state(claude_job_directory)
|
|
173
|
+
assert updated_state["bugteam_skill_invoked_at_head"] == _HEAD_SHA_CURRENT
|
|
174
|
+
assert updated_state["bugteam_skill_invoked_at_tick"] == _TICK_CURRENT
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_should_preserve_persistent_agent_keys_on_tracker_stamp_write(
|
|
178
|
+
claude_job_directory: pathlib.Path,
|
|
179
|
+
) -> None:
|
|
180
|
+
_write_state(claude_job_directory, _bugteam_phase_state())
|
|
181
|
+
_run_tracker_main_with_io(json.dumps(_bugteam_skill_payload()))
|
|
182
|
+
updated_state = _read_state(claude_job_directory)
|
|
183
|
+
assert updated_state["agents_session_id"] == _AGENTS_SESSION_ID
|
|
184
|
+
assert updated_state["persistent_agents"] == _persistent_agent_state_keys()["persistent_agents"]
|
|
@@ -10,11 +10,11 @@ each real hook script through its production ``__main__`` stdin path over a
|
|
|
10
10
|
corpus that pins those fail-soft edges plus a representative allow payload, so a
|
|
11
11
|
conversion that changes any decision is caught.
|
|
12
12
|
|
|
13
|
-
The deterministic deny
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
The deterministic deny payload for the Write/Edit blocker whose trigger
|
|
14
|
+
needs no filesystem or state setup (``open_questions_in_plans_blocker``) is
|
|
15
|
+
exercised here too; each remaining hook's full deny coverage stays in its own
|
|
16
|
+
suite, which also drives the real ``main()`` and so re-proves the decision
|
|
17
|
+
after the parser swap.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import json
|
|
@@ -28,7 +28,6 @@ import pytest
|
|
|
28
28
|
_BLOCKING_DIRECTORY = Path(__file__).resolve().parent
|
|
29
29
|
|
|
30
30
|
ALL_CONVERTED_HOOK_FILENAMES = (
|
|
31
|
-
"md_to_html_blocker.py",
|
|
32
31
|
"open_questions_in_plans_blocker.py",
|
|
33
32
|
"claude_md_orphan_file_blocker.py",
|
|
34
33
|
"pr_converge_bugteam_enforcer.py",
|
|
@@ -83,30 +82,6 @@ def test_fail_soft_payload_exits_zero_with_no_decision(hook_filename: str, stdin
|
|
|
83
82
|
)
|
|
84
83
|
|
|
85
84
|
|
|
86
|
-
def test_md_to_html_blocker_still_denies_relative_markdown_write() -> None:
|
|
87
|
-
payload = json.dumps(
|
|
88
|
-
{
|
|
89
|
-
"tool_name": "Write",
|
|
90
|
-
"tool_input": {"file_path": "notes/topic.md", "content": "# Topic"},
|
|
91
|
-
}
|
|
92
|
-
)
|
|
93
|
-
completed = _run_hook_script("md_to_html_blocker.py", payload)
|
|
94
|
-
assert completed.returncode == 0
|
|
95
|
-
assert _decision_from_stdout(completed) == "deny"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_md_to_html_blocker_still_allows_non_markdown_write() -> None:
|
|
99
|
-
payload = json.dumps(
|
|
100
|
-
{
|
|
101
|
-
"tool_name": "Write",
|
|
102
|
-
"tool_input": {"file_path": "notes/topic.txt", "content": "plain"},
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
completed = _run_hook_script("md_to_html_blocker.py", payload)
|
|
106
|
-
assert completed.returncode == 0
|
|
107
|
-
assert _decision_from_stdout(completed) is None
|
|
108
|
-
|
|
109
|
-
|
|
110
85
|
def test_open_questions_blocker_still_denies_plan_with_open_questions(
|
|
111
86
|
tmp_path: Path,
|
|
112
87
|
) -> None:
|
|
@@ -24,7 +24,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
24
24
|
| `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
|
|
25
25
|
| `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
|
|
26
26
|
| `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
|
|
27
|
-
| `doc_gist_auto_publish_constants.py` | Sentinel marker and URL patterns for the doc-gist auto-publish hook |
|
|
28
27
|
| `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
|
|
29
28
|
| `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
|
|
30
29
|
| `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
|
|
@@ -34,10 +33,8 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
34
33
|
| `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
|
|
35
34
|
| `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
|
|
36
35
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
37
|
-
| `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
|
|
38
36
|
| `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
|
|
39
37
|
| `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
|
|
40
|
-
| `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
|
|
41
38
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
42
39
|
| `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
|
|
43
40
|
| `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
|