realitydiff 0.4.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.
- realitydiff/__init__.py +8 -0
- realitydiff/__main__.py +4 -0
- realitydiff/api.py +270 -0
- realitydiff/blame.py +85 -0
- realitydiff/claim.py +104 -0
- realitydiff/cli.py +168 -0
- realitydiff/compress.py +94 -0
- realitydiff/config.py +66 -0
- realitydiff/diffing.py +210 -0
- realitydiff/hashing.py +24 -0
- realitydiff/models.py +239 -0
- realitydiff/reasoner.py +440 -0
- realitydiff/schedule.py +217 -0
- realitydiff/service.py +173 -0
- realitydiff/static/app.js +814 -0
- realitydiff/static/favicon.svg +4 -0
- realitydiff/static/index.html +175 -0
- realitydiff/static/styles.css +477 -0
- realitydiff/store.py +296 -0
- realitydiff/watcher.py +42 -0
- realitydiff/xai.py +506 -0
- realitydiff-0.4.0.dist-info/METADATA +115 -0
- realitydiff-0.4.0.dist-info/RECORD +26 -0
- realitydiff-0.4.0.dist-info/WHEEL +4 -0
- realitydiff-0.4.0.dist-info/entry_points.txt +2 -0
- realitydiff-0.4.0.dist-info/licenses/LICENSE +21 -0
realitydiff/store.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import threading
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from realitydiff.hashing import sha1_hex
|
|
11
|
+
from realitydiff.models import ClaimRecord, ClaimState, Commit, utcnow
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SCHEMA = """
|
|
15
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
created_at TEXT NOT NULL,
|
|
18
|
+
title TEXT NOT NULL,
|
|
19
|
+
watching INTEGER NOT NULL DEFAULT 1,
|
|
20
|
+
watch_interval_seconds INTEGER NOT NULL DEFAULT 300,
|
|
21
|
+
head TEXT,
|
|
22
|
+
last_watched_at TEXT,
|
|
23
|
+
working_json TEXT,
|
|
24
|
+
last_response_id TEXT,
|
|
25
|
+
compaction_json TEXT,
|
|
26
|
+
closed INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
closed_at TEXT,
|
|
28
|
+
outcome_verdict TEXT
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS commits (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
claim_id TEXT NOT NULL,
|
|
34
|
+
parent_id TEXT,
|
|
35
|
+
tree_hash TEXT NOT NULL,
|
|
36
|
+
created_at TEXT NOT NULL,
|
|
37
|
+
author TEXT NOT NULL,
|
|
38
|
+
message TEXT NOT NULL,
|
|
39
|
+
reason TEXT NOT NULL DEFAULT '',
|
|
40
|
+
previous_confidence REAL,
|
|
41
|
+
new_confidence REAL NOT NULL,
|
|
42
|
+
state_json TEXT NOT NULL,
|
|
43
|
+
FOREIGN KEY(claim_id) REFERENCES claims(id)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_commits_claim ON commits(claim_id, created_at);
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BeliefStore:
|
|
51
|
+
"""Content-addressed belief repository. Commits are immutable."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, path: str | Path) -> None:
|
|
54
|
+
self.path = Path(path)
|
|
55
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
self._lock = threading.RLock()
|
|
57
|
+
self._conn = sqlite3.connect(str(self.path), check_same_thread=False, timeout=30)
|
|
58
|
+
self._conn.row_factory = sqlite3.Row
|
|
59
|
+
with self._lock:
|
|
60
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
61
|
+
self._conn.execute("PRAGMA busy_timeout=5000")
|
|
62
|
+
self._conn.execute("PRAGMA foreign_keys=ON")
|
|
63
|
+
self._conn.executescript(SCHEMA)
|
|
64
|
+
self._migrate()
|
|
65
|
+
self._conn.commit()
|
|
66
|
+
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
self._conn.close()
|
|
69
|
+
|
|
70
|
+
def _migrate(self) -> None:
|
|
71
|
+
cols = {row[1] for row in self._conn.execute("PRAGMA table_info(claims)").fetchall()}
|
|
72
|
+
if "last_response_id" not in cols:
|
|
73
|
+
self._conn.execute("ALTER TABLE claims ADD COLUMN last_response_id TEXT")
|
|
74
|
+
if "compaction_json" not in cols:
|
|
75
|
+
self._conn.execute("ALTER TABLE claims ADD COLUMN compaction_json TEXT")
|
|
76
|
+
if "closed" not in cols:
|
|
77
|
+
self._conn.execute("ALTER TABLE claims ADD COLUMN closed INTEGER NOT NULL DEFAULT 0")
|
|
78
|
+
if "closed_at" not in cols:
|
|
79
|
+
self._conn.execute("ALTER TABLE claims ADD COLUMN closed_at TEXT")
|
|
80
|
+
if "outcome_verdict" not in cols:
|
|
81
|
+
self._conn.execute("ALTER TABLE claims ADD COLUMN outcome_verdict TEXT")
|
|
82
|
+
|
|
83
|
+
def create_claim(self, statement: str, *, claim_id: str | None = None) -> ClaimRecord:
|
|
84
|
+
created = utcnow()
|
|
85
|
+
cid = claim_id or f"cl_{sha1_hex(statement + created.isoformat())[:12]}"
|
|
86
|
+
record = ClaimRecord(id=cid, created_at=created, title=statement.strip())
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._conn.execute(
|
|
89
|
+
"""
|
|
90
|
+
INSERT INTO claims (
|
|
91
|
+
id, created_at, title, watching, watch_interval_seconds, head, working_json,
|
|
92
|
+
last_response_id, compaction_json
|
|
93
|
+
)
|
|
94
|
+
VALUES (?, ?, ?, 1, 300, NULL, ?, NULL, NULL)
|
|
95
|
+
""",
|
|
96
|
+
(record.id, record.created_at.isoformat(), record.title, None),
|
|
97
|
+
)
|
|
98
|
+
self._conn.commit()
|
|
99
|
+
return record
|
|
100
|
+
|
|
101
|
+
def get_claim(self, claim_id: str) -> ClaimRecord | None:
|
|
102
|
+
row = self._conn.execute("SELECT * FROM claims WHERE id = ?", (claim_id,)).fetchone()
|
|
103
|
+
if row:
|
|
104
|
+
return self._claim_from_row(row)
|
|
105
|
+
if len(claim_id) >= 8:
|
|
106
|
+
rows = self._conn.execute("SELECT * FROM claims WHERE id LIKE ?", (claim_id + "%",)).fetchall()
|
|
107
|
+
if len(rows) == 1:
|
|
108
|
+
return self._claim_from_row(rows[0])
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def list_claims(self) -> list[ClaimRecord]:
|
|
112
|
+
rows = self._conn.execute("SELECT * FROM claims ORDER BY created_at DESC").fetchall()
|
|
113
|
+
return [self._claim_from_row(row) for row in rows]
|
|
114
|
+
|
|
115
|
+
def set_watching(self, claim_id: str, watching: bool, interval: int | None = None) -> None:
|
|
116
|
+
claim = self.get_claim(claim_id)
|
|
117
|
+
if claim and claim.closed and watching:
|
|
118
|
+
raise RuntimeError("Claim is closed")
|
|
119
|
+
if interval is None:
|
|
120
|
+
self._conn.execute("UPDATE claims SET watching = ? WHERE id = ?", (int(watching), claim_id))
|
|
121
|
+
else:
|
|
122
|
+
self._conn.execute(
|
|
123
|
+
"UPDATE claims SET watching = ?, watch_interval_seconds = ? WHERE id = ?",
|
|
124
|
+
(int(watching), interval, claim_id),
|
|
125
|
+
)
|
|
126
|
+
self._conn.commit()
|
|
127
|
+
|
|
128
|
+
def close_claim(
|
|
129
|
+
self,
|
|
130
|
+
claim_id: str,
|
|
131
|
+
*,
|
|
132
|
+
verdict: Literal["confirmed", "falsified", "settled"],
|
|
133
|
+
closed_at: datetime | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
stamp = (closed_at or utcnow()).isoformat()
|
|
136
|
+
self._conn.execute(
|
|
137
|
+
"""
|
|
138
|
+
UPDATE claims
|
|
139
|
+
SET watching = 0, closed = 1, closed_at = ?, outcome_verdict = ?
|
|
140
|
+
WHERE id = ?
|
|
141
|
+
""",
|
|
142
|
+
(stamp, verdict, claim_id),
|
|
143
|
+
)
|
|
144
|
+
self._conn.commit()
|
|
145
|
+
|
|
146
|
+
def mark_watched(self, claim_id: str, when: datetime | None = None) -> None:
|
|
147
|
+
stamp = (when or utcnow()).isoformat()
|
|
148
|
+
self._conn.execute("UPDATE claims SET last_watched_at = ? WHERE id = ?", (stamp, claim_id))
|
|
149
|
+
self._conn.commit()
|
|
150
|
+
|
|
151
|
+
def set_model_cache(
|
|
152
|
+
self,
|
|
153
|
+
claim_id: str,
|
|
154
|
+
*,
|
|
155
|
+
response_id: str | None = None,
|
|
156
|
+
compaction: dict | None = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
payload = json.dumps(compaction) if compaction else None
|
|
159
|
+
self._conn.execute(
|
|
160
|
+
"UPDATE claims SET last_response_id = ?, compaction_json = ? WHERE id = ?",
|
|
161
|
+
(response_id, payload, claim_id),
|
|
162
|
+
)
|
|
163
|
+
self._conn.commit()
|
|
164
|
+
|
|
165
|
+
def working_state(self, claim_id: str) -> ClaimState | None:
|
|
166
|
+
row = self._conn.execute("SELECT working_json, head FROM claims WHERE id = ?", (claim_id,)).fetchone()
|
|
167
|
+
if not row:
|
|
168
|
+
return None
|
|
169
|
+
if row["working_json"]:
|
|
170
|
+
return ClaimState.model_validate_json(row["working_json"])
|
|
171
|
+
if row["head"]:
|
|
172
|
+
commit = self.get_commit(row["head"])
|
|
173
|
+
return commit.state if commit else None
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
def set_working_state(self, claim_id: str, state: ClaimState | None) -> None:
|
|
177
|
+
payload = state.model_dump_json() if state else None
|
|
178
|
+
self._conn.execute("UPDATE claims SET working_json = ? WHERE id = ?", (payload, claim_id))
|
|
179
|
+
self._conn.commit()
|
|
180
|
+
|
|
181
|
+
def head_commit(self, claim_id: str) -> Commit | None:
|
|
182
|
+
claim = self.get_claim(claim_id)
|
|
183
|
+
if not claim or not claim.head:
|
|
184
|
+
return None
|
|
185
|
+
return self.get_commit(claim.head)
|
|
186
|
+
|
|
187
|
+
def delete_claim(self, claim_id: str) -> None:
|
|
188
|
+
with self._lock:
|
|
189
|
+
self._conn.execute("DELETE FROM commits WHERE claim_id = ?", (claim_id,))
|
|
190
|
+
self._conn.execute("DELETE FROM claims WHERE id = ?", (claim_id,))
|
|
191
|
+
self._conn.commit()
|
|
192
|
+
|
|
193
|
+
def get_commit(self, commit_id: str, *, claim_id: str | None = None) -> Commit | None:
|
|
194
|
+
row = self._conn.execute("SELECT * FROM commits WHERE id = ?", (commit_id,)).fetchone()
|
|
195
|
+
if row:
|
|
196
|
+
commit = self._commit_from_row(row)
|
|
197
|
+
if claim_id and commit.claim_id != claim_id:
|
|
198
|
+
return None
|
|
199
|
+
return commit
|
|
200
|
+
if len(commit_id) < 7:
|
|
201
|
+
return None
|
|
202
|
+
sql = "SELECT * FROM commits WHERE id LIKE ?"
|
|
203
|
+
params: list[str] = [commit_id + "%"]
|
|
204
|
+
if claim_id:
|
|
205
|
+
sql += " AND claim_id = ?"
|
|
206
|
+
params.append(claim_id)
|
|
207
|
+
rows = self._conn.execute(sql, params).fetchall()
|
|
208
|
+
if len(rows) == 1:
|
|
209
|
+
return self._commit_from_row(rows[0])
|
|
210
|
+
if len(rows) > 1:
|
|
211
|
+
raise ValueError(f"Ambiguous commit prefix {commit_id}")
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def log(self, claim_id: str) -> list[Commit]:
|
|
215
|
+
commits: list[Commit] = []
|
|
216
|
+
claim = self.get_claim(claim_id)
|
|
217
|
+
current = claim.head if claim else None
|
|
218
|
+
seen: set[str] = set()
|
|
219
|
+
while current and current not in seen:
|
|
220
|
+
seen.add(current)
|
|
221
|
+
commit = self.get_commit(current)
|
|
222
|
+
if not commit:
|
|
223
|
+
break
|
|
224
|
+
commits.append(commit)
|
|
225
|
+
current = commit.parent_id
|
|
226
|
+
return commits
|
|
227
|
+
|
|
228
|
+
def append_commit(self, commit: Commit) -> Commit:
|
|
229
|
+
with self._lock:
|
|
230
|
+
existing = self.get_commit(commit.id)
|
|
231
|
+
if existing:
|
|
232
|
+
self._conn.execute("UPDATE claims SET head = ?, working_json = NULL WHERE id = ?", (commit.id, commit.claim_id))
|
|
233
|
+
self._conn.commit()
|
|
234
|
+
return existing
|
|
235
|
+
self._conn.execute(
|
|
236
|
+
"""
|
|
237
|
+
INSERT INTO commits (
|
|
238
|
+
id, claim_id, parent_id, tree_hash, created_at, author, message, reason,
|
|
239
|
+
previous_confidence, new_confidence, state_json
|
|
240
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
241
|
+
""",
|
|
242
|
+
(
|
|
243
|
+
commit.id,
|
|
244
|
+
commit.claim_id,
|
|
245
|
+
commit.parent_id,
|
|
246
|
+
commit.tree_hash,
|
|
247
|
+
commit.created_at.isoformat(),
|
|
248
|
+
commit.author,
|
|
249
|
+
commit.message,
|
|
250
|
+
commit.reason,
|
|
251
|
+
commit.previous_confidence,
|
|
252
|
+
commit.new_confidence,
|
|
253
|
+
commit.state.model_dump_json(),
|
|
254
|
+
),
|
|
255
|
+
)
|
|
256
|
+
self._conn.execute(
|
|
257
|
+
"UPDATE claims SET head = ?, working_json = NULL WHERE id = ?",
|
|
258
|
+
(commit.id, commit.claim_id),
|
|
259
|
+
)
|
|
260
|
+
self._conn.commit()
|
|
261
|
+
return commit
|
|
262
|
+
|
|
263
|
+
def _claim_from_row(self, row: sqlite3.Row) -> ClaimRecord:
|
|
264
|
+
return ClaimRecord(
|
|
265
|
+
id=row["id"],
|
|
266
|
+
created_at=datetime.fromisoformat(row["created_at"]),
|
|
267
|
+
title=row["title"],
|
|
268
|
+
watching=bool(row["watching"]),
|
|
269
|
+
watch_interval_seconds=row["watch_interval_seconds"],
|
|
270
|
+
head=row["head"],
|
|
271
|
+
last_watched_at=datetime.fromisoformat(row["last_watched_at"])
|
|
272
|
+
if row["last_watched_at"]
|
|
273
|
+
else None,
|
|
274
|
+
last_response_id=row["last_response_id"] if "last_response_id" in row.keys() else None,
|
|
275
|
+
compaction_json=row["compaction_json"] if "compaction_json" in row.keys() else None,
|
|
276
|
+
closed=bool(row["closed"]) if "closed" in row.keys() else False,
|
|
277
|
+
closed_at=datetime.fromisoformat(row["closed_at"])
|
|
278
|
+
if "closed_at" in row.keys() and row["closed_at"]
|
|
279
|
+
else None,
|
|
280
|
+
outcome_verdict=row["outcome_verdict"] if "outcome_verdict" in row.keys() else None,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def _commit_from_row(self, row: sqlite3.Row) -> Commit:
|
|
284
|
+
return Commit(
|
|
285
|
+
id=row["id"],
|
|
286
|
+
claim_id=row["claim_id"],
|
|
287
|
+
parent_id=row["parent_id"],
|
|
288
|
+
tree_hash=row["tree_hash"],
|
|
289
|
+
created_at=datetime.fromisoformat(row["created_at"]),
|
|
290
|
+
author=row["author"],
|
|
291
|
+
message=row["message"],
|
|
292
|
+
reason=row["reason"],
|
|
293
|
+
previous_confidence=row["previous_confidence"],
|
|
294
|
+
new_confidence=row["new_confidence"],
|
|
295
|
+
state=ClaimState.model_validate_json(row["state_json"]),
|
|
296
|
+
)
|
realitydiff/watcher.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from realitydiff.models import WatchResult
|
|
7
|
+
from realitydiff.service import RealityDiff
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger("realitydiff.watcher")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WatchLoop:
|
|
13
|
+
"""Continuously surveys the internet for every watching claim."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, engine: RealityDiff, *, tick_seconds: float = 15.0) -> None:
|
|
16
|
+
self.engine = engine
|
|
17
|
+
self.tick_seconds = tick_seconds
|
|
18
|
+
self._stop = threading.Event()
|
|
19
|
+
self._thread: threading.Thread | None = None
|
|
20
|
+
|
|
21
|
+
def start(self) -> None:
|
|
22
|
+
if self._thread and self._thread.is_alive():
|
|
23
|
+
return
|
|
24
|
+
self._stop.clear()
|
|
25
|
+
self._thread = threading.Thread(target=self._run, name="realitydiff-watch", daemon=True)
|
|
26
|
+
self._thread.start()
|
|
27
|
+
|
|
28
|
+
def stop(self) -> None:
|
|
29
|
+
self._stop.set()
|
|
30
|
+
if self._thread:
|
|
31
|
+
self._thread.join(timeout=2)
|
|
32
|
+
|
|
33
|
+
def _run(self) -> None:
|
|
34
|
+
while not self._stop.is_set():
|
|
35
|
+
try:
|
|
36
|
+
self._tick()
|
|
37
|
+
except Exception:
|
|
38
|
+
log.exception("watch tick failed")
|
|
39
|
+
self._stop.wait(self.tick_seconds)
|
|
40
|
+
|
|
41
|
+
def _tick(self) -> list[WatchResult]:
|
|
42
|
+
return self.engine.watch_due(author="watcher")
|