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,586 @@
1
+ """
2
+ Test wagon boundary isolation via qualified imports.
3
+
4
+ Validates conventions from:
5
+ - atdd/coder/conventions/boundaries.convention.yaml
6
+
7
+ Enforces:
8
+ - No bare layer imports (from domain.X import Y)
9
+ - No sys.path manipulation in test files
10
+ - No cross-wagon imports (wagon A importing wagon B)
11
+ - Qualified imports pattern (from {wagon}.{feature}.src.{layer}.{module} import Class)
12
+ - Package hierarchy exists (__init__.py files)
13
+ - pytest pythonpath configured
14
+
15
+ Rationale:
16
+ Multiple wagons use identical layer names (domain, application, integration).
17
+ Without qualified imports, Python cannot distinguish between:
18
+ - commit_state/sign_commit/src/domain/signature_algorithm.py
19
+ - juggle_domains/score_domains/src/domain/choice.py
20
+
21
+ Both would resolve to "domain.X" causing module shadowing when tests run together.
22
+ """
23
+
24
+ import pytest
25
+ import re
26
+ from pathlib import Path
27
+ from typing import List, Tuple, Set
28
+ import ast
29
+
30
+
31
+ # Path constants
32
+ REPO_ROOT = Path(__file__).resolve().parents[4]
33
+ PYTHON_DIR = REPO_ROOT / "python"
34
+ PYPROJECT_TOML = PYTHON_DIR / "pyproject.toml"
35
+ BOUNDARIES_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "boundaries.convention.yaml"
36
+
37
+
38
+ def find_test_files() -> List[Path]:
39
+ """Find all test files in wagons."""
40
+ if not PYTHON_DIR.exists():
41
+ return []
42
+
43
+ test_files = []
44
+ for py_file in PYTHON_DIR.rglob("test_*.py"):
45
+ # Skip __pycache__
46
+ if '__pycache__' in str(py_file):
47
+ continue
48
+ test_files.append(py_file)
49
+
50
+ return test_files
51
+
52
+
53
+ def find_implementation_files() -> List[Path]:
54
+ """
55
+ Find all implementation files in wagons (excluding tests and orchestration layers).
56
+
57
+ Excluded from wagon boundary checks:
58
+ - Test files (testing infrastructure)
59
+ - wagon.py, composition.py (wagon-level orchestration)
60
+ - shared/ directory (theme/train-level orchestration)
61
+ - contracts/ directory (neutral DTO boundary layer)
62
+ - scripts/ directory (utility scripts and tools)
63
+ """
64
+ if not PYTHON_DIR.exists():
65
+ return []
66
+
67
+ impl_files = []
68
+ for py_file in PYTHON_DIR.rglob("*.py"):
69
+ # Skip test files
70
+ if '/test/' in str(py_file) or py_file.name.startswith('test_'):
71
+ continue
72
+ # Skip __pycache__
73
+ if '__pycache__' in str(py_file):
74
+ continue
75
+ # Skip wagon.py, composition.py, and game.py (wagon/app-level orchestration)
76
+ if py_file.name in ['wagon.py', 'composition.py', 'game.py']:
77
+ continue
78
+ # Skip shared/ directory (theme/train-level orchestration - can import across wagons)
79
+ if '/shared/' in str(py_file):
80
+ continue
81
+ # Skip contracts/ directory (neutral DTO layer)
82
+ if '/contracts/' in str(py_file):
83
+ continue
84
+ # Skip scripts/ directory (utility scripts - can import across wagons for tooling)
85
+ if '/scripts/' in str(py_file):
86
+ continue
87
+
88
+ impl_files.append(py_file)
89
+
90
+ return impl_files
91
+
92
+
93
+ def extract_imports_ast(file_path: Path) -> List[Tuple[str, int]]:
94
+ """
95
+ Extract imports using AST parsing.
96
+
97
+ Excludes imports that are:
98
+ - Inside `if TYPE_CHECKING:` blocks (type-only imports, never executed)
99
+ - Inside function/method definitions (lazy imports for architecture compliance)
100
+
101
+ Returns:
102
+ List of (import_path, line_number) tuples
103
+ """
104
+ try:
105
+ with open(file_path, 'r', encoding='utf-8') as f:
106
+ content = f.read()
107
+ except Exception:
108
+ return []
109
+
110
+ imports = []
111
+
112
+ try:
113
+ tree = ast.parse(content, filename=str(file_path))
114
+
115
+ def is_inside_type_checking(node, tree):
116
+ """Check if node is inside an 'if TYPE_CHECKING:' block."""
117
+ for parent in ast.walk(tree):
118
+ if isinstance(parent, ast.If):
119
+ # Check if condition is TYPE_CHECKING
120
+ if isinstance(parent.test, ast.Name) and parent.test.id == 'TYPE_CHECKING':
121
+ # Check if the import node is in the body of this if
122
+ for child in ast.walk(parent):
123
+ if child is node:
124
+ return True
125
+ return False
126
+
127
+ def is_inside_function(node, tree):
128
+ """Check if node is inside a function/method definition."""
129
+ for parent in ast.walk(tree):
130
+ if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)):
131
+ for child in ast.walk(parent):
132
+ if child is node:
133
+ return True
134
+ return False
135
+
136
+ for node in ast.walk(tree):
137
+ if isinstance(node, ast.ImportFrom):
138
+ if node.module and node.level == 0: # Only absolute imports (not relative imports with ...)
139
+ # Skip TYPE_CHECKING and function-level imports
140
+ if not is_inside_type_checking(node, tree) and not is_inside_function(node, tree):
141
+ imports.append((node.module, node.lineno))
142
+ elif isinstance(node, ast.Import):
143
+ # Skip TYPE_CHECKING and function-level imports
144
+ if not is_inside_type_checking(node, tree) and not is_inside_function(node, tree):
145
+ for alias in node.names:
146
+ imports.append((alias.name, node.lineno))
147
+ except SyntaxError:
148
+ # Fall back to regex if AST parsing fails
149
+ return extract_imports_regex(file_path)
150
+
151
+ return imports
152
+
153
+
154
+ def extract_imports_regex(file_path: Path) -> List[Tuple[str, int]]:
155
+ """Extract imports using regex (fallback)."""
156
+ try:
157
+ with open(file_path, 'r', encoding='utf-8') as f:
158
+ lines = f.readlines()
159
+ except Exception:
160
+ return []
161
+
162
+ imports = []
163
+
164
+ for line_no, line in enumerate(lines, start=1):
165
+ # from X import Y
166
+ match = re.match(r'from\s+([^\s]+)\s+import', line)
167
+ if match:
168
+ imports.append((match.group(1), line_no))
169
+
170
+ # import X
171
+ match = re.match(r'^\s*import\s+([^\s;#]+)', line)
172
+ if match:
173
+ for imp in match.group(1).split(','):
174
+ imports.append((imp.strip(), line_no))
175
+
176
+ return imports
177
+
178
+
179
+ def check_for_syspath_manipulation(file_path: Path) -> List[Tuple[str, int]]:
180
+ """
181
+ Check if file manipulates sys.path.
182
+
183
+ Returns:
184
+ List of (line_content, line_number) tuples where sys.path is manipulated
185
+ """
186
+ try:
187
+ with open(file_path, 'r', encoding='utf-8') as f:
188
+ lines = f.readlines()
189
+ except Exception:
190
+ return []
191
+
192
+ violations = []
193
+
194
+ for line_no, line in enumerate(lines, start=1):
195
+ if 'sys.path.insert' in line or 'sys.path.append' in line:
196
+ violations.append((line.strip(), line_no))
197
+
198
+ return violations
199
+
200
+
201
+ def get_wagon_from_path(file_path: Path) -> str:
202
+ """Extract wagon name from file path."""
203
+ try:
204
+ rel_path = file_path.relative_to(PYTHON_DIR)
205
+ parts = rel_path.parts
206
+ if len(parts) > 0:
207
+ return parts[0]
208
+ except ValueError:
209
+ pass
210
+ return ""
211
+
212
+
213
+ def is_bare_layer_import(import_path: str) -> bool:
214
+ """
215
+ Check if import is a bare layer import.
216
+
217
+ Bare imports like:
218
+ - from domain.X import Y
219
+ - from application.X import Y
220
+ - from integration.X import Y
221
+ - from presentation.X import Y
222
+
223
+ Returns:
224
+ True if bare layer import
225
+ """
226
+ # Check if starts with layer name (and not a qualified path)
227
+ if import_path.startswith(('domain.', 'application.', 'integration.', 'presentation.')):
228
+ return True
229
+
230
+ # Check exact match (import domain, import application, etc.)
231
+ if import_path in ['domain', 'application', 'integration', 'presentation']:
232
+ return True
233
+
234
+ return False
235
+
236
+
237
+ def is_cross_wagon_import(file_path: Path, import_path: str) -> Tuple[bool, str, str]:
238
+ """
239
+ Check if import crosses wagon boundaries.
240
+
241
+ Returns:
242
+ (is_cross_wagon, source_wagon, target_wagon)
243
+ """
244
+ source_wagon = get_wagon_from_path(file_path)
245
+
246
+ # Check if import is from a different wagon
247
+ # Pattern: {wagon}.{feature}.src.{layer}.{module}
248
+ match = re.match(r'([^.]+)\.', import_path)
249
+ if match:
250
+ target_wagon = match.group(1)
251
+
252
+ # Check if it's a different wagon (not shared utilities, commons, or contracts)
253
+ # generate_identifiers is a utility wagon providing cross-cutting concerns
254
+ if target_wagon != source_wagon and target_wagon not in ['shared', 'commons', 'generate_identifiers', '__init__', 'contracts']:
255
+ # Verify it's an actual wagon directory
256
+ wagon_dir = PYTHON_DIR / target_wagon
257
+ if wagon_dir.exists() and wagon_dir.is_dir():
258
+ return (True, source_wagon, target_wagon)
259
+
260
+ return (False, source_wagon, "")
261
+
262
+
263
+ def check_package_hierarchy() -> List[str]:
264
+ """
265
+ Check if required __init__.py files exist for package hierarchy.
266
+
267
+ Returns:
268
+ List of missing __init__.py paths
269
+ """
270
+ missing = []
271
+
272
+ # Check python/__init__.py
273
+ if not (PYTHON_DIR / "__init__.py").exists():
274
+ missing.append("python/__init__.py")
275
+
276
+ # Check each wagon
277
+ for wagon_dir in PYTHON_DIR.iterdir():
278
+ if not wagon_dir.is_dir() or wagon_dir.name.startswith('.') or wagon_dir.name == '__pycache__':
279
+ continue
280
+
281
+ # Check wagon/__init__.py
282
+ if not (wagon_dir / "__init__.py").exists():
283
+ missing.append(f"python/{wagon_dir.name}/__init__.py")
284
+
285
+ # Check each feature in wagon
286
+ for feature_dir in wagon_dir.iterdir():
287
+ if not feature_dir.is_dir() or feature_dir.name.startswith('.') or feature_dir.name == '__pycache__':
288
+ continue
289
+
290
+ # Skip non-feature directories
291
+ if feature_dir.name in ['__pycache__', 'test', 'tests']:
292
+ continue
293
+
294
+ # Check if has src/ (indicates it's a feature)
295
+ src_dir = feature_dir / "src"
296
+ if src_dir.exists():
297
+ # Check feature/__init__.py
298
+ if not (feature_dir / "__init__.py").exists():
299
+ missing.append(f"python/{wagon_dir.name}/{feature_dir.name}/__init__.py")
300
+
301
+ return missing
302
+
303
+
304
+ def check_pytest_pythonpath() -> Tuple[bool, str]:
305
+ """
306
+ Check if pytest pythonpath is configured in pyproject.toml.
307
+
308
+ Returns:
309
+ (is_configured, message)
310
+ """
311
+ if not PYPROJECT_TOML.exists():
312
+ return (False, "python/pyproject.toml not found")
313
+
314
+ try:
315
+ with open(PYPROJECT_TOML, 'r') as f:
316
+ content = f.read()
317
+
318
+ # Check for [tool.pytest.ini_options] section with pythonpath
319
+ if 'tool.pytest.ini_options' in content and 'pythonpath' in content:
320
+ return (True, "pythonpath configured")
321
+ else:
322
+ return (False, "pythonpath not configured in [tool.pytest.ini_options]")
323
+ except Exception as e:
324
+ return (False, f"Error reading pyproject.toml: {e}")
325
+
326
+
327
+ @pytest.mark.coder
328
+ def test_no_bare_layer_imports_in_tests():
329
+ """
330
+ SPEC-BOUNDARIES-0001: Test files must use qualified imports.
331
+
332
+ Convention: boundaries.convention.yaml::namespacing.forbidden_patterns.bare_layer_imports
333
+
334
+ Forbidden:
335
+ - from domain.signature_algorithm import SignatureAlgorithm
336
+ - from application.use_cases.X import Y
337
+
338
+ Required:
339
+ - from commit_state.sign_commit.src.domain.signature_algorithm import SignatureAlgorithm
340
+ - from juggle_domains.score_domains.src.application.use_cases.X import Y
341
+
342
+ Given: All test files in python/
343
+ When: Checking imports
344
+ Then: No bare layer imports (from domain.X, from application.X, etc.)
345
+ """
346
+ test_files = find_test_files()
347
+
348
+ if not test_files:
349
+ pytest.skip("No test files found to validate")
350
+
351
+ violations = []
352
+
353
+ for test_file in test_files:
354
+ imports = extract_imports_ast(test_file)
355
+
356
+ for import_path, line_no in imports:
357
+ if is_bare_layer_import(import_path):
358
+ rel_path = test_file.relative_to(REPO_ROOT)
359
+ violations.append(
360
+ f"{rel_path}:{line_no}\n"
361
+ f" Import: from {import_path} import ...\n"
362
+ f" Issue: Bare layer import causes module shadowing\n"
363
+ f" Fix: Use qualified import from {{wagon}}.{{feature}}.src.{import_path.split('.')[0]}.{{module}}"
364
+ )
365
+
366
+ if violations:
367
+ pytest.fail(
368
+ f"\n\nFound {len(violations)} bare layer imports in test files:\n\n" +
369
+ "\n\n".join(violations[:10]) +
370
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
371
+ "\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::namespacing.forbidden_patterns"
372
+ )
373
+
374
+
375
+ @pytest.mark.coder
376
+ def test_no_syspath_manipulation_in_tests():
377
+ """
378
+ SPEC-BOUNDARIES-0002: Test files must not manipulate sys.path.
379
+
380
+ Convention: boundaries.convention.yaml::namespacing.syspath_prohibition
381
+
382
+ Forbidden in test files:
383
+ - sys.path.insert(0, str(src_path))
384
+ - sys.path.append(...)
385
+
386
+ Reason: Causes cross-wagon path collisions; use pytest pythonpath instead
387
+
388
+ Given: All test files
389
+ When: Checking for sys.path manipulation
390
+ Then: No sys.path.insert() or sys.path.append() in test files
391
+ """
392
+ test_files = find_test_files()
393
+
394
+ if not test_files:
395
+ pytest.skip("No test files found to validate")
396
+
397
+ violations = []
398
+
399
+ for test_file in test_files:
400
+ syspath_lines = check_for_syspath_manipulation(test_file)
401
+
402
+ if syspath_lines:
403
+ rel_path = test_file.relative_to(REPO_ROOT)
404
+ for line_content, line_no in syspath_lines:
405
+ violations.append(
406
+ f"{rel_path}:{line_no}\n"
407
+ f" Code: {line_content}\n"
408
+ f" Issue: Test file manipulates sys.path\n"
409
+ f" Fix: Remove sys.path manipulation; pytest pythonpath handles this"
410
+ )
411
+
412
+ if violations:
413
+ pytest.fail(
414
+ f"\n\nFound {len(violations)} sys.path manipulations in test files:\n\n" +
415
+ "\n\n".join(violations[:10]) +
416
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
417
+ "\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::namespacing.syspath_prohibition"
418
+ )
419
+
420
+
421
+ @pytest.mark.coder
422
+ def test_no_cross_wagon_imports():
423
+ """
424
+ SPEC-BOUNDARIES-0003: Wagons cannot import directly from other wagons.
425
+
426
+ Convention: boundaries.convention.yaml::interaction.forbidden_cross_wagon_imports
427
+ Cross-reference: design.convention.yaml::VC-DS-06
428
+
429
+ Forbidden:
430
+ - from juggle_domains.score_domains.src.domain.choice import Choice # in commit_state wagon
431
+
432
+ Required:
433
+ - Wagons communicate only via contracts (see contract.convention.yaml)
434
+
435
+ Given: All implementation files
436
+ When: Checking imports
437
+ Then: No imports from other wagons (only via contracts)
438
+ """
439
+ impl_files = find_implementation_files()
440
+
441
+ if not impl_files:
442
+ pytest.skip("No implementation files found to validate")
443
+
444
+ violations = []
445
+
446
+ for impl_file in impl_files:
447
+ imports = extract_imports_ast(impl_file)
448
+
449
+ for import_path, line_no in imports:
450
+ is_cross, source_wagon, target_wagon = is_cross_wagon_import(impl_file, import_path)
451
+
452
+ if is_cross:
453
+ rel_path = impl_file.relative_to(REPO_ROOT)
454
+ violations.append(
455
+ f"{rel_path}:{line_no}\n"
456
+ f" Source wagon: {source_wagon}\n"
457
+ f" Target wagon: {target_wagon}\n"
458
+ f" Import: {import_path}\n"
459
+ f" Issue: Direct cross-wagon import creates tight coupling\n"
460
+ f" Fix: Use contracts for wagon-to-wagon communication"
461
+ )
462
+
463
+ if violations:
464
+ pytest.fail(
465
+ f"\n\nFound {len(violations)} cross-wagon imports:\n\n" +
466
+ "\n\n".join(violations[:10]) +
467
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
468
+ "\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::interaction.forbidden_cross_wagon_imports"
469
+ )
470
+
471
+
472
+ @pytest.mark.coder
473
+ def test_package_hierarchy_exists():
474
+ """
475
+ SPEC-BOUNDARIES-0004: Package hierarchy must be complete.
476
+
477
+ Convention: boundaries.convention.yaml::namespacing.package_hierarchy
478
+
479
+ Required __init__.py files:
480
+ - python/__init__.py
481
+ - python/{wagon}/__init__.py
482
+ - python/{wagon}/{feature}/__init__.py
483
+
484
+ Given: Python directory structure
485
+ When: Checking for __init__.py files
486
+ Then: All required __init__.py files exist
487
+ """
488
+ missing = check_package_hierarchy()
489
+
490
+ if missing:
491
+ pytest.fail(
492
+ f"\n\nMissing {len(missing)} required __init__.py files:\n\n" +
493
+ "\n".join(f" - {path}" for path in missing) +
494
+ "\n\nPackage hierarchy is required for qualified imports to work.\n" +
495
+ "See: atdd/coder/conventions/boundaries.convention.yaml::namespacing.package_hierarchy"
496
+ )
497
+
498
+
499
+ @pytest.mark.coder
500
+ def test_pytest_pythonpath_configured():
501
+ """
502
+ SPEC-BOUNDARIES-0005: pytest pythonpath must be configured.
503
+
504
+ Convention: boundaries.convention.yaml::namespacing.test_configuration
505
+
506
+ Required in python/pyproject.toml:
507
+ [tool.pytest.ini_options]
508
+ pythonpath = ["."]
509
+
510
+ Given: python/pyproject.toml
511
+ When: Checking [tool.pytest.ini_options]
512
+ Then: pythonpath = ["."] is configured
513
+ """
514
+ is_configured, message = check_pytest_pythonpath()
515
+
516
+ if not is_configured:
517
+ pytest.fail(
518
+ f"\n\npytest pythonpath not configured:\n\n"
519
+ f" Issue: {message}\n\n"
520
+ f"Required configuration in python/pyproject.toml:\n"
521
+ f" [tool.pytest.ini_options]\n"
522
+ f" pythonpath = [\".\"]\n\n"
523
+ f"This is required for qualified imports to work across wagons.\n"
524
+ f"See: atdd/coder/conventions/boundaries.convention.yaml::namespacing.test_configuration"
525
+ )
526
+
527
+
528
+ @pytest.mark.coder
529
+ def test_no_bare_layer_imports_in_implementation():
530
+ """
531
+ SPEC-BOUNDARIES-0006: Implementation files should use qualified imports.
532
+
533
+ Convention: boundaries.convention.yaml::namespacing.forbidden_patterns.bare_layer_imports
534
+
535
+ Note: composition.py and wagon.py are excluded (they may use bare imports)
536
+
537
+ Forbidden in implementation files:
538
+ - from domain.signature_algorithm import SignatureAlgorithm
539
+ - from src.domain.X import Y
540
+
541
+ Required:
542
+ - from commit_state.sign_commit.src.domain.signature_algorithm import SignatureAlgorithm
543
+ - Relative imports within same layer: from .base_repository import BaseRepository
544
+
545
+ Given: All implementation files (excluding composition.py/wagon.py)
546
+ When: Checking imports
547
+ Then: No bare layer imports or src-relative imports
548
+ """
549
+ impl_files = find_implementation_files()
550
+
551
+ if not impl_files:
552
+ pytest.skip("No implementation files found to validate")
553
+
554
+ violations = []
555
+
556
+ for impl_file in impl_files:
557
+ imports = extract_imports_ast(impl_file)
558
+
559
+ for import_path, line_no in imports:
560
+ # Check for bare layer imports
561
+ if is_bare_layer_import(import_path):
562
+ rel_path = impl_file.relative_to(REPO_ROOT)
563
+ violations.append(
564
+ f"{rel_path}:{line_no}\n"
565
+ f" Import: from {import_path} import ...\n"
566
+ f" Issue: Bare layer import in implementation file\n"
567
+ f" Fix: Use qualified import or relative import within same layer"
568
+ )
569
+
570
+ # Check for src-relative imports
571
+ elif import_path.startswith('src.'):
572
+ rel_path = impl_file.relative_to(REPO_ROOT)
573
+ violations.append(
574
+ f"{rel_path}:{line_no}\n"
575
+ f" Import: from {import_path} import ...\n"
576
+ f" Issue: src-relative import only works with sys.path manipulation\n"
577
+ f" Fix: Use qualified import from {{wagon}}.{{feature}}.src.{{layer}}.{{module}}"
578
+ )
579
+
580
+ if violations:
581
+ pytest.fail(
582
+ f"\n\nFound {len(violations)} bare/src-relative imports in implementation:\n\n" +
583
+ "\n\n".join(violations[:10]) +
584
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
585
+ "\n\nSee: atdd/coder/conventions/boundaries.convention.yaml::namespacing.forbidden_patterns"
586
+ )