Skip to content

refactor(core): GraphRepository — complete RULE-ARCH-003 repository abstraction - #165

Merged
dgtalbug merged 5 commits into
mainfrom
feature/graph-repository
Jun 7, 2026
Merged

refactor(core): GraphRepository — complete RULE-ARCH-003 repository abstraction#165
dgtalbug merged 5 commits into
mainfrom
feature/graph-repository

Conversation

@dgtalbug

@dgtalbug dgtalbug commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Summary

Completes RULE-ARCH-003 (repository pattern). The backend-stabilization PR confined the DuckDB driver to one adapter; this puts the storage + query operations behind a GraphRepository interface — the connection is held by the adapter and never appears in a method signature, so no domain code touches a connection. A narrow ForeignGraphReader covers the extension's read-only foreign-workspace access. DuckTreeIndexer now depends on the interface instead of a DatabaseHandle.

Behavior-preserving throughout; the public contract is unchanged. Full suite green at every phase (core 274 / exporters 161 / extension 640).

Linked spec / issue

Slice classification

  • refactor — internal architecture change, no behavior delta (one additive test fake)

Affected surfaces

  • packages/core (interfaces, adapter, indexer, fake)
  • packages/extension (foreign-reader call sites + one test mock)

Phases (commit-by-commit)

  1. db52387 interfacesGraphRepository + ForeignGraphReader, no GraphDbConnection in any signature
  2. e5208f0 adapterDuckDbGraphRepository delegating to the existing storage/query fns; delegation tests vs real in-memory DuckDB
  3. d080618 indexer swapDuckTreeIndexer holds this.repo; the 15 requireDatabaseHandle()/database.connection sites become this.repo.X(); index.ts is now connection-free
  4. 594839a foreign reads + connection-free public API — extension's two foreign-read sites use DuckDbForeignGraphReader; core's public barrel drops the connection-first re-exports
  5. c7d95ff test fakeFakeGraphRepository proves substitutability through the interface

Scope decisions (where the plan met reality)

  • Connection-first fns are internalized, not deleted. After the indexer swap they're called only by the adapter and by core's own tests (via relative imports). Inlining ~17 SQL bodies into the adapter would be large churn with no behavioral gain, so they remain internal implementation the adapter delegates to. What was removed is their presence in core's public API — no GraphDbConnection-taking signature is exported anymore.
  • No ~30-site test rewrite was needed. Core tests import the bare fns by relative path, not the dropped barrel exports, so they were unaffected. Only the one extension foreign-reader mock changed.
  • The fake proves the contract, not a constructor seam. FakeGraphRepository is consumed through the GraphRepository type. Wiring it into the indexer via constructor injection would change the public createIndexer contract, so that's left as a future enhancement.

Test plan

  • pnpm check green across all packages (forced/uncached): core 274, exporters 161, extension 640
  • No GraphDbConnection appears in any GraphRepository method signature
  • index.ts has zero database.connection references — the DIP boundary is real
  • Public Indexer contract unchanged — extension suite passes with no consumer edits beyond the two foreign-reader sites
  • Adapter delegation verified against a real in-memory DuckDB; FakeGraphRepository proves substitutability
  • spec-tag guard + pnpm dedupe --check clean

Checklist

  • PR title follows Conventional Commits
  • Closes: references the tracking issue
  • format / lint / typecheck / test pass
  • No new any without justification
  • No native deps added to packages/core
  • Public contract preserved — additive/behavior-preserving; consumers + full suite unaffected

Notes for reviewers

  • The headline: ARCH-003 is now complete — the connection is owned by the adapter and never escapes into domain code or the public API. A different store (or the in-memory fake) implements the same interface.
  • The ForeignGraphReader split is deliberate (design D2): foreign reads open arbitrary DBs by path and are driven by the extension, so they don't belong on the connection-owning repository.
  • Lens selectors are intentionally NOT repository methods — they operate on the in-memory graphology graph, not the DB.

🤖 Generated with Claude Code

dgtalbug added 5 commits June 7, 2026 16:43
…hase 1)

The storage + query operations as one interface (RULE-ARCH-003), connection held
by the future adapter — no GraphDbConnection in any signature. ForeignGraphReader
is a separate read-only port for foreign-workspace DB access (the switcher), since
those reads open arbitrary DBs by path and are driven by the extension. Lens
selectors stay out (in-memory graphology, not storage).

Pure addition — no implementation, no callers yet. core 266/266, typecheck +
lint clean.

Refs: #164
… fns (phase 2)

Implements GraphRepository over an open DatabaseHandle, supplying the connection
to each existing storage/query function so callers never pass one. Stateless
DuckDbForeignGraphReader for the read-only foreign-workspace functions (they open
by path themselves). Thin relocation behind the interface — zero behavior change.

Adapter tests prove delegation against a real in-memory DuckDB (write/read
round-trip, clearWorkspace/clearFile parity, coverage/edge-kinds, dispose closes
the handle). Two test expectations corrected to real behavior: replaceFileGraph
synthesizes a DEFINES edge per file (deletedEdges=2), and clearFile matches the
absolute stored path (what the watcher passes), not the relative path.

No caller migration yet — shippable on its own. core 271/271, typecheck + lint
clean.

Refs: #164
…dle (phase 3)

DuckTreeIndexer now constructs a DuckDbGraphRepository over the opened handle and
calls this.repo.X() everywhere — the 15 requireDatabaseHandle()/database.connection
sites are gone, replaced by a single requireRepo(). initialize() builds the repo
then drives initializeSchema/applyMigrations through it; dispose() calls
repo.dispose() (which closes the handle). index.ts no longer references a
connection at all — the DIP boundary of RULE-ARCH-003 is achieved; the composition
root still names openDatabase + DuckDbGraphRepository, which is correct.

Import surface collapsed from ~19 storage/query functions to openDatabase +
DuckDbGraphRepository (the query re-exports are independent export-from lines).
Public Indexer contract unchanged: core 271/271, extension 640/640 with no
consumer edits.

Refs: #164
…free public API (phase 4)

The extension's two foreign-workspace read sites (extension.ts switch, cache/
workspaceRegistry list) now use DuckDbForeignGraphReader instead of the bare
readWorkspaceGraph/readWorkspaceIndexSummary functions. Core's public barrel
(index.ts) drops the connection-first re-exports (getPresentEdgeKinds/
getCoverageReport/neighborhood) and the bare foreign-read fns, exporting the
GraphRepository/ForeignGraphReader interfaces + DuckDbForeignGraphReader adapter
instead. The internal storage barrel stops re-exporting connection-first
functions too.

Result: no GraphDbConnection-taking signature appears anywhere in core's public
API — the only supported entry to storage is the repository interface + its
adapter (RULE-ARCH-003). The connection-first functions remain as internal
implementation the adapter delegates to (not deleted — that would mean inlining
all SQL into the adapter for no behavioral gain).

Extension test mock updated to mock the DuckDbForeignGraphReader class (via
vi.hoisted). core 271/271, extension 640/640, typecheck + lint clean.

Refs: #164
…e (phase 5)

In-memory GraphRepository implementation (per-file symbol storage, real clears,
cache round-trip; analytics reads return valid empty shapes). The type checker
enforces it satisfies the whole interface, and the test consumes it only through
the GraphRepository type — demonstrating substitutability (RULE-ARCH-003) without
a DuckDB instance.

The indexer still constructs its DuckDbGraphRepository internally; wiring a
constructor seam to inject an arbitrary repository would change the public
createIndexer contract, so that's left as a future enhancement — the fake proves
the contract is substitutable at the interface level. core 274/274.

Refs: #164
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ca6d4b0-ee31-4f8a-bee5-22130f5752ac

📥 Commits

Reviewing files that changed from the base of the PR and between a898290 and c7d95ff.

📒 Files selected for processing (10)
  • packages/core/src/index.ts
  • packages/core/src/storage/adapters/duckdbGraphRepository.test.ts
  • packages/core/src/storage/adapters/duckdbGraphRepository.ts
  • packages/core/src/storage/fakeGraphRepository.test.ts
  • packages/core/src/storage/fakeGraphRepository.ts
  • packages/core/src/storage/graphRepository.ts
  • packages/core/src/storage/index.ts
  • packages/extension/src/cache/workspaceRegistry.test.ts
  • packages/extension/src/cache/workspaceRegistry.ts
  • packages/extension/src/extension.ts

📝 Walkthrough

Summary

User-visible or reviewer-relevant changes:

  • GraphRepository abstraction introduced — Hides database connection behind a new GraphRepository interface. DuckDbGraphRepository delegates to existing storage/query functions; DuckTreeIndexer now depends on the interface instead of DatabaseHandle, eliminating ~15 direct connection sites from indexer code.
  • Separated foreign-workspace reads — New ForeignGraphReader interface (implemented by DuckDbForeignGraphReader) provides read-only, path-based access to external workspace databases. Extension code now instantiates this reader instead of calling removed barrel exports.
  • Public API narrowed — Removed connection-first and repository-level exports from storage barrel (openDatabase, readRows, runInTransaction, applyMigrations, replaceFileGraph, initializeSchema). Only DatabaseHandle, migration types, the new interfaces, and DuckDB adapter classes remain public.
  • No behavior changes — Adapter delegates transparently to existing functions; all tests pass (core 274, exporters 161, extension 640). Indexer and graph model contracts preserved.

Risks:

  • Coverage gaps — Patch coverage at 77.35%; fakeGraphRepository.ts at 65.71%, duckdbGraphRepository.ts at 84.61%, and index.ts at 82.22% have uncovered branches.
  • API breaking change — Removal of public query/storage exports (getPresentEdgeKinds, getCoverageReport, neighborhood, openDatabase, etc.) breaks any external direct usage. Extension tests required updating to use DuckDbForeignGraphReader instances.

Deferred or not validated:

  • Test fake (FakeGraphRepository) added to validate interface contract but indexer construction seam left for future work.
  • No mass rewrite of existing tests; tests continue to use real DuckDB instances via adapter.

Package boundaries & schema:

  • No schema changes. Storage responsibility shifted from domain code to adapter; domain signatures no longer carry DatabaseHandle or connection objects.
  • Extension boundary clarified: reads now routed through ForeignGraphReader rather than removed barrel helpers.

Walkthrough

This PR completes RULE-ARCH-003 by abstracting storage and query operations behind a GraphRepository interface. A new DuckDbGraphRepository adapter wraps the database handle and delegates operations to existing functions. The core indexer is refactored to use the repository instead of raw database handles across 15+ call sites. An in-memory FakeGraphRepository provides a test double. The extension migrates to use DuckDbForeignGraphReader for read-only cross-workspace access.

Changes

Graph Repository Abstraction

Layer / File(s) Summary
Repository Interface Contracts and Storage Barrel
packages/core/src/storage/graphRepository.ts, packages/core/src/storage/index.ts
GraphRepository and ForeignGraphReader interfaces define the contract for storage/query operations and cross-workspace read access. Storage barrel removes low-level connection-first function exports and exposes only repository types, adapters, and schema constants.
DuckDB Repository Adapter Implementation
packages/core/src/storage/adapters/duckdbGraphRepository.ts
DuckDbGraphRepository wraps a DatabaseHandle and implements GraphRepository by delegating all operations to existing connection-first functions. DuckDbForeignGraphReader provides stateless foreign database read access.
DuckDB Adapter Integration Tests
packages/core/src/storage/adapters/duckdbGraphRepository.test.ts
Verifies DuckDbGraphRepository against real DuckDB: file graph round-trips, symbol retrieval, workspace subgraph queries, clear operations with deletion summaries, and lifecycle disposal.
Fake Repository Implementation and Conformance Tests
packages/core/src/storage/fakeGraphRepository.ts, packages/core/src/storage/fakeGraphRepository.test.ts
FakeGraphRepository provides in-memory storage for unit testing, storing files and symbols by relative path. Tests verify substitutability via schema initialization, file round-trips, cache lifecycle, and clear operations.
Core Indexer Migrated to Repository Delegation
packages/core/src/index.ts
Refactored DuckTreeIndexer to hold a DuckDbGraphRepository instead of DatabaseHandle. All indexing, query, and lifecycle operations—initialization, file graph replacement, symbol retrieval, framework/edge resolution, workspace queries, cache validation, coverage/neighborhood reporting, and disposal—now delegate through repository methods. Exports simplified to surface only repository types and adapters.
Extension and Registry Updated for ForeignGraphReader
packages/extension/src/cache/workspaceRegistry.ts, packages/extension/src/cache/workspaceRegistry.test.ts, packages/extension/src/extension.ts
Workspace registry and extension instantiate DuckDbForeignGraphReader to call its readWorkspaceIndexSummary and readWorkspaceGraph instance methods, replacing imported standalone helpers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Possibly related PRs

  • dgtalbug/dextree#118: Modifies replaceFileGraph invocation in the indexing flow to pass per-symbol classifications, intersecting with the layer that routes file graph replacement through DuckDbGraphRepository.
  • dgtalbug/dextree#163: Updates extension workspace switching and registry to use DuckDbForeignGraphReader, the same new public API boundary that this PR exports and establishes.
  • dgtalbug/dextree#137: Adds Logger and IndexerFactoryOptions plumbing through indexer initialization, overlapping with the new repository-based initialization flow in this PR.

Suggested labels

area:core, type:feat


Poem

🏗️ Abstraction rises, connection-free,
One repository to query thee,
Adapters hide the handle's grasp,
Fakes make testing less of a clasp,
Fifteen call sites now delegate with grace.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 77.35849% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/core/src/storage/fakeGraphRepository.ts 65.71% 8 Missing and 4 partials ⚠️
packages/core/src/index.ts 82.22% 5 Missing and 3 partials ⚠️
...core/src/storage/adapters/duckdbGraphRepository.ts 84.61% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@dgtalbug
dgtalbug marked this pull request as ready for review June 7, 2026 12:50
@dgtalbug
dgtalbug merged commit b854823 into main Jun 7, 2026
12 of 14 checks passed
@dgtalbug
dgtalbug deleted the feature/graph-repository branch June 7, 2026 12:51
dgtalbug added a commit that referenced this pull request Jun 7, 2026
#167)

* feat(core): persisted precise (pass-2) resolution write path

Adds the core half of persisted precise call resolution (the unbuilt 'pass-2 LSP
fills them in' the edge schema references). New GraphRepository methods:
- getUnresolvedCallSites(workspaceRoot): CALLS edges not yet precise, each with
  the source symbol's NodeLocation (file + start line/col) for the LSP query
- findSymbolIdAt(filePath, line): map an LSP result location back to a stored
  target symbol id (file + start line; null on no/ambiguous match)
- persistPreciseEdges(resolutions): set target_id + stamp
  metadata.resolution='precise' (confidence 1.0) in one transaction

Implemented in storage/preciseResolution.ts (behind the adapter) + the in-memory
FakeGraphRepository (with a seedCallSite test helper). The existing
stampResolutionTier already guards <> 'precise', so precise edges are never
downgraded — proven by test. Real-DuckDB tests cover list/match/persist,
idempotency, and no-downgrade. core 279/279.

Refs: #166

* feat: persist precise (LSP) call resolution via opt-in command

Wires the pass-2 precise resolution end to end. New Indexer.resolvePreciseEdges
(workspaceRoot, resolver, {onProgress, isCancelled}) orchestrates the core loop:
work-list (getUnresolvedCallSites) → host resolver per site → match each result
back to a target (findSymbolIdAt) → batch persist (persistPreciseEdges). The host
owns only the LSP resolver (RULE-ARCH-005); all DB access stays behind the
repository.

Extension: createResolvePreciseEdgesCommand runs the pass under a cancellable
withProgress notification (reports N/M sites · K upgraded) and refreshes the graph
on finish. Registered as dextree.resolvePreciseEdges / 'Resolve Precise Edges
(LSP)' in activate + package.json.

Behavior-preserving + opt-in: nothing runs unless invoked; idempotent (precise
edges drop out of the work-list); graceful (cold/empty resolver leaves edges
heuristic); cancellable (persists partial). Integration tests against a real
in-memory DB cover upgrade, idempotency, cold resolver, and cancellation; command
tests cover the UI shell. Indexer mocks across extension tests gained the method.

core 282 / exporters 161 / extension 643.

Refs: #166
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Jun 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GraphRepository: complete RULE-ARCH-003 (repository abstraction over storage/query)

2 participants