Commit graph

384 commits

Author SHA1 Message Date
780f5afd69
bench: --resume for stop/start-able sweeps
Adds --resume <jsonl-path>: read the existing JSONL, build a set
of (question, mode, sample_idx) tasks already done, skip those
in the shuffled task list, and append fresh rows to the same
file. The markdown rollup uses the union of pre-existing + new
rows. Same --seed required for the order to align across the
resumed run.

Why: a 60-90 min full bench is annoying to re-run from zero when
something interrupts (network blip, kill, kernel panic). The
JSONL has been the durable artifact for ages; the bench just
didn't know how to read it.

Implementation:
  - args.resume: Path | None
  - if given: parse JSONL line-by-line, populate done_tasks set,
    reuse the resume_path's stem as the run stamp, set jsonl_path
    + md_path to the resumed paths
  - file_mode = 'a' if resuming else 'w'
  - tasks filter: drop entries already in done_tasks
  - rows initialized with existing_rows so the markdown summary
    sees the union
  - done counter starts at len(existing_rows) so the print
    countdown reflects total progress

Empirical context: the c=6 bench killed at 142/639 left a
recoverable JSONL. Future kills can resume via:

    .venv/bin/python bench/qa_sweep.py \
        --resume bench/qa_results/<stamp>.jsonl \
        --seed 0 --concurrency 4

(Same seed reproduces the shuffle so the remaining tasks come
out in the original order.)

Concurrency sweep on the smoke (15 tasks at each c):

  c=3  102s  (peak throughput)
  c=4  106s  (4% slower; chosen for full bench — more
              forgiving on single-call hiccups)
  c=5  119s  (12% slower than c=3)
  c=6  185s  (45% slower; vLLM batching ceiling)

Lesson: more concurrent ≠ faster. vLLM continuous batching has
a sweet spot around c=3-4 for this endpoint.
2026-05-02 11:07:07 -04:00
c5b0244d65
ci: disable pipeline (bench-qa runner contention)
Adds `workflow:rules: - when: never` at the top of .gitlab-ci.yml
so GitLab refuses to create any pipeline on push. The job
definitions stay in place — re-enable is a 4-line revert (delete
the workflow block) when bench infrastructure & CI runners are
decoupled.

Reason: aborist CI runners overlap with the same in-house pool
fox uses for `make bench-qa` against the live Hermes endpoint.
Auto-triggered pipelines on every push contend for the runner
& risk skewing bench latencies (already saw concurrency=4 push
mean per-call latency from 6-8s to 29s — adding background CI
jobs makes the signal noisier).

Local `make test` is unaffected — still 11s with -n auto.
2026-05-02 10:40:22 -04:00
8d6fd1b836
bench: sample-level shuffled scheduling with per-cell locks
Previously the bench scheduled at the (question, mode) cell level —
each cell ran its n samples sequentially before the next cell
started. With n=3 + concurrency=6, that meant 6 workers each
chained 3 sequential calls, vLLM's continuous batcher saw
correlated requests, and the n=3 variance signal was confounded
with 'what was vLLM doing in this same-cell batch.'

Two coupled defects, one fix:

(1) Statistical: samples-of-the-same-cell are NOT i.i.d. when run
    back-to-back. Adjacent samples share vLLM batch composition,
    KV-cache locality, and queue load. The 'variance' you see
    across n=3 is partly that batch's particular weather, not true
    model nondeterminism.

(2) Throughput: vLLM batches diverse requests well; correlated
    requests fill the batch with similar work and starve.
    Cell-grouped scheduling correlated by cache_key.

Refactor: build sample-level tasks ((q, mode, sample_idx)),
shuffle with deterministic seed (default 0), submit all to
ThreadPoolExecutor. Per-cell Lock dict (defaultdict(Lock))
serializes burn+insert against the shared cache_key — two samples
of the same cell that happen to land in adjacent worker slots
will queue on the cell's lock instead of racing. Lock contention
is rare under shuffle (samples of one cell are spread across
time) so throughput cost is near-zero.

New CLI flag --seed N for reproducible task ordering.

Changes:
  - tasks list at sample granularity
  - rng = random.Random(seed); rng.shuffle(tasks)
  - cell_locks = defaultdict(Lock); held over _run_one + sample_idx assignment
  - print line shows '#i/n' so order-of-completion is visible

Smoke at --n 1 --concurrency 6: clean, no errors, ~3 min for 15
tasks (essentially same wall-clock as cell-level scheduling at
n=1, as expected — the real win comes at n=3 where sample-level
concurrency unlocks parallelism within cells).
2026-05-02 10:16:33 -04:00
3ac13975a4
ci: GitLab pipeline with pytest-xdist parallelism cap
`.gitlab-ci.yml` runs the unit test suite (`make test`, which uses
`-n auto`) on every push. PYTEST_XDIST_AUTO_NUM_WORKERS=4 caps the
worker count so we don't oversubscribe on shared runners — pytest-
xdist's `-n auto` reads os.cpu_count() and on cgroup-limited
containers that over-reports the host's cores. Local `make test`
stays uncapped (uses full machine).

Wall-clock on the 641-test suite:
  serial:           38s
  -n auto local:    11s   (full machine, 8+ cores typically)
  -n auto + cap 4:  22s   (CI runners — 1.7× speedup, still safe)

Job structure mirrors sibling repos (unsandbox.com,
unfirehose-nextjs-logger):
  - tags: build  → in-house runner pool
  - cache keyed on pyproject.toml, paths .venv/ + .pip-cache/
  - artifacts on failure → .pytest_cache/ for triage

Excluded from CI (need live state or network access):
  - make test-live      → live Hermes endpoint + populated shards
  - make bench-qa{,-quick,-smoke} → live LLM bench
  - chain-check-shards / analyze-shards → real shard data

Optional `test-crawler` job: gated on commit-message tag
`[ci-crawler]` or manual trigger; installs `[crawler]` extras and
runs the network-bound crawler suite. allow_failure: true so it
doesn't block merges.
2026-05-02 10:16:17 -04:00
9860423dca
make: speed up tests + automate concept backfill (test 38s→11s)
Three dev-loop speedups:

(1) `make test` already on -n auto via pytest-xdist (was implicit
    serial); 38s → 11s wall-clock = 3.4× faster on the 641-test
    suite. Big inner-loop win.

(2) `make test-live` now also uses -n auto (live tests are
    independent against the Hermes endpoint; concurrency=4 doesn't
    overload it on the 17-test fixture set).

(3) `make backfill-concepts` (new) replaces the ad-hoc
    `python -c "from aborist.concepts.extract import …"` invocations
    fox was running by hand for the post-2026-05-02 concept-layer
    backfills. Parallelizes per-shard work via multiprocessing.Pool
    with CONCEPTS_WORKERS=4 (env-tunable).

    Driven by scripts/backfill_concepts.py — runs every registered
    extractor in EXTRACTORS (link_reciprocity, token_idf,
    documents_fts) across every numeric-stem shard. Skips qa.db /
    snapshots.db / crawl_*.db by default; --include-non-numeric
    opts in. Wall-clock 189s for 4 wiki shards × 3 extractors vs.
    ~260s serial estimate; modest 1.4× speedup because SQLite WAL
    + FTS5 vocab queries are I/O-bound on a single SSD (4 workers
    contend), but the unified UX & structured progress output are
    the real wins.

(4) `make bench-qa-quick` (new) — 5-question smoke fixture × all
    3 modes × 1 sample × concurrency 4. ~10s wall-clock. Sits
    between bench-qa-smoke (n=1, ~30s) and full bench-qa
    (~70min). Use as the inner-loop pre-commit signal.

Also: docs/concept-relations-design.md updated to point at the
new make target instead of the inline `python -c` block.

No behavior change in the test suite or LLM pipeline; pure tooling.
2026-05-02 10:11:16 -04:00
c49e1beb1f
qa(client): persistent httpx.Client; HTTP/1.1 keep-alive across calls
OpenAICompatibleClient.chat_completion previously constructed a
fresh httpx.Client per request inside a 'with' block:

    with httpx.Client(timeout=self.timeout) as client:
        resp = client.post(url, headers=headers, json=payload)

Each call paid a TLS handshake (~100-300ms) — wall-clock cost
that bench --concurrency surfaces sharply. With 426 calls per
bench sweep, the throwaway-client pattern was burning roughly
1-2 minutes of pure connection setup per sweep.

Move the httpx.Client to __init__ so it lives across calls.
HTTP/1.1 keep-alive holds the TCP+TLS connection open between
chat_completion invocations from the same client instance;
httpx's internal connection pool is thread-safe so the bench's
ThreadPoolExecutor can share one client across worker threads.

Add close() / __enter__ / __exit__ for clean shutdown — the
client now has connection pool state worth releasing
explicitly. Existing callers that don't use the context-manager
form work unchanged because httpx.Client cleanup runs on GC.

Smoke at --concurrency 6 against hermes.ai.unturf.com: 15 cells
in 2m24s, no errors. vLLM continuous-batching observed: first 6
cells completed together at ~76s, suggesting the batch filled
and processed as a unit. Subsequent waves at 14-50s as the queue
drained.

Predicted bench savings: 1-2 min off the ~51 min full sweep.
Combined with --concurrency 6, predicted total: ~30 min full
bench (vs 51 min at concurrency=4).
2026-05-02 10:03:17 -04:00
4b356030fb
qa(prompts): collapse lattice prompts to single source of truth
The four claim-lattice prompt strings —

  CLAIM_LATTICE_SYSTEM_PROMPT
  CLAIM_LATTICE_GROUNDING_REMINDER
  CLAIM_LATTICE_JSON_SYSTEM_PROMPT
  CLAIM_LATTICE_JSON_GROUNDING_REMINDER

— were duplicated byte-for-byte between aborist/qa/runner.py
DEFAULT_POLICY (single-document path) and aborist/qa/query.py
DEFAULT_QUERY_POLICY (multi-source path). Sprint 2 surfaced the
DRY violation when its agent had to update both files in lockstep
to add Rule 9. Two sources of truth = one source of drift.

Lift the four strings to aborist/qa/prompts.py as module-level
constants. Both DEFAULT_* dicts import them and reference by
name. The surrounding policy entries legitimately differ between
the two paths (different system_prompt/grounding_reminder for
single-doc vs multi-source framing, different max_tokens,
different chunk caps) — only the lattice prompts collapse.

Net shrinkage: -240 duplicated lines across runner.py + query.py,
+153 in prompts.py = ~87 lines removed. Sole authoritative copy
lives in prompts.py.

Verified via runtime equality check: both DEFAULT_POLICY and
DEFAULT_QUERY_POLICY resolve to the same string objects post-
import, matching the prompts.py constants byte-for-byte. Test
suite (751/34) passes clean in 11s with pytest -n auto.

No behavioural change. governance_policy_hash unchanged because
the strings themselves are unchanged. Cache namespace
unaffected.
2026-05-02 09:58:45 -04:00
e7650b8eee
qa(prompt): pointer-mode chunk-specificity rule (Sprint 2)
Sprint 2 — add Rule 9 to the claim_lattice_system_prompt to
discourage lazy-anchoring. Targets the structural pattern surfaced
by the 2026-05-02 bench: 49/142 pointer-mode rows (35%) had
lazy_anchor_ratio >= 0.75, and 19/71 questions show pointer fully
HHHHH while JSON aces SSSSS — pointer cites a topic-overview chunk
where JSON's structured per-claim linkage forces specificity.

The new rule (positive form per docs/bench-maxing.md):

  9. Pick the pointer whose own text contains the claim's
     specific facts — its dates, names, places, numbers, and
     verbs. A chunk whose title matches the question's topic is
     the right anchor only when that chunk's text also states
     the claim's facts; when a different chunk states the fact
     more directly, cite that one instead.

Six lines, no negation, named failure mode. Aligns with Rule 8
(title-relevance) by definition: a chunk whose body states the
fact almost always shares stems with its title.

The pointer prompt is duplicated between aborist/qa/runner.py
DEFAULT_POLICY and aborist/qa/query.py DEFAULT_QUERY_POLICY.
Both updated in sync. (DRY violation worth cleaning up later —
the duplication can drift; one source of truth would be safer.)

Smoke (5-question fixture, n=1) — no regression, n=1 too noisy
to detect signal:

  quote   5/5 → 4/5..5/5  (oscillation, single dinosaurs flip)
  pointer 1/5 → 0/5..1/5  (sampling noise around the floor)
  JSON    5/5 → 5/5       (held)

Real signal lives in the full 71-question n=3 bench (~50 min).
The change is principled, no calibration cost, and folds into
governance_policy_hash so prior cached records auto-partition.
2026-05-02 09:48:19 -04:00
e582b2e93a
qa: per-mode max_context_chars defaults from 2026-05-02 bench
Sprint 1b — replace the flat max_context_chars=60000 default with
per-mode budgets matching the peak bucket each mode achieved on
the 2026-05-02 bench:

  quote                  → 24000 (peak 16-32KB)
  claim_lattice_pointer  → 24000 (peak 16-32KB)
  claim_lattice (JSON)   → 48000 (peak 32-64KB)

The flat 60K was past peak for quote/pointer (which degrade after
32 KB) and only marginally above peak for JSON. The bench's
recommended-context-budget table now flows back into the policy
defaults — operator-driven landing per the five-step algorithm
step 5: surface, don't auto-apply. The mapping lives in
DEFAULT_QUERY_POLICY so any tuning bumps governance_policy_hash
and partitions the cache namespace cleanly.

API boundary stays backward-compatible: an explicit
max_context_chars= caller value still wins. The per-mode default
fires only when the caller passes None (or omits the arg).

CLI: --max-context-chars default flips to None; help text spells
out the per-mode fallback.

Smoke (5-question fixture) confirmed no regressions vs the
pre-change baseline:
  quote   2/5 → 5/5  (+3)
  pointer 0/5 → 1/5  (+1)
  JSON    5/5 → 5/5  (flat — already at ceiling on this fixture)

Quote's +3 is the unexpected win: the smaller 24K budget surfaces
tighter-relevance top-K instead of drowning the model in filler.
The full bench (~50 min) is the real scoreboard but smoke shape
matches the bench's predicted lift direction.
2026-05-02 09:39:40 -04:00
3b9122395c
speed: pytest-xdist, bench smoke, concurrency default; UTF surrogate fix
Bench-max sprint 1a + sprint 3 + speed audit. Five wins, none of
them traded calibration.

UTF-16 surrogate fix (sprint 1a)
================================
Hermes occasionally emits text with lone UTF-16 surrogates. Bare
.encode('utf-8') raises UnicodeEncodeError on those, which aborted
the run with no Merkle root. Two errors per lattice mode in the
2026-05-02 bench were this exact path on the 'tell me about the
roman empire' question.

Fix: errors='surrogatepass' on the four sha256 helpers that hash
model-derived text, plus the two audit-chain encode sites in
store.py for defense-in-depth (audit body could carry user text in
some flows). The hash stays deterministic because WTF-8 bytes are
reversible & unique per input.

Touched:
  aborist/qa/dag.py:_sha256_hex            (the loud one)
  aborist/qa/keys.py:_sha256
  aborist/qa/evidence.py:_sha256_hex
  aborist/store.py: chain_audit_events + append_audit

Predicted Δ on next bench: +1pp on lattice modes (the 2 errors
become valid runs).

Smoke fixture (sprint 3)
========================
bench/qa_questions_smoke.txt — 5 questions, all anchor classes,
each currently failing pointer mode 100% while JSON aces 100% per
the 2026-05-02 bench. Wired as 'make bench-qa-smoke', --n 1
--concurrency 4, ~30-90s wall-clock depending on vLLM warmth. The
inner loop for prompt iteration; the full 71-question sweep stays
the scoreboard.

Smoke verified: pointer=0/5 STRICT, JSON=5/5, quote=2/5. Confirms
the gap pattern from the journal.

Concurrency default
===================
Makefile bench-qa now defaults to BENCH_QA_CONCURRENCY=4 (was
sequential). Override via BENCH_QA_CONCURRENCY=N. Combined with
the --concurrency landing in 0870af6, full sweep drops from ~107
min projected to ~51 min actual.

pytest-xdist (test-speed)
=========================
Added pytest-xdist>=3.5 to dev extras. 'make test' now uses
-n auto (= one worker per logical CPU). Measured: 36s → 10s on
the 641-test suite. 3.6× speedup, no test changes required.

Bench-max scoreboard (predicted lift from this commit alone):
  +1pp lattice modes (UTF fix)
  +cycle-time enabler (smoke fixture, xdist)
  no calibration cost — none of the verifier checks moved.
2026-05-02 09:29:40 -04:00
5bc42d277e
bench: 2026-05-02 journal + CLAUDE.md headline refresh
Captures the post-Rule-8 / post-warrant-generalization /
post-frame-detector bench taken with --concurrency 4 (~51 min
wall-clock on 426 runs).

Headlines (vs 2026-04-30 post-retry rerun):

  quote                 0.47 → 0.50  (+3pp)
  claim_lattice_pointer 0.24 → 0.23  (−1pp)
  claim_lattice (JSON)  0.50 → 0.44  (−6pp)

Pointer mode is essentially flat. The earlier 'pointer regressed
13pp' alarm was wrong — that compared to the pre-retry
2026-04-30 numbers (0.36) instead of the post-retry rerun (0.24)
which is the correct reference.

Real news: JSON's 6pp drop is the honesty cost of Rule 8
title-relevance promotion + warrant-class generalization. Lattice
modes now hit 99% directive coverage (D2/D3/D4/D6/D7); the 1% gap
is 2 UTF-16 surrogate errors per mode on one question.

Pointer-mode failure pattern shifted to lazy-anchor: 49/142 rows
have lazy_anchor_ratio >= 0.75. JSON's structured-per-claim
linkage absorbs Rule 8's pressure better than pointer's
prose-with-tags. 19/71 questions show JSON > pointer by 50pp+.

Recommended context budget surfaced for the first time:
- claim_lattice (JSON) peaks at 32-64KB (0.51)
- pointer + quote peak at 16-32KB

CLAUDE.md headline numbers refreshed; docs index points to the
new journal alongside the prior 2026-04-30 reference.
2026-05-02 08:50:45 -04:00
0870af60af
compress: thread-local zstd contexts; bench: --concurrency flag
zstandard.ZstdCompressor and ZstdDecompressor instances each carry an
internal libzstd context that is NOT thread-safe. Calling .compress()
or .decompress() on a single shared instance from multiple threads
corrupts the context and raises:

    ZstdError: decompression error: Data corruption detected

Surfaced when bench/qa_sweep.py learned a --concurrency flag and ran
4 (question, mode) cells in parallel. ~19% of retrievals failed on
zstd corruption before the fix. The prior comment claiming the
singletons were 'stateless across calls — safe to share across
threads' was wrong.

Replace the module-level singletons with threading.local() caches.
Each thread reuses its own ZstdCompressor / ZstdDecompressor; no
contention across threads. Init cost is negligible vs decompression.

bench/qa_sweep.py:

  --concurrency N (default 1) parallelizes (question, mode) CELLS
  using a ThreadPoolExecutor. Samples within a cell stay sequential
  so burn-then-insert against a single cache_key never races itself.
  Lock-protected JSONL writes & progress prints. Exception in any
  worker surfaces via fut.result().

vLLM handles concurrent requests well; 4-8 is a reasonable starting
point. With --n 2 --concurrency 4, expect ~10 min wall-clock for the
full 426-run sweep against hermes.ai.unturf.com (vs ~2h sequential).
2026-05-02 07:36:33 -04:00
9810fd6218
CLAUDE.md: ladder rendering + closed-tickets sweep
Three small fixes flagged by the whitepaper rewrite sweep:

1. Note the four-rung ladder render in the providence-cache layer
   description (audit_mode is schema-level; lattice modes display
   POINTER-LINKED → ANCHOR-WARRANTED → EVIDENCE-WARRANTED, with
   ENTAILMENT-VERIFIED reserved).

2. Add a Conventions entry for the ladder right after the three
   answer modes — names the rung discrimination rules, what each
   violation demotes to, and that quote/span/entity/paraphrase
   modes keep their original tokens.

3. Replace the 'Open tickets' section with a one-liner pointing to
   docs/TICKETS.md as authoritative; all five shipped tickets are
   closed as of 2026-05-02. The previous listing claimed #000001-3
   were open, which has been false since the prior session.
2026-05-02 07:03:58 -04:00
7887a588f9
qa(retrieval): documents_fts FTS5 index — title search 1250× faster
Phase 2 of the holistic-tuning pair. Replaces the un-indexable
LOWER(title) LIKE '%tok%' title-search with an FTS5 MATCH-based
lookup. The structural fix that was deferred when the >5-token
bypass landed (commit 1d70c4f).

(1) Schema: new `documents_fts` virtual table over the title column.
    Contentless mode (same trick as chunks_fts) — stores only the
    inverted index, not a copy of the title. Joins back to documents
    via rowid for the post-filter caller.

(2) Extractor: backfill_documents_fts (registered as evidence_kind
    "documents_fts"). Reset-and-rebuild the index from documents in
    one INSERT...SELECT. Idempotent. Cost: ~2.5s per 870k-doc shard.

(3) `_search_titles` rewritten: try FTS5 MATCH first, fall back to
    LOWER(title) LIKE only on shards lacking documents_fts data
    (legacy ingest pre-this-commit). MATCH expression OR-joins
    quoted query tokens; falls through to LIKE form on tokenizer
    edge cases.

(4) Removed the `>5 accept_tokens` bypass in `_search_corpus`
    that commit 1d70c4f added as a workaround for the LIKE
    full-scan cost. With FTS5 the title search is sub-second
    regardless of token count, so synonym-expanded title search
    is affordable at any query length.

Live verified on the 19-token brain-tech query with 50 accept_tokens
(post-IDF expansion):
  Pre-FTS5:   ~50s (LIKE '%tok%' × 870k docs × 4 shards)
  Post-FTS5:  ~0.04s   ← 1250× speedup
  End-to-end query: 10.4s (was 11.6-13s; the saved title-search
    time partially absorbed by Hermes inference variance).

Backfill cost: 10.5s wall-clock across 4 wiki shards + 1 crawl
shard. Storage: ~30 MB per wiki shard for documents_fts (well
inside the 90 MB/shard concept-layer budget). Re-running is
idempotent (DELETE FROM ... INSERT INTO ...).

Tests: 641 passed (no regression).
2026-05-02 06:57:10 -04:00
b7f087f26c
qa(concepts): per-token IDF for cap-time ranking — replaces alphabetical truncation
Phase 1 of the holistic-tuning pair fox requested 2026-05-02. Replaces
the dumb alphabetical-truncation heuristic in synonym_expand's cap
path with corpus-derived IDF ranking — rare topical synonyms win the
truncation, common-corpus noise drops.

(1) New per-shard table: concept_token_idf (token, doc_freq, total_docs,
    derived_at). Indexed on doc_freq for ORDER BY ranking. Folds into
    the cross-shard UNION view via _SHARDABLE_TABLES.

(2) Extractor: backfill_token_idf (registered as evidence_kind
    "token_idf"). Reads chunk-frequency from FTS5's fts5vocab virtual
    table, bounded by the union of token + target columns in
    concept_relations (only synonym tokens get an IDF row, not the 6M
    full corpus vocabulary). Idempotent INSERT OR REPLACE.

(3) Query layer: _load_token_idf sums per-token doc_freq across all
    shards (cross-shard ranking). _get_indices return tuple grew from
    (manual, derived, rivalry) → (manual, derived, rivalry, idf). Cache
    LRU keyed on shards_dir mtime so the IDF lookup is per-process-once.

(4) synonym_expand cap-time truncation: when expanded set exceeds
    MAX_TOTAL_TOKENS, sort neighbors by (doc_freq ASC, token ASC) so
    rare tokens come first. Tokens missing from concept_token_idf get
    a sentinel high-rarity score (treats unknown as hapax — fail-safe
    when the IDF backfill hasn't run yet).

Live verified on the brain-tech 19-token query:
  Pre-IDF expansion (alphabetical 50): bumper, buster, channel,
    convention, dave, duck, esp, field, glen, mind, minimum, ...
  Post-IDF expansion (rare-first 50): neuroimaging, neurons,
    neuroscience, neurotechnology, psychokinesis, telepathic,
    telepathy, thinking, tms, transcranial — all the brain-tech
    terms that lost the alphabetical race in pre-IDF now win.

Backfill cost: ~50s wall-clock across 4 wiki shards (~22k tokens
indexed per shard). Fts5vocab over chunks_fts is the right
infrastructure — no full corpus scan needed since we filter by the
concept_relations token set. Storage: ~1 MB per shard for the IDF
table. Stays well inside the ~90 MB/shard concept-layer budget fox
set earlier.

Tests: 641 passed (no regression). Live fixtures (boss baby,
amazon river/rainforest) all pass — ranking doesn't break gating.
2026-05-02 06:53:55 -04:00
8f4cb470aa
README: fix stale /downloads/ whitepaper URL
The link was still pointing at the old pre-Providence slug
(/downloads/merkle-reverse-rag-whitepaper.pdf). Canonical URL is
/merkle-providence-reverse-rag-whitepaper.pdf at the static root,
where `make publish-and-commit` keeps it fresh on every whitepaper
edit. Same fix as the unsandbox.com Phoenix templates (commit
0d1a69c) — last remaining stale reference.
2026-05-02 00:00:07 -04:00
141453b0ee
qa(verifier): promote deflection sidecar to soft-demote violation
Two compounding things in one commit because they're tightly coupled
(the new violation is what makes the new fixtures meaningful):

(1) DEFLECTION_DETECTED — new soft-demote violation kind.

    `aborist.qa.inspect.diagnose_deflection` was a read-only sidecar:
    when the question's subject anchor (last content token after
    stopword strip) is missing from the answer, it returned
    `kind="deflection"`. Useful telemetry but never affected the
    verdict. 2026-05-02 fox surfaced two cases where the missing
    signal mattered:

      Q: "who burns the amazon river?"
      A: "Deforestation in the Amazon Rainforest is primarily caused
          by human activities..." [E10 | Deforestation of the Amazon
          Rainforest]
      Pre: EVIDENCE-WARRANTED 1/1   ← silent river→rainforest topic shift
      Post: ANCHOR-WARRANTED-PARTIAL 1/2 with DEFLECTION_DETECTED

    The verifier now runs diagnose_deflection() after all hard checks.
    On `kind == "deflection"` it appends a DEFLECTION_DETECTED
    violation (rationale + subject_anchor + overlap_ratio) and caps
    audit_mode at HYBRID. Renderer's four-rung ladder picks it up via
    `_SOFT_DEMOTE_VIOLATION_KINDS` (cli.py) and demotes EVIDENCE-
    WARRANTED → ANCHOR-WARRANTED automatically.

    Folds into verifier_policy_hash via new
    `claim_lattice_deflection_check_enabled: True` policy field
    (defaults on). Bumps the 9-dim cache_key when toggled.

    Wired into both verify_claim_lattice (pointer) and
    verify_claim_lattice_json (JSON). Deferred import keeps verify.py
    module-load clean.

(2) Three live fixtures gating the river/rainforest/deflection family:

    - test_amazon_river_burning_who_deflects_or_grounds_river
      Gates the silent topic-shift pattern: if model deflects, the
      verifier MUST catch it (DEFLECTION_DETECTED present OR
      UNGROUNDED OR "river" actually in answer). Failure mode this
      catches: EVIDENCE-WARRANTED with rainforest answer to a river
      question and no soft-demote.

    - test_amazon_river_burning_culture_refused_or_ungrounded
      Adversarial-impossible mirror of Mars-BDFL: rivers don't burn,
      so model must refuse / deflect / refute. Failure mode:
      STRICT/HYBRID with affirmative culture-burns-river claim.

    - test_amazon_rainforest_burning_culture_grounds_or_deflection_caught
      Feasible-but-framing-specific: corpus has deforestation
      content but no "culture" framing. Either grounds with rainforest
      content OR a soft-demote/UNGROUNDED catches the framing gap.

Live verified (2026-05-02): all 3 pass against the real Hermes
endpoint. 641 unit tests still pass (deflection check skipped on
fixtures lacking question or rendered_text).
2026-05-01 23:44:32 -04:00
04069f89f3
tests(qa-live): Boss Baby — out-of-corpus / post-2010 honest UNGROUNDED gate
Captures the canonical "model knows it but corpus doesn't" case fox
surfaced 2026-05-02:

  Q: what is boss baby?
  Expected: UNGROUNDED  (the 2010-11 wiki snapshot has no Boss Baby
  coverage — the 2017 DreamWorks film didn't exist; the 2010 Marla
  Frazee picture book had just been published & no article)
  Model: emits training-prior content about the film/book
  Verifier: 0/N verified — no evidence chunk anchors the claims

This is the failure-by-honest-admission case the verifier must
preserve. Two compounding false-positive risks the test guards:

  1. Verifier accepting the claim against an unrelated chunk that
     happens to share content tokens (Death of Baby P, Cake Boss,
     Beanie Baby — all adjacent-token noise observed at top-K).
     This is the warrant-failure class warrant-lite was built to
     catch — if it ever passes here, anchor-class warrant has a hole.
  2. Verifier accepting via OR-mode FTS5 noise — a token-level match
     of "baby" or "boss" in unrelated content surfacing as positive
     grounding.

Acceptable behaviors:
  A. UNGROUNDED (verifier returns 0/N — corpus-truth over prior)
  B. STRICT/HYBRID without "boss baby" anywhere in the answer
     (deflection — model declined the false-corpus question and
     answered an adjacent grounded fact, like Mars-BDFL → Guido
     pattern).

Failure: STRICT/HYBRID + "boss baby" in answer = false-positive
grounding. Test fails by name pointing at the warrant gap.

Live verified (2026-05-02): UNGROUNDED 0/2, model emitted both the
2017-film claim AND the 2010-book claim, verifier rejected both
honestly. Test passes in 10s.
2026-05-01 23:23:34 -04:00
326badf6d8
docs: README label refresh + per-module reference + Graphviz diagrams
Three things in one commit because they're tightly coupled (README
points at the diagrams; diagrams index in modules/index.md points
back at README; module pages embed the diagrams).

(1) README — label refresh:
    - Quickstart label changed from STRICT/HYBRID/UNGROUNDED to the
      four-rung ladder POINTER-LINKED → ANCHOR-WARRANTED →
      EVIDENCE-WARRANTED → UNGROUNDED with -PARTIAL suffix on HYBRID.
    - Verifier section spells out both layers (schema trichotomy +
      display ladder), the seven hard checks of claim_lattice, and
      the five anchor classes of warrant.
    - Architecture tree updated: concepts/ package added, qa/
      sub-modules expanded (warrant, evidence, parse_claims, dag),
      verify.py described as quote/span/entity/paraphrase + claim_lattice.
    - Concept overlay description updated for corpus-derived layer
      (concept_relations table, link_reciprocity extractor, 1.6%
      tax cite).
    - Test count: 326+ → 641+.

(2) docs/diagrams/ — Graphviz dot sources:
    - aborist-modules.dot — top-level package graph (substrate /
      storage / sources / retrieval / qa / mesh / cli)
    - query-pipeline.dot — question → cache → retrieval → LLM →
      verify → render → cache write, with phase budgets
    - ingest-pipeline.dot — source doc → canonicalize → chunk →
      Merkle → upsert (+ optional distill)
    - verifier-ladder.dot — (audit_mode, violations) → display rung
      decision tree
    Existing mesh-*.dot kept as-is. Makefile `make docs` target
    extended to also emit .svg alongside the existing .png so the
    diagrams render in markdown viewers.

(3) docs/modules/ — per-module reference pages:
    - index.md (links to every diagram + every module page)
    - merkle.md, document.md, store.md, ingest.md, evict.md,
      sources.md, search.md, concepts.md, qa.md, distill.md,
      wikitext.md
    Each page is a one-screenful concise reference: what the
    module is for, public API, key invariants, embedded diagrams
    where useful, link to source. Mesh stays at the existing
    docs/mesh.md + docs/mesh-deploy.md (already comprehensive).

Tests: 641 passed (no code change).
2026-05-01 23:19:01 -04:00
2f7132b9ec
qa(retrieval): synonym-aware FTS5 OR-fallback restores brain-tech surfacing
The previous fix capped OR-mode at top-5 longest tokens which made
the long brain-tech query fast (118s → 13s) but lost the synonym
retrieval that surfaced Telepathy / Neurotechnology — those titles
weren't reachable via the original query tokens alone.

Fix: pass synonym-expanded tokens to the OR-mode fallback as
``extra_or_tokens``. The merged pool keeps the top-5-longest cap so
retrieval cost is unchanged, but long synonym tokens like
"neurotechnology" (15 chars) and "neuroimaging" (12 chars) now beat
shorter query tokens like "thoughts" (8) by length & surface the
right titles.

OR pool example for the brain-tech query:
  Pre-synonym:  reconstruct, technology, understand, available, thoughts
  Post-synonym: consciousness, neuroimaging, clairvoyance, compensation, reconstruct
  → 3/5 brain-tech terms surface naturally in OR-mode.

AND mode stays unchanged (synonyms in AND would relax the strict-
relevance constraint & pull in noise — wrong tradeoff). The
synonym signal flows ONLY into OR-mode-fallback.

Live verified on the 19-token brain-tech query:
  - 13s total (Hermes 6-8s + retrieval ~1s + verify ~2s)
  - 2/2 verified pairs
  - Cited Functional neuroimaging E5 + FreeSurfer E9 (both real
    brain-tech corpus sources, not Universal-pragmatics nonsense)

`_search_corpus` recomputes synonym_expand(qtokens) once into
`or_synonym_pool` (caches in concepts.query._CACHE after first
shard); each shard's FTS5Backend.search() receives the same set
through `extra_or_tokens=`. Cost stays sub-second per shard.

Tests: 641 passed (no regression).
2026-05-01 22:56:50 -04:00
1d70c4fd25
qa(retrieval): fix 50→100s hangs in title-LIKE & FTS5 OR-fallback
Both retrieval phases were doing O(corpus × tokens) full-scan work
on long queries — synonym work surfaced the latent issues but they
predate it. Hermes is 6-8s; retrieval needs to be sub-second.

(1) `_search_titles` (LIKE-based title backup search) is fundamentally
    O(corpus × |accept_tokens|) — `LOWER(title) LIKE '%tok%'` can't
    use any index. 50 patterns × 870k docs × 4 shards = ~56s pure
    full-scan. Skipped for queries with > 5 accept_tokens, where
    FTS5 BM25 already returns better candidates. Title-LIKE remains
    a backup for short focused queries (1-5 tokens).

(2) FTS5 OR-mode fallback was OR-joining ALL post-stopword query
    tokens. A 19-token query OR-clause matched millions of docs;
    BM25 then ranked them all. 13s/shard observed.
    Capped to top-5 LONGEST tokens (proxy for rarity / topical
    specificity). Long words like "neurotechnology" matter; short
    words like "soon" don't. OR-mode now 0.25s/shard.

(3) Stopword set extended with 6 connector words (one, some,
    another, without, soon, currently). Conservative addition —
    these have zero topical signal. Reduces the AND-mode token
    count, raising the chance AND succeeds before falling back to
    OR.

Verified live: the 19-token brain-tech query that hung indefinitely
now returns in 18s (Hermes + verify + render dominate). Source
`_search_corpus` no longer expands synonyms for the title-LIKE call
either — synonym expansion stays in `_filter_by_title_relevance`
(post-retrieval, in-memory, cheap regardless of accept-set size)
where it actually does useful work.

Tests: 641 passed (no regression).
2026-05-01 22:52:02 -04:00
50a1bbdc0e
qa(concepts): direct-neighbor expansion + degree caps + manual/derived split
Two compounding crashes/hangs in the corpus-derived synonym layer
that the bench surfaced 2026-05-02:

(1) Union-find chained reciprocal-link clusters into one giant
    connected component (54,538 tokens for any seed in the 4-shard
    Wikipedia corpus). One query token expanded to the entire
    synonym alphabet, tripping SQLite's expression-tree-depth=1000
    limit on the OR-clause in `_search_titles`. Replaced with
    direct-neighbor adjacency only.

(2) Even direct-neighbor expansion was too noisy on generic tokens:
    a 19-token query expanded to 419 tokens (every token had ~22
    reciprocal-link neighbors averaging out to topic-adjacency
    noise). Title-LIKE on 419 patterns × 3.47M docs × 4 shards
    hung indefinitely.

Fix: split index by evidence_kind, cap derived expansion only.

- ``manual_index`` — manual_legacy + manual rows. Curated; ALWAYS
  expand regardless of per-token degree. The brain-tech / AMD-
  family / Mac / Linux / etc. seed groups have legitimately many
  members per token after seed.py started writing clique edges.
- ``derived_index`` — link_reciprocity & corpus-extracted edges.
  Subject to MAX_NEIGHBORS_PER_TOKEN=8 cap. Generic tokens
  ("person", "thoughts") have huge degree from Wikipedia link
  noise; specific named entities have small focused neighborhoods
  that pass the cap.
- ``MAX_TOTAL_TOKENS=50`` overall cap on expanded set. Bounds
  the SQL clause count so title-LIKE search stays tractable.

seed.py: write CLIQUE edges within each legacy group (every
member-pair, not just anchor→member). Preserves the legacy
frozenset semantic where any member retrieves every other
member. Quadratic in group size but groups stay small (largest
is the 30-member brain-tech → 435 pairs).

Live shard 000 re-seeded: 80 → 747 manual_legacy rows. Idempotent
re-seed via INSERT OR IGNORE — re-running adds nothing new.

Verified:
- 19-token query: 419 → 50 expanded (cap saturated)
- thoughts: 30+ tokens incl. brain-tech members preserved
- athlon: full AMD-family clique (amd, duron, opteron, ryzen, …)

Tests: 14/14 concept tests pass.
2026-05-01 22:34:23 -04:00
9b09eb7b84
qa(concepts): direct-neighbor synonym expansion (replaces union-find closure)
Why: union-find over the full Wikipedia reciprocal-link synonym
graph collapses everything into one giant connected component
(54k+ tokens for any seed in a 4-shard corpus). That made one
query token expand to the entire synonym alphabet, which:

(a) overwhelmed retrieval relevance — every query pulled in
    thousands of off-topic candidates via synonym chaining;
(b) tripped SQLite's expression-tree-depth limit when
    _search_titles built a per-token OR clause (fox-surfaced
    2026-05-02 long-question regression — the qa/query.py side
    of that fix landed in faed58d via a separate OR-chain cap;
    this is the concepts-side fix to the same underlying
    explosion).

Direct-neighbor adjacency: each token maps to its immediate
synonym partners only, no transitive chaining. Symmetric — a
(A,B) edge registers both A→B and B→A. Preserves the legacy
frozenset semantics (every member of a group expanded to the
others in that group, but groups didn't chain).

Test impact: three test_concepts.py tests assert the old
transitive-closure behavior:
  - test_synonym_expand_amd_pulls_athlon_and_back (expects
    athlon → {amd, duron, thunderbird})
  - test_mac_windows_rivalry
  - test_brain_tech_synonym_group_includes_telepathy_and_thoughts

They fail under direct-neighbor expansion. Either update the test
fixtures to seed direct edges (athlon, duron) etc., or update the
test expectations to match the new direct-neighbor contract.
Deferred — design choice, fox to decide which path matches intent.
2026-05-01 22:22:08 -04:00
faed58dd0a
qa(retrieval): fix _search_titles SQLite tree-depth overflow on long queries
Regression fox surfaced 2026-05-02:
  Q: "what technology are currently or soon available which may
      enable one person to reconstruct and understand some or a
      portion of another persons thoughts or ideas without
      speaking or sign language."
  → sqlite3.OperationalError: Expression tree is too large
    (maximum depth 1000)

Root cause: the v1 of _search_titles (commit 0052845) chained N
``CASE WHEN ... THEN 1 ELSE 0 END + ...`` expressions for the
title_score column. Each CASE WHEN is multiple tree nodes; +-
chained N times exceeded SQLite's default 1000-depth bound on
question texts with ~30+ content tokens.

Fix: simplified SQL — OR-chain WHERE + ORDER BY LENGTH(title) ASC
+ LIMIT bumped 4x to compensate for the lost smart sorting. The
caller's post-filter (word-boundary stem-aware token-set
intersect) does the actual title-relevance ranking; SQL just
needs to surface enough candidates for the post-filter to grade.

Also: cap the OR-chain at MAX_TITLE_LIKE_TOKENS=24 so pathological
200-token queries don't cascade SQL expression growth even
defensively. Beyond ~24 tokens the post-filter is doing all the
work; extra LIKEs just inflate candidate sets without signal.

5 new tests in tests/test_query.py covering the regression at
unit / integration / functional layers:

- test_unit_search_titles_handles_long_question_without_crash
  50-token query through _search_titles directly. Pre-fix raised
  sqlite3.OperationalError; post-fix returns row list.
- test_unit_search_titles_handles_zero_tokens
  Defensive: empty token list → empty result, no SQL executed.
- test_unit_search_titles_caps_or_chain_at_max_tokens
  200-token pathological query — bounded by MAX_TITLE_LIKE_TOKENS,
  doesn't crash.
- test_integration_query_completes_on_long_question
  End-to-end query() with StubClient + long question completes
  without the SQLite error. Pre-fix raised before reaching the
  LLM call.
- test_functional_long_question_returns_sources
  The neurotech doc (richest body match) appears in top-K despite
  the long-question retrieval path.

Test_query.py: 35 → 40 passing. Full suite (excluding parallel
test_concepts churn from concepts/query.py rewrite): no other
regressions.
2026-05-01 22:19:20 -04:00
4ba4a815e0
bench: log-scale buckets to 1M; per-mode peak-bucket recommendation
Two paired changes addressing fox's "learn this hyperparameter
from model use, not hard coding" + the 1M-context-window
caveat:

(1) Log-scale prompt-size buckets extend from 8KB through 1M+:

    <8KB / 8-16KB / 16-32KB / 32-64KB / 64-128KB / 128-256KB /
    256-512KB / 512K-1M / >=1M

    The same bench harness now covers 8B-class models (Hermes 82K
    context, max useful prompt ~32-64KB) through 1M-context models
    (Gemini 1.5 Pro, Claude with extended context, Llama 4) without
    code change. A model whose context window stops at 82K simply
    never populates the giant buckets; a 1M-context model fills
    them and finds its own sweet spot.

(2) New "recommended context budget (learned from this bench)"
    section — per-mode peak-strict-rate bucket. Operator-driven
    landing per the five-step algorithm step 5: surfaced, not
    auto-applied. Minimum sample size of 5 runs per bucket so
    statistical noise doesn't masquerade as signal. Tie-break on
    smaller-bucket-wins so equivalent strict-rates favor the
    cheaper choice.

The substrate is now self-tuning at the OBSERVATION layer: bench
records what budget actually grades best per model. Per-model
profile JSON (storing the recommended budget back into
~/.aborist/model_profiles/<model>.json) is the next beat once
this surface is observable in real bench runs.

3 new bench tests cover the recommended-budget section, the
sample-size floor, and the giant-context bucket coverage.
Full suite: 746 passed (was 738, +8).

Connects to D8 (automate after test-pinning): the bench tells us;
we don't guess.
2026-05-01 21:55:06 -04:00
5247d8e282
docs(concepts): design reference + 1.6% storage-tax rationale
New `docs/concept-relations-design.md`: architecture reference for
the per-shard concept_relations layer that replaced the legacy
frozenset module (commit 5fd458a). Covers:

- Why phase 1 (hand-curated frozensets) didn't scale.
- Append-only schema + the three by-construction properties (idempotent
  re-derivation via UNIQUE, per-shard storage, Merkle-orthogonal).
- Built-in `link_reciprocity_synonym` extractor reading the existing
  `edges` table — no new crawler, works for Wikipedia AND HTML sites.
- Measured storage: 95.58 MB across 4 wiki shards (3.47M docs,
  10.75M resolved edges, 55,148 reciprocal pairs, 289,848 synonyms),
  4m16s wall-clock backfill. 1.6% tax on the 6 GB corpus.
- Three storage compactions considered & rejected, each with the
  specific trade-off it loses on (drop idx_concept_evid → painful
  purge debugging; BLOB source_root → schema inconsistency; FK
  normalization → JOIN in retrieval hot path).
- How-to: backfill, manual add, purge.
- Adding new extractors.
- Deferred follow-ons (CLI commands, Wikipedia See-also extractor,
  category extractor, hatnote extractor).

CLAUDE.md item 5 in the retrieval-pipeline list updated to point at
the new module path (aborist/concepts/) and the design doc.

TICKETS.md reference list updated to mention the new design doc.
2026-05-01 21:39:09 -04:00
5fd458aa41
qa(concepts): corpus-derived concept_relations table replaces frozensets
Phase 2 of the concepts/ layer. The Apr 27 commit (c6182ae) shipped
hand-curated frozensets in aborist/qa/concepts.py with a TODO to
"derive from Wikipedia's category graph or 'See also' sections" —
that's this commit. Fox's 2026-05-01 critique landed it: a 7-entry
list of arbitrary frozensets won't scale to a 3.47M-doc corpus, &
adding domains shouldn't require a Python edit + commit + redeploy.

Architecture:

(1) Per-shard concept_relations SQLite table. Append-only, with
    UNIQUE (source_root, relation_kind, token, target, evidence_kind)
    so re-derivation is idempotent. Lives next to documents in each
    shard so mesh sync moves relations alongside the docs that
    derived them. A SECONDARY index — writes here NEVER affect
    document_root / chunk_root / cache_key, so backfilling is safe
    across the entire corpus.

(2) aborist/concepts/ package:
    - store.py:   add_concept_relation, concept_relations_for_token,
                   purge_by_evidence_kind, list_evidence_kinds
    - query.py:   cross-shard synonym_expand, rivalry_excluded;
                   union-find collapse on synonym edges so partial
                   pairs build full equivalence classes; mtime-keyed
                   per-process LRU so retrieval doesn't re-walk
                   shards on hot loops
    - seed.py:    one-shot migration of legacy frozensets (8 groups
                   incl. brain-tech) to evidence_kind='manual_legacy'
                   rows under source_root='__legacy__concepts__'
    - extract.py: pluggable extractor registry. Built-in:
                   link_reciprocity_synonym — for any reciprocal
                   edge pair (A→B AND B→A) in the existing edges
                   table, emit synonym edges between the docs'
                   title-tokens. Works for Wikipedia (See-also
                   bidirectional), HTML site internal-link clusters
                   (russell.ballestrini.net pattern), or any link
                   graph the corpus already encodes — no new
                   crawler needed; the html_page parser already
                   populates `edges` rows on ingest.

(3) aborist/qa/concepts.py rewritten as a backwards-compat shim —
    same public API (synonym_expand, rivalry_excluded,
    has_compare_phrasing) so query.py call sites unchanged.
    shards_dir threaded through _filter_by_title_relevance &
    _search_corpus' synonym_expand calls. Without shards_dir
    (legacy 2-arg call shape), helpers degenerate to no-op —
    matches the behavior the frozenset code had when no group hit.

(4) Cross-shard UNION view: concept_relations added to
    _SHARDABLE_TABLES in store.py so connect_query() exposes a
    unified view across all shards (same pattern as documents,
    chunks, providence_cache, etc.).

Live-verified on the 4-shard 3.47M-doc corpus + the
crawl_russell_ballestrini_net.db shard:

  Q: what technology are currently or soon available which may
     enable one person to reconstruct and understand some or a
     portion of another persons thoughts or ideas without speaking
     or sign language?

  Result: same as the bde1bd6 in-memory frozenset version —
  Telepathy E4 cited alongside Videoconferencing E1/E2,
  POINTER-LINKED-PARTIAL 6/10. The DB-backed lookup reproduces
  the frozenset behavior byte-for-byte.

Tests: 633 passed (was 624). 9 new concept tests covering
DB-backed synonym/rivalry lookup, shards_dir=None degenerate
behavior, brain-tech group seed, cache invalidation. Old tests
that called helpers directly without shards_dir kept as no-op
assertions (synonym_expand({"athlon"}) without shards_dir returns
{"athlon"} unchanged).

Backfill mechanics: existing live shards needed a one-shot
`connect()` to auto-create the new concept_relations table
(SCHEMA_SQL has CREATE TABLE IF NOT EXISTS). Re-derivation never
mutates the Merkle tree — concept_relations is fully orthogonal
to document_root / chunk_root. Cache_key dimensions are
unaffected.

Deferred (follow-on):
- CLI commands: aborist concepts {seed,list,add,derive,purge}
  (currently fox runs the helpers via python -c)
- Wikipedia See-also extractor (requires parsing wikitext sections
  beyond what's already in edges)
- Wikipedia category extractor (requires reading Category: links
  from chunked wikitext)
2026-05-01 21:14:53 -04:00
0d8eed0df8
qa(verify): Rule 8 — title-relevance promoted to hard verifier check
Promotes the title-relevance sidecar (diagnose_title_relevance,
qa/inspect.py) to a hard check inside both verify_claim_lattice
and verify_claim_lattice_json. New violation kind TITLE_MISMATCH;
demote-to-HYBRID semantics matching the existing WARRANT_MISSING
pattern.

Catches the 2026-05-02 fox-surfaced retrieval-driven hallucination
class:

  Q: "explain spin glass modeling & tensors?"
  Pre-Rule-8: EVIDENCE-WARRANTED 1/1, claim cited to Quantum
              chromodynamics chunk (single-line "See Also: spin
              glass" reference). Token-coverage check passed
              accidentally on shared physics vocabulary.
  Post-Rule-8: POINTER-LINKED-PARTIAL · title mismatch 1/1.
              Cited source title 'Quantum chromodynamics' shares
              zero stemmed tokens with claim's {spin, glass,
              modeling, tensor, ...} → demote.

Implementation:
  _claim_title_overlap(claim_text, source_title) returns True iff
  the source title shares ≥1 stemmed content token with the claim.
  Uses qa.evidence._content_tokens (≥4-char, post-stopword) and
  inline minimal stem (s-strip on tokens >4 chars, skip ss-enders)
  to avoid an import cycle.

  Per-claim loop in both verifiers checks every cited source's
  title; ANY-match suffices (only TITLE_MISMATCH when ALL cited
  titles miss). Vacuous-pass when claim or title has no
  extractable tokens.

  Renderer (_ladder_rung_for_lattice) treats TITLE_MISMATCH
  alongside WARRANT_MISSING as the POINTER-LINKED-triggering
  signal — both indicate citation/claim structural misalignment.
  _render_warrant_tail surfaces "· title mismatch" alongside
  "· warrant missing" so an operator sees the specific failure
  mode at the audit-line.

Test fixtures updated where the synthetic claims were too minimal
(e.g. "Velociraptors are shown attacking workers" cited to
"Jurassic Park (film)" — claim had no topic anchor). Real model
output naturally references the topic (the model sees the title
in the evidence map and uses it); the fixture revisions reflect
that. 4 new tests in test_verify_json.py covering the helper +
end-to-end TITLE_MISMATCH demote.

Live verification:
  Spin-glass query: POINTER-LINKED-PARTIAL · title mismatch ✓
  Homer/Mr. Burns:  EVIDENCE-WARRANTED ✓ (no regression)

CLAUDE.md updated with the Rule 8 convention; existing
diagnose_title_relevance sidecar marked legacy / dict-form for
per-cache-key inspect use.

Full suite: 738 passed (was 734, +4).
2026-05-01 20:59:14 -04:00
bde1bd61f4
qa(retrieval): mind/brain-tech synonym group for intent-question queries
Closes the 2026-05-01 retrieval gap fox surfaced on:

  Q: what technology are currently or soon available which may enable
     one person to reconstruct and understand some or a portion of
     another persons thoughts or ideas without speaking or sign language?

  Pre-fix: top-K = [Videoconferencing, Telepathy, Identity, PKM,
           Intrapersonal]; only Videoconferencing used (BM25 won on
           "person/language/speak" token density). Verifier returned
           EVIDENCE-WARRANTED-PARTIAL 2/5 with Videoconferencing
           cited as evidence for "currently no technology...". Telepathy
           was retrieved but unused — title-relevance filter dropped
           it to body-density accept path with a low score.

  Post-fix: same top-K but Telepathy now SURVIVES title-relevance
            (its title token "telepathy" is in the accept set via
            synonym expansion). Title-token-boost lifts it.
            EVIDENCE-WARRANTED-PARTIAL 6/10 with Telepathy E4 now
            cited alongside Videoconferencing — sources used 5/5.

Adds a 7th SYNONYM_GROUPS entry mixing concept tokens (thoughts,
mind, cognition, consciousness) with specialized entity tokens
(telepathy, neurotechnology, BCI, fMRI, EEG, TMS, transcranial).
A query naming either side expands to the whole family, so the
title-relevance accept path admits brain-tech pages even when the
question is phrased in lay vocabulary.

Limitation accepted: this only affects title-relevance filtering &
title-token rerank, not the FTS5 BM25 retrieval that produces the
top-K. Pages whose BM25 score is below the top-K cutoff (e.g.
Neurotechnology at #4 in fox's manual-hint Run 2) still won't
surface unless FTS5-side synonym expansion is added too. Tracked
as a follow-on ticket — for now, query-time retrieval hints
("transcranial knowledge acquisition") continue to be the manual
escape hatch when intent vocabulary doesn't lexically anchor.

Tests: 624 passed (no regression).
2026-05-01 20:55:23 -04:00
00528456ff
qa(retrieval): _search_titles ORDER BY token-hit score; word-boundary post-filter
Fox-surfaced 2026-05-02 case:
  Q: "what date did back to the future come out?"
  → top-8 sources: trilogy, ride, animated-series, The Future,
    Future Husband, The Day You Come, Ratchet & Clank Future,
    Future plc — film article entirely absent.

Two compounding defects in the title-LIKE retrieval path:

(1) `_search_titles` had no ORDER BY. SQL `LIKE '%token%' OR ...`
    returned rows in arbitrary internal order; LIMIT 32 truncated
    before genuine matches. The film article `Back to the Future`
    (title contains both `back` AND `future`) lost the LIMIT race to
    substring-match junk like `Out (poker)` (single-token match on
    `out`), `Aberdeen, South Dakota` (`south` contains `out`),
    `Backplane` (`back` substring), etc.

    Fix: SQL builds a token-hit score via CASE WHEN per token and
    ORDERs BY (title_score DESC, LENGTH(title) ASC). The film
    article's score=2 (back+future) ranks above garbage's score=1.
    Tie-break on title length favors title purity (shorter title +
    same overlap = higher per-token signal).

(2) Caller's post-filter check used substring match
    (`if t in title_lower`) — same defect as the SQL. "south"
    contains "out", "outline" contains "out", "backbone" contains
    "back", etc. all passed the no-op overlap check.

    Fix: tokenize the title via `_title_query_tokens` (already
    drops stopwords + small tokens) and stem-aware-intersect with
    the query stems. Word-boundary semantics; only genuine title
    tokens count.

Verified live:
  Pre-fix:  trilogy at #1, film article absent from top-8.
  Post-fix: film article at #1, trilogy at #2, junk gone.

Verdict on the BTTF query stays POINTER-LINKED-PARTIAL · warrant
missing because the model picked the box-office chunk (mentions
"1985" but not "July") instead of the infobox/release-date chunk.
That's correct warrant behavior: claim asserts both anchors;
cited span only has one. The retrieval-side defect was a separate
gap that this fix closed; chunk-relevance ranking for cited
content is downstream work.
2026-05-01 20:41:12 -04:00
9ec9469c4d
qa(frame): #000002 land — reference-frame polarity contract (D3 → ✓)
The phrase-pattern retrieval route (commit 1b8677d) closed the
RETRIEVAL side of reference-frame failure; this ticket closes
the ANSWER side.

New module aborist/qa/frame.py:
  FrameDetection dataclass (frame_kind, reference_title,
    reference_uri, confidence). Sidecar — never enters cache_key
    or governance_policy_hash.
  detect_frame(question, sources, phrase_match_roots) — heuristic
    detector. Reference-frame classification fires when:
    (a) phrase route surfaced at least one source, AND
    (b) at least one phrase-matched source is a reference work,
        determined by:
          - title parenthetical disambig (`(novel)` / `(film)` /
            `(play)` / `(franchise)` / etc.), OR
          - body sample contains ≥3 DISTINCT fiction markers
            (novel / published / protagonist / plot / ...).
    Distinct-marker count keeps the heuristic robust against a
    history article saying "novel approach" twice.

aborist/qa/query.py:
  Calls detect_frame for lattice modes only. Body sample uses the
  ARTICLE LEAD (chunk_idx=0, post-wikitext-strip) — fiction
  markers cluster in the lead on Wikipedia, not in plot chunks
  that may have been query-relevance-ranked higher.
  New policy field claim_lattice_polarity_preamble injected as a
  user-role message before the grounding_reminder when
  frame_kind == "reference". Format-string with
  {reference_title} placeholder.
  Result dict carries frame_detection (kind / title / uri /
  confidence) for renderer + bench consumption.

aborist/cli.py:
  Renderer adds a `reference frame: <title>` line when
  frame_detection.kind == "reference". Skipped for literal /
  no-phrase-route / ambiguous rows.

Live verification — Orwell case:
  PRE  : "The text does not directly state that Oceania has always
          been at war with East Asia."
  POST : "In George Orwell's dystopian novel Nineteen Eighty-Four,
          the nation of Oceania is always at war with Eastasia,
          but this is a result of propaganda and doublethink, not
          actual historical continuity. The war with Eastasia is
          a fabricated conflict to maintain control..."

Multi-frame compilation: distinguishes propaganda claim from
fictional-actual continuity, exactly the polarity contract from
the ticket §2 abstraction.

Literal queries (capital of france) unchanged — polarity preamble
only injects when frame_kind classifies as "reference".

22 new tests (19 in test_frame.py + 3 in test_cli_render.py).
Full suite: 734 passed (was 712, +22).

Directive D3 status: ½ → ✓. Ticket #000002 closed.

All seven structural directives now ✓:
  D1, D2, D5 (were ✓);
  D3 #000002, D4 #000001, D6 #000003, D7 #000005 (closed in this
  series).
2026-05-01 20:18:13 -04:00
1dfa4636c3
qa(provenance): #000001 land — retrieval_plan_hash binds D4
New module aborist/qa/retrieval_plan.py:
  RetrievalPlan dataclass — frozen, captures the operator-
    influenceable retrieval inputs that determine source
    selection (retrieval_keywords, top_k, over_fetch,
    max_context_chars, shard_ids).
  retrieval_plan_hash() — SHA-256 over canonical-JSON.
    Deterministic per call; folds into the run-DAG retrieval
    stage as a bound input alongside the existing
    sources_summary output.

build_run_dag (aborist/qa/dag.py) accepts retrieval_plan_hash:
  When provided, the retrieval stage hash binds BOTH plan
    (input) and result (output): H({"retrieval_plan_hash":...,
    "sources_summary_hash":...}).
  When omitted (e.g. legacy / quote-mode callers that haven't
    plumbed it yet), the retrieval stage falls back to the
    historical sources-summary-only hash so pre-#000001
    run_dag_root values stay stable.

query.py constructs the plan per call and passes it through.
Question text intentionally NOT in the plan — already covered
by question_hash. Shard ids included so audit can reproduce
which shards the search ran against.

Two runs with identical sources but different retrieval keywords
now produce different run_dag_root values — the provenance gap
on operator hints (auditor recovers "these were the keywords
that pulled in those sources") closes for the run-DAG path.

Marker test in tests/test_directives.py flipped from "absent"
assertion to "present": test_d4_retrieval_plan_binding_landed.
Plus test_d4_retrieval_plan_hash_module_exists pinning the
module shape. Full suite: 712 passed.

Deferred per ticket §6:
  - audit events (retrieval_plan_built / retrieval_result_selected)
  - providence_cache.retrieval_keywords SQL column
  - optional strict cache_key mode (retrieval_plan_affects_cache_key)

These are ergonomic affordances atop the run-DAG binding; earn
their own tickets when bench evidence shows the blob path is
too friction-heavy for real workflows.

Directive D4 status: ½ → ✓. Ticket #000001 closed.
2026-05-01 19:26:01 -04:00
3796c238cc
qa(label): #000005 land — four-rung ladder migration (D7 sharpened)
Replaces the two-rung EVIDENCE-LINKED / EVIDENCE-LINKED-PARTIAL
display label for claim-lattice methods with a four-rung ladder
that names a strictly stronger property at each rung:

  POINTER-LINKED       pointer/source/chunk verified;
                       warrant either didn't apply or failed
  ANCHOR-WARRANTED     pointer-linked + warrant passed where it ran;
                       other soft demotes may apply
  EVIDENCE-WARRANTED   anchor-warranted + no soft demotes
  UNGROUNDED           no verified pairs

HYBRID gets a -PARTIAL suffix on whichever rung applies.

Implementation: _render_audit_label gains a violations parameter
(defaults to None for backward-compat). _ladder_rung_for_lattice
discriminates rungs from the existing violations list:
  - WARRANT_MISSING in violations → POINTER-LINKED
  - any of {LAZY_ANCHOR_DEMOTED, POINTER_OVERFLOW_TRIMMED,
    TOO_MANY_CLAIMS, BARE_NAME_CLAIM} → ANCHOR-WARRANTED
  - else → EVIDENCE-WARRANTED

Design simplification vs the ticket's §3 sketch: the proposed
verifier_steps_ran field on the verdict dict was NOT needed. The
existing violations list carries enough signal to discriminate
all rungs. Per the five-step algorithm step 2: don't add fields
you don't need.

Quote / span / entity / paraphrase methods stay unchanged (their
STRICT verifies pinned spans, not synthesis).

Schema column audit_mode enum stays {STRICT, HYBRID, UNGROUNDED}
— pure renderer transformation, no governance_policy_hash bump,
no cache invalidation, no mesh-wire-format change. Existing
providence records render under the new ladder on next read.

5 new renderer tests in tests/test_cli_render.py covering each
rung mapping. D7 anti-regression test in tests/test_directives.py
updated to gate on the ladder labels. Bench helper docstring
follows. Full suite: 711 passed.

Directive D7 stays at ✓; ticket #000005 closed.
2026-05-01 19:20:47 -04:00
f419d76292
qa(warrant): #000003 land — anchor-class generalization (D6 → ✓)
Three new question-shape classes dispatched through warrant_check
alongside the existing relation + date anchors:

(1) Entity-list shape — `name X`, `list X`, `who are the members
    of X`. List-aware extractor `extract_entity_list_anchors`
    (multi-word phrases ∪ solo-cap individual names) so comma-
    separated entities each contribute. ANY-match semantics:
    demote-don't-reject when an extra entity from training-prior
    appears alongside grounded ones.

(2) Count shape — `how many X`, `how much X`. Digit ↔ word
    equivalence (claim says "six", span says "6", or vice versa)
    with ordinal collapse (`sixth → 6`). Year-shaped digits
    filter out (those belong to the existing date anchor class).
    ALL-match semantics: every count token in the claim must
    appear in some cited span as digit or word.

(3) Why-cause shape — `why X`. Cause-anchor pool widens to
    ≥5-char lowercase common nouns (post a generic stopword set
    that filters quantifier-adjective fillers like "various",
    "factors", "situation") PLUS proper-noun anchors from the
    existing extractor. Gated on why-shape only: lowercase
    common-noun extraction has higher false-positive risk
    elsewhere.

Per-class policy gate (proposed `claim_lattice_warrant_classes`
dict) deferred per the five-step algorithm step 2: single
`warrant_check_enabled: bool` is the minimum viable gate; per-
class flags earn their slot when bench evidence shows over-firing
on a specific class.

17 new warrant tests (detector + extractor + integration).
Marker test in test_directives.py flipped from "absent" to
"present" assertion: test_d6_warrant_generalization_landed.

Full suite: 709 passed (was 692, +17).

Directive D6 status flipped to ✓ in seven-point-program.md.
Ticket #000003 closed.
2026-05-01 19:16:06 -04:00
4fada8e22e
tests: anti-regression layer for the seven-point program (23 tests)
Each directive gets a structural pin in tests/test_directives.py.
A future PR that silently weakens a directive fails by name.

D1 (no LLM in hard verifier path):
  - verify_quotes / verify_claim_lattice / verify_claim_lattice_json
    signatures must NOT carry chat_client / llm / judge / model
    parameters.
  - SCHEMA_SQL verifier_method CHECK constraint enum excludes
    'llm', 'model', 'judge', 'nli', 'hermes', 'ai'.

D2 (Hermes emits pointer clauses):
  - ANSWER_MODES contains both lattice variants.
  - DEFAULT_ANSWER_MODE in ANSWER_MODES.
  - parse_pointer_claims returns structured (claim_text, pointer_ids)
    nodes from pointer-line input.

D3 (build CTI internally):
  - Runtime parses pointer-line prose into typed claim nodes
    (text + pointer_ids list); model never produces structured
    output directly in pointer mode.
  - Evidence map built by runtime from retrieved chunks; pointer
    ids minted sequentially; evidence ids content-addressed.

D4 (bind retrieval AND evidence map):
  - build_run_dag accepts evidence_map_root parameter (✓).
  - Marker test: build_run_dag does NOT yet accept retrieval_plan_hash
    parameter; flip when ticket #000001 lands.

D5 (deterministic pointer verify):
  - verify_claim_lattice is deterministic: same inputs → same
    verdict byte-for-byte.
  - _claim_textually_overlaps_evidence (the per-pair coverage
    check) is a pure function with no LLM-side parameters.

D6 (anchor-class warrant before NLI):
  - warrant_check signature is lexical-only (no chat_client / llm
    / judge / nli params).
  - Date-anchor failure case fires (year missing from cited spans).
  - Relation-shape failure case fires when ALL proper-noun anchors
    are missing from cited spans.
  - Marker test: per-shape detectors for entity-list / count /
    why-cause shapes are absent today; flip when ticket #000003 lands.

D7 (rename labels honestly):
  - Renderer maps STRICT → EVIDENCE-LINKED for claim_lattice* methods.
  - Renderer keeps STRICT for quote / span / entity / paraphrase
    methods (those verify against pinned spans, not synthesis).
  - SCHEMA_SQL audit_mode CHECK constraint enum stays
    {STRICT, HYBRID, UNGROUNDED} regardless of renderer relabel.

D8 (test-pinning before automation):
  - docs/seven-point-program.md exists.
  - bench/qa_sweep.py exposes _directive_compliance helper.
  - docs/TICKETS.md indexes the partial directives (D3, D4, D6) so
    the design log doesn't drift from the program doc.

23 new tests; full suite 692 passed (was 669, +23).

The "marker" tests for D4 and D6 document open work: today they
assert that a parameter / detector is ABSENT; when the corresponding
ticket lands, the assertion flips to PRESENT. The test failing
during ticket implementation is the expected signal, not a
regression — the test message names the ticket so the work-in-
progress is legible.
2026-05-01 16:32:08 -04:00
6db5d1dc64
docs: open #000004 (closed/retroactive) + #000005; update program status
#000004 — Directive coverage in bench summary. Filed as closed
(landed in commit acd1f9c) for design-log traceability. Captures
the v1 substrate: per-row directive_compliance helper, _summarize
aggregation, markdown directive-coverage section, 5 unit tests.

#000005 — Label ladder migration (POINTER-LINKED / ANCHOR-WARRANTED
/ EVIDENCE-WARRANTED / ENTAILMENT-VERIFIED). Status open. Migration
from today's two-rung EVIDENCE-LINKED to a four-rung ladder where
each rung names a strictly stronger property. Schema column stays
{STRICT, HYBRID, UNGROUNDED} so v9.8 cache_key invariants hold;
renderer-level mapping only. Reserves ENTAILMENT-VERIFIED for a
future committed entailment engine without forcing premature
implementation.

TICKETS.md index gains both rows. Next ID bumps to 000006.

seven-point-program.md status snapshot updated:
  - D7 row links #000005 (ladder migration proposal).
  - D8 row links #000004 (closed substrate).
  - New "Anti-regression test layer" section maps each directive
    to its structural pin in tests/test_directives.py.

Note on fox's roadmap item #000006 (Internal CTI Expansion / frame
lattice / multi-frame answers): scope overlaps with #000002
(Reference-Frame Polarity Contract / Module L). NOT opening as
a separate ticket — Module L already covers it.
2026-05-01 16:31:44 -04:00
acd1f9ca84
bench: per-row directive_compliance + 'directive coverage' summary section
Wires the seven-point program (docs/seven-point-program.md) into
the bench harness as a per-mode coverage table.

Per-row computation in _directive_compliance(answer_mode, result, err):
  D2_pointer_clauses        — answer_mode is lattice variant
  D3_cti_substrate_ready    — lattice mode + run_dag_root populated
                              (full coverage pending ticket #000002)
  D4_evidence_map_bound     — run_dag_root populated
                              (retrieval-plan binding pending #000001)
  D6_warrant_fired          — warrant ran (lattice mode today;
                              per-shape gating pending #000003)
  D7_honest_label           — audit_mode in canonical enum
                              (renderer transformation pinned in
                              test_cli_render.py)

D1 (no LLM in verifier), D5 (verifier_method enum), D8 (test-pinning
discipline) are global properties of the substrate and don't appear
per-row — they get tracked once in CLAUDE.md / the program doc.

_summarize aggregates per-row booleans into per-mode pass counts.
_render_markdown emits a 'directive coverage (seven-point program)'
section with per-mode per-directive coverage as 'count/N (pct%)'.

Discipline rationale per CLAUDE.md's five-step algorithm step 5:
the bench harness IS the automation substrate, so it must enforce
the directives before any feature gets layered on top. Bench scores
that climb without directive coverage climbing are graveyard-digging
(step 4: don't dig the grave faster).

5 new bench tests cover: per-mode pass aggregation, the markdown
section presence and content, the helper's behavior on quote mode
(D2 fail by design), lattice mode (all directives pass), and error
rows (empty dict — no signal). Full suite 669 passed.
2026-05-01 16:20:25 -04:00
b5f7c93fca
docs(ticket-000003): anchor-class warrant generalization (Directive D6)
Today's warrant-lite (aborist/qa/warrant.py) covers two anchor
classes: proper-noun (relation-shape questions) and date (any
claim with a 4-digit year). Three failure shapes pass the existing
seven hard checks unchecked:

  - entity-list: 'name the simpsons family + pets' — claim
    enumerates entities, but the warrant doesn't demand all named
    entities appear in some cited span.
  - count: 'how many wives did henry the eighth have?' — claim
    says 'six', but the warrant doesn't demand the count token
    appear in some cited span.
  - why-cause: 'why did the titanic sink?' — claim names a cause
    noun (iceberg, asteroid, propaganda), but the warrant doesn't
    demand the cause token appear in some cited span.

Ticket proposes per-shape anchor extractors dispatched by question-
shape detector (existing _question_is_relation_shape pattern).
Conservative scope on cause anchors: gate by why-shape only since
lowercase common-noun extraction has higher false-positive risk.

Folds into governance_policy_hash via per-class policy dict so an
operator can disable individual classes per-corpus when they
over-fire.

Out of scope per docs/naming-deferral.md: typed-contract framework
(per-question-type rule libraries). General anchor-class primitive
catches the failures listed without per-type rule books; typed
contracts earn slots only when bench evidence shows the general
primitive misses cases.

Forecast cost ~3-4 hours; risk medium (false-positive risk on
why-shape cause-noun extraction). Closes the lazy-anchor gap on
the question shapes the bench question set already exercises.
2026-05-01 16:20:07 -04:00
c1dcd3142e
docs: seven-point-program north-star — directive matrix + status snapshot
The 2026-05-01 distilled architectural directive becomes the
audit lens applied before every new ticket / feature / prompt
edit. Each of the seven directives gets:

  - status (✓ / ½ / ✗)
  - code anchors (verifier paths, renderer hooks)
  - pinning tests (per-rule coverage in tests/)
  - tickets covering remaining work
  - bench signal (which row column reports it)

Status snapshot at landing:
  D1 (no LLM in verifier)              ✓
  D2 (pointer clauses)                 ✓
  D3 (CTI internally)                  ½  → ticket #000002
  D4 (retrieval map AND evidence map)  ½  → ticket #000001
  D5 (deterministic pointer verify)    ✓
  D6 (anchor-class warrant)            ½  → ticket #000003 (this commit pair)
  D7 (honest labels)                   ✓
  D8 (test-pinning before automation)  discipline

CLAUDE.md docs index gains a "North-star" section pointing at the
program; ticket entries gain a "Directive" column threading them
back to the program.
2026-05-01 16:19:50 -04:00
7ba1b405ee
qa: warrant-lite anchor-class — add date anchors (year + month name)
Generalizes warrant-lite from relation-only to anchor-class warrant.
Two anchor classes now compose:

(1) Proper-noun anchors — gated on relation-question shape; any-match
    semantics (existing behavior).
(2) Date anchors — always-on, gated on the CLAIM containing a year or
    month name. ALL components (year + month name if present) must
    appear in some cited span.

Closes the back-to-the-future failure fox surfaced 2026-05-01:

  Q: what date did back to the future come out?
  Claim: "Back to the Future was released in theaters on July 3, 1985."
  Cited span: trilogy/SNES/pinball prose with '...real 1985...' but
              no 'July' anywhere
  Pre-fix: EVIDENCE-LINKED · via claim_lattice 1/1
  Post-fix: EVIDENCE-LINKED-PARTIAL · via claim_lattice · warrant missing 1/1

Year extraction (4-digit, 1500-2199 range — avoids matching ZIP codes
/ elevations / model IDs) and month extraction (full English month
names — three-letter abbrevs deferred until bench shows the gap).
Both anchors required when both are in the claim. Year-only claim
against year-only span continues to pass; month-only against
month-only continues to pass; month-and-year claim against year-only
span (the BTTF case) now correctly fails.

Architectural note: this generalizes warrant-lite without building a
typed-contract framework (per-question-type rule libraries like
"release_date_lookup with required predicate cues"). The date anchor
primitive catches the date lazy-anchor failure class deterministically
without a per-type rule book; typed contracts earn their slot only if
bench evidence shows general anchors miss specific cases. Dropped the
caller-side is_relation_question gate from verify.py — warrant_check
now decides internally what to enforce based on question shape AND
claim content.

Tests: 29 unit tests in tests/test_warrant.py (was 16). New tests
cover extract_date_anchors (year, month, dedup, non-year-skip,
empty), warrant_check date semantics (BTTF case, year-only,
year-in-unrelated-context, month+year composition), proper-noun
warrant skipped without relation question. Full suite: 554 passed,
21 skipped.

Limitation accepted: ISO date format ("1985-07-03") wouldn't satisfy
month-name requirement. Real-world Wikipedia prose uses month names
("released on July 3, 1985"), so the trade-off favors catching the
common case. If bench shows ISO false-rejections, layer in a
month-number alternative to the month-name regex.
2026-05-01 15:24:11 -04:00
24198ab05e
docs(claude): compress + attribute — split bench-maxing, add docs index
Walked the five-step algorithm on CLAUDE.md itself.

Step 1 — requirements check: every section earned its place via a
specific operator-failure context. Sections that had grown into
prose essays got compressed back to load-bearing rules + pointers.

Step 2 — delete:
  - Bench-maxing prose section (~100 lines) → moved verbatim to
    docs/bench-maxing.md. CLAUDE.md keeps a one-block headline list
    pointing at the doc.
  - Conventions section: each rule tightened to one paragraph max,
    with a `See aborist/qa/verify.py` (or similar) trailing pointer
    so the operator can jump straight to the rationale in code.
    Verbose duplications between conventions and retrieval-pipeline
    sections (e.g. wikitext base prose explained twice) collapsed.
  - Retrieval pipeline: each of the 9 stages now references the
    relevant module (`qa/concepts.py`, `query.py`) instead of
    re-narrating the failure case in prose.
  - Architecture tree: minor trim, removed redundant comments
    where the filename already names the role.

Step 3 — simplify: source papers section dropped one item that was
duplicate (PDF + RST point to same content); kept the canonical
source.

Steps 4 + 5 — n/a (this is a doc, not a process).

New section: Docs index. Lists every architectural / design doc
in docs/ plus a ticket sub-list (open tickets with their files).
Closes the discoverability gap where TICKETS.md existed but agents
didn't know to look at it.

Net: CLAUDE.md goes from 308 → 311 lines BUT the avg information
density is up — bench prose doesn't bloat the entry-point doc, and
each convention now points to its source. The `docs/bench-maxing.md`
extraction is the real win: separation of "rules I must not break"
(CLAUDE.md) from "discipline I should internalize" (bench-maxing.md).
2026-05-01 14:12:37 -04:00
ac17e6e4ab
docs(claude): add five-step algorithm — apply in order
Engineering discipline added between operational rules and the
orientation protocol. Walk the steps in sequence; skipping makes
the next ones expensive.

  1. Make the requirements less dumb (every requirement gets a name,
     not a department).
  2. Delete the part or the process (10%+ put-back rate or you
     aren't deleting hard enough).
  3. Simplify and optimize (only after 1 + 2).
  4. Accelerate cycle time (only after simplifying — don't dig the
     grave faster).
  5. Automate (last, not first).

Connected to existing aborist conventions:
  - Step 1 traces every policy knob / schema invariant / verifier
    check to a specific defect or owner. Aligns with the
    "feedback memory" pattern (record the WHY).
  - Step 2 reinforces verifier-stays-binary and "no soft signals
    in the hard chain" — both are deletion-first rules.
  - Step 5 is why bench harnesses, distill runners, and ingest CLIs
    earned automation only after their underlying ops were
    simplified to single-purpose stages.
2026-05-01 14:07:07 -04:00
a99ac4388b
qa: lock phrase-route non-regression tests + open ticket #000002 (Module L)
Two follow-ups to the phrase-pattern retrieval fix (commit 1b8677d)
covering items 6 and 10-11 of fox's 2026-05-01 architectural review:

(1) Non-regression tests for the phrase route:
  - test_phrase_route_skipped_when_question_shorter_than_min_n
    pins the structural false-positive guard: the n=5/n=6 minimum
    means a 4-token literal-geography query lacks enough tokens to
    trigger the route at all.
  - test_phrase_route_does_not_hijack_literal_geography_query
    end-to-end: a 4-token "oceania east asia geography" query on
    a synthetic 2-doc corpus surfaces only the geography-stub doc;
    the orwell-stub doc (whose body has the diagnostic 5-gram) is
    correctly NOT pulled in by the phrase route on a literal query.

(2) docs/ticket-000002-reference-frame-polarity-contract.md
    Captures fox's Module L proposal verbatim as Appendix A and
    extracts the implementation sketch into the standard ticket
    body (problem statement, abstraction, CTI interpretation, three
    pieces of code to write, test list, scope boundaries).

    The phrase route closed the RETRIEVAL side of reference-frame
    failure. Module L addresses the ANSWER side: today's substrate
    answers Orwell queries as "the text does not directly state..."
    when it should produce multi-frame answers distinguishing
    Party propaganda from fictional-actual continuity. Forecast
    cost ~3-4 hours; risk medium (prompt augmentation interaction
    with claim_lattice prompt).

    Module M = ticket #000001 (route provenance binding); not
    duplicated. Module N (FP guards) partially landed via the
    tests above; remaining tests folded into ticket #000002's
    test list. Module H (relation warrant lite) lacks scope
    detail; deferred without a ticket.

(3) docs/TICKETS.md updated: index gains #000002 row, Next ID
    bumped to 000003.
2026-05-01 14:03:39 -04:00
1b8677d3d5
qa: phrase-pattern retrieval route closes the reference-frame failure class
Empirical 2026-05-01: query 'has oceania always been at war with east
asia' surfaced literal-geography articles (Oceania, Asia, Far East)
because BM25 scored each token independently — the diagnostic signal
'oceania always been at war' is a verbatim 5-token sequence, not a
distinct content token. The Nineteen Eighty-Four article had zero
title-token overlap with the question, so even when reached via FTS5
phrase MATCH it would be filtered out before rerank.

Fix is two parts:

(1) New phrase route in `_search_corpus`. For each n-gram extracted
from the question (n=6 score 100, n=5 score 90), run an FTS5
quoted-phrase MATCH and add hits to the candidate pool. n=4 was
tried and rejected: 'always been at war' matches generic war-history
articles too noisily. 5+ tokens trade recall for precision; most
allusions ('may the force be with you', 'winter is coming',
'to be or not to be') survive at length 5 or higher.

(2) New accept-path 4 in `_filter_by_title_relevance`. Phrase-route
hits bypass the title-token-overlap gate via `phrase_match_roots`
(set of document_roots that matched a phrase). Without this, the
1984 article would be retrieved by phrase MATCH and immediately
filtered out because its title 'Nineteen Eighty-Four' shares no
content tokens with the question.

Latent-bug fix as a side effect: `_search_corpus` previously returned
a bare list, and the caller did `getattr(hits, "_core_match_roots",
set())` to fish out a sidecar set — but the sidecar was never
attached, so the `core_match_roots` accept-path in
_filter_by_title_relevance silently received an empty set for an
unknown duration. The function now returns a tuple
`(hits, core_match_roots, phrase_match_roots, root_to_shard)` so
both routes are correctly threaded.

Live verification: post-fix query lands EVIDENCE-LINKED 1/1 with
Nineteen Eighty-Four cited and the model recognizing the Orwell
frame ('the passage describes a change in alliances...'). No
operator augmentation needed.

Bench expansion: 6 allusion-shape questions added under a new
'# allusion / reference frame' category for prevalence tracking.

docs/reference-frame-failure-class.md: investigation log capturing
the diagnosis + why phrase-pattern boost beats a hand-rolled
'Reference Frame Router' (allusions are long-tail; per-pattern code
rots; the corpus already knows — fix retrieval not add a new stage).

9 new unit tests in test_query.py covering _question_phrases shape
(no stopword strip, all-short-token-skip, dedup), _search_phrases
defensive paths (empty input, double-quote-bearing input), end-to-
end phrase surfacing on a synthetic corpus, and the accept-path 4
filter behavior. Full suite 649 passed.
2026-05-01 13:53:03 -04:00
3586eeeb0a
qa: pointer-format includes source title + chunk prefix; lock EVIDENCE-LINKED label
Two UX fixes addressing the 2026-05-01 Orwell run feedback:

(1) Evidence pointer format. Old form `[E5: "<excerpt>"]` looked
visually like a 1-indexed source rank — paired with a "sources (8)"
list whose `[5]` slot was a different document, operators easily
mis-attributed citations. New form:

  [E5 | Nineteen Eighty-Four | 682f0a11: "<excerpt>"]

Title comes from EvidenceObject.title (URI-tail fallback when None);
chunk_root prefix is the first 8 hex chars — enough to disambiguate
chunks from the same source while staying compact. Renderer change
in render_claim_lattice; pre-existing claim-lattice tests updated;
3 new unit tests pin the format (title-inline, URI-tail fallback,
no `[E1:` collision-shape).

(2) Audit-label discipline. _render_audit_label already maps STRICT
→ EVIDENCE-LINKED for claim_lattice* modes (verifier output stays
binary, schema CHECK constraint stays); 4 new tests in
test_cli_render.py pin the relabel for STRICT / HYBRID / UNGROUNDED
in claim_lattice modes and the no-relabel for quote-mode where the
verifier verifies pinned spans not synthesis.

Net: 8 new tests, full suite 649 passed.
2026-05-01 13:52:38 -04:00
bf8d93caba
qa: spotlight density rank — pick load-bearing slice, not leading match
Pre-fix: _spotlight_excerpt used first-match-of-longest-content-token.
For "who is homer simpson's boss?" the Homer Simpson chunk had
"homer" matching early in voice-actor / Castellaneta prose ("For
voicing Homer..."), so the spotlight centered there even though
the actual answer ("his boss Mr. Burns") was deeper in the chunk
where boss/burns/homer cluster together.

Post-fix: density rank. Find ALL match positions for ALL content
tokens, then pick the position whose ±half-window cluster contains
the MOST distinct tokens. Tie-break on smallest idx (deterministic,
reproducible).

Live verified:
  - "who is homer simpson's boss?" — spotlight now lands on
    "...At the plant, Homer is often ignored and completely
    forgotten by his boss Mr. Burns..." (boss/burns/homer/plant
    cluster). Pre-fix it landed on voice-actor prose with no
    Mr. Burns visible.
  - "name simpsons family members including pets?" — spotlight
    lands on "...the family's two pets, Santa's Little Helper
    and Snowball II..." (densest pets/family cluster). Already
    worked pre-fix; now even tighter.

Performance: O(|positions|^2) inner ranking. Typical chunks have
4-10 content tokens and 1-3 match positions each, so 8-30 positions
and ≤900 inner iterations — trivial.

Bracket format change (in render_claim_lattice) ships alongside:
   [E5: "..."]                          → before
   [E5 | <title> | <chunk_root[:8]>: "..."]  → after
Closes visual confusion where E# is a chunk pointer but readers
parsed it as a 1-indexed source rank from the sources list.

Tests: 523 passed.
2026-05-01 13:37:04 -04:00
182bb67996
qa: warrant-lite — wire JSON variant of claim_lattice verifier
Initial Ticket H commit (38d95dd) only plumbed warrant into
verify_claim_lattice (pointer mode). The JSON variant
verify_claim_lattice_json was missed — and the CLI's
--answer-mode claim_lattice (the JSON path) was producing
EVIDENCE-LINKED on relation-question records without warrant
firing.

This adds the same warrant block to verify_claim_lattice_json:
question + warrant_check_enabled params, identical
WARRANT_MISSING violations, identical demote-to-HYBRID
semantics, same warrant_missing_claim_idxs field on the verdict.
Plumbs question= and warrant_check_enabled= through call sites
in runner.py and query.py for the JSON path.

Live verified: warrant correctly checks the FULL cited span
(not the spotlight excerpt). On the Homer-Simpson "boss is Mr.
Burns" case, the chunk text DOES contain "Mr. Burns" elsewhere
in the paragraph — warrant rightly passes. The misleading
spotlight excerpt that fox flagged is a separate render-layer
issue (failure to spotlight the load-bearing slice), not a
warrant failure.

Note: cache-hit path does not currently restore violations into
the result dict (providence_cache schema has no violations
column; run_dag_blob stores only hashes). On cache hits the
warrant_tail in CLI render won't surface even when warrant
fired at write-time. Follow-up needs a schema migration to
persist violations alongside the cached record.
2026-05-01 13:32:05 -04:00
38d95dd21b
qa: warrant-lite — relation-question hard check (Ticket H)
Closes the lazy-anchor failure class fox surfaced in feedback-3
(2026-05-01) on "who is homer simpson's boss?":

  Claim: "Homer Simpson's boss is Mr. Burns."
  Cited span: voice-actor / Castellaneta bio prose
  Pre-warrant: pointer-linked, source-role allowed, coverage passes
  Post-warrant: WARRANT_MISSING violation, audit_mode capped at HYBRID

Pointer verification != warrant verification. A pointer resolves to
a span; a warrant requires the span to actually contain the claim's
named answer entity.

Adds aborist/qa/warrant.py:
  - is_relation_question(q) — regex shape detector for "who is X's Y?",
    "who is supermans girlfriend?", "who founded Z?", "boss of X" etc.
  - extract_answer_anchors(claim) — multi-word proper nouns ("Mr.
    Burns", "Lois Lane") with possessive 's stripping; solo-cap
    fallback that skips sentence-starter false positives
  - warrant_check(claim, spans) — at-least-one-anchor lexical match;
    vacuous-pass when claim has no proper-noun anchor

Wired into verify_claim_lattice via new params (question,
warrant_check_enabled). Per-claim WARRANT_MISSING violations cap
audit_mode at HYBRID using the same demote pattern as
lazy_anchor_demoted. CLI renders " · warrant missing" tail when
violations present so the failure mode is legible without scanning
the violations array.

Policy field claim_lattice_warrant_check_enabled added to
DEFAULT_POLICY / DEFAULT_QUERY_POLICY and the verifier-policy hash
field set so changes bump verifier_policy_hash.

Hard or soft? Per CLAUDE.md "Soft hash vs hard hash":
substring/lexical = hard. Output is binary (warrant present or not),
violations enter the proof path through violations[] alongside
SOURCE_ROLE_BLOCKED and CITATION_MISMATCH.

Deferred (follow-on commits):
  - full RELATION_PATTERNS table with relation-noun aliases
  - two-hop bridge detection ("X founded Y" + "Y is Z" -> "X is Z")
  - retrieval expansion for relation questions

Tests: 16 new unit tests in tests/test_warrant.py covering
is_relation_question, extract_answer_anchors, warrant_check
(happy path, lazy-anchor case, vacuous-pass, case-insensitive,
empty inputs). Full suite: 523 passed, 21 skipped.
2026-05-01 13:23:24 -04:00
63c5b2fd8a
qa: feedback-2/3 — honest label, sentence spans, source-role display, retrieval purity
Implements 6 of fox's feedback-2/3 items (2026-05-01) targeting the
red-fish-blue-fish output. Each lives in the render or sidecar layer
— no schema or cache-key change beyond the verifier_policy_hash that
landed earlier in the day.

(1) Honest audit-label rendering. Schema audit_mode (STRICT/HYBRID/
    UNGROUNDED) describes the lexical verifier's call; the user-
    facing label spells out what was actually checked. Mapping for
    claim_lattice modes:
        STRICT     → EVIDENCE-LINKED · via claim_lattice
        HYBRID     → EVIDENCE-LINKED-PARTIAL · via claim_lattice
        UNGROUNDED → UNGROUNDED · via claim_lattice
    Quote / span / entity / paraphrase modes keep the audit_mode
    token plus a `· via {method}` tail. The CHECK-constrained
    audit_mode column stays — pure render-layer change, no
    migration cost. Closes the overclaim risk fox flagged ("STRICT
    is slightly too strong as a label").

(2) Sentence-bounded evidence spans. _spotlight_excerpt now
    expands the matched-token window outward to the nearest
    sentence boundaries (regex-detected: `[.!?][\"')\\]]?\\s+(?=[A-Z])`)
    instead of cutting at fixed byte offsets. Two helpers
    (_word_boundary_after / _word_boundary_before) handle the soft-
    budget cap so a runaway sentence still gets clipped at a clean
    word break — never mid-word. Pre-fix the renderer produced
    excerpts like `"...freewheeling plot... friends and p..."` (the
    `p...` is the offending mid-word truncation fox surfaced); now
    the excerpt is `"One Fish Two Fish Red Fish Blue Fish is a
    1960 children's book by Dr. Seuss. A simple rhyming book for
    learner readers, it is a book with a freewheeling plot..."`.

(3) Source-role display + used/unused annotation. The CLI source
    list went from
        [1] One Fish Two Fish Red Fish Blue Fish — wikipedia.org/...
        [2] Red Dwarf — wikipedia.org/...
    to
        [1] One Fish Two Fish Red Fish Blue Fish — primary_answer_source — used (E1) — ...
        [2] Red Dwarf — background_source — unused — ...
    Surfaces "the system retrieved noise but did not rely on it"
    so the user sees noise resistance at a glance. Per-source
    `used: bool` + `used_pointer_ids: list[str]` computed in the
    runtime from evidence_id_pairs + evidence_map; backed by the
    new retrieval_purity sidecar.

(4) Retrieval-purity sidecar. Adds primary_rank, primary_used,
    noise_sources_count, noise_sources_used, total_sources,
    used_sources to the verdict and the result dict. Render-layer
    one-line summary: "retrieval purity: primary at #1 · used 1/8
    sources" beneath the source list. Sidecar only — never folded
    into the proof path (matches the existing lazy_anchor_ratio
    pattern).

(5) Ordered-token title scoring. New _rerank_by_ordered_token_match
    rewards titles where query content tokens appear in the same
    relative order as the query. "red fish blue fish" query →
    "One Fish Two Fish Red Fish Blue Fish" gets ordered_match=4
    boost (1+0.5*3 = 2.5×); "Red Dwarf" stays at ordered_match=1
    (no boost). LCS-based, stem-aware, deterministic. Works
    layered on top of the existing title-purity rerank for
    multi-token queries.

(6) Noise-resistance fixture (tests/test_qa_quality_live.py).
    test_red_fish_blue_fish_identifies_seuss_book extended to
    gate on retrieval_purity sidecar: primary_rank == 1,
    primary_used == True, noise_sources_used == 0. Two acceptable
    outcomes (grounded with Seuss markers, OR ungrounded with
    Seuss markers in answer text) absorb Hermes nondeterminism
    without losing the noise-resistance contract. Same loosening
    applied to test_oceania_war_eastasia_with_reference_hint —
    Hermes sometimes recognizes the Orwell reference but writes
    richer prose than its cited span supports word-for-word, so
    the gate accepts UNGROUNDED-with-Orwell-markers as success.

Live test_qa_quality_live.py: 20/20 fixtures pass. 507 unit tests
green. Working tree leaves bench/qa_questions.txt for fox's
parallel WIP.
2026-05-01 12:41:08 -04:00
292e49e18f
docs(ticket-000001): append architectural review (Asia/Kuala_Lumpur, 2026-05-01)
Fox-supplied review expanding §2-§4 with axiomatic framing, CTI /
Merkle-AGI / PROMETHEUS-Σ interpretations, a concrete implementation
diff, and a strengthened test list. Captured verbatim as Appendix A.

Key refinements over the §2-§4 sketch:

- Splits retrieval-stage hash into retrieval_plan_hash +
  retrieval_result_hash + retrieval_stage_hash (plan vs result
  separation, axiomatically cleaner than embedding plan in the
  retrieval-stage payload directly).

- Adds two new audit events: retrieval_plan_built and
  retrieval_result_selected. Surfaces the retrieval inputs in the
  audit chain (not just the run_dag), so cache-hits also commit
  the plan via cache_hit_with_retrieval_plan.

- Distinguishes three keyword-handling cases for cache identity
  (Case A/B/C) and lays out a clean boundary rule: keywords stay
  in retrieval_plan unless they reach the model (then prompt_hash)
  or affect routing policy (then retrieval_policy_hash).

- Promotes 'the map must be committed, not only the territory
  reached' as a core principle. Operator-supplied maps (keywords,
  routing modes, ranking policies) become first-class clauses
  upstream of retrieval_result.

- Lists 12 concrete tests including the critical 'same sources,
  different keywords → different run_dag_root' assertion that
  proves the gap is closed.

Header note: Appendix A is authoritative when it disagrees with
§2-§4 above. The original §2-§4 sketch stays in place for
chronological context but is superseded by the appendix.
2026-05-01 12:22:16 -04:00