test_concepts_extract.py grew 20 -> 28 with the acronym_parens
extractor's 8 tests; the AUTOCOUNT discipline (CLAUDE.md) fires the
doc-counts regression on stale numeric claims.
`arborist/concepts/extract.py:acronym_parens_synonym` — new
corpus-agnostic extractor. Scans each doc's lead chunk (first 4000
chars) for `<Multi-Word Phrase> (ACRO)` where the all-caps acronym's
letters strictly match the content-word initials of the phrase, in
order, after function-word filtering. Emits bidirectional synonym
edges between the lowercased acronym and each ≥3-char content token
of the phrase, evidence_kind="acronym_parens", anchored to that doc's
document_root. Idempotent like link_reciprocity_synonym.
Why this complements link_reciprocity: Wikipedia represents
abbreviation→expansion as a one-way *redirect* (CPU →
Central processing unit), which the ingest does not record as an
edge — so the existing reciprocal-link extractor never learned the
relation. The relation IS in body text by near-universal convention
("Central processing unit (CPU) is..."), which this extractor reads.
Corpus-agnostic: HTML, blogs, textbooks benefit equally.
Conservative: strict 1:1 acronym-to-atom match (rejects HTTP-shape,
where letters land mid-word), function words filtered, repeated
definitions deduped per doc, ≥3-char target floor. 8 new tests
covering CPU bidirectional emit, RAM idempotency, FBI function-word
filter, HTTP length-mismatch reject, XYZ initial-mismatch reject,
ROM hyphenated-word handling, per-doc dedupe, registry presence.
Retrieval-side only — synonym edges reshape FTS5 candidate selection
via synonym_expand at query time, never enter audit_mode / cache_key
/ audit_event_hash. No governance hash bump, no cache invalidation.
Closes#000050 §2a's CPU/GPU abbreviation rows *upstream* of vec;
the Orwell-shape conceptual-allusion row remains the genuine #000050
justification. Operational follow-up (not code): run on each shard
via `arborist concepts derive --extractor acronym_parens` (CLI
surface itself is aspirational in docstrings; extractors are called
programmatically today). Next ID 000054 -> 000055.
Built bench/fixtures/5f/recombination-realcontext-v1.jsonl: 25 hand-built
recombination claims (deliberate re-attributions within real bench-qa
30 KB Wikipedia haystacks the pipeline produced — Mount Kenya into a
Kilimanjaro answer, Galileo onto all 63 Jupiter moons, Joshua Reynolds
onto the Mona Lisa, etc.). Ran the full 7-model × full-grid sweep vs
the 808 pooled STRICT. Result:
- bart-large-mnli / k=12 / max / θc=0.999 → 12/25 = 0.48 catch · 0/808 FP
(the real-haystack fp=0 leader)
- deberta-large-mnli / k=3 / margin / θc=0.995 → 6/25 = 0.24 (§7 #26's
'settled' config — 28/28 synthetic, 0.24 real-haystack: 4× over-estimate)
- roberta-large 0.12, MiniLM 0.08, deberta-base 0.04
So §7 #26's 'boundary closed' walks back to 'boundary PARTIALLY closed'
on real haystacks. The bottleneck is architectural: top-k by token
overlap misses the contradicting clause when it shares few subject-area
tokens with the answer (e.g. the Mount Kenya clause only shares 'Kenya'
with a Kilimanjaro claim — ranked low, NLI never sees it). Threshold
tuning doesn't lift the ceiling; a SEMANTIC candidate selector
(vec-driven, sibling of #000050/#000051's hybrid retrieval) does.
bart's pareto above fp=0: fp=0.011 catch=0.52, fp=0.057 catch=0.84,
fp=0.068 catch=0.92 — permissive operating points are on the menu if
fox+dav1d sign off. recommended_operating_point updated to
bart-large-mnli/k=12/max/θc=0.999; deberta-large/margin kept as the
synthetic-eval reference. Sixth meta-lesson instance: clean synthetic
eval doesn't predict bench-qa precision OR recall — neither contrived
dataset axis is load-bearing, only the real pipeline shape is.
Production verifier unchanged; falsification-hard stays 10/12. Still
SHADOW; runtime promotion fox+dav1d-decides.
(a) Mined the pooled n=1+3+5 bench-qa runs (808 distinct STRICT answers)
for natural recombinations at lowered θc≥0.7 → 37 would-fires, ALL
token-collision FPs on inspection (Mount Kenya pulled into a Kilimanjaro
answer, Dalí into da Vinci, Donovan into Superman). ZERO genuine
recombination errors — the boundary is theoretical-in-practice; the
failure mode is the candidate selector (top-k by token overlap) pulling
different-entity same-subject-area clauses.
(b) Re-ran the grid against the 808-cell pooled STRICT set: the §7 #25
'max@0.96' was itself a small-sample artifact — the n=5 444-cell set
lacked the high-confidence spurious hits the pooled set has. On 808
cells θc goes back to ~0.995, and at θc=0.995 only agg=margin still
catches 28/28 (max gets 27/28). microsoft/deberta-large-mnli / k=3 /
agg=margin / θc=0.995 → 28/28 synthetic recombinations · 0/808 pooled
real STRICT FP · 0/26 synthetic legit — the ONLY config in the
7-model×full-grid sweep that hits 1.0/0.0 on 808 cells, held at n=3
too. recommended_operating_point reverted to margin@0.995.
Realistic next check: ~20-30 hand-built synthetic-recombination-vs-
real-bench-qa-context fixtures (real haystack, deliberate re-attribution).
Still SHADOW; runtime promotion fox+dav1d-decides. Production verifier
unchanged; falsification-hard stays 10/12.
Meta-lesson instance five: a bigger sample can vindicate a config a
smaller one made look unnecessary — re-confirm the config choice (not
just the threshold) each time the denominator grows.
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.
ARBORIST_NLI_SHADOW=1 make bench-qa BENCH_QA_N=5 → 1125 cells, 444 real
STRICT (5x n=1, 1.6x n=3). Re-ran the 7-model mega-grid: the lexical-
candidate NLI veto robustly clears the §7 #12 gate with
microsoft/deberta-large-mnli / k=2 / agg=max / guard=max_entail /
θc≈0.96 / θe=0.9 → 28/28 synthetic recombinations (incl. both fixtures)
· 0/444 real STRICT FP · 0/26 synthetic legit FP, ~4 pts θc headroom;
roberta-large-mnli equally good (k=2/max/θc=0.95). Resolved: agg=max +
max_entail guard is the robust score-shape (§7 #24's margin win was a
sample tie); the large checkpoints (~350-400M deberta-large-mnli /
roberta-large-mnli) hit 1.0/0.0, bart-large (similar size) only ~0.71,
deberta-base at the cliff (27/28 here, 11/28 at n=3), small models
(MiniLM-82M, deberta-v3-small) cap at ~0.82 — so §7 #18's 'MiniLM is
the cost-pick' is OVERTURNED by the proper-n evidence; k=2 consistent
winner; int8-ONNX costs ~1 catch. recommended_operating_point updated.
Remaining: a bench-qa-derived recombination set (the one check not
done); n=9 if dav1d wants more; still SHADOW; runtime promotion is
fox+dav1d-decides. Production verifier unchanged; falsification-hard
stays 10/12. This run is the worked example behind CLAUDE.md's new
bench-maxing line.
`arborist.qa.evidence._content_tokens` dropped every token under 4
chars, so a short all-caps acronym (CPU, GPU, DNA, FBI, USB…) never
registered as a content token — which defeated Rule 8
(_claim_title_overlap / TITLE_MISMATCH), the subject-tokens-absent
check (Rule 9), the bare-name-claim guard, and spotlight-excerpt token
selection whenever a question/claim's topic IS an acronym. The field
case: `what is a CPU?` cited to the "CPU design" article tripped
TITLE_MISMATCH even though claim and title both contain "CPU".
Fix: keep a token if it's an all-caps 2-3-char alpha run in the source
text; everything else unchanged. The change only ever ADDS tokens, so
TITLE_MISMATCH / SUBJECT_TOKENS_ABSENT / BARE_NAME_CLAIM can only stop
firing, never start — monotone toward fewer spurious demotes; no
STRICT→non-STRICT transition is possible from it.
Versioned: `content_token_rules: "v2-acronym-aware"` added to
runner.DEFAULT_POLICY + query.DEFAULT_QUERY_POLICY +
keys._VERIFIER_POLICY_FIELDS → folds into verifier_policy_hash, prior
cache records orphan on lookup (by design; same discipline as
base_version / hyphen_fold_v1). Does NOT touch the retrieval
abbreviation→expansion gap (CPU→Central processing unit — #000050 vec
hybrid / concepts/ synonym edges; the root cause of the satellite
retrieval). 8 new tests; full suite green (2502); bench-qa-smoke clean.
Next ID 000053 -> 000054.
ARBORIST_NLI_SHADOW=1 make bench-qa BENCH_QA_N=3 → 275 real STRICT cells
(3x the n=1 sample). Re-ran the expanded grid (7 aggregations incl.
margin = max_clause(p_contra - p_entail), paired-entail guard variant,
θc to 0.999, --extra-models) over all manifest models + 4 extra
xsmall→large (microsoft/deberta-large-mnli, roberta-large-mnli,
nli-deberta-v3-small, deberta-v3-xsmall), synth-28 recombination vs the
275 STRICT cells. Result: the §7 #23 deberta-base/k=2/θc=0.99 config
does NOT survive — it catches only 11/28 at θc=0.995 (which the larger
STRICT sample forces). BUT the broader sweep found the config that does:
microsoft/deberta-large-mnli / k=3 / agg=margin / θc=0.995 → 28/28
synthetic recombinations (incl. both fixtures) + 8/12 falsification-hard,
0/275 real STRICT FP, 0/26 synthetic legit FP — a passing config at
proper n. Findings: margin is the right score-shape (single threshold,
folds the guard in); the specific checkpoint matters more than param
count (deberta-large-mnli wins clean, deberta-base collapses,
roberta/bart ~0.71-0.75 — no 'bigger is better' law). recommended_operating_point
updated. Still SHADOW; runtime promotion needs a bigger STRICT sample +
a bigger recombination set + fox/dav1d sign-off. Production verifier
unchanged; falsification-hard stays 10/12.
Meta-lesson sharpened twice: clean eval ≠ bench-qa precision (§7 #18→#20),
default config ≠ best config (§7 #22→#23), small FP sample ≠ large FP
rate (§7 #23→#24).
The §7 #22 'fails the gate' was the verdict for the DEFAULT config
(k=6/θc=0.5/θe=0.9, tuned on the clean synthetic set), not the
approach. A {model × candidate-cap k × aggregation × θc × θe} grid
sweep (bench/scripts/nli_shadow_grid.py — NLI runs once per
(model,record) over the top-12 candidate clauses, the k/agg/θ grid is
then arithmetic on cached scores; ~10s on the 4090 for 4 models) finds
clean passing configs: on the 89 real STRICT cells (n=1 bench-qa),
deberta-base-184M / k=2 / agg=max / θc=0.99 / θe=0.9 → 27/28 synthetic
recombinations caught (incl. both 5f-fal-hard fixtures), 0/89 STRICT
FP. MiniLM-82M passes too (24/28 · 0/89). Model science: 184M > 82M >
407M for fp=0 recombination recall; int8-ONNX costs ~1 catch vs fp32.
Caveats: FP side is n=1 (BENCH_QA_N=3 run in flight); recall is on the
synthetic set; flipping to a runtime demotion-only veto is fox-decides
(then nli_policy_hash folds into governance_policy_hash per §7 #2).
Manifest active defaults stay k=6/θc=0.5; recommended_operating_point
(deberta-base, k=2, agg=max, θc=0.99, θe=0.9) documented in the
manifest. 3 grid result JSONs committed. Standing lesson: neither the
clean synthetic eval NOR the default config predicts bench-qa precision
— you have to sweep. Production verifier unchanged; falsification-hard
stays 10/12.
First grid (MiniLM, n=1 bench-qa 89 STRICT cells): the §7 #22 'fails the
gate' verdict was config-specific (defaults k=6/θc=0.5/θe=0.9) — the grid
finds k=1/θc=0.93/θe=0.5 → catch 8/12 falsification-hard (incl. BOTH
recombination fixtures hard-003 Mercury + hard-005 Einstein), 0/89 STRICT
FP, 0/26 synthetic-legit FP; 18/28 synthetic recombination. So the
lexical-candidate approach is NOT a dead end. (k=1 = no haystack;
θe=0.5 tighter than the clean-set 0.9.) Full multi-model + n=3
characterization next.
bootstrap-nli-only: a venv + [nli] only — no [dev] extras (no crawler/
hessian/vec/sympy). On a CUDA host PyPI's torch wheel is the CUDA build,
so ShadowNLI auto-detects cuda and bench-nli-shadow / export-nli-onnx /
the candidate bench all run on the GPU with no further wiring. Verified
on the ai box (RTX 4090): torch 2.11+cu130, cuda True; 82M cross-encoder
batched ≈ 0.09 ms/pair (512 pairs in 0.047s — ~350x onnx-int8-cpu,
~1300x torch-cpu-batch1); 62-record synthetic shadow sweep in 2.4s wall;
24/24 tests pass. The GPU only accelerates the NLI half — make bench-qa
(Hermes LLM) still runs wherever the shard corpus is.
Speedup (§3 plan): ShadowNLI._nli_batch batches forwards
(ARBORIST_NLI_BATCH=64); device auto-detect (ARBORIST_NLI_DEVICE, else
cuda-if-available); auto-prefer an ONNX export — bench/scripts/export_nli_onnx.py
/ make export-nli-onnx exports + int8-dynamic-quantizes the pinned
checkpoint into ~/.arborist/models/nli/<ver>/onnx/ (operator state, NOT
committed), _ensure_loaded loads model_quantized.onnx via
optimum.onnxruntime (backend onnx-int8), falls back to torch silently.
torch-cpu-batch1 ~120ms/pair → onnx-int8-cpu-batched ~32ms/pair (~4x);
seconds on a 4090. optimum[onnxruntime] added to the [nli] extra; 24
tests.
Gate-item-4 verdict at proper n: ARBORIST_NLI_SHADOW=1 make bench-qa
BENCH_QA_N=1 → 223 cells (89 STRICT / 90 HYBRID / 44 UNGROUNDED; also
surfaced + fixed a lone-surrogate bug). Shadow sweep over those: NLI-as-
runtime-veto on STRICT has ~26% FP at θc 0.5, ~8% at θc 0.90, ~0% only
at θc 0.99 — and θc 0.99 gives up most recombination recall (hard
synthetic recombinations bottom out ~0.76). FAILS the §7 #12 gate on
this design. Only untried path that might pass: a Phase-3 runtime hook
running NLI on the verifier's actual matched clauses (1-3), not
top-6-by-overlap. Until then: runtime NLI demotion stays off; the 2
fixtures stay permanent boundary markers; θc stays 0.5. Production
verifier unchanged; falsification-hard stays 10/12.
Per-sentence shape check (no model) emitting kind ∈
{phrase_component_reuse, circular, vacuous, ok, empty}:
- circular: subject content-tokens ⊆ predicate's and the predicate
leads with a subject token ("Water is water").
- phrase_component_reuse: subject quotes a phrase, predicate reuses
one of that phrase's own tokens as a bare "the/a/an <token>"
referent — the 2026-05-12 field case ("the phrase 'Zionist entity'
is used as the entity"), a token collision the verifier +
deflection + title-relevance all pass and NLI returns neutral on.
Copulas inside a quoted span are skipped so 'war is peace' doesn't
break the subject/predicate split.
- vacuous: predicate is only placeholder hypernyms + filler ("X is
a thing").
Conservative — no full token-salad parsing; legit definitions pass ok.
Surfaced in inspect_cache_key + the `arborist inspect` human view
(· incoherent: <kind>). Advisory only — never writes providence_cache
/ audit_events / run_dag_root; demote-only verifier hook deliberately
not wired. 9 tests; full suite green (2500 passed).
ARBORIST_NLI_SHADOW=1 carries the raw verifier-input text into bench
rows; real Wikipedia context occasionally has U+D800–U+DFFF code points
(mangled source encoding) that json.dumps(..., ensure_ascii=False) then
refuses to UTF-8-encode → the run died at row 224/225. Scrub via
encode('utf-8','replace').decode() — U+FFFD is fine for a measurement
field. Only the two new shadow fields are touched.
Doc-only scaffold. Two more read-only/demote-only/never-in-proof-path
sidecars joining the diagnose_deflection family: (1) diagnose_coherence
— word-salad/circular/vacuous answers; lexical, no model; the near-term
win. (2) diagnose_relevance — semantic 'aboutness' (does the answer
address the question / is each claim about its cited source?); today's
checks are lexical and a token collision defeats them; a small
aboutness/reranker model (NOT NLI) under #000049 §7's discipline cage
verbatim; gated on evidence, travels with #000049's model question.
Motivating field case: the 'Zionist entity' claim_lattice query —
incoherent token-collision recombination NLI can't catch (returns
neutral) and both lexical relevance checks waved through. Flags an
upstream retrieval (polysemy/title-soup) root-cause ticket, not scoped
here. Next ID 000052 → 000053. #000049 sibling
candidate_clauses() — NLI now runs only on the top-N source clauses by
content-token overlap with the answer claim (max_candidate_clauses=6),
not the whole context; records n_candidate_clauses / best_clause_overlap
/ recombination_risk. Synthetic sweep unchanged (28/28 recombination,
0/26 legit FP, mean 1.45 candidate clauses/record). Real-traffic smoke
re-run: STRICT would-demote 30% → 20%, overall 47% → 33% — better, not
fixed; recombination-risk split doesn't separate either. Residual STRICT
false-contras at ~0.83-0.92 → θc would need ≈ 0.90 (vs the clean-set
0.5); at θc=0.90 the data in hand gives 27/28 synthetic recall, 0/26
legit FP, 0/10 smoke STRICT FP — but n=10 is too small to set on.
Next: a fuller ARBORIST_NLI_SHADOW=1 bench-qa run → sweep θc on hundreds
of STRICT cells → confirm → set it. θc stays 0.5; runtime NLI demotion
stays off. Production verifier unchanged; falsification-hard stays 10/12.
Live hook: ARBORIST_NLI_SHADOW=1 makes query() surface the verifier-input
text (gated off-by-default, never a cache_key/governance/audit_mode
input); qa_sweep.py carries it + the answer into bench rows; the shadow
sweep reads them and buckets by audit_mode. ARBORIST_NLI_SHADOW=1 make
bench-qa-smoke (15 cells) → the naive 'NLI on every context clause'
scaffold has a ~30% would-demote rate on STRICT answers — a haystack /
multiple-comparisons artifact (real contexts segment into 100-336
clauses; max-over-all almost always finds a tangential clause the model
reads as contradicting; a paraphrased STRICT answer often isn't verbatim-
entailed by any single clause so the entailment guard doesn't rescue it).
Lesson: the §7 #5 'candidate source clauses' + recombination-risk gating
is load-bearing, not optional. Do NOT enable runtime NLI demotion on the
current scaffold; next step is the candidate-clause restriction, then
re-run, then gate item 4 is meaningful. Production verifier unchanged;
falsification-hard stays 10/12.
arborist/qa/nli/ — SHADOW ONLY (never an audit_mode input; manifest not
yet in governance_policy_hash per §7 #2). manifest.json pins
cross-encoder/nli-MiniLM2-L6-H768 @ a fixed HF revision + the
bench-validated θc 0.5/θe 0.9 + 2 alternates + the Phase-3 TODO;
shadow.py = ShadowNLI/shadow_check (lazy transformers+torch behind a new
[nli] extra, clauses() segmenter, the §7 #5 clause-level Demote()
decision, degrades to available=False when [nli] absent);
bench/scripts/nli_shadow_sweep.py + make bootstrap-nli / bench-nli-shadow
(the gate-item-4 instrument); 16 tests.
First sweep (116 records — 5f-falsification packs + the arborist-nli-bench
eval sets): 28/28 synth recombination demoted, 0/26 FP on legit summaries,
0/9 fires on already-STRICT_SPAN records, 25/50 on UNGROUNDED (the
contradiction half; quiet on non-sequiturs). Gate items 1/2/3/5/6 clear
on available data; item 4 — shadow FP rate on a real live-bench-qa
sample — remains the open measurement. Production verifier unchanged;
falsification-hard stays 10/12.
fox asked: JIT-trained, gossip-pulled, or repo-embedded?
Added §2.4 — three shapes, two dead ends:
- JIT-trained per deployment: ❌ — GPU-hours over MNLI/SNLI/ANLI, not
a per-deployment step; non-deterministic across runs → different
audit_mode → breaks the verifier's reproducibility invariant.
"Training" is a one-time act by someone; the distributed thing is
the resulting fixed blob, not the recipe.
- Embedded in the git repo as data: ❌ — a few-hundred-MB binary in
.git blows the standing "fresh clone = python3.12 + venv + sqlite3"
invariant; Git LFS adds a dep and still bloats; the repo is source
+ tiny fixtures + docs, not a model registry.
- One-time obtained, content-addressed, fetched on demand, mesh-
distributable: ✓ — the repo carries a tiny manifest
(arborist/qa/nli/manifest.json: {checkpoint_sha256, source_url,
license, nli_model_version}); the weights live under
~/.arborist/models/nli/<hash>/, fetched on first use (make
fetch-nli or auto-fetch on first --enable-nli query) with the
sha256 verified fail-closed against the manifest; nli_model_version
folds into governance_policy_hash (a swap stales the cache, like
chunking_version / canonicalization_version), keeping audit_mode a
deterministic function of (answer, source, policy, pinned hash);
and once arborist's mesh grows blob-sync, a deployment with peers
pulls the checkpoint from a peer (AXFR-style, like ClouDNS slaves
pulling a zone, like a shard rehydrating from snapshot.db) instead
of the origin URL — content-addressing makes peer-pull and
origin-pull interchangeable, the mesh is the optimization not the
canonical source, cold start falls back to origin. Source: a
published off-the-shelf NLI checkpoint (MIT/Apache, recorded) or a
one-time fox/blackops-trained-and-published one.
Bottom line: not JIT-trained, not repo-embedded weights — a
content-addressed hash-pinned checkpoint, manifest in the repo,
weights under ~/.arborist/, fetched on demand (origin or peer), hash
in governance_policy_hash. Precedents cited: the textbook manifest
(pointers + licenses not texts), the [vec] extra (fastembed's
bge-small fetched not committed — #000039), ~/.arborist/ operator-
state, docs/mesh.md + snapshot rehydrate. All presupposes the §2.2/§5
discipline call came back "yes" (is a fixed NLI model allowed in the
audit_mode path?); if "no", none of it builds. Also wired §2.2 con
(b)/(c), §3, §5, §6 to point at §2.4. Doc-only.
bge-small-en-v1.5 batched on a 4090 ≈ 10^3-10^4 chunks/s → full
6.24M-chunk corpus in minutes. Drop a CUDA Embedder (fastembed
CUDAExecutionProvider, or sentence-transformers device=cuda) into
default_embedder()'s pluggable callable; int8 quant stays CPU-side
post-embed; CUDA stack lives only on the producer box. Determinism
note: GPU backfill isn't byte-identical to CPU — irrelevant, soft
data, pin model revision + recipe not output bytes.
#000051: gossip the embedding backfill over mesh — backfill once on any
CPU box, publish a vecpack (leaf_hash-keyed, soft data, cheap structural
gate, never proof path), peers pull + bulk-load. The laptop never runs
the transformer; this is the mechanism behind whitepaper §1's "the
embedding pass runs off the device". Supplies #000050's backfill prereq
as a distributable artifact.
#000050: fold the Dav1dPrometheus-review high-value items into Phase-2
scope — accept-path-5 (vec hits clear the title gate via span-level
warrant, not similarity score, so the gate doesn't drop the semantic
candidates vec exists for), six vec config fields into
governance_policy_hash + a cache-write guard until wired, run-DAG
records the vec stage, four-condition bench (A/B/C/D, C-beats-D) on
semantic-allusion + curated + adversarial-semantic-neighbor fixtures.
Backfill prereq now routes through #000051.
Next ID 000051 -> 000052.
The #000012 top-of-file Status line still said "Phase 1c (branch-set
persistence) remains proposed-not-opened" — but Phase 1c landed
2026-05-10 (d53115e: fork_score_branches table + persist_branch_score
/ branch_set_density + the default-off CLI flags), the #000037 §12
Trigger 1 probe was wired to it 2026-05-11 (dbe8249), and the ticket's
own §7 Phase 1c section + the TICKETS.md index row already say
"landed". Only the header was stale.
Refreshed: Phase 1c landed (with the probe wire — measurable, not yet
fired); §8 = the ForkScore threshold-calibration handoff from #000025
§10.14 (delta_aggregator knob #000047; harder-fixture-tier line
#000046 closed / #000048 closed / #000049 open); Phase 2 (the v8
multi-validator consensus implementation) noted as gating on a
multi-validator deployment, like #000045. Doc-only.
make bench-emergent EMERGENT_N=30 — 30 fresh 3-word-triangulation
cycles appended to bench/emergent_log.jsonl.
Aggregate: 29/30 UNGROUNDED, 1/30 HYBRID, 0 STRICT — the expected
healthy null result. The triangulator generates absurd questions
("How does the depth of one's music-making abilities on a kazoo …")
with no real corpus support; the verifier ladder honestly returns
UNGROUNDED rather than hallucinating-then-STRICTing. 0 false-positive
STRICT on garbage input.
The one HYBRID (perversely/succeeds/disagreeably) is a re-run of
failure shape E ("total deflection to unrelated trivia"): the common
words pulled Hirabah / Original-righteousness chunks via FTS5; the
model quoted them verbatim (3/4 claims verify) but the answer is
total topic-drift → DEFLECTION_DETECTED + CITATION_MISMATCH → capped
at HYBRID, not STRICT. The deflection sidecar + verbatim verifier
holding on adversarial-by-construction input — nothing new, logged as
a confirmation.
No new failure shape or correctness concern. #000006 amend records
it; the 30 entries join the --print-pending queue if a future
teacher-review pass wants a closer look. Doc + log only.
Capture the positioning fox articulated: arborist's per-document ingest
is ~10-100x cheaper than building a vector-DB representation — same
SQLite substrate, different retrieval philosophy — which is the
difference between "ingest + search runs on a phone" and "the NPU is
now a hand-warmer."
New docs/lexical-first-rationale.md (positioning/architecture
reference, not a ticket): the cost asymmetry with the measured numbers
(FTS5 + SHA-256 leaf + Merkle commit + sqlite + zstd pipeline << 1 ms/
chunk vs bge-small ONNX inference ~5-30 ms/chunk, worse contended; the
query side too — a vec query embeds the query string first, an FTS5
query is B-tree lookups); the same-SQLite-different-philosophy table
(inverted index vs dense vectors + ANN; build cost; query cost;
matching; proof-bearing); the deep version of the point — arborist IS
the Merkle Providence model and that model is cheap by construction,
embeddings are a soft signal (CLAUDE.md "soft hash vs hard hash") that
never enter a proof and are the expensive bolt-on; the edge/mobile
consequence; the honest caveat (lexical-first trades the semantic
allusion gap — which is why vec is opt-in/additive, never the default,
and the embed pass is lazy/out-of-band so the heavy transformer work
runs off-device/off-peak; int8 keeps the storage tax at +6%).
Wired in: TICKETS.md "Distinction from other docs" reference list gains
the doc; #000039 §14.6 gains a "Strategic framing" pointer to it.
(A possible follow-up: fold the mobile-viability argument into the
Merkle Providence Reverse RAG whitepaper proper — noted in the doc's
references; not done here, that's a deliberate cross-repo paper edit.)
Doc-only.
fox 2026-05-12: the attribution-aware path (#000048's deferred §2.3 —
closing the 2 recombination over-grounds in falsification-hard) is its
own ticket, not a #000048 phase. So:
#000048 → closed (at 2.1 + 2.4). Steps 2.1 + 2.4 landed 2026-05-11
(12 of 16 residual items: 4 HYBRID_ENTITY over-grounds + 8 Formulate
mis-segments → formulate-hard 12/12, falsification-hard 10/12; each
bench-gated, no STRICT-rate regression). Step 2.2 (single-clause-
containment paraphrase check) attempted + reverted — recombination-
vs-summary isn't lexical (§5 "What we learned"). The 2 residual
falsification-hard fixtures (hard-003 Mercury, hard-005 Einstein)
stand as a documented marker of where the lexical verifier stops.
Header + §5 Closure + §2.3 updated; cross-refs in #000046 / #000012
§8 / TICKETS.md repointed from "#000048 §2.3" to "#000049".
#000049 opened (doc-only, awaiting go/no-go) — "Attribution-aware
grounding check (the recombination boundary)". The recombination
class needs an attribution / dependency-parse or mini-NLI check
(distinguishing "Mercury is the largest" against "Jupiter is the
largest; Mercury is the smallest" from a legit cross-sentence
summary). Options: 2.1 hand-rolled dependency-attribution heuristic
(no model, brittle — same threshold-can't-separate problem one rung
up); 2.2 small purpose-built NLI model ([nli] extra, policy-gated,
off-by-default, bench-gated — the right capability, but forces the
"is a fixed NLI model an LLM-judge?" discipline call + a model
dependency + a non-determinism surface to pin); 2.3 do nothing (the
2 fixtures are a boundary marker, no observed real-traffic harm).
Recommends 2.3 until real-traffic recombination over-grounds show up,
then 2.2 *if* fox rules a fixed NLI model is acceptable in the
proof-adjacent path; the first decision the ticket needs is that
discipline question. Next ID 000049 → 000050. #000048 follow-up.
Doc-only — no code change.
New §14.6 captures the 2026-05-11 findings:
- Measured embed rate: ~4.3 chunks/s on the contended dev box (load
~11 on 8 cores; the embed process got ~28% of one core). One wiki
shard (~1.56M chunks) at that rate ≈ 100h ≈ 4+ days; all four ≈
~16 days. The full int8 backfill was abandoned as not feasible to
brute-force there.
- The 54K-chunk partial on 000.db confirmed ~409 B/chunk apparent
→ 384 B amortized → ~2.4 GB for the full 6.24M-chunk corpus at
int8 — the deterministic number a full backfill would only
re-confirm, so finishing it bought nothing.
- On an idle healthy box (batching + all cores) bge-small does
~50-200 chunks/s → full corpus ≈ ~9-35h (the ticket's earlier
"~17h" is the optimistic end).
- Per-chunk cost breakdown: bge-small ONNX inference ~5-30 ms/chunk
dominates; the existing arborist ingest steps (chunker + SHA-256
leaf + Merkle commit + sqlite INSERTs + zstd + FTS5) are well
under 1 ms total. So adding vec multiplies ingest by ~10-100×,
entirely in the ONNX matmuls — the non-vec ingest path is
unchanged and still runs at hundreds of chunks/s.
- Implication for §14.2: this is *why* lazy-out-of-band is the
default and `arborist ingest --embed` is the opt-in. Production
guidance: a corpus-wide backfill is a one-time batch job (hours
on idle / days on contended), best run off-peak or on a dedicated
box; it does not slow ongoing ingest (which never embeds unless
--embed is passed); a GPU/accelerated embedder is a drop-in via
the pluggable Embedder callable if backfill latency matters.
Status line updated to point at §14.6. Doc-only.
What we learned from the (reverted) step-2.2 attempt, stated as a
general principle in #000048 §5 "What we learned":
A recombination ("Mercury is the largest planet …" reusing the
source's "largest planet …" with its "Mercury") and a legitimate
cross-sentence summary ("Batman, who is the alias of Bruce Wayne,
lives in Gotham City." reusing two adjacent source sentences) are
the same SHAPE to any lexical signal — both scatter the answer's
content tokens across source clauses, and the recombination's
best-single-clause coverage (4/5 = 0.8) sits ABOVE the legit
summary's (4/6 = 0.67), so no token-coverage / clause-containment /
bigram threshold separates them in the safe direction. The
discriminating thing is *attribution* — in the source, are these
tokens attached to the same subject/predicate the answer attaches
them to? — which is a dependency / NLI question, not a string
metric. That's the boundary of the deterministic, no-LLM-judge
lexical verifier: absence signals (#000046 numeric gate, #000048
step 2.1 entity gate) and structure-of-the-model's-own-output
signals (step 2.4 segmenter) are lexical and work; "the source
contradicts this pairing" is not, and proxying it with a coverage
cut trades a small contrived-fixture win for honest demotions of
real summaries — a net loss against bench-maxing's 5-pp floor.
Updated: #000048 §2.2 (the attempted idea kept as design log + the
no-threshold-separates finding), §2.3 (now framed as the only path to
the last 2 — attribution / mini-NLI, its own ticket if ever), §3
(original plan annotated with the LANDED / ATTEMPTED+REVERTED / NOT
DONE outcome), §5 (the step-2.2 receipt + the "What we learned"
subsection + the closure recommendation). Stale cross-refs fixed:
TICKETS.md #000048 + #000046 rows, #000046 ticket Headroom section,
#000012 §8 #4 — all of which said "#000048 step 2.2 closes the last
2", now corrected to "step 2.2 attempted + reverted; the 2 recombination
fixtures stand as documented residue; #000048 §2.3 is the path if
ever wanted".
Recommendation unchanged: close#000048 at 2.1+2.4 (12 of 16 residual
items closed — formulate-hard 12/12, falsification-hard 10/12). Doc-
only — no code change.
The cache lookup in ask()/query()/canonical-persist runs outside the
write transaction, so two concurrent callers on the same cache_key can
both miss and both reach the providence_cache INSERT — the loser raised
`UNIQUE constraint failed: providence_cache.cache_key` and ask() crashed
(MOAD-0005 / TOCTOU; sibling of the af870bb append_audit fix). All three
write sites (qa/runner.py, qa/query.py, qa/canonical_cache.py) now end in
`ON CONFLICT(cache_key) DO NOTHING`, so the loser no-ops (its answer is
equivalent — same question/model/policy ⇒ same cache_key; canonical
answers are deterministic). With busy_timeout on every connection
(af870bb) the loser waits on the writer's lock then no-ops.
test_qa.py::test_concurrent_ask_same_cache_key_no_unique_crash — 6
threads run ask() on the same question concurrently; must not raise;
exactly one cache row lands. Verified it fails without the fix (5 of 6
threads raise IntegrityError).
Tried a single-clause-containment check on the paraphrase path (and
the entity path's weakest slot): a span is grounded only if a single
source clause (;/. -delimited) covers >= paraphrase_coverage of its
content tokens — a recombination spreads them across clauses. On the
contrived fixtures it works (hard-003 Mercury, hard-005 Einstein →
UNGROUNDED → falsification-hard 10/12 → 12/12). But it ALSO rejects
legitimate cross-sentence summaries — the established fox-2026-04-29
Batman case ("Batman, who is the alias of Bruce Wayne, lives in
Gotham City." against "Batman is the alias of Bruce Wayne. Batman
lives in Gotham City.": spans two clauses, best clause covers 4/6 =
0.67 → demoted, but it's a correct paraphrase). And there's no
threshold that separates the two directions — the recombination
("Mercury …": best clause covers 4/5 = 0.8) sits ABOVE the legit
summary's 0.67, so any cut that rejects Mercury also rejects Batman.
Distinguishing "recombined into a different statement" from
"summarized two adjacent sentences" needs attribution / dependency
parsing or a mini-NLI model (the §2.3 territory), not a conservative
lexical proxy.
Reverted — verify.py stays at its post-step-2.4 state; falsification-
hard stays at 10/12 with hard-003 + hard-005 as documented residue.
#000048 §5 records the attempt + the no-threshold-separates argument;
recommendation: close#000048 at 2.1+2.4 (12 of 16 residual items
closed), with the 2 recombination fixtures a marker for a future
attribution-aware verifier (its own ticket). Awaiting fox's call.
Doc-only — no code change in this commit.
#000035 closed 2026-05-11 (commit 4b85eb1: v7 substrate spec into the
repo at docs/_source/merkle-agi-dag-v7.rst, § 9.10 folded in). Update
soft-hash-channel-analysis.md §9 open-question 2 from "Phase 2 lands
when v7 gains a deployment target" to resolved: φ_PRG = HMAC-SHA-512
with a 32-byte committed seed, little-endian.
Closes the 8 mis-segments #000046 left in formulate-hard-v1.jsonl.
The parser was line/bullet-only — one line ⇒ one claim — so a line
that crammed several pointered claims onto one row ("Water is wet
[E1]; fire is hot [E2]", "X happened [E1]. Y followed [E2]") became
one monolithic claim with all the pointers, and a wrapped bullet
became two.
arborist/qa/parse_claims.py: _SEGMENT_SEP_RE splits a line on ';',
sentence boundaries ('. '/'! '/'? ' then a Capital), spaced dashes
(' - '/' — '/' – '), ' and '/' or '/' because '/' although '/' since
'/' while ', inline '(N)' enumeration markers, and commas — with
'(?![^\[]*\])' so a comma inside a [E1, E2] bracket never splits it.
_segment_line keeps the split ONLY IF every resulting non-empty
segment is a well-pointered claim — a legit single claim ("The cat
is black and white [E1].", "The cast: A, B, C [E1].") is never
broken because splitting it would manufacture pointer-less prose
fragments → guard rejects; a leading colon-terminated header with no
pointer ("Two facts:", "Key points:") is allowed and dropped. Plus a
wrapped-bullet join: a continuation line (leading whitespace then a
lowercase letter, no bullet glyph) folds its text + pointers into the
previous claim.
Effect: formulate-hard rate 4/12 → 12/12 (the pack is now at ceiling
— a harder Formulate tier would re-open below-ceiling headroom; a
#000046 follow-up). Remaining #000048 headroom: 2 STRICT_PARAPHRASE
recombinations in falsification-hard (Mercury, Einstein — step 2.2).
Bench gate: make bench-qa (n=3 × 75 × 3 = 675 cells; parse_pointer_claims
feeds the 450 claim_lattice_pointer + claim_lattice cells) after
(bench/qa_results/2026-05-11T20-26-37Z) vs the pre-step-2.4 baseline
(...T17-12-41Z = HEAD's parse_claims.py). STRICT-rate quote 0.54→0.55,
pointer 0.22→0.22, lattice 0.43→0.45 — all within the 5-pp noise
floor. Per-row diff: the segmenter changed the parsed-claim count on
the SAME answer text for 7 of the 450 lattice cells (0 in
claim_lattice, 7 in claim_lattice_pointer); of those, 2 caused an
audit_mode change — both correct: a wrap-join recovered an answer's
intended structure (4 claims, 2 pointer-less wrap-fragments → HYBRID)
into 2 well-pointered claims → STRICT; and a crammed-one-line blob (1
monolithic claim, all pointers → STRICT) split into 8 claims, some
not individually verifying → HYBRID (the honest verdict — false-
positive STRICT was the corruption). Every other lattice/quote delta
is LLM re-answer variance. No regression — the segmenter's only
visible effects on real traffic are honest improvements. Summarized
in qa-modes-bench.md Addendum 7 + ticket-000048 §5 step 2.4.
Tests: 8 new in test_claim_lattice.py (semicolon/sentence/conjunction
splits; pointerless-fragment + cast-list guards; leading-colon-header
drop; wrapped-bullet join; pointer-order/multi-pointer); existing
parse_pointer_claims tests pass untouched; test_5f_formulate_hard_pack
re-pinned 4/12 → 12/12. make test 2358 passed, 28 skipped.
#000048 → steps 2.1 + 2.4 landed; #000046 / #000012 §8 / TICKETS.md /
Makefile / fixture _meta + notes updated.
Brought the Merkle-AGI v7 formal substrate spec into the repo as
docs/_source/merkle-agi-dag-v7.rst (previously referenced only as the
un-version-controlled ~/Downloads/merkle-agi-dag_v7.txt). Section
structure converted to reStructuredText; inline math kept in the
source's literal notation; added to the docs/_source/index.rst
"Substrate" toctree (also added the pre-existing merkle-agi-v8-consensus
entry that was missing from it).
Folded ticket #000035's § 9.10 + § 9.10.1 (anchor PRG map φ_PRG;
dav1d-reviewed-final, little-endian, HMAC-SHA-512 / 32-byte seed) in at
their numbered positions, after § 9.9, with a .. note:: citing the
reference implementation (arborist/substrate/anchor_prg.py). #000035 ->
closed (Phase 1 + Phase 2 both landed); #000018 §9.2 ("which PRG?")
resolves to HMAC-SHA-512 with a 32-byte committed seed. Full upstream
v7 spec revision stays exogenous; this lands the amendment into the
tracked in-repo copy where future amendments also go.
(docs/TICKETS.md also carries the in-flight #000048 index-row update
from a concurrent session.)
Closes the 4 HYBRID_ENTITY over-grounds #000046 left in
falsification-hard-v1.jsonl. The entity strategy grants HYBRID when a
multi-word proper noun matches the source — but "Insulin was
discovered by Alexander Fleming" against "Penicillin was discovered by
Alexander Fleming" matches on the shared "Alexander Fleming" while the
swapped subject "Insulin" (the falsehood) is ignored.
arborist/qa/verify.py: _entity_salient_disagrees(answer_text, norm_ctx)
flags a >4-char Capitalized content token (stopword-filtered) or a
digit-number in the answer absent from the source.
_is_single_sentence(text) — no internal '. '/'! '/'? ' break. Gated in
verify_quotes' entity branch (proximity policy) in the weakest-grounding
slot only: not cluster AND len(verified) <= 1 AND _is_single_sentence
AND _entity_salient_disagrees → UNGROUNDED. The narrow caller-gate is
what keeps a structured multi-claim summary untouched — the Matrix cast
list (many entities, a tight cluster) and the TMNT answer (a numbered
list with parenthetical nicknames the source omits): model-added
accurate detail in a real summary isn't a contradiction, only the
single-sentence-one-weak-match shape is. The Matrix/TMNT/hybrid
entity-path regression tests still pass, pinned untouched.
Effect: falsification-hard rate 6/12 → 10/12 = 0.833 (Insulin / Berlin
/ 1889 / Pacific now correctly UNGROUNDED). The 2 live-pack fixtures it
newly demotes — 5f-fal-live-003 (the exact gap #000046 built its hard
pack around) and 5f-fal-live-028 — had expected_reason updated
HYBRID_ENTITY → UNGROUNDED (the live pack records what verify_quotes
actually does). Remaining hard-pack headroom: 2 STRICT_PARAPHRASE
recombinations (Mercury, Einstein — step 2.2) + 8 Formulate
mis-segments (step 2.4).
Bench gate: make bench-qa (n=3 × 75 × 3 = 675 cells) after
(bench/qa_results/2026-05-11T17-12-41Z) vs the pre-step-2.1 baseline
(...T14-19-51Z = HEAD's verify.py). STRICT-rate quote 0.50→0.54,
pointer 0.25→0.22, lattice 0.45→0.43 — all within the 5-pp noise
floor. Per-row diff (675 common cells, 30 quote-mode rows changed
audit_mode): 0 quote-mode rows demoted to UNGROUNDED from the entity
path — the gate fired on 0 legitimate QA answers in the whole bench.
Every transition was LLM re-answer variance (verifier quote→quote with
the verdict flipping); pointer/lattice deltas are noise too (the gate
is in verify_quotes / quote mode, not the claim-lattice verifier). No
regression — the gate is provably narrow on real traffic. Summarized in
qa-modes-bench.md Addendum 6 + ticket-000048 §5 step 2.1.
Tests: 4 new in test_verify.py (_is_single_sentence helper,
_entity_salient_disagrees helper, swapped-subject → UNGROUNDED,
gate-narrow-on-multi-claim); test_5f_falsification_hard_pack_below_ceiling
re-pinned 6/12 → 10/12; test_fork_score_positive_gamma_5f_... updated
(positive γ·Δ5f on the real lift — possibly MARGINAL given the ÷5
dilution; ACCEPT via a degraded-parent sub-scenario).
make test 2343 passed, 28 skipped.
#000048 → step 2.1 landed; #000046 / #000012 §8 / TICKETS.md /
Makefile / fixture _meta + notes / baseline JSON updated.
Wire vector quantization (the §3.1 production knob): arborist embed
--quant int8 [--rebuild]. The chunk_vecs vec0 column becomes int8[384]
vs float[384] per quant; the quant folds into VEC_BACKEND_VERSION
(...-384int8-... / ...-384float32-...) and vec_meta records it per
shard. Switching quant on an existing chunk_vecs requires --rebuild
(the vec0 element type can't be altered in place — embed_documents
raises ValueError telling you to --rebuild).
int8 serialization: scale each bge component by 127 (theoretical
[-1,1] range), clamp to [-127,127], round, serialize_int8. The same
scaling on the query vector → distances comparable; cosine is
scale-invariant so the uniform x127 cancels in the ranking.
sqlite-vec v0.1.9 quirk worked around: a bare blob inserted into a
vec0 column is interpreted as float32 regardless of the column's
declared type — int8 vectors MUST be wrapped in vec_int8(...). So
the INSERT and the MATCH now wrap the blob in vec_f32(?) (float32)
or vec_int8(?) (int8) — constructor name from a fixed dict, no
injection surface. (Discovered the hard way: a bare int8 blob into
an int8[384] column → "expected int8, but a float32 vector was
provided".)
VecBackend reads the quant from the existing chunk_vecs schema (or
defaults to float32) so search uses the matching wrapper. New module
exports: QUANTS, EMBED_QUANT, vec_backend_version(quant), existing_quant.
int8 head-to-head on crawl_appliedcombinatorics_org.db (168 chunks):
- storage: float32 1,597,440 B -> int8 417,792 B = 3.8x smaller
(~4x at corpus scale where the 1024-vector blocks fill; the
~28 KB of vec0 metadata doesn't quarter, hence 3.8 not 4.0).
- recall vs the float32 baseline:
Q "how many ways to choose k things from n":
identical top-5 (Combinations, Permutations, Exercises,
Derangements, Graph Coloring).
Q "pigeonhole principle counting":
identical top-2 (Graph Coloring, Exercises); ranks 3-4 swap
Derangements <-> Permutations at Δdistance 0.002 — sub-noise.
- embed speed unchanged (~3.9 chunks/s — model-load-dominated).
Conclusion: int8 is the obvious production config (§3.1's +6%-tax
recommendation confirmed empirically). v1 default stays float32 for
max fidelity; flipping the default to int8 is a fox call.
CLI (arborist/cli.py): arborist embed --quant {float32,int8}; output
JSON gains "quant"; embed_documents ValueError → exit 2 with the
"--rebuild" hint.
tests/test_search_vec.py (9 -> 16): test_int8_quant_roundtrips
(int8[384] schema, vec_meta version, search round-trip, quant
inferred by VecBackend), test_quant_mismatch_requires_rebuild,
test_invalid_quant_rejected.
#000039 status updated. Full suite: 2343 passed, 28 skipped.
(Unrelated parallel-clone work in the tree — Makefile, arborist/qa/
verify.py, bench/fixtures/5f/*, tests/test_bench_batteries.py,
tests/test_verify.py — is #000046's hard-fixture tier, not touched.)
`make chain-check-shards` / `chain-check` previously only counted dangling
prev_event_hash references — it missed forks (two events chained off the
same head; qa.db seq 7724/7725 was that, and the cheap check reported 0).
CHAIN_CHECK_SQL now reports the sum of: dangling prev refs + forked
parents (a prev_event_hash claimed by >1 row) + extra genesis rows
(>1 row with prev_event_hash IS NULL). 0 = a single linear chain.
(Working-tree Makefile also carried in-flight bench-5f-* target additions
from a concurrent session; they ride along in this commit.)
Tracks the headroom #000046 (closed) left: the 6 over-grounds still
in falsification-hard-v1.jsonl (4 HYBRID_ENTITY where the
entity-proximity strategy matches a shared proper noun while the
answer's other salient term is wrong; 2 STRICT_PARAPHRASE where the
false claim recombines source tokens into a different true statement —
"Mercury is the largest" — and lexical token-coverage can't tell
recombination from grounding) + the 8 mis-segments in
formulate-hard-v1.jsonl (line/bullet-only parse_pointer_claims merges
multi-claim lines / splits wrapped bullets).
Options + recommended order: 2.1 verify_quotes entity
salient-token-disagreement gate (the direct analogue of #000046's
numeric gate — narrow, lexical, bench-safe; catches the 4
HYBRID_ENTITY) → 2.4 parse_pointer_claims sentence/clause
segmentation (closes the Formulate pack) → 2.2 sequence-aware
paraphrase match (conservative threshold; catches the 2
recombinations); defer 2.3 mini-NLI (heavier; only if 2.1+2.2 leave a
residue worth a model dep). Each landing bench-gated — verify_quotes
changes run `make bench-qa` before/after, parse_pointer_claims changes
run the Formulate fixtures + a QA smoke. Doc-only proposal; no code in
this ticket.
Next ID 000048 → 000049; #000046 "Headroom" section + TICKETS.md
index updated to point at #000048.
Closes the harder-fixture-tier ticket: a real verify_quotes
tightening lifts the falsification-hard rate 4/12 → 6/12, bench-gated,
ForkScore's bench-Δ goes positive on it — the loop is closed
end-to-end.
arborist/qa/verify.py: _numeric_signature(text) extracts comma-
stripped digit-runs ('8,849' and '8849' collapse; '300' stays
distinct from '300000' ← '300,000'). _check_each_with_paraphrase
gains a gate: a span that token-covers the source ≥ paraphrase_coverage
but asserts a digit-number the source lacks (modulo thousands-comma)
is no longer paraphrase-grounded — it goes to unverified. Catches the
near-miss the lexical coverage check is blind to ("Water boils at 50
degrees" against a source saying 100 token-covers 100% because
'50'/'100' aren't >4-char content tokens). Narrow by construction:
fires only on the paraphrase fallback (verbatim/span/entity/claim-
lattice paths untouched), only on a digit-number. A rounding-
paraphrase demoting here is the honest verdict — it isn't a verbatim
grounding.
Hard pack: lifts 5f-fal-hard-004 (50 vs 100) and -007 (300 vs
300,000) to UNGROUNDED → falsification-hard rate 4/12 → 6/12 = 0.5.
The other 6 over-grounds (4 HYBRID_ENTITY + 2 recombined-no-number
STRICT_PARAPHRASE) and the Formulate hard pack are unaffected —
headroom for a bigger order/dependency-aware verifier upgrade, an
optional follow-up, not a #000046 blocker.
Bench gate: make bench-qa (n=3 × 75 questions × 3 modes = 675 cells)
before / after. STRICT-rate quote 0.53→0.50, pointer 0.23→0.25,
lattice 0.44→0.45 — all within the 5-pp noise floor. Per-row diff
(675 common cells, 74 changed audit_mode): the only clearly
gate-attributable QA shift was the fictional "our cold fusion
breakthrough" year-claim demoting STRICT→HYBRID (×3 samples) — a
correct demotion; every other transition was quote→quote /
claim_lattice→claim_lattice LLM re-answer variance. No regression on
legit answers. Artifacts: bench/qa_results/2026-05-11T13-42-38Z (before)
and ...T14-19-51Z (after) — gitignored; summarized in qa-modes-bench.md
Addendum 5 + ticket-000046 §5 Phase 3.
Worked example: fork_score on the real change — parent {5f/
falsification: 4/12} → child {5f/falsification: 6/12} → γ·Δ5f =
(1/6)/5 ≈ +0.033 > 0 (positive; a single improvement of this size is
MARGINAL by the ÷5 dilution, the test pins the full-lift-to-1.0 case
at ACCEPT).
Tests: 5 new in tests/test_verify.py (numeric_signature normalization
+ subset-matches-comma-variant + disagreement-rejected +
gate-is-narrow + number-present-still-verifies);
test_5f_falsification_hard_pack_below_ceiling re-pinned 4/12 → 6/12.
make test 2339 passed, 28 skipped.
#000046 → closed; #000012 §8 §4 + TICKETS.md row + falsification-hard
_meta / hard-004,007 notes + Makefile 8/12→6/12 comments updated.
(Makefile also carries an uncommitted chain-check-SQL improvement from
the concurrent session — NOT in this commit; staged only the
#000046-comment hunks.)
Fox: "i like both" — keep the lazy out-of-band pass as the default
AND add the eager opt-in. Plus the Phase-1 gap fix (incremental embed).
Key insight folded into the design (new ticket section 14): a chunk_id's
content is immutable in arborist — same content → same chunk_id;
different content → a NEW chunk_id (re-ingest makes a new doc_root +
new chunk_ids linked by supersedes; a chunker bump re-chunks → new
chunk_ids). So a chunk, once embedded, never needs re-embedding — the
ONLY re-embed trigger is the embedder changing (VEC_BACKEND_VERSION
bump). That makes the idempotency story clean.
arborist/search/vec.py — embed_documents() now:
- incremental=True (default): embed only chunk_ids NOT already in
chunk_vecs (chunk_id NOT IN (SELECT chunk_id FROM chunk_vecs)).
This is the after-ingest / cron / Prometheus-Sigma-sweep path —
it picks up exactly the newly-ingested chunks; re-running is a
cheap no-op once everything's embedded.
- incremental=False: re-embed every chunk with content (delete-then-
insert all) — the embedder-changed case.
- rebuild=True: DROP + recreate chunk_vecs first, then a full pass —
the clean VEC_BACKEND_VERSION-bump path (a search mid-rebuild never
mixes old- and new-model embeddings: the recreated table starts
empty and grows new-model as the pass runs). Implies non-incremental.
- Cold-evicted chunks (content NULL) still skipped; vec rows persist
and stay valid (content is identical on rehydrate).
CLI (arborist/cli.py):
- arborist embed --rebuild — the DROP+recreate+full-re-embed path
(default is incremental). Output JSON now reports "mode".
- arborist ingest --embed — eager opt-in: after the chunk+Merkle-
commit pass, incremental-embed this run's new chunks. Default
ingest does NOT embed. ingest output gains "chunks_embedded" when
--embed is set. Only surfaced when the [vec] extra is installed.
- Hoisted the _vec_ok check up to the top of build_parser so both
the ingest --embed flag and the search --backend / embed subcommand
can gate on it.
tests/test_search_vec.py (7 -> 9): test_embed_incremental_only_embeds
_new_chunks (second pass after a follow-up ingest embeds only the new
chunk; third pass is a no-op), test_embed_rebuild_re_embeds_all
(DROP+recreate+full pass; vec_meta still records the version).
Verified on crawl_appliedcombinatorics_org.db: incremental on an
already-embedded shard reports chunks_embedded=0 in ~1.8s; --rebuild
re-embeds all 168 in ~61s; semantic search after rebuild still returns
topically-correct hits ("pigeonhole principle counting" -> "AC Graph
Coloring" chunk containing "Generalized Pigeon Hole Principle").
Ticket section 14 added: the idempotency table (re-ingest / chunker
bump / cold eviction / embedder bump / superseded docs), the two
integration models (lazy default + eager opt-in; the lazy pass's
natural home is a Prometheus-Sigma unconscious-sweep task per #000037
section 3.1), the command matrix, concurrency notes, versioning.
Status line updated.
Full suite: 2339 passed, 28 skipped.
(Unrelated parallel-clone work in the working tree — Makefile,
arborist/qa/verify.py, bench/fixtures/5f/*, tests/test_bench_batteries.py,
tests/test_verify.py — is #000046's hard-fixture tier, not touched here.)
Extends the #000046 hard tier to the Formulate sub-battery.
bench/fixtures/5f/formulate-hard-v1.jsonl — 12 prose inputs that
arborist.qa.parse_claims.parse_pointer_claims SHOULD segment into a
particular claim lattice (recorded in expected_lattice). The parser
is line/bullet-based — one line ⇒ one claim, [E#] tokens attach to
it — so 8 of 12 it mis-segments: merges and-/semicolon-/dash-joined
or (1)(2)-enumerated multi-claim lines into one claim with all the
pointers, or splits a wrapped bullet into two. Those 8 fail at HEAD
on claim-count mismatch; the other 4 are well-formed bullet/numbered
lists / single claims the parser handles right. Rate at HEAD = 4/12
= 0.333, stable (parse_pointer_claims is deterministic). A
claim-lattice parser that does sentence/clause segmentation (split on
'. ', ';', subordinating conjunctions, inline enumerations) + joins
wrapped bullets lifts the rate toward 1.0 → positive γ·Δ5f for that
child fork.
make bench-5f-formulate-hard runs it (|| true past the runner's
nonzero-on-failures exit). Not in `make bench-5f` / `runner --all`.
tests/test_bench_batteries.py — test_5f_formulate_hard_pack_below_ceiling
(pins rate 4/12, source=live, the 8 fails are claim-count
mis-segments).
#000046 → "Phase 1 + Phase 2 landed"; two below-ceiling 5F subs now
exist (falsification, formulate). Closure still pending an actual
surface improvement (verify_quotes tightening — bench-gated — or
parse_pointer_claims segmentation) that lifts a rate. §5 + §6 +
TICKETS.md row updated.
(Makefile also carries an uncommitted chain-check-SQL improvement
from the concurrent #000039 session — NOT included in this commit;
staged only the bench-5f-formulate-hard hunk + the .PHONY line.)
qa.db carried a 2-way fork at seq 7724/7725 — two providence_burn events
whose prev_event_hash both pointed at row 7723's event_hash. Cause:
append_audit read the chain head and INSERTed as two separate autocommit
statements (the burn caller ran it OUTSIDE its transaction() block), so
two concurrent appenders both chained off the same head; seq AUTOINCREMENT
serialized the rows but not the hash linkage.
- append_audit wraps head-read + insert in its own BEGIN IMMEDIATE/COMMIT
when the connection isn't already in a transaction (folds in otherwise).
- connect() now sets busy_timeout=5000 on every connection (not just the
migration pass) so a peer mid-write makes us wait, not fail-fast — and
a fail-and-retry appender can't re-read a stale head.
- test_audit_chain_concurrency.py: 8 threads x 20 append_audit() on one
shard must yield a linear chain (one genesis, no forked parents);
+ the in-transaction fold-in / rollback behaviour. Verified the
concurrency test fails without the fix (8 genesis rows).
The #000025 §10.14 calibration showed _delta_5{s,t,f} mean over a
battery's 5 subs, so a single-sub gain weighs 1/5 of face value (the
5× dilution). #000047 ships the knob to pick the aggregation, default
unchanged.
WeightSet.delta_aggregator ∈ {"mean","max","sum"} (default "mean") —
a categorical field, validated in __post_init__ against
DELTA_AGGREGATORS; from_dict takes it as a string. Default unchanged →
ScoredFork output byte-identical → no fork_score.ESTIMATOR_VERSION
bump.
fork_score._aggregate(deltas, how): mean = arithmetic mean, max =
max(0.0, max_i Δ_i), sum = Σ Δ_i; empty → 0.0. _delta_5s/_delta_5t/
_delta_5f take an aggregator arg (default "mean"); the 5F efficiency
bonus is added after the aggregated base (aggregator-independent).
fork_score passes weights.delta_aggregator. The per-sub
HARD_REGRESSION_FLOOR flags are computed before aggregation, so a
single-sub regression still forces REJECT under max/sum. The chosen
aggregator is recorded in ScoredFork.weights["delta_aggregator"] (via
WeightSet.as_dict()); fork_score_branches traceability stays via the
opaque weights_id — no schema migration.
bench/scripts/fivef_threshold_calibration.py gained §5 — runs the
#000046 below-ceiling pack (5f/falsification at 0.333) and shows the
verdict / γ·Δ5f under each aggregator; bench/results/5f-threshold-
calibration-2026-05-11.md §5 is the captured record. Default stays
"mean" — the conservative, noise-robust, regression-symmetric choice
matching docs/bench-maxing.md's per-rate floor framing; v8 picks
max/sum per-deployment.
Tests: 8 new in tests/test_fork_score.py + 1 anchor in
tests/test_fivef_threshold_calibration.py; tests/test_weights.py
as_dict field-set test updated to include delta_aggregator;
test_fork_score.py AUTOCOUNT tags (#000012 §286, warrant-substrate-
cookbook.md ×2) bumped 23 → 31.
#000047 closed; #000012 §8 §3 + TICKETS.md row updated.
Full suite: 2330 passed, 28 skipped.
Implements the optional vec backend from the #000039 doc, with the
"obvious" v1 tuning, and demonstrates it on a real corpus shard.
arborist/search/vec.py (new):
- VecBackend(SearchBackend) — ANN over chunk_vecs, UNGROUNDED hits
(same as FTS5; vec changes recall, never warrant — embeddings are
soft signal, never in the proof path).
- chunk_vecs vec0 virtual table + vec_meta — sibling tables, additive,
don't touch chunks/documents/the audit chain.
- embed_documents() — batched ingest; delete-then-insert per chunk_id
(vec0 doesn't honor INSERT-OR-REPLACE — re-inserting an existing PK
is a hard UNIQUE error), so re-runs are idempotent and content-
changed → re-embed works. Skips cold-evicted chunks (content NULL).
- Pluggable Embedder callable; default = fastembed bge-small-en-v1.5
(~130 MB ONNX, downloads on first use). load_vec_extension(conn)
toggles enable_load_extension + sqlite_vec.load.
- v1 hyperparams (VEC_BACKEND_VERSION = vec-v1-bge-small-en-v1.5-
384float32-cosine-flat): model bge-small-en-v1.5, dim 384, quant
float32 (int8/binary = the production storage knob per §3.1, not
wired in v1), metric cosine (bge outputs L2-normalized, so cosine
ranking ≡ L2 ranking), ANN flat (vec0 default), top_k 20. These
five fold into governance_policy_hash in a later phase (§6).
CLI (arborist/cli.py):
- — populate chunk_vecs
for --db; prints progress + timing.
- — semantic ANN search (errors with
an install/embed hint if [vec] missing or chunk_vecs empty).
- Both surfaced only when sqlite_vec imports (mirrors the [html] /
selectolax pattern).
pyproject.toml: [vec] optional extra (sqlite-vec>=0.1.9, fastembed>=0.4);
added to [dev]. Note: sentence-transformers is the heavier "official"
embedder path §5 names; fastembed is the lightweight ONNX one.
tests/test_search_vec.py (7 tests, skip-if-no-[vec]): deterministic
stub embedder (hash → unit vector) so the suite exercises the
sqlite-vec plumbing — ext load, schema, ingest, KNN, JOIN, Hit shape,
limit, idempotent re-embed, --limit cap, empty/unpopulated — without
the heavy fastembed model. Semantic quality is demonstrated on a
shard, not unit-tested.
Demonstrated on ~/.arborist/shards/crawl_appliedcombinatorics_org.db:
168 chunks embedded in ~37 s (mostly model load); semantic queries
return topically-correct hits — "how many ways to choose k things
from n" → top hit "AC Combinations", "binomial coefficient counting"
→ "AC Introduction" (integer-solution counting) + "AC Combinatorial
Proofs". None of the query tokens need stem-match the chunk — the
semantic-allusion-gap closure the ticket promised. chain-check on
that shard reports 0 after embedding (chunk_vecs is a sibling table).
#000039 status flipped to "in progress · Phase 1 landed"; Phase 2
(RRF hybrid fusion in query.py) gated on a ≥5pp recall-lift
measurement with no STRICT-rate regression (§8).
(Unrelated: tests/test_weights.py::test_as_dict_returns_all_eleven_fields
fails in the working tree — that's a parallel-clone in-flight change
to arborist/substrate/weights.py + its test, not touched here.)