qa/providence_query: cache persist on miss + audit chain append

Phase 2 step 2 of #000072. The skeleton landed in 20faae0 looked up
providence_cache + returned on hit, but the miss branch just
returned the fresh run_query result without writing anything back —
so every call paid the LLM cost. Now misses persist:

  1. Build run_dag via arborist.qa.dag.build_run_dag (claim-lattice
     mode, 9-stage variant — same shape legacy query() emits)
  2. Append a providence_write event via arborist.store.append_audit
     INSIDE a BEGIN IMMEDIATE transaction
  3. INSERT INTO providence_cache with all 25 columns the schema
     requires (cache_key, source_root, document_uri, question_*,
     answer_text, merkle_proof placeholder, 4-dim policy hashes,
     3-dim schema versions, audit_event_hash linking to the just-
     appended event, run_dag_root + run_dag_blob, audit_mode, etc.)
  4. COMMIT — atomic; audit chain + providence_cache stay
     consistent on crash mid-way

Result dict on miss now carries audit_event_hash + run_dag_root so
the caller can reference the audit chain or replay the DAG.
status="fresh_persisted" (was "fresh" in the skeleton) names the
new behavior.

NOT done yet (deferred to subsequent steps):
  - merkle_proof is "[]" placeholder. Schema requires NOT NULL.
    Real per-chunk proofs are a follow-up; the cache row is
    consistent without them but downstream wallet verification has
    nothing to walk.
  - burn_existing doesn't emit a providence_burn audit event
    (legacy query() does — query.py:3084 region). Audit chain
    still grows monotonically on the persist side, just doesn't
    record what was burned.
  - equivalence_class fallback lookup (legacy tries both dedup-mode
    keys when fidelity allows; primary only here)

Tests (tests/test_providence_query.py, 4):
  - first call persists row + audit event
  - second call returns cache_hit ignoring different stub
  - burn_existing forces re-run + re-persist; audit chain grows
  - chain links correctly across two distinct cache_keys

283 tests pass in the broader query/corpus/sidecar/wallet/bucket/
claim_lattice/byte_identity/providence gate.
This commit is contained in:
russell@unturf.com 2026-05-31 14:09:35 -04:00
parent b1c8fb7eba
commit 17b9622a08
No known key found for this signature in database
2 changed files with 319 additions and 20 deletions

View file

@ -0,0 +1,159 @@
"""End-to-end tests for arborist.qa.providence_query.
Covers Phase 2 step 1 + step 2 of #000072:
- Step 1: cache hit / miss / burn shape (skeleton landed 20faae0)
- Step 2: persist on miss — providence_cache row + audit chain
event written atomically inside one transaction
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import Iterator
import pytest
from arborist.document import Document
from arborist.ingest import ingest_source
from arborist.qa.client import StubClient
from arborist.qa.corpus import SqliteShardCorpus
from arborist.qa.providence_query import providence_query
from arborist.source import Source
from arborist.store import connect
class _S(Source):
source_type = "test"
def __init__(self, ds): self.ds = ds
def iter_documents(self) -> Iterator[Document]:
yield from self.ds
@pytest.fixture
def setup(tmp_path):
"""Build a one-doc corpus + an empty qa.db; return (corpus, qa_path)."""
shard = tmp_path / "shard.db"
qa = tmp_path / "qa.db"
c = connect(shard)
ingest_source(c, _S([Document(
uri="test://anarchism", source_type="test", title="Anarchism",
content=(
"Anarchism is a political philosophy that promotes a stateless "
"society. " * 6
+ 'The phrase "abolition of authority" describes the core principle. ' * 4
),
)]))
c.execute("PRAGMA wal_checkpoint(FULL)")
c.close()
connect(qa).close() # init schema
corpus = SqliteShardCorpus(connect(shard))
return corpus, qa
def test_first_call_persists_with_audit_event(setup):
corpus, qa = setup
stub = 'The core principle of anarchism is the "abolition of authority". [E1]'
r = providence_query(
corpus, "what is the core principle of anarchism?",
StubClient(answer=stub),
qa_db=qa, model_id="stub", top_k=2,
)
assert r["status"] == "fresh_persisted"
assert r["audit_mode"] == "STRICT"
assert r["cache_key"]
assert r["audit_event_hash"]
assert r["run_dag_root"]
qaro = sqlite3.connect(f"file:{qa}?mode=ro", uri=True)
n_cache = qaro.execute(
"SELECT COUNT(*) FROM providence_cache WHERE cache_key = ?",
(r["cache_key"],),
).fetchone()[0]
assert n_cache == 1
n_audit = qaro.execute(
"SELECT COUNT(*) FROM audit_events WHERE event_type='providence_write'"
).fetchone()[0]
assert n_audit == 1
qaro.close()
def test_second_call_returns_cache_hit(setup):
"""Second call with the SAME question + model + corpus must hit
the cache and ignore the (different) stub answer."""
corpus, qa = setup
q = "what is the core principle of anarchism?"
stub1 = 'The core principle of anarchism is the "abolition of authority". [E1]'
r1 = providence_query(
corpus, q, StubClient(answer=stub1),
qa_db=qa, model_id="stub", top_k=2,
)
r2 = providence_query(
corpus, q, StubClient(answer="WRONG ANSWER [E1]"),
qa_db=qa, model_id="stub", top_k=2,
)
assert r2["status"] == "cache_hit"
assert r2["audit_mode"] == r1["audit_mode"]
assert r2["answer_text"] == r1["answer_text"] # NOT the wrong stub
assert r2["cache_key"] == r1["cache_key"]
def test_burn_existing_forces_re_run_and_re_persists(setup):
"""burn_existing deletes the cached row, re-runs the LLM, and
persists a NEW row (which by chance may have the same cache_key
if inputs are identical — that's expected). audit chain grows by
one event per write."""
corpus, qa = setup
q = "what is the core principle of anarchism?"
stub = 'The core principle of anarchism is the "abolition of authority". [E1]'
providence_query(
corpus, q, StubClient(answer=stub),
qa_db=qa, model_id="stub", top_k=2,
)
r2 = providence_query(
corpus, q, StubClient(answer=stub),
qa_db=qa, model_id="stub", top_k=2, burn_existing=True,
)
assert r2["status"] == "burned"
assert r2["burned_existing"] == 1
# After burn + re-persist, exactly one live row + 2 audit events
# (write + write — the burn itself doesn't currently emit a
# 'providence_burn' event in providence_query; legacy query()
# does, but that's deferred to a later sub-step).
qaro = sqlite3.connect(f"file:{qa}?mode=ro", uri=True)
n_live = qaro.execute(
"SELECT COUNT(*) FROM providence_cache "
"WHERE falsification_state = 'live'"
).fetchone()[0]
assert n_live == 1
n_audit = qaro.execute(
"SELECT COUNT(*) FROM audit_events WHERE event_type='providence_write'"
).fetchone()[0]
assert n_audit == 2
qaro.close()
def test_audit_chain_links_correctly(setup):
"""Each providence_write event must chain off the previous
head — append_audit's own discipline. Verified by walking the
chain backwards from the latest event."""
corpus, qa = setup
q = "what is the core principle of anarchism?"
stub = 'The core principle of anarchism is the "abolition of authority". [E1]'
# Write two rows via two distinct cache_keys (different model_ids).
r1 = providence_query(
corpus, q, StubClient(answer=stub),
qa_db=qa, model_id="stub", top_k=2,
)
r2 = providence_query(
corpus, q, StubClient(answer=stub),
qa_db=qa, model_id="stub2", top_k=2,
)
assert r1["cache_key"] != r2["cache_key"]
qaro = sqlite3.connect(f"file:{qa}?mode=ro", uri=True)
rows = qaro.execute(
"SELECT event_hash, prev_event_hash, event_type FROM audit_events "
"WHERE event_type='providence_write' ORDER BY ts ASC"
).fetchall()
qaro.close()
assert len(rows) == 2
# Second event's prev_event_hash must be the first event's hash.
assert rows[1][1] == rows[0][0]