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/compress.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from realitydiff.models import ClaimState, Evidence, Prediction
|
|
6
|
+
|
|
7
|
+
# Ignore retrieved_at / tiny note rewrites when deciding whether a watch is worth a commit.
|
|
8
|
+
MATERIAL_CONFIDENCE = 1.0
|
|
9
|
+
MATERIAL_WEIGHT = 0.08
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compact_belief(state: ClaimState) -> dict[str, Any]:
|
|
13
|
+
"""High-compression snapshot for the model: keep ids, urls, scores; drop timestamps."""
|
|
14
|
+
return {
|
|
15
|
+
"statement": state.statement,
|
|
16
|
+
"refined_statement": state.refined_statement,
|
|
17
|
+
"confidence": state.confidence,
|
|
18
|
+
"summary": _clip(state.summary, 420),
|
|
19
|
+
"evidence_for": [_compact_evidence(item) for item in state.evidence_for],
|
|
20
|
+
"evidence_against": [_compact_evidence(item) for item in state.evidence_against],
|
|
21
|
+
"unknowns": [
|
|
22
|
+
{"id": item.id, "question": _clip(item.question, 180), "why": _clip(item.why_it_matters, 140)}
|
|
23
|
+
for item in state.unknowns
|
|
24
|
+
],
|
|
25
|
+
"predictions": [_compact_prediction(item) for item in state.predictions],
|
|
26
|
+
"outcome": state.outcome.model_dump(mode="json") if state.outcome else None,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def is_material_change(before: ClaimState, after: ClaimState) -> bool:
|
|
31
|
+
if abs(after.confidence - before.confidence) >= MATERIAL_CONFIDENCE:
|
|
32
|
+
return True
|
|
33
|
+
if before.statement.strip() != after.statement.strip():
|
|
34
|
+
return True
|
|
35
|
+
if _evidence_signature(before.evidence_for) != _evidence_signature(after.evidence_for):
|
|
36
|
+
return True
|
|
37
|
+
if _evidence_signature(before.evidence_against) != _evidence_signature(after.evidence_against):
|
|
38
|
+
return True
|
|
39
|
+
if {u.question.strip().lower() for u in before.unknowns} != {u.question.strip().lower() for u in after.unknowns}:
|
|
40
|
+
return True
|
|
41
|
+
if _prediction_signature(before.predictions) != _prediction_signature(after.predictions):
|
|
42
|
+
return True
|
|
43
|
+
if (before.outcome is None) != (after.outcome is None):
|
|
44
|
+
return True
|
|
45
|
+
if before.outcome and after.outcome and before.outcome.model_dump(mode="json") != after.outcome.model_dump(mode="json"):
|
|
46
|
+
return True
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _compact_evidence(item: Evidence) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"id": item.id,
|
|
53
|
+
"statement": _clip(item.statement, 220),
|
|
54
|
+
"source_url": item.source_url,
|
|
55
|
+
"source_title": item.source_title,
|
|
56
|
+
"weight": round(item.weight, 2),
|
|
57
|
+
"notes": _clip(item.notes, 160),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _compact_prediction(item: Prediction) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"id": item.id,
|
|
64
|
+
"statement": _clip(item.statement, 180),
|
|
65
|
+
"due": item.due,
|
|
66
|
+
"status": item.status,
|
|
67
|
+
"how_to_falsify": _clip(item.how_to_falsify, 140),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _evidence_signature(items: list[Evidence]) -> set[tuple[str, str, str, float]]:
|
|
72
|
+
signed: set[tuple[str, str, str, float]] = set()
|
|
73
|
+
for item in items:
|
|
74
|
+
weight = round(item.weight / MATERIAL_WEIGHT) * MATERIAL_WEIGHT
|
|
75
|
+
signed.add(
|
|
76
|
+
(
|
|
77
|
+
item.id,
|
|
78
|
+
(item.source_url or "").rstrip("/").lower(),
|
|
79
|
+
item.statement.strip().lower()[:160],
|
|
80
|
+
round(weight, 2),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return signed
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _prediction_signature(items: list[Prediction]) -> set[tuple[str, str]]:
|
|
87
|
+
return {(item.statement.strip().lower()[:160], item.status) for item in items}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _clip(text: str, limit: int) -> str:
|
|
91
|
+
text = (text or "").strip()
|
|
92
|
+
if len(text) <= limit:
|
|
93
|
+
return text
|
|
94
|
+
return text[: limit - 1].rstrip() + "…"
|
realitydiff/config.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""User config for installed copies of Reality Diff.
|
|
2
|
+
|
|
3
|
+
The installer can save settings to ``~/.config/realitydiff/env`` (or
|
|
4
|
+
``$XDG_CONFIG_HOME/realitydiff/env``). Values already set in the
|
|
5
|
+
environment always win over the file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
ALLOWED_KEYS = ("XAI_API_KEY", "REALITYDIFF_DB")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def config_file() -> Path:
|
|
17
|
+
override = os.environ.get("REALITYDIFF_CONFIG")
|
|
18
|
+
if override:
|
|
19
|
+
return Path(override).expanduser()
|
|
20
|
+
base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
|
21
|
+
return Path(base) / "realitydiff" / "env"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_env(path: Path | None = None) -> None:
|
|
25
|
+
"""Read KEY=VALUE lines into os.environ without overriding existing values."""
|
|
26
|
+
path = path or config_file()
|
|
27
|
+
try:
|
|
28
|
+
text = path.read_text(encoding="utf-8")
|
|
29
|
+
except OSError:
|
|
30
|
+
return
|
|
31
|
+
found: dict[str, str] = {}
|
|
32
|
+
for raw in text.splitlines():
|
|
33
|
+
line = raw.strip()
|
|
34
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
35
|
+
continue
|
|
36
|
+
key, _, value = line.partition("=")
|
|
37
|
+
key = key.strip()
|
|
38
|
+
if key.startswith("export "):
|
|
39
|
+
key = key[len("export ") :].strip()
|
|
40
|
+
value = value.strip()
|
|
41
|
+
if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
|
|
42
|
+
value = value[1:-1]
|
|
43
|
+
if key in ALLOWED_KEYS and value:
|
|
44
|
+
found[key] = value # last line wins, like sourcing the file
|
|
45
|
+
for key, value in found.items():
|
|
46
|
+
if not os.environ.get(key):
|
|
47
|
+
os.environ[key] = value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def default_db() -> str:
|
|
51
|
+
"""Where claims are stored.
|
|
52
|
+
|
|
53
|
+
REALITYDIFF_DB wins. Inside a Reality Diff checkout (or wherever an old
|
|
54
|
+
``data/realitydiff.sqlite`` already exists) keep that path; otherwise use
|
|
55
|
+
the user data dir so an installed CLI does not drop a data/ folder
|
|
56
|
+
wherever it is run.
|
|
57
|
+
"""
|
|
58
|
+
env = os.environ.get("REALITYDIFF_DB")
|
|
59
|
+
if env:
|
|
60
|
+
return env
|
|
61
|
+
legacy = Path("data") / "realitydiff.sqlite"
|
|
62
|
+
in_checkout = Path("pyproject.toml").is_file() and Path("src", "realitydiff").is_dir()
|
|
63
|
+
if legacy.exists() or in_checkout:
|
|
64
|
+
return "data/realitydiff.sqlite"
|
|
65
|
+
base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
|
|
66
|
+
return str(Path(base) / "realitydiff" / "realitydiff.sqlite")
|
realitydiff/diffing.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from realitydiff.models import ClaimDiff, ClaimState, Evidence, FieldChange, Prediction, Unknown
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _index_evidence(items: list[Evidence]) -> dict[str, Evidence]:
|
|
9
|
+
return {item.id: item for item in items}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _index_unknowns(items: list[Unknown]) -> dict[str, Unknown]:
|
|
13
|
+
return {item.id: item for item in items}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _index_predictions(items: list[Prediction]) -> dict[str, Prediction]:
|
|
17
|
+
return {item.id: item for item in items}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _dump(value: Any) -> Any:
|
|
21
|
+
if hasattr(value, "model_dump"):
|
|
22
|
+
return value.model_dump(mode="json")
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def diff_states(
|
|
27
|
+
before: ClaimState | None,
|
|
28
|
+
after: ClaimState | None,
|
|
29
|
+
*,
|
|
30
|
+
from_id: str | None = None,
|
|
31
|
+
to_id: str | None = None,
|
|
32
|
+
reason: str = "",
|
|
33
|
+
) -> ClaimDiff:
|
|
34
|
+
changes: list[FieldChange] = []
|
|
35
|
+
prev_conf = before.confidence if before else None
|
|
36
|
+
new_conf = after.confidence if after else None
|
|
37
|
+
|
|
38
|
+
if before is None and after is None:
|
|
39
|
+
return ClaimDiff(from_id=from_id, to_id=to_id)
|
|
40
|
+
|
|
41
|
+
if before is None and after is not None:
|
|
42
|
+
changes.append(
|
|
43
|
+
FieldChange(path="statement", before=None, after=after.statement, kind="added")
|
|
44
|
+
)
|
|
45
|
+
return ClaimDiff(
|
|
46
|
+
from_id=from_id,
|
|
47
|
+
to_id=to_id,
|
|
48
|
+
previous_confidence=None,
|
|
49
|
+
new_confidence=after.confidence,
|
|
50
|
+
confidence_delta=after.confidence,
|
|
51
|
+
reason=reason or "Initial belief state.",
|
|
52
|
+
changes=changes
|
|
53
|
+
+ [
|
|
54
|
+
FieldChange(path=f"evidence_for/{item.id}", before=None, after=_dump(item), kind="added")
|
|
55
|
+
for item in after.evidence_for
|
|
56
|
+
]
|
|
57
|
+
+ [
|
|
58
|
+
FieldChange(
|
|
59
|
+
path=f"evidence_against/{item.id}", before=None, after=_dump(item), kind="added"
|
|
60
|
+
)
|
|
61
|
+
for item in after.evidence_against
|
|
62
|
+
]
|
|
63
|
+
+ [
|
|
64
|
+
FieldChange(path=f"unknowns/{item.id}", before=None, after=_dump(item), kind="added")
|
|
65
|
+
for item in after.unknowns
|
|
66
|
+
]
|
|
67
|
+
+ [
|
|
68
|
+
FieldChange(
|
|
69
|
+
path=f"predictions/{item.id}", before=None, after=_dump(item), kind="added"
|
|
70
|
+
)
|
|
71
|
+
for item in after.predictions
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if before is not None and after is None:
|
|
76
|
+
return ClaimDiff(
|
|
77
|
+
from_id=from_id,
|
|
78
|
+
to_id=to_id,
|
|
79
|
+
previous_confidence=before.confidence,
|
|
80
|
+
new_confidence=None,
|
|
81
|
+
confidence_delta=-before.confidence,
|
|
82
|
+
reason=reason or "Belief deleted.",
|
|
83
|
+
changes=[FieldChange(path="state", before=_dump(before), after=None, kind="removed")],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assert before is not None and after is not None
|
|
87
|
+
|
|
88
|
+
for field in ("statement", "refined_statement", "summary"):
|
|
89
|
+
left = getattr(before, field)
|
|
90
|
+
right = getattr(after, field)
|
|
91
|
+
if left != right:
|
|
92
|
+
changes.append(
|
|
93
|
+
FieldChange(
|
|
94
|
+
path=field,
|
|
95
|
+
before=left,
|
|
96
|
+
after=right,
|
|
97
|
+
kind="changed" if left and right else ("added" if right else "removed"),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if before.confidence != after.confidence:
|
|
102
|
+
changes.append(
|
|
103
|
+
FieldChange(
|
|
104
|
+
path="confidence",
|
|
105
|
+
before=before.confidence,
|
|
106
|
+
after=after.confidence,
|
|
107
|
+
kind="changed",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (before.outcome is None) != (after.outcome is None) or (
|
|
112
|
+
before.outcome and after.outcome and before.outcome.model_dump(mode="json") != after.outcome.model_dump(mode="json")
|
|
113
|
+
):
|
|
114
|
+
changes.append(
|
|
115
|
+
FieldChange(
|
|
116
|
+
path="outcome",
|
|
117
|
+
before=_dump(before.outcome) if before.outcome else None,
|
|
118
|
+
after=_dump(after.outcome) if after.outcome else None,
|
|
119
|
+
kind="added" if after.outcome and not before.outcome else ("removed" if before.outcome and not after.outcome else "changed"),
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
for bucket in ("evidence_for", "evidence_against"):
|
|
124
|
+
left_map = _index_evidence(getattr(before, bucket))
|
|
125
|
+
right_map = _index_evidence(getattr(after, bucket))
|
|
126
|
+
for key in sorted(set(left_map) | set(right_map)):
|
|
127
|
+
if key not in right_map:
|
|
128
|
+
changes.append(
|
|
129
|
+
FieldChange(
|
|
130
|
+
path=f"{bucket}/{key}",
|
|
131
|
+
before=_dump(left_map[key]),
|
|
132
|
+
after=None,
|
|
133
|
+
kind="removed",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
elif key not in left_map:
|
|
137
|
+
changes.append(
|
|
138
|
+
FieldChange(
|
|
139
|
+
path=f"{bucket}/{key}",
|
|
140
|
+
before=None,
|
|
141
|
+
after=_dump(right_map[key]),
|
|
142
|
+
kind="added",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
elif left_map[key].model_dump(mode="json") != right_map[key].model_dump(mode="json"):
|
|
146
|
+
changes.append(
|
|
147
|
+
FieldChange(
|
|
148
|
+
path=f"{bucket}/{key}",
|
|
149
|
+
before=_dump(left_map[key]),
|
|
150
|
+
after=_dump(right_map[key]),
|
|
151
|
+
kind="changed",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
left_u = _index_unknowns(before.unknowns)
|
|
156
|
+
right_u = _index_unknowns(after.unknowns)
|
|
157
|
+
for key in sorted(set(left_u) | set(right_u)):
|
|
158
|
+
if key not in right_u:
|
|
159
|
+
changes.append(
|
|
160
|
+
FieldChange(path=f"unknowns/{key}", before=_dump(left_u[key]), after=None, kind="removed")
|
|
161
|
+
)
|
|
162
|
+
elif key not in left_u:
|
|
163
|
+
changes.append(
|
|
164
|
+
FieldChange(path=f"unknowns/{key}", before=None, after=_dump(right_u[key]), kind="added")
|
|
165
|
+
)
|
|
166
|
+
elif left_u[key].model_dump(mode="json") != right_u[key].model_dump(mode="json"):
|
|
167
|
+
changes.append(
|
|
168
|
+
FieldChange(
|
|
169
|
+
path=f"unknowns/{key}",
|
|
170
|
+
before=_dump(left_u[key]),
|
|
171
|
+
after=_dump(right_u[key]),
|
|
172
|
+
kind="changed",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
left_p = _index_predictions(before.predictions)
|
|
177
|
+
right_p = _index_predictions(after.predictions)
|
|
178
|
+
for key in sorted(set(left_p) | set(right_p)):
|
|
179
|
+
if key not in right_p:
|
|
180
|
+
changes.append(
|
|
181
|
+
FieldChange(
|
|
182
|
+
path=f"predictions/{key}", before=_dump(left_p[key]), after=None, kind="removed"
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
elif key not in left_p:
|
|
186
|
+
changes.append(
|
|
187
|
+
FieldChange(
|
|
188
|
+
path=f"predictions/{key}", before=None, after=_dump(right_p[key]), kind="added"
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
elif left_p[key].model_dump(mode="json") != right_p[key].model_dump(mode="json"):
|
|
192
|
+
changes.append(
|
|
193
|
+
FieldChange(
|
|
194
|
+
path=f"predictions/{key}",
|
|
195
|
+
before=_dump(left_p[key]),
|
|
196
|
+
after=_dump(right_p[key]),
|
|
197
|
+
kind="changed",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
delta = round(after.confidence - before.confidence, 1)
|
|
202
|
+
return ClaimDiff(
|
|
203
|
+
from_id=from_id,
|
|
204
|
+
to_id=to_id,
|
|
205
|
+
previous_confidence=before.confidence,
|
|
206
|
+
new_confidence=after.confidence,
|
|
207
|
+
confidence_delta=delta,
|
|
208
|
+
reason=reason,
|
|
209
|
+
changes=changes,
|
|
210
|
+
)
|
realitydiff/hashing.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def canonical_json(value: Any) -> str:
|
|
9
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, default=str)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sha1_hex(payload: str | bytes) -> str:
|
|
13
|
+
data = payload.encode("utf-8") if isinstance(payload, str) else payload
|
|
14
|
+
return hashlib.sha1(data).hexdigest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def object_id(kind: str, value: Any) -> str:
|
|
18
|
+
blob = f"{kind} {canonical_json(value)}"
|
|
19
|
+
return sha1_hex(blob)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def stable_item_id(prefix: str, *parts: str) -> str:
|
|
23
|
+
material = "\n".join(part.strip() for part in parts if part)
|
|
24
|
+
return f"{prefix}_{sha1_hex(material)[:12]}"
|
realitydiff/models.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, field_validator
|
|
7
|
+
|
|
8
|
+
from realitydiff.hashing import object_id, stable_item_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def utcnow() -> datetime:
|
|
12
|
+
return datetime.now(timezone.utc)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Evidence(BaseModel):
|
|
16
|
+
id: str
|
|
17
|
+
statement: str
|
|
18
|
+
source_url: str | None = None
|
|
19
|
+
source_title: str | None = None
|
|
20
|
+
retrieved_at: datetime = Field(default_factory=utcnow)
|
|
21
|
+
weight: float = 0.5
|
|
22
|
+
notes: str = ""
|
|
23
|
+
|
|
24
|
+
@field_validator("weight")
|
|
25
|
+
@classmethod
|
|
26
|
+
def clamp_weight(cls, value: float) -> float:
|
|
27
|
+
return max(0.0, min(1.0, value))
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def make(
|
|
31
|
+
cls,
|
|
32
|
+
statement: str,
|
|
33
|
+
*,
|
|
34
|
+
source_url: str | None = None,
|
|
35
|
+
source_title: str | None = None,
|
|
36
|
+
weight: float = 0.5,
|
|
37
|
+
notes: str = "",
|
|
38
|
+
retrieved_at: datetime | None = None,
|
|
39
|
+
) -> "Evidence":
|
|
40
|
+
return cls(
|
|
41
|
+
id=stable_item_id("ev", source_url or "", statement),
|
|
42
|
+
statement=statement,
|
|
43
|
+
source_url=source_url,
|
|
44
|
+
source_title=source_title,
|
|
45
|
+
weight=weight,
|
|
46
|
+
notes=notes,
|
|
47
|
+
retrieved_at=retrieved_at or utcnow(),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Unknown(BaseModel):
|
|
52
|
+
id: str
|
|
53
|
+
question: str
|
|
54
|
+
why_it_matters: str = ""
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def make(cls, question: str, why_it_matters: str = "") -> "Unknown":
|
|
58
|
+
return cls(
|
|
59
|
+
id=stable_item_id("uk", question),
|
|
60
|
+
question=question,
|
|
61
|
+
why_it_matters=why_it_matters,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ClaimOutcome(BaseModel):
|
|
66
|
+
verdict: Literal["confirmed", "falsified", "settled"]
|
|
67
|
+
summary: str
|
|
68
|
+
source_url: str | None = None
|
|
69
|
+
source_title: str | None = None
|
|
70
|
+
closed_at: datetime = Field(default_factory=utcnow)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Prediction(BaseModel):
|
|
74
|
+
id: str
|
|
75
|
+
statement: str
|
|
76
|
+
due: str | None = None
|
|
77
|
+
status: Literal["open", "confirmed", "falsified"] = "open"
|
|
78
|
+
how_to_falsify: str = ""
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def make(
|
|
82
|
+
cls,
|
|
83
|
+
statement: str,
|
|
84
|
+
*,
|
|
85
|
+
due: str | None = None,
|
|
86
|
+
status: Literal["open", "confirmed", "falsified"] = "open",
|
|
87
|
+
how_to_falsify: str = "",
|
|
88
|
+
) -> "Prediction":
|
|
89
|
+
return cls(
|
|
90
|
+
id=stable_item_id("pr", statement),
|
|
91
|
+
statement=statement,
|
|
92
|
+
due=due,
|
|
93
|
+
status=status,
|
|
94
|
+
how_to_falsify=how_to_falsify,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ClaimState(BaseModel):
|
|
99
|
+
statement: str
|
|
100
|
+
refined_statement: str = ""
|
|
101
|
+
confidence: float = 50.0
|
|
102
|
+
evidence_for: list[Evidence] = Field(default_factory=list)
|
|
103
|
+
evidence_against: list[Evidence] = Field(default_factory=list)
|
|
104
|
+
unknowns: list[Unknown] = Field(default_factory=list)
|
|
105
|
+
predictions: list[Prediction] = Field(default_factory=list)
|
|
106
|
+
summary: str = ""
|
|
107
|
+
outcome: ClaimOutcome | None = None
|
|
108
|
+
|
|
109
|
+
@field_validator("confidence")
|
|
110
|
+
@classmethod
|
|
111
|
+
def clamp_confidence(cls, value: float) -> float:
|
|
112
|
+
return round(max(0.0, min(100.0, value)), 1)
|
|
113
|
+
|
|
114
|
+
def tree_hash(self) -> str:
|
|
115
|
+
data = self.model_dump(mode="json")
|
|
116
|
+
for bucket in ("evidence_for", "evidence_against"):
|
|
117
|
+
for item in data.get(bucket) or []:
|
|
118
|
+
item.pop("retrieved_at", None)
|
|
119
|
+
return object_id("tree", data)
|
|
120
|
+
|
|
121
|
+
def all_evidence(self) -> list[tuple[str, Evidence]]:
|
|
122
|
+
return [("for", item) for item in self.evidence_for] + [
|
|
123
|
+
("against", item) for item in self.evidence_against
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Commit(BaseModel):
|
|
128
|
+
id: str
|
|
129
|
+
claim_id: str
|
|
130
|
+
parent_id: str | None = None
|
|
131
|
+
tree_hash: str
|
|
132
|
+
created_at: datetime
|
|
133
|
+
author: str
|
|
134
|
+
message: str
|
|
135
|
+
reason: str = ""
|
|
136
|
+
previous_confidence: float | None = None
|
|
137
|
+
new_confidence: float
|
|
138
|
+
state: ClaimState
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def create(
|
|
142
|
+
cls,
|
|
143
|
+
*,
|
|
144
|
+
claim_id: str,
|
|
145
|
+
parent_id: str | None,
|
|
146
|
+
state: ClaimState,
|
|
147
|
+
author: str,
|
|
148
|
+
message: str,
|
|
149
|
+
reason: str = "",
|
|
150
|
+
previous_confidence: float | None = None,
|
|
151
|
+
created_at: datetime | None = None,
|
|
152
|
+
) -> "Commit":
|
|
153
|
+
created = created_at or utcnow()
|
|
154
|
+
tree_hash = state.tree_hash()
|
|
155
|
+
payload = {
|
|
156
|
+
"author": author,
|
|
157
|
+
"claim_id": claim_id,
|
|
158
|
+
"created_at": created.isoformat(),
|
|
159
|
+
"message": message,
|
|
160
|
+
"parent": parent_id,
|
|
161
|
+
"reason": reason,
|
|
162
|
+
"tree": tree_hash,
|
|
163
|
+
}
|
|
164
|
+
return cls(
|
|
165
|
+
id=object_id("commit", payload),
|
|
166
|
+
claim_id=claim_id,
|
|
167
|
+
parent_id=parent_id,
|
|
168
|
+
tree_hash=tree_hash,
|
|
169
|
+
created_at=created,
|
|
170
|
+
author=author,
|
|
171
|
+
message=message,
|
|
172
|
+
reason=reason,
|
|
173
|
+
previous_confidence=previous_confidence,
|
|
174
|
+
new_confidence=state.confidence,
|
|
175
|
+
state=state,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class ClaimRecord(BaseModel):
|
|
180
|
+
id: str
|
|
181
|
+
created_at: datetime
|
|
182
|
+
title: str
|
|
183
|
+
watching: bool = True
|
|
184
|
+
watch_interval_seconds: int = 300
|
|
185
|
+
head: str | None = None
|
|
186
|
+
last_watched_at: datetime | None = None
|
|
187
|
+
last_response_id: str | None = None
|
|
188
|
+
compaction_json: str | None = None
|
|
189
|
+
closed: bool = False
|
|
190
|
+
closed_at: datetime | None = None
|
|
191
|
+
outcome_verdict: Literal["confirmed", "falsified", "settled"] | None = None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class FieldChange(BaseModel):
|
|
195
|
+
path: str
|
|
196
|
+
before: Any = None
|
|
197
|
+
after: Any = None
|
|
198
|
+
kind: Literal["added", "removed", "changed"]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ClaimDiff(BaseModel):
|
|
202
|
+
from_id: str | None
|
|
203
|
+
to_id: str | None
|
|
204
|
+
previous_confidence: float | None
|
|
205
|
+
new_confidence: float | None
|
|
206
|
+
confidence_delta: float = 0.0
|
|
207
|
+
reason: str = ""
|
|
208
|
+
changes: list[FieldChange] = Field(default_factory=list)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class BlameEntry(BaseModel):
|
|
212
|
+
path: str
|
|
213
|
+
item_id: str | None = None
|
|
214
|
+
introduced_in: str
|
|
215
|
+
introduced_at: datetime
|
|
216
|
+
author: str
|
|
217
|
+
message: str
|
|
218
|
+
last_changed_in: str
|
|
219
|
+
last_changed_at: datetime
|
|
220
|
+
last_author: str
|
|
221
|
+
last_message: str
|
|
222
|
+
value: Any = None
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class BlameReport(BaseModel):
|
|
226
|
+
claim_id: str
|
|
227
|
+
head: str | None
|
|
228
|
+
entries: list[BlameEntry] = Field(default_factory=list)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class WatchResult(BaseModel):
|
|
232
|
+
claim_id: str
|
|
233
|
+
changed: bool
|
|
234
|
+
commit: Commit | None = None
|
|
235
|
+
diff: ClaimDiff | None = None
|
|
236
|
+
detail: str = ""
|
|
237
|
+
in_progress: bool = False
|
|
238
|
+
cached_tokens: int = 0
|
|
239
|
+
closed: bool = False
|