fix(impact): add HAS_METHOD and OVERRIDES to VALID_RELATION_TYPES (#350)

This commit is contained in:
Karesansui 2026-03-18 15:01:02 +09:00 • committed by GitHub
parent 6d2cb4039f
commit c717d69457
5 changed files with 73 additions and 4 deletions

View file

@ -47,7 +47,7 @@ export const VALID_NODE_LABELS = new Set([
]);
/** Valid relation types for impact analysis filtering */
export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']);
export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']);
/** Regex to detect write operations in user-supplied Cypher queries */
export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;

View file

@ -10,6 +10,10 @@ export const LOCAL_BACKEND_SEED_DATA = [
`CREATE (fn:Function {id: 'func:hash', name: 'hash', filePath: 'src/utils.ts', startLine: 1, endLine: 8, isExported: true, content: 'function hash() {}', description: 'Hash utility'})`,
// Class
`CREATE (c:Class {id: 'class:AuthService', name: 'AuthService', filePath: 'src/auth.ts', startLine: 30, endLine: 60, isExported: true, content: 'class AuthService {}', description: 'Authentication service'})`,
`CREATE (c:Class {id: 'class:BaseService', name: 'BaseService', filePath: 'src/base.ts', startLine: 1, endLine: 20, isExported: true, content: 'class BaseService {}', description: 'Base service class'})`,
// Methods
`CREATE (m:Method {id: 'method:AuthService.authenticate', name: 'authenticate', filePath: 'src/auth.ts', startLine: 35, endLine: 45, isExported: false, content: 'authenticate() {}', description: 'Authenticate user'})`,
`CREATE (m:Method {id: 'method:BaseService.authenticate', name: 'authenticate', filePath: 'src/base.ts', startLine: 5, endLine: 10, isExported: false, content: 'authenticate() {}', description: 'Base authenticate'})`,
// Community
`CREATE (c:Community {id: 'comm:auth', label: 'Auth', heuristicLabel: 'Authentication', keywords: ['auth', 'login'], description: 'Auth module', enrichedBy: 'heuristic', cohesion: 0.8, symbolCount: 3})`,
// Process
@ -25,10 +29,20 @@ export const LOCAL_BACKEND_SEED_DATA = [
CREATE (a)-[:CodeRelation {type: 'STEP_IN_PROCESS', confidence: 1.0, reason: '', step: 1}]->(p)`,
`MATCH (a:Function), (p:Process) WHERE a.id = 'func:validate' AND p.id = 'proc:login-flow'
CREATE (a)-[:CodeRelation {type: 'STEP_IN_PROCESS', confidence: 1.0, reason: '', step: 2}]->(p)`,
// HAS_METHOD: AuthService -> authenticate
`MATCH (c:Class), (m:Method) WHERE c.id = 'class:AuthService' AND m.id = 'method:AuthService.authenticate'
CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`,
// OVERRIDES: AuthService.authenticate -> BaseService.authenticate
`MATCH (a:Method), (b:Method) WHERE a.id = 'method:AuthService.authenticate' AND b.id = 'method:BaseService.authenticate'
CREATE (a)-[:CodeRelation {type: 'OVERRIDES', confidence: 1.0, reason: 'mro-resolution', step: 0}]->(b)`,
// HAS_METHOD: BaseService -> authenticate
`MATCH (c:Class), (m:Method) WHERE c.id = 'class:BaseService' AND m.id = 'method:BaseService.authenticate'
CREATE (c)-[:CodeRelation {type: 'HAS_METHOD', confidence: 1.0, reason: 'class-method', step: 0}]->(m)`,
];
export const LOCAL_BACKEND_FTS_INDEXES: FTSIndexDef[] = [
{ table: 'Function', indexName: 'function_fts', columns: ['name', 'content', 'description'] },
{ table: 'Class', indexName: 'class_fts', columns: ['name', 'content', 'description'] },
{ table: 'Method', indexName: 'method_fts', columns: ['name', 'content', 'description'] },
{ table: 'File', indexName: 'file_fts', columns: ['name', 'content'] },
];

View file

@ -106,6 +106,59 @@ withTestLbugDB('local-backend-calltool', (handle) => {
});
});
describe('impact tool relationTypes filtering', () => {
let backend: LocalBackend;
beforeAll(async () => {
const ext = handle as typeof handle & { _backend?: LocalBackend };
if (!ext._backend) {
throw new Error('LocalBackend not initialized — afterSetup did not attach _backend to handle');
}
backend = ext._backend;
});
it('filters by HAS_METHOD only', async () => {
const result = await backend.callTool('impact', {
target: 'AuthService',
direction: 'downstream',
relationTypes: ['HAS_METHOD'],
});
expect(result).not.toHaveProperty('error');
expect(result.impactedCount).toBeGreaterThanOrEqual(1);
const d1 = result.byDepth[1] || result.byDepth['1'] || [];
const names = d1.map((d: any) => d.name);
expect(names).toContain('authenticate');
// Should NOT include CALLS-reachable symbols like validate/hash
expect(names).not.toContain('validate');
expect(names).not.toContain('hash');
});
it('filters by OVERRIDES only', async () => {
const result = await backend.callTool('impact', {
target: 'authenticate',
direction: 'downstream',
relationTypes: ['OVERRIDES'],
});
expect(result).not.toHaveProperty('error');
// AuthService.authenticate overrides BaseService.authenticate
expect(result.impactedCount).toBeGreaterThanOrEqual(1);
const d1 = result.byDepth[1] || result.byDepth['1'] || [];
const names = d1.map((d: any) => d.name);
expect(names).toContain('authenticate');
});
it('does not return HAS_METHOD results when filtering by CALLS only', async () => {
const result = await backend.callTool('impact', {
target: 'AuthService',
direction: 'downstream',
relationTypes: ['CALLS'],
});
expect(result).not.toHaveProperty('error');
// AuthService has no outgoing CALLS edges, only HAS_METHOD
expect(result.impactedCount).toBe(0);
});
});
describe('tool parameter edge cases', () => {
let backend: LocalBackend;

View file

@ -76,7 +76,7 @@ withTestLbugDB('local-backend', (handle) => {
describe('relation type filtering', () => {
it('only allows valid relation types in queries', () => {
const validTypes = ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS'];
const validTypes = ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES'];
const invalidTypes = ['CONTAINS', 'STEP_IN_PROCESS', 'MEMBER_OF', 'DROP_TABLE'];
for (const t of validTypes) {

View file

@ -96,12 +96,14 @@ describe('isWriteQuery', () => {
// ─── Relation type allowlist ──────────────────────────────────────────
describe('VALID_RELATION_TYPES', () => {
it('contains exactly the expected 4 types', () => {
expect(VALID_RELATION_TYPES.size).toBe(4);
it('contains exactly the expected 6 types', () => {
expect(VALID_RELATION_TYPES.size).toBe(6);
expect(VALID_RELATION_TYPES.has('CALLS')).toBe(true);
expect(VALID_RELATION_TYPES.has('IMPORTS')).toBe(true);
expect(VALID_RELATION_TYPES.has('EXTENDS')).toBe(true);
expect(VALID_RELATION_TYPES.has('IMPLEMENTS')).toBe(true);
expect(VALID_RELATION_TYPES.has('HAS_METHOD')).toBe(true);
expect(VALID_RELATION_TYPES.has('OVERRIDES')).toBe(true);
});
it('rejects invalid relation types', () => {