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
@@ -0,0 +1,167 @@
1
+ """Behavioral tests for the JavaScript boolean-naming and banned-identifier checks."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _blocking_directory = str(Path(__file__).resolve().parent)
7
+ if _blocking_directory not in sys.path:
8
+ sys.path.insert(0, _blocking_directory)
9
+
10
+ from code_rules_js_conventions import ( # noqa: E402
11
+ check_js_banned_identifiers,
12
+ check_js_boolean_naming,
13
+ )
14
+
15
+ CONVERGE_PATH = "packages/claude-dev-env/skills/autoconverge/workflow/converge.mjs"
16
+
17
+
18
+ def test_flags_unprefixed_boolean_let_declaration() -> None:
19
+ content = "let hardeningPrOpened = false\n"
20
+ issues = check_js_boolean_naming(content, CONVERGE_PATH)
21
+ assert len(issues) == 1
22
+ assert "hardeningPrOpened" in issues[0]
23
+ assert "Line 1" in issues[0]
24
+
25
+
26
+ def test_flags_unprefixed_boolean_negation_declaration() -> None:
27
+ content = "const readyState = !pending\n"
28
+ issues = check_js_boolean_naming(content, CONVERGE_PATH)
29
+ assert len(issues) == 1
30
+ assert "readyState" in issues[0]
31
+
32
+
33
+ def test_flags_pure_negation_declaration() -> None:
34
+ content = "const ready = !pending;\n"
35
+ issues = check_js_boolean_naming(content, CONVERGE_PATH)
36
+ assert len(issues) == 1
37
+ assert "ready" in issues[0]
38
+
39
+
40
+ def test_does_not_flag_ternary_negation_string_declaration() -> None:
41
+ content = 'const label = !isActive ? "on" : "off";\n'
42
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
43
+
44
+
45
+ def test_does_not_flag_ternary_negation_number_declaration() -> None:
46
+ content = "const count = !arr.length ? 0 : arr.length;\n"
47
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
48
+
49
+
50
+ def test_does_not_flag_negation_logical_and_declaration() -> None:
51
+ content = "const name = !err && getName();\n"
52
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
53
+
54
+
55
+ def test_does_not_flag_negation_logical_or_declaration() -> None:
56
+ content = "const port = !custom || 8080;\n"
57
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
58
+
59
+
60
+ def test_flags_unprefixed_boolean_jsdoc_param() -> None:
61
+ content = (
62
+ "/**\n"
63
+ " * @param {boolean} alreadyOpened whether the PR is open\n"
64
+ " */\n"
65
+ "function note(alreadyOpened) { return alreadyOpened }\n"
66
+ )
67
+ issues = check_js_boolean_naming(content, CONVERGE_PATH)
68
+ assert len(issues) == 1
69
+ assert "alreadyOpened" in issues[0]
70
+ assert "parameter" in issues[0].lower()
71
+
72
+
73
+ def test_accepts_prefixed_boolean_declarations() -> None:
74
+ content = (
75
+ "let isReady = true\n"
76
+ "const hasItems = false\n"
77
+ "let shouldRetry = !done\n"
78
+ "var wasOpened = true\n"
79
+ )
80
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
81
+
82
+
83
+ def test_accepts_prefixed_boolean_jsdoc_param() -> None:
84
+ content = (
85
+ "/**\n"
86
+ " * @param {boolean} isOpen whether the PR is open\n"
87
+ " */\n"
88
+ "function note(isOpen) { return isOpen }\n"
89
+ )
90
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
91
+
92
+
93
+ def test_ignores_boolean_declaration_inside_string_literal() -> None:
94
+ content = 'const label = "let flagged = false"\n'
95
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
96
+
97
+
98
+ def test_ignores_boolean_declaration_inside_line_comment() -> None:
99
+ content = "// let flagged = false\nconst isReady = true\n"
100
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
101
+
102
+
103
+ def test_does_not_flag_falsely_named_non_boolean_declaration() -> None:
104
+ content = "const counted = falsely()\n"
105
+ assert check_js_boolean_naming(content, CONVERGE_PATH) == []
106
+
107
+
108
+ def test_boolean_naming_scoped_to_changed_lines() -> None:
109
+ content = "let alreadyRun = false\nconst isReady = true\n"
110
+ unchanged_only = check_js_boolean_naming(content, CONVERGE_PATH, {2})
111
+ assert unchanged_only == []
112
+ changed = check_js_boolean_naming(content, CONVERGE_PATH, {1})
113
+ assert len(changed) == 1
114
+ assert "alreadyRun" in changed[0]
115
+
116
+
117
+ def test_boolean_naming_defer_scope_returns_all() -> None:
118
+ content = "let alreadyRun = false\n"
119
+ deferred = check_js_boolean_naming(content, CONVERGE_PATH, None, True)
120
+ assert len(deferred) == 1
121
+
122
+
123
+ def test_boolean_naming_exempts_test_files() -> None:
124
+ content = "let alreadyRun = false\n"
125
+ assert check_js_boolean_naming(content, "workflow/converge.test.mjs") == []
126
+
127
+
128
+ def test_flags_banned_identifier_declaration() -> None:
129
+ content = "let data = fetchThings()\n"
130
+ issues = check_js_banned_identifiers(content, CONVERGE_PATH)
131
+ assert len(issues) == 1
132
+ assert "data" in issues[0]
133
+ assert "Line 1" in issues[0]
134
+
135
+
136
+ def test_accepts_descriptive_identifier_declaration() -> None:
137
+ content = "let allOrders = fetchThings()\n"
138
+ assert check_js_banned_identifiers(content, CONVERGE_PATH) == []
139
+
140
+
141
+ def test_banned_identifier_message_carries_descriptive_name_guidance() -> None:
142
+ content = "let data = fetchThings()\n"
143
+ issues = check_js_banned_identifiers(content, CONVERGE_PATH)
144
+ assert "use descriptive name (see CODE_RULES Naming section)" in issues[0]
145
+
146
+
147
+ def test_flags_each_banned_identifier_name() -> None:
148
+ content = "const result = compute()\nlet response = call()\nvar ctx = build()\n"
149
+ issues = check_js_banned_identifiers(content, CONVERGE_PATH)
150
+ assert len(issues) == 3
151
+
152
+
153
+ def test_banned_identifier_ignores_string_literal() -> None:
154
+ content = 'const label = "let data = 1"\n'
155
+ assert check_js_banned_identifiers(content, CONVERGE_PATH) == []
156
+
157
+
158
+ def test_banned_identifier_scoped_to_changed_lines() -> None:
159
+ content = "let data = one()\nconst allOrders = two()\n"
160
+ assert check_js_banned_identifiers(content, CONVERGE_PATH, {2}) == []
161
+ changed = check_js_banned_identifiers(content, CONVERGE_PATH, {1})
162
+ assert len(changed) == 1
163
+
164
+
165
+ def test_banned_identifier_exempts_test_files() -> None:
166
+ content = "let data = one()\n"
167
+ assert check_js_banned_identifiers(content, "workflow/converge.test.mjs") == []
@@ -0,0 +1,167 @@
1
+ """Tests for check_js_returns_object_schemaless_branch.
2
+
3
+ The check catches the JS/.mjs Category O6 docstring-prose-vs-implementation
4
+ drift PR #807 surfaced: a ``function`` whose JSDoc ``@returns {Promise<object>}``
5
+ promises a structured object, while one branch returns the agent-spawn helper
6
+ with an options object that omits ``schema``. A schema-less agent call resolves
7
+ to a transcript string, not the object the JSDoc claims, so the contract drifts
8
+ for that branch while a sibling branch that does pass ``schema`` returns a real
9
+ object.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import importlib.util
15
+ import pathlib
16
+ import sys
17
+
18
+ _HOOK_DIRECTORY = pathlib.Path(__file__).parent
19
+ if str(_HOOK_DIRECTORY) not in sys.path:
20
+ sys.path.insert(0, str(_HOOK_DIRECTORY))
21
+
22
+ _module_spec = importlib.util.spec_from_file_location(
23
+ "code_rules_imports_logging",
24
+ _HOOK_DIRECTORY / "code_rules_imports_logging.py",
25
+ )
26
+ assert _module_spec is not None
27
+ assert _module_spec.loader is not None
28
+ _imports_logging_module = importlib.util.module_from_spec(_module_spec)
29
+ _module_spec.loader.exec_module(_imports_logging_module)
30
+
31
+ check_js_returns_object_schemaless_branch = (
32
+ _imports_logging_module.check_js_returns_object_schemaless_branch
33
+ )
34
+
35
+ _MJS_PATH = "skills/autoconverge/workflow/converge.mjs"
36
+
37
+ _SHIPPED_CONVERGE_MJS = (
38
+ _HOOK_DIRECTORY.parents[1] / "skills" / "autoconverge" / "workflow" / "converge.mjs"
39
+ )
40
+
41
+
42
+ def _mixed_schema_source() -> str:
43
+ return (
44
+ "/**\n"
45
+ " * Spawn a git-utility agent for a task.\n"
46
+ " * @returns {Promise<object>} the structured output\n"
47
+ " */\n"
48
+ "function runGitTask(task, head) {\n"
49
+ " if (task === 'resolve-head') {\n"
50
+ " return convergeAgent(\n"
51
+ " `Print the HEAD SHA of ${prCoordinates}.`,\n"
52
+ " { label: 'git-utility', phase: 'Converge', schema: HEAD_SCHEMA, agentType: 'Explore' },\n"
53
+ " )\n"
54
+ " }\n"
55
+ " if (task === 'prefetch-main') {\n"
56
+ " return convergeAgent(\n"
57
+ " `Refresh the base ref for ${prCoordinates}.`,\n"
58
+ " { label: 'git-utility', phase: 'Converge', agentType: 'Explore' },\n"
59
+ " )\n"
60
+ " }\n"
61
+ " return convergeAgent(\n"
62
+ " `Report conflicts for ${prCoordinates}.`,\n"
63
+ " { label: 'git-utility', phase: 'Converge', schema: MERGE_CONFLICT_SCHEMA, agentType: 'Explore' },\n"
64
+ " )\n"
65
+ "}\n"
66
+ )
67
+
68
+
69
+ def _all_schema_source() -> str:
70
+ return _mixed_schema_source().replace(
71
+ " { label: 'git-utility', phase: 'Converge', agentType: 'Explore' },\n",
72
+ " { label: 'git-utility', phase: 'Converge', schema: FETCH_SCHEMA, agentType: 'Explore' },\n",
73
+ )
74
+
75
+
76
+ def _plain_object_return_source() -> str:
77
+ return (
78
+ "/**\n"
79
+ " * Joined fixer recovery loop.\n"
80
+ " * @returns {Promise<object>} FIX_SCHEMA result\n"
81
+ " */\n"
82
+ "async function fixerWithRecovery(head, findings, sourceLabel) {\n"
83
+ " if (!verdictPassed) {\n"
84
+ " return {\n"
85
+ " newSha: head,\n"
86
+ " pushed: false,\n"
87
+ " summary: `verify step did not pass ${sourceLabel}`,\n"
88
+ " }\n"
89
+ " }\n"
90
+ " return commitWithRecovery({\n"
91
+ " runCommit: () => runFixerTask('commit', { head }),\n"
92
+ " runVerify: () => runVerifierTask('fix-verify', { head }),\n"
93
+ " })\n"
94
+ "}\n"
95
+ )
96
+
97
+
98
+ def _transcript_typed_source() -> str:
99
+ return _mixed_schema_source().replace(
100
+ " * @returns {Promise<object>} the structured output\n",
101
+ " * @returns {Promise<*>} the agent() result\n",
102
+ )
103
+
104
+
105
+ def _destructured_parameter_mixed_schema_source() -> str:
106
+ return (
107
+ "/**\n"
108
+ " * Commit and verify with recovery.\n"
109
+ " * @returns {Promise<object>} the structured verify output\n"
110
+ " */\n"
111
+ "async function commitWithRecovery({ runCommit, runVerify, head }) {\n"
112
+ " if (runVerify) {\n"
113
+ " return convergeAgent(\n"
114
+ " `Verify the commit for ${head}.`,\n"
115
+ " { label: 'commit-recovery', phase: 'Converge', schema: VERIFY_SCHEMA, agentType: 'Explore' },\n"
116
+ " )\n"
117
+ " }\n"
118
+ " return convergeAgent(\n"
119
+ " `Commit ${head} without verification.`,\n"
120
+ " { label: 'commit-recovery', phase: 'Converge', agentType: 'Explore' },\n"
121
+ " )\n"
122
+ "}\n"
123
+ )
124
+
125
+
126
+ def test_flags_schema_less_branch_under_returns_object_claim() -> None:
127
+ issues = check_js_returns_object_schemaless_branch(_mixed_schema_source(), _MJS_PATH)
128
+ assert len(issues) == 1
129
+ assert "runGitTask" in issues[0]
130
+ assert "convergeAgent" in issues[0]
131
+
132
+
133
+ def test_flags_schema_less_branch_with_destructured_parameter() -> None:
134
+ issues = check_js_returns_object_schemaless_branch(
135
+ _destructured_parameter_mixed_schema_source(), _MJS_PATH
136
+ )
137
+ assert len(issues) == 1
138
+ assert "commitWithRecovery" in issues[0]
139
+ assert "convergeAgent" in issues[0]
140
+
141
+
142
+ def test_accepts_function_when_every_branch_passes_a_schema() -> None:
143
+ issues = check_js_returns_object_schemaless_branch(_all_schema_source(), _MJS_PATH)
144
+ assert issues == []
145
+
146
+
147
+ def test_accepts_plain_object_returns_with_no_schema_driven_helper() -> None:
148
+ issues = check_js_returns_object_schemaless_branch(_plain_object_return_source(), _MJS_PATH)
149
+ assert issues == []
150
+
151
+
152
+ def test_accepts_transcript_typed_returns_clause() -> None:
153
+ issues = check_js_returns_object_schemaless_branch(_transcript_typed_source(), _MJS_PATH)
154
+ assert issues == []
155
+
156
+
157
+ def test_skips_python_files() -> None:
158
+ issues = check_js_returns_object_schemaless_branch(
159
+ _mixed_schema_source(), "workflow/converge.py"
160
+ )
161
+ assert issues == []
162
+
163
+
164
+ def test_shipped_converge_mjs_passes_its_own_check() -> None:
165
+ shipped_source = _SHIPPED_CONVERGE_MJS.read_text(encoding="utf-8")
166
+ issues = check_js_returns_object_schemaless_branch(shipped_source, _MJS_PATH)
167
+ assert issues == []
@@ -16,6 +16,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
16
16
  | `env_var_table_code_drift_constants.py` | Table patterns, env-var-name and code-file recognizers, scan budget, and block-message text for the env-var-table code-drift blocker |
17
17
  | `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
18
18
  | `code_verifier_spawn_preflight_gate_constants.py` | Subagent type, merge-tree command flags, timeouts, and deny-message text for the code-verifier spawn pre-flight gate |
19
+ | `command_dispatch_constants.py` | Command-word regex, command-key access pattern, tokenization pattern, and anchors for the unanchored command-dispatch meta-gate |
19
20
  | `convergence_branch_constants.py` | Branch and worktree naming patterns for the convergence gate |
20
21
  | `dead_argparse_argument_constants.py` | Patterns for detecting unused argparse arguments |
21
22
  | `dead_config_field_constants.py` | Patterns for detecting unused `*Config` / `*Selectors` dataclass fields |
@@ -34,6 +35,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
34
35
  | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
35
36
  | `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
36
37
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
38
+ | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
37
39
  | `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
38
40
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
39
41
  | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
@@ -10,6 +10,8 @@ from __future__ import annotations
10
10
  import re
11
11
 
12
12
  MAX_BANNED_PREFIX_ISSUES: int = 3
13
+ MAX_POLARITY_CONTRADICTION_ISSUES: int = 3
14
+ MAX_VACUOUS_CLEANUP_ASSERTION_ISSUES: int = 3
13
15
  MAX_STUB_IMPLEMENTATION_ISSUES: int = 3
14
16
  MAX_TYPED_DICT_PAIR_ISSUES: int = 3
15
17
  MAX_TEST_BRANCHING_ISSUES: int = 3
@@ -45,6 +47,7 @@ ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX: tuple[str, ...] = (
45
47
  "json",
46
48
  )
47
49
  MAX_JS_RESUME_TASK_ENUMERATION_ISSUES: int = 5
50
+ MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES: int = 5
48
51
  MINIMUM_RESUME_TASK_ENUMERATION_ITEMS: int = 2
49
52
  DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
50
53
  MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
@@ -238,6 +241,13 @@ ALL_PATH_METADATA_ACCESS_METHOD_NAMES: frozenset[str] = frozenset(
238
241
  {"is_file", "is_dir", "exists", "stat", "lstat"}
239
242
  )
240
243
 
244
+ MAX_DOCSTRING_TYPE_CHECKING_GATE_ISSUES: int = 3
245
+ TYPE_CHECKING_IDENTIFIER_MARKER: str = "type_checking"
246
+ ALL_ABSENT_TYPE_CHECKING_GATE_DOCSTRING_PHRASES: tuple[str, ...] = (
247
+ "type_checking gate",
248
+ "type-checking-gate",
249
+ )
250
+
241
251
  MAX_DOCSTRING_INLINE_LITERAL_CLAIM_ISSUES: int = 3
242
252
  ALL_DOCSTRING_NO_INLINE_LITERAL_CLAIM_PHRASES: tuple[str, ...] = (
243
253
  "no literals appear inline",
@@ -222,6 +222,20 @@ ENUMERATION_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
222
222
  HYPHENATED_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
223
223
  r"^[a-z0-9]+(?:-[a-z0-9]+)+$"
224
224
  )
225
+ FUNCTION_WITH_JSDOC_PATTERN: re.Pattern[str] = re.compile(
226
+ r"/\*\*(?P<jsdoc>(?:(?!\*/).)*?)\*/\s*"
227
+ r"(?:async\s+)?function\s+(?P<name>\w+)\s*\(",
228
+ re.DOTALL,
229
+ )
230
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN: re.Pattern[str] = re.compile(
231
+ r"@returns?\s*\{[^}]*Promise\s*<\s*[Oo]bject\s*>[^}]*\}"
232
+ )
233
+ RETURN_CALL_OPENING_PARENTHESIS_PATTERN: re.Pattern[str] = re.compile(
234
+ r"\breturn\s+(?:await\s+)?(?P<callee>\w+)\s*\("
235
+ )
236
+ SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN: re.Pattern[str] = re.compile(
237
+ r"(?<![A-Za-z0-9_])schema\s*:"
238
+ )
225
239
  ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
226
240
  "get", "items", "keys", "values", "update", "pop",
227
241
  "setdefault", "copy", "clear",
@@ -0,0 +1,28 @@
1
+ """Constants for the unanchored command-dispatch meta-gate."""
2
+
3
+ import re
4
+
5
+ COMMAND_DISPATCH_PATH_MARKER: str = "hooks/blocking"
6
+
7
+ COMMAND_DISPATCH_LITERAL_PATTERN: re.Pattern[str] = re.compile(
8
+ r"\b(?:gh|git|npm|npx|node|python3?|pwsh|powershell|docker|kubectl|pip|cargo"
9
+ r"|yarn|pnpm)\\s[+*]"
10
+ )
11
+
12
+ COMMAND_KEY_ACCESS_PATTERN: re.Pattern[str] = re.compile(
13
+ r"""(?:\[\s*|\.get\(\s*)["']command["']"""
14
+ )
15
+
16
+ FIRST_TOKEN_TOKENIZATION_PATTERN: re.Pattern[str] = re.compile(
17
+ r"shlex\.split|\.split\("
18
+ )
19
+
20
+ ALL_REGEX_START_ANCHOR_TOKENS: tuple[str, ...] = ("^", "\\A")
21
+
22
+ COMMAND_DISPATCH_MESSAGE_SUFFIX: str = (
23
+ "matches a multi-word command as a substring - anchor the pattern to the "
24
+ "start of the command (^) or tokenize the first word (shlex.split) so a "
25
+ "command like 'echo gh pr create' is not matched"
26
+ )
27
+
28
+ MAX_COMMAND_DISPATCH_ISSUES: int = 20
@@ -0,0 +1,54 @@
1
+ """Configuration constants for the JavaScript convention checks in code_rules_enforcer."""
2
+
3
+ import re
4
+
5
+ ALL_JAVASCRIPT_BANNED_IDENTIFIERS: frozenset[str] = frozenset(
6
+ {
7
+ "result",
8
+ "data",
9
+ "output",
10
+ "response",
11
+ "value",
12
+ "item",
13
+ "temp",
14
+ "ctx",
15
+ "cfg",
16
+ "msg",
17
+ "btn",
18
+ "idx",
19
+ "cnt",
20
+ "tmp",
21
+ "elem",
22
+ "val",
23
+ }
24
+ )
25
+
26
+ _JAVASCRIPT_NEGATION_OPERAND: str = (
27
+ r"[A-Za-z_$][\w$]*"
28
+ r"(?:\s*\.\s*[A-Za-z_$][\w$]*|\s*\((?:[^()]|\([^()]*\))*\))*"
29
+ )
30
+
31
+ JAVASCRIPT_BOOLEAN_DECLARATION_PATTERN: re.Pattern[str] = re.compile(
32
+ r"\b(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*=\s*"
33
+ r"(?:(?:true|false)\b|!\s*" + _JAVASCRIPT_NEGATION_OPERAND + r"\s*(?:;|$))"
34
+ )
35
+
36
+ JAVASCRIPT_BOOLEAN_JSDOC_PARAMETER_PATTERN: re.Pattern[str] = re.compile(
37
+ r"@param\s*\{\s*boolean\s*\}\s+(?P<name>[A-Za-z_$][\w$]*)"
38
+ )
39
+
40
+ JAVASCRIPT_DECLARATION_NAME_PATTERN: re.Pattern[str] = re.compile(
41
+ r"\b(?:const|let|var)\s+(?P<name>[A-Za-z_$][\w$]*)\s*="
42
+ )
43
+
44
+ JAVASCRIPT_BOOLEAN_PREFIX_PATTERN: re.Pattern[str] = re.compile(
45
+ r"^(?:is|has|should|can|was|did)(?:[A-Z0-9_]|$)"
46
+ )
47
+
48
+ BOOLEAN_PREFIX_GUIDANCE: str = "prefix with is/has/should/can/was/did"
49
+
50
+ SINGLE_CHARACTER_NAME_LENGTH: int = 1
51
+
52
+ MAX_JAVASCRIPT_BOOLEAN_NAMING_ISSUES: int = 20
53
+
54
+ MAX_JAVASCRIPT_BANNED_IDENTIFIER_ISSUES: int = 20
@@ -0,0 +1,93 @@
1
+ """Behavior tests for the JS returns-object schema-less detection patterns.
2
+
3
+ These regexes drive ``check_js_returns_object_schemaless_branch``: they locate a
4
+ JSDoc-documented ``function`` declaration, decide whether its ``@returns`` clause
5
+ promises a ``Promise<object>``, find each ``return`` that calls a helper, and spot
6
+ a ``schema`` key inside an options object. Each test pins one pattern against the
7
+ shapes it must accept and the near-misses it must reject.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ _HOOKS_ROOT = Path(__file__).resolve().parent.parent
16
+ if str(_HOOKS_ROOT) not in sys.path:
17
+ sys.path.insert(0, str(_HOOKS_ROOT))
18
+
19
+ from hooks_constants.code_rules_enforcer_constants import (
20
+ FUNCTION_WITH_JSDOC_PATTERN,
21
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
22
+ RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
23
+ SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN,
24
+ )
25
+
26
+
27
+ def test_function_with_jsdoc_pattern_captures_name_and_jsdoc() -> None:
28
+ source = (
29
+ "/**\n * @returns {Promise<object>} the structured output\n */\n"
30
+ "function runGitTask(task, head) {\n"
31
+ )
32
+ match = FUNCTION_WITH_JSDOC_PATTERN.search(source)
33
+ assert match is not None
34
+ assert match.group("name") == "runGitTask"
35
+ assert "@returns" in match.group("jsdoc")
36
+
37
+
38
+ def test_function_with_jsdoc_pattern_captures_async_declaration() -> None:
39
+ source = "/** @returns {Promise<object>} */\nasync function fixerWithRecovery(head) {\n"
40
+ match = FUNCTION_WITH_JSDOC_PATTERN.search(source)
41
+ assert match is not None
42
+ assert match.group("name") == "fixerWithRecovery"
43
+
44
+
45
+ def test_returns_object_promise_pattern_matches_structured_object_claim() -> None:
46
+ lowercase_match = JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search(
47
+ "@returns {Promise<object>} the structured output"
48
+ )
49
+ assert lowercase_match is not None
50
+ assert lowercase_match.group(0) == "@returns {Promise<object>}"
51
+ capitalized_match = JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search(
52
+ "@return {Promise<Object>}"
53
+ )
54
+ assert capitalized_match is not None
55
+ assert capitalized_match.group(0) == "@return {Promise<Object>}"
56
+
57
+
58
+ def test_returns_object_promise_pattern_rejects_string_and_array_claims() -> None:
59
+ assert JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search("@returns {Promise<*>}") is None
60
+ assert (
61
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search("@returns {Promise<string>}") is None
62
+ )
63
+ assert (
64
+ JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN.search("@returns {Promise<object[]>}")
65
+ is None
66
+ )
67
+
68
+
69
+ def test_return_call_pattern_captures_plain_and_awaited_callees() -> None:
70
+ plain_match = RETURN_CALL_OPENING_PARENTHESIS_PATTERN.search(
71
+ " return convergeAgent(prompt, {})"
72
+ )
73
+ assert plain_match is not None
74
+ assert plain_match.group("callee") == "convergeAgent"
75
+ awaited_match = RETURN_CALL_OPENING_PARENTHESIS_PATTERN.search(
76
+ " return await commitWithRecovery({})"
77
+ )
78
+ assert awaited_match is not None
79
+ assert awaited_match.group("callee") == "commitWithRecovery"
80
+
81
+
82
+ def test_return_call_pattern_ignores_identifiers_beginning_with_return() -> None:
83
+ assert RETURN_CALL_OPENING_PARENTHESIS_PATTERN.search("const x = returnValue(payload)") is None
84
+
85
+
86
+ def test_schema_property_key_pattern_matches_only_the_options_key() -> None:
87
+ assert (
88
+ SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ label: 'x', schema: HEAD_SCHEMA }")
89
+ is not None
90
+ )
91
+ assert SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ label: 'x', schema : X }") is not None
92
+ assert SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ agentType, phase }, HEAD_SCHEMA") is None
93
+ assert SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN.search("{ myschema: X }") is None
@@ -14,7 +14,6 @@ UNUSED_IMPORT_GUIDANCE: str = (
14
14
  "remove unused import; if kept for side effects, mark with `# noqa: F401`"
15
15
  )
16
16
  TYPE_CHECKING_IDENTIFIER: str = "TYPE_CHECKING"
17
- ALL_TYPING_MODULE_NAMES: frozenset[str] = frozenset({"typing", "typing_extensions"})
18
17
 
19
18
 
20
19
  def _comment_text_from_line(line_text: str) -> str | None:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.79.0",
3
+ "version": "1.81.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,7 @@
6
6
 
7
7
  When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
8
8
 
9
- The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Nine more gate validators each cover one deterministic slice of the free-form prose. `check_docstring_length_constant_superlative_vs_exact_gate` covers a length-constant module whose docstring describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`) while the only code consuming the constant compares `len(...)` against it with `==`/`!=` — an exact-length gate that rejects every other length — and never with an ordered operator; the check scans the constant module's package tree (its own directory, or the parent package when the module sits in a `config/` subdirectory), so the mismatching consumer may sit in a sibling module. `check_docstring_fallback_branch_coverage` covers a summary that scopes a fallback to a single condition (`only when`, `falls back to ... when`) while the body routes to that same fallback call from two or more distinct early-return guards. `check_class_docstring_names_public_methods` covers a class whose docstring is a single summary line while the class exposes two or more public methods whose names the summary never spells out — the drift where a one-line class summary keeps naming its first feature after the class grows a second public entry point. `check_docstring_no_consumer_claim` covers a producer docstring asserting that no consumer reads its output yet (`producer-only artifact`, `no submission-run consumer reads it yet`) — a transitional claim that drifts the moment a reader lands and contradicts any companion `SKILL.md` that documents the consumer; this is the deterministic slice of the O8 companion-doc producer/consumer drift below. `check_docstring_returns_plural_cardinality` covers a `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) while the returned dict literal holds exactly one key in that family (`sheen_mid`) — the drift where a single-key family carries a plural noun, so the prose claims a cardinality of two or more that the dict does not hold. `check_docstring_args_single_line_scope_vs_span` covers an `Args:` entry whose prose scopes a finding to a single named line (`only when its block-anchor line is among the changed lines`) while the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper — the drift where the Args entry claims a narrower single-line scope than the span-intersection body applies, so an edit touching any non-anchor line of the span still blocks. `check_docstring_cardinal_count_matches_constant_family` covers a docstring that states a cardinal count of an outcome family (`Covers the four outcome branches: ...`) and lists those members, while the module references more members of the same `UPPER_SNAKE` constant family than the count names (`OUTCOME_OFFENDER_UNREADABLE` is imported and exercised, yet the summary stops at four) — the drift where a summary keeps the old count after the code grows another branch; this gate runs on test modules as well as production modules. `check_docstring_raises_unraisable_largezipfile` covers a `Raises:` clause that names `zipfile.LargeZipFile` while the function opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` left at its default of True — `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False, so a writer that allows ZIP64 documents an exception the body cannot produce. `check_docstring_no_network_claim_with_metadata_access` covers a function docstring promising a code path returns `without touching the network` (or a sibling no-network phrase) while the body calls a path-metadata method (`is_file`, `is_dir`, `exists`, `stat`, `lstat`) — on a network share each metadata call is a round-trip over the wire, so a cache-hit path the docstring swore avoids the network still pays a stat on every call; reword the claim to state the path is stat-checked on every call, or short-circuit to the cached path before the share is touched. The remaining free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"`, and the broader module-responsibility paragraph outside the user-facing-text-scope slice the checklist below names — has no signature, method roster, or single structural shape to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose; the audit lane below is the enforcement for everything outside the ten gated slices.
9
+ The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Ten more gate validators each cover one deterministic slice of the free-form prose. `check_docstring_names_absent_type_checking_gate` covers a module or function docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, while no identifier in the module's code carries the `type_checking` marker — the drift where the prose points a reader at a gate the body never performs; drop the `TYPE_CHECKING` gate wording, or add the detection the prose describes. `check_docstring_length_constant_superlative_vs_exact_gate` covers a length-constant module whose docstring describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`) while the only code consuming the constant compares `len(...)` against it with `==`/`!=` — an exact-length gate that rejects every other length — and never with an ordered operator; the check scans the constant module's package tree (its own directory, or the parent package when the module sits in a `config/` subdirectory), so the mismatching consumer may sit in a sibling module. `check_docstring_fallback_branch_coverage` covers a summary that scopes a fallback to a single condition (`only when`, `falls back to ... when`) while the body routes to that same fallback call from two or more distinct early-return guards. `check_class_docstring_names_public_methods` covers a class whose docstring is a single summary line while the class exposes two or more public methods whose names the summary never spells out — the drift where a one-line class summary keeps naming its first feature after the class grows a second public entry point. `check_docstring_no_consumer_claim` covers a producer docstring asserting that no consumer reads its output yet (`producer-only artifact`, `no submission-run consumer reads it yet`) — a transitional claim that drifts the moment a reader lands and contradicts any companion `SKILL.md` that documents the consumer; this is the deterministic slice of the O8 companion-doc producer/consumer drift below. `check_docstring_returns_plural_cardinality` covers a `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) while the returned dict literal holds exactly one key in that family (`sheen_mid`) — the drift where a single-key family carries a plural noun, so the prose claims a cardinality of two or more that the dict does not hold. `check_docstring_args_single_line_scope_vs_span` covers an `Args:` entry whose prose scopes a finding to a single named line (`only when its block-anchor line is among the changed lines`) while the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper — the drift where the Args entry claims a narrower single-line scope than the span-intersection body applies, so an edit touching any non-anchor line of the span still blocks. `check_docstring_cardinal_count_matches_constant_family` covers a docstring that states a cardinal count of an outcome family (`Covers the four outcome branches: ...`) and lists those members, while the module references more members of the same `UPPER_SNAKE` constant family than the count names (`OUTCOME_OFFENDER_UNREADABLE` is imported and exercised, yet the summary stops at four) — the drift where a summary keeps the old count after the code grows another branch; this gate runs on test modules as well as production modules. `check_docstring_raises_unraisable_largezipfile` covers a `Raises:` clause that names `zipfile.LargeZipFile` while the function opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` left at its default of True — `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False, so a writer that allows ZIP64 documents an exception the body cannot produce. `check_docstring_no_network_claim_with_metadata_access` covers a function docstring promising a code path returns `without touching the network` (or a sibling no-network phrase) while the body calls a path-metadata method (`is_file`, `is_dir`, `exists`, `stat`, `lstat`) — on a network share each metadata call is a round-trip over the wire, so a cache-hit path the docstring swore avoids the network still pays a stat on every call; reword the claim to state the path is stat-checked on every call, or short-circuit to the cached path before the share is touched. The remaining free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"`, and the broader module-responsibility paragraph outside the user-facing-text-scope slice the checklist below names — has no signature, method roster, or single structural shape to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose; the audit lane below is the enforcement for everything outside the eleven gated slices.
10
10
 
11
11
  ## What to check before you write the docstring
12
12
 
@@ -17,6 +17,7 @@ Read the body and the docstring side by side:
17
17
  - **Shared fallback routes.** A summary that scopes a fallback call to one condition names every condition that reaches that call. When the body routes to the same fallback from two or more early-return guards (`if a is None: fallback(); return` and `if random() < p: fallback(); return`), the prose enumerates both guards. The `check_docstring_fallback_branch_coverage` gate blocks the single-condition form of this drift at Write/Edit time.
18
18
  - **Step order.** A docstring that says `A then B then C` matches the call order in the body. A step enumeration that names the body's linear steps also names every corrective step the body guards inside an `if`/`elif` branch (`if not await cancel_and_reinitiate_update(...): return`). The `check_docstring_step_enumeration_dispatch_coverage` gate blocks the branch-guarded-dispatch form of this drift — a step-enumeration docstring that omits a two-or-more-token dispatch step the body guards inside a branch — at Write/Edit time.
19
19
  - **JS/`.mjs` resume-task enumerations.** A `spawn<Role>Agent` JSDoc that enumerates its sibling `resume<Role>Agent`'s resume tasks in a parenthetical `resume (repair-verify, hardening-verify)` list names every `task === '<name>'` branch the resume body dispatches on. The `check_js_resume_task_enumeration_coverage` gate blocks the JavaScript form of this drift — a spawn JSDoc whose resume enumeration omits a dispatched task — at Write/Edit time. This is the `.mjs` slice of the same Category O6 standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
20
+ - **JS/`.mjs` `@returns` object with a schema-less branch.** A `function` whose JSDoc `@returns {Promise<object>}` promises a structured object names a return type every branch honors. When the body returns one agent-spawn helper both with a `schema` options object and without one, the schema-less branch resolves to a transcript string, not the object the JSDoc claims. The `check_js_returns_object_schemaless_branch` gate blocks this drift — a `Promise<object>` JSDoc whose body returns the same helper with and without a `schema` key — at Write/Edit time. This is the `.mjs` slice of the same Category O6 standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
20
21
  - **Returns-clause cardinality.** A `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) matches the count of keys in that family in the returned dict literal. When the dict holds one key in the family (`sheen_mid`), the noun is singular (`the sheen stop`); a plural noun there claims two or more entries the dict does not hold. The `check_docstring_returns_plural_cardinality` gate blocks the single-key-with-plural-noun form of this drift at Write/Edit time.
21
22
  - **Length-constant superlative vs exact gate.** A module docstring that describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`, `no longer than`) matches how the code consumes the constant. When the only consumer compares `len(...)` against the constant with `==`/`!=` — an exact-length gate where every other length is rejected, not accepted at a shorter length — the superlative prose claims a range of accepted lengths the code never allows. State the exact required length (`the exact #AARRGGBB length`), not a longest/range form. The `check_docstring_length_constant_superlative_vs_exact_gate` gate blocks this drift at Write/Edit time, scanning the constant module's package tree so it sees a consumer that lives in a sibling module; a constant genuinely used as a ceiling (`len(x) <= LIMIT`) is left alone.
22
23
  - **Args single-line scope vs span body.** An `Args:` entry that scopes a finding to one named line (`a finding blocks only when its block-anchor line is among the changed lines`) matches the line breadth the body scopes by. When the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper that blocks when any line of the span is among the changed lines, the single-line Args wording understates the scope: an edit touching a non-anchor line of the span still blocks. State the Args entry on the same span breadth the body uses (`a finding blocks when any line of its block span is among the changed lines`). The `check_docstring_args_single_line_scope_vs_span` gate blocks the single-line-Args-over-span-body form of this drift at Write/Edit time.
@@ -27,6 +28,7 @@ Read the body and the docstring side by side:
27
28
  - **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
28
29
  - **Exclusion-clause distinguisher.** A docstring sentence that says a named category of input "are not" / "is not" the thing the function flags (`plain logging, screenshot, or method-on-local calls inside a branch are not dispatch steps`) keys the exclusion to the same axis the body's classification keys on. When the body decides on one axis (a call sits in an `If.test` guard versus a plain statement) but the prose excludes on a different axis (the call's receiver shape — a method on a local), the exclusion clause names a category the body still flags: a guarded method-on-local call is flagged even though the prose lists method-on-local calls as excluded. Read the body's actual branch condition, then state the exclusion on that same axis (`plain (unguarded) calls inside a branch body are not dispatch steps`), so every member the prose excludes is a member the body also excludes.
29
30
  - **Companion-doc ordering and content claims.** A `SKILL.md` (or sibling `.md`) sentence that names a produced artifact and claims its order (`sorted`, `alphabetical`, `in sorted order`) or its content (`the at-risk names`, `just the current set`) matches the producer function's docstring and body for that same artifact. A producer that builds the artifact by merging stored names with new names and appending — preserving file order, not re-sorting the union — leaves a doc that still says `sorted` drifted on both counts: the order claim is wrong, and the content claim hides the merged-in prior entries. When the producer's ordering or union changes, the same change updates the companion doc. The two move together in one commit, even when the producer edit does not touch the `.md` file.
31
+ - **TYPE_CHECKING gate claim vs code.** A docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, matches a module whose code handles TYPE_CHECKING. When no identifier in the body carries the `type_checking` marker — no `TYPE_CHECKING` load, import alias, attribute, or helper name — the prose points a reader at a gate the module never performs. State what the module does, or add the detection the prose describes. The `check_docstring_names_absent_type_checking_gate` gate blocks this drift at Write/Edit time, and covers hook infrastructure, where the import-scan gates that carry this drift class live.
30
32
 
31
33
  When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
32
34