Add incremental (ref-addressed) branch indexing (v3.0.0) - #2
Conversation
…dexing
Adds a `update: <mode>` source knob (`snapshot` default, `incremental` branch-only) so a
fast-moving branch that deploys off `main` can stay current with delta updates instead of a
full commit-addressed re-index on every HEAD advance.
- New `git.ref_key` field on every content doc: the bare commit SHA for snapshot content, or
`{host}~{org}~{repo}~{ref}` for incremental content (no `git.commit` of its own).
- A new refs "join doc" kind, `_id = git.ref_key`, carrying the citable commit for both modes
(snapshot: one per commit; incremental: one per branch, holding the live HEAD).
- Delta indexer for incremental branches: `git diff --name-status` between the previously
completed commit and the new tip drives which paths are deleted/reindexed; a missing diff
base (force-push, GC'd, first index) triggers a full branch-namespace rebuild.
- Two-phase incremental publication (`indexing` -> `ready`/`failed`) so a crash mid-update
leaves the prior commit and content in place.
- Every Agent Builder content tool now runs one universal query shape --
`WHERE git.ref_key == ?git_ref_key | LOOKUP JOIN sourcerer-refs ON git.ref_key` -- replacing
the old `git_commit` wildcard param with a required, exact `git_ref_key`.
- One-time, default-on upgrade backfill (`--no-backfill` to opt out): stamps `git.ref_key`/
`update_mode` onto pre-existing snapshot content, migrates the files/lines/refs index mappings
in place, and creates the missing per-commit join docs.
- A post-index uniqueness gate verifies every content `git.ref_key` resolves to exactly one refs
join doc, exiting non-zero on any violation.
- Docs (`README.md`, `AGENTS.md`, `sourcerer.example.yml`) and the ref-resolution skill updated
for the new `update:` knob and the universal join query.
Verified end-to-end against a local-dev cluster: a pre-upgrade snapshot baseline
(`elastic/sourcerer@v2.5.0`) upgrades in place, a new incremental source
(`elastic/serverless-gitops@main`) indexes ref-addressed and is idempotent on re-run, the
universal join query resolves a commit for both modes, and the uniqueness gate exits 0 for both.
- `sourcerer.refs.list`'s default `status: "complete"` filter silently omitted every `update: incremental` branch, whose join doc status is `"ready"`/`"indexing"`, never `"complete"`. The default (no-arg) call now also matches `status == "ready"`. - Reject `update: incremental` combined with `index.level: commit` at config-parse time. Incremental content carries no `git.commit`, so a commit-level index name (which requires one) could never be built, previously failing at runtime on every indexing attempt instead of being rejected up front.
git.ref_key is a storage/join implementation detail (how a content doc finds its refs join doc via LOOKUP JOIN) -- it never needed to be something an agent constructs or passes as a query param. Replace the `git_ref_key` param on all 9 Agent Builder content tools with `git_ref`: an exact commit SHA or branch/tag name, matched via `(git.commit == ?git_ref OR git.ref == ?git_ref)` before the LOOKUP JOIN, which still resolves `git.ref_key` internally to attach the citable commit. `sourcerer.refs.list` no longer surfaces `git.ref_key` in its output either. This restores the pre-incremental mental model for an agent: resolve a ref via `refs.list`, then pass the resolved value straight through to a content tool -- no new field to learn, no construction, no per-mode branching in the agent's own reasoning. Docs (AGENTS.md, README.md, ref-resolution SKILL.md) and tests updated accordingly.
This PR's incremental (ref-addressed) branch indexing is a big enough addition to the index
schema (new update:incremental mode, ref_key join field, incremental refs join docs) to warrant
its own index generation rather than folding it silently into v2. Rename every backing index
constant, template file, and doc reference from sourcerer-v2-* to sourcerer-v3-*:
- src/sourcerer/indices.py: FILES_INDEX_PREFIX, LINES_INDEX_PREFIX, REFS_INDEX now sourcerer-v3-*.
Read aliases (sourcerer-files/-lines/-refs) are unaffected.
- Index templates renamed sourcerer-v2-{files,lines,refs}.json -> sourcerer-v3-{...}.json, with
matching index_patterns.
- Doc/comment references across planner.py, queries.py, markers.py, schedule.py, prune/*, tests,
AGENTS.md, README.md, specs/sourcerer-yml.md, sourcerer.example.yml updated to v3.
- Added an "Upgrading from v2 to v3" section to AGENTS.md alongside the existing v1->v2 section,
covering the index rename and the git_ref Agent Builder tool param.
Existing sourcerer-v2-* installations require running `sourcerer setup` (to create the v3
templates) followed by a full re-index; the old v2-* indices can be deleted afterward. No config
schema change.
|
Thanks @simianhacker, acknowledging receipt of this PR. I've been testing it pretty heavily over the last couple days. Some minor fixes, possibly some semantic changes we can review, but most importantly, I'm almost certain I can adjust it to avoid introducing a |
…ent breaking changes when new fields are added to the mappings
…with snapshot indexing. Show the number of files and lines actually indexed in the progress bar for incremental indexing.
…tten to but never read or used in joins
… the refs join doc write so it's visible before the INV-011 gate runs
…t_ish param (resolved via a sourcerer-refs subquery) in the agent tools, and guard content reads with post-join status=='complete'
…rites one refs doc
…n (serendipitously improving search speed, too). Make incremental indexing compatible with sources[i].index.level and sources[i].index.suffix.
…ng, delete old copy, extend prune with Class D-I backstop
…ode, which better describes the behaviors of both 'snapshot' and 'incremental' since snapshots generally aren't updated.
…ase on index_level, index_suffix, and index_strategy in refs index.
…eld in the template for sourcerer-v3-refs.
|
@simianhacker I've added my contributions to this PR for review. Good news: I was able to remove More details on the notable changes: Semantic changes – worth understanding before reviewing changes:
Significant changes – worth reviewing and understanding in depth:
Smaller changes:
|
|
One more semantic change:
|
|
One last semantic change pushed: I think the public-facing values for |
|
Note: I might walk back the choice of "head" before we release this and return to "incremental" (or "delta" for brevity) to future-proof the feature to support fast-moving tags later (e.g. Kibana's deploy@ tags), which wouldn't fit the "head" metaphor. |
Delta mode now accepts git.ref_type: tag in addition to branch. This
enables cheap indexing of fast-moving tags like elastic/kibana's
deploy@{major} Serverless promotion tags: instead of minting a full
snapshot per force-update, a SHA-to-SHA diff touches only changed paths.
Changes:
- config.py: lift branch-only restriction to accept branch or tag
- utils.py: build_ref_key now takes ref_type, making branch and
same-named tag produce distinct join-doc _ids
- markers.py: thread ref_type through write_incremental_*, read,
delete_incremental_paths/branch, and count functions; replace
hardcoded "branch" literal in join doc body
- documents.py: add ref_type to build_incremental_file_doc,
iter_incremental_line_docs, _init_worker_incremental, and
index_incremental_paths; carry it into content doc git.ref_type field
and make_doc_id call
- command.py: derive ref_type from unit.kind; dispatch checkout_ref for
tags vs checkout_branch for branches; thread ref_type into all callers
- queries.py: add git.ref_type to prune sweep tuple keys (5-tuple) and
check_join_uniqueness composite agg; add
_enumerate_incremental_content_ref_pairs helper
- planner.py: update OrphanPlan and plan_orphans type annotations to
5-tuple (host, org, repo, ref_type, ref)
- prune/execute.py: unpack ref_type from 5-tuple and add to
delete-by-query filter so tag cleanup never touches same-named branch
- 9 content tool YAMLs: extend LOOKUP JOIN to include git.ref_type,
preventing join fan-out when a branch and tag share a name in delta mode
- index templates (files + lines): add git.ref_type keyword mapping
- Tests: update all markers/documents/config/incremental_index tests for
new ref_type params; add tag-kind test variants and checkout dispatch
assertions (254 tests pass)
- Docs: update AGENTS.md, README.md, sourcerer.example.yml,
specs/sourcerer-yml.md, and progress.py comment from "branch-only" to
"branch or tag"
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…plicate of git.ref. Drop git.ref from delta content docs and index sort. Replace error/failed_at with status:'failed'
…tify a commit as an orphan when its status is 'indexing'
|
Update: I was able to test and confirm that I tested this on the
I did observe that it took a couple minutes for the deltas to even begin to be indexed, which is unlike what I've seen for |
|
Quite a bit has changed since this PR was opened, so I want to summarize my understanding of where it is now. How things worked before this PRAll sources defined in It also works for indexing the commit points of branches. But branches tend to have different behaviors than release tags. That leads us to... The problem before this PRIndexing whole commit snapshots makes it difficult to keep up with branches that have frequent, small updates. For example, documentation repos are frequently updated on their default branch, even for changes as small as fixing a typo. A change that small would still trigger the indexing of another whole commit snapshot of that repo - just to reflect that one typo change. This is an unreasonable amount of processing and storage for such a small change, especially for a repo where the head of the default branch is typically all that matters to search, rather than specific points in its history. It would be better to keep a single snapshot of those repos and then add/update/remove just the files and lines that changed from the latest indexed commit to the most recent commit (i.e. their deltas). IaC has the same behavior: frequent, small updates to a branch where the current state usually matters more than its history. How this PR solves the problemThis PR introduces the concept of a source "mode" (defined in Here's how the two modes work:
The Agent Builder ES|QL tools have been modified to support both new modes seamlessly, without requiring an agent to know if it's searching content that was indexed in Ultimately, I believe this PR successfully refactors Sourcerer to support two very different modes of source curation, while keeping storage, indexing throughput, and query speeds optimal; and keeping tool interfaces intuitive to agents. There's room to make further optimizations in ways that wouldn't incur breaking changes from the current state of this PR. Therefore, I feel comfortable merging this PR and releasing it as v3.0.0, and postponing any non-breaking optimizations to future minor/patch releases. |
# Conflicts: # .claude-plugin/marketplace.json # README.md # pyproject.toml # uv.lock
The INV-00N references pointed at a private planning spec that outside readers of this repo cannot see or link to. Strip every label while keeping the plain-English rule each one annotated, so the comments stay self-explanatory on their own.
davemoore-
left a comment
There was a problem hiding this comment.
LGTM, validated with lots of offline testing. Great working with you @simianhacker
What
Adds delta (ref-addressed) branch indexing as an alternative to Sourcerer's existing
commit-addressed snapshot indexing, so a fast-moving branch (e.g. a GitOps/IaC repo that deploys
off
main) can stay current with a cheap delta update instead of a full re-index on every HEADadvance.
Set
mode: deltaon a branch source insourcerer.ymlto opt in;mode: snapshot(thedefault) is unchanged. Delta content is stored per-ref (carries
git.ref, nogit.commit); aHEAD advance diffs old tip vs. new tip and only deletes/reindexes the changed paths. Snapshot
content is stored per-commit as before (carries
git.commit, nogit.ref).Every Agent Builder content tool gains two new optional filters —
git_refandgit_ref_type—alongside the existing
git_commit. All three are wildcard filters resolved against the smallsourcerer-refsindex first, so one query shape serves both content shapes without the callerknowing which mode a source uses.
This is a big enough change to the index schema to ship as v3.0.0: backing indices move from
sourcerer-v2-*tosourcerer-v3-*. See Breaking Changes below.How to Verify
mode: deltato asourcerer.ymland runsourcerer index --config sourcerer.yml. Confirm the indexed content carriesgit.refandno
git.commit, and that a single refs join doc (keyed byhost~org~repo~ref) holds thecitable
git.commitfor that ref.changes (
sourcerer-files/sourcerer-linesdoc counts unchanged).deleted/reindexed (visible via the reported files/lines counts).
sourcerer setupagainst a fresh cluster (or one only holdingsourcerer-v2-*data), thensourcerer index github/<org>/<repo> -t <tag>— confirm it creates and populates the newsourcerer-v3-*indices from scratch (a v2->v3 index rename has no automatic migration).sourcerer setupto push the updated tool definitions, then callsourcerer.code.search/sourcerer.files.cat/etc. filtering bygit_commitagainst asnapshot source and by
git_ref(e.g.main) against a delta source — both return rows witha citable
git.commit.Query Change Example
Content docs come in two disjoint shapes: snapshot rows carry
git.commit(nogit.ref); deltarows carry
git.ref(nogit.commit). Each tool takes wildcard filtersgit_commit,git_ref,and
git_ref_type, resolves them against the smallsourcerer-refsindex, then selects matchingrows in the large content index:
git_commit = "a1b2c3d..."): matches snapshot rows, exactly like before — nojoin needed, the commit is already on the row.
git_ref = "main"): matches delta rows, and theLOOKUP JOINattaches thatref's current commit so every result is still citable to an exact commit.
Existing commit-filter usage is unchanged; ref-filtering is the new capability. There is no
synthetic join key — delta rows join back to their refs doc on
(host, org, repo, ref)directly.Breaking Changes
Bumps to v3.0.0:
sourcerer-v2-*tosourcerer-v3-*. No automaticmigration — run
sourcerer setupto create the v3 templates, then re-index every source. Theold
sourcerer-v2-*indices can be deleted once you've re-indexed.sourcerer.code.search,sourcerer.code.grep,sourcerer.files.*) gain two new optional wildcard filters,git_refandgit_ref_type;git_commitis unchanged, so existing calls keep working. Re-runsourcerer setupto push theupdated tool definitions.
mode: delta; omittingmodekeeps thehistorical snapshot behavior, so existing
sourcerer.ymlfiles work unmodified.