sooth 0.2.1__tar.gz → 0.2.2__tar.gz

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.
Files changed (34) hide show
  1. {sooth-0.2.1 → sooth-0.2.2}/PKG-INFO +61 -12
  2. sooth-0.2.2/README.md +136 -0
  3. {sooth-0.2.1 → sooth-0.2.2}/pyproject.toml +1 -1
  4. {sooth-0.2.1 → sooth-0.2.2}/src/sooth/cli.py +28 -3
  5. sooth-0.2.2/src/sooth/demo.json +1 -0
  6. {sooth-0.2.1 → sooth-0.2.2}/src/sooth/report.py +24 -0
  7. {sooth-0.2.1 → sooth-0.2.2}/tests/test_core.py +20 -1
  8. sooth-0.2.1/README.md +0 -87
  9. {sooth-0.2.1 → sooth-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {sooth-0.2.1 → sooth-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {sooth-0.2.1 → sooth-0.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  12. {sooth-0.2.1 → sooth-0.2.2}/.github/workflows/ci.yml +0 -0
  13. {sooth-0.2.1 → sooth-0.2.2}/.gitignore +0 -0
  14. {sooth-0.2.1 → sooth-0.2.2}/LICENSE +0 -0
  15. {sooth-0.2.1 → sooth-0.2.2}/docs/DESIGN.md +0 -0
  16. {sooth-0.2.1 → sooth-0.2.2}/docs/DOGFOOD.md +0 -0
  17. {sooth-0.2.1 → sooth-0.2.2}/docs/PRD.md +0 -0
  18. {sooth-0.2.1 → sooth-0.2.2}/docs/TESTING.md +0 -0
  19. {sooth-0.2.1 → sooth-0.2.2}/examples/calibration.json +0 -0
  20. {sooth-0.2.1 → sooth-0.2.2}/examples/calibration.md +0 -0
  21. {sooth-0.2.1 → sooth-0.2.2}/examples/draft.md +0 -0
  22. {sooth-0.2.1 → sooth-0.2.2}/examples/news-1-evil.md +0 -0
  23. {sooth-0.2.1 → sooth-0.2.2}/examples/news-1-output.md +0 -0
  24. {sooth-0.2.1 → sooth-0.2.2}/examples/news-1-summ.md +0 -0
  25. {sooth-0.2.1 → sooth-0.2.2}/examples/news-1.md +0 -0
  26. {sooth-0.2.1 → sooth-0.2.2}/examples/policy.md +0 -0
  27. {sooth-0.2.1 → sooth-0.2.2}/examples/pricing.md +0 -0
  28. {sooth-0.2.1 → sooth-0.2.2}/examples/source.md +0 -0
  29. {sooth-0.2.1 → sooth-0.2.2}/src/sooth/__init__.py +0 -0
  30. {sooth-0.2.1 → sooth-0.2.2}/src/sooth/claims.py +0 -0
  31. {sooth-0.2.1 → sooth-0.2.2}/src/sooth/verify.py +0 -0
  32. {sooth-0.2.1 → sooth-0.2.2}/tests/calibrate.py +0 -0
  33. {sooth-0.2.1 → sooth-0.2.2}/tests/conftest.py +0 -0
  34. {sooth-0.2.1 → sooth-0.2.2}/tests/smoke.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sooth
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Verify AI-generated text against source material. Claim-by-claim trust reports powered by Jev (TypeSafe System One).
5
5
  Project-URL: Homepage, https://github.com/naufalhilmiaji/sooth
6
6
  Project-URL: Documentation, https://github.com/naufalhilmiaji/sooth/tree/main/docs
@@ -23,15 +23,23 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  # Sooth
25
25
 
26
- Verify AI-generated text against source material. Claim-by-claim **PASS / FAIL / REVIEW**, with calibrated probabilities you can act on in CI.
26
+ **LLMs generate. Sooth verifies.**
27
27
 
28
- Built on [Jev](https://docs.typesafe.ai) (TypeSafe System One) — typed judgments and probabilities instead of generated prose. Every verdict shows its probability distribution, and the combination rules are plain code you can read. No black box.
28
+ Claim-by-claim fact-checking for AI output, designed for CI. Sooth checks every sentence in a draft against your source material and returns `PASS` / `FAIL` / `REVIEW` with calibrated probabilities and the exact source line behind each verdict.
29
29
 
30
- ```
31
- sooth --source policy.md --text draft-reply.md
30
+ [![PyPI](https://img.shields.io/pypi/v/sooth.svg)](https://pypi.org/project/sooth/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/sooth.svg)](https://pypi.org/project/sooth/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/naufalhilmiaji/sooth/blob/main/LICENSE)
33
+ [![CI](https://github.com/naufalhilmiaji/sooth/actions/workflows/ci.yml/badge.svg)](https://github.com/naufalhilmiaji/sooth/actions/workflows/ci.yml)
34
+
35
+ ## See it catch a hallucination
36
+
37
+ ```bash
38
+ pip install sooth
39
+ sooth demo # no API key needed
32
40
  ```
33
41
 
34
- Real output (planted errors in the draft vs a news source):
42
+ Real output — seven claims in an AI-written news summary, three numbers quietly wrong:
35
43
 
36
44
  ```
37
45
  # Sooth
@@ -46,14 +54,18 @@ Real output (planted errors in the draft vs a news source):
46
54
  | 7 | Para investor ritel sangat senang dengan aturan baru ini. | ➖ UNCHECKABLE | ██░░░░░░ 0.24 | P(checkable)=0.24 | |
47
55
  ```
48
56
 
57
+ Every verdict carries its probability distribution and the source span it was judged against. When Sooth is unsure, it says `REVIEW` instead of guessing.
58
+
49
59
  ## Why
50
60
 
51
- AI writes fast, nobody checks. Claims ship wrong. Sooth checks each claim against the evidence you give it — and when it is unsure, it says `REVIEW` instead of guessing.
61
+ AI writes fast, nobody checks. Claims ship wrong — usually a number, a hedge, or a name that drifted. Asking another LLM "is this right?" just produces more prose.
62
+
63
+ Sooth takes a different bet: **the model makes typed judgments, code makes the decision.** Every rule that turns a probability into a verdict is a readable line of Python, not a prompt.
52
64
 
53
65
  ## Install
54
66
 
55
67
  ```bash
56
- pip install sooth # or: pip install -e ".[dev]" from source
68
+ pip install sooth
57
69
  export TYPESAFE_API_KEY=... # get one at console.typesafe.ai
58
70
  ```
59
71
 
@@ -73,14 +85,49 @@ sooth --source policy.md --text draft.md --format plain
73
85
  # --log FILE append full judgment trace (one JSONL line per run)
74
86
  ```
75
87
 
88
+ Wire it into CI as a quality gate for generated content:
89
+
90
+ ```yaml
91
+ - name: Verify AI output
92
+ run: sooth --source docs/policy.md --text generated-reply.md
93
+ env:
94
+ TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
95
+ ```
96
+
76
97
  ## How it works
77
98
 
78
99
  1. Draft is split into claims (one sentence each).
79
- 2. Each claim gets three questions to Jev, all fanned out in parallel batches: *is this checkable?*, *does the source support it?* (`supports` / `contradicts` / `not_found`), and *do all details match exactly?*
100
+ 2. Each claim gets four questions to Jev, fanned out in parallel batches: *is this checkable?*, *does the source support it?* (`supports` / `contradicts` / `not_found`), *do all details match exactly?*, and *which source span is the evidence?*
80
101
  3. Verdicts are mapped in code: uncheckable → `UNCHECKABLE`; low confidence → `REVIEW`; then `PASS` / `FAIL`. Safeguards demote `PASS` to `REVIEW` when details drift or claim numbers are absent from the source (checked in plain code).
81
102
  4. The report shows the full probability distribution per claim — not just a label.
82
103
 
83
- The decision logic lives in [`src/sooth/verify.py`](src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
104
+ The decision logic lives in [`src/sooth/verify.py`](https://github.com/naufalhilmiaji/sooth/blob/main/src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
105
+
106
+ Judgments come from [Jev](https://docs.typesafe.ai) (TypeSafe System One), which returns typed distributions instead of generated prose.
107
+
108
+ ## Why not just use an LLM judge?
109
+
110
+ Not claiming Sooth is universally more accurate. Claiming it gives you **architectural guarantees** a prompt cannot:
111
+
112
+ | | Sooth | Generic LLM judge |
113
+ |---|-------|-------------------|
114
+ | Claim-level verification | ✅ | Sometimes |
115
+ | Source span behind each verdict | ✅ | Sometimes |
116
+ | `PASS` / `FAIL` / `REVIEW` | ✅ fixed vocabulary | Generated labels |
117
+ | Probability distribution | ✅ | Usually none |
118
+ | Verdict rules in readable code | ✅ | Prompt-dependent |
119
+ | CI exit codes | ✅ | ❌ |
120
+ | Audit log (`--log` JSONL) | ✅ | DIY |
121
+
122
+ ## Who it's for
123
+
124
+ Developers building systems that generate text from trusted material:
125
+
126
+ - **RAG pipelines** — retrieved the right document, still wrote the wrong number
127
+ - **AI agents** — before an agent's reply reaches a human
128
+ - **Support / ops copilots** — policy-backed answers, checked against the policy
129
+ - **Report & summary generation** — figures that must match the source
130
+ - **Compliance workflows** — evidence trail for every claim, not vibes
84
131
 
85
132
  ## Known limits (alpha)
86
133
 
@@ -98,11 +145,13 @@ bash tests/smoke.sh # live smoke (needs TYPESAFE_API_KEY)
98
145
  PYTHONPATH=src python3 tests/calibrate.py # live calibration, 30 labeled claims
99
146
  ```
100
147
 
101
- Docs: [PRD](docs/PRD.md) · [Design](docs/DESIGN.md) · [Testing](docs/TESTING.md)
148
+ Docs: [PRD](https://github.com/naufalhilmiaji/sooth/blob/main/docs/PRD.md) · [Design](https://github.com/naufalhilmiaji/sooth/blob/main/docs/DESIGN.md) · [Testing](https://github.com/naufalhilmiaji/sooth/blob/main/docs/TESTING.md)
102
149
 
103
150
  ## Roadmap
104
151
 
105
- - v0.2 shipped: source-span evidence (the exact source line behind each verdict, shown in the report)
152
+ - v0.2.2 shipped: `sooth demo` (offline, no API key)
153
+ - v0.2.1 shipped: source-span evidence, published on PyPI
154
+ - GitHub Action
106
155
  - later: hosted web app — paste UI, history, team review queues
107
156
 
108
157
  ## License
sooth-0.2.2/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # Sooth
2
+
3
+ **LLMs generate. Sooth verifies.**
4
+
5
+ Claim-by-claim fact-checking for AI output, designed for CI. Sooth checks every sentence in a draft against your source material and returns `PASS` / `FAIL` / `REVIEW` with calibrated probabilities and the exact source line behind each verdict.
6
+
7
+ [![PyPI](https://img.shields.io/pypi/v/sooth.svg)](https://pypi.org/project/sooth/)
8
+ [![Python](https://img.shields.io/pypi/pyversions/sooth.svg)](https://pypi.org/project/sooth/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/naufalhilmiaji/sooth/blob/main/LICENSE)
10
+ [![CI](https://github.com/naufalhilmiaji/sooth/actions/workflows/ci.yml/badge.svg)](https://github.com/naufalhilmiaji/sooth/actions/workflows/ci.yml)
11
+
12
+ ## See it catch a hallucination
13
+
14
+ ```bash
15
+ pip install sooth
16
+ sooth demo # no API key needed
17
+ ```
18
+
19
+ Real output — seven claims in an AI-written news summary, three numbers quietly wrong:
20
+
21
+ ```
22
+ # Sooth
23
+
24
+ **PASS 2 · FAIL 3 · REVIEW 1 · UNCHECKABLE 1** — threshold 0.70
25
+
26
+ | # | Claim | Verdict | P | Why (P distribution) | Evidence |
27
+ |---|-------|---------|---|----------------------|----------|
28
+ | 3 | BNBR baru menuntaskan rights issue bernilai besar di harga Rp 33. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 33 | `examples/news-1.md:25` "Saham ini juga baru menyelesaikan rights issue dalam juml…" |
29
+ | 5 | BUMI hanya perlu turun sekitar 10% untuk menyentuh level Rp 50. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 10 | `examples/news-1.md:23` "Adapun PT Bumi Resources Tbk (BUMI) di sekitar Rp192 haru…" |
30
+ | 6 | BEI juga menetapkan batas atas harga saham Rp 5.000 per saham. | ⚠️ REVIEW | ████████ 1.00 | contradicts 0.00 / not_found 1.00 / supports 0.00 · details 0.02 · missing #s: 5.000 | |
31
+ | 7 | Para investor ritel sangat senang dengan aturan baru ini. | ➖ UNCHECKABLE | ██░░░░░░ 0.24 | P(checkable)=0.24 | |
32
+ ```
33
+
34
+ Every verdict carries its probability distribution and the source span it was judged against. When Sooth is unsure, it says `REVIEW` instead of guessing.
35
+
36
+ ## Why
37
+
38
+ AI writes fast, nobody checks. Claims ship wrong — usually a number, a hedge, or a name that drifted. Asking another LLM "is this right?" just produces more prose.
39
+
40
+ Sooth takes a different bet: **the model makes typed judgments, code makes the decision.** Every rule that turns a probability into a verdict is a readable line of Python, not a prompt.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install sooth
46
+ export TYPESAFE_API_KEY=... # get one at console.typesafe.ai
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ```bash
52
+ sooth --source docs/policy.md --source tickets/t123.md --text draft-reply.md
53
+
54
+ # CI-friendly exit codes
55
+ sooth --source policy.md --text draft.md --format plain
56
+ # 0 = clean · 1 = any FAIL · 2 = any REVIEW · 3 = usage/config error
57
+
58
+ # Options
59
+ # --confidence T REVIEW below this confidence (default 0.7)
60
+ # --format md|plain
61
+ # -o FILE write report to file
62
+ # --log FILE append full judgment trace (one JSONL line per run)
63
+ ```
64
+
65
+ Wire it into CI as a quality gate for generated content:
66
+
67
+ ```yaml
68
+ - name: Verify AI output
69
+ run: sooth --source docs/policy.md --text generated-reply.md
70
+ env:
71
+ TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
72
+ ```
73
+
74
+ ## How it works
75
+
76
+ 1. Draft is split into claims (one sentence each).
77
+ 2. Each claim gets four questions to Jev, fanned out in parallel batches: *is this checkable?*, *does the source support it?* (`supports` / `contradicts` / `not_found`), *do all details match exactly?*, and *which source span is the evidence?*
78
+ 3. Verdicts are mapped in code: uncheckable → `UNCHECKABLE`; low confidence → `REVIEW`; then `PASS` / `FAIL`. Safeguards demote `PASS` to `REVIEW` when details drift or claim numbers are absent from the source (checked in plain code).
79
+ 4. The report shows the full probability distribution per claim — not just a label.
80
+
81
+ The decision logic lives in [`src/sooth/verify.py`](https://github.com/naufalhilmiaji/sooth/blob/main/src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
82
+
83
+ Judgments come from [Jev](https://docs.typesafe.ai) (TypeSafe System One), which returns typed distributions instead of generated prose.
84
+
85
+ ## Why not just use an LLM judge?
86
+
87
+ Not claiming Sooth is universally more accurate. Claiming it gives you **architectural guarantees** a prompt cannot:
88
+
89
+ | | Sooth | Generic LLM judge |
90
+ |---|-------|-------------------|
91
+ | Claim-level verification | ✅ | Sometimes |
92
+ | Source span behind each verdict | ✅ | Sometimes |
93
+ | `PASS` / `FAIL` / `REVIEW` | ✅ fixed vocabulary | Generated labels |
94
+ | Probability distribution | ✅ | Usually none |
95
+ | Verdict rules in readable code | ✅ | Prompt-dependent |
96
+ | CI exit codes | ✅ | ❌ |
97
+ | Audit log (`--log` JSONL) | ✅ | DIY |
98
+
99
+ ## Who it's for
100
+
101
+ Developers building systems that generate text from trusted material:
102
+
103
+ - **RAG pipelines** — retrieved the right document, still wrote the wrong number
104
+ - **AI agents** — before an agent's reply reaches a human
105
+ - **Support / ops copilots** — policy-backed answers, checked against the policy
106
+ - **Report & summary generation** — figures that must match the source
107
+ - **Compliance workflows** — evidence trail for every claim, not vibes
108
+
109
+ ## Known limits (alpha)
110
+
111
+ - Safeguards catch most drift and smuggling: claim numbers absent from the source demote `PASS` to `REVIEW`, and a detail-match gate flags altered hedges ("about 74%" → "over 74%"). Not perfect — read the `Why` column before trusting a verdict.
112
+ - Derived numbers (totals, values computed outside the source) look "missing" and land in `REVIEW`.
113
+ - Source + questions must fit ~64k tokens — split long documents yourself.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ pip install -e ".[dev]"
119
+ python3 tests/test_core.py # pure checks, no network
120
+ pytest # same suite
121
+ bash tests/smoke.sh # live smoke (needs TYPESAFE_API_KEY)
122
+ PYTHONPATH=src python3 tests/calibrate.py # live calibration, 30 labeled claims
123
+ ```
124
+
125
+ Docs: [PRD](https://github.com/naufalhilmiaji/sooth/blob/main/docs/PRD.md) · [Design](https://github.com/naufalhilmiaji/sooth/blob/main/docs/DESIGN.md) · [Testing](https://github.com/naufalhilmiaji/sooth/blob/main/docs/TESTING.md)
126
+
127
+ ## Roadmap
128
+
129
+ - v0.2.2 shipped: `sooth demo` (offline, no API key)
130
+ - v0.2.1 shipped: source-span evidence, published on PyPI
131
+ - GitHub Action
132
+ - later: hosted web app — paste UI, history, team review queues
133
+
134
+ ## License
135
+
136
+ MIT © Naufal Hilmiaji. Powered by [TypeSafe](https://typesafe.ai) / Jev.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sooth"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Verify AI-generated text against source material. Claim-by-claim trust reports powered by Jev (TypeSafe System One)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -9,9 +9,11 @@ from datetime import UTC, datetime
9
9
  from pathlib import Path
10
10
 
11
11
  from sooth.claims import split_claims
12
- from sooth.report import exit_code, log_record, render_markdown, render_plain
12
+ from sooth.report import exit_code, log_record, render_markdown, render_plain, verdicts_from_record
13
13
  from sooth.verify import DEFAULT_THRESHOLD, VerifyError, verify_claims
14
14
 
15
+ DEMO_RECORD = Path(__file__).with_name("demo.json")
16
+
15
17
 
16
18
  def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
17
19
  p = argparse.ArgumentParser(
@@ -19,9 +21,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
19
21
  description="Verify AI-generated text against source material. "
20
22
  "Each claim gets PASS / FAIL / REVIEW with calibrated probabilities.",
21
23
  )
22
- p.add_argument("--source", action="append", required=True, metavar="FILE",
24
+ p.add_argument("command", nargs="?", choices=["demo"],
25
+ help="run the offline demo against a bundled recorded run (no API key)")
26
+ p.add_argument("--source", action="append", metavar="FILE",
23
27
  help="ground-truth source file (repeatable)")
24
- p.add_argument("--text", required=True, metavar="FILE", help="AI-generated draft to check")
28
+ p.add_argument("--text", metavar="FILE", help="AI-generated draft to check")
25
29
  p.add_argument("--confidence", type=float, default=DEFAULT_THRESHOLD, metavar="T",
26
30
  help=f"REVIEW below this confidence (default {DEFAULT_THRESHOLD})")
27
31
  p.add_argument("--format", choices=["md", "plain"], default="md", help="report format")
@@ -30,8 +34,29 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
30
34
  return p.parse_args(argv)
31
35
 
32
36
 
37
+ def run_demo(fmt: str) -> int:
38
+ """Replay a bundled judgment trace through the real report code. No network."""
39
+ record = json.loads(DEMO_RECORD.read_text(encoding="utf-8"))
40
+ print(
41
+ "demo: replaying a recorded run "
42
+ f"({record['draft']} vs {', '.join(record['sources'])}) — no API key needed",
43
+ file=sys.stderr,
44
+ )
45
+ verdicts = verdicts_from_record(record)
46
+ report = (render_markdown if fmt == "md" else render_plain)(
47
+ verdicts, threshold=record["threshold"]
48
+ )
49
+ print(report, end="")
50
+ return exit_code(verdicts)
51
+
52
+
33
53
  def main(argv: list[str] | None = None) -> int:
34
54
  args = parse_args(argv)
55
+ if args.command == "demo":
56
+ return run_demo(args.format)
57
+ if not args.source or not args.text:
58
+ print("error: --source and --text are required (or try: sooth demo)", file=sys.stderr)
59
+ return 3
35
60
  try:
36
61
  sources = [(path, Path(path).read_text(encoding="utf-8")) for path in args.source]
37
62
  draft = Path(args.text).read_text(encoding="utf-8")
@@ -0,0 +1 @@
1
+ {"model": "jev-1.13.0", "threshold": 0.7, "sources": ["examples/news-1.md"], "draft": "examples/news-1-evil.md", "usage": {"input_tokens": 11111, "output_tokens": 1107}, "results": [{"id": "c1", "text": "Aturan baru BEI menurunkan batas bawah harga saham menjadi Rp 1 per saham mulai 28 September 2026.", "line": 3, "kind": "PASS", "p_checkable": 0.98, "choice": "supports", "probabilities": {"supports": 1.0, "not_found": 0.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.98, "missing_numbers": [], "evidence": {"id": "s1", "text": "Saham-saham Grup Bakrie kembali menjadi sorotan menjelang pemberlakuan aturan baru Bursa Efek Indonesia (BEI) yang menurunkan batas bawah harga saham menjadi Rp 1 per saham mulai 28 September 2026.", "line": 3, "source": "examples/news-1.md"}}, {"id": "c2", "text": "Menurut analis Bumiputera Sekuritas, aturan ini berlaku untuk seluruh emiten di BEI.", "line": 5, "kind": "PASS", "p_checkable": 0.96, "choice": "supports", "probabilities": {"supports": 1.0, "not_found": 0.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.82, "missing_numbers": [], "evidence": {"id": "s9", "text": "Ia menyebut aturan ini berlaku untuk seluruh saham di BEI, bukan khusus Grup Bakrie.", "line": 7, "source": "examples/news-1.md"}}, {"id": "c3", "text": "BNBR baru menuntaskan rights issue bernilai besar di harga Rp 33.", "line": 7, "kind": "FAIL", "p_checkable": 0.93, "choice": "contradicts", "probabilities": {"supports": 0.0, "not_found": 0.0, "contradicts": 1.0}, "confidence": 1.0, "details_p": 0.01, "missing_numbers": ["33"], "evidence": {"id": "s47", "text": "Saham ini juga baru menyelesaikan rights issue dalam jumlah besar di harga Rp 53.", "line": 25, "source": "examples/news-1.md"}}, {"id": "c4", "text": "Saham VIVA disuspensi di harga Rp 50.", "line": 9, "kind": "FAIL", "p_checkable": 0.92, "choice": "contradicts", "probabilities": {"supports": 0.0, "not_found": 0.0, "contradicts": 1.0}, "confidence": 1.0, "details_p": 0.02, "missing_numbers": [], "evidence": null}, {"id": "c5", "text": "BUMI hanya perlu turun sekitar 10% untuk menyentuh level Rp 50.", "line": 11, "kind": "FAIL", "p_checkable": 0.92, "choice": "contradicts", "probabilities": {"supports": 0.0, "not_found": 0.0, "contradicts": 1.0}, "confidence": 1.0, "details_p": 0.01, "missing_numbers": ["10"], "evidence": {"id": "s40", "text": "Adapun PT Bumi Resources Tbk (BUMI) di sekitar Rp192 harus turun sekitar 74%, PT Darma Henwa Tbk (DEWA) sekitar 86%, PT Bumi Resources Minerals Tbk (BRMS) sekitar 93%, dan PT Energi Mega Persada Tbk (ENRG) lebih dari 96% untuk turun ke level Rp50.", "line": 23, "source": "examples/news-1.md"}}, {"id": "c6", "text": "BEI juga menetapkan batas atas harga saham Rp 5.000 per saham di pasar reguler.", "line": 13, "kind": "REVIEW", "p_checkable": 0.57, "choice": "not_found", "probabilities": {"supports": 0.0, "not_found": 1.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.02, "missing_numbers": ["5.000"], "evidence": null}, {"id": "c7", "text": "Para investor ritel sangat senang dengan aturan baru ini.", "line": 15, "kind": "UNCHECKABLE", "p_checkable": 0.25, "choice": "not_found", "probabilities": {"supports": 0.0, "not_found": 1.0, "contradicts": 0.0}, "confidence": 1.0, "details_p": 0.02, "missing_numbers": [], "evidence": null}], "ts": "2026-09-25T09:59:57+00:00"}
@@ -123,3 +123,27 @@ def log_record(result: VerifyResult, threshold: float, sources: list[str], draft
123
123
  for v in result.verdicts
124
124
  ],
125
125
  }
126
+
127
+
128
+ def verdicts_from_record(record: dict) -> list[Verdict]:
129
+ """Inverse of log_record's `results` — replay a recorded run without the API."""
130
+ out = []
131
+ for r in record["results"]:
132
+ ev = r.get("evidence")
133
+ out.append(Verdict(
134
+ claim_id=r["id"],
135
+ claim_text=r["text"],
136
+ line=r["line"],
137
+ kind=r["kind"],
138
+ p_checkable=r.get("p_checkable"),
139
+ choice=r.get("choice"),
140
+ probabilities=r.get("probabilities") or {},
141
+ confidence=r.get("confidence"),
142
+ details_p=r.get("details_p"),
143
+ missing_numbers=tuple(r.get("missing_numbers") or ()),
144
+ evidence_id=ev.get("id") if ev else None,
145
+ evidence_text=ev.get("text") if ev else None,
146
+ evidence_line=ev.get("line") if ev else None,
147
+ evidence_source=ev.get("source") if ev else None,
148
+ ))
149
+ return out
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) # direct-run without install
13
13
 
14
14
  from sooth.claims import split_claims, split_segments
15
- from sooth.report import bar, exit_code, log_record, render_markdown
15
+ from sooth.report import bar, exit_code, log_record, render_markdown, verdicts_from_record
16
16
  from sooth.verify import (
17
17
  FAIL,
18
18
  PASS,
@@ -229,6 +229,25 @@ def test_log_record_roundtrip_no_secret():
229
229
  assert "api_key" not in json.dumps(parsed).lower()
230
230
 
231
231
 
232
+ def test_verdicts_from_record_roundtrip():
233
+ """log_record → JSON → verdicts_from_record is lossless (demo replay path)."""
234
+ result = VerifyResult(
235
+ verdicts=[
236
+ v(PASS, confidence=0.9, probabilities={"supports": 0.9}, details_p=0.9,
237
+ evidence_id="s1", evidence_text="Source line.", evidence_line=3,
238
+ evidence_source="a.md"),
239
+ v(FAIL, claim_id="c2", confidence=1.0, probabilities={"contradicts": 1.0},
240
+ missing_numbers=("33",)),
241
+ ],
242
+ model="jev-1.13.0",
243
+ usage={"input_tokens": 10, "output_tokens": 1},
244
+ )
245
+ back = verdicts_from_record(json.loads(json.dumps(log_record(result, 0.7, ["a.md"], "d.md"))))
246
+ assert [(x.claim_id, x.kind, x.confidence) for x in back] == [("c1", PASS, 0.9), ("c2", FAIL, 1.0)]
247
+ assert back[0].evidence_text == "Source line." and back[0].evidence_line == 3
248
+ assert back[1].missing_numbers == ("33",) and back[1].evidence_text is None
249
+
250
+
232
251
  if __name__ == "__main__":
233
252
  fns = [f for name, f in sorted(globals().items()) if name.startswith("test_") and callable(f)]
234
253
  for fn in fns:
sooth-0.2.1/README.md DELETED
@@ -1,87 +0,0 @@
1
- # Sooth
2
-
3
- Verify AI-generated text against source material. Claim-by-claim **PASS / FAIL / REVIEW**, with calibrated probabilities you can act on in CI.
4
-
5
- Built on [Jev](https://docs.typesafe.ai) (TypeSafe System One) — typed judgments and probabilities instead of generated prose. Every verdict shows its probability distribution, and the combination rules are plain code you can read. No black box.
6
-
7
- ```
8
- sooth --source policy.md --text draft-reply.md
9
- ```
10
-
11
- Real output (planted errors in the draft vs a news source):
12
-
13
- ```
14
- # Sooth
15
-
16
- **PASS 2 · FAIL 3 · REVIEW 1 · UNCHECKABLE 1** — threshold 0.70
17
-
18
- | # | Claim | Verdict | P | Why (P distribution) | Evidence |
19
- |---|-------|---------|---|----------------------|----------|
20
- | 3 | BNBR baru menuntaskan rights issue bernilai besar di harga Rp 33. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 33 | `examples/news-1.md:25` "Saham ini juga baru menyelesaikan rights issue dalam juml…" |
21
- | 5 | BUMI hanya perlu turun sekitar 10% untuk menyentuh level Rp 50. | ❌ FAIL | ████████ 1.00 | contradicts 1.00 / not_found 0.00 / supports 0.00 · details 0.01 · missing #s: 10 | `examples/news-1.md:23` "Adapun PT Bumi Resources Tbk (BUMI) di sekitar Rp192 haru…" |
22
- | 6 | BEI juga menetapkan batas atas harga saham Rp 5.000 per saham. | ⚠️ REVIEW | ████████ 1.00 | contradicts 0.00 / not_found 1.00 / supports 0.00 · details 0.02 · missing #s: 5.000 | |
23
- | 7 | Para investor ritel sangat senang dengan aturan baru ini. | ➖ UNCHECKABLE | ██░░░░░░ 0.24 | P(checkable)=0.24 | |
24
- ```
25
-
26
- ## Why
27
-
28
- AI writes fast, nobody checks. Claims ship wrong. Sooth checks each claim against the evidence you give it — and when it is unsure, it says `REVIEW` instead of guessing.
29
-
30
- ## Install
31
-
32
- ```bash
33
- pip install sooth # or: pip install -e ".[dev]" from source
34
- export TYPESAFE_API_KEY=... # get one at console.typesafe.ai
35
- ```
36
-
37
- ## Usage
38
-
39
- ```bash
40
- sooth --source docs/policy.md --source tickets/t123.md --text draft-reply.md
41
-
42
- # CI-friendly exit codes
43
- sooth --source policy.md --text draft.md --format plain
44
- # 0 = clean · 1 = any FAIL · 2 = any REVIEW · 3 = usage/config error
45
-
46
- # Options
47
- # --confidence T REVIEW below this confidence (default 0.7)
48
- # --format md|plain
49
- # -o FILE write report to file
50
- # --log FILE append full judgment trace (one JSONL line per run)
51
- ```
52
-
53
- ## How it works
54
-
55
- 1. Draft is split into claims (one sentence each).
56
- 2. Each claim gets three questions to Jev, all fanned out in parallel batches: *is this checkable?*, *does the source support it?* (`supports` / `contradicts` / `not_found`), and *do all details match exactly?*
57
- 3. Verdicts are mapped in code: uncheckable → `UNCHECKABLE`; low confidence → `REVIEW`; then `PASS` / `FAIL`. Safeguards demote `PASS` to `REVIEW` when details drift or claim numbers are absent from the source (checked in plain code).
58
- 4. The report shows the full probability distribution per claim — not just a label.
59
-
60
- The decision logic lives in [`src/sooth/verify.py`](src/sooth/verify.py) in a dozen readable lines. Change thresholds and rules there, not in prompts.
61
-
62
- ## Known limits (alpha)
63
-
64
- - Safeguards catch most drift and smuggling: claim numbers absent from the source demote `PASS` to `REVIEW`, and a detail-match gate flags altered hedges ("about 74%" → "over 74%"). Not perfect — read the `Why` column before trusting a verdict.
65
- - Derived numbers (totals, values computed outside the source) look "missing" and land in `REVIEW`.
66
- - Source + questions must fit ~64k tokens — split long documents yourself.
67
-
68
- ## Development
69
-
70
- ```bash
71
- pip install -e ".[dev]"
72
- python3 tests/test_core.py # pure checks, no network
73
- pytest # same suite
74
- bash tests/smoke.sh # live smoke (needs TYPESAFE_API_KEY)
75
- PYTHONPATH=src python3 tests/calibrate.py # live calibration, 30 labeled claims
76
- ```
77
-
78
- Docs: [PRD](docs/PRD.md) · [Design](docs/DESIGN.md) · [Testing](docs/TESTING.md)
79
-
80
- ## Roadmap
81
-
82
- - v0.2 shipped: source-span evidence (the exact source line behind each verdict, shown in the report)
83
- - later: hosted web app — paste UI, history, team review queues
84
-
85
- ## License
86
-
87
- MIT © Naufal Hilmiaji. Powered by [TypeSafe](https://typesafe.ai) / Jev.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes