diff --git a/gitnexus/src/core/lbug/csv-generator.ts b/gitnexus/src/core/lbug/csv-generator.ts index 4444a9687..cac005153 100644 --- a/gitnexus/src/core/lbug/csv-generator.ts +++ b/gitnexus/src/core/lbug/csv-generator.ts @@ -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 || '')), ]; diff --git a/gitnexus/src/core/lbug/lbug-adapter.ts b/gitnexus/src/core/lbug/lbug-adapter.ts index 597676455..3153ccef2 100644 --- a/gitnexus/src/core/lbug/lbug-adapter.ts +++ b/gitnexus/src/core/lbug/lbug-adapter.ts @@ -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}`; diff --git a/gitnexus/src/core/lbug/schema.ts b/gitnexus/src/core/lbug/schema.ts index 0b0c27e6b..8e2c3e0b3 100644 --- a/gitnexus/src/core/lbug/schema.ts +++ b/gitnexus/src/core/lbug/schema.ts @@ -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) diff --git a/gitnexus/test/unit/node-column-parity.test.ts b/gitnexus/test/unit/node-column-parity.test.ts new file mode 100644 index 000000000..5b4aa1374 --- /dev/null +++ b/gitnexus/test/unit/node-column-parity.test.ts @@ -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'); + }); + } +}); diff --git a/gitnexus/test/unit/spring-bean-schema.test.ts b/gitnexus/test/unit/spring-bean-schema.test.ts index a6bd72ea5..17da4f45d 100644 --- a/gitnexus/test/unit/spring-bean-schema.test.ts +++ b/gitnexus/test/unit/spring-bean-schema.test.ts @@ -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)', ); });