gitmap-core 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.
- gitmap_core/README.md +46 -0
- gitmap_core/__init__.py +100 -0
- gitmap_core/communication.py +346 -0
- gitmap_core/compat.py +408 -0
- gitmap_core/connection.py +232 -0
- gitmap_core/context.py +709 -0
- gitmap_core/diff.py +283 -0
- gitmap_core/maps.py +385 -0
- gitmap_core/merge.py +449 -0
- gitmap_core/models.py +332 -0
- gitmap_core/py.typed +0 -0
- gitmap_core/pyproject.toml +48 -0
- gitmap_core/remote.py +728 -0
- gitmap_core/repository.py +1632 -0
- gitmap_core/tests/__init__.py +1 -0
- gitmap_core/tests/test_communication.py +695 -0
- gitmap_core/tests/test_compat.py +310 -0
- gitmap_core/tests/test_connection.py +314 -0
- gitmap_core/tests/test_context.py +814 -0
- gitmap_core/tests/test_diff.py +567 -0
- gitmap_core/tests/test_init.py +153 -0
- gitmap_core/tests/test_maps.py +642 -0
- gitmap_core/tests/test_merge.py +694 -0
- gitmap_core/tests/test_models.py +410 -0
- gitmap_core/tests/test_remote.py +3014 -0
- gitmap_core/tests/test_repository.py +1639 -0
- gitmap_core/tests/test_visualize.py +902 -0
- gitmap_core/visualize.py +1217 -0
- gitmap_core-0.1.0.dist-info/METADATA +961 -0
- gitmap_core-0.1.0.dist-info/RECORD +32 -0
- gitmap_core-0.1.0.dist-info/WHEEL +4 -0
- gitmap_core-0.1.0.dist-info/licenses/LICENSE +21 -0
gitmap_core/context.py
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
"""Context graph storage for GitMap.
|
|
2
|
+
|
|
3
|
+
Provides SQLite-backed event store for tracking operations, rationales,
|
|
4
|
+
and relationships between events. Enables episodic memory and context
|
|
5
|
+
awareness for IDE agents.
|
|
6
|
+
|
|
7
|
+
Execution Context:
|
|
8
|
+
Library module - imported by other gitmap_core modules
|
|
9
|
+
|
|
10
|
+
Dependencies:
|
|
11
|
+
- sqlite3: Database operations (stdlib)
|
|
12
|
+
- uuid: ID generation (stdlib)
|
|
13
|
+
|
|
14
|
+
Metadata:
|
|
15
|
+
Version: 0.1.0
|
|
16
|
+
Author: GitMap Team
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sqlite3
|
|
22
|
+
from dataclasses import asdict
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
from uuid import uuid4
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---- Data Model Classes -------------------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Event:
|
|
35
|
+
"""Context graph event.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
id: Unique event identifier.
|
|
39
|
+
timestamp: ISO 8601 timestamp.
|
|
40
|
+
event_type: Type of event (commit, push, pull, merge, branch, diff).
|
|
41
|
+
actor: Who performed the event.
|
|
42
|
+
repo: Repository path.
|
|
43
|
+
ref: Related reference (commit ID, branch name).
|
|
44
|
+
payload: Event-specific data.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
id: str
|
|
48
|
+
timestamp: str
|
|
49
|
+
event_type: str
|
|
50
|
+
actor: str | None
|
|
51
|
+
repo: str
|
|
52
|
+
ref: str | None
|
|
53
|
+
payload: dict[str, Any]
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict[str, Any]:
|
|
56
|
+
"""Convert event to dictionary.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Dictionary representation.
|
|
60
|
+
"""
|
|
61
|
+
return asdict(self)
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dict(cls, data: dict[str, Any]) -> Event:
|
|
65
|
+
"""Create event from dictionary.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
data: Dictionary with event fields.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Event instance.
|
|
72
|
+
"""
|
|
73
|
+
return cls(**data)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_row(cls, row: sqlite3.Row) -> Event:
|
|
77
|
+
"""Create event from database row.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
row: SQLite row with event data.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Event instance.
|
|
84
|
+
"""
|
|
85
|
+
return cls(
|
|
86
|
+
id=row["id"],
|
|
87
|
+
timestamp=row["timestamp"],
|
|
88
|
+
event_type=row["event_type"],
|
|
89
|
+
actor=row["actor"],
|
|
90
|
+
repo=row["repo"],
|
|
91
|
+
ref=row["ref"],
|
|
92
|
+
payload=json.loads(row["payload"]),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class Annotation:
|
|
98
|
+
"""Annotation attached to an event.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
id: Unique annotation identifier.
|
|
102
|
+
event_id: ID of associated event (can be None for standalone lessons).
|
|
103
|
+
annotation_type: Type (rationale, lesson, outcome, issue).
|
|
104
|
+
content: Annotation text content.
|
|
105
|
+
source: Who created the annotation (user, agent, auto).
|
|
106
|
+
timestamp: ISO 8601 timestamp.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
id: str
|
|
110
|
+
event_id: str | None
|
|
111
|
+
annotation_type: str
|
|
112
|
+
content: str
|
|
113
|
+
source: str
|
|
114
|
+
timestamp: str
|
|
115
|
+
|
|
116
|
+
def to_dict(self) -> dict[str, Any]:
|
|
117
|
+
"""Convert annotation to dictionary.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Dictionary representation.
|
|
121
|
+
"""
|
|
122
|
+
return asdict(self)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def from_dict(cls, data: dict[str, Any]) -> Annotation:
|
|
126
|
+
"""Create annotation from dictionary.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
data: Dictionary with annotation fields.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Annotation instance.
|
|
133
|
+
"""
|
|
134
|
+
return cls(**data)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def from_row(cls, row: sqlite3.Row) -> Annotation:
|
|
138
|
+
"""Create annotation from database row.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
row: SQLite row with annotation data.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Annotation instance.
|
|
145
|
+
"""
|
|
146
|
+
return cls(
|
|
147
|
+
id=row["id"],
|
|
148
|
+
event_id=row["event_id"],
|
|
149
|
+
annotation_type=row["annotation_type"],
|
|
150
|
+
content=row["content"],
|
|
151
|
+
source=row["source"],
|
|
152
|
+
timestamp=row["timestamp"],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class Edge:
|
|
158
|
+
"""Relationship between events.
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
source_id: Source event ID.
|
|
162
|
+
target_id: Target event ID.
|
|
163
|
+
relationship: Type of relationship (caused_by, reverts, related_to, learned_from).
|
|
164
|
+
metadata: Optional additional data.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
source_id: str
|
|
168
|
+
target_id: str
|
|
169
|
+
relationship: str
|
|
170
|
+
metadata: dict[str, Any] | None = None
|
|
171
|
+
|
|
172
|
+
def to_dict(self) -> dict[str, Any]:
|
|
173
|
+
"""Convert edge to dictionary.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dictionary representation.
|
|
177
|
+
"""
|
|
178
|
+
return asdict(self)
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_dict(cls, data: dict[str, Any]) -> Edge:
|
|
182
|
+
"""Create edge from dictionary.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
data: Dictionary with edge fields.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Edge instance.
|
|
189
|
+
"""
|
|
190
|
+
return cls(**data)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def from_row(cls, row: sqlite3.Row) -> Edge:
|
|
194
|
+
"""Create edge from database row.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
row: SQLite row with edge data.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Edge instance.
|
|
201
|
+
"""
|
|
202
|
+
metadata = row["metadata"]
|
|
203
|
+
return cls(
|
|
204
|
+
source_id=row["source_id"],
|
|
205
|
+
target_id=row["target_id"],
|
|
206
|
+
relationship=row["relationship"],
|
|
207
|
+
metadata=json.loads(metadata) if metadata else None,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---- Context Store Class ------------------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ContextStore:
|
|
215
|
+
"""SQLite-backed context graph storage.
|
|
216
|
+
|
|
217
|
+
Manages events, annotations, and edges for context-aware
|
|
218
|
+
version control operations.
|
|
219
|
+
|
|
220
|
+
Attributes:
|
|
221
|
+
db_path: Path to SQLite database file.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def __init__(self, db_path: Path) -> None:
|
|
225
|
+
"""Initialize context store with SQLite database.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
db_path: Path to context.db file.
|
|
229
|
+
"""
|
|
230
|
+
self.db_path = db_path
|
|
231
|
+
self._conn: sqlite3.Connection | None = None
|
|
232
|
+
self._ensure_schema()
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def _connection(self) -> sqlite3.Connection:
|
|
236
|
+
"""Get or create database connection.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
SQLite connection with row factory.
|
|
240
|
+
"""
|
|
241
|
+
if self._conn is None:
|
|
242
|
+
self._conn = sqlite3.connect(str(self.db_path))
|
|
243
|
+
self._conn.row_factory = sqlite3.Row
|
|
244
|
+
# Enable WAL mode for better concurrent access
|
|
245
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
246
|
+
return self._conn
|
|
247
|
+
|
|
248
|
+
def _ensure_schema(self) -> None:
|
|
249
|
+
"""Create tables and indexes if they don't exist."""
|
|
250
|
+
conn = self._connection
|
|
251
|
+
conn.executescript("""
|
|
252
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
253
|
+
id TEXT PRIMARY KEY,
|
|
254
|
+
timestamp TEXT NOT NULL,
|
|
255
|
+
event_type TEXT NOT NULL,
|
|
256
|
+
actor TEXT,
|
|
257
|
+
repo TEXT NOT NULL,
|
|
258
|
+
ref TEXT,
|
|
259
|
+
payload JSON NOT NULL
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
CREATE TABLE IF NOT EXISTS annotations (
|
|
263
|
+
id TEXT PRIMARY KEY,
|
|
264
|
+
event_id TEXT REFERENCES events(id),
|
|
265
|
+
annotation_type TEXT NOT NULL,
|
|
266
|
+
content TEXT NOT NULL,
|
|
267
|
+
source TEXT,
|
|
268
|
+
timestamp TEXT NOT NULL
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
272
|
+
source_id TEXT REFERENCES events(id),
|
|
273
|
+
target_id TEXT REFERENCES events(id),
|
|
274
|
+
relationship TEXT NOT NULL,
|
|
275
|
+
metadata JSON,
|
|
276
|
+
PRIMARY KEY (source_id, target_id, relationship)
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_events_ref ON events(ref);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
282
|
+
CREATE INDEX IF NOT EXISTS idx_annotations_event ON annotations(event_id);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_annotations_type ON annotations(annotation_type);
|
|
284
|
+
""")
|
|
285
|
+
conn.commit()
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def generate_id() -> str:
|
|
289
|
+
"""Generate unique identifier.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
UUID string.
|
|
293
|
+
"""
|
|
294
|
+
return str(uuid4())
|
|
295
|
+
|
|
296
|
+
# ---- Event Operations -----------------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
def record_event(
|
|
299
|
+
self,
|
|
300
|
+
event_type: str,
|
|
301
|
+
repo: str,
|
|
302
|
+
payload: dict[str, Any],
|
|
303
|
+
actor: str | None = None,
|
|
304
|
+
ref: str | None = None,
|
|
305
|
+
rationale: str | None = None,
|
|
306
|
+
) -> Event:
|
|
307
|
+
"""Record a new event, optionally with rationale annotation.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
event_type: Type of event (commit, push, pull, merge, branch, diff).
|
|
311
|
+
repo: Repository path.
|
|
312
|
+
payload: Event-specific data.
|
|
313
|
+
actor: Who performed the event.
|
|
314
|
+
ref: Related reference (commit ID, branch name).
|
|
315
|
+
rationale: Optional rationale to annotate immediately.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Created Event instance.
|
|
319
|
+
"""
|
|
320
|
+
event_id = self.generate_id()
|
|
321
|
+
timestamp = datetime.now().isoformat()
|
|
322
|
+
|
|
323
|
+
conn = self._connection
|
|
324
|
+
conn.execute(
|
|
325
|
+
"""
|
|
326
|
+
INSERT INTO events (id, timestamp, event_type, actor, repo, ref, payload)
|
|
327
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
328
|
+
""",
|
|
329
|
+
[event_id, timestamp, event_type, actor, repo, ref, json.dumps(payload)],
|
|
330
|
+
)
|
|
331
|
+
conn.commit()
|
|
332
|
+
|
|
333
|
+
event = Event(
|
|
334
|
+
id=event_id,
|
|
335
|
+
timestamp=timestamp,
|
|
336
|
+
event_type=event_type,
|
|
337
|
+
actor=actor,
|
|
338
|
+
repo=repo,
|
|
339
|
+
ref=ref,
|
|
340
|
+
payload=payload,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# If rationale provided, annotate immediately
|
|
344
|
+
if rationale:
|
|
345
|
+
self.add_annotation(
|
|
346
|
+
event_id=event_id,
|
|
347
|
+
annotation_type="rationale",
|
|
348
|
+
content=rationale,
|
|
349
|
+
source="user",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return event
|
|
353
|
+
|
|
354
|
+
def get_event(self, event_id: str) -> Event | None:
|
|
355
|
+
"""Retrieve event by ID.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
event_id: Event identifier.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Event instance or None if not found.
|
|
362
|
+
"""
|
|
363
|
+
conn = self._connection
|
|
364
|
+
cursor = conn.execute(
|
|
365
|
+
"SELECT * FROM events WHERE id = ?",
|
|
366
|
+
[event_id],
|
|
367
|
+
)
|
|
368
|
+
row = cursor.fetchone()
|
|
369
|
+
return Event.from_row(row) if row else None
|
|
370
|
+
|
|
371
|
+
def get_events_by_type(
|
|
372
|
+
self,
|
|
373
|
+
event_type: str,
|
|
374
|
+
limit: int = 50,
|
|
375
|
+
) -> list[Event]:
|
|
376
|
+
"""Get events filtered by type.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
event_type: Event type to filter by.
|
|
380
|
+
limit: Maximum results to return.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
List of matching events.
|
|
384
|
+
"""
|
|
385
|
+
conn = self._connection
|
|
386
|
+
cursor = conn.execute(
|
|
387
|
+
"""
|
|
388
|
+
SELECT * FROM events
|
|
389
|
+
WHERE event_type = ?
|
|
390
|
+
ORDER BY timestamp DESC
|
|
391
|
+
LIMIT ?
|
|
392
|
+
""",
|
|
393
|
+
[event_type, limit],
|
|
394
|
+
)
|
|
395
|
+
return [Event.from_row(row) for row in cursor.fetchall()]
|
|
396
|
+
|
|
397
|
+
def get_events_by_ref(
|
|
398
|
+
self,
|
|
399
|
+
ref: str,
|
|
400
|
+
limit: int = 50,
|
|
401
|
+
) -> list[Event]:
|
|
402
|
+
"""Get events related to a specific ref.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
ref: Commit ID or branch name.
|
|
406
|
+
limit: Maximum results to return.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
List of matching events.
|
|
410
|
+
"""
|
|
411
|
+
conn = self._connection
|
|
412
|
+
cursor = conn.execute(
|
|
413
|
+
"""
|
|
414
|
+
SELECT * FROM events
|
|
415
|
+
WHERE ref = ?
|
|
416
|
+
ORDER BY timestamp DESC
|
|
417
|
+
LIMIT ?
|
|
418
|
+
""",
|
|
419
|
+
[ref, limit],
|
|
420
|
+
)
|
|
421
|
+
return [Event.from_row(row) for row in cursor.fetchall()]
|
|
422
|
+
|
|
423
|
+
def search_events(
|
|
424
|
+
self,
|
|
425
|
+
query: str,
|
|
426
|
+
event_types: list[str] | None = None,
|
|
427
|
+
start_date: str | None = None,
|
|
428
|
+
end_date: str | None = None,
|
|
429
|
+
limit: int = 50,
|
|
430
|
+
) -> list[Event]:
|
|
431
|
+
"""Search across events and annotations by keyword.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
query: Search query string.
|
|
435
|
+
event_types: Filter by event types.
|
|
436
|
+
start_date: Filter events after this date (ISO format).
|
|
437
|
+
end_date: Filter events before this date (ISO format).
|
|
438
|
+
limit: Maximum results to return.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
List of matching events.
|
|
442
|
+
"""
|
|
443
|
+
conn = self._connection
|
|
444
|
+
|
|
445
|
+
# Build query with search in payload and annotations
|
|
446
|
+
sql = """
|
|
447
|
+
SELECT DISTINCT e.* FROM events e
|
|
448
|
+
LEFT JOIN annotations a ON e.id = a.event_id
|
|
449
|
+
WHERE (
|
|
450
|
+
e.payload LIKE ?
|
|
451
|
+
OR a.content LIKE ?
|
|
452
|
+
)
|
|
453
|
+
"""
|
|
454
|
+
params: list[Any] = [f"%{query}%", f"%{query}%"]
|
|
455
|
+
|
|
456
|
+
if event_types:
|
|
457
|
+
placeholders = ",".join("?" for _ in event_types)
|
|
458
|
+
sql += f" AND e.event_type IN ({placeholders})"
|
|
459
|
+
params.extend(event_types)
|
|
460
|
+
|
|
461
|
+
if start_date:
|
|
462
|
+
sql += " AND e.timestamp >= ?"
|
|
463
|
+
params.append(start_date)
|
|
464
|
+
|
|
465
|
+
if end_date:
|
|
466
|
+
sql += " AND e.timestamp <= ?"
|
|
467
|
+
params.append(end_date)
|
|
468
|
+
|
|
469
|
+
sql += " ORDER BY e.timestamp DESC LIMIT ?"
|
|
470
|
+
params.append(limit)
|
|
471
|
+
|
|
472
|
+
cursor = conn.execute(sql, params)
|
|
473
|
+
return [Event.from_row(row) for row in cursor.fetchall()]
|
|
474
|
+
|
|
475
|
+
# ---- Annotation Operations ------------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
def add_annotation(
|
|
478
|
+
self,
|
|
479
|
+
event_id: str | None,
|
|
480
|
+
annotation_type: str,
|
|
481
|
+
content: str,
|
|
482
|
+
source: str = "user",
|
|
483
|
+
) -> Annotation:
|
|
484
|
+
"""Add annotation to an event.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
event_id: ID of event to annotate (can be None for standalone).
|
|
488
|
+
annotation_type: Type (rationale, lesson, outcome, issue).
|
|
489
|
+
content: Annotation text content.
|
|
490
|
+
source: Who created the annotation.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Created Annotation instance.
|
|
494
|
+
"""
|
|
495
|
+
annotation_id = self.generate_id()
|
|
496
|
+
timestamp = datetime.now().isoformat()
|
|
497
|
+
|
|
498
|
+
conn = self._connection
|
|
499
|
+
conn.execute(
|
|
500
|
+
"""
|
|
501
|
+
INSERT INTO annotations (id, event_id, annotation_type, content, source, timestamp)
|
|
502
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
503
|
+
""",
|
|
504
|
+
[annotation_id, event_id, annotation_type, content, source, timestamp],
|
|
505
|
+
)
|
|
506
|
+
conn.commit()
|
|
507
|
+
|
|
508
|
+
return Annotation(
|
|
509
|
+
id=annotation_id,
|
|
510
|
+
event_id=event_id,
|
|
511
|
+
annotation_type=annotation_type,
|
|
512
|
+
content=content,
|
|
513
|
+
source=source,
|
|
514
|
+
timestamp=timestamp,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def get_annotations(
|
|
518
|
+
self,
|
|
519
|
+
event_id: str,
|
|
520
|
+
) -> list[Annotation]:
|
|
521
|
+
"""Get all annotations for an event.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
event_id: Event identifier.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
List of annotations.
|
|
528
|
+
"""
|
|
529
|
+
conn = self._connection
|
|
530
|
+
cursor = conn.execute(
|
|
531
|
+
"""
|
|
532
|
+
SELECT * FROM annotations
|
|
533
|
+
WHERE event_id = ?
|
|
534
|
+
ORDER BY timestamp ASC
|
|
535
|
+
""",
|
|
536
|
+
[event_id],
|
|
537
|
+
)
|
|
538
|
+
return [Annotation.from_row(row) for row in cursor.fetchall()]
|
|
539
|
+
|
|
540
|
+
def record_lesson(
|
|
541
|
+
self,
|
|
542
|
+
content: str,
|
|
543
|
+
related_event_id: str | None = None,
|
|
544
|
+
source: str = "user",
|
|
545
|
+
) -> Annotation:
|
|
546
|
+
"""Record a learned lesson.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
content: The lesson learned.
|
|
550
|
+
related_event_id: Optional event this lesson relates to.
|
|
551
|
+
source: Source of lesson (user, agent, auto).
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
Created Annotation instance.
|
|
555
|
+
"""
|
|
556
|
+
return self.add_annotation(
|
|
557
|
+
event_id=related_event_id,
|
|
558
|
+
annotation_type="lesson",
|
|
559
|
+
content=content,
|
|
560
|
+
source=source,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# ---- Edge Operations ------------------------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
def add_edge(
|
|
566
|
+
self,
|
|
567
|
+
source_id: str,
|
|
568
|
+
target_id: str,
|
|
569
|
+
relationship: str,
|
|
570
|
+
metadata: dict[str, Any] | None = None,
|
|
571
|
+
) -> Edge:
|
|
572
|
+
"""Create relationship between events.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
source_id: Source event ID.
|
|
576
|
+
target_id: Target event ID.
|
|
577
|
+
relationship: Type of relationship (caused_by, reverts, related_to, learned_from).
|
|
578
|
+
metadata: Optional additional data.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
Created Edge instance.
|
|
582
|
+
"""
|
|
583
|
+
conn = self._connection
|
|
584
|
+
conn.execute(
|
|
585
|
+
"""
|
|
586
|
+
INSERT OR REPLACE INTO edges (source_id, target_id, relationship, metadata)
|
|
587
|
+
VALUES (?, ?, ?, ?)
|
|
588
|
+
""",
|
|
589
|
+
[source_id, target_id, relationship, json.dumps(metadata) if metadata else None],
|
|
590
|
+
)
|
|
591
|
+
conn.commit()
|
|
592
|
+
|
|
593
|
+
return Edge(
|
|
594
|
+
source_id=source_id,
|
|
595
|
+
target_id=target_id,
|
|
596
|
+
relationship=relationship,
|
|
597
|
+
metadata=metadata,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
def get_related_events(
|
|
601
|
+
self,
|
|
602
|
+
event_id: str,
|
|
603
|
+
relationship: str | None = None,
|
|
604
|
+
) -> list[tuple[Event, str]]:
|
|
605
|
+
"""Get events related to given event.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
event_id: Event identifier.
|
|
609
|
+
relationship: Filter by relationship type.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
List of (event, relationship) tuples.
|
|
613
|
+
"""
|
|
614
|
+
conn = self._connection
|
|
615
|
+
|
|
616
|
+
sql = """
|
|
617
|
+
SELECT e.*, ed.relationship FROM events e
|
|
618
|
+
JOIN edges ed ON (e.id = ed.target_id OR e.id = ed.source_id)
|
|
619
|
+
WHERE (ed.source_id = ? OR ed.target_id = ?)
|
|
620
|
+
AND e.id != ?
|
|
621
|
+
"""
|
|
622
|
+
params: list[Any] = [event_id, event_id, event_id]
|
|
623
|
+
|
|
624
|
+
if relationship:
|
|
625
|
+
sql += " AND ed.relationship = ?"
|
|
626
|
+
params.append(relationship)
|
|
627
|
+
|
|
628
|
+
cursor = conn.execute(sql, params)
|
|
629
|
+
return [(Event.from_row(row), row["relationship"]) for row in cursor.fetchall()]
|
|
630
|
+
|
|
631
|
+
# ---- Timeline Operations --------------------------------------------------------------------------------
|
|
632
|
+
|
|
633
|
+
def get_timeline(
|
|
634
|
+
self,
|
|
635
|
+
ref: str | None = None,
|
|
636
|
+
start_date: str | None = None,
|
|
637
|
+
end_date: str | None = None,
|
|
638
|
+
include_annotations: bool = True,
|
|
639
|
+
limit: int = 100,
|
|
640
|
+
) -> list[dict[str, Any]]:
|
|
641
|
+
"""Get chronological timeline of events with annotations.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
ref: Filter by specific ref.
|
|
645
|
+
start_date: Filter events after this date.
|
|
646
|
+
end_date: Filter events before this date.
|
|
647
|
+
include_annotations: Include annotations with events.
|
|
648
|
+
limit: Maximum events to return.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
List of timeline entries with events and annotations.
|
|
652
|
+
"""
|
|
653
|
+
conn = self._connection
|
|
654
|
+
|
|
655
|
+
sql = "SELECT * FROM events WHERE 1=1"
|
|
656
|
+
params: list[Any] = []
|
|
657
|
+
|
|
658
|
+
if ref:
|
|
659
|
+
sql += " AND ref = ?"
|
|
660
|
+
params.append(ref)
|
|
661
|
+
|
|
662
|
+
if start_date:
|
|
663
|
+
sql += " AND timestamp >= ?"
|
|
664
|
+
params.append(start_date)
|
|
665
|
+
|
|
666
|
+
if end_date:
|
|
667
|
+
sql += " AND timestamp <= ?"
|
|
668
|
+
params.append(end_date)
|
|
669
|
+
|
|
670
|
+
sql += " ORDER BY timestamp DESC LIMIT ?"
|
|
671
|
+
params.append(limit)
|
|
672
|
+
|
|
673
|
+
cursor = conn.execute(sql, params)
|
|
674
|
+
events = [Event.from_row(row) for row in cursor.fetchall()]
|
|
675
|
+
|
|
676
|
+
timeline = []
|
|
677
|
+
for event in events:
|
|
678
|
+
entry: dict[str, Any] = {
|
|
679
|
+
"event_id": event.id,
|
|
680
|
+
"timestamp": event.timestamp,
|
|
681
|
+
"event_type": event.event_type,
|
|
682
|
+
"actor": event.actor,
|
|
683
|
+
"ref": event.ref,
|
|
684
|
+
"payload": event.payload,
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if include_annotations:
|
|
688
|
+
annotations = self.get_annotations(event.id)
|
|
689
|
+
entry["annotations"] = [ann.to_dict() for ann in annotations]
|
|
690
|
+
|
|
691
|
+
timeline.append(entry)
|
|
692
|
+
|
|
693
|
+
return timeline
|
|
694
|
+
|
|
695
|
+
# ---- Utility Methods ------------------------------------------------------------------------------------
|
|
696
|
+
|
|
697
|
+
def close(self) -> None:
|
|
698
|
+
"""Close database connection."""
|
|
699
|
+
if self._conn:
|
|
700
|
+
self._conn.close()
|
|
701
|
+
self._conn = None
|
|
702
|
+
|
|
703
|
+
def __enter__(self) -> ContextStore:
|
|
704
|
+
"""Context manager entry."""
|
|
705
|
+
return self
|
|
706
|
+
|
|
707
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
708
|
+
"""Context manager exit."""
|
|
709
|
+
self.close()
|