claude-dev-env 1.34.1 → 1.35.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/agents/docs-agent.md +1 -1
- package/agents/project-docs-analyzer.md +0 -1
- package/agents/skill-to-agent-converter.md +0 -1
- package/commands/initialize.md +0 -1
- package/commands/readability-review.md +4 -4
- package/commands/review-plan.md +2 -4
- package/commands/stubcheck.md +1 -2
- package/hooks/blocking/code_rules_enforcer.py +250 -36
- package/hooks/blocking/test_code_rules_enforcer.py +91 -39
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +137 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +110 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +0 -13
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
- package/package.json +1 -1
- package/skills/bugteam/PROMPTS.md +0 -39
- package/skills/bugteam/SKILL.md +17 -35
- package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
- package/skills/pr-converge/SKILL.md +19 -3
- package/agents/agent-writer.md +0 -157
- package/agents/config-centralizer.md +0 -686
- package/agents/config-extraction-agent.md +0 -225
- package/agents/doc-orchestrator.md +0 -47
- package/agents/docx-agent.md +0 -211
- package/agents/magic-value-eliminator-agent.md +0 -72
- package/agents/mandatory-agent-workflow-agent.md +0 -88
- package/agents/parallel-workflow-coordinator.md +0 -779
- package/agents/pdf-agent.md +0 -302
- package/agents/project-context-loader.md +0 -238
- package/agents/readability-review-agent.md +0 -76
- package/agents/refactoring-specialist.md +0 -69
- package/agents/right-sized-engineer.md +0 -129
- package/agents/session-continuity-manager.md +0 -53
- package/agents/stub-detector-agent.md +0 -140
- package/agents/tdd-test-writer.md +0 -62
- package/agents/test-data-builder.md +0 -68
- package/agents/tooling-builder.md +0 -78
- package/agents/validation-expert.md +0 -71
- package/agents/xlsx-agent.md +0 -169
package/agents/docs-agent.md
CHANGED
|
@@ -33,7 +33,7 @@ color: cyan
|
|
|
33
33
|
|
|
34
34
|
You handle all documentation tasks: orchestrating doc workflows, analyzing project docs, and writing user-facing guides.
|
|
35
35
|
|
|
36
|
-
**Works with:** clean-coder (identify reusable utilities)
|
|
36
|
+
**Works with:** clean-coder (identify reusable utilities)
|
|
37
37
|
|
|
38
38
|
## Mode 1: Documentation Orchestration
|
|
39
39
|
|
|
@@ -8,7 +8,6 @@ color: cyan
|
|
|
8
8
|
You analyze project documentation to prevent code duplication and provide implementation guidance.
|
|
9
9
|
|
|
10
10
|
**Use before:** implementing new features (check for duplication)
|
|
11
|
-
**Works with:** refactoring-specialist (identify reusable utilities)
|
|
12
11
|
|
|
13
12
|
## Primary Responsibilities
|
|
14
13
|
|
|
@@ -162,7 +162,6 @@ Trigger this agent when:
|
|
|
162
162
|
|
|
163
163
|
Do NOT use this agent when:
|
|
164
164
|
|
|
165
|
-
- User wants to create a brand-new agent from scratch (use agent-writer instead)
|
|
166
165
|
- No skills exist to convert
|
|
167
166
|
- User wants to modify existing agent (direct editing is better)
|
|
168
167
|
- User wants to create a skill (different workflow)
|
package/commands/initialize.md
CHANGED
|
@@ -17,7 +17,6 @@ Read CLAUDE.md now and focus on:
|
|
|
17
17
|
**Agent Decision Tree:**
|
|
18
18
|
- Automation work → check available agents for automation patterns
|
|
19
19
|
- Web App Development → check available agents for framework-specific patterns
|
|
20
|
-
- Configuration/Architecture → config-extraction-agent, parallel-workflow-coordinator
|
|
21
20
|
|
|
22
21
|
**Never skip agent check to "save time" or because request "seems simple"**
|
|
23
22
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "8-dimension readability review: scores and FIXES code to 160/160. Also handles paste-rewrite via arguments."
|
|
3
|
-
allowed-tools:
|
|
3
|
+
allowed-tools: Skill, Read, Edit, Grep, Glob, Bash
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Invoke the `readability-review` skill to review and fix code readability.
|
|
7
7
|
|
|
8
8
|
## Steps
|
|
9
9
|
|
|
10
|
-
1.
|
|
11
|
-
2. The
|
|
10
|
+
1. Invoke the `readability-review` skill via the Skill tool
|
|
11
|
+
2. The skill will: load its rubric → discover target code → read it → score every function → FIX anything below 16/20 → report
|
|
12
12
|
|
|
13
13
|
## User Arguments
|
|
14
14
|
|
package/commands/review-plan.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
Review the current plan against code standards.
|
|
2
2
|
|
|
3
|
-
MANDATORY: Spawn a `readability-review-agent` via Task tool to perform the review. NEVER review inline in the main conversation.
|
|
4
|
-
|
|
5
3
|
1. Identify plan files in `.planning/phases/` or `docs/plans/`
|
|
6
|
-
2.
|
|
7
|
-
3. Report the
|
|
4
|
+
2. Invoke the `readability-review` skill via the Skill tool with the plan file paths
|
|
5
|
+
3. Report the verdict back to the user
|
package/commands/stubcheck.md
CHANGED
|
@@ -49,7 +49,7 @@ Missing details in implementation guides
|
|
|
49
49
|
**Web Automation stubs →**
|
|
50
50
|
- Automation: automation-agent or automation skill
|
|
51
51
|
- Google Sheets: sheets orchestrator agent
|
|
52
|
-
- Config issues:
|
|
52
|
+
- Config issues: clean-coder agent (extracts magic values to `config/` per CODE_RULES.md)
|
|
53
53
|
|
|
54
54
|
**Web framework stubs →**
|
|
55
55
|
- Models/views/forms: domain-specific agent
|
|
@@ -81,7 +81,6 @@ Incomplete matching logic
|
|
|
81
81
|
[HIGH] tests/test_integration.py:234
|
|
82
82
|
Missing integration test
|
|
83
83
|
→ Recommendation: Follow TDD workflow
|
|
84
|
-
→ Agent: tdd-test-writer
|
|
85
84
|
|
|
86
85
|
Would you like me to fix CRITICAL stubs now?
|
|
87
86
|
```
|
|
@@ -28,6 +28,7 @@ import json
|
|
|
28
28
|
import re
|
|
29
29
|
import sys
|
|
30
30
|
import tokenize
|
|
31
|
+
from collections.abc import Iterator
|
|
31
32
|
from pathlib import Path
|
|
32
33
|
from typing import Optional
|
|
33
34
|
|
|
@@ -50,14 +51,12 @@ ADVISORY_LINE_THRESHOLD_SOFT = 400
|
|
|
50
51
|
ADVISORY_LINE_THRESHOLD_HARD = 1000
|
|
51
52
|
|
|
52
53
|
BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_")
|
|
53
|
-
BOOLEAN_NAMING_ISSUE_CAP = 3
|
|
54
54
|
UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
TYPE_CHECKING_BLOCK_PATTERN = re.compile(r"^(?P<indent>\s*)if\s+(typing\.)?TYPE_CHECKING\s*:\s*$")
|
|
58
58
|
IMPORT_STATEMENT_PREFIXES: tuple[str, ...] = ("import ", "from ")
|
|
59
59
|
NOT_INSIDE_TYPE_CHECKING_BLOCK = -1
|
|
60
|
-
MAX_ISSUES_PER_CHECK = 3
|
|
61
60
|
FILE_GLOBAL_UPPER_SNAKE_PATTERN = re.compile(r"^_?[A-Z][A-Z0-9_]*$")
|
|
62
61
|
|
|
63
62
|
COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({
|
|
@@ -355,9 +354,6 @@ def check_imports_at_top(content: str) -> list[str]:
|
|
|
355
354
|
if stripped.startswith(IMPORT_STATEMENT_PREFIXES):
|
|
356
355
|
issues.append(f"Line {line_number}: Import inside function - move to top of file")
|
|
357
356
|
|
|
358
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
359
|
-
break
|
|
360
|
-
|
|
361
357
|
return issues
|
|
362
358
|
|
|
363
359
|
|
|
@@ -758,7 +754,7 @@ def check_constants_outside_config(content: str, file_path: str) -> list[str]:
|
|
|
758
754
|
inside_function = False
|
|
759
755
|
inside_class = False
|
|
760
756
|
|
|
761
|
-
constant_pattern = re.compile(r"^([A-Z][A-Z0-9_]{2,})
|
|
757
|
+
constant_pattern = re.compile(r"^([A-Z][A-Z0-9_]{2,})(?:\s*:\s*[^=]+)?\s*=\s*[^=]")
|
|
762
758
|
|
|
763
759
|
for line_number, line in enumerate(lines, 1):
|
|
764
760
|
stripped = line.strip()
|
|
@@ -787,9 +783,6 @@ def check_constants_outside_config(content: str, file_path: str) -> list[str]:
|
|
|
787
783
|
if constant_name not in ("__all__",):
|
|
788
784
|
issues.append(f"Line {line_number}: Constant {constant_name} - move to config/")
|
|
789
785
|
|
|
790
|
-
if len(issues) >= 3:
|
|
791
|
-
break
|
|
792
|
-
|
|
793
786
|
return issues
|
|
794
787
|
|
|
795
788
|
|
|
@@ -811,12 +804,11 @@ def _scan_function_body_constants(content: str) -> list[str]:
|
|
|
811
804
|
|
|
812
805
|
Only lines inside a function body (tracked via an indent stack) are
|
|
813
806
|
flagged. Module-level assignments and class-body assignments are ignored.
|
|
814
|
-
Returns at most MAX_ISSUES_PER_CHECK entries.
|
|
815
807
|
"""
|
|
816
808
|
advisory_issues: list[str] = []
|
|
817
809
|
lines = content.split("\n")
|
|
818
810
|
function_indent_stack: list[int] = []
|
|
819
|
-
constant_pattern = re.compile(r"^([A-Z][A-Z0-9_]{2,})
|
|
811
|
+
constant_pattern = re.compile(r"^([A-Z][A-Z0-9_]{2,})(?:\s*:\s*[^=]+)?\s*=\s*[^=]")
|
|
820
812
|
|
|
821
813
|
for line_number, line in enumerate(lines, 1):
|
|
822
814
|
stripped = line.strip()
|
|
@@ -846,9 +838,6 @@ def _scan_function_body_constants(content: str) -> list[str]:
|
|
|
846
838
|
f"Line {line_number}: Function-local constant {constant_name} - consider moving to config/"
|
|
847
839
|
)
|
|
848
840
|
|
|
849
|
-
if len(advisory_issues) >= MAX_ISSUES_PER_CHECK:
|
|
850
|
-
break
|
|
851
|
-
|
|
852
841
|
return advisory_issues
|
|
853
842
|
|
|
854
843
|
|
|
@@ -1066,8 +1055,6 @@ def check_boolean_naming(content: str, file_path: str) -> list[str]:
|
|
|
1066
1055
|
issues.append(
|
|
1067
1056
|
f"Line {line_number}: Boolean {name} - prefix with is_/has_/should_/can_"
|
|
1068
1057
|
)
|
|
1069
|
-
if len(issues) >= BOOLEAN_NAMING_ISSUE_CAP:
|
|
1070
|
-
break
|
|
1071
1058
|
return issues
|
|
1072
1059
|
|
|
1073
1060
|
|
|
@@ -1110,8 +1097,6 @@ def check_skip_decorators_in_tests(content: str, file_path: str) -> list[str]:
|
|
|
1110
1097
|
f"Line {each_decorator.lineno}: @skip decorator on test"
|
|
1111
1098
|
f" — tests must fail on missing deps"
|
|
1112
1099
|
)
|
|
1113
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
1114
|
-
return issues
|
|
1115
1100
|
|
|
1116
1101
|
return issues
|
|
1117
1102
|
|
|
@@ -1215,8 +1200,6 @@ def check_existence_check_tests(content: str, file_path: str) -> list[str]:
|
|
|
1215
1200
|
f"Line {each_node.lineno}: existence-check test"
|
|
1216
1201
|
f" — delete or replace with a behavior test"
|
|
1217
1202
|
)
|
|
1218
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
1219
|
-
return issues
|
|
1220
1203
|
|
|
1221
1204
|
return issues
|
|
1222
1205
|
|
|
@@ -1278,8 +1261,6 @@ def check_constant_equality_tests(content: str, file_path: str) -> list[str]:
|
|
|
1278
1261
|
f"Line {each_node.lineno}: constant-value test"
|
|
1279
1262
|
f" — delete; tests must cover behavior"
|
|
1280
1263
|
)
|
|
1281
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
1282
|
-
return issues
|
|
1283
1264
|
|
|
1284
1265
|
return issues
|
|
1285
1266
|
|
|
@@ -1391,8 +1372,6 @@ def check_file_global_constants_use_count(content: str, file_path: str) -> list[
|
|
|
1391
1372
|
issues.append(
|
|
1392
1373
|
f"Line {line_number}: File-global constant {each_constant_name} used by only 1 function/method - move to method scope or add a second caller"
|
|
1393
1374
|
)
|
|
1394
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
1395
|
-
break
|
|
1396
1375
|
|
|
1397
1376
|
return issues
|
|
1398
1377
|
|
|
@@ -1934,21 +1913,40 @@ def check_unused_optional_parameters(content: str, file_path: str) -> list[str]:
|
|
|
1934
1913
|
f"Line {function_node.lineno}: optional parameter {param_name}"
|
|
1935
1914
|
f" is never varied — inline default or drop"
|
|
1936
1915
|
)
|
|
1937
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
1938
|
-
return issues
|
|
1939
1916
|
|
|
1940
1917
|
return issues
|
|
1941
1918
|
|
|
1942
1919
|
|
|
1920
|
+
UNION_TYPING_NAMES: frozenset[str] = frozenset({"Optional", "Union"})
|
|
1921
|
+
|
|
1922
|
+
|
|
1943
1923
|
def _annotation_names_collection(annotation_node: ast.expr | None) -> bool:
|
|
1944
1924
|
if annotation_node is None:
|
|
1945
1925
|
return False
|
|
1946
1926
|
if isinstance(annotation_node, ast.Name):
|
|
1947
1927
|
return annotation_node.id in COLLECTION_TYPE_NAMES
|
|
1948
|
-
if isinstance(annotation_node, ast.Subscript):
|
|
1949
|
-
return _annotation_names_collection(annotation_node.value)
|
|
1950
1928
|
if isinstance(annotation_node, ast.Attribute):
|
|
1951
1929
|
return annotation_node.attr in COLLECTION_TYPE_NAMES
|
|
1930
|
+
if isinstance(annotation_node, ast.BinOp) and isinstance(annotation_node.op, ast.BitOr):
|
|
1931
|
+
return (
|
|
1932
|
+
_annotation_names_collection(annotation_node.left)
|
|
1933
|
+
or _annotation_names_collection(annotation_node.right)
|
|
1934
|
+
)
|
|
1935
|
+
if isinstance(annotation_node, ast.Subscript):
|
|
1936
|
+
outer_value = annotation_node.value
|
|
1937
|
+
is_optional_or_union_subscript = (
|
|
1938
|
+
(isinstance(outer_value, ast.Name) and outer_value.id in UNION_TYPING_NAMES)
|
|
1939
|
+
or (isinstance(outer_value, ast.Attribute) and outer_value.attr in UNION_TYPING_NAMES)
|
|
1940
|
+
)
|
|
1941
|
+
if is_optional_or_union_subscript:
|
|
1942
|
+
slice_node = annotation_node.slice
|
|
1943
|
+
if isinstance(slice_node, ast.Tuple):
|
|
1944
|
+
return any(
|
|
1945
|
+
_annotation_names_collection(each_element)
|
|
1946
|
+
for each_element in slice_node.elts
|
|
1947
|
+
)
|
|
1948
|
+
return _annotation_names_collection(slice_node)
|
|
1949
|
+
return _annotation_names_collection(outer_value)
|
|
1952
1950
|
return False
|
|
1953
1951
|
|
|
1954
1952
|
|
|
@@ -1983,8 +1981,6 @@ def check_collection_prefix(content: str, file_path: str) -> list[str]:
|
|
|
1983
1981
|
issues.append(
|
|
1984
1982
|
f"Line {target_line}: Collection constant {target_name} - prefix with ALL_ (CODE_RULES §5)"
|
|
1985
1983
|
)
|
|
1986
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
1987
|
-
break
|
|
1988
1984
|
for node in ast.walk(tree):
|
|
1989
1985
|
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
1990
1986
|
continue
|
|
@@ -1998,8 +1994,6 @@ def check_collection_prefix(content: str, file_path: str) -> list[str]:
|
|
|
1998
1994
|
issues.append(
|
|
1999
1995
|
f"Line {each_arg.lineno}: Collection parameter {each_arg.arg} - prefix with all_ (CODE_RULES §5)"
|
|
2000
1996
|
)
|
|
2001
|
-
if len(issues) >= MAX_ISSUES_PER_CHECK:
|
|
2002
|
-
return issues
|
|
2003
1997
|
return issues
|
|
2004
1998
|
|
|
2005
1999
|
|
|
@@ -2035,8 +2029,224 @@ def check_library_print(content: str, file_path: str) -> list[str]:
|
|
|
2035
2029
|
issues.append(
|
|
2036
2030
|
f"Line {node.lineno}: sys.{value_node.attr}.write - route through logger"
|
|
2037
2031
|
)
|
|
2038
|
-
|
|
2039
|
-
|
|
2032
|
+
return issues
|
|
2033
|
+
|
|
2034
|
+
|
|
2035
|
+
SELF_AND_CLS_PARAMETER_NAMES: frozenset[str] = frozenset({"self", "cls"})
|
|
2036
|
+
LOOP_INDEX_LETTER_EXEMPTIONS: frozenset[str] = frozenset({"i", "j", "k", "_"})
|
|
2037
|
+
EACH_PREFIX = "each_"
|
|
2038
|
+
BARE_EACH_TOKEN = "each"
|
|
2039
|
+
INLINE_COLLECTION_MIN_LENGTH = 3
|
|
2040
|
+
ALL_CAPS_WITH_UNDERSCORE_PATTERN = re.compile(r"^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$")
|
|
2041
|
+
DOTTED_SEGMENT_PATTERN = re.compile(r"^\.[a-z][a-z0-9_]*$")
|
|
2042
|
+
|
|
2043
|
+
|
|
2044
|
+
def _is_magic_string_literal(string_value: str) -> bool:
|
|
2045
|
+
if not string_value:
|
|
2046
|
+
return False
|
|
2047
|
+
if ALL_CAPS_WITH_UNDERSCORE_PATTERN.match(string_value):
|
|
2048
|
+
return True
|
|
2049
|
+
if DOTTED_SEGMENT_PATTERN.match(string_value):
|
|
2050
|
+
return True
|
|
2051
|
+
return False
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
def _collect_docstring_node_ids(tree: ast.Module) -> set[int]:
|
|
2055
|
+
docstring_ids: set[int] = set()
|
|
2056
|
+
docstring_owner_node_types = (
|
|
2057
|
+
ast.Module,
|
|
2058
|
+
ast.FunctionDef,
|
|
2059
|
+
ast.AsyncFunctionDef,
|
|
2060
|
+
ast.ClassDef,
|
|
2061
|
+
)
|
|
2062
|
+
for node in ast.walk(tree):
|
|
2063
|
+
if not isinstance(node, docstring_owner_node_types):
|
|
2064
|
+
continue
|
|
2065
|
+
if not node.body:
|
|
2066
|
+
continue
|
|
2067
|
+
first_statement = node.body[0]
|
|
2068
|
+
if not isinstance(first_statement, ast.Expr):
|
|
2069
|
+
continue
|
|
2070
|
+
first_value = first_statement.value
|
|
2071
|
+
if isinstance(first_value, ast.Constant) and isinstance(first_value.value, str):
|
|
2072
|
+
docstring_ids.add(id(first_value))
|
|
2073
|
+
return docstring_ids
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def _collect_fstring_part_node_ids(tree: ast.Module) -> set[int]:
|
|
2077
|
+
fstring_part_ids: set[int] = set()
|
|
2078
|
+
for node in ast.walk(tree):
|
|
2079
|
+
if not isinstance(node, ast.JoinedStr):
|
|
2080
|
+
continue
|
|
2081
|
+
for each_value in node.values:
|
|
2082
|
+
if isinstance(each_value, ast.Constant) and isinstance(each_value.value, str):
|
|
2083
|
+
fstring_part_ids.add(id(each_value))
|
|
2084
|
+
return fstring_part_ids
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
def _walk_skipping_nested_function_defs(start_node: ast.AST) -> Iterator[ast.AST]:
|
|
2088
|
+
if isinstance(start_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
2089
|
+
return
|
|
2090
|
+
nodes_to_visit: list[ast.AST] = [start_node]
|
|
2091
|
+
while nodes_to_visit:
|
|
2092
|
+
current_node = nodes_to_visit.pop()
|
|
2093
|
+
yield current_node
|
|
2094
|
+
all_child_nodes = list(ast.iter_child_nodes(current_node))
|
|
2095
|
+
for each_child_node in reversed(all_child_nodes):
|
|
2096
|
+
if isinstance(each_child_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
2097
|
+
continue
|
|
2098
|
+
nodes_to_visit.append(each_child_node)
|
|
2099
|
+
|
|
2100
|
+
|
|
2101
|
+
def check_string_literal_magic(content: str, file_path: str) -> list[str]:
|
|
2102
|
+
if is_test_file(file_path):
|
|
2103
|
+
return []
|
|
2104
|
+
if is_config_file(file_path):
|
|
2105
|
+
return []
|
|
2106
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
2107
|
+
return []
|
|
2108
|
+
try:
|
|
2109
|
+
tree = ast.parse(content)
|
|
2110
|
+
except SyntaxError:
|
|
2111
|
+
return []
|
|
2112
|
+
docstring_node_ids = _collect_docstring_node_ids(tree)
|
|
2113
|
+
fstring_part_node_ids = _collect_fstring_part_node_ids(tree)
|
|
2114
|
+
issues: list[str] = []
|
|
2115
|
+
flagged_node_ids: set[int] = set()
|
|
2116
|
+
for function_node in ast.walk(tree):
|
|
2117
|
+
if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
2118
|
+
continue
|
|
2119
|
+
for each_body_statement in function_node.body:
|
|
2120
|
+
for each_descendant in _walk_skipping_nested_function_defs(each_body_statement):
|
|
2121
|
+
if not isinstance(each_descendant, ast.Constant):
|
|
2122
|
+
continue
|
|
2123
|
+
if not isinstance(each_descendant.value, str):
|
|
2124
|
+
continue
|
|
2125
|
+
if id(each_descendant) in flagged_node_ids:
|
|
2126
|
+
continue
|
|
2127
|
+
if id(each_descendant) in docstring_node_ids:
|
|
2128
|
+
continue
|
|
2129
|
+
if id(each_descendant) in fstring_part_node_ids:
|
|
2130
|
+
continue
|
|
2131
|
+
if not _is_magic_string_literal(each_descendant.value):
|
|
2132
|
+
continue
|
|
2133
|
+
flagged_node_ids.add(id(each_descendant))
|
|
2134
|
+
issues.append(
|
|
2135
|
+
f"Line {each_descendant.lineno}: string magic value {each_descendant.value!r}"
|
|
2136
|
+
f" - extract to config/"
|
|
2137
|
+
)
|
|
2138
|
+
return issues
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def check_inline_literal_collections(content: str, file_path: str) -> list[str]:
|
|
2142
|
+
if is_test_file(file_path):
|
|
2143
|
+
return []
|
|
2144
|
+
if is_config_file(file_path):
|
|
2145
|
+
return []
|
|
2146
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
2147
|
+
return []
|
|
2148
|
+
try:
|
|
2149
|
+
tree = ast.parse(content)
|
|
2150
|
+
except SyntaxError:
|
|
2151
|
+
return []
|
|
2152
|
+
issues: list[str] = []
|
|
2153
|
+
flagged_node_ids: set[int] = set()
|
|
2154
|
+
for function_node in ast.walk(tree):
|
|
2155
|
+
if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
2156
|
+
continue
|
|
2157
|
+
for each_body_statement in function_node.body:
|
|
2158
|
+
for each_descendant in _walk_skipping_nested_function_defs(each_body_statement):
|
|
2159
|
+
if not isinstance(each_descendant, (ast.Set, ast.List)):
|
|
2160
|
+
continue
|
|
2161
|
+
if id(each_descendant) in flagged_node_ids:
|
|
2162
|
+
continue
|
|
2163
|
+
all_elements = each_descendant.elts
|
|
2164
|
+
if len(all_elements) < INLINE_COLLECTION_MIN_LENGTH:
|
|
2165
|
+
continue
|
|
2166
|
+
if not all(isinstance(each_element, ast.Constant) for each_element in all_elements):
|
|
2167
|
+
continue
|
|
2168
|
+
flagged_node_ids.add(id(each_descendant))
|
|
2169
|
+
collection_kind = "set" if isinstance(each_descendant, ast.Set) else "list"
|
|
2170
|
+
issues.append(
|
|
2171
|
+
f"Line {each_descendant.lineno}: inline {collection_kind} literal of {len(all_elements)}"
|
|
2172
|
+
f" constants in function body - extract to config/"
|
|
2173
|
+
)
|
|
2174
|
+
return issues
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
def check_loop_variable_naming(content: str, file_path: str) -> list[str]:
|
|
2178
|
+
if is_test_file(file_path):
|
|
2179
|
+
return []
|
|
2180
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
2181
|
+
return []
|
|
2182
|
+
try:
|
|
2183
|
+
tree = ast.parse(content)
|
|
2184
|
+
except SyntaxError:
|
|
2185
|
+
return []
|
|
2186
|
+
issues: list[str] = []
|
|
2187
|
+
for node in ast.walk(tree):
|
|
2188
|
+
if not isinstance(node, (ast.For, ast.AsyncFor)):
|
|
2189
|
+
continue
|
|
2190
|
+
target = node.target
|
|
2191
|
+
if not isinstance(target, ast.Name):
|
|
2192
|
+
continue
|
|
2193
|
+
target_name = target.id
|
|
2194
|
+
if target_name in LOOP_INDEX_LETTER_EXEMPTIONS:
|
|
2195
|
+
continue
|
|
2196
|
+
if target_name == BARE_EACH_TOKEN:
|
|
2197
|
+
issues.append(
|
|
2198
|
+
f"Line {target.lineno}: loop variable 'each' is a bare token without subject"
|
|
2199
|
+
f" - rename to each_<subject> (CODE_RULES §5)"
|
|
2200
|
+
)
|
|
2201
|
+
continue
|
|
2202
|
+
if target_name.startswith(EACH_PREFIX) and len(target_name) > len(EACH_PREFIX):
|
|
2203
|
+
continue
|
|
2204
|
+
issues.append(
|
|
2205
|
+
f"Line {target.lineno}: loop variable {target_name!r} - prefix with each_ (CODE_RULES §5)"
|
|
2206
|
+
)
|
|
2207
|
+
return issues
|
|
2208
|
+
|
|
2209
|
+
|
|
2210
|
+
def check_parameter_annotations(content: str, file_path: str) -> list[str]:
|
|
2211
|
+
if is_test_file(file_path):
|
|
2212
|
+
return []
|
|
2213
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
2214
|
+
return []
|
|
2215
|
+
try:
|
|
2216
|
+
tree = ast.parse(content)
|
|
2217
|
+
except SyntaxError:
|
|
2218
|
+
return []
|
|
2219
|
+
issues: list[str] = []
|
|
2220
|
+
for node in ast.walk(tree):
|
|
2221
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
2222
|
+
continue
|
|
2223
|
+
for each_arg in _collect_annotated_arguments(node):
|
|
2224
|
+
if each_arg.arg in SELF_AND_CLS_PARAMETER_NAMES:
|
|
2225
|
+
continue
|
|
2226
|
+
if each_arg.annotation is None:
|
|
2227
|
+
issues.append(
|
|
2228
|
+
f"Line {each_arg.lineno}: parameter {each_arg.arg!r} on {node.name!r} missing type annotation (CODE_RULES §6)"
|
|
2229
|
+
)
|
|
2230
|
+
return issues
|
|
2231
|
+
|
|
2232
|
+
|
|
2233
|
+
def check_return_annotations(content: str, file_path: str) -> list[str]:
|
|
2234
|
+
if is_test_file(file_path):
|
|
2235
|
+
return []
|
|
2236
|
+
if is_workflow_registry_file(file_path) or is_migration_file(file_path):
|
|
2237
|
+
return []
|
|
2238
|
+
try:
|
|
2239
|
+
tree = ast.parse(content)
|
|
2240
|
+
except SyntaxError:
|
|
2241
|
+
return []
|
|
2242
|
+
issues: list[str] = []
|
|
2243
|
+
for node in ast.walk(tree):
|
|
2244
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
2245
|
+
continue
|
|
2246
|
+
if node.returns is None:
|
|
2247
|
+
issues.append(
|
|
2248
|
+
f"Line {node.lineno}: function {node.name!r} missing return type annotation (CODE_RULES §6)"
|
|
2249
|
+
)
|
|
2040
2250
|
return issues
|
|
2041
2251
|
|
|
2042
2252
|
|
|
@@ -2061,8 +2271,7 @@ def validate_content(content: str, file_path: str, old_content: str = "") -> lis
|
|
|
2061
2271
|
all_issues.extend(check_magic_values(content, file_path))
|
|
2062
2272
|
all_issues.extend(check_fstring_structural_literals(content, file_path))
|
|
2063
2273
|
all_issues.extend(check_constants_outside_config(content, file_path))
|
|
2064
|
-
|
|
2065
|
-
print(f"[CODE_RULES advisory] {file_path}: {each_advisory}", file=sys.stderr)
|
|
2274
|
+
all_issues.extend(check_constants_outside_config_advisory(content, file_path))
|
|
2066
2275
|
all_issues.extend(check_file_global_constants_use_count(content, file_path))
|
|
2067
2276
|
all_issues.extend(check_type_escape_hatches(content, file_path))
|
|
2068
2277
|
all_issues.extend(check_banned_identifiers(content, file_path))
|
|
@@ -2073,6 +2282,11 @@ def validate_content(content: str, file_path: str, old_content: str = "") -> lis
|
|
|
2073
2282
|
all_issues.extend(check_unused_optional_parameters(content, file_path))
|
|
2074
2283
|
all_issues.extend(check_collection_prefix(content, file_path))
|
|
2075
2284
|
all_issues.extend(check_library_print(content, file_path))
|
|
2285
|
+
all_issues.extend(check_parameter_annotations(content, file_path))
|
|
2286
|
+
all_issues.extend(check_return_annotations(content, file_path))
|
|
2287
|
+
all_issues.extend(check_loop_variable_naming(content, file_path))
|
|
2288
|
+
all_issues.extend(check_inline_literal_collections(content, file_path))
|
|
2289
|
+
all_issues.extend(check_string_literal_magic(content, file_path))
|
|
2076
2290
|
check_incomplete_mocks(content, file_path)
|
|
2077
2291
|
check_duplicated_format_patterns(content, file_path)
|
|
2078
2292
|
|
|
@@ -188,34 +188,6 @@ def test_should_flag_when_every_call_passes_the_exact_default() -> None:
|
|
|
188
188
|
)
|
|
189
189
|
|
|
190
190
|
|
|
191
|
-
def test_check_unused_optional_parameters_stops_at_max_issues_per_check() -> None:
|
|
192
|
-
source = (
|
|
193
|
-
"def make_url_one(path: str, prefix: str = '/api') -> str:\n"
|
|
194
|
-
" return f'{prefix}{path}'\n"
|
|
195
|
-
"def make_url_two(path: str, prefix: str = '/api') -> str:\n"
|
|
196
|
-
" return f'{prefix}{path}'\n"
|
|
197
|
-
"def make_url_three(path: str, prefix: str = '/api') -> str:\n"
|
|
198
|
-
" return f'{prefix}{path}'\n"
|
|
199
|
-
"def make_url_four(path: str, prefix: str = '/api') -> str:\n"
|
|
200
|
-
" return f'{prefix}{path}'\n"
|
|
201
|
-
"def make_url_five(path: str, prefix: str = '/api') -> str:\n"
|
|
202
|
-
" return f'{prefix}{path}'\n"
|
|
203
|
-
"\n"
|
|
204
|
-
"def call_all() -> None:\n"
|
|
205
|
-
" make_url_one('/a')\n"
|
|
206
|
-
" make_url_two('/b')\n"
|
|
207
|
-
" make_url_three('/c')\n"
|
|
208
|
-
" make_url_four('/d')\n"
|
|
209
|
-
" make_url_five('/e')\n"
|
|
210
|
-
)
|
|
211
|
-
issues = code_rules_enforcer.check_unused_optional_parameters(
|
|
212
|
-
source, UNUSED_OPTIONAL_PRODUCTION_FILE_PATH
|
|
213
|
-
)
|
|
214
|
-
assert len(issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK, (
|
|
215
|
-
f"Expected exactly MAX_ISSUES_PER_CHECK issues, got {len(issues)}: {issues}"
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
|
|
219
191
|
INCOMPLETE_MOCK_TEST_FILE_PATH = "packages/app/tests/test_orders.py"
|
|
220
192
|
INCOMPLETE_MOCK_PRODUCTION_FILE_PATH = "packages/app/services/orders.py"
|
|
221
193
|
|
|
@@ -675,22 +647,20 @@ def test_advisory_should_still_flag_actual_method_body_constant() -> None:
|
|
|
675
647
|
assert "MAXIMUM_RETRIES" in advisory_issues[0]
|
|
676
648
|
|
|
677
649
|
|
|
678
|
-
def
|
|
679
|
-
|
|
680
|
-
"def
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
" GAMMA_CONSTANT = 3\n"
|
|
684
|
-
" DELTA_CONSTANT = 4\n"
|
|
685
|
-
" EPSILON_CONSTANT = 5\n"
|
|
650
|
+
def test_advisory_should_flag_annotated_function_body_constant() -> None:
|
|
651
|
+
source_with_annotated_function_body_constant = (
|
|
652
|
+
"def example_function() -> None:\n"
|
|
653
|
+
" MAXIMUM_RETRIES: int = 3\n"
|
|
654
|
+
" return None\n"
|
|
686
655
|
)
|
|
687
656
|
advisory_issues = code_rules_enforcer.check_constants_outside_config_advisory(
|
|
688
|
-
|
|
657
|
+
source_with_annotated_function_body_constant,
|
|
689
658
|
"example_module.py",
|
|
690
659
|
)
|
|
691
|
-
assert len(advisory_issues) ==
|
|
692
|
-
"
|
|
660
|
+
assert len(advisory_issues) == 1, (
|
|
661
|
+
"Annotated function-body UPPER_SNAKE constant (PEP 526) must surface as advisory"
|
|
693
662
|
)
|
|
663
|
+
assert "MAXIMUM_RETRIES" in advisory_issues[0]
|
|
694
664
|
|
|
695
665
|
|
|
696
666
|
def test_advisory_should_flag_outer_constants_after_nested_def() -> None:
|
|
@@ -958,3 +928,85 @@ def test_should_still_advise_when_duplicated_fstring_literal_is_long(capsys: obj
|
|
|
958
928
|
"Expected the existing /api/<x> path-shape advisory to still fire, "
|
|
959
929
|
f"got: {captured.err!r}"
|
|
960
930
|
)
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
LOOP_NAMING_PRODUCTION_FILE_PATH = "packages/app/services/loop_naming.py"
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def test_check_loop_variable_naming_flags_missing_each_prefix() -> None:
|
|
937
|
+
source = (
|
|
938
|
+
"def consume() -> None:\n"
|
|
939
|
+
" for marker in []:\n"
|
|
940
|
+
" return None\n"
|
|
941
|
+
)
|
|
942
|
+
issues = code_rules_enforcer.check_loop_variable_naming(
|
|
943
|
+
source, LOOP_NAMING_PRODUCTION_FILE_PATH
|
|
944
|
+
)
|
|
945
|
+
assert any("marker" in each_issue for each_issue in issues), (
|
|
946
|
+
f"Expected 'marker' loop variable flagged, got: {issues}"
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
INLINE_LITERAL_PRODUCTION_FILE_PATH = "packages/app/services/inline_literal.py"
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def test_check_inline_literal_collections_flags_three_string_set_in_function() -> None:
|
|
954
|
+
source = (
|
|
955
|
+
"def is_known(value: str) -> bool:\n"
|
|
956
|
+
" return value in {'true', 'false', 'none'}\n"
|
|
957
|
+
)
|
|
958
|
+
issues = code_rules_enforcer.check_inline_literal_collections(
|
|
959
|
+
source, INLINE_LITERAL_PRODUCTION_FILE_PATH
|
|
960
|
+
)
|
|
961
|
+
assert len(issues) == 1, f"Expected 3-element string set flagged, got: {issues}"
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
STRING_MAGIC_PRODUCTION_FILE_PATH = "packages/app/services/string_magic.py"
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def test_check_string_literal_magic_flags_env_var_name() -> None:
|
|
968
|
+
source = (
|
|
969
|
+
"import os\n"
|
|
970
|
+
"\n"
|
|
971
|
+
"def fetch_secret() -> str:\n"
|
|
972
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
973
|
+
)
|
|
974
|
+
issues = code_rules_enforcer.check_string_literal_magic(
|
|
975
|
+
source, STRING_MAGIC_PRODUCTION_FILE_PATH
|
|
976
|
+
)
|
|
977
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in issues), (
|
|
978
|
+
f"Expected env-var name flagged, got: {issues}"
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH = "packages/app/services/encoding.py"
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def test_check_constants_outside_config_flags_annotated_assignment() -> None:
|
|
986
|
+
source = "TEXT_FILE_ENCODING: str = 'utf-8'\n"
|
|
987
|
+
issues = code_rules_enforcer.check_constants_outside_config(
|
|
988
|
+
source, CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH
|
|
989
|
+
)
|
|
990
|
+
assert any("TEXT_FILE_ENCODING" in each_issue for each_issue in issues), (
|
|
991
|
+
f"Expected annotated UPPER_SNAKE assignment flagged, got: {issues}"
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def test_check_constants_outside_config_reports_more_than_three_constants() -> None:
|
|
996
|
+
source = (
|
|
997
|
+
"ALPHA_VALUE = 1\n"
|
|
998
|
+
"BETA_VALUE = 2\n"
|
|
999
|
+
"GAMMA_VALUE = 3\n"
|
|
1000
|
+
"DELTA_VALUE = 4\n"
|
|
1001
|
+
"EPSILON_VALUE = 5\n"
|
|
1002
|
+
"\n"
|
|
1003
|
+
"def consumer() -> int:\n"
|
|
1004
|
+
" return ALPHA_VALUE + BETA_VALUE\n"
|
|
1005
|
+
)
|
|
1006
|
+
issues = code_rules_enforcer.check_constants_outside_config(
|
|
1007
|
+
source, CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH
|
|
1008
|
+
)
|
|
1009
|
+
expected_constant_count = 5
|
|
1010
|
+
assert len(issues) == expected_constant_count, (
|
|
1011
|
+
f"Expected all {expected_constant_count} constants reported, got {len(issues)}: {issues}"
|
|
1012
|
+
)
|