Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion gitnexus/src/core/graph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export type NodeLabel =
| 'Template'
| 'Section'
| 'Route' // API route endpoint (e.g., /api/grants)
| 'Tool'; // MCP tool definition
| 'Tool' // MCP tool definition
| 'Parameter'; // Function/method parameter (first-class for data flow tracking)


import { SupportedLanguages } from '../../config/supported-languages.js';
Expand Down Expand Up @@ -82,6 +83,9 @@ export type NodeProperties = {
errorKeys?: string[],
// Middleware wrapper chain (outermost first): ['withRateLimit', 'withCSRF', 'withAuth']
middleware?: string[],
// Parameter-specific properties
paramIndex?: number, // 0-indexed position in parameter list
isRest?: boolean, // ...args rest parameter
}

export type RelationshipType =
Expand All @@ -106,6 +110,8 @@ export type RelationshipType =
| 'ENTRY_POINT_OF' // Route/Tool → Process (this endpoint starts this execution flow)
| 'WRAPS' // Function → Function (middleware wrapper chain) — Reserved: future middleware graph traversal (not yet emitted)
| 'QUERIES' // File/Function → CodeElement (ORM query to model/table)
| 'PASSES_TO' // Call-site argument maps to callee parameter
| 'DATA_FLOWS_TO' // Variable assignment / data propagation within function

export interface GraphNode {
id: string,
Expand Down
99 changes: 99 additions & 0 deletions gitnexus/src/core/ingestion/parameter-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Parameter Processor
*
* Creates Parameter nodes from extracted parameter data and builds
* PASSES_TO edges by mapping call-site argument positions to callee parameters.
*
* This is Phase B of the security analysis path (architecture assessment).
*/

import { generateId } from '../../lib/utils.js';
import type { ExtractedParameter } from './workers/parse-worker.js';

export interface ParameterNode {
id: string;
name: string;
filePath: string;
paramIndex: number;
declaredType?: string;
isRest: boolean;
/** ID of the owning function/method */
ownerId: string;
}

export interface PassesToEdge {
id: string;
/** The CALLS edge source (caller function) */
callerId: string;
/** The Parameter node being passed to */
targetParamId: string;
/** Argument position at the call site */
sourceParamIndex: number;
/** Confidence (matches CALLS edge confidence) */
confidence: number;
}

/**
* Create Parameter graph nodes from extracted parameter data.
*/
export function createParameterNodes(params: ExtractedParameter[]): ParameterNode[] {
const nodes: ParameterNode[] = [];
const seenIds = new Set<string>();

for (const p of params) {
const id = generateId('Parameter', `${p.functionId}:${p.paramName}:${p.paramIndex}`);
if (seenIds.has(id)) continue;
seenIds.add(id);

nodes.push({
id,
name: p.paramName,
filePath: p.filePath,
paramIndex: p.paramIndex,
declaredType: p.declaredType,
isRest: p.isRest,
ownerId: p.functionId,
});
}

return nodes;
}

/**
* Build PASSES_TO edges by matching call-site argument positions
* to callee parameter positions.
*
* For each CALLS edge (caller -> callee), if the callee has Parameter nodes,
* create PASSES_TO edges from the caller to each callee parameter that
* receives an argument.
*/
export function buildPassesToEdges(
callEdges: Array<{ sourceId: string; targetId: string; argCount: number }>,
calleeParamMap: Map<string, ExtractedParameter[]>,
): PassesToEdge[] {
const edges: PassesToEdge[] = [];

for (const call of callEdges) {
const params = calleeParamMap.get(call.targetId);
if (!params || params.length === 0) continue;

const argCount = call.argCount || 0;
for (const param of params) {
// Only create edge if the call site provides this argument
// Rest params receive all remaining args, so always match if argCount > 0
if (param.paramIndex >= argCount && !param.isRest) continue;

const paramNodeId = generateId('Parameter', `${param.functionId}:${param.paramName}:${param.paramIndex}`);

edges.push({
id: generateId('PASSES_TO', `${call.sourceId}->${paramNodeId}`),
callerId: call.sourceId,
targetParamId: paramNodeId,
sourceParamIndex: param.paramIndex,
confidence: 0.9,
});
}
}

return edges;
}
9 changes: 6 additions & 3 deletions gitnexus/src/core/ingestion/parsing-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { buildTypeEnv } from './type-env.js';
import type { FieldInfo, FieldExtractorContext } from './field-types.js';
import type { LanguageProvider } from './language-provider.js';
import { WorkerPool } from './workers/worker-pool.js';
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileTypeEnvBindings, ExtractedORMQuery } from './workers/parse-worker.js';
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileConstructorBindings, FileTypeEnvBindings, ExtractedORMQuery, ExtractedParameter } from './workers/parse-worker.js';
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';

export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
Expand All @@ -30,6 +30,7 @@ export interface WorkerExtractedData {
ormQueries: ExtractedORMQuery[];
constructorBindings: FileConstructorBindings[];
typeEnvBindings: FileTypeEnvBindings[];
parameters: ExtractedParameter[];
}

// ============================================================================
Expand All @@ -51,7 +52,7 @@ const processParsingWithWorkers = async (
if (lang) parseableFiles.push({ path: file.path, content: file.content });
}

if (parseableFiles.length === 0) return { imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [] };
if (parseableFiles.length === 0) return { imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], parameters: [] };

const total = files.length;

Expand All @@ -75,6 +76,7 @@ const processParsingWithWorkers = async (
const allORMQueries: ExtractedORMQuery[] = [];
const allConstructorBindings: FileConstructorBindings[] = [];
const allTypeEnvBindings: FileTypeEnvBindings[] = [];
const allParameters: ExtractedParameter[] = [];
for (const result of chunkResults) {
for (const node of result.nodes) {
graph.addNode({
Expand Down Expand Up @@ -108,6 +110,7 @@ const processParsingWithWorkers = async (
allDecoratorRoutes.push(...result.decoratorRoutes);
allToolDefs.push(...result.toolDefs);
if (result.ormQueries) allORMQueries.push(...result.ormQueries);
if (result.parameters) allParameters.push(...result.parameters);
allConstructorBindings.push(...result.constructorBindings);
allTypeEnvBindings.push(...result.typeEnvBindings);
}
Expand All @@ -128,7 +131,7 @@ const processParsingWithWorkers = async (

// Final progress
onFileProgress?.(total, total, 'done');
return { imports: allImports, calls: allCalls, assignments: allAssignments, heritage: allHeritage, routes: allRoutes, fetchCalls: allFetchCalls, decoratorRoutes: allDecoratorRoutes, toolDefs: allToolDefs, ormQueries: allORMQueries, constructorBindings: allConstructorBindings, typeEnvBindings: allTypeEnvBindings };
return { imports: allImports, calls: allCalls, assignments: allAssignments, heritage: allHeritage, routes: allRoutes, fetchCalls: allFetchCalls, decoratorRoutes: allDecoratorRoutes, toolDefs: allToolDefs, ormQueries: allORMQueries, constructorBindings: allConstructorBindings, typeEnvBindings: allTypeEnvBindings, parameters: allParameters };
};

// ============================================================================
Expand Down
80 changes: 76 additions & 4 deletions gitnexus/src/core/ingestion/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { phpFileToRouteURL } from './route-extractors/php.js';
import { extractResponseShapes, extractPHPResponseShapes } from './route-extractors/response-shapes.js';
import { extractMiddlewareChain, extractNextjsMiddlewareConfig, compileMatcher, compiledMatcherMatchesRoute } from './route-extractors/middleware.js';
import { generateId } from '../../lib/utils.js';
import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef, ExtractedORMQuery } from './workers/parse-worker.js';
import type { ExtractedFetchCall, ExtractedRoute, ExtractedDecoratorRoute, ExtractedToolDef, ExtractedORMQuery, ExtractedParameter } from './workers/parse-worker.js';
import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
import { computeMRO } from './mro-processor.js';
import { processCommunities } from './community-processor.js';
Expand Down Expand Up @@ -544,6 +544,7 @@ async function runChunkedParseAndResolve(
allDecoratorRoutes: ExtractedDecoratorRoute[];
allToolDefs: ExtractedToolDef[];
allORMQueries: ExtractedORMQuery[];
allParameters: ExtractedParameter[];
}> {
const symbolTable = ctx.symbols;

Expand Down Expand Up @@ -664,7 +665,8 @@ async function runChunkedParseAndResolve(
const allDecoratorRoutes: ExtractedDecoratorRoute[] = [];
// Accumulate MCP/RPC tool definitions (@mcp.tool(), @app.tool(), etc.)
const allToolDefs: ExtractedToolDef[] = [];
const allORMQueries: ExtractedORMQuery[] = [];
const allORMQueries: ExtractedORMQuery[] = [];
const allParameters: ExtractedParameter[] = [];

try {
for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
Expand Down Expand Up @@ -796,6 +798,9 @@ async function runChunkedParseAndResolve(
if (chunkWorkerData.ormQueries?.length) {
allORMQueries.push(...chunkWorkerData.ormQueries);
}
if (chunkWorkerData.parameters?.length) {
allParameters.push(...chunkWorkerData.parameters);
}
} else {
await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
sequentialChunkPaths.push(chunkPaths);
Expand Down Expand Up @@ -891,7 +896,7 @@ async function runChunkedParseAndResolve(
importCtx.index = EMPTY_INDEX; // Release suffix index memory (~30MB for large repos)
importCtx.normalizedFileList = [];

return { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries };
return { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries, allParameters };
}

/**
Expand Down Expand Up @@ -1113,7 +1118,7 @@ export const runPipelineFromRepo = async (
const { scannedFiles, allPaths, totalFiles } = await runScanAndStructure(repoPath, graph, onProgress);

// Phase 3+4: Chunked parse + resolve (imports, calls, heritage, routes)
const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries } = await runChunkedParseAndResolve(
const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries, allParameters } = await runChunkedParseAndResolve(
graph, ctx, scannedFiles, allPaths, totalFiles, repoPath, pipelineStart, onProgress,
);

Expand Down Expand Up @@ -1378,6 +1383,73 @@ export const runPipelineFromRepo = async (
}
}

// ── Phase 3.6b: Parameter Nodes + PASSES_TO ──────────────────────
if (allParameters.length > 0) {
const { createParameterNodes, buildPassesToEdges } = await import('./parameter-processor.js');

// Create Parameter nodes
const paramNodes = createParameterNodes(allParameters);
for (const pn of paramNodes) {
graph.addNode({
id: pn.id,
label: 'Parameter',
properties: {
name: pn.name,
filePath: pn.filePath,
paramIndex: pn.paramIndex,
declaredType: pn.declaredType,
isRest: pn.isRest,
},
});

// DEFINES edge from function to parameter
graph.addRelationship({
id: generateId('DEFINES', `${pn.ownerId}->${pn.id}`),
sourceId: pn.ownerId,
targetId: pn.id,
type: 'DEFINES',
confidence: 1.0,
reason: 'parameter-definition',
});
}

// Build PASSES_TO edges from existing CALLS edges
const callEdgesWithArgs: Array<{ sourceId: string; targetId: string; argCount: number }> = [];
for (const rel of graph.iterRelationships()) {
if (rel.type === 'CALLS') {
callEdgesWithArgs.push({
sourceId: rel.sourceId,
targetId: rel.targetId,
argCount: (rel as any).argCount || 0,
});
}
}

// Group parameters by function ID
const paramsByFunction = new Map<string, ExtractedParameter[]>();
for (const p of allParameters) {
const existing = paramsByFunction.get(p.functionId) || [];
existing.push(p);
paramsByFunction.set(p.functionId, existing);
}

const passesToEdges = buildPassesToEdges(callEdgesWithArgs, paramsByFunction);
for (const edge of passesToEdges) {
graph.addRelationship({
id: edge.id,
sourceId: edge.callerId,
targetId: edge.targetParamId,
type: 'PASSES_TO',
confidence: edge.confidence,
reason: `arg-${edge.sourceParamIndex}`,
});
}

if (isDev) {
console.log(`📎 Parameters: ${paramNodes.length} parameter nodes, ${passesToEdges.length} PASSES_TO edges`);
}
}

// ── Phase 3.7: ORM Dataflow Detection (Prisma + Supabase) ──────────
if (allORMQueries.length > 0) {
processORMQueries(graph, allORMQueries, isDev);
Expand Down
Loading
Loading