gitnexus 1.6.10-rc.219 → 1.6.10-rc.220

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,20 @@ import type Parser from 'tree-sitter';
28
28
  import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
29
29
  /** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
30
30
  export type CaptureMap = Record<string, SyntaxNode | undefined>;
31
+ export interface DefinitionPropertiesContext {
32
+ readonly nodeLabel: NodeLabel;
33
+ readonly nodeName: string;
34
+ readonly definitionNode: SyntaxNode;
35
+ readonly parsedImports: readonly ParsedImport[];
36
+ readonly isExported: boolean;
37
+ }
38
+ export type DefinitionPropertiesExtractor = (context: DefinitionPropertiesContext) => Readonly<Record<string, unknown>> | undefined;
39
+ /** Run optional provider enrichment without allowing one hook failure to drop
40
+ * the rest of the worker's language batch. */
41
+ export declare function runDefinitionPropertiesExtractor(extractor: DefinitionPropertiesExtractor, context: DefinitionPropertiesContext, onError: (error: unknown) => void): Readonly<Record<string, unknown>> | undefined;
42
+ /** Provider metadata is additive; graph identity and source-location fields
43
+ * supplied by the worker remain authoritative. */
44
+ export declare function mergeCanonicalDefinitionProperties<TCanonical extends Readonly<Record<string, unknown>>>(providerProperties: Readonly<Record<string, unknown>>, canonicalProperties: TCanonical): Record<string, unknown> & TCanonical;
31
45
  /** Configuration for AST-based framework detection patterns. */
32
46
  export interface AstFrameworkPatternConfig {
33
47
  framework: string;
@@ -215,6 +229,10 @@ interface LanguageProviderConfig {
215
229
  * constant, and static declarations. Produces VariableInfo with type, visibility,
216
230
  * isConst, isStatic, isMutable metadata. Default: undefined (no variable extraction). */
217
231
  readonly variableExtractor?: VariableExtractor;
232
+ /** Add language-owned, structured properties to a definition node. Values
233
+ * cross the worker boundary and must therefore be structured-clone-safe.
234
+ * Shared ingestion code treats these properties as opaque. */
235
+ readonly definitionPropertiesExtractor?: DefinitionPropertiesExtractor;
218
236
  /** Class/type extractor for deriving canonical qualified names for class-like symbols.
219
237
  * Uses the same provider-driven strategy pattern as method/field extraction so
220
238
  * namespace/package/module rules stay language-specific. */
@@ -8,6 +8,22 @@
8
8
  * The providers table in `languages/index.ts` uses `satisfies Record<SupportedLanguages, LanguageProvider>`
9
9
  * so adding a language to the enum without creating a provider is a compiler error.
10
10
  */
11
+ /** Run optional provider enrichment without allowing one hook failure to drop
12
+ * the rest of the worker's language batch. */
13
+ export function runDefinitionPropertiesExtractor(extractor, context, onError) {
14
+ try {
15
+ return extractor(context);
16
+ }
17
+ catch (error) {
18
+ onError(error);
19
+ return undefined;
20
+ }
21
+ }
22
+ /** Provider metadata is additive; graph identity and source-location fields
23
+ * supplied by the worker remain authoritative. */
24
+ export function mergeCanonicalDefinitionProperties(providerProperties, canonicalProperties) {
25
+ return { ...providerProperties, ...canonicalProperties };
26
+ }
11
27
  /**
12
28
  * Everything a language needs to provide.
13
29
  * Required fields must be explicitly set; optional fields have defaults
@@ -0,0 +1,7 @@
1
+ import type { DefinitionPropertiesContext } from '../../language-provider.js';
2
+ /**
3
+ * Stamp Convex runtime-dispatch metadata only when both the declaration shape
4
+ * and the factory import provenance are known. The MCP layer consumes the
5
+ * resulting property without reparsing lossy FTS text.
6
+ */
7
+ export declare function extractConvexEndpointProperties(context: DefinitionPropertiesContext): Readonly<Record<string, unknown>> | undefined;
@@ -0,0 +1,92 @@
1
+ import { assertCloneable } from '../../workers/clone-safety.js';
2
+ const GENERATED_ENDPOINT_FACTORIES = new Set([
3
+ 'query',
4
+ 'mutation',
5
+ 'action',
6
+ 'internalQuery',
7
+ 'internalMutation',
8
+ 'internalAction',
9
+ 'httpAction',
10
+ ]);
11
+ const GENERIC_ENDPOINT_FACTORIES = new Map([...GENERATED_ENDPOINT_FACTORIES].map((factory) => [`${factory}Generic`, factory]));
12
+ const normalizeModuleTarget = (targetRaw) => targetRaw.replace(/\\/g, '/').replace(/\.(?:[cm]?[jt]s)$/, '');
13
+ const isGeneratedServerModule = (targetRaw) => /(?:^|\/)_generated\/server$/.test(normalizeModuleTarget(targetRaw));
14
+ function importedConvexFactory(imports, localName) {
15
+ for (const parsedImport of imports) {
16
+ if (parsedImport.kind !== 'named' && parsedImport.kind !== 'alias')
17
+ continue;
18
+ if (parsedImport.localName !== localName)
19
+ continue;
20
+ const target = normalizeModuleTarget(parsedImport.targetRaw);
21
+ if (target === 'convex/server') {
22
+ return GENERIC_ENDPOINT_FACTORIES.get(parsedImport.importedName);
23
+ }
24
+ if (isGeneratedServerModule(target)) {
25
+ return GENERATED_ENDPOINT_FACTORIES.has(parsedImport.importedName)
26
+ ? parsedImport.importedName
27
+ : undefined;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ function matchingDeclarator(node, nodeName) {
33
+ if (node.type === 'variable_declarator' && node.childForFieldName('name')?.text === nodeName) {
34
+ return node;
35
+ }
36
+ if (node.type === 'export_statement') {
37
+ const declaration = node.childForFieldName('declaration');
38
+ return declaration ? matchingDeclarator(declaration, nodeName) : undefined;
39
+ }
40
+ if (node.type !== 'lexical_declaration' && node.type !== 'variable_declaration') {
41
+ return undefined;
42
+ }
43
+ for (let i = 0; i < node.namedChildCount; i++) {
44
+ const child = node.namedChild(i);
45
+ if (child?.type === 'variable_declarator' &&
46
+ child.childForFieldName('name')?.text === nodeName) {
47
+ return child;
48
+ }
49
+ }
50
+ return undefined;
51
+ }
52
+ function findDeclarator(node, nodeName) {
53
+ let current = node;
54
+ while (current) {
55
+ const declarator = matchingDeclarator(current, nodeName);
56
+ if (declarator)
57
+ return declarator;
58
+ if (current.type === 'program' || current.type === 'statement_block')
59
+ break;
60
+ current = current.parent;
61
+ }
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Stamp Convex runtime-dispatch metadata only when both the declaration shape
66
+ * and the factory import provenance are known. The MCP layer consumes the
67
+ * resulting property without reparsing lossy FTS text.
68
+ */
69
+ export function extractConvexEndpointProperties(context) {
70
+ if ((context.nodeLabel !== 'Const' && context.nodeLabel !== 'Function') || !context.isExported) {
71
+ return undefined;
72
+ }
73
+ const declarator = findDeclarator(context.definitionNode, context.nodeName);
74
+ const value = declarator?.childForFieldName('value');
75
+ if (!value || value.type !== 'call_expression')
76
+ return undefined;
77
+ const callee = value.childForFieldName('function');
78
+ if (!callee || callee.type !== 'identifier')
79
+ return undefined;
80
+ const factory = importedConvexFactory(context.parsedImports, callee.text);
81
+ if (factory === undefined)
82
+ return undefined;
83
+ const args = value.childForFieldName('arguments');
84
+ if (!args || args.namedChildCount !== 1)
85
+ return undefined;
86
+ const endpointDefinition = args.namedChild(0);
87
+ if (!endpointDefinition ||
88
+ !['object', 'arrow_function', 'function_expression'].includes(endpointDefinition.type)) {
89
+ return undefined;
90
+ }
91
+ return assertCloneable({ convexEndpointFactory: factory });
92
+ }
@@ -74,6 +74,7 @@ import { emitTsScopeCaptures, interpretTsImport, interpretTsTypeBinding, tsBindi
74
74
  import { emitJsScopeCaptures, interpretJsImport, interpretJsTypeBinding, jsBindingScopeFor, jsImportOwningScope, jsReceiverBinding, jsMergeBindings, jsArityCompatibility, } from './javascript/index.js';
75
75
  import { extractDispatchGuardRoutes } from '../route-extractors/dispatch-guard.js';
76
76
  import { extractDataRouteTableRoutes } from '../route-extractors/data-route-table.js';
77
+ import { extractConvexEndpointProperties } from './typescript/convex-endpoint-metadata.js';
77
78
  const extractJsTsRoutes = (...args) => [
78
79
  ...extractDispatchGuardRoutes(...args),
79
80
  ...extractDataRouteTableRoutes(...args),
@@ -347,6 +348,7 @@ export const typescriptProvider = defineLanguage({
347
348
  extractFunctionName: tsExtractFunctionName,
348
349
  }),
349
350
  variableExtractor: createVariableExtractor(typescriptVariableConfig),
351
+ definitionPropertiesExtractor: extractConvexEndpointProperties,
350
352
  classExtractor: createClassExtractor(typescriptClassConfig),
351
353
  // ── JSDoc → description (issue #2270). An exported decl is captured as the
352
354
  // inner declaration; its JSDoc precedes the wrapping `export_statement`. ──
@@ -430,6 +432,7 @@ export const javascriptProvider = defineLanguage({
430
432
  extractFunctionName: tsExtractFunctionName,
431
433
  }),
432
434
  variableExtractor: createVariableExtractor(javascriptVariableConfig),
435
+ definitionPropertiesExtractor: extractConvexEndpointProperties,
433
436
  classExtractor: createClassExtractor(javascriptClassConfig),
434
437
  // ── JSDoc → description (issue #2270). An exported decl is captured as the
435
438
  // inner declaration; its JSDoc precedes the wrapping `export_statement`. ──
@@ -69,7 +69,7 @@ import { defaultExportNameCollides } from '../languages/typescript/cjs-export-as
69
69
  import { extractVueScript, extractTemplateComponents, isVueSetupTopLevel, } from '../vue-sfc-extractor.js';
70
70
  import { buildMethodProps, arityForIdFromInfo, typeTagForId, constTagForId, buildCollisionGroups, parameterShapeIdTag, methodInfoKey, } from '../utils/method-props.js';
71
71
  import { extractTemplateArguments, templateArgumentsIdTag, templateConstraintsIdTag, } from '../utils/template-arguments.js';
72
- import { shouldHarvestModuleConstants } from '../language-provider.js';
72
+ import { mergeCanonicalDefinitionProperties, runDefinitionPropertiesExtractor, shouldHarvestModuleConstants, } from '../language-provider.js';
73
73
  import { extractParsedFile } from '../scope-extractor-bridge.js';
74
74
  import { persistParsedFileShardSync, persistDurableParsedFileShardSync, } from '../../../storage/parsedfile-store.js';
75
75
  import { extractLaravelRoutes } from '../route-extractors/laravel.js';
@@ -2112,18 +2112,30 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
2112
2112
  methodProps.scope = varInfo.scope;
2113
2113
  }
2114
2114
  }
2115
+ const isExported = language === SupportedLanguages.Vue && isVueSetup
2116
+ ? isVueSetupTopLevel(nameNode || definitionNode)
2117
+ : cachedExportCheck(provider.exportChecker, nameNode || definitionNode, nodeName);
2118
+ if (definitionNode && provider.definitionPropertiesExtractor) {
2119
+ const definitionProperties = runDefinitionPropertiesExtractor(provider.definitionPropertiesExtractor, {
2120
+ nodeLabel,
2121
+ nodeName,
2122
+ definitionNode,
2123
+ parsedImports: parsedFile?.parsedImports ?? [],
2124
+ isExported,
2125
+ }, (error) => reportWarning(`Definition property extraction failed for ${file.path}:${nodeName}: ${error instanceof Error ? error.message : String(error)}`));
2126
+ if (definitionProperties !== undefined)
2127
+ Object.assign(methodProps, definitionProperties);
2128
+ }
2115
2129
  result.nodes.push({
2116
2130
  id: nodeId,
2117
2131
  label: nodeLabel,
2118
- properties: {
2132
+ properties: mergeCanonicalDefinitionProperties(methodProps, {
2119
2133
  name: nodeName,
2120
2134
  filePath: file.path,
2121
2135
  startLine,
2122
2136
  endLine: definitionNode ? definitionNode.endPosition.row + lineOffset : startLine,
2123
2137
  language: language,
2124
- isExported: language === SupportedLanguages.Vue && isVueSetup
2125
- ? isVueSetupTopLevel(nameNode || definitionNode)
2126
- : cachedExportCheck(provider.exportChecker, nameNode || definitionNode, nodeName),
2138
+ isExported,
2127
2139
  ...(qualifiedTypeName !== undefined ? { qualifiedName: qualifiedTypeName } : {}),
2128
2140
  ...(classTemplateArguments !== undefined && classTemplateArguments.length > 0
2129
2141
  ? { templateArguments: classTemplateArguments }
@@ -2138,10 +2150,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
2138
2150
  }
2139
2151
  : {}),
2140
2152
  ...(description !== undefined ? { description } : {}),
2141
- ...methodProps,
2142
2153
  ...(declaredType !== undefined ? { declaredType } : {}),
2143
2154
  ...(returnShapeProperty ? { fromReturnShape: true, isDetail: true } : {}),
2144
- },
2155
+ }),
2145
2156
  });
2146
2157
  // enclosingClassId already computed above (before nodeId generation)
2147
2158
  const ownerId = enclosingClassId ?? objectLiteralOwnerInfo?.ownerId;
@@ -417,7 +417,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir, onNodePhaseCo
417
417
  const fileWriter = new BufferedCSVWriter(path.join(csvDir, 'file.csv'), 'id,name,filePath,content');
418
418
  const folderWriter = new BufferedCSVWriter(path.join(csvDir, 'folder.csv'), 'id,name,filePath');
419
419
  const codeElementHeader = 'id,name,filePath,startLine,endLine,isExported,content,description';
420
- const functionWriter = new BufferedCSVWriter(path.join(csvDir, 'function.csv'), codeElementHeader);
420
+ const functionWriter = new BufferedCSVWriter(path.join(csvDir, 'function.csv'), `${codeElementHeader},convexEndpointFactory`);
421
421
  const classWriter = new BufferedCSVWriter(path.join(csvDir, 'class.csv'), `${codeElementHeader},frameworkAnnotations`);
422
422
  const interfaceWriter = new BufferedCSVWriter(path.join(csvDir, 'interface.csv'), codeElementHeader);
423
423
  const methodHeader = 'id,name,filePath,startLine,endLine,isExported,content,description,parameterCount,returnType';
@@ -436,6 +436,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir, onNodePhaseCo
436
436
  const basicBlockWriter = new BufferedCSVWriter(path.join(csvDir, 'basicblock.csv'), BASICBLOCK_CSV_HEADER);
437
437
  // Multi-language node types share the same CSV shape (no isExported column)
438
438
  const multiLangHeader = 'id,name,filePath,startLine,endLine,content,description';
439
+ const constHeader = `${multiLangHeader},convexEndpointFactory`;
439
440
  const MULTI_LANG_TYPES = [
440
441
  'Struct',
441
442
  'Enum',
@@ -460,7 +461,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir, onNodePhaseCo
460
461
  const propertyHeader = 'id,name,filePath,startLine,endLine,content,description,declaredType,isDetail';
461
462
  const multiLangWriters = new Map();
462
463
  for (const t of MULTI_LANG_TYPES) {
463
- multiLangWriters.set(t, new BufferedCSVWriter(path.join(csvDir, `${t.toLowerCase()}.csv`), t === 'Property' ? propertyHeader : multiLangHeader));
464
+ multiLangWriters.set(t, new BufferedCSVWriter(path.join(csvDir, `${t.toLowerCase()}.csv`), t === 'Property' ? propertyHeader : t === 'Const' ? constHeader : multiLangHeader));
464
465
  }
465
466
  const codeWriterMap = {
466
467
  Function: functionWriter,
@@ -609,6 +610,9 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir, onNodePhaseCo
609
610
  if (node.label === 'Class') {
610
611
  row.push(escapeCSVField(formatCSVStringArray(node.properties.frameworkAnnotations)));
611
612
  }
613
+ else if (node.label === 'Function') {
614
+ row.push(escapeCSVField(String(node.properties.convexEndpointFactory ?? '')));
615
+ }
612
616
  pending = writer.addRow(row.join(','));
613
617
  }
614
618
  else {
@@ -632,7 +636,9 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir, onNodePhaseCo
632
636
  // empty BOOLEAN cell fails the COPY.
633
637
  node.properties.isDetail === true ? 'true' : 'false',
634
638
  ]
635
- : []),
639
+ : node.label === 'Const'
640
+ ? [escapeCSVField(String(node.properties.convexEndpointFactory ?? ''))]
641
+ : []),
636
642
  ].join(','));
637
643
  }
638
644
  else {
@@ -1336,9 +1336,15 @@ export const getCopyQuery = (table, filePath) => {
1336
1336
  if (table === 'Method') {
1337
1337
  return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, parameterCount, returnType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1338
1338
  }
1339
+ if (table === 'Function') {
1340
+ return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, convexEndpointFactory) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1341
+ }
1339
1342
  if (table === 'Property') {
1340
1343
  return `COPY ${t}(id, name, filePath, startLine, endLine, content, description, declaredType, isDetail) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1341
1344
  }
1345
+ if (table === 'Const') {
1346
+ return `COPY ${t}(id, name, filePath, startLine, endLine, content, description, convexEndpointFactory) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1347
+ }
1342
1348
  // TypeScript/JS code element tables have isExported; multi-language tables do not
1343
1349
  if (TABLES_WITH_EXPORTED.has(table)) {
1344
1350
  return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
@@ -1388,6 +1394,18 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
1388
1394
  : '';
1389
1395
  query = `CREATE (n:Class {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}, frameworkAnnotations: ${formatCypherStringArray(properties.frameworkAnnotations)}})`;
1390
1396
  }
1397
+ else if (label === 'Function') {
1398
+ const descPart = properties.description
1399
+ ? `, description: ${formatCypherValue(properties.description)}`
1400
+ : '';
1401
+ query = `CREATE (n:Function {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}, convexEndpointFactory: ${formatCypherValue(properties.convexEndpointFactory ?? '')}})`;
1402
+ }
1403
+ else if (label === 'Const') {
1404
+ const descPart = properties.description
1405
+ ? `, description: ${formatCypherValue(properties.description)}`
1406
+ : '';
1407
+ query = `CREATE (n:Const {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}, convexEndpointFactory: ${formatCypherValue(properties.convexEndpointFactory ?? '')}})`;
1408
+ }
1391
1409
  else if (TABLES_WITH_EXPORTED.has(label)) {
1392
1410
  const descPart = properties.description
1393
1411
  ? `, description: ${formatCypherValue(properties.description)}`
@@ -1478,6 +1496,18 @@ export const batchInsertNodesToLbug = async (nodes, dbPath) => {
1478
1496
  : '';
1479
1497
  query = `MERGE (n:Class {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.frameworkAnnotations = ${formatCypherStringArray(properties.frameworkAnnotations)}`;
1480
1498
  }
1499
+ else if (label === 'Function') {
1500
+ const descPart = properties.description
1501
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1502
+ : '';
1503
+ query = `MERGE (n:Function {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.convexEndpointFactory = ${formatCypherValue(properties.convexEndpointFactory ?? '')}`;
1504
+ }
1505
+ else if (label === 'Const') {
1506
+ const descPart = properties.description
1507
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1508
+ : '';
1509
+ query = `MERGE (n:Const {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.convexEndpointFactory = ${formatCypherValue(properties.convexEndpointFactory ?? '')}`;
1510
+ }
1481
1511
  else if (TABLES_WITH_EXPORTED.has(label)) {
1482
1512
  const descPart = properties.description
1483
1513
  ? `, n.description = ${formatCypherValue(properties.description)}`
@@ -13,7 +13,7 @@ export { NODE_TABLES, REL_TABLE_NAME, REL_TYPES, EMBEDDING_TABLE_NAME };
13
13
  export type { NodeTableName, RelType } from '../../_shared/index.js';
14
14
  export declare const FILE_SCHEMA = "\nCREATE NODE TABLE File (\n id STRING,\n name STRING,\n filePath STRING,\n content STRING,\n PRIMARY KEY (id)\n)";
15
15
  export declare const FOLDER_SCHEMA = "\nCREATE NODE TABLE Folder (\n id STRING,\n name STRING,\n filePath STRING,\n PRIMARY KEY (id)\n)";
16
- export declare const FUNCTION_SCHEMA = "\nCREATE NODE TABLE Function (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n isExported BOOLEAN,\n content STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
16
+ export declare const FUNCTION_SCHEMA = "\nCREATE NODE TABLE Function (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n isExported BOOLEAN,\n content STRING,\n description STRING,\n convexEndpointFactory STRING,\n PRIMARY KEY (id)\n)";
17
17
  export declare const CLASS_SCHEMA = "\nCREATE NODE TABLE Class (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n isExported BOOLEAN,\n content STRING,\n description STRING,\n frameworkAnnotations STRING[],\n PRIMARY KEY (id)\n)";
18
18
  export declare const INTERFACE_SCHEMA = "\nCREATE NODE TABLE Interface (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n isExported BOOLEAN,\n content STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
19
19
  export declare const METHOD_SCHEMA = "\nCREATE NODE TABLE Method (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n isExported BOOLEAN,\n content STRING,\n description STRING,\n parameterCount INT32,\n returnType STRING,\n PRIMARY KEY (id)\n)";
@@ -29,7 +29,7 @@ export declare const NAMESPACE_SCHEMA: string;
29
29
  export declare const TRAIT_SCHEMA: string;
30
30
  export declare const IMPL_SCHEMA: string;
31
31
  export declare const TYPE_ALIAS_SCHEMA: string;
32
- export declare const CONST_SCHEMA: string;
32
+ export declare const CONST_SCHEMA = "\nCREATE NODE TABLE `Const` (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n content STRING,\n description STRING,\n convexEndpointFactory STRING,\n PRIMARY KEY (id)\n)";
33
33
  export declare const STATIC_SCHEMA: string;
34
34
  export declare const VARIABLE_SCHEMA: string;
35
35
  export declare const PROPERTY_SCHEMA = "\nCREATE NODE TABLE `Property` (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n content STRING,\n description STRING,\n declaredType STRING,\n /*\n * DETAIL SYMBOL \u2014 true when this property is a member of a shape that has no\n * independent identity: the keys of an anonymous literal returned from a\n * function (R3-4).\n *\n * It exists because indexing those keys is right for the GRAPH and wrong for\n * TEXT SEARCH. They are ordinary words (message, value, timestamp) and\n * there are many of them, so letting them into the FTS result set pushes the\n * CALLABLES named after the same concept past the row cap the search applies\n * \u2014 measured: query('message') went from two processes to none on the\n * mini-repo fixture. A ranking tweak cannot fix that, because the rows never\n * come back from the FTS call in the first place.\n *\n * So the search layer gained a notion it did not have \u2014 a symbol that is\n * queryable, walkable and impact-analysable, but not a concept a text search\n * should surface on its own. buildFtsQueryCypher excludes these for the\n * Property table only; every other consumer sees them normally.\n */\n isDetail BOOLEAN,\n PRIMARY KEY (id)\n)";
@@ -44,6 +44,7 @@ CREATE NODE TABLE Function (
44
44
  isExported BOOLEAN,
45
45
  content STRING,
46
46
  description STRING,
47
+ convexEndpointFactory STRING,
47
48
  PRIMARY KEY (id)
48
49
  )`;
49
50
  export const CLASS_SCHEMA = `
@@ -152,7 +153,18 @@ export const NAMESPACE_SCHEMA = CODE_ELEMENT_BASE('Namespace');
152
153
  export const TRAIT_SCHEMA = CODE_ELEMENT_BASE('Trait');
153
154
  export const IMPL_SCHEMA = CODE_ELEMENT_BASE('Impl');
154
155
  export const TYPE_ALIAS_SCHEMA = CODE_ELEMENT_BASE('TypeAlias');
155
- export const CONST_SCHEMA = CODE_ELEMENT_BASE('Const');
156
+ export const CONST_SCHEMA = `
157
+ CREATE NODE TABLE \`Const\` (
158
+ id STRING,
159
+ name STRING,
160
+ filePath STRING,
161
+ startLine INT64,
162
+ endLine INT64,
163
+ content STRING,
164
+ description STRING,
165
+ convexEndpointFactory STRING,
166
+ PRIMARY KEY (id)
167
+ )`;
156
168
  export const STATIC_SCHEMA = CODE_ELEMENT_BASE('Static');
157
169
  export const VARIABLE_SCHEMA = CODE_ELEMENT_BASE('Variable');
158
170
  export const PROPERTY_SCHEMA = `
@@ -0,0 +1,8 @@
1
+ import { executeParameterized } from '../../core/lbug/pool-adapter.js';
2
+ export interface ConvexDispatchMetadata {
3
+ readonly factory?: string;
4
+ readonly boundary: string;
5
+ readonly staleIndex?: true;
6
+ readonly probeFailed?: true;
7
+ }
8
+ export declare function queryConvexDispatchMetadata(lbugPath: string, symbolId: string, symbolName: string, symbolType: string, runQuery?: typeof executeParameterized): Promise<ConvexDispatchMetadata | undefined>;
@@ -0,0 +1,45 @@
1
+ import { executeParameterized } from '../../core/lbug/pool-adapter.js';
2
+ import { logger } from '../../core/logger.js';
3
+ function isMissingConvexMetadataProperty(error) {
4
+ const message = error instanceof Error ? error.message : String(error ?? '');
5
+ return /cannot find property\s+convexEndpointFactory|property[^\n]*convexEndpointFactory[^\n]*not (?:defined|found)/i.test(message);
6
+ }
7
+ export async function queryConvexDispatchMetadata(lbugPath, symbolId, symbolName, symbolType, runQuery = executeParameterized) {
8
+ if (symbolType !== 'Const' && symbolType !== 'Function')
9
+ return undefined;
10
+ const nodeLabel = symbolType === 'Function' ? 'Function' : 'Const';
11
+ try {
12
+ const rows = await runQuery(lbugPath, `MATCH (n:${nodeLabel} {id: $symbolId})
13
+ RETURN n.convexEndpointFactory AS factory`, { symbolId });
14
+ const row = rows[0];
15
+ if (row === undefined)
16
+ return undefined;
17
+ const factory = String(row.factory ?? row[0] ?? '');
18
+ if (factory.length === 0)
19
+ return undefined;
20
+ return {
21
+ factory,
22
+ boundary: `${symbolName} is exported through Convex ${factory}({...}) and can be addressed through ` +
23
+ `the anyApi runtime proxy; callers across that dynamic-dispatch boundary leave no static ` +
24
+ `edge, so actual impact may be higher.`,
25
+ };
26
+ }
27
+ catch (error) {
28
+ if (isMissingConvexMetadataProperty(error)) {
29
+ return {
30
+ staleIndex: true,
31
+ boundary: 'Convex runtime-proxy metadata is unavailable because this index predates ' +
32
+ 'convexEndpointFactory; re-index before treating impact as exact.',
33
+ };
34
+ }
35
+ logger.warn({
36
+ context: 'impact:convex-metadata',
37
+ err: error instanceof Error ? error.message : String(error),
38
+ }, 'GitNexus Convex metadata probe failed (degraded)');
39
+ return {
40
+ probeFailed: true,
41
+ boundary: 'Convex runtime-proxy metadata could not be checked; impact remains a lower bound ' +
42
+ 'until the metadata probe succeeds.',
43
+ };
44
+ }
45
+ }
@@ -94,9 +94,8 @@ export interface EpistemicCauses {
94
94
  */
95
95
  readonly receiverTyping: number;
96
96
  /**
97
- * Symbols on the far side of a dispatch boundary that the traversal could not
98
- * attribute to the queried symbol: implementations plus interface-level
99
- * consumers, summed over the boundary nodes that were flagged.
97
+ * Symbols on or beyond a dispatch boundary that the traversal could not
98
+ * attribute statically: implementations plus interface-level consumers.
100
99
  *
101
100
  * Unit: SYMBOLS, not call sites — deliberately, because a call-site count is
102
101
  * not derivable on this side. The graph does not retain per-site multiplicity
@@ -106,6 +105,10 @@ export interface EpistemicCauses {
106
105
  * symbol reachable through two flagged boundary nodes is counted once per
107
106
  * node, so this is itself a lower bound.
108
107
  *
108
+ * Framework runtime-proxy metadata can prove that impact is incomplete but
109
+ * cannot provide this magnitude, so it contributes a boundary note while
110
+ * leaving this count unchanged.
111
+ *
109
112
  * It is still directly comparable in magnitude with `receiverTyping` — both
110
113
  * answer "how much is missing" — which `boundaries.length` was not.
111
114
  */
@@ -11,6 +11,7 @@ import { createHash } from 'crypto';
11
11
  import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
12
12
  import { queryClassBeanMetadata } from './bean-metadata.js';
13
13
  import { querySpringAopMetadata } from './aop-metadata.js';
14
+ import { queryConvexDispatchMetadata } from './convex-metadata.js';
14
15
  import { isValidQueryParams } from '../../core/lbug/query-params.js';
15
16
  import { toDisplayLine } from './line-display.js';
16
17
  import { LBUG_ID_PROBE_BATCH_SIZE, LBUG_QUERY_BATCH_SIZE } from '../../core/lbug/query-batch.js';
@@ -464,7 +465,7 @@ function epistemicFrom(dropped) {
464
465
  epistemic: 'exact',
465
466
  causes: {
466
467
  receiverTyping: 0,
467
- dispatchBoundary: 0,
468
+ dispatchBoundary: dropped.dispatch,
468
469
  externalBoundary: dropped.external,
469
470
  undecidedSatisfaction: 0,
470
471
  },
@@ -479,7 +480,7 @@ function epistemicFrom(dropped) {
479
480
  // would read a different magnitude than the human reading the text.
480
481
  causes: {
481
482
  receiverTyping: dropped.sites,
482
- dispatchBoundary: 0,
483
+ dispatchBoundary: dropped.dispatch,
483
484
  externalBoundary: dropped.external,
484
485
  undecidedSatisfaction: dropped.undecided,
485
486
  },
@@ -5524,7 +5525,7 @@ export class LocalBackend {
5524
5525
  this.crossLanguagePropertyCache.set(repo.lbugPath, { indexedAt: repo.indexedAt, byName });
5525
5526
  return byName;
5526
5527
  }
5527
- async computeEpistemicBoundary(repo, symId, symType, symName) {
5528
+ async computeEpistemicBoundary(repo, symId, symType, symName, direction) {
5528
5529
  const HERITAGE_TYPES = EPISTEMIC_HERITAGE_RELATION_TYPES;
5529
5530
  const CONSUMER_TYPES = EPISTEMIC_CONSUMER_RELATION_TYPES;
5530
5531
  // #2744 — call sites dropped for want of a receiver type. Checked BEFORE
@@ -5557,6 +5558,14 @@ export class LocalBackend {
5557
5558
  // the owning-type hop below would be a graph round-trip per method query in
5558
5559
  // every index that has no such record — which is every non-Go one, since Go
5559
5560
  // is the only language with a structural-satisfaction hook.
5561
+ const convexDispatchPromise = direction === 'downstream'
5562
+ ? Promise.resolve(undefined)
5563
+ : queryConvexDispatchMetadata(repo.lbugPath, symId, symName, symType);
5564
+ const interfaceRowsPromise = executeParameterized(repo.lbugPath, `MATCH (x)-[r:CodeRelation]->(iface)
5565
+ WHERE x.id = $symId AND r.type IN $heritage
5566
+ RETURN DISTINCT iface.id AS id, iface.name AS name, labels(iface)[0] AS label
5567
+ ORDER BY id
5568
+ LIMIT 25`, { symId, heritage: HERITAGE_TYPES }).catch(() => []);
5560
5569
  const undecidedSummary = meta?.undecidedInterfaceSatisfaction;
5561
5570
  const undecidedDrops = undecidedSummary === undefined
5562
5571
  ? { notes: [], undecided: 0 }
@@ -5566,10 +5575,19 @@ export class LocalBackend {
5566
5575
  ? await this.owningTypeNames(repo, symId)
5567
5576
  : []),
5568
5577
  ]);
5578
+ const convexDispatch = await convexDispatchPromise;
5569
5579
  const droppedBoundaries = {
5570
5580
  ...receiverDrops,
5571
- notes: [...receiverDrops.notes, ...undecidedDrops.notes],
5581
+ notes: [
5582
+ ...receiverDrops.notes,
5583
+ ...undecidedDrops.notes,
5584
+ ...(convexDispatch === undefined ? [] : [convexDispatch.boundary]),
5585
+ ],
5572
5586
  undecided: undecidedDrops.undecided,
5587
+ // Endpoint/probe evidence proves incompleteness but does not expose a
5588
+ // count of omitted symbols. Keep the magnitude at zero rather than
5589
+ // inventing one from the presence of a note.
5590
+ dispatch: 0,
5573
5591
  };
5574
5592
  try {
5575
5593
  // Discover the interface / abstract supertypes on the target's boundary.
@@ -5578,11 +5596,7 @@ export class LocalBackend {
5578
5596
  if (symType === 'Interface') {
5579
5597
  boundary.set(symId, { name: symName || '', label: 'Interface' });
5580
5598
  }
5581
- const ifaceRows = await executeParameterized(repo.lbugPath, `MATCH (x)-[r:CodeRelation]->(iface)
5582
- WHERE x.id = $symId AND r.type IN $heritage
5583
- RETURN DISTINCT iface.id AS id, iface.name AS name, labels(iface)[0] AS label
5584
- ORDER BY id
5585
- LIMIT 25`, { symId, heritage: HERITAGE_TYPES }).catch(() => []);
5599
+ const ifaceRows = await interfaceRowsPromise;
5586
5600
  for (const r of ifaceRows) {
5587
5601
  const id = (r.id ?? r[0]);
5588
5602
  if (id && !boundary.has(id)) {
@@ -5650,7 +5664,7 @@ export class LocalBackend {
5650
5664
  boundaries: [...droppedBoundaries.notes, ...boundaries],
5651
5665
  causes: {
5652
5666
  receiverTyping: droppedBoundaries.sites,
5653
- dispatchBoundary: dispatchBoundarySymbols,
5667
+ dispatchBoundary: droppedBoundaries.dispatch + dispatchBoundarySymbols,
5654
5668
  externalBoundary: droppedBoundaries.external,
5655
5669
  undecidedSatisfaction: droppedBoundaries.undecided,
5656
5670
  },
@@ -5713,7 +5727,7 @@ export class LocalBackend {
5713
5727
  // so every consumer would be reading a field the compiler says is absent.
5714
5728
  const epistemicPromise = opts.skipEpistemic
5715
5729
  ? Promise.resolve({})
5716
- : this.computeEpistemicBoundary(repo, symId, symType, (sym.name || sym[1]));
5730
+ : this.computeEpistemicBoundary(repo, symId, symType, (sym.name || sym[1]), direction);
5717
5731
  const beanMetadataPromise = opts.skipEpistemic || summaryOnly
5718
5732
  ? Promise.resolve(undefined)
5719
5733
  : queryClassBeanMetadata(repo.lbugPath, symId, symType);
package/dist/mcp/tools.js CHANGED
@@ -251,9 +251,10 @@ COMPLETENESS OF incoming: alongside symbol/incoming/outgoing the result carries
251
251
  - causes: { receiverTyping, dispatchBoundary, externalBoundary, undecidedSatisfaction } — machine-readable WHY. Every field counts MISSING THINGS, never sentences:
252
252
  - causes.receiverTyping (unit: call sites) > 0 — RESOLVER GAP: the analyzer dropped that many call sites on this name because it could not type the receiver, so they are missing from incoming. Do not read an absent caller as proof none exists.
253
253
  - causes.externalBoundary (unit: call sites) > 0 — the calls left the indexed program (System.out.println, fetch(...)). NOT a defect: no in-graph node could have been reached. An epistemic:'exact' result can carry this.
254
- - causes.dispatchBoundary (unit: symbols) > 0 — DI / interface dispatch: implementations plus interface-level consumers behind a boundary static analysis cannot cross. Irreducible.
254
+ - causes.dispatchBoundary (unit: symbols) > 0 — DI or interface dispatch: that many symbols sit on or beyond a boundary static analysis cannot cross. Irreducible. A symbol count, not a site count — per-site multiplicity is not retained for these edges — so compare its magnitude with receiverTyping, not its exact value. A framework runtime-proxy boundary can make epistemic lower-bound while this value remains 0 because endpoint metadata proves the gap but cannot count omitted symbols.
255
+ - causes.undecidedSatisfaction (unit: unjudged interface/type pairs) > 0 — the analyzer could not decide whether a type satisfies an interface, so no IMPLEMENTS edge exists and no dispatch boundary was left for the walk to notice. Usually fixable by making the missing dependency available to analysis.
255
256
 
256
- REQUIRES RE-INDEX: causes.receiverTyping and causes.externalBoundary come from index-time metadata only a current analyzer writes; against an older index they read as absent/0, which is indistinguishable from "nothing was dropped". Re-run \`gitnexus analyze\` before trusting a zero there.
257
+ REQUIRES RE-INDEX: causes.receiverTyping, causes.externalBoundary, causes.undecidedSatisfaction, and framework runtime-proxy boundary detection depend on index-time metadata that only a current analyzer writes. Against an older index the metadata can be absent, which is indistinguishable from "nothing was dropped" unless the schema probe detects the stale index — re-run \`gitnexus analyze\` before trusting a zero or an apparently exact result.
257
258
 
258
259
  GROUP MODE: set "repo" to "@<groupName>" to run context in each member repo (aggregated list), or "@<groupName>/<groupRepoPath>" for one member. If you use "@<groupName>" only, the member defaults to the lexicographically first key in group.yaml "repos".
259
260
 
@@ -440,11 +441,11 @@ Output includes:
440
441
  - causes: { receiverTyping, dispatchBoundary, externalBoundary, undecidedSatisfaction } — the machine-readable split of WHY, so an agent gating its own edits can tell a fixable analyzer gap from an irreducible one. Every field counts MISSING THINGS, never sentences:
441
442
  - causes.receiverTyping (unit: call sites) > 0 — the RESOLVER GAP signal: the analyzer dropped that many call sites because it could not establish the receiver's type (unresolved constructor, factory, chained expression). Those callers are absent from byDepth. Treat the result as incomplete: grep the symbol name before deleting or renaming.
442
443
  - causes.externalBoundary (unit: call sites) > 0 — those calls left the indexed program (System.out.println, fetch(...), os.environ.*). NOT a defect and NOT a reason the count is short: there is no in-graph node any edge could have reached. An epistemic:'exact' result can carry this.
443
- - causes.dispatchBoundary (unit: symbols) > 0 — DI / interface dispatch: that many implementations plus interface-level consumers sit on the far side of a boundary a static walk cannot cross. Irreducible; a compiler refuses here too. A symbol count, not a site count — per-site multiplicity is not retained for these edges — so compare its magnitude with receiverTyping, not its exact value.
444
+ - causes.dispatchBoundary (unit: symbols) > 0 — DI or interface dispatch: that many symbols sit on or beyond a boundary a static walk cannot cross. Irreducible. A symbol count, not a site count — per-site multiplicity is not retained for these edges — so compare its magnitude with receiverTyping, not its exact value. A framework runtime-proxy boundary can make epistemic lower-bound while this value remains 0 because endpoint metadata proves the gap but cannot count omitted symbols.
444
445
 
445
446
  - causes.undecidedSatisfaction (unit: unjudged interface/type pairs) > 0 — the analyzer could not DECIDE whether a type satisfies an interface (a type in a required signature named a package it could not resolve), so no IMPLEMENTS edge exists and no dispatch boundary was left for the walk to notice. Distinct from every cause above, which count decided facts that could not be attributed; this one counts questions never answered. It is the only cause that shortens a result WITHOUT leaving a trace in the graph, so an unhedged zero on a symbol reached only through such an interface would otherwise read as 'nobody calls this'. Usually fixable: it most often means a dependency is missing from the analyzed tree.
446
447
 
447
- REQUIRES RE-INDEX: causes.receiverTyping, causes.externalBoundary and causes.undecidedSatisfaction are read from index-time metadata that only a current analyzer writes. Against an older index they read as absent/0, which is indistinguishable from "nothing was dropped" — re-run \`gitnexus analyze\` before trusting a zero there.
448
+ REQUIRES RE-INDEX: causes.receiverTyping, causes.externalBoundary, causes.undecidedSatisfaction, and framework runtime-proxy boundary detection depend on index-time metadata that only a current analyzer writes. Against an older index the metadata can be absent, which is indistinguishable from "nothing was dropped" unless the schema probe detects the stale index — re-run \`gitnexus analyze\` before trusting a zero or an apparently exact result.
448
449
 
449
450
  Depth groups:
450
451
  - d=1: WILL BREAK (direct callers/importers)
@@ -564,7 +564,16 @@ import { fileURLToPath } from 'url';
564
564
  // IN-FLIGHT claim, not above origin/main. Every open PR touching gitnexus/ was
565
565
  // scanned; #3017 is the only other claimant.
566
566
  // RE-CHECK AGAINST origin/main AND OPEN PRs IMMEDIATELY BEFORE MERGING.
567
- const SCHEMA_BUMP = 72;
567
+ //
568
+ // 72 -> 74 adds import-proven Convex endpoint metadata to Const/Function worker
569
+ // output. A warm v72 cache has no convexEndpointFactory property, so the MCP
570
+ // impact probe would keep claiming exact results for unchanged endpoints. The
571
+ // parse-cache bump makes unchanged files re-parse; analyzer runner identity
572
+ // drift separately forces the graph re-emit (run-analyze.ts), and an id/schema
573
+ // migration needs both guarantees. Version 73 is intentionally skipped because
574
+ // concurrent PR #3046 (fixes #3041) claims it. Re-check main and open PRs
575
+ // immediately before merge.
576
+ const SCHEMA_BUMP = 74;
568
577
  const GITNEXUS_PKG_VERSION = (() => {
569
578
  try {
570
579
  // package.json sits at gitnexus/package.json — two levels up from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.219",
3
+ "version": "1.6.10-rc.220",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",