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,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)
|