Commit graph

195 commits

Author SHA1 Message Date
a1dd330455
qa: tighten TITLE_MISMATCH + DEFLECTION demotes from emergent-log review
Emergent stress-test log surfaced two failure shapes the curated
bench can't reach. Reviewer (claude-opus-4-7 + fox) appended
teacher: blocks to four log entries; two of them named real
defects in the demote chain. This commit ships those fixes plus
the teacher-block annotations.

Cases reviewed (2026-05-02 emergent_log.jsonl):

  ✅ Menkar/sterilizers/trifle           — working as designed
  🟡 swallowtail/upbraided/rockiest      — metaphorical deflection
  🔴 cashback/widescreens/within         — warrant gap on inferential claims
  🔴 comeliness/fetish/investitures      — topic shift earned ANCHOR-WARRANTED unfairly

Two code changes:

1) verify_claim_lattice + verify_claim_lattice_json: when EVERY
   resolving claim has TITLE_MISMATCH, demote audit_mode →
   UNGROUNDED. Previously HYBRID. Catches the cashback case
   ('widescreens offer cashback' cited to a generic Coupon
   article — n_verified=1 overclaimed when the citation was
   meaningless).

2) cli._ladder_rung_for_lattice: DEFLECTION_DETECTED moves from
   soft-demote (cap at ANCHOR-WARRANTED) to hard-demote (cap at
   POINTER-LINKED) alongside WARRANT_MISSING and TITLE_MISMATCH.
   Catches the comeliness case (model fully shifted topic to
   'Rock & Chips'; pre-fix the verified Rock & Chips claims
   landed at ANCHOR-WARRANTED unfairly).

Test updates:
  - tests/test_verify_json.py:test_verify_json_title_mismatch_demotes_to_ungrounded
    (renamed; expected outcome flipped HYBRID → UNGROUNDED)
  - 4 fixtures in tests/test_claim_lattice.py and 1 in
    tests/test_verify_json.py: claim text augmented with 'in the
    film' so the Jurassic-Park-titled fixture's title shares a
    stem with the claim. These tests were testing other paths
    (NO_EVIDENCE_POINTER, EVIDENCE_LINKED_PARTIAL, UNKNOWN_EVIDENCE_ID)
    and the title-mismatch was incidental to the fixture choice.

Teacher blocks on the four log entries record the verdict +
failure_class + recommendation + notes, with reviewer + date.
The two ✅/🟡 entries have no recommended code change; the two 🔴
entries reference this commit as the fix.

751/34 tests pass.
2026-05-02 13:33:48 -04:00
ea82e4288c
docs: consolidate docs/modules/* → docs/modules.md (single file)
12 per-module files + index = 13 files of 50-185 lines each =
1,064 lines of API reference scattered across a directory.
Each per-module file had real meat (API examples, ASCII tree,
conventions) but the cognitive cost of 'which file is this in?'
outweighed the navigation benefit.

Built via concatenation + patch-fix:

  - cat index.md + per-module files in topological order
  - rewrite ../diagrams/  -> diagrams/  (relative to docs/modules.md)
  - rewrite ../../aborist/ -> ../aborist/
  - rewrite ../TICKETS.md  -> TICKETS.md, ../mesh.md -> mesh.md, etc.
  - inter-module links (./<name>.md, <name>.md) -> #<name>-py anchors
  - demote per-module H1 -> H2, H2 -> H3, etc., so the wrapper H1
    is the only top-level heading
  - de-dup the index.md's (now-H2) 'Aborist module reference'
    header against the wrapper, replace with 'Diagrams index'
  - inject explicit <a id="<name>-py"></a> anchors after each
    module's H2 so the TOC links resolve regardless of GitHub's
    auto-slug rules
  - polish TOC link text: '[merkle.md](#merkle-py)' -> '[↓](#...)'
    (the '.md' suffix made no sense once it's an in-doc anchor)

References updated:
  - README.md (×2)

Net: 1,124 single-file lines vs 1,064 across 13 files. Slightly
larger because of the patch-fix scaffolding (anchors + section
markers), but one Cmd-F covers everything.

751/34 tests still pass.
2026-05-02 13:23:43 -04:00
7444f106fc
docs: tickets/ subdir + delete naming-deferral, ref-frame, coverage-audit
Two cleanup operations bundled (separate scopes, single commit
since they share the doc-tree settle):

1. Move ticket-NNNNNN-<slug>.md files into docs/tickets/. The
   directory makes browsing the design log easier; the index
   stays at docs/TICKETS.md (top-level pointer). Convention text
   in TICKETS.md updated to spell the new path.

2. Delete three docs whose load-bearing content has either been
   absorbed into the codebase or distilled into closed tickets:

   - docs/naming-deferral.md (147 lines) — explained why we
     don't rename claim_lattice → CTI/PROMETHEUS-Σ. Decision
     stays in place; the rationale is no longer worth a
     dedicated doc. Inline citations removed from
     cti-architecture.md (4 refs), warrant.py, ticket-000003
     (closed-ticket internal ref).
   - docs/reference-frame-failure-class.md (169 lines) — Orwell
     case journal that motivated the phrase-pattern retrieval
     route. The route shipped; the analysis is now duplicate
     with the closed Ticket #000002. Inline citation removed
     from CLAUDE.md retrieval pipeline section + frame.py.
   - docs/test-coverage-audit-2026-05-01.md (46 lines) —
     point-in-time audit checking 16/16 of fox's §11 list. Tests
     themselves live in tests/; the audit was a one-shot
     checkmark exercise.

References updated:
  CLAUDE.md, aborist/qa/frame.py, aborist/qa/retrieval_plan.py,
  aborist/qa/warrant.py, docs/cti-architecture.md, docs/TICKETS.md,
  docs/tickets/ticket-000003 + ticket-000004 (internal links).

Net: -362 lines + tickets/ subdir. 751/34 tests still pass.
2026-05-02 12:51:09 -04:00
ecc18ea724
bench: emergent stress-test — 3-word triangulation (blue-moon cadence)
scripts/bench_emergent.py + make bench-emergent + design doc.
Random word triangulation surfaces failure modes the curated
bench/qa_questions.txt doesn't reach.

Loop:
  /usr/share/dict/words → random.sample(3) →
  Hermes @ temp=0.8 weaves a creative question →
  aborist student answers via query() →
  append journey to bench/emergent_log.jsonl
  (teacher review = separate manual step, fox brings entries to
   Opus & gets judgment to append)

Word filter: ^[a-z]{5,12}$ after lowercasing. Skips short words
(too vague) + very long words (Hermes can't weave them).

Cadence: NOT every commit. ~20s per cycle (Hermes generator +
aborist student); N=10 ≈ 4 min, N=50 ≈ 17 min. Most cycles land
UNGROUNDED-by-corpus-design (random triplets rarely overlap with
2010-11 Wikipedia coverage); the interesting cases are STRICT/
HYBRID surprises and the verifier-disagreement cases the teacher
catches.

Teacher review is intentionally out of the bench script:

  - separation of concerns: generation is automated, judgment is
    contextual & needs the corpus-knowledge frame ("is this a
    2010 Wikipedia gap or a substrate failure?")
  - future flexibility: today the teacher is Claude Opus 4.7
    in this conversation; tomorrow GPT-5 or a review committee.
    Swapping teachers is a workflow change, not a code change.

Teacher output schema (appended to the same JSONL line):
  teacher.match           bool
  teacher.audit_agreement agree|disagree|unsure
  teacher.novelty_class   known_truth_grounding | emergent_synthesis
                          | novel_claim | no_signal
  teacher.score_0_5       0..5
  teacher.bench_max_signal retrieval | warrant | prompt | nil
  teacher.reasoning       one sentence
  teacher.reviewed_by     model id
  teacher.reviewed_ts     unix ts

Smoke verified (N=2, seed=42): 41s wall-clock, both UNGROUNDED
(expected — random triplets rarely overlap 2010 Wikipedia).
Append-only log seeded with the smoke entries.

Future flag (not yet wired): --generator-endpoint &
--student-endpoint to swap LLM upstreams per role.

Full design + teacher protocol: docs/bench-emergent-design.md.
2026-05-02 12:42:56 -04:00
39c3652e0a
docs: consolidate self-reference design — flat MVP + fact-Core distillation
The two self-reference design docs were a sequential pair:

  self-reference-thought-chains-design.md  (96 lines, v1 MVP that
                                            shipped via 8de0044)
  self-reference-distillation-design.md    (208 lines, v2 fact-Core
                                            successor, design-only)

v2 explicitly named v1 as 'flat MVP' that 'falls short' and itself
as 'successor' — they're one story told in two files. Merge into

  docs/self-reference-design.md  (230 lines)

Structure: §1 problem statement, §2 v1 flat MVP (shipped),
§3 v2 fact-Core distillation (proposal), §4 v2 implementation
plan, §5 deliberate non-goals, §6 bench impact, §7 architectural
payoff. One narrative; the v1/v2 demarcation stays clear.

Net: 304 → 230 lines (-74), one file instead of two.

References updated in:
  CLAUDE.md, aborist/cli.py, aborist/qa/query.py,
  aborist/qa/runner.py, aborist/sources/providence.py,
  docs/TICKETS.md, docs/cti-architecture.md (3 refs),
  tests/test_providence_source.py

10/10 test_providence_source tests still pass.
2026-05-02 12:39:42 -04:00
2bbd12a270
docs(bench): rename qa-modes-bench-2026-05-02.md → qa-modes-bench.md
The 2026-05-02 journal is the LIVING bench doc — it absorbs each
day's bench results and rolls forward. Pinning a date in the
filename made it look like a frozen snapshot when it is in fact
the working journal.

Naming pattern moving forward:

  docs/qa-modes-bench.md             — living journal (latest run)
  docs/qa-modes-bench-2026-04-30.md  — historical snapshot
                                       (frozen for the JSON-mode
                                       hardening day)

Future dated snapshots stay dated. The bare 'qa-modes-bench.md'
is always the current state of the substrate.

Updated references in:
  - CLAUDE.md (× 2)
  - aborist/qa/prompts.py
  - aborist/qa/query.py
  - docs/bench-maxing.md

The 2026-04-30 references in docs/cti-architecture.md,
docs/test-coverage-audit-2026-05-01.md, docs/verifier-semantic-gap-design.md,
docs/TICKETS.md correctly point at the historical snapshot and
stay as-is.
2026-05-02 12:27:38 -04:00
238f5b0470
docs(bench): consolidate 2026-05-02 journal into single narrative
The journal had two stapled sections — the morning 11:31Z bench
(n=2 cell-grouped) followed by an 'Update' divider and the
afternoon 15:07Z bench (n=3 sample-shuffled). Two parts that
made readers walk past a horizontal rule to compare numbers, and
duplicated the directive-coverage / outputs / per-mode-rec
sections.

Rewrote as one coherent narrative:

  - Frontmatter mentions both stamps in one table (when/what/wall)
  - 'Hardening' section walks the chronology in two beats:
    * pre-11:31Z (Rule 8, warrant gen, ladder, etc.)
    * 11:31Z → 15:07Z (Sprint 1b, Sprint 2, DRY, keep-alive,
      sample-shuffle, --resume, concurrency-sweep, surrogate v2)
  - 'Aggregate' = authoritative 15:07Z table
  - 'Δ across the day' = 3-column comparison
    (2026-04-30 → 11:31Z → 15:07Z) with net deltas
  - 'Per-bucket strict-rate' = 15:07Z bucket data
  - 'Recommended context budget' = final
  - 'Pointer-mode signal' kept (lazy-anchor analysis still valid)
  - 'Wall-clock & throughput evolution' = both benches in one
    table + concurrency sweep table
  - 'Errors' = surrogate story across both benches
  - 'Verdict' = final recommendation
  - 'Outputs' = both jsonl files

212 lines → 175 lines (-36). One source of truth, one read.
2026-05-02 12:13:27 -04:00
a0e916ee1b
docs: bench-maxing speed playbook + README harness section
bench-maxing.md gains a 'Bench harness — the speed playbook'
section capturing the 2026-05-02 speed wins as durable doctrine:

  - Sample-level shuffled scheduling vs cell-grouped (+58%
    throughput, true i.i.d. variance for n>=3)
  - Persistent httpx client (TLS handshake amortization)
  - Concurrency tuning (vLLM peak at c=3-4, brutal past c=4)
  - Per-mode max_context_chars from bench feedback (the bench
    is the substrate's voice; let it drive policy)
  - --seed for reproducibility
  - --resume for stop/start-able bench
  - Smoke fixture for inner-loop iteration
  - pytest -n auto (3.6× speedup on test suite)

README:
  - Updated whitepaper section refs (§6/7/8/9/13) — old refs
    pointed at §13.4.11/13.8/13.9 which no longer exist after
    the whitepaper rewrite landed.
  - 'Tests' section renamed 'Tests & bench' with make targets
    for bench-qa, bench-qa-smoke, test-live. Resume + concurrency
    semantics surfaced.
2026-05-02 12:05:30 -04:00
4763e5ed53
bench: 15:07Z journal — sample-shuffle results + 13.3 tasks/min
The c=4 sample-shuffled bench at 15:07Z lands the post-Sprint-1b
+ post-Sprint-2 + post-DRY + post-keep-alive + post-shuffle
state.

Headlines:

  quote                 0.50 → 0.54  (+4pp)
  claim_lattice_pointer 0.23 → 0.20  (-3pp)
  claim_lattice (JSON)  0.44 → 0.42  (-2pp)

Quote's +4pp is the cleanest lift of the sprint set: the per-mode
24KB cap (Sprint 1b) surfaces tighter retrievals that quote can
ground verbatim, and the bucket data confirms quote peaks at
8-16KB (0.58 strict-rate). JSON's peak migrated to its targeted
32-64KB bucket (0.48 strict-rate, vs 0.38 at 16-32KB) — Sprint
1b's intent confirmed at the per-bucket level even though the
aggregate slipped 2pp.

Pointer's slight drop is consistent with Sprint 2's smoke result
— the chunk-specificity Rule 9 didn't lift Hermes-3-8B's
lazy-anchoring at n=3. The structural fix will need a stronger
intervention than a prompt nudge.

Wall-clock & throughput:

  11:31Z: cell-grouped, c=4, n=2, 426 tasks, 51 min, 8.4/min
  15:07Z: sample-shuffled, c=4, n=3, 639 tasks, 48 min, 13.3/min

Sample-shuffled scheduling delivers +58% throughput at same
concurrency. n=3 (50% more work) ran in 6% LESS wall-clock.
Per-call mean latency dropped 35-42% across all modes — vLLM's
continuous batcher fills better when fed a diverse request
stream instead of cache_key-correlated cells.

Concurrency sweep: c=3 peak, c=4 within 4% (chosen), c=5 12%
slower, c=6 brutal (45% slower). vLLM saturates at c=3-4 on
this endpoint.

Errors: 6, all on 'tell me about the roman empire' question.
Root cause traced & fixed in 41d1d9b (lone UTF-16 surrogates
in Wikipedia chunk content broke httpx's outbound JSON encode
— different path from the 3b91223 SHA-256 hashers fix which
hardened the OUTPUT side). Next bench: 0 errors.
2026-05-02 11:59:54 -04:00
41d1d9b71e
qa(client): scrub lone UTF-16 surrogates before httpx encode
Six bench runs aborted with:

  UnicodeEncodeError: 'utf-8' codec can't encode characters
  in position N-M: surrogates not allowed

The error fires inside httpx's json-encode path: when the
request body's JSON contains lone surrogates (from Wikipedia
chunks ingested with invalid-UTF-8 source bytes), httpx's
.encode('utf-8') raises before the request even leaves the
client.

Earlier surrogate fixes (3b91223) hardened the OUTPUT side —
sha256 hashers now use errors='surrogatepass' so the run-DAG
roots survive surrogate-bearing model output. But the INPUT
side (corpus text injected into the prompt) was still
vulnerable: the LLM never sees the surrogate but the HTTP
client tries to send it.

Fix: scrub message content via WTF-8 → UTF-8-with-replace
roundtrip in OpenAICompatibleClient.chat_completion. Lone
surrogates become U+FFFD (REPLACEMENT CHARACTER); the prompt
serializes cleanly. Verified: 'tell me about the roman empire'
under claim_lattice mode now classifies HYBRID 5/7 instead of
erroring out (this question was 6/6 lattice runs failing on
the 2026-05-02 c=4 bench).

The scrub lives in the client because the hot path needs to
guarantee the outbound HTTP body is valid UTF-8, regardless of
what upstream code injected. Defense-in-depth: ingest-time
sanitization would be cleaner but the existing corpus already
has surrogates baked in, and re-ingest would invalidate every
document_root in 6 GB of shards.

Tests: 751/34 still pass clean in 11s with pytest -n auto.
2026-05-02 11:58:25 -04:00
0177c2d278
cli(query): per-phase timings line in human-readable render
Surfaces where the per-call cost lands. The audit-line `7.5s`
already shows total_ms but it's hidden inline with the verdict
& cache_status. The new `timings:` line (after `capacity:`) breaks
total wall-clock into its components so an operator can see at a
glance whether a slow query was Hermes-bound, search-bound, or
something else.

Format:

    timings: cache 0.00s · search 0.30s · context 0.16s ·
             llm 6.50s · persist 0.20s · **total 7.16s**

Skipped phases (cache=0 on cache-miss; persist=0 on cache-hit;
context=0 when retrieval pre-loaded) are filtered so the line
stays compact. **total** stays bold to match the audit-line
elapsed value already on screen.

JSON mode (`--json`) already exposes `timings` — this just lifts
the same data into the human render.

Tests pass (641 / -n auto / 11s).
2026-05-02 11:49:36 -04:00
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