#000061: pack format v2 — self-sufficient new-peer hydration

v1 packs (chunks-only) were under-engineered: a new peer landing on
v1 packs would have chunk bodies indexed by leaf_hash but no documents
table, no audit chain, no merkle interior, no edges — couldn't actually
hydrate. fox: "isn't what I wanted you under engineered..."

v2 packs ship every load-bearing shard table alongside chunk bodies in
the same tar.zst:

  manifest.jsonl                          # chunk catalog (unchanged)
  tables/documents.jsonl                  # array-per-line columnar JSONL
  tables/chunks.jsonl                     # without content column
  tables/merkle_nodes.jsonl
  tables/edges.jsonl                      # FAN-IN restructured
  tables/audit_events.jsonl
  tables/derivations.jsonl
  tables/concept_relations.jsonl
  tables/concept_token_idf.jsonl
  tables/providence_cache.jsonl
  tables/citation_aliases.jsonl
  tables/term_aliases.jsonl
  tables/snapshots.jsonl
  tables/document_http_meta.jsonl
  blobs/<hash[:2]>/<hash[2:]>             # raw UTF-8 chunk bodies

Two compression strategies inside the pack:

1. Array-per-line JSONL ({"_columns": [...]} header line + ["v1","v2",...]
   data lines) drops ~30% of uncompressed bytes vs object-per-row JSONL.
   zstd recovers most of that on its own, but smaller uncompressed
   footprint also speeds up stream-restore.

2. Edges fan-in restructure at pack-build time: 22M rows of
   (src_root, edge_type, dst_root, dst_uri, anchor) → ~500k unique
   (dst_uri, edge_type, anchor, dst_root) groups with src_roots as an
   array. ~5-10x compressed savings on the dominant table. Reverses on
   unpack into the per-edge live schema. Live queries unchanged.

NOT shipped (per-peer state): mesh_*, selfmodel_*, capital_ledger,
memory_*, controller_events, fork_score_branches, adapter_loss_reports,
falsifications, schema_meta, meta. NOT shipped (rebuildable): chunks_fts*,
documents_fts* — restored from chunks.content + documents.title on
unpack.

push_pack no longer appends `cold_pack_pushed` to the audit chain.
That event leaked into the next push's audit_events.jsonl dump and
broke the "two writers at the same corpus state produce identical
pack_hash" determinism property. The bucket/disc file IS the receipt;
the snapshot_root pinned inside the pack metadata binds it to a corpus
state. No load-bearing consumer of the audit row.

pull_pack restored to handle both v1 (chunks-only) and v2 (tables +
chunks) packs. For v2 it extracts tables/*.jsonl to a temp dir,
calls restore_shard_metadata (which INSERT OR IGNOREs into the live
schema and expands edges back to per-edge rows), then fills chunk
content for every leaf_hash in blobs/. Idempotent against populated
DBs (INSERT OR IGNORE all the way down). Self-cleaning temp dir.

Sizing measured 2026-05-26: ~2.1 GB per shard pack compressed (chunk
content 1.78 GB + metadata ~0.3 GB), ~8.5 GB total across 4 shards.
~20% more than v1 chunks-only for self-sufficient hydration.

24 cold-object + evict tests pass (+1 new test_push_pack_v2_hydrates_fresh_empty_db
that builds a pack from a populated DB and unpacks into a completely
empty DB to verify all tables restored). Full suite: 2558 passed,
28 skipped, 1 xfailed.
This commit is contained in:
russell@unturf.com 2026-05-25 22:21:45 -04:00
parent 57f89894e2
commit 50324b4d7a
No known key found for this signature in database
6 changed files with 654 additions and 53 deletions

View file

@ -299,6 +299,81 @@ def test_push_pack_local_dir_writes_files(tmp_path):
conn.close()
def test_push_pack_v2_hydrates_fresh_empty_db(tmp_path):
"""Pack format v2 self-sufficiency test: build a pack from a populated
DB, then unpack it into a FRESH empty DB and verify every table is
restored (not just chunks content). This is the real new-peer
hydration path."""
src_db = tmp_path / "src.db"
rx_db = tmp_path / "rx.db"
conn_src = connect(src_db)
backend = MemoryBackend()
try:
# Populate the source DB with a few docs so multiple tables get
# rows: documents, chunks, merkle_nodes, audit_events at minimum.
# Distinct content per doc — same content dedupes by document_root.
docs = [
_doc(f"html://hydrate{i}", LONG + f"\n\nDoc-specific marker {i}." * 5)
for i in range(3)
]
ingest_source(conn_src, FakeSource(docs))
src_docs = conn_src.execute(
"SELECT COUNT(*) FROM documents"
).fetchone()[0]
src_chunks = conn_src.execute(
"SELECT COUNT(*) FROM chunks"
).fetchone()[0]
src_merkle = conn_src.execute(
"SELECT COUNT(*) FROM merkle_nodes"
).fetchone()[0]
src_audit = conn_src.execute(
"SELECT COUNT(*) FROM audit_events"
).fetchone()[0]
assert src_docs == 3 and src_chunks > 0 and src_merkle > 0 and src_audit > 0
result = push_pack(conn_src, backend)
assert result["status"] == "pushed"
pack_hash = result["packs"][0]["pack_hash"]
finally:
conn_src.close()
# Brand-new empty receiving DB. No ingest, nothing — just `connect()`
# which creates the schema.
conn_rx = connect(rx_db)
try:
# Confirm rx is empty before unpack.
assert conn_rx.execute(
"SELECT COUNT(*) FROM documents"
).fetchone()[0] == 0
pull_result = pull_pack(conn_rx, backend, pack_hash)
assert pull_result["status"] == "pulled"
assert pull_result["pack_format"] == "v2"
# Every table that was non-empty in src should have rows now in rx.
assert pull_result["tables_restored"]["documents"] == src_docs
assert pull_result["tables_restored"]["chunks"] == src_chunks
assert pull_result["tables_restored"]["merkle_nodes"] == src_merkle
assert pull_result["tables_restored"]["audit_events"] == src_audit
# Chunk bodies restored.
rx_hot = conn_rx.execute(
"SELECT COUNT(*) FROM chunks WHERE tier='hot' AND content IS NOT NULL"
).fetchone()[0]
assert rx_hot == src_chunks
# FTS5 rows rebuilt.
rx_fts = conn_rx.execute("SELECT COUNT(*) FROM chunks_fts").fetchone()[0]
assert rx_fts == src_chunks
# All three documents reachable by URI.
for i in range(3):
row = conn_rx.execute(
"SELECT title FROM documents WHERE document_uri=?",
(f"html://hydrate{i}",),
).fetchone()
assert row is not None, f"document html://hydrate{i} not restored"
finally:
conn_rx.close()
def test_push_pack_no_push_writes_locally_only(tmp_path):
"""push_to_bucket=False skips bucket writes, requires local_dir."""
db = tmp_path / "local-only.db"