atdd 0.4.4__py3-none-any.whl → 0.4.6__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
@@ -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 = False) -> Dict[str, Any]:
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
- preview_only: If True, only show what would change without applying
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": [] # Track detailed 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
- if not wagon.get("manifest") and not wagon.get("path"):
567
- updated_wagons.append(wagon)
568
- preserved_drafts.append(slug)
569
- stats["preserved_drafts"] += 1
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
- # If preview only, return early
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
- with open(registry_path, "w") as f:
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 = False) -> Dict[str, Any]:
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
- preview_only: If True, only show what would change without applying
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 # No :v1 suffix - version tracked separately
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
- # If preview only, return early
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
- with open(registry_path, "w") as f:
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
- preview_only: If True, only show what would change without applying
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,211 @@ 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
- # If preview only, return early
834
- if preview_only:
835
- print("\n⚠️ Preview mode - no changes applied")
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
- # Ask for user approval
839
- print("\n❓ Do you want to apply these changes to the telemetry registry?")
840
- print(" Type 'yes' to confirm, or anything else to cancel:")
841
- response = input(" > ").strip().lower()
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
- if response != "yes":
844
- print("\n❌ Update cancelled by user")
845
- stats["cancelled"] = True
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 build_trains(self, mode: str = "interactive") -> Dict[str, Any]:
914
+ """
915
+ Build trains registry from train manifest files.
916
+ Scans plan/_trains/*.yaml files and builds plan/_trains.yaml.
917
+
918
+ Train ID convention: NN-XX-name where:
919
+ - NN = theme prefix (first 2 digits for grouping)
920
+ - XX = category within theme
921
+ - name = train slug
922
+
923
+ Args:
924
+ mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
925
+
926
+ Returns:
927
+ Statistics about the update (includes has_changes flag for check mode)
928
+ """
929
+ print("\n📊 Analyzing trains registry from manifest files...")
930
+
931
+ # Set up paths
932
+ trains_dir = self.plan_dir / "_trains"
933
+ registry_path = self.plan_dir / "_trains.yaml"
934
+
935
+ # Load existing registry
936
+ existing_trains = {}
937
+ if registry_path.exists():
938
+ with open(registry_path) as f:
939
+ registry_data = yaml.safe_load(f)
940
+ existing_trains = {t.get("train_id"): t for t in registry_data.get("trains", [])}
941
+
942
+ trains = []
943
+ stats = {
944
+ "total_manifests": 0,
945
+ "processed": 0,
946
+ "updated": 0,
947
+ "new": 0,
948
+ "errors": 0,
949
+ "preserved_drafts": 0,
950
+ "changes": []
951
+ }
952
+
953
+ # Check if trains directory exists
954
+ if not trains_dir.exists():
955
+ print(f" ⚠️ No _trains/ directory found at {trains_dir}")
956
+ # Still preserve existing drafts
957
+ for train_id, train in existing_trains.items():
958
+ if train.get("draft", False):
959
+ trains.append(train)
960
+ stats["preserved_drafts"] += 1
961
+
962
+ if stats["preserved_drafts"] > 0:
963
+ print(f"\n📋 PREVIEW:")
964
+ print(f" • {stats['preserved_drafts']} draft trains will be preserved")
965
+ output = {"trains": trains}
966
+ return self._confirm_and_apply(mode, "trains", registry_path, output, stats)
967
+
968
+ stats["has_changes"] = False
846
969
  return stats
847
970
 
848
- # Write registry
849
- registry_path = self.telemetry_dir / "_signals.yaml"
850
- output = {"signals": signals}
971
+ # Scan for train manifests
972
+ manifest_files = list(trains_dir.glob("*.yaml"))
973
+ manifest_files = [f for f in manifest_files if not f.name.startswith("_")]
974
+ stats["total_manifests"] = len(manifest_files)
851
975
 
852
- with open(registry_path, "w") as f:
853
- yaml.dump(output, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
976
+ for manifest_path in sorted(manifest_files):
977
+ try:
978
+ with open(manifest_path) as f:
979
+ manifest = yaml.safe_load(f)
854
980
 
855
- print(f"\n✅ Telemetry registry updated successfully!")
856
- print(f" Updated {stats['updated']} signals")
857
- print(f" • Added {stats['new']} new signals")
858
- print(f" 📝 Registry: {registry_path}")
981
+ if not manifest:
982
+ print(f" ⚠️ Skipping empty manifest: {manifest_path}")
983
+ continue
859
984
 
860
- return stats
985
+ train_id = manifest.get("train_id", manifest.get("train", ""))
986
+ if not train_id:
987
+ # Try to infer from filename (e.g., 01-01-setup.yaml -> 01-01-setup)
988
+ train_id = manifest_path.stem
861
989
 
862
- # Alias methods for unified API
863
- def build_planner(self, preview_only: bool = False) -> Dict[str, Any]:
864
- """Build planner registry (alias for update_wagon_registry)."""
865
- return self.update_wagon_registry(preview_only)
990
+ # Parse theme from train_id (first 2 digits)
991
+ theme = ""
992
+ if len(train_id) >= 2 and train_id[:2].isdigit():
993
+ theme = train_id[:2]
866
994
 
867
- def build_contracts(self, preview_only: bool = False) -> Dict[str, Any]:
868
- """Build contracts registry (alias for update_contract_registry)."""
869
- return self.update_contract_registry(preview_only)
995
+ # Build train entry
996
+ rel_manifest = str(manifest_path.relative_to(self.repo_root))
870
997
 
871
- def build_telemetry(self, preview_only: bool = False) -> Dict[str, Any]:
872
- """Build telemetry registry (alias for update_telemetry_registry)."""
873
- return self.update_telemetry_registry(preview_only)
998
+ entry = {
999
+ "train_id": train_id,
1000
+ "theme": theme,
1001
+ "title": manifest.get("title", manifest.get("description", "")),
1002
+ "description": manifest.get("description", ""),
1003
+ "wagons": manifest.get("wagons", []),
1004
+ "status": manifest.get("status", "planned"),
1005
+ "manifest": rel_manifest
1006
+ }
1007
+
1008
+ # Check if updating or new
1009
+ if train_id in existing_trains:
1010
+ stats["updated"] += 1
1011
+ # Check for field changes
1012
+ old = existing_trains[train_id]
1013
+ changed_fields = []
1014
+ for field in ["title", "description", "wagons", "status", "theme"]:
1015
+ if old.get(field) != entry.get(field):
1016
+ changed_fields.append(field)
1017
+ if changed_fields:
1018
+ stats["changes"].append({
1019
+ "train": train_id,
1020
+ "type": "updated",
1021
+ "fields": changed_fields
1022
+ })
1023
+ else:
1024
+ stats["new"] += 1
1025
+ stats["changes"].append({
1026
+ "train": train_id,
1027
+ "type": "new",
1028
+ "fields": ["all fields (new train)"]
1029
+ })
1030
+
1031
+ trains.append(entry)
1032
+ stats["processed"] += 1
874
1033
 
875
- def build_tester(self, preview_only: bool = False) -> Dict[str, Any]:
1034
+ except Exception as e:
1035
+ print(f" ❌ Error processing {manifest_path}: {e}")
1036
+ stats["errors"] += 1
1037
+
1038
+ # Preserve draft trains (those without manifests or with draft: true)
1039
+ for train_id, train in existing_trains.items():
1040
+ is_draft = train.get("draft", False)
1041
+ has_no_manifest = not train.get("manifest")
1042
+ if is_draft or has_no_manifest:
1043
+ if train_id not in [t.get("train_id") for t in trains]:
1044
+ trains.append(train)
1045
+ stats["preserved_drafts"] += 1
1046
+
1047
+ # Sort by train_id
1048
+ trains.sort(key=lambda t: t.get("train_id", ""))
1049
+
1050
+ # Show preview
1051
+ print(f"\n📋 PREVIEW:")
1052
+ print(f" • {stats['updated']} trains will be updated")
1053
+ print(f" • {stats['new']} new trains will be added")
1054
+ print(f" • {stats['preserved_drafts']} draft trains will be preserved")
1055
+ if stats["errors"] > 0:
1056
+ print(f" ⚠️ {stats['errors']} errors encountered")
1057
+
1058
+ # Use helper for confirm/apply
1059
+ output = {"trains": trains}
1060
+ return self._confirm_and_apply(mode, "trains", registry_path, output, stats)
1061
+
1062
+ def build_tester(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
876
1063
  """
877
1064
  Build tester registry from test files.
878
1065
  Scans atdd/tester/**/*_test.py files for URNs and metadata.
1066
+
1067
+ Args:
1068
+ mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
1069
+ preview_only: Deprecated - use mode="check" instead
879
1070
  """
1071
+ # Backwards compatibility
1072
+ if preview_only is not None:
1073
+ mode = "check" if preview_only else "interactive"
880
1074
  print("\n📊 Analyzing tester registry from test files...")
881
1075
 
882
1076
  # Load existing registry
@@ -894,12 +1088,12 @@ class RegistryBuilder:
894
1088
  "updated": 0,
895
1089
  "new": 0,
896
1090
  "errors": 0,
1091
+ "preserved_drafts": 0,
897
1092
  "changes": []
898
1093
  }
899
1094
 
900
1095
  # Scan for test files
901
1096
  if self.tester_dir.exists():
902
- # Look for both test_*.py and *_test.py patterns
903
1097
  test_files = list(self.tester_dir.glob("**/*_test.py"))
904
1098
  test_files.extend(list(self.tester_dir.glob("**/test_*.py")))
905
1099
  test_files = [f for f in test_files if not f.name.startswith("_")]
@@ -910,16 +1104,13 @@ class RegistryBuilder:
910
1104
  with open(test_file) as f:
911
1105
  content = f.read()
912
1106
 
913
- # Extract URN markers from docstring or comments
914
1107
  urns = re.findall(r'URN:\s*(\S+)', content)
915
1108
  spec_urns = re.findall(r'Spec:\s*(\S+)', content)
916
1109
  acceptance_urns = re.findall(r'Acceptance:\s*(\S+)', content)
917
1110
 
918
- # Extract wagon from path
919
1111
  rel_path = test_file.relative_to(self.tester_dir)
920
1112
  wagon = rel_path.parts[0] if len(rel_path.parts) > 1 else "unknown"
921
1113
 
922
- # Build test entry
923
1114
  for urn in urns:
924
1115
  test_entry = {
925
1116
  "urn": urn,
@@ -932,7 +1123,6 @@ class RegistryBuilder:
932
1123
  if acceptance_urns:
933
1124
  test_entry["acceptance_urn"] = acceptance_urns[0]
934
1125
 
935
- # Track changes
936
1126
  if urn in existing_tests:
937
1127
  stats["updated"] += 1
938
1128
  else:
@@ -950,45 +1140,39 @@ class RegistryBuilder:
950
1140
  print(f" ⚠️ Error processing {test_file}: {e}")
951
1141
  stats["errors"] += 1
952
1142
 
1143
+ # Preserve draft tests (file doesn't exist or draft: true)
1144
+ for urn, test in existing_tests.items():
1145
+ is_draft = test.get("draft", False)
1146
+ file_exists = test.get("file") and (self.repo_root / test.get("file")).exists()
1147
+ if is_draft or not file_exists:
1148
+ if urn not in [t.get("urn") for t in tests]:
1149
+ tests.append(test)
1150
+ stats["preserved_drafts"] += 1
1151
+
953
1152
  # Show preview
954
1153
  print(f"\n📋 PREVIEW:")
955
1154
  print(f" • {stats['updated']} tests will be updated")
956
1155
  print(f" • {stats['new']} new tests will be added")
1156
+ print(f" • {stats['preserved_drafts']} draft tests will be preserved")
957
1157
  if stats["errors"] > 0:
958
1158
  print(f" ⚠️ {stats['errors']} errors encountered")
959
1159
 
960
- if preview_only:
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
1160
+ # Use helper for confirm/apply
975
1161
  output = {"tests": tests}
976
- registry_path.parent.mkdir(parents=True, exist_ok=True)
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
1162
+ return self._confirm_and_apply(mode, "tester", registry_path, output, stats)
986
1163
 
987
- def build_coder(self, preview_only: bool = False) -> Dict[str, Any]:
1164
+ def build_coder(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
988
1165
  """
989
1166
  Build coder implementation registry from Python files.
990
1167
  Scans python/**/*.py files for implementations.
1168
+
1169
+ Args:
1170
+ mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
1171
+ preview_only: Deprecated - use mode="check" instead
991
1172
  """
1173
+ # Backwards compatibility
1174
+ if preview_only is not None:
1175
+ mode = "check" if preview_only else "interactive"
992
1176
  print("\n📊 Analyzing coder registry from Python files...")
993
1177
 
994
1178
  # Load existing registry
@@ -1006,13 +1190,13 @@ class RegistryBuilder:
1006
1190
  "updated": 0,
1007
1191
  "new": 0,
1008
1192
  "errors": 0,
1193
+ "preserved_drafts": 0,
1009
1194
  "changes": []
1010
1195
  }
1011
1196
 
1012
1197
  # Scan for Python implementation files
1013
1198
  if self.python_dir.exists():
1014
1199
  py_files = list(self.python_dir.glob("**/*.py"))
1015
- # Filter out __init__, __pycache__, and files in specific test directories
1016
1200
  py_files = [
1017
1201
  f for f in py_files
1018
1202
  if not f.name.startswith("_")
@@ -1029,18 +1213,15 @@ class RegistryBuilder:
1029
1213
  with open(py_file) as f:
1030
1214
  content = f.read()
1031
1215
 
1032
- # Extract metadata from docstring
1033
1216
  spec_urns = re.findall(r'Spec:\s*(\S+)', content)
1034
1217
  test_urns = re.findall(r'Test:\s*(\S+)', content)
1035
1218
 
1036
- # Extract wagon and layer from path
1037
1219
  rel_path = py_file.relative_to(self.python_dir)
1038
1220
  parts = rel_path.parts
1039
1221
 
1040
1222
  wagon = parts[0] if len(parts) > 0 else "unknown"
1041
1223
  layer = "unknown"
1042
1224
 
1043
- # Try to detect layer from path
1044
1225
  if "domain" in str(py_file):
1045
1226
  layer = "domain"
1046
1227
  elif "application" in str(py_file):
@@ -1050,17 +1231,15 @@ class RegistryBuilder:
1050
1231
  elif "presentation" in str(py_file):
1051
1232
  layer = "presentation"
1052
1233
 
1053
- # Generate URN
1054
1234
  component = py_file.stem
1055
1235
  impl_urn = f"impl:{wagon}:{layer}:{component}:python"
1056
1236
 
1057
- # Build implementation entry
1058
1237
  impl_entry = {
1059
1238
  "urn": impl_urn,
1060
1239
  "file": str(py_file.relative_to(self.repo_root)),
1061
1240
  "wagon": wagon,
1062
1241
  "layer": layer,
1063
- "component_type": "entity", # Default
1242
+ "component_type": "entity",
1064
1243
  "language": "python"
1065
1244
  }
1066
1245
 
@@ -1069,7 +1248,6 @@ class RegistryBuilder:
1069
1248
  if test_urns:
1070
1249
  impl_entry["test_urn"] = test_urns[0]
1071
1250
 
1072
- # Track changes
1073
1251
  if impl_urn in existing_impls:
1074
1252
  stats["updated"] += 1
1075
1253
  else:
@@ -1087,45 +1265,39 @@ class RegistryBuilder:
1087
1265
  print(f" ⚠️ Error processing {py_file}: {e}")
1088
1266
  stats["errors"] += 1
1089
1267
 
1268
+ # Preserve draft implementations (file doesn't exist or draft: true)
1269
+ for urn, impl in existing_impls.items():
1270
+ is_draft = impl.get("draft", False)
1271
+ file_exists = impl.get("file") and (self.repo_root / impl.get("file")).exists()
1272
+ if is_draft or not file_exists:
1273
+ if urn not in [i.get("urn") for i in implementations]:
1274
+ implementations.append(impl)
1275
+ stats["preserved_drafts"] += 1
1276
+
1090
1277
  # Show preview
1091
1278
  print(f"\n📋 PREVIEW:")
1092
1279
  print(f" • {stats['updated']} implementations will be updated")
1093
1280
  print(f" • {stats['new']} new implementations will be added")
1281
+ print(f" • {stats['preserved_drafts']} draft implementations will be preserved")
1094
1282
  if stats["errors"] > 0:
1095
1283
  print(f" ⚠️ {stats['errors']} errors encountered")
1096
1284
 
1097
- if preview_only:
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
1285
+ # Use helper for confirm/apply
1112
1286
  output = {"implementations": implementations}
1113
- registry_path.parent.mkdir(parents=True, exist_ok=True)
1114
- with open(registry_path, "w") as f:
1115
- yaml.dump(output, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
1116
-
1117
- print(f"\n✅ Coder registry updated successfully!")
1118
- print(f" • Updated {stats['updated']} implementations")
1119
- print(f" • Added {stats['new']} new implementations")
1120
- print(f" 📝 Registry: {registry_path}")
1287
+ return self._confirm_and_apply(mode, "coder", registry_path, output, stats)
1121
1288
 
1122
- return stats
1123
-
1124
- def build_supabase(self, preview_only: bool = False) -> Dict[str, Any]:
1289
+ def build_supabase(self, mode: str = "interactive", preview_only: bool = None) -> Dict[str, Any]:
1125
1290
  """
1126
1291
  Build supabase functions registry.
1127
1292
  Scans supabase/functions/**/ for function directories.
1293
+
1294
+ Args:
1295
+ mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
1296
+ preview_only: Deprecated - use mode="check" instead
1128
1297
  """
1298
+ # Backwards compatibility
1299
+ if preview_only is not None:
1300
+ mode = "check" if preview_only else "interactive"
1129
1301
  print("\n📊 Analyzing supabase registry from function files...")
1130
1302
 
1131
1303
  # Load existing registry
@@ -1134,7 +1306,7 @@ class RegistryBuilder:
1134
1306
  if registry_path.exists():
1135
1307
  with open(registry_path) as f:
1136
1308
  registry_data = yaml.safe_load(f)
1137
- existing_funcs = {f.get("id"): f for f in registry_data.get("functions", [])}
1309
+ existing_funcs = {fn.get("id"): fn for fn in registry_data.get("functions", [])}
1138
1310
 
1139
1311
  functions = []
1140
1312
  stats = {
@@ -1143,6 +1315,7 @@ class RegistryBuilder:
1143
1315
  "updated": 0,
1144
1316
  "new": 0,
1145
1317
  "errors": 0,
1318
+ "preserved_drafts": 0,
1146
1319
  "changes": []
1147
1320
  }
1148
1321
 
@@ -1168,7 +1341,6 @@ class RegistryBuilder:
1168
1341
  "description": f"Supabase function: {func_id}"
1169
1342
  }
1170
1343
 
1171
- # Track changes
1172
1344
  if func_id in existing_funcs:
1173
1345
  stats["updated"] += 1
1174
1346
  else:
@@ -1186,37 +1358,24 @@ class RegistryBuilder:
1186
1358
  print(f" ⚠️ Error processing {func_dir}: {e}")
1187
1359
  stats["errors"] += 1
1188
1360
 
1361
+ # Preserve draft functions (path doesn't exist or draft: true)
1362
+ for func_id, func in existing_funcs.items():
1363
+ is_draft = func.get("draft", False)
1364
+ path_exists = func.get("path") and (self.repo_root / func.get("path")).exists()
1365
+ if is_draft or not path_exists:
1366
+ if func_id not in [fn.get("id") for fn in functions]:
1367
+ functions.append(func)
1368
+ stats["preserved_drafts"] += 1
1369
+
1189
1370
  # Show preview
1190
1371
  print(f"\n📋 PREVIEW:")
1191
1372
  print(f" • {stats['updated']} functions will be updated")
1192
1373
  print(f" • {stats['new']} new functions will be added")
1374
+ print(f" • {stats['preserved_drafts']} draft functions will be preserved")
1193
1375
 
1194
- if preview_only:
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
1376
+ # Use helper for confirm/apply
1209
1377
  output = {"functions": functions}
1210
- registry_path.parent.mkdir(parents=True, exist_ok=True)
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
1378
+ return self._confirm_and_apply(mode, "supabase", registry_path, output, stats)
1220
1379
 
1221
1380
  def build_python_manifest(self, preview_only: bool = False) -> Dict[str, Any]:
1222
1381
  """
@@ -1317,19 +1476,24 @@ class RegistryBuilder:
1317
1476
 
1318
1477
  return stats
1319
1478
 
1320
- def build_all(self) -> Dict[str, Any]:
1321
- """Build all registries."""
1479
+ def build_all(self, mode: str = "interactive") -> Dict[str, Any]:
1480
+ """Build all registries.
1481
+
1482
+ Args:
1483
+ mode: "interactive" (prompt), "apply" (no prompt), or "check" (verify only)
1484
+ """
1322
1485
  print("=" * 60)
1323
1486
  print("Unified Registry Builder - Synchronizing from source files")
1324
1487
  print("=" * 60)
1325
1488
 
1326
1489
  results = {
1327
- "plan": self.build_planner(),
1328
- "contracts": self.build_contracts(),
1329
- "telemetry": self.build_telemetry(),
1330
- "tester": self.build_tester(),
1331
- "coder": self.build_coder(),
1332
- "supabase": self.build_supabase()
1490
+ "plan": self.build_planner(mode),
1491
+ "trains": self.build_trains(mode),
1492
+ "contracts": self.build_contracts(mode),
1493
+ "telemetry": self.build_telemetry(mode),
1494
+ "tester": self.build_tester(mode),
1495
+ "coder": self.build_coder(mode),
1496
+ "supabase": self.build_supabase(mode)
1333
1497
  }
1334
1498
 
1335
1499
  print("\n" + "=" * 60)
@@ -348,7 +348,7 @@ Rules:
348
348
  -->
349
349
 
350
350
  - [ ] Determine change class: PATCH / MINOR / MAJOR
351
- - [ ] Bump version in version file (pyproject.toml, package.json, etc.)
351
+ - [ ] Bump version in version file (recommended: VERSION; sync any language manifests if used)
352
352
  - [ ] Commit: "Bump version to {version}"
353
353
  - [ ] Create tag: `git tag v{version}`
354
354
  - [ ] Push with tags: `git push origin {branch} --tags`
@@ -62,6 +62,12 @@ def _read_version_from_file(path: Path) -> str:
62
62
  version = _parse_package_json_version(path)
63
63
  else:
64
64
  version = _parse_plain_version(path)
65
+ if not version:
66
+ pytest.fail(
67
+ f"Could not read version from {path}. "
68
+ "Expected first non-comment line to contain a semver like "
69
+ "'1.2.3' or '1.2.3 - short summary'."
70
+ )
65
71
 
66
72
  if not version:
67
73
  pytest.fail(f"Could not read version from {path}")
@@ -127,11 +133,17 @@ def _parse_package_json_version(path: Path) -> Optional[str]:
127
133
 
128
134
 
129
135
  def _parse_plain_version(path: Path) -> Optional[str]:
136
+ version_pattern = re.compile(
137
+ r"\bv?(?P<version>\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\b"
138
+ )
130
139
  for line in path.read_text().splitlines():
131
140
  stripped = line.strip()
132
141
  if not stripped or stripped.startswith("#"):
133
142
  continue
134
- return stripped
143
+ match = version_pattern.search(stripped)
144
+ if match:
145
+ return match.group("version")
146
+ return None
135
147
  return None
136
148
 
137
149
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atdd
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: ATDD Platform - Acceptance Test Driven Development toolkit
5
5
  License: MIT
6
6
  Requires-Python: >=3.10
@@ -58,14 +58,20 @@ atdd --help
58
58
  ```bash
59
59
  atdd init # Initialize ATDD in your project
60
60
  atdd gate # ⚠️ START EVERY SESSION WITH THIS
61
- atdd session new <capability> # Create a planning session
61
+ atdd session new <task> # Create a planning session
62
62
  atdd sync # Sync rules to agent config files
63
63
  atdd validate # Run all validators
64
64
  ```
65
65
 
66
66
  > **⚠️ `atdd gate` is required.**
67
67
  > 🤖 Tell your agent: "Run `atdd gate` and follow ATDD rigorously."
68
- > Agents skip instruction files but can't ignore tool output. No gate = no ATDD guarantees.
68
+ > Agents skip instruction files but can't ignore tool ou
69
+
70
+
71
+
72
+
73
+
74
+ tput. No gate = no ATDD guarantees.
69
75
 
70
76
  ## What It Does
71
77
 
@@ -232,14 +238,16 @@ atdd validate --html # With HTML report
232
238
 
233
239
  ### Release Versioning
234
240
 
235
- ATDD enforces release versioning via coach validators. Configure the version file and tag prefix in `.atdd/config.yaml`:
241
+ ATDD enforces release versioning via coach validators. Recommended: keep a single root `VERSION` file as the canonical source (first line like `1.2.3 - short summary`; trailing summary is ignored). Configure the version file and tag prefix in `.atdd/config.yaml`:
236
242
 
237
243
  ```yaml
238
244
  release:
239
- version_file: "pyproject.toml" # or package.json, VERSION, etc.
245
+ version_file: "VERSION" # recommended single source of truth
240
246
  tag_prefix: "v"
241
247
  ```
242
248
 
249
+ If you also publish with language-specific manifests (e.g., `pyproject.toml`, `package.json`), keep their version fields in sync with `VERSION`.
250
+
243
251
  Validation (`atdd validate coach` or `atdd validate`) requires:
244
252
  - Version file exists and contains a version
245
253
  - Git tag on HEAD matches `{tag_prefix}{version}`
@@ -1,6 +1,6 @@
1
1
  atdd/__init__.py,sha256=-S8i9OahH-t9FJkPn6nprxipnjVum3rLeVsCS74T6eY,156
2
2
  atdd/__main__.py,sha256=B0sXDQLjFN9GowTlXo4NMWwPZPjDsrT8Frq7DnbdOD8,77
3
- atdd/cli.py,sha256=NlhWL8sRLBzISgyn2KM-KT_aYnMBHt_xNP3TqfMHIhA,20364
3
+ atdd/cli.py,sha256=5O5IazSmlAPyIDmtOXE1Wi7RT_-FmtagkzETmPszE9E,22506
4
4
  atdd/conftest.py,sha256=Fj3kIhCETbj2QBCIjySBgdS3stKNRZcZzKTJr7A4LaQ,5300
5
5
  atdd/version_check.py,sha256=BlPcLwzNnm457vWUolB3wMtZEI-fXvpzfm6p8U_j7rc,6684
6
6
  atdd/coach/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -14,7 +14,7 @@ atdd/coach/commands/initializer.py,sha256=wuvzj7QwA11ilNjRZU6Bx2bLQXITdBHJxR9_mZ
14
14
  atdd/coach/commands/interface.py,sha256=FwBrJpWkfSL9n4n0HT_EC-alseXgU0bweKD4TImyHN0,40483
15
15
  atdd/coach/commands/inventory.py,sha256=qU42MnkXt1JSBh5GU7pPSKmCO27Zfga7XwMT19RquJE,20969
16
16
  atdd/coach/commands/migration.py,sha256=wRxU7emvvHqWt1MvXKkNTkPBjp0sU9g8F5Uy5yV2YfI,8177
17
- atdd/coach/commands/registry.py,sha256=76-Pe3_cN483JR1pXUdDIE5WSZjWtVV0Jl8dRtRw_9Y,58349
17
+ atdd/coach/commands/registry.py,sha256=zE_Djgo4QKaDnp9JiPAh0D9wIv5Ed1yTmMEy31ipJkY,66736
18
18
  atdd/coach/commands/session.py,sha256=MhuWXd5TR6bB3w0t8vANeZx3L476qwLT6EUQMwg-wQA,14268
19
19
  atdd/coach/commands/sync.py,sha256=SLNzhcc6IuzMofMbkH9wM9rBSk5tPfcWPKXn9TaSZ-Y,13782
20
20
  atdd/coach/commands/test_interface.py,sha256=a7ut2Hhk0PnQ5LfJZkoQwfkfkVuB5OHA4QBwOS0-jcg,16870
@@ -28,7 +28,7 @@ atdd/coach/overlays/claude.md,sha256=33mhpqhmsRhCtdWlU7cMXAJDsaVra9uBBK8URV8OtQA
28
28
  atdd/coach/schemas/config.schema.json,sha256=CpePppEAB6WiLeWVgWW3EKOxlLvMHHcWisRnJL9z_SE,1863
29
29
  atdd/coach/schemas/manifest.schema.json,sha256=WO13-YF_FgH1awh96khCtk-112b6XSC24anlY3B7GjY,2885
30
30
  atdd/coach/templates/ATDD.md,sha256=MLbrVbCETJre4c05d5FXGuf6W95Hz9E0jpE4RI9r4cg,13237
31
- atdd/coach/templates/SESSION-TEMPLATE.md,sha256=gcmfDDD6rREI20vhWXlf01AGbRbR8Hh7Q4QZX4H-pVw,9455
31
+ atdd/coach/templates/SESSION-TEMPLATE.md,sha256=cGT_0x5KLbPHOCiuM8evLGpWKIlR-aggqxiBtbjSJoo,9478
32
32
  atdd/coach/utils/__init__.py,sha256=7Jbo-heJEKSAn6I0s35z_2S4R8qGZ48PL6a2IntcNYg,148
33
33
  atdd/coach/utils/repo.py,sha256=0kiF5WpVTen0nO14u5T0RflznZhgGco2i9CwKobOh38,3757
34
34
  atdd/coach/utils/graph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -37,7 +37,7 @@ atdd/coach/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
37
37
  atdd/coach/validators/shared_fixtures.py,sha256=tdqAb4675P-oOCL08mvSCG9XpmwMCjL9iSq1W5U7-wk,12558
38
38
  atdd/coach/validators/test_enrich_wagon_registry.py,sha256=WeTwYJqoNY6mEYc-QAvQo7YVagSOjaNKxB6Q6dpWqIM,6561
39
39
  atdd/coach/validators/test_registry.py,sha256=ffN70yA_1xxL3R8gdpGbY2M8dQXyuajIZhBZ-ylNiNs,17845
40
- atdd/coach/validators/test_release_versioning.py,sha256=-H2hCRfdikVP54LHNDqW9IDmm6JLNBUZRdnF2uICvOI,5194
40
+ atdd/coach/validators/test_release_versioning.py,sha256=B40DfbtrSGguPc537zXmjT75hhySfocWLzJWqOKZQcU,5678
41
41
  atdd/coach/validators/test_session_validation.py,sha256=0VszXtFwRTO04b5CxDPO3klk0VfiqlpdbNpshjMn-qU,39079
42
42
  atdd/coach/validators/test_traceability.py,sha256=qTyobt41VBiCr6xRN2C7BPtGYvk_2poVQIe814Blt8E,15977
43
43
  atdd/coach/validators/test_update_feature_paths.py,sha256=zOKVDgEIpncSJwDh_shyyou5Pu-Ai7Z_XgF8zAbQVTA,4528
@@ -182,9 +182,9 @@ atdd/tester/validators/test_red_supabase_layer_structure.py,sha256=zbUjsMWSJE1MP
182
182
  atdd/tester/validators/test_telemetry_structure.py,sha256=uU5frZnxSlOn60iHyqhe7Pg9b0wrOV7N14D4S6Aw6TE,22626
183
183
  atdd/tester/validators/test_typescript_test_naming.py,sha256=E-TyGv_GVlTfsbyuxrtv9sOWSZS_QcpH6rrJFbWoeeU,11280
184
184
  atdd/tester/validators/test_typescript_test_structure.py,sha256=eV89SD1RaKtchBZupqhnJmaruoROosf3LwB4Fwe4UJI,2612
185
- atdd-0.4.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
186
- atdd-0.4.4.dist-info/METADATA,sha256=cBMNKGmx5Uyw5kwn_v6ezv-CKbuFd5QpsZ6hFG_rg9k,8426
187
- atdd-0.4.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
188
- atdd-0.4.4.dist-info/entry_points.txt,sha256=-C3yrA1WQQfN3iuGmSzPapA5cKVBEYU5Q1HUffSJTbY,38
189
- atdd-0.4.4.dist-info/top_level.txt,sha256=VKkf6Uiyrm4RS6ULCGM-v8AzYN8K2yg8SMqwJLoO-xs,5
190
- atdd-0.4.4.dist-info/RECORD,,
185
+ atdd-0.4.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
186
+ atdd-0.4.6.dist-info/METADATA,sha256=krXgWDxwrC0W-HkHgBtc0GoE0aXyJWqhHGPpjm3G4T8,8716
187
+ atdd-0.4.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
188
+ atdd-0.4.6.dist-info/entry_points.txt,sha256=-C3yrA1WQQfN3iuGmSzPapA5cKVBEYU5Q1HUffSJTbY,38
189
+ atdd-0.4.6.dist-info/top_level.txt,sha256=VKkf6Uiyrm4RS6ULCGM-v8AzYN8K2yg8SMqwJLoO-xs,5
190
+ atdd-0.4.6.dist-info/RECORD,,
File without changes