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.
- atdd/__init__.py +6 -0
- atdd/__main__.py +4 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- 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'])
|