ocerebro 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/bin/ocerebro.js +98 -0
- package/cerebro/__init__.py +7 -0
- package/cerebro/__main__.py +19 -0
- package/cerebro/cerebro_setup.py +459 -0
- package/hooks/__init__.py +1 -0
- package/hooks/cost_hook.py +45 -0
- package/hooks/coverage_hook.py +39 -0
- package/hooks/error_hook.py +48 -0
- package/hooks/expensive_hook.py +41 -0
- package/hooks/global_logger.py +32 -0
- package/package.json +49 -0
- package/pyproject.toml +77 -0
- package/src/__init__.py +2 -0
- package/src/cli/__init__.py +2 -0
- package/src/cli/dream.py +91 -0
- package/src/cli/gc.py +93 -0
- package/src/cli/main.py +583 -0
- package/src/cli/remember.py +74 -0
- package/src/consolidation/__init__.py +8 -0
- package/src/consolidation/checkpoints.py +96 -0
- package/src/consolidation/dream.py +465 -0
- package/src/consolidation/extractor.py +313 -0
- package/src/consolidation/promoter.py +435 -0
- package/src/consolidation/remember.py +544 -0
- package/src/consolidation/scorer.py +191 -0
- package/src/core/__init__.py +6 -0
- package/src/core/event_schema.py +55 -0
- package/src/core/jsonl_storage.py +238 -0
- package/src/core/paths.py +254 -0
- package/src/core/session_manager.py +76 -0
- package/src/diff/__init__.py +5 -0
- package/src/diff/memory_diff.py +571 -0
- package/src/forgetting/__init__.py +6 -0
- package/src/forgetting/decay.py +86 -0
- package/src/forgetting/gc.py +296 -0
- package/src/forgetting/guard_rails.py +126 -0
- package/src/hooks/__init__.py +11 -0
- package/src/hooks/core_captures.py +170 -0
- package/src/hooks/custom_loader.py +389 -0
- package/src/index/__init__.py +7 -0
- package/src/index/embeddings_db.py +419 -0
- package/src/index/metadata_db.py +230 -0
- package/src/index/queries.py +357 -0
- package/src/mcp/__init__.py +2 -0
- package/src/mcp/server.py +640 -0
- package/src/memdir/__init__.py +19 -0
- package/src/memdir/scanner.py +260 -0
- package/src/official/__init__.py +5 -0
- package/src/official/markdown_storage.py +173 -0
- package/src/official/templates.py +128 -0
- package/src/working/__init__.py +5 -0
- package/src/working/memory_view.py +150 -0
- package/src/working/yaml_storage.py +234 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Extractor: transforma eventos raw em drafts YAML para working"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
from src.core.jsonl_storage import JSONLStorage
|
|
9
|
+
from src.core.event_schema import Event, EventType
|
|
10
|
+
from src.working.yaml_storage import YAMLStorage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ExtractionResult:
|
|
15
|
+
"""Resultado da extração de eventos"""
|
|
16
|
+
session_id: str
|
|
17
|
+
project: str
|
|
18
|
+
events: List[Event] = field(default_factory=list)
|
|
19
|
+
start_event_id: Optional[str] = None
|
|
20
|
+
end_event_id: Optional[str] = None
|
|
21
|
+
summary: Dict[str, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Extractor:
|
|
25
|
+
"""
|
|
26
|
+
Extrai eventos da camada raw e gera drafts YAML para working.
|
|
27
|
+
|
|
28
|
+
Responsabilidades:
|
|
29
|
+
- Ler eventos JSONL de um projeto
|
|
30
|
+
- Filtrar eventos por sessão ou range
|
|
31
|
+
- Agrupar eventos por tipo (tool_calls, git_events, test_results, errors)
|
|
32
|
+
- Gerar resumo estruturado para draft YAML
|
|
33
|
+
- Identificar events_range para rastreabilidade
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, raw_storage: JSONLStorage, working_storage: YAMLStorage):
|
|
37
|
+
"""
|
|
38
|
+
Inicializa o Extractor.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
raw_storage: Instância do JSONLStorage para leitura
|
|
42
|
+
working_storage: Instância do YAMLStorage para escrita dos drafts
|
|
43
|
+
"""
|
|
44
|
+
self.raw_storage = raw_storage
|
|
45
|
+
self.working_storage = working_storage
|
|
46
|
+
|
|
47
|
+
def extract_session(self, project: str, session_id: str) -> ExtractionResult:
|
|
48
|
+
"""
|
|
49
|
+
Extrai todos os eventos de uma sessão.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
project: Nome do projeto
|
|
53
|
+
session_id: ID da sessão
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Resultado da extração com eventos e resumo
|
|
57
|
+
"""
|
|
58
|
+
all_events = self.raw_storage.read(project)
|
|
59
|
+
|
|
60
|
+
# Filtra eventos da sessão
|
|
61
|
+
session_events = [e for e in all_events if e.session_id == session_id]
|
|
62
|
+
|
|
63
|
+
# Ordena por timestamp
|
|
64
|
+
session_events.sort(key=lambda e: e.ts)
|
|
65
|
+
|
|
66
|
+
if not session_events:
|
|
67
|
+
return ExtractionResult(
|
|
68
|
+
session_id=session_id,
|
|
69
|
+
project=project,
|
|
70
|
+
events=[],
|
|
71
|
+
summary={"status": "no_events"}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Gera resumo
|
|
75
|
+
summary = self._generate_summary(session_events)
|
|
76
|
+
|
|
77
|
+
return ExtractionResult(
|
|
78
|
+
session_id=session_id,
|
|
79
|
+
project=project,
|
|
80
|
+
events=session_events,
|
|
81
|
+
start_event_id=session_events[0].event_id,
|
|
82
|
+
end_event_id=session_events[-1].event_id,
|
|
83
|
+
summary=summary
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def extract_range(self, project: str, start_id: str, end_id: str) -> ExtractionResult:
|
|
87
|
+
"""
|
|
88
|
+
Extrai eventos em um range de IDs.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
project: Nome do projeto
|
|
92
|
+
start_id: ID inicial (inclusive)
|
|
93
|
+
end_id: ID final (inclusive)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Resultado da extração
|
|
97
|
+
"""
|
|
98
|
+
events = self.raw_storage.read_range(project, start_id, end_id)
|
|
99
|
+
events.sort(key=lambda e: e.ts)
|
|
100
|
+
|
|
101
|
+
if not events:
|
|
102
|
+
return ExtractionResult(
|
|
103
|
+
session_id="unknown",
|
|
104
|
+
project=project,
|
|
105
|
+
events=[],
|
|
106
|
+
summary={"status": "no_events_in_range"}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Pega session_id predominante
|
|
110
|
+
session_counts: Dict[str, int] = {}
|
|
111
|
+
for e in events:
|
|
112
|
+
session_counts[e.session_id] = session_counts.get(e.session_id, 0) + 1
|
|
113
|
+
main_session = max(session_counts, key=session_counts.get)
|
|
114
|
+
|
|
115
|
+
summary = self._generate_summary(events)
|
|
116
|
+
|
|
117
|
+
return ExtractionResult(
|
|
118
|
+
session_id=main_session,
|
|
119
|
+
project=project,
|
|
120
|
+
events=events,
|
|
121
|
+
start_event_id=start_id,
|
|
122
|
+
end_event_id=end_id,
|
|
123
|
+
summary=summary
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _generate_summary(self, events: List[Event]) -> Dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
Gera resumo estruturado dos eventos.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
events: Lista de eventos
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dicionário com resumo
|
|
135
|
+
"""
|
|
136
|
+
tool_calls = [e for e in events if e.event_type == EventType.TOOL_CALL]
|
|
137
|
+
git_events = [e for e in events if e.event_type == EventType.GIT_EVENT]
|
|
138
|
+
test_results = [e for e in events if e.event_type == EventType.TEST_RESULT]
|
|
139
|
+
errors = [e for e in events if e.event_type == EventType.ERROR]
|
|
140
|
+
|
|
141
|
+
# Calcula duração aproximada
|
|
142
|
+
if events:
|
|
143
|
+
start = datetime.fromisoformat(events[0].ts.replace("Z", "+00:00"))
|
|
144
|
+
end = datetime.fromisoformat(events[-1].ts.replace("Z", "+00:00"))
|
|
145
|
+
duration_seconds = (end - start).total_seconds()
|
|
146
|
+
else:
|
|
147
|
+
duration_seconds = 0
|
|
148
|
+
|
|
149
|
+
# Extrai arquivos changed (de tool_calls com git)
|
|
150
|
+
files_changed = set()
|
|
151
|
+
for tc in tool_calls:
|
|
152
|
+
if tc.subtype == "Edit" and "file_path" in tc.payload.get("call", {}):
|
|
153
|
+
files_changed.add(tc.payload["call"]["file_path"])
|
|
154
|
+
|
|
155
|
+
# Extrai testes passing
|
|
156
|
+
tests_passed = sum(1 for t in test_results if t.payload.get("status") == "pass")
|
|
157
|
+
tests_failed = sum(1 for t in test_results if t.payload.get("status") == "fail")
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"total_events": len(events),
|
|
161
|
+
"tool_calls": len(tool_calls),
|
|
162
|
+
"git_events": len(git_events),
|
|
163
|
+
"test_results": len(test_results),
|
|
164
|
+
"errors": len(errors),
|
|
165
|
+
"tests_passed": tests_passed,
|
|
166
|
+
"tests_failed": tests_failed,
|
|
167
|
+
"files_changed": list(files_changed),
|
|
168
|
+
"duration_seconds": duration_seconds,
|
|
169
|
+
"status": "complete"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def create_draft(
|
|
173
|
+
self,
|
|
174
|
+
result: ExtractionResult,
|
|
175
|
+
draft_type: str = "session",
|
|
176
|
+
draft_name: Optional[str] = None
|
|
177
|
+
) -> Dict[str, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Cria draft YAML a partir do resultado da extração.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
result: Resultado da extração
|
|
183
|
+
draft_type: Tipo de draft (session, feature, error)
|
|
184
|
+
draft_name: Nome do draft (opcional, gera automático se None)
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dados do draft para escrita em YAML
|
|
188
|
+
"""
|
|
189
|
+
if draft_name is None:
|
|
190
|
+
draft_name = f"{draft_type}_{result.session_id[:8]}"
|
|
191
|
+
|
|
192
|
+
# Gera lista de eventos significativos
|
|
193
|
+
significant_events = []
|
|
194
|
+
for event in result.events:
|
|
195
|
+
if event.event_type in [EventType.ERROR, EventType.GIT_EVENT]:
|
|
196
|
+
significant_events.append({
|
|
197
|
+
"type": event.event_type.value,
|
|
198
|
+
"subtype": event.subtype,
|
|
199
|
+
"summary": str(event.payload)[:200]
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
# Extrai erros críticos
|
|
203
|
+
critical_errors = [
|
|
204
|
+
e for e in result.events
|
|
205
|
+
if e.event_type == EventType.ERROR
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
draft = {
|
|
209
|
+
"id": draft_name,
|
|
210
|
+
"type": draft_type,
|
|
211
|
+
"project": result.project,
|
|
212
|
+
"session_id": result.session_id,
|
|
213
|
+
"events_range": {
|
|
214
|
+
"from": result.start_event_id,
|
|
215
|
+
"to": result.end_event_id
|
|
216
|
+
},
|
|
217
|
+
"summary": result.summary,
|
|
218
|
+
"significant_events": significant_events[:10], # Max 10
|
|
219
|
+
"critical_errors": [
|
|
220
|
+
{"type": e.subtype, "context": e.payload}
|
|
221
|
+
for e in critical_errors
|
|
222
|
+
],
|
|
223
|
+
"status": "draft",
|
|
224
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
225
|
+
"needs_review": len(critical_errors) > 0 or result.summary.get("tests_failed", 0) > 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return draft
|
|
229
|
+
|
|
230
|
+
def write_draft(
|
|
231
|
+
self,
|
|
232
|
+
project: str,
|
|
233
|
+
draft: Dict[str, Any],
|
|
234
|
+
draft_type: str = "session"
|
|
235
|
+
) -> str:
|
|
236
|
+
"""
|
|
237
|
+
Escreve draft em YAML na camada working.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
project: Nome do projeto
|
|
241
|
+
draft: Dados do draft
|
|
242
|
+
draft_type: Tipo de draft
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Nome do draft escrito
|
|
246
|
+
"""
|
|
247
|
+
draft_name = draft["id"]
|
|
248
|
+
|
|
249
|
+
if draft_type == "session":
|
|
250
|
+
self.working_storage.write_session(project, draft_name, draft)
|
|
251
|
+
elif draft_type == "feature":
|
|
252
|
+
self.working_storage.write_feature(project, draft_name, draft)
|
|
253
|
+
else:
|
|
254
|
+
# Para outros tipos, usa session como fallback
|
|
255
|
+
self.working_storage.write_session(project, draft_name, draft)
|
|
256
|
+
|
|
257
|
+
return draft_name
|
|
258
|
+
|
|
259
|
+
def extract_and_write(
|
|
260
|
+
self,
|
|
261
|
+
project: str,
|
|
262
|
+
session_id: str,
|
|
263
|
+
draft_type: str = "session"
|
|
264
|
+
) -> str:
|
|
265
|
+
"""
|
|
266
|
+
Extrai sessão e escreve draft em working.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
project: Nome do projeto
|
|
270
|
+
session_id: ID da sessão
|
|
271
|
+
draft_type: Tipo de draft
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Nome do draft escrito
|
|
275
|
+
"""
|
|
276
|
+
result = self.extract_session(project, session_id)
|
|
277
|
+
|
|
278
|
+
if not result.events:
|
|
279
|
+
raise ValueError(f"Nenhum evento encontrado para sessão {session_id}")
|
|
280
|
+
|
|
281
|
+
draft = self.create_draft(result, draft_type)
|
|
282
|
+
return self.write_draft(project, draft, draft_type)
|
|
283
|
+
|
|
284
|
+
def find_incomplete_sessions(self, project: str) -> List[str]:
|
|
285
|
+
"""
|
|
286
|
+
Encontra sessões incompletas (sem checkpoint.created).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
project: Nome do projeto
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Lista de session_ids incompletas
|
|
293
|
+
"""
|
|
294
|
+
all_events = self.raw_storage.read(project)
|
|
295
|
+
|
|
296
|
+
# Agrupa por session_id
|
|
297
|
+
sessions: Dict[str, List[Event]] = {}
|
|
298
|
+
for e in all_events:
|
|
299
|
+
if e.session_id not in sessions:
|
|
300
|
+
sessions[e.session_id] = []
|
|
301
|
+
sessions[e.session_id].append(e)
|
|
302
|
+
|
|
303
|
+
# Encontra sessões sem checkpoint
|
|
304
|
+
incomplete = []
|
|
305
|
+
for session_id, events in sessions.items():
|
|
306
|
+
has_checkpoint = any(
|
|
307
|
+
e.event_type == EventType.CHECKPOINT_CREATED
|
|
308
|
+
for e in events
|
|
309
|
+
)
|
|
310
|
+
if not has_checkpoint:
|
|
311
|
+
incomplete.append(session_id)
|
|
312
|
+
|
|
313
|
+
return incomplete
|