errlore 0.1.0__py3-none-any.whl
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.
- errlore/__init__.py +16 -0
- errlore/errmem/__init__.py +21 -0
- errlore/errmem/classifier.py +54 -0
- errlore/errmem/injector.py +156 -0
- errlore/errmem/patterns.py +193 -0
- errlore/errmem/tracker.py +157 -0
- errlore/facade.py +602 -0
- errlore/io/__init__.py +21 -0
- errlore/io/jsonl_index.py +199 -0
- errlore/io/jsonl_writer.py +342 -0
- errlore/io/repair.py +199 -0
- errlore/lessons/__init__.py +16 -0
- errlore/lessons/models.py +129 -0
- errlore/lessons/store.py +495 -0
- errlore/py.typed +0 -0
- errlore/retrieval/__init__.py +66 -0
- errlore/retrieval/backend.py +117 -0
- errlore/retrieval/index.py +288 -0
- errlore/sanitize.py +117 -0
- errlore/trust/__init__.py +11 -0
- errlore/trust/engine.py +602 -0
- errlore-0.1.0.dist-info/METADATA +214 -0
- errlore-0.1.0.dist-info/RECORD +25 -0
- errlore-0.1.0.dist-info/WHEEL +4 -0
- errlore-0.1.0.dist-info/licenses/LICENSE +21 -0
errlore/io/repair.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Auto-repair for corrupted JSONL files.
|
|
2
|
+
|
|
3
|
+
Scans JSONL files, recovers glued JSON records (missing newlines),
|
|
4
|
+
drops unrecoverable lines, and rebuilds the sidecar index.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from errlore.io.jsonl_index import JSONLIndex
|
|
16
|
+
from errlore.io.jsonl_writer import JSONLWriter
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("errlore.io")
|
|
19
|
+
|
|
20
|
+
# Pattern for glued JSON objects: `}{` without a newline between them
|
|
21
|
+
_GLUED_PATTERN = re.compile(r"\}\s*\{")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _try_parse(line: str) -> list[dict[str, object]]:
|
|
25
|
+
"""Attempt to parse a line as JSON. Handles glued records."""
|
|
26
|
+
line = line.strip()
|
|
27
|
+
if not line:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
# Attempt 1: standard JSON
|
|
31
|
+
try:
|
|
32
|
+
obj = json.loads(line)
|
|
33
|
+
if isinstance(obj, dict):
|
|
34
|
+
return [obj]
|
|
35
|
+
return []
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
# Attempt 2: glued JSON (`}{` -> `}\n{`)
|
|
40
|
+
if _GLUED_PATTERN.search(line):
|
|
41
|
+
parts = _GLUED_PATTERN.sub("}\n{", line).split("\n")
|
|
42
|
+
results: list[dict[str, object]] = []
|
|
43
|
+
for part in parts:
|
|
44
|
+
part = part.strip()
|
|
45
|
+
if not part:
|
|
46
|
+
continue
|
|
47
|
+
try:
|
|
48
|
+
obj = json.loads(part)
|
|
49
|
+
if isinstance(obj, dict):
|
|
50
|
+
results.append(obj)
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
pass
|
|
53
|
+
if results:
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RepairStats:
|
|
60
|
+
"""Statistics from a JSONL repair operation."""
|
|
61
|
+
|
|
62
|
+
__slots__ = ("dropped", "fixed", "index_rebuilt", "ok", "total_lines")
|
|
63
|
+
|
|
64
|
+
def __init__(self) -> None:
|
|
65
|
+
self.ok: int = 0
|
|
66
|
+
self.fixed: int = 0
|
|
67
|
+
self.dropped: int = 0
|
|
68
|
+
self.total_lines: int = 0
|
|
69
|
+
self.index_rebuilt: int = 0
|
|
70
|
+
|
|
71
|
+
def as_dict(self) -> dict[str, int]:
|
|
72
|
+
"""Return stats as a plain dict."""
|
|
73
|
+
return {
|
|
74
|
+
"ok": self.ok,
|
|
75
|
+
"fixed": self.fixed,
|
|
76
|
+
"dropped": self.dropped,
|
|
77
|
+
"total_lines": self.total_lines,
|
|
78
|
+
"index_rebuilt": self.index_rebuilt,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def repair_file(
|
|
83
|
+
path: Path,
|
|
84
|
+
*,
|
|
85
|
+
dry_run: bool = False,
|
|
86
|
+
writer: JSONLWriter | None = None,
|
|
87
|
+
) -> RepairStats:
|
|
88
|
+
"""Repair a single JSONL file.
|
|
89
|
+
|
|
90
|
+
Recovers glued records, drops unrecoverable lines, then atomically
|
|
91
|
+
rewrites the file and rebuilds the sidecar index.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
path: Path to the JSONL file.
|
|
95
|
+
dry_run: If True, only report what would happen.
|
|
96
|
+
writer: Optional JSONLWriter instance (created internally if None).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
RepairStats with counts of ok/fixed/dropped lines.
|
|
100
|
+
"""
|
|
101
|
+
stats = RepairStats()
|
|
102
|
+
repaired: list[dict[str, object]] = []
|
|
103
|
+
|
|
104
|
+
if not path.exists():
|
|
105
|
+
# Clean up orphan index if JSONL is gone
|
|
106
|
+
idx = JSONLIndex(path)
|
|
107
|
+
if not dry_run and idx.idx_path.exists():
|
|
108
|
+
with contextlib.suppress(OSError):
|
|
109
|
+
idx.idx_path.unlink()
|
|
110
|
+
return stats
|
|
111
|
+
|
|
112
|
+
w = writer or JSONLWriter()
|
|
113
|
+
|
|
114
|
+
# A3: hold the file lock for the entire read -> parse -> rewrite cycle
|
|
115
|
+
# so that concurrent appends are not lost between read and rewrite.
|
|
116
|
+
with w.lock(path):
|
|
117
|
+
try:
|
|
118
|
+
raw = path.read_bytes()
|
|
119
|
+
except OSError as exc:
|
|
120
|
+
logger.error("Cannot read %s: %s", path, exc)
|
|
121
|
+
return stats
|
|
122
|
+
|
|
123
|
+
for line_bytes in raw.split(b"\n"):
|
|
124
|
+
stats.total_lines += 1
|
|
125
|
+
try:
|
|
126
|
+
line = line_bytes.decode("utf-8").strip()
|
|
127
|
+
except UnicodeDecodeError:
|
|
128
|
+
stats.dropped += 1
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if not line:
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
parsed = _try_parse(line)
|
|
135
|
+
if not parsed:
|
|
136
|
+
stats.dropped += 1
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
if len(parsed) == 1:
|
|
140
|
+
# Single JSON per line
|
|
141
|
+
try:
|
|
142
|
+
json.loads(line)
|
|
143
|
+
stats.ok += 1
|
|
144
|
+
except json.JSONDecodeError:
|
|
145
|
+
stats.fixed += 1
|
|
146
|
+
else:
|
|
147
|
+
# Glued records separated
|
|
148
|
+
stats.fixed += len(parsed)
|
|
149
|
+
|
|
150
|
+
repaired.extend(parsed)
|
|
151
|
+
|
|
152
|
+
if not dry_run and (stats.fixed > 0 or stats.dropped > 0):
|
|
153
|
+
w.atomic_rewrite(path, repaired)
|
|
154
|
+
|
|
155
|
+
if not dry_run:
|
|
156
|
+
idx = JSONLIndex(path)
|
|
157
|
+
stats.index_rebuilt = idx.rebuild()
|
|
158
|
+
|
|
159
|
+
return stats
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def repair_directory(
|
|
163
|
+
directory: Path,
|
|
164
|
+
*,
|
|
165
|
+
dry_run: bool = False,
|
|
166
|
+
recursive: bool = True,
|
|
167
|
+
) -> dict[str, RepairStats]:
|
|
168
|
+
"""Repair all JSONL files in a directory.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
directory: Directory to scan.
|
|
172
|
+
dry_run: If True, only report what would happen.
|
|
173
|
+
recursive: If True, scan subdirectories too.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Mapping of filename to RepairStats.
|
|
177
|
+
"""
|
|
178
|
+
if not directory.is_dir():
|
|
179
|
+
logger.error("Not a directory: %s", directory)
|
|
180
|
+
return {}
|
|
181
|
+
|
|
182
|
+
pattern = "**/*.jsonl" if recursive else "*.jsonl"
|
|
183
|
+
files = sorted(directory.glob(pattern))
|
|
184
|
+
|
|
185
|
+
# Deduplicate resolved paths
|
|
186
|
+
seen: set[str] = set()
|
|
187
|
+
unique: list[Path] = []
|
|
188
|
+
for f in files:
|
|
189
|
+
key = str(f.resolve())
|
|
190
|
+
if key not in seen:
|
|
191
|
+
seen.add(key)
|
|
192
|
+
unique.append(f)
|
|
193
|
+
|
|
194
|
+
writer = JSONLWriter()
|
|
195
|
+
results: dict[str, RepairStats] = {}
|
|
196
|
+
for path in unique:
|
|
197
|
+
results[str(path)] = repair_file(path, dry_run=dry_run, writer=writer)
|
|
198
|
+
|
|
199
|
+
return results
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Lesson subsystem: error tracking, lesson extraction, reinforcement, and decay.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
ErrorRecord -- dataclass for logged errors
|
|
5
|
+
Lesson -- dataclass for extracted lessons
|
|
6
|
+
LessonStore -- persistent store backed by JSONL via errlore.io
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from errlore.lessons.models import ErrorRecord, Lesson
|
|
10
|
+
from errlore.lessons.store import LessonStore
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ErrorRecord",
|
|
14
|
+
"Lesson",
|
|
15
|
+
"LessonStore",
|
|
16
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Data models for the lesson subsystem."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _short_id() -> str:
|
|
12
|
+
"""Generate a 12-char hex ID from uuid4."""
|
|
13
|
+
return uuid.uuid4().hex[:12]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _utc_now_iso() -> str:
|
|
17
|
+
"""Return timezone-aware UTC ISO timestamp."""
|
|
18
|
+
return datetime.now(UTC).isoformat()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ErrorRecord:
|
|
23
|
+
"""A recorded error event."""
|
|
24
|
+
|
|
25
|
+
model: str
|
|
26
|
+
task_type: str
|
|
27
|
+
error_type: str
|
|
28
|
+
message: str
|
|
29
|
+
id: str = field(default_factory=_short_id)
|
|
30
|
+
timestamp: str = field(default_factory=_utc_now_iso)
|
|
31
|
+
resolved: bool = False
|
|
32
|
+
resolution: str = ""
|
|
33
|
+
context: str = ""
|
|
34
|
+
stacktrace: str = ""
|
|
35
|
+
metadata: dict[str, Any] | None = None
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
"""Serialize to a plain dict for JSONL storage."""
|
|
39
|
+
d: dict[str, Any] = {
|
|
40
|
+
"id": self.id,
|
|
41
|
+
"model": self.model,
|
|
42
|
+
"task_type": self.task_type,
|
|
43
|
+
"error_type": self.error_type,
|
|
44
|
+
"message": self.message,
|
|
45
|
+
"timestamp": self.timestamp,
|
|
46
|
+
"resolved": self.resolved,
|
|
47
|
+
"resolution": self.resolution,
|
|
48
|
+
"context": self.context,
|
|
49
|
+
"stacktrace": self.stacktrace,
|
|
50
|
+
}
|
|
51
|
+
if self.metadata is not None:
|
|
52
|
+
d["metadata"] = self.metadata
|
|
53
|
+
return d
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, d: dict[str, Any]) -> ErrorRecord:
|
|
57
|
+
"""Deserialize from a JSONL dict."""
|
|
58
|
+
return cls(
|
|
59
|
+
id=str(d.get("id", _short_id())),
|
|
60
|
+
model=str(d.get("model", "")),
|
|
61
|
+
task_type=str(d.get("task_type", "")),
|
|
62
|
+
error_type=str(d.get("error_type", "")),
|
|
63
|
+
message=str(d.get("message", "")),
|
|
64
|
+
timestamp=str(d.get("timestamp", _utc_now_iso())),
|
|
65
|
+
resolved=bool(d.get("resolved", False)),
|
|
66
|
+
resolution=str(d.get("resolution", "")),
|
|
67
|
+
context=str(d.get("context", "")),
|
|
68
|
+
stacktrace=str(d.get("stacktrace", "")),
|
|
69
|
+
metadata=d.get("metadata"),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class Lesson:
|
|
75
|
+
"""An extracted lesson linking a problem pattern to its solution."""
|
|
76
|
+
|
|
77
|
+
pattern: str
|
|
78
|
+
solution: str
|
|
79
|
+
id: str = field(default_factory=_short_id)
|
|
80
|
+
task_type: str = ""
|
|
81
|
+
error_type: str = ""
|
|
82
|
+
confidence: float = 0.8
|
|
83
|
+
applied_count: int = 0
|
|
84
|
+
created_at: str = field(default_factory=_utc_now_iso)
|
|
85
|
+
updated_at: str = field(default_factory=_utc_now_iso)
|
|
86
|
+
source_error_id: str = ""
|
|
87
|
+
source_errors: list[str] = field(default_factory=list)
|
|
88
|
+
metadata: dict[str, Any] | None = None
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict[str, Any]:
|
|
91
|
+
"""Serialize to a plain dict for JSONL storage."""
|
|
92
|
+
d: dict[str, Any] = {
|
|
93
|
+
"id": self.id,
|
|
94
|
+
"pattern": self.pattern,
|
|
95
|
+
"solution": self.solution,
|
|
96
|
+
"task_type": self.task_type,
|
|
97
|
+
"error_type": self.error_type,
|
|
98
|
+
"confidence": self.confidence,
|
|
99
|
+
"applied_count": self.applied_count,
|
|
100
|
+
"created_at": self.created_at,
|
|
101
|
+
"updated_at": self.updated_at,
|
|
102
|
+
"source_error_id": self.source_error_id,
|
|
103
|
+
"source_errors": self.source_errors,
|
|
104
|
+
}
|
|
105
|
+
if self.metadata is not None:
|
|
106
|
+
d["metadata"] = self.metadata
|
|
107
|
+
return d
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_dict(cls, d: dict[str, Any]) -> Lesson:
|
|
111
|
+
"""Deserialize from a JSONL dict."""
|
|
112
|
+
raw_source_errors = d.get("source_errors")
|
|
113
|
+
source_errors: list[str] = (
|
|
114
|
+
[str(e) for e in raw_source_errors] if isinstance(raw_source_errors, list) else []
|
|
115
|
+
)
|
|
116
|
+
return cls(
|
|
117
|
+
id=str(d.get("id", _short_id())),
|
|
118
|
+
pattern=str(d.get("pattern", "")),
|
|
119
|
+
solution=str(d.get("solution", "")),
|
|
120
|
+
task_type=str(d.get("task_type", "")),
|
|
121
|
+
error_type=str(d.get("error_type", "")),
|
|
122
|
+
confidence=float(d.get("confidence", 0.8)),
|
|
123
|
+
applied_count=int(d.get("applied_count", 0)),
|
|
124
|
+
created_at=str(d.get("created_at", _utc_now_iso())),
|
|
125
|
+
updated_at=str(d.get("updated_at", _utc_now_iso())),
|
|
126
|
+
source_error_id=str(d.get("source_error_id", "")),
|
|
127
|
+
source_errors=source_errors,
|
|
128
|
+
metadata=d.get("metadata"),
|
|
129
|
+
)
|