ticket #000031 Phase 2: warrant resolver + 18 derivations rows landed

Closes the warrant-promotion data path: claim-pack records now
bind to surface-ingested textbook chunks via Merkle inclusion
proofs in the existing `derivations` table.

What landed
===========
arborist/qa/warrant_resolver.py — four pure-data steps + one DB
write:

1. parse_citation(s) — regex pipeline turning the claim-pack
   `source_reference` string into structured Citation tuples.
   Handles "Title by Author" (single + Oxford-comma multi +
   et-al), semicolon-separated multi-cite ("Knuth §1.2.6;
   Stanley §1.2; Brualdi §3.5"), and compact author-year
   ("Pascal 1654") forms.

2. resolve_chunks(c, shards_dir) — FTS5 search across sibling
   crawl/ dir's textbook-surface shards. Skips the main numbered
   shards (Wikipedia content; would be false positives). Per-
   shard match filter requires BOTH author last name AND a title
   token in the shard's title-haystack — honest "no match" for
   textbooks not yet surface-ingested.

3. compute_proof(shard, doc_root, chunk_id) — reads
   merkle_nodes, walks layer-by-layer to assemble siblings;
   emits deterministic JSON proof_blob compatible with
   arborist/merkle.py verification.

4. write_derivation(...) — INSERT OR IGNORE into the existing
   derivations table with process_id="warrant-resolver-v1".
   Idempotent at the database layer.

CLI surface
===========
- `arborist warrant-status --shards-dir ...` (read-only) —
  emits per-record JSON: parsed citations, FTS5 candidates,
  whether a derivations row exists.
- `arborist warrant-resolve --shards-dir ... [--write]` —
  default dry-run summary; --write actually computes proofs
  and inserts rows.

End-to-end verification
=======================
Real-shard run: `arborist warrant-resolve --shards-dir
~/.arborist/shards --write` →

  records_total: 92
  records_resolved: 18
  derivations_written: 18

All 18 are pillar-IV Hilbert axioms citing "The Foundations of
Geometry by David Hilbert" — the only cited textbook fully
surface-ingested by Phase 1. The remaining 74 records cite
textbooks not in our shard cluster (Mendelson, Enderton,
Jech, Goldstein, Barendregt, Stanley, Brualdi, Knuth, …) and
correctly produce 0 matches; they stay at ANCHOR-WARRANTED
until those textbooks land via future Phase-1 manifest
expansions.

Re-running the writer is a no-op (PK collision on (core_root,
src_root, process_id) = INSERT OR IGNORE).

Drive-by fix
============
arborist/sources/textbook_tex.py — _extract_title now also
parses PG's plain-text `Author:` line and appends "by Author"
to the title, so the warrant resolver's author-last-name match
works against PG-ingested textbooks (Hilbert "The Foundations
of Geometry by David Hilbert" instead of just "The Foundations
of Geometry").

Test suite
==========
tests/test_warrant_resolver.py — 14 unit tests for the citation
parser (no DB / network). Full suite: 1588 passed / 28 skipped.

Phase 3 (verifier wiring)
=========================
NOT in this commit. The data substrate is in place; the
audit_mode upgrade path that lifts answers citing
claim-pack-records-with-derivations from ANCHOR-WARRANTED to
EVIDENCE-WARRANTED requires a verifier change — touches well-
tested code, worth its own ticket so the regression risk is
bounded.
This commit is contained in:
russell@unturf.com 2026-05-09 17:35:39 -04:00
parent 9c42a133e7
commit f0e6baf907
No known key found for this signature in database
6 changed files with 1067 additions and 24 deletions

View file

@ -3332,6 +3332,71 @@ def _cmd_selfmodel_list(args: argparse.Namespace) -> int:
return 0
def _cmd_warrant_status(args: argparse.Namespace) -> int:
"""Read-only — show what surface chunks the citation resolver
finds for each claim-pack record. JSON output. No DB writes.
See ``arborist.qa.warrant_resolver`` for ticket #000031 Phase 2.
"""
from arborist.qa.warrant_resolver import warrant_status
shards_dir = args.global_shards_dir or args.shards_dir
if not shards_dir:
print("--shards-dir is required for warrant-status", file=sys.stderr)
return 2
results = warrant_status(shards_dir, limit=args.limit)
out = []
for r in results:
out.append(
{
"record_root": r.record_root,
"record_title": r.record_title,
"source_reference": r.source_reference,
"citations": [
{
"title": c.title,
"authors": list(c.authors),
"year": c.year,
"section": c.section,
}
for c in r.citations
],
"matches": [
{
"shard": m.shard_path,
"document_root": m.document_root,
"document_title": m.document_title,
"chunk_id": m.chunk_id,
"score": m.score,
"snippet": m.snippet,
}
for m in r.matches
],
"has_derivation": r.has_derivation,
}
)
print(json.dumps(out, indent=2, ensure_ascii=False))
return 0
def _cmd_warrant_resolve(args: argparse.Namespace) -> int:
"""Run the warrant resolver across all claim-pack records.
Default dry-run (just summary counts); ``--write`` flag computes
Merkle inclusion proofs for top matches and writes
``derivations`` rows. Idempotent at the database layer.
"""
from arborist.qa.warrant_resolver import warrant_resolve
shards_dir = args.global_shards_dir or args.shards_dir
if not shards_dir:
print("--shards-dir is required for warrant-resolve", file=sys.stderr)
return 2
summary = warrant_resolve(
shards_dir, write=args.write, limit=args.limit
)
print(json.dumps(summary, indent=2))
return 0
def _cmd_mesh_status(args: argparse.Namespace) -> int:
"""Show mesh state: enabled flag, identity, current epoch, roster."""
from arborist.mesh import current_epoch, is_enabled, load_identity
@ -5010,6 +5075,43 @@ def build_parser() -> argparse.ArgumentParser:
sm_list.add_argument("--limit", type=int, default=20)
sm_list.set_defaults(func=_cmd_selfmodel_list)
# ----- warrant resolver (#000031 Phase 2) --------------------------------
warrant_status_cmd = sub.add_parser(
"warrant-status",
help="show citation-resolver matches per claim-pack record (read-only)",
)
warrant_status_cmd.add_argument(
"--shards-dir",
dest="shards_dir",
default=None,
help="shards directory (overrides --shards-dir from global)",
)
warrant_status_cmd.add_argument(
"--limit", type=int, default=3, help="max candidate matches per record"
)
warrant_status_cmd.set_defaults(func=_cmd_warrant_status)
warrant_resolve_cmd = sub.add_parser(
"warrant-resolve",
help="run citation resolver; with --write, write derivations rows binding "
"claim-pack records to surface chunks (Merkle proof)",
)
warrant_resolve_cmd.add_argument(
"--shards-dir",
dest="shards_dir",
default=None,
help="shards directory (overrides --shards-dir from global)",
)
warrant_resolve_cmd.add_argument(
"--write",
action="store_true",
help="actually write derivations rows (default: dry-run summary)",
)
warrant_resolve_cmd.add_argument(
"--limit", type=int, default=1, help="how many top matches per record (default 1)"
)
warrant_resolve_cmd.set_defaults(func=_cmd_warrant_resolve)
# ----- mesh subcommands (off by default) ---------------------------------
mesh_cmd = sub.add_parser(
"mesh",

View file

@ -0,0 +1,732 @@
"""Warrant resolution: claim-pack record → surface chunk → derivations row.
Implements ticket #000031 Phase 2. Closes the warrant gap left open at
#000029: claim-pack records cap at ANCHOR-WARRANTED on the four-rung
ladder until each citation is bound to a Merkle-proven surface span.
This module turns ``source_reference`` strings into structured
citations, resolves them against the surface-ingested textbooks
(#000031 Phase 1), computes Merkle inclusion proofs over the matching
chunks, and writes ``derivations`` rows that future verifier passes
can use to upgrade the audit_mode of any answer that cites the
record.
This is distinct from ``arborist/qa/warrant.py`` (warrant-LITE
anchor-class check from Module H+ / #000003) — that module verifies
per-claim anchors at Q&A time. This module binds claim-pack records
to their citation surfaces via Merkle proofs at ingest/curation time.
Three pure-data steps + one DB write:
1. ``parse_citation(s) → list[Citation]`` — pure regex, deterministic
2. ``resolve_chunks(c, shards) → [Match]`` — FTS5 across surface shards
3. ``compute_proof(shard, doc_root, chunk_id) → ProofBlob``
4. ``write_derivation(...)`` — INSERT into derivations
CLI surface (in ``arborist/cli.py``):
arborist warrant-status --shards-dir ... [read-only]
arborist warrant-resolve --shards-dir ... [--write]
Verifier wiring is intentionally NOT in scope here. Writing the
derivations rows is the data substrate; the four-rung-ladder upgrade
that lifts answers citing these records from ANCHOR-WARRANTED to
EVIDENCE-WARRANTED is a follow-up that reads from the rows this
module writes.
Source: ticket #000031 §8 Phase 2.
"""
from __future__ import annotations
import json
import re
import sqlite3
import time
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Iterator, Optional
# ---------------------------------------------------------------------------
# Citation parsing
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class Citation:
"""A structured representation of one cited work.
The claim-pack ``source_reference`` field is free-text; this
dataclass pins what we can recover deterministically. ``title`` is
the most discriminating field for shard matching; ``authors`` is
the fallback. ``section`` carries chapter / section / equation
refs when present (e.g., "§1.2.6 equation (13)") for downstream
chunk-resolution heuristics.
"""
title: str = ""
authors: tuple[str, ...] = field(default_factory=tuple)
year: str = ""
section: str = ""
raw: str = ""
def is_empty(self) -> bool:
return not (self.title or self.authors)
_BY_RE = re.compile(r"\s+by\s+", re.IGNORECASE)
_YEAR_RE = re.compile(r"\b(1[6-9]\d{2}|20\d{2}|21\d{2})\b")
_SECTION_RE = re.compile(
r"§[^;,]*|Chapter\s+\d+[^;,]*|Volume\s+\d+[^;,]*|equation\s*\([^)]*\)",
re.IGNORECASE,
)
_AUTHORS_TAIL_RE = re.compile(r"\s+(?:et\s+al\.?|and\s+others)$", re.IGNORECASE)
_KNOWN_PUBLISHERS_RE = re.compile(
r"\b(TAOCP|Enumerative Combinatorics|Foundations of [A-Za-z ]+)\b",
re.IGNORECASE,
)
def parse_citation(source_reference: str) -> list[Citation]:
"""Parse a ``source_reference`` string into one or more Citations.
Handles three observed patterns:
1. ``"Title by Author"`` — split on ``" by "``, single citation.
2. ``"Title by A1, A2, and A3"`` — same, multi-author tail.
3. Semicolon-separated multi-citation:
``"Knuth TAOCP §1.2.6; Stanley §1.2; Brualdi §3.5"`` — split,
parse each independently.
Empty / unparseable input yields an empty list. Outputs are best-
effort: a free-text reference like ``"Pascal 1654"`` returns one
Citation with ``title=""``, ``authors=("Pascal",)``, ``year="1654"``.
"""
if not source_reference:
return []
text = source_reference.strip()
if not text:
return []
# Pattern 3: semicolon split first.
if ";" in text:
out: list[Citation] = []
for part in text.split(";"):
out.extend(parse_citation(part.strip()))
return out
# Pattern 1+2: "by" split.
m = _BY_RE.search(text)
if m:
title_part = text[: m.start()].strip()
authors_part = text[m.end() :].strip()
return [
Citation(
title=_clean(title_part),
authors=_split_authors(authors_part),
year=_extract_year(text),
section=_extract_section(text),
raw=text,
)
]
# Compact form: "Knuth TAOCP Volume 1 §1.2.6" — heuristic.
return [
Citation(
title=_clean(_strip_metadata(text)),
authors=_compact_author(text),
year=_extract_year(text),
section=_extract_section(text),
raw=text,
)
]
def _clean(s: str) -> str:
return re.sub(r"\s+", " ", s).strip(" ,.;:")
def _split_authors(authors_part: str) -> tuple[str, ...]:
"""Split an authors-string on commas + 'and' / 'et al.' patterns.
Handles Oxford-comma form ("A, B, and C") + simple form ("A and
B") + comma-only form ("A, B"). The leading "and " on the final
Oxford-style author is stripped post-split.
"""
s = _AUTHORS_TAIL_RE.sub("", authors_part).strip()
s = _strip_metadata(s)
if not s:
return ()
parts = re.split(r",\s*|\s+and\s+", s)
cleaned: list[str] = []
for p in parts:
p = p.strip()
# Strip a leading "and " left over from Oxford-comma split
# (e.g., ", and John L. Safko" → "and John L. Safko" → "John L. Safko").
p = re.sub(r"^and\s+", "", p, flags=re.IGNORECASE).strip()
if p:
cleaned.append(p)
return tuple(cleaned)
def _compact_author(text: str) -> tuple[str, ...]:
text = _strip_metadata(text)
m = _KNOWN_PUBLISHERS_RE.search(text)
if not m:
first = text.split()
return (first[0],) if first else ()
author_part = text[: m.start()].strip()
return tuple(p.strip() for p in author_part.split(",") if p.strip())
def _extract_year(text: str) -> str:
m = _YEAR_RE.search(text)
return m.group(1) if m else ""
def _extract_section(text: str) -> str:
matches = _SECTION_RE.findall(text)
return "; ".join(m.strip() for m in matches) if matches else ""
def _strip_metadata(text: str) -> str:
text = _SECTION_RE.sub("", text)
text = _YEAR_RE.sub("", text)
return _clean(text)
# ---------------------------------------------------------------------------
# Chunk resolution
# ---------------------------------------------------------------------------
@dataclass
class ResolutionMatch:
"""One resolved (citation → surface chunk) candidate."""
citation: Citation
shard_path: str
document_root: str
document_uri: str
document_title: str
chunk_id: int
chunk_idx: int
score: float
snippet: str
# Per-shard cached set of (lowercased title tokens). Reading the
# documents table for every (shard, citation) pair is O(N×M); cache
# the lowered title strings once per shard to bring it to O(N+M).
_SHARD_TITLE_CACHE: dict[str, str] = {}
def _shard_title_haystack(shard_path: str) -> str:
"""Concatenate every non-claim_pack surface document's title +
URI in this shard, lowercased. Cached per shard. Used as the
haystack for citation-token lookup."""
if shard_path in _SHARD_TITLE_CACHE:
return _SHARD_TITLE_CACHE[shard_path]
try:
conn = sqlite3.connect(f"file:{shard_path}?mode=ro", uri=True)
except sqlite3.OperationalError:
_SHARD_TITLE_CACHE[shard_path] = ""
return ""
try:
rows = conn.execute(
"SELECT title, document_uri FROM documents "
"WHERE source_type != 'claim_pack' AND kind = 'surface'"
).fetchall()
finally:
conn.close()
parts = []
for title, uri in rows:
parts.append((title or "").lower())
parts.append((uri or "").lower())
haystack = " ".join(parts)
_SHARD_TITLE_CACHE[shard_path] = haystack
return haystack
def _shard_matches_citation(shard_path: str, citation: Citation) -> bool:
"""True if the shard plausibly contains the cited author's work.
Heuristic: at least one of the citation's author last names
(length >= 4) AND at least one significant title token appear
in the haystack of the shard's non-claim_pack surface document
titles + URIs. Author-only or title-only matches don't count —
"Wikipedia article ABOUT Mendelson" shouldn't be confused with
"Mendelson's textbook".
This produces honest "no match" results for cited textbooks not
ingested in any shard (Mendelson, Enderton, Jech, Goldstein,
Barendregt, etc.) — exactly the records that should stay at
ANCHOR-WARRANTED until those textbooks are surface-ingested.
"""
if citation.is_empty():
return False
haystack = _shard_title_haystack(shard_path)
if not haystack:
return False
author_lasts: list[str] = []
for a in citation.authors:
last = a.split()[-1] if a else ""
# Strip "et al." artifacts
last = re.sub(r"[^A-Za-z]", "", last)
if len(last) >= 4 and last[0].isupper():
author_lasts.append(last.lower())
if not author_lasts:
return False
if not any(a in haystack for a in author_lasts):
return False
# Author hit confirmed; require AT LEAST ONE title token (length
# >= 4) to also appear, ruling out shards that mention the author
# incidentally (e.g., a biography page).
title_terms = [
t.lower() for t in (citation.title or "").split() if len(t) >= 4
]
if not title_terms:
# Compact citations like "Pascal 1654" — author-only is fine.
return True
return any(t in haystack for t in title_terms)
def _fts5_search(
shard_path: str,
query: str,
limit: int = 5,
surface_only: bool = True,
exclude_claim_pack: bool = True,
) -> list[tuple]:
"""FTS5 search against a shard. Returns rows of
``(chunk_id, idx, document_root, document_uri, title, snippet, rank)``.
``exclude_claim_pack`` filters out the very source_type the
resolver is matching FOR — claim-pack records have title +
chunk content rich in the cited keywords (they ARE the
cited material's curriculum-restated form), and an unfiltered
FTS5 match would just keep returning self-matches. The resolver
only cares about EXTERNAL surface chunks.
"""
try:
conn = sqlite3.connect(f"file:{shard_path}?mode=ro", uri=True)
except sqlite3.OperationalError:
return []
try:
clauses = []
if surface_only:
clauses.append("d.kind = 'surface'")
if exclude_claim_pack:
clauses.append("d.source_type != 'claim_pack'")
where_extra = ("AND " + " AND ".join(clauses)) if clauses else ""
rows = conn.execute(
f"""
SELECT c.chunk_id, c.idx, c.document_root, d.document_uri,
d.title, snippet(chunks_fts, 0, '[', ']', '…', 24) AS sn,
chunks_fts.rank
FROM chunks_fts
JOIN chunks c ON chunks_fts.rowid = c.chunk_id
JOIN documents d ON c.document_root = d.document_root
WHERE chunks_fts MATCH ?
{where_extra}
ORDER BY chunks_fts.rank
LIMIT ?
""",
(query, limit),
).fetchall()
return list(rows)
except sqlite3.OperationalError:
return []
finally:
conn.close()
_FTS_STOPWORDS = {"of", "the", "a", "an", "and", "or", "to", "in", "on", "is", "by"}
def _build_fts_query(citation: Citation, theorem_name: str = "") -> str:
"""Compose an FTS5 MATCH query from a citation + theorem name.
Strategy: union of significant tokens from the theorem name plus
author last name. FTS5's default tokenizer indexes lowercase ASCII
words.
"""
parts: list[str] = []
if theorem_name:
toks = [
t for t in re.findall(r"[A-Za-z]{3,}", theorem_name)
if t.lower() not in _FTS_STOPWORDS
]
parts.extend(toks)
for a in citation.authors:
last = a.split()[-1] if a else ""
if last and len(last) >= 4 and last not in parts:
parts.append(last)
if not parts and citation.title:
parts = [t for t in re.findall(r"[A-Za-z]{4,}", citation.title)][:4]
return " OR ".join(p.lower() for p in parts) if parts else ""
def resolve_chunks(
citation: Citation,
shards_dir: Path | str,
theorem_name: str = "",
limit: int = 5,
) -> list[ResolutionMatch]:
"""Search every surface shard under ``shards_dir`` for chunks that
match the citation + theorem-name signal. Returns a ranked list.
Iterates the sibling ``crawl/`` dir's textbook surface shards
(per the post-#000031-Phase-1 layout). The main numbered shards
(000.db, 001.db, …) are excluded — they hold Wikipedia content,
which a textbook-citation resolver shouldn't be matching against;
Wikipedia's "Mendelson" article is *about* Mendelson, not the
cited textbook itself, and including the main shards both bloats
runtime (3.4 M docs per shard) and produces false positives.
"""
if citation.is_empty():
return []
shards_dir = Path(shards_dir).expanduser()
candidate_shards: list[Path] = []
sibling = shards_dir.parent / "crawl"
if sibling.is_dir():
for db in sorted(sibling.glob("*.db")):
# Skip non-textbook crawl shards (e.g., the ad-hoc
# `crawl_russell_ballestrini_net.db` blog crawl).
if db.name.startswith(("crawl_russell_", "qa.", "snapshots.")):
continue
candidate_shards.append(db)
fts_query = _build_fts_query(citation, theorem_name=theorem_name)
if not fts_query:
return []
matches: list[ResolutionMatch] = []
for shard in candidate_shards:
if not _shard_matches_citation(str(shard), citation):
continue
rows = _fts5_search(str(shard), fts_query, limit=limit)
for chunk_id, idx, doc_root, doc_uri, title, snippet, rank in rows:
matches.append(
ResolutionMatch(
citation=citation,
shard_path=str(shard),
document_root=doc_root,
document_uri=doc_uri,
document_title=title or "",
chunk_id=chunk_id,
chunk_idx=idx,
score=-float(rank or 0.0),
snippet=snippet or "",
)
)
matches.sort(key=lambda m: m.score, reverse=True)
return matches[:limit]
# ---------------------------------------------------------------------------
# Merkle inclusion proof
# ---------------------------------------------------------------------------
@dataclass
class ProofBlob:
"""Serializable Merkle inclusion proof for (document_root,
chunk_idx). Deterministic JSON serialization."""
document_root: str
chunk_idx: int
chunk_id: int
leaf_hash: str
siblings: list[dict]
def to_json(self) -> str:
return json.dumps(asdict(self), sort_keys=True)
def compute_proof(
shard_path: str,
document_root: str,
chunk_id: int,
) -> Optional[ProofBlob]:
"""Compute a Merkle inclusion proof for a chunk inside its parent
document's tree. Returns None if the chunk isn't in the shard.
Reads ``merkle_nodes`` for the document_root, walks layer by layer
to assemble siblings. The proof_blob serializes deterministic JSON.
Compatible with the existing ``MerkleProof`` semantics in
``arborist/merkle.py``.
"""
try:
conn = sqlite3.connect(f"file:{shard_path}?mode=ro", uri=True)
except sqlite3.OperationalError:
return None
try:
chunk_row = conn.execute(
"SELECT idx, leaf_hash FROM chunks "
"WHERE chunk_id = ? AND document_root = ?",
(chunk_id, document_root),
).fetchone()
if chunk_row is None:
return None
chunk_idx, leaf_hash = chunk_row
nodes = conn.execute(
"SELECT layer, idx, hash FROM merkle_nodes "
"WHERE document_root = ? "
"ORDER BY layer, idx",
(document_root,),
).fetchall()
if not nodes:
# Single-chunk document — no merkle_nodes row, leaf is root.
return ProofBlob(
document_root=document_root,
chunk_idx=chunk_idx,
chunk_id=chunk_id,
leaf_hash=leaf_hash,
siblings=[],
)
by_layer: dict[int, dict[int, str]] = {}
for layer, idx, hash_ in nodes:
by_layer.setdefault(layer, {})[idx] = hash_
max_layer = max(by_layer)
siblings: list[dict] = []
cur_idx = chunk_idx
for layer in range(max_layer):
layer_map = by_layer.get(layer, {})
if cur_idx % 2 == 0:
sibling_idx = cur_idx + 1
is_left = False
else:
sibling_idx = cur_idx - 1
is_left = True
sibling_hash = layer_map.get(sibling_idx)
if sibling_hash is None:
# Odd-element rule: self-duplicate.
sibling_hash = layer_map.get(cur_idx, "")
is_left = False
siblings.append({"hash": sibling_hash, "is_left": is_left})
cur_idx = cur_idx // 2
return ProofBlob(
document_root=document_root,
chunk_idx=chunk_idx,
chunk_id=chunk_id,
leaf_hash=leaf_hash,
siblings=siblings,
)
finally:
conn.close()
# ---------------------------------------------------------------------------
# Derivations row writer
# ---------------------------------------------------------------------------
WARRANT_PROCESS_ID = "warrant-resolver-v1"
def write_derivation(
db_path: str,
record_root: str,
surface_root: str,
proof_blob: ProofBlob,
process_id: str = WARRANT_PROCESS_ID,
distilled_at: Optional[int] = None,
) -> bool:
"""Insert one derivation row binding a claim-pack record to its
surface chunk via Merkle proof. Idempotent — same (record_root,
surface_root, process_id) PK collides on re-insert with
``INSERT OR IGNORE``; returns True if the row was inserted, False
if it already existed.
Schema reuse note: ``derivations.core_root`` carries the claim-
pack record root here, even though the record's ``kind`` is
``'surface'`` not ``'core'``. The schema FK only requires
existence in ``documents``, not a specific kind. ``process_id``
discriminates this use-case (``warrant-resolver-v1``) from the
existing distillation processes (``tfidf``, ``first_sentence``).
"""
if distilled_at is None:
distilled_at = int(time.time())
conn = sqlite3.connect(db_path)
try:
cur = conn.execute(
"INSERT OR IGNORE INTO derivations "
"(core_root, src_root, proof_blob, process_id, distilled_at) "
"VALUES (?, ?, ?, ?, ?)",
(record_root, surface_root, proof_blob.to_json(), process_id, distilled_at),
)
conn.commit()
return cur.rowcount > 0
finally:
conn.close()
# ---------------------------------------------------------------------------
# Claim-pack record iteration
# ---------------------------------------------------------------------------
# `Source:` field extraction. Claim-pack ingest pipeline canonicalizes
# whitespace runs into single spaces (per `arborist.document.canonicalize`),
# so the multi-line layout of the source bundle collapses into a flat
# string at chunk time. Match `Source: <text>` up to the next field-
# marker keyword or end-of-string.
_SOURCE_RE = re.compile(
r"Source:\s*(.+?)(?=\s+(?:Group|Role|Category|Subfield)\s*:|\s*$)",
re.IGNORECASE | re.DOTALL,
)
def _decompress_chunk(content) -> str:
"""Decompress a chunks.content blob if zstd-compressed; else
decode as UTF-8. Mirrors arborist's chunk-loading convention."""
if isinstance(content, str):
return content
if not isinstance(content, (bytes, bytearray)):
return ""
if len(content) >= 4 and content[:4] == b"\x28\xb5\x2f\xfd":
try:
import zstandard
return zstandard.ZstdDecompressor().decompress(content).decode("utf-8")
except Exception:
return ""
try:
return content.decode("utf-8")
except UnicodeDecodeError:
return ""
def iter_claim_pack_records(
shards_dir: Path | str,
) -> Iterator[tuple[str, str, str, str]]:
"""Yield ``(shard_path, record_root, title, source_reference)`` for
every claim_pack record across the shards-dir.
"""
shards_dir = Path(shards_dir).expanduser()
for db in sorted(shards_dir.glob("*.db")):
if db.name.startswith(("qa.", "snapshots.")):
continue
try:
conn = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
except sqlite3.OperationalError:
continue
try:
for row in conn.execute(
"SELECT d.document_root, d.title, c.content "
"FROM documents d JOIN chunks c ON c.document_root = d.document_root "
"WHERE d.source_type = 'claim_pack' "
"ORDER BY d.ingest_ts"
):
doc_root, title, content = row
text = _decompress_chunk(content)
m = _SOURCE_RE.search(text)
source_ref = m.group(1).strip() if m else ""
yield (str(db), doc_root, title or "", source_ref)
finally:
conn.close()
def _has_derivation(db_path: str, record_root: str) -> bool:
try:
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
except sqlite3.OperationalError:
return False
try:
row = conn.execute(
"SELECT 1 FROM derivations WHERE core_root = ? AND process_id = ? LIMIT 1",
(record_root, WARRANT_PROCESS_ID),
).fetchone()
return row is not None
finally:
conn.close()
# ---------------------------------------------------------------------------
# High-level orchestration
# ---------------------------------------------------------------------------
@dataclass
class WarrantStatus:
"""Per-record summary for the ``warrant-status`` CLI."""
record_root: str
record_title: str
source_reference: str
citations: list[Citation]
matches: list[ResolutionMatch]
has_derivation: bool
def warrant_status(shards_dir: Path | str, limit: int = 5) -> list[WarrantStatus]:
"""Walk every claim-pack record and report what surface chunks the
citation resolver finds. Read-only — no DB writes.
"""
out: list[WarrantStatus] = []
for shard_path, record_root, title, source_ref in iter_claim_pack_records(shards_dir):
citations = parse_citation(source_ref)
all_matches: list[ResolutionMatch] = []
for c in citations:
all_matches.extend(
resolve_chunks(c, shards_dir, theorem_name=title, limit=limit)
)
all_matches.sort(key=lambda m: m.score, reverse=True)
out.append(
WarrantStatus(
record_root=record_root,
record_title=title,
source_reference=source_ref,
citations=citations,
matches=all_matches[:limit],
has_derivation=_has_derivation(shard_path, record_root),
)
)
return out
def warrant_resolve(
shards_dir: Path | str,
write: bool = False,
limit: int = 1,
) -> dict:
"""Run the resolver across every claim-pack record. With
``write=False`` (default) returns a dry-run summary. With
``write=True``, computes Merkle inclusion proofs for top-scoring
matches and writes ``derivations`` rows.
"""
records_total = 0
records_resolved = 0
derivations_written = 0
statuses = warrant_status(shards_dir, limit=limit)
# Build a record_root → shard_path map once so we don't iterate
# again per record.
record_shards: dict[str, str] = {}
for shard_path, record_root, _, _ in iter_claim_pack_records(shards_dir):
record_shards[record_root] = shard_path
for status in statuses:
records_total += 1
if not status.matches:
continue
records_resolved += 1
if not write:
continue
top = status.matches[0]
proof = compute_proof(top.shard_path, top.document_root, top.chunk_id)
if proof is None:
continue
host_shard = record_shards.get(status.record_root)
if host_shard and write_derivation(
host_shard, status.record_root, top.document_root, proof
):
derivations_written += 1
return {
"records_total": records_total,
"records_resolved": records_resolved,
"derivations_written": derivations_written,
"wrote": write,
}

View file

@ -310,11 +310,35 @@ class TextbookTexSource(Source):
_TITLE_RE = re.compile(r"\\title\{([^{}]+)\}")
_AUTHOR_RE = re.compile(r"\\author\{([^{}]+)\}")
_PG_TITLE_RE = re.compile(r"^Title:\s*(.+?)\s*$", re.MULTILINE)
_PG_AUTHOR_RE = re.compile(r"^Author:\s*(.+?)\s*$", re.MULTILINE)
def _extract_title(tex: str) -> str:
"""Pull the title from \\title{...} preamble (best-effort)."""
"""Pull the title — prefer ``\\title{...}`` if present (LaTeX
convention), fall back to ``Title:`` line (Project Gutenberg
boilerplate format which doesn't always wire into a LaTeX
title macro). Append author when discoverable so downstream
citation-matching code (``arborist.qa.warrant_resolver``) can
match on author last name without re-parsing the TeX."""
title = ""
m = _TITLE_RE.search(tex)
if not m:
if m:
title = m.group(1).strip()
if not title:
m = _PG_TITLE_RE.search(tex)
if m:
title = m.group(1).strip()
if not title:
return ""
return m.group(1).strip()
author = ""
m = _AUTHOR_RE.search(tex)
if m:
author = m.group(1).strip()
if not author:
m = _PG_AUTHOR_RE.search(tex)
if m:
author = m.group(1).strip()
if author:
return f"{title} by {author}"
return title

View file

@ -66,7 +66,7 @@ Newest first. Update on every open/close.
| #000034 | Hessian alignment under φ_linear | open · awaiting go/no-go (#000018 follow-up) | 2026-05-09 | — |
| #000033 | Claim-pack pillar VII (combinatorics) | closed · landed 2026-05-09 (live in shard 000.db; lift verified) | 2026-05-09 | — |
| #000032 | combinatorics@v1 π* (integer counting kernel) | closed · landed 2026-05-09 | 2026-05-09 | — |
| #000031 | Surface-ingest cited textbooks for claim-pack warrant promotion | in progress · Phase 1 (8 textbooks, 6/7 pillars) landed 2026-05-09; Phase 2 chunk-resolution ahead | 2026-05-09 | — |
| #000031 | Surface-ingest cited textbooks for claim-pack warrant promotion | in progress · Phase 1 + Phase 2 (chunk-resolver, proof writer, 18 derivations rows for Hilbert pillar IV) landed 2026-05-09; Phase 3 verifier wiring ahead | 2026-05-09 | — |
| #000030 | Math π* expansion: SymPy substrate (algebra · calculus · linalg) | closed · all 7 phases + 1b landed 2026-05-09 (`abe5988`) | 2026-05-09 | — |
| #000029 | Claim-pack source (axiom/theorem JSON bundles) | closed · landed 2026-05-09 | 2026-05-09 | — |
| #000028 | Multi-modality witness for canonical shapes | closed · landed 2026-05-09 + follow-ups (capital ledger · sample rate) | 2026-05-08 | — |

View file

@ -1,6 +1,6 @@
# Ticket #000031 — Surface-ingest cited textbooks for claim-pack warrant promotion
**Status:** in progress · Phase 1 (PD textbook surface ingest) landed 2026-05-09 covering 6/7 g4 pillars; Phase 2 (chunk-resolution + `derivations.proof_blob` warrant promotion) still ahead
**Status:** in progress · Phase 1 (PD textbook surface ingest, 6/7 pillars) + Phase 2 (chunk-resolution + `derivations.proof_blob` warrant writer, Hilbert-axiom records resolved + 18 derivations rows written) both landed 2026-05-09; Phase 3 (verifier wiring to upgrade audit_mode from ANCHOR → EVIDENCE-WARRANTED on the four-rung ladder) still ahead
**Opened:** 2026-05-09
**Scope:** Ingest the classical textbooks cited by every record in
the claim-pack source (`#000029`) — Mendelson 1997, Enderton 2001,
@ -386,27 +386,71 @@ tests; full suite: 1574 passed / 28 skipped.
yellow-light pending license decision per §2.1. Manifest entry
kept as a license-fail placeholder.
### Phase 2 — chunk-resolution + warrant promotion (NOT YET LANDED)
### Phase 2 — chunk-resolution + warrant writer (LANDED 2026-05-09)
The data substrate is in place; the warrant promotion itself
requires:
Implemented in `arborist/qa/warrant_resolver.py`:
1. **Citation parser** — `source_reference` string ("A
Mathematical Introduction to Logic by Herbert B. Enderton")
→ structured `(authors, title, year, section?)` tuple.
2. **Chunk resolver** — given a citation, FTS5-search candidate
surface shards for theorem name + key terms, return top
chunk(s).
3. **Merkle inclusion proof writer** — for each (claim_pack
record, surface chunk) match, compute the per-chunk inclusion
proof using `arborist/merkle.py`, serialize to JSON.
4. **`derivations` row writer** — `(core_root,
src_root, proof_blob, process_id="warrant-resolver-v1",
distilled_at)`.
5. **Verifier wiring** — `audit_mode` upgrade path so claims
with a `derivations` row tying back to a real surface get
**EVIDENCE-WARRANTED** instead of ANCHOR-WARRANTED on the
four-rung ladder.
1. **Citation parser** (`parse_citation`) — pure regex pipeline.
Handles three observed source-reference patterns: ``"Title by
Author"`` (single + multi-author Oxford comma + ``et al.``
tail), semicolon-separated multi-citation, and compact
``"Author Year"`` / ``"Author §section"`` form. 14 unit tests.
2. **Chunk resolver** (`resolve_chunks`) — searches sibling
``crawl/`` dir's textbook-surface shards (skipping the main
numbered shards which hold Wikipedia content). Per-shard
``_shard_matches_citation`` filter requires both author last
name AND a title token to appear in the shard's title-haystack
— produces honest "no match" for cited textbooks not yet
surface-ingested, exactly the records that should stay at
ANCHOR-WARRANTED.
3. **Merkle inclusion proof writer** (`compute_proof`) — reads
``merkle_nodes`` for the document_root, walks layer by layer
to assemble siblings; emits deterministic JSON. Compatible
with the existing ``arborist/merkle.py`` proof verification.
4. **derivations row writer** (`write_derivation`) — INSERT OR
IGNORE into the existing schema-table with
``process_id="warrant-resolver-v1"``. Idempotent at the
database layer (PK is `(core_root, src_root, process_id)`).
5. **CLI surface** — ``arborist warrant-status`` (read-only,
shows resolver matches per record as JSON) +
``arborist warrant-resolve [--write]`` (default dry-run
summary; ``--write`` actually computes proofs + inserts rows).
**Verified end-to-end** against the real shard cluster:
``arborist warrant-resolve --shards-dir ~/.arborist/shards
--write`` resolved **18 of 92 claim-pack records** (all the
Hilbert-pillar-IV axiom records that cite *The Foundations of
Geometry by David Hilbert* — the only cited textbook fully
surface-ingested in this iteration). 18 ``derivations`` rows
written, each carrying a valid Merkle proof blob. Re-running
the writer is a no-op (idempotent by PK).
The remaining 74 records cite textbooks not in our shard cluster
(Mendelson, Enderton, Jech, Goldstein, Barendregt, Stanley,
Brualdi, Knuth, …) — they correctly produce 0 matches. Adding
those textbooks (where licensing permits) is a future-ticket
expansion of Phase 1's manifest, not new code.
### Phase 3 — verifier wiring (NOT YET LANDED)
The data substrate is now in place. The four-rung-ladder upgrade
that lifts answers citing claim-pack-records-with-derivations
from ANCHOR-WARRANTED to **EVIDENCE-WARRANTED** still needs to
happen in the verifier. Sketch:
- The Q&A verifier currently produces `audit_mode` + `violations`
per answer. The four-rung ladder is computed in
`arborist.cli._render_audit_label._ladder_rung_for_lattice`
from those two.
- For each cited claim-pack record, look up whether it has a
``derivations.process_id='warrant-resolver-v1'`` row. If yes,
REMOVE any `WARRANT_MISSING` violation it would have triggered
(or add a positive `WARRANT_PROVEN` annotation).
- Display label upgrades to EVIDENCE-WARRANTED automatically per
the existing ladder logic.
This is a verifier change — touches well-tested code. Worth its
own ticket so the regression risk is bounded.
6. **Bench** — verify a known case (e.g., Modus Tollens
claim-pack record → Aristotle Prior Analytics chapter)
actually promotes from ANCHOR to EVIDENCE.

View file

@ -0,0 +1,141 @@
"""Tests for the warrant resolver (#000031 Phase 2).
Pure unit tests over the citation parser; the FTS5 resolver +
Merkle proof writer are exercised end-to-end via the
``arborist warrant-resolve`` CLI on real shards. The CLI smoke test
is documented in the ticket; this file stays offline / DB-free.
"""
from __future__ import annotations
import pytest
from arborist.qa.warrant_resolver import (
Citation,
parse_citation,
)
# --- "Title by Author" pattern (most common) -------------------------
def test_simple_title_by_author():
out = parse_citation("Introduction to Mathematical Logic by Elliott Mendelson")
assert len(out) == 1
c = out[0]
assert c.title == "Introduction to Mathematical Logic"
assert c.authors == ("Elliott Mendelson",)
def test_title_with_punctuation():
out = parse_citation("The Lambda Calculus: Its Syntax and Semantics by H.P. Barendregt")
assert len(out) == 1
c = out[0]
assert c.title == "The Lambda Calculus: Its Syntax and Semantics"
assert c.authors == ("H.P. Barendregt",)
# --- multi-author Oxford comma --------------------------------------
def test_multi_author_oxford_comma():
out = parse_citation(
"Classical Mechanics by Herbert Goldstein, Charles P. Poole, and John L. Safko"
)
assert len(out) == 1
c = out[0]
assert c.title == "Classical Mechanics"
assert c.authors == ("Herbert Goldstein", "Charles P. Poole", "John L. Safko")
def test_multi_author_et_al():
out = parse_citation("Classical Mechanics by Herbert Goldstein et al.")
assert len(out) == 1
c = out[0]
assert c.title == "Classical Mechanics"
assert c.authors == ("Herbert Goldstein",)
# --- semicolon-separated multi-citation -----------------------------
def test_semicolon_split():
out = parse_citation(
"Knuth TAOCP Volume 1 §1.2.6; Stanley §1.2; Brualdi §3.5"
)
assert len(out) == 3
assert out[0].authors == ("Knuth",)
assert out[1].authors == ("Stanley",)
assert out[2].authors == ("Brualdi",)
def test_semicolon_with_year_and_section():
out = parse_citation(
"Knuth TAOCP Volume 1 §1.2.6 equation (13); Vandermonde 1772"
)
assert len(out) == 2
assert out[0].section
assert "§1.2.6" in out[0].section
assert out[1].year == "1772"
# --- compact form ---------------------------------------------------
def test_compact_year_form():
out = parse_citation("Pascal 1654")
assert len(out) == 1
c = out[0]
assert c.year == "1654"
assert "Pascal" in c.authors
def test_compact_section_only():
out = parse_citation("Brualdi §3.5")
assert len(out) == 1
c = out[0]
assert c.authors == ("Brualdi",)
assert "§3.5" in c.section
# --- edge cases ------------------------------------------------------
def test_empty_input():
assert parse_citation("") == []
assert parse_citation(" ") == []
def test_year_extraction():
out = parse_citation("Foundations of Probability by Andrey Kolmogorov 1933")
assert len(out) == 1
assert out[0].year == "1933"
def test_section_extraction():
out = parse_citation("Classical Mechanics by Goldstein Volume 1 §3.2")
assert len(out) == 1
assert "§3.2" in out[0].section
assert "Volume 1" in out[0].section
def test_oeis_identifier():
out = parse_citation("OEIS A000108")
assert len(out) == 1
# OEIS identifiers parse as compact form — first token is "author".
assert out[0].authors == ("OEIS",)
# --- Citation dataclass invariants ----------------------------------
def test_citation_is_empty():
assert Citation().is_empty()
assert not Citation(title="Foo").is_empty()
assert not Citation(authors=("Bar",)).is_empty()
def test_raw_field_preserved():
raw = "Some weird citation by Some Author"
out = parse_citation(raw)
assert out[0].raw == raw