atdd 0.2.1__py3-none-any.whl

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 (184) hide show
  1. atdd/__init__.py +6 -0
  2. atdd/__main__.py +4 -0
  3. atdd/cli.py +404 -0
  4. atdd/coach/__init__.py +0 -0
  5. atdd/coach/commands/__init__.py +0 -0
  6. atdd/coach/commands/add_persistence_metadata.py +215 -0
  7. atdd/coach/commands/analyze_migrations.py +188 -0
  8. atdd/coach/commands/consumers.py +720 -0
  9. atdd/coach/commands/infer_governance_status.py +149 -0
  10. atdd/coach/commands/initializer.py +177 -0
  11. atdd/coach/commands/interface.py +1078 -0
  12. atdd/coach/commands/inventory.py +565 -0
  13. atdd/coach/commands/migration.py +240 -0
  14. atdd/coach/commands/registry.py +1560 -0
  15. atdd/coach/commands/session.py +430 -0
  16. atdd/coach/commands/sync.py +405 -0
  17. atdd/coach/commands/test_interface.py +399 -0
  18. atdd/coach/commands/test_runner.py +141 -0
  19. atdd/coach/commands/tests/__init__.py +1 -0
  20. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  21. atdd/coach/commands/traceability.py +4264 -0
  22. atdd/coach/conventions/session.convention.yaml +754 -0
  23. atdd/coach/overlays/__init__.py +2 -0
  24. atdd/coach/overlays/claude.md +2 -0
  25. atdd/coach/schemas/config.schema.json +34 -0
  26. atdd/coach/schemas/manifest.schema.json +101 -0
  27. atdd/coach/templates/ATDD.md +282 -0
  28. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  29. atdd/coach/utils/__init__.py +0 -0
  30. atdd/coach/utils/graph/__init__.py +0 -0
  31. atdd/coach/utils/graph/urn.py +875 -0
  32. atdd/coach/validators/__init__.py +0 -0
  33. atdd/coach/validators/shared_fixtures.py +365 -0
  34. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  35. atdd/coach/validators/test_registry.py +575 -0
  36. atdd/coach/validators/test_session_validation.py +1183 -0
  37. atdd/coach/validators/test_traceability.py +448 -0
  38. atdd/coach/validators/test_update_feature_paths.py +108 -0
  39. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  40. atdd/coder/__init__.py +1 -0
  41. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  42. atdd/coder/conventions/backend.convention.yaml +460 -0
  43. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  44. atdd/coder/conventions/commons.convention.yaml +460 -0
  45. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  46. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  47. atdd/coder/conventions/design.convention.yaml +327 -0
  48. atdd/coder/conventions/design.recipe.yaml +273 -0
  49. atdd/coder/conventions/dto.convention.yaml +660 -0
  50. atdd/coder/conventions/frontend.convention.yaml +542 -0
  51. atdd/coder/conventions/green.convention.yaml +1012 -0
  52. atdd/coder/conventions/presentation.convention.yaml +587 -0
  53. atdd/coder/conventions/refactor.convention.yaml +535 -0
  54. atdd/coder/conventions/technology.convention.yaml +206 -0
  55. atdd/coder/conventions/tests/__init__.py +0 -0
  56. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  57. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  58. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  59. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  60. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  61. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  62. atdd/coder/conventions/train.convention.yaml +325 -0
  63. atdd/coder/conventions/verification.protocol.yaml +53 -0
  64. atdd/coder/schemas/design_system.schema.json +361 -0
  65. atdd/coder/validators/__init__.py +0 -0
  66. atdd/coder/validators/test_commons_structure.py +485 -0
  67. atdd/coder/validators/test_complexity.py +416 -0
  68. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  69. atdd/coder/validators/test_design_system_compliance.py +413 -0
  70. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  71. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  72. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  73. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  74. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  75. atdd/coder/validators/test_import_boundaries.py +396 -0
  76. atdd/coder/validators/test_init_file_urns.py +593 -0
  77. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  78. atdd/coder/validators/test_presentation_convention.py +260 -0
  79. atdd/coder/validators/test_python_architecture.py +674 -0
  80. atdd/coder/validators/test_quality_metrics.py +420 -0
  81. atdd/coder/validators/test_station_master_pattern.py +244 -0
  82. atdd/coder/validators/test_train_infrastructure.py +454 -0
  83. atdd/coder/validators/test_train_urns.py +293 -0
  84. atdd/coder/validators/test_typescript_architecture.py +616 -0
  85. atdd/coder/validators/test_usecase_structure.py +421 -0
  86. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  87. atdd/conftest.py +126 -0
  88. atdd/planner/__init__.py +1 -0
  89. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  90. atdd/planner/conventions/appendix.convention.yaml +187 -0
  91. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  92. atdd/planner/conventions/component.convention.yaml +670 -0
  93. atdd/planner/conventions/criteria.convention.yaml +141 -0
  94. atdd/planner/conventions/feature.convention.yaml +371 -0
  95. atdd/planner/conventions/interface.convention.yaml +382 -0
  96. atdd/planner/conventions/steps.convention.yaml +141 -0
  97. atdd/planner/conventions/train.convention.yaml +552 -0
  98. atdd/planner/conventions/wagon.convention.yaml +275 -0
  99. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  100. atdd/planner/schemas/acceptance.schema.json +336 -0
  101. atdd/planner/schemas/appendix.schema.json +78 -0
  102. atdd/planner/schemas/component.schema.json +114 -0
  103. atdd/planner/schemas/feature.schema.json +197 -0
  104. atdd/planner/schemas/train.schema.json +192 -0
  105. atdd/planner/schemas/wagon.schema.json +281 -0
  106. atdd/planner/schemas/wmbt.schema.json +59 -0
  107. atdd/planner/validators/__init__.py +0 -0
  108. atdd/planner/validators/conftest.py +5 -0
  109. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  110. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  111. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  112. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  113. atdd/planner/validators/test_plan_wagons.py +174 -0
  114. atdd/planner/validators/test_train_validation.py +514 -0
  115. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  116. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  117. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  118. atdd/tester/__init__.py +1 -0
  119. atdd/tester/conventions/artifact.convention.yaml +257 -0
  120. atdd/tester/conventions/contract.convention.yaml +1009 -0
  121. atdd/tester/conventions/filename.convention.yaml +555 -0
  122. atdd/tester/conventions/migration.convention.yaml +509 -0
  123. atdd/tester/conventions/red.convention.yaml +797 -0
  124. atdd/tester/conventions/routing.convention.yaml +51 -0
  125. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  126. atdd/tester/schemas/a11y.tmpl.json +17 -0
  127. atdd/tester/schemas/artifact.schema.json +189 -0
  128. atdd/tester/schemas/contract.schema.json +591 -0
  129. atdd/tester/schemas/contract.tmpl.json +95 -0
  130. atdd/tester/schemas/db.tmpl.json +20 -0
  131. atdd/tester/schemas/e2e.tmpl.json +17 -0
  132. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  133. atdd/tester/schemas/event.tmpl.json +17 -0
  134. atdd/tester/schemas/http.tmpl.json +19 -0
  135. atdd/tester/schemas/job.tmpl.json +18 -0
  136. atdd/tester/schemas/load.tmpl.json +21 -0
  137. atdd/tester/schemas/metric.tmpl.json +19 -0
  138. atdd/tester/schemas/pack.schema.json +139 -0
  139. atdd/tester/schemas/realtime.tmpl.json +20 -0
  140. atdd/tester/schemas/rls.tmpl.json +18 -0
  141. atdd/tester/schemas/script.tmpl.json +16 -0
  142. atdd/tester/schemas/sec.tmpl.json +18 -0
  143. atdd/tester/schemas/storage.tmpl.json +18 -0
  144. atdd/tester/schemas/telemetry.schema.json +128 -0
  145. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  146. atdd/tester/schemas/test_filename.schema.json +194 -0
  147. atdd/tester/schemas/test_intent.schema.json +179 -0
  148. atdd/tester/schemas/unit.tmpl.json +18 -0
  149. atdd/tester/schemas/visual.tmpl.json +18 -0
  150. atdd/tester/schemas/ws.tmpl.json +17 -0
  151. atdd/tester/utils/__init__.py +0 -0
  152. atdd/tester/utils/filename.py +300 -0
  153. atdd/tester/validators/__init__.py +0 -0
  154. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  155. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  156. atdd/tester/validators/conftest.py +5 -0
  157. atdd/tester/validators/coverage_gap_report.py +321 -0
  158. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  159. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  160. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  161. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  162. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  163. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  164. atdd/tester/validators/test_contracts_structure.py +200 -0
  165. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  166. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  167. atdd/tester/validators/test_fixture_validity.py +372 -0
  168. atdd/tester/validators/test_isolation.py +487 -0
  169. atdd/tester/validators/test_migration_coverage.py +204 -0
  170. atdd/tester/validators/test_migration_criteria.py +276 -0
  171. atdd/tester/validators/test_migration_generation.py +116 -0
  172. atdd/tester/validators/test_python_test_naming.py +410 -0
  173. atdd/tester/validators/test_red_layer_validation.py +95 -0
  174. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  175. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  176. atdd/tester/validators/test_telemetry_structure.py +634 -0
  177. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  178. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  179. atdd-0.2.1.dist-info/METADATA +221 -0
  180. atdd-0.2.1.dist-info/RECORD +184 -0
  181. atdd-0.2.1.dist-info/WHEEL +5 -0
  182. atdd-0.2.1.dist-info/entry_points.txt +2 -0
  183. atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
  184. atdd-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,416 @@
1
+ """
2
+ Test Python code complexity stays within acceptable thresholds.
3
+
4
+ Validates:
5
+ - Cyclomatic complexity < 10 per function
6
+ - Nesting depth < 4 levels
7
+ - Function length < 50 lines
8
+ - No overly complex functions
9
+
10
+ Inspired by: .claude/utils/coder/complexity.py
11
+ But: Self-contained, no utility dependencies
12
+ """
13
+
14
+ import pytest
15
+ import re
16
+ from pathlib import Path
17
+ from typing import List, Tuple
18
+
19
+
20
+ # Path constants
21
+ REPO_ROOT = Path(__file__).resolve().parents[3]
22
+ PYTHON_DIR = REPO_ROOT / "python"
23
+
24
+
25
+ # Complexity thresholds
26
+ MAX_CYCLOMATIC_COMPLEXITY = 10
27
+ MAX_NESTING_DEPTH = 4
28
+ MAX_FUNCTION_LINES = 50
29
+ MAX_FUNCTION_PARAMS = 6
30
+
31
+
32
+ def find_python_files() -> List[Path]:
33
+ """Find all Python source files (excluding tests)."""
34
+ if not PYTHON_DIR.exists():
35
+ return []
36
+
37
+ files = []
38
+ for py_file in PYTHON_DIR.rglob("*.py"):
39
+ if '/test/' in str(py_file) or py_file.name.startswith('test_'):
40
+ continue
41
+ if '__pycache__' in str(py_file):
42
+ continue
43
+ if py_file.name == '__init__.py':
44
+ continue
45
+ files.append(py_file)
46
+
47
+ return files
48
+
49
+
50
+ def extract_functions(file_path: Path) -> List[Tuple[str, int, str]]:
51
+ """
52
+ Extract functions from Python file.
53
+
54
+ Returns:
55
+ List of (function_name, line_number, function_body) tuples
56
+ """
57
+ try:
58
+ with open(file_path, 'r', encoding='utf-8') as f:
59
+ content = f.read()
60
+ except Exception:
61
+ return []
62
+
63
+ functions = []
64
+ lines = content.split('\n')
65
+
66
+ i = 0
67
+ while i < len(lines):
68
+ line = lines[i]
69
+
70
+ # Match function definition: def function_name(...)
71
+ func_match = re.match(r'^\s*(async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(', line)
72
+
73
+ if func_match:
74
+ func_name = func_match.group(2)
75
+ start_line = i + 1 # Line numbers are 1-based
76
+ indent = len(line) - len(line.lstrip())
77
+
78
+ # Extract function body
79
+ body_lines = [line]
80
+ i += 1
81
+
82
+ # Find end of function (next line with same or less indentation that's not blank)
83
+ while i < len(lines):
84
+ current_line = lines[i]
85
+
86
+ # Skip blank lines and comments
87
+ if not current_line.strip() or current_line.strip().startswith('#'):
88
+ body_lines.append(current_line)
89
+ i += 1
90
+ continue
91
+
92
+ current_indent = len(current_line) - len(current_line.lstrip())
93
+
94
+ # If indentation is same or less and not blank, function ended
95
+ if current_indent <= indent and current_line.strip():
96
+ break
97
+
98
+ body_lines.append(current_line)
99
+ i += 1
100
+
101
+ function_body = '\n'.join(body_lines)
102
+ functions.append((func_name, start_line, function_body))
103
+ else:
104
+ i += 1
105
+
106
+ return functions
107
+
108
+
109
+ def calculate_cyclomatic_complexity(function_body: str) -> int:
110
+ """
111
+ Calculate cyclomatic complexity of a function.
112
+
113
+ Cyclomatic complexity = number of decision points + 1
114
+
115
+ Decision points:
116
+ - if, elif
117
+ - for, while
118
+ - and, or (in conditions)
119
+ - except
120
+ - case (match statement)
121
+ """
122
+ complexity = 1 # Base complexity
123
+
124
+ # Count decision keywords
125
+ keywords = ['if', 'elif', 'for', 'while', 'except', 'case']
126
+ for keyword in keywords:
127
+ # Match keyword as whole word
128
+ pattern = r'\b' + keyword + r'\b'
129
+ complexity += len(re.findall(pattern, function_body))
130
+
131
+ # Count boolean operators in conditions
132
+ # (simplified - count 'and' and 'or' in lines with 'if', 'elif', 'while')
133
+ condition_lines = [line for line in function_body.split('\n')
134
+ if re.search(r'\b(if|elif|while)\b', line)]
135
+
136
+ for line in condition_lines:
137
+ complexity += len(re.findall(r'\band\b', line))
138
+ complexity += len(re.findall(r'\bor\b', line))
139
+
140
+ return complexity
141
+
142
+
143
+ def calculate_nesting_depth(function_body: str) -> int:
144
+ """
145
+ Calculate maximum nesting depth in a function.
146
+
147
+ Counts nested blocks (if, for, while, with, try, etc.)
148
+ """
149
+ max_depth = 0
150
+ current_depth = 0
151
+ base_indent = None
152
+
153
+ for line in function_body.split('\n'):
154
+ stripped = line.strip()
155
+
156
+ # Skip blank lines and comments
157
+ if not stripped or stripped.startswith('#'):
158
+ continue
159
+
160
+ # Calculate indentation
161
+ indent = len(line) - len(line.lstrip())
162
+
163
+ # Set base indent from first non-empty line
164
+ if base_indent is None:
165
+ base_indent = indent
166
+ continue
167
+
168
+ # Calculate depth relative to function start
169
+ relative_indent = indent - base_indent
170
+
171
+ # Each 4 spaces = 1 level (standard Python indentation)
172
+ current_depth = relative_indent // 4
173
+
174
+ # Check if line introduces a new block
175
+ if stripped.endswith(':') and any(
176
+ stripped.startswith(kw) for kw in
177
+ ['if', 'elif', 'else', 'for', 'while', 'with', 'try', 'except', 'finally', 'def', 'class']
178
+ ):
179
+ max_depth = max(max_depth, current_depth + 1)
180
+ else:
181
+ max_depth = max(max_depth, current_depth)
182
+
183
+ return max_depth
184
+
185
+
186
+ def count_function_lines(function_body: str) -> int:
187
+ """
188
+ Count lines of code in function (excluding blank lines and comments).
189
+ """
190
+ lines = function_body.split('\n')
191
+ code_lines = 0
192
+
193
+ for line in lines:
194
+ stripped = line.strip()
195
+ # Skip blank lines and pure comment lines
196
+ if stripped and not stripped.startswith('#'):
197
+ code_lines += 1
198
+
199
+ return code_lines
200
+
201
+
202
+ def count_function_parameters(function_body: str) -> int:
203
+ """
204
+ Count number of parameters in function definition.
205
+ """
206
+ # Extract first line (function signature)
207
+ first_line = function_body.split('\n')[0]
208
+
209
+ # Extract parameters from signature
210
+ match = re.search(r'def\s+\w+\s*\((.*?)\)', first_line)
211
+ if not match:
212
+ return 0
213
+
214
+ params = match.group(1).strip()
215
+
216
+ # No parameters
217
+ if not params:
218
+ return 0
219
+
220
+ # Split by comma (simple counting)
221
+ # This is simplified - doesn't handle complex default values perfectly
222
+ param_list = [p.strip() for p in params.split(',')]
223
+
224
+ # Filter out 'self' and 'cls'
225
+ param_list = [p for p in param_list if not p.startswith('self') and not p.startswith('cls')]
226
+
227
+ return len(param_list)
228
+
229
+
230
+ @pytest.mark.coder
231
+ def test_cyclomatic_complexity_under_threshold():
232
+ """
233
+ SPEC-CODER-COMPLEXITY-0001: Functions have acceptable cyclomatic complexity.
234
+
235
+ Cyclomatic complexity measures the number of independent paths through code.
236
+ High complexity indicates code that is:
237
+ - Hard to test
238
+ - Hard to understand
239
+ - More likely to contain bugs
240
+
241
+ Threshold: < 10 (industry standard)
242
+
243
+ Given: All Python functions
244
+ When: Calculating cyclomatic complexity
245
+ Then: Complexity < 10 for all functions
246
+ """
247
+ python_files = find_python_files()
248
+
249
+ if not python_files:
250
+ pytest.skip("No Python files found")
251
+
252
+ violations = []
253
+
254
+ for py_file in python_files:
255
+ functions = extract_functions(py_file)
256
+
257
+ for func_name, line_num, func_body in functions:
258
+ # Skip very small functions (< 3 lines)
259
+ if count_function_lines(func_body) < 3:
260
+ continue
261
+
262
+ complexity = calculate_cyclomatic_complexity(func_body)
263
+
264
+ if complexity > MAX_CYCLOMATIC_COMPLEXITY:
265
+ rel_path = py_file.relative_to(REPO_ROOT)
266
+ violations.append(
267
+ f"{rel_path}:{line_num}\\n"
268
+ f" Function: {func_name}\\n"
269
+ f" Complexity: {complexity} (max: {MAX_CYCLOMATIC_COMPLEXITY})\\n"
270
+ f" Suggestion: Break into smaller functions"
271
+ )
272
+
273
+ if violations:
274
+ pytest.fail(
275
+ f"\\n\\nFound {len(violations)} complexity violations:\\n\\n" +
276
+ "\\n\\n".join(violations[:10]) +
277
+ (f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
278
+ )
279
+
280
+
281
+ @pytest.mark.coder
282
+ def test_nesting_depth_under_threshold():
283
+ """
284
+ SPEC-CODER-COMPLEXITY-0002: Functions have acceptable nesting depth.
285
+
286
+ Deep nesting makes code:
287
+ - Hard to read
288
+ - Hard to test
289
+ - More error-prone
290
+
291
+ Threshold: < 4 levels
292
+
293
+ Given: All Python functions
294
+ When: Calculating nesting depth
295
+ Then: Depth < 4 for all functions
296
+ """
297
+ python_files = find_python_files()
298
+
299
+ if not python_files:
300
+ pytest.skip("No Python files found")
301
+
302
+ violations = []
303
+
304
+ for py_file in python_files:
305
+ functions = extract_functions(py_file)
306
+
307
+ for func_name, line_num, func_body in functions:
308
+ depth = calculate_nesting_depth(func_body)
309
+
310
+ if depth > MAX_NESTING_DEPTH:
311
+ rel_path = py_file.relative_to(REPO_ROOT)
312
+ violations.append(
313
+ f"{rel_path}:{line_num}\\n"
314
+ f" Function: {func_name}\\n"
315
+ f" Nesting depth: {depth} (max: {MAX_NESTING_DEPTH})\\n"
316
+ f" Suggestion: Extract nested logic into separate functions"
317
+ )
318
+
319
+ if violations:
320
+ pytest.fail(
321
+ f"\\n\\nFound {len(violations)} nesting depth violations:\\n\\n" +
322
+ "\\n\\n".join(violations[:10]) +
323
+ (f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
324
+ )
325
+
326
+
327
+ @pytest.mark.coder
328
+ def test_function_length_under_threshold():
329
+ """
330
+ SPEC-CODER-COMPLEXITY-0003: Functions are not too long.
331
+
332
+ Long functions are:
333
+ - Hard to understand
334
+ - Hard to test
335
+ - Likely doing too much (SRP violation)
336
+
337
+ Threshold: < 50 lines of code
338
+
339
+ Given: All Python functions
340
+ When: Counting lines of code
341
+ Then: Length < 50 for all functions
342
+ """
343
+ python_files = find_python_files()
344
+
345
+ if not python_files:
346
+ pytest.skip("No Python files found")
347
+
348
+ violations = []
349
+
350
+ for py_file in python_files:
351
+ functions = extract_functions(py_file)
352
+
353
+ for func_name, line_num, func_body in functions:
354
+ lines = count_function_lines(func_body)
355
+
356
+ if lines > MAX_FUNCTION_LINES:
357
+ rel_path = py_file.relative_to(REPO_ROOT)
358
+ violations.append(
359
+ f"{rel_path}:{line_num}\\n"
360
+ f" Function: {func_name}\\n"
361
+ f" Lines: {lines} (max: {MAX_FUNCTION_LINES})\\n"
362
+ f" Suggestion: Break into smaller functions"
363
+ )
364
+
365
+ if violations:
366
+ pytest.fail(
367
+ f"\\n\\nFound {len(violations)} function length violations:\\n\\n" +
368
+ "\\n\\n".join(violations[:10]) +
369
+ (f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
370
+ )
371
+
372
+
373
+ @pytest.mark.coder
374
+ def test_function_parameter_count_under_threshold():
375
+ """
376
+ SPEC-CODER-COMPLEXITY-0004: Functions don't have too many parameters.
377
+
378
+ Too many parameters indicate:
379
+ - Function doing too much
380
+ - Poor abstraction
381
+ - Hard to call/test
382
+
383
+ Threshold: < 6 parameters
384
+
385
+ Given: All Python functions
386
+ When: Counting parameters
387
+ Then: Parameters < 6 for all functions
388
+ """
389
+ python_files = find_python_files()
390
+
391
+ if not python_files:
392
+ pytest.skip("No Python files found")
393
+
394
+ violations = []
395
+
396
+ for py_file in python_files:
397
+ functions = extract_functions(py_file)
398
+
399
+ for func_name, line_num, func_body in functions:
400
+ param_count = count_function_parameters(func_body)
401
+
402
+ if param_count > MAX_FUNCTION_PARAMS:
403
+ rel_path = py_file.relative_to(REPO_ROOT)
404
+ violations.append(
405
+ f"{rel_path}:{line_num}\\n"
406
+ f" Function: {func_name}\\n"
407
+ f" Parameters: {param_count} (max: {MAX_FUNCTION_PARAMS})\\n"
408
+ f" Suggestion: Use parameter objects or reduce responsibilities"
409
+ )
410
+
411
+ if violations:
412
+ pytest.fail(
413
+ f"\\n\\nFound {len(violations)} parameter count violations:\\n\\n" +
414
+ "\\n\\n".join(violations[:10]) +
415
+ (f"\\n\\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
416
+ )