diff --git a/gitnexus/src/core/graph/types.ts b/gitnexus/src/core/graph/types.ts index aa9b24538..475fd52b9 100644 --- a/gitnexus/src/core/graph/types.ts +++ b/gitnexus/src/core/graph/types.ts @@ -80,6 +80,7 @@ export type RelationshipType = | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' + | 'HAS_PROPERTY' | 'MEMBER_OF' | 'STEP_IN_PROCESS' diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 11830b233..0af27fdc5 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -234,9 +234,9 @@ export const processCalls = async ( }); if (propEnclosingClassId) { graph.addRelationship({ - id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`), + id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`), sourceId: propEnclosingClassId, targetId: nodeId, - type: 'HAS_METHOD', confidence: 1.0, reason: '', + type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); } } diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index e8bf5dece..7bb648f86 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -329,13 +329,14 @@ const processParsingSequential = async ( graph.addRelationship(relationship); - // ── HAS_METHOD: link method/constructor/property to enclosing class ── + // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ── if (enclosingClassId) { + const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD'; graph.addRelationship({ - id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`), + id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`), sourceId: enclosingClassId, targetId: nodeId, - type: 'HAS_METHOD', + type: memberEdgeType, confidence: 1.0, reason: '', }); diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 9100ccec4..7d16b1cc3 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -135,7 +135,7 @@ interface ParsedRelationship { id: string; sourceId: string; targetId: string; - type: 'DEFINES' | 'HAS_METHOD'; + type: 'DEFINES' | 'HAS_METHOD' | 'HAS_PROPERTY'; confidence: number; reason: string; } @@ -1045,10 +1045,10 @@ const processFileGroup = ( }); if (propEnclosingClassId) { result.relationships.push({ - id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`), + id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`), sourceId: propEnclosingClassId, targetId: nodeId, - type: 'HAS_METHOD', + type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); @@ -1286,13 +1286,14 @@ const processFileGroup = ( reason: '', }); - // ── HAS_METHOD: link method/constructor/property to enclosing class ── + // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ── if (enclosingClassId) { + const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD'; result.relationships.push({ - id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`), + id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`), sourceId: enclosingClassId, targetId: nodeId, - type: 'HAS_METHOD', + type: memberEdgeType, confidence: 1.0, reason: '', }); diff --git a/gitnexus/src/core/lbug/schema.ts b/gitnexus/src/core/lbug/schema.ts index 11f55ae7b..c55e86524 100644 --- a/gitnexus/src/core/lbug/schema.ts +++ b/gitnexus/src/core/lbug/schema.ts @@ -26,7 +26,7 @@ export type NodeTableName = typeof NODE_TABLES[number]; export const REL_TABLE_NAME = 'CodeRelation'; // Valid relation types -export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'] as const; +export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'] as const; export type RelType = typeof REL_TYPES[number]; // ============================================================================ diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 67a9109ad..acde7acb3 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -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', 'HAS_METHOD', 'OVERRIDES']); +export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', '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; diff --git a/gitnexus/src/mcp/tools.ts b/gitnexus/src/mcp/tools.ts index 8565c8bc0..e274a59b5 100644 --- a/gitnexus/src/mcp/tools.ts +++ b/gitnexus/src/mcp/tools.ts @@ -78,7 +78,7 @@ SCHEMA: - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc. - All edges via single CodeRelation table with 'type' property -- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS +- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32) EXAMPLES: @@ -94,6 +94,9 @@ EXAMPLES: • Find all methods of a class: MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType +• Find all properties of a class: + MATCH (c:Class {name: "User"})-[r:CodeRelation {type: 'HAS_PROPERTY'}]->(p:Property) RETURN p.name, p.declaredType + • Find method overrides (MRO resolution): MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason @@ -200,7 +203,7 @@ Depth groups: - d=2: LIKELY AFFECTED (indirect) - d=3: MAY NEED TESTING (transitive) -EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES +EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES Confidence: 1.0 = certain, <0.8 = fuzzy match`, inputSchema: { type: 'object', @@ -208,7 +211,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`, target: { type: 'string', description: 'Name of function, class, or file to analyze' }, direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' }, maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 }, - relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES (default: usage-based)' }, + relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES (default: usage-based)' }, includeTests: { type: 'boolean', description: 'Include test files (default: false)' }, minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' }, repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' }, diff --git a/gitnexus/test/integration/resolvers/field-types.test.ts b/gitnexus/test/integration/resolvers/field-types.test.ts index 0217796d1..beb4a638f 100644 --- a/gitnexus/test/integration/resolvers/field-types.test.ts +++ b/gitnexus/test/integration/resolvers/field-types.test.ts @@ -4,7 +4,7 @@ * * Per-language fixtures test: * 1. Property nodes are extracted with correct ownerId linkage - * 2. HAS_METHOD edges link properties to their owning classes + * 2. HAS_PROPERTY edges link properties to their owning classes * 3. Field-access chain resolution resolves user.address.save() → Address#save */ import { describe, it, expect, beforeAll } from 'vitest'; @@ -39,9 +39,8 @@ describe('Field type resolution (TypeScript)', () => { expect(properties).toContain('city'); }); - it('emits HAS_METHOD edges linking properties to classes', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const propEdges = hasMethod.filter(e => e.targetLabel === 'Property'); + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); expect(propEdges.length).toBeGreaterThanOrEqual(3); expect(edgeSet(propEdges)).toContain('User → address'); expect(edgeSet(propEdges)).toContain('User → name'); @@ -82,9 +81,8 @@ describe('Field type resolution (Java)', () => { expect(properties).toContain('city'); }); - it('emits HAS_METHOD edges linking properties to classes', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const propEdges = hasMethod.filter(e => e.targetLabel === 'Property'); + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); expect(propEdges.length).toBeGreaterThanOrEqual(3); expect(edgeSet(propEdges)).toContain('User → address'); expect(edgeSet(propEdges)).toContain('User → name'); @@ -127,9 +125,8 @@ describe('Field type resolution (C#)', () => { expect(properties).toContain('City'); }); - it('emits HAS_METHOD edges linking properties to classes', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const propEdges = hasMethod.filter(e => e.targetLabel === 'Property'); + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); expect(propEdges.length).toBeGreaterThanOrEqual(3); expect(edgeSet(propEdges)).toContain('User → Address'); expect(edgeSet(propEdges)).toContain('User → Name'); @@ -171,9 +168,8 @@ describe('Field type resolution (Go)', () => { expect(properties).toContain('City'); }); - it('emits HAS_METHOD edges linking struct fields to structs', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const propEdges = hasMethod.filter(e => e.targetLabel === 'Property'); + it('emits HAS_PROPERTY edges linking struct fields to structs', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); expect(propEdges.length).toBeGreaterThanOrEqual(2); }); @@ -213,9 +209,8 @@ describe('Field type resolution (Kotlin)', () => { expect(properties).toContain('city'); }); - it('emits HAS_METHOD edges linking properties to classes', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const propEdges = hasMethod.filter(e => e.targetLabel === 'Property'); + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); expect(propEdges.length).toBeGreaterThanOrEqual(3); expect(edgeSet(propEdges)).toContain('User → address'); expect(edgeSet(propEdges)).toContain('User → name'); @@ -258,9 +253,8 @@ describe('Field type resolution (PHP)', () => { expect(properties).toContain('city'); }); - it('emits HAS_METHOD edges linking properties to classes', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const propEdges = hasMethod.filter(e => e.targetLabel === 'Property'); + it('emits HAS_PROPERTY edges linking properties to classes', () => { + const propEdges = getRelationships(result, 'HAS_PROPERTY'); expect(propEdges.length).toBeGreaterThanOrEqual(3); }); diff --git a/gitnexus/test/integration/resolvers/ruby.test.ts b/gitnexus/test/integration/resolvers/ruby.test.ts index f153dedf0..db06830a8 100644 --- a/gitnexus/test/integration/resolvers/ruby.test.ts +++ b/gitnexus/test/integration/resolvers/ruby.test.ts @@ -124,15 +124,15 @@ describe('Ruby require_relative, heritage & property resolution', () => { expect(props).toContain('email'); }); - it('emits HAS_METHOD from User to attr_reader :name', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const edge = hasMethod.find(e => e.source === 'User' && e.target === 'name'); + it('emits HAS_PROPERTY from User to attr_reader :name', () => { + const hasProperty = getRelationships(result, 'HAS_PROPERTY'); + const edge = hasProperty.find(e => e.source === 'User' && e.target === 'name'); expect(edge).toBeDefined(); }); - it('emits HAS_METHOD from BaseModel to attr_accessor :id', () => { - const hasMethod = getRelationships(result, 'HAS_METHOD'); - const edge = hasMethod.find(e => e.source === 'BaseModel' && e.target === 'id'); + it('emits HAS_PROPERTY from BaseModel to attr_accessor :id', () => { + const hasProperty = getRelationships(result, 'HAS_PROPERTY'); + const edge = hasProperty.find(e => e.source === 'BaseModel' && e.target === 'id'); expect(edge).toBeDefined(); }); diff --git a/gitnexus/test/integration/resolvers/typescript.test.ts b/gitnexus/test/integration/resolvers/typescript.test.ts index 02538152c..45f1c8cd3 100644 --- a/gitnexus/test/integration/resolvers/typescript.test.ts +++ b/gitnexus/test/integration/resolvers/typescript.test.ts @@ -53,18 +53,23 @@ describe('TypeScript heritage resolution', () => { ]); }); - it('emits HAS_METHOD edges linking methods and properties to classes', () => { + it('emits HAS_METHOD edges linking methods to classes', () => { const hasMethod = getRelationships(result, 'HAS_METHOD'); - expect(hasMethod.length).toBe(5); + expect(hasMethod.length).toBe(4); expect(edgeSet(hasMethod)).toEqual([ 'BaseService → getName', - 'BaseService → name', 'ConsoleLogger → log', 'UserService → getUsers', 'UserService → log', ]); }); + it('emits HAS_PROPERTY edge for class fields', () => { + const hasProperty = getRelationships(result, 'HAS_PROPERTY'); + expect(hasProperty.length).toBe(1); + expect(edgeSet(hasProperty)).toEqual(['BaseService → name']); + }); + it('no OVERRIDES edges target Property nodes', () => { const overrides = getRelationships(result, 'OVERRIDES'); for (const edge of overrides) { diff --git a/gitnexus/test/unit/mro-processor.test.ts b/gitnexus/test/unit/mro-processor.test.ts index 78ca0bf14..2e4d04a59 100644 --- a/gitnexus/test/unit/mro-processor.test.ts +++ b/gitnexus/test/unit/mro-processor.test.ts @@ -292,19 +292,19 @@ describe('computeMRO', () => { addExtends(graph, 'Child', 'ParentA'); addExtends(graph, 'Child', 'ParentB'); - // Add Property nodes (same name 'name') to both parents via HAS_METHOD + // Add Property nodes (same name 'name') to both parents via HAS_PROPERTY const propA = generateId('Property', 'ParentA.name'); graph.addNode({ id: propA, label: 'Property', properties: { name: 'name', filePath: 'src/ParentA.ts' } }); graph.addRelationship({ - id: generateId('HAS_METHOD', `${parentA}->${propA}`), - sourceId: parentA, targetId: propA, type: 'HAS_METHOD', confidence: 1.0, reason: '', + id: generateId('HAS_PROPERTY', `${parentA}->${propA}`), + sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const propB = generateId('Property', 'ParentB.name'); graph.addNode({ id: propB, label: 'Property', properties: { name: 'name', filePath: 'src/ParentB.ts' } }); graph.addRelationship({ - id: generateId('HAS_METHOD', `${parentB}->${propB}`), - sourceId: parentB, targetId: propB, type: 'HAS_METHOD', confidence: 1.0, reason: '', + id: generateId('HAS_PROPERTY', `${parentB}->${propB}`), + sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const result = computeMRO(graph); @@ -328,19 +328,19 @@ describe('computeMRO', () => { const methodA = addMethod(graph, 'PA', 'doWork'); addMethod(graph, 'PB', 'doWork'); - // Property collision (should NOT trigger OVERRIDES) + // Property collision (should NOT trigger OVERRIDES — properties use HAS_PROPERTY, not HAS_METHOD) const propA = generateId('Property', 'PA.id'); graph.addNode({ id: propA, label: 'Property', properties: { name: 'id', filePath: 'src/PA.ts' } }); graph.addRelationship({ - id: generateId('HAS_METHOD', `${parentA}->${propA}`), - sourceId: parentA, targetId: propA, type: 'HAS_METHOD', confidence: 1.0, reason: '', + id: generateId('HAS_PROPERTY', `${parentA}->${propA}`), + sourceId: parentA, targetId: propA, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const propB = generateId('Property', 'PB.id'); graph.addNode({ id: propB, label: 'Property', properties: { name: 'id', filePath: 'src/PB.ts' } }); graph.addRelationship({ - id: generateId('HAS_METHOD', `${parentB}->${propB}`), - sourceId: parentB, targetId: propB, type: 'HAS_METHOD', confidence: 1.0, reason: '', + id: generateId('HAS_PROPERTY', `${parentB}->${propB}`), + sourceId: parentB, targetId: propB, type: 'HAS_PROPERTY', confidence: 1.0, reason: '', }); const result = computeMRO(graph); diff --git a/gitnexus/test/unit/schema.test.ts b/gitnexus/test/unit/schema.test.ts index 15f2a9832..87e369652 100644 --- a/gitnexus/test/unit/schema.test.ts +++ b/gitnexus/test/unit/schema.test.ts @@ -122,11 +122,11 @@ describe('LadybugDB Schema', () => { it('has all FROM/TO pairs needed for HAS_METHOD edges', () => { // HAS_METHOD sources: Class, Interface, Struct, Trait, Impl, Record - // HAS_METHOD targets: Method, Constructor, Property + // HAS_METHOD targets: Method, Constructor (Property is now HAS_PROPERTY) const sources = ['Class', 'Interface']; const backtickSources = ['Struct', 'Trait', 'Impl', 'Record']; const targets = ['Method']; - const backtickTargets = ['Constructor', 'Property']; + const backtickTargets = ['Constructor']; // Non-backtick source → non-backtick target for (const src of sources) {