atdd 0.4.5__py3-none-any.whl → 0.4.7__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/cli.py +71 -12
- atdd/coach/commands/inventory.py +91 -3
- atdd/coach/commands/registry.py +475 -202
- atdd/coach/utils/config.py +131 -0
- atdd/coach/utils/train_spec_phase.py +97 -0
- atdd/coach/validators/shared_fixtures.py +68 -1
- atdd/coach/validators/test_train_registry.py +189 -0
- atdd/coder/validators/test_train_infrastructure.py +236 -2
- atdd/planner/schemas/train.schema.json +125 -2
- atdd/planner/validators/test_train_validation.py +667 -2
- atdd/tester/validators/test_train_backend_e2e.py +371 -0
- atdd/tester/validators/test_train_frontend_e2e.py +292 -0
- atdd/tester/validators/test_train_frontend_python.py +282 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/METADATA +1 -1
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/RECORD +19 -13
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/WHEEL +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/entry_points.txt +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/licenses/LICENSE +0 -0
- {atdd-0.4.5.dist-info → atdd-0.4.7.dist-info}/top_level.txt +0 -0
atdd/cli.py
CHANGED
|
@@ -110,16 +110,60 @@ class ATDDCoach:
|
|
|
110
110
|
parallel=True
|
|
111
111
|
)
|
|
112
112
|
|
|
113
|
-
def update_registries(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
def update_registries(
|
|
114
|
+
self,
|
|
115
|
+
registry_type: str = "all",
|
|
116
|
+
apply: bool = False,
|
|
117
|
+
check: bool = False
|
|
118
|
+
) -> int:
|
|
119
|
+
"""Update registries from source files.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
registry_type: Which registry to update (all, wagons, trains, contracts, etc.)
|
|
123
|
+
apply: If True, apply changes without prompting (CI mode)
|
|
124
|
+
check: If True, only check for drift without applying (exit 1 if drift)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
0 on success, 1 if --check and drift detected
|
|
128
|
+
"""
|
|
129
|
+
# Convert flags to mode string
|
|
130
|
+
if check:
|
|
131
|
+
mode = "check"
|
|
132
|
+
elif apply:
|
|
133
|
+
mode = "apply"
|
|
134
|
+
else:
|
|
135
|
+
mode = "interactive"
|
|
136
|
+
|
|
137
|
+
# Registry type handlers
|
|
138
|
+
handlers = {
|
|
139
|
+
"wagons": self.registry_updater.update_wagon_registry,
|
|
140
|
+
"trains": self.registry_updater.build_trains,
|
|
141
|
+
"contracts": self.registry_updater.update_contract_registry,
|
|
142
|
+
"telemetry": self.registry_updater.update_telemetry_registry,
|
|
143
|
+
"tester": self.registry_updater.build_tester,
|
|
144
|
+
"coder": self.registry_updater.build_coder,
|
|
145
|
+
"supabase": self.registry_updater.build_supabase,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if registry_type == "all":
|
|
149
|
+
result = self.registry_updater.build_all(mode=mode)
|
|
150
|
+
# In check mode, return 1 if any registry has changes
|
|
151
|
+
if check:
|
|
152
|
+
has_changes = any(
|
|
153
|
+
r.get("has_changes", False) or r.get("new", 0) > 0 or len(r.get("changes", [])) > 0
|
|
154
|
+
for r in result.values()
|
|
155
|
+
)
|
|
156
|
+
return 1 if has_changes else 0
|
|
157
|
+
elif registry_type in handlers:
|
|
158
|
+
result = handlers[registry_type](mode=mode)
|
|
159
|
+
# In check mode, return 1 if this registry has changes
|
|
160
|
+
if check:
|
|
161
|
+
has_changes = result.get("has_changes", False) or result.get("new", 0) > 0 or len(result.get("changes", [])) > 0
|
|
162
|
+
return 1 if has_changes else 0
|
|
163
|
+
else:
|
|
164
|
+
print(f"Unknown registry type: {registry_type}")
|
|
165
|
+
return 1
|
|
166
|
+
|
|
123
167
|
return 0
|
|
124
168
|
|
|
125
169
|
def show_status(self) -> int:
|
|
@@ -285,9 +329,20 @@ Phase descriptions:
|
|
|
285
329
|
nargs="?",
|
|
286
330
|
type=str,
|
|
287
331
|
default="all",
|
|
288
|
-
choices=["all", "wagons", "contracts", "telemetry"],
|
|
332
|
+
choices=["all", "wagons", "trains", "contracts", "telemetry", "tester", "coder", "supabase"],
|
|
289
333
|
help="Registry type to update (default: all)"
|
|
290
334
|
)
|
|
335
|
+
registry_update_parser.add_argument(
|
|
336
|
+
"--yes", "--apply",
|
|
337
|
+
action="store_true",
|
|
338
|
+
dest="apply",
|
|
339
|
+
help="Apply changes without prompting (for CI/automation)"
|
|
340
|
+
)
|
|
341
|
+
registry_update_parser.add_argument(
|
|
342
|
+
"--check",
|
|
343
|
+
action="store_true",
|
|
344
|
+
help="Check for drift without applying (exit 1 if changes detected)"
|
|
345
|
+
)
|
|
291
346
|
|
|
292
347
|
# ----- atdd init -----
|
|
293
348
|
init_parser = subparsers.add_parser(
|
|
@@ -497,7 +552,11 @@ Phase descriptions:
|
|
|
497
552
|
coach = ATDDCoach(repo_root=repo_path)
|
|
498
553
|
|
|
499
554
|
if args.registry_command == "update":
|
|
500
|
-
return coach.update_registries(
|
|
555
|
+
return coach.update_registries(
|
|
556
|
+
registry_type=args.type,
|
|
557
|
+
apply=args.apply,
|
|
558
|
+
check=args.check
|
|
559
|
+
)
|
|
501
560
|
else:
|
|
502
561
|
registry_parser.print_help()
|
|
503
562
|
return 0
|
atdd/coach/commands/inventory.py
CHANGED
|
@@ -75,11 +75,32 @@ class RepositoryInventory:
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
def scan_trains(self) -> Dict[str, Any]:
|
|
78
|
-
"""
|
|
78
|
+
"""
|
|
79
|
+
Scan plan/ for train manifests (aggregations of wagons).
|
|
80
|
+
|
|
81
|
+
Train First-Class Spec v0.6 Section 14: Gap Reporting
|
|
82
|
+
Reports missing test/code for each platform (backend/frontend/frontend_python).
|
|
83
|
+
"""
|
|
79
84
|
plan_dir = self.repo_root / "plan"
|
|
80
85
|
|
|
81
86
|
if not plan_dir.exists():
|
|
82
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
"total": 0,
|
|
89
|
+
"trains": [],
|
|
90
|
+
"by_theme": {},
|
|
91
|
+
"train_ids": [],
|
|
92
|
+
"detail_files": 0,
|
|
93
|
+
"missing_test_backend": [],
|
|
94
|
+
"missing_test_frontend": [],
|
|
95
|
+
"missing_test_frontend_python": [],
|
|
96
|
+
"missing_code_backend": [],
|
|
97
|
+
"missing_code_frontend": [],
|
|
98
|
+
"missing_code_frontend_python": [],
|
|
99
|
+
"gaps": {
|
|
100
|
+
"test": {"backend": 0, "frontend": 0, "frontend_python": 0},
|
|
101
|
+
"code": {"backend": 0, "frontend": 0, "frontend_python": 0}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
83
104
|
|
|
84
105
|
# Load trains registry
|
|
85
106
|
trains_file = plan_dir / "_trains.yaml"
|
|
@@ -103,6 +124,14 @@ class RepositoryInventory:
|
|
|
103
124
|
by_theme = defaultdict(int)
|
|
104
125
|
train_ids = []
|
|
105
126
|
|
|
127
|
+
# Gap tracking (Section 14)
|
|
128
|
+
missing_test_backend = []
|
|
129
|
+
missing_test_frontend = []
|
|
130
|
+
missing_test_frontend_python = []
|
|
131
|
+
missing_code_backend = []
|
|
132
|
+
missing_code_frontend = []
|
|
133
|
+
missing_code_frontend_python = []
|
|
134
|
+
|
|
106
135
|
for train in all_trains:
|
|
107
136
|
train_id = train.get("train_id", "unknown")
|
|
108
137
|
train_ids.append(train_id)
|
|
@@ -118,6 +147,46 @@ class RepositoryInventory:
|
|
|
118
147
|
theme = theme_map.get(theme_digit, "unknown")
|
|
119
148
|
by_theme[theme] += 1
|
|
120
149
|
|
|
150
|
+
# Gap analysis
|
|
151
|
+
expectations = train.get("expectations", {})
|
|
152
|
+
test_fields = train.get("test", {})
|
|
153
|
+
code_fields = train.get("code", {})
|
|
154
|
+
|
|
155
|
+
# Normalize test/code to dict form
|
|
156
|
+
if isinstance(test_fields, str):
|
|
157
|
+
test_fields = {"backend": [test_fields]}
|
|
158
|
+
elif isinstance(test_fields, list):
|
|
159
|
+
test_fields = {"backend": test_fields}
|
|
160
|
+
|
|
161
|
+
if isinstance(code_fields, str):
|
|
162
|
+
code_fields = {"backend": [code_fields]}
|
|
163
|
+
elif isinstance(code_fields, list):
|
|
164
|
+
code_fields = {"backend": code_fields}
|
|
165
|
+
|
|
166
|
+
# Check backend gaps (default expectation is True for backend)
|
|
167
|
+
expects_backend = expectations.get("backend", True)
|
|
168
|
+
if expects_backend:
|
|
169
|
+
if not test_fields.get("backend"):
|
|
170
|
+
missing_test_backend.append(train_id)
|
|
171
|
+
if not code_fields.get("backend"):
|
|
172
|
+
missing_code_backend.append(train_id)
|
|
173
|
+
|
|
174
|
+
# Check frontend gaps
|
|
175
|
+
expects_frontend = expectations.get("frontend", False)
|
|
176
|
+
if expects_frontend:
|
|
177
|
+
if not test_fields.get("frontend"):
|
|
178
|
+
missing_test_frontend.append(train_id)
|
|
179
|
+
if not code_fields.get("frontend"):
|
|
180
|
+
missing_code_frontend.append(train_id)
|
|
181
|
+
|
|
182
|
+
# Check frontend_python gaps
|
|
183
|
+
expects_frontend_python = expectations.get("frontend_python", False)
|
|
184
|
+
if expects_frontend_python:
|
|
185
|
+
if not test_fields.get("frontend_python"):
|
|
186
|
+
missing_test_frontend_python.append(train_id)
|
|
187
|
+
if not code_fields.get("frontend_python"):
|
|
188
|
+
missing_code_frontend_python.append(train_id)
|
|
189
|
+
|
|
121
190
|
# Find train detail files
|
|
122
191
|
train_detail_files = list((plan_dir / "_trains").glob("*.yaml")) if (plan_dir / "_trains").exists() else []
|
|
123
192
|
|
|
@@ -125,7 +194,26 @@ class RepositoryInventory:
|
|
|
125
194
|
"total": len(all_trains),
|
|
126
195
|
"by_theme": dict(by_theme),
|
|
127
196
|
"train_ids": train_ids,
|
|
128
|
-
"detail_files": len(train_detail_files)
|
|
197
|
+
"detail_files": len(train_detail_files),
|
|
198
|
+
# Gap reporting (Section 14)
|
|
199
|
+
"missing_test_backend": missing_test_backend,
|
|
200
|
+
"missing_test_frontend": missing_test_frontend,
|
|
201
|
+
"missing_test_frontend_python": missing_test_frontend_python,
|
|
202
|
+
"missing_code_backend": missing_code_backend,
|
|
203
|
+
"missing_code_frontend": missing_code_frontend,
|
|
204
|
+
"missing_code_frontend_python": missing_code_frontend_python,
|
|
205
|
+
"gaps": {
|
|
206
|
+
"test": {
|
|
207
|
+
"backend": len(missing_test_backend),
|
|
208
|
+
"frontend": len(missing_test_frontend),
|
|
209
|
+
"frontend_python": len(missing_test_frontend_python)
|
|
210
|
+
},
|
|
211
|
+
"code": {
|
|
212
|
+
"backend": len(missing_code_backend),
|
|
213
|
+
"frontend": len(missing_code_frontend),
|
|
214
|
+
"frontend_python": len(missing_code_frontend_python)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
129
217
|
}
|
|
130
218
|
|
|
131
219
|
def scan_wagons(self) -> Dict[str, Any]:
|