fix(workspace): strip qualified prefix in custom contract resolution, default workspace_deps to false

resolveSymbol for custom contracts now strips the "provider::" prefix
before querying graph nodes, so workspace-generated contracts like
"mathlex::Expression" correctly resolve to the "Expression" symbol.

Change workspace_deps default from true to false for safe rollout —
existing groups won't silently gain 6-ecosystem scans on upgrade.
This commit is contained in:
Christian C. Berclaz 2026-05-03 19:48:44 +02:00
parent 0a330878db
commit 363c562273
No known key found for this signature in database
3 changed files with 49 additions and 11 deletions

View file

@ -13,7 +13,7 @@ const DEFAULT_DETECT = {
topics: true,
shared_libs: true,
embedding_fallback: true,
workspace_deps: true,
workspace_deps: false,
};
const DEFAULT_MATCHING = {

View file

@ -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;

View file

@ -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<string, unknown> | undefined;
const dbExecutors = new Map<
string,
(cypher: string, params?: Record<string, unknown>) => Promise<Record<string, unknown>[]>
>([
[
'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 [];