Commit graph

488 commits

Author SHA1 Message Date
68c6665db9
#000049 §7 #22: make bootstrap-nli-only (lean GPU/CPU NLI compute-node setup) + GPU benchmark
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.
2026-05-12 17:55:54 -04:00
87d9db15c7
#000049 §7 #22: speedup (batch + cuda auto-detect + ONNX-int8 export) + the gate-item-4 verdict at proper n
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.
2026-05-12 17:21:32 -04:00
96ff586da1
#000052 §3.1: diagnose_coherence — lexical answer-shape sidecar
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).
2026-05-12 17:19:07 -04:00
42a0f933ff
bench/qa_sweep: scrub lone surrogates from the NLI-shadow answer_text/context fields before json.dumps
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.
2026-05-12 17:04:33 -04:00
f955082ebe
#000049: fix loose 'real traffic' wording → 'bench-qa traffic' (defined in §7 #20: arborist's own query pipeline driven by the curated bench question set, not live users) 2026-05-12 16:54:28 -04:00
59d8a17a14
#000052: open — relevance + coherence meta-cognition (answer-shape sidecars)
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
2026-05-12 14:49:10 -04:00
02f8dfec07
#000049 §7 #21: candidate-clause restriction in ShadowNLI.check — helps, doesn't close it
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.
2026-05-12 14:34:44 -04:00
f4de936ff9
#000049 §7 #20: first real-traffic NLI shadow sweep — the haystack problem
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.
2026-05-12 14:28:44 -04:00
70ecda3d6c
#000049 Phase 2: NLI shadow scaffold in arborist (§7 #19)
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.
2026-05-12 14:14:21 -04:00
87c92162a1
#000049 §7 #18: harder-set 5-candidate NLI bench — 28+26 cases; the two-threshold rule is load-bearing (entailment guard filters spurious competing-superlative contradictions, 0% guarded FP vs ~4% single-threshold); MiniLM-82M cost-pick 2026-05-12 14:04:41 -04:00
23e934aa55
#000049: Phase-2 candidate-NLI bench — first run (§7 #18); both candidates 18/18 catch, 0/18 FP on synthetic set; harness in ~/git/arborist-nli-bench 2026-05-12 13:57:54 -04:00
57bb26ba4e
#000049: incorporate de novo review (2026-05-13) — shadow-path approved, governance-hash correction, demotion-only rule, NLI manifest fields, nli_pair@v1, bench gate, roadmap 2026-05-12 13:06:42 -04:00
7ff37f8d70
docs: fold 2026-05-13 Dav1d review amendments into #000050 + #000051
#000051 §6a: vecpack manifest + entry_root (transit integrity, not
embedding-correctness proof); vec_backend_version must commit the full
reproducible input (model/tokenizer revisions, preprocessing/pooling/
normalization policies, named quant recipe); trust tiers 0-3 (untrusted
OK for raw search, cached hybrid QA needs Tier 2+ and #000050 wiring);
streaming/resumable/bounded-memory import + disk budget; bad-row
thresholds + duplicate policy; exception matrix; soft audit events;
extra CLI (inspect/verify/--dry-run/--replace/--trust-policy); SQD
canonical-bytes discipline now, pi* module later; 15 test additions.

#000050 §5a: governance_policy_hash must cover the fusion policy
(retrieval_backend, vec_top_k, rrf_k, route set, accept_path_version,
fallback/coverage policy, candidate budget) not just the 6 vec fields;
RRF must survive the post-merge rerank (Option A: RRF score is one
bounded feature in the existing reranker) + deterministic tie-break;
split accept-path-5 into 5a warranted-survival vs 5b exploratory-
UNGROUNDED-survival (5b hard-excluded from verified-evidence counting);
mixed-fleet vec_coverage metadata + bench-validity rule; exact bench
denominators + row schema + good-label-by-mode; consume #000051
metadata in run-DAG; implementation order (governance+cache-guard
before RRF code can persist); 12 test additions.
2026-05-12 12:57:39 -04:00
e391f1f01c
#000049: expand — where would the NLI weights live? (§2.4 provenance & distribution)
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.
2026-05-12 12:36:09 -04:00
0d55fea3a6
docs: #000051 — add GPU-producer fast path (CUDA Embedder, minutes not days)
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.
2026-05-12 12:18:27 -04:00
07b8ceb638
docs: open #000051 (federated vecpack distribution) + expand #000050 Phase-2 scope
#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.
2026-05-12 12:17:04 -04:00
b5da0473a1
docs/#000012: refresh the header status line — Phase 1c landed (was stale)
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.
2026-05-12 11:47:04 -04:00
eba4c3a545
#000006 — +30 emergent cycles (2026-05-12); verifier-ladder health re-confirmed
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.
2026-05-12 11:28:57 -04:00
1d4d7c549c
docs: lexical-first-rationale.md — why the cheap retrieval path is the default
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.
2026-05-12 09:23:50 -04:00
906606072d
ticket #000039: close — Phase 0 + Phase 1 landed; Phase 2 split to #000050
#000039 (the optional sqlite-vec retrieval backend) closed 2026-05-12.
Shipped: Phase 0 doc + Phase 1 — arborist/search/vec.py (VecBackend,
chunk_vecs vec0 + vec_meta sibling tables, embed_documents
incremental/--rebuild, pluggable Embedder w/ fastembed bge-small-en-v1.5
default), CLI (arborist embed [--limit/--batch-size/--quant/--rebuild]
+ search --backend vec + ingest --embed eager opt-in), [vec] extra,
--quant {float32,int8} with the int8 head-to-head (3.8-4x smaller,
recall ~= float32 — int8 is the production config), ingest integration
+ idempotency (§14), embed-throughput measurement (§14.6 — ~4 chunks/s
contended, ~2.4 GB int8 full-corpus, full backfill abandoned as a
days-long batch job, non-vec ingest unchanged), 16 vec tests.
Demonstrated on crawl_appliedcombinatorics_org.db + a 54K-chunk partial
on wiki shard 000. UNGROUNDED hits, never proof path; vec config folds
into governance_policy_hash (noted in Phase 1, wired in Phase 2).

Phase 2 (RRF hybrid fusion in query.py — wire VecBackend as a 5th
retrieval route alongside the 4 FTS5 routes) → new ticket #000050,
gated on (a) a corpus backfill and (b) a recall bench clearing the
5pp floor. New doc-only scaffold docs/tickets/ticket-000050-vec-rrf-
hybrid-fusion.md (the design already lives in #000039 §4.2 + §8;
#000050 is the tracked continuation). Next ID 000050 -> 000051.
TICKETS.md: #000039 row -> closed, #000050 row added.

(Working tree also has parallel-clone work — ticket-000048-*.md
modified, ticket-000049-*.md untracked — not touched here.)
2026-05-12 08:33:45 -04:00
20f6061f83
#000048 closed; #000049 opened — the attribution-aware verifier is its own ticket
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.
2026-05-12 08:31:57 -04:00
2ad2dceada
ticket #000039: record embed-throughput measurement + ingest-stays-fast rationale (§14.6)
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.
2026-05-12 07:14:12 -04:00
5428b3b1d6
#000048 — fold the step-2.2 finding through the ticket + the cross-refs
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.
2026-05-11 17:34:18 -04:00
bbfd2ddc17
qa: providence_cache INSERT — ON CONFLICT(cache_key) DO NOTHING
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).
2026-05-11 17:29:12 -04:00
f6ac72270c
#000048 step 2.2 — attempted, reverted (not feasible as a conservative lexical change)
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.
2026-05-11 17:27:43 -04:00
1908fba162
docs/#000018 §9.2: mark resolved — φ_PRG = HMAC-SHA-512 (#000035 closed)
#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.
2026-05-11 17:20:48 -04:00
7bb11ed62f
#000048 step 2.4 — parse_pointer_claims clause segmentation
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.
2026-05-11 17:09:06 -04:00
4b85eb1fcf
#000035 closed — v7 substrate spec into the repo; § 9.10 folded in
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.)
2026-05-11 17:06:19 -04:00
9899a33b7b
#000048 step 2.1 — verify_quotes entity salient-token-disagreement gate
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.
2026-05-11 13:57:45 -04:00
06f5a11651
ticket #000039: --quant {float32,int8} + int8 head-to-head
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.)
2026-05-11 13:16:51 -04:00
7a64939e2b
chain-check: detect audit-chain forks + extra genesis rows
`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.)
2026-05-11 13:01:37 -04:00
9559c2ed31
#000048 — open: verifier upgrade for the #000046 hard-pack headroom
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.
2026-05-11 12:53:01 -04:00
724f433790
#000046 closed — verify_quotes paraphrase numeric-agreement gate (Phase 3)
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.)
2026-05-11 11:03:02 -04:00
86d47e6aee
ticket #000039: ingest integration + incremental embed (both models)
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.)
2026-05-11 10:39:24 -04:00
0c1bad8494
#000046 Phase 2 — HARD Formulate tier (second below-ceiling pack)
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.)
2026-05-11 08:40:44 -04:00
af870bbf23
store: append_audit atomic head-read+insert — no audit-chain forks
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).
2026-05-11 08:29:57 -04:00
3ea27aa471
#000047 — close: delta_aggregator knob on ForkScore (Option D)
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.
2026-05-11 08:27:38 -04:00
38d9116c88
ticket #000039 Phase 1: sqlite-vec semantic retrieval backend
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.)
2026-05-11 08:23:29 -04:00
01d697f961
#000046 Phase 1 — HARD live-path Falsification tier (first below-ceiling pack)
Closes the "everything is at rate 1.0 so fork_score's bench-Δ terms
are inert" gap the #000025 §10.14 calibration surfaced — at least on
the 5F/falsification axis.

bench/fixtures/5f/falsification-hard-v1.jsonl — 12 near-misses, each a
FALSE/unsupported claim whose correct verdict is UNGROUNDED (recorded
in expected_reason). 8 of 12 are over-grounded by
arborist.qa.verify.verify_quotes at HEAD — its paraphrase
token-coverage strategy returns STRICT_PARAPHRASE, its entity-proximity
strategy returns HYBRID_ENTITY, both matching on incidental overlap
(shared entities/numbers, the same key terms stated in the opposite
direction) — so those tasks fail by design; the other 4 the verifier
handles correctly. Rate at HEAD = 4/12 = 0.333, stable (verify_quotes
is pure-lexical / deterministic). Built around the pre-documented gap
5f-fal-live-003.

make bench-5f-falsification-hard runs the pack; make
bench-fork-baseline-hard pins it to
bench/results/baseline-falsification-hard.json. Both targets `|| true`
past the runner's nonzero-on-failures exit (8 fixtures fail by design;
the JSON is still written).

tests/test_bench_batteries.py — test_5f_falsification_hard_pack_below_ceiling
(pins rate 4/12, source=live, every fixture asserts UNGROUNDED, the 8
fails are over-grounds not abstentions) +
test_fork_score_positive_gamma_5f_on_hard_falsification_improvement
(the worked example: fork_score(parent={5f/falsification: 1/3},
child={5f/falsification: 1.0}) → gamma*Delta5f ≈ +0.133 > 0, verdict
ACCEPT, no regression flags — the bench Δ-rate carrying signal it
can't carry while every canonical pack is at ceiling).

NOT in `make bench-5f` / `make bench-5s5t5f` / `make
bench-fork-baseline` / `runner --all` — the hard pack is a separate,
deliberately-failing artifact pinned on its own.

#000046 flipped to "in progress · Phase 1 landed"; closure pending an
actual verify_quotes tightening that lifts the rate (a separate,
larger task). ticket-000012 §8 §4 + TICKETS.md row updated.

Full suite: 2314 passed, 28 skipped.
2026-05-11 08:16:01 -04:00
a53afa6148
#000046 + #000047 — open the two #000025 §10.14 follow-up tickets
The 5F threshold calibration (#000025 §10.14) surfaced two loose ends
that #000012's v8 acceptance protocol owns; opening them as proper
proposals so they don't get lost.

#000046 — Harder 5S/5T/5F fixture tier (below-ceiling baselines).
Every canonical 5S/5T/5F pack runs at rate 1.0 at HEAD, so fork_score's
α·Δ5s+β·Δ5t+γ·Δ5f terms are inert (a child can only move them ≤ 0).
The embedded packs can't be made "harder" (their evaluator is the
gold); a harder tier is necessarily a live-path tier where a real,
imperfect arborist surface produces wrong/partial output. Recommends
Option A scoped narrow: a `falsification-hard-v1.jsonl` built around
the verify_quotes entity-strategy gap already documented by
5f-fal-live-003, confirm the rate sits below 1.0 and is stable, pin it
via `make bench-fork-baseline-hard`, demonstrate a planted surface fix
lifting the rate. Status: open · awaiting go/no-go.

#000047 — ForkScore _delta_* aggregator (mean vs max vs sum). _delta_*
means over a battery's 5 sub-batteries, so a single-sub gain is worth
a fifth of face value (the 5× dilution from the §10.14 calibration:
+0.06 on one 5F sub → MARGINAL, same +0.06 on all five → ACCEPT).
Recommends Option D: parameterize delta_aggregator on WeightSet,
default "mean", record it in the breakdown + fork_score_branches; bump
ESTIMATOR_VERSION only if the default changes. Defer implementation
until #000046 lands a below-ceiling baseline so the aggregator choice
is benchable, not guessed. Status: open · awaiting go/no-go; parks
behind #000046.

TICKETS.md index rows + Next ID bump (000046 → 000048) landed in
8599ce3 (swept in by a concurrent commit; content is correct).
ticket-000012 §8 §3/§4 updated to name #000047 / #000046.
2026-05-11 08:03:45 -04:00
8599ce3b2c
ticket #000036: add KAT-regen tooling + close
"One more iteration then close" (fox): added committed KAT-regeneration
scripts for both the T3 calculator and φ_PRG — the regen step was a
throwaway temp script before; now it's reproducible and the phi_prg
test's skipif reason ("run scripts/generate_phi_prg_kat.py") points at
a file that exists. Then closed #000036.

New scripts:
- scripts/generate_t3_bound_kat.py — regenerates
  bench/fixtures/t3-bound/known-answer-tests.jsonl from a fixed 12-config
  list (the §7 worked examples under max_envelope + non-default-C_B*
  + g=0 edge + explicit-b1_model pins for the other three models).
- scripts/generate_phi_prg_kat.py — regenerates
  bench/fixtures/phi-prg/known-answer-tests.jsonl from a fixed 10-entry
  list (placeholder/random seeds, one-bit-flip variants, block-boundary
  dim_h=16/17, 4096 counter-rollover stress).
- Both verified to reproduce the committed fixture data lines byte-
  for-byte (only the header comments changed, to reference the script).
  Each docstring states: run after any algorithm change, then bump the
  module version (CALCULATOR_VERSION / PHI_PRG_VERSION) so the fixture's
  version field changes too.

Doc/test:
- test_t3_bound_calculator.py skipif reason now references the regen
  script (matches the phi_prg test pattern).
- #000035 §3.3 + t3-bound.md §10.1 reference the regen scripts.

Closure (#000036):
- Status → closed · 2026-05-11 in the ticket file + TICKETS.md row.
  Phase 1 + dav1d Tier-1/Tier-2 (Option B in v1) + KAT-regen tooling
  all landed; all §5 acceptance criteria met; both dav1d closure
  blockers cleared. Continuation: empirical C_B1/C_B2/C_B3 tightening
  under #000043 (parks on v7 deployment data); landing the bound's
  framing into a v7 plastic-training spec parks on that spec gaining
  a deployment target; R2's architectural integrations (Merkle audit-
  event commitment, SQD canonicalization, CTI clause-lattice, 5F
  trigger, ForkScore security-risk) are separate tickets if wanted.
- t3-bound.md header flipped to "closed 2026-05-11".

Full suite: 2312 passed, 28 skipped.
2026-05-11 08:02:25 -04:00
101b101281
ticket #000035: flip φ_PRG counter big-endian → little-endian to match v7 TLV
v7's canonical integer byte-order was confirmed little-endian by
inspecting merkle-agi-dag_v7.txt §A1 — every to_bytes / astype in the
TLV encoding is little-endian (TLV length prefixes to_bytes(4,'little'),
enc_int to_bytes(8,'little'), quantized tensors '<i8'); no big-endian
anywhere. Per dav1d's 2026-05-11 review rule ("if v7 TLV canonical
integer encoding is little-endian, flip §3.4 to little-endian before
KAT freeze"), flip done — this is the -le variant.

Implementation (arborist/substrate/anchor_prg.py):
- PHI_PRG_VERSION → "phi-prg-v1-hmac-sha512-le" (still "v1";
  the -le suffix records the endianness; future re-flip MUST bump).
- _expand: counter.to_bytes(4, 'big') → 'little'.
- _bytes_to_floats: int.from_bytes(..., 'big') → 'little' (the
  uint32-word interpretation, for full consistency with v7).
- Module + function docstrings updated: little-endian throughout,
  with the merkle-agi-dag_v7.txt §A1 verification note.
- Note: at counter=0 the bytes are identical regardless of
  endianness, so 5 of the 10 KAT entries (dim_h ≤ 16, single block)
  keep the same output_sha256; the 5 multi-block entries (dim_h 17/
  32/64×3/4096) change.

KAT fixture (bench/fixtures/phi-prg/known-answer-tests.jsonl):
- Regenerated under the little-endian counter. Each entry now also
  carries a "version" field (phi-prg-v1-hmac-sha512-le). Header
  comment updated.

Tests (tests/test_anchor_prg.py, 30 → 31):
- test_module_exports_version_string: assert the -le suffix.
- test_bytes_to_floats_midpoint_maps_to_zero: 2^31 is b'\x00\x00\x00\x80'
  in little-endian, not b'\x80\x00\x00\x00'.
- New test_bytes_to_floats_reads_little_endian: pins the byte-order
  so an accidental re-flip is caught.
- test_phi_prg_first_block_matches_direct_hmac: uint32-word reads
  little-endian (counter=0 bytes unchanged either way).
- test_phi_prg_known_answer_tests: assert kat['version'] == module
  version when present.

Spec text (#000035 §3.4): folded the little-endian variant of
dav1d's §9.10 wording — counter_le32, uint32_le word reads, an
"all integers little-endian, matching v7 TLV §A1" preamble, and an
"Endianness — RESOLVED 2026-05-11" note replacing the open
big-vs-little question. soft-hash-channel-analysis.md §9.2/§11 +
#000035 status + TICKETS.md row updated. AUTOCOUNT for
test_anchor_prg.py bumped 30 → 31; PHI_PRG_VERSION refs in docs
bumped to -le.

Full suite: 2312 passed, 28 skipped.
2026-05-11 07:47:35 -04:00
d78dccc8ed
#000025 §10.11 + §10.13 + §10.14 — close the 5F battery
Closes the three open Phase-1b items of #000025; every §10 closure
criterion is now met, so the ticket flips to closed.

§10.14 — ForkScore threshold-calibration handoff to #000012.
bench/scripts/fivef_threshold_calibration.py (make bench-5f-threshold-
calibration) runs the canonical 5S/5T/5F packs + the 5F live packs and
reports baseline rates, observability granularity (1/n), and fork_score
verdicts on the parent vs synthetic child perturbations →
bench/results/5f-threshold-calibration-2026-05-11.md. Findings written
into ticket-000012 §8: keep SIGNAL_FLOOR / HARD_REGRESSION_FLOOR at
0.05; the small 5S packs (syntax n=10, semantics n=8) are coarser than
the floors so any regression there trips hard-reject (intended zero-
tolerance); the 5x averaging dilution in _delta_*; ceiling saturation
(every pack at 1.0 -> delta-rate terms <= 0). No constant change
shipped. 6 tests in tests/test_fivef_threshold_calibration.py.

§10.13 — feedback latency / efficiency on real workload.
run_feedback_loop now computes feedback_latency (listed in §5.5 since
Phase 1a, never implemented) — wall-clock seconds to apply a live
chain against its temp shard, surfaced per-task
(feedback_latency_seconds) + battery (feedback_latency_mean_seconds,
feedback_live_task_count). For live chains feedback_efficiency's cost
denominator switched from len(chain) (count of requested ops) to the
persisted footprint _persisted_cost = audit-event rows the chain
actually wrote + their body bytes / 1e6. Embedded chains keep
len(chain) and report feedback_latency_seconds = None. Latency is a
wall-clock field (run-to-run variable, like BatteryResult.timestamp)
and is not a fork_score input. 3 tests in tests/test_bench_batteries.py.

§10.11 — real selfmodel finetuning chains.
bench/scripts/selfmodel_chain_snapshot.py (make bench-5f-selfmodel-
snapshot) appends one chained SelfModel snapshot per run to a
persistent shard (~/.arborist/shards/selfmodel-chain.db, override via
ARBORIST_SELFMODEL_CHAIN_DB) with one CapabilityClaim per sub-battery
(metric = "5S-syntax" etc., measured_value = that pack's rate,
eval_digest = the pack's fixture digest, threshold = SIGNAL_FLOOR).
snapshot() auto-parents, so each snapshot is a distinct root and the
lineage grows by one per run. run_finetuning gains a third dispatch
mode — shard-chain (gated on a task's selfmodel_shard key) — via
_chain_finetuning_measure: reads the two most-recent snapshots
(latest() = child, its parent_selfmodel_root = parent) and measures
improvement on target_capability between them. This is the real
lineage replacing Phase-1a's synthetic parent->child pairs; the
chained delta reflects genuine cross-run drift (0.0 today — the
embedded packs are at ceiling). Operator pack
bench/fixtures/5f/finetuning-shardchain-v1.jsonl (6 tasks) + make
bench-5f-finetuning-shardchain; not in `make bench-5f`, `make test`,
or a fresh checkout (a missing/too-short chain fails honestly). The
real chain shard was bootstrapped 2-deep on 2026-05-11; make
chain-check-shards reports 0 breaks on it (and all other shards).
10 tests in tests/test_selfmodel_chain.py.

Full suite: 2311 passed, 28 skipped.
2026-05-11 07:41:37 -04:00
1dc47e01b3
analyze: walk audit chains read-only — no migration DDL on a read op
`arborist analyze`'s per-shard audit-chain loop opened each shard with
connect(), which on first open of a file runs executescript(SCHEMA_SQL)
plus all forward migrations — DDL on a read-only walk, on 6 GB shards,
and (until 71c9848) the trigger for the fork_score_branches crash.

New store.connect_readonly(db_path): mode=ro URI, no schema bootstrap,
no migration probes. _cmd_analyze's chain-check loop uses it.
2026-05-11 07:36:39 -04:00
71c98487b7
store: idempotent forward migrations — survive concurrent connect()
`arborist analyze` crashed during orientation with
`sqlite3.OperationalError: table fork_score_branches already exists`:
check-sqlite_master-then-CREATE forward migrations let two connect()
calls racing a fresh shard both pass the probe and both issue CREATE.

- CREATE TABLE/INDEX inside the _migrate_* helpers now IF NOT EXISTS
  (probe stays as the fast-path skip).
- _migrate_audit_mode's ALTER ADD COLUMN routes through
  _add_column_if_missing (PRAGMA fast-path + duplicate-column catch).
- connect() raises busy_timeout=5000 for the one migration pass so a
  racing connect() waits on a peer's _rebuild_* write txn.
- test_store_migration_concurrency.py: source-level "all CREATE is
  IF NOT EXISTS" pin, busy_timeout assertion, 8-thread connect smoke.
- test_store_migration_memoization.py: _MigrationProbeCounter.NAMES
  was missing two migrations; synced + de-hardcoded counts.
2026-05-11 07:28:35 -04:00
e894634406
ticket #000035: fold dav1d 2026-05-11 §3.4 review into spec text + impl
dav1d returned the §3.4 φ_PRG anchor-map review with a decision set:
HMAC-SHA-512 / 32-byte seed / uint32-be counter from 0 / SHALL-replace
all LOCKED; manifest field renamed; float-map prose corrected; two
ADDs (exhaustion guard + seed-independence rule); M1-policy separation.

Spec text (#000035 §3.4):
- Folded dav1d's full corrected §9.10 wording (RESPONSE_1 §1).
- Manifest field phi_prg_seed → anchor_prg_seed (purpose-scoped, not
  implementation-scoped; phi_prg_seed kept only as a code-local alias;
  phi_seed / m1_anchor_seed rejected as too vague / too policy-tied).
- Float map 2·(u32/2^32)−1 unchanged (KAT compat) but the prose now
  says "uniform over a 2^32-point grid in [-1, 1) with negligible
  finite-grid mean −2^−32" — NOT "unbiased". -1.0 reachable, +1.0
  not. If exact zero-mean is ever needed → midpoint map x =
  2·((u32+0.5)/2^32)−1 with a PHI_PRG_VERSION bump + new KATs, never
  a silent change.
- Added dim_h ≤ 16·2^32 exhaustion guard (4-byte counter ceiling).
- Added seed-independence + single-purpose-seed requirements (seed
  must be generated independently of model/data, not adversary-
  selected, not reused for other PRG domains — no domain-separation
  tag in v1).
- Added §9.10.1: M1 enablement is a mitigation-selection-policy
  decision (e.g. skippable under #000034 NO_ALIGNMENT), not a §9.10
  function-definition question; "MUST NOT claim M1 while still using
  embed_hard_to_vec" prevents fake-M1 deployments.
- Added an endianness-confirmation note: big-endian is pinned to the
  impl + KATs; flip only if v7 TLV convention turns out little-endian
  (would need a PHI_PRG_VERSION bump).
- SHALL-replace wording kept (RFC-2119 strong mandate inside M1).

Implementation (arborist/substrate/anchor_prg.py):
- New dim_h > 16·2^32 → ValueError guard (clean message naming the
  ceiling rather than overflowing the counter deep in _expand).
- bool dim_h now rejected explicitly (isinstance(True, int) is True).
- Module + function docstrings updated: manifest field is
  anchor_prg_seed; seed-independence / single-purpose rules; corrected
  float-map distribution wording (negligible mean −2^−32, not exactly
  zero); endianness note.

Tests (tests/test_anchor_prg.py, 27 → 30):
- test_phi_prg_rejects_bool_dim_h (True/False params).
- test_phi_prg_rejects_dim_h_above_counter_ceiling.

Doc cross-refs: soft-hash-channel-analysis.md §9.2 + §11 status note
the dav1d-reviewed §9.10 wording + anchor_prg_seed field name.
#000035 ticket status + TICKETS.md row updated. AUTOCOUNT markers
for test_anchor_prg.py bumped 27 → 30 across 5 doc files.

Full suite: 2291 passed, 28 skipped.
2026-05-11 07:14:38 -04:00
1d1a942d57
ticket #000036 Tier-2: dav1d Option B (conservative B1 envelope) applied in v1
Per fox: apply the conservative max_envelope B1 model by changing the
v1 calculator's default — NOT by forking a v2. CALCULATOR_VERSION stays
"t3-bound-v1-bottou-refinement" (the descriptor names the unchanged B3
term); b1_model is echoed in the output AND the inputs dict so KAT
replays are unambiguous about which model produced a row.

Calculator (bench/scripts/t3_bound_calculator.py):
- New b1_model kwarg + --b1-model CLI flag, choices:
    max_envelope         (default)  max(fraction_channels, aggregate_bias)
    fraction_channels               g · W · log₂(1 + G/σ)
    aggregate_bias                      W · log₂(1 + g·G/σ)
    effective_control_v1            g · W · log₂(1 + g·G/σ)   (old non-worst-case)
- Default is now max_envelope — genuinely upper-bounding across both
  interpretations of g (dav1d review §3 closure blocker, RESOLVED).
- Every output reports all three concrete B1 variants
  (B1_fraction_channels / B1_aggregate_bias / B1_effective_control_v1),
  b1_selected, and both SNR readings (snr_grad = g·G/σ,
  snr_per_channel = G/σ) regardless of which b1_model was requested.
- model_assumptions[] now carries f"B1_model_{b1_model}".
- inputs echo now includes c_b1/c_b2/c_b3/b1_model (replay-complete).
- Invalid b1_model rejected with a ValueError naming the field.
- Baseline I_window: 625.8716 (effective_control_v1) → 6183.0154
  (max_envelope: B1=aggregate_bias 5849.63 dominates fraction_channels
  1729.72), certification_status NOT_CERTIFIED_BY_BOUND at W=10000.

KAT fixture (bench/fixtures/t3-bound/known-answer-tests.jsonl):
- Regenerated 2026-05-11 — 12 entries: the 8 §7-derived configs under
  the new max_envelope default, a g=0 edge case, plus explicit-mode
  pins for effective_control_v1 / fraction_channels / aggregate_bias.
- Each entry carries b1_model, expected_b1_selected,
  expected_b1_{fraction_channels,aggregate_bias,effective_control_v1},
  expected_snr_per_channel, expected_certification_status.

Tests (tests/test_t3_bound_calculator.py, 75 → 83):
- test_t3_bound_known_answer_tests no longer skips (fixture active);
  pins b1_model, b1_selected, certification_status + numbers, tolerates
  optional new fields on older fixtures.
- New: test_b1_max_envelope_exact_formula, test_invalid_b1_model_rejected,
  test_cli_b1_model_flag (effective_control_v1 / fraction_channels /
  aggregate_bias). test_b1_exact_formula renamed
  test_b1_effective_control_v1_exact_formula and now passes the explicit
  model. Updated baseline / below-256 / CLI tests for the new numbers.

Doc (docs/soft-hash-channel-t3-bound.md):
- Header + §0 + §3.1 + §6 + §7 (worked examples) + §8 (operator
  guidance W-solving) + §10 (closure blockers RESOLVED) + §10.1 +
  §11 (calculator schema) + §12 all updated for the max_envelope
  default. §8: target-256 W drops from ~4196 to ~415 steps under the
  conservative model — the ~10× cost of not assuming which g-reading
  holds; operators who can measure effective-control applies can use
  --b1-model effective_control_v1 for the looser W (a calibration
  claim they must justify, not a default).

Status (#000036 ticket + TICKETS.md): both prior dav1d closure
blockers cleared (B1 worst-case model + active KAT fixture); remaining
= fox's final close-or-iterate call.

AUTOCOUNT markers bumped 75 → 83. Full suite: 2288 passed, 28 skipped.
2026-05-11 07:06:50 -04:00
dbe824944a
#000012 Phase 1c follow-through: wire #000037 §12 Trigger 1 probe to fork_score_branches
trigger_1_branch_density in bench/prometheus_sigma_trigger_probe.py read
the fork_score_branches table via branch_set_density() instead of the
"density check not yet implemented" stub. It groups rows by branch_set_id,
fires when the most-recently-recorded checkpoint carries >= 4 branches
(BRANCH_DENSITY_FLOOR), and surfaces n_checkpoints / latest_density /
max_density / n_checkpoints_clearing_floor in the markdown report so
section 12's "regularly" qualifier stays visible. Density sums across
shards per branch_set_id.

With no branch sets persisted yet the probe reports "table present but
empty across shards" (data_available True, fires False) rather than a
false negative. Re-ran the probe against the live shards:
bench/results/prometheus-sigma-triggers-2026-05-11.md.

6 new tests in tests/test_prometheus_trigger_probe.py (probe loaded via
importlib): no-table to no-data, empty-table to data-available-no-fire,
latest-checkpoint->=4 to fires, earlier-dense-but-latest-sparse to no-fire,
density-sums-across-shards, report-renders-density-lines.

Updated #000012 Phase 1c landing receipt, #000037 section 12 Trigger 1
note, and the TICKETS.md index rows for both. Pure measurement: no
mutation, no LLM call, no schema change.
2026-05-11 06:56:09 -04:00
da62f8047c
ticket #000036 Tier-1: apply dav1d 2026-05-11 review polish (no math change)
dav1d's review (RESPONSE_1 + RESPONSE_2) returned 2026-05-11. This
lands the Tier-1 items — everything that doesn't change numeric
outputs or invalidate the KAT discipline. The Tier-2 B1 conservative-
envelope (v2 calculator) is a separate decision and stays a closure
blocker.

Calculator (bench/scripts/t3_bound_calculator.py):
- Recommendation wording: "M2's single-window guarantee is broken"
  → "this conservative bound CANNOT CERTIFY M2's residual". An upper
  bound exceeding 256 bits means we cannot certify, NOT that the
  adversary can steer 256 bits — the prior wording overclaimed.
- New structured output fields: b1_model ("effective_control_v1"),
  certification_status ∈ {CERTIFIED_BY_BOUND, NOT_CERTIFIED_BY_BOUND},
  certification_threshold_bits (256), model_assumptions[]. Callers
  read a machine-readable status, not just prose.
- Input validation hardening: _require_finite_float / _require_positive_int
  helpers reject bools (isinstance(True, int) is True in Python — a
  real leak risk for a security calculator) and NaN / ±inf for every
  numeric input and constant.
- gradient_fraction = 0 now accepted (no T2 surface; B1 = 0; T3's
  LR + batch-order channels still contribute) — improves component
  isolation. CLI help + module docstring updated accordingly.
- Numeric outputs UNCHANGED: baseline still 625.8716 / 292.4813 /
  300.0 / 33.3904; b1_model stays effective_control_v1; KAT discipline
  intact.

Tests (tests/test_t3_bound_calculator.py, 53 → 75):
- Hard-coded cwd="/home/fox/git/arborist" → pathlib.Path(__file__).
  resolve().parents[1] so the suite runs on any checkout.
- New: test_gradient_fraction_zero_accepted, test_bool_rejected_for_int_fields,
  test_bool_rejected_for_float_fields, test_nonfinite_numbers_rejected,
  test_output_carries_b1_model_and_certification_fields,
  test_certification_status_certified_below_threshold.
- test_recommendation_exceeds_sha256 now also asserts "CANNOT CERTIFY"
  + certification_status == NOT_CERTIFIED_BY_BOUND.

Doc (docs/soft-hash-channel-t3-bound.md):
- §0 reworked into a reviewer brief recording dav1d's findings
  (§2 accepted, §4 accepted, §5 accepted as model-bound, §3 = closure
  blocker, wording/validation = applied).
- New §3.1: the B1-double-g issue spelled out — effective_control_v1
  vs fraction_channels vs aggregate_bias vs max_envelope, with the
  baseline-spread table (292 / 1730 / 5850 / 5850 bits); v2 path
  described.
- §5: "B3 is a model-bound, not a directly-quoted theorem" note.
- §10: items 1-2 are now the closure blockers (B1 envelope v2; active
  KAT fixture); items 3-7 are tightening paths (#000043). New §10.1
  records what the 2026-05-11 hardening pass already landed.
- §11: calculator-output example updated to show the new fields +
  corrected recommendation wording.
- §12: references add the dav1d review + clarify Bottou-Bousquet
  "inspires" (not "underlies") the §5 model-bound.

Status (#000036 ticket + TICKETS.md row): review-returned + Tier-1-
applied; closure blockers = B1 v2 envelope (awaits fox go/no-go) +
active KAT fixture. R2's architectural integrations (Merkle audit-
event commitment, SQD canonicalization, CTI clause-lattice, 5F
trigger, ForkScore security-risk) noted as out-of-scope (separate
tickets if wanted).

AUTOCOUNT markers in docs/calculator-test-patterns.md +
docs/warrant-substrate-cookbook.md bumped 53 → 75.

Full suite: 2264 passed, 28 skipped.
2026-05-11 06:39:27 -04:00
d53115efd7
#000012 Phase 1c: branch-set persistence — fork_score_branches table + CLI
Phase 1a scores one (parent, child) fork at a time; Phase 1b is the
consensus paper. Neither persists multiple candidate branches at the
same checkpoint — and #000037 §12 Trigger 1 ("ForkScore regularly
receives ≥4 candidate branches per checkpoint") gates the multi-
branch path of the Prometheus-Σ controller on this data existing.
Phase 1c lands the missing seam.

Schema (arborist/store.py): _migrate_fork_score_branches creates the
sibling table with PK (branch_set_id, branch_id) + indexes on
branch_set_id and parent_root. Sibling — never enters
audit_events.event_hash preimage, so re-scoring or back-filling
cannot break the audit chain.

Helpers (arborist/substrate/fork_score.py): persist_branch_score
upserts one row via ON CONFLICT (branch_set_id, branch_id) DO UPDATE
so re-scoring the same fork under the same checkpoint is a clean
overwrite, not a duplicate. branch_set_density(conn, branch_set_id)
returns the count of distinct branches recorded under a checkpoint
— the function the #000037 §12 Trigger 1 probe reads.
ESTIMATOR_VERSION = "fork-score-v1" pins the producer generation on
every persisted row.

CLI (arborist/cli.py): arborist substrate score gains six new flags
(--branch-set, --branch-id, --parent-root, --child-root,
--persist-shard, --weights-id). Default off — --branch-set absent
preserves Phase 1a pure-function semantics for every existing
caller. When present, requires --parent-root and either --branch-id
or --child-root; missing inputs return exit code 2.

Tests (tests/test_fork_score.py, count 18 → 23): migration creates
the table + both indexes; persist writes one row carrying
parent/child roots + verdict + weights_id + estimator_version;
upsert on the PK refreshes child_root + weights_id + recorded_at
without duplicating; branch_set_density counts per-checkpoint and
ignores cross-set rows; breakdown_blob round-trips as canonical
JSON whose values sum to the persisted score.

Status sync: #000012 §7 Phase 1c flipped from "proposed, not yet
open" to "landed 2026-05-10" with the original proposal preserved
below as design log. TICKETS row 117 mirror-updated. AUTOCOUNT
counters in #000012 + cookbook bumped 18 → 23 plus the cookbook's
fork_score.py LOC row refreshed (298 → 386 module, 403 → 609
tests, density 1.35 → 1.58).

End-to-end smoke verified: arborist substrate score writes a
fork_score_branches row with the expected schema (verdict / weights_id
/ estimator_version) and the row survives a clean SQLite read.
2026-05-10 20:12:30 -04:00