claude-dev-env 1.81.0 → 1.83.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/CLAUDE.md +16 -13
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
- package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/code_rules_constants_config.py +159 -1
- package/hooks/blocking/code_rules_docstrings.py +312 -9
- package/hooks/blocking/code_rules_enforcer.py +29 -0
- package/hooks/blocking/code_rules_imports_logging.py +867 -1
- package/hooks/blocking/code_rules_naming_collection.py +141 -0
- package/hooks/blocking/code_rules_string_magic.py +68 -0
- package/hooks/blocking/conventional_pr_title_gate.py +444 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/reviewer_spawn_gate.py +182 -0
- package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
- package/hooks/blocking/state_description_blocker.py +96 -5
- package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
- package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
- package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
- package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
- package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
- package/hooks/blocking/test_conventional_pr_title_gate.py +640 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
- package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
- package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
- package/hooks/blocking/test_state_description_blocker.py +135 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/blocking_check_limits.py +43 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +58 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
- package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +4 -1
- package/rules/claude-md-orphan-file.md +5 -0
- package/rules/docstring-prose-matches-implementation.md +10 -1
- package/rules/env-var-table-code-drift.md +5 -0
- package/rules/es-exe-file-search.md +17 -0
- package/rules/no-historical-clutter.md +12 -1
- package/rules/orphan-css-class.md +5 -0
- package/rules/package-inventory-stale-entry.md +10 -0
- package/rules/paired-test-coverage.md +5 -0
- package/rules/plain-illustrative-docstrings.md +5 -0
- package/rules/verify-before-asking.md +7 -0
- package/rules/verify-runtime-state.md +40 -0
- package/rules/windows-filesystem-safe.md +8 -0
- package/rules/workers-done-before-complete.md +33 -0
- package/rules/workflow-substitution-slots.md +5 -0
- package/skills/CLAUDE.md +1 -1
- package/skills/autoconverge/SKILL.md +96 -5
- package/skills/autoconverge/reference/gotchas.md +5 -5
- package/skills/autoconverge/workflow/converge.contract.test.mjs +228 -27
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +154 -21
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +153 -49
- package/skills/autoconverge/workflow/converge_multi.mjs +6 -2
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +26 -0
- package/skills/everything-search/SKILL.md +5 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Tests for the naive-datetime-construction check in code_rules_imports_logging.py.
|
|
2
|
+
|
|
3
|
+
The check catches the DST-fold-ambiguity class Copilot flagged on
|
|
4
|
+
python-automation PR #557: ``datetime.fromtimestamp(epoch)`` builds a datetime
|
|
5
|
+
with no timezone, and a later ``.astimezone()`` on that naive value has to guess
|
|
6
|
+
the local offset. The check flags the naive-producing constructors
|
|
7
|
+
(``fromtimestamp``/``utcfromtimestamp`` without ``tz=``, and ``utcnow``) and
|
|
8
|
+
leaves a ``tz=``-carrying call, and a plain ``now()``, alone.
|
|
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
|
+
PRODUCTION_FILE_PATH = "/project/src/export_verification.py"
|
|
31
|
+
TEST_FILE_PATH = "/project/src/test_export_verification.py"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def check_naive_datetime_construction(content: str, file_path: str) -> list[str]:
|
|
35
|
+
return code_rules_enforcer.check_naive_datetime_construction(content, file_path)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_flags_fromtimestamp_without_timezone() -> None:
|
|
39
|
+
content = (
|
|
40
|
+
"from datetime import datetime\n"
|
|
41
|
+
"def describe(stamp: float) -> str:\n"
|
|
42
|
+
" return datetime.fromtimestamp(stamp).astimezone().isoformat()\n"
|
|
43
|
+
)
|
|
44
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
45
|
+
assert len(issues) == 1
|
|
46
|
+
assert "fromtimestamp" in issues[0]
|
|
47
|
+
assert "tz=" in issues[0]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_flags_utcfromtimestamp_and_utcnow() -> None:
|
|
51
|
+
content = (
|
|
52
|
+
"from datetime import datetime\n"
|
|
53
|
+
"def build(stamp: float) -> tuple[object, object]:\n"
|
|
54
|
+
" return datetime.utcfromtimestamp(stamp), datetime.utcnow()\n"
|
|
55
|
+
)
|
|
56
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
57
|
+
assert len(issues) == 2
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_flags_module_qualified_datetime_call() -> None:
|
|
61
|
+
content = (
|
|
62
|
+
"import datetime\n"
|
|
63
|
+
"def describe(stamp: float) -> object:\n"
|
|
64
|
+
" return datetime.datetime.fromtimestamp(stamp)\n"
|
|
65
|
+
)
|
|
66
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
67
|
+
assert len(issues) == 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_allows_fromtimestamp_with_timezone_keyword() -> None:
|
|
71
|
+
content = (
|
|
72
|
+
"from datetime import datetime, timezone\n"
|
|
73
|
+
"def describe(stamp: float) -> str:\n"
|
|
74
|
+
" return datetime.fromtimestamp(stamp, tz=timezone.utc).isoformat()\n"
|
|
75
|
+
)
|
|
76
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
77
|
+
assert issues == []
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_allows_fromtimestamp_with_positional_timezone() -> None:
|
|
81
|
+
content = (
|
|
82
|
+
"from datetime import datetime, timezone\n"
|
|
83
|
+
"def describe(stamp: float) -> str:\n"
|
|
84
|
+
" return datetime.fromtimestamp(stamp, timezone.utc).isoformat()\n"
|
|
85
|
+
)
|
|
86
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
87
|
+
assert issues == []
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_allows_module_qualified_fromtimestamp_with_positional_timezone() -> None:
|
|
91
|
+
content = (
|
|
92
|
+
"import datetime\n"
|
|
93
|
+
"def describe(stamp: float) -> object:\n"
|
|
94
|
+
" return datetime.datetime.fromtimestamp(stamp, datetime.timezone.utc)\n"
|
|
95
|
+
)
|
|
96
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
97
|
+
assert issues == []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_flags_utcfromtimestamp_with_second_positional_argument() -> None:
|
|
101
|
+
content = (
|
|
102
|
+
"from datetime import datetime\n"
|
|
103
|
+
"def describe(stamp: float, other: float) -> object:\n"
|
|
104
|
+
" return datetime.utcfromtimestamp(stamp, other)\n"
|
|
105
|
+
)
|
|
106
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
107
|
+
assert len(issues) == 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_ignores_unrelated_datetime_attribute_chain() -> None:
|
|
111
|
+
content = (
|
|
112
|
+
"def describe(foo: object, stamp: float) -> object:\n"
|
|
113
|
+
" return foo.datetime.fromtimestamp(stamp)\n"
|
|
114
|
+
)
|
|
115
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
116
|
+
assert issues == []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_flags_fromtimestamp_with_none_timezone_keyword() -> None:
|
|
120
|
+
content = (
|
|
121
|
+
"from datetime import datetime\n"
|
|
122
|
+
"def describe(stamp: float) -> object:\n"
|
|
123
|
+
" return datetime.fromtimestamp(stamp, tz=None)\n"
|
|
124
|
+
)
|
|
125
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
126
|
+
assert len(issues) == 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_flags_fromtimestamp_with_none_positional_timezone() -> None:
|
|
130
|
+
content = (
|
|
131
|
+
"from datetime import datetime\n"
|
|
132
|
+
"def describe(stamp: float) -> object:\n"
|
|
133
|
+
" return datetime.fromtimestamp(stamp, None)\n"
|
|
134
|
+
)
|
|
135
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
136
|
+
assert len(issues) == 1
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_flags_utcfromtimestamp_even_with_timezone_keyword() -> None:
|
|
140
|
+
content = (
|
|
141
|
+
"from datetime import datetime, timezone\n"
|
|
142
|
+
"def describe(stamp: float) -> object:\n"
|
|
143
|
+
" return datetime.utcfromtimestamp(stamp, tz=timezone.utc)\n"
|
|
144
|
+
)
|
|
145
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
146
|
+
assert len(issues) == 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_utcnow_remediation_points_at_now_not_utcnow_tz() -> None:
|
|
150
|
+
content = (
|
|
151
|
+
"from datetime import datetime\n"
|
|
152
|
+
"def stamp() -> object:\n"
|
|
153
|
+
" return datetime.utcnow()\n"
|
|
154
|
+
)
|
|
155
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
156
|
+
assert len(issues) == 1
|
|
157
|
+
assert "now(tz=" in issues[0]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_utcfromtimestamp_remediation_points_at_fromtimestamp() -> None:
|
|
161
|
+
content = (
|
|
162
|
+
"from datetime import datetime\n"
|
|
163
|
+
"def build(stamp: float) -> object:\n"
|
|
164
|
+
" return datetime.utcfromtimestamp(stamp)\n"
|
|
165
|
+
)
|
|
166
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
167
|
+
assert len(issues) == 1
|
|
168
|
+
assert "fromtimestamp(" in issues[0]
|
|
169
|
+
assert "tz=" in issues[0]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_utcfromtimestamp_remediation_uses_generic_placeholder() -> None:
|
|
173
|
+
content = (
|
|
174
|
+
"from datetime import datetime\n"
|
|
175
|
+
"def build(epoch_seconds: float) -> object:\n"
|
|
176
|
+
" return datetime.utcfromtimestamp(epoch_seconds)\n"
|
|
177
|
+
)
|
|
178
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
179
|
+
assert len(issues) == 1
|
|
180
|
+
assert "..., tz=" in issues[0]
|
|
181
|
+
assert "stamp, tz=" not in issues[0]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_remediation_does_not_mandate_astimezone() -> None:
|
|
185
|
+
content = (
|
|
186
|
+
"from datetime import datetime\n"
|
|
187
|
+
"def stamp() -> object:\n"
|
|
188
|
+
" return datetime.utcnow()\n"
|
|
189
|
+
)
|
|
190
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
191
|
+
assert len(issues) == 1
|
|
192
|
+
assert "then .astimezone()" not in issues[0]
|
|
193
|
+
assert "only when local display" in issues[0]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_allows_plain_now() -> None:
|
|
197
|
+
content = (
|
|
198
|
+
"from datetime import datetime\n"
|
|
199
|
+
"def stamp() -> str:\n"
|
|
200
|
+
" return datetime.now().isoformat()\n"
|
|
201
|
+
)
|
|
202
|
+
issues = check_naive_datetime_construction(content, PRODUCTION_FILE_PATH)
|
|
203
|
+
assert issues == []
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_exempts_test_files() -> None:
|
|
207
|
+
content = (
|
|
208
|
+
"from datetime import datetime\n"
|
|
209
|
+
"def describe(stamp: float) -> object:\n"
|
|
210
|
+
" return datetime.fromtimestamp(stamp)\n"
|
|
211
|
+
)
|
|
212
|
+
issues = check_naive_datetime_construction(content, TEST_FILE_PATH)
|
|
213
|
+
assert issues == []
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import importlib.util
|
|
5
|
+
|
|
6
|
+
ENFORCER_PATH = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
7
|
+
specification = importlib.util.spec_from_file_location("code_rules_enforcer", ENFORCER_PATH)
|
|
8
|
+
code_rules_enforcer = importlib.util.module_from_spec(specification)
|
|
9
|
+
specification.loader.exec_module(code_rules_enforcer)
|
|
10
|
+
|
|
11
|
+
PRODUCTION_FILE_PATH = "packages/app/services/foo.py"
|
|
12
|
+
CONFTEST_FILE_PATH = "packages/app/tests/conftest.py"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_should_flag_referenced_underscore_loop_variable_in_conftest() -> None:
|
|
16
|
+
source = (
|
|
17
|
+
"import sys\n"
|
|
18
|
+
"\n"
|
|
19
|
+
"for _foreign_module_name in ['a', 'b']:\n"
|
|
20
|
+
" del sys.modules[_foreign_module_name]\n"
|
|
21
|
+
)
|
|
22
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
23
|
+
source, CONFTEST_FILE_PATH
|
|
24
|
+
)
|
|
25
|
+
assert any("_foreign_module_name" in each_issue for each_issue in issues), (
|
|
26
|
+
f"Expected referenced underscore loop variable flagged, got: {issues}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_should_flag_referenced_underscore_loop_variable_in_production() -> None:
|
|
31
|
+
source = (
|
|
32
|
+
"import sys\n"
|
|
33
|
+
"\n"
|
|
34
|
+
"def purge(all_names: list) -> None:\n"
|
|
35
|
+
" for _name in all_names:\n"
|
|
36
|
+
" del sys.modules[_name]\n"
|
|
37
|
+
)
|
|
38
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
39
|
+
source, PRODUCTION_FILE_PATH
|
|
40
|
+
)
|
|
41
|
+
assert any("_name" in each_issue for each_issue in issues), (
|
|
42
|
+
f"Expected referenced underscore loop variable flagged, got: {issues}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_should_not_flag_unreferenced_underscore_throwaway() -> None:
|
|
47
|
+
source = "def spin(count: int) -> None:\n for _unused in range(count):\n pass\n"
|
|
48
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
49
|
+
source, PRODUCTION_FILE_PATH
|
|
50
|
+
)
|
|
51
|
+
assert issues == [], f"Genuinely unused underscore throwaway must pass, got: {issues}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_should_not_flag_bare_underscore_target() -> None:
|
|
55
|
+
source = "def spin(count: int) -> None:\n for _ in range(count):\n return None\n"
|
|
56
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
57
|
+
source, PRODUCTION_FILE_PATH
|
|
58
|
+
)
|
|
59
|
+
assert issues == [], f"Bare '_' target must pass, got: {issues}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_should_not_flag_named_loop_variable_without_underscore() -> None:
|
|
63
|
+
source = (
|
|
64
|
+
"import sys\n\nfor each_module_name in ['a', 'b']:\n del sys.modules[each_module_name]\n"
|
|
65
|
+
)
|
|
66
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
67
|
+
source, CONFTEST_FILE_PATH
|
|
68
|
+
)
|
|
69
|
+
assert issues == [], f"each_-prefixed loop variable without underscore must pass, got: {issues}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_should_not_flag_underscore_loop_variable_shadowed_by_comprehension() -> None:
|
|
73
|
+
source = (
|
|
74
|
+
"def collect(all_names: list, other: list) -> list:\n"
|
|
75
|
+
" all_other: list = []\n"
|
|
76
|
+
" for _name in all_names:\n"
|
|
77
|
+
" all_other = [_name for _name in other]\n"
|
|
78
|
+
" return all_other\n"
|
|
79
|
+
)
|
|
80
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
81
|
+
source, PRODUCTION_FILE_PATH
|
|
82
|
+
)
|
|
83
|
+
assert issues == [], (
|
|
84
|
+
f"Comprehension rebinding the loop name shadows the outer read, must pass, got: {issues}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_should_flag_underscore_loop_variable_read_inside_non_rebinding_comprehension() -> None:
|
|
89
|
+
source = (
|
|
90
|
+
"def collect(all_names: list, other: list) -> list:\n"
|
|
91
|
+
" matches: list = []\n"
|
|
92
|
+
" for _name in all_names:\n"
|
|
93
|
+
" matches = [each_row for each_row in other if each_row == _name]\n"
|
|
94
|
+
" return matches\n"
|
|
95
|
+
)
|
|
96
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
97
|
+
source, PRODUCTION_FILE_PATH
|
|
98
|
+
)
|
|
99
|
+
assert any("_name" in each_issue for each_issue in issues), (
|
|
100
|
+
f"A comprehension reading the outer loop name without rebinding it must flag, got: {issues}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_should_not_flag_underscore_loop_variable_shadowed_by_lambda_parameter() -> None:
|
|
105
|
+
source = (
|
|
106
|
+
"def build(all_names: list) -> list:\n"
|
|
107
|
+
" all_callables: list = []\n"
|
|
108
|
+
" for _name in all_names:\n"
|
|
109
|
+
" all_callables.append(lambda _name: _name)\n"
|
|
110
|
+
" return all_callables\n"
|
|
111
|
+
)
|
|
112
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
113
|
+
source, PRODUCTION_FILE_PATH
|
|
114
|
+
)
|
|
115
|
+
assert issues == [], (
|
|
116
|
+
f"A lambda parameter shadowing the loop name owns its body, must pass, got: {issues}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_should_not_flag_underscore_loop_variable_shadowed_by_nested_def_parameter() -> None:
|
|
121
|
+
source = (
|
|
122
|
+
"def build(all_names: list) -> None:\n"
|
|
123
|
+
" for _handler in all_names:\n"
|
|
124
|
+
" def inner(_handler: object) -> object:\n"
|
|
125
|
+
" return _handler\n"
|
|
126
|
+
)
|
|
127
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
128
|
+
source, PRODUCTION_FILE_PATH
|
|
129
|
+
)
|
|
130
|
+
assert issues == [], (
|
|
131
|
+
f"A nested-def parameter shadowing the loop name owns its body, must pass, got: {issues}"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_should_not_flag_outer_underscore_loop_variable_shadowed_by_nested_loop() -> None:
|
|
136
|
+
source = (
|
|
137
|
+
"import sys\n"
|
|
138
|
+
"\n"
|
|
139
|
+
"def purge(first: list, second: list) -> None:\n"
|
|
140
|
+
" for _name in first:\n"
|
|
141
|
+
" for _name in second:\n"
|
|
142
|
+
" del sys.modules[_name]\n"
|
|
143
|
+
)
|
|
144
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
145
|
+
source, PRODUCTION_FILE_PATH
|
|
146
|
+
)
|
|
147
|
+
assert not any("Line 4" in each_issue for each_issue in issues), (
|
|
148
|
+
f"Outer loop whose name a nested loop rebinds is never read in the outer scope, "
|
|
149
|
+
f"got: {issues}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_should_flag_underscore_loop_variable_read_before_nested_shadowing_loop() -> None:
|
|
154
|
+
source = (
|
|
155
|
+
"import sys\n"
|
|
156
|
+
"\n"
|
|
157
|
+
"def purge(first: list, second: list) -> None:\n"
|
|
158
|
+
" for _name in first:\n"
|
|
159
|
+
" del sys.modules[_name]\n"
|
|
160
|
+
" for _name in second:\n"
|
|
161
|
+
" pass\n"
|
|
162
|
+
)
|
|
163
|
+
issues = code_rules_enforcer.check_referenced_underscore_loop_variable(
|
|
164
|
+
source, PRODUCTION_FILE_PATH
|
|
165
|
+
)
|
|
166
|
+
assert any("Line 4" in each_issue for each_issue in issues), (
|
|
167
|
+
f"The outer loop body reads its own name before the nested loop rebinds it, "
|
|
168
|
+
f"must flag, got: {issues}"
|
|
169
|
+
)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Tests for check_js_bare_flag_return_directive.
|
|
2
|
+
|
|
3
|
+
The check catches a JS/.mjs self-contradiction. A converge workflow states a
|
|
4
|
+
full result-object contract that rules out a bare status flag ("the full down
|
|
5
|
+
result {sha, clean:false, down:true, findings:[]}, never a bare down flag"),
|
|
6
|
+
then, in its agent-prompt prose, tells the agent to "return down: true" — the
|
|
7
|
+
same bare flag the workflow just ruled out. A StructuredOutput run whose
|
|
8
|
+
schema needs every field would reject a lone {down:true}, so the shorthand
|
|
9
|
+
drifts from the contract stated a couple of lines above.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_HOOK_DIRECTORY = Path(__file__).parent
|
|
18
|
+
if str(_HOOK_DIRECTORY) not in sys.path:
|
|
19
|
+
sys.path.insert(0, str(_HOOK_DIRECTORY))
|
|
20
|
+
|
|
21
|
+
from code_rules_imports_logging import ( # noqa: E402
|
|
22
|
+
check_js_bare_flag_return_directive,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_MJS_PATH = "skills/autoconverge/workflow/converge.mjs"
|
|
26
|
+
|
|
27
|
+
_DOWN_CONTRACT = (
|
|
28
|
+
"the full down result {sha, clean:false, down:true, findings:[]}, "
|
|
29
|
+
"never a bare down flag"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _contract_with_bare_directive_source() -> str:
|
|
34
|
+
return (
|
|
35
|
+
"const PREAMBLE =\n"
|
|
36
|
+
f" 'When the run carries a result schema, {_DOWN_CONTRACT}.'\n\n"
|
|
37
|
+
"function gate(head) {\n"
|
|
38
|
+
" return spawn(\n"
|
|
39
|
+
" `Poll for the review; once the budget runs out with no review on "
|
|
40
|
+
"HEAD, return down: true.`,\n"
|
|
41
|
+
" { label: 'gate' },\n"
|
|
42
|
+
" )\n"
|
|
43
|
+
"}\n"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _contract_with_full_result_source() -> str:
|
|
48
|
+
return _contract_with_bare_directive_source().replace(
|
|
49
|
+
"return down: true",
|
|
50
|
+
"return {sha:`${head}`, clean:false, down:true, findings:[]}",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _bare_directive_without_contract_source() -> str:
|
|
55
|
+
return (
|
|
56
|
+
"function gate(head) {\n"
|
|
57
|
+
" return spawn(\n"
|
|
58
|
+
" `Poll for a review; once the budget runs out, return down: true.`,\n"
|
|
59
|
+
" { label: 'gate' },\n"
|
|
60
|
+
" )\n"
|
|
61
|
+
"}\n"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _return_the_result_phrase_source() -> str:
|
|
66
|
+
return (
|
|
67
|
+
"const PREAMBLE = 'the full down result {...}, never a bare down flag'\n\n"
|
|
68
|
+
"function gate(head) {\n"
|
|
69
|
+
" return spawn(\n"
|
|
70
|
+
" `Out-of-usage notice on HEAD -> return the down result above and stop.`,\n"
|
|
71
|
+
" { label: 'gate' },\n"
|
|
72
|
+
" )\n"
|
|
73
|
+
"}\n"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _contract_with_bare_false_directive_source() -> str:
|
|
78
|
+
return (
|
|
79
|
+
"const PREAMBLE =\n"
|
|
80
|
+
f" 'When the run carries a result schema, {_DOWN_CONTRACT}.'\n\n"
|
|
81
|
+
"function gate(head) {\n"
|
|
82
|
+
" return spawn(\n"
|
|
83
|
+
" `Poll for the review; once HEAD stays clean with no findings, "
|
|
84
|
+
"return down: false.`,\n"
|
|
85
|
+
" { label: 'gate' },\n"
|
|
86
|
+
" )\n"
|
|
87
|
+
"}\n"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _other_flag_name_source() -> str:
|
|
92
|
+
return (
|
|
93
|
+
"const PREAMBLE = 'the full result {clean, stale}, never a bare stale flag'\n\n"
|
|
94
|
+
"function gate(head) {\n"
|
|
95
|
+
" return spawn(\n"
|
|
96
|
+
" `If nothing lands in the window, return stale: true.`,\n"
|
|
97
|
+
" { label: 'gate' },\n"
|
|
98
|
+
" )\n"
|
|
99
|
+
"}\n"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _capitalized_contract_with_bare_directive_source() -> str:
|
|
104
|
+
return (
|
|
105
|
+
"const PREAMBLE =\n"
|
|
106
|
+
" 'When the run carries a result schema, the full down result "
|
|
107
|
+
"{sha, clean:false, down:true, findings:[]}, Never a bare Down flag.'\n\n"
|
|
108
|
+
"function gate(head) {\n"
|
|
109
|
+
" return spawn(\n"
|
|
110
|
+
" `Poll for the review; once the budget runs out with no review on "
|
|
111
|
+
"HEAD, return down: true.`,\n"
|
|
112
|
+
" { label: 'gate' },\n"
|
|
113
|
+
" )\n"
|
|
114
|
+
"}\n"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _contract_with_capitalized_directive_source() -> str:
|
|
119
|
+
return (
|
|
120
|
+
"const PREAMBLE =\n"
|
|
121
|
+
f" 'When the run carries a result schema, {_DOWN_CONTRACT}.'\n\n"
|
|
122
|
+
"function gate(head) {\n"
|
|
123
|
+
" return spawn(\n"
|
|
124
|
+
" `Poll for the review; once the budget runs out with no review on "
|
|
125
|
+
"HEAD, Return down: true.`,\n"
|
|
126
|
+
" { label: 'gate' },\n"
|
|
127
|
+
" )\n"
|
|
128
|
+
"}\n"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_flags_bare_down_directive_when_the_workflow_states_the_contract() -> None:
|
|
133
|
+
issues = check_js_bare_flag_return_directive(
|
|
134
|
+
_contract_with_bare_directive_source(), _MJS_PATH
|
|
135
|
+
)
|
|
136
|
+
assert len(issues) == 1
|
|
137
|
+
assert "return down" in issues[0]
|
|
138
|
+
assert "never a bare down flag" in issues[0]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_flags_bare_down_false_directive_when_the_workflow_states_the_contract() -> None:
|
|
142
|
+
issues = check_js_bare_flag_return_directive(
|
|
143
|
+
_contract_with_bare_false_directive_source(), _MJS_PATH
|
|
144
|
+
)
|
|
145
|
+
assert len(issues) == 1
|
|
146
|
+
assert "return down" in issues[0]
|
|
147
|
+
assert "never a bare down flag" in issues[0]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_generalizes_to_any_flag_name_the_contract_rules_out() -> None:
|
|
151
|
+
issues = check_js_bare_flag_return_directive(_other_flag_name_source(), _MJS_PATH)
|
|
152
|
+
assert len(issues) == 1
|
|
153
|
+
assert "return stale" in issues[0]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_accepts_a_full_result_object_return() -> None:
|
|
157
|
+
issues = check_js_bare_flag_return_directive(
|
|
158
|
+
_contract_with_full_result_source(), _MJS_PATH
|
|
159
|
+
)
|
|
160
|
+
assert issues == []
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_accepts_a_bare_directive_when_the_workflow_states_no_contract() -> None:
|
|
164
|
+
issues = check_js_bare_flag_return_directive(
|
|
165
|
+
_bare_directive_without_contract_source(), _MJS_PATH
|
|
166
|
+
)
|
|
167
|
+
assert issues == []
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_accepts_return_the_result_prose_that_names_no_bare_flag() -> None:
|
|
171
|
+
issues = check_js_bare_flag_return_directive(
|
|
172
|
+
_return_the_result_phrase_source(), _MJS_PATH
|
|
173
|
+
)
|
|
174
|
+
assert issues == []
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_flags_bare_directive_when_contract_uses_capitalized_never() -> None:
|
|
178
|
+
source = _capitalized_contract_with_bare_directive_source()
|
|
179
|
+
issues = check_js_bare_flag_return_directive(source, _MJS_PATH)
|
|
180
|
+
assert len(issues) == 1
|
|
181
|
+
assert "return down" in issues[0]
|
|
182
|
+
quoted_contract_phrase = "Never a bare Down flag"
|
|
183
|
+
assert quoted_contract_phrase in issues[0]
|
|
184
|
+
assert quoted_contract_phrase in source
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_flags_capitalized_return_directive_against_lowercase_contract() -> None:
|
|
188
|
+
issues = check_js_bare_flag_return_directive(
|
|
189
|
+
_contract_with_capitalized_directive_source(), _MJS_PATH
|
|
190
|
+
)
|
|
191
|
+
assert len(issues) == 1
|
|
192
|
+
assert "never a bare down flag" in issues[0]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_skips_python_files() -> None:
|
|
196
|
+
issues = check_js_bare_flag_return_directive(
|
|
197
|
+
_contract_with_bare_directive_source(), "workflow/converge.py"
|
|
198
|
+
)
|
|
199
|
+
assert issues == []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _contract_with_six_bare_directives_source() -> str:
|
|
203
|
+
directive_lines = "\n".join(
|
|
204
|
+
f" gate{each_index}(head) {{ return spawn(`return down: true.`) }}"
|
|
205
|
+
for each_index in range(1, 7)
|
|
206
|
+
)
|
|
207
|
+
return (
|
|
208
|
+
"const PREAMBLE =\n"
|
|
209
|
+
f" 'When the run carries a result schema, {_DOWN_CONTRACT}.'\n\n"
|
|
210
|
+
f"{directive_lines}\n"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_flags_directive_when_only_directive_line_changed_but_contract_sits_elsewhere() -> None:
|
|
215
|
+
directive_line_number = 6
|
|
216
|
+
issues = check_js_bare_flag_return_directive(
|
|
217
|
+
_contract_with_bare_directive_source(),
|
|
218
|
+
_MJS_PATH,
|
|
219
|
+
all_changed_lines={directive_line_number},
|
|
220
|
+
)
|
|
221
|
+
assert len(issues) == 1
|
|
222
|
+
assert "return down" in issues[0]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_flags_when_only_the_contract_line_changed_and_the_directive_line_did_not() -> None:
|
|
226
|
+
contract_line_number = 2
|
|
227
|
+
issues = check_js_bare_flag_return_directive(
|
|
228
|
+
_contract_with_bare_directive_source(),
|
|
229
|
+
_MJS_PATH,
|
|
230
|
+
all_changed_lines={contract_line_number},
|
|
231
|
+
)
|
|
232
|
+
assert len(issues) == 1
|
|
233
|
+
assert "return down" in issues[0]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_accepts_when_neither_the_contract_nor_the_directive_line_changed() -> None:
|
|
237
|
+
unrelated_line_number = 3
|
|
238
|
+
issues = check_js_bare_flag_return_directive(
|
|
239
|
+
_contract_with_bare_directive_source(),
|
|
240
|
+
_MJS_PATH,
|
|
241
|
+
all_changed_lines={unrelated_line_number},
|
|
242
|
+
)
|
|
243
|
+
assert issues == []
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_flags_a_changed_line_violation_past_the_cap_of_unchanged_ones() -> None:
|
|
247
|
+
sixth_directive_line_number = 9
|
|
248
|
+
issues = check_js_bare_flag_return_directive(
|
|
249
|
+
_contract_with_six_bare_directives_source(),
|
|
250
|
+
_MJS_PATH,
|
|
251
|
+
all_changed_lines={sixth_directive_line_number},
|
|
252
|
+
)
|
|
253
|
+
assert len(issues) == 1
|
|
254
|
+
assert f"Line {sixth_directive_line_number}:" in issues[0]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def test_defer_scope_to_caller_returns_the_finding_regardless_of_changed_lines() -> None:
|
|
258
|
+
unrelated_line_number = 1
|
|
259
|
+
issues = check_js_bare_flag_return_directive(
|
|
260
|
+
_contract_with_bare_directive_source(),
|
|
261
|
+
_MJS_PATH,
|
|
262
|
+
all_changed_lines={unrelated_line_number},
|
|
263
|
+
defer_scope_to_caller=True,
|
|
264
|
+
)
|
|
265
|
+
assert len(issues) == 1
|
|
266
|
+
assert "return down" in issues[0]
|