claude-dev-env 1.79.0 → 1.81.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.
Files changed (76) hide show
  1. package/_shared/pr-loop/scripts/CLAUDE.md +3 -1
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +116 -30
  3. package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +2 -0
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +13 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +113 -0
  8. package/_shared/pr-loop/scripts/terminology_sweep.py +467 -0
  9. package/_shared/pr-loop/scripts/tests/CLAUDE.md +8 -0
  10. package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +339 -0
  12. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
  13. package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
  14. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +297 -0
  15. package/audit-rubrics/CLAUDE.md +3 -2
  16. package/audit-rubrics/category_rubrics/CLAUDE.md +2 -1
  17. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +2 -1
  18. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +13 -1
  19. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +19 -0
  20. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +46 -0
  21. package/audit-rubrics/prompts/CLAUDE.md +2 -1
  22. package/audit-rubrics/prompts/category-a-api-contracts.md +1 -0
  23. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +19 -7
  24. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +14 -0
  25. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +51 -0
  26. package/docs/CODE_RULES.md +2 -2
  27. package/hooks/blocking/CLAUDE.md +2 -0
  28. package/hooks/blocking/code_rules_annotations_length.py +59 -11
  29. package/hooks/blocking/code_rules_banned_identifiers.py +48 -9
  30. package/hooks/blocking/code_rules_command_dispatch.py +140 -0
  31. package/hooks/blocking/code_rules_docstrings.py +93 -0
  32. package/hooks/blocking/code_rules_enforcer.py +58 -4
  33. package/hooks/blocking/code_rules_imports_logging.py +136 -1
  34. package/hooks/blocking/code_rules_js_conventions.py +246 -0
  35. package/hooks/blocking/code_rules_naming_collection.py +5 -0
  36. package/hooks/blocking/code_rules_test_assertions.py +3 -0
  37. package/hooks/blocking/code_rules_unused_imports.py +7 -66
  38. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +7 -0
  39. package/hooks/blocking/test_code_rules_command_dispatch.py +95 -0
  40. package/hooks/blocking/test_code_rules_enforcer_annotations.py +20 -2
  41. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +10 -2
  42. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +8 -4
  43. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +9 -3
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_type_checking_gate.py +164 -0
  45. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
  46. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +112 -18
  47. package/hooks/blocking/test_code_rules_js_conventions.py +167 -0
  48. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
  49. package/hooks/hooks_constants/CLAUDE.md +2 -0
  50. package/hooks/hooks_constants/blocking_check_limits.py +10 -0
  51. package/hooks/hooks_constants/code_rules_enforcer_constants.py +14 -0
  52. package/hooks/hooks_constants/command_dispatch_constants.py +28 -0
  53. package/hooks/hooks_constants/js_conventions_constants.py +54 -0
  54. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
  55. package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
  56. package/package.json +1 -1
  57. package/rules/docstring-prose-matches-implementation.md +3 -1
  58. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +43 -1
  59. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +2 -2
  60. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +1 -4
  61. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +100 -4
  62. package/skills/autoconverge/SKILL.md +28 -7
  63. package/skills/autoconverge/reference/convergence.md +3 -3
  64. package/skills/autoconverge/reference/stop-conditions.md +1 -1
  65. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  66. package/skills/autoconverge/workflow/converge.contract.test.mjs +58 -122
  67. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +289 -5
  68. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
  69. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
  70. package/skills/autoconverge/workflow/converge.mjs +235 -247
  71. package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
  72. package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
  73. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
  74. package/skills/pr-converge/SKILL.md +28 -2
  75. package/skills/pr-converge/reference/convergence-gates.md +13 -1
  76. package/skills/pr-converge/reference/state-schema.md +11 -0
@@ -89,10 +89,28 @@ def test_should_flag_async_function_without_return_annotation() -> None:
89
89
  )
90
90
 
91
91
 
92
- def test_should_skip_return_check_in_test_files() -> None:
92
+ def test_should_skip_return_check_for_test_file_helper() -> None:
93
93
  source = "def fetch(url: str):\n return url\n"
94
94
  issues = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH)
95
- assert issues == [], f"Test files must be exempt, got: {issues}"
95
+ assert issues == [], f"Non-test helper in a test file must be exempt, got: {issues}"
96
+
97
+
98
+ def test_should_flag_test_function_missing_return_annotation() -> None:
99
+ source = "def test_loads():\n assert True\n"
100
+ issues = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH)
101
+ assert any("test_loads" in each_issue for each_issue in issues), (
102
+ f"Pytest test function must require a return annotation, got: {issues}"
103
+ )
104
+
105
+
106
+ def test_should_scope_test_function_return_annotation_to_changed_lines() -> None:
107
+ source = "def helper():\n return 1\n\n\ndef test_uses_it():\n assert helper()\n"
108
+ unchanged = code_rules_enforcer.check_return_annotations(source, TEST_FILE_PATH, {2})
109
+ assert unchanged == []
110
+ on_definition = code_rules_enforcer.check_return_annotations(
111
+ source, TEST_FILE_PATH, {5}
112
+ )
113
+ assert any("test_uses_it" in each_issue for each_issue in on_definition)
96
114
 
97
115
 
98
116
  def test_should_flag_unannotated_known_fixture_in_test_file() -> None:
@@ -95,10 +95,18 @@ def test_should_not_flag_name_containing_banned_substring() -> None:
95
95
  assert issues == []
96
96
 
97
97
 
98
- def test_should_skip_test_files() -> None:
98
+ def test_should_scope_test_file_banned_identifier_to_changed_lines() -> None:
99
+ content = "def test_thing():\n result = compute()\n assert result\n"
100
+ unchanged_only = check_banned_identifiers(content, TEST_FILE_PATH, {1, 3})
101
+ assert unchanged_only == []
102
+ on_changed_line = check_banned_identifiers(content, TEST_FILE_PATH, {2})
103
+ assert any("result" in each_issue for each_issue in on_changed_line)
104
+
105
+
106
+ def test_should_flag_newly_written_test_file_banned_identifier() -> None:
99
107
  content = "def test_thing():\n result = compute()\n assert result\n"
100
108
  issues = check_banned_identifiers(content, TEST_FILE_PATH)
101
- assert issues == []
109
+ assert any("'result'" in each_issue for each_issue in issues)
102
110
 
103
111
 
104
112
  def test_should_skip_hook_infrastructure() -> None:
@@ -117,11 +117,15 @@ def test_should_flag_btn_assignment() -> None:
117
117
  )
118
118
 
119
119
 
120
- def test_should_not_flag_import_alias_in_test_file() -> None:
120
+ def test_should_scope_import_alias_in_test_file_to_changed_lines() -> None:
121
121
  content = "from config import rebase_constants as rc\n"
122
- issues = check_banned_identifiers(content, TEST_FILE_PATH)
123
- assert issues == [], (
124
- f"Test files are exempt from banned-identifier checks, got: {issues}"
122
+ unchanged_only = check_banned_identifiers(content, TEST_FILE_PATH, {2})
123
+ assert unchanged_only == [], (
124
+ f"A banned alias on an untouched line must not block, got: {unchanged_only}"
125
+ )
126
+ on_changed_line = check_banned_identifiers(content, TEST_FILE_PATH, {1})
127
+ assert any("'rc'" in each_issue for each_issue in on_changed_line), (
128
+ f"A newly written banned alias in a test file must flag, got: {on_changed_line}"
125
129
  )
126
130
 
127
131
 
@@ -10,8 +10,12 @@ orphan CSS class slip past the gate again.
10
10
  This module reads ``validate_content``'s source and asserts every ``check_*``
11
11
  attribute on the enforcer module appears in it. A check that is intentionally
12
12
  not wired must be listed in ``KNOWN_UNDISPATCHED_CHECKS`` with a reason in this
13
- docstring; no such checks exist today. The companion ``test_code_rules_enforcer_
14
- cap_meta.py`` guards the payload-cap convention; this module guards the wiring.
13
+ docstring. ``check_unanchored_command_dispatch`` is listed there: it guards a
14
+ ``hooks/blocking`` command classifier, and the whole ``validate_content`` verdict
15
+ stays off hook-infrastructure files, so the enforcer dispatches it from
16
+ ``_hook_infrastructure_blocking_issues`` instead. The companion
17
+ ``test_code_rules_enforcer_cap_meta.py`` guards the payload-cap convention; this
18
+ module guards the wiring.
15
19
  """
16
20
 
17
21
  from __future__ import annotations
@@ -34,7 +38,9 @@ assert _hook_specification.loader is not None
34
38
  _hook_module = importlib.util.module_from_spec(_hook_specification)
35
39
  _hook_specification.loader.exec_module(_hook_module)
36
40
 
37
- KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset()
41
+ KNOWN_UNDISPATCHED_CHECKS: frozenset[str] = frozenset(
42
+ {"check_unanchored_command_dispatch"}
43
+ )
38
44
 
39
45
 
40
46
  def _all_check_function_names() -> list[str]:
@@ -0,0 +1,164 @@
1
+ """Tests for check_docstring_names_absent_type_checking_gate — Category O6.
2
+
3
+ A module or function docstring that names a ``TYPE_CHECKING`` gate-detection step,
4
+ or a ``type-checking-gate`` helper family, drifts once no identifier in the body
5
+ handles TYPE_CHECKING: the prose points a reader at machinery the module does not
6
+ hold. This is the deterministic slice of Category O6 (docstring prose versus
7
+ implementation drift) for a TYPE_CHECKING gate claim. The check covers hook
8
+ infrastructure, where the import-scan gates that carry this drift class live.
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_type_checking_gate(content: str, file_path: str) -> list[str]:
32
+ return code_rules_enforcer.check_docstring_names_absent_type_checking_gate(content, file_path)
33
+
34
+
35
+ PRODUCTION_HOOK_PATH = "/home/user/.claude/hooks/blocking/code_rules_unused_imports.py"
36
+ TEST_FILE_PATH = "/home/user/.claude/hooks/blocking/test_code_rules_unused_imports.py"
37
+ MYPY_GATE_PRODUCTION_PATH = "/home/user/project/scripts/run_mypy_gate.py"
38
+
39
+
40
+ def test_flags_module_docstring_naming_type_checking_gate_helpers() -> None:
41
+ content = (
42
+ '"""Unused module-level import check and its import-range and '
43
+ 'type-checking-gate helpers."""\n'
44
+ "\n"
45
+ "import ast\n"
46
+ "\n"
47
+ "\n"
48
+ "def check_unused(content: str) -> list[str]:\n"
49
+ ' """Flag unused imports."""\n'
50
+ " return []\n"
51
+ )
52
+ issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
53
+ assert len(issues) == 1
54
+ assert issues[0].startswith("Line 1:")
55
+ assert "type-checking-gate" in issues[0]
56
+
57
+
58
+ def test_flags_function_docstring_naming_type_checking_gate_detection() -> None:
59
+ content = (
60
+ "import ast\n"
61
+ "\n"
62
+ "\n"
63
+ "def check_unused_module_level_imports(content: str) -> list[str]:\n"
64
+ ' """Flag module-level imports never referenced.\n'
65
+ "\n"
66
+ " When ``full_file_content`` is provided, the ``__all__`` /\n"
67
+ " ``TYPE_CHECKING`` gate detection and reference scanning run against\n"
68
+ " ``full_file_content``.\n"
69
+ ' """\n'
70
+ " return []\n"
71
+ )
72
+ issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
73
+ assert len(issues) == 1
74
+ assert "type_checking gate" in issues[0]
75
+
76
+
77
+ def test_flags_both_module_and_function_docstrings_on_the_real_drift() -> None:
78
+ content = (
79
+ '"""Unused module-level import check and its import-range and '
80
+ 'type-checking-gate helpers."""\n'
81
+ "\n"
82
+ "import ast\n"
83
+ "\n"
84
+ "\n"
85
+ "def check_unused_module_level_imports(content: str) -> list[str]:\n"
86
+ ' """Flag module-level imports never referenced.\n'
87
+ "\n"
88
+ " The ``__all__`` / ``TYPE_CHECKING`` gate detection and reference\n"
89
+ " scanning run against ``full_file_content``.\n"
90
+ ' """\n'
91
+ " return []\n"
92
+ )
93
+ issues = check_type_checking_gate(content, PRODUCTION_HOOK_PATH)
94
+ assert len(issues) == 2
95
+
96
+
97
+ def test_passes_when_code_declares_a_type_checking_gate_helper() -> None:
98
+ content = (
99
+ '"""Unused module-level import check and its type-checking-gate helpers."""\n'
100
+ "\n"
101
+ "import ast\n"
102
+ "\n"
103
+ "\n"
104
+ "def _module_body_declares_type_checking_gate(tree: ast.Module) -> bool:\n"
105
+ ' """Return True when the module guards imports behind TYPE_CHECKING."""\n'
106
+ " return False\n"
107
+ )
108
+ assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
109
+
110
+
111
+ def test_passes_when_body_names_type_checking_guard() -> None:
112
+ content = (
113
+ "import ast\n"
114
+ "from typing import TYPE_CHECKING\n"
115
+ "\n"
116
+ "\n"
117
+ "def check_unused(content: str) -> list[str]:\n"
118
+ ' """Skip imports the ``TYPE_CHECKING`` gate detection guards."""\n'
119
+ " if TYPE_CHECKING:\n"
120
+ " return []\n"
121
+ " return []\n"
122
+ )
123
+ assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
124
+
125
+
126
+ def test_passes_when_no_docstring_names_the_gate() -> None:
127
+ content = (
128
+ '"""Unused module-level import check and its import-range helpers."""\n'
129
+ "\n"
130
+ "import ast\n"
131
+ "\n"
132
+ "\n"
133
+ "def check_unused(content: str) -> list[str]:\n"
134
+ ' """Flag module-level imports never referenced."""\n'
135
+ " return []\n"
136
+ )
137
+ assert check_type_checking_gate(content, PRODUCTION_HOOK_PATH) == []
138
+
139
+
140
+ def test_passes_on_mypy_static_type_checker_gate_docstring() -> None:
141
+ content = (
142
+ "\"\"\"Run the project's type-checking gate and report failures.\"\"\"\n"
143
+ "\n"
144
+ "import subprocess\n"
145
+ "\n"
146
+ "\n"
147
+ "def run_gate() -> int:\n"
148
+ ' return subprocess.run(["mypy", "hooks"]).returncode\n'
149
+ )
150
+ assert check_type_checking_gate(content, MYPY_GATE_PRODUCTION_PATH) == []
151
+
152
+
153
+ def test_test_files_are_exempt() -> None:
154
+ content = (
155
+ '"""Unused module-level import check and its type-checking-gate helpers."""\n'
156
+ "\n"
157
+ "import ast\n"
158
+ "\n"
159
+ "\n"
160
+ "def check_unused(content: str) -> list[str]:\n"
161
+ ' """Flag unused imports."""\n'
162
+ " return []\n"
163
+ )
164
+ assert check_type_checking_gate(content, TEST_FILE_PATH) == []
@@ -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)
@@ -5,9 +5,12 @@ ALL_REPOSITORY_ROOT_MARKER_FILENAMES, VENV_DIRECTORY_NAME and other
5
5
  imports that survived into a PR without ever being referenced.
6
6
 
7
7
  The detector is intentionally narrow: it only flags `from X import Y`
8
- or `import X` where Y/X is never referenced in the file body, the file
9
- does not declare `__all__`, and the file does not use TYPE_CHECKING
10
- conditional imports. The narrow scope keeps false positives low.
8
+ or `import X` where Y/X is never referenced in the file body and the
9
+ file does not declare `__all__`. A `TYPE_CHECKING` guard block no longer
10
+ exempts the whole file its guarded imports live nested inside the
11
+ `if TYPE_CHECKING:` node so they are never flag candidates, while a
12
+ genuinely dead top-level runtime import in the same file is still
13
+ flagged. The narrow scope keeps false positives low.
11
14
  """
12
15
 
13
16
  from __future__ import annotations
@@ -102,7 +105,7 @@ def test_should_skip_file_with_dunder_all_annotated_assignment() -> None:
102
105
  )
103
106
 
104
107
 
105
- def test_should_skip_file_using_type_checking_block() -> None:
108
+ def test_should_flag_dead_top_level_import_in_type_checking_file() -> None:
106
109
  source = (
107
110
  "from typing import TYPE_CHECKING\n"
108
111
  "from hooks_constants.constants import UNUSED_NAME\n"
@@ -114,20 +117,101 @@ def test_should_skip_file_using_type_checking_block() -> None:
114
117
  " return None\n"
115
118
  )
116
119
  issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
120
+ assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
121
+ "A dead top-level runtime import must be flagged even when the file "
122
+ f"also declares a TYPE_CHECKING block, got: {issues}"
123
+ )
124
+ assert not any("OtherName" in each_issue for each_issue in issues), (
125
+ "A TYPE_CHECKING-guarded import is nested inside the if block and is "
126
+ f"never a flag candidate, got: {issues}"
127
+ )
128
+
129
+
130
+ def test_should_not_flag_standard_type_checking_idiom() -> None:
131
+ source = (
132
+ "from typing import TYPE_CHECKING\n"
133
+ "from hooks_constants.constants import RUNTIME_NAME\n"
134
+ "\n"
135
+ "if TYPE_CHECKING:\n"
136
+ " from somewhere import TypeOnly\n"
137
+ "\n"
138
+ "def run(node: TypeOnly) -> None:\n"
139
+ " RUNTIME_NAME()\n"
140
+ )
141
+ issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
117
142
  assert issues == [], (
118
- f"TYPE_CHECKING-using files have annotation-only imports skip, got: {issues}"
143
+ "The standard TYPE_CHECKING idiom TYPE_CHECKING referenced by the "
144
+ "guard, the runtime import used in the body, and the guarded import "
145
+ f"used in an annotation — must produce no findings, got: {issues}"
119
146
  )
120
147
 
121
148
 
122
- def test_should_skip_test_files() -> None:
149
+ def test_should_flag_dead_import_beside_type_checking_string_annotation() -> None:
123
150
  source = (
124
- "from hooks_constants.constants import UNUSED_NAME\n"
151
+ "from typing import TYPE_CHECKING\n"
152
+ "from shared_utils.config import portal_specific_timeouts\n"
153
+ "\n"
154
+ "if TYPE_CHECKING:\n"
155
+ " from shared_utils.base import BaseAutomation\n"
156
+ "\n"
157
+ 'async def run(automation: "BaseAutomation") -> None:\n'
158
+ " return None\n"
159
+ )
160
+ issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
161
+ assert any("portal_specific_timeouts" in each_issue for each_issue in issues), (
162
+ "A dead top-level import in a module that guards its annotation-only "
163
+ f"import under TYPE_CHECKING must still be flagged, got: {issues}"
164
+ )
165
+ assert not any("BaseAutomation" in each_issue for each_issue in issues), (
166
+ "The TYPE_CHECKING-guarded import used in a string annotation must not "
167
+ f"be flagged, got: {issues}"
168
+ )
169
+
170
+
171
+ def test_should_flag_dead_import_in_workflow_handler_file() -> None:
172
+ workflow_handler_path = (
173
+ "shared_utils/web_automation/samsung_portal/workflow/binary/delete_handlers.py"
174
+ )
175
+ source = (
176
+ "from typing import TYPE_CHECKING\n"
177
+ "from shared_utils.config import portal_specific_timeouts\n"
178
+ "\n"
179
+ "if TYPE_CHECKING:\n"
180
+ " from shared_utils.base import BaseAutomation\n"
181
+ "\n"
182
+ 'async def run(automation: "BaseAutomation") -> None:\n'
183
+ " return None\n"
184
+ )
185
+ issues = check_unused_module_level_imports(source, workflow_handler_path)
186
+ assert any("portal_specific_timeouts" in each_issue for each_issue in issues), (
187
+ "A handler file under a workflow/ directory is not a state/module "
188
+ "registry; a dead import in it must be flagged, not exempted by the "
189
+ f"workflow path, got: {issues}"
190
+ )
191
+
192
+
193
+ def test_should_flag_unused_import_in_test_file() -> None:
194
+ source = (
195
+ "import asyncio\n"
125
196
  "\n"
126
197
  "def test_thing() -> None:\n"
127
198
  " assert True\n"
128
199
  )
129
200
  issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
130
- assert issues == [], f"Test files exempt, got: {issues}"
201
+ assert any("asyncio" in each_issue for each_issue in issues), (
202
+ f"Unused import in a test file must be flagged, got: {issues}"
203
+ )
204
+
205
+
206
+ def test_should_not_flag_used_import_in_test_file() -> None:
207
+ source = (
208
+ "import asyncio\n"
209
+ "\n"
210
+ "def test_thing() -> None:\n"
211
+ " asyncio.run(main())\n"
212
+ )
213
+ issues = check_unused_module_level_imports(source, TEST_FILE_PATH)
214
+ assert issues == [], f"Used import in a test file must not be flagged, got: {issues}"
131
215
 
132
216
 
133
217
  def test_should_handle_syntax_error_gracefully() -> None:
@@ -364,7 +448,7 @@ def test_should_skip_when_full_file_declares_dunder_all() -> None:
364
448
  )
365
449
 
366
450
 
367
- def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
451
+ def test_should_flag_dead_fragment_import_when_full_file_uses_type_checking() -> None:
368
452
  fragment = "from hooks_constants.constants import NEW_NAME\n"
369
453
  full_file = (
370
454
  "from typing import TYPE_CHECKING\n"
@@ -379,9 +463,9 @@ def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
379
463
  issues = check_unused_module_level_imports(
380
464
  fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
381
465
  )
382
- assert issues == [], (
383
- "TYPE_CHECKING gate in the post-edit file must skip the scan, "
384
- f"got: {issues}"
466
+ assert any("NEW_NAME" in each_issue for each_issue in issues), (
467
+ "A TYPE_CHECKING block in the post-edit file must not exempt a dead "
468
+ f"import added by the edit fragment, got: {issues}"
385
469
  )
386
470
 
387
471
 
@@ -472,7 +556,7 @@ def test_should_flag_import_when_only_shadowed_local_name_is_loaded() -> None:
472
556
  )
473
557
 
474
558
 
475
- def test_should_skip_when_type_checking_uses_imported_alias() -> None:
559
+ def test_should_not_flag_guarded_import_under_imported_type_checking_alias() -> None:
476
560
  source = (
477
561
  "from typing import TYPE_CHECKING as IS_TYPE_CHECKING\n"
478
562
  "from hooks_constants.constants import UNUSED_NAME\n"
@@ -484,12 +568,17 @@ def test_should_skip_when_type_checking_uses_imported_alias() -> None:
484
568
  " return None\n"
485
569
  )
486
570
  issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
487
- assert issues == [], (
488
- f"TYPE_CHECKING imported aliases must skip annotation-only files, got: {issues}"
571
+ assert not any("OtherName" in each_issue for each_issue in issues), (
572
+ "An import guarded under an aliased TYPE_CHECKING is nested in the if "
573
+ f"block and is never a flag candidate, got: {issues}"
574
+ )
575
+ assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
576
+ "A dead top-level import beside an aliased TYPE_CHECKING guard is still "
577
+ f"flagged, got: {issues}"
489
578
  )
490
579
 
491
580
 
492
- def test_should_skip_when_type_checking_uses_module_alias() -> None:
581
+ def test_should_not_flag_guarded_import_under_module_type_checking_alias() -> None:
493
582
  source = (
494
583
  "import typing as t\n"
495
584
  "from hooks_constants.constants import UNUSED_NAME\n"
@@ -501,8 +590,13 @@ def test_should_skip_when_type_checking_uses_module_alias() -> None:
501
590
  " return None\n"
502
591
  )
503
592
  issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH)
504
- assert issues == [], (
505
- f"TYPE_CHECKING module aliases must skip annotation-only files, got: {issues}"
593
+ assert not any("OtherName" in each_issue for each_issue in issues), (
594
+ "An import guarded under a module-qualified TYPE_CHECKING is nested in "
595
+ f"the if block and is never a flag candidate, got: {issues}"
596
+ )
597
+ assert any("UNUSED_NAME" in each_issue for each_issue in issues), (
598
+ "A dead top-level import beside a module-qualified TYPE_CHECKING guard "
599
+ f"is still flagged, got: {issues}"
506
600
  )
507
601
 
508
602