ouroboros-ai 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.
Potentially problematic release.
This version of ouroboros-ai might be problematic. Click here for more details.
- ouroboros/__init__.py +15 -0
- ouroboros/__main__.py +9 -0
- ouroboros/bigbang/__init__.py +39 -0
- ouroboros/bigbang/ambiguity.py +464 -0
- ouroboros/bigbang/interview.py +530 -0
- ouroboros/bigbang/seed_generator.py +610 -0
- ouroboros/cli/__init__.py +9 -0
- ouroboros/cli/commands/__init__.py +7 -0
- ouroboros/cli/commands/config.py +79 -0
- ouroboros/cli/commands/init.py +425 -0
- ouroboros/cli/commands/run.py +201 -0
- ouroboros/cli/commands/status.py +85 -0
- ouroboros/cli/formatters/__init__.py +31 -0
- ouroboros/cli/formatters/panels.py +157 -0
- ouroboros/cli/formatters/progress.py +112 -0
- ouroboros/cli/formatters/tables.py +166 -0
- ouroboros/cli/main.py +60 -0
- ouroboros/config/__init__.py +81 -0
- ouroboros/config/loader.py +292 -0
- ouroboros/config/models.py +332 -0
- ouroboros/core/__init__.py +62 -0
- ouroboros/core/ac_tree.py +401 -0
- ouroboros/core/context.py +472 -0
- ouroboros/core/errors.py +246 -0
- ouroboros/core/seed.py +212 -0
- ouroboros/core/types.py +205 -0
- ouroboros/evaluation/__init__.py +110 -0
- ouroboros/evaluation/consensus.py +350 -0
- ouroboros/evaluation/mechanical.py +351 -0
- ouroboros/evaluation/models.py +235 -0
- ouroboros/evaluation/pipeline.py +286 -0
- ouroboros/evaluation/semantic.py +302 -0
- ouroboros/evaluation/trigger.py +278 -0
- ouroboros/events/__init__.py +5 -0
- ouroboros/events/base.py +80 -0
- ouroboros/events/decomposition.py +153 -0
- ouroboros/events/evaluation.py +248 -0
- ouroboros/execution/__init__.py +44 -0
- ouroboros/execution/atomicity.py +451 -0
- ouroboros/execution/decomposition.py +481 -0
- ouroboros/execution/double_diamond.py +1386 -0
- ouroboros/execution/subagent.py +275 -0
- ouroboros/observability/__init__.py +63 -0
- ouroboros/observability/drift.py +383 -0
- ouroboros/observability/logging.py +504 -0
- ouroboros/observability/retrospective.py +338 -0
- ouroboros/orchestrator/__init__.py +78 -0
- ouroboros/orchestrator/adapter.py +391 -0
- ouroboros/orchestrator/events.py +278 -0
- ouroboros/orchestrator/runner.py +597 -0
- ouroboros/orchestrator/session.py +486 -0
- ouroboros/persistence/__init__.py +23 -0
- ouroboros/persistence/checkpoint.py +511 -0
- ouroboros/persistence/event_store.py +183 -0
- ouroboros/persistence/migrations/__init__.py +1 -0
- ouroboros/persistence/migrations/runner.py +100 -0
- ouroboros/persistence/migrations/scripts/001_initial.sql +20 -0
- ouroboros/persistence/schema.py +56 -0
- ouroboros/persistence/uow.py +230 -0
- ouroboros/providers/__init__.py +28 -0
- ouroboros/providers/base.py +133 -0
- ouroboros/providers/claude_code_adapter.py +212 -0
- ouroboros/providers/litellm_adapter.py +316 -0
- ouroboros/py.typed +0 -0
- ouroboros/resilience/__init__.py +67 -0
- ouroboros/resilience/lateral.py +595 -0
- ouroboros/resilience/stagnation.py +727 -0
- ouroboros/routing/__init__.py +60 -0
- ouroboros/routing/complexity.py +272 -0
- ouroboros/routing/downgrade.py +664 -0
- ouroboros/routing/escalation.py +340 -0
- ouroboros/routing/router.py +204 -0
- ouroboros/routing/tiers.py +247 -0
- ouroboros/secondary/__init__.py +40 -0
- ouroboros/secondary/scheduler.py +467 -0
- ouroboros/secondary/todo_registry.py +483 -0
- ouroboros_ai-0.1.0.dist-info/METADATA +607 -0
- ouroboros_ai-0.1.0.dist-info/RECORD +81 -0
- ouroboros_ai-0.1.0.dist-info/WHEEL +4 -0
- ouroboros_ai-0.1.0.dist-info/entry_points.txt +2 -0
- ouroboros_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""TODO Registry for capturing improvements during execution.
|
|
2
|
+
|
|
3
|
+
This module implements Story 7-1: TODO Registry - capturing discovered
|
|
4
|
+
improvements without disrupting the primary execution flow.
|
|
5
|
+
|
|
6
|
+
Design Principles:
|
|
7
|
+
- Non-blocking registration: TODOs are registered asynchronously
|
|
8
|
+
- Event sourcing: All state changes are persisted as events
|
|
9
|
+
- Immutable data: TODO items are frozen dataclasses
|
|
10
|
+
- Result type: Expected failures use Result, not exceptions
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from ouroboros.secondary import TodoRegistry, Todo, Priority, TodoStatus
|
|
14
|
+
from ouroboros.persistence import EventStore
|
|
15
|
+
|
|
16
|
+
store = EventStore("sqlite+aiosqlite:///events.db")
|
|
17
|
+
await store.initialize()
|
|
18
|
+
|
|
19
|
+
registry = TodoRegistry(store)
|
|
20
|
+
|
|
21
|
+
# Register a TODO (non-blocking)
|
|
22
|
+
await registry.register(
|
|
23
|
+
description="Refactor authentication module",
|
|
24
|
+
context="execution-123",
|
|
25
|
+
priority=Priority.MEDIUM,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Get pending TODOs
|
|
29
|
+
result = await registry.get_pending()
|
|
30
|
+
if result.is_ok:
|
|
31
|
+
for todo in result.value:
|
|
32
|
+
print(f"{todo.priority}: {todo.description}")
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from datetime import UTC, datetime
|
|
37
|
+
from enum import StrEnum
|
|
38
|
+
from uuid import uuid4
|
|
39
|
+
|
|
40
|
+
from ouroboros.core.errors import PersistenceError
|
|
41
|
+
from ouroboros.core.types import Result
|
|
42
|
+
from ouroboros.events.base import BaseEvent
|
|
43
|
+
from ouroboros.observability.logging import get_logger
|
|
44
|
+
from ouroboros.persistence.event_store import EventStore
|
|
45
|
+
|
|
46
|
+
log = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Priority(StrEnum):
|
|
50
|
+
"""Priority levels for TODO items.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
HIGH: Critical improvements that should be addressed first
|
|
54
|
+
MEDIUM: Standard improvements with moderate impact
|
|
55
|
+
LOW: Nice-to-have improvements with minimal urgency
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
HIGH = "high"
|
|
59
|
+
MEDIUM = "medium"
|
|
60
|
+
LOW = "low"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def sort_order(self) -> int:
|
|
64
|
+
"""Return numeric sort order (lower = higher priority)."""
|
|
65
|
+
orders = {Priority.HIGH: 0, Priority.MEDIUM: 1, Priority.LOW: 2}
|
|
66
|
+
return orders[self]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TodoStatus(StrEnum):
|
|
70
|
+
"""Lifecycle status of a TODO item.
|
|
71
|
+
|
|
72
|
+
Attributes:
|
|
73
|
+
PENDING: Awaiting processing in secondary loop
|
|
74
|
+
IN_PROGRESS: Currently being processed
|
|
75
|
+
DONE: Successfully completed
|
|
76
|
+
FAILED: Processing attempted but failed
|
|
77
|
+
SKIPPED: Intentionally skipped (user decision or timeout)
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
PENDING = "pending"
|
|
81
|
+
IN_PROGRESS = "in_progress"
|
|
82
|
+
DONE = "done"
|
|
83
|
+
FAILED = "failed"
|
|
84
|
+
SKIPPED = "skipped"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_terminal(self) -> bool:
|
|
88
|
+
"""Return True if this is a terminal state."""
|
|
89
|
+
return self in (TodoStatus.DONE, TodoStatus.FAILED, TodoStatus.SKIPPED)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class Todo:
|
|
94
|
+
"""Immutable TODO item representing a discovered improvement.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
id: Unique identifier (UUID)
|
|
98
|
+
description: Human-readable description of the improvement
|
|
99
|
+
context: Where the TODO was discovered (e.g., execution ID, file path)
|
|
100
|
+
priority: Importance level for processing order
|
|
101
|
+
created_at: When the TODO was registered (UTC)
|
|
102
|
+
status: Current lifecycle status
|
|
103
|
+
error_message: Error details if status is FAILED
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
id: str
|
|
107
|
+
description: str
|
|
108
|
+
context: str
|
|
109
|
+
priority: Priority
|
|
110
|
+
created_at: datetime
|
|
111
|
+
status: TodoStatus = TodoStatus.PENDING
|
|
112
|
+
error_message: str | None = None
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def create(
|
|
116
|
+
cls,
|
|
117
|
+
description: str,
|
|
118
|
+
context: str,
|
|
119
|
+
priority: Priority = Priority.MEDIUM,
|
|
120
|
+
) -> "Todo":
|
|
121
|
+
"""Factory method to create a new TODO.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
description: What improvement is needed
|
|
125
|
+
context: Where it was discovered
|
|
126
|
+
priority: How urgently it should be addressed
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
New Todo instance with generated ID and timestamp
|
|
130
|
+
"""
|
|
131
|
+
return cls(
|
|
132
|
+
id=str(uuid4()),
|
|
133
|
+
description=description,
|
|
134
|
+
context=context,
|
|
135
|
+
priority=priority,
|
|
136
|
+
created_at=datetime.now(UTC),
|
|
137
|
+
status=TodoStatus.PENDING,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def with_status(
|
|
141
|
+
self,
|
|
142
|
+
status: TodoStatus,
|
|
143
|
+
error_message: str | None = None,
|
|
144
|
+
) -> "Todo":
|
|
145
|
+
"""Return a new Todo with updated status.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
status: New status
|
|
149
|
+
error_message: Error details if status is FAILED
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
New Todo instance with updated status
|
|
153
|
+
"""
|
|
154
|
+
return Todo(
|
|
155
|
+
id=self.id,
|
|
156
|
+
description=self.description,
|
|
157
|
+
context=self.context,
|
|
158
|
+
priority=self.priority,
|
|
159
|
+
created_at=self.created_at,
|
|
160
|
+
status=status,
|
|
161
|
+
error_message=error_message,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Event type constants
|
|
166
|
+
EVENT_TODO_CREATED = "todo.created"
|
|
167
|
+
EVENT_TODO_STATUS_CHANGED = "todo.status.changed"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _create_todo_event(todo: Todo) -> BaseEvent:
|
|
171
|
+
"""Create a TODO creation event.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
todo: The TODO that was created
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
BaseEvent for persistence
|
|
178
|
+
"""
|
|
179
|
+
return BaseEvent(
|
|
180
|
+
type=EVENT_TODO_CREATED,
|
|
181
|
+
aggregate_type="todo",
|
|
182
|
+
aggregate_id=todo.id,
|
|
183
|
+
data={
|
|
184
|
+
"description": todo.description,
|
|
185
|
+
"context": todo.context,
|
|
186
|
+
"priority": todo.priority.value,
|
|
187
|
+
"created_at": todo.created_at.isoformat(),
|
|
188
|
+
"status": todo.status.value,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _create_status_change_event(
|
|
194
|
+
todo: Todo,
|
|
195
|
+
old_status: TodoStatus,
|
|
196
|
+
new_status: TodoStatus,
|
|
197
|
+
error_message: str | None = None,
|
|
198
|
+
) -> BaseEvent:
|
|
199
|
+
"""Create a TODO status change event.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
todo: The TODO being updated
|
|
203
|
+
old_status: Previous status
|
|
204
|
+
new_status: New status
|
|
205
|
+
error_message: Error details if transitioning to FAILED
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
BaseEvent for persistence
|
|
209
|
+
"""
|
|
210
|
+
data = {
|
|
211
|
+
"old_status": old_status.value,
|
|
212
|
+
"new_status": new_status.value,
|
|
213
|
+
}
|
|
214
|
+
if error_message:
|
|
215
|
+
data["error_message"] = error_message
|
|
216
|
+
|
|
217
|
+
return BaseEvent(
|
|
218
|
+
type=EVENT_TODO_STATUS_CHANGED,
|
|
219
|
+
aggregate_type="todo",
|
|
220
|
+
aggregate_id=todo.id,
|
|
221
|
+
data=data,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _reconstruct_todo_from_events(events: list[BaseEvent]) -> Todo | None:
|
|
226
|
+
"""Reconstruct a Todo from its event history.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
events: List of events for a single TODO aggregate
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Reconstructed Todo or None if no events
|
|
233
|
+
"""
|
|
234
|
+
if not events:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
# Find the creation event
|
|
238
|
+
creation_event = None
|
|
239
|
+
for event in events:
|
|
240
|
+
if event.type == EVENT_TODO_CREATED:
|
|
241
|
+
creation_event = event
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if creation_event is None:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
data = creation_event.data
|
|
248
|
+
todo = Todo(
|
|
249
|
+
id=creation_event.aggregate_id,
|
|
250
|
+
description=data["description"],
|
|
251
|
+
context=data["context"],
|
|
252
|
+
priority=Priority(data["priority"]),
|
|
253
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
254
|
+
status=TodoStatus(data["status"]),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Apply status change events
|
|
258
|
+
for event in events:
|
|
259
|
+
if event.type == EVENT_TODO_STATUS_CHANGED:
|
|
260
|
+
todo = todo.with_status(
|
|
261
|
+
TodoStatus(event.data["new_status"]),
|
|
262
|
+
event.data.get("error_message"),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return todo
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass
|
|
269
|
+
class TodoRegistry:
|
|
270
|
+
"""Registry for TODO items with event-sourced persistence.
|
|
271
|
+
|
|
272
|
+
Provides non-blocking registration of TODOs during execution
|
|
273
|
+
and retrieval for secondary loop processing.
|
|
274
|
+
|
|
275
|
+
Attributes:
|
|
276
|
+
_event_store: EventStore for persistence
|
|
277
|
+
_todo_ids: In-memory index of registered TODO IDs
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
_event_store: EventStore
|
|
281
|
+
_todo_ids: set[str] = field(default_factory=set)
|
|
282
|
+
|
|
283
|
+
async def register(
|
|
284
|
+
self,
|
|
285
|
+
description: str,
|
|
286
|
+
context: str,
|
|
287
|
+
priority: Priority = Priority.MEDIUM,
|
|
288
|
+
) -> Result[Todo, PersistenceError]:
|
|
289
|
+
"""Register a new TODO item.
|
|
290
|
+
|
|
291
|
+
This is a non-blocking operation that persists the TODO
|
|
292
|
+
and returns immediately without waiting for processing.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
description: What improvement is needed
|
|
296
|
+
context: Where it was discovered (e.g., execution ID)
|
|
297
|
+
priority: How urgently it should be addressed
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Result containing the created Todo or PersistenceError
|
|
301
|
+
"""
|
|
302
|
+
todo = Todo.create(description, context, priority)
|
|
303
|
+
event = _create_todo_event(todo)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
await self._event_store.append(event)
|
|
307
|
+
self._todo_ids.add(todo.id)
|
|
308
|
+
|
|
309
|
+
log.info(
|
|
310
|
+
"todo.registered",
|
|
311
|
+
todo_id=todo.id,
|
|
312
|
+
priority=priority.value,
|
|
313
|
+
context=context,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return Result.ok(todo)
|
|
317
|
+
|
|
318
|
+
except PersistenceError as e:
|
|
319
|
+
log.error(
|
|
320
|
+
"todo.registration.failed",
|
|
321
|
+
todo_id=todo.id,
|
|
322
|
+
error=str(e),
|
|
323
|
+
)
|
|
324
|
+
return Result.err(e)
|
|
325
|
+
|
|
326
|
+
async def update_status(
|
|
327
|
+
self,
|
|
328
|
+
todo_id: str,
|
|
329
|
+
new_status: TodoStatus,
|
|
330
|
+
error_message: str | None = None,
|
|
331
|
+
) -> Result[Todo, PersistenceError]:
|
|
332
|
+
"""Update the status of a TODO item.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
todo_id: ID of the TODO to update
|
|
336
|
+
new_status: New status to set
|
|
337
|
+
error_message: Error details if transitioning to FAILED
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Result containing the updated Todo or PersistenceError
|
|
341
|
+
"""
|
|
342
|
+
# Get current state
|
|
343
|
+
result = await self.get_by_id(todo_id)
|
|
344
|
+
if result.is_err:
|
|
345
|
+
return Result.err(result.error)
|
|
346
|
+
|
|
347
|
+
todo = result.value
|
|
348
|
+
if todo is None:
|
|
349
|
+
return Result.err(
|
|
350
|
+
PersistenceError(
|
|
351
|
+
f"TODO not found: {todo_id}",
|
|
352
|
+
operation="update_status",
|
|
353
|
+
details={"todo_id": todo_id},
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
old_status = todo.status
|
|
358
|
+
event = _create_status_change_event(todo, old_status, new_status, error_message)
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
await self._event_store.append(event)
|
|
362
|
+
updated_todo = todo.with_status(new_status, error_message)
|
|
363
|
+
|
|
364
|
+
log.info(
|
|
365
|
+
"todo.status.updated",
|
|
366
|
+
todo_id=todo_id,
|
|
367
|
+
old_status=old_status.value,
|
|
368
|
+
new_status=new_status.value,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return Result.ok(updated_todo)
|
|
372
|
+
|
|
373
|
+
except PersistenceError as e:
|
|
374
|
+
log.error(
|
|
375
|
+
"todo.status.update.failed",
|
|
376
|
+
todo_id=todo_id,
|
|
377
|
+
error=str(e),
|
|
378
|
+
)
|
|
379
|
+
return Result.err(e)
|
|
380
|
+
|
|
381
|
+
async def get_by_id(self, todo_id: str) -> Result[Todo | None, PersistenceError]:
|
|
382
|
+
"""Retrieve a TODO by its ID.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
todo_id: The TODO's unique identifier
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Result containing the Todo (or None if not found) or PersistenceError
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
events = await self._event_store.replay("todo", todo_id)
|
|
392
|
+
todo = _reconstruct_todo_from_events(events)
|
|
393
|
+
return Result.ok(todo)
|
|
394
|
+
|
|
395
|
+
except PersistenceError as e:
|
|
396
|
+
log.error(
|
|
397
|
+
"todo.retrieval.failed",
|
|
398
|
+
todo_id=todo_id,
|
|
399
|
+
error=str(e),
|
|
400
|
+
)
|
|
401
|
+
return Result.err(e)
|
|
402
|
+
|
|
403
|
+
async def get_pending(
|
|
404
|
+
self,
|
|
405
|
+
limit: int | None = None,
|
|
406
|
+
) -> Result[list[Todo], PersistenceError]:
|
|
407
|
+
"""Retrieve all pending TODOs sorted by priority.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
limit: Maximum number of TODOs to return (None = all)
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Result containing list of pending Todos sorted by priority
|
|
414
|
+
"""
|
|
415
|
+
todos: list[Todo] = []
|
|
416
|
+
|
|
417
|
+
for todo_id in self._todo_ids:
|
|
418
|
+
result = await self.get_by_id(todo_id)
|
|
419
|
+
if result.is_err:
|
|
420
|
+
return Result.err(result.error)
|
|
421
|
+
|
|
422
|
+
todo = result.value
|
|
423
|
+
if todo is not None and todo.status == TodoStatus.PENDING:
|
|
424
|
+
todos.append(todo)
|
|
425
|
+
|
|
426
|
+
# Sort by priority (HIGH first) then by creation time (oldest first)
|
|
427
|
+
todos.sort(key=lambda t: (t.priority.sort_order, t.created_at))
|
|
428
|
+
|
|
429
|
+
if limit is not None:
|
|
430
|
+
todos = todos[:limit]
|
|
431
|
+
|
|
432
|
+
return Result.ok(todos)
|
|
433
|
+
|
|
434
|
+
async def get_all(self) -> Result[list[Todo], PersistenceError]:
|
|
435
|
+
"""Retrieve all TODOs regardless of status.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Result containing list of all Todos
|
|
439
|
+
"""
|
|
440
|
+
todos: list[Todo] = []
|
|
441
|
+
|
|
442
|
+
for todo_id in self._todo_ids:
|
|
443
|
+
result = await self.get_by_id(todo_id)
|
|
444
|
+
if result.is_err:
|
|
445
|
+
return Result.err(result.error)
|
|
446
|
+
|
|
447
|
+
todo = result.value
|
|
448
|
+
if todo is not None:
|
|
449
|
+
todos.append(todo)
|
|
450
|
+
|
|
451
|
+
# Sort by creation time (newest first)
|
|
452
|
+
todos.sort(key=lambda t: t.created_at, reverse=True)
|
|
453
|
+
return Result.ok(todos)
|
|
454
|
+
|
|
455
|
+
def count_pending(self) -> int:
|
|
456
|
+
"""Return count of tracked TODO IDs.
|
|
457
|
+
|
|
458
|
+
Note: This is the count of IDs in memory, which may include
|
|
459
|
+
non-pending items. For accurate pending count, use get_pending().
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
Number of TODO IDs tracked in memory
|
|
463
|
+
"""
|
|
464
|
+
return len(self._todo_ids)
|
|
465
|
+
|
|
466
|
+
async def get_stats(self) -> Result[dict[str, int], PersistenceError]:
|
|
467
|
+
"""Get statistics about TODOs by status.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Result containing dict mapping status to count
|
|
471
|
+
"""
|
|
472
|
+
stats: dict[str, int] = {status.value: 0 for status in TodoStatus}
|
|
473
|
+
|
|
474
|
+
for todo_id in self._todo_ids:
|
|
475
|
+
result = await self.get_by_id(todo_id)
|
|
476
|
+
if result.is_err:
|
|
477
|
+
return Result.err(result.error)
|
|
478
|
+
|
|
479
|
+
todo = result.value
|
|
480
|
+
if todo is not None:
|
|
481
|
+
stats[todo.status.value] += 1
|
|
482
|
+
|
|
483
|
+
return Result.ok(stats)
|