Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d6c3ce3
feat: type domain/analysis modules, parser WASM maps, and add canStri…
carlos-alm Mar 21, 2026
cc65d63
chore: remove original .js files replaced by TypeScript conversions
carlos-alm Mar 21, 2026
2d5b3d2
Revert "chore: remove original .js files replaced by TypeScript conve…
carlos-alm Mar 21, 2026
292c317
fix: merge main after upstream PR merges
carlos-alm Mar 21, 2026
1e77de0
fix: drop misleading underscore prefix on used variables in node-vers…
carlos-alm Mar 21, 2026
1574c1e
fix: add Statement.raw() to vendor types and remove unnecessary as-an…
carlos-alm Mar 21, 2026
41c6f4d
fix: move inline SQL queries in context.ts behind db repository layer
carlos-alm Mar 21, 2026
aa2a7d0
fix: align customDbPath type to string in implementations.ts
carlos-alm Mar 21, 2026
208e30f
fix: eliminate N+1 re-query in buildTransitiveCallers by passing call…
carlos-alm Mar 21, 2026
6db5ae7
Merge branch 'main' into feat/ts-deferred-followups
carlos-alm Mar 21, 2026
81f7efe
Merge branch 'main' into feat/ts-deferred-followups
carlos-alm Mar 21, 2026
3e3cb50
fix: hoist db.prepare() out of BFS loops and include n.id in SELECT t…
carlos-alm Mar 21, 2026
b3fc6d5
fix: promote TS_BACKFILL_EXTS to module-level constant, remove 3 dupl…
carlos-alm Mar 21, 2026
7a86aa1
chore: update package-lock.json
carlos-alm Mar 21, 2026
87b7ab7
Merge branch 'feat/ts-deferred-followups' of https://github.com/optav…
carlos-alm Mar 21, 2026
59cc1e1
fix: eliminate duplicate findCallers() call and hoist db.prepare() ou…
carlos-alm Mar 22, 2026
204c3a6
fix: auto-format exports.ts and parser.ts
carlos-alm Mar 22, 2026
b4f3979
fix: hoist all db.prepare() calls out of fileNodes.map() loop in expo…
carlos-alm Mar 22, 2026
a4597aa
fix: resolve merge conflicts with main
carlos-alm Mar 22, 2026
c85f05d
fix: hoist db.prepare() calls and cache schema probes per Greptile P2…
carlos-alm Mar 22, 2026
2d30003
fix: resolve TypeScript type mismatches from merge conflict resolution
carlos-alm Mar 22, 2026
5d965c9
fix: hoist db.prepare() out of changedRanges loop in findAffectedFunc…
carlos-alm Mar 22, 2026
3d33583
fix: add null guard for result.get(relPath) in typeMap backfill (#558)
carlos-alm Mar 22, 2026
eec9cc7
Merge remote-tracking branch 'origin/main' into fix/review-558
carlos-alm Mar 23, 2026
bf724f3
fix: resolve DisplayOpts type incompatibility with file-utils.ts type…
carlos-alm Mar 23, 2026
5819775
fix: deduplicate LanguageRegistryEntry, align DB type imports, type W…
carlos-alm Mar 23, 2026
4b197cd
fix: convert db.prepare() calls to StmtCache/cachedStmt pattern in ex…
carlos-alm Mar 23, 2026
00b2fd7
fix: de-duplicate findCallers in contextData, convert bare db.prepare…
carlos-alm Mar 23, 2026
16cbf74
fix: remove stale limit/offset from MCP impact_analysis and constrain…
carlos-alm Mar 23, 2026
55b2103
fix: guard exportedNodesStmt preparation behind hasExportedCol check
carlos-alm Mar 23, 2026
16ddea5
fix: add file field to ChildNodeRow and remove intersection cast (#558)
carlos-alm Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/db/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export {
getFileNodesAll,
getFunctionNodeId,
getImportEdges,
getLineCountForNode,
getMaxEndLineForFile,
getNodeId,
hasCfgTables,
hasCoChanges,
Expand Down
2 changes: 2 additions & 0 deletions src/db/repository/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export {
findNodesForTriage,
findNodesWithFanIn,
getFunctionNodeId,
getLineCountForNode,
getMaxEndLineForFile,
getNodeId,
iterateFunctionNodes,
listFunctionNodes,
Expand Down
34 changes: 34 additions & 0 deletions src/db/repository/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,37 @@ export function findNodeByQualifiedName(db, qualifiedName, opts = {}) {
'SELECT * FROM nodes WHERE qualified_name = ? ORDER BY file, line',
).all(qualifiedName);
}

// ─── Metric helpers ──────────────────────────────────────────────────────

const _getLineCountForNodeStmt = new WeakMap();

/**
* Get line_count from node_metrics for a given node.
* @param {object} db
* @param {number} nodeId
* @returns {{ line_count: number } | undefined}
*/
export function getLineCountForNode(db, nodeId) {
return cachedStmt(
_getLineCountForNodeStmt,
db,
'SELECT line_count FROM node_metrics WHERE node_id = ?',
).get(nodeId);
}

const _getMaxEndLineForFileStmt = new WeakMap();

/**
* Get the maximum end_line across all nodes in a file.
* @param {object} db
* @param {string} file
* @returns {{ max_end: number | null } | undefined}
*/
export function getMaxEndLineForFile(db, file) {
return cachedStmt(
_getMaxEndLineForFileStmt,
db,
'SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?',
).get(file);
}
177 changes: 177 additions & 0 deletions src/domain/analysis/brief.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type BetterSqlite3 from 'better-sqlite3';
import {
findDistinctCallers,
findFileNodes,
findImportDependents,
findImportSources,
findImportTargets,
findNodesByFile,
openReadonlyOrFail,
} from '../../db/index.js';
import { loadConfig } from '../../infrastructure/config.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import type { ImportEdgeRow, NodeRow, RelatedNodeRow } from '../../types.js';

/** Symbol kinds meaningful for a file brief — excludes parameters, properties, constants. */
const BRIEF_KINDS = new Set([
'function',
'method',
'class',
'interface',
'type',
'struct',
'enum',
'trait',
'record',
'module',
]);

/**
* Compute file risk tier from symbol roles and max fan-in.
*/
function computeRiskTier(
symbols: Array<{ role: string | null; callerCount: number }>,
highThreshold = 10,
mediumThreshold = 3,
): 'high' | 'medium' | 'low' {
let maxCallers = 0;
let hasCoreRole = false;
for (const s of symbols) {
if (s.callerCount > maxCallers) maxCallers = s.callerCount;
if (s.role === 'core') hasCoreRole = true;
}
if (maxCallers >= highThreshold || hasCoreRole) return 'high';
if (maxCallers >= mediumThreshold) return 'medium';
return 'low';
}

/**
* BFS to count transitive callers for a single node.
* Lightweight variant — only counts, does not collect details.
*/
function countTransitiveCallers(
db: BetterSqlite3.Database,
startId: number,
noTests: boolean,
maxDepth = 5,
): number {
const visited = new Set([startId]);
let frontier = [startId];

for (let d = 1; d <= maxDepth; d++) {
const nextFrontier: number[] = [];
for (const fid of frontier) {
const callers = findDistinctCallers(db, fid) as RelatedNodeRow[];
for (const c of callers) {
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
visited.add(c.id);
nextFrontier.push(c.id);
}
}
}
frontier = nextFrontier;
if (frontier.length === 0) break;
}

return visited.size - 1;
}

/**
* Count transitive file-level import dependents via BFS.
* Depth-bounded to match countTransitiveCallers and keep hook latency predictable.
*/
function countTransitiveImporters(
db: BetterSqlite3.Database,
fileNodeIds: number[],
noTests: boolean,
maxDepth = 5,
): number {
const visited = new Set(fileNodeIds);
let frontier = [...fileNodeIds];

for (let d = 1; d <= maxDepth; d++) {
const nextFrontier: number[] = [];
for (const current of frontier) {
const dependents = findImportDependents(db, current) as RelatedNodeRow[];
for (const dep of dependents) {
if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
visited.add(dep.id);
nextFrontier.push(dep.id);
}
}
}
frontier = nextFrontier;
if (frontier.length === 0) break;
}

return visited.size - fileNodeIds.length;
}

/**
* Produce a token-efficient file brief: symbols with roles and caller counts,
* importer info with transitive count, and file risk tier.
*/
export function briefData(
file: string,
customDbPath: string,
// biome-ignore lint/suspicious/noExplicitAny: config shape is dynamic
opts: { noTests?: boolean; config?: any } = {},
) {
const db = openReadonlyOrFail(customDbPath);
try {
const noTests = opts.noTests || false;
const config = opts.config || loadConfig();
const callerDepth = config.analysis?.briefCallerDepth ?? 5;
const importerDepth = config.analysis?.briefImporterDepth ?? 5;
const highRiskCallers = config.analysis?.briefHighRiskCallers ?? 10;
const mediumRiskCallers = config.analysis?.briefMediumRiskCallers ?? 3;
const fileNodes = findFileNodes(db, `%${file}%`) as NodeRow[];
if (fileNodes.length === 0) {
return { file, results: [] };
}

const results = fileNodes.map((fn) => {
// Direct importers
let importedBy = findImportSources(db, fn.id) as ImportEdgeRow[];
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
const directImporters = [...new Set(importedBy.map((i) => i.file))];

// Transitive importer count
const totalImporterCount = countTransitiveImporters(db, [fn.id], noTests, importerDepth);

// Direct imports
let importsTo = findImportTargets(db, fn.id) as ImportEdgeRow[];
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));

// Symbol definitions with roles and caller counts
const defs = (findNodesByFile(db, fn.file) as NodeRow[]).filter((d) =>
BRIEF_KINDS.has(d.kind),
);
const symbols = defs.map((d) => {
const callerCount = countTransitiveCallers(db, d.id, noTests, callerDepth);
return {
name: d.name,
kind: d.kind,
line: d.line,
role: d.role || null,
callerCount,
};
});

const riskTier = computeRiskTier(symbols, highRiskCallers, mediumRiskCallers);

return {
file: fn.file,
risk: riskTier,
imports: importsTo.map((i) => i.file),
importedBy: directImporters,
totalImporterCount,
symbols,
};
});

return { file, results };
} finally {
db.close();
}
}
Loading
Loading