claude-dev-env 1.85.0 → 1.86.1
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/code_rules_gate.py +18 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +142 -7
- package/_shared/pr-loop/scripts/terminology_sweep.py +288 -92
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +89 -0
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +180 -91
- package/agents/code-quality-agent.md +1 -1
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +7 -4
- package/hooks/blocking/session_edit_stage_gate.py +654 -0
- package/hooks/blocking/test_session_edit_stage_gate.py +537 -0
- package/hooks/hooks.json +30 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +92 -0
- package/hooks/hooks_constants/task_list_loop_starter_constants.py +18 -0
- package/hooks/lifecycle/CLAUDE.md +1 -1
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/session_file_edit_tracker.py +224 -0
- package/hooks/observability/test_session_file_edit_tracker.py +174 -0
- package/hooks/session/CLAUDE.md +6 -2
- package/hooks/session/session_edit_tracker_cleanup.py +120 -0
- package/hooks/session/task_list_loop_starter.py +36 -0
- package/hooks/session/test_session_edit_tracker_cleanup.py +157 -0
- package/hooks/session/test_task_list_loop_starter.py +53 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/re-stage-before-commit.md +31 -0
- package/skills/autoconverge/SKILL.md +4 -4
- package/skills/autoconverge/reference/gotchas.md +3 -2
- package/skills/autoconverge/reference/stop-conditions.md +22 -6
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +528 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +29 -21
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +77 -8
- package/skills/autoconverge/workflow/converge.mjs +456 -59
|
@@ -64,6 +64,7 @@ def run_git_in_repository(repository_root: Path, *arguments: str) -> str:
|
|
|
64
64
|
encoding="utf-8",
|
|
65
65
|
errors="replace",
|
|
66
66
|
check=True,
|
|
67
|
+
env=gate_module.repository_environment(),
|
|
67
68
|
)
|
|
68
69
|
return completion.stdout
|
|
69
70
|
|
|
@@ -2329,3 +2330,91 @@ def test_parse_arguments_applies_documented_defaults() -> None:
|
|
|
2329
2330
|
assert parsed_arguments.repo_root is None
|
|
2330
2331
|
assert parsed_arguments.only_under == []
|
|
2331
2332
|
assert parsed_arguments.paths == []
|
|
2333
|
+
|
|
2334
|
+
|
|
2335
|
+
def test_staged_mode_resolves_scan_roots_from_outside_the_repository(
|
|
2336
|
+
temporary_git_repository: Path,
|
|
2337
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
2338
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
2339
|
+
) -> None:
|
|
2340
|
+
package_directory = temporary_git_repository / "pkg"
|
|
2341
|
+
config_directory = package_directory / "config"
|
|
2342
|
+
write_file(package_directory / "__init__.py", "")
|
|
2343
|
+
write_file(config_directory / "__init__.py", "")
|
|
2344
|
+
write_file(
|
|
2345
|
+
config_directory / "pkg_constants.py",
|
|
2346
|
+
'"""Frame constants."""\n\nFRAME_COUNT: int = 9\n',
|
|
2347
|
+
)
|
|
2348
|
+
write_file(
|
|
2349
|
+
package_directory / "consumer.py",
|
|
2350
|
+
'"""Consumer of the frame constants."""\n\n'
|
|
2351
|
+
"from pkg.config.pkg_constants import FRAME_COUNT\n\n"
|
|
2352
|
+
"frame_total = FRAME_COUNT\n",
|
|
2353
|
+
)
|
|
2354
|
+
commit_all_files(temporary_git_repository, "initial")
|
|
2355
|
+
write_file(
|
|
2356
|
+
config_directory / "pkg_constants.py",
|
|
2357
|
+
'"""Frame constants."""\n\nFRAME_COUNT: int = 9\nPLATE_COUNT: int = 4\n',
|
|
2358
|
+
)
|
|
2359
|
+
write_file(
|
|
2360
|
+
package_directory / "consumer.py",
|
|
2361
|
+
'"""Consumer of the frame constants."""\n\n'
|
|
2362
|
+
"from pkg.config.pkg_constants import FRAME_COUNT, PLATE_COUNT\n\n"
|
|
2363
|
+
"frame_total = FRAME_COUNT + PLATE_COUNT\n",
|
|
2364
|
+
)
|
|
2365
|
+
stage_file(temporary_git_repository, "pkg/config/pkg_constants.py")
|
|
2366
|
+
stage_file(temporary_git_repository, "pkg/consumer.py")
|
|
2367
|
+
|
|
2368
|
+
monkeypatch.chdir(tmp_path_factory.mktemp("elsewhere"))
|
|
2369
|
+
exit_code = gate_module.main(
|
|
2370
|
+
["--repo-root", str(temporary_git_repository), "--staged"]
|
|
2371
|
+
)
|
|
2372
|
+
|
|
2373
|
+
assert exit_code == 0
|
|
2374
|
+
|
|
2375
|
+
|
|
2376
|
+
def test_paths_from_git_staged_ignores_inherited_git_environment(
|
|
2377
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
2378
|
+
) -> None:
|
|
2379
|
+
target_repository = tmp_path / "target_repository"
|
|
2380
|
+
target_repository.mkdir()
|
|
2381
|
+
initialize_git_repository(target_repository)
|
|
2382
|
+
write_file(target_repository / "pkg" / "example_module.py", "EXAMPLE = 1\n")
|
|
2383
|
+
stage_file(target_repository, "pkg/example_module.py")
|
|
2384
|
+
victim_repository = tmp_path / "victim_repository"
|
|
2385
|
+
victim_repository.mkdir()
|
|
2386
|
+
initialize_git_repository(victim_repository)
|
|
2387
|
+
monkeypatch.setenv("GIT_DIR", str(victim_repository / ".git"))
|
|
2388
|
+
monkeypatch.setenv("GIT_WORK_TREE", str(victim_repository))
|
|
2389
|
+
monkeypatch.setenv("GIT_INDEX_FILE", str(victim_repository / ".git" / "index"))
|
|
2390
|
+
|
|
2391
|
+
all_staged_names = [
|
|
2392
|
+
each_path.name
|
|
2393
|
+
for each_path in gate_module.paths_from_git_staged(target_repository)
|
|
2394
|
+
]
|
|
2395
|
+
|
|
2396
|
+
assert all_staged_names == ["example_module.py"]
|
|
2397
|
+
|
|
2398
|
+
|
|
2399
|
+
def test_run_staged_test_files_runs_pytest_without_git_environment(
|
|
2400
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
2401
|
+
) -> None:
|
|
2402
|
+
repository_root = tmp_path / "repository_under_test"
|
|
2403
|
+
repository_root.mkdir()
|
|
2404
|
+
initialize_git_repository(repository_root)
|
|
2405
|
+
staged_test_body = (
|
|
2406
|
+
"import os\n"
|
|
2407
|
+
"\n"
|
|
2408
|
+
"\n"
|
|
2409
|
+
"def test_environment_carries_no_git_variables() -> None:\n"
|
|
2410
|
+
" all_git_names = sorted(\n"
|
|
2411
|
+
" each_name for each_name in os.environ"
|
|
2412
|
+
" if each_name.startswith('GIT_')\n"
|
|
2413
|
+
" )\n"
|
|
2414
|
+
" assert all_git_names == []\n"
|
|
2415
|
+
)
|
|
2416
|
+
write_file(repository_root / "test_environment_scrub.py", staged_test_body)
|
|
2417
|
+
stage_file(repository_root, "test_environment_scrub.py")
|
|
2418
|
+
monkeypatch.setenv("GIT_AUTHOR_NAME", "Hostile Hook Environment")
|
|
2419
|
+
|
|
2420
|
+
assert gate_module.run_staged_test_files(repository_root) == 0
|
|
@@ -5,6 +5,8 @@ import subprocess
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from types import ModuleType
|
|
7
7
|
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def _load_sweep_module() -> ModuleType:
|
|
10
12
|
module_path = Path(__file__).parent.parent / "terminology_sweep.py"
|
|
@@ -23,7 +25,7 @@ main = sweep_module.main
|
|
|
23
25
|
parse_added_lines = sweep_module._parse_added_lines
|
|
24
26
|
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
_hermetic_git_environment = sweep_module.repository_environment
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def _init_git_repository(repository_path: Path) -> None:
|
|
@@ -37,7 +39,7 @@ def _init_git_repository(repository_path: Path) -> None:
|
|
|
37
39
|
cwd=repository_path,
|
|
38
40
|
check=True,
|
|
39
41
|
capture_output=True,
|
|
40
|
-
env=
|
|
42
|
+
env=_hermetic_git_environment(),
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
|
|
@@ -200,7 +202,7 @@ def test_does_not_flag_y_to_ies_plural_variant() -> None:
|
|
|
200
202
|
assert sweep_diff(diff) == []
|
|
201
203
|
|
|
202
204
|
|
|
203
|
-
def
|
|
205
|
+
def test_still_flags_non_plural_divergent_tail() -> None:
|
|
204
206
|
diff = (
|
|
205
207
|
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
206
208
|
"--- a/api/quota.py\n"
|
|
@@ -213,102 +215,23 @@ def test_does_not_flag_spaced_prose_sharing_identifier_prefix() -> None:
|
|
|
213
215
|
"@@ -0,0 +1,1 @@\n"
|
|
214
216
|
"+The premium request budget gates the run.\n"
|
|
215
217
|
)
|
|
216
|
-
assert sweep_diff(diff) == []
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def test_does_not_flag_ordinary_sentence_sharing_loop_variable_prefix() -> None:
|
|
220
|
-
diff = (
|
|
221
|
-
"diff --git a/api/scan.py b/api/scan.py\n"
|
|
222
|
-
"--- a/api/scan.py\n"
|
|
223
|
-
"+++ b/api/scan.py\n"
|
|
224
|
-
"@@ -0,0 +1,2 @@\n"
|
|
225
|
-
"+each_node = walk(tree)\n"
|
|
226
|
-
'+DESCRIPTION = "Each attempt polls the review endpoint once."\n'
|
|
227
|
-
)
|
|
228
|
-
assert sweep_diff(diff) == []
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def test_flags_near_miss_inside_code_comment() -> None:
|
|
232
|
-
diff = (
|
|
233
|
-
"diff --git a/api/quota.mjs b/api/quota.mjs\n"
|
|
234
|
-
"--- a/api/quota.mjs\n"
|
|
235
|
-
"+++ b/api/quota.mjs\n"
|
|
236
|
-
"@@ -0,0 +1,2 @@\n"
|
|
237
|
-
"+const premium_interactions = 5\n"
|
|
238
|
-
"+// the premium-request path resets the counter\n"
|
|
239
|
-
)
|
|
240
218
|
findings = sweep_diff(diff)
|
|
241
219
|
assert len(findings) == 1
|
|
242
|
-
assert "premium
|
|
243
|
-
|
|
220
|
+
assert "premium request" in findings[0]
|
|
244
221
|
|
|
245
|
-
def test_flags_near_miss_inside_code_string_literal() -> None:
|
|
246
|
-
diff = (
|
|
247
|
-
"diff --git a/api/quota.mjs b/api/quota.mjs\n"
|
|
248
|
-
"--- a/api/quota.mjs\n"
|
|
249
|
-
"+++ b/api/quota.mjs\n"
|
|
250
|
-
"@@ -0,0 +1,2 @@\n"
|
|
251
|
-
"+const premium_interactions = 5\n"
|
|
252
|
-
'+throw new Error("the premium-request budget")\n'
|
|
253
|
-
)
|
|
254
|
-
findings = sweep_diff(diff)
|
|
255
|
-
assert len(findings) == 1
|
|
256
|
-
assert "premium-request" in findings[0]
|
|
257
222
|
|
|
258
|
-
|
|
259
|
-
def test_does_not_flag_prose_inside_code_string_literal() -> None:
|
|
260
|
-
diff = (
|
|
261
|
-
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
262
|
-
"--- a/api/quota.py\n"
|
|
263
|
-
"+++ b/api/quota.py\n"
|
|
264
|
-
"@@ -0,0 +1,2 @@\n"
|
|
265
|
-
"+premium_interactions = 5\n"
|
|
266
|
-
'+message = "the premium-request budget"\n'
|
|
267
|
-
)
|
|
268
|
-
assert sweep_diff(diff) == []
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def test_does_not_flag_prose_inside_code_docstring() -> None:
|
|
223
|
+
def test_flags_near_miss_inside_code_comment() -> None:
|
|
272
224
|
diff = (
|
|
273
225
|
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
274
226
|
"--- a/api/quota.py\n"
|
|
275
227
|
"+++ b/api/quota.py\n"
|
|
276
228
|
"@@ -0,0 +1,2 @@\n"
|
|
277
229
|
"+premium_interactions = 5\n"
|
|
278
|
-
|
|
279
|
-
)
|
|
280
|
-
assert sweep_diff(diff) == []
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def test_does_not_flag_prose_in_test_file() -> None:
|
|
284
|
-
diff = (
|
|
285
|
-
"diff --git a/api/quota.test.mjs b/api/quota.test.mjs\n"
|
|
286
|
-
"--- a/api/quota.test.mjs\n"
|
|
287
|
-
"+++ b/api/quota.test.mjs\n"
|
|
288
|
-
"@@ -0,0 +1,2 @@\n"
|
|
289
|
-
"+const premium_interactions = 5\n"
|
|
290
|
-
"+// the premium-request path resets the counter\n"
|
|
291
|
-
)
|
|
292
|
-
assert sweep_diff(diff) == []
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def test_flags_prose_in_test_named_markdown_doc() -> None:
|
|
296
|
-
diff = (
|
|
297
|
-
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
298
|
-
"--- a/api/quota.py\n"
|
|
299
|
-
"+++ b/api/quota.py\n"
|
|
300
|
-
"@@ -0,0 +1,1 @@\n"
|
|
301
|
-
"+premium_interactions = 5\n"
|
|
302
|
-
"diff --git a/docs/test_plan.md b/docs/test_plan.md\n"
|
|
303
|
-
"--- a/docs/test_plan.md\n"
|
|
304
|
-
"+++ b/docs/test_plan.md\n"
|
|
305
|
-
"@@ -0,0 +1,1 @@\n"
|
|
306
|
-
"+The premium-request budget gates the run.\n"
|
|
230
|
+
"+# the premium-request path resets the counter\n"
|
|
307
231
|
)
|
|
308
232
|
findings = sweep_diff(diff)
|
|
309
233
|
assert len(findings) == 1
|
|
310
234
|
assert "premium-request" in findings[0]
|
|
311
|
-
assert "test_plan.md" in findings[0]
|
|
312
235
|
|
|
313
236
|
|
|
314
237
|
def test_no_findings_when_no_multiword_identifier_introduced() -> None:
|
|
@@ -345,9 +268,7 @@ def test_main_exits_zero_when_clean(tmp_path: Path) -> None:
|
|
|
345
268
|
|
|
346
269
|
def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
|
|
347
270
|
_init_git_repository(tmp_path)
|
|
348
|
-
(tmp_path / "quota.py").write_text(
|
|
349
|
-
"premium_interactions = 5\n", encoding="utf-8"
|
|
350
|
-
)
|
|
271
|
+
(tmp_path / "quota.py").write_text("premium_interactions = 5\n", encoding="utf-8")
|
|
351
272
|
(tmp_path / "README.md").write_text(
|
|
352
273
|
"The premium-request budget gates the run.\n", encoding="utf-8"
|
|
353
274
|
)
|
|
@@ -356,7 +277,7 @@ def test_staged_terminology_findings_flags_staged_prose(tmp_path: Path) -> None:
|
|
|
356
277
|
cwd=tmp_path,
|
|
357
278
|
check=True,
|
|
358
279
|
capture_output=True,
|
|
359
|
-
env=
|
|
280
|
+
env=_hermetic_git_environment(),
|
|
360
281
|
)
|
|
361
282
|
findings = staged_terminology_findings(tmp_path)
|
|
362
283
|
assert any("premium-request" in each_finding for each_finding in findings)
|
|
@@ -370,7 +291,7 @@ def test_staged_terminology_findings_empty_when_clean(tmp_path: Path) -> None:
|
|
|
370
291
|
cwd=tmp_path,
|
|
371
292
|
check=True,
|
|
372
293
|
capture_output=True,
|
|
373
|
-
env=
|
|
294
|
+
env=_hermetic_git_environment(),
|
|
374
295
|
)
|
|
375
296
|
assert staged_terminology_findings(tmp_path) == []
|
|
376
297
|
|
|
@@ -389,9 +310,177 @@ def test_parse_added_lines_counts_pre_increment_content_as_added_line() -> None:
|
|
|
389
310
|
all_added_lines = parse_added_lines(diff_with_pre_increment)
|
|
390
311
|
|
|
391
312
|
assert [
|
|
392
|
-
(each_line_number, each_text)
|
|
313
|
+
(each_line_number, each_text)
|
|
314
|
+
for _, each_line_number, each_text in all_added_lines
|
|
393
315
|
] == [
|
|
394
316
|
(1, "++counter;"),
|
|
395
317
|
(2, "let first_total = read_first_value();"),
|
|
396
318
|
(3, "let second_total = read_second_value();"),
|
|
397
319
|
]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_does_not_flag_window_containing_a_stopword() -> None:
|
|
323
|
+
diff = (
|
|
324
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
325
|
+
"--- a/api/quota.py\n"
|
|
326
|
+
"+++ b/api/quota.py\n"
|
|
327
|
+
"@@ -0,0 +1,1 @@\n"
|
|
328
|
+
"+to_path = destination\n"
|
|
329
|
+
"diff --git a/docs/README.md b/docs/README.md\n"
|
|
330
|
+
"--- a/docs/README.md\n"
|
|
331
|
+
"+++ b/docs/README.md\n"
|
|
332
|
+
"@@ -0,0 +1,1 @@\n"
|
|
333
|
+
"+Writes the report to a fresh location.\n"
|
|
334
|
+
)
|
|
335
|
+
assert sweep_diff(diff) == []
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_does_not_flag_tail_word_that_is_identifier_vocabulary() -> None:
|
|
339
|
+
diff = (
|
|
340
|
+
"diff --git a/api/layout.py b/api/layout.py\n"
|
|
341
|
+
"--- a/api/layout.py\n"
|
|
342
|
+
"+++ b/api/layout.py\n"
|
|
343
|
+
"@@ -0,0 +1,1 @@\n"
|
|
344
|
+
"+target_width = box_height\n"
|
|
345
|
+
"diff --git a/docs/README.md b/docs/README.md\n"
|
|
346
|
+
"--- a/docs/README.md\n"
|
|
347
|
+
"+++ b/docs/README.md\n"
|
|
348
|
+
"@@ -0,0 +1,1 @@\n"
|
|
349
|
+
"+Scale the art until it covers the target box.\n"
|
|
350
|
+
)
|
|
351
|
+
assert sweep_diff(diff) == []
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def test_staged_skips_identifier_already_in_base_tree(tmp_path: Path) -> None:
|
|
355
|
+
_init_git_repository(tmp_path)
|
|
356
|
+
(tmp_path / "api.py").write_text("box_height = 4\n", encoding="utf-8")
|
|
357
|
+
subprocess.run(
|
|
358
|
+
["git", "add", "."],
|
|
359
|
+
cwd=tmp_path,
|
|
360
|
+
check=True,
|
|
361
|
+
capture_output=True,
|
|
362
|
+
env=_hermetic_git_environment(),
|
|
363
|
+
)
|
|
364
|
+
subprocess.run(
|
|
365
|
+
["git", "commit", "-m", "base"],
|
|
366
|
+
cwd=tmp_path,
|
|
367
|
+
check=True,
|
|
368
|
+
capture_output=True,
|
|
369
|
+
env=_hermetic_git_environment(),
|
|
370
|
+
)
|
|
371
|
+
all_api_lines = ("box_height = 4", "box_frame_total = box_height")
|
|
372
|
+
(tmp_path / "api.py").write_text(chr(10).join(all_api_lines), encoding="utf-8")
|
|
373
|
+
(tmp_path / "README.md").write_text(
|
|
374
|
+
"The box is read from the capture.", encoding="utf-8"
|
|
375
|
+
)
|
|
376
|
+
subprocess.run(
|
|
377
|
+
["git", "add", "."],
|
|
378
|
+
cwd=tmp_path,
|
|
379
|
+
check=True,
|
|
380
|
+
capture_output=True,
|
|
381
|
+
env=_hermetic_git_environment(),
|
|
382
|
+
)
|
|
383
|
+
assert staged_terminology_findings(tmp_path) == []
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def test_does_not_flag_escape_sequence_letter_as_prose() -> None:
|
|
387
|
+
diff = (
|
|
388
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
389
|
+
"--- a/api/quota.py\n"
|
|
390
|
+
"+++ b/api/quota.py\n"
|
|
391
|
+
"@@ -0,0 +1,2 @@\n"
|
|
392
|
+
"+box_frame_total = 5\n"
|
|
393
|
+
'+content = "box_height = 4\\nbox_frame_total = 5\\n"\n'
|
|
394
|
+
)
|
|
395
|
+
assert sweep_diff(diff) == []
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_skips_string_literal_fragments_in_test_files() -> None:
|
|
399
|
+
diff = (
|
|
400
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
401
|
+
"--- a/api/quota.py\n"
|
|
402
|
+
"+++ b/api/quota.py\n"
|
|
403
|
+
"@@ -0,0 +1,1 @@\n"
|
|
404
|
+
"+premium_interactions = 5\n"
|
|
405
|
+
"diff --git a/tests/test_quota.py b/tests/test_quota.py\n"
|
|
406
|
+
"--- a/tests/test_quota.py\n"
|
|
407
|
+
"+++ b/tests/test_quota.py\n"
|
|
408
|
+
"@@ -0,0 +1,2 @@\n"
|
|
409
|
+
'+fixture_text = "the premium-request budget gates the run"\n'
|
|
410
|
+
"+# the premium-request path resets the counter\n"
|
|
411
|
+
)
|
|
412
|
+
findings = sweep_diff(diff)
|
|
413
|
+
assert len(findings) == 1
|
|
414
|
+
assert "test_quota.py:2" in findings[0]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_repository_environment_strips_every_git_variable(
|
|
418
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
419
|
+
) -> None:
|
|
420
|
+
monkeypatch.setenv("GIT_DIR", "hostile-git-dir")
|
|
421
|
+
monkeypatch.setenv("PRESERVED_SETTING", "kept")
|
|
422
|
+
|
|
423
|
+
scrubbed_environment = sweep_module.repository_environment()
|
|
424
|
+
|
|
425
|
+
assert "GIT_DIR" not in scrubbed_environment
|
|
426
|
+
assert scrubbed_environment["PRESERVED_SETTING"] == "kept"
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def test_skips_markdown_inline_code_spans() -> None:
|
|
430
|
+
diff = (
|
|
431
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
432
|
+
"--- a/api/quota.py\n"
|
|
433
|
+
"+++ b/api/quota.py\n"
|
|
434
|
+
"@@ -0,0 +1,1 @@\n"
|
|
435
|
+
"+scale_expression_reference = 1\n"
|
|
436
|
+
"diff --git a/docs/guide.md b/docs/guide.md\n"
|
|
437
|
+
"--- a/docs/guide.md\n"
|
|
438
|
+
"+++ b/docs/guide.md\n"
|
|
439
|
+
"@@ -0,0 +1,1 @@\n"
|
|
440
|
+
"+Its `scale_expression.py` evaluates the layer's `scale` prop here.\n"
|
|
441
|
+
)
|
|
442
|
+
assert sweep_diff(diff) == []
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_skips_identifier_shaped_string_literals() -> None:
|
|
446
|
+
diff = (
|
|
447
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
448
|
+
"--- a/api/quota.py\n"
|
|
449
|
+
"+++ b/api/quota.py\n"
|
|
450
|
+
"@@ -0,0 +1,2 @@\n"
|
|
451
|
+
"+icon_entry = 1\n"
|
|
452
|
+
'+MASTER_UID = "HOMESCREEN_APPICONS_APP_ICON_IMAGE"\n'
|
|
453
|
+
)
|
|
454
|
+
assert sweep_diff(diff) == []
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_skips_string_literal_fragments_in_dot_test_named_files() -> None:
|
|
458
|
+
diff = (
|
|
459
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
460
|
+
"--- a/api/quota.py\n"
|
|
461
|
+
"+++ b/api/quota.py\n"
|
|
462
|
+
"@@ -0,0 +1,1 @@\n"
|
|
463
|
+
"+premium_interactions = 5\n"
|
|
464
|
+
"diff --git a/api/quota.test.mjs b/api/quota.test.mjs\n"
|
|
465
|
+
"--- a/api/quota.test.mjs\n"
|
|
466
|
+
"+++ b/api/quota.test.mjs\n"
|
|
467
|
+
"@@ -0,0 +1,1 @@\n"
|
|
468
|
+
'+const fixture_line = "the premium-request budget gates the run"\n'
|
|
469
|
+
)
|
|
470
|
+
assert sweep_diff(diff) == []
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_skips_string_literal_fragments_in_tests_directory_files() -> None:
|
|
474
|
+
diff = (
|
|
475
|
+
"diff --git a/api/quota.py b/api/quota.py\n"
|
|
476
|
+
"--- a/api/quota.py\n"
|
|
477
|
+
"+++ b/api/quota.py\n"
|
|
478
|
+
"@@ -0,0 +1,1 @@\n"
|
|
479
|
+
"+premium_interactions = 5\n"
|
|
480
|
+
"diff --git a/tests/fixtures.py b/tests/fixtures.py\n"
|
|
481
|
+
"--- a/tests/fixtures.py\n"
|
|
482
|
+
"+++ b/tests/fixtures.py\n"
|
|
483
|
+
"@@ -0,0 +1,1 @@\n"
|
|
484
|
+
'+sample_line = "the premium-request budget gates the run"\n'
|
|
485
|
+
)
|
|
486
|
+
assert sweep_diff(diff) == []
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -88,6 +88,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
88
88
|
| `reviewer_spawn_gate.py` | PreToolUse (Bash) | A sentinel-marked autoconverge reviewer-spawn command (Copilot review request, Bugbot rerun comment) run while `reviewer_availability.py` reports that reviewer down or out of quota |
|
|
89
89
|
| `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to opening the file locally via `Show-Asset.ps1` |
|
|
90
90
|
| `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
|
|
91
|
+
| `session_edit_stage_gate.py` | PreToolUse (Bash) | A `git commit` that would drop files edited this session because they are tracked but left unstaged |
|
|
91
92
|
| `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
|
|
92
93
|
| `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
|
|
93
94
|
| `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
|
|
@@ -501,13 +501,16 @@ def check_dead_module_constants(
|
|
|
501
501
|
|
|
502
502
|
Returns:
|
|
503
503
|
One violation message per dead module-level constant, capped at the
|
|
504
|
-
configured maximum. Returns an empty list when the file is exempt,
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
504
|
+
configured maximum. Returns an empty list when the file is exempt, the
|
|
505
|
+
path is relative (the scan root cannot be resolved against a known
|
|
506
|
+
base, so the check cannot prove a constant dead), no constant is in
|
|
507
|
+
scope (none defined, or none exported when ``__all__`` is declared),
|
|
508
|
+
the scan exceeds the file cap, or a SyntaxError prevents parsing.
|
|
508
509
|
"""
|
|
509
510
|
if _module_is_exempt_from_constant_check(file_path):
|
|
510
511
|
return []
|
|
512
|
+
if not Path(file_path).is_absolute():
|
|
513
|
+
return []
|
|
511
514
|
effective_content = content if full_file_content is None else full_file_content
|
|
512
515
|
try:
|
|
513
516
|
tree = ast.parse(effective_content)
|