indexer: file-level delta indexing keyed on (path, content_sha) (#104) - #114
Merged
IceRhymers merged 9 commits intoJul 25, 2026
Merged
Conversation
…mary Thread an injected shas_fn (default indexer.store.read_indexed_shas) through run -> _index_one -> _index_one_inner -> _index_one_branch. Called on a short-lived engine.connect() before embedding, and only when the branch's stored index_semantics_version already matches INDEX_SEMANTICS_VERSION (the same gate index_repo applies authoritatively). Narrows _precompute_chunk_writer's file list to every file the advisory read did not classify as unchanged. _precompute_chunk_writer's chunk_writer closure now guards against an uncovered path (warn-and-skip, never delete an uncovered file's chunks -- defence-in-depth for a path the single-writer invariant says is unreachable). BranchOutcome gains semantic_degraded; run() aggregates every degraded branch into one run-completion WARNING, since chunk coverage no longer self-heals on the next run under delta indexing.
Six tests in tests/integration/test_store.py re-index at a version-matching stamp, so the delta gate introduced by earlier commits on this branch opens and their assertions change: - test_rerun_is_idempotent, test_mark_and_sweep_removes_deleted_file, test_index_repo_records_the_sweep_phase, test_sweep_is_repo_scoped, test_per_branch_cas_resume_is_independent_per_branch: symbols=1/2 -> 0 (a re-run at unchanged content classifies unchanged, not a re-insert count). Companion test_rerun_is_idempotent_preserves_row_identity added for the stronger row-identity property the original was reaching for. - test_reindex_replaces_stale_edges_for_the_same_file, test_reindex_to_zero_edges_sheds_all_rows: these two deliberately hold content_sha identical while varying extraction output (the "unconditional-delete guard"), which the delta path would otherwise skip. Forced closed via the existing `UPDATE repo_branches SET index_semantics_version = NULL` idiom rather than relaxing their assertions -- in production, identical content_sha with divergent extraction can only happen via an extractor change, which mandates the same version bump.
… gate, EXPLAIN) New tests/integration/test_store_delta.py, runnable locally against codesearch-pg (no chunks table, no lakebase_* extension -- that's what makes it runnable at all): row-identity proof for the unchanged path, a small 1-changed/1-added/1-deleted delta, multi-branch membership-only dedup, a semantics-version mismatch forcing the full path, the provenance gate (a stale sibling branch forces the full path), both directions of the branches-array/repo_branches membership invariant, the empty-seen-set guard and CAS still holding with delta on, the zero-parse stamp fix (the BLOCKER regression this branch's earlier commits fixed), and an EXPLAIN proof that the pre-read is index-served (ix_files_branches_gin / Index Only Scan on uq_files_repo_path_sha, zero heap fetches) rather than a Seq Scan. tests/integration/test_store_chunk_writer.py gains the chunk-side delta cases (unchanged file never calls chunk_writer and preserves chunk ids; membership-only backfills previously-missing chunks) and reviews test_reindex_is_idempotent_for_chunks against the same six-test treatment (symbols 2 -> 0). All Lakebase-deferred -- this module's fixture needs lakebase_vector, which no local Postgres provides -- reasoned through rather than locally verified.
… and the scope note - runbook §2: the new indexer.store `delta write set ...` line and how to read it. - runbook §4: correct the force-reindex SQL from `repos` to `repo_branches` -- the skip seam reads repo_branches only, so the documented `UPDATE repos SET index_semantics_version = NULL` has always been a no-op against it. New §4.1 documents the two accepted delta regressions (a degraded branch no longer self-heals its chunk coverage; the chunk cap is now enforced per run) and their remedies. §5: extends the INDEX_SEMANTICS_VERSION bump obligation to the embedding model and SEMANTIC_EMBEDDING_DIM, neither of which the tripwire watches. - app/config.py: comment on semantic_max_chunks_per_repo recording its new per-run scope. - app/db/models.py: INDEX_SEMANTICS_VERSION docstring extended to match; no version bump (indexer/languages.py and the other two tripwire-watched files are untouched by this branch).
… gaps An independent code-reviewer pass (separate context) found no CRITICAL/HIGH issues but several real MEDIUM/LOW findings, all fixed here: - test_reindex_with_identical_items_does_not_duplicate_edges was silently neutered by the delta gate: run 2 re-indexed byte-identical content at the same head_sha, so main.py classified unchanged and the edge write path was never exercised at all -- the assertion passed for the wrong reason. Same §2.6a (b) treatment as the two guard tests: force the gate closed. - The EXPLAIN test's projected-columns assertions were vacuously true: EXPLAIN (FORMAT JSON) omits the `Output` field entirely without VERBOSE, so `"content" not in ""` always passed. Added VERBOSE and switched to an exact per-column check (a naive substring check would false-positive on `content_sha` containing `content`). - Re-enabling the indexer job's `semantic.enabled` flag after a period disabled no longer backfills chunks on its own under delta indexing (same root cause as a degraded-precompute branch); documented in semantic-enablement.md with the stamp-clearing remedy. - Runbook "Who can run this" still named `repos`; the statement above it was already corrected to `repo_branches`. - Local-coupling nit in the membership-only classification (`delta_on and membership_ok`, rather than relying on membership_ok's non-local initialization); a misattributed comment in the empty-seen-set-guard invariant test; a garbled sentence in the INDEX_SEMANTICS_VERSION docstring; a dangling indexer/AGENTS.md cross-reference; a docstring note on the advisory read's discarded `present` set and its cost. make lint, make test, and make test-integration all re-run clean after these changes (204 passed / 8 failed / 43 errors against local Postgres, same named failures as the pre-#104 baseline -- all Lakebase-only).
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #104
Summary
Makes indexing incremental at the file level. Today any HEAD move on a branch
rewrites every file, deletes/reinserts every symbol and edge, and re-embeds
every chunk. This adds a per-file classification inside
index_repo'sexisting transaction — unchanged (write nothing), membership-only (a
row with this
(path, content_sha)already exists under another branch ofthis repo; union the branch array in), or changed/new (today's full write
path) — gated on the branch's stored
index_semantics_versionalreadymatching
INDEX_SEMANTICS_VERSION.indexer/job.pygains a matching advisoryread so the embedder is only ever called for files the delta path will
actually rewrite.
Executed from the critic-approved plan at
.omc/plans/issue-104/approved-plan.md(not checked in —
.omc/is gitignored); this PR body summarizes its outcome.What changed
indexer/store.py:read_repo_content_shas/read_indexed_shas(statements 3a/3b), the provenance gate (statement 4,
_repo_is_wholly_at_current_version), three-way per-file classification,batched membership-only
UPDATE … RETURNING(_union_membership), thedelta write set …INFO line, and a base-case fix: a run that parses zerofiles no longer advances
index_semantics_version(onlylast_indexed_commit) — without this, a zero-parse run straddling aversion bump could manufacture a false "current version" base case and
serve stale extraction under delta forever.
indexer/job.py: an injectedshas_fnseam (defaultread_indexed_shas), called on a short-lived connection before embedding,gated on the same stamp check; narrows
_precompute_chunk_writer's inputto not-already-carried files; a
covered-set guard on thechunk_writerclosure (warn-and-skip rather than delete an uncovered path's chunks); a
BranchOutcome.semantic_degradedflag and an aggregate run-completionWARNING naming every branch whose chunk precompute failed this run (no
longer self-healing on the next run under delta indexing).
tests/unit/test_store_delta.py(statement-inventory testsagainst a fake connection) and
tests/integration/test_store_delta.py(row-identity proof, small delta, multi-branch dedup, semantics-version
mismatch, the provenance gate, both directions of the branches/
repo_branches membership invariant, the zero-parse stamp fix, and an
EXPLAIN proof the pre-read is index-served);
tests/unit/test_job.pygainsthe
shas_fnseam/embed-narrowing tests and the degraded-branch WARNINGtests; six existing
tests/integration/test_store.pyre-index testsupdated for the delta gate (four value updates; two deliberately re-force
the gate CLOSED via
UPDATE repo_branches SET index_semantics_version = NULLrather than relaxing their assertions, since they prove theunconditional-delete guard the delta path would otherwise skip);
tests/integration/test_store_chunk_writer.pygains the chunk-side deltacases (Lakebase-deferred, see below).
docs/runbooks/indexing-parallelism.mdgains thedelta write set …line, a correction to the force-reindex SQL (repos→repo_branches— the skip seam has only ever readrepo_branches, so thepreviously-documented
UPDATE repos SET index_semantics_version = NULLwasa no-op), and the two accepted regressions below with their remedies;
docs/runbooks/semantic-enablement.mddocuments that re-enablingsemantic.enabledno longer backfills chunks on its own; theINDEX_SEMANTICS_VERSIONbump obligation (app/db/models.py) is extendedto the embedding model /
SEMANTIC_EMBEDDING_DIM, andapp/config.py'ssemantic_max_chunks_per_repocomment records its newper-run scope.
Accepted regressions (documented in-code, in the runbook, and here)
indefinitely, not just until the next successful run — only a changed
file re-embeds now. Mitigated with the aggregate WARNING above; remedy is
clearing that branch's semantics stamp.
semantic_max_chunks_per_repois now a per-run bound, not aper-branch-corpus bound — re-enforced in full on every first index and
every semantics bump.
Neither is a defect in the mechanism; both are consequences of "unchanged
files are never rewritten" that weren't true before.
Scope discipline
indexer/languages.py,indexer/symbols.py,indexer/parse.py(thesemantics-tripwire-watched files) are untouched.
INDEX_SEMANTICS_VERSIONstays
4.git diff --name-only origin/integration/indexer-performance...HEAD— verified,no watched file appears.
TEMP TABLE, no change to thetransaction shape, the CAS baseline,
StaleIndexError, the sweep, orreconciliation.
for the pre-existing gap this work surfaced (the semantics tripwire doesn't
actually run in CI — the
unitjob's shallow checkout can't resolveorigin/master) — out of scope for this PR, linked rather than fixed here.zero-parse stamp fix (a narrow, deliberate carve-out from indexer: file-level delta indexing keyed on (path, content_sha) #104's declared
"Out of scope: … branch-level stamp semantics"), the two accepted
regressions, and that extraction is not skipped (epic items about total
wall-clock time restated as db-write + embed time, proportional to the
delta — item 4's db-write half is what this issue delivers; indexer: single-pass in-memory tarball ingestion (drop extract-to-disk) #106/indexer: process-pool symbol/edge extraction #108 own
the parse-side cost).
Gates (fresh, this session, against
codesearch-pg/ pgvector:pg16)make lintmake testmake test-integration(local Postgres)The local integration gate is not "all green" — this repo has no local
Postgres image with the Lakebase-only extensions (
lakebase_vector,lakebase_ann,lakebase_bm25) orSET ROLEgrant fixtures six modulesneed, and that's pre-existing, not introduced here. Every failure/error is
one of:
test_semantic_rrf.py(12),test_reconcile.py(17),test_migrations.py(11),
test_webui_semantic.py(2),test_commit_search.py(2),test_mcp_server.py(1) — identical, by name, to the pre-existingbaseline recorded before this branch's work started (190 passed / 8
failed / 41 errors on
integration/indexer-performance@649ba80).test_store_chunk_writer.py— 6 errors, not the baseline's 4: the+2 are this PR's own new chunk-side delta tests, which need the same
lakebase_vectorfixture as the rest of that module and are Lakebase-deferred by design (see that module's docstring) — reasoned through, never
locally verified.
No other module regressed.
tests/integration/test_store_delta.py(new, 12tests) runs and passes locally in full, including the EXPLAIN access-path
proof (
ix_files_branches_ginfor the branch-carried read;Index Only Scan +
Heap Fetches: 0onuq_files_repo_path_shafor therepo-wide read — the pre-committed downgrade path for an unattainable
Heap Fetches: 0was not needed).Review
An independent
code-reviewerpass (separate context, full diff against theplan) returned no CRITICAL/HIGH findings after actively trying to break
the delta-skip induction, the zero-parse fix, the provenance gate, and the
advisory/authoritative read divergence handling — all held. It found 2
MEDIUM + 8 LOW issues, all fixed in the "Address review pass 1" commit: a
test that the delta gate had silently neutered (re-indexing byte-identical
content at the same
head_shamade the file classify unchanged, so the edgewrite path it was meant to test never ran), vacuous EXPLAIN assertions
(missing
VERBOSE, so Postgres never populated the field being asserted on),a semantic re-enablement documentation gap, and several doc/comment
precision nits. Gates re-run clean after the fixes (see table above).
Measurement (issue AC5)
Semantic/embed phase can't run locally (no embedder configured), so per the
plan's fallback this measures the
index_repodb-write phase directly —iter_source_files+extract_fileover a real, actively-developed corpus(this repo's own working tree), run twice: once as a first index, once at
identical content and a new
head_sha(the real-world "HEAD moved, nothingchanged" case).
Before (
indexer/store.pyat86e64d7, pre-#104, 205 files):After (this branch, 218 files):
Before this change, a re-run at unchanged content costs the same as a full
index (no measurable improvement — 1.084s → 1.133s). After, the unchanged
re-run's db-write cost drops ~145x (1.157s → 0.008s) and writes zero
symbol/edge rows, confirming AC1. Extraction/parse cost is unaffected either
way, as documented (#104 does not skip parsing — see the epic-body note
above).
Blockers
None. Ready for review.