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,191 @@
|
|
|
1
|
+
"""Scorer RFM para memórias do Cerebro"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ScoringConfig:
|
|
11
|
+
"""Configuração de pesos do scoring RFM"""
|
|
12
|
+
recency_weight: float = 0.3
|
|
13
|
+
frequency_weight: float = 0.2
|
|
14
|
+
importance_weight: float = 0.3
|
|
15
|
+
links_weight: float = 0.2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Scorer:
|
|
19
|
+
"""
|
|
20
|
+
Calcula scores RFM (Recency, Frequency, Importance) para memórias.
|
|
21
|
+
|
|
22
|
+
O score total é uma combinação ponderada de:
|
|
23
|
+
- Recência: quão recente foi o último acesso
|
|
24
|
+
- Frequência: quantas vezes foi acessada
|
|
25
|
+
- Importância: baseada em severity/impact
|
|
26
|
+
- Links: quantas conexões com outras memórias
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: ScoringConfig):
|
|
30
|
+
"""
|
|
31
|
+
Inicializa o Scorer.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
config: Configuração de pesos
|
|
35
|
+
"""
|
|
36
|
+
self.config = config
|
|
37
|
+
|
|
38
|
+
def calculate(self, memory: Dict[str, Any]) -> float:
|
|
39
|
+
"""
|
|
40
|
+
Calcula score total RFM.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
memory: Dados da memória
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Score entre 0.0 e 1.0
|
|
47
|
+
"""
|
|
48
|
+
r = self._recency_score(memory.get("last_accessed"))
|
|
49
|
+
f = self._frequency_score(memory.get("access_count", 0))
|
|
50
|
+
i = self._importance_score(memory)
|
|
51
|
+
l = self._links_score(memory.get("related_to", []))
|
|
52
|
+
|
|
53
|
+
total = (
|
|
54
|
+
self.config.recency_weight * r +
|
|
55
|
+
self.config.frequency_weight * f +
|
|
56
|
+
self.config.importance_weight * i +
|
|
57
|
+
self.config.links_weight * l
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return min(1.0, max(0.0, total))
|
|
61
|
+
|
|
62
|
+
def _recency_score(self, last_accessed: datetime) -> float:
|
|
63
|
+
"""
|
|
64
|
+
Score de recência (0-1).
|
|
65
|
+
|
|
66
|
+
Usa decaimento exponencial baseado em dias desde último acesso.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
last_accessed: Data do último acesso
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Score de recência
|
|
73
|
+
"""
|
|
74
|
+
if not last_accessed:
|
|
75
|
+
return 0.0
|
|
76
|
+
|
|
77
|
+
# BUG-01 FIX: Usa timezone-aware datetime
|
|
78
|
+
now = datetime.now(timezone.utc)
|
|
79
|
+
# Normaliza para timezone-aware se last_accessed for naive (fallback de segurança)
|
|
80
|
+
if last_accessed.tzinfo is None:
|
|
81
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
82
|
+
days_ago = (now - last_accessed).days
|
|
83
|
+
return math.exp(-0.05 * days_ago)
|
|
84
|
+
|
|
85
|
+
def _frequency_score(self, access_count: int) -> float:
|
|
86
|
+
"""
|
|
87
|
+
Score de frequência (0-1).
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
access_count: Número de acessos
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Score de frequência
|
|
94
|
+
"""
|
|
95
|
+
return 1.0 - math.exp(-0.1 * access_count)
|
|
96
|
+
|
|
97
|
+
def _importance_score(self, memory: Dict[str, Any]) -> float:
|
|
98
|
+
"""
|
|
99
|
+
Score de importância baseado em severity/impact.
|
|
100
|
+
|
|
101
|
+
WARN-03 FIX: Considera tanto errors (severity) quanto decisions (status)
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
memory: Dados da memória
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Score de importância
|
|
108
|
+
"""
|
|
109
|
+
# WARN-03 FIX: Erros usam severity, decisions usam status
|
|
110
|
+
mem_type = memory.get("type", "")
|
|
111
|
+
|
|
112
|
+
if mem_type == "error":
|
|
113
|
+
severity_map = {"critical": 1.0, "high": 0.8, "medium": 0.5, "low": 0.2}
|
|
114
|
+
return severity_map.get(memory.get("severity", "low"), 0.2)
|
|
115
|
+
|
|
116
|
+
if mem_type == "decision":
|
|
117
|
+
# Decisões approved têm alta importância
|
|
118
|
+
status_map = {
|
|
119
|
+
"approved": 0.8,
|
|
120
|
+
"superseded": 0.4,
|
|
121
|
+
"deprecated": 0.1,
|
|
122
|
+
"draft": 0.3
|
|
123
|
+
}
|
|
124
|
+
return status_map.get(memory.get("status", "draft"), 0.3)
|
|
125
|
+
|
|
126
|
+
return 0.2
|
|
127
|
+
|
|
128
|
+
def _links_score(self, related_to: list) -> float:
|
|
129
|
+
"""
|
|
130
|
+
Score de links (0-1).
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
related_to: Lista de IDs relacionados
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Score de links
|
|
137
|
+
"""
|
|
138
|
+
if not related_to:
|
|
139
|
+
return 0.0
|
|
140
|
+
return min(1.0, len(related_to) * 0.25)
|
|
141
|
+
|
|
142
|
+
def apply_decay(self, score: float, days: int, decay_rate: float) -> float:
|
|
143
|
+
"""
|
|
144
|
+
Aplica decay temporal ao score.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
score: Score base
|
|
148
|
+
days: Dias de decaimento
|
|
149
|
+
decay_rate: Taxa de decaimento
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Score com decay aplicado
|
|
153
|
+
"""
|
|
154
|
+
return score * math.exp(-decay_rate * days)
|
|
155
|
+
|
|
156
|
+
def calculate_all_scores(self, memory: Dict[str, Any]) -> Dict[str, float]:
|
|
157
|
+
"""
|
|
158
|
+
Calcula todos os scores individuais e total.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
memory: Dados da memória
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dicionário com todos os scores
|
|
165
|
+
"""
|
|
166
|
+
# BUG-01 FIX: Usa timezone-aware datetime
|
|
167
|
+
now = datetime.now(timezone.utc)
|
|
168
|
+
last_accessed = memory.get("last_accessed")
|
|
169
|
+
# Normaliza se for naive
|
|
170
|
+
if last_accessed and isinstance(last_accessed, datetime) and last_accessed.tzinfo is None:
|
|
171
|
+
last_accessed = last_accessed.replace(tzinfo=timezone.utc)
|
|
172
|
+
|
|
173
|
+
r = self._recency_score(last_accessed)
|
|
174
|
+
f = self._frequency_score(memory.get("access_count", 0))
|
|
175
|
+
i = self._importance_score(memory)
|
|
176
|
+
l = self._links_score(memory.get("related_to", []))
|
|
177
|
+
|
|
178
|
+
total = (
|
|
179
|
+
self.config.recency_weight * r +
|
|
180
|
+
self.config.frequency_weight * f +
|
|
181
|
+
self.config.importance_weight * i +
|
|
182
|
+
self.config.links_weight * l
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"recency_score": min(1.0, max(0.0, r)),
|
|
187
|
+
"frequency_score": min(1.0, max(0.0, f)),
|
|
188
|
+
"importance_score": min(1.0, max(0.0, i)),
|
|
189
|
+
"links_score": min(1.0, max(0.0, l)),
|
|
190
|
+
"total_score": min(1.0, max(0.0, total))
|
|
191
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Core do Cerebro: schema, storage bruto, session manager"""
|
|
2
|
+
from .event_schema import Event, EventType, EventOrigin
|
|
3
|
+
from .jsonl_storage import JSONLStorage
|
|
4
|
+
from .session_manager import SessionManager
|
|
5
|
+
|
|
6
|
+
__all__ = ["Event", "EventType", "EventOrigin", "JSONLStorage", "SessionManager"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventType(str, Enum):
|
|
9
|
+
TOOL_CALL = "tool_call"
|
|
10
|
+
GIT_EVENT = "git_event"
|
|
11
|
+
TEST_RESULT = "test_result"
|
|
12
|
+
ERROR = "error"
|
|
13
|
+
CHECKPOINT_CREATED = "checkpoint.created"
|
|
14
|
+
PROMOTION_PERFORMED = "promotion.performed"
|
|
15
|
+
MEMORY_GC = "memory.gc"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EventOrigin(str, Enum):
|
|
19
|
+
CLAUDE_CODE = "claude-code"
|
|
20
|
+
USER = "user"
|
|
21
|
+
CI = "ci"
|
|
22
|
+
HOOK = "hook"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Event(BaseModel):
|
|
26
|
+
"""Evento bruto do Cerebro"""
|
|
27
|
+
|
|
28
|
+
project: str = Field(..., min_length=1)
|
|
29
|
+
origin: EventOrigin
|
|
30
|
+
event_type: EventType
|
|
31
|
+
subtype: str = Field(default="")
|
|
32
|
+
payload: Dict[str, Any] = Field(default_factory=dict)
|
|
33
|
+
tags: list[str] = Field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
# Campos gerados automaticamente
|
|
36
|
+
event_id: str = Field(default_factory=lambda: f"evt_{uuid.uuid4().hex[:8]}")
|
|
37
|
+
session_id: str = Field(default_factory=lambda: f"sess_{uuid.uuid4().hex[:8]}")
|
|
38
|
+
ts: str = Field(default_factory=lambda: datetime.utcnow().isoformat() + "Z")
|
|
39
|
+
|
|
40
|
+
@field_validator("tags")
|
|
41
|
+
@classmethod
|
|
42
|
+
def validate_tags(cls, v):
|
|
43
|
+
return [tag.lower().replace(" ", "-") for tag in v]
|
|
44
|
+
|
|
45
|
+
def to_json_line(self) -> str:
|
|
46
|
+
"""Serializa para JSON line"""
|
|
47
|
+
import json
|
|
48
|
+
return self.model_dump_json()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_json_line(cls, line: str) -> "Event":
|
|
52
|
+
"""Deserializa de JSON line"""
|
|
53
|
+
import json
|
|
54
|
+
data = json.loads(line)
|
|
55
|
+
return cls(**data)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Armazenamento JSONL append-only com rotação mensal"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from src.core.event_schema import Event
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JSONLStorage:
|
|
10
|
+
"""
|
|
11
|
+
Armazenamento append-only de eventos em formato JSONL.
|
|
12
|
+
|
|
13
|
+
Arquivos são organizados por projeto e mês:
|
|
14
|
+
raw/{project}/events-YYYY-MM.jsonl
|
|
15
|
+
|
|
16
|
+
Rotação automática por mês - cada arquivo contém eventos de um mês específico.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, base_dir: Path):
|
|
20
|
+
"""
|
|
21
|
+
Inicializa o armazenamento JSONL.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_dir: Diretório base para a pasta raw/
|
|
25
|
+
"""
|
|
26
|
+
self.base_dir = Path(base_dir)
|
|
27
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
def _get_project_dir(self, project: str) -> Path:
|
|
30
|
+
"""Retorna diretório do projeto"""
|
|
31
|
+
return self.base_dir / project
|
|
32
|
+
|
|
33
|
+
def _get_current_file(self, project: str) -> Path:
|
|
34
|
+
"""
|
|
35
|
+
Retorna o arquivo JSONL atual para um projeto.
|
|
36
|
+
|
|
37
|
+
O arquivo é nomeado com o mês atual: events-YYYY-MM.jsonl
|
|
38
|
+
|
|
39
|
+
WINDOWS FIX: Usa datetime.now(timezone.utc) ao invés de utcnow() deprecated
|
|
40
|
+
"""
|
|
41
|
+
project_dir = self._get_project_dir(project)
|
|
42
|
+
project_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
# WINDOWS FIX: datetime.now(timezone.utc) ao invés de datetime.utcnow()
|
|
45
|
+
from datetime import datetime, timezone
|
|
46
|
+
month_str = datetime.now(timezone.utc).strftime("%Y-%m")
|
|
47
|
+
return project_dir / f"events-{month_str}.jsonl"
|
|
48
|
+
|
|
49
|
+
def append(self, event: Event) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Anexa um evento ao arquivo JSONL do projeto.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
event: Evento a ser anexado
|
|
55
|
+
"""
|
|
56
|
+
jsonl_file = self._get_current_file(event.project)
|
|
57
|
+
|
|
58
|
+
with open(jsonl_file, "a", encoding="utf-8") as f:
|
|
59
|
+
f.write(event.model_dump_json() + "\n")
|
|
60
|
+
|
|
61
|
+
def read(self, project: str) -> List[Event]:
|
|
62
|
+
"""
|
|
63
|
+
Lê todos os eventos de um projeto.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
project: Nome do projeto
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Lista de eventos ordenados por timestamp
|
|
70
|
+
"""
|
|
71
|
+
project_dir = self._get_project_dir(project)
|
|
72
|
+
if not project_dir.exists():
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
events = []
|
|
76
|
+
for jsonl_file in sorted(project_dir.glob("events-*.jsonl")):
|
|
77
|
+
with open(jsonl_file, "r", encoding="utf-8") as f:
|
|
78
|
+
for line in f:
|
|
79
|
+
line = line.strip()
|
|
80
|
+
if line:
|
|
81
|
+
events.append(Event.model_validate_json(line))
|
|
82
|
+
|
|
83
|
+
return events
|
|
84
|
+
|
|
85
|
+
def read_iter(self, project: str):
|
|
86
|
+
"""
|
|
87
|
+
Gerador — não carrega tudo em memória.
|
|
88
|
+
|
|
89
|
+
PERFORMANCE FIX: Permite iterar sobre eventos sem carregar todos em memória
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
project: Nome do projeto
|
|
93
|
+
|
|
94
|
+
Yields:
|
|
95
|
+
Eventos um por um
|
|
96
|
+
"""
|
|
97
|
+
project_dir = self._get_project_dir(project)
|
|
98
|
+
if not project_dir.exists():
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
for jsonl_file in sorted(project_dir.glob("events-*.jsonl")):
|
|
102
|
+
with open(jsonl_file, "r", encoding="utf-8") as f:
|
|
103
|
+
for line in f:
|
|
104
|
+
line = line.strip()
|
|
105
|
+
if line:
|
|
106
|
+
yield Event.model_validate_json(line)
|
|
107
|
+
|
|
108
|
+
def read_last_n(self, project: str, n: int = 1000) -> List[Event]:
|
|
109
|
+
"""
|
|
110
|
+
Lê apenas os N eventos mais recentes.
|
|
111
|
+
|
|
112
|
+
PERFORMANCE FIX: Usa deque com maxlen para memória constante O(n)
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
project: Nome do projeto
|
|
116
|
+
n: Número máximo de eventos (padrão: 1000)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Lista dos N eventos mais recentes
|
|
120
|
+
"""
|
|
121
|
+
from collections import deque
|
|
122
|
+
return list(deque(self.read_iter(project), maxlen=n))
|
|
123
|
+
|
|
124
|
+
def read_since(self, project: str, since: str) -> List[Event]:
|
|
125
|
+
"""
|
|
126
|
+
Lê eventos desde uma data específica.
|
|
127
|
+
|
|
128
|
+
WARN-05 FIX: Filtra por data durante a leitura para evitar carregar tudo em memória
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
project: Nome do projeto
|
|
132
|
+
since: Data mínima (ISO format, ex: "2026-03-01T00:00:00Z")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Lista de eventos após a data especificada
|
|
136
|
+
"""
|
|
137
|
+
from datetime import datetime, timezone
|
|
138
|
+
|
|
139
|
+
project_dir = self._get_project_dir(project)
|
|
140
|
+
if not project_dir.exists():
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
# Parse da data de início
|
|
144
|
+
try:
|
|
145
|
+
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
|
146
|
+
except ValueError:
|
|
147
|
+
# Fallback: lê tudo
|
|
148
|
+
return self.read(project)
|
|
149
|
+
|
|
150
|
+
events = []
|
|
151
|
+
for jsonl_file in sorted(project_dir.glob("events-*.jsonl")):
|
|
152
|
+
with open(jsonl_file, "r", encoding="utf-8") as f:
|
|
153
|
+
for line in f:
|
|
154
|
+
line = line.strip()
|
|
155
|
+
if not line:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
event = Event.model_validate_json(line)
|
|
160
|
+
# Parse do timestamp do evento
|
|
161
|
+
event_ts = event.ts.replace("Z", "+00:00")
|
|
162
|
+
event_dt = datetime.fromisoformat(event_ts)
|
|
163
|
+
|
|
164
|
+
if event_dt >= since_dt:
|
|
165
|
+
events.append(event)
|
|
166
|
+
except Exception:
|
|
167
|
+
# Ignora eventos com timestamp inválido
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
return events
|
|
171
|
+
|
|
172
|
+
def read_range(self, project: str, start_id: str, end_id: str) -> List[Event]:
|
|
173
|
+
"""
|
|
174
|
+
Lê eventos em um intervalo de IDs.
|
|
175
|
+
|
|
176
|
+
HIGH FIX: Valida existência dos IDs antes de filtrar
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
project: Nome do projeto
|
|
180
|
+
start_id: ID inicial (inclusive)
|
|
181
|
+
end_id: ID final (inclusive)
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Lista de eventos no intervalo especificado
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ValueError: Se start_id ou end_id não existirem
|
|
188
|
+
"""
|
|
189
|
+
if not start_id or not end_id:
|
|
190
|
+
raise ValueError("start_id e end_id são obrigatórios")
|
|
191
|
+
|
|
192
|
+
all_events = self.read(project)
|
|
193
|
+
ids = {e.event_id for e in all_events}
|
|
194
|
+
|
|
195
|
+
if start_id not in ids:
|
|
196
|
+
raise ValueError(f"start_id não encontrado: {start_id}")
|
|
197
|
+
if end_id not in ids:
|
|
198
|
+
raise ValueError(f"end_id não encontrado: {end_id}")
|
|
199
|
+
|
|
200
|
+
in_range = False
|
|
201
|
+
result = []
|
|
202
|
+
|
|
203
|
+
for event in all_events:
|
|
204
|
+
if event.event_id == start_id:
|
|
205
|
+
in_range = True
|
|
206
|
+
|
|
207
|
+
if in_range:
|
|
208
|
+
result.append(event)
|
|
209
|
+
|
|
210
|
+
if event.event_id == end_id:
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
def get_file_stats(self, project: str) -> dict:
|
|
216
|
+
"""
|
|
217
|
+
Retorna estatísticas dos arquivos de um projeto.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
project: Nome do projeto
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dicionário com estatísticas por arquivo
|
|
224
|
+
"""
|
|
225
|
+
project_dir = self._get_project_dir(project)
|
|
226
|
+
if not project_dir.exists():
|
|
227
|
+
return {}
|
|
228
|
+
|
|
229
|
+
stats = {}
|
|
230
|
+
for jsonl_file in sorted(project_dir.glob("events-*.jsonl")):
|
|
231
|
+
size_bytes = jsonl_file.stat().st_size
|
|
232
|
+
line_count = sum(1 for _ in open(jsonl_file, "r", encoding="utf-8"))
|
|
233
|
+
stats[jsonl_file.name] = {
|
|
234
|
+
"size_bytes": size_bytes,
|
|
235
|
+
"event_count": line_count
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return stats
|