jev-trust 0.1.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nautilus Assay (Yiluo Technology)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: jev-trust
3
+ Version: 0.1.0
4
+ Summary: Trust middleware for Jev decision APIs - log every call, measure calibration in YOUR domain, spend confidence at its verified exchange rate.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/chunxiaoxx/nautilus-compass
7
+ Project-URL: Research, https://github.com/chunxiaoxx/nautilus-compass/discussions/59
8
+ Project-URL: Wall, https://compass.nautilus.social/wall.html
9
+ Keywords: jev,calibration,trust,ai,agents,decision-making,verification,ece,brier
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Requires-Python: >=3.8
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # jev-trust
22
+
23
+ **Trust middleware for Jev decision APIs — spend confidence at its verified exchange rate.**
24
+
25
+ Jev gives you calibrated-looking probabilities. But calibration is a property of
26
+ **(model, domain)** pairs, not models. We measured hosted Jev 1.13 at
27
+ ECE 0.041 on closed deterministic tasks — and **accuracy 50% while stating
28
+ 91% confidence** on synthetic email triage ([Assay research #1/#2](https://github.com/chunxiaoxx/nautilus-compass/discussions/59)).
29
+ Same model. Same day. The number you actually care about is the one for *your* domain,
30
+ and nobody has measured it yet — including the vendor.
31
+
32
+ `jev-trust` wraps the Jev API and measures it in *your* domain while you work:
33
+
34
+ - **Every call logged** — append-only JSONL: question, answer, stated confidence, effective confidence, usage, timestamp.
35
+ - **Outcomes fed back** — as ground truth arrives, `record_outcome()` builds your domain's calibration record: accuracy, Brier, ECE, and calibration currency **C = 1 − ECE**.
36
+ - **Effective confidence on every answer** — `r.effective_confidence` = what the stated confidence is *worth* here so far (observed accuracy of the matching confidence bin, ≥5 samples; else C-adjusted; else `None` = insufficient evidence).
37
+ - **Overconfidence alerts** — a callback fires when a decision is confident enough to act on but the domain hasn't earned that confidence yet.
38
+ - **Signed evidence** — one call signs the whole log (ed25519). Publish log + sig + pubkey; anyone — including [Nautilus Assay](https://compass.nautilus.social/wall.html) — can independently recompute your numbers.
39
+
40
+ Zero dependencies. Pure stdlib. Python ≥ 3.8.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install jev-trust
46
+ ```
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ from jev_trust import TrustedJev
52
+
53
+ jev = TrustedJev(api_key=API_KEY, domain="email-triage")
54
+
55
+ # decide() never lets unexamined confidence through:
56
+ r = jev.decide(state, {"q": {"type": "choice", "instructions": "...",
57
+ "criteria": {...}}})["q"]
58
+
59
+ r.decision # 'alpha'
60
+ r.stated_confidence # 0.91 <- what Jev claims
61
+ r.effective_confidence # 0.50 <- what 0.91 is worth in YOUR domain so far
62
+ r.basis # 'bin_observed' | 'C_adjusted' | 'insufficient_n'
63
+ r.domain_verdict # FACE_VALUE | DISCOUNT | DOWNGRADE | UNVERIFIED
64
+
65
+ # feed ground truth back as it arrives:
66
+ jev.record_outcome("q", truth)
67
+
68
+ # your domain's running scorecard:
69
+ jev.stats()
70
+ # {'n_outcomes': 42, 'accuracy': 0.52, 'brier': 0.41, 'ece': 0.39,
71
+ # 'C': 0.61, 'verdict': 'DISCOUNT', ...}
72
+ ```
73
+
74
+ ## The alert that pays for itself
75
+
76
+ ```python
77
+ def on_overconfidence(r):
78
+ # fired when stated >= 0.90 but the domain hasn't earned FACE_VALUE
79
+ log.warning(f"{r.qid}: Jev says {r.stated_confidence}, "
80
+ f"worth {r.effective_confidence} here — route to human?")
81
+
82
+ jev = TrustedJev(api_key=API_KEY, domain="fraud-flag",
83
+ alert_confidence=0.90, on_overconfidence=on_overconfidence)
84
+ ```
85
+
86
+ ## Verdicts (Assay calibration-currency reading levels)
87
+
88
+ | Verdict | Condition | Reading |
89
+ |---|---|---|
90
+ | `UNVERIFIED` | < 20 outcomes in domain | stated confidence is an unbacked claim |
91
+ | `FACE_VALUE` | C ≥ 0.80 | use stated confidence as-is |
92
+ | `DISCOUNT` | 0.50 ≤ C < 0.80 | multiply trust by C |
93
+ | `DOWNGRADE` | C < 0.50 | route to human review |
94
+
95
+ The library ships with Assay's published reference rates (informational — your
96
+ verdict is always computed from *your* outcomes):
97
+
98
+ ```python
99
+ jev_trust.ASSAY_REFERENCE_RATES
100
+ # {'closed-deterministic': {'C': 0.959, ...}, # n=240, ECE 0.041
101
+ # 'adversarial-stress': {'C': 0.988, ...}, # n=200, ECE 0.012
102
+ # 'synthetic-email-choice': {'C': 0.086, ...}} # acc 50% @ conf 91%
103
+ ```
104
+
105
+ ## Signed evidence — make your numbers independently checkable
106
+
107
+ ```python
108
+ sig_path = jev.sign_log() # writes <log>.jsonl.sig
109
+ jev.keys.pub_hex # publish this next to the log
110
+
111
+ # anyone can verify:
112
+ from jev_trust import verify_log
113
+ verify_log("session.jsonl", "session.jsonl.sig", pub_hex) # -> VALID
114
+ ```
115
+
116
+ Same canonical-JSON + ed25519 scheme as
117
+ [assay-verify](https://pypi.org/project/assay-verify/) — a signed `jev-trust`
118
+ log is directly submittable to Nautilus Assay for independent recomputation
119
+ (the evidence format behind their [Domain Calibration Reports](https://compass.nautilus.social/dcr.html)).
120
+
121
+ ## Why this exists
122
+
123
+ Nautilus Assay independently verifies AI-agent performance claims. Their two
124
+ public Jev studies found: overall accuracy 92.2% / Brier 0.048 on closed tasks
125
+ (good), but domain calibration collapses on distribution shift — 50% accuracy
126
+ at 91% stated confidence in one synthetic triage domain. Vendor benchmarks
127
+ can't see your domain. `jev-trust` is the always-on instrument that can.
128
+
129
+ Methodology: ECE = equal-width 10-bin top-label; Brier = mean (1 − p_true)²
130
+ (binary-identical to the standard (p − y)² form). Identical formulas to Assay's
131
+ published verification code.
132
+
133
+ ## License
134
+
135
+ MIT. © 2026 Nautilus Assay.
@@ -0,0 +1,115 @@
1
+ # jev-trust
2
+
3
+ **Trust middleware for Jev decision APIs — spend confidence at its verified exchange rate.**
4
+
5
+ Jev gives you calibrated-looking probabilities. But calibration is a property of
6
+ **(model, domain)** pairs, not models. We measured hosted Jev 1.13 at
7
+ ECE 0.041 on closed deterministic tasks — and **accuracy 50% while stating
8
+ 91% confidence** on synthetic email triage ([Assay research #1/#2](https://github.com/chunxiaoxx/nautilus-compass/discussions/59)).
9
+ Same model. Same day. The number you actually care about is the one for *your* domain,
10
+ and nobody has measured it yet — including the vendor.
11
+
12
+ `jev-trust` wraps the Jev API and measures it in *your* domain while you work:
13
+
14
+ - **Every call logged** — append-only JSONL: question, answer, stated confidence, effective confidence, usage, timestamp.
15
+ - **Outcomes fed back** — as ground truth arrives, `record_outcome()` builds your domain's calibration record: accuracy, Brier, ECE, and calibration currency **C = 1 − ECE**.
16
+ - **Effective confidence on every answer** — `r.effective_confidence` = what the stated confidence is *worth* here so far (observed accuracy of the matching confidence bin, ≥5 samples; else C-adjusted; else `None` = insufficient evidence).
17
+ - **Overconfidence alerts** — a callback fires when a decision is confident enough to act on but the domain hasn't earned that confidence yet.
18
+ - **Signed evidence** — one call signs the whole log (ed25519). Publish log + sig + pubkey; anyone — including [Nautilus Assay](https://compass.nautilus.social/wall.html) — can independently recompute your numbers.
19
+
20
+ Zero dependencies. Pure stdlib. Python ≥ 3.8.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install jev-trust
26
+ ```
27
+
28
+ ## Quickstart
29
+
30
+ ```python
31
+ from jev_trust import TrustedJev
32
+
33
+ jev = TrustedJev(api_key=API_KEY, domain="email-triage")
34
+
35
+ # decide() never lets unexamined confidence through:
36
+ r = jev.decide(state, {"q": {"type": "choice", "instructions": "...",
37
+ "criteria": {...}}})["q"]
38
+
39
+ r.decision # 'alpha'
40
+ r.stated_confidence # 0.91 <- what Jev claims
41
+ r.effective_confidence # 0.50 <- what 0.91 is worth in YOUR domain so far
42
+ r.basis # 'bin_observed' | 'C_adjusted' | 'insufficient_n'
43
+ r.domain_verdict # FACE_VALUE | DISCOUNT | DOWNGRADE | UNVERIFIED
44
+
45
+ # feed ground truth back as it arrives:
46
+ jev.record_outcome("q", truth)
47
+
48
+ # your domain's running scorecard:
49
+ jev.stats()
50
+ # {'n_outcomes': 42, 'accuracy': 0.52, 'brier': 0.41, 'ece': 0.39,
51
+ # 'C': 0.61, 'verdict': 'DISCOUNT', ...}
52
+ ```
53
+
54
+ ## The alert that pays for itself
55
+
56
+ ```python
57
+ def on_overconfidence(r):
58
+ # fired when stated >= 0.90 but the domain hasn't earned FACE_VALUE
59
+ log.warning(f"{r.qid}: Jev says {r.stated_confidence}, "
60
+ f"worth {r.effective_confidence} here — route to human?")
61
+
62
+ jev = TrustedJev(api_key=API_KEY, domain="fraud-flag",
63
+ alert_confidence=0.90, on_overconfidence=on_overconfidence)
64
+ ```
65
+
66
+ ## Verdicts (Assay calibration-currency reading levels)
67
+
68
+ | Verdict | Condition | Reading |
69
+ |---|---|---|
70
+ | `UNVERIFIED` | < 20 outcomes in domain | stated confidence is an unbacked claim |
71
+ | `FACE_VALUE` | C ≥ 0.80 | use stated confidence as-is |
72
+ | `DISCOUNT` | 0.50 ≤ C < 0.80 | multiply trust by C |
73
+ | `DOWNGRADE` | C < 0.50 | route to human review |
74
+
75
+ The library ships with Assay's published reference rates (informational — your
76
+ verdict is always computed from *your* outcomes):
77
+
78
+ ```python
79
+ jev_trust.ASSAY_REFERENCE_RATES
80
+ # {'closed-deterministic': {'C': 0.959, ...}, # n=240, ECE 0.041
81
+ # 'adversarial-stress': {'C': 0.988, ...}, # n=200, ECE 0.012
82
+ # 'synthetic-email-choice': {'C': 0.086, ...}} # acc 50% @ conf 91%
83
+ ```
84
+
85
+ ## Signed evidence — make your numbers independently checkable
86
+
87
+ ```python
88
+ sig_path = jev.sign_log() # writes <log>.jsonl.sig
89
+ jev.keys.pub_hex # publish this next to the log
90
+
91
+ # anyone can verify:
92
+ from jev_trust import verify_log
93
+ verify_log("session.jsonl", "session.jsonl.sig", pub_hex) # -> VALID
94
+ ```
95
+
96
+ Same canonical-JSON + ed25519 scheme as
97
+ [assay-verify](https://pypi.org/project/assay-verify/) — a signed `jev-trust`
98
+ log is directly submittable to Nautilus Assay for independent recomputation
99
+ (the evidence format behind their [Domain Calibration Reports](https://compass.nautilus.social/dcr.html)).
100
+
101
+ ## Why this exists
102
+
103
+ Nautilus Assay independently verifies AI-agent performance claims. Their two
104
+ public Jev studies found: overall accuracy 92.2% / Brier 0.048 on closed tasks
105
+ (good), but domain calibration collapses on distribution shift — 50% accuracy
106
+ at 91% stated confidence in one synthetic triage domain. Vendor benchmarks
107
+ can't see your domain. `jev-trust` is the always-on instrument that can.
108
+
109
+ Methodology: ECE = equal-width 10-bin top-label; Brier = mean (1 − p_true)²
110
+ (binary-identical to the standard (p − y)² form). Identical formulas to Assay's
111
+ published verification code.
112
+
113
+ ## License
114
+
115
+ MIT. © 2026 Nautilus Assay.
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ """jev-trust · trust middleware for Jev decision APIs.
3
+
4
+ Three lines::
5
+
6
+ from jev_trust import TrustedJev
7
+
8
+ jev = TrustedJev(api_key=..., domain="email-triage")
9
+ r = jev.decide(state, {"q": q})["q"]
10
+ r.stated_confidence, r.effective_confidence, r.domain_verdict
11
+
12
+ Every call is logged; record outcomes and the session measures how much
13
+ Jev's stated confidence is actually worth in YOUR domain (accuracy, Brier,
14
+ ECE, calibration currency C = 1 - ECE). Logs sign with ed25519 for
15
+ independent recomputation (Assay-compatible).
16
+ """
17
+ from .calib import (CalibrationState, Prediction, ece_toplabel, brier,
18
+ verdict_for, effective_confidence,
19
+ FACE_VALUE, DISCOUNT, DOWNGRADE, UNVERIFIED)
20
+ from .client import JevClient, JevAPIError, top_label, correctness
21
+ from .receipt import KeyPair, ReceiptResult, verify_log
22
+ from .trust import TrustedJev, TrustResult, ASSAY_REFERENCE_RATES
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = [
26
+ "TrustedJev", "TrustResult", "ASSAY_REFERENCE_RATES",
27
+ "CalibrationState", "Prediction", "ece_toplabel", "brier",
28
+ "verdict_for", "effective_confidence",
29
+ "FACE_VALUE", "DISCOUNT", "DOWNGRADE", "UNVERIFIED",
30
+ "JevClient", "JevAPIError", "top_label", "correctness",
31
+ "KeyPair", "ReceiptResult", "verify_log",
32
+ ]
@@ -0,0 +1,148 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Runtime calibration tracking — the same math Assay uses (ECE top-label, Brier).
3
+
4
+ ECE = sum over equal-width confidence bins of (bin share) * |bin accuracy - bin confidence|.
5
+ For binary (noul) questions the top-label confidence is max(p, 1-p); for choice
6
+ questions it is the probability of the chosen option. Identical formulas to
7
+ nautilus-compass verifypack calibration checks (cross-validated against
8
+ sklearn-style references in Assay research #1/#2).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from typing import List, Optional, Tuple
14
+
15
+ DEFAULT_BINS = 10
16
+ MIN_BIN_SAMPLES = 5 # below this a bin's observed accuracy is not trustworthy
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Prediction:
21
+ """One recorded decision/outcome pair."""
22
+ qid: str
23
+ stated_confidence: float # top-label confidence the model claimed
24
+ correct: int # 1 / 0 — was the top-label decision right
25
+ p_true: float # probability the model assigned to the TRUE outcome
26
+
27
+
28
+ def _top_label_binary(p: float) -> float:
29
+ """Jev noul answers state P(yes); top-label confidence = max(p, 1-p)."""
30
+ return max(p, 1.0 - p)
31
+
32
+
33
+ def brier(preds: List[Prediction]) -> float:
34
+ """mean((1 - p_true)^2) — probability mass the model denied the truth.
35
+ For binary questions identical to the standard (p - y)^2 form."""
36
+ if not preds:
37
+ raise ValueError("no predictions")
38
+ return round(sum((1.0 - pr.p_true) ** 2 for pr in preds) / len(preds), 8)
39
+
40
+
41
+ def _bin_of(conf: float, nbins: int) -> int:
42
+ return min(int(conf * nbins), nbins - 1)
43
+
44
+
45
+ def ece_toplabel(preds: List[Prediction], nbins: int = DEFAULT_BINS) -> float:
46
+ """Equal-width binned top-label ECE."""
47
+ if not preds:
48
+ raise ValueError("no predictions")
49
+ bins: dict = {}
50
+ for pr in preds:
51
+ b = _bin_of(pr.stated_confidence, nbins)
52
+ acc_sum, conf_sum, cnt = bins.get(b, (0.0, 0.0, 0))
53
+ bins[b] = (acc_sum + pr.correct, conf_sum + pr.stated_confidence, cnt + 1)
54
+ total = len(preds)
55
+ ece = 0.0
56
+ for acc_sum, conf_sum, cnt in bins.values():
57
+ ece += (cnt / total) * abs(acc_sum / cnt - conf_sum / cnt)
58
+ return round(ece, 8)
59
+
60
+
61
+ @dataclass
62
+ class CalibrationState:
63
+ """Accumulated calibration evidence for one domain."""
64
+ preds: List[Prediction] = field(default_factory=list)
65
+
66
+ def add(self, pred: Prediction) -> "CalibrationState":
67
+ self.preds.append(pred)
68
+ return self
69
+
70
+ @property
71
+ def n(self) -> int:
72
+ return len(self.preds)
73
+
74
+ @property
75
+ def accuracy(self) -> Optional[float]:
76
+ if not self.preds:
77
+ return None
78
+ return round(sum(p.correct for p in self.preds) / len(self.preds), 4)
79
+
80
+ @property
81
+ def ece(self) -> Optional[float]:
82
+ if not self.preds:
83
+ return None
84
+ return ece_toplabel(self.preds)
85
+
86
+ @property
87
+ def brier(self) -> Optional[float]:
88
+ if not self.preds:
89
+ return None
90
+ return brier(self.preds)
91
+
92
+ @property
93
+ def C(self) -> Optional[float]:
94
+ """Calibration currency: C = 1 - ECE (Assay definition)."""
95
+ if not self.preds:
96
+ return None
97
+ return round(1.0 - self.ece, 4)
98
+
99
+ def bin_accuracy(self, conf: float, nbins: int = DEFAULT_BINS,
100
+ min_samples: int = MIN_BIN_SAMPLES) -> Optional[float]:
101
+ """Observed accuracy among past decisions that stated confidence in the
102
+ same bin — 'what is this confidence worth in my domain'. None if the bin
103
+ has too few samples to say anything."""
104
+ b = _bin_of(conf, nbins)
105
+ same = [p.correct for p in self.preds if _bin_of(p.stated_confidence, nbins) == b]
106
+ if len(same) < min_samples:
107
+ return None
108
+ return round(sum(same) / len(same), 4)
109
+
110
+
111
+ # ── trust verdicts (Assay calibration-currency reading levels) ────────────
112
+
113
+ FACE_VALUE_C = 0.80
114
+ DISCOUNT_C = 0.50
115
+
116
+ FACE_VALUE = "FACE_VALUE" # C >= 0.80: stated confidence may be used at face value
117
+ DISCOUNT = "DISCOUNT" # 0.50 <= C < 0.80: trust stated confidence times C
118
+ DOWNGRADE = "DOWNGRADE" # C < 0.50: route to human review
119
+ UNVERIFIED = "UNVERIFIED" # not enough measured evidence in this domain yet
120
+
121
+
122
+ def verdict_for(C: Optional[float], n: int, min_n: int = 20) -> str:
123
+ """Verdict from measured calibration currency. Needs min_n outcomes before
124
+ any 'verified' verdict is issued — below that everything is UNVERIFIED
125
+ (the same discipline Assay applies to its own numbers)."""
126
+ if C is None or n < min_n:
127
+ return UNVERIFIED
128
+ if C >= FACE_VALUE_C:
129
+ return FACE_VALUE
130
+ if C >= DISCOUNT_C:
131
+ return DISCOUNT
132
+ return DOWNGRADE
133
+
134
+
135
+ def effective_confidence(state: CalibrationState, stated: float) -> Tuple[Optional[float], str]:
136
+ """How much a stated confidence is worth in this domain, given evidence.
137
+
138
+ Priority: (1) observed accuracy of the matching confidence bin (>=5 samples);
139
+ (2) binless fallback: stated confidence adjusted by measured C; (3) None —
140
+ insufficient evidence, caller must fall back to its own policy."""
141
+ if state.n == 0:
142
+ return None, UNVERIFIED
143
+ bin_acc = state.bin_accuracy(stated)
144
+ if bin_acc is not None:
145
+ return bin_acc, "bin_observed"
146
+ if state.n >= 20:
147
+ return round(0.5 + state.C * (stated - 0.5), 4), "C_adjusted"
148
+ return None, "insufficient_n"
@@ -0,0 +1,106 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Minimal Jev API client (TypeSafe System One endpoint), pure stdlib.
3
+
4
+ The transport is injectable so tests run offline and callers can plug in
5
+ their own HTTP stack / mocks.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import time
11
+ import urllib.request
12
+ from typing import Callable, Dict, Optional
13
+
14
+ DEFAULT_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
15
+ DEFAULT_MODEL = "jev-latest"
16
+ DEFAULT_TIMEOUT = 60
17
+
18
+ Transport = Callable[[str, bytes, Dict[str, str]], dict]
19
+ """transport(url, body_bytes, headers) -> parsed json response dict"""
20
+
21
+
22
+ def _urllib_transport(url: str, body: bytes, headers: Dict[str, str]) -> dict:
23
+ req = urllib.request.Request(url, data=body, method="POST", headers=headers)
24
+ with urllib.request.urlopen(req, timeout=DEFAULT_TIMEOUT) as r:
25
+ return json.loads(r.read())
26
+
27
+
28
+ class JevClient:
29
+ def __init__(self, api_key: str, endpoint: str = DEFAULT_ENDPOINT,
30
+ model: str = DEFAULT_MODEL, transport: Optional[Transport] = None):
31
+ self.api_key = api_key
32
+ self.endpoint = endpoint
33
+ self.model = model
34
+ self._transport = transport or _urllib_transport
35
+ self.calls = 0
36
+ self.errors = 0
37
+
38
+ def decide(self, state: dict, questions: dict, retries: int = 2,
39
+ retry_delay: float = 1.0) -> dict:
40
+ """Call Jev once. questions = {qid: {type: noul|choice|score, ...}}.
41
+ Returns the raw response dict {answers: {...}, usage: {...}}.
42
+ Transient errors are retried; content is never retried (no fishing
43
+ for better answers — Assay discipline)."""
44
+ body = json.dumps({"model": self.model, "state": state,
45
+ "questions": questions}).encode()
46
+ headers = {"Authorization": "Bearer " + self.api_key,
47
+ "Content-Type": "application/json"}
48
+ last_err = None
49
+ for attempt in range(retries + 1):
50
+ self.calls += 1
51
+ try:
52
+ return self._transport(self.endpoint, body, headers)
53
+ except Exception as e: # network/5xx class only reaches here
54
+ self.errors += 1
55
+ last_err = e
56
+ if attempt < retries:
57
+ time.sleep(retry_delay * (attempt + 1))
58
+ raise JevAPIError(f"jev api failed after {retries + 1} attempts: {last_err}")
59
+
60
+
61
+ class JevAPIError(RuntimeError):
62
+ pass
63
+
64
+
65
+ # ── answer normalization ──────────────────────────────────────────────────
66
+
67
+ def top_label(answer: dict) -> dict:
68
+ """Normalize a Jev answer into {decision, stated_confidence, probabilities}.
69
+
70
+ - noul: {noul: 0.93, type: noul} -> decision yes/no
71
+ - choice: {choice: 'alpha', probabilities: {...}} -> decision 'alpha'
72
+ - score: {score: 0.8, type: score} -> value 0.8, confidence 1.0
73
+ """
74
+ t = answer.get("type")
75
+ if t == "noul":
76
+ p = float(answer["noul"])
77
+ decision = "yes" if p >= 0.5 else "no"
78
+ return {"decision": decision, "stated_confidence": max(p, 1.0 - p),
79
+ "probabilities": {"yes": p, "no": 1.0 - p}}
80
+ if t == "choice":
81
+ probs = dict(answer.get("probabilities") or {})
82
+ decision = answer.get("choice")
83
+ stated = answer.get("confidence")
84
+ if stated is None:
85
+ stated = max(probs.values()) if probs else 1.0
86
+ return {"decision": decision, "stated_confidence": float(stated),
87
+ "probabilities": probs}
88
+ if t == "score":
89
+ return {"decision": float(answer["score"]), "stated_confidence": 1.0,
90
+ "probabilities": None}
91
+ raise ValueError(f"unknown answer type: {t!r}")
92
+
93
+
94
+ def correctness(top: dict, truth) -> int:
95
+ """Was the top-label decision correct, given ground truth?
96
+ noul: truth in {0,1} vs decision yes/no; choice: string equality.
97
+ score answers carry no notion of correctness — raise rather than guess."""
98
+ decision = top["decision"]
99
+ if isinstance(decision, float):
100
+ raise ValueError("score answers have no top-label correctness")
101
+ if decision in ("yes", "no"):
102
+ yes = {1: "yes", 0: "no", True: "yes", False: "no"}.get(truth)
103
+ if yes is None:
104
+ raise ValueError(f"bad truth for noul answer: {truth!r}")
105
+ return 1 if decision == yes else 0
106
+ return 1 if str(decision) == str(truth) else 0
@@ -0,0 +1,129 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ed25519 (RFC 8032) — vendored reference implementation.
3
+
4
+ Same source as assay-verify (sdks/assay-verify) and nautilus-compass
5
+ tools/verifypack; the three implementations are cross-validated (VALID
6
+ both ways, 2026-09-19). Pure python, zero dependencies.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import secrets
12
+
13
+ _p = 2**255 - 19
14
+ _q = 2**252 + 27742317777372353535851937790883648493
15
+
16
+
17
+ def _inv(x: int) -> int:
18
+ return pow(x, _p - 2, _p)
19
+
20
+
21
+ _d = -121665 * _inv(121666) % _p
22
+ _I = pow(2, (_p - 1) // 4, _p)
23
+
24
+
25
+ def _xrecover(y: int) -> int:
26
+ xx = (y * y - 1) * _inv(_d * y * y + 1)
27
+ x = pow(xx, (_p + 3) // 8, _p)
28
+ if (x * x - xx) % _p != 0:
29
+ x = x * _I % _p
30
+ if x % 2 != 0:
31
+ x = _p - x
32
+ return x
33
+
34
+
35
+ _By = 4 * _inv(5) % _p
36
+ _Bx = _xrecover(_By)
37
+ _B = (_Bx % _p, _By % _p, 1, _Bx * _By % _p)
38
+
39
+
40
+ def _edwards_add(P, Q):
41
+ x1, y1, z1, t1 = P
42
+ x2, y2, z2, t2 = Q
43
+ a = (y1 - x1) * (y2 - x2) % _p
44
+ b = (y1 + x1) * (y2 + x2) % _p
45
+ c = t1 * 2 * _d * t2 % _p
46
+ dd = z1 * 2 * z2 % _p
47
+ e, f, g, h = b - a, dd - c, dd + c, b + a
48
+ return (e * f % _p, g * h % _p, f * g % _p, e * h % _p)
49
+
50
+
51
+ def _scalarmult(P, e):
52
+ if e == 0:
53
+ return (0, 1, 1, 0)
54
+ Q = _scalarmult(P, e // 2)
55
+ Q = _edwards_add(Q, Q)
56
+ if e & 1:
57
+ Q = _edwards_add(Q, P)
58
+ return Q
59
+
60
+
61
+ def _point_decompress(s: bytes):
62
+ if len(s) != 32:
63
+ raise ValueError("bad point")
64
+ y = int.from_bytes(s, "little")
65
+ sign = y >> 255
66
+ y &= (1 << 255) - 1
67
+ x = _xrecover(y)
68
+ if x & 1 != sign:
69
+ x = _p - x
70
+ P = (x, y, 1, x * y % _p)
71
+ if not (0 <= x < _p and 0 <= y < _p):
72
+ raise ValueError("out of range")
73
+ return P
74
+
75
+
76
+ def _sha512(m: bytes) -> bytes:
77
+ return hashlib.sha512(m).digest()
78
+
79
+
80
+ def _sha512_modq(m: bytes) -> int:
81
+ return int.from_bytes(_sha512(m), "little") % _q
82
+
83
+
84
+ def _encode_point(P) -> bytes:
85
+ x, y, z, _ = P
86
+ zinv = _inv(z)
87
+ x = x * zinv % _p
88
+ y = y * zinv % _p
89
+ return (y | ((x & 1) << 255)).to_bytes(32, "little")
90
+
91
+
92
+ def sign(msg: bytes, seed: bytes) -> bytes:
93
+ h = _sha512(seed)
94
+ a = int.from_bytes(h[:32], "little")
95
+ a &= (1 << 254) - 8
96
+ a |= 1 << 254
97
+ pub = _encode_point(_scalarmult(_B, a))
98
+ prefix = h[32:]
99
+ r = _sha512_modq(prefix + msg)
100
+ Rs = _encode_point(_scalarmult(_B, r))
101
+ s = (r + _sha512_modq(Rs + pub + msg) * a) % _q
102
+ return Rs + s.to_bytes(32, "little")
103
+
104
+
105
+ def verify(public: bytes, msg: bytes, signature: bytes) -> bool:
106
+ if len(public) != 32 or len(signature) != 64:
107
+ return False
108
+ try:
109
+ A = _point_decompress(public)
110
+ R = _point_decompress(signature[:32])
111
+ except ValueError:
112
+ return False
113
+ S = int.from_bytes(signature[32:], "little")
114
+ h = _sha512_modq(signature[:32] + public + msg)
115
+ sB = _scalarmult(_B, S)
116
+ hA = _scalarmult(A, h)
117
+ RhA = _edwards_add(R, hA)
118
+ return (sB[1] * RhA[2] - RhA[1] * sB[2]) % _p == 0 \
119
+ and (sB[0] * RhA[2] - RhA[0] * sB[2]) % _p == 0
120
+
121
+
122
+ def keypair() -> tuple:
123
+ seed = secrets.token_bytes(32)
124
+ h = _sha512(seed)
125
+ a = int.from_bytes(h[:32], "little")
126
+ a &= (1 << 254) - 8
127
+ a |= 1 << 254
128
+ pub = _encode_point(_scalarmult(_B, a))
129
+ return seed, pub