claude-dev-env 1.78.0 → 1.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
- package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.test.mjs +3 -2
- package/hooks/blocking/CLAUDE.md +3 -2
- package/hooks/blocking/code_rules_docstrings.py +609 -16
- package/hooks/blocking/code_rules_enforcer.py +41 -0
- package/hooks/blocking/code_rules_imports_logging.py +136 -1
- package/hooks/blocking/code_rules_naming_collection.py +76 -1
- package/hooks/blocking/code_rules_paired_test.py +240 -22
- package/hooks/blocking/code_rules_test_assertions.py +159 -1
- package/hooks/blocking/code_rules_unused_imports.py +7 -63
- package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
- package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
- package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
- package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/blocking_check_limits.py +79 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
- package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
- package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +57 -54
- package/rules/env-var-table-code-drift.md +24 -0
- package/rules/package-inventory-stale-entry.md +4 -4
- package/rules/paired-test-coverage.md +12 -5
- package/skills/autoconverge/SKILL.md +26 -5
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
- package/skills/autoconverge/workflow/converge.mjs +110 -228
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
- package/skills/pr-converge/SKILL.md +28 -2
- package/skills/pr-converge/reference/convergence-gates.md +13 -1
- package/skills/pr-converge/reference/state-schema.md +11 -0
|
@@ -0,0 +1,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 == []
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Tests for the env-var-table code-drift blocker.
|
|
2
|
+
|
|
3
|
+
Each test builds a real on-disk directory holding a code file and a markdown
|
|
4
|
+
doc, then drives the blocker's detection function against that tree. The drift
|
|
5
|
+
the blocker catches: a markdown env-var summary table row attributes an
|
|
6
|
+
environment variable to a code file whose source never references that variable.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_blocking_dir = str(Path(__file__).resolve().parent)
|
|
15
|
+
if _blocking_dir not in sys.path:
|
|
16
|
+
sys.path.insert(0, _blocking_dir)
|
|
17
|
+
|
|
18
|
+
from env_var_table_code_drift_blocker import ( # noqa: E402
|
|
19
|
+
find_drift_rows,
|
|
20
|
+
is_markdown_file,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _write(file_path: Path, content: str) -> None:
|
|
25
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
file_path.write_text(content, encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _anchor_repo_root(repo_root: Path) -> None:
|
|
30
|
+
(repo_root / ".git").mkdir(parents=True, exist_ok=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_is_markdown_file_accepts_md_and_rejects_python() -> None:
|
|
34
|
+
assert is_markdown_file("docs/configuration.md") is True
|
|
35
|
+
assert is_markdown_file("auth/google_auth.py") is False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_flags_variable_absent_from_named_code_file(tmp_path: Path) -> None:
|
|
39
|
+
_anchor_repo_root(tmp_path)
|
|
40
|
+
_write(tmp_path / "auth" / "google_auth.py", "def load():\n return read_bws_secret()\n")
|
|
41
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
42
|
+
content = (
|
|
43
|
+
"## Summary: Environment Variables\n\n"
|
|
44
|
+
"| Variable | Used By | Purpose |\n"
|
|
45
|
+
"|----------|---------|---------|\n"
|
|
46
|
+
"| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | Path to JSON |\n"
|
|
47
|
+
)
|
|
48
|
+
drift_rows = find_drift_rows(content, doc_path.parent)
|
|
49
|
+
assert drift_rows == ["GOOGLE_APPLICATION_CREDENTIALS -> auth/google_auth.py"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_passes_when_variable_present_in_code_file(tmp_path: Path) -> None:
|
|
53
|
+
_anchor_repo_root(tmp_path)
|
|
54
|
+
_write(
|
|
55
|
+
tmp_path / "automation_logging" / "__init__.py",
|
|
56
|
+
'topic = os.environ["NTFY_TOPIC"]\n',
|
|
57
|
+
)
|
|
58
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
59
|
+
content = (
|
|
60
|
+
"| Variable | Used By | Purpose |\n"
|
|
61
|
+
"|----------|---------|---------|\n"
|
|
62
|
+
"| `NTFY_TOPIC` | `automation_logging/__init__.py` | ntfy topic |\n"
|
|
63
|
+
)
|
|
64
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_skips_row_whose_code_file_is_absent(tmp_path: Path) -> None:
|
|
68
|
+
_anchor_repo_root(tmp_path)
|
|
69
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
70
|
+
content = (
|
|
71
|
+
"| Variable | Used By | Purpose |\n"
|
|
72
|
+
"|----------|---------|---------|\n"
|
|
73
|
+
"| `MISSING_VAR` | `nowhere/ghost.py` | unresolved |\n"
|
|
74
|
+
)
|
|
75
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_ignores_rows_inside_a_fenced_code_block(tmp_path: Path) -> None:
|
|
79
|
+
_anchor_repo_root(tmp_path)
|
|
80
|
+
_write(tmp_path / "auth" / "google_auth.py", "no variable here\n")
|
|
81
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
82
|
+
content = "```\n| `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | sample |\n```\n"
|
|
83
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_ignores_row_whose_second_cell_is_not_a_code_file(tmp_path: Path) -> None:
|
|
87
|
+
_anchor_repo_root(tmp_path)
|
|
88
|
+
doc_path = tmp_path / "docs" / "configuration.md"
|
|
89
|
+
content = (
|
|
90
|
+
"| Variable | Purpose | Default |\n"
|
|
91
|
+
"|----------|---------|---------|\n"
|
|
92
|
+
"| `SOME_FLAG` | `enables the thing` | off |\n"
|
|
93
|
+
)
|
|
94
|
+
assert find_drift_rows(content, doc_path.parent) == []
|
|
@@ -311,6 +311,52 @@ def test_is_inventoried_production_file_accepts_production_file(tmp_path: Path):
|
|
|
311
311
|
assert is_inventoried_production_file(str(production_file_path)) is True
|
|
312
312
|
|
|
313
313
|
|
|
314
|
+
SKILL_MD_SCRIPTS_LAYOUT = (
|
|
315
|
+
"# base-theme-match\n\n"
|
|
316
|
+
"## Layout\n\n"
|
|
317
|
+
"| Path | Role |\n"
|
|
318
|
+
"|---|---|\n"
|
|
319
|
+
"| `scripts/discover_candidates.py` | Discovers candidates. |\n"
|
|
320
|
+
"| `scripts/resolve_stp.py` | Resolves the STP filename. |\n"
|
|
321
|
+
"| `scripts/select_stp.py` | Selects the STP filename. |\n"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _skill_scripts_directory(tmp_path: Path) -> Path:
|
|
326
|
+
"""Return a skill package's scripts/ directory under a SKILL.md Layout table."""
|
|
327
|
+
skill_directory = tmp_path / "base-theme-match"
|
|
328
|
+
skill_directory.mkdir()
|
|
329
|
+
(skill_directory / "SKILL.md").write_text(SKILL_MD_SCRIPTS_LAYOUT, encoding="utf-8")
|
|
330
|
+
scripts_directory = skill_directory / "scripts"
|
|
331
|
+
scripts_directory.mkdir()
|
|
332
|
+
_write_sibling_files(scripts_directory, ["discover_candidates.py", "resolve_stp.py"])
|
|
333
|
+
return scripts_directory
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_blocks_new_script_absent_from_parent_skill_layout(tmp_path: Path):
|
|
337
|
+
scripts_directory = _skill_scripts_directory(tmp_path)
|
|
338
|
+
new_file_path = scripts_directory / "stp_selection.py"
|
|
339
|
+
result = _run_hook(
|
|
340
|
+
"Write",
|
|
341
|
+
{"file_path": str(new_file_path), "content": "x = 1\n"},
|
|
342
|
+
)
|
|
343
|
+
assert result.returncode == 0
|
|
344
|
+
payload = json.loads(result.stdout)
|
|
345
|
+
assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
346
|
+
assert "stp_selection.py" in payload["hookSpecificOutput"]["permissionDecisionReason"]
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_allows_new_script_named_in_parent_skill_layout(tmp_path: Path):
|
|
350
|
+
scripts_directory = _skill_scripts_directory(tmp_path)
|
|
351
|
+
new_file_path = scripts_directory / "select_stp.py"
|
|
352
|
+
result = _run_hook(
|
|
353
|
+
"Write",
|
|
354
|
+
{"file_path": str(new_file_path), "content": "x = 1\n"},
|
|
355
|
+
)
|
|
356
|
+
assert result.returncode == 0
|
|
357
|
+
assert result.stdout.strip() == ""
|
|
358
|
+
|
|
359
|
+
|
|
314
360
|
def test_find_stale_inventory_returns_survey_for_omission(tmp_path: Path):
|
|
315
361
|
package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
|
|
316
362
|
_write_sibling_files(package_directory, ["dialer_compose.py", "compose_dialer_cli.py"])
|
|
@@ -632,24 +632,24 @@ def test_dispatcher_write_applies_both_groups() -> None:
|
|
|
632
632
|
assert "blocking/plain_language_blocker.py" in all_write_script_paths, (
|
|
633
633
|
"plain_language_blocker (Group B) must be in Write applicable set"
|
|
634
634
|
)
|
|
635
|
-
assert len(all_write_entries) ==
|
|
636
|
-
f"Write tool must apply to all
|
|
635
|
+
assert len(all_write_entries) == 19, (
|
|
636
|
+
f"Write tool must apply to all 19 hosted hooks, got {len(all_write_entries)}"
|
|
637
637
|
)
|
|
638
638
|
|
|
639
639
|
|
|
640
640
|
def test_dispatcher_edit_applies_both_groups() -> None:
|
|
641
641
|
"""Edit tool triggers both Group A and Group B hooks through the dispatcher."""
|
|
642
642
|
all_edit_entries = _applicable_entries_for_tool(EDIT_TOOL_NAME)
|
|
643
|
-
assert len(all_edit_entries) ==
|
|
644
|
-
f"Edit tool must apply to all
|
|
643
|
+
assert len(all_edit_entries) == 19, (
|
|
644
|
+
f"Edit tool must apply to all 19 hosted hooks, got {len(all_edit_entries)}"
|
|
645
645
|
)
|
|
646
646
|
|
|
647
647
|
|
|
648
648
|
def test_dispatcher_multi_edit_applies_only_group_b() -> None:
|
|
649
|
-
"""MultiEdit tool triggers only Group B (
|
|
649
|
+
"""MultiEdit tool triggers only Group B (8 hooks), not Group A."""
|
|
650
650
|
all_multi_edit_entries = _applicable_entries_for_tool(MULTI_EDIT_TOOL_NAME)
|
|
651
|
-
assert len(all_multi_edit_entries) ==
|
|
652
|
-
f"MultiEdit tool must apply to exactly
|
|
651
|
+
assert len(all_multi_edit_entries) == 8, (
|
|
652
|
+
f"MultiEdit tool must apply to exactly 8 Group-B hooks, got {len(all_multi_edit_entries)}"
|
|
653
653
|
)
|
|
654
654
|
|
|
655
655
|
|
|
@@ -13,6 +13,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
13
13
|
| `bot_mention_comment_blocker_constants.py` | Patterns for detecting bot @-mentions in PR comments |
|
|
14
14
|
| `claude_md_orphan_file_blocker_constants.py` | Table patterns, file extensions, scan budget, and block-message text for the CLAUDE.md orphan-file blocker |
|
|
15
15
|
| `code_rules_enforcer_constants.py` | File-extension sets, test-path patterns, advisory line thresholds, boolean-name prefixes |
|
|
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 |
|
|
16
17
|
| `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
|
|
17
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 |
|
|
18
19
|
| `convergence_branch_constants.py` | Branch and worktree naming patterns for the convergence gate |
|
|
@@ -45,12 +45,14 @@ ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX: tuple[str, ...] = (
|
|
|
45
45
|
"json",
|
|
46
46
|
)
|
|
47
47
|
MAX_JS_RESUME_TASK_ENUMERATION_ISSUES: int = 5
|
|
48
|
+
MAX_JS_RETURNS_OBJECT_SCHEMALESS_ISSUES: int = 5
|
|
48
49
|
MINIMUM_RESUME_TASK_ENUMERATION_ITEMS: int = 2
|
|
49
50
|
DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
|
|
50
51
|
MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
|
|
51
52
|
DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT: int = 2
|
|
52
53
|
MAX_DOCSTRING_NO_CONSUMER_CLAIM_ISSUES: int = 3
|
|
53
54
|
MAX_DOCSTRING_UNGUARDED_PAYLOAD_CLAIM_ISSUES: int = 3
|
|
55
|
+
MAX_DOCSTRING_NO_NETWORK_CLAIM_ISSUES: int = 3
|
|
54
56
|
MAX_STALE_TEST_NAME_TARGET_ISSUES: int = 3
|
|
55
57
|
STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT: int = 2
|
|
56
58
|
MAX_MODULE_DOCSTRING_CHECK_ROSTER_ISSUES: int = 5
|
|
@@ -79,6 +81,36 @@ MINIMUM_TOKENS_FOR_DISPATCH_CALLEE: int = 2
|
|
|
79
81
|
MAX_DOCSTRING_UNDEFINED_CONSTANT_ISSUES: int = 3
|
|
80
82
|
MAX_DOCSTRING_RETURNS_PLURAL_CARDINALITY_ISSUES: int = 5
|
|
81
83
|
SINGLE_DICT_KEY_COUNT_FOR_PLURAL_CARDINALITY_DRIFT: int = 1
|
|
84
|
+
MAX_LENGTH_CONSTANT_SUPERLATIVE_ISSUES: int = 3
|
|
85
|
+
LENGTH_GATE_PACKAGE_SCAN_FILE_LIMIT: int = 400
|
|
86
|
+
LENGTH_CONFIG_SUBDIRECTORY_NAME: str = "config"
|
|
87
|
+
ALL_LENGTH_CONSTANT_NAME_SUFFIXES: tuple[str, ...] = ("_LENGTH", "_LEN")
|
|
88
|
+
ALL_LENGTH_SUPERLATIVE_RANGE_PHRASES: tuple[str, ...] = (
|
|
89
|
+
"longest",
|
|
90
|
+
"maximum length",
|
|
91
|
+
"no longer than",
|
|
92
|
+
)
|
|
93
|
+
MAX_DOCSTRING_FIELD_RUNMODE_OUTCOME_ISSUES: int = 5
|
|
94
|
+
ALL_DOCSTRING_RUNMODE_FLAG_FIELD_NAME_TOKENS: tuple[str, ...] = ("dry_run",)
|
|
95
|
+
ALL_DOCSTRING_PER_RECORD_WRITE_OUTCOME_PHRASES: tuple[str, ...] = (
|
|
96
|
+
"was written",
|
|
97
|
+
"is written",
|
|
98
|
+
"were written",
|
|
99
|
+
"nothing was written",
|
|
100
|
+
"no file was written",
|
|
101
|
+
"wrote",
|
|
102
|
+
"written to disk",
|
|
103
|
+
"written to the file",
|
|
104
|
+
)
|
|
105
|
+
ALL_DOCSTRING_RUN_MODE_PHRASES: tuple[str, ...] = (
|
|
106
|
+
"dry run",
|
|
107
|
+
"dry-run",
|
|
108
|
+
"execute run",
|
|
109
|
+
"--execute",
|
|
110
|
+
"run mode",
|
|
111
|
+
"live run",
|
|
112
|
+
"not an execute",
|
|
113
|
+
)
|
|
82
114
|
MAX_DOCSTRING_RAISES_LARGEZIPFILE_ISSUES: int = 5
|
|
83
115
|
DOCSTRING_LARGE_ZIP_FILE_EXCEPTION_NAME: str = "LargeZipFile"
|
|
84
116
|
ZIPFILE_WRITER_CLASS_NAME: str = "ZipFile"
|
|
@@ -187,6 +219,26 @@ ALL_DOCSTRING_GUARDED_FAILURE_CLAIM_PHRASES: tuple[str, ...] = (
|
|
|
187
219
|
"malformed payload yields none",
|
|
188
220
|
)
|
|
189
221
|
|
|
222
|
+
ALL_DOCSTRING_NO_NETWORK_CLAIM_PHRASES: tuple[str, ...] = (
|
|
223
|
+
"without touching the network",
|
|
224
|
+
"without touching the share",
|
|
225
|
+
"without touching the network share",
|
|
226
|
+
"without hitting the network",
|
|
227
|
+
"without hitting the share",
|
|
228
|
+
"without a network call",
|
|
229
|
+
"without any network call",
|
|
230
|
+
"without network access",
|
|
231
|
+
"without a network round-trip",
|
|
232
|
+
"without a network round trip",
|
|
233
|
+
"no network access",
|
|
234
|
+
"no network round-trip",
|
|
235
|
+
"no network round trip",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
ALL_PATH_METADATA_ACCESS_METHOD_NAMES: frozenset[str] = frozenset(
|
|
239
|
+
{"is_file", "is_dir", "exists", "stat", "lstat"}
|
|
240
|
+
)
|
|
241
|
+
|
|
190
242
|
MAX_DOCSTRING_INLINE_LITERAL_CLAIM_ISSUES: int = 3
|
|
191
243
|
ALL_DOCSTRING_NO_INLINE_LITERAL_CLAIM_PHRASES: tuple[str, ...] = (
|
|
192
244
|
"no literals appear inline",
|
|
@@ -228,3 +280,30 @@ ALL_TEST_INDICATING_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
|
228
280
|
"UNIT_TEST",
|
|
229
281
|
}
|
|
230
282
|
)
|
|
283
|
+
|
|
284
|
+
MAX_MODULE_DOCSTRING_DATA_SCHEMA_SCOPE_ISSUES: int = 1
|
|
285
|
+
MODULE_DOCSTRING_DATA_SCHEMA_CONSTANT_SAMPLE_LIMIT: int = 6
|
|
286
|
+
ALL_USER_FACING_TEXT_SCOPE_DOCSTRING_PHRASES: tuple[str, ...] = (
|
|
287
|
+
"user-facing",
|
|
288
|
+
"user facing",
|
|
289
|
+
)
|
|
290
|
+
ALL_DATA_SCHEMA_CONSTANT_NAME_MARKERS: tuple[str, ...] = (
|
|
291
|
+
"_FIELD_",
|
|
292
|
+
"_KEY_",
|
|
293
|
+
"_SCHEMA_",
|
|
294
|
+
"_ENCODING",
|
|
295
|
+
"_FORMAT_STRING",
|
|
296
|
+
)
|
|
297
|
+
ALL_DATA_SCHEMA_DOCSTRING_ACKNOWLEDGEMENT_PHRASES: tuple[str, ...] = (
|
|
298
|
+
"field key",
|
|
299
|
+
"schema key",
|
|
300
|
+
"schema",
|
|
301
|
+
"metadata",
|
|
302
|
+
"encoding",
|
|
303
|
+
"format string",
|
|
304
|
+
"runtime config",
|
|
305
|
+
"runtime-config",
|
|
306
|
+
"data schema",
|
|
307
|
+
"data-schema",
|
|
308
|
+
"config constant",
|
|
309
|
+
)
|
|
@@ -39,6 +39,20 @@ DENY_REASON_ISSUE_PREVIEW_COUNT = 10
|
|
|
39
39
|
ALL_BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_", "was_", "did_")
|
|
40
40
|
UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
41
41
|
|
|
42
|
+
ALL_POLARITY_ANTONYM_TOKEN_PAIRS: tuple[tuple[str, str], ...] = (
|
|
43
|
+
("allowed", "forbidden"),
|
|
44
|
+
("allowed", "denied"),
|
|
45
|
+
("allowed", "blocked"),
|
|
46
|
+
("permitted", "forbidden"),
|
|
47
|
+
("permitted", "denied"),
|
|
48
|
+
("included", "excluded"),
|
|
49
|
+
("valid", "invalid"),
|
|
50
|
+
("enabled", "disabled"),
|
|
51
|
+
("visible", "hidden"),
|
|
52
|
+
("present", "missing"),
|
|
53
|
+
)
|
|
54
|
+
POLARITY_TOKEN_BOUNDARY_PATTERN: str = r"(?:^|_)%s(?:_|$)"
|
|
55
|
+
|
|
42
56
|
ALL_MUST_CHECK_RETURN_FUNCTION_NAMES: frozenset[str] = frozenset({"find_and_click", "write_outcome"})
|
|
43
57
|
|
|
44
58
|
DOCSTRING_ARG_ENTRY_PATTERN: re.Pattern[str] = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*[:(]")
|
|
@@ -208,6 +222,20 @@ ENUMERATION_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
|
208
222
|
HYPHENATED_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
209
223
|
r"^[a-z0-9]+(?:-[a-z0-9]+)+$"
|
|
210
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
|
+
)
|
|
211
239
|
ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
|
|
212
240
|
"get", "items", "keys", "values", "update", "pop",
|
|
213
241
|
"setdefault", "copy", "clear",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Constants for the env-var-table code-drift blocker.
|
|
2
|
+
|
|
3
|
+
Holds the markdown filename matcher, the table-row and cell patterns, the
|
|
4
|
+
env-var-name and code-file-extension recognizers, the bounded-scan budgets, and
|
|
5
|
+
the block-message strings. The blocker imports each of these by name.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MARKDOWN_FILE_EXTENSION",
|
|
14
|
+
"TABLE_ROW_PATTERN",
|
|
15
|
+
"CODE_FENCE_PATTERN",
|
|
16
|
+
"SEPARATOR_CELL_PATTERN",
|
|
17
|
+
"BACKTICK_TOKEN_PATTERN",
|
|
18
|
+
"ENV_VAR_NAME_PATTERN",
|
|
19
|
+
"ALL_CODE_FILE_EXTENSIONS",
|
|
20
|
+
"ALL_NOISE_DIRECTORY_NAMES",
|
|
21
|
+
"GIT_DIRECTORY_NAME",
|
|
22
|
+
"MINIMUM_ENV_VAR_ROW_CELL_COUNT",
|
|
23
|
+
"MAX_SUBTREE_FILES_SCANNED",
|
|
24
|
+
"MAX_DRIFT_ISSUES",
|
|
25
|
+
"DRIFT_MESSAGE_TEMPLATE",
|
|
26
|
+
"DRIFT_ADDITIONAL_CONTEXT",
|
|
27
|
+
"DRIFT_SYSTEM_MESSAGE",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
MARKDOWN_FILE_EXTENSION = ".md"
|
|
31
|
+
|
|
32
|
+
TABLE_ROW_PATTERN = re.compile(r"^\s*\|")
|
|
33
|
+
CODE_FENCE_PATTERN = re.compile(r"^\s*(```|~~~)")
|
|
34
|
+
SEPARATOR_CELL_PATTERN = re.compile(r"^[:\-\s]+$")
|
|
35
|
+
BACKTICK_TOKEN_PATTERN = re.compile(r"`([^`]+)`")
|
|
36
|
+
ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]{1,}$")
|
|
37
|
+
|
|
38
|
+
ALL_CODE_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
39
|
+
{".py", ".mjs", ".js", ".ts", ".ps1", ".sh"}
|
|
40
|
+
)
|
|
41
|
+
ALL_NOISE_DIRECTORY_NAMES: frozenset[str] = frozenset(
|
|
42
|
+
{".git", "__pycache__", "node_modules", ".pytest_cache", ".mypy_cache", ".ruff_cache"}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
GIT_DIRECTORY_NAME = ".git"
|
|
46
|
+
MINIMUM_ENV_VAR_ROW_CELL_COUNT = 2
|
|
47
|
+
|
|
48
|
+
MAX_SUBTREE_FILES_SCANNED = 20000
|
|
49
|
+
MAX_DRIFT_ISSUES = 20
|
|
50
|
+
|
|
51
|
+
DRIFT_MESSAGE_TEMPLATE = (
|
|
52
|
+
"Env-var summary table in {file} attributes an environment variable to a "
|
|
53
|
+
"code file that does not read it: {drift}. The code file no longer "
|
|
54
|
+
"references the variable, so the table points a reader at a consumer "
|
|
55
|
+
"relationship the code does not have. Remove or correct the row so the "
|
|
56
|
+
"summary matches the code."
|
|
57
|
+
)
|
|
58
|
+
DRIFT_ADDITIONAL_CONTEXT = (
|
|
59
|
+
"Each `VARIABLE` | `code/file.py` row in a markdown env-var summary table "
|
|
60
|
+
"names a code file that reads that variable. When the code file exists but "
|
|
61
|
+
"its source never references the variable name, the row is stale. Drop the "
|
|
62
|
+
"row, or point it at the variable the file actually reads."
|
|
63
|
+
)
|
|
64
|
+
DRIFT_SYSTEM_MESSAGE = "Blocked: env-var summary table attributes a variable to a code file that does not read it."
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Constants for the package-inventory stale-entry blocker.
|
|
2
2
|
|
|
3
3
|
A package directory documents its own files in a sibling inventory document —
|
|
4
|
-
a ``README.md`` Layout table
|
|
4
|
+
a ``README.md`` Layout table, a ``CLAUDE.md`` "Key files" list, or a skill
|
|
5
|
+
``SKILL.md`` Layout table that maps the ``scripts/`` subdirectory — whose entries
|
|
5
6
|
name each file in backticks. When a new production code file lands in that
|
|
6
7
|
directory and the inventory carries no entry naming it, the inventory disagrees
|
|
7
8
|
with the directory on the package's file set, and a reader trusting the
|
|
@@ -19,6 +20,8 @@ import re
|
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"ALL_INVENTORY_DOCUMENT_NAMES",
|
|
23
|
+
"SKILL_INVENTORY_DOCUMENT_NAME",
|
|
24
|
+
"SCRIPTS_SUBDIRECTORY_NAME",
|
|
22
25
|
"ALL_PRODUCTION_CODE_EXTENSIONS",
|
|
23
26
|
"PYTHON_FILE_EXTENSION",
|
|
24
27
|
"ALL_TEST_FILE_MARKERS",
|
|
@@ -35,7 +38,13 @@ __all__ = [
|
|
|
35
38
|
"STALE_INVENTORY_ADDITIONAL_CONTEXT",
|
|
36
39
|
]
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
SKILL_INVENTORY_DOCUMENT_NAME: str = "SKILL.md"
|
|
42
|
+
|
|
43
|
+
SCRIPTS_SUBDIRECTORY_NAME: str = "scripts"
|
|
44
|
+
|
|
45
|
+
ALL_INVENTORY_DOCUMENT_NAMES: frozenset[str] = frozenset(
|
|
46
|
+
{"README.md", "CLAUDE.md", SKILL_INVENTORY_DOCUMENT_NAME}
|
|
47
|
+
)
|
|
39
48
|
|
|
40
49
|
PYTHON_FILE_EXTENSION: str = ".py"
|
|
41
50
|
|
|
@@ -96,16 +105,18 @@ STALE_INVENTORY_MESSAGE_TEMPLATE: str = (
|
|
|
96
105
|
|
|
97
106
|
STALE_INVENTORY_SYSTEM_MESSAGE: str = (
|
|
98
107
|
"New production file is absent from its package inventory (README.md / "
|
|
99
|
-
"CLAUDE.md) - add the inventory entry in this same change"
|
|
108
|
+
"CLAUDE.md / SKILL.md) - add the inventory entry in this same change"
|
|
100
109
|
)
|
|
101
110
|
|
|
102
111
|
STALE_INVENTORY_ADDITIONAL_CONTEXT: str = (
|
|
103
|
-
"A package directory whose README.md or
|
|
104
|
-
"backticks is a maintained inventory of the package's file set. A
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
112
|
+
"A package directory whose README.md, CLAUDE.md, or SKILL.md lists its files "
|
|
113
|
+
"in backticks is a maintained inventory of the package's file set. A skill "
|
|
114
|
+
"SKILL.md Layout table that maps the scripts/ subdirectory counts as the "
|
|
115
|
+
"inventory for files in that subdirectory. A new production code file (.py, "
|
|
116
|
+
".mjs, .js, .ts, .ps1, .sh) in an inventoried directory carries one entry "
|
|
117
|
+
"naming it. Add a row to the README.md or SKILL.md table or a bullet to the "
|
|
118
|
+
"CLAUDE.md list naming this file, describing what it does, in the same change "
|
|
119
|
+
"that creates the file. Exempt files (no entry needed): "
|
|
109
120
|
"__init__.py, conftest.py, setup.py, _path_setup.py, files under config/ or "
|
|
110
121
|
"tests/, and test files (test_*.py, *_test.py, *.spec.*, *.test.*)."
|
|
111
122
|
)
|
|
@@ -21,7 +21,15 @@ MINIMUM_COVERED_PUBLIC_FUNCTIONS: int = 1
|
|
|
21
21
|
MAX_PAIRED_TEST_COVERAGE_ISSUES: int = 25
|
|
22
22
|
MISSING_PAIRED_TEST_GUIDANCE: str = (
|
|
23
23
|
"is exercised by no test in the module's paired test suite, though that"
|
|
24
|
-
" suite already
|
|
25
|
-
"
|
|
26
|
-
" or side effect (CODE_RULES TDD
|
|
24
|
+
" suite already exercises this module (covering another public function or"
|
|
25
|
+
" referencing a private helper) - add a behavioral test that calls this"
|
|
26
|
+
" function and asserts on its return value or side effect (CODE_RULES TDD"
|
|
27
|
+
" paired-test rule)"
|
|
28
|
+
)
|
|
29
|
+
TEST_SUITE_OMITS_FUNCTION_GUIDANCE: str = (
|
|
30
|
+
"is a public function this stem-matched test suite defines its module for"
|
|
31
|
+
" but exercises nowhere, though the suite already covers another public"
|
|
32
|
+
" function in that module - add a behavioral test that calls this function"
|
|
33
|
+
" and asserts on its return value or side effect, or remove the function"
|
|
34
|
+
" (CODE_RULES TDD paired-test rule)"
|
|
27
35
|
)
|
|
@@ -129,6 +129,10 @@ ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
|
129
129
|
script_relative_path="blocking/package_inventory_stale_blocker.py",
|
|
130
130
|
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
131
131
|
),
|
|
132
|
+
HostedHookEntry(
|
|
133
|
+
script_relative_path="blocking/env_var_table_code_drift_blocker.py",
|
|
134
|
+
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
135
|
+
),
|
|
132
136
|
HostedHookEntry(
|
|
133
137
|
script_relative_path="blocking/pytest_testpaths_orphan_blocker.py",
|
|
134
138
|
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|