atdd 0.2.1__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/__init__.py +6 -0
- atdd/__main__.py +4 -0
- atdd/cli.py +404 -0
- atdd/coach/__init__.py +0 -0
- atdd/coach/commands/__init__.py +0 -0
- atdd/coach/commands/add_persistence_metadata.py +215 -0
- atdd/coach/commands/analyze_migrations.py +188 -0
- atdd/coach/commands/consumers.py +720 -0
- atdd/coach/commands/infer_governance_status.py +149 -0
- atdd/coach/commands/initializer.py +177 -0
- atdd/coach/commands/interface.py +1078 -0
- atdd/coach/commands/inventory.py +565 -0
- atdd/coach/commands/migration.py +240 -0
- atdd/coach/commands/registry.py +1560 -0
- atdd/coach/commands/session.py +430 -0
- atdd/coach/commands/sync.py +405 -0
- atdd/coach/commands/test_interface.py +399 -0
- atdd/coach/commands/test_runner.py +141 -0
- atdd/coach/commands/tests/__init__.py +1 -0
- atdd/coach/commands/tests/test_telemetry_array_validation.py +235 -0
- atdd/coach/commands/traceability.py +4264 -0
- atdd/coach/conventions/session.convention.yaml +754 -0
- atdd/coach/overlays/__init__.py +2 -0
- atdd/coach/overlays/claude.md +2 -0
- atdd/coach/schemas/config.schema.json +34 -0
- atdd/coach/schemas/manifest.schema.json +101 -0
- atdd/coach/templates/ATDD.md +282 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +327 -0
- atdd/coach/utils/__init__.py +0 -0
- atdd/coach/utils/graph/__init__.py +0 -0
- atdd/coach/utils/graph/urn.py +875 -0
- atdd/coach/validators/__init__.py +0 -0
- atdd/coach/validators/shared_fixtures.py +365 -0
- atdd/coach/validators/test_enrich_wagon_registry.py +167 -0
- atdd/coach/validators/test_registry.py +575 -0
- atdd/coach/validators/test_session_validation.py +1183 -0
- atdd/coach/validators/test_traceability.py +448 -0
- atdd/coach/validators/test_update_feature_paths.py +108 -0
- atdd/coach/validators/test_validate_contract_consumers.py +297 -0
- atdd/coder/__init__.py +1 -0
- atdd/coder/conventions/adapter.recipe.yaml +88 -0
- atdd/coder/conventions/backend.convention.yaml +460 -0
- atdd/coder/conventions/boundaries.convention.yaml +666 -0
- atdd/coder/conventions/commons.convention.yaml +460 -0
- atdd/coder/conventions/complexity.recipe.yaml +109 -0
- atdd/coder/conventions/component-naming.convention.yaml +178 -0
- atdd/coder/conventions/design.convention.yaml +327 -0
- atdd/coder/conventions/design.recipe.yaml +273 -0
- atdd/coder/conventions/dto.convention.yaml +660 -0
- atdd/coder/conventions/frontend.convention.yaml +542 -0
- atdd/coder/conventions/green.convention.yaml +1012 -0
- atdd/coder/conventions/presentation.convention.yaml +587 -0
- atdd/coder/conventions/refactor.convention.yaml +535 -0
- atdd/coder/conventions/technology.convention.yaml +206 -0
- atdd/coder/conventions/tests/__init__.py +0 -0
- atdd/coder/conventions/tests/test_adapter_recipe.py +302 -0
- atdd/coder/conventions/tests/test_complexity_recipe.py +289 -0
- atdd/coder/conventions/tests/test_component_taxonomy.py +278 -0
- atdd/coder/conventions/tests/test_component_urn_naming.py +165 -0
- atdd/coder/conventions/tests/test_thinness_recipe.py +286 -0
- atdd/coder/conventions/thinness.recipe.yaml +82 -0
- atdd/coder/conventions/train.convention.yaml +325 -0
- atdd/coder/conventions/verification.protocol.yaml +53 -0
- atdd/coder/schemas/design_system.schema.json +361 -0
- atdd/coder/validators/__init__.py +0 -0
- atdd/coder/validators/test_commons_structure.py +485 -0
- atdd/coder/validators/test_complexity.py +416 -0
- atdd/coder/validators/test_cross_language_consistency.py +431 -0
- atdd/coder/validators/test_design_system_compliance.py +413 -0
- atdd/coder/validators/test_dto_testing_patterns.py +268 -0
- atdd/coder/validators/test_green_cross_stack_layers.py +168 -0
- atdd/coder/validators/test_green_layer_dependencies.py +148 -0
- atdd/coder/validators/test_green_python_layer_structure.py +103 -0
- atdd/coder/validators/test_green_supabase_layer_structure.py +103 -0
- atdd/coder/validators/test_import_boundaries.py +396 -0
- atdd/coder/validators/test_init_file_urns.py +593 -0
- atdd/coder/validators/test_preact_layer_boundaries.py +221 -0
- atdd/coder/validators/test_presentation_convention.py +260 -0
- atdd/coder/validators/test_python_architecture.py +674 -0
- atdd/coder/validators/test_quality_metrics.py +420 -0
- atdd/coder/validators/test_station_master_pattern.py +244 -0
- atdd/coder/validators/test_train_infrastructure.py +454 -0
- atdd/coder/validators/test_train_urns.py +293 -0
- atdd/coder/validators/test_typescript_architecture.py +616 -0
- atdd/coder/validators/test_usecase_structure.py +421 -0
- atdd/coder/validators/test_wagon_boundaries.py +586 -0
- atdd/conftest.py +126 -0
- atdd/planner/__init__.py +1 -0
- atdd/planner/conventions/acceptance.convention.yaml +538 -0
- atdd/planner/conventions/appendix.convention.yaml +187 -0
- atdd/planner/conventions/artifact-naming.convention.yaml +852 -0
- atdd/planner/conventions/component.convention.yaml +670 -0
- atdd/planner/conventions/criteria.convention.yaml +141 -0
- atdd/planner/conventions/feature.convention.yaml +371 -0
- atdd/planner/conventions/interface.convention.yaml +382 -0
- atdd/planner/conventions/steps.convention.yaml +141 -0
- atdd/planner/conventions/train.convention.yaml +552 -0
- atdd/planner/conventions/wagon.convention.yaml +275 -0
- atdd/planner/conventions/wmbt.convention.yaml +258 -0
- atdd/planner/schemas/acceptance.schema.json +336 -0
- atdd/planner/schemas/appendix.schema.json +78 -0
- atdd/planner/schemas/component.schema.json +114 -0
- atdd/planner/schemas/feature.schema.json +197 -0
- atdd/planner/schemas/train.schema.json +192 -0
- atdd/planner/schemas/wagon.schema.json +281 -0
- atdd/planner/schemas/wmbt.schema.json +59 -0
- atdd/planner/validators/__init__.py +0 -0
- atdd/planner/validators/conftest.py +5 -0
- atdd/planner/validators/test_draft_wagon_registry.py +374 -0
- atdd/planner/validators/test_plan_cross_refs.py +240 -0
- atdd/planner/validators/test_plan_uniqueness.py +224 -0
- atdd/planner/validators/test_plan_urn_resolution.py +268 -0
- atdd/planner/validators/test_plan_wagons.py +174 -0
- atdd/planner/validators/test_train_validation.py +514 -0
- atdd/planner/validators/test_wagon_urn_chain.py +648 -0
- atdd/planner/validators/test_wmbt_consistency.py +327 -0
- atdd/planner/validators/test_wmbt_vocabulary.py +632 -0
- atdd/tester/__init__.py +1 -0
- atdd/tester/conventions/artifact.convention.yaml +257 -0
- atdd/tester/conventions/contract.convention.yaml +1009 -0
- atdd/tester/conventions/filename.convention.yaml +555 -0
- atdd/tester/conventions/migration.convention.yaml +509 -0
- atdd/tester/conventions/red.convention.yaml +797 -0
- atdd/tester/conventions/routing.convention.yaml +51 -0
- atdd/tester/conventions/telemetry.convention.yaml +458 -0
- atdd/tester/schemas/a11y.tmpl.json +17 -0
- atdd/tester/schemas/artifact.schema.json +189 -0
- atdd/tester/schemas/contract.schema.json +591 -0
- atdd/tester/schemas/contract.tmpl.json +95 -0
- atdd/tester/schemas/db.tmpl.json +20 -0
- atdd/tester/schemas/e2e.tmpl.json +17 -0
- atdd/tester/schemas/edge_function.tmpl.json +17 -0
- atdd/tester/schemas/event.tmpl.json +17 -0
- atdd/tester/schemas/http.tmpl.json +19 -0
- atdd/tester/schemas/job.tmpl.json +18 -0
- atdd/tester/schemas/load.tmpl.json +21 -0
- atdd/tester/schemas/metric.tmpl.json +19 -0
- atdd/tester/schemas/pack.schema.json +139 -0
- atdd/tester/schemas/realtime.tmpl.json +20 -0
- atdd/tester/schemas/rls.tmpl.json +18 -0
- atdd/tester/schemas/script.tmpl.json +16 -0
- atdd/tester/schemas/sec.tmpl.json +18 -0
- atdd/tester/schemas/storage.tmpl.json +18 -0
- atdd/tester/schemas/telemetry.schema.json +128 -0
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +143 -0
- atdd/tester/schemas/test_filename.schema.json +194 -0
- atdd/tester/schemas/test_intent.schema.json +179 -0
- atdd/tester/schemas/unit.tmpl.json +18 -0
- atdd/tester/schemas/visual.tmpl.json +18 -0
- atdd/tester/schemas/ws.tmpl.json +17 -0
- atdd/tester/utils/__init__.py +0 -0
- atdd/tester/utils/filename.py +300 -0
- atdd/tester/validators/__init__.py +0 -0
- atdd/tester/validators/cleanup_duplicate_headers.py +116 -0
- atdd/tester/validators/cleanup_duplicate_headers_v2.py +135 -0
- atdd/tester/validators/conftest.py +5 -0
- atdd/tester/validators/coverage_gap_report.py +321 -0
- atdd/tester/validators/fix_dual_ac_references.py +179 -0
- atdd/tester/validators/remove_duplicate_lines.py +93 -0
- atdd/tester/validators/test_acceptance_urn_filename_mapping.py +359 -0
- atdd/tester/validators/test_acceptance_urn_separator.py +166 -0
- atdd/tester/validators/test_artifact_naming_category.py +307 -0
- atdd/tester/validators/test_contract_schema_compliance.py +706 -0
- atdd/tester/validators/test_contracts_structure.py +200 -0
- atdd/tester/validators/test_coverage_adequacy.py +797 -0
- atdd/tester/validators/test_dual_ac_reference.py +225 -0
- atdd/tester/validators/test_fixture_validity.py +372 -0
- atdd/tester/validators/test_isolation.py +487 -0
- atdd/tester/validators/test_migration_coverage.py +204 -0
- atdd/tester/validators/test_migration_criteria.py +276 -0
- atdd/tester/validators/test_migration_generation.py +116 -0
- atdd/tester/validators/test_python_test_naming.py +410 -0
- atdd/tester/validators/test_red_layer_validation.py +95 -0
- atdd/tester/validators/test_red_python_layer_structure.py +87 -0
- atdd/tester/validators/test_red_supabase_layer_structure.py +90 -0
- atdd/tester/validators/test_telemetry_structure.py +634 -0
- atdd/tester/validators/test_typescript_test_naming.py +301 -0
- atdd/tester/validators/test_typescript_test_structure.py +84 -0
- atdd-0.2.1.dist-info/METADATA +221 -0
- atdd-0.2.1.dist-info/RECORD +184 -0
- atdd-0.2.1.dist-info/WHEEL +5 -0
- atdd-0.2.1.dist-info/entry_points.txt +2 -0
- atdd-0.2.1.dist-info/licenses/LICENSE +674 -0
- atdd-0.2.1.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
|