aborist/arborist

modified:   .gitlab-ci.yml
	modified:   bench/qa_questions.txt
	modified:   bench/qa_sweep.py
	modified:   bench/run.sh
	modified:   docs/TICKETS.md
	modified:   docs/_source/README.md
	modified:   docs/_source/_ext/makefile_targets.py
	modified:   docs/_source/api/cli.rst
	modified:   docs/_source/api/distill.rst
	modified:   docs/_source/api/mesh.rst
	modified:   docs/_source/api/qa.rst
	modified:   docs/_source/api/retrieval.rst
	modified:   docs/_source/api/storage.rst
	modified:   docs/_source/api/substrate.rst
	modified:   docs/_source/concepts.rst
	modified:   docs/_source/conf.py
	modified:   docs/_source/cookbook.rst
	modified:   docs/_source/index.rst
	modified:   docs/_source/license.rst
	modified:   docs/_source/quickstart.rst
	modified:   docs/bench-maxing.md
	modified:   docs/benchmarks.md
	modified:   docs/cti-architecture.md
	modified:   docs/diagrams/aborist-modules.dot
	modified:   docs/diagrams/aborist-modules.svg
	modified:   docs/diagrams/mesh-data-flow.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.dot
	modified:   docs/diagrams/mesh-epoch-lifecycle.svg
	modified:   docs/diagrams/mesh-group-decisions.dot
	modified:   docs/diagrams/mesh-group-decisions.svg
	modified:   docs/diagrams/mesh-identity-stack.dot
	modified:   docs/diagrams/mesh-secret-envelope.dot
	modified:   docs/mesh.md
	modified:   docs/qa-modes-bench.md
	modified:   docs/seven-point-program.md
	modified:   docs/tickets/ticket-000001-retrieval-keywords-audit-gap.md
	modified:   docs/tickets/ticket-000002-reference-frame-polarity-contract.md
	modified:   docs/tickets/ticket-000003-anchor-class-warrant.md
	modified:   docs/tickets/ticket-000005-label-ladder-migration.md
	modified:   docs/tickets/ticket-000006-bench-emergent-findings.md
	modified:   docs/tickets/ticket-000007-query-layer-hyphen-fold.md
	modified:   docs/tickets/ticket-000008-broad-quantifier-preflight-guard.md
	modified:   docs/tickets/ticket-000009-quantifier-preflight-dag-binding.md
	modified:   docs/tickets/ticket-000010-metacognition-preflight-guard.md
	modified:   docs/tickets/ticket-000011-soft-preflight-hint-sidecar.md
	modified:   scripts/backfill_concepts.py
	modified:   scripts/bench_emergent.py
	modified:   tests/crawler/test_async_web_fetcher.py
	modified:   tests/crawler/test_bridge.py
	modified:   tests/crawler/test_web_fetch.py
	modified:   tests/test_bench_qa_sweep.py
	modified:   tests/test_burn.py
	modified:   tests/test_burn_doc.py
	modified:   tests/test_claim_lattice.py
	modified:   tests/test_cli_render.py
	modified:   tests/test_compress.py
	modified:   tests/test_concepts.py
	modified:   tests/test_dag.py
	modified:   tests/test_directives.py
	modified:   tests/test_distill.py
	modified:   tests/test_distill_recursive.py
	modified:   tests/test_evict.py
	modified:   tests/test_frame.py
	modified:   tests/test_grok_source.py
	modified:   tests/test_html_source.py
	modified:   tests/test_ingest.py
	modified:   tests/test_inspect.py
	modified:   tests/test_journal.py
	modified:   tests/test_keys.py
	modified:   tests/test_llm_context_base.py
	modified:   tests/test_merkle.py
	modified:   tests/test_mesh.py
	modified:   tests/test_mesh_aead.py
	modified:   tests/test_mesh_chain.py
	modified:   tests/test_mesh_cli.py
	modified:   tests/test_mesh_cli_pull.py
	modified:   tests/test_mesh_wire.py
	modified:   tests/test_mesh_wire_e2e.py
	modified:   tests/test_metacognition.py
	modified:   tests/test_migration_audit_mode.py
	modified:   tests/test_providence_source.py
	modified:   tests/test_qa.py
	modified:   tests/test_qa_quality_live.py
	modified:   tests/test_quantifier_caps.py
	modified:   tests/test_quantifier_classifier.py
	modified:   tests/test_quantifier_phase4.py
	modified:   tests/test_quantifier_reminder.py
	modified:   tests/test_query.py
	modified:   tests/test_reclassify.py
	modified:   tests/test_repair.py
	modified:   tests/test_resume.py
	modified:   tests/test_snapshot.py
	modified:   tests/test_soft_preflight.py
	modified:   tests/test_tfidf.py
	modified:   tests/test_vcs_source.py
	modified:   tests/test_verify.py
	modified:   tests/test_verify_json.py
	modified:   tests/test_versioned_ingest.py
	modified:   tests/test_warrant.py
	modified:   tests/test_wikipedia_old.py
	modified:   tests/test_wikipedia_xml.py
	modified:   tests/test_wikitext.py
This commit is contained in:
russell@unturf.com 2026-05-07 09:31:49 -04:00
parent 2c9d00ac95
commit 8d6961fcc1
No known key found for this signature in database
102 changed files with 716 additions and 716 deletions

View file

@ -1,4 +1,4 @@
# GitLab CI for aborist. # GitLab CI for arborist.
# #
# Runs the unit test suite on every push. Lives at the same shape as # Runs the unit test suite on every push. Lives at the same shape as
# the sibling repos (`unsandbox.com`, `unfirehose-nextjs-logger`): # the sibling repos (`unsandbox.com`, `unfirehose-nextjs-logger`):
@ -7,7 +7,7 @@
# #
# What's NOT in CI: # What's NOT in CI:
# - `make test-live` — needs the live Hermes endpoint + populated # - `make test-live` — needs the live Hermes endpoint + populated
# shards under ~/.aborist/shards. Run by hand via `make test-live` # shards under ~/.arborist/shards. Run by hand via `make test-live`
# when iterating on QA quality. # when iterating on QA quality.
# - `make test-crawler` — needs `[crawler]` extras + network access # - `make test-crawler` — needs `[crawler]` extras + network access
# to real HTML sites. Opt-in via `make test-crawler` locally. # to real HTML sites. Opt-in via `make test-crawler` locally.

View file

@ -1,4 +1,4 @@
# aborist QA-quality benchmark question set. # arborist QA-quality benchmark question set.
# #
# One question per line. `#`-prefixed lines and blank lines ignored. # One question per line. `#`-prefixed lines and blank lines ignored.
# Designed for the 2003-05-16 Wikipedia cur snapshot — questions must # Designed for the 2003-05-16 Wikipedia cur snapshot — questions must

View file

@ -1,6 +1,6 @@
"""QA-quality benchmark sweep. """QA-quality benchmark sweep.
Runs a fixed question set through ``aborist.qa.query.query`` under each Runs a fixed question set through ``arborist.qa.query.query`` under each
answer mode and tabulates STRICT / HYBRID / UNGROUNDED counts, the answer mode and tabulates STRICT / HYBRID / UNGROUNDED counts, the
``n_verified / n_quotes`` ratio, latency, and lazy-anchor signals. ``n_verified / n_quotes`` ratio, latency, and lazy-anchor signals.
@ -36,7 +36,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
from threading import Lock from threading import Lock
# Defer aborist imports until argparse runs so `--help` works without # Defer arborist imports until argparse runs so `--help` works without
# the package installed. # the package installed.
ANSWER_MODES = ("quote", "claim_lattice_pointer", "claim_lattice") ANSWER_MODES = ("quote", "claim_lattice_pointer", "claim_lattice")
@ -134,19 +134,19 @@ def _run_one(
can flip individual fields for an A/B cycle without touching can flip individual fields for an A/B cycle without touching
the policy defaults. the policy defaults.
""" """
from aborist.qa.client import OpenAICompatibleClient from arborist.qa.client import OpenAICompatibleClient
from aborist.qa.query import DEFAULT_QUERY_POLICY, query from arborist.qa.query import DEFAULT_QUERY_POLICY, query
policy = dict(DEFAULT_QUERY_POLICY) policy = dict(DEFAULT_QUERY_POLICY)
policy["answer_mode"] = answer_mode policy["answer_mode"] = answer_mode
if policy_overrides: if policy_overrides:
policy.update(policy_overrides) policy.update(policy_overrides)
api_key = os.environ.get("ABORIST_LLM_API_KEY") api_key = os.environ.get("ARBORIST_LLM_API_KEY")
client = OpenAICompatibleClient(base_url=endpoint, api_key=api_key) client = OpenAICompatibleClient(base_url=endpoint, api_key=api_key)
if qa_db is None: if qa_db is None:
qa_db = (shards_dir / "qa.db") if shards_dir else (Path.home() / ".aborist" / "qa.db") qa_db = (shards_dir / "qa.db") if shards_dir else (Path.home() / ".arborist" / "qa.db")
t0 = time.monotonic() t0 = time.monotonic()
err: str | None = None err: str | None = None
@ -171,7 +171,7 @@ def _run_one(
# never mentions the question's subject) at bench-aggregate # never mentions the question's subject) at bench-aggregate
# scale so a creeping "model deflects rather than refuses" # scale so a creeping "model deflects rather than refuses"
# regression is legible across runs. # regression is legible across runs.
from aborist.qa.inspect import diagnose_deflection, diagnose_metaphor_deflection from arborist.qa.inspect import diagnose_deflection, diagnose_metaphor_deflection
deflection = diagnose_deflection(question, result.get("answer_text") or "") deflection = diagnose_deflection(question, result.get("answer_text") or "")
metaphor = diagnose_metaphor_deflection( metaphor = diagnose_metaphor_deflection(
question, result.get("answer_text") or "" question, result.get("answer_text") or ""
@ -493,7 +493,7 @@ def _render_markdown(
) -> str: ) -> str:
lines: list[str] = [] lines: list[str] = []
n_runs = len(rows) n_runs = len(rows)
lines.append(f"# aborist QA-quality benchmark — {started_utc}") lines.append(f"# arborist QA-quality benchmark — {started_utc}")
lines.append("") lines.append("")
lines.append( lines.append(
f"questions: {len(questions)} · modes: {len(modes)} · samples per cell: " f"questions: {len(questions)} · modes: {len(modes)} · samples per cell: "
@ -730,7 +730,7 @@ def _render_markdown(
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__) ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--questions", type=Path, default=Path("bench/qa_questions.txt")) ap.add_argument("--questions", type=Path, default=Path("bench/qa_questions.txt"))
ap.add_argument("--shards-dir", type=Path, default=Path.home() / ".aborist" / "shards") ap.add_argument("--shards-dir", type=Path, default=Path.home() / ".arborist" / "shards")
ap.add_argument("--qa-db", type=Path, default=None) ap.add_argument("--qa-db", type=Path, default=None)
ap.add_argument("--out-dir", type=Path, default=Path("bench/qa_results")) ap.add_argument("--out-dir", type=Path, default=Path("bench/qa_results"))
ap.add_argument("--top-k", type=int, default=8) ap.add_argument("--top-k", type=int, default=8)
@ -765,9 +765,9 @@ def main(argv: list[str] | None = None) -> int:
"re-rendered from the union of pre-existing + " "re-rendered from the union of pre-existing + "
"new rows. Stop/start-able bench.") "new rows. Stop/start-able bench.")
ap.add_argument("--endpoint", default=os.environ.get( ap.add_argument("--endpoint", default=os.environ.get(
"ABORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1")) "ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"))
ap.add_argument("--model", default=os.environ.get( ap.add_argument("--model", default=os.environ.get(
"ABORIST_LLM_MODEL", "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic")) "ARBORIST_LLM_MODEL", "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"))
ap.add_argument( ap.add_argument(
"--policy", action="append", default=None, metavar="KEY=VALUE", "--policy", action="append", default=None, metavar="KEY=VALUE",
help=( help=(

View file

@ -12,10 +12,10 @@
set -uo pipefail set -uo pipefail
BENCH_DOCS=${1:-5000} BENCH_DOCS=${1:-5000}
BENCH_DIR=${BENCH_DIR:-/tmp/aborist-bench} BENCH_DIR=${BENCH_DIR:-/tmp/arborist-bench}
SHARDS=${SHARDS:-4} SHARDS=${SHARDS:-4}
DUMP=${DUMP:-data/20030516_cur_tablesql.bz2} DUMP=${DUMP:-data/20030516_cur_tablesql.bz2}
ABORIST=${ABORIST:-.venv/bin/aborist} ARBORIST=${ARBORIST:-.venv/bin/arborist}
mkdir -p "$BENCH_DIR" mkdir -p "$BENCH_DIR"
@ -40,7 +40,7 @@ bench_serial() {
local db=$BENCH_DIR/serial.db local db=$BENCH_DIR/serial.db
rm -f "$db" "$db-"* rm -f "$db" "$db-"*
local wall local wall
wall=$(run_silent "$ABORIST" --db "$db" ingest --quiet \ wall=$(run_silent "$ARBORIST" --db "$db" ingest --quiet \
--source wikipedia_cur --path "$DUMP" --limit "$BENCH_DOCS") --source wikipedia_cur --path "$DUMP" --limit "$BENCH_DOCS")
local docs local docs
docs=$(sqlite3 "$db" "SELECT COUNT(*) FROM documents") docs=$(sqlite3 "$db" "SELECT COUNT(*) FROM documents")
@ -54,7 +54,7 @@ bench_parallel_shared() {
local start end wall local start end wall
start=$(date +%s.%N) start=$(date +%s.%N)
for i in $(seq 0 $((SHARDS - 1))); do for i in $(seq 0 $((SHARDS - 1))); do
"$ABORIST" --db "$db" ingest --quiet \ "$ARBORIST" --db "$db" ingest --quiet \
--source wikipedia_cur --path "$DUMP" \ --source wikipedia_cur --path "$DUMP" \
--shard "$i/$SHARDS" --limit "$per" >/dev/null 2>&1 & --shard "$i/$SHARDS" --limit "$per" >/dev/null 2>&1 &
done done
@ -74,7 +74,7 @@ bench_attached() {
local start end wall local start end wall
start=$(date +%s.%N) start=$(date +%s.%N)
for i in $(seq 0 $((SHARDS - 1))); do for i in $(seq 0 $((SHARDS - 1))); do
"$ABORIST" ingest --quiet \ "$ARBORIST" ingest --quiet \
--source wikipedia_cur --path "$DUMP" \ --source wikipedia_cur --path "$DUMP" \
--shards-dir "$dir" --shard "$i/$SHARDS" --limit "$per" >/dev/null 2>&1 & --shards-dir "$dir" --shard "$i/$SHARDS" --limit "$per" >/dev/null 2>&1 &
done done
@ -82,12 +82,12 @@ bench_attached() {
end=$(date +%s.%N) end=$(date +%s.%N)
wall=$(echo "$end - $start" | bc -l) wall=$(echo "$end - $start" | bc -l)
local docs local docs
docs=$("$ABORIST" --shards-dir "$dir" stats 2>/dev/null \ docs=$("$ARBORIST" --shards-dir "$dir" stats 2>/dev/null \
| python3 -c "import json,sys; print(json.load(sys.stdin)['documents_total'])") | python3 -c "import json,sys; print(json.load(sys.stdin)['documents_total'])")
printf "attached,%s,%s\n" "$wall" "$docs" printf "attached,%s,%s\n" "$wall" "$docs"
} }
echo "=== aborist ETL benchmark — $BENCH_DOCS docs target, $SHARDS shards ===" echo "=== arborist ETL benchmark — $BENCH_DOCS docs target, $SHARDS shards ==="
echo echo
results+=("$(bench_serial)") results+=("$(bench_serial)")

View file

@ -1,6 +1,6 @@
# Aborist tickets — index # Arborist tickets — index
Aborist uses a flat `ticket-NNNNNN-<slug>.md` convention for actionable Arborist uses a flat `ticket-NNNNNN-<slug>.md` convention for actionable
proposals: defects with a known root cause, design proposals awaiting proposals: defects with a known root cause, design proposals awaiting
go/no-go, or scoped enhancements queued for future implementation. go/no-go, or scoped enhancements queued for future implementation.

View file

@ -1,4 +1,4 @@
# Aborist API Reference (Sphinx) # Arborist API Reference (Sphinx)
This directory contains Sphinx configuration to generate API documentation from docstrings. This directory contains Sphinx configuration to generate API documentation from docstrings.

View file

@ -139,7 +139,7 @@ def generate_rst(all_targets: dict[str, str], output_path: Path) -> None:
"Makefile reference", "Makefile reference",
"==================", "==================",
"", "",
"Every aborist workflow lives behind a ``make`` target. This page is", "Every arborist workflow lives behind a ``make`` target. This page is",
"auto-generated from the project ``Makefile``'s ``## description``", "auto-generated from the project ``Makefile``'s ``## description``",
"annotations at Sphinx build time, so it stays in sync with the source.", "annotations at Sphinx build time, so it stays in sync with the source.",
"", "",

View file

@ -1,8 +1,8 @@
CLI: Command-line interface CLI: Command-line interface
============================ ============================
Entry point for all aborist operations. Entry point for all arborist operations.
.. automodule:: aborist.cli .. automodule:: arborist.cli
:members: :members:
:undoc-members: :undoc-members:

View file

@ -3,7 +3,7 @@ Distillation: surface → core compression
Surface-to-core extraction and recursive distillation. Surface-to-core extraction and recursive distillation.
.. automodule:: aborist.distill .. automodule:: arborist.distill
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View file

@ -1,9 +1,9 @@
Federation: multiplayer aborist Federation: multiplayer arborist
================================ ================================
Gossip-based mesh for cross-peer data sharing. Gossip-based mesh for cross-peer data sharing.
.. automodule:: aborist.mesh .. automodule:: arborist.mesh
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View file

@ -6,62 +6,62 @@ Question answering, caching, verification, evidence mapping.
keys keys
---- ----
.. automodule:: aborist.qa.keys .. automodule:: arborist.qa.keys
:members: :members:
:undoc-members: :undoc-members:
runner runner
------ ------
.. automodule:: aborist.qa.runner .. automodule:: arborist.qa.runner
:members: :members:
:undoc-members: :undoc-members:
query query
----- -----
.. automodule:: aborist.qa.query .. automodule:: arborist.qa.query
:members: :members:
:undoc-members: :undoc-members:
verify verify
------ ------
.. automodule:: aborist.qa.verify .. automodule:: arborist.qa.verify
:members: :members:
:undoc-members: :undoc-members:
evidence evidence
-------- --------
.. automodule:: aborist.qa.evidence .. automodule:: arborist.qa.evidence
:members: :members:
:undoc-members: :undoc-members:
quantifier quantifier
---------- ----------
.. automodule:: aborist.qa.quantifier .. automodule:: arborist.qa.quantifier
:members: :members:
:undoc-members: :undoc-members:
metacognition metacognition
------------- -------------
.. automodule:: aborist.qa.metacognition .. automodule:: arborist.qa.metacognition
:members: :members:
:undoc-members: :undoc-members:
dag dag
--- ---
.. automodule:: aborist.qa.dag .. automodule:: arborist.qa.dag
:members: :members:
:undoc-members: :undoc-members:
client client
------ ------
.. automodule:: aborist.qa.client .. automodule:: arborist.qa.client
:members: :members:
:undoc-members: :undoc-members:

View file

@ -6,7 +6,7 @@ Full-text search, concept relations (synonym/rivalry overlay).
search search
------ ------
.. automodule:: aborist.search .. automodule:: arborist.search
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
@ -14,22 +14,22 @@ search
sources sources
------- -------
.. automodule:: aborist.source .. automodule:: arborist.source
:members: :members:
:undoc-members: :undoc-members:
.. automodule:: aborist.sources.wikipedia .. automodule:: arborist.sources.wikipedia
:members: :members:
:undoc-members: :undoc-members:
.. automodule:: aborist.sources.html_page .. automodule:: arborist.sources.html_page
:members: :members:
:undoc-members: :undoc-members:
concepts concepts
-------- --------
.. automodule:: aborist.concepts .. automodule:: arborist.concepts
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:

View file

@ -6,7 +6,7 @@ v9.8 SQLite schema, audit chain, and cross-shard views.
store store
----- -----
.. automodule:: aborist.store .. automodule:: arborist.store
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
@ -14,7 +14,7 @@ store
ingest ingest
------ ------
.. automodule:: aborist.ingest .. automodule:: arborist.ingest
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
@ -22,6 +22,6 @@ ingest
evict evict
----- -----
.. automodule:: aborist.evict .. automodule:: arborist.evict
:members: :members:
:undoc-members: :undoc-members:

View file

@ -6,7 +6,7 @@ Pure Merkle tree and document primitives.
merkle merkle
------ ------
.. automodule:: aborist.merkle .. automodule:: arborist.merkle
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
@ -14,7 +14,7 @@ merkle
document document
-------- --------
.. automodule:: aborist.document .. automodule:: arborist.document
:members: :members:
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
@ -22,6 +22,6 @@ document
wikitext wikitext
-------- --------
.. automodule:: aborist.wikitext .. automodule:: arborist.wikitext
:members: :members:
:undoc-members: :undoc-members:

View file

@ -1,12 +1,12 @@
Concepts Concepts
======== ========
Aborist is a content-addressed document store that gives every cached Arborist is a content-addressed document store that gives every cached
LLM answer a verifiable Merkle proof tying it back to its source LLM answer a verifiable Merkle proof tying it back to its source
documents. This page is the orientation: what the system is, the documents. This page is the orientation: what the system is, the
core abstractions you'll see in code and docs, and how they compose. core abstractions you'll see in code and docs, and how they compose.
What aborist is What arborist is
--------------- ---------------
A reference implementation of two papers stacked: A reference implementation of two papers stacked:
@ -26,8 +26,8 @@ Three layers stacked on one SQLite file (per shard):
- **Providence cache** — Q&A records keyed on the v9.8 8-dimension - **Providence cache** — Q&A records keyed on the v9.8 8-dimension
invariant; every record carries an audit_mode and a Merkle proof. invariant; every record carries an audit_mode and a Merkle proof.
.. figure:: diagrams/aborist-modules.svg .. figure:: diagrams/arborist-modules.svg
:alt: Aborist module graph :alt: Arborist module graph
:width: 100% :width: 100%
Top-level module graph. Substrate (merkle, document) at the bottom; Top-level module graph. Substrate (merkle, document) at the bottom;
@ -41,7 +41,7 @@ canonicalization compute **bit-identical** ``document_root`` hashes.
That is the v9.8 admissibility property: identity by content, not by That is the v9.8 admissibility property: identity by content, not by
location. location.
Aborist uses fox's existing Go Merkle conventions verbatim Arborist uses fox's existing Go Merkle conventions verbatim
(``proxy.unturf.com/pkg/verified/merkle.go``): (``proxy.unturf.com/pkg/verified/merkle.go``):
- **Leaf hash:** ``sha256(0x00 || canonical_chunk_bytes)``. - **Leaf hash:** ``sha256(0x00 || canonical_chunk_bytes)``.
@ -78,7 +78,7 @@ with ``event_hash = sha256(prev_event_hash || canonical(body))``.
Linear chain per shard, verified by ``make chain-check-shards`` (any Linear chain per shard, verified by ``make chain-check-shards`` (any
break is the loudest possible signal). break is the loudest possible signal).
Always write via ``aborist.store.append_audit`` — never insert into Always write via ``arborist.store.append_audit`` — never insert into
``audit_events`` directly. ``audit_events`` directly.
The trichotomy and the four-rung ladder The trichotomy and the four-rung ladder
@ -168,7 +168,7 @@ framing, and **never write to providence_cache or extend the audit
chain**. That invariant is what keeps ``audit_mode`` a binary chain**. That invariant is what keeps ``audit_mode`` a binary
classification rather than a soft score. classification rather than a soft score.
See :doc:`api/qa` (``aborist.qa.inspect``). See :doc:`api/qa` (``arborist.qa.inspect``).
Where to go next Where to go next
---------------- ----------------

View file

@ -1,15 +1,15 @@
"""Sphinx configuration for Aborist API reference.""" """Sphinx configuration for Arborist API reference."""
import sys import sys
from pathlib import Path from pathlib import Path
# Add aborist package + local _ext (Sphinx extensions) to path # Add arborist package + local _ext (Sphinx extensions) to path
project_root = Path(__file__).parent.parent.parent project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
sys.path.insert(0, str(Path(__file__).parent / "_ext")) sys.path.insert(0, str(Path(__file__).parent / "_ext"))
# Project info # Project info
project = "Aborist" project = "Arborist"
copyright = "2026, agent blackops" copyright = "2026, agent blackops"
author = "fox/timehexon" author = "fox/timehexon"
version = "9.8" version = "9.8"
@ -37,7 +37,7 @@ napoleon_attr_annotations = True
# HTML output # HTML output
html_theme = "sphinx_book_theme" html_theme = "sphinx_book_theme"
html_static_path = ["_static"] html_static_path = ["_static"]
html_title = "Aborist API Reference" html_title = "Arborist API Reference"
# Standard sphinx-book-theme layout: full project TOC on the left, # Standard sphinx-book-theme layout: full project TOC on the left,
# 'On this page' (current page sections) on the right. show_toc_level=3 # 'On this page' (current page sections) on the right. show_toc_level=3

View file

@ -2,8 +2,8 @@ Cookbook
======== ========
Recipes for common workflows beyond the quickstart. Each starts from Recipes for common workflows beyond the quickstart. Each starts from
a working aborist install (``make bootstrap`` already run) and a a working arborist install (``make bootstrap`` already run) and a
populated shards directory under ``~/.aborist/shards/``. populated shards directory under ``~/.arborist/shards/``.
Re-crawl a website to detect changes Re-crawl a website to detect changes
------------------------------------- -------------------------------------
@ -56,7 +56,7 @@ time to fail. See :doc:`api/qa` for the ``ProvidenceSource`` impl.
Query across mixed corpora Query across mixed corpora
-------------------------- --------------------------
Every shard under ``~/.aborist/shards/`` is queried automatically. Every shard under ``~/.arborist/shards/`` is queried automatically.
Mix Wikipedia, your Grok export, a crawled site, and your own git Mix Wikipedia, your Grok export, a crawled site, and your own git
repos in one query — retrieval ranks across all of them. repos in one query — retrieval ranks across all of them.
@ -81,9 +81,9 @@ no auth). Point at any OpenAI-compatible endpoint via env:
.. code-block:: sh .. code-block:: sh
export ABORIST_LLM_ENDPOINT="https://your-vllm.example.com/v1" export ARBORIST_LLM_ENDPOINT="https://your-vllm.example.com/v1"
export ABORIST_LLM_MODEL="meta-llama/Llama-3.1-70B-Instruct" export ARBORIST_LLM_MODEL="meta-llama/Llama-3.1-70B-Instruct"
export ABORIST_LLM_API_KEY="..." # optional; many vLLM deploys are open export ARBORIST_LLM_API_KEY="..." # optional; many vLLM deploys are open
make query Q="..." make query Q="..."
The model id folds into ``model_profile_hash`` (one of the 8 cache The model id folds into ``model_profile_hash`` (one of the 8 cache
@ -140,4 +140,4 @@ keywords without changing what the LLM sees as the question:
make query Q="What did Orwell mean by always at war?" K="1984 Oceania Eastasia" make query Q="What did Orwell mean by always at war?" K="1984 Oceania Eastasia"
Provenance gap on this is tracked in Provenance gap on this is tracked in
:doc:`api/qa` (``aborist.qa.query``). :doc:`api/qa` (``arborist.qa.query``).

View file

@ -1,4 +1,4 @@
Aborist API Reference Arborist API Reference
===================== =====================
Generated from docstrings. Replaces the static modules.md. Generated from docstrings. Replaces the static modules.md.

View file

@ -1,7 +1,7 @@
License License
======= =======
Aborist ships under the **GNU Affero General Public License v3.0 (only)** Arborist ships under the **GNU Affero General Public License v3.0 (only)**
with a Permacomputer Preamble. Full text below, sourced from the with a Permacomputer Preamble. Full text below, sourced from the
``LICENSE`` file at the repository root (single source of truth — this ``LICENSE`` file at the repository root (single source of truth — this
page just renders it). page just renders it).

View file

@ -7,18 +7,18 @@ the same query, verify, falsify, and inspect surfaces.
Install Install
------- -------
Aborist needs Python 3.10+, GNU make, ``curl``, and ``bzip2``. SQLite Arborist needs Python 3.10+, GNU make, ``curl``, and ``bzip2``. SQLite
3.35+ ships with CPython. 3.35+ ships with CPython.
.. code-block:: sh .. code-block:: sh
git clone https://git.unturf.com/engineering/unturf/aborist.git git clone https://git.unturf.com/engineering/unturf/arborist.git
cd aborist cd arborist
make bootstrap # one-time: venv + dev extras make bootstrap # one-time: venv + dev extras
``make bootstrap`` creates ``.venv/``, installs the package in editable ``make bootstrap`` creates ``.venv/``, installs the package in editable
mode with ``[dev,html]`` extras, and exposes ``aborist`` at mode with ``[dev,html]`` extras, and exposes ``arborist`` at
``.venv/bin/aborist``. No system-wide install. Re-running is a no-op ``.venv/bin/arborist``. No system-wide install. Re-running is a no-op
if the venv is up to date. if the venv is up to date.
After bootstrap, every workflow lives behind a ``make`` target. Run After bootstrap, every workflow lives behind a ``make`` target. Run
@ -54,7 +54,7 @@ Path B — Crawl any live website and query it
make query Q="who is Russell Ballestrini?" # cross-shard; picks up new shard automatically make query Q="who is Russell Ballestrini?" # cross-shard; picks up new shard automatically
The crawl shard is named after the seed hostname The crawl shard is named after the seed hostname
(``crawl_russell_ballestrini_net.db``) under ``~/.aborist/shards/``. (``crawl_russell_ballestrini_net.db``) under ``~/.arborist/shards/``.
``FAST=1`` enables aggressive crawling for your own sites; ``MAX=N`` ``FAST=1`` enables aggressive crawling for your own sites; ``MAX=N``
caps discovery; ``DEPTH=N`` bounds BFS. Robots ``Disallow`` is always caps discovery; ``DEPTH=N`` bounds BFS. Robots ``Disallow`` is always
honored. After ingest, ``make recrawl-check DOMAIN=...`` does a honored. After ingest, ``make recrawl-check DOMAIN=...`` does a
@ -98,15 +98,15 @@ Llama-3.1-8B, 82K context, no auth). Override:
.. code-block:: sh .. code-block:: sh
export ABORIST_LLM_ENDPOINT="https://your-vllm.example/v1" export ARBORIST_LLM_ENDPOINT="https://your-vllm.example/v1"
export ABORIST_LLM_MODEL="meta-llama/Llama-3.1-70B-Instruct" export ARBORIST_LLM_MODEL="meta-llama/Llama-3.1-70B-Instruct"
export ABORIST_LLM_API_KEY="..." export ARBORIST_LLM_API_KEY="..."
Where to next Where to next
------------- -------------
* :doc:`api/makefile` — every ``make`` target with a one-line description * :doc:`api/makefile` — every ``make`` target with a one-line description
* :doc:`api/cli` — direct ``aborist`` CLI reference * :doc:`api/cli` — direct ``arborist`` CLI reference
* :doc:`api/qa` — Q&A pipeline internals (verifier, evidence, DAG) * :doc:`api/qa` — Q&A pipeline internals (verifier, evidence, DAG)
* :doc:`api/substrate` — Merkle tree + document primitives * :doc:`api/substrate` — Merkle tree + document primitives
* :doc:`license` — full AGPL + Permacomputer Preamble * :doc:`license` — full AGPL + Permacomputer Preamble

View file

@ -48,7 +48,7 @@ during the JSON-mode hardening journey:
whatever partial content the model produced and lands in the whatever partial content the model produced and lands in the
unverified bucket honestly. Retrying spends another inference unverified bucket honestly. Retrying spends another inference
round and may produce identical garbage. See round and may produce identical garbage. See
`_repair_truncated_json` in `aborist/qa/verify.py` for the `_repair_truncated_json` in `arborist/qa/verify.py` for the
pattern: walk once tracking string state + bracket stack, close / pattern: walk once tracking string state + bracket stack, close /
drop / balance at end-of-input. Conservative repairs only — never drop / balance at end-of-input. Conservative repairs only — never
insert content, never fabricate keys. insert content, never fabricate keys.

View file

@ -1,6 +1,6 @@
# Benchmarks — orientation, harnesses, fixtures, discipline # Benchmarks — orientation, harnesses, fixtures, discipline
Aborist ships two bench harnesses, four question fixtures, and a Arborist ships two bench harnesses, four question fixtures, and a
signal-floor discipline pinned across the substrate. This doc is signal-floor discipline pinned across the substrate. This doc is
the canonical entry point. Three companion docs cover specifics: the canonical entry point. Three companion docs cover specifics:
@ -41,7 +41,7 @@ output (`quantifier_intensity`, `scope_bound_hint`,
Picks three random words from `/usr/share/dict/words`, asks Picks three random words from `/usr/share/dict/words`, asks
Hermes (at temp 0.8) to weave them into a creative question Hermes (at temp 0.8) to weave them into a creative question
paragraph, then sends that paragraph to aborist. Logs every cycle paragraph, then sends that paragraph to arborist. Logs every cycle
to `bench/emergent_log.jsonl` as one JSONL line. Designed for to `bench/emergent_log.jsonl` as one JSONL line. Designed for
**blue-moon cadence**, not every-commit benching — the **blue-moon cadence**, not every-commit benching — the
combinatoric word space surfaces failure shapes the curated bench combinatoric word space surfaces failure shapes the curated bench
@ -109,13 +109,13 @@ make bench-qa
# Same but with limit / different file # Same but with limit / different file
.venv/bin/python bench/qa_sweep.py \ .venv/bin/python bench/qa_sweep.py \
--questions bench/qa_questions_quantifier_subset.txt \ --questions bench/qa_questions_quantifier_subset.txt \
--shards-dir ~/.aborist/shards --n 3 --concurrency 4 \ --shards-dir ~/.arborist/shards --n 3 --concurrency 4 \
--out-dir bench/qa_results --out-dir bench/qa_results
# A/B cycle: flip a policy knob for one bench cell # A/B cycle: flip a policy knob for one bench cell
.venv/bin/python bench/qa_sweep.py \ .venv/bin/python bench/qa_sweep.py \
--questions bench/qa_questions_quantifier_subset.txt \ --questions bench/qa_questions_quantifier_subset.txt \
--shards-dir ~/.aborist/shards --n 3 --concurrency 4 \ --shards-dir ~/.arborist/shards --n 3 --concurrency 4 \
--policy quantifier_guard_apply_caps=true \ --policy quantifier_guard_apply_caps=true \
--policy quantifier_reminder_enabled=true --policy quantifier_reminder_enabled=true
@ -219,7 +219,7 @@ random-word stress cycles. Each entry has:
"ts": <unix>, "iso_ts": "2026-05-04T...", "ts": <unix>, "iso_ts": "2026-05-04T...",
"words": ["dismally", "heats", "Jamaicans"], "words": ["dismally", "heats", "Jamaicans"],
"question": "<model-generated question paragraph>", "question": "<model-generated question paragraph>",
"answer": "<aborist's answer>", "answer": "<arborist's answer>",
"audit_mode": "STRICT|HYBRID|UNGROUNDED", "audit_mode": "STRICT|HYBRID|UNGROUNDED",
"verifier_method": "claim_lattice", "verifier_method": "claim_lattice",
"n_quotes": 3, "n_verified": 0, "n_quotes": 3, "n_verified": 0,
@ -292,7 +292,7 @@ seven-point-program directives (D2/D3/D4/D6/D7). Substrate-level
hygiene check; should be ~100% on lattice modes. hygiene check; should be ~100% on lattice modes.
**Audit-line tails** (rendered by **Audit-line tails** (rendered by
`aborist/cli.py:_render_warrant_tail`): `arborist/cli.py:_render_warrant_tail`):
- Hard tails: `· warrant missing`, `· title mismatch`, - Hard tails: `· warrant missing`, `· title mismatch`,
`· format collapsed`, `· broad cap N`, `· broad rejected`, `· format collapsed`, `· broad cap N`, `· broad rejected`,
`· broad unbounded`, `· broad runaway`, `· false premise`, `· broad unbounded`, `· broad runaway`, `· false premise`,
@ -311,20 +311,20 @@ warnings worth the operator's attention.
```bash ```bash
# Inspect preflight stage payload from any cache row # Inspect preflight stage payload from any cache row
aborist providence --show-preflight CACHE_KEY_PREFIX arborist providence --show-preflight CACHE_KEY_PREFIX
# Single query with all preflight knobs flipped # Single query with all preflight knobs flipped
aborist query "winners of all major sports?" \ arborist query "winners of all major sports?" \
--apply-quantifier-caps --reject-broad --soft-preflight --apply-quantifier-caps --reject-broad --soft-preflight
# Dry-run — assemble context but skip LLM # Dry-run — assemble context but skip LLM
aborist query "..." --dry-run arborist query "..." --dry-run
# Force fresh inference (delete prior cache row first) # Force fresh inference (delete prior cache row first)
aborist query "..." --burn arborist query "..." --burn
# Show raw JSON record instead of human render # Show raw JSON record instead of human render
aborist query "..." --json arborist query "..." --json
``` ```
The `Makefile` exposes most of these via shorthand: The `Makefile` exposes most of these via shorthand:

View file

@ -63,10 +63,10 @@ The animating principle: **the model proposes, the runtime structures, the verif
- which falsification triggers stale a record - which falsification triggers stale a record
**Today's codebase:** **Today's codebase:**
- `aborist/qa/runner.py:ask` — orchestrates the full lookup → infer → verify → admissibility flow for the per-document path - `arborist/qa/runner.py:ask` — orchestrates the full lookup → infer → verify → admissibility flow for the per-document path
- `aborist/qa/query.py:query` — same orchestration for the multi-source RAG path - `arborist/qa/query.py:query` — same orchestration for the multi-source RAG path
- `DEFAULT_POLICY` (runner) and `DEFAULT_QUERY_POLICY` (query) — the policy dict - `DEFAULT_POLICY` (runner) and `DEFAULT_QUERY_POLICY` (query) — the policy dict
- `aborist.qa.keys.governance_policy_hash` — folds policy into the cache_key - `arborist.qa.keys.governance_policy_hash` — folds policy into the cache_key
**Status:** Implicit. The layer exists as code but is not named. The de-novo doc proposes naming it; the codebase doesn't need a `prometheus.py` module today — the dispatch logic in `runner.py` and `query.py` IS PROMETHEUS-Σ. **Status:** Implicit. The layer exists as code but is not named. The de-novo doc proposes naming it; the codebase doesn't need a `prometheus.py` module today — the dispatch logic in `runner.py` and `query.py` IS PROMETHEUS-Σ.
@ -79,15 +79,15 @@ The animating principle: **the model proposes, the runtime structures, the verif
- the boundary between hard checks (lexical, deterministic) and soft checks (heuristic, demote-only) - the boundary between hard checks (lexical, deterministic) and soft checks (heuristic, demote-only)
**Today's codebase:** **Today's codebase:**
- `aborist/qa/parse_claims.py:parse_pointer_claims` — the model's pointer-line output → `ParsedClaim` records (the CTI compile step) - `arborist/qa/parse_claims.py:parse_pointer_claims` — the model's pointer-line output → `ParsedClaim` records (the CTI compile step)
- `aborist/qa/evidence.py:EvidenceObject` — typed evidence nodes with `evidence_id` (content-addressed) + `pointer_id` (prompt-facing) - `arborist/qa/evidence.py:EvidenceObject` — typed evidence nodes with `evidence_id` (content-addressed) + `pointer_id` (prompt-facing)
- `aborist/qa/evidence.py:render_claim_lattice` — the renderer that interpolates source spans by offset - `arborist/qa/evidence.py:render_claim_lattice` — the renderer that interpolates source spans by offset
- `aborist/qa/verify.py:verify_claim_lattice` — the deterministic verifier (six hard checks + sidecar signals) - `arborist/qa/verify.py:verify_claim_lattice` — the deterministic verifier (six hard checks + sidecar signals)
- `aborist/qa/verify.py:verify_claim_lattice_json` — same for the JSON variant - `arborist/qa/verify.py:verify_claim_lattice_json` — same for the JSON variant
- `aborist/qa/quantifier.py:classify_question_quantifier` — pure 10-rung intensity classifier (#000008); feeds the preflight stage's `classifier` clause - `arborist/qa/quantifier.py:classify_question_quantifier` — pure 10-rung intensity classifier (#000008); feeds the preflight stage's `classifier` clause
- `aborist/qa/model_profiles.py:cap_for_intensity` — per-model claim-cap profiles (#000008); feeds the `answer_contract` clause's `claim_cap_resolved` - `arborist/qa/model_profiles.py:cap_for_intensity` — per-model claim-cap profiles (#000008); feeds the `answer_contract` clause's `claim_cap_resolved`
- `aborist/qa/quantifier_reminder.py:broad_quantifier_reminder` — broad-question reminder text generator (#000008); feeds the `prompt_contract` clause - `arborist/qa/quantifier_reminder.py:broad_quantifier_reminder` — broad-question reminder text generator (#000008); feeds the `prompt_contract` clause
- `aborist/qa/metacognition.py:preflight_question` — meta-cognition QuestionState detector suite (#000010): temporal, contradiction, false-premise-lite, out-of-corpus. Feeds the preflight stage's `question_state` clause. - `arborist/qa/metacognition.py:preflight_question` — meta-cognition QuestionState detector suite (#000010): temporal, contradiction, false-premise-lite, out-of-corpus. Feeds the preflight stage's `question_state` clause.
**Status:** Real. CTI is what `claim_lattice` already is. The architectural insight from fox's synthesis — that this is an INTERNAL bytecode for reasoning, not a model output format — is the right reading: the model emits weak pointer lines; the runtime parses them into the CTI lattice; the lattice is what the verifier and renderer operate on. The pre-answer preflight contract (#000008 quantifier classifier + #000010 metacognition QuestionState) extends CTI upstream of retrieval — the runtime now classifies the *question* before generating an answer, so policy can refuse, cap, or reject without burning an LLM call. **Status:** Real. CTI is what `claim_lattice` already is. The architectural insight from fox's synthesis — that this is an INTERNAL bytecode for reasoning, not a model output format — is the right reading: the model emits weak pointer lines; the runtime parses them into the CTI lattice; the lattice is what the verifier and renderer operate on. The pre-answer preflight contract (#000008 quantifier classifier + #000010 metacognition QuestionState) extends CTI upstream of retrieval — the runtime now classifies the *question* before generating an answer, so policy can refuse, cap, or reject without burning an LLM call.
@ -119,12 +119,12 @@ The animating principle: **the model proposes, the runtime structures, the verif
**Audit-replay payoff:** two cache rows that share the same question + same model output + same verifier verdict but different preflight policy state (cap on/off, reminder on/off, reject path taken/not, metacog gates) now produce **different `run_dag_root`** values. **Audit-replay payoff:** two cache rows that share the same question + same model output + same verifier verdict but different preflight policy state (cap on/off, reminder on/off, reject path taken/not, metacog gates) now produce **different `run_dag_root`** values.
**Today's codebase:** **Today's codebase:**
- `aborist/qa/dag.py:build_run_dag` — emits the 7/8/9/10-stage DAG; returns `{root, nodes, blob}`. `preflight_hash` parameter optional for backward-compat with legacy roots. - `arborist/qa/dag.py:build_run_dag` — emits the 7/8/9/10-stage DAG; returns `{root, nodes, blob}`. `preflight_hash` parameter optional for backward-compat with legacy roots.
- `aborist/qa/dag.py:build_reject_run_dag` — emits the 3-stage reject-path DAG. - `arborist/qa/dag.py:build_reject_run_dag` — emits the 3-stage reject-path DAG.
- `aborist/qa/dag.py:preflight_node_hash` + `build_preflight_node_payload` — canonical hash of the preflight clause set. - `arborist/qa/dag.py:preflight_node_hash` + `build_preflight_node_payload` — canonical hash of the preflight clause set.
- `aborist/merkle.py` — the non-commutative HashCombine + odd-self-duplicate Merkle conventions (Python port of `proxy.unturf.com/pkg/verified/merkle.go`) - `arborist/merkle.py` — the non-commutative HashCombine + odd-self-duplicate Merkle conventions (Python port of `proxy.unturf.com/pkg/verified/merkle.go`)
- `aborist.store.append_audit` — the only legal entry point for `audit_events`; computes `event_hash = sha256(prev_event_hash || canonical(body))` - `arborist.store.append_audit` — the only legal entry point for `audit_events`; computes `event_hash = sha256(prev_event_hash || canonical(body))`
- `aborist/qa/keys.py:cache_key` — the 8-dim composite hash that gates record reuse - `arborist/qa/keys.py:cache_key` — the 8-dim composite hash that gates record reuse
**Status:** Real. The base 7/9-stage DAG was the F-track work earlier in the session; preflight binding landed 2026-05-03 (#000009 base, `c36e85c`) and was restructured to nested-clause shape 2026-05-04 (`111dda6`). **Status:** Real. The base 7/9-stage DAG was the F-track work earlier in the session; preflight binding landed 2026-05-03 (#000009 base, `c36e85c`) and was restructured to nested-clause shape 2026-05-04 (`111dda6`).
@ -136,9 +136,9 @@ The animating principle: **the model proposes, the runtime structures, the verif
- the renderer pulls source text by `(chunk_root, offset_start, offset_end)` — the model NEVER owns the quote text; the runtime interpolates it - the renderer pulls source text by `(chunk_root, offset_start, offset_end)` — the model NEVER owns the quote text; the runtime interpolates it
**Today's codebase:** **Today's codebase:**
- `aborist/qa/evidence.py` — `EvidenceObject(source_root, document_uri, chunk_root, offset_start, offset_end, source_role, text_hash, span, evidence_id, pointer_id)`. The `pointer_id` (E1, E2, …) is what the model sees in the prompt; the `evidence_id` (content-addressed) is the cache/run-DAG handle. The runtime maps pointer_id → object → content-addressed evidence_id internally. - `arborist/qa/evidence.py` — `EvidenceObject(source_root, document_uri, chunk_root, offset_start, offset_end, source_role, text_hash, span, evidence_id, pointer_id)`. The `pointer_id` (E1, E2, …) is what the model sees in the prompt; the `evidence_id` (content-addressed) is the cache/run-DAG handle. The runtime maps pointer_id → object → content-addressed evidence_id internally.
- `aborist/qa/verify.py:verify_claim_lattice` step 6 (citation-overlap check) — the `claim → cited_span` lexical-coverage gate - `arborist/qa/verify.py:verify_claim_lattice` step 6 (citation-overlap check) — the `claim → cited_span` lexical-coverage gate
- `aborist/qa/evidence.py:render_claim_lattice` — interpolates source spans by offset, not Hermes text. Synthetic-elision is impossible by construction in pointer mode because the model never produces the quote string. - `arborist/qa/evidence.py:render_claim_lattice` — interpolates source spans by offset, not Hermes text. Synthetic-elision is impossible by construction in pointer mode because the model never produces the quote string.
**Status:** Real. Pointer mode makes the reverse-RAG direction explicit; the model produces the LEFT side of the chain (claim) and points to the RIGHT side (evidence_id) — the runtime walks the rest. **Status:** Real. Pointer mode makes the reverse-RAG direction explicit; the model produces the LEFT side of the chain (claim) and points to the RIGHT side (evidence_id) — the runtime walks the rest.
@ -151,9 +151,9 @@ The animating principle: **the model proposes, the runtime structures, the verif
- the deliberate honesty boundary: NOT semantic truth, NOT hallucination elimination, NOT formal proof — just admissible provenance - the deliberate honesty boundary: NOT semantic truth, NOT hallucination elimination, NOT formal proof — just admissible provenance
**Today's codebase:** **Today's codebase:**
- `aborist/qa/keys.py` — all eight dimensions hashed into `cache_key` - `arborist/qa/keys.py` — all eight dimensions hashed into `cache_key`
- `aborist/store.py` — schema, audit chain, transaction helpers - `arborist/store.py` — schema, audit chain, transaction helpers
- `aborist/qa/runner.py` + `aborist/qa/query.py` — the cache-lookup-then-infer-then-write flow - `arborist/qa/runner.py` + `arborist/qa/query.py` — the cache-lookup-then-infer-then-write flow
**Status:** Real and honest. CLAUDE.md "Schema invariants (do not break)" enforces this boundary. **Status:** Real and honest. CLAUDE.md "Schema invariants (do not break)" enforces this boundary.
@ -165,8 +165,8 @@ The animating principle: **the model proposes, the runtime structures, the verif
- nothing else — every other artifact (the lattice, the verdict, the rendered prose, the run DAG) comes from the runtime - nothing else — every other artifact (the lattice, the verdict, the rendered prose, the run DAG) comes from the runtime
**Today's codebase:** **Today's codebase:**
- `aborist/qa/client.py:OpenAICompatibleClient` — HTTP client with retry on 502/503/504 - `arborist/qa/client.py:OpenAICompatibleClient` — HTTP client with retry on 502/503/504
- `aborist/qa/client.py:StubClient` — offline test stub - `arborist/qa/client.py:StubClient` — offline test stub
- `claim_lattice_system_prompt` + `claim_lattice_grounding_reminder` — the prompt fields that frame Hermes' role - `claim_lattice_system_prompt` + `claim_lattice_grounding_reminder` — the prompt fields that frame Hermes' role
- `claim_lattice_json_system_prompt` + `claim_lattice_json_grounding_reminder` — JSON variant - `claim_lattice_json_system_prompt` + `claim_lattice_json_grounding_reminder` — JSON variant

View file

@ -1,12 +1,12 @@
// Aborist module graph — top-level packages & their dependencies. // Arborist module graph — top-level packages & their dependencies.
// //
// Arrows point in the import direction (A → B means A imports from B). // Arrows point in the import direction (A → B means A imports from B).
// Cluster boxes group modules with shared concerns (storage, retrieval, // Cluster boxes group modules with shared concerns (storage, retrieval,
// federation, etc.). // federation, etc.).
// //
// Render: dot -Tsvg aborist-modules.dot -o aborist-modules.svg // Render: dot -Tsvg arborist-modules.dot -o arborist-modules.svg
digraph aborist_modules { digraph arborist_modules {
rankdir=LR rankdir=LR
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10] node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=10]
edge [fontname="Helvetica", fontsize=9] edge [fontname="Helvetica", fontsize=9]

View file

@ -3,11 +3,11 @@
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.43.0 (0) <!-- Generated by graphviz version 2.43.0 (0)
--> -->
<!-- Title: aborist_modules Pages: 1 --> <!-- Title: arborist_modules Pages: 1 -->
<svg width="1663pt" height="1147pt" <svg width="1663pt" height="1147pt"
viewBox="0.00 0.00 1663.00 1146.81" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> viewBox="0.00 0.00 1663.00 1146.81" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1142.81)"> <g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1142.81)">
<title>aborist_modules</title> <title>arborist_modules</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-1142.81 1659,-1142.81 1659,4 -4,4"/> <polygon fill="white" stroke="transparent" points="-4,4 -4,-1142.81 1659,-1142.81 1659,4 -4,4"/>
<g id="clust1" class="cluster"> <g id="clust1" class="cluster">
<title>cluster_substrate</title> <title>cluster_substrate</title>

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

View file

@ -1,6 +1,6 @@
// aborist/mesh — what flows between peers. // arborist/mesh — what flows between peers.
// //
// Aborist is content-addressed: two peers ingesting the same dump // Arborist is content-addressed: two peers ingesting the same dump
// compute bit-identical document_roots. Gossip exchanges identifiers, // compute bit-identical document_roots. Gossip exchanges identifiers,
// derivations, falsifications, and witness signatures. Bulk content // derivations, falsifications, and witness signatures. Bulk content
// only flows on cache miss, by Merkle proof of inclusion. // only flows on cache miss, by Merkle proof of inclusion.

View file

@ -1,4 +1,4 @@
// aborist/mesh — epoch state machine. // arborist/mesh — epoch state machine.
// //
// Every roster mutation bumps a counter. Old epochs persist forever // Every roster mutation bumps a counter. Old epochs persist forever
// (signatures stay verifiable). New gossip is encrypted under the // (signatures stay verifiable). New gossip is encrypted under the
@ -18,7 +18,7 @@ digraph mesh_epoch_lifecycle {
eN [label="epoch N+1\nfresh secret\nfresh envelope", fillcolor="#d6ffd6"]; eN [label="epoch N+1\nfresh secret\nfresh envelope", fillcolor="#d6ffd6"];
historic [label="every prior epoch\nstays on disk\n(roster + signatures verifiable)", fillcolor="#ffffff", shape=note]; historic [label="every prior epoch\nstays on disk\n(roster + signatures verifiable)", fillcolor="#ffffff", shape=note];
none -> e0 [label="aborist mesh init --group <name>"]; none -> e0 [label="arborist mesh init --group <name>"];
e0 -> e1 [label="add | kick | rotate"]; e0 -> e1 [label="add | kick | rotate"];
e1 -> eN [label="add | kick | rotate"]; e1 -> eN [label="add | kick | rotate"];
eN -> eN [label="rotate (no roster change)"]; eN -> eN [label="rotate (no roster change)"];
@ -31,8 +31,8 @@ digraph mesh_epoch_lifecycle {
style="rounded,dashed"; style="rounded,dashed";
color="#666666"; color="#666666";
node [fillcolor="#e8f0ff", shape=note]; node [fillcolor="#e8f0ff", shape=note];
op_add [label="aborist mesh add\n--member-id bob ...\nadmin only"]; op_add [label="arborist mesh add\n--member-id bob ...\nadmin only"];
op_kick [label="aborist mesh kick\n--member-id bob ...\nadmin only"]; op_kick [label="arborist mesh kick\n--member-id bob ...\nadmin only"];
op_rotate [label="aborist mesh rotate\nany current member"]; op_rotate [label="arborist mesh rotate\nany current member"];
} }
} }

View file

@ -34,7 +34,7 @@
<title>none&#45;&gt;e0</title> <title>none&#45;&gt;e0</title>
<path fill="none" stroke="black" d="M195.03,-30.5C254.65,-30.5 330.75,-30.5 389.75,-30.5"/> <path fill="none" stroke="black" d="M195.03,-30.5C254.65,-30.5 330.75,-30.5 389.75,-30.5"/>
<polygon fill="black" stroke="black" points="389.81,-34 399.81,-30.5 389.81,-27 389.81,-34"/> <polygon fill="black" stroke="black" points="389.81,-34 399.81,-30.5 389.81,-27 389.81,-34"/>
<text text-anchor="middle" x="297.5" y="-33.5" font-family="Helvetica,sans-Serif" font-size="10.00">aborist mesh init &#45;&#45;group &lt;name&gt;</text> <text text-anchor="middle" x="297.5" y="-33.5" font-family="Helvetica,sans-Serif" font-size="10.00">arborist mesh init &#45;&#45;group &lt;name&gt;</text>
</g> </g>
<!-- e1 --> <!-- e1 -->
<g id="node3" class="node"> <g id="node3" class="node">
@ -99,7 +99,7 @@
<polygon fill="#e8f0ff" stroke="black" points="167,-248 22,-248 22,-195 173,-195 173,-242 167,-248"/> <polygon fill="#e8f0ff" stroke="black" points="167,-248 22,-248 22,-195 173,-195 173,-242 167,-248"/>
<polyline fill="none" stroke="black" points="167,-248 167,-242 "/> <polyline fill="none" stroke="black" points="167,-248 167,-242 "/>
<polyline fill="none" stroke="black" points="173,-242 167,-242 "/> <polyline fill="none" stroke="black" points="173,-242 167,-242 "/>
<text text-anchor="middle" x="97.5" y="-232.8" font-family="Helvetica,sans-Serif" font-size="14.00">aborist mesh add</text> <text text-anchor="middle" x="97.5" y="-232.8" font-family="Helvetica,sans-Serif" font-size="14.00">arborist mesh add</text>
<text text-anchor="middle" x="97.5" y="-217.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id bob ...</text> <text text-anchor="middle" x="97.5" y="-217.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id bob ...</text>
<text text-anchor="middle" x="97.5" y="-202.8" font-family="Helvetica,sans-Serif" font-size="14.00">admin only</text> <text text-anchor="middle" x="97.5" y="-202.8" font-family="Helvetica,sans-Serif" font-size="14.00">admin only</text>
</g> </g>
@ -109,7 +109,7 @@
<polygon fill="#e8f0ff" stroke="black" points="167,-177 22,-177 22,-124 173,-124 173,-171 167,-177"/> <polygon fill="#e8f0ff" stroke="black" points="167,-177 22,-177 22,-124 173,-124 173,-171 167,-177"/>
<polyline fill="none" stroke="black" points="167,-177 167,-171 "/> <polyline fill="none" stroke="black" points="167,-177 167,-171 "/>
<polyline fill="none" stroke="black" points="173,-171 167,-171 "/> <polyline fill="none" stroke="black" points="173,-171 167,-171 "/>
<text text-anchor="middle" x="97.5" y="-161.8" font-family="Helvetica,sans-Serif" font-size="14.00">aborist mesh kick</text> <text text-anchor="middle" x="97.5" y="-161.8" font-family="Helvetica,sans-Serif" font-size="14.00">arborist mesh kick</text>
<text text-anchor="middle" x="97.5" y="-146.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id bob ...</text> <text text-anchor="middle" x="97.5" y="-146.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id bob ...</text>
<text text-anchor="middle" x="97.5" y="-131.8" font-family="Helvetica,sans-Serif" font-size="14.00">admin only</text> <text text-anchor="middle" x="97.5" y="-131.8" font-family="Helvetica,sans-Serif" font-size="14.00">admin only</text>
</g> </g>
@ -119,7 +119,7 @@
<polygon fill="#e8f0ff" stroke="black" points="171,-105.5 18,-105.5 18,-67.5 177,-67.5 177,-99.5 171,-105.5"/> <polygon fill="#e8f0ff" stroke="black" points="171,-105.5 18,-105.5 18,-67.5 177,-67.5 177,-99.5 171,-105.5"/>
<polyline fill="none" stroke="black" points="171,-105.5 171,-99.5 "/> <polyline fill="none" stroke="black" points="171,-105.5 171,-99.5 "/>
<polyline fill="none" stroke="black" points="177,-99.5 171,-99.5 "/> <polyline fill="none" stroke="black" points="177,-99.5 171,-99.5 "/>
<text text-anchor="middle" x="97.5" y="-90.3" font-family="Helvetica,sans-Serif" font-size="14.00">aborist mesh rotate</text> <text text-anchor="middle" x="97.5" y="-90.3" font-family="Helvetica,sans-Serif" font-size="14.00">arborist mesh rotate</text>
<text text-anchor="middle" x="97.5" y="-75.3" font-family="Helvetica,sans-Serif" font-size="14.00">any current member</text> <text text-anchor="middle" x="97.5" y="-75.3" font-family="Helvetica,sans-Serif" font-size="14.00">any current member</text>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
// aborist/mesh — group operator decision tree. // arborist/mesh — group operator decision tree.
// //
// When a group steward (admin) faces a roster question, this diagram // When a group steward (admin) faces a roster question, this diagram
// shows which CLI verb solves it and what guarantee comes out the // shows which CLI verb solves it and what guarantee comes out the
@ -19,10 +19,10 @@ digraph mesh_group_decisions {
q_who [label="add, remove,\nor refresh?", shape=diamond, style="filled", fillcolor="#fff7d6"]; q_who [label="add, remove,\nor refresh?", shape=diamond, style="filled", fillcolor="#fff7d6"];
q_admin [label="are you\nadmin in\ncurrent epoch?", shape=diamond, style="filled", fillcolor="#fff7d6"]; q_admin [label="are you\nadmin in\ncurrent epoch?", shape=diamond, style="filled", fillcolor="#fff7d6"];
a_init [label="aborist mesh init --group <name>\nthen mesh enable\n(creates epoch 0; founder = sole admin)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"]; a_init [label="arborist mesh init --group <name>\nthen mesh enable\n(creates epoch 0; founder = sole admin)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_add [label="aborist mesh add\n--member-id <id>\n--sign-pub <hex>\n--dh-pub <hex>\n[--role admin]", shape=box, style="rounded,filled", fillcolor="#d6ffd6"]; a_add [label="arborist mesh add\n--member-id <id>\n--sign-pub <hex>\n--dh-pub <hex>\n[--role admin]", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_kick [label="aborist mesh kick\n--member-id <id>\n--reason '...'", shape=box, style="rounded,filled", fillcolor="#d6ffd6"]; a_kick [label="arborist mesh kick\n--member-id <id>\n--reason '...'", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_rotate [label="aborist mesh rotate\n--reason '...'\n(any current member)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"]; a_rotate [label="arborist mesh rotate\n--reason '...'\n(any current member)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
g_admin [label="permission denied\n(only admins of the\ncurrent epoch may add or kick)", shape=box, style="rounded,filled", fillcolor="#ffe0e0"]; g_admin [label="permission denied\n(only admins of the\ncurrent epoch may add or kick)", shape=box, style="rounded,filled", fillcolor="#ffe0e0"];

View file

@ -48,7 +48,7 @@
<g id="node6" class="node"> <g id="node6" class="node">
<title>a_init</title> <title>a_init</title>
<path fill="#d6ffd6" stroke="black" d="M1104,-575.5C1104,-575.5 830,-575.5 830,-575.5 824,-575.5 818,-569.5 818,-563.5 818,-563.5 818,-534.5 818,-534.5 818,-528.5 824,-522.5 830,-522.5 830,-522.5 1104,-522.5 1104,-522.5 1110,-522.5 1116,-528.5 1116,-534.5 1116,-534.5 1116,-563.5 1116,-563.5 1116,-569.5 1110,-575.5 1104,-575.5"/> <path fill="#d6ffd6" stroke="black" d="M1104,-575.5C1104,-575.5 830,-575.5 830,-575.5 824,-575.5 818,-569.5 818,-563.5 818,-563.5 818,-534.5 818,-534.5 818,-528.5 824,-522.5 830,-522.5 830,-522.5 1104,-522.5 1104,-522.5 1110,-522.5 1116,-528.5 1116,-534.5 1116,-534.5 1116,-563.5 1116,-563.5 1116,-569.5 1110,-575.5 1104,-575.5"/>
<text text-anchor="middle" x="967" y="-560.3" font-family="Helvetica,sans-Serif" font-size="14.00">aborist mesh init &#45;&#45;group &lt;name&gt;</text> <text text-anchor="middle" x="967" y="-560.3" font-family="Helvetica,sans-Serif" font-size="14.00">arborist mesh init &#45;&#45;group &lt;name&gt;</text>
<text text-anchor="middle" x="967" y="-545.3" font-family="Helvetica,sans-Serif" font-size="14.00">then mesh enable</text> <text text-anchor="middle" x="967" y="-545.3" font-family="Helvetica,sans-Serif" font-size="14.00">then mesh enable</text>
<text text-anchor="middle" x="967" y="-530.3" font-family="Helvetica,sans-Serif" font-size="14.00">(creates epoch 0; founder = sole admin)</text> <text text-anchor="middle" x="967" y="-530.3" font-family="Helvetica,sans-Serif" font-size="14.00">(creates epoch 0; founder = sole admin)</text>
</g> </g>
@ -77,7 +77,7 @@
<g id="node9" class="node"> <g id="node9" class="node">
<title>a_rotate</title> <title>a_rotate</title>
<path fill="#d6ffd6" stroke="black" d="M826,-299.5C826,-299.5 680,-299.5 680,-299.5 674,-299.5 668,-293.5 668,-287.5 668,-287.5 668,-258.5 668,-258.5 668,-252.5 674,-246.5 680,-246.5 680,-246.5 826,-246.5 826,-246.5 832,-246.5 838,-252.5 838,-258.5 838,-258.5 838,-287.5 838,-287.5 838,-293.5 832,-299.5 826,-299.5"/> <path fill="#d6ffd6" stroke="black" d="M826,-299.5C826,-299.5 680,-299.5 680,-299.5 674,-299.5 668,-293.5 668,-287.5 668,-287.5 668,-258.5 668,-258.5 668,-252.5 674,-246.5 680,-246.5 680,-246.5 826,-246.5 826,-246.5 832,-246.5 838,-252.5 838,-258.5 838,-258.5 838,-287.5 838,-287.5 838,-293.5 832,-299.5 826,-299.5"/>
<text text-anchor="middle" x="753" y="-284.3" font-family="Helvetica,sans-Serif" font-size="14.00">aborist mesh rotate</text> <text text-anchor="middle" x="753" y="-284.3" font-family="Helvetica,sans-Serif" font-size="14.00">arborist mesh rotate</text>
<text text-anchor="middle" x="753" y="-269.3" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;reason &#39;...&#39;</text> <text text-anchor="middle" x="753" y="-269.3" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;reason &#39;...&#39;</text>
<text text-anchor="middle" x="753" y="-254.3" font-family="Helvetica,sans-Serif" font-size="14.00">(any current member)</text> <text text-anchor="middle" x="753" y="-254.3" font-family="Helvetica,sans-Serif" font-size="14.00">(any current member)</text>
</g> </g>
@ -114,7 +114,7 @@
<g id="node7" class="node"> <g id="node7" class="node">
<title>a_add</title> <title>a_add</title>
<path fill="#d6ffd6" stroke="black" d="M180.5,-173C180.5,-173 61.5,-173 61.5,-173 55.5,-173 49.5,-167 49.5,-161 49.5,-161 49.5,-102 49.5,-102 49.5,-96 55.5,-90 61.5,-90 61.5,-90 180.5,-90 180.5,-90 186.5,-90 192.5,-96 192.5,-102 192.5,-102 192.5,-161 192.5,-161 192.5,-167 186.5,-173 180.5,-173"/> <path fill="#d6ffd6" stroke="black" d="M180.5,-173C180.5,-173 61.5,-173 61.5,-173 55.5,-173 49.5,-167 49.5,-161 49.5,-161 49.5,-102 49.5,-102 49.5,-96 55.5,-90 61.5,-90 61.5,-90 180.5,-90 180.5,-90 186.5,-90 192.5,-96 192.5,-102 192.5,-102 192.5,-161 192.5,-161 192.5,-167 186.5,-173 180.5,-173"/>
<text text-anchor="middle" x="121" y="-157.8" font-family="Helvetica,sans-Serif" font-size="14.00">aborist mesh add</text> <text text-anchor="middle" x="121" y="-157.8" font-family="Helvetica,sans-Serif" font-size="14.00">arborist mesh add</text>
<text text-anchor="middle" x="121" y="-142.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id &lt;id&gt;</text> <text text-anchor="middle" x="121" y="-142.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id &lt;id&gt;</text>
<text text-anchor="middle" x="121" y="-127.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;sign&#45;pub &lt;hex&gt;</text> <text text-anchor="middle" x="121" y="-127.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;sign&#45;pub &lt;hex&gt;</text>
<text text-anchor="middle" x="121" y="-112.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;dh&#45;pub &lt;hex&gt;</text> <text text-anchor="middle" x="121" y="-112.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;dh&#45;pub &lt;hex&gt;</text>
@ -131,7 +131,7 @@
<g id="node8" class="node"> <g id="node8" class="node">
<title>a_kick</title> <title>a_kick</title>
<path fill="#d6ffd6" stroke="black" d="M384.5,-158C384.5,-158 265.5,-158 265.5,-158 259.5,-158 253.5,-152 253.5,-146 253.5,-146 253.5,-117 253.5,-117 253.5,-111 259.5,-105 265.5,-105 265.5,-105 384.5,-105 384.5,-105 390.5,-105 396.5,-111 396.5,-117 396.5,-117 396.5,-146 396.5,-146 396.5,-152 390.5,-158 384.5,-158"/> <path fill="#d6ffd6" stroke="black" d="M384.5,-158C384.5,-158 265.5,-158 265.5,-158 259.5,-158 253.5,-152 253.5,-146 253.5,-146 253.5,-117 253.5,-117 253.5,-111 259.5,-105 265.5,-105 265.5,-105 384.5,-105 384.5,-105 390.5,-105 396.5,-111 396.5,-117 396.5,-117 396.5,-146 396.5,-146 396.5,-152 390.5,-158 384.5,-158"/>
<text text-anchor="middle" x="325" y="-142.8" font-family="Helvetica,sans-Serif" font-size="14.00">aborist mesh kick</text> <text text-anchor="middle" x="325" y="-142.8" font-family="Helvetica,sans-Serif" font-size="14.00">arborist mesh kick</text>
<text text-anchor="middle" x="325" y="-127.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id &lt;id&gt;</text> <text text-anchor="middle" x="325" y="-127.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;member&#45;id &lt;id&gt;</text>
<text text-anchor="middle" x="325" y="-112.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;reason &#39;...&#39;</text> <text text-anchor="middle" x="325" y="-112.8" font-family="Helvetica,sans-Serif" font-size="14.00">&#45;&#45;reason &#39;...&#39;</text>
</g> </g>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
// aborist/mesh — cryptographic identity stack per peer. // arborist/mesh — cryptographic identity stack per peer.
// //
// Each peer carries two keypairs and one shared epoch secret. // Each peer carries two keypairs and one shared epoch secret.
// Keys are raw 32-byte forms; everything lives in the standard SQLite db. // Keys are raw 32-byte forms; everything lives in the standard SQLite db.

View file

@ -1,4 +1,4 @@
// aborist/mesh — per-member secret envelope. // arborist/mesh — per-member secret envelope.
// //
// Every epoch carries one fresh 32-byte symmetric secret. That single // Every epoch carries one fresh 32-byte symmetric secret. That single
// secret is wrapped N times, once per member, using ECDH between the // secret is wrapped N times, once per member, using ECDH between the

View file

@ -1,7 +1,7 @@
# Mesh — multiplayer aborist # Mesh — multiplayer arborist
Aborist on a single laptop is a content-addressed forest of documents. Arborist on a single laptop is a content-addressed forest of documents.
Aborist across many laptops is a *gossip-able* forest. Two peers that Arborist across many laptops is a *gossip-able* forest. Two peers that
ingest the same Wikipedia dump compute bit-identical `document_root`s, ingest the same Wikipedia dump compute bit-identical `document_root`s,
so any peer can verify another peer's claim by re-deriving the hash so any peer can verify another peer's claim by re-deriving the hash
locally. Mesh is wire-and-trust scaffolding wrapped around that fact. locally. Mesh is wire-and-trust scaffolding wrapped around that fact.
@ -20,10 +20,10 @@ Lookups, ingests, distillations, and Q&A run identically with or without
mesh. Initialization is two commands and is reversible. mesh. Initialization is two commands and is reversible.
``` ```
aborist mesh init --group myteam # mint Ed25519 + X25519 keys; create epoch 0 arborist mesh init --group myteam # mint Ed25519 + X25519 keys; create epoch 0
aborist mesh enable # flip gating flag on arborist mesh enable # flip gating flag on
aborist mesh status # always-safe inspection arborist mesh status # always-safe inspection
aborist mesh disable # flag back off; keys + history stay on disk arborist mesh disable # flag back off; keys + history stay on disk
``` ```
## Identity stack ## Identity stack
@ -71,14 +71,14 @@ between their own X25519 priv and a rotator's X25519 pub, then unwraps
their own slot. An evicted peer simply has no slot, so unwrap raises their own slot. An evicted peer simply has no slot, so unwrap raises
`ValueError` — they are opaque to gossip from epoch+1 onward by design. `ValueError` — they are opaque to gossip from epoch+1 onward by design.
Code: `aborist/mesh/state.py::_wrap_secret_for_members` and Code: `arborist/mesh/state.py::_wrap_secret_for_members` and
`unwrap_secret_for_self`. `unwrap_secret_for_self`.
## What flows between peers ## What flows between peers
![data flow](diagrams/mesh-data-flow.png) ![data flow](diagrams/mesh-data-flow.png)
Aborist is content-addressed, so identifiers are short and bodies are Arborist is content-addressed, so identifiers are short and bodies are
optional. A typical sync round looks like: optional. A typical sync round looks like:
1. **alice** signs and sends `ANNOUNCE_ROOT(document_root, source_uri, 1. **alice** signs and sends `ANNOUNCE_ROOT(document_root, source_uri,
@ -120,10 +120,10 @@ is rejected; an admin who wants out runs `mesh disable` instead.
``` ```
# bob mints his keys and shares pubs (out-of-band: signal, in person, signed file). # bob mints his keys and shares pubs (out-of-band: signal, in person, signed file).
aborist mesh init --group myteam --member-id bob # bob's machine arborist mesh init --group myteam --member-id bob # bob's machine
# alice (admin) enrolls bob at her machine: # alice (admin) enrolls bob at her machine:
aborist mesh add --member-id bob \ arborist mesh add --member-id bob \
--sign-pub <bob_sign_pub_hex> \ --sign-pub <bob_sign_pub_hex> \
--dh-pub <bob_dh_pub_hex> --dh-pub <bob_dh_pub_hex>
# epoch bumps. fresh secret wrapped to alice + bob. # epoch bumps. fresh secret wrapped to alice + bob.
@ -135,7 +135,7 @@ design, joining doesn't grant retroactive access.
### Kick a member (admin only) ### Kick a member (admin only)
``` ```
aborist mesh kick --member-id dave --reason "left team 2026-04-28" arborist mesh kick --member-id dave --reason "left team 2026-04-28"
``` ```
Epoch bumps. New secret wrapped to everyone *except* dave. Dave's prior Epoch bumps. New secret wrapped to everyone *except* dave. Dave's prior
@ -148,7 +148,7 @@ Cannot leave a roster with no admins. Promote first, then kick.
### Rotate the secret (any member) ### Rotate the secret (any member)
``` ```
aborist mesh rotate --reason "scheduled monthly hygiene" arborist mesh rotate --reason "scheduled monthly hygiene"
``` ```
Same roster, fresh secret. Use on suspected secret leak when no Same roster, fresh secret. Use on suspected secret leak when no
@ -203,19 +203,19 @@ Mesh does not defend against:
## Related code ## Related code
- `aborist/mesh/__init__.py` — module entrypoint, public API - `arborist/mesh/__init__.py` — module entrypoint, public API
- `aborist/mesh/crypto.py` — Ed25519 + X25519 + ChaCha20-Poly1305 wrappers - `arborist/mesh/crypto.py` — Ed25519 + X25519 + ChaCha20-Poly1305 wrappers
- `aborist/mesh/state.py` — DB writes + audit chain hooks; epoch rotation - `arborist/mesh/state.py` — DB writes + audit chain hooks; epoch rotation
- `aborist/mesh/members.py` — `add_member`, `kick_member`, `scheduled_rotate` - `arborist/mesh/members.py` — `add_member`, `kick_member`, `scheduled_rotate`
- `aborist/cli.py` — `_cmd_mesh_*` argparse handlers - `arborist/cli.py` — `_cmd_mesh_*` argparse handlers
- `aborist/store.py` — `mesh_identity`, `mesh_roster`, `mesh_epochs` schema - `arborist/store.py` — `mesh_identity`, `mesh_roster`, `mesh_epochs` schema
## Related docs ## Related docs
- `README.md` — top-level overview, ingest flow, single-peer Q&A - `README.md` — top-level overview, ingest flow, single-peer Q&A
- `CLAUDE.md` — schema invariants, conventions, audit chain rules - `CLAUDE.md` — schema invariants, conventions, audit chain rules
- `~/git/proxy.unturf.com/pkg/verified/merkle.go` — fox's Go merkle - `~/git/proxy.unturf.com/pkg/verified/merkle.go` — fox's Go merkle
reference. Aborist's Python port mirrors the conventions. reference. Arborist's Python port mirrors the conventions.
## Rendering the diagrams ## Rendering the diagrams

View file

@ -2,7 +2,7 @@
**Date:** 2026-05-02 **Date:** 2026-05-02
**Endpoint:** `https://hermes.ai.unturf.com/v1` (Hermes-3-Llama-3.1-8B-FP8-Dynamic, vLLM, 82K ctx) **Endpoint:** `https://hermes.ai.unturf.com/v1` (Hermes-3-Llama-3.1-8B-FP8-Dynamic, vLLM, 82K ctx)
**Corpus:** Wikipedia 2003-05-16 cur snapshot, sharded under `~/.aborist/shards` **Corpus:** Wikipedia 2003-05-16 cur snapshot, sharded under `~/.arborist/shards`
Two sweeps landed today, each 71 questions × 3 modes: Two sweeps landed today, each 71 questions × 3 modes:
@ -28,7 +28,7 @@ Substrate-side (between `2026-05-02T11:31Z` and `2026-05-02T15:07Z`):
- **Sprint 1b** — per-mode `max_context_chars`. Bench's recommended-context-budget table flows back into `DEFAULT_QUERY_POLICY["max_context_chars_by_mode"]`: quote 24 KB, pointer 24 KB, JSON 48 KB. Folds into `governance_policy_hash`. - **Sprint 1b** — per-mode `max_context_chars`. Bench's recommended-context-budget table flows back into `DEFAULT_QUERY_POLICY["max_context_chars_by_mode"]`: quote 24 KB, pointer 24 KB, JSON 48 KB. Folds into `governance_policy_hash`.
- **Sprint 2** — pointer Rule 9: chunk-specificity instruction added to the lattice-pointer system prompt. - **Sprint 2** — pointer Rule 9: chunk-specificity instruction added to the lattice-pointer system prompt.
- **DRY collapse** — the four lattice prompts (system + grounding × pointer + JSON) lifted to `aborist/qa/prompts.py` as a single source of truth, imported by both `runner.DEFAULT_POLICY` and `query.DEFAULT_QUERY_POLICY`. - **DRY collapse** — the four lattice prompts (system + grounding × pointer + JSON) lifted to `arborist/qa/prompts.py` as a single source of truth, imported by both `runner.DEFAULT_POLICY` and `query.DEFAULT_QUERY_POLICY`.
- **httpx persistent client** — the chat-completion path used to construct a fresh `httpx.Client` per call, paying a TLS handshake every request. Move to `__init__`; HTTP/1.1 keep-alive across calls. Save 1-2 min on a 426-call bench. - **httpx persistent client** — the chat-completion path used to construct a fresh `httpx.Client` per call, paying a TLS handshake every request. Move to `__init__`; HTTP/1.1 keep-alive across calls. Save 1-2 min on a 426-call bench.
- **Sample-level shuffled bench scheduling** — every `(question, mode, sample_idx)` is a task, shuffled with `--seed`, dispatched concurrently. Per-cell `Lock` dict serializes burn-then-write on the shared cache_key. True i.i.d. n=3 variance; vLLM batcher fed a diverse request stream. - **Sample-level shuffled bench scheduling** — every `(question, mode, sample_idx)` is a task, shuffled with `--seed`, dispatched concurrently. Per-cell `Lock` dict serializes burn-then-write on the shared cache_key. True i.i.d. n=3 variance; vLLM batcher fed a diverse request stream.
- **Bench `--resume`** — read existing JSONL, skip done tasks, append fresh rows. Stop/start-able. - **Bench `--resume`** — read existing JSONL, skip done tasks, append fresh rows. Stop/start-able.
@ -152,7 +152,7 @@ vLLM saturates at c=3-4 on this endpoint. More concurrent requests fill the batc
Across both benches: **2 errors at 11:31Z, 6 errors at 15:07Z** — all on the same question (`tell me about the roman empire`). Lone UTF-16 surrogates in Wikipedia chunk content, two distinct paths: Across both benches: **2 errors at 11:31Z, 6 errors at 15:07Z** — all on the same question (`tell me about the roman empire`). Lone UTF-16 surrogates in Wikipedia chunk content, two distinct paths:
- v1 (`3b91223`) — hardened SHA-256 hashers on the OUTPUT side (`aborist/qa/dag.py`, `keys.py`, `evidence.py`, `store.py` audit chain) with `errors='surrogatepass'` so the run-DAG roots survive surrogate-bearing model output. - v1 (`3b91223`) — hardened SHA-256 hashers on the OUTPUT side (`arborist/qa/dag.py`, `keys.py`, `evidence.py`, `store.py` audit chain) with `errors='surrogatepass'` so the run-DAG roots survive surrogate-bearing model output.
- v2 (`41d1d9b`) — scrubs message content INSIDE `OpenAICompatibleClient.chat_completion` before httpx's outbound JSON encode. The corpus chunk text was the path; httpx's `.encode('utf-8')` on the request body raised before the call left the client. - v2 (`41d1d9b`) — scrubs message content INSIDE `OpenAICompatibleClient.chat_completion` before httpx's outbound JSON encode. The corpus chunk text was the path; httpx's `.encode('utf-8')` on the request body raised before the call left the client.
Verified: `tell me about the roman empire` under `claim_lattice` now classifies HYBRID 5/7 instead of erroring. Next bench will land 0 errors. Verified: `tell me about the roman empire` under `claim_lattice` now classifies HYBRID 5/7 instead of erroring. Next bench will land 0 errors.

View file

@ -25,7 +25,7 @@ not a model self-grade.
- **Status**: ✓ enforced. - **Status**: ✓ enforced.
- **Code anchors**: - **Code anchors**:
`aborist/qa/verify.py:verify_quotes`, `arborist/qa/verify.py:verify_quotes`,
`verify_claim_lattice`, `verify_claim_lattice_json` — pure functions `verify_claim_lattice`, `verify_claim_lattice_json` — pure functions
with no chat-client parameter. with no chat-client parameter.
- **Pinning tests**: `tests/test_verify*.py`, - **Pinning tests**: `tests/test_verify*.py`,
@ -43,9 +43,9 @@ never types the quote string itself.
- **Status**: ✓ enforced for lattice modes. - **Status**: ✓ enforced for lattice modes.
- **Code anchors**: - **Code anchors**:
`aborist/qa/parse_claims.py:parse_pointer_claims`, `arborist/qa/parse_claims.py:parse_pointer_claims`,
`aborist/qa/verify.py:CLAIM_LATTICE_JSON_SCHEMA`, `arborist/qa/verify.py:CLAIM_LATTICE_JSON_SCHEMA`,
`aborist/qa/runner.py` (system prompts). `arborist/qa/runner.py` (system prompts).
- **Pinning tests**: `tests/test_claim_lattice.py`, - **Pinning tests**: `tests/test_claim_lattice.py`,
`tests/test_verify_json.py`. `tests/test_verify_json.py`.
- **Bench signal**: rows with `answer_mode ∈ - **Bench signal**: rows with `answer_mode ∈
@ -66,9 +66,9 @@ frame and discarding the others.
quantifier classifier + #000010 metacognition QuestionState) quantifier classifier + #000010 metacognition QuestionState)
landed 2026-05-03. Answer-side multi-frame compilation still landed 2026-05-03. Answer-side multi-frame compilation still
pending. pending.
- **Code anchors**: `aborist/qa/quantifier.py:classify_question_quantifier` - **Code anchors**: `arborist/qa/quantifier.py:classify_question_quantifier`
(#000008), `aborist/qa/metacognition.py:preflight_question` (#000008), `arborist/qa/metacognition.py:preflight_question`
(#000010), `aborist/qa/dag.py:preflight_node_hash` / (#000010), `arborist/qa/dag.py:preflight_node_hash` /
`build_preflight_node_payload` (#000009 — five nested CTI `build_preflight_node_payload` (#000009 — five nested CTI
clauses bind into the run-DAG `preflight` stage). Future: clauses bind into the run-DAG `preflight` stage). Future:
runtime-side polarity contract emission for multi-frame answers. runtime-side polarity contract emission for multi-frame answers.
@ -96,17 +96,17 @@ the **preflight contract** governing the run (#000009 — guard /
cap / reminder / reject decisions hash-bound). cap / reminder / reject decisions hash-bound).
- **Status**: ¾ — `evidence_map_root` lives in the run-DAG - **Status**: ¾ — `evidence_map_root` lives in the run-DAG
(`aborist/qa/dag.py`); `retrieval_plan_hash` landed via #000001. (`arborist/qa/dag.py`); `retrieval_plan_hash` landed via #000001.
Preflight stage (#000009) added the upstream control commitment: Preflight stage (#000009) added the upstream control commitment:
classifier output + answer/prompt/evidence contracts + classifier output + answer/prompt/evidence contracts +
`governance_policy_hash` / `model_profile_hash` references all `governance_policy_hash` / `model_profile_hash` references all
fold into a single `preflight` Merkle leaf. Quote mode now 7→8 fold into a single `preflight` Merkle leaf. Quote mode now 7→8
stages; pointer-mode CTI 9→10 stages; reject-broad path emits a stages; pointer-mode CTI 9→10 stages; reject-broad path emits a
3-stage minimal DAG (`question → preflight → final_label`). 3-stage minimal DAG (`question → preflight → final_label`).
- **Code anchors**: `aborist/qa/dag.py:build_run_dag` (legacy + - **Code anchors**: `arborist/qa/dag.py:build_run_dag` (legacy +
preflight-extended shapes), `build_reject_run_dag` (3-stage), preflight-extended shapes), `build_reject_run_dag` (3-stage),
`preflight_node_hash` / `build_preflight_node_payload` (5-clause `preflight_node_hash` / `build_preflight_node_payload` (5-clause
payload), `aborist/qa/evidence.py:evidence_map_root`. payload), `arborist/qa/evidence.py:evidence_map_root`.
- **Pinning tests**: `tests/test_dag.py` (24 tests including - **Pinning tests**: `tests/test_dag.py` (24 tests including
preflight stage + reject path), `tests/test_evidence.py`. preflight stage + reject path), `tests/test_evidence.py`.
- **Open ticket**: [#000001 Retrieval-keywords audit gap](ticket-000001-retrieval-keywords-audit-gap.md) - **Open ticket**: [#000001 Retrieval-keywords audit gap](ticket-000001-retrieval-keywords-audit-gap.md)
@ -125,14 +125,14 @@ citation coverage, pointer-count cap, lexical anchor warrant).
No NLI, no embeddings, no model self-grading. No NLI, no embeddings, no model self-grading.
- **Status**: ✓ enforced. - **Status**: ✓ enforced.
- **Code anchors**: `aborist/qa/verify.py:verify_claim_lattice` — - **Code anchors**: `arborist/qa/verify.py:verify_claim_lattice` —
seven hard checks documented inline. seven hard checks documented inline.
- **Pinning tests**: `tests/test_claim_lattice.py` (per-rule - **Pinning tests**: `tests/test_claim_lattice.py` (per-rule
coverage), `tests/test_verify_json.py`, coverage), `tests/test_verify_json.py`,
`tests/test_verify.py`. `tests/test_verify.py`.
- **Bench signal**: every row carries `verifier_method ∈ {quote, - **Bench signal**: every row carries `verifier_method ∈ {quote,
span, entity, paraphrase, claim_lattice, none}` (CHECK constraint span, entity, paraphrase, claim_lattice, none}` (CHECK constraint
in `aborist/store.py`). in `arborist/store.py`).
### D6 — General anchor-class warrant before semantic NLI ### D6 — General anchor-class warrant before semantic NLI
@ -147,8 +147,8 @@ exists in the proof path.
- **Status**: ½ — relation-shape only. Entity-list / why-cause / - **Status**: ½ — relation-shape only. Entity-list / why-cause /
when-date shapes pass through unchecked. when-date shapes pass through unchecked.
- **Code anchors**: - **Code anchors**:
`aborist/qa/verify.py:_extract_anchor_candidates`, `arborist/qa/verify.py:_extract_anchor_candidates`,
`aborist/qa/verify.py:_question_is_relation_shape`. `arborist/qa/verify.py:_question_is_relation_shape`.
- **Pinning tests**: `tests/test_claim_lattice.py` covers - **Pinning tests**: `tests/test_claim_lattice.py` covers
relation-shape; entity-list / cause / date shapes have no warrant relation-shape; entity-list / cause / date shapes have no warrant
test today. test today.
@ -175,7 +175,7 @@ stays `STRICT`/`HYBRID`/`UNGROUNDED` so v9.8 cache_key invariants
hold. hold.
- **Status**: ✓ enforced for lattice modes. - **Status**: ✓ enforced for lattice modes.
- **Code anchors**: `aborist/cli.py:_render_audit_label`. - **Code anchors**: `arborist/cli.py:_render_audit_label`.
- **Pinning tests**: `tests/test_cli_render.py` - **Pinning tests**: `tests/test_cli_render.py`
`test_render_label_strict_in_claim_lattice_becomes_evidence_linked` `test_render_label_strict_in_claim_lattice_becomes_evidence_linked`
and siblings. and siblings.
@ -253,7 +253,7 @@ passed at the original snapshot.
Three additional tickets landed extending D1, D3, and D4: Three additional tickets landed extending D1, D3, and D4:
- **#000008** (closed `4f2b5a6`, 2026-05-03) — Broad-quantifier - **#000008** (closed `4f2b5a6`, 2026-05-03) — Broad-quantifier
preflight guard. New modules `aborist/qa/quantifier.py`, preflight guard. New modules `arborist/qa/quantifier.py`,
`model_profiles.py`, `quantifier_reminder.py`. Seven new `model_profiles.py`, `quantifier_reminder.py`. Seven new
policy fields fold into `governance_policy_hash`; six-level policy fields fold into `governance_policy_hash`; six-level
disable hierarchy. Defaults flipped (after n=5 verification): disable hierarchy. Defaults flipped (after n=5 verification):
@ -261,7 +261,7 @@ Three additional tickets landed extending D1, D3, and D4:
opt-in with JSON-only allowlist. opt-in with JSON-only allowlist.
- **#000010** (closed, 2026-05-03) — Meta-Cognition Preflight - **#000010** (closed, 2026-05-03) — Meta-Cognition Preflight
Guard. New module `aborist/qa/metacognition.py` with Guard. New module `arborist/qa/metacognition.py` with
`QuestionState` dataclass + four pure detectors (temporal, `QuestionState` dataclass + four pure detectors (temporal,
contradiction, false-premise-lite, out-of-corpus). Six new contradiction, false-premise-lite, out-of-corpus). Six new
policy fields, all default-on except policy fields, all default-on except

View file

@ -62,7 +62,7 @@ aren't even queryable to know which records to re-run.
### 1.2 What gets logged today ### 1.2 What gets logged today
The `run_dag` retrieval stage at `aborist/qa/dag.py:155` hashes only the The `run_dag` retrieval stage at `arborist/qa/dag.py:155` hashes only the
`sources_summary` (output): `sources_summary` (output):
```python ```python
@ -164,27 +164,27 @@ question." The 8-dim `cache_key` invariant stays intact.
## 3. Implementation sketch (when scheduled) ## 3. Implementation sketch (when scheduled)
1. **`aborist/qa/dag.py`** — `build_run_dag` gains `retrieval_inputs` 1. **`arborist/qa/dag.py`** — `build_run_dag` gains `retrieval_inputs`
parameter; embeds it into the retrieval-stage hash. Backward-compat: parameter; embeds it into the retrieval-stage hash. Backward-compat:
when `retrieval_inputs` is None, fall back to the current when `retrieval_inputs` is None, fall back to the current
sources-summary-only hash. sources-summary-only hash.
2. **`aborist/qa/query.py`** — at the existing run_dag construction 2. **`arborist/qa/query.py`** — at the existing run_dag construction
site, pass `{"question": ..., "retrieval_keywords": ..., site, pass `{"question": ..., "retrieval_keywords": ...,
"top_k": ..., "over_fetch": ..., "max_context_chars": ...}` as "top_k": ..., "over_fetch": ..., "max_context_chars": ...}` as
`retrieval_inputs`. `retrieval_inputs`.
3. **`aborist/store.py`** — schema migration: add 3. **`arborist/store.py`** — schema migration: add
`retrieval_keywords TEXT` (nullable) to `providence_cache`. Mirror `retrieval_keywords TEXT` (nullable) to `providence_cache`. Mirror
the existing `_rebuild_providence_cache_*` pattern only if a CHECK the existing `_rebuild_providence_cache_*` pattern only if a CHECK
constraint or column-default constraint requires it (otherwise a constraint or column-default constraint requires it (otherwise a
plain `ALTER TABLE ... ADD COLUMN` suffices). plain `ALTER TABLE ... ADD COLUMN` suffices).
4. **`aborist/qa/query.py`** persist site — include `retrieval_keywords` 4. **`arborist/qa/query.py`** persist site — include `retrieval_keywords`
in the INSERT. in the INSERT.
5. **`aborist/qa/verify.py`** + relevant CLI render — surface keywords 5. **`arborist/qa/verify.py`** + relevant CLI render — surface keywords
in `aborist providence` output and `aborist inspect` so an operator in `arborist providence` output and `arborist inspect` so an operator
can see at a glance whether a record was retrieval-augmented. can see at a glance whether a record was retrieval-augmented.
6. **Tests:** 6. **Tests:**
@ -233,16 +233,16 @@ question." The 8-dim `cache_key` invariant stays intact.
**Closed 2026-05-02.** Run-DAG binding scope landed: **Closed 2026-05-02.** Run-DAG binding scope landed:
- New module `aborist/qa/retrieval_plan.py` — `RetrievalPlan` - New module `arborist/qa/retrieval_plan.py` — `RetrievalPlan`
dataclass + `retrieval_plan_hash` function. Captures dataclass + `retrieval_plan_hash` function. Captures
`retrieval_keywords`, `top_k`, `over_fetch`, `max_context_chars`, `retrieval_keywords`, `top_k`, `over_fetch`, `max_context_chars`,
`shard_ids`. SHA-256 over canonical-JSON; deterministic per call. `shard_ids`. SHA-256 over canonical-JSON; deterministic per call.
- `aborist/qa/dag.py:build_run_dag` accepts `retrieval_plan_hash` - `arborist/qa/dag.py:build_run_dag` accepts `retrieval_plan_hash`
parameter. When provided, the retrieval stage hash binds BOTH parameter. When provided, the retrieval stage hash binds BOTH
the plan (input) and `sources_summary` (output). When omitted, the plan (input) and `sources_summary` (output). When omitted,
falls back to the historical sources-summary-only hash so falls back to the historical sources-summary-only hash so
pre-#000001 records keep their `run_dag_root` values stable. pre-#000001 records keep their `run_dag_root` values stable.
- `aborist/qa/query.py` constructs the plan per call from the - `arborist/qa/query.py` constructs the plan per call from the
query-time inputs (keywords from `--retrieval-keywords`, query-time inputs (keywords from `--retrieval-keywords`,
`top_k` / `over_fetch` / `max_context_chars` from policy + `top_k` / `over_fetch` / `max_context_chars` from policy +
args) plus the resolved shard set, computes the hash, and args) plus the resolved shard set, computes the hash, and
@ -791,7 +791,7 @@ J — Retrieval Plan Provenance Binding
### A.10 Concrete diff ### A.10 Concrete diff
#### `aborist/qa/retrieval.py` #### `arborist/qa/retrieval.py`
Return a structured object: Return a structured object:
@ -822,14 +822,14 @@ class RetrievalPlan:
} }
``` ```
#### `aborist/qa/hash.py` #### `arborist/qa/hash.py`
```python ```python
def retrieval_plan_hash(plan: RetrievalPlan) -> str: def retrieval_plan_hash(plan: RetrievalPlan) -> str:
return hash_json("aborist-retrieval-plan-v1", plan.canonical()) return hash_json("arborist-retrieval-plan-v1", plan.canonical())
``` ```
#### `aborist/qa/dag.py` #### `arborist/qa/dag.py`
Change retrieval node from: Change retrieval node from:

View file

@ -120,7 +120,7 @@ The additive insight:
Three new pieces, each scoped tight: Three new pieces, each scoped tight:
### 4.1 Frame detector (`aborist/qa/frame.py`) ### 4.1 Frame detector (`arborist/qa/frame.py`)
Given a question + retrieved sources, detect the reference frame: Given a question + retrieved sources, detect the reference frame:
@ -242,7 +242,7 @@ To add when this ticket lands:
**Closed 2026-05-02.** Landed via: **Closed 2026-05-02.** Landed via:
- New module `aborist/qa/frame.py` — `FrameDetection` dataclass - New module `arborist/qa/frame.py` — `FrameDetection` dataclass
+ `detect_frame(question, sources, phrase_match_roots)`. Detects + `detect_frame(question, sources, phrase_match_roots)`. Detects
reference-frame queries via the conjunction of (phrase route reference-frame queries via the conjunction of (phrase route
fired) AND (phrase-matched source is a reference work — title fired) AND (phrase-matched source is a reference work — title
@ -251,7 +251,7 @@ To add when this ticket lands:
`protagonist` / `plot`). Distinct-marker count (not total) keeps `protagonist` / `plot`). Distinct-marker count (not total) keeps
the heuristic robust against single-marker repetition (e.g. a the heuristic robust against single-marker repetition (e.g. a
history article saying "novel approach" twice doesn't trip). history article saying "novel approach" twice doesn't trip).
- `aborist/qa/query.py` calls `detect_frame` for lattice modes, - `arborist/qa/query.py` calls `detect_frame` for lattice modes,
using the article LEAD (chunk_idx=0, post-wikitext-strip) as the using the article LEAD (chunk_idx=0, post-wikitext-strip) as the
body sample so fiction markers cluster where they appear on body sample so fiction markers cluster where they appear on
Wikipedia (lead paragraph). Wikipedia (lead paragraph).

View file

@ -20,7 +20,7 @@ covers that path separately).
## 1. Problem statement ## 1. Problem statement
Today's warrant-lite (`aborist/qa/warrant.py:warrant_check`) covers Today's warrant-lite (`arborist/qa/warrant.py:warrant_check`) covers
two anchor classes: two anchor classes:
- **Proper-noun anchors** (relation-shape questions): for - **Proper-noun anchors** (relation-shape questions): for
@ -128,7 +128,7 @@ shape to constrain it.
### 3.1 Question-shape detector extension ### 3.1 Question-shape detector extension
`aborist/qa/warrant.py` already has `_question_is_relation_shape`. `arborist/qa/warrant.py` already has `_question_is_relation_shape`.
Add: Add:
```python ```python
@ -251,7 +251,7 @@ To add when this ticket lands:
## 6. Status ## 6. Status
**Closed 2026-05-02.** Landed via per-shape detectors + extractors **Closed 2026-05-02.** Landed via per-shape detectors + extractors
in `aborist/qa/warrant.py`: in `arborist/qa/warrant.py`:
- `_question_is_entity_list_shape` / `_question_is_count_shape` / - `_question_is_entity_list_shape` / `_question_is_count_shape` /
`_question_is_why_shape` regex detectors. `_question_is_why_shape` regex detectors.

View file

@ -110,13 +110,13 @@ ladder migration render under the new ladder identically.
## 5. Implementation sketch ## 5. Implementation sketch
1. **Verifier enhancement** (`aborist/qa/verify.py`): both 1. **Verifier enhancement** (`arborist/qa/verify.py`): both
`verify_claim_lattice` and `verify_claim_lattice_json` add a `verify_claim_lattice` and `verify_claim_lattice_json` add a
`verifier_steps_ran: list[str]` field on the verdict dict `verifier_steps_ran: list[str]` field on the verdict dict
listing which verifier paths fired (`pointer_resolve`, listing which verifier paths fired (`pointer_resolve`,
`source_role`, `coverage`, `pointer_cap`, `warrant`, `source_role`, `coverage`, `pointer_cap`, `warrant`,
`lazy_anchor_demote`). `lazy_anchor_demote`).
2. **Renderer mapping** (`aborist/cli.py:_render_audit_label`): 2. **Renderer mapping** (`arborist/cli.py:_render_audit_label`):
add the four-rung mapping function gated on add the four-rung mapping function gated on
`verifier_method.startswith("claim_lattice")`. `verifier_method.startswith("claim_lattice")`.
3. **Bench update** (`bench/qa_sweep.py`): per-row 3. **Bench update** (`bench/qa_sweep.py`): per-row
@ -159,7 +159,7 @@ ladder migration render under the new ladder identically.
**Closed 2026-05-02.** Landed via: **Closed 2026-05-02.** Landed via:
- `_render_audit_label(audit_mode, verifier_method, violations)` - `_render_audit_label(audit_mode, verifier_method, violations)`
in `aborist/cli.py` — three-arg signature; `violations` defaults in `arborist/cli.py` — three-arg signature; `violations` defaults
to `None` for backward-compat with callers that don't have the to `None` for backward-compat with callers that don't have the
list yet. list yet.
- `_ladder_rung_for_lattice` helper computes the rung from - `_ladder_rung_for_lattice` helper computes the rung from

View file

@ -20,7 +20,7 @@ discipline (POINTER-LINKED → ANCHOR-WARRANTED → EVIDENCE-WARRANTED
1. Three random words from `/usr/share/dict/words` (`^[a-z]{5,12}$`). 1. Three random words from `/usr/share/dict/words` (`^[a-z]{5,12}$`).
2. Hermes @ temp=0.8 wove a creative question paragraph. 2. Hermes @ temp=0.8 wove a creative question paragraph.
3. Aborist answered via `claim_lattice` mode (full pipeline). 3. Arborist answered via `claim_lattice` mode (full pipeline).
4. Append journey to `bench/emergent_log.jsonl`. 4. Append journey to `bench/emergent_log.jsonl`.
Aggregate distribution: Aggregate distribution:
@ -297,7 +297,7 @@ language-discourse vocabulary. The model wove a definition-style
answer using overlapping vocabulary that the verifier's per-claim answer using overlapping vocabulary that the verifier's per-claim
citation-coverage check passed. citation-coverage check passed.
`aborist inspect --cache-key 08dbd2c1...` confirms STRICT 1/1 `arborist inspect --cache-key 08dbd2c1...` confirms STRICT 1/1
verified, no unverified spans, no violations. The verifier verified, no unverified spans, no violations. The verifier
genuinely accepted the claim. genuinely accepted the claim.
@ -347,7 +347,7 @@ that the lexical Rule-5 check cannot catch alone.
1. **Rule 9 — SUBJECT_TOKENS_ABSENT detector — landed inline (no 1. **Rule 9 — SUBJECT_TOKENS_ABSENT detector — landed inline (no
separate ticket).** New per-claim check in separate ticket).** New per-claim check in
`aborist/qa/verify.py`. For each resolving claim, compute the `arborist/qa/verify.py`. For each resolving claim, compute the
set of content tokens shared by question AND claim set of content tokens shared by question AND claim
(`_parroted_subject_tokens_absent`). For each, check substring (`_parroted_subject_tokens_absent`). For each, check substring
presence in the union of cited evidence spans (lower-cased, presence in the union of cited evidence spans (lower-cased,

View file

@ -98,7 +98,7 @@ breadth metric for non-hyphen queries.
- Zero blast radius for non-hyphen queries. - Zero blast radius for non-hyphen queries.
**Cons:** **Cons:**
- Body-FTS still uses `aborist/search/fts5.py:_query_tokens` (a - Body-FTS still uses `arborist/search/fts5.py:_query_tokens` (a
separate function) and stays on AND-mode `bi AND polar AND rare`. separate function) and stays on AND-mode `bi AND polar AND rare`.
Body-FTS won't pull in `Bipolar disorder` directly. Title-route + Body-FTS won't pull in `Bipolar disorder` directly. Title-route +
accept-path 5 + rerank carries the recall load. Acceptable: title accept-path 5 + rerank carries the recall load. Acceptable: title
@ -140,7 +140,7 @@ bench shows it's worth the version-bump cost.
## 3. Implementation sketch ## 3. Implementation sketch
1. **`aborist/qa/query.py`** — new module-level constant 1. **`arborist/qa/query.py`** — new module-level constant
`_HYPHEN_RUN_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)+")` `_HYPHEN_RUN_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)+")`
plus pure helper: plus pure helper:
@ -234,7 +234,7 @@ bench shows it's worth the version-bump cost.
Closed 2026-05-02. Landed Option A as designed: Closed 2026-05-02. Landed Option A as designed:
- `aborist/qa/query.py` — `_HYPHEN_RUN_RE` constant + - `arborist/qa/query.py` — `_HYPHEN_RUN_RE` constant +
`_hyphen_fold_variants(s)` helper. `_title_query_tokens(s)` `_hyphen_fold_variants(s)` helper. `_title_query_tokens(s)`
additively merges the variants. `_filter_by_title_relevance` additively merges the variants. `_filter_by_title_relevance`
gains optional `hyphen_fold_anchors` kwarg + accept-path 5 gains optional `hyphen_fold_anchors` kwarg + accept-path 5
@ -242,8 +242,8 @@ Closed 2026-05-02. Landed Option A as designed:
when title-breadth fails). `_rerank` threads the kwarg. when title-breadth fails). `_rerank` threads the kwarg.
`_search_corpus` caller computes `_search_corpus` caller computes
`_hyphen_fold_variants(retrieval_query)` and passes through. `_hyphen_fold_variants(retrieval_query)` and passes through.
- `aborist/qa/query.py:DEFAULT_QUERY_POLICY` and - `arborist/qa/query.py:DEFAULT_QUERY_POLICY` and
`aborist/qa/runner.py:DEFAULT_POLICY` — `hyphen_fold_v1: True` `arborist/qa/runner.py:DEFAULT_POLICY` — `hyphen_fold_v1: True`
marker. Folds into `governance_policy_hash` so records marker. Folds into `governance_policy_hash` so records
produced under the new rule cache-split cleanly. produced under the new rule cache-split cleanly.
- `tests/test_query.py` — 4 new tests: - `tests/test_query.py` — 4 new tests:

View file

@ -61,7 +61,7 @@ intensity (Section 2). Fox sketched it as:
The categories below derive from formal-semantics quantifier theory The categories below derive from formal-semantics quantifier theory
(Mostowski generalized quantifiers; Barwise-Cooper; Partee D- vs A- (Mostowski generalized quantifiers; Barwise-Cooper; Partee D- vs A-
quantifiers) intersected with the operational axis aborist actually quantifiers) intersected with the operational axis arborist actually
needs: **expected number of claims in the answer**. Categories that needs: **expected number of claims in the answer**. Categories that
don't change the expected answer length are dropped from this table don't change the expected answer length are dropped from this table
and surfaced as adjacent dimensions in Section 2.1. and surfaced as adjacent dimensions in Section 2.1.
@ -114,7 +114,7 @@ ladder, and may warrant their own ticket later.
modify a numeric bound (`about ten`) — fold into the modify a numeric bound (`about ten`) — fold into the
SMALL_NUM_EXPLICIT or COMPARATIVE_BOUND rung with the bound. SMALL_NUM_EXPLICIT or COMPARATIVE_BOUND rung with the bound.
### 2.2 Why these specific categories matter for aborist ### 2.2 Why these specific categories matter for arborist
Each new rung names a *distinct expected-answer-length distribution*: Each new rung names a *distinct expected-answer-length distribution*:
@ -226,7 +226,7 @@ Original (superseded):
Land Option D. Concrete plan: Land Option D. Concrete plan:
1. New module `aborist/qa/quantifier.py` with 1. New module `arborist/qa/quantifier.py` with
`classify_question_quantifier(question: str) -> dict` returning `classify_question_quantifier(question: str) -> dict` returning
`{"intensity": "ALL"|"MANY"|"FEW"|"SMALL_NUM"|"SINGULAR"|"ABSENT", `{"intensity": "ALL"|"MANY"|"FEW"|"SMALL_NUM"|"SINGULAR"|"ABSENT",
"matched_token": str, "explicit_count": int | None}`. Pure "matched_token": str, "explicit_count": int | None}`. Pure
@ -240,9 +240,9 @@ Land Option D. Concrete plan:
across the board, so opting in costs nothing). across the board, so opting in costs nothing).
3. Per-model profile registry. `model_profile_hash` (already in the 3. Per-model profile registry. `model_profile_hash` (already in the
8-dim cache key) gets a quantifier-cap profile attached. The 8-dim cache key) gets a quantifier-cap profile attached. The
profile lives in `aborist/qa/model_profiles.py` (new file). profile lives in `arborist/qa/model_profiles.py` (new file).
4. Preflight wiring in `aborist/qa/runner.py:ask` and 4. Preflight wiring in `arborist/qa/runner.py:ask` and
`aborist/qa/query.py:query`: classify the question, look up the `arborist/qa/query.py:query`: classify the question, look up the
intensity-keyed cap from the model profile, override the intensity-keyed cap from the model profile, override the
`claim_lattice_max_claims_per_answer` for this call. `claim_lattice_max_claims_per_answer` for this call.
5. Optional reminder injection in the prompt builder (gated on 5. Optional reminder injection in the prompt builder (gated on
@ -366,7 +366,7 @@ Originally tracked here as a gap — closed in this same session.
Landed: Landed:
- `aborist/qa/query.py:2580-2592` — `format_collapsed` and - `arborist/qa/query.py:2580-2592` — `format_collapsed` and
`raw_answer` surfaced on the `query()` result dict. `raw_answer` surfaced on the `query()` result dict.
- `bench/qa_sweep.py:_run_one` — three new fields per row: - `bench/qa_sweep.py:_run_one` — three new fields per row:
`format_collapsed`, `violation_kinds` (sorted unique kind `format_collapsed`, `violation_kinds` (sorted unique kind
@ -428,7 +428,7 @@ enhancement, not blocking.
MANY? The "tell me all there is to know" precedent (york-england MANY? The "tell me all there is to know" precedent (york-england
case, ticket #000006) suggests yes — operationally it produces case, ticket #000006) suggests yes — operationally it produces
the same enumeration pressure. the same enumeration pressure.
- Does the per-model profile belong in `aborist/qa/model_profiles.py` - Does the per-model profile belong in `arborist/qa/model_profiles.py`
or extend the existing `model_profile_hash` derivation in or extend the existing `model_profile_hash` derivation in
`keys.py`? Initial answer: new file, hash-derived from the profile `keys.py`? Initial answer: new file, hash-derived from the profile
dict so changing a cap invalidates prior records. dict so changing a cap invalidates prior records.
@ -441,7 +441,7 @@ enhancement, not blocking.
surfaces through the existing TOO_MANY_CLAIMS violation path on surfaces through the existing TOO_MANY_CLAIMS violation path on
the audit-line tail. the audit-line tail.
- This ticket does NOT touch retrieval. Quantifier guard runs at - This ticket does NOT touch retrieval. Quantifier guard runs at
policy layer only; retrieval pipeline (`aborist/qa/query.py` policy layer only; retrieval pipeline (`arborist/qa/query.py`
Sections 19) stays untouched. Sections 19) stays untouched.
## 8. Status ## 8. Status
@ -476,9 +476,9 @@ Implementation commit chain (all on `origin/main`):
|-----------|-------|----------------------------------------------------------|-------| |-----------|-------|----------------------------------------------------------|-------|
| `2ffed00` | 0 | bench-harness extension (FC rate, violation kinds) | +5 | | `2ffed00` | 0 | bench-harness extension (FC rate, violation kinds) | +5 |
| `5e8d662` | 0.x | bench-row telemetry (pointer/bracket/profile fields) | +5 | | `5e8d662` | 0.x | bench-row telemetry (pointer/bracket/profile fields) | +5 |
| `926b05e` | 1 | pure quantifier classifier `aborist/qa/quantifier.py` | +61 | | `926b05e` | 1 | pure quantifier classifier `arborist/qa/quantifier.py` | +61 |
| `84d5b5c` | 2 | model-profile caps `aborist/qa/model_profiles.py` | +19 | | `84d5b5c` | 2 | model-profile caps `arborist/qa/model_profiles.py` | +19 |
| `6f90f21` | 3 | reminder injection `aborist/qa/quantifier_reminder.py` | +19 | | `6f90f21` | 3 | reminder injection `arborist/qa/quantifier_reminder.py` | +19 |
| `5a60e85` | 4 | CLI flags + 4 violation kinds + reject-broad early-return | +16 | | `5a60e85` | 4 | CLI flags + 4 violation kinds + reject-broad early-return | +16 |
Total: **906 tests passing (120 new); 36 skipped.** Total: **906 tests passing (120 new); 36 skipped.**
@ -503,7 +503,7 @@ quantifier_guard_modes ["claim_lattice_pointer",
reject_broad, caps_by_intensity, guard_modes, plus Phase 0 reject_broad, caps_by_intensity, guard_modes, plus Phase 0
format_collapse_check_enabled) format_collapse_check_enabled)
4. Per-mode: `quantifier_guard_modes` (quote opts out by default) 4. Per-mode: `quantifier_guard_modes` (quote opts out by default)
5. Per-model: `aborist/qa/model_profiles.py` PROFILES dict 5. Per-model: `arborist/qa/model_profiles.py` PROFILES dict
6. Master via governance: all 7 quantifier policy fields fold into 6. Master via governance: all 7 quantifier policy fields fold into
`_VERIFIER_POLICY_FIELDS` so flipping any of them invalidates `_VERIFIER_POLICY_FIELDS` so flipping any of them invalidates
prior cache records on lookup prior cache records on lookup
@ -511,7 +511,7 @@ quantifier_guard_modes ["claim_lattice_pointer",
Live end-to-end verification (post-`5a60e85`): Live end-to-end verification (post-`5a60e85`):
``` ```
$ aborist query --reject-broad "Winners of all major sports?" $ arborist query --reject-broad "Winners of all major sports?"
Winners of all major sports? Winners of all major sports?
UNGROUNDED · via BROAD_QUANTIFIER_REJECTED · ALL ("all") UNGROUNDED · via BROAD_QUANTIFIER_REJECTED · ALL ("all")
· cap was 8 0/0 0.0s (preflight) · cap was 8 0/0 0.0s (preflight)
@ -521,7 +521,7 @@ Winners of all major sports?
universe. Narrow ... or run with --allow-broad for exploratory universe. Narrow ... or run with --allow-broad for exploratory
enumeration. enumeration.
$ aborist query --reject-broad "name all members of the Beatles" $ arborist query --reject-broad "name all members of the Beatles"
name all members of the Beatles name all members of the Beatles
UNGROUNDED · via claim_lattice · title mismatch 4/4 20.9s UNGROUNDED · via claim_lattice · title mismatch 4/4 20.9s
[Beatles enumerated; scope_bound_hint=bounded → not rejected] [Beatles enumerated; scope_bound_hint=bounded → not rejected]
@ -747,7 +747,7 @@ _RE`, `_POINTER_ID_RE`) avoid per-row recompilation.
#### Phase 1 — Pure quantifier classifier (LANDED `926b05e`) #### Phase 1 — Pure quantifier classifier (LANDED `926b05e`)
`aborist/qa/quantifier.py` with `classify_question_quantifier()`, `arborist/qa/quantifier.py` with `classify_question_quantifier()`,
pure function (no I/O, no model call, no retrieval call). Returns: pure function (no I/O, no model call, no retrieval call). Returns:
```json ```json
@ -780,7 +780,7 @@ refinement.
#### Phase 2 — Model-profile claim-budget contract (LANDED `84d5b5c`) #### Phase 2 — Model-profile claim-budget contract (LANDED `84d5b5c`)
`aborist/qa/model_profiles.py` ships two profiles: `arborist/qa/model_profiles.py` ships two profiles:
- `adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic` — tight caps for - `adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic` — tight caps for
broad intensities (ALL=8, COMPREHENSIVE=5, OPEN_REQUEST=5). broad intensities (ALL=8, COMPREHENSIVE=5, OPEN_REQUEST=5).
@ -821,7 +821,7 @@ the default with the looked-up cap.
#### Phase 3 — Prompt reminder (LANDED `6f90f21`, default OFF) #### Phase 3 — Prompt reminder (LANDED `6f90f21`, default OFF)
`aborist/qa/quantifier_reminder.py` ships `arborist/qa/quantifier_reminder.py` ships
`broad_quantifier_reminder()` with two templates: `broad_quantifier_reminder()` with two templates:
- **Bounded universe** (`all members of the Beatles`): - **Bounded universe** (`all members of the Beatles`):
@ -854,7 +854,7 @@ beyond the §10.8 5pp floor.
#### Phase 4 — CLI flags + violation tails (LANDED `5a60e85`) #### Phase 4 — CLI flags + violation tails (LANDED `5a60e85`)
CLI flags on `aborist query`: CLI flags on `arborist query`:
``` ```
--no-quantifier-guard Level 2 disable: kills guard for one call --no-quantifier-guard Level 2 disable: kills guard for one call
@ -1204,7 +1204,7 @@ BROAD_QUANTIFIER_SCOPE_UNBOUND scope-bound hint fired in 10.1;
``` ```
Folds into `governance_policy_hash` via the §1 hard constraint and Folds into `governance_policy_hash` via the §1 hard constraint and
into `_SOFT_DEMOTE_VIOLATION_KINDS` in `aborist/cli.py`. into `_SOFT_DEMOTE_VIOLATION_KINDS` in `arborist/cli.py`.
This keeps the schema and the four-rung ladder unchanged. Display This keeps the schema and the four-rung ladder unchanged. Display
labels carry the broad-quantifier signal via the existing tail labels carry the broad-quantifier signal via the existing tail
@ -1304,7 +1304,7 @@ answer_pointer_count: int count of [E\d+] tags in raw_answer
answer_chars_with_brackets: int char count of bracket-tagged regions answer_chars_with_brackets: int char count of bracket-tagged regions
model_profile_id: str e.g. "hermes-3-llama-3.1-8b-fp8" model_profile_id: str e.g. "hermes-3-llama-3.1-8b-fp8"
(with the FP8 suffix; the (with the FP8 suffix; the
ABORIST_LLM_MODEL default uses the ARBORIST_LLM_MODEL default uses the
FP8-Dynamic variant) FP8-Dynamic variant)
scope_bound_hint: str classifier output (10.1): scope_bound_hint: str classifier output (10.1):
bounded / unbounded / unknown bounded / unbounded / unknown
@ -1457,7 +1457,7 @@ From most granular to most global:
Level 1 — Per-test override Level 1 — Per-test override
Tests pass policy={"quantifier_guard_enabled": False}. Tests pass policy={"quantifier_guard_enabled": False}.
Same mechanism claim_lattice_warrant_check_enabled already uses Same mechanism claim_lattice_warrant_check_enabled already uses
(aborist/qa/runner.py:191). One line. No environment changes. (arborist/qa/runner.py:191). One line. No environment changes.
Level 2 — Per-call CLI flag Level 2 — Per-call CLI flag
--no-quantifier-guard kills Phase 1+2 for one call --no-quantifier-guard kills Phase 1+2 for one call
@ -1483,7 +1483,7 @@ Level 4 — Per-mode gating
on baseline; its failure shape is different. on baseline; its failure shape is different.
Level 5 — Per-model profile Level 5 — Per-model profile
aborist/qa/model_profiles.py keys by model_profile_id. arborist/qa/model_profiles.py keys by model_profile_id.
Models without an entry fall back to the "default" profile, Models without an entry fall back to the "default" profile,
which has the conservative caps from §10.6 (no Hermes-tight which has the conservative caps from §10.6 (no Hermes-tight
tightening). tightening).
@ -1612,18 +1612,18 @@ Single-source-of-truth for **what was actually built** in the
| Path | Purpose | Phase | | Path | Purpose | Phase |
|-------------------------------------|------------------------------------------------------------------------|-------| |-------------------------------------|------------------------------------------------------------------------|-------|
| `aborist/qa/quantifier.py` | Pure 10-rung classifier + scope-bound heuristic | 1 | | `arborist/qa/quantifier.py` | Pure 10-rung classifier + scope-bound heuristic | 1 |
| `aborist/qa/model_profiles.py` | PROFILES dict + `cap_for_intensity()` + `EXPLICIT_COUNT` sentinel | 2 | | `arborist/qa/model_profiles.py` | PROFILES dict + `cap_for_intensity()` + `EXPLICIT_COUNT` sentinel | 2 |
| `aborist/qa/quantifier_reminder.py` | `broad_quantifier_reminder()` with bounded vs unbounded templates | 3 | | `arborist/qa/quantifier_reminder.py` | `broad_quantifier_reminder()` with bounded vs unbounded templates | 3 |
### 11.2 Modified modules (new behavior) ### 11.2 Modified modules (new behavior)
| Path | Change | Phase | | Path | Change | Phase |
|-----------------------|---------------------------------------------------------------------------------------------|-------| |-----------------------|---------------------------------------------------------------------------------------------|-------|
| `aborist/qa/query.py` | classifier wired post-policy-resolution; `effective_max_claims` lookup; reject-broad early-return; reminder injection | 1, 2, 3, 4 | | `arborist/qa/query.py` | classifier wired post-policy-resolution; `effective_max_claims` lookup; reject-broad early-return; reminder injection | 1, 2, 3, 4 |
| `aborist/qa/runner.py` | same wiring as query() for `aborist ask` parity; new policy fields in DEFAULT_POLICY | 1, 2, 3, 4 | | `arborist/qa/runner.py` | same wiring as query() for `arborist ask` parity; new policy fields in DEFAULT_POLICY | 1, 2, 3, 4 |
| `aborist/qa/keys.py` | 7 new fields in `_VERIFIER_POLICY_FIELDS` for governance-hash binding | 2, 3, 4 | | `arborist/qa/keys.py` | 7 new fields in `_VERIFIER_POLICY_FIELDS` for governance-hash binding | 2, 3, 4 |
| `aborist/cli.py` | 4 new CLI flags; 4 new violation kinds in `_SOFT_DEMOTE_VIOLATION_KINDS`; `_render_warrant_tail` extended; `_render_query_human` branch for `broad_quantifier_rejected` status | 4 | | `arborist/cli.py` | 4 new CLI flags; 4 new violation kinds in `_SOFT_DEMOTE_VIOLATION_KINDS`; `_render_warrant_tail` extended; `_render_query_human` branch for `broad_quantifier_rejected` status | 4 |
| `bench/qa_sweep.py` | `_bracket_diagnostics()`; module regexes; 8 new fields in `_run_one`; aggregation in `_summarize`; new markdown section | 0, 0.x | | `bench/qa_sweep.py` | `_bracket_diagnostics()`; module regexes; 8 new fields in `_run_one`; aggregation in `_summarize`; new markdown section | 0, 0.x |
### 11.3 New test files ### 11.3 New test files
@ -1640,7 +1640,7 @@ Total new tests: **120**; full suite **906 passing, 36 skipped.**
### 11.4 New policy fields (governance-hash bound) ### 11.4 New policy fields (governance-hash bound)
All seven fold into `_VERIFIER_POLICY_FIELDS` (`aborist/qa/keys.py`) All seven fold into `_VERIFIER_POLICY_FIELDS` (`arborist/qa/keys.py`)
so flipping any of them invalidates prior cache records on lookup. so flipping any of them invalidates prior cache records on lookup.
``` ```
@ -1653,7 +1653,7 @@ quantifier_reject_broad False Phase 4 reject default off
claim_lattice_format_collapse_check_enabled True (Phase 0 prereq) claim_lattice_format_collapse_check_enabled True (Phase 0 prereq)
``` ```
### 11.5 New CLI flags (`aborist query`) ### 11.5 New CLI flags (`arborist query`)
``` ```
--no-quantifier-guard Level 2 disable (kills guard for one call) --no-quantifier-guard Level 2 disable (kills guard for one call)
@ -1666,7 +1666,7 @@ claim_lattice_format_collapse_check_enabled True (Phase 0 prereq)
Three soft demotes (cap ladder at ANCHOR-WARRANTED) + one hard demote Three soft demotes (cap ladder at ANCHOR-WARRANTED) + one hard demote
(early-return UNGROUNDED). All four render audit-line tails via (early-return UNGROUNDED). All four render audit-line tails via
`_render_warrant_tail` in `aborist/cli.py`: `_render_warrant_tail` in `arborist/cli.py`:
``` ```
BROAD_QUANTIFIER_RUNAWAY soft → "broad runaway" BROAD_QUANTIFIER_RUNAWAY soft → "broad runaway"
@ -1711,33 +1711,33 @@ Choices made during implementation that didn't pre-exist in §9 / §10
but are now binding via tests + commits: but are now binding via tests + commits:
1. **`RUNG_PRIORITY` puts COMPREHENSIVE above OPEN_REQUEST** 1. **`RUNG_PRIORITY` puts COMPREHENSIVE above OPEN_REQUEST**
(`aborist/qa/quantifier.py:_RUNG_PRIORITY`). Caught during Phase 1 (`arborist/qa/quantifier.py:_RUNG_PRIORITY`). Caught during Phase 1
tests when `tell me everything about all wars` matched both tests when `tell me everything about all wars` matched both
templates. COMPREHENSIVE wins because §2.2 calls it "strictly templates. COMPREHENSIVE wins because §2.2 calls it "strictly
stronger than ALL"; OPEN_REQUEST is a softer shape-detector. stronger than ALL"; OPEN_REQUEST is a softer shape-detector.
2. **Bounded-domain anchor regex list** 2. **Bounded-domain anchor regex list**
(`aborist/qa/quantifier.py:_BOUNDED_DOMAIN_ANCHORS`). Hand-curated (`arborist/qa/quantifier.py:_BOUNDED_DOMAIN_ANCHORS`). Hand-curated
list covering Beatles, US states/presidents, planets, founding list covering Beatles, US states/presidents, planets, founding
fathers, continents, oceans, plus a year-anchor `(?:19|20)\d{2}` fathers, continents, oceans, plus a year-anchor `(?:19|20)\d{2}`
pattern and a season/year/championship/tournament prefix. Future pattern and a season/year/championship/tournament prefix. Future
refinement (§10.1 noted): corpus-arity check. refinement (§10.1 noted): corpus-arity check.
3. **Reminder templates** (`aborist/qa/quantifier_reminder.py`). 3. **Reminder templates** (`arborist/qa/quantifier_reminder.py`).
Two-template split (bounded vs unbounded); the bounded template Two-template split (bounded vs unbounded); the bounded template
omits the "do not enumerate from training prior" clause because omits the "do not enumerate from training prior" clause because
the corpus has the answer set. Unknown scope falls back to the the corpus has the answer set. Unknown scope falls back to the
stricter unbounded template (over-warn rather than under-warn). stricter unbounded template (over-warn rather than under-warn).
4. **Reject-broad answer_text format** (`aborist/qa/query.py`). 4. **Reject-broad answer_text format** (`arborist/qa/query.py`).
`BROAD-QUANTIFIER PREFLIGHT REJECTED · scope unbounded` header + `BROAD-QUANTIFIER PREFLIGHT REJECTED · scope unbounded` header +
actionable narrowing hints (year, league, country, category) + actionable narrowing hints (year, league, country, category) +
`--allow-broad` escape-hatch mention. Format chosen so an `--allow-broad` escape-hatch mention. Format chosen so an
operator reading raw output sees both the WHY and the HOW-TO-FIX. operator reading raw output sees both the WHY and the HOW-TO-FIX.
5. **`_render_query_human` branch for `broad_quantifier_rejected` 5. **`_render_query_human` branch for `broad_quantifier_rejected`
status** (`aborist/cli.py`). Dedicated render path so the new status** (`arborist/cli.py`). Dedicated render path so the new
status doesn't fall through to the generic "unknown error" status doesn't fall through to the generic "unknown error"
handler. Renders intensity, matched token, cap, and the rejection handler. Renders intensity, matched token, cap, and the rejection
text on a single audit-line + body. text on a single audit-line + body.
6. **EXPLICIT_COUNT defensive fallback to MANY cap** 6. **EXPLICIT_COUNT defensive fallback to MANY cap**
(`aborist/qa/model_profiles.py:cap_for_intensity`). When the (`arborist/qa/model_profiles.py:cap_for_intensity`). When the
classifier fired SMALL_NUM_EXPLICIT or COMPARATIVE_BOUND but classifier fired SMALL_NUM_EXPLICIT or COMPARATIVE_BOUND but
didn't extract a count (shouldn't happen but covers regressions), didn't extract a count (shouldn't happen but covers regressions),
fall back to the same profile's MANY cap rather than crash. fall back to the same profile's MANY cap rather than crash.
@ -1750,10 +1750,10 @@ but are now binding via tests + commits:
### 11.10 Live verification artifacts ### 11.10 Live verification artifacts
- `aborist query --reject-broad "Winners of all major sports?"` → - `arborist query --reject-broad "Winners of all major sports?"` →
preflight rejection, 0.0s, no LLM call. Rendered output recorded preflight rejection, 0.0s, no LLM call. Rendered output recorded
in §8 Status. in §8 Status.
- `aborist query --reject-broad "name all members of the Beatles"` - `arborist query --reject-broad "name all members of the Beatles"`
→ ran the LLM (scope_bound_hint=bounded correctly suppressed → ran the LLM (scope_bound_hint=bounded correctly suppressed
rejection), enumerated 4 verified Beatles members. Rendered output rejection), enumerated 4 verified Beatles members. Rendered output
recorded in §8 Status. recorded in §8 Status.
@ -2239,10 +2239,10 @@ quote:
Code changes (commit pending): Code changes (commit pending):
- `aborist/qa/runner.py`: `quantifier_reminder_enabled=True`, - `arborist/qa/runner.py`: `quantifier_reminder_enabled=True`,
new field `quantifier_apply_caps_modes=["claim_lattice"]`. new field `quantifier_apply_caps_modes=["claim_lattice"]`.
- `aborist/qa/query.py`: same defaults. - `arborist/qa/query.py`: same defaults.
- `aborist/qa/keys.py`: `quantifier_apply_caps_modes` added to - `arborist/qa/keys.py`: `quantifier_apply_caps_modes` added to
`_VERIFIER_POLICY_FIELDS` so flipping it bumps governance hash. `_VERIFIER_POLICY_FIELDS` so flipping it bumps governance hash.
- Cap-application gate now reads: - Cap-application gate now reads:
`if apply_caps AND mode in apply_caps_modes AND cap is not None:` `if apply_caps AND mode in apply_caps_modes AND cap is not None:`
@ -2257,20 +2257,20 @@ Code changes (commit pending):
``` ```
# Default behavior (no flag): reminder ON for lattice modes; cap # Default behavior (no flag): reminder ON for lattice modes; cap
# OFF (operator opt-in only). # OFF (operator opt-in only).
$ aborist query "winners of all major sports?" $ arborist query "winners of all major sports?"
# Flip cap on for one call (JSON-only by default): # Flip cap on for one call (JSON-only by default):
$ aborist query --apply-quantifier-caps "..." $ arborist query --apply-quantifier-caps "..."
# Override the allowlist to include pointer mode too (Option D): # Override the allowlist to include pointer mode too (Option D):
$ aborist query --apply-quantifier-caps \ $ arborist query --apply-quantifier-caps \
--policy quantifier_apply_caps_modes='["claim_lattice","claim_lattice_pointer"]' "..." --policy quantifier_apply_caps_modes='["claim_lattice","claim_lattice_pointer"]' "..."
# Disable the reminder for one call: # Disable the reminder for one call:
$ aborist query --policy quantifier_reminder_enabled=false "..." $ arborist query --policy quantifier_reminder_enabled=false "..."
# Master kill: # Master kill:
$ aborist query --no-quantifier-guard "..." $ arborist query --no-quantifier-guard "..."
``` ```
**Six-level disable hierarchy still intact** — no flag forced upon **Six-level disable hierarchy still intact** — no flag forced upon

View file

@ -146,7 +146,7 @@ split if a future clause needs independent hashing.**
### 3.3 Where the node lives in code ### 3.3 Where the node lives in code
- New helper in `aborist/qa/dag.py` (alongside existing per-stage - New helper in `arborist/qa/dag.py` (alongside existing per-stage
helpers): `_quantifier_preflight_node(quantifier, policy, helpers): `_quantifier_preflight_node(quantifier, policy,
effective_max_claims, reminder_injected)`. effective_max_claims, reminder_injected)`.
- Wire into `query()` at the same point quantifier classification - Wire into `query()` at the same point quantifier classification
@ -176,7 +176,7 @@ preflight contract").
## 4. Implementation sketch ## 4. Implementation sketch
``` ```
1. New function in aborist/qa/dag.py: 1. New function in arborist/qa/dag.py:
def quantifier_preflight_node(quantifier, policy, ...) def quantifier_preflight_node(quantifier, policy, ...)
2. Wire it into query() between policy resolution and t_start. 2. Wire it into query() between policy resolution and t_start.
3. Wire it into runner.ask() symmetrically. 3. Wire it into runner.ask() symmetrically.
@ -219,17 +219,17 @@ This ticket closes the loop.
**Closed · landed 2026-05-03 (zero-shot).** Mechanism shipped end- **Closed · landed 2026-05-03 (zero-shot).** Mechanism shipped end-
to-end: to-end:
- `aborist/qa/dag.py:preflight_node_hash()` — combines - `arborist/qa/dag.py:preflight_node_hash()` — combines
QuestionState + quantifier classifier output + behavioral QuestionState + quantifier classifier output + behavioral
policy_state into one canonical SHA-256 hex. policy_state into one canonical SHA-256 hex.
- `aborist/qa/dag.py:build_run_dag()` — new optional - `arborist/qa/dag.py:build_run_dag()` — new optional
`preflight_hash` parameter inserts a `preflight` stage at `preflight_hash` parameter inserts a `preflight` stage at
position 1 (between `question` and `retrieval`). Quote-mode position 1 (between `question` and `retrieval`). Quote-mode
shape becomes 8 stages; pointer-mode CTI shape becomes 10 shape becomes 8 stages; pointer-mode CTI shape becomes 10
stages. Backward-compatible: when `preflight_hash` is None, stages. Backward-compatible: when `preflight_hash` is None,
shapes stay 7/9 exactly so legacy `run_dag_root` values shapes stay 7/9 exactly so legacy `run_dag_root` values
re-validate. re-validate.
- `aborist/qa/query.py` + `aborist/qa/runner.py` — both build - `arborist/qa/query.py` + `arborist/qa/runner.py` — both build
the preflight payload from `question_state`, the quantifier the preflight payload from `question_state`, the quantifier
dict, and a 10-field `policy_state` capturing the *behavioral* dict, and a 10-field `policy_state` capturing the *behavioral*
decisions taken on this run (guard_enabled, apply_caps_active, decisions taken on this run (guard_enabled, apply_caps_active,
@ -276,7 +276,7 @@ preflight contract").
### 7.2 What's NOT in this ticket ### 7.2 What's NOT in this ticket
- **CLI flag for inspecting preflight node**: a future - **CLI flag for inspecting preflight node**: a future
`aborist providence --show-preflight <cache_key>` would render `arborist providence --show-preflight <cache_key>` would render
the preflight payload from `run_dag_blob`. Out of scope here. the preflight payload from `run_dag_blob`. Out of scope here.
- **Bench harness preflight-hash field**: bench rows could - **Bench harness preflight-hash field**: bench rows could
surface `preflight_hash` (12-char prefix like `cache_key`) surface `preflight_hash` (12-char prefix like `cache_key`)
@ -369,7 +369,7 @@ Confirmed. Corrections stay strictly in the audit-binding lane.
**A — reject-path DAG** (the critical gap): **A — reject-path DAG** (the critical gap):
`aborist/qa/dag.py:build_reject_run_dag()` ships. Builds a 3-stage `arborist/qa/dag.py:build_reject_run_dag()` ships. Builds a 3-stage
`question → preflight → final_label` DAG for the reject-broad `question → preflight → final_label` DAG for the reject-broad
early-return path. `query()` now wires it in and returns early-return path. `query()` now wires it in and returns
`run_dag_root` + `run_dag_blob` on the rejection result dict. `run_dag_root` + `run_dag_blob` on the rejection result dict.

View file

@ -158,7 +158,7 @@ follow-up; first pass surfaces it on the result dict only).
### 6.1 Quantifier (REUSE from #000008) ### 6.1 Quantifier (REUSE from #000008)
Already shipped as `aborist/qa/quantifier.py`. Returns intensity + Already shipped as `arborist/qa/quantifier.py`. Returns intensity +
`scope_bound_hint`. Meta-cognition wraps it; doesn't duplicate. `scope_bound_hint`. Meta-cognition wraps it; doesn't duplicate.
### 6.2 Temporal sensitivity ### 6.2 Temporal sensitivity
@ -204,7 +204,7 @@ record `false_premise_suspected` so the audit-line can surface it.
### 6.5 Reference-frame detector ### 6.5 Reference-frame detector
Reuse the existing frame-detection logic from ticket #000002 Reuse the existing frame-detection logic from ticket #000002
(`aborist/qa/query.py:_detect_frame`). Already classifies (`arborist/qa/query.py:_detect_frame`). Already classifies
`reference` vs `literal` vs `ambiguous`. Meta-cognition wraps the `reference` vs `literal` vs `ambiguous`. Meta-cognition wraps the
output as `reference_frames: list[str]`. output as `reference_frames: list[str]`.
@ -216,11 +216,11 @@ has no matching upload, sets `corpus_status: "out_of_corpus"`.
Conservative — defaults to `"likely_in_corpus"` for typical Conservative — defaults to `"likely_in_corpus"` for typical
encyclopedic questions. encyclopedic questions.
## 7. Implementation in Aborist ## 7. Implementation in Arborist
### 7.1 New module ### 7.1 New module
`aborist/qa/metacognition.py` with: `arborist/qa/metacognition.py` with:
```python ```python
def preflight_question( def preflight_question(
@ -325,13 +325,13 @@ to fox.
### 12.1 Phase 1 — module + detectors (LANDED `55efb04`) ### 12.1 Phase 1 — module + detectors (LANDED `55efb04`)
`aborist/qa/metacognition.py` shipped with: `arborist/qa/metacognition.py` shipped with:
- `QuestionState` dataclass (frozen, JSON-serializable via `to_dict`) - `QuestionState` dataclass (frozen, JSON-serializable via `to_dict`)
- `preflight_question()` pure function - `preflight_question()` pure function
- 4 new deterministic detectors (temporal, contradiction, false- - 4 new deterministic detectors (temporal, contradiction, false-
premise-lite, out-of-corpus) premise-lite, out-of-corpus)
- Reuses `aborist.qa.quantifier.classify_question_quantifier` - Reuses `arborist.qa.quantifier.classify_question_quantifier`
- 8 LogicalStatus values + 3 PreflightResult values - 8 LogicalStatus values + 3 PreflightResult values
- 6 per-detector enable switches in policy - 6 per-detector enable switches in policy
- `preflight_policy_hash` for governance binding (Phase 3) - `preflight_policy_hash` for governance binding (Phase 3)
@ -372,7 +372,7 @@ metacognition_block_on_contradiction False
All folded into `_VERIFIER_POLICY_FIELDS` so flipping any of them All folded into `_VERIFIER_POLICY_FIELDS` so flipping any of them
invalidates prior cache records. invalidates prior cache records.
CLI flags on `aborist query`: CLI flags on `arborist query`:
``` ```
--no-preflight disable the guard for one call --no-preflight disable the guard for one call

View file

@ -61,7 +61,7 @@ class SoftPreflightHint:
The hint always carries the `SOFT_` prefix on its `classifier_label` The hint always carries the `SOFT_` prefix on its `classifier_label`
so it can never be mistaken for a deterministic verdict by: so it can never be mistaken for a deterministic verdict by:
- The audit-line tail renderer in `aborist/cli.py:_render_warrant_tail` - The audit-line tail renderer in `arborist/cli.py:_render_warrant_tail`
- The verifier (which doesn't read sidecar fields anyway) - The verifier (which doesn't read sidecar fields anyway)
- Bench aggregations - Bench aggregations
- Operators inspecting bench JSONL or run-DAG blobs - Operators inspecting bench JSONL or run-DAG blobs
@ -69,11 +69,11 @@ so it can never be mistaken for a deterministic verdict by:
## 3. Where it lives ## 3. Where it lives
``` ```
aborist/qa/soft_preflight.py — sidecar implementation arborist/qa/soft_preflight.py — sidecar implementation
(new module) (new module)
``` ```
Mirrors `aborist/qa/inspect.py` (the existing read-only sidecar Mirrors `arborist/qa/inspect.py` (the existing read-only sidecar
diagnostic for span classification) — same architectural pattern: diagnostic for span classification) — same architectural pattern:
soft signal, never enters proof path, never written to providence soft signal, never enters proof path, never written to providence
cache, never bumps governance hash on use. cache, never bumps governance hash on use.
@ -99,7 +99,7 @@ remains driven by the hard policy.
## 5. Implementation sketch ## 5. Implementation sketch
```python ```python
# aborist/qa/soft_preflight.py # arborist/qa/soft_preflight.py
def soft_preflight_question( def soft_preflight_question(
question: str, question: str,
@ -149,17 +149,17 @@ def soft_preflight_question(
## 6. Integration points ## 6. Integration points
- `aborist/qa/query.py` — call `soft_preflight_question()` after - `arborist/qa/query.py` — call `soft_preflight_question()` after
`preflight_question()` (the hard call) but before the main LLM `preflight_question()` (the hard call) but before the main LLM
call. Soft hint surfaces on result dict as call. Soft hint surfaces on result dict as
`soft_preflight_hint`; merges into the run-DAG `preflight` `soft_preflight_hint`; merges into the run-DAG `preflight`
stage payload as a sub-field of `question_state`. stage payload as a sub-field of `question_state`.
- `aborist/qa/runner.py` — same wiring. - `arborist/qa/runner.py` — same wiring.
- `aborist/cli.py` — new `--soft-preflight` flag (per-call - `arborist/cli.py` — new `--soft-preflight` flag (per-call
override; default off matches policy default). override; default off matches policy default).
- `bench/qa_sweep.py` — bench rows pick up `soft_preflight_label` - `bench/qa_sweep.py` — bench rows pick up `soft_preflight_label`
+ `soft_preflight_confidence` as bounded-projection fields. + `soft_preflight_confidence` as bounded-projection fields.
- `aborist/cli.py:_render_warrant_tail` — soft hints render as - `arborist/cli.py:_render_warrant_tail` — soft hints render as
`· soft: <label>` on the audit-line (clearly differentiated from `· soft: <label>` on the audit-line (clearly differentiated from
the hard `· false premise` etc. tails). Operator can tell at a the hard `· false premise` etc. tails). Operator can tell at a
glance whether a tail is hard or soft. glance whether a tail is hard or soft.
@ -225,7 +225,7 @@ hash never enters the proof path" rule (cf. whitepaper
## 11. What landed ## 11. What landed
- **`aborist/qa/soft_preflight.py`** — `SoftPreflightHint` - **`arborist/qa/soft_preflight.py`** — `SoftPreflightHint`
dataclass + `soft_preflight_question()` pure function. dataclass + `soft_preflight_question()` pure function.
9 canonical labels (`SOFT_WELL_FORMED`, 9 canonical labels (`SOFT_WELL_FORMED`,
`SOFT_FALSE_PREMISE_SUSPECTED`, `SOFT_CONTRADICTION_SUSPECTED`, `SOFT_FALSE_PREMISE_SUSPECTED`, `SOFT_CONTRADICTION_SUSPECTED`,
@ -238,18 +238,18 @@ hash never enters the proof path" rule (cf. whitepaper
(raises → `SOFT_PARSE_FAIL`, drift → `SOFT_PARSE_FAIL`). (raises → `SOFT_PARSE_FAIL`, drift → `SOFT_PARSE_FAIL`).
`SOFT_PREFLIGHT_VERSION = "soft-preflight-v0.1"`. `SOFT_PREFLIGHT_VERSION = "soft-preflight-v0.1"`.
- **`aborist/qa/query.py`** — wired into the post-classifier / - **`arborist/qa/query.py`** — wired into the post-classifier /
pre-retrieval segment. One short LLM call (~200ms median), pre-retrieval segment. One short LLM call (~200ms median),
result surfaces as `soft_preflight_hint` on the result dict result surfaces as `soft_preflight_hint` on the result dict
+ `soft_preflight_ms` in the timings dict. + `soft_preflight_ms` in the timings dict.
- **`aborist/qa/runner.py` + `aborist/qa/query.py`** — - **`arborist/qa/runner.py` + `arborist/qa/query.py`** —
`soft_preflight_enabled: False` policy default; cache row `soft_preflight_enabled: False` policy default; cache row
identity unchanged (NOT folded into `_VERIFIER_POLICY_FIELDS` identity unchanged (NOT folded into `_VERIFIER_POLICY_FIELDS`
per §4 — soft hints don't gate cache identity). per §4 — soft hints don't gate cache identity).
- **`aborist/cli.py`** — `--soft-preflight` flag on - **`arborist/cli.py`** — `--soft-preflight` flag on
`aborist query`. Audit-line tail renders soft hints as `arborist query`. Audit-line tail renders soft hints as
`· soft: <label>` (e.g. `· soft: time sensitive`) — distinct `· soft: <label>` (e.g. `· soft: time sensitive`) — distinct
from hard tails so operators see the signal separation at a from hard tails so operators see the signal separation at a
glance. SOFT_DISABLED / SOFT_PARSE_FAIL / SOFT_WELL_FORMED glance. SOFT_DISABLED / SOFT_PARSE_FAIL / SOFT_WELL_FORMED

View file

@ -8,10 +8,10 @@ contend.
Wired into `make backfill-concepts`. Usage: Wired into `make backfill-concepts`. Usage:
python scripts/backfill_concepts.py --shards-dir ~/.aborist/shards python scripts/backfill_concepts.py --shards-dir ~/.arborist/shards
python scripts/backfill_concepts.py --shards-dir ~/.aborist/shards \ python scripts/backfill_concepts.py --shards-dir ~/.arborist/shards \
--extractors link_reciprocity,token_idf,documents_fts --extractors link_reciprocity,token_idf,documents_fts
python scripts/backfill_concepts.py --shards-dir ~/.aborist/shards \ python scripts/backfill_concepts.py --shards-dir ~/.arborist/shards \
--workers 8 --workers 8
Skips shards that aren't numeric-stem (`000.db`, `001.db`, …) by Skips shards that aren't numeric-stem (`000.db`, `001.db`, …) by
@ -28,8 +28,8 @@ import sys
import time import time
from pathlib import Path from pathlib import Path
from aborist.concepts.extract import EXTRACTORS from arborist.concepts.extract import EXTRACTORS
from aborist.store import connect, discover_shards from arborist.store import connect, discover_shards
# Default extractor order. link_reciprocity must run BEFORE token_idf # Default extractor order. link_reciprocity must run BEFORE token_idf
@ -66,7 +66,7 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument( parser.add_argument(
"--shards-dir", "--shards-dir",
type=Path, type=Path,
default=Path.home() / ".aborist" / "shards", default=Path.home() / ".arborist" / "shards",
help="Directory containing *.db shards.", help="Directory containing *.db shards.",
) )
parser.add_argument( parser.add_argument(
@ -74,7 +74,7 @@ def main(argv: list[str] | None = None) -> int:
default=",".join(DEFAULT_EXTRACTORS), default=",".join(DEFAULT_EXTRACTORS),
help=( help=(
"Comma-separated extractor names (registered in " "Comma-separated extractor names (registered in "
"aborist.concepts.extract.EXTRACTORS). Default: " "arborist.concepts.extract.EXTRACTORS). Default: "
f"{','.join(DEFAULT_EXTRACTORS)}" f"{','.join(DEFAULT_EXTRACTORS)}"
), ),
) )

View file

@ -2,7 +2,7 @@
Pick three random words from /usr/share/dict/words, ask Hermes at Pick three random words from /usr/share/dict/words, ask Hermes at
temp 0.8 to weave them into a creative question paragraph, send that temp 0.8 to weave them into a creative question paragraph, send that
paragraph to aborist, log the whole journey (words question paragraph to arborist, log the whole journey (words question
answer audit_mode sources timings) as a single JSONL line. answer audit_mode sources timings) as a single JSONL line.
Designed for **blue moon cadence**, NOT every-commit benching. The Designed for **blue moon cadence**, NOT every-commit benching. The
@ -128,7 +128,7 @@ def run_one_cycle(
top_k: int, top_k: int,
) -> dict: ) -> dict:
"""One emergent cycle: words → question → answer → log entry dict.""" """One emergent cycle: words → question → answer → log entry dict."""
from aborist.qa.query import DEFAULT_QUERY_POLICY, query from arborist.qa.query import DEFAULT_QUERY_POLICY, query
t_pick = time.time() t_pick = time.time()
question = generate_question(client, model_id, words) question = generate_question(client, model_id, words)
@ -159,7 +159,7 @@ def run_one_cycle(
# ignored. Surfaced 2026-05-02 by the swallowtail/upbraided/rockiest # ignored. Surfaced 2026-05-02 by the swallowtail/upbraided/rockiest
# emergent log entry. Off-the-binary-chain — gives the teacher # emergent log entry. Off-the-binary-chain — gives the teacher
# reviewer a flag for poetic-question-vs-literal-answer mismatches. # reviewer a flag for poetic-question-vs-literal-answer mismatches.
from aborist.qa.inspect import diagnose_metaphor_deflection from arborist.qa.inspect import diagnose_metaphor_deflection
metaphor = diagnose_metaphor_deflection( metaphor = diagnose_metaphor_deflection(
question or "", result.get("answer_text") or "" question or "", result.get("answer_text") or ""
) )
@ -206,14 +206,14 @@ def append_log(entry: dict, log_path: Path) -> None:
def emit_unfirehose_session(entry: dict, model_id: str) -> None: def emit_unfirehose_session(entry: dict, model_id: str) -> None:
"""Write one unfirehose/1.0 session for this bench cycle. """Write one unfirehose/1.0 session for this bench cycle.
Same auto-discovery path as `aborist.cli._emit_query_journal`: Same auto-discovery path as `arborist.cli._emit_query_journal`:
~/.aborist/unfirehose/{slug}/{session_uuid}.jsonl. Each cycle gets ~/.arborist/unfirehose/{slug}/{session_uuid}.jsonl. Each cycle gets
its own session file (one prompt one answer = one session). its own session file (one prompt one answer = one session).
Failures must NEVER break the bench loop wrapped at the call Failures must NEVER break the bench loop wrapped at the call
site. site.
""" """
from aborist.journal import SessionWriter from arborist.journal import SessionWriter
aborist_meta = { arborist_meta = {
"audit_mode": entry.get("audit_mode"), "audit_mode": entry.get("audit_mode"),
"verifier_method": entry.get("verifier_method"), "verifier_method": entry.get("verifier_method"),
"n_quotes": entry.get("n_quotes"), "n_quotes": entry.get("n_quotes"),
@ -236,7 +236,7 @@ def emit_unfirehose_session(entry: dict, model_id: str) -> None:
s.system_message( s.system_message(
"bench-emergent cycle: 3-word triangulation", "bench-emergent cycle: 3-word triangulation",
subtype="init", subtype="init",
aborist_meta={"words": entry.get("words"), "harness_role": "generator"}, arborist_meta={"words": entry.get("words"), "harness_role": "generator"},
) )
s.user_message(entry.get("question") or "") s.user_message(entry.get("question") or "")
s.assistant_message( s.assistant_message(
@ -244,7 +244,7 @@ def emit_unfirehose_session(entry: dict, model_id: str) -> None:
model=model_id, model=model_id,
provider="hermes", provider="hermes",
duration_ms=int((entry.get("answer_seconds") or 0) * 1000) or None, duration_ms=int((entry.get("answer_seconds") or 0) * 1000) or None,
aborist_meta=aborist_meta, arborist_meta=arborist_meta,
) )
@ -284,23 +284,23 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument( parser.add_argument(
"--shards-dir", "--shards-dir",
type=Path, type=Path,
default=Path.home() / ".aborist" / "shards", default=Path.home() / ".arborist" / "shards",
) )
parser.add_argument( parser.add_argument(
"--qa-db", "--qa-db",
type=Path, type=Path,
default=Path.home() / ".aborist" / "shards" / "qa.db", default=Path.home() / ".arborist" / "shards" / "qa.db",
) )
parser.add_argument("--answer-mode", default="claim_lattice") parser.add_argument("--answer-mode", default="claim_lattice")
parser.add_argument("--top-k", type=int, default=8) parser.add_argument("--top-k", type=int, default=8)
parser.add_argument( parser.add_argument(
"--endpoint", "--endpoint",
default=os.environ.get("ABORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"), default=os.environ.get("ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"),
) )
parser.add_argument( parser.add_argument(
"--model", "--model",
default=os.environ.get( default=os.environ.get(
"ABORIST_LLM_MODEL", "ARBORIST_LLM_MODEL",
"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic", "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
), ),
) )
@ -320,9 +320,9 @@ def main(argv: list[str] | None = None) -> int:
parser.error(f"shards dir not found: {ns.shards_dir}") parser.error(f"shards dir not found: {ns.shards_dir}")
rng = random.Random(ns.seed) rng = random.Random(ns.seed)
from aborist.qa.client import OpenAICompatibleClient from arborist.qa.client import OpenAICompatibleClient
api_key = os.environ.get("ABORIST_LLM_API_KEY") api_key = os.environ.get("ARBORIST_LLM_API_KEY")
client = OpenAICompatibleClient(base_url=ns.endpoint, api_key=api_key) client = OpenAICompatibleClient(base_url=ns.endpoint, api_key=api_key)
print(f">> emergent bench: {ns.n} cycles · seed={ns.seed} · log={ns.log_path}", flush=True) print(f">> emergent bench: {ns.n} cycles · seed={ns.seed} · log={ns.log_path}", flush=True)

View file

@ -10,7 +10,7 @@ from unittest.mock import Mock, AsyncMock, patch, MagicMock
import asyncio import asyncio
import aiohttp import aiohttp
from aborist.sources.crawler.async_web_fetcher import ( from arborist.sources.crawler.async_web_fetcher import (
CrawlMode, CrawlMode,
MediaItem, MediaItem,
strip_uri_fragment, strip_uri_fragment,

View file

@ -2,7 +2,7 @@
These tests stub the network. The verbatim crawler tests in this same These tests stub the network. The verbatim crawler tests in this same
directory exercise the underlying AsyncWebFetcher; here we exercise directory exercise the underlying AsyncWebFetcher; here we exercise
the aborist bridge logic on top of it (BFS bounds, same-domain filter, the arborist bridge logic on top of it (BFS bounds, same-domain filter,
ETag capture, conditional HEAD classification). ETag capture, conditional HEAD classification).
""" """
@ -16,14 +16,14 @@ from unittest.mock import patch
import httpx import httpx
import pytest import pytest
from aborist.sources.crawler.bridge import ( from arborist.sources.crawler.bridge import (
_normalize, _normalize,
_same_domain, _same_domain,
crawl_seed, crawl_seed,
ingest_crawled, ingest_crawled,
recrawl_check, recrawl_check,
) )
from aborist.store import connect from arborist.store import connect
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -71,7 +71,7 @@ class _StubFetcher:
def _run_crawl(page_links, seed, depth, max_pages): def _run_crawl(page_links, seed, depth, max_pages):
"""Helper: drive _crawl_seed_async with a stub fetcher.""" """Helper: drive _crawl_seed_async with a stub fetcher."""
import asyncio import asyncio
from aborist.sources.crawler import bridge from arborist.sources.crawler import bridge
return asyncio.run( return asyncio.run(
bridge._crawl_seed_async( bridge._crawl_seed_async(
@ -162,7 +162,7 @@ def test_crawl_seed_fast_flag_creates_fast_mode_fetcher(monkeypatch):
async def fetch_webpage(self, url, extract_links=False, **kw): async def fetch_webpage(self, url, extract_links=False, **kw):
return None, [] return None, []
from aborist.sources.crawler import bridge from arborist.sources.crawler import bridge
monkeypatch.setattr(bridge, "AsyncWebFetcher", _CapturingFetcher) monkeypatch.setattr(bridge, "AsyncWebFetcher", _CapturingFetcher)
bridge.crawl_seed("https://x.com/", max_depth=1, max_pages=1, fast=True) bridge.crawl_seed("https://x.com/", max_depth=1, max_pages=1, fast=True)
@ -180,7 +180,7 @@ def test_crawl_seed_default_creates_polite_fetcher(monkeypatch):
async def fetch_webpage(self, url, extract_links=False, **kw): async def fetch_webpage(self, url, extract_links=False, **kw):
return None, [] return None, []
from aborist.sources.crawler import bridge from arborist.sources.crawler import bridge
monkeypatch.setattr(bridge, "AsyncWebFetcher", _CapturingFetcher) monkeypatch.setattr(bridge, "AsyncWebFetcher", _CapturingFetcher)
bridge.crawl_seed("https://x.com/", max_depth=1, max_pages=1) bridge.crawl_seed("https://x.com/", max_depth=1, max_pages=1)
@ -202,7 +202,7 @@ def test_crawl_seed_passes_extract_links_true():
return "<html></html>", [] return "<html></html>", []
import asyncio import asyncio
from aborist.sources.crawler import bridge from arborist.sources.crawler import bridge
fetcher = _RecordingFetcher() fetcher = _RecordingFetcher()
asyncio.run( asyncio.run(
@ -289,7 +289,7 @@ def test_ingest_crawled_writes_http_meta(http_meta_db):
} }
} }
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(http_meta_db) conn = connect(http_meta_db)
try: try:
result = ingest_crawled(conn, [url]) result = ingest_crawled(conn, [url])
@ -324,7 +324,7 @@ def test_ingest_crawled_skips_non_html(http_meta_db):
"body": "%PDF-1.4 binary", "body": "%PDF-1.4 binary",
} }
} }
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(http_meta_db) conn = connect(http_meta_db)
try: try:
result = ingest_crawled(conn, [url]) result = ingest_crawled(conn, [url])
@ -354,7 +354,7 @@ def test_ingest_crawled_skips_feed_urls_by_path(http_meta_db, feed_url):
# Response stub never gets used because the path filter is pre-fetch, # Response stub never gets used because the path filter is pre-fetch,
# but we provide one in case the filter regresses. # but we provide one in case the filter regresses.
responses = {feed_url: {"status": 200, "headers": {"content-type": "text/html"}}} responses = {feed_url: {"status": 200, "headers": {"content-type": "text/html"}}}
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(http_meta_db) conn = connect(http_meta_db)
try: try:
result = ingest_crawled(conn, [feed_url]) result = ingest_crawled(conn, [feed_url])
@ -386,7 +386,7 @@ def test_ingest_crawled_skips_feed_content_types(http_meta_db, ct):
"body": "<?xml version='1.0'?><feed></feed>", "body": "<?xml version='1.0'?><feed></feed>",
} }
} }
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(http_meta_db) conn = connect(http_meta_db)
try: try:
result = ingest_crawled(conn, [url]) result = ingest_crawled(conn, [url])
@ -406,7 +406,7 @@ def test_ingest_crawled_keeps_xhtml(http_meta_db):
"body": "<html><body><h1>T</h1>Real content here.</body></html>", "body": "<html><body><h1>T</h1>Real content here.</body></html>",
} }
} }
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(http_meta_db) conn = connect(http_meta_db)
try: try:
result = ingest_crawled(conn, [url]) result = ingest_crawled(conn, [url])
@ -423,7 +423,7 @@ def test_ingest_crawled_keeps_xhtml(http_meta_db):
def _seed_meta(conn, *, document_root: str, document_uri: str, etag: str | None = None): def _seed_meta(conn, *, document_root: str, document_uri: str, etag: str | None = None):
"""Insert a documents row + http_meta row for recheck tests.""" """Insert a documents row + http_meta row for recheck tests."""
from aborist.store import transaction from arborist.store import transaction
with transaction(conn): with transaction(conn):
conn.execute( conn.execute(
@ -451,7 +451,7 @@ def test_recrawl_check_classifies_304_as_fresh(tmp_path):
conn.close() conn.close()
responses = {"https://x.com/p": {"head_status": 304}} responses = {"https://x.com/p": {"head_status": 304}}
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(db) conn = connect(db)
try: try:
result = recrawl_check(conn, limit=10) result = recrawl_check(conn, limit=10)
@ -471,7 +471,7 @@ def test_recrawl_check_classifies_200_as_stale(tmp_path):
conn.close() conn.close()
responses = {"https://x.com/q": {"head_status": 200}} responses = {"https://x.com/q": {"head_status": 200}}
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(db) conn = connect(db)
try: try:
result = recrawl_check(conn, limit=10) result = recrawl_check(conn, limit=10)
@ -490,7 +490,7 @@ def test_recrawl_check_classifies_404_as_gone(tmp_path):
conn.close() conn.close()
responses = {"https://x.com/r": {"head_status": 404}} responses = {"https://x.com/r": {"head_status": 404}}
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(db) conn = connect(db)
try: try:
result = recrawl_check(conn, limit=10) result = recrawl_check(conn, limit=10)
@ -512,7 +512,7 @@ def test_recrawl_check_domain_filter(tmp_path):
"https://keep.com/a": {"head_status": 304}, "https://keep.com/a": {"head_status": 304},
# drop.com would error if the filter is broken; we expect it to be skipped. # drop.com would error if the filter is broken; we expect it to be skipped.
} }
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(db) conn = connect(db)
try: try:
result = recrawl_check(conn, domain="keep.com", limit=10) result = recrawl_check(conn, domain="keep.com", limit=10)
@ -531,7 +531,7 @@ def test_recrawl_check_updates_last_checked_at(tmp_path):
conn.close() conn.close()
responses = {"https://x.com/s": {"head_status": 304}} responses = {"https://x.com/s": {"head_status": 304}}
with patch("aborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)): with patch("arborist.sources.crawler.bridge.httpx.Client", _patched_httpx_get(responses)):
conn = connect(db) conn = connect(db)
try: try:
recrawl_check(conn, limit=10) recrawl_check(conn, limit=10)

View file

@ -6,7 +6,7 @@ Tests URL extraction, sources footer building, and web fetch helpers.
import pytest import pytest
from unittest.mock import Mock, AsyncMock, patch from unittest.mock import Mock, AsyncMock, patch
from aborist.sources.crawler.web_fetch import ( from arborist.sources.crawler.web_fetch import (
extract_url_from_content, extract_url_from_content,
is_web_fetch_request, is_web_fetch_request,
build_sources_footer, build_sources_footer,
@ -85,8 +85,8 @@ class TestURLExtraction:
@pytest.mark.skip( @pytest.mark.skip(
reason="chat-bot fetch triggers (has_web_fetch_trigger) were " reason="chat-bot fetch triggers (has_web_fetch_trigger) were "
"dropped during the aborist lift — aborist has no chat surface; " "dropped during the arborist lift — arborist has no chat surface; "
"see aborist/sources/crawler/__init__.py for rationale." "see arborist/sources/crawler/__init__.py for rationale."
) )
class TestWebFetchDetection: class TestWebFetchDetection:
"""Test web fetch request detection.""" """Test web fetch request detection."""
@ -106,8 +106,8 @@ class TestWebFetchDetection:
@pytest.mark.skip( @pytest.mark.skip(
reason="chat-bot fresh-fetch triggers (has_fresh_fetch_trigger) were " reason="chat-bot fresh-fetch triggers (has_fresh_fetch_trigger) were "
"dropped during the aborist lift — aborist has no chat surface; " "dropped during the arborist lift — arborist has no chat surface; "
"see aborist/sources/crawler/__init__.py for rationale." "see arborist/sources/crawler/__init__.py for rationale."
) )
class TestFreshFetchDetection: class TestFreshFetchDetection:
"""Test fresh fetch intent detection.""" """Test fresh fetch intent detection."""
@ -300,7 +300,7 @@ class TestWebFetchIntegration:
"""Test successful fetch and cache operation""" """Test successful fetch and cache operation"""
# This would require mocking web_cache_manager # This would require mocking web_cache_manager
# For now, just verify the function exists and has correct signature # For now, just verify the function exists and has correct signature
from aborist.sources.crawler.web_fetch import fetch_and_cache from arborist.sources.crawler.web_fetch import fetch_and_cache
import inspect import inspect
sig = inspect.signature(fetch_and_cache) sig = inspect.signature(fetch_and_cache)
assert 'url' in sig.parameters assert 'url' in sig.parameters
@ -312,7 +312,7 @@ class TestWebFetchIntegration:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_fetch_and_cache_with_progress_callback(self): async def test_fetch_and_cache_with_progress_callback(self):
"""Test fetch with progress callback""" """Test fetch with progress callback"""
from aborist.sources.crawler.web_fetch import fetch_and_cache from arborist.sources.crawler.web_fetch import fetch_and_cache
# Mock web cache manager # Mock web cache manager
mock_manager = Mock() mock_manager = Mock()

View file

@ -18,7 +18,7 @@ import pytest
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def qa_sweep(): def qa_sweep():
"""Import bench/qa_sweep.py as a module despite living outside the """Import bench/qa_sweep.py as a module despite living outside the
aborist package. Avoids polluting the package or requiring a arborist package. Avoids polluting the package or requiring a
pip-install of the bench harness.""" pip-install of the bench harness."""
bench_path = Path(__file__).parent.parent / "bench" / "qa_sweep.py" bench_path = Path(__file__).parent.parent / "bench" / "qa_sweep.py"
spec = importlib.util.spec_from_file_location("qa_sweep_under_test", bench_path) spec = importlib.util.spec_from_file_location("qa_sweep_under_test", bench_path)

View file

@ -19,8 +19,8 @@ import time
import pytest import pytest
from aborist.cli import _burn_cache_key from arborist.cli import _burn_cache_key
from aborist.store import append_audit, connect, transaction from arborist.store import append_audit, connect, transaction
def _seed_providence_record(db_path, *, cache_key: str, question: str = "q?", audit_mode: str = "HYBRID") -> None: def _seed_providence_record(db_path, *, cache_key: str, question: str = "q?", audit_mode: str = "HYBRID") -> None:
@ -250,8 +250,8 @@ def test_burn_preserves_audit_chain(tmp_path):
def test_burn_cli_invocation(tmp_path, capsys): def test_burn_cli_invocation(tmp_path, capsys):
"""`aborist burn` end-to-end through build_parser, including JSON output.""" """`arborist burn` end-to-end through build_parser, including JSON output."""
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "qa.db" db = tmp_path / "qa.db"
KEY = "fa" * 32 KEY = "fa" * 32
@ -279,8 +279,8 @@ def test_burn_kindergarten_burns_only_recent_rows(tmp_path, capsys):
test seeds two rows, backdates one to 2h ago, and asserts the test seeds two rows, backdates one to 2h ago, and asserts the
fresh one burns and the old one stays.""" fresh one burns and the old one stays."""
import time as _time import time as _time
from aborist.cli import build_parser from arborist.cli import build_parser
from aborist.store import transaction from arborist.store import transaction
db = tmp_path / "qa.db" db = tmp_path / "qa.db"
OLD = "ab" * 32 OLD = "ab" * 32
@ -326,7 +326,7 @@ def test_burn_kindergarten_burns_only_recent_rows(tmp_path, capsys):
def test_burn_kindergarten_dry_run_writes_nothing(tmp_path, capsys): def test_burn_kindergarten_dry_run_writes_nothing(tmp_path, capsys):
"""--dry-run reports what would burn but doesn't write or audit.""" """--dry-run reports what would burn but doesn't write or audit."""
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "qa.db" db = tmp_path / "qa.db"
KEY = "ef" * 32 KEY = "ef" * 32
@ -369,7 +369,7 @@ def test_burn_kindergarten_dry_run_writes_nothing(tmp_path, capsys):
def test_burn_kindergarten_zero_seconds_burns_everything(tmp_path, capsys): def test_burn_kindergarten_zero_seconds_burns_everything(tmp_path, capsys):
"""0-second window = burn every live providence_cache row.""" """0-second window = burn every live providence_cache row."""
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "qa.db" db = tmp_path / "qa.db"
K1 = "11" * 32 K1 = "11" * 32
@ -399,7 +399,7 @@ def test_burn_kindergarten_zero_seconds_burns_everything(tmp_path, capsys):
def test_burn_kindergarten_respects_children_gate_without_force(tmp_path, capsys): def test_burn_kindergarten_respects_children_gate_without_force(tmp_path, capsys):
"""A row with falsifications is REFUSED unless --force. Audit chain """A row with falsifications is REFUSED unless --force. Audit chain
still grows on burned rows; refused rows leave no audit entry.""" still grows on burned rows; refused rows leave no audit entry."""
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "qa.db" db = tmp_path / "qa.db"
BURNABLE = "33" * 32 BURNABLE = "33" * 32
@ -423,7 +423,7 @@ def test_burn_kindergarten_respects_children_gate_without_force(tmp_path, capsys
def test_burn_cli_returns_non_zero_on_refused(tmp_path, capsys): def test_burn_cli_returns_non_zero_on_refused(tmp_path, capsys):
"""Non-zero exit when refused so scripts can detect and react.""" """Non-zero exit when refused so scripts can detect and react."""
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "qa.db" db = tmp_path / "qa.db"
KEY = "fb" * 32 KEY = "fb" * 32

View file

@ -18,13 +18,13 @@ import json
import time import time
from typing import Iterator from typing import Iterator
from aborist.cli import _burn_core_root, _burn_document_root from arborist.cli import _burn_core_root, _burn_document_root
from aborist.distill import FirstSentenceDistiller from arborist.distill import FirstSentenceDistiller
from aborist.distill.runner import distill_existing from arborist.distill.runner import distill_existing
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.source import Source from arborist.source import Source
from aborist.store import append_audit, connect, transaction from arborist.store import append_audit, connect, transaction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -56,7 +56,7 @@ def _doc(uri: str, content: str, *, edges=None) -> Document:
# Long enough that the default tok-512-v1 chunker emits multiple chunks. # Long enough that the default tok-512-v1 chunker emits multiple chunks.
_LONG = ( _LONG = (
"Aborist tends trees and forests of cross-linked information. " * 40 "Arborist tends trees and forests of cross-linked information. " * 40
+ "\n\n" + "\n\n"
+ "Burn is the kindergarten leaf removal — children gate enforced. " * 40 + "Burn is the kindergarten leaf removal — children gate enforced. " * 40
) )
@ -460,7 +460,7 @@ def test_burn_document_preserves_audit_chain(tmp_path):
def test_burn_cli_document_invocation(tmp_path, capsys): def test_burn_cli_document_invocation(tmp_path, capsys):
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "burn.db" db = tmp_path / "burn.db"
root = _ingest_one(db, "html://cli-doc") root = _ingest_one(db, "html://cli-doc")
@ -484,7 +484,7 @@ def test_burn_cli_document_invocation(tmp_path, capsys):
def test_burn_cli_core_invocation(tmp_path, capsys): def test_burn_cli_core_invocation(tmp_path, capsys):
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "burn.db" db = tmp_path / "burn.db"
_, core_root = _ingest_and_distill(db, "html://cli-core") _, core_root = _ingest_and_distill(db, "html://cli-core")
@ -507,7 +507,7 @@ def test_burn_cli_core_invocation(tmp_path, capsys):
def test_burn_cli_default_kind_is_providence(tmp_path, capsys): def test_burn_cli_default_kind_is_providence(tmp_path, capsys):
"""Backwards compat: bare `burn --cache-key X` still works (no --kind).""" """Backwards compat: bare `burn --cache-key X` still works (no --kind)."""
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "burn.db" db = tmp_path / "burn.db"
# Seed a providence record reusing the existing test_burn helper logic. # Seed a providence record reusing the existing test_burn helper logic.
@ -552,7 +552,7 @@ def test_burn_cli_default_kind_is_providence(tmp_path, capsys):
def test_burn_cli_document_returns_non_zero_on_refused(tmp_path, capsys): def test_burn_cli_document_returns_non_zero_on_refused(tmp_path, capsys):
from aborist.cli import build_parser from arborist.cli import build_parser
db = tmp_path / "burn.db" db = tmp_path / "burn.db"
root = _ingest_one(db, "html://cli-refuse") root = _ingest_one(db, "html://cli-refuse")

View file

@ -13,8 +13,8 @@ from pathlib import Path
import pytest import pytest
from aborist.qa.dag import build_run_dag, verify_run_dag from arborist.qa.dag import build_run_dag, verify_run_dag
from aborist.qa.evidence import ( from arborist.qa.evidence import (
build_evidence_map, build_evidence_map,
evidence_map_by_evidence_id, evidence_map_by_evidence_id,
evidence_map_by_pointer_id, evidence_map_by_pointer_id,
@ -22,11 +22,11 @@ from aborist.qa.evidence import (
render_claim_lattice, render_claim_lattice,
render_evidence_map, render_evidence_map,
) )
from aborist.qa.keys import governance_policy_hash from arborist.qa.keys import governance_policy_hash
from aborist.qa.parse_claims import parse_pointer_claims from arborist.qa.parse_claims import parse_pointer_claims
from aborist.qa.runner import DEFAULT_POLICY from arborist.qa.runner import DEFAULT_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
from aborist.qa.verify import ( from arborist.qa.verify import (
ANSWER_MODES, ANSWER_MODES,
DEFAULT_ANSWER_MODE, DEFAULT_ANSWER_MODE,
verify_claim_lattice, verify_claim_lattice,
@ -470,7 +470,7 @@ def test_run_dag_root_changes_when_parsed_lattice_changes():
def test_verifier_method_check_accepts_claim_lattice(tmp_path: Path): def test_verifier_method_check_accepts_claim_lattice(tmp_path: Path):
"""Fresh shards include 'claim_lattice' in the verifier_method CHECK.""" """Fresh shards include 'claim_lattice' in the verifier_method CHECK."""
from aborist.store import connect from arborist.store import connect
db = tmp_path / "fresh.db" db = tmp_path / "fresh.db"
conn = connect(db) conn = connect(db)
@ -486,7 +486,7 @@ def test_verifier_method_check_accepts_claim_lattice(tmp_path: Path):
def test_legacy_shard_migrates_in_place(tmp_path: Path): def test_legacy_shard_migrates_in_place(tmp_path: Path):
"""An old shard with the pre-claim_lattice CHECK gets rebuilt on open """An old shard with the pre-claim_lattice CHECK gets rebuilt on open
AND keeps run_dag_root / run_dag_blob populated.""" AND keeps run_dag_root / run_dag_blob populated."""
from aborist.store import connect from arborist.store import connect
db = tmp_path / "legacy.db" db = tmp_path / "legacy.db"
c = sqlite3.connect(db) c = sqlite3.connect(db)
@ -803,12 +803,12 @@ def test_per_chunk_evidence_map_query_path(tmp_path):
"""A single source with three chunks should produce three evidence """A single source with three chunks should produce three evidence
objects in the prompt-facing map not one whole-doc entry.""" objects in the prompt-facing map not one whole-doc entry."""
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.qa.query import DEFAULT_QUERY_POLICY, query from arborist.qa.query import DEFAULT_QUERY_POLICY, query
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):
source_type = "test" source_type = "test"
@ -872,7 +872,7 @@ def test_per_chunk_evidence_map_query_path(tmp_path):
def test_chunk_query_relevance_scores_by_overlap(): def test_chunk_query_relevance_scores_by_overlap():
from aborist.qa.query import _chunk_query_relevance from arborist.qa.query import _chunk_query_relevance
qstem = {"brachiosaurus", "film"} qstem = {"brachiosaurus", "film"}
# Many distinct + many mentions wins. # Many distinct + many mentions wins.
@ -895,7 +895,7 @@ def test_chunk_query_relevance_scores_by_overlap():
def test_chunk_query_relevance_stem_aware(): def test_chunk_query_relevance_stem_aware():
from aborist.qa.query import _chunk_query_relevance from arborist.qa.query import _chunk_query_relevance
# Query has the plural "dinosaurs"; chunk has the singular "dinosaur". # Query has the plural "dinosaurs"; chunk has the singular "dinosaur".
# The stem strip in _body_count_with_stem should still match. # The stem strip in _body_count_with_stem should still match.
@ -911,16 +911,16 @@ def test_query_orders_chunks_by_relevance_g03(tmp_path):
of the document G0.3 must promote it to E1 so a model that of the document G0.3 must promote it to E1 so a model that
lazy-anchors on early pointers still cites the relevant chunk.""" lazy-anchors on early pointers still cites the relevant chunk."""
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.qa.evidence import build_evidence_map from arborist.qa.evidence import build_evidence_map
from aborist.qa.query import ( from arborist.qa.query import (
DEFAULT_QUERY_POLICY, _load_doc_chunks, _chunk_query_relevance, DEFAULT_QUERY_POLICY, _load_doc_chunks, _chunk_query_relevance,
) )
from aborist.qa.query import _stem_token_for_match, _title_query_tokens from arborist.qa.query import _stem_token_for_match, _title_query_tokens
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):
source_type = "test" source_type = "test"
@ -1066,7 +1066,7 @@ def test_lazy_anchor_signals_not_in_run_dag_payload():
signals recoverable from claim_statuses, and threading them in signals recoverable from claim_statuses, and threading them in
would make run_dag_root depend on the model's anchoring habits.""" would make run_dag_root depend on the model's anchoring habits."""
import inspect as _inspect import inspect as _inspect
from aborist.qa.dag import build_run_dag from arborist.qa.dag import build_run_dag
sig = _inspect.signature(build_run_dag) sig = _inspect.signature(build_run_dag)
params = set(sig.parameters) params = set(sig.parameters)
assert "pointer_id_distribution" not in params assert "pointer_id_distribution" not in params
@ -1078,11 +1078,11 @@ def test_per_chunk_evidence_map_uses_distinct_chunk_roots(tmp_path):
different chunks of the same source produce different evidence_ids different chunks of the same source produce different evidence_ids
even though they share source_root.""" even though they share source_root."""
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa.query import _load_doc_chunks from arborist.qa.query import _load_doc_chunks
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):
source_type = "test" source_type = "test"

View file

@ -1,4 +1,4 @@
"""Pretty-print + ensure_ascii=False on `aborist query` output. """Pretty-print + ensure_ascii=False on `arborist query` output.
The CLI default emits a human-readable render of the query result; The CLI default emits a human-readable render of the query result;
``--json`` switches to the raw record. Both paths must: ``--json`` switches to the raw record. Both paths must:
@ -12,7 +12,7 @@ import json
import pytest import pytest
from aborist.cli import _render_query_human from arborist.cli import _render_query_human
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -139,7 +139,7 @@ def test_render_includes_short_cache_key_with_pointer_to_json():
def test_unicode_round_trips_through_print_path(capsys): def test_unicode_round_trips_through_print_path(capsys):
"""Pin: a result with a Pokémon-style answer prints é, not \\u00e9. """Pin: a result with a Pokémon-style answer prints é, not \\u00e9.
Uses argparse via build_parser to exercise the real CLI dispatch.""" Uses argparse via build_parser to exercise the real CLI dispatch."""
from aborist.cli import build_parser from arborist.cli import build_parser
parser = build_parser() parser = build_parser()
# Build a fake result and call _cmd_query's render path indirectly. # Build a fake result and call _cmd_query's render path indirectly.

View file

@ -4,16 +4,16 @@ from __future__ import annotations
import sqlite3 import sqlite3
from aborist.compress import ( from arborist.compress import (
_MIN_COMPRESS_BYTES, _MIN_COMPRESS_BYTES,
_ZSTD_MAGIC, _ZSTD_MAGIC,
is_compressed, is_compressed,
pack_chunk, pack_chunk,
unpack_chunk, unpack_chunk,
) )
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.sources.wikipedia_xml import WikipediaXmlDump from arborist.sources.wikipedia_xml import WikipediaXmlDump
from aborist.store import connect from arborist.store import connect
def test_pack_passes_short_text_through_uncompressed(): def test_pack_passes_short_text_through_uncompressed():
@ -107,7 +107,7 @@ def test_ingest_writes_compressed_for_large_chunks(tmp_path):
fixture = tmp_path / "wp.xml" fixture = tmp_path / "wp.xml"
fixture.write_text(xml, encoding="utf-8") fixture.write_text(xml, encoding="utf-8")
db_path = tmp_path / "aborist.db" db_path = tmp_path / "arborist.db"
conn = connect(db_path) conn = connect(db_path)
try: try:
ingest_source(conn, WikipediaXmlDump(fixture)) ingest_source(conn, WikipediaXmlDump(fixture))
@ -131,7 +131,7 @@ def test_ingest_then_search_round_trip_finds_compressed_doc(tmp_path):
"""FTS5 still indexes plaintext; chunks.content stays compressed; reads """FTS5 still indexes plaintext; chunks.content stays compressed; reads
that go through unpack_chunk reconstruct the original. The whole that go through unpack_chunk reconstruct the original. The whole
pipeline keeps working.""" pipeline keeps working."""
from aborist.search import FTS5Backend from arborist.search import FTS5Backend
big_body = "merkle providence wikipedia anarchism " * 200 # ~7.4 KB big_body = "merkle providence wikipedia anarchism " * 200 # ~7.4 KB
xml = f"""<?xml version="1.0" encoding="UTF-8"?> xml = f"""<?xml version="1.0" encoding="UTF-8"?>
@ -150,7 +150,7 @@ def test_ingest_then_search_round_trip_finds_compressed_doc(tmp_path):
fixture = tmp_path / "wp.xml" fixture = tmp_path / "wp.xml"
fixture.write_text(xml, encoding="utf-8") fixture.write_text(xml, encoding="utf-8")
db_path = tmp_path / "aborist.db" db_path = tmp_path / "arborist.db"
conn = connect(db_path) conn = connect(db_path)
try: try:
ingest_source(conn, WikipediaXmlDump(fixture)) ingest_source(conn, WikipediaXmlDump(fixture))

View file

@ -13,15 +13,15 @@ from pathlib import Path
import pytest import pytest
from aborist.concepts import invalidate_cache as _invalidate_cache from arborist.concepts import invalidate_cache as _invalidate_cache
from aborist.concepts.query import invalidate_cache from arborist.concepts.query import invalidate_cache
from aborist.concepts.seed import seed_legacy_concepts from arborist.concepts.seed import seed_legacy_concepts
from aborist.qa.concepts import ( from arborist.qa.concepts import (
has_compare_phrasing, has_compare_phrasing,
rivalry_excluded, rivalry_excluded,
synonym_expand, synonym_expand,
) )
from aborist.store import connect from arborist.store import connect
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -171,6 +171,6 @@ def test_invalidate_cache_does_not_break_lookup(seeded_shards):
def test_export_invalidate_cache_alias(seeded_shards): def test_export_invalidate_cache_alias(seeded_shards):
"""The package-level ``invalidate_cache`` is the same callable as """The package-level ``invalidate_cache`` is the same callable as
``aborist.concepts.query.invalidate_cache`` exposed at top level ``arborist.concepts.query.invalidate_cache`` exposed at top level
for callers that don't want to import the implementation module.""" for callers that don't want to import the implementation module."""
assert _invalidate_cache is invalidate_cache assert _invalidate_cache is invalidate_cache

View file

@ -13,7 +13,7 @@ exactly as recorded.
from __future__ import annotations from __future__ import annotations
from aborist.qa.dag import ( from arborist.qa.dag import (
build_run_dag, build_run_dag,
localize_failure, localize_failure,
preflight_node_hash, preflight_node_hash,
@ -262,7 +262,7 @@ def test_preflight_node_hash_changes_with_policy_refs():
def test_preflight_node_hash_includes_node_version(): def test_preflight_node_hash_includes_node_version():
"""node_version field is part of the hashed payload so a future """node_version field is part of the hashed payload so a future
schema bump (preflight-node-v2 etc.) invalidates legacy nodes.""" schema bump (preflight-node-v2 etc.) invalidates legacy nodes."""
from aborist.qa.dag import ( from arborist.qa.dag import (
PREFLIGHT_NODE_VERSION, PREFLIGHT_NODE_VERSION,
build_preflight_node_payload, build_preflight_node_payload,
) )
@ -373,7 +373,7 @@ def test_reject_run_dag_three_stage_shape():
question preflight final_label. Audit replay can read the question preflight final_label. Audit replay can read the
stage list and tell instantly that this row is a preflight stage list and tell instantly that this row is a preflight
rejection (3 stages = reject path).""" rejection (3 stages = reject path)."""
from aborist.qa.dag import build_reject_run_dag, preflight_node_hash from arborist.qa.dag import build_reject_run_dag, preflight_node_hash
pre = preflight_node_hash( pre = preflight_node_hash(
question_state={"preflight_result": "PREFLIGHT_BLOCKED"}, question_state={"preflight_result": "PREFLIGHT_BLOCKED"},
quantifier={"intensity": "ALL", "is_broad": True, quantifier={"intensity": "ALL", "is_broad": True,
@ -396,7 +396,7 @@ def test_reject_run_dag_root_changes_with_preflight_hash():
"""Two reject runs that differ only in the preflight payload """Two reject runs that differ only in the preflight payload
(e.g. different policy state at rejection time) must produce (e.g. different policy state at rejection time) must produce
different run_dag_root values.""" different run_dag_root values."""
from aborist.qa.dag import build_reject_run_dag, preflight_node_hash from arborist.qa.dag import build_reject_run_dag, preflight_node_hash
pre_a = preflight_node_hash( pre_a = preflight_node_hash(
question_state={"preflight_result": "PREFLIGHT_BLOCKED"}, question_state={"preflight_result": "PREFLIGHT_BLOCKED"},
quantifier={"intensity": "ALL"}, quantifier={"intensity": "ALL"},
@ -422,7 +422,7 @@ def test_reject_run_dag_round_trips_through_verify():
"""3-stage reject DAG must verify the same way as the standard """3-stage reject DAG must verify the same way as the standard
7/9/8/10-stage shapes.""" 7/9/8/10-stage shapes."""
import json import json
from aborist.qa.dag import build_reject_run_dag, preflight_node_hash from arborist.qa.dag import build_reject_run_dag, preflight_node_hash
pre = preflight_node_hash( pre = preflight_node_hash(
question_state={"preflight_result": "PREFLIGHT_BLOCKED"}, question_state={"preflight_result": "PREFLIGHT_BLOCKED"},
quantifier={"intensity": "ALL"}, quantifier={"intensity": "ALL"},
@ -442,8 +442,8 @@ def test_reject_run_dag_round_trips_through_verify():
def test_extract_preflight_hash_from_blob_with_preflight(): def test_extract_preflight_hash_from_blob_with_preflight():
"""Pull preflight stage hash out of a persisted run_dag_blob.""" """Pull preflight stage hash out of a persisted run_dag_blob."""
import json import json
from aborist.qa.dag import build_run_dag, preflight_node_hash from arborist.qa.dag import build_run_dag, preflight_node_hash
from aborist.qa.query import _extract_preflight_hash_from_blob from arborist.qa.query import _extract_preflight_hash_from_blob
pre = preflight_node_hash( pre = preflight_node_hash(
question_state={"preflight_result": "PREFLIGHT_OK"}, question_state={"preflight_result": "PREFLIGHT_OK"},
quantifier={"intensity": "SINGULAR"}, quantifier={"intensity": "SINGULAR"},
@ -458,14 +458,14 @@ def test_extract_preflight_hash_returns_none_for_legacy_blob():
"""Legacy blobs (no preflight stage) return None — this is the """Legacy blobs (no preflight stage) return None — this is the
fall-through path for cache rows written before #000009.""" fall-through path for cache rows written before #000009."""
import json import json
from aborist.qa.query import _extract_preflight_hash_from_blob from arborist.qa.query import _extract_preflight_hash_from_blob
out = build_run_dag(**_kw()) # no preflight_hash out = build_run_dag(**_kw()) # no preflight_hash
blob = json.dumps(out, separators=(",", ":")) blob = json.dumps(out, separators=(",", ":"))
assert _extract_preflight_hash_from_blob(blob) is None assert _extract_preflight_hash_from_blob(blob) is None
def test_extract_preflight_hash_returns_none_for_empty_or_invalid(): def test_extract_preflight_hash_returns_none_for_empty_or_invalid():
from aborist.qa.query import _extract_preflight_hash_from_blob from arborist.qa.query import _extract_preflight_hash_from_blob
assert _extract_preflight_hash_from_blob(None) is None assert _extract_preflight_hash_from_blob(None) is None
assert _extract_preflight_hash_from_blob("") is None assert _extract_preflight_hash_from_blob("") is None
assert _extract_preflight_hash_from_blob("not json {{{") is None assert _extract_preflight_hash_from_blob("not json {{{") is None

View file

@ -24,7 +24,7 @@ def test_d1_verify_quotes_signature_has_no_chat_client():
"""The hard verifier never accepts a chat-client / LLM judge. """The hard verifier never accepts a chat-client / LLM judge.
A future PR that adds a chat_client parameter to verify_quotes A future PR that adds a chat_client parameter to verify_quotes
would silently introduce a soft-signal-as-hard-check leak.""" would silently introduce a soft-signal-as-hard-check leak."""
from aborist.qa.verify import verify_quotes from arborist.qa.verify import verify_quotes
sig = inspect.signature(verify_quotes) sig = inspect.signature(verify_quotes)
forbidden = {"chat_client", "client", "llm", "judge", "model"} forbidden = {"chat_client", "client", "llm", "judge", "model"}
@ -37,7 +37,7 @@ def test_d1_verify_quotes_signature_has_no_chat_client():
def test_d1_verify_claim_lattice_signature_has_no_chat_client(): def test_d1_verify_claim_lattice_signature_has_no_chat_client():
from aborist.qa.verify import verify_claim_lattice from arborist.qa.verify import verify_claim_lattice
sig = inspect.signature(verify_claim_lattice) sig = inspect.signature(verify_claim_lattice)
forbidden = {"chat_client", "client", "llm", "judge", "model"} forbidden = {"chat_client", "client", "llm", "judge", "model"}
@ -49,7 +49,7 @@ def test_d1_verify_claim_lattice_signature_has_no_chat_client():
def test_d1_verify_claim_lattice_json_signature_has_no_chat_client(): def test_d1_verify_claim_lattice_json_signature_has_no_chat_client():
from aborist.qa.verify import verify_claim_lattice_json from arborist.qa.verify import verify_claim_lattice_json
sig = inspect.signature(verify_claim_lattice_json) sig = inspect.signature(verify_claim_lattice_json)
forbidden = {"chat_client", "client", "llm", "judge", "model"} forbidden = {"chat_client", "client", "llm", "judge", "model"}
@ -65,7 +65,7 @@ def test_d1_verifier_method_enum_excludes_llm_judges():
include any LLM-as-judge token. If a future migration adds include any LLM-as-judge token. If a future migration adds
`llm`, `model`, `judge`, or `nli` to the enum, the verifier `llm`, `model`, `judge`, or `nli` to the enum, the verifier
started accepting soft signals into the hard chain.""" started accepting soft signals into the hard chain."""
from aborist.store import SCHEMA_SQL from arborist.store import SCHEMA_SQL
# Find the verifier_method CHECK constraint in SCHEMA_SQL. # Find the verifier_method CHECK constraint in SCHEMA_SQL.
match = re.search( match = re.search(
@ -97,14 +97,14 @@ def test_d1_verifier_method_enum_excludes_llm_judges():
def test_d2_answer_modes_include_lattice_variants(): def test_d2_answer_modes_include_lattice_variants():
"""ANSWER_MODES must contain both lattice variants so an agent """ANSWER_MODES must contain both lattice variants so an agent
can pick a pointer-clause-emitting mode.""" can pick a pointer-clause-emitting mode."""
from aborist.qa.verify import ANSWER_MODES from arborist.qa.verify import ANSWER_MODES
assert "claim_lattice_pointer" in ANSWER_MODES assert "claim_lattice_pointer" in ANSWER_MODES
assert "claim_lattice" in ANSWER_MODES assert "claim_lattice" in ANSWER_MODES
def test_d2_default_answer_mode_is_a_known_mode(): def test_d2_default_answer_mode_is_a_known_mode():
from aborist.qa.verify import ANSWER_MODES, DEFAULT_ANSWER_MODE from arborist.qa.verify import ANSWER_MODES, DEFAULT_ANSWER_MODE
assert DEFAULT_ANSWER_MODE in ANSWER_MODES assert DEFAULT_ANSWER_MODE in ANSWER_MODES
@ -113,7 +113,7 @@ def test_d2_pointer_parser_exists_and_returns_claim_nodes():
"""The pointer-line parser is the surface that turns model """The pointer-line parser is the surface that turns model
output into internal claim nodes. Its absence would mean output into internal claim nodes. Its absence would mean
Hermes has nowhere to emit pointer clauses to.""" Hermes has nowhere to emit pointer clauses to."""
from aborist.qa.parse_claims import parse_pointer_claims from arborist.qa.parse_claims import parse_pointer_claims
sample = "Steve Jobs co-founded Apple. [E1]\n" sample = "Steve Jobs co-founded Apple. [E1]\n"
out = parse_pointer_claims(sample) out = parse_pointer_claims(sample)
@ -136,7 +136,7 @@ def test_d3_runtime_owns_claim_lattice_construction():
pointer mode it produces prose that the runtime structures. pointer mode it produces prose that the runtime structures.
A regression that asks the model for structured JSON in pointer A regression that asks the model for structured JSON in pointer
mode would violate this directive.""" mode would violate this directive."""
from aborist.qa.parse_claims import parse_pointer_claims from arborist.qa.parse_claims import parse_pointer_claims
multi_line = ( multi_line = (
"Steve Jobs co-founded Apple. [E1]\n" "Steve Jobs co-founded Apple. [E1]\n"
@ -158,7 +158,7 @@ def test_d3_evidence_map_built_from_runtime_chunks_not_model_input():
chunks, not threaded in from the model's prompt-side chunks, not threaded in from the model's prompt-side
invention. Two chunks two evidence objects with deterministic invention. Two chunks two evidence objects with deterministic
pointer ids.""" pointer ids."""
from aborist.qa.evidence import build_evidence_map from arborist.qa.evidence import build_evidence_map
chunks = [ chunks = [
{ {
@ -198,7 +198,7 @@ def test_d4_run_dag_carries_evidence_map_root():
"""The 9-stage CTI run-DAG must commit the evidence_map_root. """The 9-stage CTI run-DAG must commit the evidence_map_root.
Without this, two runs whose retrieval surfaced different chunks Without this, two runs whose retrieval surfaced different chunks
would produce indistinguishable run_dag_roots.""" would produce indistinguishable run_dag_roots."""
from aborist.qa.dag import build_run_dag from arborist.qa.dag import build_run_dag
sig = inspect.signature(build_run_dag) sig = inspect.signature(build_run_dag)
assert "evidence_map_root" in sig.parameters, ( assert "evidence_map_root" in sig.parameters, (
@ -216,7 +216,7 @@ def test_d4_retrieval_plan_binding_landed():
Two runs with identical sources but different retrieval keywords Two runs with identical sources but different retrieval keywords
produce different `run_dag_root` values provenance closes the produce different `run_dag_root` values provenance closes the
'how did retrieval choose these sources' gap.""" 'how did retrieval choose these sources' gap."""
from aborist.qa.dag import build_run_dag from arborist.qa.dag import build_run_dag
sig = inspect.signature(build_run_dag) sig = inspect.signature(build_run_dag)
assert "retrieval_plan_hash" in sig.parameters, ( assert "retrieval_plan_hash" in sig.parameters, (
@ -251,8 +251,8 @@ def test_d4_retrieval_plan_binding_landed():
def test_d4_retrieval_plan_hash_module_exists(): def test_d4_retrieval_plan_hash_module_exists():
"""RetrievalPlan dataclass + retrieval_plan_hash function landed """RetrievalPlan dataclass + retrieval_plan_hash function landed
in `aborist.qa.retrieval_plan` per ticket #000001.""" in `arborist.qa.retrieval_plan` per ticket #000001."""
from aborist.qa.retrieval_plan import RetrievalPlan, retrieval_plan_hash from arborist.qa.retrieval_plan import RetrievalPlan, retrieval_plan_hash
plan = RetrievalPlan( plan = RetrievalPlan(
retrieval_keywords="orwell 1984", retrieval_keywords="orwell 1984",
@ -285,8 +285,8 @@ def test_d5_verify_claim_lattice_is_deterministic():
"""Same inputs → same verdict, byte-for-byte. No randomness, no """Same inputs → same verdict, byte-for-byte. No randomness, no
time-dependent state, no hidden global. Regression here means time-dependent state, no hidden global. Regression here means
the verifier started consulting some non-deterministic source.""" the verifier started consulting some non-deterministic source."""
from aborist.qa.evidence import build_evidence_map from arborist.qa.evidence import build_evidence_map
from aborist.qa.verify import verify_claim_lattice from arborist.qa.verify import verify_claim_lattice
chunks = [{ chunks = [{
"source_root": "f" * 64, "source_root": "f" * 64,
@ -310,7 +310,7 @@ def test_d5_seven_hard_checks_are_pure_functions():
"""Spot check: the per-check helper for citation overlap """Spot check: the per-check helper for citation overlap
(`_claim_textually_overlaps_evidence`) is a pure function no (`_claim_textually_overlaps_evidence`) is a pure function no
network, no chat client, no global state.""" network, no chat client, no global state."""
from aborist.qa.verify import _claim_textually_overlaps_evidence from arborist.qa.verify import _claim_textually_overlaps_evidence
sig = inspect.signature(_claim_textually_overlaps_evidence) sig = inspect.signature(_claim_textually_overlaps_evidence)
forbidden = {"chat_client", "client", "llm", "judge", "model"} forbidden = {"chat_client", "client", "llm", "judge", "model"}
@ -326,7 +326,7 @@ def test_d6_warrant_check_exists_and_is_lexical():
"""warrant_check must exist as a deterministic lexical check. """warrant_check must exist as a deterministic lexical check.
A future replacement that imports an NLI model into the hard A future replacement that imports an NLI model into the hard
path would violate D6's 'before NLI' constraint.""" path would violate D6's 'before NLI' constraint."""
from aborist.qa.warrant import warrant_check from arborist.qa.warrant import warrant_check
sig = inspect.signature(warrant_check) sig = inspect.signature(warrant_check)
forbidden = {"chat_client", "client", "llm", "judge", "model", "nli"} forbidden = {"chat_client", "client", "llm", "judge", "model", "nli"}
@ -342,7 +342,7 @@ def test_d6_warrant_fires_on_date_anchor_mismatch():
class. This case is the deterministic anchor (year strings class. This case is the deterministic anchor (year strings
are case-trivial; ALL-match semantics). Regression here means are case-trivial; ALL-match semantics). Regression here means
the date-anchor extractor or check silently broke.""" the date-anchor extractor or check silently broke."""
from aborist.qa.warrant import warrant_check from arborist.qa.warrant import warrant_check
# Claim asserts "1985"; cited span has no 1985 anywhere. # Claim asserts "1985"; cited span has no 1985 anywhere.
ok, missing = warrant_check( ok, missing = warrant_check(
@ -365,7 +365,7 @@ def test_d6_warrant_relation_shape_fires_when_all_anchors_missing():
extracted anchor must appear in some cited span. This test extracted anchor must appear in some cited span. This test
exercises a true failure where the claim's only proper-noun exercises a true failure where the claim's only proper-noun
anchor is missing from every cited span.""" anchor is missing from every cited span."""
from aborist.qa.warrant import warrant_check from arborist.qa.warrant import warrant_check
ok, missing = warrant_check( ok, missing = warrant_check(
claim_text="Mr. Burns is the boss.", claim_text="Mr. Burns is the boss.",
@ -386,7 +386,7 @@ def test_d6_warrant_generalization_landed():
"""Per-shape warrant detectors landed via ticket #000003. """Per-shape warrant detectors landed via ticket #000003.
Entity-list / count / why-cause shapes now dispatch through Entity-list / count / why-cause shapes now dispatch through
warrant_check alongside the original relation + date classes.""" warrant_check alongside the original relation + date classes."""
import aborist.qa.warrant as warrant_mod import arborist.qa.warrant as warrant_mod
expected = ( expected = (
"_question_is_entity_list_shape", "_question_is_entity_list_shape",
@ -416,7 +416,7 @@ def test_d7_renderer_relabels_strict_for_claim_lattice():
(POINTER-LINKED / ANCHOR-WARRANTED / EVIDENCE-WARRANTED) replaces (POINTER-LINKED / ANCHOR-WARRANTED / EVIDENCE-WARRANTED) replaces
the previous two-rung EVIDENCE-LINKED/EVIDENCE-LINKED-PARTIAL the previous two-rung EVIDENCE-LINKED/EVIDENCE-LINKED-PARTIAL
surface as of #000005.""" surface as of #000005."""
from aborist.cli import _render_audit_label from arborist.cli import _render_audit_label
# No violations + STRICT → top rung. # No violations + STRICT → top rung.
assert "EVIDENCE-WARRANTED" in _render_audit_label("STRICT", "claim_lattice", []) assert "EVIDENCE-WARRANTED" in _render_audit_label("STRICT", "claim_lattice", [])
@ -431,7 +431,7 @@ def test_d7_renderer_keeps_strict_for_pinned_span_methods():
"""Quote / span / entity / paraphrase verify against pinned """Quote / span / entity / paraphrase verify against pinned
spans, not synthesis. STRICT is honest there; renderer must spans, not synthesis. STRICT is honest there; renderer must
NOT apply the ladder relabel.""" NOT apply the ladder relabel."""
from aborist.cli import _render_audit_label from arborist.cli import _render_audit_label
for method in ("quote", "span", "entity", "paraphrase"): for method in ("quote", "span", "entity", "paraphrase"):
label = _render_audit_label("STRICT", method, []) label = _render_audit_label("STRICT", method, [])
@ -447,7 +447,7 @@ def test_d7_audit_mode_enum_canonical_set():
"""Schema column must keep the canonical 3-value enum so v9.8 """Schema column must keep the canonical 3-value enum so v9.8
cache_key invariants hold. Renderer-level relabel (above) cache_key invariants hold. Renderer-level relabel (above)
doesn't touch this.""" doesn't touch this."""
from aborist.store import SCHEMA_SQL from arborist.store import SCHEMA_SQL
match = re.search( match = re.search(
r"audit_mode\s+TEXT[^,]+CHECK\s*\(\s*audit_mode\s+IN\s*\(([^)]+)\)\)", r"audit_mode\s+TEXT[^,]+CHECK\s*\(\s*audit_mode\s+IN\s*\(([^)]+)\)\)",

View file

@ -5,13 +5,13 @@ from __future__ import annotations
import json import json
from typing import Iterator from typing import Iterator
from aborist.distill import FirstSentenceDistiller from arborist.distill import FirstSentenceDistiller
from aborist.distill.runner import distill_existing from arborist.distill.runner import distill_existing
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.merkle import proof_from_dict, verify_proof from arborist.merkle import proof_from_dict, verify_proof
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):

View file

@ -5,13 +5,13 @@ from __future__ import annotations
import json import json
from typing import Iterator from typing import Iterator
from aborist.distill import FirstSentenceDistiller from arborist.distill import FirstSentenceDistiller
from aborist.distill.runner import distill_existing from arborist.distill.runner import distill_existing
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.merkle import proof_from_dict, verify_proof from arborist.merkle import proof_from_dict, verify_proof
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):

View file

@ -5,13 +5,13 @@ from __future__ import annotations
import json import json
from typing import Iterator from typing import Iterator
from aborist.distill import FirstSentenceDistiller from arborist.distill import FirstSentenceDistiller
from aborist.distill.runner import distill_existing from arborist.distill.runner import distill_existing
from aborist.document import Document from arborist.document import Document
from aborist.evict import evict_to_cold, rehydrate from arborist.evict import evict_to_cold, rehydrate
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):
@ -226,7 +226,7 @@ def test_rehydrate_nothing_to_do_when_all_hot(tmp_path):
def test_rehydrate_non_rehydratable_source(tmp_path): def test_rehydrate_non_rehydratable_source(tmp_path):
"""A source_type without a registered fetcher (and no override) is honest about it.""" """A source_type without a registered fetcher (and no override) is honest about it."""
from aborist.source import Source from arborist.source import Source
class WikiSource(Source): class WikiSource(Source):
source_type = "wikipedia_cur" source_type = "wikipedia_cur"

View file

@ -7,7 +7,7 @@ indicators.
""" """
from __future__ import annotations from __future__ import annotations
from aborist.qa.frame import ( from arborist.qa.frame import (
FrameDetection, FrameDetection,
_body_indicates_reference_work, _body_indicates_reference_work,
_source_is_reference_work, _source_is_reference_work,

View file

@ -13,15 +13,15 @@ from pathlib import Path
import pytest import pytest
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.sources.grok import ( from arborist.sources.grok import (
GrokExportSource, GrokExportSource,
GrokMediaPostsSource, GrokMediaPostsSource,
_extract_urls, _extract_urls,
_normalize_ts, _normalize_ts,
_scrub, _scrub,
) )
from aborist.store import connect from arborist.store import connect
def _make_export(tmp_path: Path) -> Path: def _make_export(tmp_path: Path) -> Path:
@ -224,7 +224,7 @@ def test_resolve_missing_path_raises(tmp_path):
def test_grok_ingest_round_trip(tmp_path): def test_grok_ingest_round_trip(tmp_path):
"""End-to-end: ingest a tiny export into a fresh DB, verify counts.""" """End-to-end: ingest a tiny export into a fresh DB, verify counts."""
root = _make_export(tmp_path) root = _make_export(tmp_path)
db_path = tmp_path / "aborist.db" db_path = tmp_path / "arborist.db"
conn = connect(db_path) conn = connect(db_path)
try: try:
conv_stats = ingest_source(conn, GrokExportSource(path=root)) conv_stats = ingest_source(conn, GrokExportSource(path=root))

View file

@ -7,7 +7,7 @@ import pytest
# Skip if optional extras are not installed. # Skip if optional extras are not installed.
selectolax = pytest.importorskip("selectolax") selectolax = pytest.importorskip("selectolax")
from aborist.sources.html_page import parse_html from arborist.sources.html_page import parse_html
SAMPLE_HTML = """<!doctype html> SAMPLE_HTML = """<!doctype html>

View file

@ -4,12 +4,12 @@ from __future__ import annotations
from typing import Iterator from typing import Iterator
from aborist.document import Document, Edge from arborist.document import Document, Edge
from aborist.ingest import ingest_source, verify_random_sample from arborist.ingest import ingest_source, verify_random_sample
from aborist.search import FTS5Backend from arborist.search import FTS5Backend
from aborist.search.base import AuditMode from arborist.search.base import AuditMode
from aborist.source import Source from arborist.source import Source
from aborist.store import connect, stats from arborist.store import connect, stats
class FakeSource(Source): class FakeSource(Source):

View file

@ -1,6 +1,6 @@
"""Sidecar diagnostic for unverified spans. """Sidecar diagnostic for unverified spans.
`aborist inspect --cache-key X` reads a providence_cache record, pulls `arborist inspect --cache-key X` reads a providence_cache record, pulls
the same source chunks the verifier saw, and classifies each the same source chunks the verifier saw, and classifies each
unverified_quote into one of: verbatim_in_base, verbatim_in_raw_only, unverified_quote into one of: verbatim_in_base, verbatim_in_raw_only,
trailing_artifact, paraphrase, partial_paraphrase, no_overlap. trailing_artifact, paraphrase, partial_paraphrase, no_overlap.
@ -18,7 +18,7 @@ from pathlib import Path
import pytest import pytest
from aborist.qa.inspect import ( from arborist.qa.inspect import (
_classify_span, _classify_span,
_normalize, _normalize,
diagnose_deflection, diagnose_deflection,
@ -26,7 +26,7 @@ from aborist.qa.inspect import (
diagnose_title_relevance, diagnose_title_relevance,
inspect_cache_key, inspect_cache_key,
) )
from aborist.store import append_audit, connect, transaction from arborist.store import append_audit, connect, transaction
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -303,7 +303,7 @@ def _seed_doc(shard: Path, *, document_root: str, document_uri: str,
"""Insert a documents row + one hot chunk with the given text.""" """Insert a documents row + one hot chunk with the given text."""
conn = connect(shard) conn = connect(shard)
try: try:
from aborist.compress import pack_chunk from arborist.compress import pack_chunk
with transaction(conn): with transaction(conn):
conn.execute( conn.execute(
@ -666,7 +666,7 @@ def test_metaphor_deflection_filters_common_ly_nouns():
"""Naive .endswith('ly') would pick up 'butterfly', 'family', """Naive .endswith('ly') would pick up 'butterfly', 'family',
'italy', 'july' etc. as adverbs. The block-list filters them so 'italy', 'july' etc. as adverbs. The block-list filters them so
they don't inflate cue count.""" they don't inflate cue count."""
from aborist.qa.inspect import _extract_metaphor_cues from arborist.qa.inspect import _extract_metaphor_cues
cues = _extract_metaphor_cues( cues = _extract_metaphor_cues(
"the butterfly flew over italy in july with the family" "the butterfly flew over italy in july with the family"
) )
@ -694,7 +694,7 @@ def test_metaphor_deflection_under_threshold_returns_no_signal():
def test_register_metaphor_dictionary_unions_into_wordlist(tmp_path): def test_register_metaphor_dictionary_unions_into_wordlist(tmp_path):
"""Custom wordlist registers, unions into the default, and the """Custom wordlist registers, unions into the default, and the
suffix tests pick up domain-specific stems.""" suffix tests pick up domain-specific stems."""
import aborist.qa.inspect as m import arborist.qa.inspect as m
# Reset cache so the test sees a clean slate. # Reset cache so the test sees a clean slate.
saved_cache = m._english_wordlist_cache saved_cache = m._english_wordlist_cache
@ -722,10 +722,10 @@ def test_register_metaphor_dictionary_unions_into_wordlist(tmp_path):
m._extra_dict_paths = saved_extra m._extra_dict_paths = saved_extra
def test_aborist_metaphor_dicts_env_var_supplements(monkeypatch, tmp_path): def test_arborist_metaphor_dicts_env_var_supplements(monkeypatch, tmp_path):
"""Setting ABORIST_METAPHOR_DICTS=path1:path2 unions both into """Setting ARBORIST_METAPHOR_DICTS=path1:path2 unions both into
the default wordlist on first lookup.""" the default wordlist on first lookup."""
import aborist.qa.inspect as m import arborist.qa.inspect as m
saved_cache = m._english_wordlist_cache saved_cache = m._english_wordlist_cache
m._english_wordlist_cache = None m._english_wordlist_cache = None
@ -735,7 +735,7 @@ def test_aborist_metaphor_dicts_env_var_supplements(monkeypatch, tmp_path):
d1.write_text("widgetspeak\n") d1.write_text("widgetspeak\n")
d2 = tmp_path / "d2.txt" d2 = tmp_path / "d2.txt"
d2.write_text("frizzlebop\n") d2.write_text("frizzlebop\n")
monkeypatch.setenv("ABORIST_METAPHOR_DICTS", f"{d1}:{d2}") monkeypatch.setenv("ARBORIST_METAPHOR_DICTS", f"{d1}:{d2}")
wl = m._english_wordlist() wl = m._english_wordlist()
assert "widgetspeak" in wl assert "widgetspeak" in wl
@ -747,7 +747,7 @@ def test_aborist_metaphor_dicts_env_var_supplements(monkeypatch, tmp_path):
def test_register_metaphor_dictionary_idempotent(tmp_path): def test_register_metaphor_dictionary_idempotent(tmp_path):
"""Registering the same path twice is a no-op (cache invalidates """Registering the same path twice is a no-op (cache invalidates
once, second call is a no-op since path already in list).""" once, second call is a no-op since path already in list)."""
import aborist.qa.inspect as m import arborist.qa.inspect as m
saved_extra = list(m._extra_dict_paths) saved_extra = list(m._extra_dict_paths)
saved_cache = m._english_wordlist_cache saved_cache = m._english_wordlist_cache

View file

@ -5,7 +5,7 @@ The contract being tested is the unfirehose/1.0 schema:
- one message per subsequent line - one message per subsequent line
- session_end system message on close - session_end system message on close
- ``$schema: "unfirehose/1.0"`` on every record - ``$schema: "unfirehose/1.0"`` on every record
- aborist-specific extras under namespaced ``aborist_meta`` - arborist-specific extras under namespaced ``arborist_meta``
""" """
from __future__ import annotations from __future__ import annotations
@ -13,7 +13,7 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from aborist.journal import ( from arborist.journal import (
HARNESS_NAME, HARNESS_NAME,
UNFIREHOSE_SCHEMA, UNFIREHOSE_SCHEMA,
SessionWriter, SessionWriter,
@ -27,7 +27,7 @@ def _read_lines(path: Path) -> list[dict]:
def test_slugify_cwd_basic(): def test_slugify_cwd_basic():
assert slugify_cwd("/home/fox/git/aborist") == "-home-fox-git-aborist" assert slugify_cwd("/home/fox/git/arborist") == "-home-fox-git-arborist"
def test_slugify_cwd_dots_become_hyphens(): def test_slugify_cwd_dots_become_hyphens():
@ -84,15 +84,15 @@ def test_close_writes_session_end(tmp_path):
assert last["durationMs"] >= 0 assert last["durationMs"] >= 0
def test_aborist_meta_passes_through(tmp_path): def test_arborist_meta_passes_through(tmp_path):
meta = {"audit_mode": "STRICT", "n_verified": 2, "cache_key": "abc123"} meta = {"audit_mode": "STRICT", "n_verified": 2, "cache_key": "abc123"}
with SessionWriter(first_prompt="q", root=tmp_path) as s: with SessionWriter(first_prompt="q", root=tmp_path) as s:
s.assistant_message("answer", aborist_meta=meta) s.assistant_message("answer", arborist_meta=meta)
rows = _read_lines(list(tmp_path.glob("**/*.jsonl"))[0]) rows = _read_lines(list(tmp_path.glob("**/*.jsonl"))[0])
asst = next(r for r in rows if r["type"] == "message" and r["role"] == "assistant") asst = next(r for r in rows if r["type"] == "message" and r["role"] == "assistant")
assert asst["aborist_meta"]["audit_mode"] == "STRICT" assert asst["arborist_meta"]["audit_mode"] == "STRICT"
assert asst["aborist_meta"]["n_verified"] == 2 assert asst["arborist_meta"]["n_verified"] == 2
assert asst["aborist_meta"]["cache_key"] == "abc123" assert asst["arborist_meta"]["cache_key"] == "abc123"
def test_session_id_used_as_filename(tmp_path): def test_session_id_used_as_filename(tmp_path):

View file

@ -1,6 +1,6 @@
"""Cache-key dimension hashes — pure functions, deterministic. """Cache-key dimension hashes — pure functions, deterministic.
The 8-dim cache_key invariants are spread across `aborist/qa/keys.py`. The 8-dim cache_key invariants are spread across `arborist/qa/keys.py`.
Most behavior is covered indirectly by `test_qa.py` (cache hits, etc.); Most behavior is covered indirectly by `test_qa.py` (cache hits, etc.);
this file pins the hash-input canonicalization rules so a future tweak this file pins the hash-input canonicalization rules so a future tweak
to question normalization can't silently change the equivalence class to question normalization can't silently change the equivalence class
@ -9,7 +9,7 @@ of "what counts as the same question."
from __future__ import annotations from __future__ import annotations
from aborist.qa.keys import ( from arborist.qa.keys import (
cache_key, cache_key,
conversation_hash, conversation_hash,
governance_policy_hash, governance_policy_hash,
@ -51,7 +51,7 @@ def test_question_hash_lowercases():
def test_question_hash_collapses_internal_spaces(): def test_question_hash_collapses_internal_spaces():
"""canonicalize() collapses runs of spaces to a single space. Tabs/ """canonicalize() collapses runs of spaces to a single space. Tabs/
newlines map to newline (a separate equivalence class see newlines map to newline (a separate equivalence class see
aborist.document.canonicalize), so tab vs space is intentionally arborist.document.canonicalize), so tab vs space is intentionally
NOT in the same bucket.""" NOT in the same bucket."""
assert question_hash("who is X") == question_hash("who is X") assert question_hash("who is X") == question_hash("who is X")
@ -248,7 +248,7 @@ def test_verifier_policy_hash_only_hashes_verifier_subset():
governance_policy_hash is to surface verifier-rule changes governance_policy_hash is to surface verifier-rule changes
independently from prompt / sampling-knob changes. independently from prompt / sampling-knob changes.
""" """
from aborist.qa.keys import verifier_policy_hash from arborist.qa.keys import verifier_policy_hash
base = { base = {
"answer_mode": "claim_lattice_pointer", "answer_mode": "claim_lattice_pointer",
@ -273,7 +273,7 @@ def test_verifier_policy_hash_changes_when_verifier_field_changes():
of these means the verifier rules differ and a new cache record of these means the verifier rules differ and a new cache record
is required. is required.
""" """
from aborist.qa.keys import verifier_policy_hash from arborist.qa.keys import verifier_policy_hash
base = { base = {
"answer_mode": "claim_lattice_pointer", "answer_mode": "claim_lattice_pointer",
@ -296,7 +296,7 @@ def test_verifier_policy_hash_changes_when_verifier_field_changes():
def test_verifier_policy_hash_empty_policy_is_stable(): def test_verifier_policy_hash_empty_policy_is_stable():
"""Empty policy → constant hash. Stable across runs.""" """Empty policy → constant hash. Stable across runs."""
from aborist.qa.keys import verifier_policy_hash from arborist.qa.keys import verifier_policy_hash
assert verifier_policy_hash({}) == verifier_policy_hash({}) assert verifier_policy_hash({}) == verifier_policy_hash({})

View file

@ -19,12 +19,12 @@ from pathlib import Path
import pytest import pytest
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.qa.runner import DEFAULT_POLICY, ask from arborist.qa.runner import DEFAULT_POLICY, ask
from aborist.store import connect from arborist.store import connect
from aborist.wikitext import BASE_VERSION from arborist.wikitext import BASE_VERSION
WIKITEXT_DOC = ( WIKITEXT_DOC = (
@ -156,7 +156,7 @@ def test_base_version_changes_governance_hash(tmp_path):
answers (under raw-wikitext policy) from satisfying lookups under answers (under raw-wikitext policy) from satisfying lookups under
the new (stripped) policy on the same question/source they remain the new (stripped) policy on the same question/source they remain
distinct cache_keys.""" distinct cache_keys."""
from aborist.qa.keys import governance_policy_hash from arborist.qa.keys import governance_policy_hash
p_with = dict(DEFAULT_POLICY) p_with = dict(DEFAULT_POLICY)
p_with["base_version"] = "wikitext-base-v1" p_with["base_version"] = "wikitext-base-v1"

View file

@ -10,7 +10,7 @@ from __future__ import annotations
import pytest import pytest
from aborist.merkle import ( from arborist.merkle import (
HASH_LEN, HASH_LEN,
MerkleTree, MerkleTree,
ZERO_HASH, ZERO_HASH,
@ -117,7 +117,7 @@ def test_tampered_sibling_fails_verification():
leaves = [_leaf(f"chunk-{i}") for i in range(4)] leaves = [_leaf(f"chunk-{i}") for i in range(4)]
tree = MerkleTree.build(leaves) tree = MerkleTree.build(leaves)
proof = tree.proof(0) proof = tree.proof(0)
from aborist.merkle import ProofNode from arborist.merkle import ProofNode
bad_siblings = list(proof.siblings) bad_siblings = list(proof.siblings)
s0 = bad_siblings[0] s0 = bad_siblings[0]
@ -136,7 +136,7 @@ def test_swapped_is_left_flag_fails():
leaves = [_leaf(f"chunk-{i}") for i in range(4)] leaves = [_leaf(f"chunk-{i}") for i in range(4)]
tree = MerkleTree.build(leaves) tree = MerkleTree.build(leaves)
proof = tree.proof(1) proof = tree.proof(1)
from aborist.merkle import MerkleProof, ProofNode from arborist.merkle import MerkleProof, ProofNode
flipped = MerkleProof( flipped = MerkleProof(
leaf=proof.leaf, leaf=proof.leaf,

View file

@ -1,6 +1,6 @@
"""Tests for the mesh layer: crypto primitives, state machine, eviction. """Tests for the mesh layer: crypto primitives, state machine, eviction.
Mesh state lives in the standard aborist DB. These tests build a fresh Mesh state lives in the standard arborist DB. These tests build a fresh
DB per test via tmp_path so cross-test contamination is impossible. DB per test via tmp_path so cross-test contamination is impossible.
""" """
@ -10,7 +10,7 @@ import os
import pytest import pytest
from aborist.mesh import ( from arborist.mesh import (
aead_decrypt, aead_decrypt,
aead_encrypt, aead_encrypt,
current_epoch, current_epoch,
@ -24,13 +24,13 @@ from aborist.mesh import (
sign, sign,
verify, verify,
) )
from aborist.mesh.members import add_member, kick_member, scheduled_rotate from arborist.mesh.members import add_member, kick_member, scheduled_rotate
from aborist.mesh.state import ( from arborist.mesh.state import (
MeshRosterEntry, MeshRosterEntry,
roster_at, roster_at,
unwrap_secret_for_self, unwrap_secret_for_self,
) )
from aborist.store import connect from arborist.store import connect
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -21,10 +21,10 @@ from pathlib import Path
import pytest import pytest
from aborist.mesh import init_identity from arborist.mesh import init_identity
from aborist.mesh.members import add_member from arborist.mesh.members import add_member
from aborist.mesh.state import recover_epoch_secret from arborist.mesh.state import recover_epoch_secret
from aborist.mesh.wire import ( from arborist.mesh.wire import (
MeshWireServer, MeshWireServer,
TYPE_ANNOUNCE_FALSIFICATION, TYPE_ANNOUNCE_FALSIFICATION,
TYPE_ANNOUNCE_ROOT, TYPE_ANNOUNCE_ROOT,
@ -32,7 +32,7 @@ from aborist.mesh.wire import (
decrypt_envelope_body, decrypt_envelope_body,
encrypt_body_for_envelope, encrypt_body_for_envelope,
) )
from aborist.store import connect from arborist.store import connect
def _bootstrap_two_peer_db(tmp_path: Path) -> tuple[Path, Path, dict, dict]: def _bootstrap_two_peer_db(tmp_path: Path) -> tuple[Path, Path, dict, dict]:
@ -55,7 +55,7 @@ def _bootstrap_two_peer_db(tmp_path: Path) -> tuple[Path, Path, dict, dict]:
import json as _json import json as _json
import sqlite3 as _sqlite3 import sqlite3 as _sqlite3
from aborist.store import append_audit, latest_event_hash from arborist.store import append_audit, latest_event_hash
alice_db = tmp_path / "alice.db" alice_db = tmp_path / "alice.db"
bob_db = tmp_path / "bob.db" bob_db = tmp_path / "bob.db"
@ -409,7 +409,7 @@ def test_mixed_cleartext_and_encrypted_round_trip(tmp_path):
"""Alice sends one cleartext ANNOUNCE_ROOT, then one encrypted """Alice sends one cleartext ANNOUNCE_ROOT, then one encrypted
ANNOUNCE_FALSIFICATION. Bob accepts both; audit chain logs the ANNOUNCE_FALSIFICATION. Bob accepts both; audit chain logs the
`encrypted` flag accurately on each.""" `encrypted` flag accurately on each."""
from aborist.mesh.wire import MeshWireClient from arborist.mesh.wire import MeshWireClient
alice_db, bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path) alice_db, bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
bob_srv = MeshWireServer(bob_db, host="127.0.0.1", port=0) bob_srv = MeshWireServer(bob_db, host="127.0.0.1", port=0)
@ -463,7 +463,7 @@ def test_cleartext_default_envelope_unchanged(tmp_path):
identically to the v1 wire format no `encrypted_body` field on identically to the v1 wire format no `encrypted_body` field on
the canonical bytes, no signature change, no audit-body change. the canonical bytes, no signature change, no audit-body change.
""" """
from aborist.mesh.wire import MeshWireClient from arborist.mesh.wire import MeshWireClient
alice_db, bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path) alice_db, bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
bob_srv = MeshWireServer(bob_db, host="127.0.0.1", port=0) bob_srv = MeshWireServer(bob_db, host="127.0.0.1", port=0)

View file

@ -21,15 +21,15 @@ from pathlib import Path
import pytest import pytest
from aborist.mesh import init_identity from arborist.mesh import init_identity
from aborist.mesh.crypto import sign as _sign from arborist.mesh.crypto import sign as _sign
from aborist.mesh.members import add_member from arborist.mesh.members import add_member
from aborist.mesh.wire import ( from arborist.mesh.wire import (
MeshWireServer, MeshWireServer,
TYPE_ANNOUNCE_ROOT, TYPE_ANNOUNCE_ROOT,
WireEnvelope, WireEnvelope,
) )
from aborist.store import connect from arborist.store import connect
def _bootstrap_three_peer_db(tmp_path: Path): def _bootstrap_three_peer_db(tmp_path: Path):
@ -40,7 +40,7 @@ def _bootstrap_three_peer_db(tmp_path: Path):
roster at ``current_epoch`` contains bob and carol so signature roster at ``current_epoch`` contains bob and carol so signature
verification finds their sign_pubs when receiving from either. verification finds their sign_pubs when receiving from either.
""" """
from aborist.mesh.state import current_epoch as _current_epoch from arborist.mesh.state import current_epoch as _current_epoch
alice_db = tmp_path / "alice.db" alice_db = tmp_path / "alice.db"
bob_db = tmp_path / "bob.db" bob_db = tmp_path / "bob.db"

View file

@ -1,4 +1,4 @@
"""CLI integration: `aborist mesh serve` and `aborist mesh sync`. """CLI integration: `arborist mesh serve` and `arborist mesh sync`.
Wire-layer protocol behavior is covered in test_mesh_wire.py and Wire-layer protocol behavior is covered in test_mesh_wire.py and
test_mesh_wire_e2e.py these tests just exercise the argparse plumbing, test_mesh_wire_e2e.py these tests just exercise the argparse plumbing,
@ -11,13 +11,13 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from aborist.cli import build_parser from arborist.cli import build_parser
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.mesh import init_identity, set_enabled from arborist.mesh import init_identity, set_enabled
from aborist.mesh.members import add_member from arborist.mesh.members import add_member
from aborist.mesh.wire import MeshWireServer from arborist.mesh.wire import MeshWireServer
from aborist.store import connect from arborist.store import connect
class _OneDocSource: class _OneDocSource:
@ -166,7 +166,7 @@ def test_sync_announces_falsifications(tmp_path, capsys):
the falsification. Alice's audit chain gains one mesh_received event the falsification. Alice's audit chain gains one mesh_received event
whose body carries an ANNOUNCE_FALSIFICATION envelope.""" whose body carries an ANNOUNCE_FALSIFICATION envelope."""
import time as _time import time as _time
from aborist.store import append_audit, transaction from arborist.store import append_audit, transaction
alice_db, bob_db, _alice, _bob = _bootstrap_two_peers(tmp_path) alice_db, bob_db, _alice, _bob = _bootstrap_two_peers(tmp_path)
@ -232,7 +232,7 @@ def test_sync_no_falsifications_flag_skips_them(tmp_path, capsys):
"""--no-falsifications skips the falsification broadcast. Bob's """--no-falsifications skips the falsification broadcast. Bob's
falsification is NOT announced. Alice gets only the root broadcasts.""" falsification is NOT announced. Alice gets only the root broadcasts."""
import time as _time import time as _time
from aborist.store import append_audit, transaction from arborist.store import append_audit, transaction
alice_db, bob_db, _alice, _bob = _bootstrap_two_peers(tmp_path) alice_db, bob_db, _alice, _bob = _bootstrap_two_peers(tmp_path)
b_conn = connect(bob_db) b_conn = connect(bob_db)
@ -322,7 +322,7 @@ def test_sync_kindergarten_holds_fresh_records(tmp_path, capsys):
A document ingested 30 seconds ago must NOT broadcast under the A document ingested 30 seconds ago must NOT broadcast under the
default 1-hour kindergarten window.""" default 1-hour kindergarten window."""
import time as _time import time as _time
from aborist.store import transaction from arborist.store import transaction
alice_db, bob_db, _alice, _bob = _bootstrap_two_peers(tmp_path) alice_db, bob_db, _alice, _bob = _bootstrap_two_peers(tmp_path)
_ingest(bob_db, uri="https://example.com/old", content="Old prose.", title="Old") _ingest(bob_db, uri="https://example.com/old", content="Old prose.", title="Old")

View file

@ -1,4 +1,4 @@
"""CLI integration: `aborist mesh pull`. """CLI integration: `arborist mesh pull`.
Closes the request half of the gossip loop. Wire-layer Merkle / signature Closes the request half of the gossip loop. Wire-layer Merkle / signature
verification is covered in test_mesh_wire_e2e.py these tests cover the verification is covered in test_mesh_wire_e2e.py these tests cover the
@ -11,13 +11,13 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from aborist.cli import build_parser from arborist.cli import build_parser
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.mesh import init_identity, set_enabled from arborist.mesh import init_identity, set_enabled
from aborist.mesh.members import add_member from arborist.mesh.members import add_member
from aborist.mesh.wire import MeshWireServer from arborist.mesh.wire import MeshWireServer
from aborist.store import connect from arborist.store import connect
class _OneDocSource: class _OneDocSource:

View file

@ -11,13 +11,13 @@ from pathlib import Path
import pytest import pytest
from aborist.mesh import init_identity from arborist.mesh import init_identity
from aborist.mesh.members import add_member from arborist.mesh.members import add_member
from aborist.mesh.crypto import ( from arborist.mesh.crypto import (
generate_dh_keypair, generate_dh_keypair,
generate_signing_keypair, generate_signing_keypair,
) )
from aborist.mesh.wire import ( from arborist.mesh.wire import (
ALL_TYPES, ALL_TYPES,
PATH_ANNOUNCE, PATH_ANNOUNCE,
PATH_INFO, PATH_INFO,
@ -30,7 +30,7 @@ from aborist.mesh.wire import (
WireEnvelope, WireEnvelope,
verify_envelope_sig, verify_envelope_sig,
) )
from aborist.store import connect from arborist.store import connect
def _bootstrap_two_peer_db(tmp_path: Path) -> tuple[Path, Path, dict, dict]: def _bootstrap_two_peer_db(tmp_path: Path) -> tuple[Path, Path, dict, dict]:
@ -205,7 +205,7 @@ def test_verify_envelope_sig_accepts_known_member(tmp_path):
body={"document_root": "11" * 32, "source_uri": "https://x"}, body={"document_root": "11" * 32, "source_uri": "https://x"},
) )
sig_payload = env.canonical_bytes() sig_payload = env.canonical_bytes()
from aborist.mesh.crypto import sign as _sign from arborist.mesh.crypto import sign as _sign
sig = _sign(alice_id.sign_priv, sig_payload) sig = _sign(alice_id.sign_priv, sig_payload)
a_conn = connect(alice_db) a_conn = connect(alice_db)
@ -225,7 +225,7 @@ def test_verify_envelope_sig_rejects_tampered_body(tmp_path):
ts=100, ts=100,
body={"document_root": "aa" * 32, "source_uri": "https://x"}, body={"document_root": "aa" * 32, "source_uri": "https://x"},
) )
from aborist.mesh.crypto import sign as _sign from arborist.mesh.crypto import sign as _sign
sig = _sign(alice_id.sign_priv, env_orig.canonical_bytes()) sig = _sign(alice_id.sign_priv, env_orig.canonical_bytes())
env_tampered = WireEnvelope( env_tampered = WireEnvelope(
@ -252,7 +252,7 @@ def test_verify_envelope_sig_rejects_unknown_sender(tmp_path):
ts=0, ts=0,
body={"document_root": "00" * 32, "source_uri": "https://x"}, body={"document_root": "00" * 32, "source_uri": "https://x"},
) )
from aborist.mesh.crypto import sign as _sign from arborist.mesh.crypto import sign as _sign
sig = _sign(sign_priv, env.canonical_bytes()) sig = _sign(sign_priv, env.canonical_bytes())
a_conn = connect(alice_db) a_conn = connect(alice_db)
try: try:
@ -272,7 +272,7 @@ def test_verify_envelope_sig_rejects_wrong_key_for_known_member(tmp_path):
ts=0, ts=0,
body={"document_root": "00" * 32, "source_uri": "https://x"}, body={"document_root": "00" * 32, "source_uri": "https://x"},
) )
from aborist.mesh.crypto import sign as _sign from arborist.mesh.crypto import sign as _sign
sig = _sign(rogue_priv, env.canonical_bytes()) sig = _sign(rogue_priv, env.canonical_bytes())
a_conn = connect(alice_db) a_conn = connect(alice_db)
try: try:
@ -288,7 +288,7 @@ def test_verify_envelope_sig_rejects_wrong_key_for_known_member(tmp_path):
def test_handle_announce_writes_audit_event(tmp_path): def test_handle_announce_writes_audit_event(tmp_path):
"""Bob sends ANNOUNCE_ROOT to Alice's server; Alice's chain gains an event.""" """Bob sends ANNOUNCE_ROOT to Alice's server; Alice's chain gains an event."""
from aborist.mesh.wire import MeshWireServer from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path) alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
@ -340,7 +340,7 @@ def test_handle_announce_writes_audit_event(tmp_path):
def test_handle_announce_rejects_bad_sig(tmp_path): def test_handle_announce_rejects_bad_sig(tmp_path):
"""Sig from wrong key → 401, no audit event.""" """Sig from wrong key → 401, no audit event."""
from aborist.mesh.wire import MeshWireServer from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path) alice_db, _bob_db, _alice_id, _bob_id = _bootstrap_two_peer_db(tmp_path)
rogue_priv, _ = generate_signing_keypair() rogue_priv, _ = generate_signing_keypair()
@ -352,7 +352,7 @@ def test_handle_announce_rejects_bad_sig(tmp_path):
ts=0, ts=0,
body={"document_root": "00" * 32, "source_uri": "x"}, body={"document_root": "00" * 32, "source_uri": "x"},
) )
from aborist.mesh.crypto import sign as _sign from arborist.mesh.crypto import sign as _sign
sig = _sign(rogue_priv, env.canonical_bytes()) sig = _sign(rogue_priv, env.canonical_bytes())
import base64 as _b64 import base64 as _b64
signed = {"envelope": env.__dict__, "sig_b64": _b64.b64encode(sig).decode()} signed = {"envelope": env.__dict__, "sig_b64": _b64.b64encode(sig).decode()}
@ -381,7 +381,7 @@ def test_handle_announce_rejects_bad_sig(tmp_path):
def test_handle_announce_rejects_request_body_type(tmp_path): def test_handle_announce_rejects_request_body_type(tmp_path):
"""The /announce endpoint refuses REQUEST_BODY / DELIVER_BODY.""" """The /announce endpoint refuses REQUEST_BODY / DELIVER_BODY."""
from aborist.mesh.wire import MeshWireServer from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path) alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
env = WireEnvelope( env = WireEnvelope(
@ -404,7 +404,7 @@ def test_handle_announce_rejects_request_body_type(tmp_path):
def test_handle_request_404_on_unknown_root(tmp_path): def test_handle_request_404_on_unknown_root(tmp_path):
"""Asking for a doc the responder doesn't have returns 404 cleanly.""" """Asking for a doc the responder doesn't have returns 404 cleanly."""
from aborist.mesh.wire import MeshWireServer from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path) alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
env = WireEnvelope( env = WireEnvelope(
@ -427,7 +427,7 @@ def test_handle_request_404_on_unknown_root(tmp_path):
def test_announce_falsification_round_trips(tmp_path): def test_announce_falsification_round_trips(tmp_path):
"""Falsifications propagate as ANNOUNCE_FALSIFICATION.""" """Falsifications propagate as ANNOUNCE_FALSIFICATION."""
from aborist.mesh.wire import MeshWireServer from arborist.mesh.wire import MeshWireServer
alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path) alice_db, _bob_db, _alice_id, bob_id = _bootstrap_two_peer_db(tmp_path)
env = WireEnvelope( env = WireEnvelope(

View file

@ -17,16 +17,16 @@ from pathlib import Path
import pytest import pytest
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.mesh import init_identity from arborist.mesh import init_identity
from aborist.mesh.members import add_member from arborist.mesh.members import add_member
from aborist.mesh.wire import ( from arborist.mesh.wire import (
MeshWireClient, MeshWireClient,
MeshWireServer, MeshWireServer,
TYPE_ANNOUNCE_ROOT, TYPE_ANNOUNCE_ROOT,
) )
from aborist.store import connect from arborist.store import connect
@pytest.fixture @pytest.fixture
@ -180,7 +180,7 @@ def test_request_body_rejects_tampered_response(two_peers, monkeypatch):
title="X", title="X",
) )
with MeshWireClient(two_peers["bob"]["db"], two_peers["alice"]["srv"].url) as c: with MeshWireClient(two_peers["bob"]["db"], two_peers["alice"]["srv"].url) as c:
from aborist.mesh import wire as _wire from arborist.mesh import wire as _wire
monkeypatch.setattr(_wire, "_merkle_root_matches", lambda *a, **k: False) monkeypatch.setattr(_wire, "_merkle_root_matches", lambda *a, **k: False)
with pytest.raises(ValueError, match="Merkle-derive"): with pytest.raises(ValueError, match="Merkle-derive"):
c.request_body(root=document_root) c.request_body(root=document_root)

View file

@ -10,7 +10,7 @@ from __future__ import annotations
import pytest import pytest
from aborist.qa.metacognition import ( from arborist.qa.metacognition import (
PREFLIGHT_VERSION, PREFLIGHT_VERSION,
QuestionState, QuestionState,
detect_contradiction, detect_contradiction,
@ -305,12 +305,12 @@ def test_question_state_is_serializable():
def test_metacognition_field_is_in_verifier_policy_fields(field): def test_metacognition_field_is_in_verifier_policy_fields(field):
"""All six policy fields must be in _VERIFIER_POLICY_FIELDS so """All six policy fields must be in _VERIFIER_POLICY_FIELDS so
flipping any of them invalidates prior cache records on lookup.""" flipping any of them invalidates prior cache records on lookup."""
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS from arborist.qa.keys import _VERIFIER_POLICY_FIELDS
assert field in _VERIFIER_POLICY_FIELDS assert field in _VERIFIER_POLICY_FIELDS
def test_governance_hash_changes_when_metacognition_enabled_flips(): def test_governance_hash_changes_when_metacognition_enabled_flips():
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash from arborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash
base = dict.fromkeys(_VERIFIER_POLICY_FIELDS, "default") base = dict.fromkeys(_VERIFIER_POLICY_FIELDS, "default")
base["metacognition_enabled"] = True base["metacognition_enabled"] = True
h_on = verifier_policy_hash(base) h_on = verifier_policy_hash(base)
@ -320,7 +320,7 @@ def test_governance_hash_changes_when_metacognition_enabled_flips():
def test_governance_hash_changes_when_block_on_contradiction_flips(): def test_governance_hash_changes_when_block_on_contradiction_flips():
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash from arborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash
base = dict.fromkeys(_VERIFIER_POLICY_FIELDS, "default") base = dict.fromkeys(_VERIFIER_POLICY_FIELDS, "default")
base["metacognition_block_on_contradiction"] = False base["metacognition_block_on_contradiction"] = False
h_off = verifier_policy_hash(base) h_off = verifier_policy_hash(base)
@ -332,8 +332,8 @@ def test_governance_hash_changes_when_block_on_contradiction_flips():
def test_default_policy_has_metacognition_enabled(): def test_default_policy_has_metacognition_enabled():
"""Master switch default-on per ticket #000010 §7.3 — detectors """Master switch default-on per ticket #000010 §7.3 — detectors
are pure-on-question so the cost is negligible.""" are pure-on-question so the cost is negligible."""
from aborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY from arborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
assert RUNNER_POLICY["metacognition_enabled"] is True assert RUNNER_POLICY["metacognition_enabled"] is True
assert DEFAULT_QUERY_POLICY["metacognition_enabled"] is True assert DEFAULT_QUERY_POLICY["metacognition_enabled"] is True
@ -341,8 +341,8 @@ def test_default_policy_has_metacognition_enabled():
def test_default_policy_block_on_contradiction_off(): def test_default_policy_block_on_contradiction_off():
"""Default to label-only on contradictions. False-positive risk """Default to label-only on contradictions. False-positive risk
not yet bench-validated; opt-in via --block-on-contradiction.""" not yet bench-validated; opt-in via --block-on-contradiction."""
from aborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY from arborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
assert RUNNER_POLICY["metacognition_block_on_contradiction"] is False assert RUNNER_POLICY["metacognition_block_on_contradiction"] is False
assert DEFAULT_QUERY_POLICY["metacognition_block_on_contradiction"] is False assert DEFAULT_QUERY_POLICY["metacognition_block_on_contradiction"] is False
@ -360,7 +360,7 @@ def _result_with_question_state(state: QuestionState) -> dict:
def test_tail_renders_false_premise(): def test_tail_renders_false_premise():
from aborist.cli import _render_warrant_tail from arborist.cli import _render_warrant_tail
state = preflight_question( state = preflight_question(
"When did Mr. Burns become Homer's biological father?" "When did Mr. Burns become Homer's biological father?"
) )
@ -369,28 +369,28 @@ def test_tail_renders_false_premise():
def test_tail_renders_contradictory(): def test_tail_renders_contradictory():
from aborist.cli import _render_warrant_tail from arborist.cli import _render_warrant_tail
state = preflight_question("Which unmarried spouse is Homer married to?") state = preflight_question("Which unmarried spouse is Homer married to?")
tail = _render_warrant_tail(_result_with_question_state(state)) tail = _render_warrant_tail(_result_with_question_state(state))
assert "contradictory" in tail assert "contradictory" in tail
def test_tail_renders_stale_risk(): def test_tail_renders_stale_risk():
from aborist.cli import _render_warrant_tail from arborist.cli import _render_warrant_tail
state = preflight_question("Who is the current CEO of OpenAI?") state = preflight_question("Who is the current CEO of OpenAI?")
tail = _render_warrant_tail(_result_with_question_state(state)) tail = _render_warrant_tail(_result_with_question_state(state))
assert "stale risk" in tail assert "stale risk" in tail
def test_tail_renders_out_of_corpus(): def test_tail_renders_out_of_corpus():
from aborist.cli import _render_warrant_tail from arborist.cli import _render_warrant_tail
state = preflight_question("What does my uploaded contract say?") state = preflight_question("What does my uploaded contract say?")
tail = _render_warrant_tail(_result_with_question_state(state)) tail = _render_warrant_tail(_result_with_question_state(state))
assert "out of corpus" in tail assert "out of corpus" in tail
def test_tail_omits_metacog_when_well_formed(): def test_tail_omits_metacog_when_well_formed():
from aborist.cli import _render_warrant_tail from arborist.cli import _render_warrant_tail
state = preflight_question("what is the capital of france?") state = preflight_question("what is the capital of france?")
tail = _render_warrant_tail(_result_with_question_state(state)) tail = _render_warrant_tail(_result_with_question_state(state))
# well_formed should NOT produce any metacog tail tokens. # well_formed should NOT produce any metacog tail tokens.
@ -403,7 +403,7 @@ def test_tail_omits_metacog_when_well_formed():
def test_tail_combines_metacog_with_existing_kinds(): def test_tail_combines_metacog_with_existing_kinds():
"""Multiple tails compose: a TITLE_MISMATCH from the verifier """Multiple tails compose: a TITLE_MISMATCH from the verifier
plus a stale_risk from the preflight should both surface.""" plus a stale_risk from the preflight should both surface."""
from aborist.cli import _render_warrant_tail from arborist.cli import _render_warrant_tail
state = preflight_question("Who is the current CEO of OpenAI?") state = preflight_question("Who is the current CEO of OpenAI?")
result = _result_with_question_state(state) result = _result_with_question_state(state)
result["violations"] = [{"kind": "TITLE_MISMATCH"}] result["violations"] = [{"kind": "TITLE_MISMATCH"}]

View file

@ -14,7 +14,7 @@ from __future__ import annotations
import sqlite3 import sqlite3
from aborist.store import connect from arborist.store import connect
# Pre-v9.8-audit-mode providence_cache (snapshot of the schema before the # Pre-v9.8-audit-mode providence_cache (snapshot of the schema before the

View file

@ -9,7 +9,7 @@ docs/self-reference-design.md:
4. anti-recursion: records whose answer cites a self-reference URI 4. anti-recursion: records whose answer cites a self-reference URI
are excluded (first-generation only) are excluded (first-generation only)
Plus the classifier-side: documents with `aborist://providence/` Plus the classifier-side: documents with `arborist://providence/`
URIs classify as `self_reference_source`. URIs classify as `self_reference_source`.
""" """
@ -20,12 +20,12 @@ import time
import pytest import pytest
from aborist.qa.query import _classify_source_role from arborist.qa.query import _classify_source_role
from aborist.sources.providence import ( from arborist.sources.providence import (
PROVIDENCE_URI_PREFIX, PROVIDENCE_URI_PREFIX,
ProvidenceSource, ProvidenceSource,
) )
from aborist.store import connect from arborist.store import connect
def _seed(conn: sqlite3.Connection, **fields) -> str: def _seed(conn: sqlite3.Connection, **fields) -> str:
@ -160,7 +160,7 @@ def test_kindergarten_zero_admits_all_strict_live(tmp_path):
def test_anti_recursion_excludes_self_referencing_records(tmp_path): def test_anti_recursion_excludes_self_referencing_records(tmp_path):
"""A STRICT record whose own answer text contains """A STRICT record whose own answer text contains
`aborist://providence/...` is excluded first-generation only. `arborist://providence/...` is excluded first-generation only.
Prevents echo-chamber chains where a wrong-but-STRICT record Prevents echo-chamber chains where a wrong-but-STRICT record
keeps getting recompiled into deeper claims.""" keeps getting recompiled into deeper claims."""
db = tmp_path / "p.db" db = tmp_path / "p.db"

View file

@ -9,20 +9,20 @@ from __future__ import annotations
import hashlib import hashlib
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.merkle import proof_from_dict, verify_proof from arborist.merkle import proof_from_dict, verify_proof
from aborist.qa import ask from arborist.qa import ask
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.qa.keys import ( from arborist.qa.keys import (
cache_key, cache_key,
conversation_hash, conversation_hash,
governance_policy_hash, governance_policy_hash,
model_profile_hash, model_profile_hash,
question_hash, question_hash,
) )
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):
@ -195,7 +195,7 @@ def test_unknown_document(tmp_path):
def test_cold_source_refuses(tmp_path): def test_cold_source_refuses(tmp_path):
"""Source must be hot — answer derived from evicted content can't be proved.""" """Source must be hot — answer derived from evicted content can't be proved."""
from aborist.evict import evict_to_cold from arborist.evict import evict_to_cold
db = tmp_path / "cold.db" db = tmp_path / "cold.db"
conn = connect(db) conn = connect(db)

View file

@ -2,9 +2,9 @@
These tests are deliberately gated they hit These tests are deliberately gated they hit
``hermes.ai.unturf.com/v1`` and require a populated shard set under ``hermes.ai.unturf.com/v1`` and require a populated shard set under
``~/.aborist/shards`` (default) or the path passed via ``~/.arborist/shards`` (default) or the path passed via
``ABORIST_LIVE_SHARDS_DIR``. Default `make test` does NOT run these; ``ARBORIST_LIVE_SHARDS_DIR``. Default `make test` does NOT run these;
opt in via ``ABORIST_LIVE_TESTS=1`` (the ``make test-live`` target opt in via ``ARBORIST_LIVE_TESTS=1`` (the ``make test-live`` target
sets it). sets it).
Why this exists: Why this exists:
@ -44,24 +44,24 @@ from pathlib import Path
import pytest import pytest
_LIVE_OPT_IN = os.environ.get("ABORIST_LIVE_TESTS") == "1" _LIVE_OPT_IN = os.environ.get("ARBORIST_LIVE_TESTS") == "1"
_SHARDS_DIR = Path( _SHARDS_DIR = Path(
os.environ.get("ABORIST_LIVE_SHARDS_DIR") os.environ.get("ARBORIST_LIVE_SHARDS_DIR")
or (Path.home() / ".aborist" / "shards") or (Path.home() / ".arborist" / "shards")
) )
_QA_DB = _SHARDS_DIR / "qa.db" _QA_DB = _SHARDS_DIR / "qa.db"
_ENDPOINT = os.environ.get( _ENDPOINT = os.environ.get(
"ABORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1" "ARBORIST_LLM_ENDPOINT", "https://hermes.ai.unturf.com/v1"
) )
_MODEL = os.environ.get( _MODEL = os.environ.get(
"ABORIST_LLM_MODEL", "ARBORIST_LLM_MODEL",
"adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic", "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic",
) )
pytestmark = pytest.mark.skipif( pytestmark = pytest.mark.skipif(
not _LIVE_OPT_IN, not _LIVE_OPT_IN,
reason="live QA tests gated by ABORIST_LIVE_TESTS=1; " reason="live QA tests gated by ARBORIST_LIVE_TESTS=1; "
"run via `make test-live` or set the env var", "run via `make test-live` or set the env var",
) )
@ -75,10 +75,10 @@ def _ask(question: str, *, mode: str = "claim_lattice", top_k: int = 8) -> dict:
# Lazy import so collecting the file doesn't pull qa modules in # Lazy import so collecting the file doesn't pull qa modules in
# default-skip mode. # default-skip mode.
from aborist.qa.client import OpenAICompatibleClient from arborist.qa.client import OpenAICompatibleClient
from aborist.qa.query import DEFAULT_QUERY_POLICY, query from arborist.qa.query import DEFAULT_QUERY_POLICY, query
api_key = os.environ.get("ABORIST_LLM_API_KEY") api_key = os.environ.get("ARBORIST_LLM_API_KEY")
client = OpenAICompatibleClient(base_url=_ENDPOINT, api_key=api_key) client = OpenAICompatibleClient(base_url=_ENDPOINT, api_key=api_key)
policy = dict(DEFAULT_QUERY_POLICY) policy = dict(DEFAULT_QUERY_POLICY)
policy["answer_mode"] = mode policy["answer_mode"] = mode

View file

@ -1,7 +1,7 @@
"""Quantifier cap table + governance hash + dry-run discipline. """Quantifier cap table + governance hash + dry-run discipline.
Ticket #000008 Phase 2: per-model cap profiles Ticket #000008 Phase 2: per-model cap profiles
(``aborist/qa/model_profiles.py``) and the four policy fields that (``arborist/qa/model_profiles.py``) and the four policy fields that
gate cap behavior: gate cap behavior:
- ``quantifier_guard_enabled`` master kill - ``quantifier_guard_enabled`` master kill
@ -23,8 +23,8 @@ from __future__ import annotations
import pytest import pytest
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash from arborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash
from aborist.qa.model_profiles import ( from arborist.qa.model_profiles import (
EXPLICIT_COUNT, EXPLICIT_COUNT,
PROFILES, PROFILES,
cap_for_intensity, cap_for_intensity,
@ -230,8 +230,8 @@ def test_default_reminder_enabled_for_lattice_modes():
eliminates FORMAT_COLLAPSED, reduces NO_EVIDENCE_POINTER 33%, eliminates FORMAT_COLLAPSED, reduces NO_EVIDENCE_POINTER 33%,
boosts mean ratio +17pp pointer / +21pp JSON, rescues JSON boosts mean ratio +17pp pointer / +21pp JSON, rescues JSON
UNGROUNDED 71.""" UNGROUNDED 71."""
from aborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY from arborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
assert RUNNER_POLICY["quantifier_reminder_enabled"] is True assert RUNNER_POLICY["quantifier_reminder_enabled"] is True
assert DEFAULT_QUERY_POLICY["quantifier_reminder_enabled"] is True assert DEFAULT_QUERY_POLICY["quantifier_reminder_enabled"] is True
@ -242,8 +242,8 @@ def test_default_apply_caps_modes_is_json_only():
TOO_MANY_CLAIMS 20× without moving the 0/45 STRICT floor. TOO_MANY_CLAIMS 20× without moving the 0/45 STRICT floor.
Default the apply-caps allowlist to JSON-only so flipping Default the apply-caps allowlist to JSON-only so flipping
apply_caps=True doesn't add wasted cap-noise on pointer mode.""" apply_caps=True doesn't add wasted cap-noise on pointer mode."""
from aborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY from arborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
assert RUNNER_POLICY["quantifier_apply_caps_modes"] == ["claim_lattice"] assert RUNNER_POLICY["quantifier_apply_caps_modes"] == ["claim_lattice"]
assert DEFAULT_QUERY_POLICY["quantifier_apply_caps_modes"] == ["claim_lattice"] assert DEFAULT_QUERY_POLICY["quantifier_apply_caps_modes"] == ["claim_lattice"]
@ -271,7 +271,7 @@ def test_apply_caps_default_off_preserves_dry_run_discipline():
even with reminder default-on. Dry-run discipline (§10.11.3) even with reminder default-on. Dry-run discipline (§10.11.3)
survives the §12 bench cycle operators flip apply_caps via survives the §12 bench cycle operators flip apply_caps via
--apply-quantifier-caps after their own bench review.""" --apply-quantifier-caps after their own bench review."""
from aborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY from arborist.qa.runner import DEFAULT_POLICY as RUNNER_POLICY
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
assert RUNNER_POLICY["quantifier_guard_apply_caps"] is False assert RUNNER_POLICY["quantifier_guard_apply_caps"] is False
assert DEFAULT_QUERY_POLICY["quantifier_guard_apply_caps"] is False assert DEFAULT_QUERY_POLICY["quantifier_guard_apply_caps"] is False

View file

@ -14,7 +14,7 @@ from __future__ import annotations
import pytest import pytest
from aborist.qa.quantifier import ( from arborist.qa.quantifier import (
CLASSIFIER_VERSION, CLASSIFIER_VERSION,
classify_question_quantifier, classify_question_quantifier,
) )

View file

@ -17,13 +17,13 @@ from __future__ import annotations
import pytest import pytest
from aborist.cli import ( from arborist.cli import (
_SOFT_DEMOTE_VIOLATION_KINDS, _SOFT_DEMOTE_VIOLATION_KINDS,
_ladder_rung_for_lattice, _ladder_rung_for_lattice,
_render_query_human, _render_query_human,
_render_warrant_tail, _render_warrant_tail,
) )
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash from arborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash
# ---------------------------------------------------------------- soft-demote registration # ---------------------------------------------------------------- soft-demote registration

View file

@ -15,8 +15,8 @@ from __future__ import annotations
import pytest import pytest
from aborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash from arborist.qa.keys import _VERIFIER_POLICY_FIELDS, verifier_policy_hash
from aborist.qa.quantifier_reminder import broad_quantifier_reminder from arborist.qa.quantifier_reminder import broad_quantifier_reminder
# ---------------------------------------------------------------- gating # ---------------------------------------------------------------- gating

View file

@ -12,12 +12,12 @@ from __future__ import annotations
import json import json
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa import query from arborist.qa import query
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):
@ -339,7 +339,7 @@ def test_classify_source_role_separates_primary_from_noisy():
be secondary; The Lost World should be sequel; off-topic background. be secondary; The Lost World should be sequel; off-topic background.
Catches the case where peripheral pages with strong title overlap Catches the case where peripheral pages with strong title overlap
used to share the primary slot.""" used to share the primary slot."""
from aborist.qa.query import _classify_source_role from arborist.qa.query import _classify_source_role
qstems = {"dinosaur", "jurassic", "park", "film"} qstems = {"dinosaur", "jurassic", "park", "film"}
assert _classify_source_role("Jurassic Park (film)", qstems) == "primary_answer_source" assert _classify_source_role("Jurassic Park (film)", qstems) == "primary_answer_source"
@ -378,7 +378,7 @@ def test_query_persists_run_dag_root_on_record_and_result(tmp_path):
"""Per-run Merkle-DAG fingerprint surfaces on both the result dict """Per-run Merkle-DAG fingerprint surfaces on both the result dict
& the persisted providence_cache row. Recomputing the root from & the persisted providence_cache row. Recomputing the root from
the persisted blob matches what was stored.""" the persisted blob matches what was stored."""
from aborist.qa.dag import verify_run_dag from arborist.qa.dag import verify_run_dag
main_db = tmp_path / "corpus.db" main_db = tmp_path / "corpus.db"
qa_db = tmp_path / "qa.db" qa_db = tmp_path / "qa.db"
@ -561,7 +561,7 @@ def test_query_burn_existing_forces_fresh_inference(tmp_path):
def test_query_burn_existing_writes_audit_event(tmp_path): def test_query_burn_existing_writes_audit_event(tmp_path):
"""Each --burn writes a providence_burn audit event so the chain """Each --burn writes a providence_burn audit event so the chain
records the bust. Verifies one event lands per burn.""" records the bust. Verifies one event lands per burn."""
from aborist.store import connect as _connect from arborist.store import connect as _connect
main_db = tmp_path / "corpus.db" main_db = tmp_path / "corpus.db"
qa_db = tmp_path / "qa.db" qa_db = tmp_path / "qa.db"
@ -947,7 +947,7 @@ def test_question_phrases_returns_sliding_n_grams_no_stopword_strip():
"""`_question_phrases` extracts verbatim n-token windows. Function """`_question_phrases` extracts verbatim n-token windows. Function
words are kept diagnostic value of an allusion is the EXACT words are kept diagnostic value of an allusion is the EXACT
sequence ('always been at war' >> 'always war').""" sequence ('always been at war' >> 'always war')."""
from aborist.qa.query import _question_phrases from arborist.qa.query import _question_phrases
out = _question_phrases("has oceania always been at war with east asia", n=4) out = _question_phrases("has oceania always been at war with east asia", n=4)
# 9-token query, 4-gram window → 6 phrases, all preserved verbatim # 9-token query, 4-gram window → 6 phrases, all preserved verbatim
# (lowercase) and deduped. # (lowercase) and deduped.
@ -961,7 +961,7 @@ def test_question_phrases_returns_sliding_n_grams_no_stopword_strip():
def test_question_phrases_n_5_yields_five_token_phrases(): def test_question_phrases_n_5_yields_five_token_phrases():
"""5-grams trade recall for precision; 'oceania always been at war' """5-grams trade recall for precision; 'oceania always been at war'
is a much stronger Orwell signal than 'always been at war' alone.""" is a much stronger Orwell signal than 'always been at war' alone."""
from aborist.qa.query import _question_phrases from arborist.qa.query import _question_phrases
out = _question_phrases("has oceania always been at war with east asia", n=5) out = _question_phrases("has oceania always been at war with east asia", n=5)
assert "oceania always been at war" in out assert "oceania always been at war" in out
assert "always been at war with" in out assert "always been at war with" in out
@ -972,7 +972,7 @@ def test_question_phrases_n_5_yields_five_token_phrases():
def test_question_phrases_skips_when_question_shorter_than_n(): def test_question_phrases_skips_when_question_shorter_than_n():
"""`who is X?` is too short to yield 4-grams. Empty output is the """`who is X?` is too short to yield 4-grams. Empty output is the
expected behavior (the body BM25 + title routes still cover it).""" expected behavior (the body BM25 + title routes still cover it)."""
from aborist.qa.query import _question_phrases from arborist.qa.query import _question_phrases
assert _question_phrases("who is X?", n=4) == [] assert _question_phrases("who is X?", n=4) == []
assert _question_phrases("", n=4) == [] assert _question_phrases("", n=4) == []
@ -981,7 +981,7 @@ def test_question_phrases_drops_all_short_token_phrases():
"""A window of all 1-3 char tokens is boilerplate ('to be or not') """A window of all 1-3 char tokens is boilerplate ('to be or not')
drops to avoid over-matching. The skip rule fires only when ALL drops to avoid over-matching. The skip rule fires only when ALL
tokens in the window are <4 chars.""" tokens in the window are <4 chars."""
from aborist.qa.query import _question_phrases from arborist.qa.query import _question_phrases
# All ≤3-char tokens — drop. # All ≤3-char tokens — drop.
assert _question_phrases("to be or not", n=4) == [] assert _question_phrases("to be or not", n=4) == []
# Mixed: at least one ≥4-char token → keep. # Mixed: at least one ≥4-char token → keep.
@ -992,7 +992,7 @@ def test_question_phrases_drops_all_short_token_phrases():
def test_question_phrases_lowercases_and_dedupes(): def test_question_phrases_lowercases_and_dedupes():
"""Output is lowercase, deduped on string equality. Same """Output is lowercase, deduped on string equality. Same
sequence in different cases collapses to one phrase.""" sequence in different cases collapses to one phrase."""
from aborist.qa.query import _question_phrases from arborist.qa.query import _question_phrases
out = _question_phrases("Always been at war Always been at war", n=4) out = _question_phrases("Always been at war Always been at war", n=4)
# Repeated sequence appears only once in the output. # Repeated sequence appears only once in the output.
assert out.count("always been at war") == 1 assert out.count("always been at war") == 1
@ -1000,7 +1000,7 @@ def test_question_phrases_lowercases_and_dedupes():
def test_search_phrases_returns_empty_on_no_phrases(tmp_path): def test_search_phrases_returns_empty_on_no_phrases(tmp_path):
"""Defensive: empty phrase list yields no rows, no exceptions.""" """Defensive: empty phrase list yields no rows, no exceptions."""
from aborist.qa.query import _search_phrases from arborist.qa.query import _search_phrases
main_db = tmp_path / "corpus.db" main_db = tmp_path / "corpus.db"
conn = connect(main_db) conn = connect(main_db)
try: try:
@ -1014,7 +1014,7 @@ def test_search_phrases_returns_empty_on_no_phrases(tmp_path):
def test_search_phrases_skips_phrases_with_double_quotes(tmp_path): def test_search_phrases_skips_phrases_with_double_quotes(tmp_path):
"""Adversarial input safety: phrases containing `"` would break """Adversarial input safety: phrases containing `"` would break
the FTS5 quoted-phrase syntax. The function silently drops them.""" the FTS5 quoted-phrase syntax. The function silently drops them."""
from aborist.qa.query import _search_phrases from arborist.qa.query import _search_phrases
main_db = tmp_path / "corpus.db" main_db = tmp_path / "corpus.db"
conn = connect(main_db) conn = connect(main_db)
try: try:
@ -1075,7 +1075,7 @@ def test_phrase_route_skipped_when_question_shorter_than_min_n():
tokens to trigger the n=5/n=6 phrase route. Short conventional tokens to trigger the n=5/n=6 phrase route. Short conventional
queries route through body-BM25 + title-LIKE only phrase routing queries route through body-BM25 + title-LIKE only phrase routing
is structurally biased toward longer allusion-shape questions.""" is structurally biased toward longer allusion-shape questions."""
from aborist.qa.query import _question_phrases from arborist.qa.query import _question_phrases
# 4 tokens after extraction → empty 5-gram and 6-gram outputs. # 4 tokens after extraction → empty 5-gram and 6-gram outputs.
assert _question_phrases("oceania east asia geography", n=5) == [] assert _question_phrases("oceania east asia geography", n=5) == []
assert _question_phrases("oceania east asia geography", n=6) == [] assert _question_phrases("oceania east asia geography", n=6) == []
@ -1136,7 +1136,7 @@ def test_filter_keeps_phrase_match_root_with_no_title_overlap():
"""Direct unit test for accept-path 4: a hit whose title shares """Direct unit test for accept-path 4: a hit whose title shares
zero content tokens with the question, but whose document_root is zero content tokens with the question, but whose document_root is
in `phrase_match_roots`, must pass the filter.""" in `phrase_match_roots`, must pass the filter."""
from aborist.qa.query import _Hit, _filter_by_title_relevance from arborist.qa.query import _Hit, _filter_by_title_relevance
hits = [ hits = [
# Title shares NO content tokens with the question. Without # Title shares NO content tokens with the question. Without
# accept-path 4 (phrase_match_roots), it would be dropped. # accept-path 4 (phrase_match_roots), it would be dropped.
@ -1211,7 +1211,7 @@ def test_unit_search_titles_handles_long_question_without_crash(tmp_path):
OR-chain + ORDER BY LENGTH(title) handles arbitrary token OR-chain + ORDER BY LENGTH(title) handles arbitrary token
counts. Cap at MAX_TITLE_LIKE_TOKENS bounds the OR-chain counts. Cap at MAX_TITLE_LIKE_TOKENS bounds the OR-chain
growth defensively.""" growth defensively."""
from aborist.qa.query import _search_titles from arborist.qa.query import _search_titles
main_db, _qa_db = _build_titled_corpus(tmp_path) main_db, _qa_db = _build_titled_corpus(tmp_path)
conn = connect(main_db) conn = connect(main_db)
@ -1239,7 +1239,7 @@ def test_unit_search_titles_handles_long_question_without_crash(tmp_path):
def test_unit_search_titles_handles_zero_tokens(): def test_unit_search_titles_handles_zero_tokens():
"""Defensive: zero tokens → empty list, no SQL executed.""" """Defensive: zero tokens → empty list, no SQL executed."""
from aborist.qa.query import _search_titles from arborist.qa.query import _search_titles
rows = _search_titles(None, [], 32) rows = _search_titles(None, [], 32)
assert rows == [] assert rows == []
@ -1251,7 +1251,7 @@ def test_unit_search_titles_caps_or_chain_at_max_tokens(tmp_path):
expression-tree growth. Beyond the cap, additional tokens expression-tree growth. Beyond the cap, additional tokens
are dropped at the SQL layer; the post-filter in the caller are dropped at the SQL layer; the post-filter in the caller
sees them all and does the actual filtering.""" sees them all and does the actual filtering."""
from aborist.qa.query import _search_titles from arborist.qa.query import _search_titles
main_db, _qa_db = _build_titled_corpus(tmp_path) main_db, _qa_db = _build_titled_corpus(tmp_path)
conn = connect(main_db) conn = connect(main_db)
@ -1334,7 +1334,7 @@ def test_functional_long_question_returns_sources(tmp_path):
def test_unit_hyphen_fold_variants_emits_joined_form(): def test_unit_hyphen_fold_variants_emits_joined_form():
"""`bi-polar` → {"bipolar"}; multiple hyphen runs emit one """`bi-polar` → {"bipolar"}; multiple hyphen runs emit one
joined variant each; non-hyphenated input emits nothing.""" joined variant each; non-hyphenated input emits nothing."""
from aborist.qa.query import _hyphen_fold_variants from arborist.qa.query import _hyphen_fold_variants
assert _hyphen_fold_variants("bi-polar is rare?") == {"bipolar"} assert _hyphen_fold_variants("bi-polar is rare?") == {"bipolar"}
assert _hyphen_fold_variants("high-school co-op") == { assert _hyphen_fold_variants("high-school co-op") == {
@ -1349,7 +1349,7 @@ def test_unit_hyphen_fold_variants_emits_joined_form():
def test_unit_title_query_tokens_includes_hyphen_fold_additively(): def test_unit_title_query_tokens_includes_hyphen_fold_additively():
"""Hyphen-fold is additive — `bi-polar is rare?` produces both """Hyphen-fold is additive — `bi-polar is rare?` produces both
the split forms (bi, polar) AND the joined form (bipolar).""" the split forms (bi, polar) AND the joined form (bipolar)."""
from aborist.qa.query import _title_query_tokens from arborist.qa.query import _title_query_tokens
toks = _title_query_tokens("bi-polar is rare?") toks = _title_query_tokens("bi-polar is rare?")
assert "bi" in toks assert "bi" in toks
@ -1363,7 +1363,7 @@ def test_unit_title_query_tokens_includes_hyphen_fold_additively():
def test_unit_title_query_tokens_no_hyphen_unchanged(): def test_unit_title_query_tokens_no_hyphen_unchanged():
"""Non-hyphen input behaves exactly as before — pin that the """Non-hyphen input behaves exactly as before — pin that the
fold doesn't add spurious tokens for plain queries.""" fold doesn't add spurious tokens for plain queries."""
from aborist.qa.query import _title_query_tokens from arborist.qa.query import _title_query_tokens
assert _title_query_tokens("anarchism political philosophy") == { assert _title_query_tokens("anarchism political philosophy") == {
"anarchism", "anarchism",

View file

@ -1,4 +1,4 @@
"""`aborist reclassify` — re-runs the verifier against live providence records. """`arborist reclassify` — re-runs the verifier against live providence records.
Covers: Covers:
- Stale UNGROUNDED records become STRICT/HYBRID/etc when the new verifier - Stale UNGROUNDED records become STRICT/HYBRID/etc when the new verifier
@ -16,13 +16,13 @@ import argparse
import json import json
from typing import Iterator from typing import Iterator
from aborist.cli import _cmd_reclassify from arborist.cli import _cmd_reclassify
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa import query from arborist.qa import query
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class _FakeSource(Source): class _FakeSource(Source):

View file

@ -19,13 +19,13 @@ from __future__ import annotations
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa import query from arborist.qa import query
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.qa.repair import mechanical_repair from arborist.qa.repair import mechanical_repair
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
# ---------------------------------------------------------------- mechanical_repair # ---------------------------------------------------------------- mechanical_repair
@ -200,7 +200,7 @@ def test_query_reprompt_rewrites_on_paraphrase_failure(tmp_path):
client = _SeqClient() client = _SeqClient()
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
policy = dict(DEFAULT_QUERY_POLICY) policy = dict(DEFAULT_QUERY_POLICY)
policy["repair_enabled"] = True policy["repair_enabled"] = True
policy["repair_max_reprompts"] = 1 policy["repair_max_reprompts"] = 1
@ -245,7 +245,7 @@ def test_query_repair_enabled_promotes_synthetic_elision_to_strict(tmp_path):
) )
# Build a policy variant with repair_enabled=True. # Build a policy variant with repair_enabled=True.
from aborist.qa.query import DEFAULT_QUERY_POLICY from arborist.qa.query import DEFAULT_QUERY_POLICY
policy = dict(DEFAULT_QUERY_POLICY) policy = dict(DEFAULT_QUERY_POLICY)
policy["repair_enabled"] = True policy["repair_enabled"] = True

View file

@ -4,10 +4,10 @@ from __future__ import annotations
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.source import Source from arborist.source import Source
from aborist.store import connect, get_meta, set_meta from arborist.store import connect, get_meta, set_meta
class IndexedSource(Source): class IndexedSource(Source):
@ -54,7 +54,7 @@ def test_resume_skips_processed(tmp_path):
conn = connect(db) conn = connect(db)
try: try:
# Pre-seed a high-water as if a prior run got through doc 4. # Pre-seed a high-water as if a prior run got through doc 4.
with __import__("aborist").store.transaction(conn): with __import__("arborist").store.transaction(conn):
set_meta(conn, "source_high_water:indexed_test", "4") set_meta(conn, "source_high_water:indexed_test", "4")
src = IndexedSource(n_docs=10) src = IndexedSource(n_docs=10)
result = ingest_source(conn, src, batch_size=5, resume=True) result = ingest_source(conn, src, batch_size=5, resume=True)

View file

@ -10,9 +10,9 @@ import time
import pytest import pytest
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.snapshot import ( from arborist.snapshot import (
EMPTY_SNAPSHOT_ROOT, EMPTY_SNAPSHOT_ROOT,
compute_snapshot_root, compute_snapshot_root,
create_snapshot, create_snapshot,
@ -20,8 +20,8 @@ from aborist.snapshot import (
list_snapshots, list_snapshots,
verify_snapshot, verify_snapshot,
) )
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class _FakeSource(Source): class _FakeSource(Source):

View file

@ -15,7 +15,7 @@ from __future__ import annotations
import pytest import pytest
from aborist.qa.soft_preflight import ( from arborist.qa.soft_preflight import (
SOFT_PREFLIGHT_VERSION, SOFT_PREFLIGHT_VERSION,
SoftPreflightHint, SoftPreflightHint,
_normalize_label, _normalize_label,

View file

@ -5,13 +5,13 @@ from __future__ import annotations
import json import json
from typing import Iterator from typing import Iterator
from aborist.distill import TfidfKeywordDistiller from arborist.distill import TfidfKeywordDistiller
from aborist.distill.runner import distill_existing from arborist.distill.runner import distill_existing
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.merkle import proof_from_dict, verify_proof from arborist.merkle import proof_from_dict, verify_proof
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):

View file

@ -14,9 +14,9 @@ from pathlib import Path
import pytest import pytest
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.sources.vcs import GitRepoSource, MercurialRepoSource from arborist.sources.vcs import GitRepoSource, MercurialRepoSource
from aborist.store import connect from arborist.store import connect
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -120,7 +120,7 @@ def test_git_source_yields_text_files_and_skips_binary(tmp_path):
def test_git_source_round_trip_and_supersedes_chain(tmp_path): def test_git_source_round_trip_and_supersedes_chain(tmp_path):
repo = tmp_path / "demo-repo" repo = tmp_path / "demo-repo"
_mk_git_repo(repo) _mk_git_repo(repo)
db = tmp_path / "aborist.db" db = tmp_path / "arborist.db"
# First ingest: 2 docs (README.md, src.py). # First ingest: 2 docs (README.md, src.py).
conn = connect(db) conn = connect(db)

View file

@ -12,7 +12,7 @@ Tests cover the full trichotomy under each path plus regressions:
from __future__ import annotations from __future__ import annotations
from aborist.qa.verify import ( from arborist.qa.verify import (
extract_claim_spans, extract_claim_spans,
extract_proper_nouns, extract_proper_nouns,
extract_quotes, extract_quotes,
@ -360,7 +360,7 @@ def test_entity_path_hybrid_when_some_proper_nouns_unverified():
def test_wikitext_context_strips_markup_before_substring_test(): def test_wikitext_context_strips_markup_before_substring_test():
"""Without the strip the verifier sees [[Cloud Strife]] in source and a """Without the strip the verifier sees [[Cloud Strife]] in source and a
bare 'Cloud Strife' in the answer's quoted span; substring fails. With bare 'Cloud Strife' in the answer's quoted span; substring fails. With
aborist.wikitext.to_base wired into verify_quotes, the context becomes arborist.wikitext.to_base wired into verify_quotes, the context becomes
plain prose and the model's quote verifies. This is the case from the plain prose and the model's quote verifies. This is the case from the
real FF7 query that prompted wiring wikitext-base-v1 into the verifier.""" real FF7 query that prompted wiring wikitext-base-v1 into the verifier."""
raw_wikitext_context = ( raw_wikitext_context = (

View file

@ -12,8 +12,8 @@ import json
import pytest import pytest
from aborist.qa.evidence import EvidenceObject from arborist.qa.evidence import EvidenceObject
from aborist.qa.verify import _lenient_json_parse, verify_claim_lattice_json from arborist.qa.verify import _lenient_json_parse, verify_claim_lattice_json
# ---------------------------------------------------------------- lenient parser # ---------------------------------------------------------------- lenient parser
@ -207,7 +207,7 @@ def test_verify_json_blocks_disallowed_source_role():
def test_claim_title_overlap_passes_when_title_shares_token(): def test_claim_title_overlap_passes_when_title_shares_token():
"""Rule 8 helper: title shares ≥1 stemmed content token with claim.""" """Rule 8 helper: title shares ≥1 stemmed content token with claim."""
from aborist.qa.verify import _claim_title_overlap from arborist.qa.verify import _claim_title_overlap
assert _claim_title_overlap( assert _claim_title_overlap(
"Homer Simpson's boss is Mr. Burns.", "Homer Simpson's boss is Mr. Burns.",
@ -227,7 +227,7 @@ def test_claim_title_overlap_passes_when_title_shares_token():
def test_claim_title_overlap_fails_on_qcd_for_spin_glass(): def test_claim_title_overlap_fails_on_qcd_for_spin_glass():
"""Rule 8 helper: spin-glass case from 2026-05-02. Claim about """Rule 8 helper: spin-glass case from 2026-05-02. Claim about
spin glass cited to Quantum chromodynamics no token overlap.""" spin glass cited to Quantum chromodynamics no token overlap."""
from aborist.qa.verify import _claim_title_overlap from arborist.qa.verify import _claim_title_overlap
assert not _claim_title_overlap( assert not _claim_title_overlap(
"Spin glass modeling involves the use of mathematical tensors.", "Spin glass modeling involves the use of mathematical tensors.",
@ -237,7 +237,7 @@ def test_claim_title_overlap_fails_on_qcd_for_spin_glass():
def test_claim_title_overlap_vacuous_pass_on_empty(): def test_claim_title_overlap_vacuous_pass_on_empty():
"""Defensive: empty title or empty claim → vacuous pass.""" """Defensive: empty title or empty claim → vacuous pass."""
from aborist.qa.verify import _claim_title_overlap from arborist.qa.verify import _claim_title_overlap
assert _claim_title_overlap("Some claim here.", None) assert _claim_title_overlap("Some claim here.", None)
assert _claim_title_overlap("Some claim here.", "") assert _claim_title_overlap("Some claim here.", "")
@ -347,13 +347,13 @@ def test_runner_ask_json_mode_passes_guided_json_extra_body(tmp_path):
through the chat client. Stub captures the kwargs so we can assert.""" through the chat client. Stub captures the kwargs so we can assert."""
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.qa import ask from arborist.qa import ask
from aborist.qa.client import StubClient from arborist.qa.client import StubClient
from aborist.qa.runner import DEFAULT_POLICY from arborist.qa.runner import DEFAULT_POLICY
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class _FakeSource(Source): class _FakeSource(Source):
source_type = "test" source_type = "test"

View file

@ -5,10 +5,10 @@ from __future__ import annotations
import time import time
from typing import Iterator from typing import Iterator
from aborist.document import Document from arborist.document import Document
from aborist.ingest import ingest_source from arborist.ingest import ingest_source
from aborist.source import Source from arborist.source import Source
from aborist.store import connect from arborist.store import connect
class FakeSource(Source): class FakeSource(Source):

View file

@ -16,7 +16,7 @@ Covers:
from __future__ import annotations from __future__ import annotations
from aborist.qa.warrant import ( from arborist.qa.warrant import (
_question_is_count_shape, _question_is_count_shape,
_question_is_entity_list_shape, _question_is_entity_list_shape,
_question_is_why_shape, _question_is_why_shape,

View file

@ -7,7 +7,7 @@ from pathlib import Path
import pytest import pytest
from aborist.sources import WikipediaOldDump, WikipediaSqlDump from arborist.sources import WikipediaOldDump, WikipediaSqlDump
# Minimal fabricated 'old' table dump matching the 2003-05-16 schema. # Minimal fabricated 'old' table dump matching the 2003-05-16 schema.

Some files were not shown because too many files have changed in this diff Show more