Enumerates the concrete query-words-share-zero-tokens-with-target-title cases the §2 gate's "semantic-allusion fixtures" must include, as a running list: Orwell→Eastasia (genuine conceptual allusion — the case that justifies the vec layer), "what is a CPU?"→Central processing unit, "what is a GPU?"→Graphics processing unit (abbreviation→expansion subclass — also fixable upstream by a concepts/ synonym edge; bench records which fix closes each row). Field cases 2026-05-13, fox. #000053 fixed the verifier's separate acronym blind spot but not this retrieval gap.
23 KiB
Ticket #000050 — Vec RRF hybrid fusion (the #000039 Phase 2)
Status: open · awaiting go/no-go — gated on (a) a corpus backfill
and (b) a bench measurement (see "Gate" below). Doc-only scaffold;
the design already lives in docs/tickets/ticket-000039-sqlite-vec-optional-backend.md
§4.2 (RRF) + §8 (the measurement gate). #000039 closed 2026-05-12
with Phase 0 (doc) + Phase 1 (arborist/search/vec.py — VecBackend,
chunk_vecs, embed_documents, arborist embed / search --backend vec, [vec] extra, ingest integration, --quant {float32,int8})
landed; this ticket carries the remaining Phase 2.
Opened: 2026-05-12
Scope: Wire VecBackend as a fifth retrieval route in
arborist/qa/query.py, merged with the four existing FTS5 routes
(body BM25, title-LIKE, core-keyword TF-IDF, phrase-pattern) via
Reciprocal Rank Fusion (RRF — Σ_routes 1/(k+rank), k=60),
so the assembled context for a Q&A run draws from both lexical and
semantic candidates. Vec hits stay UNGROUNDED (soft signal, never
proof path); the post-merge filter chain (rivalry exclusion,
four-accept-paths title-relevance, stem-aware token matching,
per-source context cap, Rule-8 title-mismatch) runs on vec hits
exactly as on FTS5 hits. Not a replacement for any FTS5 route —
vec is additive (#000039 §4).
Audience: fox + future blackops shifts + whoever runs the
recall bench.
Hard constraint: vec retrieval never enters the proof path
(hits land audit_mode=UNGROUNDED). The 8-dim cache_key invariant
is untouched — the vec hyperparams fold into governance_policy_hash
(#000039 §6), which Phase 2 must wire (Phase 1 left it noted-only).
RRF (rank-based) is the merge, not raw-score addition — BM25 scores
and cosine distances aren't comparable scales. No change to the four
FTS5 routes' behavior; hybrid is opt-in / governance-gated until the
bench clears it.
1. Why this is a separate ticket
#000039's phased plan: Phase 0 (doc) → Phase 1 (the backend + ingest path) → Phase 2 (hybrid fusion), which opens only when Phase 1 measures ≥ 5pp recall lift on the bench fixtures with no STRICT-rate regression (#000039 §8). Phase 1 shipped 2026-05-11 and #000039 closed 2026-05-12; Phase 2 is real, scoped, and remaining, so it gets its own ticket per the repo convention rather than leaving #000039 open indefinitely.
2. Prerequisites (the gate)
Phase 2 implementation does not open until both:
- A corpus backfill exists — anywhere, not necessarily where
the bench runs.
arborist embed --quant int8over the shard set. Per #000039 §14.6 this is a one-time batch job — hours on an idle box, days on a contended one (~4 chunks/s measured on the dev box; ~50-200/s idle), ~2.4 GB total at int8 (+6 % over the 38 GB shards). The embed pass is ~10–100× heavier than the rest of ingest, so it does not run on a laptop — it runs once on any box with CPU (a cloud node, a Prometheus-Σ unconscious-sweep box — #000037 §3.1) and the resultingchunk_vecsis distributed as a vecpack over mesh so every peer pulls + bulk-loads it (sub-ms per chunk on the receiver). That distribution mechanism is #000051 — this prereq is "a vecpack exists and has been imported on the bench box", not "fox embedded the corpus locally". - A recall bench clears the gate. Run four conditions on the
bench fixtures (the §2a semantic-allusion fixture set + the
bench-emergent shapes + the curated set + the adversarial
semantic-neighbor pack from §3 below): (A) FTS5-only baseline,
(B) vec-only, (C) FTS5
- vec RRF hybrid, (D) FTS5 + vec candidate-union but the
existing reranker only (no RRF). D is the control that
distinguishes "RRF helped" from "more candidates helped" — if C
≈ D, RRF is not earning its complexity. Gate (5pp signal floor per
docs/bench-maxing.md):
- vec-only (B) beats FTS5-only (A) by ≥ 5pp recall@K on ≥ 1 semantic-allusion fixture and surfaces ≥ 1 correct candidate absent from A's top-K. (B is a diagnostic, not required to match A's overall STRICT-rate — vec is allowed to lose on exact title / rare-token shapes; punishing it for not being FTS5 is the wrong test.)
- hybrid (C) lifts the proof-quality-label rate ≥ 5pp over A on the target fixtures (STRICT for quote/span/entity/paraphrase; EVIDENCE-WARRANTED for lattice modes — "the current proof-quality label family", not literally "STRICT") with no statistically meaningful regression on the lexical/title/false-neighbor fixtures and no UNGROUNDED-rate regression.
- C must beat D (or be no worse) — else ship the cheaper union,
not RRF.
If hybrid lift is sub-floor: park here — vec stays opt-in per-call
(
--backend vec), hybrid does NOT become the default, and the ~2.4 GB tax isn't paid for nothing.
- vec RRF hybrid, (D) FTS5 + vec candidate-union but the
existing reranker only (no RRF). D is the control that
distinguishes "RRF helped" from "more candidates helped" — if C
≈ D, RRF is not earning its complexity. Gate (5pp signal floor per
2a. Semantic-allusion fixture set (the cases the §2 bench must include)
The §2 gate's "semantic-allusion fixtures" aren't a vague category — this is the running list of concrete cases where the query's words share zero tokens with the target article's title, so the four FTS5 routes (body BM25, title-LIKE, title-token, phrase-pattern) structurally can't surface it, but a dense-vector route should. The vec-only bench (condition B) must beat FTS5-only (A) by ≥ 5pp recall@K on ≥ 1 of these and surface ≥ 1 correct candidate absent from A's top-K; the hybrid bench (C) must lift the proof-quality-label rate on the set. When a new field case of this shape shows up, add a row here.
| query | target article (title shares 0 tokens with query) | why FTS5 misses it |
|---|---|---|
| "what did Orwell call the country at war with Oceania?" | Nineteen Eighty-Four (→ Eastasia / Eurasia) | query tokens {orwell, country, war, oceania}; the article's distinctive content ("Eastasia", "always been at war with") isn't in the title, and the title shares no token with the query — only the verbatim phrase route ("always been at war") rescues a piece of it today (CLAUDE.md retrieval-pipeline §1) |
| "what is a CPU?" | Central processing unit | query token "CPU" ≠ the title "Central processing unit" — FTS5 pulls the "CPU *" satellites (CPU design, CPU socket, CPU time, CPU cache, CPU-Z, CPU multiplier, CPU disambiguation) where "CPU" is in the title; the canonical article never surfaces. (Field case 2026-05-13, fox. Note: #000053 fixed the verifier's separate acronym blind spot — EVIDENCE-WARRANTED instead of a spurious TITLE_MISMATCH — but the answer is still grounded in "CPU design", not the real article; that's this gap.) |
| "what is a GPU?" | Graphics processing unit | identical shape to CPU — FTS5 returns GPU cluster / GPU (disambiguation) / Molecular modeling on GPU / Evergreen (GPU family) / etc.; observed 0/1 UNGROUNDED because the model recited the (modern) GPU-lead definition and no 2003-dump GPU-* satellite carried that text. (Field case 2026-05-13, fox.) |
(The abbreviation→expansion subclass — CPU↔Central processing unit,
GPU↔Graphics processing unit — is also addressable upstream by a
concepts/ synonym edge, cheaper than vec; both fixes are
complementary and the bench should record which one (or both) closes
each row. The Orwell row is not an abbreviation case — it's genuine
conceptual allusion, the kind only dense vectors reach — so it's the
one that actually justifies the vec layer if the abbreviation cases
turn out to be all concepts/ could-have-fixed.)
3. Implementation sketch (when the gate clears)
RRF merge. arborist/qa/query.py: after the four FTS5 routes
produce ranked hit lists and before the rerank/filter chain, add a
fifth list from VecBackend.search(query, limit=K). Merge all five by
RRF: score(doc) = Σ_route 1/(60 + rank_route(doc)), before the
accept-path filtering. Carry route provenance on each merged hit
(routes: [...], route_ranks: {body_bm25: 14, vec: 2},
rrf_score) — needed for the four-condition bench and for later
failure analysis. The merged ranking then feeds the existing
body-coverage sqrt rerank, title-token boost,
_filter_by_title_relevance, rivalry exclusion, stem-aware matching,
per-source cap, wikitext-base prose — unchanged.
Accept-path-5 (semantic-warrant). This is the load-bearing
addition, not an afterthought: the existing title-relevance gate may
drop exactly the low-title-overlap semantic candidates vec exists to
find (the "what did Orwell call the country at war with Oceania?" →
Nineteen Eighty-Four / Eastasia case — right chunk, zero query/title
token overlap), so without a vec-safe accept path the bench can show
no lift even when vec retrieval works. Add a fifth accept path in
_filter_by_title_relevance that lets a vec hit survive low title
overlap only if it earns a stronger span-level signal — at least
one of: (a) the cited span contains a deterministic anchor from the
question or the generated claim; (b) a claim-lattice pointer resolves
to a span whose anchor-class warrant matches the claim; (c) an
entity/predicate/date/quote warrant passes on the span; (d) a
concept_relations synonym edge bridges a query anchor to a span
anchor; (e) the answer is explicitly exploratory / UNGROUNDED and
does not reach an EVIDENCE-WARRANTED / STRICT label. In short: vec
may bypass title overlap only by earning a stronger warrant, never by
embedding proximity alone. Decide the exact predicate set at
implementation time from the bench, but the principle — "vec hits
clear the gate via warrant strength, not similarity score" — is
non-negotiable.
Mixed fleet. Skip the vec route cleanly when chunk_vecs is
absent/empty on a shard (some shards have an imported vecpack — #000051
— some don't) — falls back to FTS5-only for that shard, with an audit
note vec_route_unavailable so a hybrid-labelled run never silently
ran FTS5-only.
Governance — wire it before any cached hybrid run, not "later".
Fold the six vec config fields — embedder_name,
embedder_version (pinned revision/digest, separate from the name),
quantization (named with its recipe: int8sym =
symmetric-global-scale ×127/clamp/round, not bare int8), dimension,
distance_metric, ann_index — into governance_policy_hash
(#000039 §6), summarized by the compact VEC_BACKEND_VERSION token
(e.g. vec-v1-bge-small-en-v1.5-384int8sym-l2norm-flat; the token is
a derived summary, the six fields are the auditable payload). Until
that wiring lands, a cache-write guard: a QA run whose retrieval
backend is vec/hybrid may return an answer but must not persist a
providence_cache record (status vec_experimental_no_cache) —
because changing the embedder changes the candidates changes the
context changes the answer, and an un-versioned cached hybrid answer is
exactly the silent provenance drift the substrate exists to prevent.
The 8-dim cache_key invariant is untouched (existing dim, not a new
one). The hash must also cover the fusion policy — rrf_k,
vec_top_k, the route set, accept_path_version, the fallback /
coverage policy, the candidate-budget policy — not just the six vec
fields; see §5a.1 (changing rrf_k shifts the answer as much as
changing the embedder).
Run-DAG metadata. A hybrid run's per-run Merkle-DAG records the vec
retrieval stage: route="vec", VEC_BACKEND_VERSION, the six config
fields, top_k, a query_embedding_hash, and the candidate
(chunk_id, quantized_distance) list (or a candidate_root over
them). governance_policy_hash says which regime was allowed; the
run-DAG says what actually happened this run — both must be visible
for replay (same lesson as the --retrieval-keywords audit gap,
#000001).
The knob. arborist query / the bench harness: a
--retrieval-backend {fts5,hybrid} knob (default fts5 until the
bench clears hybrid, then flipping the default is a fox call + a
governance_policy_hash bump). No vec-only QA path except
explicitly-experimental (vec-only is a retrieval diagnostic, not an
answer regime).
Adversarial semantic-neighbor fixtures. Vec opens a new failure class: a chunk that is semantically near the query but factually wrong — same topic / wrong entity, same title family / wrong franchise installment, same era / wrong date, a paraphrase that retrieves the background source instead of the primary one (the "Mr. Burns" / "which Back to the Future" shape, now reachable through a semantic side door). Build a small adversarial pack for the §2 bench whose pass condition is: hybrid must not raise the rate of confidently-wrong warranted answers, and the title-relevance / Rule-8 hard checks must still catch the mismatched vec hits. This pack is part of the gate, not a nice-to-have.
4. Out of scope
- The corpus backfill itself (a batch op — #000039 §14.6) and its distribution (vecpack export/import + mesh payload — #000051). Prereq #1 above is "a vecpack exists and is imported on the bench box", which #000051 delivers.
- A GPU/accelerated embedder (drop-in via
default_embedder()'s pluggableEmbeddercallable; orthogonal — #000039 §14.6). - int8 vs float32 default (settled: int8 is the production config
per #000039's head-to-head; switching the v1 default is a
separate
VEC_BACKEND_VERSIONbump, not Phase 2's call). - binary quantization + two-stage re-rank (#000039 §3.1's storage-emergency option; its own future ticket if corpus scale ever makes +0.8 % vs +6 % matter).
5. Acceptance criteria
arborist/qa/query.pymerges a fifth (vec) route via RRF (with route provenance on each hit), behind a--retrieval-backend {fts5,hybrid}knob; FTS5-only behavior byte-unchanged when the knob saysfts5orchunk_vecsis absent (mixed-fleet fallback emits thevec_route_unavailableaudit note).- Accept-path-5 lands in
_filter_by_title_relevance: a low-title- overlap vec hit survives only via a stronger span-level warrant (anchor / pointer-warrant / entity-predicate-date-quote / concept-synonym / explicitly-UNGROUNDED), never via similarity score alone. - The six vec config fields fold into
governance_policy_hash, summarized byVEC_BACKEND_VERSION(recipe-named quant, e.g.int8sym); and a cache-write guard blocksprovidence_cachepersistence forvec/hybridruns until that wiring is live (statusvec_experimental_no_cache). - A hybrid run's run-DAG records the vec retrieval stage (backend
version, six fields,
top_k, query-embedding hash, candidate chunk_ids + quantized distances /candidate_root). - Bench data recorded for all four conditions (A FTS5-only / B
vec-only / C RRF hybrid / D candidate-union-no-RRF) on the
semantic-allusion + curated + adversarial-neighbor fixtures,
showing the §2 gate cleared — including C-beats-D — (or, if it
didn't, this ticket parks with the result documented and
hybridleft non-default; if C ≈ D, ship the union, drop RRF). - Tests: RRF math + route provenance; vec-absent fallback + the audit note; accept-path-5 (a known semantic-allusion hit survives low title overlap only with a warrant, a bare-similarity hit does not); cache-write guard fires for un-versioned hybrid; filter chain + Rule-8 still reject adversarial semantic-neighbor hits; a bench-row in the QA-modes journal.
5a. Review amendments — 2026-05-13 (Dav1d de-novo review)
Fold into the Phase-2 spec before code. None change the architecture
(vec additive, RRF rank-based, soft/UNGROUNDED, FTS5 default,
8-dim cache-key untouched) — they close measurement and provenance
gaps.
-
governance_policy_hashmust cover the fusion policy, not just the six vec-config fields. Hybrid answer context depends just as much onretrieval_backend(fts5|hybrid),vec_top_k,rrf_k(default 60), thehybrid_route_set(which five routes),accept_path_version(the title-relevance / semantic-warrant gate version), thefallback_policy(what happens whenchunk_vecsis absent), and thecandidate_budget_policy(final top-K × per-source cap interaction). All of these + the six vec-config fields fold intogovernance_policy_hash. Still no ninth cache-key dimension — it all rides the existinggovernance_policy_hashdim. Serialize as canonical JSON (sorted keys, explicit scalar types, recipe-named quant, stable version token) → sha256 → the hash. (π* formalization asvec_config@v1is later, not a blocker.) -
RRF must survive the post-merge rerank — else the bench tests the old reranker, not RRF. If the existing body-coverage
sqrtrerank + title-token boost fully re-sort the candidate list after RRF, the fused order is erased and "RRF failed" is unmeasurable. Implementation rule (Option A): the normalized RRF score becomes one feature in the existing reranker —final_score = norm_rrf_score + bounded_body_coverage_boost + bounded_title_boost − hard_penalties— with the boosts bounded so they adjust within the RRF prior, not overwrite it. Hard filters still drop candidates. Addtest_rrf_signal_survives_post_merge_rerank. Deterministic tie-break:sort key = (−rrf_score, source_role_priority, source_root_hex, chunk_id)(exact key doesn't matter; stable + documented does) —test_rrf_tie_break_is_deterministic. -
Split accept-path-5 into 5a vs 5b. 5a — semantic-warrant survival: a low-title-overlap vec hit that earns a stronger span-level warrant (span-contains-anchor / claim-pointer-warrant / entity-predicate-date-quote / concept-synonym-bridge) may enter normal context and later become evidence-warranted. 5b — exploratory-
UNGROUNDEDsurvival: a hit admitted only because the answer is explicitly exploratory may enter context only under the exploratory label and is hard-excluded from verified-evidence counting — it can never support a STRICT / EVIDENCE-WARRANTED output. Don't merge these into one path: an "exploratory" route must not become a back-door bridge into warranted answer context. Tests:test_accept_path_5_warranted_span_survives_low_title_overlap,test_accept_path_5_similarity_only_does_not_survive_low_title_overlap,test_exploratory_ungrounded_vec_candidate_cannot_increment_verified_count. -
Mixed-fleet coverage is explicit, not silent. When a hybrid run spans shards where some have
chunk_vecsand some don't, the run-DAG recordsvec_coverage = {shards_total, shards_with_vec, shards_without_vec, unavailable_shards:[…], coverage_mode: full|partial}. Bench-validity rule: a hybrid bench result is valid only at the declared coverage threshold — ideally full coverage on the benchmark shards. Production rule: a hybrid-labelled answer's audit metadata states full-vs-partial coverage. Else an operator thinks they ran hybrid while most shards silently fell back to FTS5. Tests:test_vec_absent_on_one_shard_records_partial_coverage,test_hybrid_label_does_not_silently_mean_fts5_only. -
Bench needs exact denominators + a row schema. Per-query bench row:
{query_id, condition: fts5|vec|rrf_hybrid|union_no_rrf, retrieval_backend_version, vec_coverage_mode, top_k, proof_quality_label, primary_answer_source_rank, used_sources, wrong_warrant: bool, ungrounded_output: bool, latency_ms, context_chars}. Derived:target_fixture_lift = rate(C good) − rate(A good);lexical_regression = rate(C good | lexical) − rate(A good | lexical);semantic_false_neighbor_rate = rate(confident-wrong-warranted | adversarial-neighbor);ungrounded_regression = rate(C UNGROUNDED out) − rate(A UNGROUNDED out). "Good label" by mode: quote/span/entity/paraphrase → STRICT or EVIDENCE-WARRANTED; lattice → EVIDENCE-WARRANTED (or the current top verified label); exploratory mode → excluded from proof-quality lift entirely. -
Consume #000051 metadata in the run-DAG (audit/replay, never proof):
{vecpack_id, vecpack_manifest_root, vecpack_trust_tier, vecpack_import_event}. If a hybrid answer rested on gossiped embeddings, replay needs to know which vecpack supplied them — and #000050's cache-policy decides which trust tiers (#000051 §6a.4) a cached hybrid answer may rest on (Tier 2+ once governance-hash wiring is live; Tier 0/1 → raw search / experimental only). -
Implementation order — governance + cache guard before any RRF code can persist. (1)
governance_policy_hashwiring for vec + fusion policy; (2) cache-write guard forvec/hybridwhen the policy hash is incomplete; (3) mixed-fleet fallback +vec_coveragemetadata; (4) run-DAG vec-stage metadata; (5) RRF merge as the fifth route behind--retrieval-backend hybrid; (6) accept-path-5a; (7) accept-path-5b (if still needed after 5a); (8) four-condition bench harness rows; (9) adversarial semantic-neighbor fixture pack; (10) the C-vs-A-vs-D decision (keep RRF / ship union / keep vec opt-in). -
Test additions (append to §5.6):
test_hybrid_governance_hash_includes_rrf_k_and_vec_top_k,test_hybrid_cache_write_blocked_without_vec_policy_hash,test_rrf_score_does_not_use_raw_bm25_or_raw_distance,test_rrf_tie_break_is_deterministic,test_rrf_signal_survives_post_merge_rerank,test_vec_absent_on_one_shard_records_partial_coverage,test_hybrid_label_does_not_silently_mean_fts5_only,test_accept_path_5_warranted_span_survives_low_title_overlap,test_accept_path_5_similarity_only_does_not_survive_low_title_overlap,test_exploratory_ungrounded_vec_candidate_cannot_increment_verified_count,test_rule8_rejects_wrong_franchise_semantic_neighbor,test_candidate_union_control_written_to_bench_journal.
6. References
- #000039 — the parent (Phase 0 + Phase 1, closed 2026-05-12); §4 (additive not replacement), §4.2 (RRF design), §8 (the gate), §14 (ingest integration), §14.6 (embed throughput).
- #000051 — federated vecpack distribution. Supplies prereq #1
(a corpus backfill) as a distributable artifact — backfill once
on any CPU box, gossip the
chunk_vecsover mesh — so the laptop never embeds. Read it for why vec data is safe to gossip with a cheap structural gate (soft data, never proof path). docs/bench-maxing.md— the 5pp signal floor + bench discipline.docs/benchmarks.md— bench harness orientation.- #000037 §3.1 — the Prometheus-Σ unconscious-sweep task that's the natural home for the embed backfill and the vecpack producer.
- CLAUDE.md "Retrieval pipeline" — the four FTS5 routes vec joins; "soft hash vs hard hash" — why vec stays out of the proof path.