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.
Files changed (44) hide show
  1. package/agents/docs-agent.md +1 -1
  2. package/agents/project-docs-analyzer.md +0 -1
  3. package/agents/skill-to-agent-converter.md +0 -1
  4. package/commands/initialize.md +0 -1
  5. package/commands/readability-review.md +4 -4
  6. package/commands/review-plan.md +2 -4
  7. package/commands/stubcheck.md +1 -2
  8. package/hooks/blocking/code_rules_enforcer.py +250 -36
  9. package/hooks/blocking/test_code_rules_enforcer.py +91 -39
  10. package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
  11. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +137 -0
  12. package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
  13. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +0 -18
  14. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
  15. package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
  16. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +110 -0
  17. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +0 -13
  18. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
  19. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
  20. package/package.json +1 -1
  21. package/skills/bugteam/PROMPTS.md +0 -39
  22. package/skills/bugteam/SKILL.md +17 -35
  23. package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
  24. package/skills/pr-converge/SKILL.md +19 -3
  25. package/agents/agent-writer.md +0 -157
  26. package/agents/config-centralizer.md +0 -686
  27. package/agents/config-extraction-agent.md +0 -225
  28. package/agents/doc-orchestrator.md +0 -47
  29. package/agents/docx-agent.md +0 -211
  30. package/agents/magic-value-eliminator-agent.md +0 -72
  31. package/agents/mandatory-agent-workflow-agent.md +0 -88
  32. package/agents/parallel-workflow-coordinator.md +0 -779
  33. package/agents/pdf-agent.md +0 -302
  34. package/agents/project-context-loader.md +0 -238
  35. package/agents/readability-review-agent.md +0 -76
  36. package/agents/refactoring-specialist.md +0 -69
  37. package/agents/right-sized-engineer.md +0 -129
  38. package/agents/session-continuity-manager.md +0 -53
  39. package/agents/stub-detector-agent.md +0 -140
  40. package/agents/tdd-test-writer.md +0 -62
  41. package/agents/test-data-builder.md +0 -68
  42. package/agents/tooling-builder.md +0 -78
  43. package/agents/validation-expert.md +0 -71
  44. package/agents/xlsx-agent.md +0 -169
@@ -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), validation-expert (path changes trigger doc updates)
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)
@@ -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: Task, Read, Edit, Grep, Glob
3
+ allowed-tools: Skill, Read, Edit, Grep, Glob, Bash
4
4
  ---
5
5
 
6
- Launch the readability-review agent to review and fix code readability.
6
+ Invoke the `readability-review` skill to review and fix code readability.
7
7
 
8
8
  ## Steps
9
9
 
10
- 1. Launch a `readability-review-agent` via the Task tool
11
- 2. The agent will: load its rubric → discover target code → read it → score every function → FIX anything below 16/20 → report
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
 
@@ -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. Spawn `readability-review-agent` with full review instructions from the `review-plan` skill
7
- 3. Report the agent's verdict back to the user
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
@@ -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: config-extraction-agent or config-extraction skill
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,})\s*=\s*[^=]")
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,})\s*=\s*[^=]")
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
- if len(issues) >= MAX_ISSUES_PER_CHECK:
2039
- break
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
- for each_advisory in check_constants_outside_config_advisory(content, file_path):
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 test_advisory_cap_matches_max_issues_per_check_constant() -> None:
679
- many_constants_source = (
680
- "def crowded_function():\n"
681
- " ALPHA_CONSTANT = 1\n"
682
- " BETA_CONSTANT = 2\n"
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
- many_constants_source,
657
+ source_with_annotated_function_body_constant,
689
658
  "example_module.py",
690
659
  )
691
- assert len(advisory_issues) == code_rules_enforcer.MAX_ISSUES_PER_CHECK, (
692
- "Advisory cap must equal MAX_ISSUES_PER_CHECK, not a hardcoded literal"
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
+ )