claude-dev-env 1.92.1 → 1.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -2
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +340 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +165 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +2 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +74 -78
- package/skills/orchestrator-refresh/SKILL.md +24 -12
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +56 -0
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- package/skills/verified-build/SKILL.md +4 -9
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Test-module dead-scaffolding checks: dead constant, unused helper parameter.
|
|
2
|
+
|
|
3
|
+
Both checks run on a test module where scaffolding left after an edit hides in
|
|
4
|
+
plain sight: when a removed monkeypatch line was the last reader of a module
|
|
5
|
+
constant, or the last user of a private helper's parameter, the leftover is
|
|
6
|
+
dead code the checks flag at Write/Edit time.
|
|
7
|
+
|
|
8
|
+
The dead-constant check runs only on a ``test_*.py`` / ``*_test.py`` module of
|
|
9
|
+
test functions, which exports nothing, so a single-file scan proves a constant
|
|
10
|
+
no other line reads is dead. A ``conftest.py``, a shared helper module reached
|
|
11
|
+
only through the ``tests/`` directory gate, and a dedicated constants or
|
|
12
|
+
``config/`` module all export their constants to importer modules, so the
|
|
13
|
+
cross-module ``check_dead_module_constants`` governs their constants and the
|
|
14
|
+
dead-constant check leaves them alone.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
from typing import TypeGuard
|
|
19
|
+
|
|
20
|
+
from code_rules_dead_module_constant import _is_dedicated_constants_module
|
|
21
|
+
from code_rules_shared import is_strict_test_file
|
|
22
|
+
from hooks_constants.test_layout_constants import (
|
|
23
|
+
CLASS_METHOD_FIRST_PARAMETER_NAME,
|
|
24
|
+
DEAD_TEST_CONSTANT_GUIDANCE,
|
|
25
|
+
FIXTURE_DECORATOR_MARKER,
|
|
26
|
+
MAX_TEST_LAYOUT_ISSUES,
|
|
27
|
+
MINIMUM_CONSTANT_NAME_LENGTH,
|
|
28
|
+
PRIVATE_NAME_PREFIX,
|
|
29
|
+
SELF_PARAMETER_NAME,
|
|
30
|
+
TEST_FUNCTION_MODULE_BASENAME_PATTERN,
|
|
31
|
+
UNUSED_TEST_HELPER_PARAMETER_GUIDANCE,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_FunctionNode = ast.FunctionDef | ast.AsyncFunctionDef
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_module(content: str) -> ast.Module | None:
|
|
38
|
+
"""Return the parsed module, or None when the content does not parse."""
|
|
39
|
+
try:
|
|
40
|
+
return ast.parse(content)
|
|
41
|
+
except SyntaxError:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
def _is_dead_constant_scan_target(file_path: str) -> bool:
|
|
45
|
+
"""Return whether a single-file dead-constant scan can judge this module.
|
|
46
|
+
|
|
47
|
+
::
|
|
48
|
+
|
|
49
|
+
tests/test_report.py -> scan (test functions export nothing)
|
|
50
|
+
tests/conftest.py -> skip (fixtures shared with siblings)
|
|
51
|
+
tests/expected_values.py -> skip (a shared module exports)
|
|
52
|
+
hooks_constants/test_layout_constants.py -> skip (a constants module exports)
|
|
53
|
+
|
|
54
|
+
A ``test_*`` / ``*_test`` module of test functions exports nothing, so a
|
|
55
|
+
constant no other line reads is dead. A ``conftest.py``, a shared helper
|
|
56
|
+
module reached only through the ``tests/`` directory gate, and a dedicated
|
|
57
|
+
constants or ``config/`` module all export to importer modules, where the
|
|
58
|
+
cross-module ``check_dead_module_constants`` governs their constants.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
file_path: The destination path of the write.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True only for a ``test_*.py`` / ``*_test.py`` module that is not a
|
|
65
|
+
dedicated constants or ``config/`` module.
|
|
66
|
+
"""
|
|
67
|
+
if _is_dedicated_constants_module(file_path):
|
|
68
|
+
return False
|
|
69
|
+
basename = file_path.lower().replace("\\", "/").rsplit("/", 1)[-1]
|
|
70
|
+
return TEST_FUNCTION_MODULE_BASENAME_PATTERN.match(basename) is not None
|
|
71
|
+
|
|
72
|
+
def _is_private_constant_name(name: str) -> bool:
|
|
73
|
+
"""Return whether a name is a private UPPER_SNAKE constant identifier.
|
|
74
|
+
|
|
75
|
+
::
|
|
76
|
+
|
|
77
|
+
_ABSENT_DOTENV_FILENAME -> private constant, judged
|
|
78
|
+
EXPECTED_TOTAL -> public constant, left alone
|
|
79
|
+
|
|
80
|
+
Only a leading-underscore name qualifies. A public module constant exports
|
|
81
|
+
to importer modules, so the cross-module ``check_dead_module_constants``
|
|
82
|
+
governs it and this single-file scan leaves it alone.
|
|
83
|
+
"""
|
|
84
|
+
if not name.startswith(PRIVATE_NAME_PREFIX):
|
|
85
|
+
return False
|
|
86
|
+
if len(name) < MINIMUM_CONSTANT_NAME_LENGTH:
|
|
87
|
+
return False
|
|
88
|
+
if not name.replace("_", "").isalnum():
|
|
89
|
+
return False
|
|
90
|
+
return name == name.upper() and any(each_char.isalpha() for each_char in name)
|
|
91
|
+
|
|
92
|
+
def _assignment_targets(statement: ast.stmt) -> list[ast.expr]:
|
|
93
|
+
"""Return the assignment targets of a plain or annotated assignment."""
|
|
94
|
+
if isinstance(statement, ast.Assign):
|
|
95
|
+
return list(statement.targets)
|
|
96
|
+
if isinstance(statement, ast.AnnAssign) and statement.value is not None:
|
|
97
|
+
return [statement.target]
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
def _statement_constant_targets(statement: ast.stmt) -> list[tuple[str, int]]:
|
|
101
|
+
"""Return (name, line) for each private UPPER_SNAKE constant one statement binds."""
|
|
102
|
+
named_targets: list[tuple[str, int]] = []
|
|
103
|
+
for each_target in _assignment_targets(statement):
|
|
104
|
+
if isinstance(each_target, ast.Name) and _is_private_constant_name(each_target.id):
|
|
105
|
+
named_targets.append((each_target.id, statement.lineno))
|
|
106
|
+
return named_targets
|
|
107
|
+
|
|
108
|
+
def _module_constant_targets(tree: ast.Module) -> list[tuple[str, int]]:
|
|
109
|
+
"""Return (name, line) for every module-scope private UPPER_SNAKE constant, in order."""
|
|
110
|
+
constant_targets: list[tuple[str, int]] = []
|
|
111
|
+
for each_statement in tree.body:
|
|
112
|
+
constant_targets.extend(_statement_constant_targets(each_statement))
|
|
113
|
+
return constant_targets
|
|
114
|
+
|
|
115
|
+
def _referenced_names(tree: ast.Module) -> set[str]:
|
|
116
|
+
"""Return every name the module reads plus every string literal it holds.
|
|
117
|
+
|
|
118
|
+
A ``Load``-context name is a read; a ``Store`` target (the constant's own
|
|
119
|
+
definition) is not, so a constant read nowhere else stays out of the set.
|
|
120
|
+
String literals are unioned in so an ``__all__`` or ``getattr`` string name
|
|
121
|
+
still counts.
|
|
122
|
+
"""
|
|
123
|
+
all_nodes = list(ast.walk(tree))
|
|
124
|
+
load_names = {
|
|
125
|
+
each_node.id
|
|
126
|
+
for each_node in all_nodes
|
|
127
|
+
if isinstance(each_node, ast.Name) and isinstance(each_node.ctx, ast.Load)
|
|
128
|
+
}
|
|
129
|
+
literal_values = {
|
|
130
|
+
each_node.value
|
|
131
|
+
for each_node in all_nodes
|
|
132
|
+
if isinstance(each_node, ast.Constant) and isinstance(each_node.value, str)
|
|
133
|
+
}
|
|
134
|
+
return load_names | literal_values
|
|
135
|
+
|
|
136
|
+
def _dead_constant_messages(tree: ast.Module, referenced_names: set[str]) -> list[str]:
|
|
137
|
+
"""Return one message per module constant absent from the referenced set."""
|
|
138
|
+
issues: list[str] = []
|
|
139
|
+
for each_name, each_line in _module_constant_targets(tree):
|
|
140
|
+
if each_name in referenced_names:
|
|
141
|
+
continue
|
|
142
|
+
issues.append(
|
|
143
|
+
f"Line {each_line}: constant {each_name!r} - {DEAD_TEST_CONSTANT_GUIDANCE}"
|
|
144
|
+
)
|
|
145
|
+
if len(issues) >= MAX_TEST_LAYOUT_ISSUES:
|
|
146
|
+
break
|
|
147
|
+
return issues
|
|
148
|
+
|
|
149
|
+
def check_dead_test_module_constant(content: str, file_path: str) -> list[str]:
|
|
150
|
+
"""Flag a private module-level constant in a test-function module no line reads.
|
|
151
|
+
|
|
152
|
+
::
|
|
153
|
+
|
|
154
|
+
tests/test_report.py, a private UPPER_SNAKE constant no Load reference
|
|
155
|
+
and no string literal names -> dead scaffolding, flag it
|
|
156
|
+
tests/test_report.py, a public constant (EXPECTED_TOTAL) -> skip
|
|
157
|
+
tests/conftest.py / tests/expected_values.py / *_constants.py -> skip
|
|
158
|
+
|
|
159
|
+
A private constant read by no reference and named in no string literal is
|
|
160
|
+
dead scaffolding an edit stranded. A public constant, a ``conftest.py``, a
|
|
161
|
+
shared helper module under ``tests/``, and a dedicated constants or
|
|
162
|
+
``config/`` module all export to importer modules, so the cross-module
|
|
163
|
+
``check_dead_module_constants`` judges those and this check skips them.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
content: The post-edit file content under validation.
|
|
167
|
+
file_path: The destination path, used for the test-function-module gate.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
One message per dead private constant, capped at the configured maximum.
|
|
171
|
+
"""
|
|
172
|
+
if not _is_dead_constant_scan_target(file_path):
|
|
173
|
+
return []
|
|
174
|
+
tree = _parse_module(content)
|
|
175
|
+
if tree is None:
|
|
176
|
+
return []
|
|
177
|
+
return _dead_constant_messages(tree, _referenced_names(tree))
|
|
178
|
+
|
|
179
|
+
def _node_names_fixture(node: ast.AST) -> bool:
|
|
180
|
+
"""Return whether one decorator sub-node spells the pytest fixture marker."""
|
|
181
|
+
if isinstance(node, ast.Name):
|
|
182
|
+
return FIXTURE_DECORATOR_MARKER in node.id
|
|
183
|
+
if isinstance(node, ast.Attribute):
|
|
184
|
+
return FIXTURE_DECORATOR_MARKER in node.attr
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _has_fixture_decorator(function_node: _FunctionNode) -> bool:
|
|
188
|
+
"""Return whether a function carries a pytest fixture decorator.
|
|
189
|
+
|
|
190
|
+
A fixture parameter is injected by pytest and read by name only, so a
|
|
191
|
+
fixture must never be judged for unused parameters.
|
|
192
|
+
"""
|
|
193
|
+
for each_decorator in function_node.decorator_list:
|
|
194
|
+
if any(
|
|
195
|
+
_node_names_fixture(each_node) for each_node in ast.walk(each_decorator)
|
|
196
|
+
):
|
|
197
|
+
return True
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
def _is_judged_parameter(parameter_name: str) -> bool:
|
|
201
|
+
"""Return whether a parameter name is worth judging for being unread.
|
|
202
|
+
|
|
203
|
+
``self`` and ``cls`` are receiver names, and an underscore-prefixed name is
|
|
204
|
+
a conventional throwaway, so none of the three are judged.
|
|
205
|
+
"""
|
|
206
|
+
if parameter_name in {SELF_PARAMETER_NAME, CLASS_METHOD_FIRST_PARAMETER_NAME}:
|
|
207
|
+
return False
|
|
208
|
+
return not parameter_name.startswith(PRIVATE_NAME_PREFIX)
|
|
209
|
+
|
|
210
|
+
def _candidate_parameters(function_node: _FunctionNode) -> list[ast.arg]:
|
|
211
|
+
"""Return the parameters of a function worth judging for being unread."""
|
|
212
|
+
arguments = function_node.args
|
|
213
|
+
all_parameters = arguments.posonlyargs + arguments.args + arguments.kwonlyargs
|
|
214
|
+
return [each for each in all_parameters if _is_judged_parameter(each.arg)]
|
|
215
|
+
|
|
216
|
+
def _read_parameter_names(function_node: _FunctionNode) -> set[str]:
|
|
217
|
+
"""Return every name read anywhere in a function body."""
|
|
218
|
+
return {
|
|
219
|
+
each_node.id
|
|
220
|
+
for each_node in ast.walk(function_node)
|
|
221
|
+
if isinstance(each_node, ast.Name) and isinstance(each_node.ctx, ast.Load)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def _unused_parameter_messages(function_node: _FunctionNode) -> list[str]:
|
|
225
|
+
"""Return one message per candidate parameter the function body never reads."""
|
|
226
|
+
read_names = _read_parameter_names(function_node)
|
|
227
|
+
issues: list[str] = []
|
|
228
|
+
for each_parameter in _candidate_parameters(function_node):
|
|
229
|
+
if each_parameter.arg in read_names:
|
|
230
|
+
continue
|
|
231
|
+
issues.append(
|
|
232
|
+
f"Line {each_parameter.lineno}: parameter {each_parameter.arg!r}"
|
|
233
|
+
f" on {function_node.name!r} - {UNUSED_TEST_HELPER_PARAMETER_GUIDANCE}"
|
|
234
|
+
)
|
|
235
|
+
return issues
|
|
236
|
+
|
|
237
|
+
def _is_private_plain_function(statement: ast.stmt) -> TypeGuard[_FunctionNode]:
|
|
238
|
+
"""Return whether a statement is a private, non-fixture module-level function."""
|
|
239
|
+
if not isinstance(statement, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
240
|
+
return False
|
|
241
|
+
if not statement.name.startswith(PRIVATE_NAME_PREFIX):
|
|
242
|
+
return False
|
|
243
|
+
return not _has_fixture_decorator(statement)
|
|
244
|
+
|
|
245
|
+
def check_unused_test_helper_parameter(content: str, file_path: str) -> list[str]:
|
|
246
|
+
"""Flag a parameter of a private test helper that its body never reads.
|
|
247
|
+
|
|
248
|
+
::
|
|
249
|
+
|
|
250
|
+
def _configuration(monkeypatch, tmp_path): neither read -> flag
|
|
251
|
+
return DatabaseConfig.from_environment()
|
|
252
|
+
|
|
253
|
+
A private module-level function carries no pytest fixture decorator, so its
|
|
254
|
+
parameter is never injected by name, and one the body never reads is dead.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
content: The post-edit file content under validation.
|
|
258
|
+
file_path: The destination path, used for the test-file gate.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
One message per unused private-helper parameter, capped at the maximum.
|
|
262
|
+
"""
|
|
263
|
+
if not is_strict_test_file(file_path):
|
|
264
|
+
return []
|
|
265
|
+
tree = _parse_module(content)
|
|
266
|
+
if tree is None:
|
|
267
|
+
return []
|
|
268
|
+
issues: list[str] = []
|
|
269
|
+
for each_statement in tree.body:
|
|
270
|
+
if _is_private_plain_function(each_statement):
|
|
271
|
+
issues.extend(_unused_parameter_messages(each_statement))
|
|
272
|
+
return issues[:MAX_TEST_LAYOUT_ISSUES]
|
|
@@ -15,7 +15,7 @@ found by skipping ``VAR=value`` assignments and launcher wrappers (``timeout``,
|
|
|
15
15
|
``nohup``, ``nice``, ``stdbuf``, ``setsid``, ``env``) with their flags and duration
|
|
16
16
|
arguments. A segment is evaluated only when its leading program's basename is
|
|
17
17
|
``ssh``/``scp``/``sftp`` (with or without a ``.exe`` suffix) AND a token in that same
|
|
18
|
-
segment holds the NAS
|
|
18
|
+
segment holds the NAS host (resolved from the ``CLAUDE_NAS_*`` environment variables or ``~/.claude/local-identity.json``) as a connection host — bare,
|
|
19
19
|
``user@host``, or a ``host:path`` target. The address inside a remote-command
|
|
20
20
|
argument or a remote path component on another host does not count.
|
|
21
21
|
|
|
@@ -30,6 +30,7 @@ without an ssh-family leading program, and a command shlex cannot tokenize.
|
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
import json
|
|
33
|
+
import re
|
|
33
34
|
import shlex
|
|
34
35
|
import sys
|
|
35
36
|
from pathlib import Path
|
|
@@ -39,19 +40,21 @@ if _hooks_dir not in sys.path:
|
|
|
39
40
|
sys.path.insert(0, _hooks_dir)
|
|
40
41
|
|
|
41
42
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
43
|
+
from hooks_constants.local_identity import ( # noqa: E402
|
|
44
|
+
bare_ssh_binary_deny_message,
|
|
45
|
+
missing_batch_mode_deny_message,
|
|
46
|
+
nas_host,
|
|
47
|
+
)
|
|
42
48
|
from hooks_constants.nas_ssh_binary_enforcer_constants import ( # noqa: E402
|
|
43
49
|
ALL_LAUNCHER_WRAPPER_COMMANDS,
|
|
44
50
|
ALL_OPENSSH_BINARY_PATH_SUFFIXES,
|
|
45
51
|
ALL_SHELL_CONTROL_OPERATOR_TOKENS,
|
|
46
52
|
ALL_SSH_FAMILY_COMMAND_BASENAMES,
|
|
47
|
-
BARE_SSH_BINARY_MESSAGE,
|
|
48
53
|
BASH_TOOL_NAME,
|
|
49
54
|
BATCH_MODE_PATTERN,
|
|
50
55
|
CONTROL_OPERATOR_SPLIT_PATTERN,
|
|
51
56
|
LAUNCHER_DURATION_PATTERN,
|
|
52
57
|
LEADING_ASSIGNMENT_PATTERN,
|
|
53
|
-
MISSING_BATCH_MODE_MESSAGE,
|
|
54
|
-
NAS_HOST_TOKEN_PATTERN,
|
|
55
58
|
)
|
|
56
59
|
|
|
57
60
|
|
|
@@ -67,6 +70,10 @@ def _is_openssh_binary_path(token: str) -> bool:
|
|
|
67
70
|
)
|
|
68
71
|
|
|
69
72
|
|
|
73
|
+
def _nas_host_token_pattern() -> re.Pattern[str]:
|
|
74
|
+
return re.compile(rf"(?:^|@){re.escape(nas_host())}(?::|$)")
|
|
75
|
+
|
|
76
|
+
|
|
70
77
|
def _split_into_segments(all_command_tokens: list[str]) -> list[list[str]]:
|
|
71
78
|
all_exploded_tokens: list[str] = []
|
|
72
79
|
for each_token in all_command_tokens:
|
|
@@ -101,8 +108,10 @@ def _effective_leading_program(all_segment_tokens: list[str]) -> str | None:
|
|
|
101
108
|
return None
|
|
102
109
|
|
|
103
110
|
|
|
104
|
-
def _segment_references_nas(
|
|
105
|
-
|
|
111
|
+
def _segment_references_nas(
|
|
112
|
+
all_segment_tokens: list[str], nas_host_pattern: re.Pattern[str]
|
|
113
|
+
) -> bool:
|
|
114
|
+
return any(nas_host_pattern.search(each_token) for each_token in all_segment_tokens)
|
|
106
115
|
|
|
107
116
|
|
|
108
117
|
def _segment_carries_batch_mode(all_segment_tokens: list[str]) -> bool:
|
|
@@ -114,10 +123,10 @@ def _find_nas_ssh_violation(command: str) -> str | None:
|
|
|
114
123
|
|
|
115
124
|
::
|
|
116
125
|
|
|
117
|
-
ssh -p
|
|
118
|
-
"…/OpenSSH/ssh.exe" -p
|
|
126
|
+
ssh -p 22 operator@nas.example.local "ls" flag: bare-binary message
|
|
127
|
+
"…/OpenSSH/ssh.exe" -p 22 operator@nas… flag: missing-batch-mode message
|
|
119
128
|
"…/OpenSSH/ssh.exe" -o BatchMode=yes …NAS ok: None
|
|
120
|
-
ssh
|
|
129
|
+
ssh operator@example.com "ls" ok: None
|
|
121
130
|
|
|
122
131
|
Tokenizes the command, splits it into simple-command segments, and evaluates
|
|
123
132
|
each segment whose leading program is an ssh-family word AND whose tokens carry
|
|
@@ -135,18 +144,19 @@ def _find_nas_ssh_violation(command: str) -> str | None:
|
|
|
135
144
|
all_command_tokens = shlex.split(command)
|
|
136
145
|
except ValueError:
|
|
137
146
|
return None
|
|
147
|
+
nas_host_pattern = _nas_host_token_pattern()
|
|
138
148
|
for each_segment in _split_into_segments(all_command_tokens):
|
|
139
149
|
leading_program = _effective_leading_program(each_segment)
|
|
140
150
|
if leading_program is None:
|
|
141
151
|
continue
|
|
142
152
|
if _token_basename(leading_program) not in ALL_SSH_FAMILY_COMMAND_BASENAMES:
|
|
143
153
|
continue
|
|
144
|
-
if not _segment_references_nas(each_segment):
|
|
154
|
+
if not _segment_references_nas(each_segment, nas_host_pattern):
|
|
145
155
|
continue
|
|
146
156
|
if not _is_openssh_binary_path(leading_program):
|
|
147
|
-
return
|
|
157
|
+
return bare_ssh_binary_deny_message()
|
|
148
158
|
if not _segment_carries_batch_mode(each_segment):
|
|
149
|
-
return
|
|
159
|
+
return missing_batch_mode_deny_message()
|
|
150
160
|
return None
|
|
151
161
|
|
|
152
162
|
|