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/coach/commands/registry.py
CHANGED
|
@@ -160,6 +160,74 @@ class RegistryBuilder:
|
|
|
160
160
|
self.python_dir = repo_root / "python"
|
|
161
161
|
self.supabase_dir = repo_root / "supabase"
|
|
162
162
|
|
|
163
|
+
# ========================================================================
|
|
164
|
+
# MODE HANDLING - Unified confirmation and apply logic
|
|
165
|
+
# ========================================================================
|
|
166
|
+
# Handles interactive, apply, and check modes for all registries
|
|
167
|
+
# ========================================================================
|
|
168
|
+
|
|
169
|
+
def _confirm_and_apply(
|
|
170
|
+
self,
|
|
171
|
+
mode: str,
|
|
172
|
+
registry_name: str,
|
|
173
|
+
registry_path: Path,
|
|
174
|
+
output_data: Dict[str, Any],
|
|
175
|
+
stats: Dict[str, Any],
|
|
176
|
+
preview_msg: str = ""
|
|
177
|
+
) -> Dict[str, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Handle confirmation and apply based on mode.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
mode: "interactive", "apply", or "check"
|
|
183
|
+
registry_name: Human-readable name for messages (e.g., "wagon", "contract")
|
|
184
|
+
registry_path: Path to the registry file
|
|
185
|
+
output_data: Data to write to the registry
|
|
186
|
+
stats: Statistics dict to update with results
|
|
187
|
+
preview_msg: Optional custom preview message
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Updated stats dict with has_changes flag
|
|
191
|
+
"""
|
|
192
|
+
has_changes = stats.get("new", 0) > 0 or len(stats.get("changes", [])) > 0
|
|
193
|
+
stats["has_changes"] = has_changes
|
|
194
|
+
|
|
195
|
+
if mode == "check":
|
|
196
|
+
if has_changes:
|
|
197
|
+
print(f"\n⚠️ Drift detected in {registry_name} registry")
|
|
198
|
+
else:
|
|
199
|
+
print(f"\n✅ {registry_name.capitalize()} registry is in sync")
|
|
200
|
+
return stats
|
|
201
|
+
|
|
202
|
+
if mode == "apply":
|
|
203
|
+
# Write without prompting
|
|
204
|
+
registry_path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
with open(registry_path, "w") as f:
|
|
206
|
+
yaml.dump(output_data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
207
|
+
|
|
208
|
+
print(f"\n✅ {registry_name.capitalize()} registry updated successfully!")
|
|
209
|
+
print(f" 📝 Registry: {registry_path}")
|
|
210
|
+
return stats
|
|
211
|
+
|
|
212
|
+
# Interactive mode - ask for confirmation
|
|
213
|
+
print(f"\n❓ Do you want to apply these changes to the {registry_name} registry?")
|
|
214
|
+
print(" Type 'yes' to confirm, or anything else to cancel:")
|
|
215
|
+
response = input(" > ").strip().lower()
|
|
216
|
+
|
|
217
|
+
if response != "yes":
|
|
218
|
+
print("\n❌ Update cancelled by user")
|
|
219
|
+
stats["cancelled"] = True
|
|
220
|
+
return stats
|
|
221
|
+
|
|
222
|
+
# Write registry
|
|
223
|
+
registry_path.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
with open(registry_path, "w") as f:
|
|
225
|
+
yaml.dump(output_data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
226
|
+
|
|
227
|
+
print(f"\n✅ {registry_name.capitalize()} registry updated successfully!")
|
|
228
|
+
print(f" 📝 Registry: {registry_path}")
|
|
229
|
+
return stats
|
|
230
|
+
|
|
163
231
|
# ========================================================================
|
|
164
232
|
# DOMAIN LAYER - Pure Business Logic (Change Detection)
|
|
165
233
|
# ========================================================================
|
|
@@ -469,16 +537,20 @@ class RegistryBuilder:
|
|
|
469
537
|
# Reads/writes YAML files, scans directories for source files
|
|
470
538
|
# ========================================================================
|
|
471
539
|
|
|
472
|
-
def update_wagon_registry(self, preview_only: bool =
|
|
540
|
+
def update_wagon_registry(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
473
541
|
"""
|
|
474
542
|
Update plan/_wagons.yaml from wagon manifest files.
|
|
475
543
|
|
|
476
544
|
Args:
|
|
477
|
-
|
|
545
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
546
|
+
preview_only: Deprecated - use mode="check" instead
|
|
478
547
|
|
|
479
548
|
Returns:
|
|
480
|
-
Statistics about the update
|
|
549
|
+
Statistics about the update (includes has_changes flag for check mode)
|
|
481
550
|
"""
|
|
551
|
+
# Backwards compatibility
|
|
552
|
+
if preview_only is not None:
|
|
553
|
+
mode = "check" if preview_only else "interactive"
|
|
482
554
|
print("📊 Analyzing wagon registry from manifest files...")
|
|
483
555
|
|
|
484
556
|
# Load existing registry
|
|
@@ -500,7 +572,7 @@ class RegistryBuilder:
|
|
|
500
572
|
"updated": 0,
|
|
501
573
|
"new": 0,
|
|
502
574
|
"preserved_drafts": 0,
|
|
503
|
-
"changes": []
|
|
575
|
+
"changes": []
|
|
504
576
|
}
|
|
505
577
|
|
|
506
578
|
for manifest_path in sorted(manifest_files):
|
|
@@ -539,7 +611,6 @@ class RegistryBuilder:
|
|
|
539
611
|
# Check if updating or new
|
|
540
612
|
if slug in existing_wagons:
|
|
541
613
|
stats["updated"] += 1
|
|
542
|
-
# Track field-level changes
|
|
543
614
|
changes = self._detect_changes(slug, existing_wagons[slug], entry)
|
|
544
615
|
if changes:
|
|
545
616
|
stats["changes"].append({
|
|
@@ -560,13 +631,17 @@ class RegistryBuilder:
|
|
|
560
631
|
except Exception as e:
|
|
561
632
|
print(f" ❌ Error processing {manifest_path}: {e}")
|
|
562
633
|
|
|
563
|
-
# Preserve draft wagons (those without manifests)
|
|
634
|
+
# Preserve draft wagons (those without manifests or with draft: true)
|
|
564
635
|
preserved_drafts = []
|
|
565
636
|
for slug, wagon in existing_wagons.items():
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
637
|
+
is_draft = wagon.get("draft", False)
|
|
638
|
+
has_no_manifest = not wagon.get("manifest") and not wagon.get("path")
|
|
639
|
+
if is_draft or has_no_manifest:
|
|
640
|
+
# Check if already added from manifest scan
|
|
641
|
+
if slug not in [w.get("wagon") for w in updated_wagons]:
|
|
642
|
+
updated_wagons.append(wagon)
|
|
643
|
+
preserved_drafts.append(slug)
|
|
644
|
+
stats["preserved_drafts"] += 1
|
|
570
645
|
|
|
571
646
|
# Sort by wagon slug
|
|
572
647
|
updated_wagons.sort(key=lambda w: w.get("wagon", ""))
|
|
@@ -580,44 +655,24 @@ class RegistryBuilder:
|
|
|
580
655
|
# Print detailed change report
|
|
581
656
|
self._print_change_report(stats["changes"], preserved_drafts)
|
|
582
657
|
|
|
583
|
-
#
|
|
584
|
-
if preview_only:
|
|
585
|
-
print("\n⚠️ Preview mode - no changes applied")
|
|
586
|
-
return stats
|
|
587
|
-
|
|
588
|
-
# Ask for user approval
|
|
589
|
-
print("\n❓ Do you want to apply these changes to the registry?")
|
|
590
|
-
print(" Type 'yes' to confirm, or anything else to cancel:")
|
|
591
|
-
response = input(" > ").strip().lower()
|
|
592
|
-
|
|
593
|
-
if response != "yes":
|
|
594
|
-
print("\n❌ Update cancelled by user")
|
|
595
|
-
stats["cancelled"] = True
|
|
596
|
-
return stats
|
|
597
|
-
|
|
598
|
-
# Write updated registry
|
|
658
|
+
# Use helper for confirm/apply
|
|
599
659
|
output = {"wagons": updated_wagons}
|
|
600
|
-
|
|
601
|
-
yaml.dump(output, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
602
|
-
|
|
603
|
-
print(f"\n✅ Registry updated successfully!")
|
|
604
|
-
print(f" • Updated {stats['updated']} wagons")
|
|
605
|
-
print(f" • Added {stats['new']} new wagons")
|
|
606
|
-
print(f" • Preserved {stats['preserved_drafts']} draft wagons")
|
|
607
|
-
print(f" 📝 Registry: {registry_path}")
|
|
608
|
-
|
|
609
|
-
return stats
|
|
660
|
+
return self._confirm_and_apply(mode, "wagon", registry_path, output, stats)
|
|
610
661
|
|
|
611
|
-
def update_contract_registry(self, preview_only: bool =
|
|
662
|
+
def update_contract_registry(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
612
663
|
"""
|
|
613
664
|
Update contracts/_artifacts.yaml from contract schema files.
|
|
614
665
|
|
|
615
666
|
Args:
|
|
616
|
-
|
|
667
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
668
|
+
preview_only: Deprecated - use mode="check" instead
|
|
617
669
|
|
|
618
670
|
Returns:
|
|
619
|
-
Statistics about the update
|
|
671
|
+
Statistics about the update (includes has_changes flag for check mode)
|
|
620
672
|
"""
|
|
673
|
+
# Backwards compatibility
|
|
674
|
+
if preview_only is not None:
|
|
675
|
+
mode = "check" if preview_only else "interactive"
|
|
621
676
|
print("\n📊 Analyzing contract registry from schema files...")
|
|
622
677
|
|
|
623
678
|
# Load existing registry
|
|
@@ -635,6 +690,7 @@ class RegistryBuilder:
|
|
|
635
690
|
"updated": 0,
|
|
636
691
|
"new": 0,
|
|
637
692
|
"errors": 0,
|
|
693
|
+
"preserved_drafts": 0,
|
|
638
694
|
"changes": []
|
|
639
695
|
}
|
|
640
696
|
|
|
@@ -657,7 +713,7 @@ class RegistryBuilder:
|
|
|
657
713
|
# Build artifact entry
|
|
658
714
|
rel_path = str(schema_path.relative_to(self.repo_root))
|
|
659
715
|
|
|
660
|
-
artifact_id = schema_id
|
|
716
|
+
artifact_id = schema_id
|
|
661
717
|
artifact = {
|
|
662
718
|
"id": artifact_id,
|
|
663
719
|
"urn": f"contract:{schema_id}",
|
|
@@ -694,55 +750,44 @@ class RegistryBuilder:
|
|
|
694
750
|
print(f" ⚠️ Error processing {schema_path}: {e}")
|
|
695
751
|
stats["errors"] += 1
|
|
696
752
|
|
|
753
|
+
# Preserve draft artifacts (path doesn't exist or draft: true)
|
|
754
|
+
for artifact_id, artifact in existing_artifacts.items():
|
|
755
|
+
is_draft = artifact.get("draft", False)
|
|
756
|
+
path_exists = artifact.get("path") and (self.repo_root / artifact.get("path")).exists()
|
|
757
|
+
if is_draft or not path_exists:
|
|
758
|
+
if artifact_id not in [a.get("id") for a in artifacts]:
|
|
759
|
+
artifacts.append(artifact)
|
|
760
|
+
stats["preserved_drafts"] += 1
|
|
761
|
+
|
|
697
762
|
# Show preview
|
|
698
763
|
print(f"\n📋 PREVIEW:")
|
|
699
764
|
print(f" • {stats['updated']} artifacts will be updated")
|
|
700
765
|
print(f" • {stats['new']} new artifacts will be added")
|
|
766
|
+
print(f" • {stats['preserved_drafts']} draft artifacts will be preserved")
|
|
701
767
|
if stats["errors"] > 0:
|
|
702
768
|
print(f" ⚠️ {stats['errors']} errors encountered")
|
|
703
769
|
|
|
704
770
|
# Print detailed change report
|
|
705
771
|
self._print_contract_change_report(stats["changes"])
|
|
706
772
|
|
|
707
|
-
#
|
|
708
|
-
if preview_only:
|
|
709
|
-
print("\n⚠️ Preview mode - no changes applied")
|
|
710
|
-
return stats
|
|
711
|
-
|
|
712
|
-
# Ask for user approval
|
|
713
|
-
print("\n❓ Do you want to apply these changes to the contract registry?")
|
|
714
|
-
print(" Type 'yes' to confirm, or anything else to cancel:")
|
|
715
|
-
response = input(" > ").strip().lower()
|
|
716
|
-
|
|
717
|
-
if response != "yes":
|
|
718
|
-
print("\n❌ Update cancelled by user")
|
|
719
|
-
stats["cancelled"] = True
|
|
720
|
-
return stats
|
|
721
|
-
|
|
722
|
-
# Write registry
|
|
723
|
-
registry_path = self.contracts_dir / "_artifacts.yaml"
|
|
773
|
+
# Use helper for confirm/apply
|
|
724
774
|
output = {"artifacts": artifacts}
|
|
775
|
+
return self._confirm_and_apply(mode, "contract", registry_path, output, stats)
|
|
725
776
|
|
|
726
|
-
|
|
727
|
-
yaml.dump(output, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
728
|
-
|
|
729
|
-
print(f"\n✅ Contract registry updated successfully!")
|
|
730
|
-
print(f" • Updated {stats['updated']} artifacts")
|
|
731
|
-
print(f" • Added {stats['new']} new artifacts")
|
|
732
|
-
print(f" 📝 Registry: {registry_path}")
|
|
733
|
-
|
|
734
|
-
return stats
|
|
735
|
-
|
|
736
|
-
def update_telemetry_registry(self, preview_only: bool = False) -> Dict[str, Any]:
|
|
777
|
+
def update_telemetry_registry(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
737
778
|
"""
|
|
738
779
|
Update telemetry/_signals.yaml from telemetry signal files.
|
|
739
780
|
|
|
740
781
|
Args:
|
|
741
|
-
|
|
782
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
783
|
+
preview_only: Deprecated - use mode="check" instead
|
|
742
784
|
|
|
743
785
|
Returns:
|
|
744
|
-
Statistics about the update
|
|
786
|
+
Statistics about the update (includes has_changes flag for check mode)
|
|
745
787
|
"""
|
|
788
|
+
# Backwards compatibility
|
|
789
|
+
if preview_only is not None:
|
|
790
|
+
mode = "check" if preview_only else "interactive"
|
|
746
791
|
print("\n📊 Analyzing telemetry registry from signal files...")
|
|
747
792
|
|
|
748
793
|
# Load existing registry
|
|
@@ -760,6 +805,7 @@ class RegistryBuilder:
|
|
|
760
805
|
"updated": 0,
|
|
761
806
|
"new": 0,
|
|
762
807
|
"errors": 0,
|
|
808
|
+
"preserved_drafts": 0,
|
|
763
809
|
"changes": []
|
|
764
810
|
}
|
|
765
811
|
|
|
@@ -820,63 +866,320 @@ class RegistryBuilder:
|
|
|
820
866
|
print(f" ⚠️ Error processing {signal_path}: {e}")
|
|
821
867
|
stats["errors"] += 1
|
|
822
868
|
|
|
869
|
+
# Preserve draft signals (path doesn't exist or draft: true)
|
|
870
|
+
for signal_id, signal in existing_signals.items():
|
|
871
|
+
is_draft = signal.get("draft", False)
|
|
872
|
+
path_exists = signal.get("path") and (self.repo_root / signal.get("path")).exists()
|
|
873
|
+
if is_draft or not path_exists:
|
|
874
|
+
if signal_id not in [s.get("id") for s in signals]:
|
|
875
|
+
signals.append(signal)
|
|
876
|
+
stats["preserved_drafts"] += 1
|
|
877
|
+
|
|
823
878
|
# Show preview
|
|
824
879
|
print(f"\n📋 PREVIEW:")
|
|
825
880
|
print(f" • {stats['updated']} signals will be updated")
|
|
826
881
|
print(f" • {stats['new']} new signals will be added")
|
|
882
|
+
print(f" • {stats['preserved_drafts']} draft signals will be preserved")
|
|
827
883
|
if stats["errors"] > 0:
|
|
828
884
|
print(f" ⚠️ {stats['errors']} errors encountered")
|
|
829
885
|
|
|
830
886
|
# Print detailed change report
|
|
831
887
|
self._print_telemetry_change_report(stats["changes"])
|
|
832
888
|
|
|
833
|
-
#
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
return stats
|
|
889
|
+
# Use helper for confirm/apply
|
|
890
|
+
output = {"signals": signals}
|
|
891
|
+
return self._confirm_and_apply(mode, "telemetry", registry_path, output, stats)
|
|
837
892
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
893
|
+
# Alias methods for unified API
|
|
894
|
+
def build_planner(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
895
|
+
"""Build planner registry (alias for update_wagon_registry)."""
|
|
896
|
+
# Backwards compatibility: preview_only=True maps to mode="check"
|
|
897
|
+
if preview_only is not None:
|
|
898
|
+
mode = "check" if preview_only else "interactive"
|
|
899
|
+
return self.update_wagon_registry(mode)
|
|
842
900
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
901
|
+
def build_contracts(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
902
|
+
"""Build contracts registry (alias for update_contract_registry)."""
|
|
903
|
+
if preview_only is not None:
|
|
904
|
+
mode = "check" if preview_only else "interactive"
|
|
905
|
+
return self.update_contract_registry(mode)
|
|
906
|
+
|
|
907
|
+
def build_telemetry(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
908
|
+
"""Build telemetry registry (alias for update_telemetry_registry)."""
|
|
909
|
+
if preview_only is not None:
|
|
910
|
+
mode = "check" if preview_only else "interactive"
|
|
911
|
+
return self.update_telemetry_registry(mode)
|
|
912
|
+
|
|
913
|
+
def _normalize_test_code_field(self, field_value: Any) -> Dict[str, List[str]]:
|
|
914
|
+
"""
|
|
915
|
+
Normalize test/code field to canonical structure.
|
|
916
|
+
|
|
917
|
+
Train First-Class Spec v0.6 Section 5: Test/Code Field Typing Normalization
|
|
918
|
+
- string -> {"backend": [string]}
|
|
919
|
+
- list -> {"backend": list}
|
|
920
|
+
- dict -> normalize each sub-field to list
|
|
921
|
+
"""
|
|
922
|
+
if field_value is None:
|
|
923
|
+
return {}
|
|
924
|
+
|
|
925
|
+
if isinstance(field_value, str):
|
|
926
|
+
return {"backend": [field_value]}
|
|
927
|
+
elif isinstance(field_value, list):
|
|
928
|
+
return {"backend": field_value}
|
|
929
|
+
elif isinstance(field_value, dict):
|
|
930
|
+
result = {}
|
|
931
|
+
for key in ["backend", "frontend", "frontend_python"]:
|
|
932
|
+
if key in field_value:
|
|
933
|
+
val = field_value[key]
|
|
934
|
+
result[key] = [val] if isinstance(val, str) else (val or [])
|
|
935
|
+
return result
|
|
936
|
+
return {}
|
|
937
|
+
|
|
938
|
+
def _extract_wagons_from_participants(self, participants: List[str]) -> List[str]:
|
|
939
|
+
"""
|
|
940
|
+
Extract wagon names from participants list.
|
|
941
|
+
|
|
942
|
+
Train First-Class Spec v0.6 Section 4: Participants is Canonical Wagon Source
|
|
943
|
+
"""
|
|
944
|
+
wagons = []
|
|
945
|
+
for participant in participants:
|
|
946
|
+
if isinstance(participant, str) and participant.startswith("wagon:"):
|
|
947
|
+
wagon_name = participant.replace("wagon:", "")
|
|
948
|
+
wagons.append(wagon_name)
|
|
949
|
+
return wagons
|
|
950
|
+
|
|
951
|
+
def build_trains(self, mode: str = "interactive") -> Dict[str, Any]:
|
|
952
|
+
"""
|
|
953
|
+
Build trains registry from train manifest files.
|
|
954
|
+
Scans plan/_trains/*.yaml files and builds plan/_trains.yaml.
|
|
955
|
+
|
|
956
|
+
Train ID convention: NN-XX-name where:
|
|
957
|
+
- NN = theme prefix (first 2 digits for grouping)
|
|
958
|
+
- XX = category within theme
|
|
959
|
+
- name = train slug
|
|
960
|
+
|
|
961
|
+
Train First-Class Spec v0.6 Normalization:
|
|
962
|
+
- Section 1: Normalize file→path (deprecation)
|
|
963
|
+
- Section 4: Extract wagons from participants
|
|
964
|
+
- Section 5: Normalize test/code fields to {backend/frontend/frontend_python: []}
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
Statistics about the update (includes has_changes flag for check mode)
|
|
971
|
+
"""
|
|
972
|
+
import warnings
|
|
973
|
+
|
|
974
|
+
print("\n📊 Analyzing trains registry from manifest files...")
|
|
975
|
+
|
|
976
|
+
# Set up paths
|
|
977
|
+
trains_dir = self.plan_dir / "_trains"
|
|
978
|
+
registry_path = self.plan_dir / "_trains.yaml"
|
|
979
|
+
|
|
980
|
+
# Load existing registry
|
|
981
|
+
existing_trains = {}
|
|
982
|
+
if registry_path.exists():
|
|
983
|
+
with open(registry_path) as f:
|
|
984
|
+
registry_data = yaml.safe_load(f)
|
|
985
|
+
existing_trains = {t.get("train_id"): t for t in registry_data.get("trains", [])}
|
|
986
|
+
|
|
987
|
+
trains = []
|
|
988
|
+
stats = {
|
|
989
|
+
"total_manifests": 0,
|
|
990
|
+
"processed": 0,
|
|
991
|
+
"updated": 0,
|
|
992
|
+
"new": 0,
|
|
993
|
+
"errors": 0,
|
|
994
|
+
"preserved_drafts": 0,
|
|
995
|
+
"file_to_path_migrations": 0,
|
|
996
|
+
"changes": []
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
# Check if trains directory exists
|
|
1000
|
+
if not trains_dir.exists():
|
|
1001
|
+
print(f" ⚠️ No _trains/ directory found at {trains_dir}")
|
|
1002
|
+
# Still preserve existing drafts
|
|
1003
|
+
for train_id, train in existing_trains.items():
|
|
1004
|
+
if train.get("draft", False):
|
|
1005
|
+
trains.append(train)
|
|
1006
|
+
stats["preserved_drafts"] += 1
|
|
1007
|
+
|
|
1008
|
+
if stats["preserved_drafts"] > 0:
|
|
1009
|
+
print(f"\n📋 PREVIEW:")
|
|
1010
|
+
print(f" • {stats['preserved_drafts']} draft trains will be preserved")
|
|
1011
|
+
output = {"trains": trains}
|
|
1012
|
+
return self._confirm_and_apply(mode, "trains", registry_path, output, stats)
|
|
1013
|
+
|
|
1014
|
+
stats["has_changes"] = False
|
|
846
1015
|
return stats
|
|
847
1016
|
|
|
848
|
-
#
|
|
849
|
-
|
|
850
|
-
|
|
1017
|
+
# Scan for train manifests
|
|
1018
|
+
manifest_files = list(trains_dir.glob("*.yaml"))
|
|
1019
|
+
manifest_files = [f for f in manifest_files if not f.name.startswith("_")]
|
|
1020
|
+
stats["total_manifests"] = len(manifest_files)
|
|
851
1021
|
|
|
852
|
-
|
|
853
|
-
|
|
1022
|
+
for manifest_path in sorted(manifest_files):
|
|
1023
|
+
try:
|
|
1024
|
+
with open(manifest_path) as f:
|
|
1025
|
+
manifest = yaml.safe_load(f)
|
|
854
1026
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
print(f" 📝 Registry: {registry_path}")
|
|
1027
|
+
if not manifest:
|
|
1028
|
+
print(f" ⚠️ Skipping empty manifest: {manifest_path}")
|
|
1029
|
+
continue
|
|
859
1030
|
|
|
860
|
-
|
|
1031
|
+
train_id = manifest.get("train_id", manifest.get("train", ""))
|
|
1032
|
+
if not train_id:
|
|
1033
|
+
# Try to infer from filename (e.g., 01-01-setup.yaml -> 01-01-setup)
|
|
1034
|
+
train_id = manifest_path.stem
|
|
1035
|
+
|
|
1036
|
+
# Parse theme from train_id (first digit maps to theme name)
|
|
1037
|
+
theme = ""
|
|
1038
|
+
theme_map = {
|
|
1039
|
+
"0": "commons", "1": "mechanic", "2": "scenario", "3": "match",
|
|
1040
|
+
"4": "sensory", "5": "player", "6": "league", "7": "audience",
|
|
1041
|
+
"8": "monetization", "9": "partnership"
|
|
1042
|
+
}
|
|
1043
|
+
if train_id and train_id[0].isdigit():
|
|
1044
|
+
theme = theme_map.get(train_id[0], "")
|
|
861
1045
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
"""Build planner registry (alias for update_wagon_registry)."""
|
|
865
|
-
return self.update_wagon_registry(preview_only)
|
|
1046
|
+
# Build train entry
|
|
1047
|
+
rel_manifest = str(manifest_path.relative_to(self.repo_root))
|
|
866
1048
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1049
|
+
# Section 1: Normalize file→path
|
|
1050
|
+
path_value = manifest.get("path")
|
|
1051
|
+
file_value = manifest.get("file")
|
|
1052
|
+
if file_value and not path_value:
|
|
1053
|
+
# Migrate file to path
|
|
1054
|
+
path_value = file_value
|
|
1055
|
+
stats["file_to_path_migrations"] += 1
|
|
1056
|
+
warnings.warn(
|
|
1057
|
+
f"Train {train_id}: 'file' field is deprecated, migrating to 'path'",
|
|
1058
|
+
DeprecationWarning,
|
|
1059
|
+
stacklevel=2
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
# Section 4: Extract wagons from participants
|
|
1063
|
+
participants = manifest.get("participants", [])
|
|
1064
|
+
wagons = self._extract_wagons_from_participants(participants)
|
|
1065
|
+
|
|
1066
|
+
# Also include explicitly listed wagons
|
|
1067
|
+
explicit_wagons = manifest.get("wagons", [])
|
|
1068
|
+
if explicit_wagons:
|
|
1069
|
+
# Validate subset relationship
|
|
1070
|
+
explicit_set = set(explicit_wagons)
|
|
1071
|
+
participant_set = set(wagons)
|
|
1072
|
+
if not explicit_set.issubset(participant_set) and participant_set:
|
|
1073
|
+
extra = explicit_set - participant_set
|
|
1074
|
+
warnings.warn(
|
|
1075
|
+
f"Train {train_id}: registry wagons {extra} not in YAML participants",
|
|
1076
|
+
UserWarning,
|
|
1077
|
+
stacklevel=2
|
|
1078
|
+
)
|
|
1079
|
+
wagons = explicit_wagons # Use explicit if provided
|
|
1080
|
+
|
|
1081
|
+
# Section 5: Normalize test/code fields
|
|
1082
|
+
test_normalized = self._normalize_test_code_field(manifest.get("test"))
|
|
1083
|
+
code_normalized = self._normalize_test_code_field(manifest.get("code"))
|
|
870
1084
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1085
|
+
entry = {
|
|
1086
|
+
"train_id": train_id,
|
|
1087
|
+
"theme": theme,
|
|
1088
|
+
"title": manifest.get("title", manifest.get("description", "")),
|
|
1089
|
+
"description": manifest.get("description", ""),
|
|
1090
|
+
"wagons": wagons,
|
|
1091
|
+
"status": manifest.get("status", "planned"),
|
|
1092
|
+
"manifest": rel_manifest
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
# Add path if present
|
|
1096
|
+
if path_value:
|
|
1097
|
+
entry["path"] = path_value
|
|
1098
|
+
|
|
1099
|
+
# Add primary_wagon if present
|
|
1100
|
+
primary_wagon = manifest.get("primary_wagon")
|
|
1101
|
+
if primary_wagon:
|
|
1102
|
+
entry["primary_wagon"] = primary_wagon
|
|
1103
|
+
|
|
1104
|
+
# Add normalized test/code if present
|
|
1105
|
+
if test_normalized:
|
|
1106
|
+
entry["test"] = test_normalized
|
|
1107
|
+
if code_normalized:
|
|
1108
|
+
entry["code"] = code_normalized
|
|
1109
|
+
|
|
1110
|
+
# Add expectations if present
|
|
1111
|
+
expectations = manifest.get("expectations")
|
|
1112
|
+
if expectations:
|
|
1113
|
+
entry["expectations"] = expectations
|
|
1114
|
+
|
|
1115
|
+
# Check if updating or new
|
|
1116
|
+
if train_id in existing_trains:
|
|
1117
|
+
stats["updated"] += 1
|
|
1118
|
+
# Check for field changes
|
|
1119
|
+
old = existing_trains[train_id]
|
|
1120
|
+
changed_fields = []
|
|
1121
|
+
for field in ["title", "description", "wagons", "status", "theme", "path", "test", "code", "expectations"]:
|
|
1122
|
+
if old.get(field) != entry.get(field):
|
|
1123
|
+
changed_fields.append(field)
|
|
1124
|
+
if changed_fields:
|
|
1125
|
+
stats["changes"].append({
|
|
1126
|
+
"train": train_id,
|
|
1127
|
+
"type": "updated",
|
|
1128
|
+
"fields": changed_fields
|
|
1129
|
+
})
|
|
1130
|
+
else:
|
|
1131
|
+
stats["new"] += 1
|
|
1132
|
+
stats["changes"].append({
|
|
1133
|
+
"train": train_id,
|
|
1134
|
+
"type": "new",
|
|
1135
|
+
"fields": ["all fields (new train)"]
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
trains.append(entry)
|
|
1139
|
+
stats["processed"] += 1
|
|
1140
|
+
|
|
1141
|
+
except Exception as e:
|
|
1142
|
+
print(f" ❌ Error processing {manifest_path}: {e}")
|
|
1143
|
+
stats["errors"] += 1
|
|
1144
|
+
|
|
1145
|
+
# Preserve draft trains (those without manifests or with draft: true)
|
|
1146
|
+
for train_id, train in existing_trains.items():
|
|
1147
|
+
is_draft = train.get("draft", False)
|
|
1148
|
+
has_no_manifest = not train.get("manifest")
|
|
1149
|
+
if is_draft or has_no_manifest:
|
|
1150
|
+
if train_id not in [t.get("train_id") for t in trains]:
|
|
1151
|
+
trains.append(train)
|
|
1152
|
+
stats["preserved_drafts"] += 1
|
|
1153
|
+
|
|
1154
|
+
# Sort by train_id
|
|
1155
|
+
trains.sort(key=lambda t: t.get("train_id", ""))
|
|
1156
|
+
|
|
1157
|
+
# Show preview
|
|
1158
|
+
print(f"\n📋 PREVIEW:")
|
|
1159
|
+
print(f" • {stats['updated']} trains will be updated")
|
|
1160
|
+
print(f" • {stats['new']} new trains will be added")
|
|
1161
|
+
print(f" • {stats['preserved_drafts']} draft trains will be preserved")
|
|
1162
|
+
if stats["file_to_path_migrations"] > 0:
|
|
1163
|
+
print(f" ⚠️ {stats['file_to_path_migrations']} file→path migrations (deprecation)")
|
|
1164
|
+
if stats["errors"] > 0:
|
|
1165
|
+
print(f" ⚠️ {stats['errors']} errors encountered")
|
|
1166
|
+
|
|
1167
|
+
# Use helper for confirm/apply
|
|
1168
|
+
output = {"trains": trains}
|
|
1169
|
+
return self._confirm_and_apply(mode, "trains", registry_path, output, stats)
|
|
874
1170
|
|
|
875
|
-
def build_tester(self, preview_only: bool =
|
|
1171
|
+
def build_tester(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
876
1172
|
"""
|
|
877
1173
|
Build tester registry from test files.
|
|
878
1174
|
Scans atdd/tester/**/*_test.py files for URNs and metadata.
|
|
1175
|
+
|
|
1176
|
+
Args:
|
|
1177
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
1178
|
+
preview_only: Deprecated - use mode="check" instead
|
|
879
1179
|
"""
|
|
1180
|
+
# Backwards compatibility
|
|
1181
|
+
if preview_only is not None:
|
|
1182
|
+
mode = "check" if preview_only else "interactive"
|
|
880
1183
|
print("\n📊 Analyzing tester registry from test files...")
|
|
881
1184
|
|
|
882
1185
|
# Load existing registry
|
|
@@ -894,12 +1197,12 @@ class RegistryBuilder:
|
|
|
894
1197
|
"updated": 0,
|
|
895
1198
|
"new": 0,
|
|
896
1199
|
"errors": 0,
|
|
1200
|
+
"preserved_drafts": 0,
|
|
897
1201
|
"changes": []
|
|
898
1202
|
}
|
|
899
1203
|
|
|
900
1204
|
# Scan for test files
|
|
901
1205
|
if self.tester_dir.exists():
|
|
902
|
-
# Look for both test_*.py and *_test.py patterns
|
|
903
1206
|
test_files = list(self.tester_dir.glob("**/*_test.py"))
|
|
904
1207
|
test_files.extend(list(self.tester_dir.glob("**/test_*.py")))
|
|
905
1208
|
test_files = [f for f in test_files if not f.name.startswith("_")]
|
|
@@ -910,16 +1213,13 @@ class RegistryBuilder:
|
|
|
910
1213
|
with open(test_file) as f:
|
|
911
1214
|
content = f.read()
|
|
912
1215
|
|
|
913
|
-
# Extract URN markers from docstring or comments
|
|
914
1216
|
urns = re.findall(r'URN:\s*(\S+)', content)
|
|
915
1217
|
spec_urns = re.findall(r'Spec:\s*(\S+)', content)
|
|
916
1218
|
acceptance_urns = re.findall(r'Acceptance:\s*(\S+)', content)
|
|
917
1219
|
|
|
918
|
-
# Extract wagon from path
|
|
919
1220
|
rel_path = test_file.relative_to(self.tester_dir)
|
|
920
1221
|
wagon = rel_path.parts[0] if len(rel_path.parts) > 1 else "unknown"
|
|
921
1222
|
|
|
922
|
-
# Build test entry
|
|
923
1223
|
for urn in urns:
|
|
924
1224
|
test_entry = {
|
|
925
1225
|
"urn": urn,
|
|
@@ -932,7 +1232,6 @@ class RegistryBuilder:
|
|
|
932
1232
|
if acceptance_urns:
|
|
933
1233
|
test_entry["acceptance_urn"] = acceptance_urns[0]
|
|
934
1234
|
|
|
935
|
-
# Track changes
|
|
936
1235
|
if urn in existing_tests:
|
|
937
1236
|
stats["updated"] += 1
|
|
938
1237
|
else:
|
|
@@ -950,45 +1249,39 @@ class RegistryBuilder:
|
|
|
950
1249
|
print(f" ⚠️ Error processing {test_file}: {e}")
|
|
951
1250
|
stats["errors"] += 1
|
|
952
1251
|
|
|
1252
|
+
# Preserve draft tests (file doesn't exist or draft: true)
|
|
1253
|
+
for urn, test in existing_tests.items():
|
|
1254
|
+
is_draft = test.get("draft", False)
|
|
1255
|
+
file_exists = test.get("file") and (self.repo_root / test.get("file")).exists()
|
|
1256
|
+
if is_draft or not file_exists:
|
|
1257
|
+
if urn not in [t.get("urn") for t in tests]:
|
|
1258
|
+
tests.append(test)
|
|
1259
|
+
stats["preserved_drafts"] += 1
|
|
1260
|
+
|
|
953
1261
|
# Show preview
|
|
954
1262
|
print(f"\n📋 PREVIEW:")
|
|
955
1263
|
print(f" • {stats['updated']} tests will be updated")
|
|
956
1264
|
print(f" • {stats['new']} new tests will be added")
|
|
1265
|
+
print(f" • {stats['preserved_drafts']} draft tests will be preserved")
|
|
957
1266
|
if stats["errors"] > 0:
|
|
958
1267
|
print(f" ⚠️ {stats['errors']} errors encountered")
|
|
959
1268
|
|
|
960
|
-
|
|
961
|
-
print("\n⚠️ Preview mode - no changes applied")
|
|
962
|
-
return stats
|
|
963
|
-
|
|
964
|
-
# Ask for confirmation
|
|
965
|
-
print("\n❓ Do you want to apply these changes to the tester registry?")
|
|
966
|
-
print(" Type 'yes' to confirm, or anything else to cancel:")
|
|
967
|
-
response = input(" > ").strip().lower()
|
|
968
|
-
|
|
969
|
-
if response != "yes":
|
|
970
|
-
print("\n❌ Update cancelled by user")
|
|
971
|
-
stats["cancelled"] = True
|
|
972
|
-
return stats
|
|
973
|
-
|
|
974
|
-
# Write registry
|
|
1269
|
+
# Use helper for confirm/apply
|
|
975
1270
|
output = {"tests": tests}
|
|
976
|
-
|
|
977
|
-
with open(registry_path, "w") as f:
|
|
978
|
-
yaml.dump(output, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
979
|
-
|
|
980
|
-
print(f"\n✅ Tester registry updated successfully!")
|
|
981
|
-
print(f" • Updated {stats['updated']} tests")
|
|
982
|
-
print(f" • Added {stats['new']} new tests")
|
|
983
|
-
print(f" 📝 Registry: {registry_path}")
|
|
984
|
-
|
|
985
|
-
return stats
|
|
1271
|
+
return self._confirm_and_apply(mode, "tester", registry_path, output, stats)
|
|
986
1272
|
|
|
987
|
-
def build_coder(self, preview_only: bool =
|
|
1273
|
+
def build_coder(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
988
1274
|
"""
|
|
989
1275
|
Build coder implementation registry from Python files.
|
|
990
1276
|
Scans python/**/*.py files for implementations.
|
|
1277
|
+
|
|
1278
|
+
Args:
|
|
1279
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
1280
|
+
preview_only: Deprecated - use mode="check" instead
|
|
991
1281
|
"""
|
|
1282
|
+
# Backwards compatibility
|
|
1283
|
+
if preview_only is not None:
|
|
1284
|
+
mode = "check" if preview_only else "interactive"
|
|
992
1285
|
print("\n📊 Analyzing coder registry from Python files...")
|
|
993
1286
|
|
|
994
1287
|
# Load existing registry
|
|
@@ -1006,13 +1299,13 @@ class RegistryBuilder:
|
|
|
1006
1299
|
"updated": 0,
|
|
1007
1300
|
"new": 0,
|
|
1008
1301
|
"errors": 0,
|
|
1302
|
+
"preserved_drafts": 0,
|
|
1009
1303
|
"changes": []
|
|
1010
1304
|
}
|
|
1011
1305
|
|
|
1012
1306
|
# Scan for Python implementation files
|
|
1013
1307
|
if self.python_dir.exists():
|
|
1014
1308
|
py_files = list(self.python_dir.glob("**/*.py"))
|
|
1015
|
-
# Filter out __init__, __pycache__, and files in specific test directories
|
|
1016
1309
|
py_files = [
|
|
1017
1310
|
f for f in py_files
|
|
1018
1311
|
if not f.name.startswith("_")
|
|
@@ -1029,18 +1322,15 @@ class RegistryBuilder:
|
|
|
1029
1322
|
with open(py_file) as f:
|
|
1030
1323
|
content = f.read()
|
|
1031
1324
|
|
|
1032
|
-
# Extract metadata from docstring
|
|
1033
1325
|
spec_urns = re.findall(r'Spec:\s*(\S+)', content)
|
|
1034
1326
|
test_urns = re.findall(r'Test:\s*(\S+)', content)
|
|
1035
1327
|
|
|
1036
|
-
# Extract wagon and layer from path
|
|
1037
1328
|
rel_path = py_file.relative_to(self.python_dir)
|
|
1038
1329
|
parts = rel_path.parts
|
|
1039
1330
|
|
|
1040
1331
|
wagon = parts[0] if len(parts) > 0 else "unknown"
|
|
1041
1332
|
layer = "unknown"
|
|
1042
1333
|
|
|
1043
|
-
# Try to detect layer from path
|
|
1044
1334
|
if "domain" in str(py_file):
|
|
1045
1335
|
layer = "domain"
|
|
1046
1336
|
elif "application" in str(py_file):
|
|
@@ -1050,17 +1340,15 @@ class RegistryBuilder:
|
|
|
1050
1340
|
elif "presentation" in str(py_file):
|
|
1051
1341
|
layer = "presentation"
|
|
1052
1342
|
|
|
1053
|
-
# Generate URN
|
|
1054
1343
|
component = py_file.stem
|
|
1055
1344
|
impl_urn = f"impl:{wagon}:{layer}:{component}:python"
|
|
1056
1345
|
|
|
1057
|
-
# Build implementation entry
|
|
1058
1346
|
impl_entry = {
|
|
1059
1347
|
"urn": impl_urn,
|
|
1060
1348
|
"file": str(py_file.relative_to(self.repo_root)),
|
|
1061
1349
|
"wagon": wagon,
|
|
1062
1350
|
"layer": layer,
|
|
1063
|
-
"component_type": "entity",
|
|
1351
|
+
"component_type": "entity",
|
|
1064
1352
|
"language": "python"
|
|
1065
1353
|
}
|
|
1066
1354
|
|
|
@@ -1069,7 +1357,6 @@ class RegistryBuilder:
|
|
|
1069
1357
|
if test_urns:
|
|
1070
1358
|
impl_entry["test_urn"] = test_urns[0]
|
|
1071
1359
|
|
|
1072
|
-
# Track changes
|
|
1073
1360
|
if impl_urn in existing_impls:
|
|
1074
1361
|
stats["updated"] += 1
|
|
1075
1362
|
else:
|
|
@@ -1087,45 +1374,39 @@ class RegistryBuilder:
|
|
|
1087
1374
|
print(f" ⚠️ Error processing {py_file}: {e}")
|
|
1088
1375
|
stats["errors"] += 1
|
|
1089
1376
|
|
|
1377
|
+
# Preserve draft implementations (file doesn't exist or draft: true)
|
|
1378
|
+
for urn, impl in existing_impls.items():
|
|
1379
|
+
is_draft = impl.get("draft", False)
|
|
1380
|
+
file_exists = impl.get("file") and (self.repo_root / impl.get("file")).exists()
|
|
1381
|
+
if is_draft or not file_exists:
|
|
1382
|
+
if urn not in [i.get("urn") for i in implementations]:
|
|
1383
|
+
implementations.append(impl)
|
|
1384
|
+
stats["preserved_drafts"] += 1
|
|
1385
|
+
|
|
1090
1386
|
# Show preview
|
|
1091
1387
|
print(f"\n📋 PREVIEW:")
|
|
1092
1388
|
print(f" • {stats['updated']} implementations will be updated")
|
|
1093
1389
|
print(f" • {stats['new']} new implementations will be added")
|
|
1390
|
+
print(f" • {stats['preserved_drafts']} draft implementations will be preserved")
|
|
1094
1391
|
if stats["errors"] > 0:
|
|
1095
1392
|
print(f" ⚠️ {stats['errors']} errors encountered")
|
|
1096
1393
|
|
|
1097
|
-
|
|
1098
|
-
print("\n⚠️ Preview mode - no changes applied")
|
|
1099
|
-
return stats
|
|
1100
|
-
|
|
1101
|
-
# Ask for confirmation
|
|
1102
|
-
print("\n❓ Do you want to apply these changes to the coder registry?")
|
|
1103
|
-
print(" Type 'yes' to confirm, or anything else to cancel:")
|
|
1104
|
-
response = input(" > ").strip().lower()
|
|
1105
|
-
|
|
1106
|
-
if response != "yes":
|
|
1107
|
-
print("\n❌ Update cancelled by user")
|
|
1108
|
-
stats["cancelled"] = True
|
|
1109
|
-
return stats
|
|
1110
|
-
|
|
1111
|
-
# Write registry
|
|
1394
|
+
# Use helper for confirm/apply
|
|
1112
1395
|
output = {"implementations": implementations}
|
|
1113
|
-
|
|
1114
|
-
with open(registry_path, "w") as f:
|
|
1115
|
-
yaml.dump(output, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
1396
|
+
return self._confirm_and_apply(mode, "coder", registry_path, output, stats)
|
|
1116
1397
|
|
|
1117
|
-
|
|
1118
|
-
print(f" • Updated {stats['updated']} implementations")
|
|
1119
|
-
print(f" • Added {stats['new']} new implementations")
|
|
1120
|
-
print(f" 📝 Registry: {registry_path}")
|
|
1121
|
-
|
|
1122
|
-
return stats
|
|
1123
|
-
|
|
1124
|
-
def build_supabase(self, preview_only: bool = False) -> Dict[str, Any]:
|
|
1398
|
+
def build_supabase(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
|
|
1125
1399
|
"""
|
|
1126
1400
|
Build supabase functions registry.
|
|
1127
1401
|
Scans supabase/functions/**/ for function directories.
|
|
1402
|
+
|
|
1403
|
+
Args:
|
|
1404
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
1405
|
+
preview_only: Deprecated - use mode="check" instead
|
|
1128
1406
|
"""
|
|
1407
|
+
# Backwards compatibility
|
|
1408
|
+
if preview_only is not None:
|
|
1409
|
+
mode = "check" if preview_only else "interactive"
|
|
1129
1410
|
print("\n📊 Analyzing supabase registry from function files...")
|
|
1130
1411
|
|
|
1131
1412
|
# Load existing registry
|
|
@@ -1134,7 +1415,7 @@ class RegistryBuilder:
|
|
|
1134
1415
|
if registry_path.exists():
|
|
1135
1416
|
with open(registry_path) as f:
|
|
1136
1417
|
registry_data = yaml.safe_load(f)
|
|
1137
|
-
existing_funcs = {
|
|
1418
|
+
existing_funcs = {fn.get("id"): fn for fn in registry_data.get("functions", [])}
|
|
1138
1419
|
|
|
1139
1420
|
functions = []
|
|
1140
1421
|
stats = {
|
|
@@ -1143,6 +1424,7 @@ class RegistryBuilder:
|
|
|
1143
1424
|
"updated": 0,
|
|
1144
1425
|
"new": 0,
|
|
1145
1426
|
"errors": 0,
|
|
1427
|
+
"preserved_drafts": 0,
|
|
1146
1428
|
"changes": []
|
|
1147
1429
|
}
|
|
1148
1430
|
|
|
@@ -1168,7 +1450,6 @@ class RegistryBuilder:
|
|
|
1168
1450
|
"description": f"Supabase function: {func_id}"
|
|
1169
1451
|
}
|
|
1170
1452
|
|
|
1171
|
-
# Track changes
|
|
1172
1453
|
if func_id in existing_funcs:
|
|
1173
1454
|
stats["updated"] += 1
|
|
1174
1455
|
else:
|
|
@@ -1186,37 +1467,24 @@ class RegistryBuilder:
|
|
|
1186
1467
|
print(f" ⚠️ Error processing {func_dir}: {e}")
|
|
1187
1468
|
stats["errors"] += 1
|
|
1188
1469
|
|
|
1470
|
+
# Preserve draft functions (path doesn't exist or draft: true)
|
|
1471
|
+
for func_id, func in existing_funcs.items():
|
|
1472
|
+
is_draft = func.get("draft", False)
|
|
1473
|
+
path_exists = func.get("path") and (self.repo_root / func.get("path")).exists()
|
|
1474
|
+
if is_draft or not path_exists:
|
|
1475
|
+
if func_id not in [fn.get("id") for fn in functions]:
|
|
1476
|
+
functions.append(func)
|
|
1477
|
+
stats["preserved_drafts"] += 1
|
|
1478
|
+
|
|
1189
1479
|
# Show preview
|
|
1190
1480
|
print(f"\n📋 PREVIEW:")
|
|
1191
1481
|
print(f" • {stats['updated']} functions will be updated")
|
|
1192
1482
|
print(f" • {stats['new']} new functions will be added")
|
|
1483
|
+
print(f" • {stats['preserved_drafts']} draft functions will be preserved")
|
|
1193
1484
|
|
|
1194
|
-
|
|
1195
|
-
print("\n⚠️ Preview mode - no changes applied")
|
|
1196
|
-
return stats
|
|
1197
|
-
|
|
1198
|
-
# Ask for confirmation
|
|
1199
|
-
print("\n❓ Do you want to apply these changes to the supabase registry?")
|
|
1200
|
-
print(" Type 'yes' to confirm, or anything else to cancel:")
|
|
1201
|
-
response = input(" > ").strip().lower()
|
|
1202
|
-
|
|
1203
|
-
if response != "yes":
|
|
1204
|
-
print("\n❌ Update cancelled by user")
|
|
1205
|
-
stats["cancelled"] = True
|
|
1206
|
-
return stats
|
|
1207
|
-
|
|
1208
|
-
# Write registry
|
|
1485
|
+
# Use helper for confirm/apply
|
|
1209
1486
|
output = {"functions": functions}
|
|
1210
|
-
|
|
1211
|
-
with open(registry_path, "w") as f:
|
|
1212
|
-
yaml.dump(output, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
1213
|
-
|
|
1214
|
-
print(f"\n✅ Supabase registry updated successfully!")
|
|
1215
|
-
print(f" • Updated {stats['updated']} functions")
|
|
1216
|
-
print(f" • Added {stats['new']} new functions")
|
|
1217
|
-
print(f" 📝 Registry: {registry_path}")
|
|
1218
|
-
|
|
1219
|
-
return stats
|
|
1487
|
+
return self._confirm_and_apply(mode, "supabase", registry_path, output, stats)
|
|
1220
1488
|
|
|
1221
1489
|
def build_python_manifest(self, preview_only: bool = False) -> Dict[str, Any]:
|
|
1222
1490
|
"""
|
|
@@ -1317,19 +1585,24 @@ class RegistryBuilder:
|
|
|
1317
1585
|
|
|
1318
1586
|
return stats
|
|
1319
1587
|
|
|
1320
|
-
def build_all(self) -> Dict[str, Any]:
|
|
1321
|
-
"""Build all registries.
|
|
1588
|
+
def build_all(self, mode: str = "interactive") -> Dict[str, Any]:
|
|
1589
|
+
"""Build all registries.
|
|
1590
|
+
|
|
1591
|
+
Args:
|
|
1592
|
+
mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
|
|
1593
|
+
"""
|
|
1322
1594
|
print("=" * 60)
|
|
1323
1595
|
print("Unified Registry Builder - Synchronizing from source files")
|
|
1324
1596
|
print("=" * 60)
|
|
1325
1597
|
|
|
1326
1598
|
results = {
|
|
1327
|
-
"plan": self.build_planner(),
|
|
1328
|
-
"
|
|
1329
|
-
"
|
|
1330
|
-
"
|
|
1331
|
-
"
|
|
1332
|
-
"
|
|
1599
|
+
"plan": self.build_planner(mode),
|
|
1600
|
+
"trains": self.build_trains(mode),
|
|
1601
|
+
"contracts": self.build_contracts(mode),
|
|
1602
|
+
"telemetry": self.build_telemetry(mode),
|
|
1603
|
+
"tester": self.build_tester(mode),
|
|
1604
|
+
"coder": self.build_coder(mode),
|
|
1605
|
+
"supabase": self.build_supabase(mode)
|
|
1333
1606
|
}
|
|
1334
1607
|
|
|
1335
1608
|
print("\n" + "=" * 60)
|