Skip to content

Commit a16cf19

Browse files
committed
merge: resolve conflicts with origin/main in ROADMAP.md
Take main's corrected #57 section anchors; keep HEAD's v2.7.0 version reference. Impact: 10 functions changed, 11 affected
2 parents 41d664f + 2bd997a commit a16cf19

11 files changed

Lines changed: 288 additions & 364 deletions

File tree

docs/roadmap/ROADMAP.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,7 @@ With analysis data loss fixed, optimize the 1-file rebuild path end-to-end. Curr
11101110

11111111
**Goal:** Migrate the codebase from plain JavaScript to TypeScript, leveraging the clean module boundaries established in Phase 3. Incremental module-by-module migration starting from leaf modules inward.
11121112

1113-
**Why after Phase 3:** The architectural refactoring creates small, well-bounded modules with explicit interfaces (Repository, Engine, BaseExtractor, Pipeline stages, Command objects). These are natural type boundaries -- typing monolithic 2,000-line files that are about to be split would be double work.
1113+
**Why after Phase 4:** The architectural refactoring (Phase 3) creates small, well-bounded modules with explicit interfaces. Phase 4 moves the remaining hot-path visitor code to Rust — doing TS migration first would mean rewriting those visitors to TypeScript only to delete them when porting to Rust. With both phases complete, the JS layer is purely orchestration and presentation, which is the ideal surface for TypeScript.
11141114

11151115
### 5.1 -- Project Setup
11161116

@@ -1227,14 +1227,14 @@ Migrate top-level orchestration and entry points:
12271227
- No coverage thresholds enforced in CI (coverage report runs locally only)
12281228
- Embedding tests in separate workflow requiring HuggingFace token
12291229
- 312 `setTimeout`/`sleep` instances in tests — potential flakiness under load
1230-
- No dependency audit step in CI (see also [5.7](#47----supply-chain-security--audit))
1230+
- No dependency audit step in CI (see also [5.7](#57----supply-chain-security--audit))
12311231

12321232
**Deliverables:**
12331233

12341234
1. **Coverage gate** -- add `vitest --coverage` to CI with minimum threshold (e.g. 80% lines/branches); fail the pipeline when coverage drops below the threshold
12351235
2. **Unified test workflow** -- merge embedding tests into the main CI workflow using a securely stored `HF_TOKEN` secret; eliminate the separate workflow
12361236
3. **Timer cleanup** -- audit and reduce `setTimeout`/`sleep` usage in tests; replace with deterministic waits (event-based, polling with backoff, or `vi.useFakeTimers()`) to reduce flakiness
1237-
4. > _Dependency audit step is covered by [5.7](#47----supply-chain-security--audit) deliverable 1._
1237+
4. > _Dependency audit step is covered by [5.7](#57----supply-chain-security--audit) deliverable 1._
12381238
12391239
**Affected files:** `.github/workflows/ci.yml`, `vitest.config.js`, `tests/`
12401240

src/db/connection.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import path from 'node:path';
33
import Database from 'better-sqlite3';
44
import { warn } from '../infrastructure/logger.js';
55
import { DbError } from '../shared/errors.js';
6+
import { Repository } from './repository/base.js';
7+
import { SqliteRepository } from './repository/sqlite-repository.js';
68

79
function isProcessAlive(pid) {
810
try {
@@ -86,3 +88,32 @@ export function openReadonlyOrFail(customPath) {
8688
}
8789
return new Database(dbPath, { readonly: true });
8890
}
91+
92+
/**
93+
* Open a Repository from either an injected instance or a DB path.
94+
*
95+
* When `opts.repo` is a Repository instance, returns it directly (no DB opened).
96+
* Otherwise opens a readonly SQLite DB and wraps it in SqliteRepository.
97+
*
98+
* @param {string} [customDbPath] - Path to graph.db (ignored when opts.repo is set)
99+
* @param {object} [opts]
100+
* @param {Repository} [opts.repo] - Pre-built Repository to use instead of SQLite
101+
* @returns {{ repo: Repository, close(): void }}
102+
*/
103+
export function openRepo(customDbPath, opts = {}) {
104+
if (opts.repo != null) {
105+
if (!(opts.repo instanceof Repository)) {
106+
throw new TypeError(
107+
`openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(opts.repo)}`,
108+
);
109+
}
110+
return { repo: opts.repo, close() {} };
111+
}
112+
const db = openReadonlyOrFail(customDbPath);
113+
return {
114+
repo: new SqliteRepository(db),
115+
close() {
116+
db.close();
117+
},
118+
};
119+
}

src/db/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Barrel re-export — keeps all existing `import { ... } from '…/db/index.js'` working.
2-
export { closeDb, findDbPath, openDb, openReadonlyOrFail } from './connection.js';
2+
export { closeDb, findDbPath, openDb, openReadonlyOrFail, openRepo } from './connection.js';
33
export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
44
export {
55
fanInJoinSQL,

src/domain/analysis/symbol-lookup.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
findNodesWithFanIn,
1313
listFunctionNodes,
1414
openReadonlyOrFail,
15+
Repository,
1516
} from '../../db/index.js';
1617
import { isTestFile } from '../../infrastructure/test-filter.js';
1718
import { ALL_SYMBOL_KINDS } from '../../shared/kinds.js';
@@ -23,11 +24,16 @@ const FUNCTION_KINDS = ['function', 'method', 'class'];
2324
/**
2425
* Find nodes matching a name query, ranked by relevance.
2526
* Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
27+
*
28+
* @param {object} dbOrRepo - A better-sqlite3 Database or a Repository instance
2629
*/
27-
export function findMatchingNodes(db, name, opts = {}) {
30+
export function findMatchingNodes(dbOrRepo, name, opts = {}) {
2831
const kinds = opts.kind ? [opts.kind] : opts.kinds?.length ? opts.kinds : FUNCTION_KINDS;
2932

30-
const rows = findNodesWithFanIn(db, `%${name}%`, { kinds, file: opts.file });
33+
const isRepo = dbOrRepo instanceof Repository;
34+
const rows = isRepo
35+
? dbOrRepo.findNodesWithFanIn(`%${name}%`, { kinds, file: opts.file })
36+
: findNodesWithFanIn(dbOrRepo, `%${name}%`, { kinds, file: opts.file });
3137

3238
const nodes = opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
3339

src/features/communities.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'node:path';
2-
import { openReadonlyOrFail } from '../db/index.js';
2+
import { openRepo } from '../db/index.js';
33
import { louvainCommunities } from '../graph/algorithms/louvain.js';
44
import { buildDependencyGraph } from '../graph/builders/dependency.js';
55
import { paginateResult } from '../shared/paginate.js';
@@ -26,15 +26,15 @@ function getDirectory(filePath) {
2626
* @returns {{ communities: object[], modularity: number, drift: object, summary: object }}
2727
*/
2828
export function communitiesData(customDbPath, opts = {}) {
29-
const db = openReadonlyOrFail(customDbPath);
29+
const { repo, close } = openRepo(customDbPath, opts);
3030
let graph;
3131
try {
32-
graph = buildDependencyGraph(db, {
32+
graph = buildDependencyGraph(repo, {
3333
fileLevel: !opts.functions,
3434
noTests: opts.noTests,
3535
});
3636
} finally {
37-
db.close();
37+
close();
3838
}
3939

4040
// Handle empty or trivial graphs

src/features/sequence.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* sequence-diagram conventions.
77
*/
88

9-
import { findCallees, openReadonlyOrFail } from '../db/index.js';
9+
import { openRepo } from '../db/index.js';
10+
import { SqliteRepository } from '../db/repository/sqlite-repository.js';
1011
import { findMatchingNodes } from '../domain/queries.js';
1112
import { isTestFile } from '../infrastructure/test-filter.js';
1213
import { paginateResult } from '../shared/paginate.js';
@@ -85,19 +86,19 @@ function buildAliases(files) {
8586
* @returns {{ entry, participants, messages, depth, totalMessages, truncated }}
8687
*/
8788
export function sequenceData(name, dbPath, opts = {}) {
88-
const db = openReadonlyOrFail(dbPath);
89+
const { repo, close } = openRepo(dbPath, opts);
8990
try {
9091
const maxDepth = opts.depth || 10;
9192
const noTests = opts.noTests || false;
9293
const withDataflow = opts.dataflow || false;
9394

9495
// Phase 1: Direct LIKE match
95-
let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;
96+
let matchNode = findMatchingNodes(repo, name, opts)[0] ?? null;
9697

9798
// Phase 2: Prefix-stripped matching
9899
if (!matchNode) {
99100
for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
100-
matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
101+
matchNode = findMatchingNodes(repo, `${prefix}${name}`, opts)[0] ?? null;
101102
if (matchNode) break;
102103
}
103104
}
@@ -133,7 +134,7 @@ export function sequenceData(name, dbPath, opts = {}) {
133134
const nextFrontier = [];
134135

135136
for (const fid of frontier) {
136-
const callees = findCallees(db, fid);
137+
const callees = repo.findCallees(fid);
137138

138139
const caller = idToNode.get(fid);
139140

@@ -163,18 +164,17 @@ export function sequenceData(name, dbPath, opts = {}) {
163164

164165
if (d === maxDepth && frontier.length > 0) {
165166
// Only mark truncated if at least one frontier node has further callees
166-
const hasMoreCalls = frontier.some((fid) => findCallees(db, fid).length > 0);
167+
const hasMoreCalls = frontier.some((fid) => repo.findCallees(fid).length > 0);
167168
if (hasMoreCalls) truncated = true;
168169
}
169170
}
170171

171172
// Dataflow annotations: add return arrows
172173
if (withDataflow && messages.length > 0) {
173-
const hasTable = db
174-
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='dataflow'")
175-
.get();
174+
const hasTable = repo.hasDataflowTable();
176175

177-
if (hasTable) {
176+
if (hasTable && repo instanceof SqliteRepository) {
177+
const db = repo.db;
178178
// Build name|file lookup for O(1) target node access
179179
const nodeByNameFile = new Map();
180180
for (const n of idToNode.values()) {
@@ -281,7 +281,7 @@ export function sequenceData(name, dbPath, opts = {}) {
281281
}
282282
return result;
283283
} finally {
284-
db.close();
284+
close();
285285
}
286286
}
287287

src/features/triage.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { findNodesForTriage, openReadonlyOrFail } from '../db/index.js';
1+
import { openRepo } from '../db/index.js';
22
import { DEFAULT_WEIGHTS, scoreRisk } from '../graph/classifiers/risk.js';
33
import { warn } from '../infrastructure/logger.js';
44
import { isTestFile } from '../infrastructure/test-filter.js';
@@ -14,7 +14,7 @@ import { paginateResult } from '../shared/paginate.js';
1414
* @returns {{ items: object[], summary: object, _pagination?: object }}
1515
*/
1616
export function triageData(customDbPath, opts = {}) {
17-
const db = openReadonlyOrFail(customDbPath);
17+
const { repo, close } = openRepo(customDbPath, opts);
1818
try {
1919
const noTests = opts.noTests || false;
2020
const fileFilter = opts.file || null;
@@ -26,7 +26,7 @@ export function triageData(customDbPath, opts = {}) {
2626

2727
let rows;
2828
try {
29-
rows = findNodesForTriage(db, {
29+
rows = repo.findNodesForTriage({
3030
noTests,
3131
file: fileFilter,
3232
kind: kindFilter,
@@ -115,7 +115,7 @@ export function triageData(customDbPath, opts = {}) {
115115
offset: opts.offset,
116116
});
117117
} finally {
118-
db.close();
118+
close();
119119
}
120120
}
121121

src/graph/builders/dependency.js

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,39 @@
33
* Replaces inline graph construction in cycles.js, communities.js, viewer.js, export.js.
44
*/
55

6-
import { getCallableNodes, getCallEdges, getFileNodesAll, getImportEdges } from '../../db/index.js';
6+
import {
7+
getCallableNodes,
8+
getCallEdges,
9+
getFileNodesAll,
10+
getImportEdges,
11+
Repository,
12+
} from '../../db/index.js';
713
import { isTestFile } from '../../infrastructure/test-filter.js';
814
import { CodeGraph } from '../model.js';
915

1016
/**
11-
* @param {object} db - Open better-sqlite3 database (readonly)
17+
* @param {object} dbOrRepo - Open better-sqlite3 database (readonly) or a Repository instance
1218
* @param {object} [opts]
1319
* @param {boolean} [opts.fileLevel=true] - File-level (imports) or function-level (calls)
1420
* @param {boolean} [opts.noTests=false] - Exclude test files
1521
* @param {number} [opts.minConfidence] - Minimum edge confidence (function-level only)
1622
* @returns {CodeGraph}
1723
*/
18-
export function buildDependencyGraph(db, opts = {}) {
24+
export function buildDependencyGraph(dbOrRepo, opts = {}) {
1925
const fileLevel = opts.fileLevel !== false;
2026
const noTests = opts.noTests || false;
2127

2228
if (fileLevel) {
23-
return buildFileLevelGraph(db, noTests);
29+
return buildFileLevelGraph(dbOrRepo, noTests);
2430
}
25-
return buildFunctionLevelGraph(db, noTests, opts.minConfidence);
31+
return buildFunctionLevelGraph(dbOrRepo, noTests, opts.minConfidence);
2632
}
2733

28-
function buildFileLevelGraph(db, noTests) {
34+
function buildFileLevelGraph(dbOrRepo, noTests) {
2935
const graph = new CodeGraph();
36+
const isRepo = dbOrRepo instanceof Repository;
3037

31-
let nodes = getFileNodesAll(db);
38+
let nodes = isRepo ? dbOrRepo.getFileNodesAll() : getFileNodesAll(dbOrRepo);
3239
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
3340

3441
const nodeIds = new Set();
@@ -37,7 +44,7 @@ function buildFileLevelGraph(db, noTests) {
3744
nodeIds.add(n.id);
3845
}
3946

40-
const edges = getImportEdges(db);
47+
const edges = isRepo ? dbOrRepo.getImportEdges() : getImportEdges(dbOrRepo);
4148
for (const e of edges) {
4249
if (!nodeIds.has(e.source_id) || !nodeIds.has(e.target_id)) continue;
4350
const src = String(e.source_id);
@@ -51,10 +58,11 @@ function buildFileLevelGraph(db, noTests) {
5158
return graph;
5259
}
5360

54-
function buildFunctionLevelGraph(db, noTests, minConfidence) {
61+
function buildFunctionLevelGraph(dbOrRepo, noTests, minConfidence) {
5562
const graph = new CodeGraph();
63+
const isRepo = dbOrRepo instanceof Repository;
5664

57-
let nodes = getCallableNodes(db);
65+
let nodes = isRepo ? dbOrRepo.getCallableNodes() : getCallableNodes(dbOrRepo);
5866
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
5967

6068
const nodeIds = new Set();
@@ -70,11 +78,16 @@ function buildFunctionLevelGraph(db, noTests, minConfidence) {
7078

7179
let edges;
7280
if (minConfidence != null) {
73-
edges = db
74-
.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND confidence >= ?")
75-
.all(minConfidence);
81+
if (isRepo) {
82+
// minConfidence filtering not supported by Repository — fall back to getCallEdges
83+
edges = dbOrRepo.getCallEdges();
84+
} else {
85+
edges = dbOrRepo
86+
.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls' AND confidence >= ?")
87+
.all(minConfidence);
88+
}
7689
} else {
77-
edges = getCallEdges(db);
90+
edges = isRepo ? dbOrRepo.getCallEdges() : getCallEdges(dbOrRepo);
7891
}
7992

8093
for (const e of edges) {

0 commit comments

Comments
 (0)