mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 14:59:47 +00:00
Edge extractor now emits three edge kinds:
- calls (v0.20 baseline; v0.34 W1 added qualified-name receiver
resolution for JS/TS/TSX + Python)
- imports (NEW in v0.34 W2; JS/TS/TSX + Python at depth)
- references (NEW in v0.34 W2; TS-only)
Why this matters: Leiden clusters on a calls-only graph produce overfit
garbage (GitNexus showed 0.052 cluster/node on calls-only — useless).
Adding imports + references densifies the graph so W4-5's clusters can
land meaningful communities. Per design doc Constraint #1.
- src/core/chunkers/edge-extractor.ts: new extractImportEdges and
extractReferenceEdges functions + combined extractAllEdges wrapper.
ExtractedEdge.edgeType widened to 'calls' | 'imports' | 'references'.
- src/core/chunkers/code.ts: switched the chunker's edge-extraction call
site from extractCallEdges to extractAllEdges so imports + references
flow into code_edges_symbol alongside calls.
- src/core/chunkers/symbol-resolver.ts: EDGE_EXTRACTOR_VERSION_TS bumped
to 2026-05-14T01:00:00Z so the next dream cycle re-walks every chunk.
Language scope per D18 from eng review:
- JS/TS/TSX: imports + references emitted
- Python: imports emitted, references skipped (Python type hints too
sparse for v0.34; v0.35 may revisit)
- Ruby/Go/Rust/Java: calls only — no imports, no references. Honest
coverage matrix; code_blast/code_flow return 'unsupported_language'
response for these langs (W2 commit 4 wires this).
Edge schema reused: code_edges_symbol.edge_type is the existing TEXT
column populated by the unique constraint
(from_chunk_id, to_symbol_qualified, edge_type). Adding new types
doesn't conflict with existing calls edges.
test/code-intel/edge-densification.test.ts: 13 hermetic tests covering
named/default/namespace/aliased/side-effect imports for JS/TS, from-x-
import-y + import-pkg for Python, function parameter + return type
references for TS, and unsupported-language returns-empty contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
6.0 KiB
TypeScript
162 lines
6.0 KiB
TypeScript
/**
|
|
* v0.34 W2 — edge densification: imports + references edges.
|
|
*
|
|
* Tests that `extractImportEdges`, `extractReferenceEdges`, and the
|
|
* combined `extractAllEdges` emit the right edge types for each language
|
|
* shipped at depth (JS/TS/TSX + Python imports; TS only for references).
|
|
*/
|
|
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
import {
|
|
extractAllEdges,
|
|
extractImportEdges,
|
|
extractReferenceEdges,
|
|
} from '../../src/core/chunkers/edge-extractor.ts';
|
|
|
|
let Parser: any;
|
|
let TSLang: any;
|
|
let PYLang: any;
|
|
|
|
beforeAll(async () => {
|
|
try {
|
|
const ts = await import('web-tree-sitter');
|
|
Parser = (ts as any).Parser ?? (ts as any).default;
|
|
await Parser.init();
|
|
const tsWasmPath = require.resolve('tree-sitter-wasms/out/tree-sitter-typescript.wasm');
|
|
const pyWasmPath = require.resolve('tree-sitter-wasms/out/tree-sitter-python.wasm');
|
|
TSLang = await Parser.Language.load(tsWasmPath);
|
|
PYLang = await Parser.Language.load(pyWasmPath);
|
|
} catch {
|
|
Parser = null;
|
|
}
|
|
});
|
|
|
|
function parseTS(source: string): any {
|
|
if (!Parser || !TSLang) return null;
|
|
const p = new Parser();
|
|
p.setLanguage(TSLang);
|
|
return p.parse(source);
|
|
}
|
|
|
|
function parsePY(source: string): any {
|
|
if (!Parser || !PYLang) return null;
|
|
const p = new Parser();
|
|
p.setLanguage(PYLang);
|
|
return p.parse(source);
|
|
}
|
|
|
|
describe('W2: imports edges — JS/TS', () => {
|
|
test('named imports emit one edge per symbol', () => {
|
|
const tree = parseTS(`import { useState, useEffect } from 'react';`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.edgeType === 'imports' && e.toSymbol === 'react::useState')).toBe(true);
|
|
expect(edges.some((e) => e.edgeType === 'imports' && e.toSymbol === 'react::useEffect')).toBe(true);
|
|
});
|
|
|
|
test('default import emits module::default', () => {
|
|
const tree = parseTS(`import React from 'react';`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.toSymbol === 'react::default')).toBe(true);
|
|
});
|
|
|
|
test('namespace import emits module::*', () => {
|
|
const tree = parseTS(`import * as React from 'react';`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.toSymbol === 'react::*')).toBe(true);
|
|
});
|
|
|
|
test('aliased named imports use the source name not the alias', () => {
|
|
const tree = parseTS(`import { useState as us } from 'react';`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.toSymbol === 'react::useState')).toBe(true);
|
|
});
|
|
|
|
test('side-effect import emits module::*', () => {
|
|
const tree = parseTS(`import 'reflect-metadata';`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.toSymbol === 'reflect-metadata::*')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('W2: imports edges — Python', () => {
|
|
test('from x import y, z emits one edge per symbol', () => {
|
|
const tree = parsePY(`from os import path, getcwd`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'python');
|
|
expect(edges.some((e) => e.toSymbol === 'os::path')).toBe(true);
|
|
expect(edges.some((e) => e.toSymbol === 'os::getcwd')).toBe(true);
|
|
});
|
|
|
|
test('import pkg emits pkg::*', () => {
|
|
const tree = parsePY(`import json`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'python');
|
|
expect(edges.some((e) => e.toSymbol === 'json::*')).toBe(true);
|
|
});
|
|
|
|
test('aliased import uses source not alias', () => {
|
|
const tree = parsePY(`from os import path as p`);
|
|
if (!tree) return;
|
|
const edges = extractImportEdges(tree, 'python');
|
|
expect(edges.some((e) => e.toSymbol === 'os::path')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('W2: references edges — TS only', () => {
|
|
test('function parameter type references emit edges', () => {
|
|
const tree = parseTS(`function foo(x: MyType): void {}`);
|
|
if (!tree) return;
|
|
const edges = extractReferenceEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.edgeType === 'references' && e.toSymbol === 'MyType')).toBe(true);
|
|
});
|
|
|
|
test('return type annotation emits edge', () => {
|
|
const tree = parseTS(`function foo(): MyReturnType { return null as any; }`);
|
|
if (!tree) return;
|
|
const edges = extractReferenceEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.toSymbol === 'MyReturnType')).toBe(true);
|
|
});
|
|
|
|
test('python returns empty (references is TS-only for v0.34)', () => {
|
|
const tree = parsePY(`def foo(x: int) -> int: return x`);
|
|
if (!tree) return;
|
|
const edges = extractReferenceEdges(tree, 'python');
|
|
expect(edges).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('W2: combined extractAllEdges', () => {
|
|
test('returns calls + imports + references union', () => {
|
|
const tree = parseTS(`
|
|
import { useState } from 'react';
|
|
function foo(x: MyType) {
|
|
useState();
|
|
}
|
|
`);
|
|
if (!tree) return;
|
|
const edges = extractAllEdges(tree, 'typescript');
|
|
expect(edges.some((e) => e.edgeType === 'imports')).toBe(true);
|
|
expect(edges.some((e) => e.edgeType === 'references')).toBe(true);
|
|
expect(edges.some((e) => e.edgeType === 'calls')).toBe(true);
|
|
});
|
|
|
|
test('Ruby/Go/Rust/Java: calls only (no imports, no references)', () => {
|
|
// Per D18: only JS/TS/TSX + Python emit imports.
|
|
// We can't easily parse Ruby/Go/Rust here without their grammars loaded,
|
|
// but the contract is encoded in the function signature — these
|
|
// languages return [] from extractImportEdges. Confirm via direct call
|
|
// with a null tree shape.
|
|
const nullTree = { rootNode: { namedChildren: [] } };
|
|
expect(extractImportEdges(nullTree, 'ruby')).toEqual([]);
|
|
expect(extractImportEdges(nullTree, 'go')).toEqual([]);
|
|
expect(extractImportEdges(nullTree, 'rust')).toEqual([]);
|
|
expect(extractImportEdges(nullTree, 'java')).toEqual([]);
|
|
expect(extractReferenceEdges(nullTree, 'ruby')).toEqual([]);
|
|
expect(extractReferenceEdges(nullTree, 'python')).toEqual([]);
|
|
});
|
|
});
|