atdd 0.1.0__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.
Files changed (183) hide show
  1. atdd/__init__.py +0 -0
  2. atdd/cli.py +404 -0
  3. atdd/coach/__init__.py +0 -0
  4. atdd/coach/commands/__init__.py +0 -0
  5. atdd/coach/commands/add_persistence_metadata.py +215 -0
  6. atdd/coach/commands/analyze_migrations.py +188 -0
  7. atdd/coach/commands/consumers.py +720 -0
  8. atdd/coach/commands/infer_governance_status.py +149 -0
  9. atdd/coach/commands/initializer.py +177 -0
  10. atdd/coach/commands/interface.py +1078 -0
  11. atdd/coach/commands/inventory.py +565 -0
  12. atdd/coach/commands/migration.py +240 -0
  13. atdd/coach/commands/registry.py +1560 -0
  14. atdd/coach/commands/session.py +430 -0
  15. atdd/coach/commands/sync.py +405 -0
  16. atdd/coach/commands/test_interface.py +399 -0
  17. atdd/coach/commands/test_runner.py +141 -0
  18. atdd/coach/commands/tests/__init__.py +1 -0
  19. atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
  20. atdd/coach/commands/traceability.py +4264 -0
  21. atdd/coach/conventions/session.convention.yaml +754 -0
  22. atdd/coach/overlays/__init__.py +2 -0
  23. atdd/coach/overlays/claude.md +2 -0
  24. atdd/coach/schemas/config.schema.json +34 -0
  25. atdd/coach/schemas/manifest.schema.json +101 -0
  26. atdd/coach/templates/ATDD.md +282 -0
  27. atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
  28. atdd/coach/utils/__init__.py +0 -0
  29. atdd/coach/utils/graph/__init__.py +0 -0
  30. atdd/coach/utils/graph/urn.py +875 -0
  31. atdd/coach/validators/__init__.py +0 -0
  32. atdd/coach/validators/shared_fixtures.py +365 -0
  33. atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
  34. atdd/coach/validators/test_registry.py +575 -0
  35. atdd/coach/validators/test_session_validation.py +1183 -0
  36. atdd/coach/validators/test_traceability.py +448 -0
  37. atdd/coach/validators/test_update_feature_paths.py +108 -0
  38. atdd/coach/validators/test_validate_contract_consumers.py +297 -0
  39. atdd/coder/__init__.py +1 -0
  40. atdd/coder/conventions/adapter.recipe.yaml +88 -0
  41. atdd/coder/conventions/backend.convention.yaml +460 -0
  42. atdd/coder/conventions/boundaries.convention.yaml +666 -0
  43. atdd/coder/conventions/commons.convention.yaml +460 -0
  44. atdd/coder/conventions/complexity.recipe.yaml +109 -0
  45. atdd/coder/conventions/component-naming.convention.yaml +178 -0
  46. atdd/coder/conventions/design.convention.yaml +327 -0
  47. atdd/coder/conventions/design.recipe.yaml +273 -0
  48. atdd/coder/conventions/dto.convention.yaml +660 -0
  49. atdd/coder/conventions/frontend.convention.yaml +542 -0
  50. atdd/coder/conventions/green.convention.yaml +1012 -0
  51. atdd/coder/conventions/presentation.convention.yaml +587 -0
  52. atdd/coder/conventions/refactor.convention.yaml +535 -0
  53. atdd/coder/conventions/technology.convention.yaml +206 -0
  54. atdd/coder/conventions/tests/__init__.py +0 -0
  55. atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
  56. atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
  57. atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
  58. atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
  59. atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
  60. atdd/coder/conventions/thinness.recipe.yaml +82 -0
  61. atdd/coder/conventions/train.convention.yaml +325 -0
  62. atdd/coder/conventions/verification.protocol.yaml +53 -0
  63. atdd/coder/schemas/design_system.schema.json +361 -0
  64. atdd/coder/validators/__init__.py +0 -0
  65. atdd/coder/validators/test_commons_structure.py +485 -0
  66. atdd/coder/validators/test_complexity.py +416 -0
  67. atdd/coder/validators/test_cross_language_consistency.py +431 -0
  68. atdd/coder/validators/test_design_system_compliance.py +413 -0
  69. atdd/coder/validators/test_dto_testing_patterns.py +268 -0
  70. atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
  71. atdd/coder/validators/test_green_layer_dependencies.py +148 -0
  72. atdd/coder/validators/test_green_python_layer_structure.py +103 -0
  73. atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
  74. atdd/coder/validators/test_import_boundaries.py +396 -0
  75. atdd/coder/validators/test_init_file_urns.py +593 -0
  76. atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
  77. atdd/coder/validators/test_presentation_convention.py +260 -0
  78. atdd/coder/validators/test_python_architecture.py +674 -0
  79. atdd/coder/validators/test_quality_metrics.py +420 -0
  80. atdd/coder/validators/test_station_master_pattern.py +244 -0
  81. atdd/coder/validators/test_train_infrastructure.py +454 -0
  82. atdd/coder/validators/test_train_urns.py +293 -0
  83. atdd/coder/validators/test_typescript_architecture.py +616 -0
  84. atdd/coder/validators/test_usecase_structure.py +421 -0
  85. atdd/coder/validators/test_wagon_boundaries.py +586 -0
  86. atdd/conftest.py +126 -0
  87. atdd/planner/__init__.py +1 -0
  88. atdd/planner/conventions/acceptance.convention.yaml +538 -0
  89. atdd/planner/conventions/appendix.convention.yaml +187 -0
  90. atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
  91. atdd/planner/conventions/component.convention.yaml +670 -0
  92. atdd/planner/conventions/criteria.convention.yaml +141 -0
  93. atdd/planner/conventions/feature.convention.yaml +371 -0
  94. atdd/planner/conventions/interface.convention.yaml +382 -0
  95. atdd/planner/conventions/steps.convention.yaml +141 -0
  96. atdd/planner/conventions/train.convention.yaml +552 -0
  97. atdd/planner/conventions/wagon.convention.yaml +275 -0
  98. atdd/planner/conventions/wmbt.convention.yaml +258 -0
  99. atdd/planner/schemas/acceptance.schema.json +336 -0
  100. atdd/planner/schemas/appendix.schema.json +78 -0
  101. atdd/planner/schemas/component.schema.json +114 -0
  102. atdd/planner/schemas/feature.schema.json +197 -0
  103. atdd/planner/schemas/train.schema.json +192 -0
  104. atdd/planner/schemas/wagon.schema.json +281 -0
  105. atdd/planner/schemas/wmbt.schema.json +59 -0
  106. atdd/planner/validators/__init__.py +0 -0
  107. atdd/planner/validators/conftest.py +5 -0
  108. atdd/planner/validators/test_draft_wagon_registry.py +374 -0
  109. atdd/planner/validators/test_plan_cross_refs.py +240 -0
  110. atdd/planner/validators/test_plan_uniqueness.py +224 -0
  111. atdd/planner/validators/test_plan_urn_resolution.py +268 -0
  112. atdd/planner/validators/test_plan_wagons.py +174 -0
  113. atdd/planner/validators/test_train_validation.py +514 -0
  114. atdd/planner/validators/test_wagon_urn_chain.py +648 -0
  115. atdd/planner/validators/test_wmbt_consistency.py +327 -0
  116. atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
  117. atdd/tester/__init__.py +1 -0
  118. atdd/tester/conventions/artifact.convention.yaml +257 -0
  119. atdd/tester/conventions/contract.convention.yaml +1009 -0
  120. atdd/tester/conventions/filename.convention.yaml +555 -0
  121. atdd/tester/conventions/migration.convention.yaml +509 -0
  122. atdd/tester/conventions/red.convention.yaml +797 -0
  123. atdd/tester/conventions/routing.convention.yaml +51 -0
  124. atdd/tester/conventions/telemetry.convention.yaml +458 -0
  125. atdd/tester/schemas/a11y.tmpl.json +17 -0
  126. atdd/tester/schemas/artifact.schema.json +189 -0
  127. atdd/tester/schemas/contract.schema.json +591 -0
  128. atdd/tester/schemas/contract.tmpl.json +95 -0
  129. atdd/tester/schemas/db.tmpl.json +20 -0
  130. atdd/tester/schemas/e2e.tmpl.json +17 -0
  131. atdd/tester/schemas/edge_function.tmpl.json +17 -0
  132. atdd/tester/schemas/event.tmpl.json +17 -0
  133. atdd/tester/schemas/http.tmpl.json +19 -0
  134. atdd/tester/schemas/job.tmpl.json +18 -0
  135. atdd/tester/schemas/load.tmpl.json +21 -0
  136. atdd/tester/schemas/metric.tmpl.json +19 -0
  137. atdd/tester/schemas/pack.schema.json +139 -0
  138. atdd/tester/schemas/realtime.tmpl.json +20 -0
  139. atdd/tester/schemas/rls.tmpl.json +18 -0
  140. atdd/tester/schemas/script.tmpl.json +16 -0
  141. atdd/tester/schemas/sec.tmpl.json +18 -0
  142. atdd/tester/schemas/storage.tmpl.json +18 -0
  143. atdd/tester/schemas/telemetry.schema.json +128 -0
  144. atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
  145. atdd/tester/schemas/test_filename.schema.json +194 -0
  146. atdd/tester/schemas/test_intent.schema.json +179 -0
  147. atdd/tester/schemas/unit.tmpl.json +18 -0
  148. atdd/tester/schemas/visual.tmpl.json +18 -0
  149. atdd/tester/schemas/ws.tmpl.json +17 -0
  150. atdd/tester/utils/__init__.py +0 -0
  151. atdd/tester/utils/filename.py +300 -0
  152. atdd/tester/validators/__init__.py +0 -0
  153. atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
  154. atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
  155. atdd/tester/validators/conftest.py +5 -0
  156. atdd/tester/validators/coverage_gap_report.py +321 -0
  157. atdd/tester/validators/fix_dual_ac_references.py +179 -0
  158. atdd/tester/validators/remove_duplicate_lines.py +93 -0
  159. atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
  160. atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
  161. atdd/tester/validators/test_artifact_naming_category.py +307 -0
  162. atdd/tester/validators/test_contract_schema_compliance.py +706 -0
  163. atdd/tester/validators/test_contracts_structure.py +200 -0
  164. atdd/tester/validators/test_coverage_adequacy.py +797 -0
  165. atdd/tester/validators/test_dual_ac_reference.py +225 -0
  166. atdd/tester/validators/test_fixture_validity.py +372 -0
  167. atdd/tester/validators/test_isolation.py +487 -0
  168. atdd/tester/validators/test_migration_coverage.py +204 -0
  169. atdd/tester/validators/test_migration_criteria.py +276 -0
  170. atdd/tester/validators/test_migration_generation.py +116 -0
  171. atdd/tester/validators/test_python_test_naming.py +410 -0
  172. atdd/tester/validators/test_red_layer_validation.py +95 -0
  173. atdd/tester/validators/test_red_python_layer_structure.py +87 -0
  174. atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
  175. atdd/tester/validators/test_telemetry_structure.py +634 -0
  176. atdd/tester/validators/test_typescript_test_naming.py +301 -0
  177. atdd/tester/validators/test_typescript_test_structure.py +84 -0
  178. atdd-0.1.0.dist-info/METADATA +191 -0
  179. atdd-0.1.0.dist-info/RECORD +183 -0
  180. atdd-0.1.0.dist-info/WHEEL +5 -0
  181. atdd-0.1.0.dist-info/entry_points.txt +2 -0
  182. atdd-0.1.0.dist-info/licenses/LICENSE +674 -0
  183. atdd-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,430 @@
1
+ """
2
+ Session management for ATDD sessions.
3
+
4
+ Manages session files in atdd-sessions/ directory:
5
+ - Create new sessions from template
6
+ - List sessions from manifest
7
+ - Archive completed sessions
8
+
9
+ Usage:
10
+ atdd session new my-feature # Create SESSION-NN-my-feature.md
11
+ atdd session new my-feature --type migration # Specify session type
12
+ atdd session list # List all sessions
13
+ atdd session archive 01 # Archive SESSION-01-*.md
14
+
15
+ Convention: src/atdd/coach/conventions/session.convention.yaml
16
+ """
17
+ import re
18
+ import shutil
19
+ from datetime import date
20
+ from pathlib import Path
21
+ from typing import Dict, List, Optional, Any
22
+
23
+ import yaml
24
+
25
+
26
+ class SessionManager:
27
+ """Manage session files."""
28
+
29
+ VALID_TYPES = {
30
+ "implementation",
31
+ "migration",
32
+ "refactor",
33
+ "analysis",
34
+ "planning",
35
+ "cleanup",
36
+ "tracking",
37
+ }
38
+
39
+ def __init__(self, target_dir: Optional[Path] = None):
40
+ """
41
+ Initialize the SessionManager.
42
+
43
+ Args:
44
+ target_dir: Target directory containing atdd-sessions/. Defaults to cwd.
45
+ """
46
+ self.target_dir = target_dir or Path.cwd()
47
+ self.sessions_dir = self.target_dir / "atdd-sessions"
48
+ self.archive_dir = self.sessions_dir / "archive"
49
+ self.atdd_config_dir = self.target_dir / ".atdd"
50
+ self.manifest_file = self.atdd_config_dir / "manifest.yaml"
51
+
52
+ # Package template location
53
+ self.package_root = Path(__file__).parent.parent # src/atdd/coach
54
+ self.template_source = self.package_root / "templates" / "SESSION-TEMPLATE.md"
55
+
56
+ def _check_initialized(self) -> bool:
57
+ """Check if ATDD is initialized."""
58
+ if not self.sessions_dir.exists():
59
+ print(f"Error: ATDD not initialized. Run 'atdd init' first.")
60
+ print(f"Expected: {self.sessions_dir}")
61
+ return False
62
+ if not self.manifest_file.exists():
63
+ print(f"Error: Manifest not found. Run 'atdd init' first.")
64
+ print(f"Expected: {self.manifest_file}")
65
+ return False
66
+ return True
67
+
68
+ def _load_manifest(self) -> Dict[str, Any]:
69
+ """Load the manifest.yaml file."""
70
+ with open(self.manifest_file) as f:
71
+ return yaml.safe_load(f) or {}
72
+
73
+ def _save_manifest(self, manifest: Dict[str, Any]) -> None:
74
+ """Save the manifest.yaml file."""
75
+ with open(self.manifest_file, "w") as f:
76
+ yaml.dump(manifest, f, default_flow_style=False, sort_keys=False)
77
+
78
+ def _get_next_session_number(self, manifest: Dict[str, Any]) -> str:
79
+ """Get the next available session number."""
80
+ sessions = manifest.get("sessions", [])
81
+ if not sessions:
82
+ return "01"
83
+
84
+ # Find the highest session number
85
+ max_num = 0
86
+ for session in sessions:
87
+ session_id = session.get("id", "00")
88
+ try:
89
+ num = int(session_id)
90
+ if num > max_num:
91
+ max_num = num
92
+ except ValueError:
93
+ continue
94
+
95
+ # Also check for session files not in manifest
96
+ for f in self.sessions_dir.glob("SESSION-*.md"):
97
+ match = re.match(r"SESSION-(\d+)-", f.name)
98
+ if match:
99
+ try:
100
+ num = int(match.group(1))
101
+ if num > max_num:
102
+ max_num = num
103
+ except ValueError:
104
+ continue
105
+
106
+ return f"{max_num + 1:02d}"
107
+
108
+ def _slugify(self, text: str) -> str:
109
+ """Convert text to kebab-case slug."""
110
+ # Convert to lowercase
111
+ slug = text.lower()
112
+ # Replace spaces and underscores with hyphens
113
+ slug = re.sub(r"[\s_]+", "-", slug)
114
+ # Remove non-alphanumeric characters except hyphens
115
+ slug = re.sub(r"[^a-z0-9-]", "", slug)
116
+ # Remove consecutive hyphens
117
+ slug = re.sub(r"-+", "-", slug)
118
+ # Remove leading/trailing hyphens
119
+ slug = slug.strip("-")
120
+ return slug
121
+
122
+ def new(self, slug: str, session_type: str = "implementation") -> int:
123
+ """
124
+ Create new session from template.
125
+
126
+ Args:
127
+ slug: Session slug (will be converted to kebab-case).
128
+ session_type: Type of session (implementation, migration, etc.).
129
+
130
+ Returns:
131
+ 0 on success, 1 on error.
132
+ """
133
+ if not self._check_initialized():
134
+ return 1
135
+
136
+ # Validate session type
137
+ if session_type not in self.VALID_TYPES:
138
+ print(f"Error: Invalid session type '{session_type}'")
139
+ print(f"Valid types: {', '.join(sorted(self.VALID_TYPES))}")
140
+ return 1
141
+
142
+ # Load manifest
143
+ manifest = self._load_manifest()
144
+
145
+ # Get next session number
146
+ session_num = self._get_next_session_number(manifest)
147
+
148
+ # Slugify the name
149
+ slug = self._slugify(slug)
150
+ if not slug:
151
+ print("Error: Invalid slug - results in empty string")
152
+ return 1
153
+
154
+ # Generate filename
155
+ filename = f"SESSION-{session_num}-{slug}.md"
156
+ session_path = self.sessions_dir / filename
157
+
158
+ if session_path.exists():
159
+ print(f"Error: Session already exists: {session_path}")
160
+ return 1
161
+
162
+ # Read template
163
+ if not self.template_source.exists():
164
+ print(f"Error: Template not found: {self.template_source}")
165
+ return 1
166
+
167
+ template_content = self.template_source.read_text()
168
+
169
+ # Replace placeholders in template
170
+ today = date.today().isoformat()
171
+ title = slug.replace("-", " ").title()
172
+
173
+ # Replace frontmatter placeholders
174
+ content = template_content
175
+ content = re.sub(r'session:\s*"\{NN\}"', f'session: "{session_num}"', content)
176
+ content = re.sub(r'title:\s*"\{Title\}"', f'title: "{title}"', content)
177
+ content = re.sub(r'date:\s*"\{YYYY-MM-DD\}"', f'date: "{today}"', content)
178
+ content = re.sub(r'type:\s*"\{type\}"', f'type: "{session_type}"', content)
179
+
180
+ # Replace markdown header
181
+ content = re.sub(
182
+ r"# SESSION-\{NN\}: \{Title\}",
183
+ f"# SESSION-{session_num}: {title}",
184
+ content,
185
+ )
186
+
187
+ # Write session file
188
+ session_path.write_text(content)
189
+ print(f"Created: {session_path}")
190
+
191
+ # Update manifest
192
+ session_entry = {
193
+ "id": session_num,
194
+ "slug": slug,
195
+ "file": filename,
196
+ "type": session_type,
197
+ "status": "INIT",
198
+ "created": today,
199
+ "archived": None,
200
+ }
201
+
202
+ if "sessions" not in manifest:
203
+ manifest["sessions"] = []
204
+ manifest["sessions"].append(session_entry)
205
+
206
+ self._save_manifest(manifest)
207
+ print(f"Updated: {self.manifest_file}")
208
+
209
+ print(f"\nSession created: {filename}")
210
+ print(f" Type: {session_type}")
211
+ print(f" Status: INIT")
212
+ print(f"\nNext: Edit {session_path} and update status to PLANNED")
213
+
214
+ return 0
215
+
216
+ def list(self) -> int:
217
+ """
218
+ List sessions from manifest.
219
+
220
+ Returns:
221
+ 0 on success, 1 on error.
222
+ """
223
+ if not self._check_initialized():
224
+ return 1
225
+
226
+ manifest = self._load_manifest()
227
+ sessions = manifest.get("sessions", [])
228
+
229
+ if not sessions:
230
+ print("No sessions found.")
231
+ print("Create one with: atdd session new my-feature")
232
+ return 0
233
+
234
+ # Print header
235
+ print("\n" + "=" * 70)
236
+ print("ATDD Sessions")
237
+ print("=" * 70)
238
+ print(f"{'ID':<4} {'Status':<10} {'Type':<15} {'File':<40}")
239
+ print("-" * 70)
240
+
241
+ # Group by status
242
+ active = []
243
+ archived = []
244
+
245
+ for session in sessions:
246
+ if session.get("archived"):
247
+ archived.append(session)
248
+ else:
249
+ active.append(session)
250
+
251
+ # Print active sessions
252
+ for session in active:
253
+ session_id = session.get("id", "??")
254
+ status = session.get("status", "UNKNOWN")
255
+ session_type = session.get("type", "unknown")
256
+ filename = session.get("file", "unknown")
257
+
258
+ print(f"{session_id:<4} {status:<10} {session_type:<15} {filename:<40}")
259
+
260
+ if archived:
261
+ print("\n--- Archived ---")
262
+ for session in archived:
263
+ session_id = session.get("id", "??")
264
+ status = session.get("status", "UNKNOWN")
265
+ session_type = session.get("type", "unknown")
266
+ filename = session.get("file", "unknown")
267
+
268
+ print(f"{session_id:<4} {status:<10} {session_type:<15} {filename:<40}")
269
+
270
+ print("-" * 70)
271
+ print(f"Total: {len(sessions)} sessions ({len(active)} active, {len(archived)} archived)")
272
+
273
+ return 0
274
+
275
+ def archive(self, session_id: str) -> int:
276
+ """
277
+ Move session to archive/.
278
+
279
+ Args:
280
+ session_id: Session ID (e.g., "01" or "1").
281
+
282
+ Returns:
283
+ 0 on success, 1 on error.
284
+ """
285
+ if not self._check_initialized():
286
+ return 1
287
+
288
+ # Normalize session ID to 2-digit
289
+ try:
290
+ session_num = int(session_id)
291
+ session_id_normalized = f"{session_num:02d}"
292
+ except ValueError:
293
+ print(f"Error: Invalid session ID '{session_id}'")
294
+ return 1
295
+
296
+ # Load manifest
297
+ manifest = self._load_manifest()
298
+ sessions = manifest.get("sessions", [])
299
+
300
+ # Find session in manifest
301
+ session_entry = None
302
+ session_index = None
303
+ for i, s in enumerate(sessions):
304
+ if s.get("id") == session_id_normalized:
305
+ session_entry = s
306
+ session_index = i
307
+ break
308
+
309
+ if session_entry is None:
310
+ print(f"Error: Session {session_id_normalized} not found in manifest")
311
+ return 1
312
+
313
+ if session_entry.get("archived"):
314
+ print(f"Error: Session {session_id_normalized} is already archived")
315
+ return 1
316
+
317
+ # Find session file
318
+ filename = session_entry.get("file")
319
+ session_path = self.sessions_dir / filename
320
+
321
+ if not session_path.exists():
322
+ # Try to find file by pattern
323
+ pattern = f"SESSION-{session_id_normalized}-*.md"
324
+ matches = list(self.sessions_dir.glob(pattern))
325
+ if matches:
326
+ session_path = matches[0]
327
+ filename = session_path.name
328
+ else:
329
+ print(f"Error: Session file not found: {filename}")
330
+ return 1
331
+
332
+ # Ensure archive directory exists
333
+ self.archive_dir.mkdir(parents=True, exist_ok=True)
334
+
335
+ # Move file to archive
336
+ archive_path = self.archive_dir / filename
337
+ shutil.move(str(session_path), str(archive_path))
338
+ print(f"Moved: {session_path} -> {archive_path}")
339
+
340
+ # Update manifest
341
+ session_entry["archived"] = date.today().isoformat()
342
+ session_entry["file"] = f"archive/{filename}"
343
+ manifest["sessions"][session_index] = session_entry
344
+
345
+ self._save_manifest(manifest)
346
+ print(f"Updated: {self.manifest_file}")
347
+
348
+ print(f"\nSession {session_id_normalized} archived successfully")
349
+
350
+ return 0
351
+
352
+ def sync(self) -> int:
353
+ """
354
+ Sync manifest with actual session files.
355
+
356
+ Scans atdd-sessions/ and updates manifest to match actual files.
357
+
358
+ Returns:
359
+ 0 on success, 1 on error.
360
+ """
361
+ if not self._check_initialized():
362
+ return 1
363
+
364
+ manifest = self._load_manifest()
365
+ existing_sessions = {s.get("file"): s for s in manifest.get("sessions", [])}
366
+
367
+ # Scan for session files
368
+ found_files = set()
369
+ new_sessions = []
370
+
371
+ # Scan main directory
372
+ for f in self.sessions_dir.glob("SESSION-*.md"):
373
+ if f.name == "SESSION-TEMPLATE.md":
374
+ continue
375
+
376
+ found_files.add(f.name)
377
+
378
+ if f.name not in existing_sessions:
379
+ # Parse filename to extract info
380
+ match = re.match(r"SESSION-(\d+)-(.+)\.md", f.name)
381
+ if match:
382
+ session_id = match.group(1)
383
+ slug = match.group(2)
384
+
385
+ new_sessions.append({
386
+ "id": session_id,
387
+ "slug": slug,
388
+ "file": f.name,
389
+ "type": "unknown",
390
+ "status": "UNKNOWN",
391
+ "created": date.today().isoformat(),
392
+ "archived": None,
393
+ })
394
+
395
+ # Scan archive directory
396
+ if self.archive_dir.exists():
397
+ for f in self.archive_dir.glob("SESSION-*.md"):
398
+ archive_path = f"archive/{f.name}"
399
+ found_files.add(archive_path)
400
+
401
+ if archive_path not in existing_sessions:
402
+ match = re.match(r"SESSION-(\d+)-(.+)\.md", f.name)
403
+ if match:
404
+ session_id = match.group(1)
405
+ slug = match.group(2)
406
+
407
+ new_sessions.append({
408
+ "id": session_id,
409
+ "slug": slug,
410
+ "file": archive_path,
411
+ "type": "unknown",
412
+ "status": "UNKNOWN",
413
+ "created": date.today().isoformat(),
414
+ "archived": date.today().isoformat(),
415
+ })
416
+
417
+ # Add new sessions to manifest
418
+ if new_sessions:
419
+ manifest["sessions"] = manifest.get("sessions", []) + new_sessions
420
+ print(f"Added {len(new_sessions)} new session(s) to manifest")
421
+
422
+ # Report missing files
423
+ for filename, session in existing_sessions.items():
424
+ if filename not in found_files and f"archive/{filename}" not in found_files:
425
+ print(f"Warning: Session file not found: {filename}")
426
+
427
+ self._save_manifest(manifest)
428
+ print(f"Manifest synced: {self.manifest_file}")
429
+
430
+ return 0