Commit graph

26 commits

Author SHA1 Message Date
dcd2783507
merge: burn docs/cores (worktree #4) 2026-04-28 18:02:17 -04:00
bc67ad80e8
mesh: 'pull' CLI verb — fetch document body from a peer
Closes the request half of the gossip loop. MeshWireClient already
verifies signature + Merkle root on delivered bodies; this verb adds
the CLI surface, gating, local re-ingest through the standard chunker,
and a 'mesh_pulled' audit event. Re-ingest is rejected if the local
document_root differs from the requested root.
2026-04-28 17:33:27 -04:00
58d5656352
cli: extend 'aborist burn' to documents and cores
burn --kind {providence,document,core}; default 'providence' preserves
the prior --cache-key surface (no breaking change).

Children gates for document/core leaves:
  derivations.src_root        downstream cores derived from this doc
  edges.dst_root              other docs link to this doc
  providence_cache.source_root Q&A grounded in this doc

derivations has ON DELETE CASCADE on BOTH core_root and src_root, so a
naive DELETE FROM documents would silently cascade-prune downstream
derivations and orphan deeper cores. Gate before delete; --force to
override with the count recorded in the audit body.

Burn manually clears chunks_fts (FTS5 has no FK) and outbound edges
(no FK either) before deleting the document. chunks + merkle_nodes +
inbound derivations cascade via FK.

Audit event types are document_burn / core_burn — distinct from
providence_burn so chain consumers can tell leaf kinds apart.

CLAUDE.md "Cores never evict" remains intact: burn is operator-driven
removal, evict is the automated cold-tier compression that touches
only kind='surface'.

Makefile: KIND=providence|document|core selects the leaf type;
KEY for providence, ROOT for document/core. Single 'burn' target keeps
the surface DRY.

Tests (tests/test_burn_doc.py, 13 new):
- happy path: chunks + merkle_nodes vanish via cascade
- refused on derivation downstream / providence ref / incoming edge
- --force overrides and records counts
- core burn happy path (no children) preserves parent surface
- core burn refused with downstream derivation
- audit chain integrity preserved (no dangling prev_event_hash)
- CLI invocations for document, core, default-providence, refused

Full suite: 247 passed, 1 skipped.
2026-04-28 17:32:40 -04:00
5427a61768
mesh: 'serve' and 'sync' CLI verbs over the wire layer
Wires the gossip-wire core (commit f141bab) into the user-facing CLI:

  aborist mesh serve --host 127.0.0.1 --port 8400
    Spawns MeshWireServer, blocks until SIGINT. Refuses if mesh isn't
    initialized or .enabled is off — same gate every other mesh verb
    enforces. Stdout is unbuffered JSON status lines.

  aborist mesh sync --peer http://other.example:8400 [--limit N]
    Enumerates local documents (most-recent N, default 100), fires one
    ANNOUNCE_ROOT per document via MeshWireClient, reports counts of
    acknowledged vs. errored. Pulling missing roots back from the peer
    is a v2 addition — this verb pushes only.

Tests (6): gating refusals on uninitialized + disabled mesh; sync
round-trip with two peers proving Alice's audit chain gains one
'mesh_received' per Bob announce; unreachable-peer reporting; serve
smoke-test via start_in_thread + GET /mesh/info.
2026-04-28 17:05:31 -04:00
4a994b7ead
cli: 'aborist burn' — kindergarten leaf delete with audit event
Burn deletes a providence_cache row outright; falsify (the existing
verb) flips state and keeps the row. Burn is for early/scratch use
during a tree's genesis when downstream consumers haven't ingested
the answer yet.

Children gate: refuses if any falsifications reference the cache_key.
Caller can override with --force; the audit event records forced=True
plus the child count at burn time so the chain can reconstruct what
was orphaned. Refusal is non-destructive AND skips the audit event so
scripts can retry without polluting history.

Always writes a 'providence_burn' audit event regardless of force.
Body fields: cache_key, burned_audit_mode, burned_n_verified,
burned_state, question_text, reason, by_actor, child_falsifications_at_burn,
forced. The chain-break query (`make chain-check`) shows 0 after burn —
the new event extends the prior chain cleanly.

Surface:
  aborist burn --cache-key <hex> [--reason '...'] [--force]
  make burn KEY=<hex> REASON='...' [FORCE=1]

Tests (7): happy-path delete + audit event, refusal on children,
--force override, unknown-key not-found, chain-integrity preserved,
CLI happy path, CLI non-zero exit on refusal.
2026-04-28 17:03:30 -04:00
01a4390693
verify: layered strategies + entity policies, rename VISUAL → UNGROUNDED
- aborist/qa/verify.py: three strategies tried in sequence. quote uses
  sequential pairing (1st & 2nd `"`, 3rd & 4th, ...) which eliminates
  the phantom inter-pair captures regex pairing produced on adjacent
  quote pairs like `"title" prose "quote"`. span checks bullet/sentence
  lines verbatim. entity matches multi-word proper nouns; gated by
  policy ∈ {strict, hybrid, drop, proximity}. Default proximity
  promotes to STRICT only when N=3 verified entities cluster within
  W=300 chars in source — separates structural grounding (cast list,
  infobox) from incidental mention (scattered plot summary).

- aborist/store.py: schema CHECK now `('STRICT','HYBRID','UNGROUNDED')`.
  Migration helper rebuilds legacy `('STRICT','HYBRID','VISUAL')`
  tables via temp-table copy, translating VISUAL → UNGROUNDED in the
  SELECT. Idempotent — DDL inspection skips the rebuild on already-
  migrated DBs.

- aborist/cli.py: new `aborist reclassify` re-runs the verifier
  against existing live records under the current entity policy; no
  LLM calls. --compare runs all four policies side-by-side, --dry-run
  reports transitions without writing. Cold-source records skipped.
  One providence_reclassify audit event per changed row.

- AuditMode.UNGROUNDED replaces VISUAL across search backend, FTS5,
  test fixtures, CLAUDE.md. The substrate (Merkle-AGI) name was
  about FOR-style visualization; in the RAG layer the semantic is
  "no recoverable grounding," so the label now says that.

DEFAULT_QUERY_POLICY gains entity_policy + entity_proximity_n +
entity_proximity_window so any tuning folds into governance_policy_hash
and invalidates cache cleanly.
2026-04-28 16:58:31 -04:00
1ef0ac753a
qa: post-LLM faithfulness verifier sets STRICT/HYBRID/VISUAL audit_mode
Adds aborist/qa/verify.py — three-strategy verifier (explicit quotes,
bullet/sentence spans, multi-word proper nouns) that lexically checks
every claim against retrieved context under norm-v1 + lowercase. The
strategy that fires is recorded as verifier_method for diagnostics.
Entity strategy gates classification via an entity_policy
(strict/hybrid/proximity/drop) so a single proper-noun match no longer
overclaims STRICT — proximity (default) requires a cluster of 3+
verified entities within 300 chars.

Wires into ask() and query(). System prompts now require verbatim
quoted spans for every factual claim, restated via a user-turn
grounding_reminder one message before sources arrive (recent
user-turn instructions outweigh decayed system-turn rules under long
context in Hermes).

providence_cache gains 5 columns (audit_mode, n_quotes, n_verified,
unverified_quotes, verifier_method) with CHECK constraints. A
connect-time _migrate_audit_mode() ALTERs legacy DBs idempotently.
Cache hits return the persisted audit_mode rather than asserting
STRICT unconditionally.

CLI:
  emergent     list VISUAL/HYBRID records; --aggregate ranks
               unverified quotes by frequency (corpus-growth signal).
  reclassify   re-run the verifier against live providence records;
               cold-source records skipped; --dry-run reports
               transitions without writing; each change writes one
               'providence_reclassify' audit event.
2026-04-28 15:45:08 -04:00
1f5bf7318d
add corpus-level snapshots: single-hash identity for the forest
A snapshot_root is MerkleTree.build([sorted DISTINCT document_roots]).root
— one 32-byte hash naming the entire content-addressed forest at a
point in time. Two peers that ingested the same dump compute bit-
identical snapshot_roots, so cross-machine "are we synced?" becomes
an O(1) hash comparison; the same root doubles as the TF-IDF
scope_root we sketched in the mesh design (snapshot_root *is* a
scope), and pins Q&A answers to a verifiable corpus state.

Schema: one new table, additive over existing data.
  snapshots(snapshot_root PK, taken_at, audit_event_hash, doc_count,
            parent_snapshot, reason)
  + idx_snapshots_taken_at

API:
  compute_snapshot_root(conn, *, document_roots=None) -> (root, count)
  create_snapshot(conn, *, reason, parent_snapshot=None) -> dict
  verify_snapshot(conn, snapshot_root) -> dict (matches: bool)
  diff_against_current(conn, snapshot_root) -> dict (added/removed/unchanged)
  list_snapshots(conn, *, limit) -> list[dict]

CLI:
  aborist snapshot create [--reason "..."] [--parent <hex>]
  aborist snapshot list   [--limit N]
  aborist snapshot verify <snapshot_root>
  aborist snapshot diff   <snapshot_root>

Cross-shard: with --shards-dir + --db, the snapshot is computed over
the cluster-wide UNION view but persisted into args.db (a dedicated
snapshots store, conventionally ~/.aborist/shards/snapshots.db).
parent_snapshot auto-links to the latest prior snapshot in the
writer DB, giving a chain for free.

Each snapshot creation writes an audit_event of type 'snapshot_create'
with subject_root=snapshot_root, so the corpus's pinned states are
themselves tamper-evidently logged.

Defect caught + fixed during live test on the 2010 enwiki ingest:
verify and diff disagreed on the same connection because compute used
a list (with cross-shard duplicate document_roots) while diff used a
set. Two shards can land identical document_roots when canonicalize()
maps two structurally-similar pages to the same byte stream — rare
but real. Switched the underlying SQL to SELECT DISTINCT so a
membership snapshot is always order- AND multiplicity-independent.

Live test: 2010-11 enwiki corpus snapshotted at
  43797e46605de08dbab06cdcaf5be7ad78243b193c56f8580200dee6bcc7e1b9
  doc_count: 3,468,134 (after dedup)
verify + diff round-trip both report identical against current state.

11 new tests covering empty corpus, single-doc degenerate, order
independence, drift detection, audit-chain pinning, parent auto-link,
and idempotent creation on unchanged corpus. 136 tests + 1 skipped.
2026-04-27 21:29:10 -04:00
aa8caeeece
mesh: cryptographic foundation, off by default
Phase 1 of the federation/gossip layer fox sketched as the natural
extension of v9.8 admissibility's content-addressed identity. Two
peers ingesting the same dump already compute identical document_roots
and identical 8-dim cache_keys; the mesh layer is the wire-and-trust
plumbing that lets them dedup answers, exchange Merkle proofs, and
cleanly distrust an evicted member without a hard fork.

Cryptography (cryptography lib, audited):
  Ed25519       — every membership mutation + (future) gossip envelope
                  is signed by the actor's pubkey.
  X25519 ECDH   — wraps each epoch's symmetric mesh secret to every
                  current member's DH pubkey via HKDF-derived AEAD key.
  ChaCha20-P1305— AEAD for envelope payloads + per-member secret wrap.

State machine:
  mesh_identity   — singleton; this peer's keys + group name
  mesh_roster     — per-epoch (member_id, sign_pub, dh_pub, role)
  mesh_epochs     — epoch_id -> {started_at, started_event_hash,
                                  secret_envelope JSON, reason}
  meta:mesh.enabled flag — off by default; gates everything

Eviction works by rotating to a new epoch whose envelope omits the
kicked member. Their prior signatures stay verifiable (the older
roster row is retained), but any gossip from epoch+1 onward is
opaque to them — the secret was never shared with their pubkey.

Authority gate: only roster members with role='admin' can add or
kick. Self-kick is rejected explicitly. The last admin can't be
kicked. Schedule-rotate (refresh secret, no roster change) is open
to any current member as a session-hygiene op.

Audit-chain integration: every mesh state mutation writes an audit
event (mesh_init, mesh_enable/disable, mesh_epoch_rotate). The
epoch's started_event_hash backfills into mesh_epochs after the
audit row commits, giving each epoch a tamper-evident pin into the
ledger.

CLI subcommands: mesh init, mesh status, mesh enable, mesh disable,
mesh members, mesh add, mesh kick, mesh rotate. All read-only or
local-state-only — no network code paths in this commit.

The HTTP gossip wire (`mesh sync`, `mesh serve`) is the next phase.
Schema, cryptography, and roster state machine are all in place to
support it without further migration.
2026-04-27 19:00:24 -04:00
082143e158
add git and mercurial repo sources for self-play
GitRepoSource walks the working tree at HEAD via `git ls-tree` + `git show`,
yielding one Document per text file. URI shape `git://<repo>/file/<path>`
intentionally omits the commit hash — re-ingest after new commits produces
fresh document_roots that aborist's prior-doc detection chains via
`supersedes` edges, so the audit trail and Merkle tree grow as the repo
grows. Binaries skipped via NUL-byte + UTF-8 decode probes; >5 MB files
filtered out by default. Commit hash + timestamp + subject ride along in
`extra` (informational only; not part of the Merkle commitment).

MercurialRepoSource mirrors via `hg manifest` + `hg cat`. Same supersedes
semantics, same shape.

Makefile adds:
  make ingest-self                          (this repo -> aborist-self.db)
  make ingest-git GIT_REPO=/path/to/repo    (arbitrary git clone)
  make ingest-hg  HG_REPO=/path/to/repo     (mercurial)

Each lands in its own shard file alongside existing shards/grok.db,
keeping per-shard write paths independent of the wikipedia 4-way ingest's
WAL writer lock.
2026-04-27 18:17:39 -04:00
f90b7c69f0
add Phase IV Wikipedia XML + abstract sources
Drops in for any dated snapshot in dumps.wikimedia.org/archive — pages-articles.xml.bz2
and pages-meta-current.xml.bz2 (cur snapshot, single-revision-per-page) plus
pages-meta-history.xml.bz2 (multi_revision=True). abstract.xml feed yields
pre-distilled summaries at ~1/100th the chunk volume. Streams .bz2/.gz directly
via iterparse with bounded memory; same shard / resume contract as the SQL
source. Falls back to title-prefix namespace filtering when older export
schemas omit the per-page <ns> element (e.g. enwiki 20101011).

Makefile defaults target enwiki 20101011 (6.2 GB). Override with WP_XML_YEAR /
WP_XML_MONTH / WP_XML_DATE / WP_XML_LANG to fetch any other archived snapshot.
2026-04-27 17:24:28 -04:00
9ba06cd809
add Grok export source: conversations + media posts
aborist/sources/grok.py exposes two Source subclasses for ingesting
xAI's user data export:

  GrokExportSource    walks ttl/<period>/export_data/<user-id>/ and
                      reads prod-grok-backend.json. Yields one Document
                      per conversation with:
                        - URI: grok://conversation/<conversation-id>
                        - title: the conversation's auto-generated title
                        - content: full message text in turn order
                      source_type='grok_export'.

  GrokMediaPostsSource same export, but yields per media-generation
                      post (image/video prompts) under URI
                      grok://media/<post-id>.
                      source_type='grok_media'.

Both auto-walk down from the export root so callers can pass the
top-level directory xAI delivered (e.g., ~/Downloads/<user-uuid>/).

Wired into the CLI: aborist ingest --source {grok_export,grok_media}
accepts --path <export-root>. tests/test_grok_source.py covers the
walk + parse + Document shape with a fabricated mini-export fixture.

Once ingested, conversations become normal queryable docs in the
shard cluster — your prior chats become memory the corpus can
consult during RAG.

79 tests passing.
2026-04-27 13:49:42 -04:00
1abe6d4adc
falsification CLI: mark wrong cached answers stale/failed
aborist [--shards-dir DIR] providence --falsify CACHE_KEY \
        --state {failed,stale,quarantined} --reason "X" --by-actor NAME

Searches every shard for the cache_key (it lives in exactly one),
flips falsification_state on the providence_cache row, appends a row
to the falsifications log, and writes a 'falsify' audit event so the
chain captures the act with from_state/to_state/reason/by_actor.

Future cache lookups skip records whose state != 'live', so a
falsified answer never gets reused. The record itself is preserved
(history matters); only its admissibility changes.

Makefile target:
  make falsify KEY=<hex> REASON='why' [STATE=failed]

Demo on the live shard cluster: marked the AMD/Pentium-4 conflation
answer as failed, verified state flipped + audit row recorded +
falsifications log captured fox as actor + the Barton-clock-vs-PR
explanation as reason. Re-asking the same question generated a fresh
cache_key (corpus state shifted between attempts) so a new LLM call
ran — exactly the v9.8 admissibility behavior: each (8-dim cache_key)
is independently live/failed.

66 tests still passing.
2026-04-27 13:03:06 -04:00
76446d4349
add aborist activity — agent-readable recent timeline
New CLI: aborist [--shards-dir DIR] activity [--limit N] [--since-seconds S]
            [--preview-chars N]

Aggregates across all shards into a single JSON document with:

  shards         the SQLite files queried
  corpus         counts: documents_total/surface/core,
                 providence_total/live/stale/failed,
                 audit_events_total
  recent_qa      providence_cache rows (cache_key, question, answer
                 preview, hit_count, state, last_hit_at) newest first
  recent_ingests audit events of type=ingest with body's document_uri,
                 source_type, chunks, supersedes link
  recent_derives audit events of type=derive (distillation results,
                 compression_ratio, compression_depth)
  recent_falsifications  records marked failed/stale/quarantined,
                 with reason and by_actor

--limit caps each list independently. --since-seconds filters to recent
events only. --preview-chars trims long answers.

Designed for an agent to read before deciding the next action — what
was just asked, what just got integrated, what's the current state.

Makefile target: make activity ACTIVITY_LIMIT=N

66 tests still passing.
2026-04-27 12:58:23 -04:00
fc039555cc
multi-source corpus query: pose a question, the tree pulls related cached docs
aborist/qa/query.py exposes query() — the user-facing RAG flow:

  1. FTS5 search across all shards (chunks_fts can't be UNION'd as a
     view, so each shard's index is queried independently and merged
     by score).
  2. Top-K distinct documents are selected within a max-context-chars
     budget (default 60 KB so a 768-token response fits Hermes-3's
     82 K context window comfortably).
  3. context_root = Merkle root over the sorted source document_roots.
     That's the v9.8 'source' dimension for multi-source answers —
     a verifier can recompute it from the listed source roots.
  4. 8-dim cache_key over (context_root, question_hash, model_profile,
     conversation, governance_policy, schema, canonicalization,
     chunking). Hit returns STRICT immediately; miss calls Hermes and
     persists.

CLI: aborist [--shards-dir DIR] query "<question>"
  Default qa_db is <shards-dir>/qa.db (or ~/.aborist/qa.db). Uses the
  same OpenAICompatibleClient/StubClient as `ask`. --dry-run skips the
  LLM and returns context-only.

Search escape fix: the prior FTS5 escape ANDed every token including
stopwords + punctuation, so "What is anarcho-capitalism?" required
the doc to literally contain "what" + "is" + "anarcho-capitalism?" —
zero hits. New tokenizer drops stopwords + punctuation and ORs the
remaining content tokens; BM25 ranks the multi-token matches highest.

Live demo against the 122k-doc 4-shard cluster:
  Q "What is anarcho-capitalism?"  6.1 s wall miss / 0.45 s cache hit
  Q "Who was George Washington?"   10.3 s wall miss
Both answers cite the source URIs Hermes was given.

57 tests passing (4 new query tests covering search → context →
cache → audit chain).
2026-04-27 11:43:57 -04:00
649aeec79a
progress reporter + structured benchmark
aborist/progress.py — stdlib-only rate-limited stderr reporter.
  Periodic lines (default every 2s) showing:
    elapsed | seen | inserted | docs/s now | docs/s avg | percent | ETA
  Wired into ingest_source via a `progress` parameter; CLI default-on
  with --quiet to suppress and --total-estimate N to enable percent/ETA.

  Demo on a 5k-doc ingest with --progress-interval 0.8:
    [    1s] |   200 seen |   200 new |  193 now | ( 193 avg) docs/s |   4.0% | ETA  24s
    [    7s] | 3,200 seen | 3,200 new |  458 now | ( 424 avg) docs/s |  64.0% | ETA   3s
    [   12s] | 5,001 seen | 5,000 new |  392 now | ( 413 avg) docs/s | 100.0% | ETA   0s

bench/run.sh + `make bench` — reproducible 5000-doc workload through
three configs:
  serial            single process, single SQLite
  parallel-shared   N shards, one shared SQLite (WAL serialized)
  attached          N shards, per-shard SQLite (true parallel writes)

Output is a one-shot table plus CSV at /tmp/aborist-bench/results.csv
so the ratchet is visible as we keep optimizing. Override via
BENCH_DOCS=N and SHARDS=N.

Latest baseline (this commit, on this machine):
  config             wall_s    docs   docs/s
  serial              11.74    5000    425.7
  parallel-shared     10.39    5000    481.1
  attached             7.99    5000    625.5

53 tests still passing.
2026-04-27 11:37:20 -04:00
db3e3c00ab
resumable ingest + per-shard audit chain integrity
Resume (rsync-style)
  Each shard DB gains a `meta` table. After every successful batch
  flush, ingest_source persists `source_high_water:<source_type>` ->
  the largest row id seen (cur_id or old_id). On --resume, the source
  reads it back and skips rows whose id is <= the mark, so an ingest
  killed at any point can be re-run cheaply: already-cached docs are
  fast-forwarded past without re-hashing or DB writes.

  WikipediaSqlDump now exposes `start_id` (skip threshold) and
  `last_id` (running max). cur_id is surfaced in Document.extra
  alongside old_id so both tables behave the same.

  CLI: aborist ingest --resume

  Demo on shared DB (cur):
    Round 1: --limit 3000 --resume   high_water = 5714
    Round 2: --limit 5000 --resume   skips 1..5714, picks up at 5715
    Round 3: --limit 100  --resume   skips 1..15362, ingests 100 more
  Always idempotent on re-run; no dups, no missing docs.

Per-shard audit chain integrity for sharded analyze
  Cross-shard analyze previously reported nonsense breaks counts —
  each shard owns its own audit chain (genesis -> ... -> latest), and
  the UNION view interleaves them so cross-shard transitions look
  like break events. The fix: when --shards-dir is set, open each
  shard's DB directly and run _check_audit_chain on it, then
  aggregate.

  Output now reads:
    "audit_chain": {
      "events": <total>,
      "breaks": 0,
      "shards": [{"shard": "000.db", "events": N, "breaks": 0}, ...]
    }

53 tests passing. Tests cover: high-water write, skip-on-resume,
idempotency across two resume runs, kill-and-resume continuity.
2026-04-27 11:29:27 -04:00
a3fd8ddc53
shard-aware distill: iterate per-shard, cores stay in source shard
aborist --shards-dir DIR distill iterates over each shard's DB and
runs distill_existing on it. Cores live in the same shard as their
source surface so the per-shard derivation chain stays
self-contained.

Per-shard processing also enables trivial parallelism via the
distill-shards-parallel Makefile target — one process per shard, no
DB contention because each shard is its own file.

End-to-end demo on 4-way 10k cur ingest:
  ingest:  17.6 s wall (4 parallel)
  distill: 6.2 s sequential per-shard (9185 cores from 10000 surface)
  verify:  30/30 random Merkle proofs across shards

Aggregated counters cover all shards plus per-shard breakdown in the
output JSON so the user sees both the union and the parts.

Note: cross-shard analyze reports an inflated audit_chain.breaks count
because each shard owns its own chain genesis -> ... -> latest, but the
UNION view sees them interleaved. Fix coming next.
2026-04-27 11:24:17 -04:00
7d2bf4e7fa
phase 2: attach-forever sharding — zero merge cost
Each ingest shard owns its own SQLite file. N processes write in
true parallel (no WAL writer-lock contention). Cross-shard reads
attach all shards as UNION ALL views over the standard tables, so
existing query code (`SELECT * FROM documents`) works unchanged.

CLI:
  aborist ingest --shards-dir DIR --shard 0/4    # write to DIR/000.db
  aborist --shards-dir DIR stats                  # read across all shards
  aborist --shards-dir DIR analyze                #   ditto
  aborist --shards-dir DIR verify -n 20           #   Merkle round-trips
                                                  #   on a sample drawn
                                                  #   from all shards

Makefile:
  SHARDS_DIR=~/.aborist/shards
  ingest-cur-attached SHARDS=4   ingest-old-attached SHARDS=4
  stats-shards                   cross-shard stats

Implementation lives in store.connect_query():
  - opens a fresh in-memory connection
  - ATTACH DATABASE per shard file under aliases sh000, sh001, ...
  - CREATE TEMP VIEW <table> AS UNION ALL SELECT * FROM <alias>.<table>
    for the eight shardable tables (documents, chunks, merkle_nodes,
    edges, derivations, providence_cache, audit_events, falsifications)
  - exposes a _shards temp table for tools that want the shard list

Benchmark (20k cur, 4 cores):
  Phase 1a serial            55.3 s
  Phase 1b shared-WAL        43.1 s
  Phase 2 attached-forever   40.1 s    (no WAL contention)

The 7% gap vs Phase 1b is bz2-decompression duplication: each shard
decodes the full dump independently. Architectural wins are bigger
than that number — write isolation, per-shard crash recovery,
audit chain stays per-shard checkable, shards can migrate to other
disks, new shards are pure append (never rewrite existing files).

Merge cost remains: zero. `ls $SHARDS_DIR/*.db` is the registry.
2026-04-27 11:17:17 -04:00
d0857fb43d
phase 1b: --shard rank/total + parallel Makefile targets
Workers each parse the full SQL stream but only emit rows whose index
satisfies (idx % total == rank). N processes cooperate against a
single shared SQLite file; WAL+synchronous=NORMAL handles writer
contention via BEGIN IMMEDIATE serialization.

Implementation chose OS-level processes over in-process mp.Queue: the
gain ceiling is identical (writer is the bottleneck either way) and
the surface area is one new flag plus stride-filter logic instead of
a queue-based pipeline with picklable workers.

Makefile gains:
  ingest-cur-parallel SHARDS=4
  ingest-old-parallel SHARDS=4

Benchmark on 20k cur articles:
  serial            55.3 s   361 docs/s   1.0x CPU usage
  4-way parallel    43.1 s   464 docs/s   ~3.0x CPU usage   1.28x wall

Both runs landed 19,999 unique documents — correctness verified.

The write-lock is now the bottleneck; further gains require Phase 2
attach-forever sharding (each shard owns its own SQLite file, queries
fan out via ATTACH DATABASE, zero merge cost).
2026-04-27 11:10:25 -04:00
85ae761df8
ETL throughput: 4x parser, 2.5x full ingest
Profile of the prior implementation (cProfile on 2000-doc ingest):
  total       23.6s
  parser      20.1s   (85% of time, 27.7M list.append calls)
  DB writes    2.9s

Three changes compound:

1. PRAGMA pack in store.connect():
     synchronous=NORMAL    skip per-commit fsync (durable under WAL replay)
     cache_size=-65536     64 MB page cache
     temp_store=MEMORY     temp tables in RAM
     mmap_size=256 MB      page-cache reads bypass read() syscall

2. Batched transactions in ingest_source and distill_existing:
     200 docs/tx default, --batch-size CLI flag.
     One BEGIN/COMMIT per batch instead of per doc.
     Audit chain SELECT-then-INSERT still chains correctly because
     SQLite reads its own uncommitted writes within a transaction.
     Idempotent on collision (INSERT OR IGNORE / pre-existence check)
     so partial-batch re-runs are safe.

3. Parser rewrite (the actual bottleneck): replaced char-by-char
   list-append loops with str.find / string slicing.
     _decode_mysql_string fast-path: if no '\\\\' in input, return as-is.
     _split_values_tuples uses _find_string_end (str.find with
       backslash-parity check) instead of per-char scanning.
     _iter_insert_statements buffered scan (str.find for ';' and "'")
       crossing read-chunk boundaries via in_string flag.
     ~13x fewer Python function calls, semantics identical.
   _upsert_edges now batches via executemany.

After: 2000 docs in 6.0s; 5000 old revisions in 12.8s wall (was 32.3s).
Throughput 85 -> 333 docs/sec. 49 tests still passing.

Cheaper to fail = cheaper to experiment.
2026-04-27 08:51:52 -04:00
f623250135
add 'aborist analyze' + correct default LLM endpoint
analyze emits a compression-spectrum report:
  compression_depth_histogram   surface vs depth=1, 2, ...
  distillers                    per-process compression ratio mean/min/max
  documents_by_source_kind      source/kind crosstab
  chunks_by_tier                hot/warm/cold distribution
  audit_chain                   event count + integrity (breaks must be 0)
  gravity_top_inbound           the 'gravity wells' of the forest

Default LLM endpoint moves from uncloseai.com (marketing site, no /v1)
to hermes.ai.unturf.com/v1 — the actual reachable Hermes-3 inference
endpoint serving adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic on vLLM
with an 82K context window. uncloseai.com still works as a brand
override via ABORIST_LLM_ENDPOINT.
2026-04-27 08:36:38 -04:00
a056acfb5b
prepare full Wikipedia 2003-05-16 ingest: cur + old (revisions)
The 2003-05-16 archive ships three files:
  20030516_cur_tablesql.bz2   82 MB  current snapshot (single revision/page)
  old_tablesqlbz2.1          640 MiB \
  old_tablesqlbz2.2          252 MiB / split halves of old (full revision
                                       history). Concatenate before bzcat.

Generalize the parser:
  WikipediaSqlDump(table='cur'|'old')  — shared statement parser, single
                                         column-position contract for the
                                         first 4 fields (id/ns/title/text)
  WikipediaCurDump  — back-compat wrapper, table='cur'
  WikipediaOldDump  — new, table='old'; old has no is_redirect, every
                      revision is real

Old rows surface old_id and old_timestamp via Document.extra so a
downstream pass can sort revisions chronologically before re-ingesting
through the supersedes-edge path.

Makefile gains:
  fetch-cur / fetch-old / fetch (both)
  ingest-cur / ingest-old / ingest (cur default)
  WP_OLD target concatenates the two split parts
CLI ingest --source now accepts wikipedia_cur or wikipedia_old.

Smoke (real dump): 5 revisions of "AtlasShrugged/Companies" yielded
correctly with old_id=2..10, timestamps from January 2002.
2026-04-27 08:10:42 -04:00
57cf1b183b
add Q&A layer: v9.8 providence_cache writes with Merkle-bound proofs
aborist/qa/ implements the cache-first answer flow from the providence
whitepaper, scaled up to v9.8's full 8-dim admissibility invariant.

cache_key = SHA-256 of:
  source_root | question_hash | model_profile_hash | conversation_hash
  | governance_policy_hash | schema_version | canonicalization_version
  | chunking_version

Any drift in any dimension yields a distinct cache_key — prior records
cannot serve. Falsification states (failed/stale/quarantined) gate
every cache hit.

- qa/keys.py        — pure hash functions, deterministic & testable
- qa/client.py      — ChatClient Protocol + StubClient + OpenAI-compatible
                      HTTP client (vllm/llama.cpp/uncloseai compatible)
- qa/runner.py      — ask(): lookup -> hit (no LLM call, hit_count++)
                      OR miss (call client, write record, audit event,
                      proof binds answer to source root)
- cli.py            — `aborist ask` and `aborist providence` subcommands
- pyproject.toml    — httpx promoted from extras to core (used by both
                      html and qa); selectolax stays in [html] extras

Smoke (StubClient, no network): cache miss writes record with chunk_0
Merkle proof reconstructing source_root; cache hit returns same record
without calling client; 1085 audit events chained 0 breaks across
ingest/derive/evict/rehydrate/providence_write.
2026-04-27 08:01:23 -04:00
d4b1163c69
recursive distillation: core -> depth+1 core
Distillers now scan kind='surface' OR kind='core'. Cores generate
deeper cores with compression_depth incremented. Each round of
recursion tightens the planet toward its center.

Audit body for derive events now records src_kind and the resulting
compression_depth so the chain reflects the layer transition.

Smoke against the existing 503 surface + 478 depth=1 core corpus
produced N deeper cores with proofs still binding back to their
depth=1 sources.
2026-04-27 07:54:30 -04:00
856b3116d7
phase 0 explore: aborist core + sources + distill + evict
A content-addressed, Merkle-committed document store implementing the
runtime spec from Merkle Providence Reverse RAG (April 2026 whitepaper)
and Merkle-AGI v9.8 admissibility ledger. Ports proxy.unturf.com Go
merkle conventions to Python: non-commutative HashCombine with 0x03
prefix, explicit IsLeft per sibling, self-duplicate odd elements.

What's in:

- merkle.py — proof generation/verification, JSON serialization
- store.py — v9.8 SQLite schema: 8-dim providence_cache key,
  falsification_state, append-only audit chain, surface/core kind,
  hot/warm/cold tier, derivations, edges
- ingest.py — Source -> normalize -> chunk -> merkle -> upsert,
  idempotent on document_root collision
- search/ — SearchBackend ABC with explicit AuditMode (STRICT/HYBRID/
  VISUAL), FTS5 backend returning VISUAL hits
- sources/ — wikipedia.py (streaming bz2/MySQL extended-INSERT parser
  for 2003-era cur dumps); html_page.py (selectolax + httpx, robots.txt
  honored automatically)
- distill/ — Distiller ABC + first-sentence-v1 stub. Runner generates
  per-contributing-chunk Merkle proofs binding cores back to source
  document_root.
- evict.py — hot->cold demote (NULLs content, drops FTS row, retains
  leaf_hash). rehydrate() refetches via source pipeline; matching root
  restores content, mismatching root marks providence stale and writes
  rehydrate_drift event. Cores never evict.
- cli.py — ingest / search / verify / stats / distill / evict /
  rehydrate
- 31 tests covering merkle round-trip, ingest+audit, chunker version
  binding, html parse, distillation proof verification, evict+
  rehydrate including drift detection.

Smoke: 503 Wikipedia 2003-05-16 + 3 fox-owned HTML pages ingested,
478 cores produced (24 surface->core merkle dedups), 7 chunks evicted
to cold and round-tripped via rehydrate, 987 audit events chained 0
breaks.
2026-04-27 07:53:18 -04:00