Skip to content

Derive dependency edges at index time instead of on every graph request - #5

Merged
ShreeBohara merged 1 commit into
mainfrom
feat/persist-dependency-graph
Aug 10, 2026
Merged

Derive dependency edges at index time instead of on every graph request#5
ShreeBohara merged 1 commit into
mainfrom
feat/persist-dependency-graph

Conversation

@ShreeBohara

Copy link
Copy Markdown
Owner

Prerequisite for putting the graph in Neo4j — and worth landing on its own.

The problem

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 proportional to repository size, with blocking file I/O on the event loop. The 45-second TTL cache existed to hide this, and being an in-process dict it bought nothing across workers.
  2. The graph was coupled to the clone still existing. After a redeploy, container restart or 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 request paid for work whose inputs only change on re-index.

The change

Derivation 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.

  • CodeDependency — indexed on repository_id and on each endpoint, unique on (repo, source, target, relation). Wired into Repository.dependencies with delete-orphan cascade: without it, DELETE /api/repos/{id} would leave orphaned edges that union into a later re-import.
  • _persist_dependency_graph runs after embedding, wrapped so a derivation failure cannot fail an otherwise good index. It reuses LearningService's resolution helpers rather than reimplementing them, so there's still one definition of what an edge is, and offloads the blocking reads via asyncio.to_thread.
  • _load_or_derive_edges prefers 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.
  • Edges whose endpoints aren't 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.

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:

shutil.rmtree(clone)
edges = service._load_or_derive_edges(...)
assert {(e.source, e.target) for e in edges} == persisted_pairs

derived = service._build_deterministic_edges(...)
assert not derived, "clone is gone, so on-demand derivation has nothing to read"

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

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>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
codebaseqa-web Ready Ready Preview Aug 9, 2026 8:06pm

@ShreeBohara
ShreeBohara merged commit 0cf2937 into main Aug 10, 2026
4 checks passed
@ShreeBohara
ShreeBohara deleted the feat/persist-dependency-graph branch August 10, 2026 01:17
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.

1 participant