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,260 @@
|
|
|
1
|
+
"""Scan de arquivos de memória do Claude Code.
|
|
2
|
+
|
|
3
|
+
Replica exatamente:
|
|
4
|
+
- src/memdir/memoryScan.ts — scanMemoryFiles() + formatMemoryManifest()
|
|
5
|
+
- Lê apenas primeiras 30 linhas (frontmatter)
|
|
6
|
+
- Exclui MEMORY.md do scan
|
|
7
|
+
- Sort por mtime DESC (newest-first)
|
|
8
|
+
- Cap em 200 arquivos
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Literal, Optional
|
|
16
|
+
|
|
17
|
+
from src.core.paths import MAX_MEMORY_FILES, FRONTMATTER_MAX_LINES, MEMORY_INDEX_FILENAME
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MemoryType = Literal['user', 'feedback', 'project', 'reference']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class MemoryHeader:
|
|
25
|
+
"""Cabeçalho de arquivo de memória.
|
|
26
|
+
|
|
27
|
+
Replica MemoryHeader do Claude Code (memoryScan.ts).
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
filename: Path relativo dentro do memory_dir
|
|
31
|
+
file_path: Path absoluto
|
|
32
|
+
mtime_ms: Timestamp em ms (para sort newest-first)
|
|
33
|
+
description: Descrição do frontmatter
|
|
34
|
+
type: Tipo de memória (user/feedback/project/reference)
|
|
35
|
+
name: Nome da memória (do frontmatter)
|
|
36
|
+
"""
|
|
37
|
+
filename: str
|
|
38
|
+
file_path: Path
|
|
39
|
+
mtime_ms: float
|
|
40
|
+
description: Optional[str] = None
|
|
41
|
+
type: Optional[MemoryType] = None
|
|
42
|
+
name: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
def to_manifest_line(self) -> str:
|
|
45
|
+
"""Formata como linha do manifesto MEMORY.md.
|
|
46
|
+
|
|
47
|
+
Replica formatMemoryManifest() do Claude Code.
|
|
48
|
+
|
|
49
|
+
Formato: "- [type] filename (ISO timestamp): description"
|
|
50
|
+
Exemplo: "- [feedback] feedback_testing.md (2026-03-15T10:00:00Z): não usar mocks"
|
|
51
|
+
"""
|
|
52
|
+
# Converte mtime_ms para datetime
|
|
53
|
+
mtime_sec = self.mtime_ms / 1000
|
|
54
|
+
dt = datetime.fromtimestamp(mtime_sec, tz=timezone.utc)
|
|
55
|
+
iso_str = dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
56
|
+
|
|
57
|
+
# Constrói linha
|
|
58
|
+
parts = ["-"]
|
|
59
|
+
|
|
60
|
+
# Adiciona type se existir
|
|
61
|
+
if self.type:
|
|
62
|
+
parts.append(f"[{self.type}]")
|
|
63
|
+
|
|
64
|
+
# Adiciona filename
|
|
65
|
+
parts.append(self.filename)
|
|
66
|
+
|
|
67
|
+
# Adiciona timestamp
|
|
68
|
+
parts.append(f"({iso_str}):")
|
|
69
|
+
|
|
70
|
+
# Adiciona descrição se existir
|
|
71
|
+
if self.description:
|
|
72
|
+
parts.append(self.description)
|
|
73
|
+
|
|
74
|
+
return " ".join(parts)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_frontmatter(text: str) -> dict:
|
|
78
|
+
"""
|
|
79
|
+
Parse frontmatter YAML de arquivo de memória.
|
|
80
|
+
|
|
81
|
+
Frontmatter esperado:
|
|
82
|
+
```
|
|
83
|
+
---
|
|
84
|
+
name: nome da memória
|
|
85
|
+
description: uma linha — usada para decidir relevância futura
|
|
86
|
+
type: user | feedback | project | reference
|
|
87
|
+
---
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
text: Conteúdo do arquivo (pelo menos primeiras 30 linhas)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dict com name, description, type extraídos
|
|
95
|
+
"""
|
|
96
|
+
result = {
|
|
97
|
+
"name": None,
|
|
98
|
+
"description": None,
|
|
99
|
+
"type": None
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Verifica se tem frontmatter
|
|
103
|
+
if not text.strip().startswith("---"):
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
# Encontra fim do frontmatter
|
|
107
|
+
lines = text.split("\n")
|
|
108
|
+
if len(lines) < 2:
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
# Procura --- de fechamento
|
|
112
|
+
end_idx = -1
|
|
113
|
+
for i in range(1, min(len(lines), 15)): # Frontmatter normalmente < 15 linhas
|
|
114
|
+
if lines[i].strip() == "---":
|
|
115
|
+
end_idx = i
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
if end_idx == -1:
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
# Parse linhas do frontmatter
|
|
122
|
+
for i in range(1, end_idx):
|
|
123
|
+
line = lines[i].strip()
|
|
124
|
+
if ":" in line:
|
|
125
|
+
key, value = line.split(":", 1)
|
|
126
|
+
key = key.strip().lower()
|
|
127
|
+
value = value.strip()
|
|
128
|
+
|
|
129
|
+
if key == "name":
|
|
130
|
+
result["name"] = value
|
|
131
|
+
elif key == "description":
|
|
132
|
+
result["description"] = value
|
|
133
|
+
elif key == "type":
|
|
134
|
+
if value in ['user', 'feedback', 'project', 'reference']:
|
|
135
|
+
result["type"] = value
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def scan_memory_files(memory_dir: Path) -> List[MemoryHeader]:
|
|
141
|
+
"""
|
|
142
|
+
Scan de arquivos de memória.
|
|
143
|
+
|
|
144
|
+
Replica scanMemoryFiles() do Claude Code (memoryScan.ts).
|
|
145
|
+
|
|
146
|
+
- readdir recursivo
|
|
147
|
+
- filtra *.md excluindo MEMORY.md
|
|
148
|
+
- lê só as primeiras 30 linhas (frontmatter apenas)
|
|
149
|
+
- parseia frontmatter: name, description, type
|
|
150
|
+
- sort por mtime DESC (newest-first)
|
|
151
|
+
- cap em MAX_MEMORY_FILES=200
|
|
152
|
+
- retorna [] em caso de erro (nunca levanta exceção)
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
memory_dir: Diretório de memória para scan
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Lista de MemoryHeader ordenada por mtime DESC
|
|
159
|
+
"""
|
|
160
|
+
if not memory_dir.exists():
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Coleta todos os arquivos .md (recursivo)
|
|
165
|
+
md_files = list(memory_dir.rglob("*.md"))
|
|
166
|
+
except Exception:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
headers = []
|
|
170
|
+
|
|
171
|
+
for file_path in md_files:
|
|
172
|
+
try:
|
|
173
|
+
# Exclui MEMORY.md — regra explícita no código do Claude Code
|
|
174
|
+
if file_path.name == MEMORY_INDEX_FILENAME:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# Pega mtime em milissegundos
|
|
178
|
+
stat = file_path.stat()
|
|
179
|
+
mtime_ms = stat.st_mtime * 1000
|
|
180
|
+
|
|
181
|
+
# Lê apenas primeiras 30 linhas (FRONTMATTER_MAX_LINES)
|
|
182
|
+
try:
|
|
183
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
184
|
+
lines = []
|
|
185
|
+
for i, line in enumerate(f):
|
|
186
|
+
if i >= FRONTMATTER_MAX_LINES:
|
|
187
|
+
break
|
|
188
|
+
lines.append(line)
|
|
189
|
+
content = "".join(lines)
|
|
190
|
+
except Exception:
|
|
191
|
+
content = ""
|
|
192
|
+
|
|
193
|
+
# Parse frontmatter
|
|
194
|
+
frontmatter = parse_frontmatter(content)
|
|
195
|
+
|
|
196
|
+
# Cria header
|
|
197
|
+
rel_path = file_path.relative_to(memory_dir)
|
|
198
|
+
header = MemoryHeader(
|
|
199
|
+
filename=str(rel_path),
|
|
200
|
+
file_path=file_path,
|
|
201
|
+
mtime_ms=mtime_ms,
|
|
202
|
+
description=frontmatter.get("description"),
|
|
203
|
+
type=frontmatter.get("type"),
|
|
204
|
+
name=frontmatter.get("name")
|
|
205
|
+
)
|
|
206
|
+
headers.append(header)
|
|
207
|
+
|
|
208
|
+
except Exception:
|
|
209
|
+
# Silenciosamente ignora arquivos com erro
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Sort por mtime DESC (newest-first)
|
|
213
|
+
headers.sort(key=lambda h: h.mtime_ms, reverse=True)
|
|
214
|
+
|
|
215
|
+
# Cap em MAX_MEMORY_FILES
|
|
216
|
+
return headers[:MAX_MEMORY_FILES]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def format_memory_manifest(memories: List[MemoryHeader]) -> str:
|
|
220
|
+
"""
|
|
221
|
+
Formata lista de memórias como manifesto para injeção no prompt.
|
|
222
|
+
|
|
223
|
+
Replica formatMemoryManifest() do Claude Code (memoryScan.ts).
|
|
224
|
+
|
|
225
|
+
Formato:
|
|
226
|
+
```
|
|
227
|
+
Existing memories:
|
|
228
|
+
- [feedback] feedback_testing.md (2026-03-15T10:00:00Z): não usar mocks
|
|
229
|
+
- [project] project_deadline.md (2026-03-14T08:00:00Z): release até sexta
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
memories: Lista de MemoryHeader
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
String formatada para injeção no prompt
|
|
237
|
+
"""
|
|
238
|
+
if not memories:
|
|
239
|
+
return "Existing memories: (none)"
|
|
240
|
+
|
|
241
|
+
lines = ["Existing memories:"]
|
|
242
|
+
|
|
243
|
+
for mem in memories:
|
|
244
|
+
lines.append(mem.to_manifest_line())
|
|
245
|
+
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_existing_memories_summary(memory_dir: Path) -> str:
|
|
250
|
+
"""
|
|
251
|
+
Scan + format em uma única função utilitária.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
memory_dir: Diretório de memória para scan
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Resumo formatado para injeção no prompt
|
|
258
|
+
"""
|
|
259
|
+
memories = scan_memory_files(memory_dir)
|
|
260
|
+
return format_memory_manifest(memories)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Armazenamento Markdown para camada Official"""
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MarkdownStorage:
|
|
10
|
+
"""
|
|
11
|
+
Armazenamento Markdown para camada Official.
|
|
12
|
+
|
|
13
|
+
Armazena decisões e erros em formato Markdown com frontmatter YAML.
|
|
14
|
+
Organização:
|
|
15
|
+
- official/{project}/decisions/{name}.md
|
|
16
|
+
- official/{project}/errors/{name}.md
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, base_path: Path):
|
|
20
|
+
"""
|
|
21
|
+
Inicializa o armazenamento Markdown.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
base_path: Diretório base para a pasta official/
|
|
25
|
+
"""
|
|
26
|
+
self.base_path = base_path
|
|
27
|
+
self.base_path.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
def _ensure_project_dir(self, project: str, subdir: str) -> Path:
|
|
30
|
+
"""
|
|
31
|
+
Garante que diretório do projeto existe.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
project: Nome do projeto
|
|
35
|
+
subdir: Subdiretório (decisions ou errors)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Path do diretório criado
|
|
39
|
+
"""
|
|
40
|
+
dir_path = self.base_path / project / subdir
|
|
41
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
return dir_path
|
|
43
|
+
|
|
44
|
+
def _parse_frontmatter(self, content: str) -> Tuple[Dict[str, Any], str]:
|
|
45
|
+
"""
|
|
46
|
+
Extrai frontmatter YAML do conteúdo Markdown.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
content: Conteúdo completo do arquivo Markdown
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tupla com (frontmatter, corpo)
|
|
53
|
+
"""
|
|
54
|
+
match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
|
|
55
|
+
|
|
56
|
+
if not match:
|
|
57
|
+
return {}, content
|
|
58
|
+
|
|
59
|
+
frontmatter = yaml.safe_load(match.group(1))
|
|
60
|
+
body = match.group(2)
|
|
61
|
+
|
|
62
|
+
return frontmatter, body
|
|
63
|
+
|
|
64
|
+
def _format_frontmatter(self, frontmatter: Dict[str, Any]) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Formata frontmatter YAML.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
frontmatter: Dicionário com dados do frontmatter
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
String formatada com delimitadores YAML
|
|
73
|
+
"""
|
|
74
|
+
return f"---\n{yaml.dump(frontmatter, allow_unicode=True, default_flow_style=False)}---\n"
|
|
75
|
+
|
|
76
|
+
def write_decision(self, project: str, name: str, frontmatter: Dict[str, Any], content: str) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Escreve decisão em Markdown.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
project: Nome do projeto
|
|
82
|
+
name: Nome da decisão
|
|
83
|
+
frontmatter: Dados do frontmatter
|
|
84
|
+
content: Corpo da decisão
|
|
85
|
+
"""
|
|
86
|
+
dir_path = self._ensure_project_dir(project, "decisions")
|
|
87
|
+
md_path = dir_path / f"{name}.md"
|
|
88
|
+
|
|
89
|
+
frontmatter["type"] = "decision"
|
|
90
|
+
full_content = self._format_frontmatter(frontmatter) + content
|
|
91
|
+
|
|
92
|
+
md_path.write_text(full_content, encoding="utf-8")
|
|
93
|
+
|
|
94
|
+
def read_decision(self, project: str, name: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
95
|
+
"""
|
|
96
|
+
Lê decisão de Markdown.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
project: Nome do projeto
|
|
100
|
+
name: Nome da decisão
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Tupla com (frontmatter, corpo) ou (None, None) se não existir
|
|
104
|
+
"""
|
|
105
|
+
md_path = self.base_path / project / "decisions" / f"{name}.md"
|
|
106
|
+
|
|
107
|
+
if not md_path.exists():
|
|
108
|
+
return None, None
|
|
109
|
+
|
|
110
|
+
content = md_path.read_text(encoding="utf-8")
|
|
111
|
+
return self._parse_frontmatter(content)
|
|
112
|
+
|
|
113
|
+
def write_error(self, project: str, name: str, frontmatter: Dict[str, Any], content: str) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Escreve erro em Markdown.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
project: Nome do projeto
|
|
119
|
+
name: Nome do erro
|
|
120
|
+
frontmatter: Dados do frontmatter
|
|
121
|
+
content: Corpo do post-mortem
|
|
122
|
+
"""
|
|
123
|
+
dir_path = self._ensure_project_dir(project, "errors")
|
|
124
|
+
md_path = dir_path / f"{name}.md"
|
|
125
|
+
|
|
126
|
+
frontmatter["type"] = "error"
|
|
127
|
+
full_content = self._format_frontmatter(frontmatter) + content
|
|
128
|
+
|
|
129
|
+
md_path.write_text(full_content, encoding="utf-8")
|
|
130
|
+
|
|
131
|
+
def read_error(self, project: str, name: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
132
|
+
"""
|
|
133
|
+
Lê erro de Markdown.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
project: Nome do projeto
|
|
137
|
+
name: Nome do erro
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Tupla com (frontmatter, corpo) ou (None, None) se não existir
|
|
141
|
+
"""
|
|
142
|
+
md_path = self.base_path / project / "errors" / f"{name}.md"
|
|
143
|
+
|
|
144
|
+
if not md_path.exists():
|
|
145
|
+
return None, None
|
|
146
|
+
|
|
147
|
+
content = md_path.read_text(encoding="utf-8")
|
|
148
|
+
return self._parse_frontmatter(content)
|
|
149
|
+
|
|
150
|
+
def list_official(self, project: str, subdir: str) -> List[Dict[str, Any]]:
|
|
151
|
+
"""
|
|
152
|
+
Lista itens de um subdiretório official.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
project: Nome do projeto
|
|
156
|
+
subdir: Subdiretório (decisions ou errors)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Lista de frontmatters com nome do arquivo
|
|
160
|
+
"""
|
|
161
|
+
dir_path = self.base_path / project / subdir
|
|
162
|
+
|
|
163
|
+
if not dir_path.exists():
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
items = []
|
|
167
|
+
for md_file in sorted(dir_path.glob("*.md")):
|
|
168
|
+
frontmatter, _ = self._parse_frontmatter(md_file.read_text(encoding="utf-8"))
|
|
169
|
+
if frontmatter:
|
|
170
|
+
frontmatter["_file"] = md_file.name
|
|
171
|
+
items.append(frontmatter)
|
|
172
|
+
|
|
173
|
+
return items
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Templates para camada Official do Cerebro"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorTemplate:
|
|
7
|
+
"""Template para post-mortem de erro"""
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def frontmatter(
|
|
11
|
+
error_id: str,
|
|
12
|
+
severity: str,
|
|
13
|
+
status: str,
|
|
14
|
+
category: str,
|
|
15
|
+
area: str,
|
|
16
|
+
project: str,
|
|
17
|
+
tags: List[str] = None,
|
|
18
|
+
related_to: List[str] = None,
|
|
19
|
+
similar_to: List[str] = None
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""Cria frontmatter para erro"""
|
|
22
|
+
return {
|
|
23
|
+
"id": error_id,
|
|
24
|
+
"type": "error",
|
|
25
|
+
"status": status,
|
|
26
|
+
"severity": severity,
|
|
27
|
+
"impact": severity,
|
|
28
|
+
"category": category,
|
|
29
|
+
"area": area,
|
|
30
|
+
"project": project,
|
|
31
|
+
"tags": tags or [],
|
|
32
|
+
"related_to": related_to or [],
|
|
33
|
+
"similar_to": similar_to or []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def body(
|
|
38
|
+
error_original: str,
|
|
39
|
+
causa_raiz: str,
|
|
40
|
+
solucao_aplicada: str,
|
|
41
|
+
prevencao_futura: Optional[str] = None
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Cria corpo do post-mortem de erro"""
|
|
44
|
+
sections = [
|
|
45
|
+
"# Erro Original",
|
|
46
|
+
"",
|
|
47
|
+
error_original,
|
|
48
|
+
"",
|
|
49
|
+
"# Causa Raiz",
|
|
50
|
+
"",
|
|
51
|
+
causa_raiz,
|
|
52
|
+
"",
|
|
53
|
+
"# Solução Aplicada",
|
|
54
|
+
"",
|
|
55
|
+
solucao_aplicada
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if prevencao_futura:
|
|
59
|
+
sections.extend([
|
|
60
|
+
"",
|
|
61
|
+
"# Prevenção Futura",
|
|
62
|
+
"",
|
|
63
|
+
prevencao_futura
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
return "\n".join(sections)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DecisionTemplate:
|
|
70
|
+
"""Template para decisão arquitetural"""
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def frontmatter(
|
|
74
|
+
decision_id: str,
|
|
75
|
+
title: str,
|
|
76
|
+
status: str,
|
|
77
|
+
date: str,
|
|
78
|
+
project: str,
|
|
79
|
+
tags: List[str] = None,
|
|
80
|
+
related_to: List[str] = None
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Cria frontmatter para decisão"""
|
|
83
|
+
return {
|
|
84
|
+
"id": decision_id,
|
|
85
|
+
"type": "decision",
|
|
86
|
+
"title": title,
|
|
87
|
+
"status": status,
|
|
88
|
+
"date": date,
|
|
89
|
+
"project": project,
|
|
90
|
+
"tags": tags or [],
|
|
91
|
+
"related_to": related_to or []
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def body(
|
|
96
|
+
contexto: str,
|
|
97
|
+
decisao: str,
|
|
98
|
+
alternativas: Optional[str] = None,
|
|
99
|
+
consequencias: Optional[str] = None
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Cria corpo da decisão"""
|
|
102
|
+
sections = [
|
|
103
|
+
"# Contexto",
|
|
104
|
+
"",
|
|
105
|
+
contexto,
|
|
106
|
+
"",
|
|
107
|
+
"# Decisão",
|
|
108
|
+
"",
|
|
109
|
+
decisao
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
if alternativas:
|
|
113
|
+
sections.extend([
|
|
114
|
+
"",
|
|
115
|
+
"# Alternativas Consideradas",
|
|
116
|
+
"",
|
|
117
|
+
alternativas
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
if consequencias:
|
|
121
|
+
sections.extend([
|
|
122
|
+
"",
|
|
123
|
+
"# Consequências",
|
|
124
|
+
"",
|
|
125
|
+
consequencias
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
return "\n".join(sections)
|