diff --git a/gitnexus/src/core/group/config-parser.ts b/gitnexus/src/core/group/config-parser.ts index d55969a73..4703e6329 100644 --- a/gitnexus/src/core/group/config-parser.ts +++ b/gitnexus/src/core/group/config-parser.ts @@ -13,7 +13,7 @@ const DEFAULT_DETECT = { topics: true, shared_libs: true, embedding_fallback: true, - workspace_deps: true, + workspace_deps: false, }; const DEFAULT_MATCHING = { diff --git a/gitnexus/src/core/group/extractors/manifest-extractor.ts b/gitnexus/src/core/group/extractors/manifest-extractor.ts index b65b7712d..3185f05e1 100644 --- a/gitnexus/src/core/group/extractors/manifest-extractor.ts +++ b/gitnexus/src/core/group/extractors/manifest-extractor.ts @@ -269,17 +269,19 @@ export class ManifestExtractor { { contract: link.contract }, ); } else if (link.type === 'custom') { - // V1: exact name-only match on code-definition nodes. - // Positive allowlist mirrors other contract types. If multiple code - // symbols share the same name, ORDER BY filePath ASC LIMIT 1 picks - // the alphabetically-first occurrence deterministically. + // Workspace extractors produce qualified contracts like "mathlex::Expression". + // Graph nodes store the unqualified symbol name ("Expression"), so strip + // the "provider::" prefix before querying. + const symbolName = link.contract.includes('::') + ? link.contract.split('::').pop()! + : link.contract; rows = await executor( `MATCH (n:Function|Method|Class|Interface|Struct|Enum|Trait|Constructor|TypeAlias|Impl|Macro|Union|Typedef|Property|Record|Delegate|Annotation|Template|Const|Static|CodeElement) - WHERE n.name = $contract + WHERE n.name = $symbolName RETURN n.id AS uid, n.name AS name, n.filePath AS filePath ORDER BY n.filePath ASC LIMIT 1`, - { contract: link.contract }, + { symbolName }, ); } else { return null; diff --git a/gitnexus/test/unit/group/manifest-extractor.test.ts b/gitnexus/test/unit/group/manifest-extractor.test.ts index 997445f2e..bb4e203fa 100644 --- a/gitnexus/test/unit/group/manifest-extractor.test.ts +++ b/gitnexus/test/unit/group/manifest-extractor.test.ts @@ -596,7 +596,7 @@ describe('ManifestExtractor', () => { [ 'engine/thales', async (_cypher, params) => { - if (params?.contract === 'Expression') { + if (params?.symbolName === 'Expression') { return [ { uid: 'uid-expression-struct', @@ -611,7 +611,7 @@ describe('ManifestExtractor', () => { [ 'parser/mathlex', async (_cypher, params) => { - if (params?.contract === 'Expression') { + if (params?.symbolName === 'Expression') { return [ { uid: 'uid-expression-enum', @@ -640,6 +640,42 @@ describe('ManifestExtractor', () => { expect(result.crossLinks[0].matchType).toBe('manifest'); }); + it('custom contract with qualified name (provider::Symbol) strips prefix before graph query', async () => { + const links: GroupManifestLink[] = [ + { + from: 'parser/mathlex', + to: 'engine/thales', + type: 'custom', + contract: 'mathlex::Expression', + role: 'provider', + }, + ]; + + let capturedParams: Record | undefined; + const dbExecutors = new Map< + string, + (cypher: string, params?: Record) => Promise[]> + >([ + [ + 'parser/mathlex', + async (_cypher, params) => { + capturedParams = params; + if (params?.symbolName === 'Expression') { + return [{ uid: 'uid-expr', name: 'Expression', filePath: 'src/ast.rs' }]; + } + return []; + }, + ], + ['engine/thales', async () => []], + ]); + + const result = await extractor.extractFromManifest(links, dbExecutors); + const provider = result.contracts.find((c) => c.role === 'provider'); + + expect(capturedParams?.symbolName).toBe('Expression'); + expect(provider?.symbolUid).toBe('uid-expr'); + }); + it('falls back to synthetic uid when custom symbol not found in graph', async () => { const links: GroupManifestLink[] = [ { @@ -714,7 +750,7 @@ describe('ManifestExtractor', () => { [ 'parser/mathlex', async (_cypher, params) => { - if (params?.contract === 'Token') { + if (params?.symbolName === 'Token') { return [{ uid: 'uid-token-first', name: 'Token', filePath: 'src/ast.rs' }]; } return []; @@ -723,7 +759,7 @@ describe('ManifestExtractor', () => { [ 'engine/thales', async (_cypher, params) => { - if (params?.contract === 'Token') { + if (params?.symbolName === 'Token') { return [{ uid: 'uid-token-consumer', name: 'Token', filePath: 'src/lexer.rs' }]; } return [];