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,413 @@
1
+ """
2
+ Test design system compliance for Preact frontend.
3
+
4
+ Validates:
5
+ - Presentation components use design system primitives (maintain-ux)
6
+ - No raw CSS values bypass design tokens
7
+ - No orphaned design system exports (unused primitives)
8
+
9
+ Location: web/src/
10
+ Design System: web/src/maintain-ux/
11
+ """
12
+
13
+ import pytest
14
+ import re
15
+ from pathlib import Path
16
+ from typing import List, Set, Dict, Tuple
17
+
18
+
19
+ # Path constants
20
+ REPO_ROOT = Path(__file__).resolve().parents[4]
21
+ WEB_SRC = REPO_ROOT / "web" / "src"
22
+ MAINTAIN_UX = WEB_SRC / "maintain-ux"
23
+ PRIMITIVES_DIR = MAINTAIN_UX / "primitives"
24
+ COMPONENTS_DIR = MAINTAIN_UX / "components"
25
+ FOUNDATIONS_DIR = MAINTAIN_UX / "foundations"
26
+
27
+
28
+ # Allowed design system import paths
29
+ DESIGN_SYSTEM_IMPORTS = [
30
+ "@/maintain-ux/primitives",
31
+ "@/maintain-ux/components",
32
+ "@/maintain-ux/foundations",
33
+ "@maintain-ux/primitives",
34
+ "@maintain-ux/components",
35
+ "@maintain-ux/foundations",
36
+ "../primitives",
37
+ "../components",
38
+ "../foundations",
39
+ "./primitives",
40
+ "./components",
41
+ "./foundations",
42
+ ]
43
+
44
+
45
+ def get_presentation_files() -> List[Path]:
46
+ """Find all presentation layer TypeScript files"""
47
+ if not WEB_SRC.exists():
48
+ return []
49
+
50
+ files = []
51
+ for f in WEB_SRC.rglob("*.tsx"):
52
+ # Skip test files
53
+ if ".test." in f.name or "/tests/" in str(f):
54
+ continue
55
+ # Skip design system internal files
56
+ if "/maintain-ux/" in str(f):
57
+ continue
58
+ # Only presentation layer
59
+ if "/presentation/" in str(f):
60
+ files.append(f)
61
+
62
+ return files
63
+
64
+
65
+ def get_all_ui_files() -> List[Path]:
66
+ """Find all UI component files (presentation + pages)"""
67
+ if not WEB_SRC.exists():
68
+ return []
69
+
70
+ files = []
71
+ for f in WEB_SRC.rglob("*.tsx"):
72
+ # Skip test files
73
+ if ".test." in f.name or "/tests/" in str(f):
74
+ continue
75
+ # Skip design system internal files
76
+ if "/maintain-ux/" in str(f):
77
+ continue
78
+ files.append(f)
79
+
80
+ return files
81
+
82
+
83
+ def extract_imports(file_path: Path) -> List[str]:
84
+ """Extract import statements from TypeScript file"""
85
+ try:
86
+ content = file_path.read_text(encoding='utf-8')
87
+ except Exception:
88
+ return []
89
+
90
+ import_pattern = r"import\s+.+\s+from\s+['\"](.+)['\"]"
91
+ return re.findall(import_pattern, content)
92
+
93
+
94
+ def extract_imported_names(file_path: Path) -> List[Tuple[str, str]]:
95
+ """Extract imported names and their source paths"""
96
+ try:
97
+ content = file_path.read_text(encoding='utf-8')
98
+ except Exception:
99
+ return []
100
+
101
+ results = []
102
+
103
+ # Match: import { X, Y } from 'path'
104
+ pattern = r"import\s+\{([^}]+)\}\s+from\s+['\"]([^'\"]+)['\"]"
105
+ for match in re.finditer(pattern, content):
106
+ names = [n.strip().split(' as ')[0] for n in match.group(1).split(',')]
107
+ path = match.group(2)
108
+ for name in names:
109
+ if name:
110
+ results.append((name.strip(), path))
111
+
112
+ # Match: import X from 'path'
113
+ pattern2 = r"import\s+(\w+)\s+from\s+['\"]([^'\"]+)['\"]"
114
+ for match in re.finditer(pattern2, content):
115
+ name = match.group(1)
116
+ path = match.group(2)
117
+ if name not in ['type', 'React', 'h']:
118
+ results.append((name, path))
119
+
120
+ return results
121
+
122
+
123
+ def get_design_system_exports() -> Dict[str, Set[str]]:
124
+ """Get all exported names from design system"""
125
+ exports = {
126
+ 'primitives': set(),
127
+ 'components': set(),
128
+ 'foundations': set(),
129
+ }
130
+
131
+ # Check primitives index
132
+ primitives_index = PRIMITIVES_DIR / "index.ts"
133
+ if primitives_index.exists():
134
+ content = primitives_index.read_text(encoding='utf-8')
135
+ # Match: export { X, Y } from './Z'
136
+ for match in re.finditer(r"export\s+\{([^}]+)\}", content):
137
+ names = [n.strip().split(' as ')[-1] for n in match.group(1).split(',')]
138
+ exports['primitives'].update(n.strip() for n in names if n.strip())
139
+
140
+ # Also check display/index.ts
141
+ display_index = PRIMITIVES_DIR / "display" / "index.ts"
142
+ if display_index.exists():
143
+ content = display_index.read_text(encoding='utf-8')
144
+ for match in re.finditer(r"export\s+\{([^}]+)\}", content):
145
+ names = [n.strip().split(' as ')[-1] for n in match.group(1).split(',')]
146
+ exports['primitives'].update(n.strip() for n in names if n.strip())
147
+
148
+ # Check components index
149
+ components_index = COMPONENTS_DIR / "index.ts"
150
+ if components_index.exists():
151
+ content = components_index.read_text(encoding='utf-8')
152
+ for match in re.finditer(r"export\s+\{([^}]+)\}", content):
153
+ names = [n.strip().split(' as ')[-1] for n in match.group(1).split(',')]
154
+ exports['components'].update(n.strip() for n in names if n.strip())
155
+
156
+ # Check foundations index
157
+ foundations_index = FOUNDATIONS_DIR / "index.ts"
158
+ if foundations_index.exists():
159
+ content = foundations_index.read_text(encoding='utf-8')
160
+ for match in re.finditer(r"export\s+\{([^}]+)\}", content):
161
+ names = [n.strip().split(' as ')[-1] for n in match.group(1).split(',')]
162
+ exports['foundations'].update(n.strip() for n in names if n.strip())
163
+ # Also match: export * from './X'
164
+ for match in re.finditer(r"export\s+\*\s+from\s+['\"]\.\/(\w+)['\"]", content):
165
+ submodule = match.group(1)
166
+ subfile = FOUNDATIONS_DIR / f"{submodule}.ts"
167
+ if subfile.exists():
168
+ subcontent = subfile.read_text(encoding='utf-8')
169
+ for submatch in re.finditer(r"export\s+(?:const|function|class)\s+(\w+)", subcontent):
170
+ exports['foundations'].add(submatch.group(1))
171
+
172
+ # Filter out type exports (Props interfaces)
173
+ for key in exports:
174
+ exports[key] = {e for e in exports[key] if not e.endswith('Props')}
175
+
176
+ return exports
177
+
178
+
179
+ def find_design_system_usage() -> Set[str]:
180
+ """Find all design system imports used across the codebase"""
181
+ used = set()
182
+
183
+ for f in WEB_SRC.rglob("*.ts"):
184
+ if "/maintain-ux/" in str(f):
185
+ continue
186
+ imports = extract_imported_names(f)
187
+ for name, path in imports:
188
+ if any(ds in path for ds in ['maintain-ux', '@maintain-ux']):
189
+ used.add(name)
190
+
191
+ for f in WEB_SRC.rglob("*.tsx"):
192
+ if "/maintain-ux/" in str(f):
193
+ continue
194
+ imports = extract_imported_names(f)
195
+ for name, path in imports:
196
+ if any(ds in path for ds in ['maintain-ux', '@maintain-ux']):
197
+ used.add(name)
198
+
199
+ return used
200
+
201
+
202
+ def extract_raw_color_values(file_path: Path) -> List[Tuple[int, str]]:
203
+ """Find raw hex/rgb color values not from design tokens"""
204
+ try:
205
+ content = file_path.read_text(encoding='utf-8')
206
+ except Exception:
207
+ return []
208
+
209
+ violations = []
210
+ lines = content.split('\n')
211
+
212
+ for i, line in enumerate(lines, 1):
213
+ # Skip imports and comments
214
+ if line.strip().startswith('import') or line.strip().startswith('//'):
215
+ continue
216
+ # Skip if it's referencing colors token
217
+ if 'colors.' in line or 'colors[' in line:
218
+ continue
219
+
220
+ # Find hex colors (but allow #fff, #000 as they're common)
221
+ hex_matches = re.findall(r'#[0-9a-fA-F]{6}\b', line)
222
+ for match in hex_matches:
223
+ # Allow white/black/common grays
224
+ if match.lower() not in ['#ffffff', '#000000', '#1a1a1a', '#fff', '#000']:
225
+ violations.append((i, f"Raw hex color: {match}"))
226
+
227
+ # Find rgb/rgba colors (skip if in design token definition)
228
+ if 'rgba(' in line.lower() and 'colors' not in line:
229
+ violations.append((i, "Raw rgba() color"))
230
+
231
+ return violations
232
+
233
+
234
+ @pytest.mark.coder
235
+ def test_presentation_uses_design_system_primitives():
236
+ """
237
+ SPEC-CODER-DESIGN-001: Presentation layer must use design system primitives.
238
+
239
+ GIVEN: TypeScript file in presentation layer
240
+ WHEN: Analyzing imports for UI elements
241
+ THEN: Uses primitives from @/maintain-ux/primitives or @/maintain-ux/components
242
+
243
+ Rationale: Consistent UI through reusable design system components
244
+ """
245
+ violations = []
246
+
247
+ for f in get_presentation_files():
248
+ imports = extract_imports(f)
249
+
250
+ # Check if file uses preact/h but doesn't import from design system
251
+ has_jsx = f.suffix == '.tsx'
252
+ has_design_system_import = any(
253
+ any(ds in imp for ds in DESIGN_SYSTEM_IMPORTS)
254
+ for imp in imports
255
+ )
256
+
257
+ # If it's a .tsx file with no design system imports, flag it
258
+ # (Allow commons imports for utilities)
259
+ if has_jsx and not has_design_system_import:
260
+ # Check if it has any actual JSX
261
+ try:
262
+ content = f.read_text(encoding='utf-8')
263
+ # Look for JSX return statements
264
+ if re.search(r'return\s*\(?\s*<', content):
265
+ rel_path = f.relative_to(REPO_ROOT)
266
+ violations.append(
267
+ f"{rel_path}\n"
268
+ f" Issue: Presentation component with JSX but no design system imports\n"
269
+ f" Fix: Import primitives from @/maintain-ux/primitives or @/maintain-ux/components"
270
+ )
271
+ except Exception:
272
+ pass
273
+
274
+ if violations:
275
+ pytest.fail(
276
+ f"\n\nFound {len(violations)} presentation files without design system imports:\n\n" +
277
+ "\n\n".join(violations[:10]) +
278
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
279
+ "\n\nPresentation layer should use design system primitives for consistency."
280
+ )
281
+
282
+
283
+ @pytest.mark.coder
284
+ def test_ui_files_use_design_tokens_for_colors():
285
+ """
286
+ SPEC-CODER-DESIGN-002: UI files should use design tokens for colors.
287
+
288
+ GIVEN: TypeScript/TSX file with styling
289
+ WHEN: Analyzing for color values
290
+ THEN: Colors come from design tokens, not raw hex/rgb values
291
+
292
+ Rationale: Consistent theming through centralized color definitions
293
+ """
294
+ all_violations = []
295
+
296
+ for f in get_all_ui_files():
297
+ violations = extract_raw_color_values(f)
298
+ if violations:
299
+ rel_path = f.relative_to(REPO_ROOT)
300
+ for line_num, issue in violations[:3]: # Max 3 per file
301
+ all_violations.append(
302
+ f"{rel_path}:{line_num}\n"
303
+ f" {issue}\n"
304
+ f" Fix: Use colors from @/maintain-ux/foundations"
305
+ )
306
+
307
+ # Allow some violations during migration (warning, not failure)
308
+ if len(all_violations) > 20:
309
+ pytest.fail(
310
+ f"\n\nFound {len(all_violations)} raw color values (>20 threshold):\n\n" +
311
+ "\n\n".join(all_violations[:10]) +
312
+ (f"\n\n... and {len(all_violations) - 10} more" if len(all_violations) > 10 else "") +
313
+ "\n\nUse colors from @/maintain-ux/foundations for consistency."
314
+ )
315
+
316
+
317
+ @pytest.mark.coder
318
+ def test_no_orphaned_design_system_exports():
319
+ """
320
+ SPEC-CODER-DESIGN-003: Design system exports should be used.
321
+
322
+ GIVEN: Exports from maintain-ux/primitives and maintain-ux/components
323
+ WHEN: Scanning codebase for imports
324
+ THEN: All exports are imported somewhere (no orphaned code)
325
+
326
+ Rationale: Remove dead code, keep design system lean
327
+ """
328
+ exports = get_design_system_exports()
329
+ used = find_design_system_usage()
330
+
331
+ # Combine all exports
332
+ all_exports = exports['primitives'] | exports['components']
333
+
334
+ # Find orphaned (exported but never imported)
335
+ orphaned = all_exports - used
336
+
337
+ # Filter out common false positives
338
+ false_positives = {'type', 'h', 'Fragment'}
339
+ orphaned = orphaned - false_positives
340
+
341
+ if orphaned:
342
+ # Group by category
343
+ orphaned_primitives = orphaned & exports['primitives']
344
+ orphaned_components = orphaned & exports['components']
345
+
346
+ message = f"\n\nFound {len(orphaned)} orphaned design system exports:\n"
347
+
348
+ if orphaned_primitives:
349
+ message += f"\n Primitives ({len(orphaned_primitives)}):\n"
350
+ message += "".join(f" - {name}\n" for name in sorted(orphaned_primitives))
351
+
352
+ if orphaned_components:
353
+ message += f"\n Components ({len(orphaned_components)}):\n"
354
+ message += "".join(f" - {name}\n" for name in sorted(orphaned_components))
355
+
356
+ message += "\nConsider removing unused exports to keep design system lean."
357
+
358
+ # Warn but don't fail if under threshold
359
+ if len(orphaned) > 5:
360
+ pytest.fail(message)
361
+ else:
362
+ pytest.skip(f"Minor: {len(orphaned)} orphaned exports (under threshold)")
363
+
364
+
365
+ @pytest.mark.coder
366
+ def test_design_system_uses_foundations():
367
+ """
368
+ SPEC-CODER-DESIGN-004: Design system primitives should use foundations.
369
+
370
+ GIVEN: Primitive or component in maintain-ux
371
+ WHEN: Checking for spacing/color values
372
+ THEN: Uses tokens from foundations (spacing, colors)
373
+
374
+ Rationale: Design system itself must be consistent
375
+ """
376
+ violations = []
377
+
378
+ for category_dir in [PRIMITIVES_DIR, COMPONENTS_DIR]:
379
+ if not category_dir.exists():
380
+ continue
381
+
382
+ for f in category_dir.rglob("*.tsx"):
383
+ # Skip index files
384
+ if f.name == "index.ts":
385
+ continue
386
+
387
+ try:
388
+ content = f.read_text(encoding='utf-8')
389
+ except Exception:
390
+ continue
391
+
392
+ # Check if it imports from foundations
393
+ imports = extract_imports(f)
394
+ uses_foundations = any('../foundations' in imp or './foundations' in imp for imp in imports)
395
+
396
+ # Check for raw pixel values in styles (allow small values like 2px, 3px for borders)
397
+ raw_pixels = re.findall(r":\s*['\"]?(\d{2,}px)['\"]?", content)
398
+
399
+ if raw_pixels and not uses_foundations:
400
+ rel_path = f.relative_to(REPO_ROOT)
401
+ violations.append(
402
+ f"{rel_path}\n"
403
+ f" Raw pixel values: {', '.join(raw_pixels[:5])}\n"
404
+ f" Fix: Import spacing from ../foundations"
405
+ )
406
+
407
+ if violations:
408
+ pytest.fail(
409
+ f"\n\nFound {len(violations)} design system files with raw values:\n\n" +
410
+ "\n\n".join(violations[:10]) +
411
+ (f"\n\n... and {len(violations) - 10} more" if len(violations) > 10 else "") +
412
+ "\n\nDesign system should use its own foundations for consistency."
413
+ )
@@ -0,0 +1,268 @@
1
+ """
2
+ Test DTO testing patterns enforcement.
3
+
4
+ Validates conventions from:
5
+ - atdd/coder/conventions/dto.convention.yaml (lines 567-599)
6
+
7
+ Enforces:
8
+ - Integration tests MUST use ID comparison (not object identity) when asserting DTO→Entity conversions
9
+ - Pattern: assert entity.id in {dto.id for dto in dtos} ✅
10
+ - Antipattern: assert entity in dtos ❌
11
+
12
+ Rationale:
13
+ After DTO→Entity conversion via mapper, object identity fails because:
14
+ - Mapper creates new entity instances
15
+ - DTO and Entity are different types/instances
16
+ - Python 'in' operator uses __eq__ or identity
17
+ - IDs are stable across DTO/Entity boundary per contract
18
+
19
+ This pattern was discovered fixing 18 integration tests in pace-dilemmas.
20
+ All failures were caused by incorrect "assert entity in dto_list" assertions.
21
+ """
22
+
23
+ import pytest
24
+ import ast
25
+ from pathlib import Path
26
+ from typing import List, Tuple, Set
27
+
28
+
29
+ # Path constants
30
+ REPO_ROOT = Path(__file__).resolve().parents[4]
31
+ PYTHON_DIR = REPO_ROOT / "python"
32
+ DTO_CONVENTION = REPO_ROOT / "atdd" / "coder" / "conventions" / "dto.convention.yaml"
33
+
34
+
35
+ def find_integration_test_files() -> List[Path]:
36
+ """
37
+ Find all integration test files in wagons.
38
+
39
+ Integration tests are the primary location for DTO→Entity boundary testing.
40
+ Unit tests typically work within a single layer.
41
+ """
42
+ if not PYTHON_DIR.exists():
43
+ return []
44
+
45
+ integration_tests = []
46
+ for test_file in PYTHON_DIR.rglob("test_*.py"):
47
+ # Skip __pycache__
48
+ if '__pycache__' in str(test_file):
49
+ continue
50
+
51
+ # Only include integration test directories
52
+ if '/integration/' in str(test_file):
53
+ integration_tests.append(test_file)
54
+
55
+ return integration_tests
56
+
57
+
58
+ def extract_assert_in_statements(file_path: Path) -> List[Tuple[int, str, str]]:
59
+ """
60
+ Extract "assert X in Y" statements using AST parsing.
61
+
62
+ Returns:
63
+ List of (line_number, left_expr, right_expr) tuples
64
+ """
65
+ try:
66
+ with open(file_path, 'r', encoding='utf-8') as f:
67
+ content = f.read()
68
+ except Exception:
69
+ return []
70
+
71
+ try:
72
+ tree = ast.parse(content)
73
+ except SyntaxError:
74
+ return []
75
+
76
+ assertions = []
77
+
78
+ for node in ast.walk(tree):
79
+ if isinstance(node, ast.Assert):
80
+ # Check if this is "assert X in Y" pattern
81
+ if isinstance(node.test, ast.Compare):
82
+ # node.test.left is the left side of comparison
83
+ # node.test.ops contains comparison operators (e.g., [In()])
84
+ # node.test.comparators contains right sides
85
+
86
+ for op, comparator in zip(node.test.ops, node.test.comparators):
87
+ if isinstance(op, ast.In):
88
+ # Found "assert X in Y"
89
+ left_expr = ast.unparse(node.test.left)
90
+ right_expr = ast.unparse(comparator)
91
+ assertions.append((node.lineno, left_expr, right_expr))
92
+
93
+ return assertions
94
+
95
+
96
+ def looks_like_entity_access(expr: str) -> bool:
97
+ """
98
+ Check if expression looks like entity attribute access.
99
+
100
+ Examples:
101
+ - "dilemma.fragment_a" → True
102
+ - "result.fragment_b" → True
103
+ - "returned_entity" → True (ambiguous, but flag it)
104
+ - "fragments" → False
105
+ - "dto_list" → False
106
+ """
107
+ # Look for attribute access with common entity field names
108
+ entity_patterns = [
109
+ 'fragment_a', 'fragment_b', # Dilemma entities
110
+ 'returned', 'result', 'entity', # Common test variable names
111
+ 'selected', 'choice', 'decision', # Domain-specific
112
+ ]
113
+
114
+ lower_expr = expr.lower()
115
+ return any(pattern in lower_expr for pattern in entity_patterns)
116
+
117
+
118
+ def looks_like_dto_list(expr: str) -> bool:
119
+ """
120
+ Check if expression looks like a list of DTOs.
121
+
122
+ Examples:
123
+ - "fragments" → True
124
+ - "dto_list" → True
125
+ - "available" → True
126
+ - "pool" → True
127
+ - "result.id" → False (not a list)
128
+ """
129
+ # Exclude expressions with .id (those are already using ID comparison)
130
+ if '.id' in expr:
131
+ return False
132
+
133
+ # Common list variable names
134
+ list_patterns = [
135
+ 'fragments', 'dtos', 'list',
136
+ 'available', 'pool', 'choices',
137
+ 'warm_library', 'hot_pool',
138
+ ]
139
+
140
+ lower_expr = expr.lower()
141
+ return any(pattern in lower_expr for pattern in list_patterns)
142
+
143
+
144
+ def is_id_comparison(left_expr: str, right_expr: str) -> bool:
145
+ """
146
+ Check if this is already using ID comparison (correct pattern).
147
+
148
+ Examples of correct patterns:
149
+ - assert entity.id in {dto.id for dto in dtos}
150
+ - assert fragment.id in fragment_ids
151
+ - assert result.id in [f.id for f in fragments]
152
+ """
153
+ # Left side should have .id
154
+ if '.id' not in left_expr:
155
+ return False
156
+
157
+ # Right side should have .id (set comprehension or list comprehension)
158
+ if '.id' in right_expr:
159
+ return True
160
+
161
+ # Right side might be a pre-computed ID set (e.g., fragment_ids)
162
+ if 'id' in right_expr.lower() and ('set' in right_expr.lower() or '_ids' in right_expr.lower()):
163
+ return True
164
+
165
+ return False
166
+
167
+
168
+ class TestDTOTestingPatterns:
169
+ """
170
+ Enforce DTO testing patterns from dto.convention.yaml.
171
+
172
+ Convention: atdd/coder/conventions/dto.convention.yaml lines 567-599
173
+ """
174
+
175
+ def test_integration_tests_use_id_comparison_not_object_identity(self):
176
+ """
177
+ ENFORCE: Integration tests MUST use ID comparison across DTO/Entity boundary
178
+
179
+ Pattern: assert entity.id in {dto.id for dto in dtos} ✅
180
+ Antipattern: assert entity in dtos ❌
181
+
182
+ Convention reference: dto.convention.yaml lines 567-599
183
+
184
+ This test scans all integration test files for "assert X in Y" patterns
185
+ and flags potential violations where:
186
+ - X looks like an entity (e.g., dilemma.fragment_a, returned_entity)
187
+ - Y looks like a DTO list (e.g., fragments, dto_list)
188
+ - Neither X nor Y use .id (meaning it's object identity, not ID comparison)
189
+ """
190
+ integration_tests = find_integration_test_files()
191
+
192
+ if not integration_tests:
193
+ pytest.skip("No integration tests found")
194
+
195
+ violations = []
196
+
197
+ for test_file in integration_tests:
198
+ assertions = extract_assert_in_statements(test_file)
199
+
200
+ for line_num, left_expr, right_expr in assertions:
201
+ # Check if this is already using ID comparison (correct pattern)
202
+ if is_id_comparison(left_expr, right_expr):
203
+ continue # ✅ Already correct
204
+
205
+ # Check if this looks like entity in dto_list (antipattern)
206
+ if looks_like_entity_access(left_expr) and looks_like_dto_list(right_expr):
207
+ violations.append({
208
+ 'file': test_file.relative_to(REPO_ROOT),
209
+ 'line': line_num,
210
+ 'assertion': f"assert {left_expr} in {right_expr}",
211
+ 'suggestion': f"assert {left_expr}.id in {{{right_expr[0]}.id for {right_expr[0]} in {right_expr}}}"
212
+ })
213
+
214
+ # Report violations with helpful message
215
+ if violations:
216
+ error_msg = [
217
+ "\n❌ Found integration tests using object identity instead of ID comparison",
218
+ "\nConvention: dto.convention.yaml lines 567-599",
219
+ "\nPattern: After DTO→Entity conversion, use ID comparison not object identity\n"
220
+ ]
221
+
222
+ for v in violations:
223
+ error_msg.append(f"\n{v['file']}:{v['line']}")
224
+ error_msg.append(f" ❌ Antipattern: {v['assertion']}")
225
+ error_msg.append(f" ✅ Fix: {v['suggestion']}")
226
+
227
+ error_msg.append("\n\nWhy this matters:")
228
+ error_msg.append(" - Mapper creates new entity instances")
229
+ error_msg.append(" - Entity ≠ DTO (different types)")
230
+ error_msg.append(" - Python 'in' uses __eq__ or identity")
231
+ error_msg.append(" - IDs are stable across DTO/Entity boundary")
232
+
233
+ pytest.fail('\n'.join(error_msg))
234
+
235
+ def test_dto_convention_documents_testing_pattern(self):
236
+ """
237
+ META: Verify the DTO convention file documents this testing pattern.
238
+
239
+ Ensures the convention file contains:
240
+ - testing_patterns.dto_entity_boundary_assertions section
241
+ - Antipattern example
242
+ - Correct pattern example
243
+ """
244
+ if not DTO_CONVENTION.exists():
245
+ pytest.skip("DTO convention file not found")
246
+
247
+ content = DTO_CONVENTION.read_text()
248
+
249
+ # Check for key sections
250
+ assert 'testing_patterns' in content, \
251
+ "DTO convention missing 'testing_patterns' section"
252
+
253
+ assert 'dto_entity_boundary_assertions' in content, \
254
+ "DTO convention missing 'dto_entity_boundary_assertions' pattern"
255
+
256
+ assert 'antipattern' in content.lower(), \
257
+ "DTO convention missing antipattern example"
258
+
259
+ assert 'assert returned_entity in dto_list' in content or 'in dto_list' in content, \
260
+ "DTO convention missing antipattern code example"
261
+
262
+ assert 'assert returned_entity.id in' in content or '.id in' in content, \
263
+ "DTO convention missing correct pattern code example"
264
+
265
+
266
+ if __name__ == '__main__':
267
+ # Run with: pytest atdd/coder/test_dto_testing_patterns.py -v
268
+ pytest.main([__file__, '-v'])