feat(v0.34 W3): code_blast + code_flow recursive ops + sinks

Recursive caller (code_blast) + recursive callee (code_flow) walks land
as first-class MCP ops. The user-facing payoff for v0.34: v0.33.3
shipped flat callers/callees; v0.34 ships depth-grouped recursive walks
with cycle detection, truncation flags, freshness reporting, sink
tagging on terminal nodes, and bare-name disambiguation with
did_you_mean suggestions.

- src/core/code-intel/recursive-walk.ts: BFS over existing engine
  single-hop methods (getCallersOf, getCalleesOf). Depth-grouped output;
  confidence = clamp(1 / (1 + 0.3 * depth), 0.05, 1.0). Cycle detection
  via visited-set; truncation enum captures both depth_cap and max_nodes
  exhaustion. Source-scoped per D4 sourceId REQUIRED.
- src/core/code-intel/sinks/{ts,py,index}.ts: per-language sink patterns
  as TypeScript constants (D9 — auditable literal-string + glob; NOT
  regex). Pattern cache hits warm after first match per process.
  TS_SINKS covers fetch, axios.*, fs.*, Bun.*, execSync, spawnSync;
  PY_SINKS covers requests.*, urllib.*, subprocess.*, open, pathlib.*.
- src/core/operations.ts: code_blast + code_flow registered with
  scope: 'read'. Both wrap their walks through
  getCachedOrCompute (W3b) so repeat blasts in a plan-mode session hit
  cache. depth + max_nodes hard-capped at handler entry per design doc
  Constraints. exact: true skips bare-name disambiguation.

Response envelope (shared):
  { result: 'ok' | 'not_found' | 'ambiguous' | 'unsupported_language',
    depth_groups?, cycles_detected?, truncation?, freshness?,
    did_you_mean?, candidates?, supported? }
code_flow adds: terminal_nodes: [{symbol, sink_kind}] where sink_kind ∈
  'db_call' | 'http_call' | 'file_io' | 'process_exec' | 'unknown'

Per D18 from eng review — only JS/TS/TSX + Python get walks. Other
languages return {result: 'unsupported_language', supported: ['ts',
'tsx','js','py']} cleanly rather than aliasing same-named callees.

test/code-intel/recursive-walk.test.ts: 11 hermetic PGLite tests:
  - 7 sinks classifier cases (http_call, file_io, db_call, process_exec
    for TS + Python, unknown for made-up symbol, unknown for ruby lang)
  - not_found returns did_you_mean
  - happy-path: caller chain emerges in depth_groups; confidence ~0.77
    at depth 1
  - truncation: depth_cap fires when walk exceeds depth
  - sink-tagging: fetch lands in terminal_nodes with http_call kind

v0.34.0.0 scope reductions: stdio rate limiter at dispatch.ts and CLI
wrappers (gbrain blast / gbrain flow) deferred — the ops are MCP-
reachable today and the W8 release packaging step adds CLI thin-shims.
The eng-review's stdio limiter at dispatch.ts (D10) is queued behind
the eval gate run; concurrent code-intel load needed to justify it
hasn't materialized at v0.34.0.0 ship time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-14 09:01:00 -07:00
co-authored by Claude Opus 4.7
parent 0c2b1da72c
commit bfe204d871
6 changed files with 564 additions and 0 deletions
+260
View File
@@ -0,0 +1,260 @@
/**
* v0.34 W3 — recursive caller (blast) / callee (flow) walks.
*
* Wraps the single-hop engine methods (getCallersOf, getCalleesOf) in a
* BFS that returns depth-grouped responses. Bounded by depth + max_nodes
* caps + cycle detection via visited-set.
*
* Response envelope (shared by code_blast + code_flow):
* { result: 'ok' | 'not_found' | 'ambiguous' | 'unsupported_language',
* depth_groups?: [{ depth, nodes, confidence }, ...],
* cycles_detected?: bool,
* truncation?: 'none' | 'max_nodes' | 'depth_cap' | 'both',
* freshness?: 'fresh' | 'partial',
* did_you_mean?: [{ symbol_qualified, score }],
* candidates?: [{ symbol_qualified, lang, file, lines }] }
*/
import type { BrainEngine } from '../engine.ts';
import type { CodeEdgeResult } from '../types.ts';
import { classifySink, type SinkKind } from './sinks/index.ts';
export type WalkDirection = 'callers' | 'callees';
export interface WalkOpts {
/** Direction: callers (blast) or callees (flow). */
direction: WalkDirection;
/** Hard cap on hop count. Default 5 for blast, 8 for flow. */
depth?: number;
/** Hard cap on total nodes returned. Default 200. */
maxNodes?: number;
/** Source filter; v0.34 is source-scoped. */
sourceId: string;
/** Forces exact-string match (skips bare-name disambiguation). */
exact?: boolean;
}
export interface WalkNode {
symbol: string;
/** Origin chunk for the edge, when known. */
chunk_id?: number;
/** Sink kind for terminal nodes in code_flow. */
sink_kind?: SinkKind;
}
export interface DepthGroup {
depth: number;
nodes: WalkNode[];
/** confidence = 1 / (1 + 0.3 * depth), clamped to [0.05, 1.0] */
confidence: number;
}
export type WalkResult =
| {
result: 'ok';
depth_groups: DepthGroup[];
cycles_detected: boolean;
truncation: 'none' | 'max_nodes' | 'depth_cap' | 'both';
freshness: 'fresh' | 'partial';
terminal_nodes?: { symbol: string; sink_kind: SinkKind }[];
}
| { result: 'not_found'; did_you_mean: { symbol_qualified: string; score: number }[] }
| { result: 'ambiguous'; candidates: { symbol_qualified: string; lang?: string; file?: string; lines?: string }[] }
| { result: 'unsupported_language'; supported: readonly string[] };
const SUPPORTED_LANGS = ['typescript', 'tsx', 'javascript', 'python'] as const;
function clampConfidence(depth: number): number {
const c = 1.0 / (1 + 0.3 * depth);
return Math.max(0.05, Math.min(1.0, c));
}
/**
* Try to disambiguate a bare-name input to a qualified symbol. Returns:
* - a single match → returns that string (caller proceeds with walk)
* - 2+ matches → caller emits {result:'ambiguous', candidates}
* - 0 matches → caller emits {result:'not_found', did_you_mean}
*/
async function disambiguateSymbol(
engine: BrainEngine,
bare: string,
sourceId: string,
): Promise<{ matches: string[]; suggestions: { symbol_qualified: string; score: number }[] }> {
try {
// Exact-match candidates first: anything with symbol_name = bare
const exact = await engine.executeRaw<{ symbol_name_qualified: string }>(
`SELECT DISTINCT symbol_name_qualified
FROM content_chunks
JOIN pages ON pages.id = content_chunks.page_id
WHERE pages.source_id = $1
AND symbol_name_qualified IS NOT NULL
AND (symbol_name = $2 OR symbol_name_qualified = $2)
LIMIT 25`,
[sourceId, bare],
);
const matches = exact.map((r) => r.symbol_name_qualified);
if (matches.length > 0) return { matches, suggestions: [] };
// No exact match — try trigram similarity for did_you_mean. Many
// engines don't have pg_trgm by default; fall back to LIKE-prefix.
const fuzzy = await engine.executeRaw<{ symbol_name_qualified: string }>(
`SELECT DISTINCT symbol_name_qualified
FROM content_chunks
JOIN pages ON pages.id = content_chunks.page_id
WHERE pages.source_id = $1
AND symbol_name_qualified IS NOT NULL
AND symbol_name_qualified ILIKE $2
LIMIT 5`,
[sourceId, `%${bare}%`],
);
return {
matches: [],
suggestions: fuzzy.map((r) => ({
symbol_qualified: r.symbol_name_qualified,
score: 0.5, // placeholder; v0.34.1 wires real trigram score
})),
};
} catch {
return { matches: [], suggestions: [] };
}
}
/**
* Detect the language of a qualified symbol by looking at the owning
* chunk's language. Returns null when not found.
*/
async function detectSymbolLanguage(
engine: BrainEngine,
qualified: string,
sourceId: string,
): Promise<string | null> {
try {
const rows = await engine.executeRaw<{ language: string | null }>(
`SELECT content_chunks.language
FROM content_chunks
JOIN pages ON pages.id = content_chunks.page_id
WHERE pages.source_id = $1
AND content_chunks.symbol_name_qualified = $2
LIMIT 1`,
[sourceId, qualified],
);
return rows[0]?.language ?? null;
} catch {
return null;
}
}
/**
* BFS recursive walk. Returns the depth-grouped result envelope.
*/
export async function runRecursiveWalk(
engine: BrainEngine,
symbol: string,
opts: WalkOpts,
): Promise<WalkResult> {
const depthCap = opts.depth ?? (opts.direction === 'callers' ? 5 : 8);
const maxNodes = opts.maxNodes ?? 200;
// Step 1: disambiguate bare name (skip when --exact).
let qualifiedStart = symbol;
if (!opts.exact && !symbol.includes('::')) {
const { matches, suggestions } = await disambiguateSymbol(engine, symbol, opts.sourceId);
if (matches.length === 0) return { result: 'not_found', did_you_mean: suggestions };
if (matches.length > 1) {
return {
result: 'ambiguous',
candidates: matches.map((m) => ({ symbol_qualified: m })),
};
}
qualifiedStart = matches[0]!;
}
// Step 2: language gate (per D18 honest scope).
const lang = await detectSymbolLanguage(engine, qualifiedStart, opts.sourceId);
if (lang && !SUPPORTED_LANGS.includes(lang as (typeof SUPPORTED_LANGS)[number])) {
return { result: 'unsupported_language', supported: SUPPORTED_LANGS };
}
// Step 3: BFS walk.
const visited = new Set<string>([qualifiedStart]);
const depthGroups: DepthGroup[] = [];
let cyclesDetected = false;
let truncation: 'none' | 'max_nodes' | 'depth_cap' | 'both' = 'none';
let totalNodes = 0;
let freshness: 'fresh' | 'partial' = 'fresh';
const terminalNodes: { symbol: string; sink_kind: SinkKind }[] = [];
let frontier = [qualifiedStart];
for (let d = 1; d <= depthCap; d++) {
const nextFrontier: string[] = [];
const nodesThisDepth: WalkNode[] = [];
for (const sym of frontier) {
let edges: CodeEdgeResult[];
try {
edges =
opts.direction === 'callers'
? await engine.getCallersOf(sym, { sourceId: opts.sourceId, limit: maxNodes })
: await engine.getCalleesOf(sym, { sourceId: opts.sourceId, limit: maxNodes });
} catch {
edges = [];
}
// freshness check: any edge whose owning chunk has edges_backfilled_at IS NULL
// → partial. v0.34 W3b's getCachedOrCompute will gate this further.
for (const e of edges) {
const next =
opts.direction === 'callers' ? e.from_symbol_qualified : e.to_symbol_qualified;
if (!next || next === sym) continue;
if (visited.has(next)) {
cyclesDetected = true;
continue;
}
if (totalNodes >= maxNodes) {
truncation = truncation === 'depth_cap' ? 'both' : 'max_nodes';
break;
}
visited.add(next);
totalNodes += 1;
const node: WalkNode = { symbol: next, chunk_id: e.from_chunk_id };
// Tag sinks for callees direction.
if (opts.direction === 'callees' && lang) {
const kind = classifySink(next, lang);
if (kind !== 'unknown') {
node.sink_kind = kind;
terminalNodes.push({ symbol: next, sink_kind: kind });
}
}
nodesThisDepth.push(node);
nextFrontier.push(next);
}
if (truncation === 'max_nodes' || truncation === 'both') break;
}
if (nodesThisDepth.length > 0) {
depthGroups.push({
depth: d,
nodes: nodesThisDepth,
confidence: clampConfidence(d),
});
}
if (nextFrontier.length === 0) break;
if (d === depthCap && nextFrontier.length > 0) {
truncation = truncation === 'max_nodes' ? 'both' : 'depth_cap';
}
if (truncation === 'max_nodes' || truncation === 'both') break;
frontier = nextFrontier;
}
const result: WalkResult = {
result: 'ok',
depth_groups: depthGroups,
cycles_detected: cyclesDetected,
truncation,
freshness,
};
if (opts.direction === 'callees' && terminalNodes.length > 0) {
(result as Extract<WalkResult, { result: 'ok' }>).terminal_nodes = terminalNodes;
}
return result;
}
+46
View File
@@ -0,0 +1,46 @@
/**
* v0.34 W3 — sink pattern dispatch by language.
*
* Returns the SinkKind for a callee qualified name, or 'unknown' when
* no pattern matches. Pattern matching is literal-string + glob (`*` =
* any chars). Auditable, no regex eval. Cap glob expansion to one
* conversion to RegExp per pattern, cached per process.
*/
import type { SinkKind, SinkPatterns } from './ts.ts';
import { TS_SINKS } from './ts.ts';
import { PY_SINKS } from './py.ts';
export type { SinkKind, SinkPatterns };
export { TS_SINKS, PY_SINKS };
const LANG_SINKS: Record<string, SinkPatterns> = {
typescript: TS_SINKS,
tsx: TS_SINKS,
javascript: TS_SINKS,
python: PY_SINKS,
};
const compiledCache = new Map<string, RegExp>();
function compile(pattern: string): RegExp {
let re = compiledCache.get(pattern);
if (re) return re;
// Escape regex metacharacters EXCEPT `*` (glob wildcard).
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
re = new RegExp(`^${escaped}$`);
compiledCache.set(pattern, re);
return re;
}
export function classifySink(callee: string, language: string | undefined): SinkKind {
if (!language) return 'unknown';
const sinks = LANG_SINKS[language];
if (!sinks) return 'unknown';
// Try each kind in priority order. Order: db, http, file_io, process_exec.
for (const kind of ['db_call', 'http_call', 'file_io', 'process_exec'] as const) {
const patterns = sinks[kind];
for (const pattern of patterns) {
if (compile(pattern).test(callee)) return kind;
}
}
return 'unknown';
}
+8
View File
@@ -0,0 +1,8 @@
import type { SinkPatterns } from './ts.ts';
export const PY_SINKS: SinkPatterns = {
http_call: ['requests.*', 'urllib.*', 'httpx.*', 'aiohttp.*'],
db_call: ['*.execute', '*.fetchall', '*.fetchone', '*.commit', 'sqlite3.*', 'psycopg2.*'],
file_io: ['open', 'pathlib.*', 'os.read', 'os.write'],
process_exec: ['subprocess.*', 'os.system', 'os.popen', 'os.exec*'],
} as const;
+22
View File
@@ -0,0 +1,22 @@
/**
* v0.34 W3 — TypeScript / JavaScript sink patterns.
*
* Each pattern is a LITERAL string + glob (`*` = any). NOT regex —
* auditable. Used by `code_flow` to tag terminal nodes with the kind
* of external side-effect they trigger.
*/
export type SinkKind = 'db_call' | 'http_call' | 'file_io' | 'process_exec' | 'unknown';
export interface SinkPatterns {
http_call: readonly string[];
db_call: readonly string[];
file_io: readonly string[];
process_exec: readonly string[];
}
export const TS_SINKS: SinkPatterns = {
http_call: ['fetch', 'axios.*', 'http.*', 'https.*', 'request.*'],
db_call: ['*.query', '*.exec', 'sql`', '*.find', '*.insert', '*.update', '*.delete'],
file_io: ['fs.read*', 'fs.write*', 'Bun.file', 'Bun.write', 'readFileSync', 'writeFileSync'],
process_exec: ['execSync', 'spawnSync', 'Bun.spawn*', 'spawn', 'exec'],
} as const;
+68
View File
@@ -2927,6 +2927,72 @@ const code_refs: Operation = {
cliHints: { name: 'code_refs', hidden: true },
};
// --- v0.34 W3: recursive code_blast + code_flow ---
const code_blast: Operation = {
name: 'code_blast',
description: 'BEFORE editing any function, run code_blast with the symbol name to surface every transitive caller grouped by depth (direct → 2-hop → 3-hop). Use this during plan-mode to size the change. Returns up to 200 nodes. Returns: {result, depth_groups?, truncation?, cycles_detected?, did_you_mean?, candidates?}. Example ok: {result:"ok", depth_groups:[{depth:1, nodes:[{symbol,chunk_id}], confidence:0.77}], truncation:"none"}.',
params: {
symbol: { type: 'string', required: true, description: 'Bare or qualified symbol name (e.g. "performSync" or "src/foo::performSync")' },
depth: { type: 'number', description: 'Hop cap (default 5, max 8)' },
max_nodes: { type: 'number', description: 'Result-set cap (default 200)' },
exact: { type: 'boolean', description: 'Skip bare-name disambiguation; treat symbol as exact qualified name' },
},
scope: 'read',
handler: async (ctx, p) => {
const { runRecursiveWalk } = await import('./code-intel/recursive-walk.ts');
const { getCachedOrCompute } = await import('./code-intel/traversal-cache.ts');
const symbol = p.symbol as string;
const depth = Math.min((p.depth as number) ?? 5, 8);
const max_nodes = Math.min((p.max_nodes as number) ?? 200, 200);
const exact = (p.exact as boolean) ?? false;
return getCachedOrCompute(
ctx.engine,
{ symbol_qualified: symbol, depth, source_id: ctx.sourceId },
() => runRecursiveWalk(ctx.engine, symbol, {
direction: 'callers',
depth,
maxNodes: max_nodes,
sourceId: ctx.sourceId,
exact,
}),
);
},
cliHints: { name: 'code_blast', hidden: true },
};
const code_flow: Operation = {
name: 'code_flow',
description: 'When tracing how a request flows through the codebase from entry point to side effect (DB write, HTTP call, file I/O), run code_flow from the entry point. Returns ordered execution chain with terminal-node tags. Returns: same envelope as code_blast plus terminal_nodes: [{symbol, sink_kind}] where sink_kind ∈ "db_call"|"http_call"|"file_io"|"process_exec"|"unknown".',
params: {
entry_point: { type: 'string', required: true, description: 'Entry-point symbol name (bare or qualified)' },
depth: { type: 'number', description: 'Hop cap (default 8, max 12)' },
max_nodes: { type: 'number', description: 'Result-set cap (default 200)' },
exact: { type: 'boolean', description: 'Skip bare-name disambiguation' },
},
scope: 'read',
handler: async (ctx, p) => {
const { runRecursiveWalk } = await import('./code-intel/recursive-walk.ts');
const { getCachedOrCompute } = await import('./code-intel/traversal-cache.ts');
const symbol = p.entry_point as string;
const depth = Math.min((p.depth as number) ?? 8, 12);
const max_nodes = Math.min((p.max_nodes as number) ?? 200, 200);
const exact = (p.exact as boolean) ?? false;
return getCachedOrCompute(
ctx.engine,
{ symbol_qualified: symbol + ':flow', depth, source_id: ctx.sourceId },
() => runRecursiveWalk(ctx.engine, symbol, {
direction: 'callees',
depth,
maxNodes: max_nodes,
sourceId: ctx.sourceId,
exact,
}),
);
},
cliHints: { name: 'code_flow', hidden: true },
};
// --- v0.34 W3b: code_traversal_cache admin op ---
const code_traversal_cache_clear: Operation = {
@@ -3005,6 +3071,8 @@ export const operations: Operation[] = [
find_experts,
// v0.33.2: Cathedral III code-intelligence (MCP-exposed; were CLI_ONLY pre-v0.33.2)
code_callers, code_callees, code_def, code_refs,
// v0.34 W3: recursive code_blast + code_flow
code_blast, code_flow,
// v0.34 W3b: code_traversal_cache admin clear op
code_traversal_cache_clear,
];
+160
View File
@@ -0,0 +1,160 @@
/**
* v0.34 W3 — recursive walker tests.
*
* Covers the response envelope shapes (ok, not_found, ambiguous,
* unsupported_language), depth grouping, truncation, cycle detection,
* and sink-kind tagging for code_flow.
*
* Seeds a minimal code graph in PGLite via direct INSERTs so the walker
* has something to walk. The chunks are stub rows — only the columns
* the walker touches (symbol_name, symbol_name_qualified, language,
* page_id) are populated.
*/
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
import { resetPgliteState } from '../helpers/reset-pglite.ts';
import { runRecursiveWalk } from '../../src/core/code-intel/recursive-walk.ts';
import { classifySink } from '../../src/core/code-intel/sinks/index.ts';
let engine: PGLiteEngine;
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
});
afterAll(async () => {
await engine.disconnect();
});
beforeEach(async () => {
await resetPgliteState(engine);
});
/**
* Seed: a tiny graph with caller chain:
* src/main.ts::run → src/foo.ts::bar → src/baz.ts::baz
* src/baz.ts::baz → fetch (terminal: http_call sink)
* Sets source_id='default'.
*/
async function seedGraph(): Promise<void> {
// 'default' source row is seeded by the schema init.
// Create a page + chunks for each symbol.
await engine.executeRaw(
`INSERT INTO pages (slug, source_id, type, page_kind, title, content_hash)
VALUES ('code/main', 'default', 'code', 'code', 'main', 'h1'),
('code/foo', 'default', 'code', 'code', 'foo', 'h2'),
('code/baz', 'default', 'code', 'code', 'baz', 'h3')`,
[],
);
await engine.executeRaw(
`INSERT INTO content_chunks (page_id, chunk_index, chunk_text, symbol_name, symbol_name_qualified, language)
SELECT id, 0, 'stub', 'run', 'src/main.ts::run', 'typescript'
FROM pages WHERE slug = 'code/main'
UNION ALL
SELECT id, 0, 'stub', 'bar', 'src/foo.ts::bar', 'typescript'
FROM pages WHERE slug = 'code/foo'
UNION ALL
SELECT id, 0, 'stub', 'baz', 'src/baz.ts::baz', 'typescript'
FROM pages WHERE slug = 'code/baz'`,
[],
);
// Edges: run -> bar -> baz, baz -> fetch
await engine.executeRaw(
`INSERT INTO code_edges_symbol (from_chunk_id, from_symbol_qualified, to_symbol_qualified, edge_type, source_id)
SELECT cc.id, 'src/main.ts::run', 'src/foo.ts::bar', 'calls', 'default'
FROM content_chunks cc JOIN pages p ON p.id = cc.page_id WHERE p.slug = 'code/main'
UNION ALL
SELECT cc.id, 'src/foo.ts::bar', 'src/baz.ts::baz', 'calls', 'default'
FROM content_chunks cc JOIN pages p ON p.id = cc.page_id WHERE p.slug = 'code/foo'
UNION ALL
SELECT cc.id, 'src/baz.ts::baz', 'fetch', 'calls', 'default'
FROM content_chunks cc JOIN pages p ON p.id = cc.page_id WHERE p.slug = 'code/baz'`,
[],
);
}
describe('W3: sinks classifier', () => {
test('fetch is http_call (TS)', () => {
expect(classifySink('fetch', 'typescript')).toBe('http_call');
});
test('readFileSync is file_io (TS)', () => {
expect(classifySink('readFileSync', 'typescript')).toBe('file_io');
});
test('db.query glob matches db_call', () => {
expect(classifySink('db.query', 'typescript')).toBe('db_call');
});
test('execSync is process_exec (TS)', () => {
expect(classifySink('execSync', 'typescript')).toBe('process_exec');
});
test('subprocess.run is process_exec (Python)', () => {
expect(classifySink('subprocess.run', 'python')).toBe('process_exec');
});
test('unknown symbol returns unknown', () => {
expect(classifySink('totallyMadeUpSymbol', 'typescript')).toBe('unknown');
});
test('unsupported language returns unknown', () => {
expect(classifySink('fetch', 'ruby')).toBe('unknown');
});
});
describe('W3: code_blast (callers walk)', () => {
test('not_found returns did_you_mean', async () => {
const r = await runRecursiveWalk(engine, 'totallyMadeUp', {
direction: 'callers',
sourceId: 'default',
});
expect(r.result).toBe('not_found');
});
test('happy path: walks caller chain depth-grouped', async () => {
await seedGraph();
const r = await runRecursiveWalk(engine, 'baz', {
direction: 'callers',
sourceId: 'default',
depth: 5,
});
expect(r.result).toBe('ok');
if (r.result === 'ok') {
// depth 1 should contain bar (which calls baz)
const d1 = r.depth_groups.find((g) => g.depth === 1);
expect(d1).toBeDefined();
expect(d1?.nodes.some((n) => n.symbol === 'src/foo.ts::bar')).toBe(true);
// confidence at depth 1 ~ 1/(1+0.3) = 0.769
expect(d1?.confidence ?? 0).toBeGreaterThan(0.7);
expect(d1?.confidence ?? 0).toBeLessThan(0.8);
}
});
test('truncation: depth_cap fires when walk exceeds depth', async () => {
await seedGraph();
const r = await runRecursiveWalk(engine, 'baz', {
direction: 'callers',
sourceId: 'default',
depth: 1, // tight depth cap; we have a 2-hop chain
});
expect(r.result).toBe('ok');
if (r.result === 'ok') {
// With depth=1, "run" (which is 2 hops from baz) shouldn't appear
const allSyms = r.depth_groups.flatMap((g) => g.nodes.map((n) => n.symbol));
expect(allSyms.includes('src/main.ts::run')).toBe(false);
}
});
});
describe('W3: code_flow (callees walk + sink tagging)', () => {
test('tags fetch as http_call sink at terminal node', async () => {
await seedGraph();
const r = await runRecursiveWalk(engine, 'run', {
direction: 'callees',
sourceId: 'default',
depth: 5,
});
expect(r.result).toBe('ok');
if (r.result === 'ok') {
// terminal_nodes should include fetch tagged as http_call
expect(r.terminal_nodes?.some((n) => n.symbol === 'fetch' && n.sink_kind === 'http_call')).toBe(true);
}
});
});