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,435 @@
1
+ """Promoter: promove drafts de working para official"""
2
+
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+ from dataclasses import dataclass
7
+
8
+ from src.working.yaml_storage import YAMLStorage
9
+ from src.official.markdown_storage import MarkdownStorage
10
+ from src.official.templates import ErrorTemplate, DecisionTemplate
11
+
12
+
13
+ @dataclass
14
+ class PromotionResult:
15
+ """Resultado da promoção"""
16
+ success: bool
17
+ source_type: str
18
+ source_id: str
19
+ target_type: str
20
+ target_path: str
21
+ promoted_at: str
22
+ metadata: Dict[str, Any] = None
23
+
24
+
25
+ class Promoter:
26
+ """
27
+ Promove drafts de working para official.
28
+
29
+ Responsabilidades:
30
+ - Ler drafts YAML de working
31
+ - Transformar em Markdown com frontmatter
32
+ - Escrever em official/{type}/{project}/
33
+ - Registrar evento promotion.performed
34
+ - Suportar supervisão humana para casos ambíguos
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ working_storage: YAMLStorage,
40
+ official_storage: MarkdownStorage
41
+ ):
42
+ """
43
+ Inicializa o Promoter.
44
+
45
+ Args:
46
+ working_storage: Instância do YAMLStorage
47
+ official_storage: Instância do MarkdownStorage
48
+ """
49
+ self.working_storage = working_storage
50
+ self.official_storage = official_storage
51
+
52
+ def promote_session(
53
+ self,
54
+ project: str,
55
+ session_id: str,
56
+ promote_to: str = "decision"
57
+ ) -> Optional[PromotionResult]:
58
+ """
59
+ Promove sessão para official.
60
+
61
+ Args:
62
+ project: Nome do projeto
63
+ session_id: ID da sessão
64
+ promote_to: Tipo de promoção (decision, error)
65
+
66
+ Returns:
67
+ Resultado da promoção ou None se falhar
68
+ """
69
+ draft = self.working_storage.read_session(project, session_id)
70
+
71
+ if not draft:
72
+ return None
73
+
74
+ return self._promote_draft(
75
+ project=project,
76
+ draft=draft,
77
+ draft_type="session",
78
+ promote_to=promote_to
79
+ )
80
+
81
+ def promote_feature(
82
+ self,
83
+ project: str,
84
+ feature_name: str,
85
+ promote_to: str = "decision"
86
+ ) -> Optional[PromotionResult]:
87
+ """
88
+ Promove feature para official.
89
+
90
+ Args:
91
+ project: Nome do projeto
92
+ feature_name: Nome da feature
93
+ promote_to: Tipo de promoção
94
+
95
+ Returns:
96
+ Resultado da promoção ou None se falhar
97
+ """
98
+ draft = self.working_storage.read_feature(project, feature_name)
99
+
100
+ if not draft:
101
+ return None
102
+
103
+ return self._promote_draft(
104
+ project=project,
105
+ draft=draft,
106
+ draft_type="feature",
107
+ promote_to=promote_to
108
+ )
109
+
110
+ def _promote_draft(
111
+ self,
112
+ project: str,
113
+ draft: Dict[str, Any],
114
+ draft_type: str,
115
+ promote_to: str
116
+ ) -> PromotionResult:
117
+ """
118
+ Promove draft para official.
119
+
120
+ Args:
121
+ project: Nome do projeto
122
+ draft: Dados do draft
123
+ draft_type: Tipo do draft
124
+ promote_to: Tipo de promoção
125
+
126
+ Returns:
127
+ Resultado da promoção
128
+ """
129
+ draft_id = draft.get("id", "unknown")
130
+
131
+ if promote_to == "decision":
132
+ return self._promote_to_decision(project, draft, draft_id)
133
+ elif promote_to == "error":
134
+ return self._promote_to_error(project, draft, draft_id)
135
+ else:
136
+ raise ValueError(f"Tipo de promoção desconhecido: {promote_to}")
137
+
138
+ def _promote_to_decision(
139
+ self,
140
+ project: str,
141
+ draft: Dict[str, Any],
142
+ draft_id: str
143
+ ) -> PromotionResult:
144
+ """
145
+ Promove draft para decisão arquitetural.
146
+
147
+ Args:
148
+ project: Nome do projeto
149
+ draft: Dados do draft
150
+ draft_id: ID do draft
151
+
152
+ Returns:
153
+ Resultado da promoção
154
+ """
155
+ summary = draft.get("summary", {})
156
+
157
+ # Gera título da decisão
158
+ title = draft.get("title", f"Decisão {draft_id}")
159
+ if not draft.get("title") and summary.get("files_changed"):
160
+ title = f"Mudanças em {', '.join(summary['files_changed'][:2])}"
161
+
162
+ # Prepara frontmatter
163
+ frontmatter = DecisionTemplate.frontmatter(
164
+ decision_id=draft_id,
165
+ title=title,
166
+ status="approved",
167
+ date=datetime.now(timezone.utc).isoformat()[:10],
168
+ project=project,
169
+ tags=["auto-promoted", draft.get("type", "session")]
170
+ )
171
+
172
+ # Adiciona metadados do events_range
173
+ if "events_range" in draft:
174
+ frontmatter["events_from"] = draft["events_range"].get("from")
175
+ frontmatter["events_to"] = draft["events_range"].get("to")
176
+
177
+ # Gera corpo
178
+ body_sections = [
179
+ "## Resumo",
180
+ "",
181
+ f"Sessão: {draft.get('session_id', 'N/A')}",
182
+ f"Total de eventos: {summary.get('total_events', 0)}",
183
+ ""
184
+ ]
185
+
186
+ # Adiciona arquivos changed
187
+ if summary.get("files_changed"):
188
+ body_sections.extend([
189
+ "## Arquivos Modificados",
190
+ ""
191
+ ])
192
+ for f in summary["files_changed"]:
193
+ body_sections.append(f"- `{f}`")
194
+ body_sections.append("")
195
+
196
+ # Adiciona eventos significativos
197
+ if draft.get("significant_events"):
198
+ body_sections.extend([
199
+ "## Eventos Significativos",
200
+ ""
201
+ ])
202
+ for evt in draft["significant_events"][:5]:
203
+ body_sections.append(f"- **{evt.get('type', 'unknown')}** ({evt.get('subtype', '')}): {evt.get('summary', '')[:100]}")
204
+ body_sections.append("")
205
+
206
+ # Adiciona testes
207
+ if summary.get("tests_passed") or summary.get("tests_failed"):
208
+ body_sections.extend([
209
+ "## Testes",
210
+ "",
211
+ f"- Passando: {summary.get('tests_passed', 0)}",
212
+ f"- Falhando: {summary.get('tests_failed', 0)}",
213
+ ""
214
+ ])
215
+
216
+ content = "\n".join(body_sections)
217
+
218
+ # Escreve em official
219
+ self.official_storage.write_decision(
220
+ project=project,
221
+ name=draft_id,
222
+ frontmatter=frontmatter,
223
+ content=content
224
+ )
225
+
226
+ return PromotionResult(
227
+ success=True,
228
+ source_type=draft.get("type", "session"),
229
+ source_id=draft_id,
230
+ target_type="decision",
231
+ target_path=f"official/{project}/decisions/{draft_id}.md",
232
+ promoted_at=datetime.now(timezone.utc).isoformat(),
233
+ metadata={"title": title}
234
+ )
235
+
236
+ def _promote_to_error(
237
+ self,
238
+ project: str,
239
+ draft: Dict[str, Any],
240
+ draft_id: str
241
+ ) -> PromotionResult:
242
+ """
243
+ Promove draft para erro documentado.
244
+
245
+ Args:
246
+ project: Nome do projeto
247
+ draft: Dados do draft
248
+ draft_id: ID do draft
249
+
250
+ Returns:
251
+ Resultado da promoção
252
+ """
253
+ critical_errors = draft.get("critical_errors", [])
254
+
255
+ if not critical_errors:
256
+ # Se não há erros críticos, não faz sentido promover como erro
257
+ return PromotionResult(
258
+ success=False,
259
+ source_type=draft.get("type", "session"),
260
+ source_id=draft_id,
261
+ target_type="error",
262
+ target_path="",
263
+ promoted_at=datetime.now(timezone.utc).isoformat(),
264
+ metadata={"reason": "no_critical_errors"}
265
+ )
266
+
267
+ # Pega primeiro erro crítico
268
+ error = critical_errors[0]
269
+
270
+ # Prepara frontmatter
271
+ frontmatter = ErrorTemplate.frontmatter(
272
+ error_id=draft_id,
273
+ severity="high",
274
+ status="resolved",
275
+ category=error.get("type", "unknown"),
276
+ area="auto-detected",
277
+ project=project,
278
+ tags=["auto-promoted"]
279
+ )
280
+
281
+ # Gera corpo
282
+ error_original = str(error.get("context", {}))[:500]
283
+
284
+ # BUG-02 FIX: Extrai causa raiz e solução dos dados do erro
285
+ causa_raiz = error.get("message") or error.get("details") or error.get("root_cause") or ""
286
+ solucao = error.get("resolution") or error.get("solution") or error.get("fix_applied") or ""
287
+
288
+ body = ErrorTemplate.body(
289
+ error_original=error_original,
290
+ causa_raiz=causa_raiz,
291
+ solucao_aplicada=solucao,
292
+ prevencao_futura=None
293
+ )
294
+
295
+ # Escreve em official
296
+ self.official_storage.write_error(
297
+ project=project,
298
+ name=draft_id,
299
+ frontmatter=frontmatter,
300
+ content=body
301
+ )
302
+
303
+ return PromotionResult(
304
+ success=True,
305
+ source_type=draft.get("type", "session"),
306
+ source_id=draft_id,
307
+ target_type="error",
308
+ target_path=f"official/{project}/errors/{draft_id}.md",
309
+ promoted_at=datetime.now(timezone.utc).isoformat(),
310
+ metadata={"error_type": error.get("type")}
311
+ )
312
+
313
+ def promote_with_review(
314
+ self,
315
+ project: str,
316
+ draft_id: str,
317
+ draft_type: str,
318
+ promote_to: str,
319
+ review_callback=None
320
+ ) -> Optional[PromotionResult]:
321
+ """
322
+ Promove draft com revisão opcional.
323
+
324
+ Args:
325
+ project: Nome do projeto
326
+ draft_id: ID do draft
327
+ draft_type: Tipo do draft (session, feature)
328
+ promote_to: Tipo de promoção (decision, error)
329
+ review_callback: Callback para revisão (recebe draft, retorna approve/skip/reject)
330
+
331
+ Returns:
332
+ Resultado da promoção ou None se skip/reject
333
+ """
334
+ # Lê draft
335
+ if draft_type == "session":
336
+ draft = self.working_storage.read_session(project, draft_id)
337
+ elif draft_type == "feature":
338
+ draft = self.working_storage.read_feature(project, draft_id)
339
+ else:
340
+ return None
341
+
342
+ if not draft:
343
+ return None
344
+
345
+ # Chama callback de revisão se fornecido
346
+ if review_callback:
347
+ action = review_callback(draft)
348
+ if action == "skip":
349
+ return None
350
+ elif action == "reject":
351
+ draft["status"] = "rejected"
352
+ if draft_type == "session":
353
+ self.working_storage.write_session(project, draft_id, draft)
354
+ else:
355
+ self.working_storage.write_feature(project, draft_id, draft)
356
+ return None
357
+
358
+ # Promove
359
+ return self._promote_draft(
360
+ project=project,
361
+ draft=draft,
362
+ draft_type=draft_type,
363
+ promote_to=promote_to
364
+ )
365
+
366
+ def list_pending_promotions(self, project: str) -> List[Dict[str, Any]]:
367
+ """
368
+ Lista drafts pendentes de promoção.
369
+
370
+ Args:
371
+ project: Nome do projeto
372
+
373
+ Returns:
374
+ Lista de drafts com status=draft ou needs_review
375
+ """
376
+ pending = []
377
+
378
+ # Verifica sessions
379
+ sessions = self.working_storage.list_sessions(project)
380
+ for session in sessions:
381
+ if session.get("status") in ["draft", "needs_review"]:
382
+ pending.append({
383
+ "type": "session",
384
+ "id": session.get("id"),
385
+ "status": session.get("status"),
386
+ "needs_review": session.get("needs_review", False)
387
+ })
388
+
389
+ # Verifica features
390
+ features = self.working_storage.list_features(project)
391
+ for feature in features:
392
+ if feature.get("status") in ["draft", "needs_review"]:
393
+ pending.append({
394
+ "type": "feature",
395
+ "id": feature.get("id"),
396
+ "status": feature.get("status"),
397
+ "needs_review": feature.get("needs_review", False)
398
+ })
399
+
400
+ return pending
401
+
402
+ def mark_promoted(
403
+ self,
404
+ project: str,
405
+ draft_id: str,
406
+ draft_type: str,
407
+ result: PromotionResult
408
+ ) -> None:
409
+ """
410
+ Marca draft como promovido.
411
+
412
+ Args:
413
+ project: Nome do projeto
414
+ draft_id: ID do draft
415
+ draft_type: Tipo do draft
416
+ result: Resultado da promoção
417
+ """
418
+ draft = {
419
+ "status": "promoted",
420
+ "promoted_at": result.promoted_at,
421
+ "promoted_to": result.target_type,
422
+ "promoted_path": result.target_path
423
+ }
424
+
425
+ # Atualiza draft original
426
+ if draft_type == "session":
427
+ existing = self.working_storage.read_session(project, draft_id)
428
+ if existing:
429
+ existing.update(draft)
430
+ self.working_storage.write_session(project, draft_id, existing)
431
+ elif draft_type == "feature":
432
+ existing = self.working_storage.read_feature(project, draft_id)
433
+ if existing:
434
+ existing.update(draft)
435
+ self.working_storage.write_feature(project, draft_id, existing)