atdd 0.1.0__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 (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,485 @@
1
+ """
2
+ Test commons structure consistency across Python and Frontend.
3
+
4
+ Validates both structural patterns:
5
+ - Python: Feature-first (complex features have internal layers, utilities are flat)
6
+ - Frontend: Layer-first (all code organized by architectural layer)
7
+
8
+ Both patterns enforce:
9
+ - Domain layer purity (no framework imports)
10
+ - Consistent naming (commons, not shared)
11
+ - Proper dependency direction
12
+
13
+ Convention: atdd/coder/conventions/commons.convention.yaml
14
+ """
15
+
16
+ import pytest
17
+ import re
18
+ from pathlib import Path
19
+ from typing import List
20
+
21
+
22
+ REPO_ROOT = Path(__file__).resolve().parents[4]
23
+ PYTHON_COMMONS = REPO_ROOT / "python" / "commons"
24
+ WEB_COMMONS = REPO_ROOT / "web" / "src" / "commons"
25
+ WEB_SRC = REPO_ROOT / "web" / "src"
26
+
27
+
28
+ # ============================================================================
29
+ # CROSS-STACK VALIDATION
30
+ # ============================================================================
31
+
32
+
33
+ @pytest.mark.coder
34
+ def test_commons_exists_in_both_stacks():
35
+ """
36
+ SPEC-CODER-COMMONS-0001: Commons exists in Python and Frontend.
37
+
38
+ GIVEN: Project with polyglot codebase
39
+ WHEN: Checking for commons directories
40
+ THEN: Both python/commons and web/src/commons exist
41
+
42
+ Validates: Consistent naming across stacks
43
+ """
44
+ missing = []
45
+
46
+ if not PYTHON_COMMONS.exists():
47
+ missing.append(f"python/commons/ (expected at {PYTHON_COMMONS})")
48
+
49
+ if not WEB_COMMONS.exists():
50
+ missing.append(f"web/src/commons/ (expected at {WEB_COMMONS})")
51
+
52
+ if missing:
53
+ pytest.fail(
54
+ f"\n\nMissing commons directories:\n" +
55
+ "\n".join(f" - {m}" for m in missing)
56
+ )
57
+
58
+
59
+ @pytest.mark.coder
60
+ def test_no_shared_directory_exists():
61
+ """
62
+ SPEC-CODER-COMMONS-0003: Old 'shared' directory should not exist.
63
+
64
+ GIVEN: Project migrated to commons convention
65
+ WHEN: Checking for legacy shared directory
66
+ THEN: web/src/shared should not exist
67
+
68
+ Validates: Migration from shared to commons complete
69
+ """
70
+ old_shared = REPO_ROOT / "web" / "src" / "shared"
71
+
72
+ if old_shared.exists():
73
+ files = list(old_shared.rglob("*"))
74
+ pytest.fail(
75
+ f"\n\nLegacy 'shared' directory still exists at web/src/shared\n"
76
+ f"Contains {len(files)} files.\n"
77
+ f"Migrate to web/src/commons/ and delete."
78
+ )
79
+
80
+
81
+ # ============================================================================
82
+ # FRONTEND STRUCTURE VALIDATION (Layer-First)
83
+ # ============================================================================
84
+
85
+
86
+ @pytest.mark.coder
87
+ def test_frontend_commons_has_layer_structure():
88
+ """
89
+ SPEC-CODER-COMMONS-0002: Frontend commons has domain/application/integration layers.
90
+
91
+ GIVEN: web/src/commons directory
92
+ WHEN: Checking layer subdirectories
93
+ THEN: domain/, application/, integration/ exist
94
+
95
+ Validates: Layer-first structure for frontend
96
+ """
97
+ if not WEB_COMMONS.exists():
98
+ pytest.skip("web/src/commons does not exist")
99
+
100
+ expected_layers = ["domain", "application", "integration"]
101
+ missing_layers = []
102
+
103
+ for layer in expected_layers:
104
+ layer_path = WEB_COMMONS / layer
105
+ if not layer_path.exists():
106
+ missing_layers.append(layer)
107
+
108
+ if missing_layers:
109
+ pytest.fail(
110
+ f"\n\nMissing layers in web/src/commons/:\n" +
111
+ "\n".join(f" - {layer}/" for layer in missing_layers) +
112
+ f"\n\nExpected structure (layer-first):\n" +
113
+ " web/src/commons/\n" +
114
+ " +-- domain/ # Framework-agnostic types\n" +
115
+ " +-- application/ # Hooks, context\n" +
116
+ " +-- integration/ # Clients, adapters"
117
+ )
118
+
119
+
120
+ @pytest.mark.coder
121
+ def test_path_alias_uses_commons():
122
+ """
123
+ SPEC-CODER-COMMONS-0004: Path aliases use @commons, not @shared.
124
+
125
+ GIVEN: tsconfig.json and vite.config.ts
126
+ WHEN: Checking path aliases
127
+ THEN: @commons is defined, @shared is not
128
+
129
+ Validates: Correct path alias configuration
130
+ """
131
+ tsconfig_path = REPO_ROOT / "web" / "tsconfig.json"
132
+ vite_config_path = REPO_ROOT / "web" / "vite.config.ts"
133
+
134
+ issues = []
135
+
136
+ if tsconfig_path.exists():
137
+ content = tsconfig_path.read_text()
138
+ if "@shared" in content:
139
+ issues.append("tsconfig.json still contains @shared alias")
140
+ if "@commons" not in content:
141
+ issues.append("tsconfig.json missing @commons alias")
142
+
143
+ if vite_config_path.exists():
144
+ content = vite_config_path.read_text()
145
+ if "'@shared'" in content or '"@shared"' in content:
146
+ issues.append("vite.config.ts still contains @shared alias")
147
+ if "'@commons'" not in content and '"@commons"' not in content:
148
+ issues.append("vite.config.ts missing @commons alias")
149
+
150
+ if issues:
151
+ pytest.fail(
152
+ "\n\nPath alias configuration issues:\n" +
153
+ "\n".join(f" - {i}" for i in issues)
154
+ )
155
+
156
+
157
+ @pytest.mark.coder
158
+ def test_no_imports_from_shared():
159
+ """
160
+ SPEC-CODER-COMMONS-0005: No imports from @shared or ./shared.
161
+
162
+ GIVEN: All TypeScript files in web/src
163
+ WHEN: Checking import statements
164
+ THEN: No imports reference @shared or relative shared paths
165
+
166
+ Validates: All imports migrated to @commons
167
+ """
168
+ if not WEB_SRC.exists():
169
+ pytest.skip("web/src does not exist")
170
+
171
+ violations: List[str] = []
172
+
173
+ for ts_file in WEB_SRC.rglob("*.ts"):
174
+ _check_file_for_shared_imports(ts_file, violations)
175
+
176
+ for tsx_file in WEB_SRC.rglob("*.tsx"):
177
+ _check_file_for_shared_imports(tsx_file, violations)
178
+
179
+ if violations:
180
+ pytest.fail(
181
+ f"\n\nFound {len(violations)} files still importing from 'shared':\n\n" +
182
+ "\n".join(violations[:10]) +
183
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "")
184
+ )
185
+
186
+
187
+ def _check_file_for_shared_imports(file_path: Path, violations: List[str]) -> None:
188
+ """Check a file for shared imports."""
189
+ try:
190
+ content = file_path.read_text()
191
+ except Exception:
192
+ return
193
+
194
+ patterns = [
195
+ r"from\s+['\"]@shared",
196
+ r"from\s+['\"]\.\.?/shared",
197
+ r"import\s+.*from\s+['\"]@shared",
198
+ r"import\s+.*from\s+['\"]\.\.?/shared",
199
+ ]
200
+
201
+ for pattern in patterns:
202
+ if re.search(pattern, content):
203
+ rel_path = file_path.relative_to(REPO_ROOT)
204
+ violations.append(f" - {rel_path}")
205
+ break
206
+
207
+
208
+ @pytest.mark.coder
209
+ def test_frontend_domain_no_framework_imports():
210
+ """
211
+ SPEC-CODER-COMMONS-0006: Frontend domain layer has no framework imports.
212
+
213
+ GIVEN: Files in web/src/commons/domain/
214
+ WHEN: Checking import statements
215
+ THEN: No preact, react, or @tanstack imports
216
+
217
+ Validates: Domain layer purity (no preact, react, @tanstack)
218
+ """
219
+ domain_dir = WEB_COMMONS / "domain"
220
+
221
+ if not domain_dir.exists():
222
+ pytest.skip("web/src/commons/domain does not exist")
223
+
224
+ forbidden = ["preact", "react", "@tanstack", "@maintain-ux"]
225
+ violations: List[str] = []
226
+
227
+ for ts_file in domain_dir.rglob("*.ts"):
228
+ try:
229
+ content = ts_file.read_text()
230
+ except Exception:
231
+ continue
232
+
233
+ for forbidden_import in forbidden:
234
+ if f"from '{forbidden_import}" in content or f'from "{forbidden_import}' in content:
235
+ rel_path = ts_file.relative_to(REPO_ROOT)
236
+ violations.append(f" - {rel_path}: imports {forbidden_import}")
237
+
238
+ if violations:
239
+ pytest.fail(
240
+ f"\n\nFrontend domain layer should be framework-agnostic:\n" +
241
+ "\n".join(violations)
242
+ )
243
+
244
+
245
+ @pytest.mark.coder
246
+ def test_frontend_commons_has_index_files():
247
+ """
248
+ SPEC-CODER-COMMONS-0007: Frontend commons has proper barrel exports.
249
+
250
+ GIVEN: web/src/commons directory
251
+ WHEN: Checking for index.ts files
252
+ THEN: Root and each layer has index.ts
253
+
254
+ Validates: Public API structure
255
+ """
256
+ if not WEB_COMMONS.exists():
257
+ pytest.skip("web/src/commons does not exist")
258
+
259
+ expected_index_files = [
260
+ WEB_COMMONS / "index.ts",
261
+ WEB_COMMONS / "domain" / "index.ts",
262
+ WEB_COMMONS / "application" / "index.ts",
263
+ WEB_COMMONS / "integration" / "index.ts",
264
+ ]
265
+
266
+ missing = [f for f in expected_index_files if not f.exists()]
267
+
268
+ if missing:
269
+ pytest.fail(
270
+ f"\n\nMissing index.ts barrel exports:\n" +
271
+ "\n".join(f" - {f.relative_to(REPO_ROOT)}" for f in missing)
272
+ )
273
+
274
+
275
+ # ============================================================================
276
+ # PYTHON STRUCTURE VALIDATION (Feature-First)
277
+ # ============================================================================
278
+
279
+
280
+ @pytest.mark.coder
281
+ def test_python_commons_has_init_files():
282
+ """
283
+ SPEC-CODER-COMMONS-0008: Python commons has __init__.py files.
284
+
285
+ GIVEN: python/commons directory
286
+ WHEN: Checking for __init__.py files
287
+ THEN: Root has __init__.py
288
+
289
+ Validates: Python package structure
290
+ """
291
+ if not PYTHON_COMMONS.exists():
292
+ pytest.skip("python/commons does not exist")
293
+
294
+ init_file = PYTHON_COMMONS / "__init__.py"
295
+
296
+ if not init_file.exists():
297
+ pytest.fail(
298
+ f"\n\nMissing __init__.py in python/commons/\n"
299
+ f"Expected: {init_file.relative_to(REPO_ROOT)}"
300
+ )
301
+
302
+
303
+ @pytest.mark.coder
304
+ def test_python_events_has_internal_layer_structure():
305
+ """
306
+ SPEC-CODER-COMMONS-0009: Python events feature has internal layer structure.
307
+
308
+ GIVEN: python/commons/events directory
309
+ WHEN: Checking for internal layers
310
+ THEN: events/src/application/ports/ and events/src/integration/ exist
311
+
312
+ Validates: Feature-first pattern for complex features
313
+ """
314
+ events_dir = PYTHON_COMMONS / "events"
315
+
316
+ if not events_dir.exists():
317
+ pytest.skip("python/commons/events does not exist")
318
+
319
+ expected_paths = [
320
+ events_dir / "src" / "application" / "ports",
321
+ events_dir / "src" / "integration",
322
+ ]
323
+
324
+ missing = [p for p in expected_paths if not p.exists()]
325
+
326
+ if missing:
327
+ pytest.fail(
328
+ f"\n\nPython events feature missing internal layer structure:\n" +
329
+ "\n".join(f" - {p.relative_to(REPO_ROOT)}" for p in missing) +
330
+ f"\n\nExpected structure (feature-first with internal layers):\n" +
331
+ " python/commons/events/\n" +
332
+ " +-- src/\n" +
333
+ " +-- application/ports/ # EventBusPort\n" +
334
+ " +-- integration/ # Queues, adapters"
335
+ )
336
+
337
+
338
+ @pytest.mark.coder
339
+ def test_python_resilience_is_flat():
340
+ """
341
+ SPEC-CODER-COMMONS-0010: Python resilience is flat (no internal layers).
342
+
343
+ GIVEN: python/commons/resilience directory
344
+ WHEN: Checking structure
345
+ THEN: Contains .py files directly, no src/application/integration subdirs
346
+
347
+ Validates: Flat structure for simple utilities
348
+ """
349
+ resilience_dir = PYTHON_COMMONS / "resilience"
350
+
351
+ if not resilience_dir.exists():
352
+ pytest.skip("python/commons/resilience does not exist")
353
+
354
+ # Check that utility files exist at root
355
+ expected_files = ["retry.py", "circuit_breaker.py"]
356
+ missing_files = [f for f in expected_files if not (resilience_dir / f).exists()]
357
+
358
+ # Check that no unnecessary layer subdirs exist
359
+ unnecessary_subdirs = []
360
+ for subdir in ["src", "application", "integration", "domain"]:
361
+ if (resilience_dir / subdir).exists():
362
+ unnecessary_subdirs.append(subdir)
363
+
364
+ issues = []
365
+
366
+ if missing_files:
367
+ issues.append(
368
+ f"Missing utility files: {', '.join(missing_files)}"
369
+ )
370
+
371
+ if unnecessary_subdirs:
372
+ issues.append(
373
+ f"Unnecessary layer subdirs (resilience should be flat): {', '.join(unnecessary_subdirs)}"
374
+ )
375
+
376
+ if issues:
377
+ pytest.fail(
378
+ f"\n\nPython resilience structure issues:\n" +
379
+ "\n".join(f" - {i}" for i in issues) +
380
+ f"\n\nExpected structure (flat for simple utilities):\n" +
381
+ " python/commons/resilience/\n" +
382
+ " +-- __init__.py\n" +
383
+ " +-- retry.py\n" +
384
+ " +-- circuit_breaker.py"
385
+ )
386
+
387
+
388
+ @pytest.mark.coder
389
+ def test_python_domain_no_framework_imports():
390
+ """
391
+ SPEC-CODER-COMMONS-0011: Python domain layer has no framework imports.
392
+
393
+ GIVEN: Files in python/commons/domain/ and python/commons/validation.py
394
+ WHEN: Checking import statements
395
+ THEN: No flask, fastapi, or django imports
396
+
397
+ Validates: Domain layer purity (no flask, fastapi, django)
398
+ """
399
+ if not PYTHON_COMMONS.exists():
400
+ pytest.skip("python/commons does not exist")
401
+
402
+ forbidden = ["flask", "fastapi", "django", "sqlalchemy", "requests"]
403
+ violations: List[str] = []
404
+
405
+ # Check domain directory
406
+ domain_dir = PYTHON_COMMONS / "domain"
407
+ if domain_dir.exists():
408
+ for py_file in domain_dir.rglob("*.py"):
409
+ _check_python_file_for_framework_imports(py_file, forbidden, violations)
410
+
411
+ # Check validation.py (also domain layer)
412
+ validation_file = PYTHON_COMMONS / "validation.py"
413
+ if validation_file.exists():
414
+ _check_python_file_for_framework_imports(validation_file, forbidden, violations)
415
+
416
+ if violations:
417
+ pytest.fail(
418
+ f"\n\nPython domain layer should be framework-agnostic:\n" +
419
+ "\n".join(violations)
420
+ )
421
+
422
+
423
+ def _check_python_file_for_framework_imports(
424
+ file_path: Path, forbidden: List[str], violations: List[str]
425
+ ) -> None:
426
+ """Check a Python file for framework imports."""
427
+ try:
428
+ content = file_path.read_text()
429
+ except Exception:
430
+ return
431
+
432
+ for forbidden_import in forbidden:
433
+ patterns = [
434
+ rf"^import\s+{forbidden_import}",
435
+ rf"^from\s+{forbidden_import}",
436
+ ]
437
+ for pattern in patterns:
438
+ if re.search(pattern, content, re.MULTILINE):
439
+ rel_path = file_path.relative_to(REPO_ROOT)
440
+ violations.append(f" - {rel_path}: imports {forbidden_import}")
441
+ break
442
+
443
+
444
+ # ============================================================================
445
+ # STRUCTURAL PATTERN DOCUMENTATION
446
+ # ============================================================================
447
+
448
+
449
+ @pytest.mark.coder
450
+ def test_structural_patterns_documented():
451
+ """
452
+ SPEC-CODER-COMMONS-0012: Structural patterns are documented in convention.
453
+
454
+ GIVEN: commons.convention.yaml
455
+ WHEN: Checking for structure_patterns section
456
+ THEN: Both python (feature-first) and frontend (layer-first) patterns documented
457
+
458
+ Validates: Convention documents intentional divergence
459
+ """
460
+ convention_file = REPO_ROOT / "atdd" / "coder" / "conventions" / "commons.convention.yaml"
461
+
462
+ if not convention_file.exists():
463
+ pytest.fail(
464
+ f"\n\nMissing convention file:\n"
465
+ f" - {convention_file.relative_to(REPO_ROOT)}"
466
+ )
467
+
468
+ content = convention_file.read_text()
469
+
470
+ required_sections = [
471
+ "structure_patterns:",
472
+ "feature-first",
473
+ "layer-first",
474
+ "divergence_rationale:",
475
+ ]
476
+
477
+ missing = [s for s in required_sections if s not in content]
478
+
479
+ if missing:
480
+ pytest.fail(
481
+ f"\n\nConvention file missing structural pattern documentation:\n" +
482
+ "\n".join(f" - {s}" for s in missing) +
483
+ f"\n\nThe intentional divergence between Python (feature-first) and\n"
484
+ f"Frontend (layer-first) should be documented in commons.convention.yaml"
485
+ )