File size: 25,475 Bytes
28c70af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT OR Apache-2.0
"""The Savante UI's data layer, offline (stdlib only; Gradio is not imported): proofs of data, CIDs, .history
search, .memory, metrics, and the view server's routes. A temporary state directory; nothing touches the canon.
run: python3 testing/test_ui.py"""
import json, os, sys, tempfile, threading, urllib.request, urllib.error
from pathlib import Path

tmp = Path(tempfile.mkdtemp(prefix="bankml-ui-"))
os.environ.update(BANKML_UI_STATE=str(tmp), RAGE_PATH=str(tmp / "no-rage"), SAVANTE_CANON=str(tmp / "no-canon"), BANKML_VOICE_DIR=str(tmp / "voice"), BANKML_EXPORT_DIR=str(tmp / "export"))
sys.dont_write_bytecode = True
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "sAGI"))
import savante as u  # noqa: E402

fails = 0
def check(name, ok):
    global fails
    fails += not ok
    print(("ok   " if ok else "FAIL ") + name)

# CIDv1 raw of "abc" — the value mindX rage.py and the canon's ledger construction give
check("cid_v1_raw('abc')", u.cid_v1_raw(b"abc") == "bafkreif2pall7dybz7vecqka3zo24irdwabwdi4wc55jznaq75q7eaavvu")

# .history: records, commitment, inclusion proofs (odd and even sizes), tamper detection
for i in range(5):
    u.history_append({"ts": 1790000000 + i, "sent_at": f"2026-09-28T10:0{i}:00.000-0700", "first_token_s": 10.0 + i, "response_s": 20.0 + 2 * i,
                      "session": "s1", "user": f"question {i} about verification" if i % 2 else f"question {i} about ternary kernels",
                      "assistant": f"answer {i}", "receipt": {"prompt_tokens": 300, "completion_tokens": 20, "response_sha256": u.sha256(f"answer {i}".encode())}})
c = u.commitment(u.HISTORY)
check("commitment counts 5 records, has a root and a CID", c["records"] == 5 and len(c["merkle_root"]) == 64 and c["file_cid"].startswith("bafkrei"))
lines = u._lines(u.HISTORY)
check("every record's inclusion proof verifies", all(u.verify_inclusion(lines[k], u.inclusion_proof(u.HISTORY, k)) for k in range(5)))
check("a changed byte fails its proof", not u.verify_inclusion(lines[2].replace(b"answer 2", b"answer X"), u.inclusion_proof(u.HISTORY, 2)))
check("a proof for another record fails", not u.verify_inclusion(lines[1], u.inclusion_proof(u.HISTORY, 3)))
check("out-of-range proof is empty", u.inclusion_proof(u.HISTORY, 9) == {})
# RFC 6962: the certificate-transparency reference roots, and the second-preimage cases the old tree allowed
ct = [bytes.fromhex(x) for x in ["", "00", "10", "2021", "3031", "40414243", "5051525354555657", "606162636465666768696a6b6c6d6e6f"]]
check("Merkle roots equal RFC 6962's reference vectors (1, 3 and 8 leaves)",
      u.merkle_root([u._leaf(x) for x in ct[:1]]) == "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d"
      and u.merkle_root([u._leaf(x) for x in ct[:3]]) == "aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77"
      and u.merkle_root([u._leaf(x) for x in ct]) == "5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328")
lv = [u._leaf(l) for l in lines]
node = bytes.fromhex(lv[0]) + bytes.fromhex(lv[1])  # an interior node's preimage, offered as a "line"
fake = {"scheme": u.MERKLE_SCHEME, "record": 0, "records": 4, "leaf": u._leaf(node), "path": [u._node(lv[2], lv[3])], "merkle_root": u.merkle_root(lv[:4])}
check("an interior node cannot pass as a record (leaves and nodes are domain-separated)", not u.verify_inclusion(node, fake))
check("3 records and 4 records with the last repeated have different roots", u.merkle_root(lv[:3]) != u.merkle_root(lv[:3] + lv[2:3]))
check("a proof moved to another index fails", not u.verify_inclusion(lines[1], {**u.inclusion_proof(u.HISTORY, 1), "record": 0}))

# a torn last line (a crash mid-write) must not blank the history, and the next record must not be glued onto it
import shutil as _sh
torn = tmp / "torn.history"
_sh.copy(u.HISTORY, torn)
with open(torn, "ab") as f:
    f.write(b'{"ts": 1, "user": "half a rec')
u.jsonl_append(torn, {"ts": 2, "user": "after the crash"})
recs = u.jsonl_read(torn)
check("a torn line is skipped and counted, the rest survives, and the next append starts on a new line",
      len(recs) == 6 and recs[-1]["user"] == "after the crash" and u.JSONL_BAD[str(torn)] == 1)
check("a receipt is checked against the raw answer (think block and whitespace included)",
      u._hash_ok({"assistant": "hi", "assistant_raw": "<think>x</think>\n hi ", "receipt": {"response_sha256": u.sha256("<think>x</think>\n hi ".encode())}})
      and u._hash_ok({"assistant": "hi", "receipt": {"response_sha256": u.sha256(b"hi")}}))
u.INFLIGHT.clear()
u.INFLIGHT["lost"] = __import__("time").time() - u.STALE_S - 1
check("a lost request (a closed tab) expires instead of blocking model switches forever", u.chat_busy() is False and not u.INFLIGHT)

# embeddings (bge-m3 through Ollama), offline: a fake Ollama whose vectors are bags of words hashed into 1,024 dims
import http.server as _hs, threading as _th, embed as _em, math as _m
def _vec(t):
    v = [0.0] * _em.DIMS
    for w in __import__("re").findall(r"[a-z0-9]+", t.lower()):
        v[int(__import__("hashlib").sha256(w.encode()).hexdigest(), 16) % _em.DIMS] += 1.0
    return v
class _FakeOllama(_hs.BaseHTTPRequestHandler):
    def do_GET(self):
        b = json.dumps({"models": []}).encode()
        self.send_response(200); self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
    def do_POST(self):
        req = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        b = json.dumps({"embeddings": [_vec(t) for t in req["input"]]}).encode()
        self.send_response(200); self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
    def log_message(self, *a):
        pass
_fo = _hs.ThreadingHTTPServer(("127.0.0.1", 0), _FakeOllama)
_th.Thread(target=_fo.serve_forever, daemon=True).start()
_em.OLLAMA = f"http://127.0.0.1:{_fo.server_address[1]}"
_em.provenance = lambda: {"model": "bge-m3", "digest": "d1" * 32, "bytes": 1, "licence": "mit", "open": True, "dims": _em.DIMS}
_em._mem_available = lambda: 8 * 10 ** 9
r_idx = _em.index(u.HISTORY, u.history_all())
check("embed.index: every exchange embedded once, cached beside .history with the model digest",
      r_idx["embedded"] == 5 and _em.index(u.HISTORY, u.history_all())["embedded"] == 0 and len(_em.cached(u.HISTORY)) == 5
      and all(abs(sum(x * x for x in v) - 1) < 1e-5 for v in _em.cached(u.HISTORY).values()))
_em.provenance = lambda: {"model": "bge-m3", "digest": "e2" * 32, "bytes": 1, "licence": "mit", "open": True, "dims": _em.DIMS}
check("a cache written by other weights is not used (vectors are tied to the model's digest)", _em.cached(u.HISTORY) == {})
_em.provenance = lambda: {"model": "bge-m3", "digest": "d1" * 32, "bytes": 1, "licence": "mit", "open": True, "dims": _em.DIMS}
check("reciprocal rank fusion: first in both beats first in one", [i for i, _ in _em.fuse([1, 2, 3], [2, 3, 1])][0] == 2
      and _em.fuse([7], [])[0][0] == 7)
hits_h, eng_h = u.history_search("ternary kernels")
check("the ragebar fuses BM25 and bge-m3 and says so", "bge-m3" in eng_h and "reciprocal rank" in eng_h and hits_h)
_mem = _em._mem_available
_em._mem_available = lambda: 10 ** 8
_em._QCACHE.clear()
hits_b, eng_b = u.history_search("verification")
check("without the memory to load bge-m3, the ragebar is BM25 alone (and still answers)", "bge-m3" not in eng_b and hits_b)
_em._mem_available = _mem
_em.provenance = lambda: {}
check("without bge-m3 in Ollama, embed says why and search stands on BM25", not _em.status()["ready"] and "bge-m3" not in u.history_search("verification")[1])
_fo.shutdown()

# the history window moves in steps, so the engine's prompt cache is reused (KoboldCpp's Smart Context idea; vLLM's
# chained block hashes show a one-exchange slide defeats it)
starts = [u.window_start(n) for n in range(1, 61)]
check("the window moves 8 times in 60 turns (a one-exchange slide moves 48 times)", sum(1 for a, b in zip(starts, starts[1:]) if a != b) == 8
      and all(12 <= n - s <= 17 for n, s in zip(range(13, 61), starts[12:])))
tt = [(f"q{i}", f"a{i}\n\n<sub>⏱ sent 12:00:00 · receipt</sub>") for i in range(30)]
mm = u.build_messages("S", tt, "Q", False)
check("the model never sees the chat's footer (clock, receipt)", "<sub>" not in json.dumps(mm) and mm[2]["content"] == "a18")
cnt = lambda t: max(1, len(t) // 4)  # noqa: E731 — a stand-in tokenizer, offline
big = [("q" * 100, "a" * 1200) for _ in range(13)]
u.build_messages("s" * 3600, big, "Q", False, ctx_tokens=2048, reserve_tokens=256, count=cnt)
check("never silent amnesia: when history cannot all fit, as much as fits is sent (0.1.8 sent none), and it is recorded",
      u.LAST_WINDOW["sent"] >= 2 and u.LAST_WINDOW["of"] == 13 and u.LAST_WINDOW["ctx"] == 2048)
inf = {}
u.build_messages("s", [("q" * 50, "a" * 200)] * 20, "Q", False, ctx_tokens=32768, count=cnt, info=inf, session="roomy")
check("with room, the history note stays quiet (the ordinary 12–17 window is not a trim)", inf["sent"] == 14 and not inf["trimmed"])
seq = []
for k in range(12, 19):  # consecutive turns in one session once the context binds: the start is kept while it fits
    inf = {}
    u.build_messages("s" * 3600, [("q" * 100, "a" * 800)] * k, "Q", False, ctx_tokens=4096, reserve_tokens=256, count=cnt, info=inf, session="tight")
    seq.append(k - inf["sent"])
check("when the context binds, the start is kept while it fits (a warm prompt cache), not moved every turn",
      sum(1 for a, b in zip(seq, seq[1:]) if a != b) <= 2)
try:
    u.build_messages("s" * 9000, [], "Q", False, ctx_tokens=2048, count=cnt)
    check("a system prompt and question that cannot fit are refused with a reason, not sent", False)
except u.ContextTooSmall:
    check("a system prompt and question that cannot fit are refused with a reason, not sent", True)
check("an answer's own <sub> survives; only the chat's clock footer is removed",
      u.model_text("H\n\n<sub>2</sub>O\n\n<sub>⏱ sent 12:00</sub>") == "H\n\n<sub>2</sub>O")
check("the window fits the engine's context, moving in whole steps", len(u.build_messages("S", tt, "Q", False, ctx_tokens=60, reserve_tokens=10)) < len(mm))

# search (built-in BM25 here: RAGE_PATH points nowhere)
hits, engine = u.history_search("ternary kernels")
check("history search finds the ternary questions first (built-in engine)", engine.startswith("RAGE-shaped") and {h[1] for h in hits[:3]} == {0, 2, 4})
check("empty query returns nothing", u.history_search("  ")[0] == [])

# metrics from recorded timings
m = u.metrics()
check("metrics: 5 exchanges, median response 24 s, all hashes match", m["exchanges"] == 5 and abs(m["response_s"]["median"] - 24.0) < 1e-9 and m["hash_ok"] == 5)
check("metrics: prefill = prompt tokens / first token", abs(m["prefill_tok_s"]["median"] - 300 / 12.0) < 1e-9)

# .memory
u.memory_add("the operator prefers verdicts", {"kind": "typed"})
u.memory_add("second note", {"kind": "response"})
check(".memory holds 2 notes and a prompt block", len(u.memory_all()) == 2 and "second note" in u.memory_block() and "not evidence" in u.memory_block())
u.memory_remove(1)
check(".memory remove by number", [x["text"] for x in u.memory_all()] == ["second note"])

# custom agents: keccak256, the doctrine root, derive / verify / save, preflight, the template untouched
os.environ["BANKML_AGENTS"] = str(tmp / "agents")
import importlib, agents  # noqa: E402
importlib.reload(agents)
check("keccak256('') and ('abc') vectors", agents.keccak256(b"").hex() == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
      and agents.keccak256(b"abc").hex() == "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45")
try:  # an independent implementation, where installed: every length 0..400 crosses the 136-byte rate twice
    from Crypto.Hash import keccak as _k
    check("keccak256 == pycryptodome for every length 0..400", all(agents.keccak256((bytes(range(256)) * 2)[:n]) ==
          _k.new(digest_bits=256, data=(bytes(range(256)) * 2)[:n]).digest() for n in range(401)))
except ImportError:
    print("skip  keccak256 vs pycryptodome (not installed)")
real = next((p for p in (Path.home() / "cryptoAGI" / "savante", Path.home() / "savante") if p.exists()), Path.home() / "cryptoAGI" / "savante")
if (real / "savante.persona").is_file():
    sp, sl = json.loads((real / "savante.persona").read_bytes()), json.loads((real / "savante.commitments.json").read_bytes())
    check("doctrine root reproduces Savante's published ledger value", agents.doctrine_root(sp, sl["doctrine_root"]["pointers"]) == sl["doctrine_root"]["value"])
else:
    print("skip  doctrine root vs Savante (no ~/cryptoAGI/savante here)")
tpl = {"persona": "tpl", "name": "Template", "source": "s", "format": "f", "system_prompt": "You are the template.", "mantra": "m", "oath": "o",
       "bdi": {"beliefs": [{"id": "b", "belief": "x"}]}, "skills": {"primary": "p", "taxonomy": [], "defer_triggers": [], "validation": []},
       "safety": {"scope": "s"}, "embodiment": {}, "token": {"intelligence": {"tool_allowlist": ["read"]}, "bindings": {"erc7857": "SECRET"}}}
tpl_bytes = json.dumps(tpl, sort_keys=True).encode()
slug = agents.derive(tpl, {"artifacts": {"identity": {"sha256": "ab"}}, "doctrine_root": {"value": "0x01"}}, "Auditor Ada", system_prompt="You are Ada.", description="an auditor")
f = agents.files(slug)
check("derive writes persona, prompt, card, ledger; it verifies", slug == "auditor_ada" and all(f[k].is_file() for k in ("persona", "prompt", "agentcard.json", "commitments.json"))
      and all(ok for _, ok, _ in agents.verify(slug)))
pd = json.loads(f["persona"].read_text())
check("derived identity is new; the template's bindings are not copied", pd["name"] == "Auditor Ada" and pd["token"]["bindings"]["erc7857"] is None
      and json.dumps(tpl, sort_keys=True).encode() == tpl_bytes)
card = json.loads(f["agentcard.json"].read_text())
check("agent card: ERC-8004 registration-v1, not_yet_minted, derived_from recorded", card["type"].endswith("#registration-v1") and card["bankml"]["status"] == "not_yet_minted"
      and card["bankml"]["derived_from"]["persona_sha256"] == "ab")
r0 = json.loads(f["commitments.json"].read_text())["doctrine_root"]["value"]
led = agents.save(slug, prompt_text="You are Ada, revised.")
check("editing .prompt re-ledgers (prompt hash changes; the doctrine root, over the persona, does not)", led["doctrine_root"]["value"] == r0
      and led["artifacts"]["prompt"]["sha256"] == u.sha256(b"You are Ada, revised.\n"))
pd["voice_examples"] = ["I check before I speak."]
led = agents.save(slug, persona_text=json.dumps(pd))
check("editing a field outside the doctrine re-ledgers with the same doctrine root", led["doctrine_root"]["value"] == r0
      and led["doctrine_root"]["fixed_at_derivation"] == r0 and all(ok for _, ok, _ in agents.verify(slug)))
try:
    agents.save(slug, persona_text=json.dumps(dict(pd, mantra="a new mantra")))
    check("a doctrine clause cannot be edited after derivation (the docs' promise, now enforced)", False)
except ValueError as e:
    check("a doctrine clause cannot be edited after derivation (the docs' promise, now enforced)", "/mantra" in str(e)
          and json.loads(f["persona"].read_text())["mantra"] != "a new mantra")
led_p = f["commitments.json"]
lj = json.loads(led_p.read_text())
lj["doctrine_root"]["pointers"] = ["/name"]  # a ledger that narrows its own check
led_p.write_text(json.dumps(lj))
check("verify uses bankml's pointer list, not the one in the ledger it checks", not dict((n, ok) for n, ok, _ in agents.verify(slug))["doctrine root"])
agents.rebind(slug)
f["prompt"].write_text("tampered\n")
check("a hand edit outside save() is caught by verify", not dict((n, ok) for n, ok, _ in agents.verify(slug))["prompt"])
for bad, why in ((dict(pd, weight=0.5), "float"), ({k: v for k, v in pd.items() if k != "oath"}, "missing clause")):
    try:
        agents.save(slug, persona_text=json.dumps(bad))
        check(f"preflight refuses a {why}", False)
    except ValueError:
        check(f"preflight refuses a {why}", True)
try:
    agents.agent_dir("../etc")
    check("agent_dir rejects path traversal", False)
except ValueError:
    check("agent_dir rejects path traversal", True)

# the chosen aivatar: validated by content and size, ledgered, and part of the THOT bundle
png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 64
led = agents.set_aivatar(slug, png)
check("a PNG aivatar is accepted and ledgered", led["artifacts"]["aivatar"]["sha256"] == u.sha256(png) and all(ok for _, ok, _ in agents.verify(slug)))
for bad, why in ((b"not an image at all", "a non-image"), (b"\x89PNG\r\n\x1a\n" + b"\x00" * (2 * 1024 * 1024), "an image over 2 MB")):
    try:
        agents.set_aivatar(slug, bad)
        check(f"the aivatar refuses {why}", False)
    except ValueError:
        check(f"the aivatar refuses {why}", True)

# THOT manifests: the spec's test vectors (where those repositories are here), and a bankml bundle's lineage
import subprocess, thot  # noqa: E402
VEC = [("savante", real, "1fcca89", "savante.thot.json", "0x235da8e993dc8af2c077f50d698962446b872b17b1e5b033d5c5d976532b8880",
        "0xdc1d80957cf831aee6638fd569e22cf0f6e5a1ec99ddde91cfecb5a15408fbe1", "bafkreieerglzkjrkmwrxhmrk53bf3tvhp3aut3qlatwpdtoutmspuxrh4i"),
       ("jaimla", Path.home() / "cryptoAGI" / "jaimla", "8b57ccf", "jaimla.thot.json", "0x7f5bcc69dc9d50854189762dba0a97b3a48090ee546a0423aa9722917740e086",
        "0xd7cb5363324645b4f6d0df19902fe9cd15dcb1c36ae925ee5ff2278315f6a258", "bafkreidczapw72pldcf5pxyoz2hhdleauto7ajvjgun7w2iiq7ccwwhvku"),
       ("luvai", Path.home() / "cryptoAGI" / "luvai", "0c1eef7", "luvai.thot.json", "0x2feb7ecfcffe91c035951298e5d977d880ec475edb87fff5f3038efc8d44f94c",
        "0xf594d1b2ab3f98b20ca203227e71a437ac86d82f6c4d4942227ecf8ee018309b", "bafkreiefvcfiw2rkrcjzblcj5ivrux7mtpsha4fqaulgvteycpr4zbwqtq")]
for name, repo, commit, fn, br, mr, cid in VEC:
    g = subprocess.run(["git", "-C", str(repo), "show", f"{commit}:{fn}"], capture_output=True, text=True)
    if g.returncode:
        print(f"skip  THOT test vector {name} (repository not here)")
        continue
    m = json.loads(g.stdout)
    recs = thot.records(m["facets"])
    check(f"THOT spec vector {name}: bundle_root, Merkle root and identity CID", thot.bundle_root(recs)[0] == br and thot.merkle_root(recs) == mr
          and thot.identity(m)["cid"] == cid)
m1 = thot.build(slug)
check("a bankml THOT bundle verifies, with the aivatar as a facet", thot.verify(slug) == [] and m1["merkle"]["populated"] == 6
      and any(x["facet"] == "x-bankml.aivatar" for x in m1["facets"]))
check("the bundle commits history by digest and holds none of its text", "question" not in (agents.agent_dir(slug) / f"{slug}.thot.json").read_text()
      and any(x["facet"] == "x-bankml.history" for x in m1["facets"]))
m1b = thot.build(slug)
f["history"].write_text(f["history"].read_text() + '{"user": "new"}\n')
m2 = thot.build(slug)
check("re-bind keeps the generation; a facet change is n+1 with parent = the previous CID",
      m1b["bundle"]["generation"] == m1["bundle"]["generation"] and m2["bundle"]["generation"] == m1b["bundle"]["generation"] + 1
      and m2["bundle"]["parent"] == m1b["identity"]["cid"] and thot.verify(slug) == [])

# Savante's voice: the pronunciation table, speech from markdown, one render, the view's audio gate
import speak  # noqa: E402
t2 = {"format": "voaice-pronunciation/1", "version": 2, "entries": [{"match": "PYTHAIML", "say": "Pith AI M L"}, {"match": "SAVANTE", "say": "Sav ont"},
                                                                  {"match": "PYTHAI", "say": "Pith AI"}]}
check("pronunciation: one pass, longest match first, case-insensitive", speak.say("savante and PYTHAIML and pythai", t2) == "Sav ont and Pith AI M L and Pith AI")
check("pronunciation is idempotent (a spoken form holds no match)", speak.say(speak.say("Savante", t2), t2) == "Sav ont")
sp = speak.speech("# Title\n\nSee [the docs](https://x.y) and `code`. Second sentence!\n\n```sh\nrm -rf /\n```\n\n| a | b |\n|---|---|\n\n- a **bold** item")
check("speech(): code and tables dropped, links read as text, markers gone", "rm -rf" not in " ".join(sp) and "https" not in " ".join(sp)
      and "See the docs and code." in sp and "Second sentence!" in sp and "a bold item" in sp)
check("speech() keeps savante_sagi as written; say() speaks it as words", speak.speech("one seat of core_command, savante_sagi.") == ["one seat of core_command, savante_sagi."]
      and speak.say("APPROVE_WITH_CONDITIONS", t2) == "APPROVE WITH CONDITIONS")
ok_v, why_v = speak.available()
if ok_v:
    it = speak.render(["I am Savante."])
    check("a statement renders to Opus in Savante's voice, cached by key", len(it) == 1 and it[0]["file"].is_file() and it[0]["file"].read_bytes()[:4] == b"OggS"
          and speak.cached(["I am Savante."])[0]["key"] == it[0]["key"])
else:
    print(f"skip  voice render ({why_v})")
check("say(): SCIEN·TIFIC, bankml and a pythai address are said, not spelled", speak.say("SCIEN·TIFIC at scientific.pythai.net runs bankml", t2)
      == "Sci-en, Tiffic at scientific dot Pith AI dot net runs bank M L")
check("say(): TECHNICAL.md notation read aloud", speak.say("w ∈ {−1, 0, +1}; 3⁵ = 243 ≤ 256 (§III.5)", t2)
      == "w in minus one, zero, or plus one; three to the fifth equals 243 at most 256")
check("say(): names keep their separators; files, formats and references are read properly",
      speak.say("Professor / OVERLORD", t2) == "Professor / OVERLORD"
      and speak.say("mindX DAIO · savante_sagi", t2) == "mindX DAIO · Sav ont sagi"
      and speak.say("see (q1_0.rs, 0.0.4) now", t2) == "see now" and speak.say("bankml.rs", t2) == "bank M L dot R S"
      and speak.say("PQ2_0 = 142 and Q4_K_M", t2) == "P Q two zero equals 142 and Q four K M"
      and speak.say("Bankml, as in §III.4.", t2) == "bank M L, as in section three point four."
      and speak.say("(16 + 2) × 8 / 128", t2) == "(16 plus 2) times 8 over 128")
check("speech(): list markers only at line starts; an inline + survives", speak.speech("- item one\n\nThe cost is (16 + 2) bits.") == ["item one", "The cost is (16 + 2) bits."])
rc = speak.reading_chapters()
check("the reading: thesis, II.1, III.2, III.5 and III.8 from TECHNICAL.md", [t for t, _ in rc][0] == "The thesis" and len(rc) == 5
      and rc[-1][0] == "Can a binary computer perform a ternary operation?" and rc[-1][1][0] == "Yes.")
try:
    speak.export("t-incomplete", [("c", ["never rendered, never exported"])])
    check("an export is refused until every sentence is rendered", False)
except RuntimeError:
    check("an export is refused until every sentence is rendered", not (speak.EXPORT_DIR / "t-incomplete.opus").exists())
(speak.EXPORT_DIR).mkdir(parents=True, exist_ok=True)
(speak.EXPORT_DIR / "t-broken.opus").write_bytes(b"OggS")
(speak.EXPORT_DIR / "t-broken.json").write_text('{"sig": "abc', encoding="utf-8")
check("a damaged export record means not current, never an error", speak.export_state("t-broken", [("c", ["x"])])["current"] is False)
if ok_v:
    speak.render(["It ends."])
    e = speak.export("t-set", [("one", ["I am Savante."]), ("two", ["It ends."])])
    pr = subprocess.run(["ffprobe", "-v", "error", "-show_chapters", "-of", "json", str(e["file"])], capture_output=True, text=True)
    check("a complete export: one Ogg Opus file, one chapter mark per chapter, current until a clip changes",
          e["current"] and e["file"].read_bytes()[:4] == b"OggS" and len(json.loads(pr.stdout or "{}").get("chapters", [])) == 2
          and not speak.export_state("t-set", [("one", ["I am Savante."]), ("two", ["It ends.", "A new line."])])["current"])
import view  # noqa: E402
check("view's audio route: only a 24-hex key the manifest lists", view.audio_file("../../etc/passwd") is None and view.audio_file("0" * 24) is None
      and (not ok_v or view.audio_file(it[0]["key"]) is not None))

# the view server: fixed routes, commitments only, no traversal
from http.server import ThreadingHTTPServer
srv = ThreadingHTTPServer(("127.0.0.1", 0), view.H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{srv.server_address[1]}"
def get(path, method="GET"):
    try:
        with urllib.request.urlopen(urllib.request.Request(base + path, method=method), timeout=10) as r:
            return r.status, r.read()
    except urllib.error.HTTPError as e:
        return e.code, b""
st, body = get("/api/state")
state = json.loads(body)
check("view /api/state carries the .history commitment", st == 200 and state["private"]["history"]["merkle_root"] == c["merkle_root"])
check("view /api/state carries no .history content", b"question 1" not in body and b"answer 3" not in body)
check("view refuses traversal and unknown paths", get("/api/result?name=../../etc/passwd")[0] == 404 and get("/etc/passwd")[0] == 404)
check("view refuses POST", get("/api/state", "POST")[0] == 405)
st_k, body_k = get("/knobs.js")
check("view serves the DreamKnob bundle at /knobs.js", st_k == 200 and b"SavanteKnobs" in body_k)
check("view's export route: only a named, complete export", get("/export/t-set.opus")[0] == 404 and get("/export/..%2Fmanifest.opus")[0] == 404
      and view.export_file("Savante") is None)
srv.shutdown()

print(f"{'all ok' if not fails else f'{fails} FAILED'}")
sys.exit(1 if fails else 0)