claude-dev-env 1.78.0 → 1.80.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/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
- package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.test.mjs +3 -2
- package/hooks/blocking/CLAUDE.md +3 -2
- package/hooks/blocking/code_rules_docstrings.py +609 -16
- package/hooks/blocking/code_rules_enforcer.py +41 -0
- package/hooks/blocking/code_rules_imports_logging.py +136 -1
- package/hooks/blocking/code_rules_naming_collection.py +76 -1
- package/hooks/blocking/code_rules_paired_test.py +240 -22
- package/hooks/blocking/code_rules_test_assertions.py +159 -1
- package/hooks/blocking/code_rules_unused_imports.py +7 -63
- package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
- package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
- package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
- package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/blocking_check_limits.py +79 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
- package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
- package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +57 -54
- package/rules/env-var-table-code-drift.md +24 -0
- package/rules/package-inventory-stale-entry.md +4 -4
- package/rules/paired-test-coverage.md +12 -5
- package/skills/autoconverge/SKILL.md +26 -5
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
- package/skills/autoconverge/workflow/converge.mjs +110 -228
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
- package/skills/pr-converge/SKILL.md +28 -2
- package/skills/pr-converge/reference/convergence-gates.md +13 -1
- package/skills/pr-converge/reference/state-schema.md +11 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Tests for check_docstring_field_runmode_outcome — O6 run-mode-vs-per-record drift.
|
|
2
|
+
|
|
3
|
+
A dataclass field whose name marks a run-mode flag (``is_dry_run``) is documented
|
|
4
|
+
in the class Attributes block with per-record write-outcome prose ("True when no
|
|
5
|
+
STP was written"), while the value is set the same way for every record from the
|
|
6
|
+
run mode (``is_dry_run=not is_execute``). An already-OK record in an execute run
|
|
7
|
+
writes no file yet still stores ``False``, so the per-record prose misleads every
|
|
8
|
+
reader. This is the deterministic single-file slice of Category O6 drift.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_enforcer_module() -> ModuleType:
|
|
19
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
20
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
21
|
+
assert spec is not None
|
|
22
|
+
assert spec.loader is not None
|
|
23
|
+
module = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(module)
|
|
25
|
+
return module
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_docstring_field_runmode_outcome(content: str, file_path: str) -> list[str]:
|
|
32
|
+
return code_rules_enforcer.check_docstring_field_runmode_outcome(content, file_path)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PRODUCTION_FILE_PATH = "/project/src/jsonl_writer.py"
|
|
36
|
+
TEST_FILE_PATH = "/project/src/test_jsonl_writer.py"
|
|
37
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _drifted_record() -> str:
|
|
41
|
+
return (
|
|
42
|
+
"@dataclass(frozen=True)\n"
|
|
43
|
+
"class PerThemeJsonlRecord:\n"
|
|
44
|
+
' """Exact schema for the per-theme JSONL line.\n'
|
|
45
|
+
"\n"
|
|
46
|
+
" Attributes:\n"
|
|
47
|
+
" theme_name: Theme directory basename.\n"
|
|
48
|
+
" is_dry_run: True when no STP was written.\n"
|
|
49
|
+
' """\n'
|
|
50
|
+
"\n"
|
|
51
|
+
" theme_name: str\n"
|
|
52
|
+
" is_dry_run: bool\n"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_mode_record() -> str:
|
|
57
|
+
return (
|
|
58
|
+
"@dataclass(frozen=True)\n"
|
|
59
|
+
"class PerThemeJsonlRecord:\n"
|
|
60
|
+
' """Exact schema for the per-theme JSONL line.\n'
|
|
61
|
+
"\n"
|
|
62
|
+
" Attributes:\n"
|
|
63
|
+
" theme_name: Theme directory basename.\n"
|
|
64
|
+
" is_dry_run: True for a dry run; False for an execute run.\n"
|
|
65
|
+
' """\n'
|
|
66
|
+
"\n"
|
|
67
|
+
" theme_name: str\n"
|
|
68
|
+
" is_dry_run: bool\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_flags_run_mode_field_documented_as_per_record_write_outcome() -> None:
|
|
73
|
+
issues = check_docstring_field_runmode_outcome(_drifted_record(), PRODUCTION_FILE_PATH)
|
|
74
|
+
assert len(issues) == 1
|
|
75
|
+
assert "is_dry_run" in issues[0]
|
|
76
|
+
assert "run-mode" in issues[0]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_run_mode_phrasing_is_left_alone() -> None:
|
|
80
|
+
issues = check_docstring_field_runmode_outcome(_run_mode_record(), PRODUCTION_FILE_PATH)
|
|
81
|
+
assert issues == []
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_multiline_description_continuation_still_flags() -> None:
|
|
85
|
+
source = (
|
|
86
|
+
"@dataclass\n"
|
|
87
|
+
"class Record:\n"
|
|
88
|
+
' """Schema.\n'
|
|
89
|
+
"\n"
|
|
90
|
+
" Attributes:\n"
|
|
91
|
+
" is_dry_run: True for the record when\n"
|
|
92
|
+
" no STP was written.\n"
|
|
93
|
+
' """\n'
|
|
94
|
+
"\n"
|
|
95
|
+
" is_dry_run: bool\n"
|
|
96
|
+
)
|
|
97
|
+
issues = check_docstring_field_runmode_outcome(source, PRODUCTION_FILE_PATH)
|
|
98
|
+
assert len(issues) == 1
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_non_run_mode_field_with_write_outcome_is_left_alone() -> None:
|
|
102
|
+
source = (
|
|
103
|
+
"@dataclass\n"
|
|
104
|
+
"class Record:\n"
|
|
105
|
+
' """Schema.\n'
|
|
106
|
+
"\n"
|
|
107
|
+
" Attributes:\n"
|
|
108
|
+
" stp_path: Path the STP was written to disk.\n"
|
|
109
|
+
' """\n'
|
|
110
|
+
"\n"
|
|
111
|
+
" stp_path: str\n"
|
|
112
|
+
)
|
|
113
|
+
issues = check_docstring_field_runmode_outcome(source, PRODUCTION_FILE_PATH)
|
|
114
|
+
assert issues == []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_test_file_is_exempt() -> None:
|
|
118
|
+
issues = check_docstring_field_runmode_outcome(_drifted_record(), TEST_FILE_PATH)
|
|
119
|
+
assert issues == []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_hook_infrastructure_is_exempt() -> None:
|
|
123
|
+
issues = check_docstring_field_runmode_outcome(_drifted_record(), HOOK_INFRASTRUCTURE_PATH)
|
|
124
|
+
assert issues == []
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_syntax_error_returns_no_issues() -> None:
|
|
128
|
+
issues = check_docstring_field_runmode_outcome("class Record(\n", PRODUCTION_FILE_PATH)
|
|
129
|
+
assert issues == []
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Tests for check_docstring_length_constant_superlative_vs_exact_gate.
|
|
2
|
+
|
|
3
|
+
A config module defines an integer ``*_LENGTH`` constant and its docstring
|
|
4
|
+
describes it with a superlative word ("the longest color string the swatch
|
|
5
|
+
accepts"), while the only consumer treats the constant as an exact-length
|
|
6
|
+
equality gate ("len(hex_color) != COLOR_AARRGGBB_LENGTH") in a sibling module.
|
|
7
|
+
A shorter valid string is rejected, not accepted at a shorter length, so the
|
|
8
|
+
"longest" prose claims a range of accepted lengths the code never allows. This
|
|
9
|
+
is the deterministic exact-gate slice of Category O6/O8
|
|
10
|
+
docstring-vs-implementation drift.
|
|
11
|
+
|
|
12
|
+
The package trees these tests build live under a neutral OS temp directory
|
|
13
|
+
rather than the pytest ``tmp_path`` fixture: the production check exempts any
|
|
14
|
+
path containing "test", and the pytest fixture root carries that substring, so a
|
|
15
|
+
fixture-rooted package would be skipped as test code and prove nothing.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib.util
|
|
21
|
+
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from types import ModuleType
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load_enforcer_module() -> ModuleType:
|
|
27
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
28
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
29
|
+
assert spec is not None
|
|
30
|
+
assert spec.loader is not None
|
|
31
|
+
module = importlib.util.module_from_spec(spec)
|
|
32
|
+
spec.loader.exec_module(module)
|
|
33
|
+
return module
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_length_gate(content: str, file_path: str) -> list[str]:
|
|
40
|
+
return code_rules_enforcer.check_docstring_length_constant_superlative_vs_exact_gate(
|
|
41
|
+
content, file_path
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
SUPERLATIVE_SWATCH_MODULE = '''"""Color-swatch rendering constants for the HTML run report.
|
|
46
|
+
|
|
47
|
+
These constants name the longest color string the swatch accepts and the
|
|
48
|
+
channel maximum the alpha byte is divided by.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from typing import Final
|
|
52
|
+
|
|
53
|
+
COLOR_AARRGGBB_LENGTH: Final[int] = 9
|
|
54
|
+
'''
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _run_gate_over_package(
|
|
58
|
+
swatch_content: str, consumer_body: str, swatch_basename: str = "color_swatch.py"
|
|
59
|
+
) -> list[str]:
|
|
60
|
+
with tempfile.TemporaryDirectory() as temp_root:
|
|
61
|
+
config_directory = Path(temp_root) / "stp_contrast_fix" / "config"
|
|
62
|
+
config_directory.mkdir(parents=True)
|
|
63
|
+
swatch_path = config_directory / swatch_basename
|
|
64
|
+
swatch_path.write_text(swatch_content, encoding="utf-8")
|
|
65
|
+
consumer_path = config_directory.parent / "html_report_fragments.py"
|
|
66
|
+
consumer_path.write_text(consumer_body, encoding="utf-8")
|
|
67
|
+
return check_length_gate(swatch_content, str(swatch_path))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_superlative_describing_exact_length_gate_is_flagged() -> None:
|
|
71
|
+
consumer_body = (
|
|
72
|
+
"from config.color_swatch import COLOR_AARRGGBB_LENGTH\n\n\n"
|
|
73
|
+
"def to_css(hex_color: str) -> str | None:\n"
|
|
74
|
+
' if not hex_color.startswith("#") or len(hex_color) != '
|
|
75
|
+
"COLOR_AARRGGBB_LENGTH:\n"
|
|
76
|
+
" return None\n"
|
|
77
|
+
" return hex_color\n"
|
|
78
|
+
)
|
|
79
|
+
issues = _run_gate_over_package(SUPERLATIVE_SWATCH_MODULE, consumer_body)
|
|
80
|
+
assert len(issues) == 1
|
|
81
|
+
assert "COLOR_AARRGGBB_LENGTH" in issues[0]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_ordered_comparison_treated_as_maximum_is_not_flagged() -> None:
|
|
85
|
+
consumer_body = (
|
|
86
|
+
"from config.color_swatch import COLOR_AARRGGBB_LENGTH\n\n\n"
|
|
87
|
+
"def truncated(hex_color: str) -> bool:\n"
|
|
88
|
+
" return len(hex_color) <= COLOR_AARRGGBB_LENGTH\n"
|
|
89
|
+
)
|
|
90
|
+
assert _run_gate_over_package(SUPERLATIVE_SWATCH_MODULE, consumer_body) == []
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_no_superlative_phrase_is_not_flagged() -> None:
|
|
94
|
+
plain_module = (
|
|
95
|
+
'"""The exact required #AARRGGBB length the swatch renders."""\n\n'
|
|
96
|
+
"from typing import Final\n\n"
|
|
97
|
+
"COLOR_AARRGGBB_LENGTH: Final[int] = 9\n"
|
|
98
|
+
)
|
|
99
|
+
consumer_body = (
|
|
100
|
+
"from config.color_swatch import COLOR_AARRGGBB_LENGTH\n\n\n"
|
|
101
|
+
"def to_css(hex_color: str) -> bool:\n"
|
|
102
|
+
" return len(hex_color) == COLOR_AARRGGBB_LENGTH\n"
|
|
103
|
+
)
|
|
104
|
+
assert _run_gate_over_package(plain_module, consumer_body) == []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_constant_without_length_suffix_is_not_flagged() -> None:
|
|
108
|
+
non_length_module = (
|
|
109
|
+
'"""The longest swatch threshold value used by the renderer."""\n\n'
|
|
110
|
+
"from typing import Final\n\n"
|
|
111
|
+
"COLOR_CHANNEL_MAXIMUM: Final[int] = 255\n"
|
|
112
|
+
)
|
|
113
|
+
consumer_body = (
|
|
114
|
+
"from config.color_swatch import COLOR_CHANNEL_MAXIMUM\n\n\n"
|
|
115
|
+
"def saturated(channel: str) -> bool:\n"
|
|
116
|
+
" return len(channel) == COLOR_CHANNEL_MAXIMUM\n"
|
|
117
|
+
)
|
|
118
|
+
assert _run_gate_over_package(non_length_module, consumer_body) == []
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_constant_with_no_length_comparison_is_not_flagged() -> None:
|
|
122
|
+
consumer_body = (
|
|
123
|
+
"from config.color_swatch import COLOR_AARRGGBB_LENGTH\n\n\n"
|
|
124
|
+
"def pad(hex_color: str) -> str:\n"
|
|
125
|
+
" return hex_color.ljust(COLOR_AARRGGBB_LENGTH)\n"
|
|
126
|
+
)
|
|
127
|
+
assert _run_gate_over_package(SUPERLATIVE_SWATCH_MODULE, consumer_body) == []
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_superlative_in_function_docstring_is_not_flagged() -> None:
|
|
131
|
+
function_docstring_module = (
|
|
132
|
+
'"""The exact #AARRGGBB length the swatch renders."""\n\n'
|
|
133
|
+
"from typing import Final\n\n"
|
|
134
|
+
"COLOR_AARRGGBB_LENGTH: Final[int] = 9\n\n\n"
|
|
135
|
+
"def describe() -> str:\n"
|
|
136
|
+
' """Return the longest color string the swatch accepts."""\n'
|
|
137
|
+
' return "swatch"\n'
|
|
138
|
+
)
|
|
139
|
+
consumer_body = (
|
|
140
|
+
"from config.color_swatch import COLOR_AARRGGBB_LENGTH\n\n\n"
|
|
141
|
+
"def to_css(hex_color: str) -> bool:\n"
|
|
142
|
+
" return len(hex_color) == COLOR_AARRGGBB_LENGTH\n"
|
|
143
|
+
)
|
|
144
|
+
assert _run_gate_over_package(function_docstring_module, consumer_body) == []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_superlative_bound_to_other_constant_is_not_flagged() -> None:
|
|
148
|
+
two_constant_module = (
|
|
149
|
+
'"""Color-swatch rendering constants for the HTML run report.\n\n'
|
|
150
|
+
"COLOR_AARRGGBB_LENGTH is the exact #AARRGGBB length the swatch renders.\n"
|
|
151
|
+
"MAX_NAME_LENGTH is the longest swatch label the legend lays out.\n"
|
|
152
|
+
'"""\n\n'
|
|
153
|
+
"from typing import Final\n\n"
|
|
154
|
+
"COLOR_AARRGGBB_LENGTH: Final[int] = 9\n"
|
|
155
|
+
"MAX_NAME_LENGTH: Final[int] = 32\n"
|
|
156
|
+
)
|
|
157
|
+
consumer_body = (
|
|
158
|
+
"from config.color_swatch import COLOR_AARRGGBB_LENGTH, MAX_NAME_LENGTH\n\n\n"
|
|
159
|
+
"def to_css(hex_color: str) -> str | None:\n"
|
|
160
|
+
" if len(hex_color) != COLOR_AARRGGBB_LENGTH:\n"
|
|
161
|
+
" return None\n"
|
|
162
|
+
" return hex_color\n\n\n"
|
|
163
|
+
"def fits(label: str) -> bool:\n"
|
|
164
|
+
" return len(label) <= MAX_NAME_LENGTH\n"
|
|
165
|
+
)
|
|
166
|
+
assert _run_gate_over_package(two_constant_module, consumer_body) == []
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_superlative_bound_to_exact_gated_constant_is_flagged() -> None:
|
|
170
|
+
two_constant_module = (
|
|
171
|
+
'"""Color-swatch rendering constants for the HTML run report.\n\n'
|
|
172
|
+
"COLOR_AARRGGBB_LENGTH is the longest #AARRGGBB string the swatch accepts.\n"
|
|
173
|
+
"MAX_NAME_LENGTH is the exact swatch label width the legend lays out.\n"
|
|
174
|
+
'"""\n\n'
|
|
175
|
+
"from typing import Final\n\n"
|
|
176
|
+
"COLOR_AARRGGBB_LENGTH: Final[int] = 9\n"
|
|
177
|
+
"MAX_NAME_LENGTH: Final[int] = 32\n"
|
|
178
|
+
)
|
|
179
|
+
consumer_body = (
|
|
180
|
+
"from config.color_swatch import COLOR_AARRGGBB_LENGTH, MAX_NAME_LENGTH\n\n\n"
|
|
181
|
+
"def to_css(hex_color: str) -> str | None:\n"
|
|
182
|
+
" if len(hex_color) != COLOR_AARRGGBB_LENGTH:\n"
|
|
183
|
+
" return None\n"
|
|
184
|
+
" return hex_color\n\n\n"
|
|
185
|
+
"def fits(label: str) -> bool:\n"
|
|
186
|
+
" return len(label) <= MAX_NAME_LENGTH\n"
|
|
187
|
+
)
|
|
188
|
+
issues = _run_gate_over_package(two_constant_module, consumer_body)
|
|
189
|
+
assert len(issues) == 1
|
|
190
|
+
assert "COLOR_AARRGGBB_LENGTH" in issues[0]
|
|
191
|
+
assert "MAX_NAME_LENGTH" not in issues[0]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_syntax_error_returns_empty() -> None:
|
|
195
|
+
issues = check_length_gate(
|
|
196
|
+
"def broken(\n", "/stp_contrast_fix/config/color_swatch.py"
|
|
197
|
+
)
|
|
198
|
+
assert issues == []
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Tests for check_docstring_no_network_claim_with_metadata_access — Category O6.
|
|
2
|
+
|
|
3
|
+
A docstring promising a code path returns "without touching the network" drifts
|
|
4
|
+
when the body calls a path-metadata method (``is_file``, ``stat``, ...): on a
|
|
5
|
+
network share each metadata call is a round-trip over the wire, so the no-network
|
|
6
|
+
claim is false. This is the deterministic slice of Category O6 (docstring prose
|
|
7
|
+
versus implementation drift) for a no-network claim.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_enforcer_module() -> ModuleType:
|
|
18
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
20
|
+
assert spec is not None
|
|
21
|
+
assert spec.loader is not None
|
|
22
|
+
module = importlib.util.module_from_spec(spec)
|
|
23
|
+
spec.loader.exec_module(module)
|
|
24
|
+
return module
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_docstring_no_network_claim(content: str, file_path: str) -> list[str]:
|
|
31
|
+
return code_rules_enforcer.check_docstring_no_network_claim_with_metadata_access(
|
|
32
|
+
content, file_path
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
PRODUCTION_FILE_PATH = "/project/shared_utils/bws/secret_fetch.py"
|
|
37
|
+
TEST_FILE_PATH = "/project/shared_utils/bws/tests/test_secret_fetch.py"
|
|
38
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_flags_no_network_claim_with_is_file_and_stat() -> None:
|
|
42
|
+
content = (
|
|
43
|
+
"def _ensure_local_bws_cache() -> str:\n"
|
|
44
|
+
' """Return the local cache path, repopulating when stale.\n'
|
|
45
|
+
"\n"
|
|
46
|
+
" An existing cache is returned without touching the network; a\n"
|
|
47
|
+
" missing or size-mismatched cache is repopulated from the bundled\n"
|
|
48
|
+
" executable.\n"
|
|
49
|
+
' """\n'
|
|
50
|
+
" if BUNDLED_BWS_EXECUTABLE_PATH.is_file():\n"
|
|
51
|
+
" bundled_size = BUNDLED_BWS_EXECUTABLE_PATH.stat().st_size\n"
|
|
52
|
+
" return str(LOCAL_BWS_CACHE_PATH)\n"
|
|
53
|
+
" return str(LOCAL_BWS_CACHE_PATH)\n"
|
|
54
|
+
)
|
|
55
|
+
issues = check_docstring_no_network_claim(content, PRODUCTION_FILE_PATH)
|
|
56
|
+
assert len(issues) == 1
|
|
57
|
+
assert "_ensure_local_bws_cache" in issues[0]
|
|
58
|
+
assert "without touching the network" in issues[0]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_flags_no_network_access_phrase_with_exists() -> None:
|
|
62
|
+
content = (
|
|
63
|
+
"def read_warm_cache(cache_path) -> str:\n"
|
|
64
|
+
' """Serve the warm cache with no network access."""\n'
|
|
65
|
+
" if cache_path.exists():\n"
|
|
66
|
+
" return cache_path.read_text()\n"
|
|
67
|
+
" return ''\n"
|
|
68
|
+
)
|
|
69
|
+
assert len(check_docstring_no_network_claim(content, PRODUCTION_FILE_PATH)) == 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_passes_when_claim_present_but_no_metadata_access() -> None:
|
|
73
|
+
content = (
|
|
74
|
+
"def read_warm_cache(cache_path) -> str:\n"
|
|
75
|
+
' """Serve the warm cache without touching the network."""\n'
|
|
76
|
+
" return cache_path.read_text()\n"
|
|
77
|
+
)
|
|
78
|
+
assert check_docstring_no_network_claim(content, PRODUCTION_FILE_PATH) == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_passes_when_metadata_access_but_no_network_claim() -> None:
|
|
82
|
+
content = (
|
|
83
|
+
"def _ensure_local_bws_cache(share_path) -> str:\n"
|
|
84
|
+
' """Return the local cache path, repopulating when stale.\n'
|
|
85
|
+
"\n"
|
|
86
|
+
" The bundled share is stat-checked on every call to validate the\n"
|
|
87
|
+
" cache against the bundled executable size.\n"
|
|
88
|
+
' """\n'
|
|
89
|
+
" if share_path.is_file():\n"
|
|
90
|
+
" return str(share_path)\n"
|
|
91
|
+
" return ''\n"
|
|
92
|
+
)
|
|
93
|
+
assert check_docstring_no_network_claim(content, PRODUCTION_FILE_PATH) == []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_test_files_are_exempt() -> None:
|
|
97
|
+
content = (
|
|
98
|
+
"def _ensure_local_bws_cache(share_path) -> str:\n"
|
|
99
|
+
' """Return the cache without touching the network."""\n'
|
|
100
|
+
" if share_path.is_file():\n"
|
|
101
|
+
" return str(share_path)\n"
|
|
102
|
+
" return ''\n"
|
|
103
|
+
)
|
|
104
|
+
assert check_docstring_no_network_claim(content, TEST_FILE_PATH) == []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_hook_infrastructure_is_exempt() -> None:
|
|
108
|
+
content = (
|
|
109
|
+
"def _ensure_local_bws_cache(share_path) -> str:\n"
|
|
110
|
+
' """Return the cache without touching the network."""\n'
|
|
111
|
+
" if share_path.is_file():\n"
|
|
112
|
+
" return str(share_path)\n"
|
|
113
|
+
" return ''\n"
|
|
114
|
+
)
|
|
115
|
+
assert check_docstring_no_network_claim(content, HOOK_INFRASTRUCTURE_PATH) == []
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Tests for check_docstring_documents_unreferenced_parameter.
|
|
2
|
+
|
|
3
|
+
A parameter named in the ``Args:`` block but referenced nowhere in the body is
|
|
4
|
+
dead: the function does not read it, yet the docstring describes behavior keyed
|
|
5
|
+
to it. The common shape is a flag a caller wired in before the real logic moved
|
|
6
|
+
up a level, leaving the parameter and its Args line claiming a behavior the body
|
|
7
|
+
does not implement.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_enforcer_module() -> ModuleType:
|
|
18
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
19
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
20
|
+
assert spec is not None
|
|
21
|
+
assert spec.loader is not None
|
|
22
|
+
module = importlib.util.module_from_spec(spec)
|
|
23
|
+
spec.loader.exec_module(module)
|
|
24
|
+
return module
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def check_documents_unreferenced_parameter(content: str, file_path: str) -> list[str]:
|
|
31
|
+
return code_rules_enforcer.check_docstring_documents_unreferenced_parameter(content, file_path)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def validate_content(content: str, file_path: str, old_content: str) -> list[str]:
|
|
35
|
+
return code_rules_enforcer.validate_content(content, file_path, old_content)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
PRODUCTION_FILE_PATH = "/project/src/per_theme_loop.py"
|
|
39
|
+
TEST_FILE_PATH = "/project/src/test_per_theme_loop.py"
|
|
40
|
+
HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _function_with_dead_documented_flag() -> str:
|
|
44
|
+
return (
|
|
45
|
+
"def run_per_theme_loop_and_finalize(themes: list, is_no_notify: bool) -> int:\n"
|
|
46
|
+
' """Run the per-theme loop and finalize the report.\n'
|
|
47
|
+
"\n"
|
|
48
|
+
" Args:\n"
|
|
49
|
+
" themes: The themes to process.\n"
|
|
50
|
+
" is_no_notify: When True, suppresses opening the HTML report.\n"
|
|
51
|
+
"\n"
|
|
52
|
+
" Returns:\n"
|
|
53
|
+
" The resolved exit code.\n"
|
|
54
|
+
' """\n'
|
|
55
|
+
" exit_code = 0\n"
|
|
56
|
+
" for each_theme in themes:\n"
|
|
57
|
+
" exit_code = max(exit_code, each_theme.run())\n"
|
|
58
|
+
" render_and_write_html_report(themes)\n"
|
|
59
|
+
" return exit_code\n"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_should_flag_documented_parameter_never_referenced() -> None:
|
|
64
|
+
issues = check_documents_unreferenced_parameter(
|
|
65
|
+
_function_with_dead_documented_flag(), PRODUCTION_FILE_PATH
|
|
66
|
+
)
|
|
67
|
+
assert any("is_no_notify" in each for each in issues), (
|
|
68
|
+
f"Expected dead 'is_no_notify' flag, got: {issues!r}"
|
|
69
|
+
)
|
|
70
|
+
assert len(issues) == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_should_not_flag_parameter_referenced_in_body() -> None:
|
|
74
|
+
source = (
|
|
75
|
+
"def run_per_theme_loop_and_finalize(themes: list, is_no_notify: bool) -> int:\n"
|
|
76
|
+
' """Run the per-theme loop and finalize the report.\n'
|
|
77
|
+
"\n"
|
|
78
|
+
" Args:\n"
|
|
79
|
+
" themes: The themes to process.\n"
|
|
80
|
+
" is_no_notify: When True, suppresses opening the HTML report.\n"
|
|
81
|
+
"\n"
|
|
82
|
+
" Returns:\n"
|
|
83
|
+
" The resolved exit code.\n"
|
|
84
|
+
' """\n'
|
|
85
|
+
" exit_code = 0\n"
|
|
86
|
+
" for each_theme in themes:\n"
|
|
87
|
+
" exit_code = max(exit_code, each_theme.run())\n"
|
|
88
|
+
" if not is_no_notify:\n"
|
|
89
|
+
" open_html_report(themes)\n"
|
|
90
|
+
" return exit_code\n"
|
|
91
|
+
)
|
|
92
|
+
issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
|
|
93
|
+
assert issues == [], f"Referenced parameter must not be flagged, got: {issues!r}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_should_not_flag_when_kwargs_present() -> None:
|
|
97
|
+
source = (
|
|
98
|
+
"def render(themes: list, is_no_notify: bool, **overrides) -> int:\n"
|
|
99
|
+
' """Render the report.\n'
|
|
100
|
+
"\n"
|
|
101
|
+
" Args:\n"
|
|
102
|
+
" themes: The themes to process.\n"
|
|
103
|
+
" is_no_notify: When True, suppresses opening the report.\n"
|
|
104
|
+
' """\n'
|
|
105
|
+
" settings = dict(overrides)\n"
|
|
106
|
+
" for each_theme in themes:\n"
|
|
107
|
+
" each_theme.render(settings)\n"
|
|
108
|
+
" return 0\n"
|
|
109
|
+
)
|
|
110
|
+
issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
|
|
111
|
+
assert issues == [], f"**kwargs functions must be skipped, got: {issues!r}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_should_skip_private_function() -> None:
|
|
115
|
+
source = (
|
|
116
|
+
"def _drive(themes: list, is_no_notify: bool) -> int:\n"
|
|
117
|
+
' """Drive internally.\n'
|
|
118
|
+
"\n"
|
|
119
|
+
" Args:\n"
|
|
120
|
+
" themes: The themes to process.\n"
|
|
121
|
+
" is_no_notify: When True, suppresses the report.\n"
|
|
122
|
+
' """\n'
|
|
123
|
+
" exit_code = 0\n"
|
|
124
|
+
" for each_theme in themes:\n"
|
|
125
|
+
" exit_code = max(exit_code, each_theme.run())\n"
|
|
126
|
+
" return exit_code\n"
|
|
127
|
+
)
|
|
128
|
+
issues = check_documents_unreferenced_parameter(source, PRODUCTION_FILE_PATH)
|
|
129
|
+
assert issues == [], f"Private functions exempt, got: {issues!r}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_should_skip_test_file() -> None:
|
|
133
|
+
issues = check_documents_unreferenced_parameter(
|
|
134
|
+
_function_with_dead_documented_flag(), TEST_FILE_PATH
|
|
135
|
+
)
|
|
136
|
+
assert issues == [], f"Test files exempt, got: {issues!r}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_should_skip_hook_infrastructure() -> None:
|
|
140
|
+
issues = check_documents_unreferenced_parameter(
|
|
141
|
+
_function_with_dead_documented_flag(), HOOK_INFRASTRUCTURE_PATH
|
|
142
|
+
)
|
|
143
|
+
assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_should_handle_syntax_error_gracefully() -> None:
|
|
147
|
+
issues = check_documents_unreferenced_parameter("def fetch(\n", PRODUCTION_FILE_PATH)
|
|
148
|
+
assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_validate_content_surfaces_unreferenced_parameter() -> None:
|
|
152
|
+
issues = validate_content(
|
|
153
|
+
_function_with_dead_documented_flag(), PRODUCTION_FILE_PATH, old_content=""
|
|
154
|
+
)
|
|
155
|
+
matching_issues = [
|
|
156
|
+
each for each in issues if "is_no_notify" in each and "never references" in each
|
|
157
|
+
]
|
|
158
|
+
assert matching_issues, (
|
|
159
|
+
f"Expected validate_content to surface the dead-parameter issue, got: {issues!r}"
|
|
160
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Enforcer-dispatch tests for the JS returns-object schema-less branch check.
|
|
2
|
+
|
|
3
|
+
These drive ``validate_content`` end-to-end on a ``.mjs`` payload, so they prove
|
|
4
|
+
the check is wired into the JavaScript branch of the enforcer, not just callable
|
|
5
|
+
in isolation. The drift: a ``function`` whose JSDoc ``@returns {Promise<object>}``
|
|
6
|
+
promises a structured object while one branch returns the agent helper with an
|
|
7
|
+
options object that omits ``schema`` and so resolves to a transcript string.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib.util
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import ModuleType
|
|
16
|
+
|
|
17
|
+
_HOOKS_DIRECTORY = str(Path(__file__).resolve().parent.parent)
|
|
18
|
+
if _HOOKS_DIRECTORY not in sys.path:
|
|
19
|
+
sys.path.insert(0, _HOOKS_DIRECTORY)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_enforcer_module() -> ModuleType:
|
|
23
|
+
module_path = Path(__file__).parent / "code_rules_enforcer.py"
|
|
24
|
+
spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
|
|
25
|
+
assert spec is not None
|
|
26
|
+
assert spec.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
code_rules_enforcer = _load_enforcer_module()
|
|
33
|
+
|
|
34
|
+
_MJS_PATH = "skills/autoconverge/workflow/converge.mjs"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _mixed_schema_mjs() -> str:
|
|
38
|
+
return (
|
|
39
|
+
"/**\n"
|
|
40
|
+
" * Spawn a git-utility agent.\n"
|
|
41
|
+
" * @returns {Promise<object>} the structured output\n"
|
|
42
|
+
" */\n"
|
|
43
|
+
"function runGitTask(task, head) {\n"
|
|
44
|
+
" if (task === 'resolve-head') {\n"
|
|
45
|
+
" return convergeAgent(`head ${x}`, { label: 'g', schema: HEAD_SCHEMA, agentType: 'Explore' })\n"
|
|
46
|
+
" }\n"
|
|
47
|
+
" return convergeAgent(`fetch ${x}`, { label: 'g', agentType: 'Explore' })\n"
|
|
48
|
+
"}\n"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _all_schema_mjs() -> str:
|
|
53
|
+
return _mixed_schema_mjs().replace(
|
|
54
|
+
"{ label: 'g', agentType: 'Explore' })",
|
|
55
|
+
"{ label: 'g', schema: FETCH_SCHEMA, agentType: 'Explore' })",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _has_returns_object_finding(all_issues: list[str]) -> bool:
|
|
60
|
+
return any("runGitTask" in each_issue and "@returns" in each_issue for each_issue in all_issues)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_enforcer_reports_schema_less_branch_under_returns_object() -> None:
|
|
64
|
+
drift_source = _mixed_schema_mjs()
|
|
65
|
+
all_issues = code_rules_enforcer.validate_content(drift_source, _MJS_PATH, drift_source)
|
|
66
|
+
assert _has_returns_object_finding(all_issues)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_enforcer_accepts_every_branch_with_a_schema() -> None:
|
|
70
|
+
clean_source = _all_schema_mjs()
|
|
71
|
+
all_issues = code_rules_enforcer.validate_content(clean_source, _MJS_PATH, clean_source)
|
|
72
|
+
assert not _has_returns_object_finding(all_issues)
|