mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-11 08:37:35 +00:00
feat(graph): expose isTestCode on definition nodes
Test-file classification was reachable only from inside the codebase
(`isTestFilePath`). A Cypher consumer — the `cypher` MCP tool, or an agent
reasoning about the graph — had no way to ask "is this test code?" and had to
reimplement the path heuristic in the query itself. That reimplementation is
fragile in the exact way the duplicated predicate was: it drifts from the real
definition, silently, with no test covering it.
Add `isTestCode BOOLEAN` to the five definition-bearing node tables (Function,
Class, Interface, Method, CodeElement — the ones already carrying `isExported`)
and derive it centrally in the CSV writer from each node's `filePath`, so it
cannot be forgotten at an emit site.
Naming: `isTestCode`, not `isTestEntryPoint`. The predicate classifies the FILE,
so it is true for a helper in a test file as well as for the case the framework
invokes. Distinguishing those needs name patterns (`test_*`, `Test*`) and is a
separate question; claiming "entry point" here would overstate what the value
means.
What this buys, measured on a 12.4k-file Python/Go/TypeScript repository — the
whole reachability picture, in Cypher, with no path heuristics:
lang bucket n
go has_caller 1231
go framework_entrypoint 5
go test_code 2965
go UNEXPLAINED 38 (0.9%)
python has_caller 4495
python framework_entrypoint 183
python test_code 6477
python UNEXPLAINED 398 (3.4%)
The residual is what should stay non-zero: genuinely unreferenced code plus
dynamic dispatch static resolution cannot see. That is the dead-code signal, and
driving it to zero would destroy it.
MIGRATION: this changes the graph DDL, so SCHEMA_FINGERPRINT changes and every
existing index is rebuilt once on next analyze. Verified: the fingerprint guard
detects it and forces the re-analyze automatically — no user action, no stale
column.
Also adds `test/unit/node-column-parity.test.ts`. A node column lives in three
places that must agree — the DDL, the CSV header/row builder, and the explicit
COPY column list in `getCopyQuery`. Updating only the first two produced
`COPY failed for Function: Number of columns mismatch. Expected 8 but got 9`
at load time, after a full parse had already run. The new test pins every table's
COPY list to its DDL, in order, and asserts the parse is non-vacuous so a broken
regex cannot make it pass silently. `spring-bean-schema.test.ts` already guarded
this for Class alone; its expectation is updated and the new test generalizes it.
`npx tsc --noEmit` clean; 27 tests across the two schema-parity suites, 417 across
the schema/lbug/csv suites.
This commit is contained in:
parent
d4ab43be96
commit
da488a37a0
5 changed files with 98 additions and 6 deletions
|
|
@ -18,6 +18,7 @@ import path from 'path';
|
|||
import type { GraphNode, GraphRelationship } from 'gitnexus-shared';
|
||||
import { KnowledgeGraph } from '../graph/types.js';
|
||||
import { NodeTableName, RELATION_SCHEMA } from './schema.js';
|
||||
import { isTestFilePath } from '../ingestion/utils/test-file-path.js';
|
||||
import { VALID_NODE_TABLES, parseRelationSchemaPairs, RelPairRouter } from './rel-pair-routing.js';
|
||||
import { parseTruthyEnv } from '../ingestion/utils/env.js';
|
||||
import { SYMBOL_NODE_LABELS } from '../ingestion/utils/symbol-labels.js';
|
||||
|
|
@ -439,7 +440,8 @@ export const streamAllCSVsToDisk = async (
|
|||
'id,name,filePath,content',
|
||||
);
|
||||
const folderWriter = new BufferedCSVWriter(path.join(csvDir, 'folder.csv'), 'id,name,filePath');
|
||||
const codeElementHeader = 'id,name,filePath,startLine,endLine,isExported,content,description';
|
||||
const codeElementHeader =
|
||||
'id,name,filePath,startLine,endLine,isExported,isTestCode,content,description';
|
||||
const functionWriter = new BufferedCSVWriter(
|
||||
path.join(csvDir, 'function.csv'),
|
||||
codeElementHeader,
|
||||
|
|
@ -453,7 +455,7 @@ export const streamAllCSVsToDisk = async (
|
|||
codeElementHeader,
|
||||
);
|
||||
const methodHeader =
|
||||
'id,name,filePath,startLine,endLine,isExported,content,description,parameterCount,returnType';
|
||||
'id,name,filePath,startLine,endLine,isExported,isTestCode,content,description,parameterCount,returnType';
|
||||
const methodWriter = new BufferedCSVWriter(path.join(csvDir, 'method.csv'), methodHeader);
|
||||
const codeElemWriter = new BufferedCSVWriter(
|
||||
path.join(csvDir, 'codeelement.csv'),
|
||||
|
|
@ -615,6 +617,7 @@ export const streamAllCSVsToDisk = async (
|
|||
escapeCSVNumber(node.properties.startLine, -1),
|
||||
escapeCSVNumber(node.properties.endLine, -1),
|
||||
node.properties.isExported ? 'true' : 'false',
|
||||
isTestFilePath(node.properties.filePath) ? 'true' : 'false',
|
||||
escapeCSVField(content),
|
||||
escapeCSVField(formatFtsDescription(node.properties.description || '')),
|
||||
escapeCSVNumber(node.properties.parameterCount, 0),
|
||||
|
|
@ -687,6 +690,7 @@ export const streamAllCSVsToDisk = async (
|
|||
escapeCSVNumber(node.properties.startLine, -1),
|
||||
escapeCSVNumber(node.properties.endLine, -1),
|
||||
node.properties.isExported ? 'true' : 'false',
|
||||
isTestFilePath(node.properties.filePath) ? 'true' : 'false',
|
||||
escapeCSVField(content),
|
||||
escapeCSVField(formatFtsDescription(node.properties.description || '')),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1478,17 +1478,17 @@ export const getCopyQuery = (table: NodeTableName, filePath: string): string =>
|
|||
return `COPY ${t}(id, filePath, startLine, endLine, text, callees, calleeIds) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
if (table === 'Class') {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, frameworkAnnotations) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, isTestCode, content, description, frameworkAnnotations) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
if (table === 'Method') {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, parameterCount, returnType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, isTestCode, content, description, parameterCount, returnType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
if (table === 'Property') {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, content, description, declaredType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
// TypeScript/JS code element tables have isExported; multi-language tables do not
|
||||
if (TABLES_WITH_EXPORTED.has(table)) {
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, isTestCode, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
}
|
||||
// Multi-language tables (Struct, Impl, Trait, Macro, etc.)
|
||||
return `COPY ${t}(id, name, filePath, startLine, endLine, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ CREATE NODE TABLE Function (
|
|||
startLine INT64,
|
||||
endLine INT64,
|
||||
isExported BOOLEAN,
|
||||
isTestCode BOOLEAN,
|
||||
content STRING,
|
||||
description STRING,
|
||||
PRIMARY KEY (id)
|
||||
|
|
@ -62,6 +63,7 @@ CREATE NODE TABLE Class (
|
|||
startLine INT64,
|
||||
endLine INT64,
|
||||
isExported BOOLEAN,
|
||||
isTestCode BOOLEAN,
|
||||
content STRING,
|
||||
description STRING,
|
||||
frameworkAnnotations STRING[],
|
||||
|
|
@ -76,6 +78,7 @@ CREATE NODE TABLE Interface (
|
|||
startLine INT64,
|
||||
endLine INT64,
|
||||
isExported BOOLEAN,
|
||||
isTestCode BOOLEAN,
|
||||
content STRING,
|
||||
description STRING,
|
||||
PRIMARY KEY (id)
|
||||
|
|
@ -89,6 +92,7 @@ CREATE NODE TABLE Method (
|
|||
startLine INT64,
|
||||
endLine INT64,
|
||||
isExported BOOLEAN,
|
||||
isTestCode BOOLEAN,
|
||||
content STRING,
|
||||
description STRING,
|
||||
parameterCount INT32,
|
||||
|
|
@ -104,6 +108,7 @@ CREATE NODE TABLE CodeElement (
|
|||
startLine INT64,
|
||||
endLine INT64,
|
||||
isExported BOOLEAN,
|
||||
isTestCode BOOLEAN,
|
||||
content STRING,
|
||||
description STRING,
|
||||
PRIMARY KEY (id)
|
||||
|
|
|
|||
83
gitnexus/test/unit/node-column-parity.test.ts
Normal file
83
gitnexus/test/unit/node-column-parity.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { NODE_SCHEMA_QUERIES } from '../../src/core/lbug/schema.js';
|
||||
import { getCopyQuery } from '../../src/core/lbug/lbug-adapter.js';
|
||||
import type { NodeTableName } from '../../src/core/lbug/schema.js';
|
||||
|
||||
/**
|
||||
* A node column lives in THREE places that must agree:
|
||||
*
|
||||
* 1. the `CREATE NODE TABLE` DDL (`lbug/schema.ts`)
|
||||
* 2. the CSV header + row builder (`lbug/csv-generator.ts`)
|
||||
* 3. the explicit COPY column list (`lbug/lbug-adapter.ts` getCopyQuery)
|
||||
*
|
||||
* Adding `isTestCode` to only (1) and (2) produced
|
||||
* `COPY failed for Function: Number of columns mismatch. Expected 8 but got 9`
|
||||
* at load time, AFTER a full parse — the whole analyze wasted before the failure
|
||||
* surfaced. The DDL and the COPY list are the pair that can disagree silently,
|
||||
* so pin them to each other here.
|
||||
*/
|
||||
|
||||
/** Parse column names out of a `CREATE NODE TABLE X (...)` statement. */
|
||||
function ddlColumns(stmt: string): { table: string; columns: string[] } | null {
|
||||
const m = /CREATE NODE TABLE (\w+) \(([\s\S]*)\)/.exec(stmt);
|
||||
if (!m) return null;
|
||||
const [, table, body] = m;
|
||||
const columns = body
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0 && !l.startsWith('PRIMARY KEY'))
|
||||
.map((l) => l.replace(/,$/, '').split(/\s+/)[0])
|
||||
.filter((c): c is string => Boolean(c));
|
||||
return { table: table!, columns };
|
||||
}
|
||||
|
||||
/** Parse column names out of a `COPY X(a, b, c) FROM ...` statement. */
|
||||
function copyColumns(query: string): string[] | null {
|
||||
const m = /COPY\s+`?\w+`?\(([^)]*)\)/.exec(query);
|
||||
if (!m) return null;
|
||||
return m[1]!.split(',').map((c) => c.trim());
|
||||
}
|
||||
|
||||
const ddl = NODE_SCHEMA_QUERIES.map(ddlColumns).filter(
|
||||
(d): d is { table: string; columns: string[] } => d !== null,
|
||||
);
|
||||
|
||||
describe('node DDL and COPY column lists agree', () => {
|
||||
it('parsed the DDL at all (guard against a regex that silently matches nothing)', () => {
|
||||
expect(ddl.length).toBeGreaterThan(5);
|
||||
const fn = ddl.find((d) => d.table === 'Function');
|
||||
expect(fn?.columns).toContain('id');
|
||||
expect(fn?.columns).toContain('isTestCode');
|
||||
});
|
||||
|
||||
for (const { table, columns } of ddl) {
|
||||
it(`${table}: COPY column list matches its DDL, in order`, () => {
|
||||
const query = getCopyQuery(table as NodeTableName, '/tmp/x.csv');
|
||||
const copied = copyColumns(query);
|
||||
// A table whose COPY omits an explicit list is out of scope — the
|
||||
// positional form cannot drift from the DDL.
|
||||
if (copied === null) return;
|
||||
expect(copied).toEqual(columns);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('isTestCode is declared everywhere a definition table needs it', () => {
|
||||
// The five tables carrying `isExported` are the definition-bearing ones; a
|
||||
// symbol in any of them can live in test code, so all five need the flag for
|
||||
// the reachability query to be answerable without path heuristics.
|
||||
const EXPECTED = ['Function', 'Class', 'Interface', 'Method', 'CodeElement'];
|
||||
|
||||
for (const table of EXPECTED) {
|
||||
it(`${table} declares isTestCode in its DDL`, () => {
|
||||
const entry = ddl.find((d) => d.table === table);
|
||||
expect(entry, `${table} not found in NODE_SCHEMA_QUERIES`).toBeDefined();
|
||||
expect(entry!.columns).toContain('isTestCode');
|
||||
});
|
||||
|
||||
it(`${table} COPY includes isTestCode`, () => {
|
||||
const copied = copyColumns(getCopyQuery(table as NodeTableName, '/tmp/x.csv'));
|
||||
expect(copied).toContain('isTestCode');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -15,7 +15,7 @@ describe('Spring Bean Class persistence schema', () => {
|
|||
expect(CLASS_SCHEMA).toContain('frameworkAnnotations STRING[]');
|
||||
|
||||
expect(getCopyQuery('Class', '/tmp/class.csv')).toContain(
|
||||
'(id, name, filePath, startLine, endLine, isExported, content, description, frameworkAnnotations)',
|
||||
'(id, name, filePath, startLine, endLine, isExported, isTestCode, content, description, frameworkAnnotations)',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue