Derive dependency edges at index time instead of on every graph request - #5
Merged
Merged
Conversation
Prerequisite for putting the graph in Neo4j, and worth landing on its own.
generate_graph re-derived the whole edge set on every cache miss, and
_build_deterministic_edges did it by reading every source file off disk inside the
request. Three consequences:
1. Graph latency was proportional to repository size, with blocking file I/O on the
event loop. The 45-second in-process TTL cache existed to hide this, and being
per-process it bought nothing across workers.
2. The graph was coupled to the clone still being present. After a redeploy, a
container restart or a volume reset, derivation silently fell back to the
unresolved import strings on CodeFile.imports and produced a worse graph for the
same repository, with nothing indicating it had happened.
3. Every graph request paid for work whose inputs only change when the repo is
re-indexed.
Derivation now runs once, at the end of indexing, while the clone is guaranteed to
exist, into a new code_dependencies table. The read path is a single indexed query.
- database.py: CodeDependency (source_path, target_path, relation, weight, confidence),
indexed on repository_id and on each endpoint, unique on
(repo, source, target, relation). Added to Repository.dependencies with
delete-orphan cascade -- without it, DELETE /api/repos/{id} would leave orphaned
edges that union into a later re-import of the same repository.
- indexing_service.py: _persist_dependency_graph runs after embedding, wrapped so a
derivation failure cannot fail an otherwise good index. It reuses LearningService's
existing resolution helpers rather than reimplementing them, so there is still one
definition of what an edge is, and offloads the blocking file reads with
asyncio.to_thread. _reset_repository_index_data now clears edges too.
- learning_service.py: _load_or_derive_edges prefers persisted rows and falls back to
on-demand derivation for repositories indexed before this table existed or whose
derivation step failed; both self-heal on the next re-index, and the fallback logs
that it ran. Edges whose endpoints are not in the requested node set are dropped --
scope filters and the node cap can remove a node while its edges remain, which would
otherwise ship an edge to a node the client never received.
_build_deterministic_edges is unchanged, so its existing unit tests still cover the
derivation logic directly.
Verified: 6 new tests, 117 total (was 111), ruff clean. The important one deletes the
clone and asserts the graph is still complete from the database, while asserting that
on-demand derivation returns nothing in that same state -- which is exactly the silent
degradation this removes. Others cover cascade-on-delete, stale-edge clearing on
re-index, and out-of-scope edge filtering.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Prerequisite for putting the graph in Neo4j — and worth landing on its own.
The problem
generate_graphre-derived the whole edge set on every cache miss, and_build_deterministic_edgesdid it by reading every source file off disk inside the request. Three consequences:CodeFile.importsand produced a worse graph for the same repository — with nothing indicating it had happened.The change
Derivation runs once, at the end of indexing, while the clone is guaranteed to exist, into a new
code_dependenciestable. The read path is a single indexed query.CodeDependency— indexed onrepository_idand on each endpoint, unique on(repo, source, target, relation). Wired intoRepository.dependencieswithdelete-orphancascade: without it,DELETE /api/repos/{id}would leave orphaned edges that union into a later re-import._persist_dependency_graphruns after embedding, wrapped so a derivation failure cannot fail an otherwise good index. It reusesLearningService's resolution helpers rather than reimplementing them, so there's still one definition of what an edge is, and offloads the blocking reads viaasyncio.to_thread._load_or_derive_edgesprefers persisted rows, falling back to on-demand derivation for repos indexed before this table existed or whose derivation failed — both self-heal on re-index, and the fallback logs that it ran._build_deterministic_edgesis unchanged, so its existing unit tests still cover the derivation logic directly.Verification
117 tests pass (was 111), ruff clean.
The test that matters deletes the clone, then asserts the graph is still complete from the database — and asserts that on-demand derivation returns nothing in that same state:
That second assertion is the point: it demonstrates the silent degradation this removes, rather than just claiming it.
Others cover cascade-on-delete, stale-edge clearing on re-index, and out-of-scope filtering.
Why this is a separate PR from Neo4j
The Neo4j read model needs edges that exist independently of the working tree — that's this. Landing it separately means the performance and correctness win is reviewable on its own, and doesn't depend on whether Neo4j is adopted.
🤖 Generated with Claude Code