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 CHANGED
@@ -110,16 +110,60 @@ class ATDDCoach:
110
110
  parallel=True
111
111
  )
112
112
 
113
- def update_registries(self, registry_type: str = "all") -> int:
114
- """Update registries from source files."""
115
- if registry_type == "wagons":
116
- self.registry_updater.update_wagon_registry()
117
- elif registry_type == "contracts":
118
- self.registry_updater.update_contract_registry()
119
- elif registry_type == "telemetry":
120
- self.registry_updater.update_telemetry_registry()
121
- else: # all
122
- self.registry_updater.update_all()
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(registry_type=args.type)
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
@@ -75,11 +75,32 @@ class RepositoryInventory:
75
75
  }
76
76
 
77
77
  def scan_trains(self) -> Dict[str, Any]:
78
- """Scan plan/ for train manifests (aggregations of wagons)."""
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 {"total": 0, "trains": []}
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]: