Skip to content

feat: multi-invocation incremental builds — --sync-key, changes/verify subcommands, snapshot hardening - #398

Open
ww2283 wants to merge 7 commits into
StarTrail-org:mainfrom
ww2283:feature/multi-invocation-incremental-upstream
Open

feat: multi-invocation incremental builds — --sync-key, changes/verify subcommands, snapshot hardening#398
ww2283 wants to merge 7 commits into
StarTrail-org:mainfrom
ww2283:feature/multi-invocation-incremental-upstream

Conversation

@ww2283

@ww2283 ww2283 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Motivation

Callers that split large index builds into checkpointed multi-leg invocations (e.g. automation driving leann build with varying --docs lists) hit two limitations:

  1. Snapshot identity is a hash of the entire --docs list — any variation between invocations creates a fresh snapshot namespace, losing all Merkle incrementality: unchanged files re-embed and duplicate.
  2. No way to inspect pending changes or index integrity — a build interrupted mid-write (SIGKILL) can leave cross-artifact inconsistency that a subsequent build completes with exit 0.

Changes (all opt-in / additive; no default behavior changes)

  • leann build --sync-key <key> — caller-supplied stable snapshot identity. Two builds with different --docs lists but the same key share one snapshot; files common to both are embedded once. Mismatched keys are rejected unless --force rekeys.
  • leann changes <index> [--docs ...] [--sync-key K] — non-mutating Merkle diff; prints one JSON doc {added, modified, removed} (sorted) on stdout, exit 0. Errors (exit 1) on missing index, empty sync scope, wrong sync key, or corrupt snapshot rather than reporting a false clean delta.
  • leann verify <index> — cross-artifact integrity check: passages.jsonl vs offsets pickle, IVF id-map exact inversion, FAISS ntotal vs id-map cardinality, mapped ids present in the inverted lists, next_id monotonicity, sync snapshots unpickle. Exit 0 healthy / 1 with findings.
  • Zero-chunk deltas commit the snapshot — a delta producing no chunks previously skipped the snapshot write, so the same no-op delta was rediscovered forever. Guarded: if any document failed to load, the build aborts without committing so failures stay visible.
  • LEANN_NO_REGISTER=1 — skip global ~/.leann/projects.json registration, for automation running builds in disposable directories.
  • Snapshot robustness: corrupt snapshots and unreadable sync_roots.json fail loud on build (--force resets); a transiently unreadable file keeps its previous hash instead of being classified as removed (which deleted its chunks); unreadable subtrees abort change detection instead of reading as mass removal; leann watch skips a tick on these errors instead of dying.

Testing

40+ new tests across tests/test_sync_key.py, test_cli_changes.py, test_cli_verify.py, test_zero_chunk_commit.py, test_no_register.py, plus additions to test_sync.py/test_watch_sync_scope.py. The verify fixtures build a real IndexIVFFlat + DirectMap.Hashtable. The changes went through three review rounds (two independent reviewer passes + a re-review) on our fork before this PR; 17 findings fixed.

Rebased onto current main (dc85934); import leann + the touched suites pass and ruff is clean on this branch.

ww2283 added 4 commits August 19, 2026 14:03
…mmands, zero-chunk snapshot commit, LEANN_NO_REGISTER (#2)

* feat(cli): --sync-key stable snapshot identity

Keyed builds use one global FileSynchronizer over the expanded file
manifest (snapshot sync_key_{sha256(key)[:12]}.pickle) instead of
per-docs-list hashed snapshots, so varying --docs lists across
invocations keep merkle incrementality. Key lifecycle persisted in
sync_roots.json (reuse stored key; rekey requires --force).

load_snapshot() now raises SnapshotCorruptError on unreadable pickles
(missing file still means first build); keyed builds fail loudly on
corruption instead of warn-and-skip.

Also: remove-only deltas on non-IVF backends now trigger the full
rebuild instead of returning early (previously gated on IVF).

* feat(cli): leann changes subcommand

Non-mutating merkle diff against the stored snapshot: prints one JSON
document {added, modified, removed} (sorted) on stdout, diagnostics on
stderr. Scope comes from --docs or, when omitted, from the index's
stored sync_roots.json. Snapshot errors are strict (nonzero exit, no
false-clean report) instead of build's warn-and-skip.

LeannCLI no longer eagerly creates .leann/indexes at construction, so
read-only commands leave the working directory untouched.

* feat(cli): leann verify subcommand

Cross-artifact integrity check: passages.jsonl/.idx offset and id-set
consistency for any backend, plus IVF invariants (id-map exact
inverses, id-map values vs offset-map keys, FAISS ntotal vs id-map
cardinality, next_id monotonicity, missing index file). Exit 0 clean,
1 with a findings report. Covers vector/id-map/passage artifacts only
(not BM25 sqlite or ids.txt).

* fix(cli): commit snapshot on safely-empty incremental deltas

An add/modify-only delta that loads successfully but produces zero
chunks now commits the snapshot and sync config, so the same no-op
delta is not rediscovered on every subsequent build. Loader failures
and removal-bearing deltas still leave the snapshot untouched.

Snapshot pickles and sync_roots.json now publish via tmp + os.replace
so an interrupted write cannot corrupt existing state.

* feat(registry): LEANN_NO_REGISTER env switch

LEANN_NO_REGISTER=1 makes register_project_directory a no-op before
any path traversal or ~/.leann creation, for automation running builds
in disposable directories.

* fix(cli): verify reads real on-disk IVF artifact names

E2E smoke against a real IVF build showed verify looked for
documents.leann.{meta.json → wrong via with_suffix, ivf_id_map.json,
index}; the backend actually writes documents.leann.meta.json,
documents.ivf_id_map.json and documents.index (id map and FAISS file
use the stem without .leann). Test fixtures now mirror the real
layout.

* fix(cli): harden incremental build/changes/verify error paths from PR #2 review

- refuse empty-delta snapshot commit when document loads failed (masked data loss)
- changes: exit 1 on missing index, empty scope, or wrong --sync-key
- corrupt sync snapshot / sync_roots.json fail loud; --force resets
- sync: unreadable existing file keeps previous hash instead of 'removed'
- verify: no crash on non-numeric id-map keys; no misleading findings when idx unreadable
- drop dead 'changes --json' flag; register project dir on empty-delta commit
- changelog entry for the PR's features

* fix: address Gemini review findings for PR #2

- abort incremental build before mutation on any document load failure
- verify: snapshot presence/unpickle checks, IVF type + mapped-id invlists
  check, type validation for decodable-but-malformed artifacts
- verify test fixture builds a real IndexIVFFlat (was IndexFlatL2)
- sync: os.walk onerror fails loud instead of mass-removal on unreadable subtree
- changes: index-existence check applies with --docs too
- sync_roots.json wrong-JSON-type handled by the --force recovery path

7 issues fixed, 0 false positives, 0 skipped
See .doc/pr-review-comments/PR-2-multi-invocation-incremental.md

* fix: watch survives fail-loud errors; changes rejects key on unkeyed index

* fix: CI lint/type errors in new tests
With two faiss SWIG builds loaded in one process (faiss-cpu + the hnsw bundle, as on CI), downcast_index silently loses invlists access and the mapped-id check was skipped, returning a false healthy verdict. A subprocess loads exactly one build.
ubuntu-22.04's apt patchelf (0.14.3) fell below current auditwheel's >=0.14.5 floor, failing every Linux wheel-repair job.
@ww2283

ww2283 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Note on the Linux build failures: they are unrelated to this PR's code — current auditwheel (installed unpinned in build-reusable.yml) raised its floor to patchelf >= 0.14.5, while ubuntu-22.04's apt ships 0.14.3, so every auditwheel repair step fails with ValueError: patchelf 0.14.3 found. This will affect all PRs and release builds, not just this one.

I've folded a one-line fix into this branch (ec83163): pip-install patchelf>=0.14.5 into the same build env as auditwheel, so the repair step resolves the newer binary from .uv-build/bin. Happy to split it into a separate PR if you'd prefer to keep this one feature-only.

(The earlier macos failure was a real bug in this PR's verify tests — fixed in 1c2f04b: with faiss-cpu and the hnsw-bundled faiss both loaded, SWIG type-table clashes made downcast_index(...).invlists return None, silently skipping the mapped-id check; it now runs in a single-faiss subprocess.)

ww2283 added 2 commits August 19, 2026 16:32
* fix(verify): allow duplicate content-hash passage ids (#5)

Content-hash ids are legitimately many-to-one for byte-identical
chunks; verify's bijection invariants flagged healthy indexes.
- jsonl dup ids allowed only when text is identical per id
- idx cardinality compared against unique jsonl ids
- IVF passage_to_id checked as last-wins partial inverse over
  deduped id_to_passage values

* docs: changelog entry for verify duplicate-id fix (#5)

* fix(verify): harden duplicate-id conflict check against mixed types

Codex review of PR #6: type-prefixed text hash so 1 vs "1" counts as
different text, and type-independent sort of conflicting ids so mixed
int/str ids report findings instead of raising TypeError.
@ww2283

ww2283 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Appended 076ea38 — fix for a verify false-positive found in live use after this series: with --id-scheme=content-hash, byte-identical chunks legitimately share a passage id, so verify's bijection invariants flagged healthy indexes. Invariants are now checked against the actual many-to-one contract (duplicate jsonl ids allowed only with identical text; idx cardinality vs unique ids; passage_to_id validated as a last-wins partial inverse). Includes a duplicate-content test fixture; 18 verify tests pass. Details: ww2283#6.

@ASuresh0524

Copy link
Copy Markdown
Collaborator

@ww2283 Can we fix CI?

@ww2283

ww2283 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@ASuresh0524 of course, the ci is too comprehensive:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants