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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +288 -0
  3. package/bin/ocerebro.js +98 -0
  4. package/cerebro/__init__.py +7 -0
  5. package/cerebro/__main__.py +19 -0
  6. package/cerebro/cerebro_setup.py +459 -0
  7. package/hooks/__init__.py +1 -0
  8. package/hooks/cost_hook.py +45 -0
  9. package/hooks/coverage_hook.py +39 -0
  10. package/hooks/error_hook.py +48 -0
  11. package/hooks/expensive_hook.py +41 -0
  12. package/hooks/global_logger.py +32 -0
  13. package/package.json +49 -0
  14. package/pyproject.toml +77 -0
  15. package/src/__init__.py +2 -0
  16. package/src/cli/__init__.py +2 -0
  17. package/src/cli/dream.py +91 -0
  18. package/src/cli/gc.py +93 -0
  19. package/src/cli/main.py +583 -0
  20. package/src/cli/remember.py +74 -0
  21. package/src/consolidation/__init__.py +8 -0
  22. package/src/consolidation/checkpoints.py +96 -0
  23. package/src/consolidation/dream.py +465 -0
  24. package/src/consolidation/extractor.py +313 -0
  25. package/src/consolidation/promoter.py +435 -0
  26. package/src/consolidation/remember.py +544 -0
  27. package/src/consolidation/scorer.py +191 -0
  28. package/src/core/__init__.py +6 -0
  29. package/src/core/event_schema.py +55 -0
  30. package/src/core/jsonl_storage.py +238 -0
  31. package/src/core/paths.py +254 -0
  32. package/src/core/session_manager.py +76 -0
  33. package/src/diff/__init__.py +5 -0
  34. package/src/diff/memory_diff.py +571 -0
  35. package/src/forgetting/__init__.py +6 -0
  36. package/src/forgetting/decay.py +86 -0
  37. package/src/forgetting/gc.py +296 -0
  38. package/src/forgetting/guard_rails.py +126 -0
  39. package/src/hooks/__init__.py +11 -0
  40. package/src/hooks/core_captures.py +170 -0
  41. package/src/hooks/custom_loader.py +389 -0
  42. package/src/index/__init__.py +7 -0
  43. package/src/index/embeddings_db.py +419 -0
  44. package/src/index/metadata_db.py +230 -0
  45. package/src/index/queries.py +357 -0
  46. package/src/mcp/__init__.py +2 -0
  47. package/src/mcp/server.py +640 -0
  48. package/src/memdir/__init__.py +19 -0
  49. package/src/memdir/scanner.py +260 -0
  50. package/src/official/__init__.py +5 -0
  51. package/src/official/markdown_storage.py +173 -0
  52. package/src/official/templates.py +128 -0
  53. package/src/working/__init__.py +5 -0
  54. package/src/working/memory_view.py +150 -0
  55. 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