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.
@@ -0,0 +1,8 @@
1
+ """Reality Diff — Git for beliefs."""
2
+
3
+ from realitydiff.claim import Claim
4
+ from realitydiff.models import ClaimState, Commit
5
+ from realitydiff.service import RealityDiff
6
+
7
+ __all__ = ["Claim", "ClaimState", "Commit", "RealityDiff"]
8
+ __version__ = "0.4.0"
@@ -0,0 +1,4 @@
1
+ from realitydiff.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
realitydiff/api.py ADDED
@@ -0,0 +1,270 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from contextlib import asynccontextmanager
5
+ from pathlib import Path
6
+
7
+ from fastapi import FastAPI, HTTPException, Query, Request
8
+ from fastapi.responses import FileResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from pydantic import BaseModel, Field
11
+ from starlette.exceptions import HTTPException as StarletteHTTPException
12
+
13
+ from realitydiff import __version__
14
+ from realitydiff.config import default_db, load_env
15
+ from realitydiff.models import BlameReport, ClaimDiff, ClaimRecord, Commit, WatchResult
16
+ from realitydiff.schedule import install as install_service
17
+ from realitydiff.schedule import status as service_status
18
+ from realitydiff.schedule import uninstall as uninstall_service
19
+ from realitydiff.service import RealityDiff
20
+ from realitydiff.watcher import WatchLoop
21
+ from realitydiff.xai import XAIError
22
+
23
+
24
+ STATIC = Path(__file__).parent / "static"
25
+ load_env()
26
+ DEFAULT_DB = default_db()
27
+ log = logging.getLogger("realitydiff.api")
28
+
29
+
30
+ class CreateClaimBody(BaseModel):
31
+ statement: str = Field(min_length=3, max_length=2000)
32
+ author: str = Field(default="reasoner", max_length=80)
33
+
34
+
35
+ class CommitBody(BaseModel):
36
+ message: str = Field(min_length=1, max_length=500)
37
+ author: str = Field(default="user", max_length=80)
38
+ reason: str = Field(default="", max_length=4000)
39
+
40
+
41
+ class RevertBody(BaseModel):
42
+ sha: str = Field(min_length=7, max_length=64)
43
+ author: str = Field(default="user", max_length=80)
44
+
45
+
46
+ class ServiceBody(BaseModel):
47
+ interval_seconds: int = Field(default=300, ge=30, le=86_400)
48
+
49
+
50
+ class WatchConfigBody(BaseModel):
51
+ watching: bool = True
52
+ interval_seconds: int | None = Field(default=None, ge=30, le=86_400)
53
+
54
+
55
+ def create_app(
56
+ engine: RealityDiff | None = None,
57
+ *,
58
+ start_watcher: bool = True,
59
+ ) -> FastAPI:
60
+ engine = engine or RealityDiff.open(DEFAULT_DB)
61
+ watcher = WatchLoop(engine)
62
+
63
+ @asynccontextmanager
64
+ async def lifespan(_app: FastAPI):
65
+ if start_watcher:
66
+ watcher.start()
67
+ yield
68
+ watcher.stop()
69
+
70
+ app = FastAPI(title="Reality Diff", version=__version__, lifespan=lifespan)
71
+ app.state.engine = engine
72
+ app.state.watcher = watcher
73
+
74
+ @app.exception_handler(StarletteHTTPException)
75
+ async def _http_exc(request: Request, exc: StarletteHTTPException):
76
+ detail = exc.detail
77
+ if not isinstance(detail, str):
78
+ detail = str(detail)
79
+ return JSONResponse(status_code=exc.status_code, content={"detail": detail})
80
+
81
+ @app.get("/api/health")
82
+ def health() -> dict[str, str]:
83
+ try:
84
+ engine.store.list_claims()
85
+ db = "ok"
86
+ except Exception as exc:
87
+ db = f"error:{exc}"
88
+ status = "ok" if db == "ok" else "degraded"
89
+ return {"status": status, "name": "realitydiff", "version": __version__, "db": db}
90
+
91
+ @app.get("/api/claims")
92
+ def list_claims() -> list[dict]:
93
+ out = []
94
+ for record in engine.store.list_claims():
95
+ head = engine.store.head_commit(record.id)
96
+ history = engine.store.log(record.id)
97
+ out.append(
98
+ {
99
+ **record.model_dump(mode="json"),
100
+ "confidence": head.state.confidence if head else None,
101
+ "summary": head.state.summary if head else "",
102
+ "commit_count": len(history),
103
+ "spark": [c.new_confidence for c in reversed(history[-24:])],
104
+ }
105
+ )
106
+ return out
107
+
108
+ @app.post("/api/claims")
109
+ def create_claim(body: CreateClaimBody) -> dict:
110
+ try:
111
+ living, commit = engine.open_claim(body.statement, author=body.author)
112
+ except ValueError as exc:
113
+ raise HTTPException(400, str(exc)) from exc
114
+ except XAIError as exc:
115
+ raise HTTPException(502, f"Reasoner failed: {exc}") from exc
116
+ except Exception as exc:
117
+ log.exception("open_claim failed")
118
+ raise HTTPException(502, f"Reasoner failed: {exc}") from exc
119
+ return {
120
+ "claim": living.refresh().model_dump(mode="json"),
121
+ "commit": commit.model_dump(mode="json"),
122
+ }
123
+
124
+ @app.get("/api/claims/{claim_id}")
125
+ def get_claim(claim_id: str) -> dict:
126
+ living = _claim(engine, claim_id)
127
+ head = living.head
128
+ working = living.working
129
+ return {
130
+ "claim": living.refresh().model_dump(mode="json"),
131
+ "head": head.model_dump(mode="json") if head else None,
132
+ "working": working.model_dump(mode="json") if working else None,
133
+ "dirty": bool(head and working and head.state.tree_hash() != working.tree_hash()),
134
+ }
135
+
136
+ @app.get("/api/claims/{claim_id}/log")
137
+ def claim_log(claim_id: str) -> list[Commit]:
138
+ return _claim(engine, claim_id).log()
139
+
140
+ @app.get("/api/claims/{claim_id}/diff")
141
+ def claim_diff(
142
+ claim_id: str,
143
+ a: str | None = Query(default=None),
144
+ b: str | None = Query(default=None),
145
+ ) -> ClaimDiff:
146
+ try:
147
+ return _claim(engine, claim_id).diff(a, b)
148
+ except ValueError as exc:
149
+ raise HTTPException(400, str(exc)) from exc
150
+
151
+ @app.get("/api/claims/{claim_id}/blame")
152
+ def claim_blame(claim_id: str) -> BlameReport:
153
+ return _claim(engine, claim_id).blame()
154
+
155
+ @app.post("/api/claims/{claim_id}/commit")
156
+ def claim_commit(claim_id: str, body: CommitBody) -> Commit:
157
+ try:
158
+ return _claim(engine, claim_id).commit(body.message, author=body.author, reason=body.reason)
159
+ except RuntimeError as exc:
160
+ raise HTTPException(409, str(exc)) from exc
161
+
162
+ @app.post("/api/claims/{claim_id}/revert")
163
+ def claim_revert(claim_id: str, body: RevertBody) -> Commit:
164
+ try:
165
+ return _claim(engine, claim_id).revert(body.sha, author=body.author)
166
+ except KeyError as exc:
167
+ raise HTTPException(404, str(exc)) from exc
168
+ except (ValueError, RuntimeError) as exc:
169
+ raise HTTPException(400, str(exc)) from exc
170
+
171
+ @app.get("/api/claims/{claim_id}/at/{sha}")
172
+ def claim_at(claim_id: str, sha: str) -> dict:
173
+ living = _claim(engine, claim_id)
174
+ try:
175
+ commit = living._resolve(sha)
176
+ except ValueError as exc:
177
+ raise HTTPException(400, str(exc)) from exc
178
+ if commit is None:
179
+ raise HTTPException(404, f"Unknown commit {sha}")
180
+ return {
181
+ "claim": living.refresh().model_dump(mode="json"),
182
+ "commit": commit.model_dump(mode="json"),
183
+ "parent_id": commit.parent_id,
184
+ "is_head": living.head is not None and living.head.id == commit.id,
185
+ "browse": True,
186
+ }
187
+
188
+ @app.get("/api/claims/{claim_id}/tree")
189
+ def claim_tree(claim_id: str) -> dict:
190
+ living = _claim(engine, claim_id)
191
+ history = living.log()
192
+ nodes = []
193
+ for commit in reversed(history):
194
+ nodes.append(
195
+ {
196
+ "id": commit.id,
197
+ "parent_id": commit.parent_id,
198
+ "message": commit.message,
199
+ "author": commit.author,
200
+ "created_at": commit.created_at.isoformat(),
201
+ "confidence": commit.new_confidence,
202
+ "previous_confidence": commit.previous_confidence,
203
+ "reason": commit.reason,
204
+ "is_head": living.head is not None and living.head.id == commit.id,
205
+ }
206
+ )
207
+ return {"claim_id": claim_id, "head": living.head.id if living.head else None, "nodes": nodes}
208
+
209
+ @app.post("/api/claims/{claim_id}/watch")
210
+ def claim_watch(claim_id: str) -> WatchResult:
211
+ _claim(engine, claim_id)
212
+ try:
213
+ return engine.watch(claim_id, author="watcher")
214
+ except XAIError as exc:
215
+ raise HTTPException(502, f"Watch failed: {exc}") from exc
216
+ except Exception as exc:
217
+ log.exception("watch failed")
218
+ raise HTTPException(502, f"Watch failed: {exc}") from exc
219
+
220
+ @app.post("/api/claims/{claim_id}/watching")
221
+ def set_watching(claim_id: str, body: WatchConfigBody) -> ClaimRecord:
222
+ _claim(engine, claim_id)
223
+ try:
224
+ engine.store.set_watching(claim_id, body.watching, body.interval_seconds)
225
+ except RuntimeError as exc:
226
+ raise HTTPException(409, str(exc)) from exc
227
+ record = engine.store.get_claim(claim_id)
228
+ assert record is not None
229
+ return record
230
+
231
+ @app.get("/api/service")
232
+ def get_service() -> dict:
233
+ return {**service_status(), "db": str(Path(engine.store.path).expanduser().resolve())}
234
+
235
+ @app.post("/api/service")
236
+ def put_service(body: ServiceBody) -> dict:
237
+ try:
238
+ return install_service(body.interval_seconds, db=DEFAULT_DB)
239
+ except Exception as exc:
240
+ raise HTTPException(500, f"Could not install watch service: {exc}") from exc
241
+
242
+ @app.delete("/api/service")
243
+ def drop_service() -> dict:
244
+ try:
245
+ return uninstall_service()
246
+ except Exception as exc:
247
+ raise HTTPException(500, f"Could not remove watch service: {exc}") from exc
248
+
249
+ if STATIC.exists():
250
+ app.mount("/static", StaticFiles(directory=STATIC), name="static")
251
+
252
+ @app.get("/")
253
+ def index() -> FileResponse:
254
+ return FileResponse(STATIC / "index.html", headers={"Cache-Control": "no-store"})
255
+
256
+ @app.get("/favicon.svg")
257
+ def favicon() -> FileResponse:
258
+ path = STATIC / "favicon.svg"
259
+ if not path.exists():
260
+ raise HTTPException(404, "no favicon")
261
+ return FileResponse(path, media_type="image/svg+xml")
262
+
263
+ return app
264
+
265
+
266
+ def _claim(engine: RealityDiff, claim_id: str):
267
+ try:
268
+ return engine.claim(claim_id)
269
+ except KeyError as exc:
270
+ raise HTTPException(404, f"Unknown claim {claim_id}") from exc
realitydiff/blame.py ADDED
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from realitydiff.models import BlameEntry, BlameReport, ClaimState, Commit
4
+
5
+
6
+ def blame_history(claim_id: str, history: list[Commit]) -> BlameReport:
7
+ """history is newest-first (git log order)."""
8
+ if not history:
9
+ return BlameReport(claim_id=claim_id, head=None, entries=[])
10
+
11
+ chronological = list(reversed(history))
12
+ head = history[0].id
13
+ first_seen: dict[str, tuple[Commit, object]] = {}
14
+ last_seen: dict[str, tuple[Commit, object]] = {}
15
+
16
+ def note(path: str, commit: Commit, value: object) -> None:
17
+ if path not in first_seen:
18
+ first_seen[path] = (commit, value)
19
+ last_seen[path] = (commit, value)
20
+
21
+ previous: ClaimState | None = None
22
+ for commit in chronological:
23
+ state = commit.state
24
+ if previous is None or previous.statement != state.statement:
25
+ note("statement", commit, state.statement)
26
+ if previous is None or previous.refined_statement != state.refined_statement:
27
+ note("refined_statement", commit, state.refined_statement)
28
+ if previous is None or previous.confidence != state.confidence:
29
+ note("confidence", commit, state.confidence)
30
+ if previous is None or previous.summary != state.summary:
31
+ note("summary", commit, state.summary)
32
+ if previous is None or (
33
+ (previous.outcome is None) != (state.outcome is None)
34
+ or (
35
+ previous.outcome
36
+ and state.outcome
37
+ and previous.outcome.model_dump(mode="json") != state.outcome.model_dump(mode="json")
38
+ )
39
+ ):
40
+ note("outcome", commit, state.outcome.model_dump(mode="json") if state.outcome else None)
41
+
42
+ prev_for = {item.id: item for item in previous.evidence_for} if previous else {}
43
+ prev_against = {item.id: item for item in previous.evidence_against} if previous else {}
44
+ prev_unknowns = {item.id: item for item in previous.unknowns} if previous else {}
45
+ prev_preds = {item.id: item for item in previous.predictions} if previous else {}
46
+
47
+ for item in state.evidence_for:
48
+ dumped = item.model_dump(mode="json")
49
+ if prev_for.get(item.id) is None or prev_for[item.id].model_dump(mode="json") != dumped:
50
+ note(f"evidence_for/{item.id}", commit, dumped)
51
+ for item in state.evidence_against:
52
+ dumped = item.model_dump(mode="json")
53
+ if prev_against.get(item.id) is None or prev_against[item.id].model_dump(mode="json") != dumped:
54
+ note(f"evidence_against/{item.id}", commit, dumped)
55
+ for item in state.unknowns:
56
+ dumped = item.model_dump(mode="json")
57
+ if prev_unknowns.get(item.id) is None or prev_unknowns[item.id].model_dump(mode="json") != dumped:
58
+ note(f"unknowns/{item.id}", commit, dumped)
59
+ for item in state.predictions:
60
+ dumped = item.model_dump(mode="json")
61
+ if prev_preds.get(item.id) is None or prev_preds[item.id].model_dump(mode="json") != dumped:
62
+ note(f"predictions/{item.id}", commit, dumped)
63
+ previous = state
64
+
65
+ entries: list[BlameEntry] = []
66
+ for path, (introduced, value) in first_seen.items():
67
+ last_commit, last_value = last_seen[path]
68
+ item_id = path.split("/", 1)[1] if "/" in path else None
69
+ entries.append(
70
+ BlameEntry(
71
+ path=path,
72
+ item_id=item_id,
73
+ introduced_in=introduced.id,
74
+ introduced_at=introduced.created_at,
75
+ author=introduced.author,
76
+ message=introduced.message,
77
+ last_changed_in=last_commit.id,
78
+ last_changed_at=last_commit.created_at,
79
+ last_author=last_commit.author,
80
+ last_message=last_commit.message,
81
+ value=last_value if last_value is not None else value,
82
+ )
83
+ )
84
+ entries.sort(key=lambda entry: entry.path)
85
+ return BlameReport(claim_id=claim_id, head=head, entries=entries)
realitydiff/claim.py ADDED
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from realitydiff.blame import blame_history
4
+ from realitydiff.diffing import diff_states
5
+ from realitydiff.models import BlameReport, ClaimDiff, ClaimRecord, ClaimState, Commit
6
+ from realitydiff.store import BeliefStore
7
+
8
+
9
+ class Claim:
10
+ """Living claim with git-like operations: commit, diff, blame, revert."""
11
+
12
+ def __init__(self, store: BeliefStore, record: ClaimRecord) -> None:
13
+ self._store = store
14
+ self.id = record.id
15
+ self.record = record
16
+
17
+ def refresh(self) -> ClaimRecord:
18
+ record = self._store.get_claim(self.id)
19
+ if record is None:
20
+ raise KeyError(self.id)
21
+ self.record = record
22
+ return record
23
+
24
+ @property
25
+ def head(self) -> Commit | None:
26
+ return self._store.head_commit(self.id)
27
+
28
+ @property
29
+ def working(self) -> ClaimState | None:
30
+ return self._store.working_state(self.id)
31
+
32
+ def checkout(self) -> ClaimState:
33
+ state = self.working or (self.head.state if self.head else None)
34
+ if state is None:
35
+ raise RuntimeError("Claim has no state yet")
36
+ return state
37
+
38
+ def commit(self, message: str, *, author: str = "user", reason: str = "") -> Commit:
39
+ working = self._store.working_state(self.id)
40
+ head = self._store.head_commit(self.id)
41
+ if working is None:
42
+ if head is None:
43
+ raise RuntimeError("Nothing to commit")
44
+ working = head.state
45
+ if head and working.tree_hash() == head.state.tree_hash():
46
+ raise RuntimeError("Working tree clean")
47
+ commit = Commit.create(
48
+ claim_id=self.id,
49
+ parent_id=head.id if head else None,
50
+ state=working,
51
+ author=author,
52
+ message=message,
53
+ reason=reason,
54
+ previous_confidence=head.state.confidence if head else None,
55
+ )
56
+ return self._store.append_commit(commit)
57
+
58
+ def diff(self, a: str | None = None, b: str | None = None) -> ClaimDiff:
59
+ if a is None and b is None:
60
+ head = self.head
61
+ working = self.working
62
+ if head is None:
63
+ return diff_states(None, working, to_id="WORKING")
64
+ return diff_states(head.state, working, from_id=head.id, to_id="WORKING")
65
+ left = self._resolve(a) if a else None
66
+ right = self._resolve(b) if b else self.head
67
+ return diff_states(
68
+ left.state if left else None,
69
+ right.state if right else None,
70
+ from_id=left.id if left else None,
71
+ to_id=right.id if right else None,
72
+ reason=(right.reason if right else ""),
73
+ )
74
+
75
+ def log(self) -> list[Commit]:
76
+ return self._store.log(self.id)
77
+
78
+ def blame(self) -> BlameReport:
79
+ return blame_history(self.id, self.log())
80
+
81
+ def revert(self, sha: str, *, author: str = "user") -> Commit:
82
+ target = self._store.get_commit(sha, claim_id=self.id)
83
+ if target is None:
84
+ raise KeyError(f"Unknown commit {sha}")
85
+ head = self.head
86
+ if head is None:
87
+ raise RuntimeError("Nothing to revert")
88
+ if target.state.tree_hash() == head.state.tree_hash():
89
+ raise RuntimeError("HEAD already matches that commit")
90
+ commit = Commit.create(
91
+ claim_id=self.id,
92
+ parent_id=head.id,
93
+ state=target.state,
94
+ author=author,
95
+ message=f"Revert to {sha[:12]}",
96
+ reason=f"Restored belief snapshot {sha[:12]}. Previous confidence {head.state.confidence}% → {target.state.confidence}%.",
97
+ previous_confidence=head.state.confidence,
98
+ )
99
+ return self._store.append_commit(commit)
100
+
101
+ def _resolve(self, ref: str) -> Commit | None:
102
+ if ref.upper() in {"HEAD", "WORKING"}:
103
+ return self.head
104
+ return self._store.get_commit(ref, claim_id=self.id)
realitydiff/cli.py ADDED
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from realitydiff import __version__
9
+ from realitydiff.config import config_file, default_db, load_env
10
+ from realitydiff.service import RealityDiff
11
+ from realitydiff.xai import XAIError
12
+
13
+ load_env()
14
+ DEFAULT_DB = default_db()
15
+
16
+
17
+ def _db_path() -> str:
18
+ return str(Path(DEFAULT_DB).expanduser().resolve())
19
+
20
+
21
+ def _engine() -> RealityDiff:
22
+ return RealityDiff.open(DEFAULT_DB)
23
+
24
+
25
+ def _print(data) -> None:
26
+ if hasattr(data, "model_dump"):
27
+ print(json.dumps(data.model_dump(mode="json"), indent=2, default=str))
28
+ else:
29
+ print(json.dumps(data, indent=2, default=str))
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ parser = argparse.ArgumentParser(prog="realitydiff", description="Git for beliefs")
34
+ parser.add_argument("--version", action="version", version=f"realitydiff {__version__}")
35
+ sub = parser.add_subparsers(dest="cmd", required=True)
36
+
37
+ open_p = sub.add_parser("open", help="Create a living claim and survey the internet")
38
+ open_p.add_argument("statement")
39
+
40
+ watch_p = sub.add_parser("watch", help="Search for new evidence and maybe commit")
41
+ watch_p.add_argument("claim_id")
42
+
43
+ sub.add_parser("watch-all", help="Watch every due claim once (used by the installed service)")
44
+
45
+ log_p = sub.add_parser("log", help="Commit history")
46
+ log_p.add_argument("claim_id")
47
+
48
+ show_p = sub.add_parser("show", help="HEAD state")
49
+ show_p.add_argument("claim_id")
50
+
51
+ diff_p = sub.add_parser("diff", help="Diff two commits (default HEAD vs WORKING)")
52
+ diff_p.add_argument("claim_id")
53
+ diff_p.add_argument("a", nargs="?")
54
+ diff_p.add_argument("b", nargs="?")
55
+
56
+ blame_p = sub.add_parser("blame", help="Who introduced each piece of the belief")
57
+ blame_p.add_argument("claim_id")
58
+
59
+ revert_p = sub.add_parser("revert", help="Restore a prior belief snapshot as a new commit")
60
+ revert_p.add_argument("claim_id")
61
+ revert_p.add_argument("sha")
62
+
63
+ commit_p = sub.add_parser("commit", help="Commit the working tree")
64
+ commit_p.add_argument("claim_id")
65
+ commit_p.add_argument("-m", "--message", required=True)
66
+
67
+ ls_p = sub.add_parser("ls", help="List claims")
68
+
69
+ serve_p = sub.add_parser("serve", help="Run the web UI")
70
+ serve_p.add_argument("--host", default="0.0.0.0")
71
+ serve_p.add_argument("--port", type=int, default=8000)
72
+
73
+ svc = sub.add_parser("service", help="Install a background timer that runs claim.watch()")
74
+ svc_sub = svc.add_subparsers(dest="service_cmd", required=True)
75
+ inst = svc_sub.add_parser("install", help="Install a LaunchAgent / systemd user timer / Windows task")
76
+ inst.add_argument("--interval", type=int, default=300, help="Seconds between watches (min 30)")
77
+ svc_sub.add_parser("uninstall", help="Remove the background watch service")
78
+ svc_sub.add_parser("status", help="Show whether the service is installed")
79
+
80
+ args = parser.parse_args(argv)
81
+ if args.cmd == "serve":
82
+ import uvicorn
83
+
84
+ print(f"realitydiff: database {_db_path()}", file=sys.stderr)
85
+ uvicorn.run("realitydiff.api:create_app", host=args.host, port=args.port, factory=True, reload=False)
86
+ return 0
87
+
88
+ if args.cmd == "service":
89
+ from realitydiff.schedule import install, status, uninstall
90
+
91
+ if args.service_cmd == "install":
92
+ _print(install(args.interval, db=DEFAULT_DB))
93
+ return 0
94
+ if args.service_cmd == "uninstall":
95
+ _print(uninstall())
96
+ return 0
97
+ _print({**status(), "db": _db_path()})
98
+ return 0
99
+
100
+ try:
101
+ engine = _engine()
102
+ except XAIError as exc:
103
+ print(f"realitydiff: {exc}. Export it or save it in {config_file()}", file=sys.stderr)
104
+ return 2
105
+ if args.cmd == "watch-all":
106
+ _print([item.model_dump(mode="json") for item in engine.watch_due()])
107
+ return 0
108
+ if args.cmd == "open":
109
+ living, commit = engine.open_claim(args.statement)
110
+ record = living.refresh()
111
+ _print(
112
+ {
113
+ "claim_id": living.id,
114
+ "commit": commit.id,
115
+ "confidence": commit.new_confidence,
116
+ "reason": commit.reason,
117
+ "closed": record.closed,
118
+ "outcome_verdict": record.outcome_verdict,
119
+ }
120
+ )
121
+ return 0
122
+ if args.cmd == "ls":
123
+ _print([record.model_dump(mode="json") for record in engine.store.list_claims()])
124
+ return 0
125
+ if args.cmd == "watch":
126
+ _print(engine.watch(args.claim_id))
127
+ return 0
128
+ living = engine.claim(args.claim_id)
129
+ if args.cmd == "log":
130
+ _print(
131
+ [
132
+ {
133
+ "id": c.id,
134
+ "parent": c.parent_id,
135
+ "confidence": c.new_confidence,
136
+ "message": c.message,
137
+ "reason": c.reason,
138
+ "author": c.author,
139
+ "at": c.created_at.isoformat(),
140
+ }
141
+ for c in living.log()
142
+ ]
143
+ )
144
+ return 0
145
+ if args.cmd == "show":
146
+ head = living.head
147
+ if head is None:
148
+ print("no HEAD", file=sys.stderr)
149
+ return 1
150
+ _print(head)
151
+ return 0
152
+ if args.cmd == "diff":
153
+ _print(living.diff(args.a, args.b))
154
+ return 0
155
+ if args.cmd == "blame":
156
+ _print(living.blame())
157
+ return 0
158
+ if args.cmd == "revert":
159
+ _print(living.revert(args.sha))
160
+ return 0
161
+ if args.cmd == "commit":
162
+ _print(living.commit(args.message))
163
+ return 0
164
+ return 1
165
+
166
+
167
+ if __name__ == "__main__":
168
+ raise SystemExit(main())