atdd 0.7.1__py3-none-any.whl → 0.7.3__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/coach/commands/registry.py +6 -6
- atdd/coach/commands/traceability.py +5 -5
- atdd/coach/conventions/naming.convention.yaml +296 -0
- atdd/coach/conventions/session.convention.yaml +1 -0
- atdd/coach/schemas/config.schema.json +26 -0
- atdd/coach/templates/ATDD.md +1 -0
- atdd/coach/templates/SESSION-TEMPLATE.md +6 -1
- atdd/coach/validators/test_session_archive_status.py +339 -0
- atdd/coach/validators/test_session_manifest_alignment.py +387 -0
- atdd/coach/validators/test_session_validation.py +161 -0
- atdd/planner/conventions/train.convention.yaml +17 -0
- atdd/planner/validators/test_plan_wagons.py +70 -3
- atdd/tester/conventions/telemetry.convention.yaml +25 -20
- atdd/tester/schemas/telemetry_tracking_manifest.schema.json +1 -1
- {atdd-0.7.1.dist-info → atdd-0.7.3.dist-info}/METADATA +1 -1
- {atdd-0.7.1.dist-info → atdd-0.7.3.dist-info}/RECORD +20 -17
- {atdd-0.7.1.dist-info → atdd-0.7.3.dist-info}/WHEEL +0 -0
- {atdd-0.7.1.dist-info → atdd-0.7.3.dist-info}/entry_points.txt +0 -0
- {atdd-0.7.1.dist-info → atdd-0.7.3.dist-info}/licenses/LICENSE +0 -0
- {atdd-0.7.1.dist-info → atdd-0.7.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session archive status validation.
|
|
3
|
+
|
|
4
|
+
Validates archive placement aligns with session status:
|
|
5
|
+
1. COMPLETE sessions should be in archive/ (warning)
|
|
6
|
+
2. Sessions in archive/ should be COMPLETE or OBSOLETE (warning)
|
|
7
|
+
|
|
8
|
+
These are warnings, not failures, to allow migration flexibility.
|
|
9
|
+
|
|
10
|
+
Convention: src/atdd/coach/conventions/session.convention.yaml
|
|
11
|
+
Config: .atdd/config.yaml (coach.archive_status_warnings)
|
|
12
|
+
|
|
13
|
+
Run: atdd validate coach
|
|
14
|
+
"""
|
|
15
|
+
import pytest
|
|
16
|
+
import yaml
|
|
17
|
+
import fnmatch
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Dict, List, Any, Optional
|
|
20
|
+
|
|
21
|
+
from atdd.coach.utils.repo import find_repo_root
|
|
22
|
+
from atdd.coach.utils.config import load_atdd_config
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ============================================================================
|
|
26
|
+
# Configuration
|
|
27
|
+
# ============================================================================
|
|
28
|
+
|
|
29
|
+
REPO_ROOT = find_repo_root()
|
|
30
|
+
SESSIONS_DIR = REPO_ROOT / "atdd-sessions"
|
|
31
|
+
ARCHIVE_DIR = SESSIONS_DIR / "archive"
|
|
32
|
+
|
|
33
|
+
# Terminal statuses that should be archived
|
|
34
|
+
ARCHIVABLE_STATUSES = {"COMPLETE", "OBSOLETE"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ============================================================================
|
|
38
|
+
# Fixtures
|
|
39
|
+
# ============================================================================
|
|
40
|
+
|
|
41
|
+
@pytest.fixture
|
|
42
|
+
def config() -> Dict[str, Any]:
|
|
43
|
+
"""Load ATDD configuration."""
|
|
44
|
+
return load_atdd_config(REPO_ROOT)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def archive_warnings_enabled(config: Dict[str, Any]) -> bool:
|
|
49
|
+
"""Check if archive status warnings are enabled."""
|
|
50
|
+
return config.get("coach", {}).get("archive_status_warnings", True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def session_files() -> List[Path]:
|
|
55
|
+
"""Get all session files in main directory (not archive)."""
|
|
56
|
+
if not SESSIONS_DIR.exists():
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
return sorted([
|
|
60
|
+
f for f in SESSIONS_DIR.glob("SESSION-*.md")
|
|
61
|
+
if f.name != "SESSION-TEMPLATE.md"
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@pytest.fixture
|
|
66
|
+
def archived_files() -> List[Path]:
|
|
67
|
+
"""Get all session files in archive directory."""
|
|
68
|
+
if not ARCHIVE_DIR.exists():
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
return sorted(ARCHIVE_DIR.glob("SESSION-*.md"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_session_status(path: Path) -> Optional[str]:
|
|
75
|
+
"""Parse status from session file frontmatter."""
|
|
76
|
+
content = path.read_text()
|
|
77
|
+
|
|
78
|
+
if not content.startswith("---"):
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
parts = content.split("---", 2)
|
|
82
|
+
if len(parts) < 3:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
87
|
+
status = str(frontmatter.get("status", "")).upper()
|
|
88
|
+
# Handle status with extra info (e.g., "ACTIVE - working")
|
|
89
|
+
return status.split()[0] if status else None
|
|
90
|
+
except yaml.YAMLError:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ============================================================================
|
|
95
|
+
# Archive Status Validation Tests
|
|
96
|
+
# ============================================================================
|
|
97
|
+
|
|
98
|
+
@pytest.mark.coach
|
|
99
|
+
def test_complete_sessions_should_be_archived(
|
|
100
|
+
session_files: List[Path],
|
|
101
|
+
archive_warnings_enabled: bool
|
|
102
|
+
):
|
|
103
|
+
"""
|
|
104
|
+
SPEC-COACH-ARCHIVE-001: COMPLETE sessions should be in archive/
|
|
105
|
+
|
|
106
|
+
Given: Session files in main directory
|
|
107
|
+
When: Checking status
|
|
108
|
+
Then: Sessions with COMPLETE/OBSOLETE status should be in archive/
|
|
109
|
+
|
|
110
|
+
Note: This is a warning, not a failure, to allow migration flexibility.
|
|
111
|
+
"""
|
|
112
|
+
if not archive_warnings_enabled:
|
|
113
|
+
pytest.skip("Archive status warnings disabled in config")
|
|
114
|
+
|
|
115
|
+
if not session_files:
|
|
116
|
+
pytest.skip("No session files found")
|
|
117
|
+
|
|
118
|
+
should_archive = []
|
|
119
|
+
|
|
120
|
+
for path in session_files:
|
|
121
|
+
status = parse_session_status(path)
|
|
122
|
+
if status in ARCHIVABLE_STATUSES:
|
|
123
|
+
should_archive.append(f"{path.name} (status: {status})")
|
|
124
|
+
|
|
125
|
+
if should_archive:
|
|
126
|
+
print(
|
|
127
|
+
f"\n⚠️ Found {len(should_archive)} completed sessions not in archive/:\n" +
|
|
128
|
+
"\n".join(f" - {s}" for s in should_archive) +
|
|
129
|
+
"\n\nConsider running 'atdd session archive <id>' to move them."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.coach
|
|
134
|
+
def test_archived_sessions_should_be_complete(
|
|
135
|
+
archived_files: List[Path],
|
|
136
|
+
archive_warnings_enabled: bool
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
SPEC-COACH-ARCHIVE-002: Archived sessions should have COMPLETE/OBSOLETE status
|
|
140
|
+
|
|
141
|
+
Given: Session files in archive/ directory
|
|
142
|
+
When: Checking status
|
|
143
|
+
Then: All archived sessions should have terminal status
|
|
144
|
+
|
|
145
|
+
Note: This is a warning, not a failure, to allow migration flexibility.
|
|
146
|
+
"""
|
|
147
|
+
if not archive_warnings_enabled:
|
|
148
|
+
pytest.skip("Archive status warnings disabled in config")
|
|
149
|
+
|
|
150
|
+
if not archived_files:
|
|
151
|
+
pytest.skip("No archived session files found")
|
|
152
|
+
|
|
153
|
+
not_complete = []
|
|
154
|
+
|
|
155
|
+
for path in archived_files:
|
|
156
|
+
status = parse_session_status(path)
|
|
157
|
+
if status and status not in ARCHIVABLE_STATUSES:
|
|
158
|
+
not_complete.append(f"{path.name} (status: {status})")
|
|
159
|
+
|
|
160
|
+
if not_complete:
|
|
161
|
+
print(
|
|
162
|
+
f"\n⚠️ Found {len(not_complete)} archived sessions without terminal status:\n" +
|
|
163
|
+
"\n".join(f" - {s}" for s in not_complete) +
|
|
164
|
+
"\n\nEither update status to COMPLETE/OBSOLETE or move back to atdd-sessions/."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.mark.coach
|
|
169
|
+
def test_archive_directory_structure():
|
|
170
|
+
"""
|
|
171
|
+
SPEC-COACH-ARCHIVE-003: Archive directory exists if sessions directory exists
|
|
172
|
+
|
|
173
|
+
Given: atdd-sessions/ directory exists
|
|
174
|
+
When: Checking for archive/ subdirectory
|
|
175
|
+
Then: archive/ directory should exist (create if missing)
|
|
176
|
+
"""
|
|
177
|
+
if not SESSIONS_DIR.exists():
|
|
178
|
+
pytest.skip("Sessions directory not found")
|
|
179
|
+
|
|
180
|
+
if not ARCHIVE_DIR.exists():
|
|
181
|
+
print(
|
|
182
|
+
f"\n⚠️ Archive directory does not exist: {ARCHIVE_DIR}\n"
|
|
183
|
+
"Consider creating it: mkdir -p atdd-sessions/archive/"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ============================================================================
|
|
188
|
+
# Stale Session Detection
|
|
189
|
+
# ============================================================================
|
|
190
|
+
|
|
191
|
+
@pytest.mark.coach
|
|
192
|
+
def test_stale_active_sessions(session_files: List[Path]):
|
|
193
|
+
"""
|
|
194
|
+
SPEC-COACH-ARCHIVE-004: Detect potentially stale ACTIVE sessions
|
|
195
|
+
|
|
196
|
+
Given: Session files with ACTIVE status
|
|
197
|
+
When: Checking file modification time
|
|
198
|
+
Then: Warn about sessions not modified in 7+ days
|
|
199
|
+
"""
|
|
200
|
+
if not session_files:
|
|
201
|
+
pytest.skip("No session files found")
|
|
202
|
+
|
|
203
|
+
from datetime import datetime, timedelta
|
|
204
|
+
|
|
205
|
+
stale_threshold = timedelta(days=7)
|
|
206
|
+
now = datetime.now()
|
|
207
|
+
stale = []
|
|
208
|
+
|
|
209
|
+
for path in session_files:
|
|
210
|
+
status = parse_session_status(path)
|
|
211
|
+
if status != "ACTIVE":
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
mtime = datetime.fromtimestamp(path.stat().st_mtime)
|
|
215
|
+
age = now - mtime
|
|
216
|
+
|
|
217
|
+
if age > stale_threshold:
|
|
218
|
+
days = age.days
|
|
219
|
+
stale.append(f"{path.name} (last modified {days} days ago)")
|
|
220
|
+
|
|
221
|
+
if stale:
|
|
222
|
+
print(
|
|
223
|
+
f"\n⚠️ Found {len(stale)} potentially stale ACTIVE sessions:\n" +
|
|
224
|
+
"\n".join(f" - {s}" for s in stale) +
|
|
225
|
+
"\n\nConsider updating status to BLOCKED, COMPLETE, or OBSOLETE."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@pytest.mark.coach
|
|
230
|
+
def test_blocked_sessions_have_reason(session_files: List[Path]):
|
|
231
|
+
"""
|
|
232
|
+
SPEC-COACH-ARCHIVE-005: BLOCKED sessions should document reason
|
|
233
|
+
|
|
234
|
+
Given: Session files with BLOCKED status
|
|
235
|
+
When: Checking session log
|
|
236
|
+
Then: Most recent log entry should explain the blocker
|
|
237
|
+
"""
|
|
238
|
+
if not session_files:
|
|
239
|
+
pytest.skip("No session files found")
|
|
240
|
+
|
|
241
|
+
import re
|
|
242
|
+
|
|
243
|
+
missing_reason = []
|
|
244
|
+
|
|
245
|
+
for path in session_files:
|
|
246
|
+
status = parse_session_status(path)
|
|
247
|
+
if status != "BLOCKED":
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
content = path.read_text()
|
|
251
|
+
|
|
252
|
+
# Check for Blocked: section in session log
|
|
253
|
+
has_blocked_entry = bool(re.search(
|
|
254
|
+
r"\*\*Blocked:\*\*\s*\n\s*-\s*\S",
|
|
255
|
+
content
|
|
256
|
+
))
|
|
257
|
+
|
|
258
|
+
# Or check frontmatter for blocker field
|
|
259
|
+
if content.startswith("---"):
|
|
260
|
+
parts = content.split("---", 2)
|
|
261
|
+
if len(parts) >= 3:
|
|
262
|
+
try:
|
|
263
|
+
frontmatter = yaml.safe_load(parts[1])
|
|
264
|
+
has_blocked_entry = has_blocked_entry or bool(
|
|
265
|
+
frontmatter.get("blocker") or
|
|
266
|
+
frontmatter.get("blocked_by") or
|
|
267
|
+
frontmatter.get("blocked_reason")
|
|
268
|
+
)
|
|
269
|
+
except yaml.YAMLError:
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
if not has_blocked_entry:
|
|
273
|
+
missing_reason.append(path.name)
|
|
274
|
+
|
|
275
|
+
if missing_reason:
|
|
276
|
+
print(
|
|
277
|
+
f"\n⚠️ Found {len(missing_reason)} BLOCKED sessions without documented reason:\n" +
|
|
278
|
+
"\n".join(f" - {m}" for m in missing_reason) +
|
|
279
|
+
"\n\nAdd **Blocked:** entry to Session Log or 'blocker' field to frontmatter."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ============================================================================
|
|
284
|
+
# Summary
|
|
285
|
+
# ============================================================================
|
|
286
|
+
|
|
287
|
+
@pytest.mark.coach
|
|
288
|
+
def test_archive_status_summary(
|
|
289
|
+
session_files: List[Path],
|
|
290
|
+
archived_files: List[Path]
|
|
291
|
+
):
|
|
292
|
+
"""
|
|
293
|
+
Generate archive status summary.
|
|
294
|
+
|
|
295
|
+
This test always passes but prints a summary.
|
|
296
|
+
"""
|
|
297
|
+
print("\n" + "=" * 60)
|
|
298
|
+
print("SESSION ARCHIVE STATUS SUMMARY")
|
|
299
|
+
print("=" * 60)
|
|
300
|
+
|
|
301
|
+
# Count by status in main directory
|
|
302
|
+
main_statuses: Dict[str, int] = {}
|
|
303
|
+
for path in session_files:
|
|
304
|
+
status = parse_session_status(path) or "UNKNOWN"
|
|
305
|
+
main_statuses[status] = main_statuses.get(status, 0) + 1
|
|
306
|
+
|
|
307
|
+
# Count by status in archive
|
|
308
|
+
archive_statuses: Dict[str, int] = {}
|
|
309
|
+
for path in archived_files:
|
|
310
|
+
status = parse_session_status(path) or "UNKNOWN"
|
|
311
|
+
archive_statuses[status] = archive_statuses.get(status, 0) + 1
|
|
312
|
+
|
|
313
|
+
print(f"\nMain directory ({len(session_files)} files):")
|
|
314
|
+
for status, count in sorted(main_statuses.items()):
|
|
315
|
+
marker = "⚠️ " if status in ARCHIVABLE_STATUSES else " "
|
|
316
|
+
print(f" {marker}{status}: {count}")
|
|
317
|
+
|
|
318
|
+
print(f"\nArchive directory ({len(archived_files)} files):")
|
|
319
|
+
for status, count in sorted(archive_statuses.items()):
|
|
320
|
+
marker = "⚠️ " if status not in ARCHIVABLE_STATUSES else " "
|
|
321
|
+
print(f" {marker}{status}: {count}")
|
|
322
|
+
|
|
323
|
+
# Summary advice
|
|
324
|
+
main_archivable = sum(
|
|
325
|
+
main_statuses.get(s, 0) for s in ARCHIVABLE_STATUSES
|
|
326
|
+
)
|
|
327
|
+
archive_active = sum(
|
|
328
|
+
archive_statuses.get(s, 0) for s in archive_statuses
|
|
329
|
+
if s not in ARCHIVABLE_STATUSES
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if main_archivable > 0 or archive_active > 0:
|
|
333
|
+
print("\n⚠️ Recommendations:")
|
|
334
|
+
if main_archivable > 0:
|
|
335
|
+
print(f" - Archive {main_archivable} completed session(s)")
|
|
336
|
+
if archive_active > 0:
|
|
337
|
+
print(f" - Review {archive_active} non-terminal archived session(s)")
|
|
338
|
+
|
|
339
|
+
print("\n" + "=" * 60)
|