mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fixed constructor to method relation not getting stored in kuzu issue
This commit is contained in:
parent
46330fa301
commit
53f17ddf17
20 changed files with 1754 additions and 110 deletions
239
.cursor/plans/enhance_523ca41c.plan.md
Normal file
239
.cursor/plans/enhance_523ca41c.plan.md
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
---
|
||||
name: Enhance
|
||||
overview: Restructure GitNexus LLM tools to leverage clusters and processes for better code understanding. Remove unused highlight tool, add new tools (explore, overview), enhance existing tools with cluster/process context, and improve impact analysis reliability.
|
||||
todos: []
|
||||
---
|
||||
|
||||
# Enhanced LLM Tools with Cluster and Process Integration
|
||||
|
||||
## Summary
|
||||
|
||||
Consolidate GitNexus from 6 tools to **7 focused tools** that leverage the pre-computed clusters (Communities) and processes for richer context. Remove the highlight tool, add `explore` and `overview` tools, and enhance `search` and `blastRadius` with cluster/process awareness.
|
||||
|
||||
## Final Tool Set
|
||||
|
||||
| Tool | Status | Purpose ||------|--------|---------|| `search` | Enhance | Hybrid search + group results by process/cluster || `grep` | Keep | Regex pattern search || `read` | Keep | Read file content || `explore` | **New** | Deep dive on one symbol, cluster, or process || `overview` | **New** | Codebase map (all clusters + all processes) || `impact` | Enhance | Rename from blastRadius, add process/cluster context, increase limits || `cypher` | Keep | Raw graph queries || `highlight` | **Remove** | No longer needed |
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph tools [LLM Tools Layer]
|
||||
search[search]
|
||||
grep[grep]
|
||||
read[read]
|
||||
explore[explore]
|
||||
overview[overview]
|
||||
impact[impact]
|
||||
cypher[cypher]
|
||||
end
|
||||
|
||||
subgraph graph [Knowledge Graph]
|
||||
nodes[Nodes: File, Function, Class...]
|
||||
communities[Community Nodes]
|
||||
processes[Process Nodes]
|
||||
edges[CodeRelation Edges]
|
||||
memberOf[MEMBER_OF Edges]
|
||||
stepIn[STEP_IN_PROCESS Edges]
|
||||
end
|
||||
|
||||
search --> edges
|
||||
search --> communities
|
||||
search --> processes
|
||||
explore --> communities
|
||||
explore --> processes
|
||||
explore --> memberOf
|
||||
explore --> stepIn
|
||||
overview --> communities
|
||||
overview --> processes
|
||||
impact --> edges
|
||||
impact --> communities
|
||||
impact --> processes
|
||||
cypher --> graph
|
||||
```
|
||||
|
||||
|
||||
|
||||
## File Changes
|
||||
|
||||
### 1. Remove Highlight Tool
|
||||
|
||||
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)
|
||||
|
||||
- Delete the `highlightTool` definition (lines ~395-414)
|
||||
- Remove `highlightTool` from the returned array (line ~862)
|
||||
- Remove highlight marker logic from `blastRadius` output (line ~814-816)
|
||||
|
||||
**File:** [gitnexus/src/core/llm/agent.ts](gitnexus/src/core/llm/agent.ts)
|
||||
|
||||
- Remove highlight references from system prompt (lines 70, 77)
|
||||
- Update tool list in prompt to reflect new tools
|
||||
|
||||
**File:** [gitnexus/src/core/llm/types.ts](gitnexus/src/core/llm/types.ts)
|
||||
|
||||
- Remove `'highlight'` from `AgentStreamChunk.type` union (line 180)
|
||||
- Remove `highlightNodeIds` property (line 187-188)
|
||||
|
||||
### 2. Add `explore` Tool
|
||||
|
||||
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)New tool that auto-detects target type and returns comprehensive context:
|
||||
|
||||
```typescript
|
||||
explore({
|
||||
target: string, // Name of symbol, cluster, or process
|
||||
type?: 'symbol' | 'cluster' | 'process' // Optional, auto-detected
|
||||
})
|
||||
```
|
||||
|
||||
**Functionality:**
|
||||
|
||||
- For symbols: Query node, get MEMBER_OF cluster, get STEP_IN_PROCESS processes, get 1-hop connections
|
||||
- For clusters: Query Community node, get members via MEMBER_OF, get processes that touch this cluster
|
||||
- For processes: Query Process node, get steps via STEP_IN_PROCESS with step order, get clusters touched
|
||||
|
||||
**Cypher queries needed:**
|
||||
|
||||
```cypher
|
||||
-- Symbol cluster membership
|
||||
MATCH (s {name: $name})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
RETURN c.label, c.description
|
||||
|
||||
-- Symbol process participation
|
||||
MATCH (s {name: $name})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
RETURN p.label, r.step, p.stepCount
|
||||
|
||||
-- Process steps in order
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: $processId})
|
||||
RETURN s.name, s.filePath, r.step
|
||||
ORDER BY r.step
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 3. Add `overview` Tool
|
||||
|
||||
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)New tool that returns codebase structure:
|
||||
|
||||
```typescript
|
||||
overview() // No parameters
|
||||
```
|
||||
|
||||
**Functionality:**
|
||||
|
||||
- Query all Community nodes with member counts
|
||||
- Query all Process nodes with step counts and types
|
||||
- Calculate cluster dependencies (cross-cluster CALLS)
|
||||
- Identify critical paths (most connected processes)
|
||||
|
||||
**Output format:**
|
||||
|
||||
```javascript
|
||||
CLUSTERS (N total):
|
||||
| Cluster | Symbols | Cohesion | Description |
|
||||
...
|
||||
|
||||
PROCESSES (N total):
|
||||
| Process | Steps | Type | Clusters |
|
||||
...
|
||||
|
||||
CRITICAL PATHS:
|
||||
- LoginFlow (45 edges)
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 4. Enhance `search` Tool
|
||||
|
||||
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)Modify existing search to group results by process:**Current:** Returns flat list with 1-hop connections**Enhanced:** Groups results by process, adds cluster context**Changes:**
|
||||
|
||||
- After hybrid search, query STEP_IN_PROCESS for each result
|
||||
- Group results by process ID
|
||||
- Sort processes by number of matching results (relevance)
|
||||
- Add cluster label for each result via MEMBER_OF query
|
||||
- Keep 1-hop connections as optional detail
|
||||
|
||||
**New parameter:**
|
||||
|
||||
```typescript
|
||||
search({
|
||||
query: string,
|
||||
groupByProcess?: boolean, // Default: true
|
||||
limit?: number
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 5. Enhance `impact` Tool (rename from blastRadius)
|
||||
|
||||
**File:** [gitnexus/src/core/llm/tools.ts](gitnexus/src/core/llm/tools.ts)**Rename:** `blastRadiusTool` to `impactTool`**Enhancements:**
|
||||
|
||||
1. Increase LIMIT clauses: 100 to 300 (depth 1), 100 to 200 (depth 2), 50 to 100 (depth 3)
|
||||
2. Add affected processes section (query STEP_IN_PROCESS for all affected symbols)
|
||||
3. Add affected clusters section (query MEMBER_OF for all affected symbols)
|
||||
4. Add risk assessment summary
|
||||
5. Surface confidence scores more prominently (group by confidence level)
|
||||
|
||||
**New output sections:**
|
||||
|
||||
```javascript
|
||||
AFFECTED PROCESSES:
|
||||
- LoginFlow - BROKEN at step 2
|
||||
- SignupFlow - BROKEN at step 1
|
||||
|
||||
AFFECTED CLUSTERS:
|
||||
- Authentication (direct)
|
||||
- API Routes (indirect)
|
||||
|
||||
RISK: CRITICAL
|
||||
- N direct callers
|
||||
- N processes affected
|
||||
- N clusters affected
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 6. Increase Process Detection Limits
|
||||
|
||||
**File:** [gitnexus/src/core/ingestion/process-processor.ts](gitnexus/src/core/ingestion/process-processor.ts)Change default config (lines 27-32):
|
||||
|
||||
```typescript
|
||||
const DEFAULT_CONFIG: ProcessDetectionConfig = {
|
||||
maxTraceDepth: 10, // Keep
|
||||
maxBranching: 4, // Was 3
|
||||
maxProcesses: 75, // Was 50
|
||||
minSteps: 2, // Keep
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 7. Update System Prompt
|
||||
|
||||
**File:** [gitnexus/src/core/llm/agent.ts](gitnexus/src/core/llm/agent.ts)Update BASE_SYSTEM_PROMPT to reflect new tools:
|
||||
|
||||
```javascript
|
||||
## TOOLS
|
||||
- **search** - Hybrid search. Results grouped by process with cluster context.
|
||||
- **grep** - Regex pattern search for exact strings.
|
||||
- **read** - Read file content.
|
||||
- **explore** - Deep dive on a symbol, cluster, or process. Shows membership, participation, connections.
|
||||
- **overview** - Codebase map showing all clusters and processes.
|
||||
- **impact** - Impact analysis. Shows affected processes, clusters, and risk level.
|
||||
- **cypher** - Raw Cypher queries against the graph.
|
||||
|
||||
## GRAPH SCHEMA
|
||||
Nodes: File, Folder, Function, Class, Interface, Method, Community, Process
|
||||
Relations: CodeRelation with type: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Remove highlight tool (cleanup)
|
||||
2. Increase process detection limits
|
||||
3. Add overview tool (simplest new tool)
|
||||
4. Add explore tool
|
||||
5. Enhance impact tool
|
||||
18
.sisyphus/drafts/gitnexus-brainstorming.md
Normal file
18
.sisyphus/drafts/gitnexus-brainstorming.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Draft: Gitnexus Brainstorming - Clustering & Process Maps
|
||||
|
||||
## Initial Context
|
||||
- Project: **GitnexusV2**
|
||||
- Structure:
|
||||
- `gitnexus/` (Likely the core application)
|
||||
- `gitnexus-mcp/` (Likely a Model Context Protocol server)
|
||||
- Goal: Make it accurate and usable for smaller/dumber models.
|
||||
- Current Focus: Implementing **Clustering** and **Process Maps**.
|
||||
|
||||
## Findings
|
||||
- **Clustering**: Found `gitnexus/src/core/ingestion/cluster-enricher.ts`.
|
||||
- **Process Maps**: No files matched `*process*map*` yet. Searching content next.
|
||||
|
||||
## Open Questions
|
||||
- How is "process map" defined in this context? (Graph, mermaid diagram, flowchart?)
|
||||
- What is the input for clustering? (Code chunks, files, commits?)
|
||||
- What is the intended output for "smaller models"? (Simplified context, summaries?)
|
||||
34
.sisyphus/drafts/noodlbox-comparison.md
Normal file
34
.sisyphus/drafts/noodlbox-comparison.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Draft: Gitnexus vs Noodlbox Strategy
|
||||
|
||||
## Objectives
|
||||
- Understand GitnexusV2 current state and goals.
|
||||
- Analyze Noodlbox capabilities from provided URL.
|
||||
- Compare features, architecture, and value proposition.
|
||||
- Provide strategic views and recommendations.
|
||||
|
||||
## Research Findings
|
||||
- [GitnexusV2]: Zero-server, browser-native (WASM), KuzuDB based. Graph + Vector hybrid search.
|
||||
- [Noodlbox]: CLI-first, heavy install. Has "Session Hooks" and "Search Hooks" via plugins/CLI.
|
||||
|
||||
## Comparison Points
|
||||
- **Core Philosophy**: Both bet on "Knowledge Graph + MCP" as the future. Noodlbox validates Gitnexus's direction.
|
||||
- **Architecture**:
|
||||
- *Noodlbox*: CLI/Binary based. Likely local server management.
|
||||
- *Gitnexus*: Zero-server, Browser-native (WASM). Lower friction, higher privacy.
|
||||
- **Features**:
|
||||
- *Communities/Processes*: Both have them. Noodlbox uses them for "context injection". Gitnexus uses them for "visual exploration + query".
|
||||
- *Impact Analysis*: Noodlbox has polished workflows (e.g., `detect_impact staged`). Gitnexus has the engine (`blastRadius`) but maybe not the specific workflow wrappers yet.
|
||||
- **UX/Integration**:
|
||||
- *Noodlbox*: "Hooks" (Session/Search) are a killer feature. Proactively injecting context into the agent's session.
|
||||
- *Gitnexus*: Powerful tools, but relies on agent *pulling* data?
|
||||
|
||||
## Strategic Views
|
||||
1. **Validation**: The market direction is confirmed. You are building the right thing.
|
||||
2. **differentiation**: Lean into "Zero-Setup / Browser-Native". Noodlbox requires `noodl init` and CLI handling. Gitnexus could just *be*.
|
||||
3. **Opportunity**: Steal the "Session/Search Hooks" pattern. Make the agent smarter *automatically* without the user asking "check impact".
|
||||
4. **Workflow Polish**: Noodlbox's `/detect_impact staged` is a great specific use case. Gitnexus should wrap `blastRadius` into similar concrete workflows.
|
||||
|
||||
## Technical Feasibility (Interception)
|
||||
- **Cursor**: Use `.cursorrules` to "shadow" default tools. Instruct agent to ALWAYS use `gitnexus_search` instead of `grep`.
|
||||
- **Claude Code**: Likely uses a private plugin API for `PreToolUse`. We can't match this exactly without an official plugin, but we can approximate it with strong prompt instructions in `AGENTS.md`.
|
||||
- **MCP Shadowing**: Define tools with names that conflict (e.g., `grep`)? No, unsafe. Better to use "Virtual Hooks" via system prompt instructions.
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Activity, Search, Database, Terminal, Eye, Loader2, CheckCircle, XCircle, Clock, FileText, Zap } from 'lucide-react';
|
||||
import { Activity, Search, Database, Terminal, Loader2, CheckCircle, XCircle, Clock, FileText, Zap, Map, Compass } from 'lucide-react';
|
||||
import { getMCPClient, type ActivityEvent } from '../core/mcp/mcp-client';
|
||||
|
||||
// Tool icons
|
||||
|
|
@ -16,8 +16,9 @@ const TOOL_ICONS: Record<string, typeof Search> = {
|
|||
cypher: Database,
|
||||
grep: Terminal,
|
||||
read: FileText,
|
||||
blastRadius: Activity,
|
||||
highlight: Eye,
|
||||
impact: Activity,
|
||||
overview: Map,
|
||||
explore: Compass,
|
||||
};
|
||||
|
||||
// Tool colors
|
||||
|
|
@ -27,8 +28,9 @@ const TOOL_COLORS: Record<string, string> = {
|
|||
cypher: 'text-purple-400',
|
||||
grep: 'text-green-400',
|
||||
read: 'text-blue-400',
|
||||
blastRadius: 'text-rose-400',
|
||||
highlight: 'text-teal-400',
|
||||
impact: 'text-rose-400',
|
||||
overview: 'text-indigo-400',
|
||||
explore: 'text-teal-400',
|
||||
};
|
||||
|
||||
export function ActivityFeed() {
|
||||
|
|
|
|||
|
|
@ -238,27 +238,74 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
const results = await runQuery(query);
|
||||
return results;
|
||||
}}
|
||||
onBlastRadius={async (nodeId, hops = 2) => {
|
||||
// Run blast radius query
|
||||
onImpact={async (nodeId: string, hops = 2) => {
|
||||
// Run impact analysis query
|
||||
const query = `
|
||||
MATCH (start)-[*1..${hops}]-(connected)
|
||||
WHERE start.id = '${nodeId}' OR start.name = '${nodeId}'
|
||||
RETURN DISTINCT connected.id AS id, connected.name AS name, labels(connected) AS labels
|
||||
`;
|
||||
const results = await runQuery(query);
|
||||
// Trigger ripple animation on blast radius results
|
||||
// Trigger ripple animation on impact results
|
||||
const nodeIds = results.map((r: any) => r.id).filter(Boolean);
|
||||
if (nodeIds.length > 0) {
|
||||
triggerNodeAnimation(nodeIds, 'ripple');
|
||||
}
|
||||
return results;
|
||||
}}
|
||||
onHighlight={(nodeIds) => {
|
||||
// Highlight nodes in the graph
|
||||
setHighlightedNodeIds(new Set(nodeIds));
|
||||
// Trigger glow animation on highlighted nodes
|
||||
if (nodeIds.length > 0) {
|
||||
triggerNodeAnimation(nodeIds, 'glow');
|
||||
onOverview={async () => {
|
||||
// Return codebase overview: clusters + processes
|
||||
const clustersQuery = `
|
||||
MATCH (c:Community)
|
||||
OPTIONAL MATCH (c)<-[:CodeRelation {type: 'MEMBER_OF'}]-(m)
|
||||
RETURN c.id AS id, c.label AS label, c.cohesion AS cohesion, c.description AS description, count(m) AS memberCount
|
||||
ORDER BY memberCount DESC
|
||||
LIMIT 50
|
||||
`;
|
||||
const processesQuery = `
|
||||
MATCH (p:Process)
|
||||
RETURN p.id AS id, p.label AS label, p.processType AS type, p.stepCount AS steps
|
||||
ORDER BY p.stepCount DESC
|
||||
LIMIT 50
|
||||
`;
|
||||
const [clusters, processes] = await Promise.all([
|
||||
runQuery(clustersQuery),
|
||||
runQuery(processesQuery),
|
||||
]);
|
||||
return { clusters, processes };
|
||||
}}
|
||||
onExplore={async (target: string, type?: 'symbol' | 'cluster' | 'process') => {
|
||||
// Explore a specific target
|
||||
if (type === 'cluster' || target.startsWith('comm_')) {
|
||||
const query = `
|
||||
MATCH (c:Community)
|
||||
WHERE c.id = '${target}' OR c.label CONTAINS '${target}'
|
||||
OPTIONAL MATCH (c)<-[:CodeRelation {type: 'MEMBER_OF'}]-(m)
|
||||
RETURN c.id AS id, c.label AS label, c.description AS description, collect(m.name)[0..10] AS members
|
||||
LIMIT 1
|
||||
`;
|
||||
return await runQuery(query);
|
||||
} else if (type === 'process' || target.startsWith('proc_')) {
|
||||
const query = `
|
||||
MATCH (p:Process)
|
||||
WHERE p.id = '${target}' OR p.label CONTAINS '${target}'
|
||||
OPTIONAL MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p)
|
||||
RETURN p.id AS id, p.label AS label, p.stepCount AS steps, collect({name: s.name, step: r.step})[0..20] AS trace
|
||||
LIMIT 1
|
||||
`;
|
||||
return await runQuery(query);
|
||||
} else {
|
||||
// Symbol exploration
|
||||
const query = `
|
||||
MATCH (n)
|
||||
WHERE n.name = '${target}' OR n.id ENDS WITH ':${target}'
|
||||
OPTIONAL MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
OPTIONAL MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, label(n) AS nodeType,
|
||||
c.label AS cluster, collect({process: p.label, step: r.step}) AS processes
|
||||
LIMIT 1
|
||||
`;
|
||||
return await runQuery(query);
|
||||
}
|
||||
}}
|
||||
getContext={async () => {
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|||
interface MCPToggleProps {
|
||||
onSearch?: (query: string, limit?: number) => Promise<any>;
|
||||
onCypher?: (query: string) => Promise<any>;
|
||||
onBlastRadius?: (nodeId: string, hops?: number) => Promise<any>;
|
||||
onHighlight?: (nodeIds: string[], color?: string) => void;
|
||||
onImpact?: (nodeId: string, hops?: number) => Promise<any>;
|
||||
onGrep?: (pattern: string, caseSensitive?: boolean, maxResults?: number) => Promise<any>;
|
||||
onRead?: (filePath: string, startLine?: number, endLine?: number) => Promise<any>;
|
||||
onOverview?: () => Promise<any>;
|
||||
onExplore?: (target: string, type?: 'symbol' | 'cluster' | 'process') => Promise<any>;
|
||||
showOnboardingTip?: boolean;
|
||||
getContext?: () => Promise<CodebaseContext | null>;
|
||||
}
|
||||
|
|
@ -37,10 +38,11 @@ const MCP_CONFIG = `{
|
|||
export function MCPToggle({
|
||||
onSearch,
|
||||
onCypher,
|
||||
onBlastRadius,
|
||||
onHighlight,
|
||||
onImpact,
|
||||
onGrep,
|
||||
onRead,
|
||||
onOverview,
|
||||
onExplore,
|
||||
showOnboardingTip = false,
|
||||
getContext,
|
||||
}: MCPToggleProps = {}) {
|
||||
|
|
@ -92,10 +94,11 @@ export function MCPToggle({
|
|||
// Register tool handlers
|
||||
if (onSearch) client.registerHandler('search', async (params) => onSearch(params.query, params.limit));
|
||||
if (onCypher) client.registerHandler('cypher', async (params) => onCypher(params.query));
|
||||
if (onBlastRadius) client.registerHandler('blastRadius', async (params) => onBlastRadius(params.nodeId, params.hops));
|
||||
if (onHighlight) client.registerHandler('highlight', async (params) => { onHighlight(params.nodeIds, params.color); return { highlighted: params.nodeIds.length }; });
|
||||
if (onImpact) client.registerHandler('impact', async (params) => onImpact(params.nodeId, params.hops));
|
||||
if (onGrep) client.registerHandler('grep', async (params) => onGrep(params.pattern, params.caseSensitive, params.maxResults));
|
||||
if (onRead) client.registerHandler('read', async (params) => onRead(params.filePath, params.startLine, params.endLine));
|
||||
if (onOverview) client.registerHandler('overview', async () => onOverview());
|
||||
if (onExplore) client.registerHandler('explore', async (params) => onExplore(params.target, params.type));
|
||||
if (getContext) client.registerHandler('context', async () => getContext());
|
||||
|
||||
setStatus('connected');
|
||||
|
|
@ -114,7 +117,7 @@ export function MCPToggle({
|
|||
} catch {
|
||||
setStatus('error');
|
||||
}
|
||||
}, [onSearch, onCypher, onBlastRadius, onHighlight, onGrep, onRead, getContext]);
|
||||
}, [onSearch, onCypher, onImpact, onGrep, onRead, onOverview, onExplore, getContext]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
const client = getMCPClient();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export type NodeLabel =
|
|||
| 'Import'
|
||||
| 'Type'
|
||||
| 'CodeElement'
|
||||
| 'Community';
|
||||
| 'Community'
|
||||
| 'Process';
|
||||
|
||||
|
||||
export type NodeProperties = {
|
||||
|
|
@ -31,6 +32,12 @@ export type NodeProperties = {
|
|||
keywords?: string[],
|
||||
description?: string,
|
||||
enrichedBy?: 'heuristic' | 'llm',
|
||||
// Process-specific properties
|
||||
processType?: 'intra_community' | 'cross_community',
|
||||
stepCount?: number,
|
||||
communities?: string[],
|
||||
entryPointId?: string,
|
||||
terminalId?: string,
|
||||
}
|
||||
|
||||
export type RelationshipType =
|
||||
|
|
@ -45,6 +52,7 @@ export type RelationshipType =
|
|||
| 'IMPLEMENTS'
|
||||
| 'EXTENDS'
|
||||
| 'MEMBER_OF'
|
||||
| 'STEP_IN_PROCESS'
|
||||
|
||||
export interface GraphNode {
|
||||
id: string,
|
||||
|
|
@ -61,6 +69,8 @@ export interface GraphRelationship {
|
|||
confidence: number,
|
||||
/** Resolution reason: 'import-resolved', 'same-file', 'fuzzy-global', or empty for non-CALLS */
|
||||
reason: string,
|
||||
/** Step number for STEP_IN_PROCESS relationships (1-indexed) */
|
||||
step?: number,
|
||||
}
|
||||
|
||||
export interface KnowledgeGraph {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const FUNCTION_NODE_TYPES = new Set([
|
|||
'local_function_statement',
|
||||
// Rust
|
||||
'function_item',
|
||||
'impl_item', // Methods inside impl blocks
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
@ -51,21 +52,46 @@ const findEnclosingFunction = (
|
|||
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
||||
// Found enclosing function - try to get its name
|
||||
let funcName: string | null = null;
|
||||
let label = 'Function';
|
||||
|
||||
// Different node types have different name locations
|
||||
if (current.type === 'function_declaration' ||
|
||||
current.type === 'function_definition' ||
|
||||
current.type === 'async_function_declaration' ||
|
||||
current.type === 'generator_function_declaration') {
|
||||
current.type === 'generator_function_declaration' ||
|
||||
current.type === 'function_item') { // Rust function
|
||||
// Named function: function foo() {}
|
||||
const nameNode = current.childForFieldName?.('name') ||
|
||||
current.children?.find((c: any) => c.type === 'identifier' || c.type === 'property_identifier');
|
||||
funcName = nameNode?.text;
|
||||
} else if (current.type === 'impl_item') {
|
||||
// Rust method inside impl block: wrapper around function_item or const_item
|
||||
// We need to look inside for the function_item
|
||||
const funcItem = current.children?.find((c: any) => c.type === 'function_item');
|
||||
if (funcItem) {
|
||||
const nameNode = funcItem.childForFieldName?.('name') ||
|
||||
funcItem.children?.find((c: any) => c.type === 'identifier');
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
}
|
||||
} else if (current.type === 'method_definition') {
|
||||
// Method: foo() {} inside class
|
||||
// Method: foo() {} inside class (JS/TS)
|
||||
const nameNode = current.childForFieldName?.('name') ||
|
||||
current.children?.find((c: any) => c.type === 'property_identifier');
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (current.type === 'method_declaration') {
|
||||
// Java method: public void foo() {}
|
||||
const nameNode = current.childForFieldName?.('name') ||
|
||||
current.children?.find((c: any) => c.type === 'identifier');
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (current.type === 'constructor_declaration') {
|
||||
// Java constructor: public ClassName() {}
|
||||
const nameNode = current.childForFieldName?.('name') ||
|
||||
current.children?.find((c: any) => c.type === 'identifier');
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method'; // Treat constructors as methods for process detection
|
||||
} else if (current.type === 'arrow_function' || current.type === 'function_expression') {
|
||||
// Arrow/expression: const foo = () => {} - check parent variable declarator
|
||||
const parent = current.parent;
|
||||
|
|
@ -78,12 +104,18 @@ const findEnclosingFunction = (
|
|||
|
||||
if (funcName) {
|
||||
// Look up the function in symbol table to get its node ID
|
||||
// Try exact match first
|
||||
const nodeId = symbolTable.lookupExact(filePath, funcName);
|
||||
if (nodeId) return nodeId;
|
||||
|
||||
// Fallback: generate ID based on name and file
|
||||
const fallbackLabel = current.type === 'method_definition' ? 'Method' : 'Function';
|
||||
return generateId(fallbackLabel, `${filePath}:${funcName}`);
|
||||
// Try construct ID manually if lookup fails (common for non-exported internal functions)
|
||||
// Format should match what parsing-processor generates: "Function:path/to/file:funcName"
|
||||
// Check if we already have a node with this ID in the symbol table to be safe
|
||||
const generatedId = generateId(label, `${filePath}:${funcName}`);
|
||||
|
||||
// Ideally we should verify this ID exists, but strictly speaking if we are inside it,
|
||||
// it SHOULD exist. Returning it is better than falling back to File.
|
||||
return generatedId;
|
||||
}
|
||||
|
||||
// Couldn't determine function name - try parent (might be nested)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { processImports, createImportMap } from './import-processor';
|
|||
import { processCalls } from './call-processor';
|
||||
import { processHeritage } from './heritage-processor';
|
||||
import { processCommunities, CommunityDetectionResult } from './community-processor';
|
||||
import { processProcesses, ProcessDetectionResult } from './process-processor';
|
||||
import { createSymbolTable } from './symbol-table';
|
||||
import { createASTCache } from './ast-cache';
|
||||
import { PipelineProgress, PipelineResult } from '../../types/pipeline';
|
||||
|
|
@ -210,12 +211,70 @@ export const runPipelineFromFiles = async (
|
|||
});
|
||||
});
|
||||
|
||||
// Phase 8: Process Detection (98-99%)
|
||||
onProgress({
|
||||
phase: 'processes',
|
||||
percent: 98,
|
||||
message: 'Detecting execution flows...',
|
||||
stats: { filesProcessed: files.length, totalFiles: files.length, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
|
||||
const processResult = await processProcesses(
|
||||
graph,
|
||||
communityResult.memberships,
|
||||
(message, progress) => {
|
||||
const processProgress = 98 + (progress * 0.01);
|
||||
onProgress({
|
||||
phase: 'processes',
|
||||
percent: Math.round(processProgress),
|
||||
message,
|
||||
stats: { filesProcessed: files.length, totalFiles: files.length, nodesCreated: graph.nodeCount },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Log process detection results
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
|
||||
}
|
||||
|
||||
// Add Process nodes to the graph
|
||||
processResult.processes.forEach(proc => {
|
||||
graph.addNode({
|
||||
id: proc.id,
|
||||
label: 'Process' as const,
|
||||
properties: {
|
||||
name: proc.label,
|
||||
filePath: '',
|
||||
heuristicLabel: proc.heuristicLabel,
|
||||
processType: proc.processType,
|
||||
stepCount: proc.stepCount,
|
||||
communities: proc.communities,
|
||||
entryPointId: proc.entryPointId,
|
||||
terminalId: proc.terminalId,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add STEP_IN_PROCESS relationships
|
||||
processResult.steps.forEach(step => {
|
||||
graph.addRelationship({
|
||||
id: `${step.nodeId}_step_${step.step}_${step.processId}`,
|
||||
type: 'STEP_IN_PROCESS',
|
||||
sourceId: step.nodeId,
|
||||
targetId: step.processId,
|
||||
confidence: 1.0,
|
||||
reason: 'trace-detection',
|
||||
step: step.step,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Phase 8: Complete (100%)
|
||||
// Phase 9: Complete (100%)
|
||||
onProgress({
|
||||
phase: 'complete',
|
||||
percent: 100,
|
||||
message: `Graph complete! ${communityResult.stats.totalCommunities} communities detected.`,
|
||||
message: `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`,
|
||||
stats: {
|
||||
filesProcessed: files.length,
|
||||
totalFiles: files.length,
|
||||
|
|
@ -226,7 +285,7 @@ export const runPipelineFromFiles = async (
|
|||
// Cleanup WASM memory before returning
|
||||
astCache.clear();
|
||||
|
||||
return { graph, fileContents, communityResult };
|
||||
return { graph, fileContents, communityResult, processResult };
|
||||
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
|
|
|
|||
393
gitnexus/src/core/ingestion/process-processor.ts
Normal file
393
gitnexus/src/core/ingestion/process-processor.ts
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
/**
|
||||
* Process Detection Processor
|
||||
*
|
||||
* Detects execution flows (Processes) in the code graph by:
|
||||
* 1. Finding entry points (functions with no internal callers)
|
||||
* 2. Tracing forward via CALLS edges (BFS)
|
||||
* 3. Grouping and deduplicating similar paths
|
||||
* 4. Labeling with heuristic names
|
||||
*
|
||||
* Processes help agents understand how features work through the codebase.
|
||||
*/
|
||||
|
||||
import { KnowledgeGraph, GraphNode, GraphRelationship, NodeLabel } from '../graph/types';
|
||||
import { CommunityMembership } from './community-processor';
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
export interface ProcessDetectionConfig {
|
||||
maxTraceDepth: number; // Maximum steps to trace (default: 10)
|
||||
maxBranching: number; // Max branches to follow per node (default: 3)
|
||||
maxProcesses: number; // Maximum processes to detect (default: 50)
|
||||
minSteps: number; // Minimum steps for a valid process (default: 2)
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: ProcessDetectionConfig = {
|
||||
maxTraceDepth: 10,
|
||||
maxBranching: 4,
|
||||
maxProcesses: 75,
|
||||
minSteps: 2,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface ProcessNode {
|
||||
id: string; // "proc_handleLogin_createSession"
|
||||
label: string; // "HandleLogin → CreateSession"
|
||||
heuristicLabel: string;
|
||||
processType: 'intra_community' | 'cross_community';
|
||||
stepCount: number;
|
||||
communities: string[]; // Community IDs touched
|
||||
entryPointId: string;
|
||||
terminalId: string;
|
||||
trace: string[]; // Ordered array of node IDs
|
||||
}
|
||||
|
||||
export interface ProcessStep {
|
||||
nodeId: string;
|
||||
processId: string;
|
||||
step: number; // 1-indexed position in trace
|
||||
}
|
||||
|
||||
export interface ProcessDetectionResult {
|
||||
processes: ProcessNode[];
|
||||
steps: ProcessStep[];
|
||||
stats: {
|
||||
totalProcesses: number;
|
||||
crossCommunityCount: number;
|
||||
avgStepCount: number;
|
||||
entryPointsFound: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN PROCESSOR
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Detect processes (execution flows) in the knowledge graph
|
||||
*
|
||||
* This runs AFTER community detection, using CALLS edges to trace flows.
|
||||
*/
|
||||
export const processProcesses = async (
|
||||
knowledgeGraph: KnowledgeGraph,
|
||||
memberships: CommunityMembership[],
|
||||
onProgress?: (message: string, progress: number) => void,
|
||||
config: Partial<ProcessDetectionConfig> = {}
|
||||
): Promise<ProcessDetectionResult> => {
|
||||
const cfg = { ...DEFAULT_CONFIG, ...config };
|
||||
|
||||
onProgress?.('Finding entry points...', 0);
|
||||
|
||||
// Build lookup maps
|
||||
const membershipMap = new Map<string, string>();
|
||||
memberships.forEach(m => membershipMap.set(m.nodeId, m.communityId));
|
||||
|
||||
const callsEdges = buildCallsGraph(knowledgeGraph);
|
||||
const reverseCallsEdges = buildReverseCallsGraph(knowledgeGraph);
|
||||
const nodeMap = new Map<string, GraphNode>();
|
||||
knowledgeGraph.nodes.forEach(n => nodeMap.set(n.id, n));
|
||||
|
||||
// Step 1: Find entry points (functions that call others but have few callers)
|
||||
const entryPoints = findEntryPoints(knowledgeGraph, reverseCallsEdges, callsEdges);
|
||||
|
||||
onProgress?.(`Found ${entryPoints.length} entry points, tracing flows...`, 20);
|
||||
|
||||
onProgress?.(`Found ${entryPoints.length} entry points, tracing flows...`, 20);
|
||||
|
||||
// Step 2: Trace processes from each entry point
|
||||
const allTraces: string[][] = [];
|
||||
|
||||
for (let i = 0; i < entryPoints.length && allTraces.length < cfg.maxProcesses * 2; i++) {
|
||||
const entryId = entryPoints[i];
|
||||
const traces = traceFromEntryPoint(entryId, callsEdges, cfg);
|
||||
|
||||
// Filter out traces that are too short
|
||||
traces.filter(t => t.length >= cfg.minSteps).forEach(t => allTraces.push(t));
|
||||
|
||||
if (i % 10 === 0) {
|
||||
onProgress?.(`Tracing entry point ${i + 1}/${entryPoints.length}...`, 20 + (i / entryPoints.length) * 40);
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(`Found ${allTraces.length} traces, deduplicating...`, 60);
|
||||
|
||||
// Step 3: Deduplicate similar traces
|
||||
const uniqueTraces = deduplicateTraces(allTraces);
|
||||
|
||||
// Step 4: Limit to max processes (prioritize longer traces)
|
||||
const limitedTraces = uniqueTraces
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.slice(0, cfg.maxProcesses);
|
||||
|
||||
onProgress?.(`Creating ${limitedTraces.length} process nodes...`, 80);
|
||||
|
||||
// Step 5: Create process nodes
|
||||
const processes: ProcessNode[] = [];
|
||||
const steps: ProcessStep[] = [];
|
||||
|
||||
limitedTraces.forEach((trace, idx) => {
|
||||
const entryPointId = trace[0];
|
||||
const terminalId = trace[trace.length - 1];
|
||||
|
||||
// Get communities touched
|
||||
const communitiesSet = new Set<string>();
|
||||
trace.forEach(nodeId => {
|
||||
const comm = membershipMap.get(nodeId);
|
||||
if (comm) communitiesSet.add(comm);
|
||||
});
|
||||
const communities = Array.from(communitiesSet);
|
||||
|
||||
// Determine process type
|
||||
const processType: 'intra_community' | 'cross_community' =
|
||||
communities.length > 1 ? 'cross_community' : 'intra_community';
|
||||
|
||||
// Generate label
|
||||
const entryNode = nodeMap.get(entryPointId);
|
||||
const terminalNode = nodeMap.get(terminalId);
|
||||
const entryName = entryNode?.properties.name || 'Unknown';
|
||||
const terminalName = terminalNode?.properties.name || 'Unknown';
|
||||
const heuristicLabel = `${capitalize(entryName)} → ${capitalize(terminalName)}`;
|
||||
|
||||
const processId = `proc_${idx}_${sanitizeId(entryName)}`;
|
||||
|
||||
processes.push({
|
||||
id: processId,
|
||||
label: heuristicLabel,
|
||||
heuristicLabel,
|
||||
processType,
|
||||
stepCount: trace.length,
|
||||
communities,
|
||||
entryPointId,
|
||||
terminalId,
|
||||
trace,
|
||||
});
|
||||
|
||||
// Create step relationships
|
||||
trace.forEach((nodeId, stepIdx) => {
|
||||
steps.push({
|
||||
nodeId,
|
||||
processId,
|
||||
step: stepIdx + 1, // 1-indexed
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onProgress?.('Process detection complete!', 100);
|
||||
|
||||
// Calculate stats
|
||||
const crossCommunityCount = processes.filter(p => p.processType === 'cross_community').length;
|
||||
const avgStepCount = processes.length > 0
|
||||
? processes.reduce((sum, p) => sum + p.stepCount, 0) / processes.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
processes,
|
||||
steps,
|
||||
stats: {
|
||||
totalProcesses: processes.length,
|
||||
crossCommunityCount,
|
||||
avgStepCount: Math.round(avgStepCount * 10) / 10,
|
||||
entryPointsFound: entryPoints.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER: Build CALLS adjacency list
|
||||
// ============================================================================
|
||||
|
||||
type AdjacencyList = Map<string, string[]>;
|
||||
|
||||
const buildCallsGraph = (graph: KnowledgeGraph): AdjacencyList => {
|
||||
const adj = new Map<string, string[]>();
|
||||
|
||||
graph.relationships.forEach(rel => {
|
||||
if (rel.type === 'CALLS') {
|
||||
if (!adj.has(rel.sourceId)) {
|
||||
adj.set(rel.sourceId, []);
|
||||
}
|
||||
adj.get(rel.sourceId)!.push(rel.targetId);
|
||||
}
|
||||
});
|
||||
|
||||
return adj;
|
||||
};
|
||||
|
||||
const buildReverseCallsGraph = (graph: KnowledgeGraph): AdjacencyList => {
|
||||
const adj = new Map<string, string[]>();
|
||||
|
||||
graph.relationships.forEach(rel => {
|
||||
if (rel.type === 'CALLS') {
|
||||
if (!adj.has(rel.targetId)) {
|
||||
adj.set(rel.targetId, []);
|
||||
}
|
||||
adj.get(rel.targetId)!.push(rel.sourceId);
|
||||
}
|
||||
});
|
||||
|
||||
return adj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find functions/methods that are good entry points for tracing.
|
||||
*
|
||||
* An entry point is a function that:
|
||||
* 1. Has outgoing CALLS (so we can trace forward)
|
||||
* 2. Ranked by having high outgoing/incoming call ratio
|
||||
*
|
||||
* We prioritize functions that call many others but are called by few.
|
||||
*/
|
||||
const findEntryPoints = (
|
||||
graph: KnowledgeGraph,
|
||||
reverseCallsEdges: AdjacencyList,
|
||||
callsEdges: AdjacencyList
|
||||
): string[] => {
|
||||
const symbolTypes = new Set<NodeLabel>(['Function', 'Method']);
|
||||
const entryPointCandidates: { id: string; score: number; callers: number; callees: number }[] = [];
|
||||
|
||||
graph.nodes.forEach(node => {
|
||||
if (symbolTypes.has(node.label)) {
|
||||
const callers = reverseCallsEdges.get(node.id) || [];
|
||||
const callees = callsEdges.get(node.id) || [];
|
||||
|
||||
// Must have at least 1 outgoing call to trace forward
|
||||
if (callees.length > 0) {
|
||||
// Score: ratio of outgoing to incoming calls
|
||||
// Higher ratio = better entry point (calls many, called by few)
|
||||
// Add 1 to denominators to avoid division by zero
|
||||
const score = callees.length / (callers.length + 1);
|
||||
|
||||
entryPointCandidates.push({
|
||||
id: node.id,
|
||||
score,
|
||||
callers: callers.length,
|
||||
callees: callees.length
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by score descending and return top candidates
|
||||
const sorted = entryPointCandidates.sort((a, b) => b.score - a.score);
|
||||
|
||||
// DEBUG: Log top candidates
|
||||
if (sorted.length > 0) {
|
||||
console.log(`[Process Debug] Top 5 entry point candidates:`);
|
||||
sorted.slice(0, 5).forEach((c, i) => {
|
||||
const node = graph.nodes.find(n => n.id === c.id);
|
||||
console.log(` ${i+1}. ${node?.properties.name} - calls: ${c.callees}, callers: ${c.callers}, score: ${c.score.toFixed(2)}`);
|
||||
});
|
||||
}
|
||||
|
||||
return sorted
|
||||
.slice(0, 200) // Limit to prevent explosion
|
||||
.map(c => c.id);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER: Trace from entry point (BFS)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Trace forward from an entry point using BFS.
|
||||
* Returns all distinct paths up to maxDepth.
|
||||
*/
|
||||
const traceFromEntryPoint = (
|
||||
entryId: string,
|
||||
callsEdges: AdjacencyList,
|
||||
config: ProcessDetectionConfig
|
||||
): string[][] => {
|
||||
const traces: string[][] = [];
|
||||
|
||||
// BFS with path tracking
|
||||
// Each queue item: [currentNodeId, pathSoFar]
|
||||
const queue: [string, string[]][] = [[entryId, [entryId]]];
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0 && traces.length < config.maxBranching * 3) {
|
||||
const [currentId, path] = queue.shift()!;
|
||||
|
||||
// Get outgoing calls
|
||||
const callees = callsEdges.get(currentId) || [];
|
||||
|
||||
if (callees.length === 0) {
|
||||
// Terminal node - this is a complete trace
|
||||
if (path.length >= config.minSteps) {
|
||||
traces.push([...path]);
|
||||
}
|
||||
} else if (path.length >= config.maxTraceDepth) {
|
||||
// Max depth reached - save what we have
|
||||
if (path.length >= config.minSteps) {
|
||||
traces.push([...path]);
|
||||
}
|
||||
} else {
|
||||
// Continue tracing - limit branching
|
||||
const limitedCallees = callees.slice(0, config.maxBranching);
|
||||
let addedBranch = false;
|
||||
|
||||
for (const calleeId of limitedCallees) {
|
||||
// Avoid cycles
|
||||
if (!path.includes(calleeId)) {
|
||||
queue.push([calleeId, [...path, calleeId]]);
|
||||
addedBranch = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If all branches were cycles, save current path as terminal
|
||||
if (!addedBranch && path.length >= config.minSteps) {
|
||||
traces.push([...path]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return traces;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER: Deduplicate traces
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Merge traces that are subsets of other traces.
|
||||
* Keep longer traces, remove redundant shorter ones.
|
||||
*/
|
||||
const deduplicateTraces = (traces: string[][]): string[][] => {
|
||||
if (traces.length === 0) return [];
|
||||
|
||||
// Sort by length descending
|
||||
const sorted = [...traces].sort((a, b) => b.length - a.length);
|
||||
const unique: string[][] = [];
|
||||
|
||||
for (const trace of sorted) {
|
||||
// Check if this trace is a subset of any already-added trace
|
||||
const traceKey = trace.join('->');
|
||||
const isSubset = unique.some(existing => {
|
||||
const existingKey = existing.join('->');
|
||||
return existingKey.includes(traceKey);
|
||||
});
|
||||
|
||||
if (!isSubset) {
|
||||
unique.push(trace);
|
||||
}
|
||||
}
|
||||
|
||||
return unique;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER: String utilities
|
||||
// ============================================================================
|
||||
|
||||
const capitalize = (s: string): string => {
|
||||
if (!s) return s;
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
};
|
||||
|
||||
const sanitizeId = (s: string): string => {
|
||||
return s.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 20).toLowerCase();
|
||||
};
|
||||
|
|
@ -226,9 +226,35 @@ const generateCommunityCSV = (nodes: GraphNode[]): string => {
|
|||
return rows.join('\n');
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RELATIONSHIP CSV GENERATOR (Single Table)
|
||||
// ============================================================================
|
||||
/**
|
||||
* Generate CSV for Process nodes
|
||||
* Headers: id,label,heuristicLabel,processType,stepCount,communities,entryPointId,terminalId
|
||||
*/
|
||||
const generateProcessCSV = (nodes: GraphNode[]): string => {
|
||||
const headers = ['id', 'label', 'heuristicLabel', 'processType', 'stepCount', 'communities', 'entryPointId', 'terminalId'];
|
||||
const rows: string[] = [headers.join(',')];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.label !== 'Process') continue;
|
||||
|
||||
// Handle communities array (string[])
|
||||
const communities = (node.properties as any).communities || [];
|
||||
const communitiesStr = `[${communities.map((c: string) => `'${c.replace(/'/g, "''")}'`).join(',')}]`;
|
||||
|
||||
rows.push([
|
||||
escapeCSVField(node.id),
|
||||
escapeCSVField(node.properties.name || ''), // label stores name
|
||||
escapeCSVField((node.properties as any).heuristicLabel || ''),
|
||||
escapeCSVField((node.properties as any).processType || ''),
|
||||
escapeCSVNumber((node.properties as any).stepCount, 0),
|
||||
escapeCSVField(communitiesStr), // Needs CSV escaping because it contains commas!
|
||||
escapeCSVField((node.properties as any).entryPointId || ''),
|
||||
escapeCSVField((node.properties as any).terminalId || ''),
|
||||
].join(','));
|
||||
}
|
||||
|
||||
return rows.join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate CSV for the single CodeRelation table
|
||||
|
|
@ -238,7 +264,7 @@ const generateCommunityCSV = (nodes: GraphNode[]): string => {
|
|||
* reason: 'import-resolved' | 'same-file' | 'fuzzy-global' (or empty for non-CALLS)
|
||||
*/
|
||||
const generateRelationCSV = (graph: KnowledgeGraph): string => {
|
||||
const headers = ['from', 'to', 'type', 'confidence', 'reason'];
|
||||
const headers = ['from', 'to', 'type', 'confidence', 'reason', 'step'];
|
||||
const rows: string[] = [headers.join(',')];
|
||||
|
||||
for (const rel of graph.relationships) {
|
||||
|
|
@ -248,6 +274,7 @@ const generateRelationCSV = (graph: KnowledgeGraph): string => {
|
|||
escapeCSVField(rel.type),
|
||||
escapeCSVNumber(rel.confidence, 1.0),
|
||||
escapeCSVField(rel.reason),
|
||||
escapeCSVNumber((rel as any).step, 0),
|
||||
].join(','));
|
||||
}
|
||||
|
||||
|
|
@ -278,6 +305,7 @@ export const generateAllCSVs = (
|
|||
nodeCSVs.set('Method', generateCodeElementCSV(nodes, 'Method', fileContents));
|
||||
nodeCSVs.set('CodeElement', generateCodeElementCSV(nodes, 'CodeElement', fileContents));
|
||||
nodeCSVs.set('Community', generateCommunityCSV(nodes));
|
||||
nodeCSVs.set('Process', generateProcessCSV(nodes));
|
||||
|
||||
// Generate single relation CSV
|
||||
const relCSV = generateRelationCSV(graph);
|
||||
|
|
|
|||
|
|
@ -120,12 +120,15 @@ export const loadGraphToKuzu = async (
|
|||
for (const line of relLines) {
|
||||
try {
|
||||
// Parse CSV - handle quoted fields and numeric confidence
|
||||
// Format: "from","to","type",confidence,"reason"
|
||||
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)"/);
|
||||
// Parse CSV - handle quoted fields and numeric confidence
|
||||
// Format: "from","to","type",confidence,"reason",step
|
||||
// Note: step is unquoted numeric
|
||||
const match = line.match(/"([^"]*)","([^"]*)","([^"]*)",([0-9.]+),"([^"]*)",([0-9-]+)/);
|
||||
if (!match) continue;
|
||||
|
||||
const [, fromId, toId, relType, confidenceStr, reason] = match;
|
||||
const [, fromId, toId, relType, confidenceStr, reason, stepStr] = match;
|
||||
const confidence = parseFloat(confidenceStr) || 1.0;
|
||||
const step = parseInt(stepStr) || 0;
|
||||
|
||||
// Extract labels from node IDs
|
||||
// Community nodes have IDs like "comm_14" (no colon)
|
||||
|
|
@ -134,6 +137,9 @@ export const loadGraphToKuzu = async (
|
|||
if (nodeId.startsWith('comm_')) {
|
||||
return 'Community';
|
||||
}
|
||||
if (nodeId.startsWith('proc_')) {
|
||||
return 'Process';
|
||||
}
|
||||
return nodeId.split(':')[0];
|
||||
};
|
||||
|
||||
|
|
@ -150,7 +156,7 @@ export const loadGraphToKuzu = async (
|
|||
const insertQuery = `
|
||||
MATCH (a:${fromLabel} {id: '${fromId.replace(/'/g, "''")}'}),
|
||||
(b:${toLabel} {id: '${toId.replace(/'/g, "''")}'})
|
||||
CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}', confidence: ${confidence}, reason: '${reason.replace(/'/g, "''")}'}]->(b)
|
||||
CREATE (a)-[:${REL_TABLE_NAME} {type: '${relType}', confidence: ${confidence}, reason: '${reason.replace(/'/g, "''")}', step: ${step}}]->(b)
|
||||
`;
|
||||
await conn.query(insertQuery);
|
||||
insertedRels++;
|
||||
|
|
@ -162,6 +168,7 @@ export const loadGraphToKuzu = async (
|
|||
const [, fromId, toId, relType] = match;
|
||||
const getNodeLabel = (nodeId: string): string => {
|
||||
if (nodeId.startsWith('comm_')) return 'Community';
|
||||
if (nodeId.startsWith('proc_')) return 'Process';
|
||||
return nodeId.split(':')[0];
|
||||
};
|
||||
const fromLabel = getNodeLabel(fromId);
|
||||
|
|
@ -229,6 +236,9 @@ const getCopyQuery = (table: NodeTableName, path: string): string => {
|
|||
if (table === 'Community') {
|
||||
return `COPY Community(id, label, heuristicLabel, keywords, description, enrichedBy, cohesion, symbolCount) FROM "${path}" (HEADER=true, PARALLEL=false)`;
|
||||
}
|
||||
if (table === 'Process') {
|
||||
return `COPY Process(id, label, heuristicLabel, processType, stepCount, communities, entryPointId, terminalId) FROM "${path}" (HEADER=true, PARALLEL=false)`;
|
||||
}
|
||||
// All code element tables: Function, Class, Interface, Method, CodeElement
|
||||
return `COPY ${table}(id, name, filePath, startLine, endLine, content) FROM "${path}" (HEADER=true, PARALLEL=false)`;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
// NODE TABLE NAMES
|
||||
// ============================================================================
|
||||
export const NODE_TABLES = [
|
||||
'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community',
|
||||
'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community', 'Process',
|
||||
// Multi-language support
|
||||
'Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
|
||||
'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module'
|
||||
|
|
@ -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', 'MEMBER_OF'] as const;
|
||||
export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'MEMBER_OF', 'STEP_IN_PROCESS'] as const;
|
||||
export type RelType = typeof REL_TYPES[number];
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -127,6 +127,23 @@ CREATE NODE TABLE Community (
|
|||
PRIMARY KEY (id)
|
||||
)`;
|
||||
|
||||
// ============================================================================
|
||||
// PROCESS NODE TABLE (for execution flow detection)
|
||||
// ============================================================================
|
||||
|
||||
export const PROCESS_SCHEMA = `
|
||||
CREATE NODE TABLE Process (
|
||||
id STRING,
|
||||
label STRING,
|
||||
heuristicLabel STRING,
|
||||
processType STRING,
|
||||
stepCount INT32,
|
||||
communities STRING[],
|
||||
entryPointId STRING,
|
||||
terminalId STRING,
|
||||
PRIMARY KEY (id)
|
||||
)`;
|
||||
|
||||
// ============================================================================
|
||||
// MULTI-LANGUAGE NODE TABLE SCHEMAS
|
||||
// ============================================================================
|
||||
|
|
@ -206,6 +223,8 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
|
|||
FROM Function TO \`Enum\`,
|
||||
FROM Function TO Namespace,
|
||||
FROM Function TO TypeAlias,
|
||||
FROM Function TO \`Module\`,
|
||||
FROM Function TO Impl,
|
||||
FROM Class TO Method,
|
||||
FROM Class TO Function,
|
||||
FROM Class TO Class,
|
||||
|
|
@ -223,6 +242,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
|
|||
FROM Template TO Method,
|
||||
FROM Template TO Class,
|
||||
FROM Template TO \`Struct\`,
|
||||
FROM \`Module\` TO \`Module\`,
|
||||
FROM CodeElement TO Community,
|
||||
FROM Interface TO Community,
|
||||
FROM \`Struct\` TO Community,
|
||||
|
|
@ -231,6 +251,10 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
|
|||
FROM \`Struct\` TO Method,
|
||||
FROM \`Enum\` TO Community,
|
||||
FROM \`Macro\` TO Community,
|
||||
FROM \`Macro\` TO Function,
|
||||
FROM \`Macro\` TO Method,
|
||||
FROM \`Module\` TO Function,
|
||||
FROM \`Module\` TO Method,
|
||||
FROM Typedef TO Community,
|
||||
FROM \`Union\` TO Community,
|
||||
FROM Namespace TO Community,
|
||||
|
|
@ -247,11 +271,28 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
|
|||
FROM Constructor TO Community,
|
||||
FROM Constructor TO Interface,
|
||||
FROM Constructor TO Class,
|
||||
FROM Constructor TO Method,
|
||||
FROM Constructor TO Function,
|
||||
FROM Constructor TO Constructor,
|
||||
FROM Constructor TO \`Struct\`,
|
||||
FROM Constructor TO \`Macro\`,
|
||||
FROM Constructor TO Template,
|
||||
FROM Template TO Community,
|
||||
FROM \`Module\` TO Community,
|
||||
FROM Function TO Process,
|
||||
FROM Method TO Process,
|
||||
FROM Class TO Process,
|
||||
FROM Interface TO Process,
|
||||
FROM \`Struct\` TO Process,
|
||||
FROM Constructor TO Process,
|
||||
FROM \`Module\` TO Process,
|
||||
FROM \`Macro\` TO Process,
|
||||
FROM Impl TO Process,
|
||||
FROM Typedef TO Process,
|
||||
type STRING,
|
||||
confidence DOUBLE,
|
||||
reason STRING
|
||||
reason STRING,
|
||||
step INT32
|
||||
)`;
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -288,6 +329,7 @@ export const NODE_SCHEMA_QUERIES = [
|
|||
METHOD_SCHEMA,
|
||||
CODE_ELEMENT_SCHEMA,
|
||||
COMMUNITY_SCHEMA,
|
||||
PROCESS_SCHEMA,
|
||||
// Multi-language support
|
||||
STRUCT_SCHEMA,
|
||||
ENUM_SCHEMA,
|
||||
|
|
|
|||
|
|
@ -67,26 +67,26 @@ You are an investigator. For each question:
|
|||
3. **Trace** → Use cypher to follow connections in the graph
|
||||
4. **Cite** → Ground every finding with [[file:line]] or [[Type:Name]]
|
||||
5. **Validate** → Use cypher to validate the results and confirm completeness of context before final output. ( MUST DO )
|
||||
6. **Highlight** → Visualize key nodes with highlight
|
||||
|
||||
## 🛠️ TOOLS
|
||||
- **\`search\`** — Hybrid search (keyword + semantic). Returns code matches with graph connections.
|
||||
- **\`search\`** — Hybrid search. Results grouped by process with cluster context.
|
||||
- **\`cypher\`** — Cypher queries against the graph. Use \`{{QUERY_VECTOR}}\` for vector search.
|
||||
- **\`grep\`** — Regex search. Best for exact strings, TODOs, error codes.
|
||||
- **\`read\`** — Read file content. Always use after search/grep to see full code.
|
||||
- **\`highlight\`** — Highlight nodes in the visual graph.
|
||||
- **\`blastRadius\`** — Impact analysis. Output is graph-verified (trusted). Run optional grep for dynamic patterns if thoroughness needed.
|
||||
- **\`explore\`** — Deep dive on a symbol, cluster, or process. Shows membership, participation, connections.
|
||||
- **\`overview\`** — Codebase map showing all clusters and processes.
|
||||
- **\`impact\`** — Impact analysis. Shows affected processes, clusters, and risk level.
|
||||
|
||||
## 📊 GRAPH SCHEMA
|
||||
Nodes: File, Folder, Function, Class, Interface, Method, CodeElement
|
||||
Relation: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS
|
||||
Nodes: File, Folder, Function, Class, Interface, Method, Community, Process
|
||||
Relations: \`CodeRelation\` with \`type\` property: CONTAINS, DEFINES, IMPORTS, CALLS, EXTENDS, IMPLEMENTS, MEMBER_OF, STEP_IN_PROCESS
|
||||
|
||||
Cypher examples:
|
||||
- \`MATCH (f:Function) RETURN f.name LIMIT 10\`
|
||||
- \`MATCH (f:File)-[:CodeRelation {type: 'IMPORTS'}]->(g:File) RETURN f.name, g.name\`
|
||||
|
||||
## 📝CRITICAL RULES
|
||||
- **blastRadius output is trusted.** Do NOT re-validate with cypher. Optionally run the suggested grep commands for dynamic patterns.
|
||||
- **impact output is trusted.** Do NOT re-validate with cypher. Optionally run the suggested grep commands for dynamic patterns.
|
||||
- **Cite or retract.** Never state something you can't ground.
|
||||
- **Read before concluding.** Don't guess from names alone.
|
||||
- **Retry on failure.** If a tool fails, fix the input and try again.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
/**
|
||||
* Graph RAG Tools for LangChain Agent
|
||||
*
|
||||
* Consolidated tools (6 total):
|
||||
* - search: Hybrid search (BM25 + semantic + RRF) with 1-hop expansion
|
||||
* Consolidated tools (7 total):
|
||||
* - search: Hybrid search (BM25 + semantic + RRF), grouped by process/cluster
|
||||
* - cypher: Execute Cypher queries (auto-embeds {{QUERY_VECTOR}} if present)
|
||||
* - grep: Regex pattern search across files
|
||||
* - read: Read file content by path
|
||||
* - highlight: Highlight nodes in graph UI
|
||||
* - blastRadius: Impact analysis (what depends on / is affected by changes)
|
||||
* - overview: Codebase map (clusters + processes)
|
||||
* - explore: Deep dive on a symbol, cluster, or process
|
||||
* - impact: Impact analysis (what depends on / is affected by changes)
|
||||
*/
|
||||
|
||||
import { tool } from '@langchain/core/tools';
|
||||
|
|
@ -36,8 +37,9 @@ export const createGraphRAGTools = (
|
|||
* Unified search tool: BM25 + Semantic + RRF, with 1-hop graph context
|
||||
*/
|
||||
const searchTool = tool(
|
||||
async ({ query, limit }: { query: string; limit?: number }) => {
|
||||
async ({ query, limit, groupByProcess }: { query: string; limit?: number; groupByProcess?: boolean }) => {
|
||||
const k = limit ?? 10;
|
||||
const shouldGroup = groupByProcess ?? true;
|
||||
|
||||
// Step 1: Hybrid search (BM25 + semantic with RRF)
|
||||
let searchResults: any[] = [];
|
||||
|
|
@ -62,12 +64,26 @@ export const createGraphRAGTools = (
|
|||
return `No code found matching "${query}". Try different terms or use grep for exact patterns.`;
|
||||
}
|
||||
|
||||
// Step 2: Get 1-hop connections for each result
|
||||
const resultsWithContext: string[] = [];
|
||||
type ProcessInfo = { id: string; label: string; step?: number; stepCount?: number };
|
||||
type ResultInfo = {
|
||||
idx: number;
|
||||
nodeId: string;
|
||||
name: string;
|
||||
label: string;
|
||||
filePath: string;
|
||||
location: string;
|
||||
sources: string;
|
||||
score: string;
|
||||
connections: string;
|
||||
clusterLabel: string;
|
||||
processes: ProcessInfo[];
|
||||
};
|
||||
|
||||
const results: ResultInfo[] = [];
|
||||
|
||||
for (let i = 0; i < Math.min(searchResults.length, k); i++) {
|
||||
const r = searchResults[i];
|
||||
const nodeId = r.nodeId || r.id;
|
||||
const nodeId = r.nodeId || r.id || '';
|
||||
const name = r.name || r.filePath?.split('/').pop() || 'Unknown';
|
||||
const label = r.label || 'File';
|
||||
const filePath = r.filePath || '';
|
||||
|
|
@ -91,7 +107,6 @@ export const createGraphRAGTools = (
|
|||
`;
|
||||
const connRes = await executeQuery(connectionsQuery);
|
||||
if (connRes.length > 0) {
|
||||
// Result is nested array: [[outgoing], [incoming]] or {outgoing: [], incoming: []}
|
||||
const row = connRes[0];
|
||||
const rawOutgoing = Array.isArray(row) ? row[0] : (row.outgoing || []);
|
||||
const rawIncoming = Array.isArray(row) ? row[1] : (row.incoming || []);
|
||||
|
|
@ -116,18 +131,130 @@ export const createGraphRAGTools = (
|
|||
}
|
||||
}
|
||||
|
||||
resultsWithContext.push(
|
||||
`[${i + 1}] ${label}: ${name}${score}\n ID: ${nodeId}\n File: ${filePath}${location}\n Found by: ${sources}${connections}`
|
||||
);
|
||||
// Cluster membership
|
||||
let clusterLabel = 'Unclustered';
|
||||
if (nodeId) {
|
||||
try {
|
||||
const nodeLabel = nodeId.split(':')[0];
|
||||
const clusterQuery = `
|
||||
MATCH (n:${nodeLabel} {id: '${nodeId.replace(/'/g, "''")}'})
|
||||
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
RETURN c.label AS label
|
||||
LIMIT 1
|
||||
`;
|
||||
const clusterRes = await executeQuery(clusterQuery);
|
||||
if (clusterRes.length > 0) {
|
||||
const row = clusterRes[0];
|
||||
const labelValue = Array.isArray(row) ? row[0] : row.label;
|
||||
if (labelValue) clusterLabel = labelValue;
|
||||
}
|
||||
} catch {
|
||||
// Skip cluster lookup if query fails
|
||||
}
|
||||
}
|
||||
|
||||
// Process participation
|
||||
const processes: ProcessInfo[] = [];
|
||||
if (nodeId) {
|
||||
try {
|
||||
const nodeLabel = nodeId.split(':')[0];
|
||||
const processQuery = `
|
||||
MATCH (n:${nodeLabel} {id: '${nodeId.replace(/'/g, "''")}'})
|
||||
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
RETURN p.id AS id, p.label AS label, r.step AS step, p.stepCount AS stepCount
|
||||
ORDER BY r.step
|
||||
`;
|
||||
const procRes = await executeQuery(processQuery);
|
||||
for (const row of procRes) {
|
||||
const id = Array.isArray(row) ? row[0] : row.id;
|
||||
const labelValue = Array.isArray(row) ? row[1] : row.label;
|
||||
const step = Array.isArray(row) ? row[2] : row.step;
|
||||
const stepCount = Array.isArray(row) ? row[3] : row.stepCount;
|
||||
if (id && labelValue) {
|
||||
processes.push({ id, label: labelValue, step, stepCount });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip process lookup if query fails
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
idx: i + 1,
|
||||
nodeId,
|
||||
name,
|
||||
label,
|
||||
filePath,
|
||||
location,
|
||||
sources,
|
||||
score,
|
||||
connections,
|
||||
clusterLabel,
|
||||
processes,
|
||||
});
|
||||
}
|
||||
|
||||
return `Found ${searchResults.length} matches:\n\n${resultsWithContext.join('\n\n')}`;
|
||||
const formatResult = (r: ResultInfo, stepInfo?: ProcessInfo) => {
|
||||
const stepLabel = stepInfo?.step ? ` (step ${stepInfo.step}/${stepInfo.stepCount ?? '?'})` : '';
|
||||
return `[${r.idx}] ${r.label}: ${r.name}${r.score}${stepLabel}\n ID: ${r.nodeId}\n File: ${r.filePath}${r.location}\n Cluster: ${r.clusterLabel}\n Found by: ${r.sources}${r.connections}`;
|
||||
};
|
||||
|
||||
if (!shouldGroup) {
|
||||
return `Found ${searchResults.length} matches:\n\n${results.map(r => formatResult(r)).join('\n\n')}`;
|
||||
}
|
||||
|
||||
// Group by process (or "No process")
|
||||
const processMap = new Map<string, { label: string; stepCount?: number; entries: { result: ResultInfo; step?: number; stepCount?: number }[] }>();
|
||||
const noProcessKey = '__no_process__';
|
||||
|
||||
for (const r of results) {
|
||||
if (r.processes.length === 0) {
|
||||
if (!processMap.has(noProcessKey)) {
|
||||
processMap.set(noProcessKey, { label: 'No process', entries: [] });
|
||||
}
|
||||
processMap.get(noProcessKey)!.entries.push({ result: r });
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const p of r.processes) {
|
||||
if (!processMap.has(p.id)) {
|
||||
processMap.set(p.id, { label: p.label, stepCount: p.stepCount, entries: [] });
|
||||
}
|
||||
processMap.get(p.id)!.entries.push({ result: r, step: p.step, stepCount: p.stepCount });
|
||||
}
|
||||
}
|
||||
|
||||
const sortedProcesses = Array.from(processMap.entries()).sort((a, b) => {
|
||||
const aCount = a[1].entries.length;
|
||||
const bCount = b[1].entries.length;
|
||||
return bCount - aCount;
|
||||
});
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`Found ${searchResults.length} matches grouped by process:`);
|
||||
lines.push('');
|
||||
|
||||
for (const [pid, group] of sortedProcesses) {
|
||||
const stepInfo = group.stepCount ? `, ${group.stepCount} steps` : '';
|
||||
const header = pid === noProcessKey
|
||||
? `NO PROCESS (${group.entries.length} matches)`
|
||||
: `PROCESS: ${group.label} (${group.entries.length} matches${stepInfo})`;
|
||||
lines.push(header);
|
||||
group.entries.forEach(entry => {
|
||||
const stepLabel = entry.step ? { id: pid, label: group.label, step: entry.step, stepCount: entry.stepCount } : undefined;
|
||||
lines.push(formatResult(entry.result, stepLabel));
|
||||
});
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n').trim();
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
description: 'Search for code by keywords or concepts. Combines keyword matching and semantic understanding. Returns relevant code with their graph connections (what calls them, what they import, etc.).',
|
||||
description: 'Search for code by keywords or concepts. Combines keyword matching and semantic understanding. Groups results by process with cluster context.',
|
||||
schema: z.object({
|
||||
query: z.string().describe('What you are looking for (e.g., "authentication middleware", "database connection")'),
|
||||
groupByProcess: z.boolean().optional().nullable().describe('Group results by process (default: true)'),
|
||||
limit: z.number().optional().nullable().describe('Max results to return (default: 10)'),
|
||||
}),
|
||||
}
|
||||
|
|
@ -389,35 +516,364 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
);
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 5: HIGHLIGHT (Highlight nodes in graph UI)
|
||||
// TOOL 5: OVERVIEW (Codebase map)
|
||||
// ============================================================================
|
||||
|
||||
const highlightTool = tool(
|
||||
async ({ nodeIds, description }: { nodeIds: string[]; description?: string }) => {
|
||||
if (!nodeIds || nodeIds.length === 0) {
|
||||
return 'No node IDs provided.';
|
||||
const overviewTool = tool(
|
||||
async () => {
|
||||
try {
|
||||
const clustersQuery = `
|
||||
MATCH (c:Community)
|
||||
RETURN c.id AS id, c.label AS label, c.cohesion AS cohesion, c.symbolCount AS symbolCount, c.description AS description
|
||||
ORDER BY c.symbolCount DESC
|
||||
LIMIT 200
|
||||
`;
|
||||
const processesQuery = `
|
||||
MATCH (p:Process)
|
||||
RETURN p.id AS id, p.label AS label, p.processType AS type, p.stepCount AS stepCount, p.communities AS communities
|
||||
ORDER BY p.stepCount DESC
|
||||
LIMIT 200
|
||||
`;
|
||||
const depsQuery = `
|
||||
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b)
|
||||
MATCH (a)-[:CodeRelation {type: 'MEMBER_OF'}]->(c1:Community)
|
||||
MATCH (b)-[:CodeRelation {type: 'MEMBER_OF'}]->(c2:Community)
|
||||
WHERE c1.id <> c2.id
|
||||
RETURN c1.label AS \`from\`, c2.label AS \`to\`, COUNT(*) AS calls
|
||||
ORDER BY calls DESC
|
||||
LIMIT 15
|
||||
`;
|
||||
const criticalQuery = `
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
RETURN p.label AS label, COUNT(r) AS steps
|
||||
ORDER BY steps DESC
|
||||
LIMIT 10
|
||||
`;
|
||||
|
||||
const [clusters, processes, deps, critical] = await Promise.all([
|
||||
executeQuery(clustersQuery),
|
||||
executeQuery(processesQuery),
|
||||
executeQuery(depsQuery),
|
||||
executeQuery(criticalQuery),
|
||||
]);
|
||||
|
||||
const clusterLines = clusters.map((row: any) => {
|
||||
const label = Array.isArray(row) ? row[1] : row.label;
|
||||
const symbols = Array.isArray(row) ? row[3] : row.symbolCount;
|
||||
const cohesion = Array.isArray(row) ? row[2] : row.cohesion;
|
||||
const desc = Array.isArray(row) ? row[4] : row.description;
|
||||
const cohesionText = cohesion !== null && cohesion !== undefined ? Number(cohesion).toFixed(2) : '';
|
||||
return `| ${label || ''} | ${symbols ?? ''} | ${cohesionText} | ${desc ?? ''} |`;
|
||||
});
|
||||
|
||||
const processLines = processes.map((row: any) => {
|
||||
const label = Array.isArray(row) ? row[1] : row.label;
|
||||
const steps = Array.isArray(row) ? row[3] : row.stepCount;
|
||||
const type = Array.isArray(row) ? row[2] : row.type;
|
||||
const communities = Array.isArray(row) ? row[4] : row.communities;
|
||||
const clusterText = Array.isArray(communities) ? communities.length : (communities ? 1 : 0);
|
||||
return `| ${label || ''} | ${steps ?? ''} | ${type ?? ''} | ${clusterText} |`;
|
||||
});
|
||||
|
||||
const depLines = deps.map((row: any) => {
|
||||
const from = Array.isArray(row) ? row[0] : row.from;
|
||||
const to = Array.isArray(row) ? row[1] : row.to;
|
||||
const calls = Array.isArray(row) ? row[2] : row.calls;
|
||||
return `- ${from} -> ${to} (${calls} calls)`;
|
||||
});
|
||||
|
||||
const criticalLines = critical.map((row: any) => {
|
||||
const label = Array.isArray(row) ? row[0] : row.label;
|
||||
const steps = Array.isArray(row) ? row[1] : row.steps;
|
||||
return `- ${label} (${steps} steps)`;
|
||||
});
|
||||
|
||||
return [
|
||||
`CLUSTERS (${clusters.length} total):`,
|
||||
`| Cluster | Symbols | Cohesion | Description |`,
|
||||
`| --- | --- | --- | --- |`,
|
||||
...clusterLines,
|
||||
``,
|
||||
`PROCESSES (${processes.length} total):`,
|
||||
`| Process | Steps | Type | Clusters |`,
|
||||
`| --- | --- | --- | --- |`,
|
||||
...processLines,
|
||||
``,
|
||||
`CLUSTER DEPENDENCIES:`,
|
||||
...(depLines.length > 0 ? depLines : ['- None found']),
|
||||
``,
|
||||
`CRITICAL PATHS:`,
|
||||
...(criticalLines.length > 0 ? criticalLines : ['- None found']),
|
||||
].join('\n');
|
||||
} catch (error) {
|
||||
return `Overview error: ${error instanceof Error ? error.message : String(error)}`;
|
||||
}
|
||||
|
||||
const marker = `[HIGHLIGHT_NODES:${nodeIds.join(',')}]`;
|
||||
const desc = description || `Highlighting ${nodeIds.length} node(s)`;
|
||||
|
||||
return `${desc}\n\n${marker}\n\nNodes highlighted in the graph.`;
|
||||
},
|
||||
{
|
||||
name: 'highlight',
|
||||
description: 'Highlight nodes in the visual graph. Use node IDs from search/cypher results (format: Label:filepath:name).',
|
||||
name: 'overview',
|
||||
description: 'Codebase map showing all clusters and processes, plus cross-cluster dependencies.',
|
||||
schema: z.object({}),
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 6: EXPLORE (Deep dive on symbol, cluster, or process)
|
||||
// ============================================================================
|
||||
|
||||
const exploreTool = tool(
|
||||
async ({ target, type }: { target: string; type?: 'symbol' | 'cluster' | 'process' | null }) => {
|
||||
const safeTarget = target.replace(/'/g, "''");
|
||||
let resolvedType = type ?? null;
|
||||
let processRow: any | null = null;
|
||||
let communityRow: any | null = null;
|
||||
let symbolRow: any | null = null;
|
||||
|
||||
const getRowValue = (row: any, idx: number, key: string) => Array.isArray(row) ? row[idx] : row[key];
|
||||
|
||||
if (!resolvedType || resolvedType === 'process') {
|
||||
const processQuery = `
|
||||
MATCH (p:Process)
|
||||
WHERE p.id = '${safeTarget}' OR p.label = '${safeTarget}'
|
||||
RETURN p.id AS id, p.label AS label, p.processType AS type, p.stepCount AS stepCount
|
||||
LIMIT 1
|
||||
`;
|
||||
const processRes = await executeQuery(processQuery);
|
||||
if (processRes.length > 0) {
|
||||
processRow = processRes[0];
|
||||
resolvedType = 'process';
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedType || resolvedType === 'cluster') {
|
||||
const communityQuery = `
|
||||
MATCH (c:Community)
|
||||
WHERE c.id = '${safeTarget}' OR c.label = '${safeTarget}' OR c.heuristicLabel = '${safeTarget}'
|
||||
RETURN c.id AS id, c.label AS label, c.cohesion AS cohesion, c.symbolCount AS symbolCount, c.description AS description
|
||||
LIMIT 1
|
||||
`;
|
||||
const communityRes = await executeQuery(communityQuery);
|
||||
if (communityRes.length > 0) {
|
||||
communityRow = communityRes[0];
|
||||
resolvedType = 'cluster';
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedType || resolvedType === 'symbol') {
|
||||
const symbolQuery = `
|
||||
MATCH (n)
|
||||
WHERE n.name = '${safeTarget}' OR n.id = '${safeTarget}' OR n.filePath = '${safeTarget}'
|
||||
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, label(n) AS nodeType
|
||||
LIMIT 5
|
||||
`;
|
||||
const symbolRes = await executeQuery(symbolQuery);
|
||||
if (symbolRes.length > 0) {
|
||||
symbolRow = symbolRes[0];
|
||||
resolvedType = 'symbol';
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolvedType) {
|
||||
return `Could not find "${target}" as a symbol, cluster, or process. Try search first.`;
|
||||
}
|
||||
|
||||
if (resolvedType === 'process') {
|
||||
const pid = getRowValue(processRow, 0, 'id');
|
||||
const label = getRowValue(processRow, 1, 'label');
|
||||
const ptype = getRowValue(processRow, 2, 'type');
|
||||
const stepCount = getRowValue(processRow, 3, 'stepCount');
|
||||
|
||||
const stepsQuery = `
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${pid.replace(/'/g, "''")}'})
|
||||
RETURN s.name AS name, s.filePath AS filePath, r.step AS step
|
||||
ORDER BY r.step
|
||||
`;
|
||||
const clustersQuery = `
|
||||
MATCH (c:Community)<-[:CodeRelation {type: 'MEMBER_OF'}]-(s)
|
||||
MATCH (s)-[:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process {id: '${pid.replace(/'/g, "''")}'})
|
||||
RETURN DISTINCT c.id AS id, c.label AS label, c.description AS description
|
||||
ORDER BY c.label
|
||||
LIMIT 20
|
||||
`;
|
||||
|
||||
const [steps, clusters] = await Promise.all([
|
||||
executeQuery(stepsQuery),
|
||||
executeQuery(clustersQuery),
|
||||
]);
|
||||
|
||||
const stepLines = steps.map((row: any) => {
|
||||
const name = getRowValue(row, 0, 'name');
|
||||
const filePath = getRowValue(row, 1, 'filePath');
|
||||
const step = getRowValue(row, 2, 'step');
|
||||
return `- ${step}. ${name} (${filePath || 'n/a'})`;
|
||||
});
|
||||
|
||||
const clusterLines = clusters.map((row: any) => {
|
||||
const clabel = getRowValue(row, 1, 'label');
|
||||
const desc = getRowValue(row, 2, 'description');
|
||||
return `- ${clabel}${desc ? ` — ${desc}` : ''}`;
|
||||
});
|
||||
|
||||
return [
|
||||
`PROCESS: ${label}`,
|
||||
`Type: ${ptype || 'n/a'}`,
|
||||
`Steps: ${stepCount ?? steps.length}`,
|
||||
``,
|
||||
`STEPS:`,
|
||||
...(stepLines.length > 0 ? stepLines : ['- None found']),
|
||||
``,
|
||||
`CLUSTERS TOUCHED:`,
|
||||
...(clusterLines.length > 0 ? clusterLines : ['- None found']),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
if (resolvedType === 'cluster') {
|
||||
const cid = getRowValue(communityRow, 0, 'id');
|
||||
const label = getRowValue(communityRow, 1, 'label');
|
||||
const cohesion = getRowValue(communityRow, 2, 'cohesion');
|
||||
const symbolCount = getRowValue(communityRow, 3, 'symbolCount');
|
||||
const description = getRowValue(communityRow, 4, 'description');
|
||||
|
||||
const membersQuery = `
|
||||
MATCH (c:Community {id: '${cid.replace(/'/g, "''")}'})<-[:CodeRelation {type: 'MEMBER_OF'}]-(m)
|
||||
RETURN m.name AS name, m.filePath AS filePath, label(m) AS nodeType
|
||||
LIMIT 50
|
||||
`;
|
||||
const processesQuery = `
|
||||
MATCH (c:Community {id: '${cid.replace(/'/g, "''")}'})<-[:CodeRelation {type: 'MEMBER_OF'}]-(s)
|
||||
MATCH (s)-[:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
RETURN DISTINCT p.id AS id, p.label AS label, p.stepCount AS stepCount
|
||||
ORDER BY p.stepCount DESC
|
||||
LIMIT 20
|
||||
`;
|
||||
|
||||
const [members, processes] = await Promise.all([
|
||||
executeQuery(membersQuery),
|
||||
executeQuery(processesQuery),
|
||||
]);
|
||||
|
||||
const memberLines = members.map((row: any) => {
|
||||
const name = getRowValue(row, 0, 'name');
|
||||
const filePath = getRowValue(row, 1, 'filePath');
|
||||
const nodeType = getRowValue(row, 2, 'nodeType');
|
||||
return `- ${nodeType}: ${name} (${filePath || 'n/a'})`;
|
||||
});
|
||||
|
||||
const processLines = processes.map((row: any) => {
|
||||
const plabel = getRowValue(row, 1, 'label');
|
||||
const steps = getRowValue(row, 2, 'stepCount');
|
||||
return `- ${plabel} (${steps} steps)`;
|
||||
});
|
||||
|
||||
return [
|
||||
`CLUSTER: ${label}`,
|
||||
`Symbols: ${symbolCount ?? members.length}`,
|
||||
`Cohesion: ${cohesion !== null && cohesion !== undefined ? Number(cohesion).toFixed(2) : 'n/a'}`,
|
||||
`Description: ${description || 'n/a'}`,
|
||||
``,
|
||||
`TOP MEMBERS:`,
|
||||
...(memberLines.length > 0 ? memberLines : ['- None found']),
|
||||
``,
|
||||
`PROCESSES TOUCHING THIS CLUSTER:`,
|
||||
...(processLines.length > 0 ? processLines : ['- None found']),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
if (resolvedType === 'symbol') {
|
||||
const nodeId = getRowValue(symbolRow, 0, 'id');
|
||||
const name = getRowValue(symbolRow, 1, 'name');
|
||||
const filePath = getRowValue(symbolRow, 2, 'filePath');
|
||||
const nodeType = getRowValue(symbolRow, 3, 'nodeType');
|
||||
|
||||
const clusterQuery = `
|
||||
MATCH (n:${nodeType} {id: '${String(nodeId).replace(/'/g, "''")}'})
|
||||
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
RETURN c.label AS label, c.description AS description
|
||||
LIMIT 1
|
||||
`;
|
||||
const processQuery = `
|
||||
MATCH (n:${nodeType} {id: '${String(nodeId).replace(/'/g, "''")}'})
|
||||
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
RETURN p.label AS label, r.step AS step, p.stepCount AS stepCount
|
||||
ORDER BY r.step
|
||||
`;
|
||||
const connectionsQuery = `
|
||||
MATCH (n:${nodeType} {id: '${String(nodeId).replace(/'/g, "''")}'})
|
||||
OPTIONAL MATCH (n)-[r1:CodeRelation]->(dst)
|
||||
OPTIONAL MATCH (src)-[r2:CodeRelation]->(n)
|
||||
RETURN
|
||||
collect(DISTINCT {name: dst.name, type: r1.type, confidence: r1.confidence}) AS outgoing,
|
||||
collect(DISTINCT {name: src.name, type: r2.type, confidence: r2.confidence}) AS incoming
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const [clusterRes, processRes, connRes] = await Promise.all([
|
||||
executeQuery(clusterQuery),
|
||||
executeQuery(processQuery),
|
||||
executeQuery(connectionsQuery),
|
||||
]);
|
||||
|
||||
const clusterLabel = clusterRes.length > 0 ? getRowValue(clusterRes[0], 0, 'label') : 'Unclustered';
|
||||
const clusterDesc = clusterRes.length > 0 ? getRowValue(clusterRes[0], 1, 'description') : '';
|
||||
|
||||
const processLines = processRes.map((row: any) => {
|
||||
const plabel = getRowValue(row, 0, 'label');
|
||||
const step = getRowValue(row, 1, 'step');
|
||||
const stepCount = getRowValue(row, 2, 'stepCount');
|
||||
return `- ${plabel} (step ${step}/${stepCount ?? '?'})`;
|
||||
});
|
||||
|
||||
let connections = 'None';
|
||||
if (connRes.length > 0) {
|
||||
const row = connRes[0];
|
||||
const rawOutgoing = Array.isArray(row) ? row[0] : (row.outgoing || []);
|
||||
const rawIncoming = Array.isArray(row) ? row[1] : (row.incoming || []);
|
||||
const outgoing = (rawOutgoing || []).filter((c: any) => c && c.name).slice(0, 5);
|
||||
const incoming = (rawIncoming || []).filter((c: any) => c && c.name).slice(0, 5);
|
||||
|
||||
const fmt = (c: any, dir: 'out' | 'in') => {
|
||||
const conf = c.confidence ? Math.round(c.confidence * 100) : 100;
|
||||
return dir === 'out'
|
||||
? `-[${c.type} ${conf}%]-> ${c.name}`
|
||||
: `<-[${c.type} ${conf}%]- ${c.name}`;
|
||||
};
|
||||
const outList = outgoing.map((c: any) => fmt(c, 'out'));
|
||||
const inList = incoming.map((c: any) => fmt(c, 'in'));
|
||||
if (outList.length || inList.length) {
|
||||
connections = [...outList, ...inList].join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
`SYMBOL: ${nodeType} ${name}`,
|
||||
`ID: ${nodeId}`,
|
||||
`File: ${filePath || 'n/a'}`,
|
||||
`Cluster: ${clusterLabel}${clusterDesc ? ` — ${clusterDesc}` : ''}`,
|
||||
``,
|
||||
`PROCESSES:`,
|
||||
...(processLines.length > 0 ? processLines : ['- None found']),
|
||||
``,
|
||||
`CONNECTIONS:`,
|
||||
connections,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return `Unable to explore "${target}".`;
|
||||
},
|
||||
{
|
||||
name: 'explore',
|
||||
description: 'Deep dive on a symbol, cluster, or process. Shows membership, participation, and connections.',
|
||||
schema: z.object({
|
||||
nodeIds: z.array(z.string()).describe('Node IDs to highlight (e.g., ["Function:src/utils.ts:calculate"])'),
|
||||
description: z.string().optional().nullable().describe('What these nodes represent'),
|
||||
target: z.string().describe('Name or ID of a symbol, cluster, or process'),
|
||||
type: z.enum(['symbol', 'cluster', 'process']).optional().nullable().describe('Optional target type (auto-detected if omitted)'),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// TOOL 6: BLAST RADIUS (Impact analysis)
|
||||
// TOOL 7: IMPACT (Impact analysis)
|
||||
// ============================================================================
|
||||
|
||||
const blastRadiusTool = tool(
|
||||
const impactTool = tool(
|
||||
async ({ target, direction, maxDepth, relationTypes, includeTests, minConfidence }: {
|
||||
target: string;
|
||||
direction: 'upstream' | 'downstream';
|
||||
|
|
@ -505,7 +961,7 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
LIMIT 300
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
|
|
@ -522,7 +978,7 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
LIMIT 300
|
||||
`
|
||||
: isFileTarget
|
||||
? `
|
||||
|
|
@ -541,7 +997,7 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
LIMIT 300
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
|
|
@ -558,10 +1014,10 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r.type AS edgeType,
|
||||
r.confidence AS confidence,
|
||||
r.reason AS reason
|
||||
LIMIT 100
|
||||
LIMIT 300
|
||||
`;
|
||||
depthQueries.push(executeQuery(d1Query).catch(err => {
|
||||
if (import.meta.env.DEV) console.warn('Blast radius d=1 query failed:', err);
|
||||
if (import.meta.env.DEV) console.warn('Impact d=1 query failed:', err);
|
||||
return [];
|
||||
}));
|
||||
|
||||
|
|
@ -586,7 +1042,7 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r2.type AS edgeType,
|
||||
r2.confidence AS confidence,
|
||||
r2.reason AS reason
|
||||
LIMIT 100
|
||||
LIMIT 200
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
|
|
@ -606,10 +1062,10 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r2.type AS edgeType,
|
||||
r2.confidence AS confidence,
|
||||
r2.reason AS reason
|
||||
LIMIT 100
|
||||
LIMIT 200
|
||||
`;
|
||||
depthQueries.push(executeQuery(d2Query).catch(err => {
|
||||
if (import.meta.env.DEV) console.warn('Blast radius d=2 query failed:', err);
|
||||
if (import.meta.env.DEV) console.warn('Impact d=2 query failed:', err);
|
||||
return [];
|
||||
}));
|
||||
}
|
||||
|
|
@ -637,7 +1093,7 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r3.type AS edgeType,
|
||||
r3.confidence AS confidence,
|
||||
r3.reason AS reason
|
||||
LIMIT 50
|
||||
LIMIT 100
|
||||
`
|
||||
: `
|
||||
MATCH (target {id: '${targetId.replace(/'/g, "''")}'})
|
||||
|
|
@ -659,10 +1115,10 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
r3.type AS edgeType,
|
||||
r3.confidence AS confidence,
|
||||
r3.reason AS reason
|
||||
LIMIT 50
|
||||
LIMIT 100
|
||||
`;
|
||||
depthQueries.push(executeQuery(d3Query).catch(err => {
|
||||
if (import.meta.env.DEV) console.warn('Blast radius d=3 query failed:', err);
|
||||
if (import.meta.env.DEV) console.warn('Impact d=3 query failed:', err);
|
||||
return [];
|
||||
}));
|
||||
}
|
||||
|
|
@ -721,9 +1177,108 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
return `No ${direction} dependencies found for "${target}" (types: ${activeRelTypes.join(', ')}). This code appears to be ${direction === 'upstream' ? 'unused (not called by anything)' : 'self-contained (no outgoing dependencies)'}.`;
|
||||
}
|
||||
|
||||
const depth1 = byDepth.get(1) || [];
|
||||
const depth2 = byDepth.get(2) || [];
|
||||
const depth3 = byDepth.get(3) || [];
|
||||
|
||||
// Confidence buckets
|
||||
const confidenceBuckets = { high: 0, medium: 0, low: 0 };
|
||||
for (const nodes of byDepth.values()) {
|
||||
for (const n of nodes) {
|
||||
const conf = n.confidence ?? 1;
|
||||
if (conf >= 0.9) confidenceBuckets.high += 1;
|
||||
else if (conf >= 0.8) confidenceBuckets.medium += 1;
|
||||
else confidenceBuckets.low += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Affected processes and clusters
|
||||
const maxIdsForContext = 500;
|
||||
const trimmedIds = allNodeIds.slice(0, maxIdsForContext);
|
||||
const idList = trimmedIds.map(id => `'${id.replace(/'/g, "''")}'`).join(', ');
|
||||
let affectedProcesses: Array<{ label: string; hits: number; minStep: number | null; stepCount: number | null }> = [];
|
||||
let affectedClusters: Array<{ label: string; hits: number; impact: string }> = [];
|
||||
|
||||
if (trimmedIds.length > 0) {
|
||||
const processQuery = `
|
||||
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
||||
WHERE s.id IN [${idList}]
|
||||
RETURN p.label AS label, COUNT(DISTINCT s.id) AS hits, MIN(r.step) AS minStep, p.stepCount AS stepCount
|
||||
ORDER BY hits DESC
|
||||
LIMIT 20
|
||||
`;
|
||||
const clusterQuery = `
|
||||
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
WHERE s.id IN [${idList}]
|
||||
RETURN c.label AS label, COUNT(DISTINCT s.id) AS hits
|
||||
ORDER BY hits DESC
|
||||
LIMIT 20
|
||||
`;
|
||||
const directIdList = depth1.map(n => `'${n.id.replace(/'/g, "''")}'`).join(', ');
|
||||
const directClusterQuery = depth1.length > 0 ? `
|
||||
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
||||
WHERE s.id IN [${directIdList}]
|
||||
RETURN DISTINCT c.label AS label
|
||||
` : '';
|
||||
|
||||
const [processRes, clusterRes, directClusterRes] = await Promise.all([
|
||||
executeQuery(processQuery),
|
||||
executeQuery(clusterQuery),
|
||||
directClusterQuery ? executeQuery(directClusterQuery) : Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const directClusterSet = new Set<string>();
|
||||
directClusterRes.forEach((row: any) => {
|
||||
const label = Array.isArray(row) ? row[0] : row.label;
|
||||
if (label) directClusterSet.add(label);
|
||||
});
|
||||
|
||||
affectedProcesses = processRes.map((row: any) => ({
|
||||
label: Array.isArray(row) ? row[0] : row.label,
|
||||
hits: Array.isArray(row) ? row[1] : row.hits,
|
||||
minStep: Array.isArray(row) ? row[2] : row.minStep,
|
||||
stepCount: Array.isArray(row) ? row[3] : row.stepCount,
|
||||
}));
|
||||
|
||||
affectedClusters = clusterRes.map((row: any) => {
|
||||
const label = Array.isArray(row) ? row[0] : row.label;
|
||||
const hits = Array.isArray(row) ? row[1] : row.hits;
|
||||
const impact = directClusterSet.has(label) ? 'direct' : 'indirect';
|
||||
return { label, hits, impact };
|
||||
});
|
||||
}
|
||||
|
||||
const directCount = depth1.length;
|
||||
const processCount = affectedProcesses.length;
|
||||
const clusterCount = affectedClusters.length;
|
||||
let risk = 'LOW';
|
||||
if (directCount >= 30 || processCount >= 5 || clusterCount >= 5 || totalAffected >= 200) {
|
||||
risk = 'CRITICAL';
|
||||
} else if (directCount >= 15 || processCount >= 3 || clusterCount >= 3 || totalAffected >= 100) {
|
||||
risk = 'HIGH';
|
||||
} else if (directCount >= 5 || totalAffected >= 30) {
|
||||
risk = 'MEDIUM';
|
||||
}
|
||||
|
||||
// ===== COMPACT TABULAR OUTPUT =====
|
||||
const lines: string[] = [
|
||||
`🔴 BLAST RADIUS: ${target} | ${direction} | ${totalAffected} affected`,
|
||||
`🔴 IMPACT: ${target} | ${direction} | ${totalAffected} affected`,
|
||||
`Confidence: High ${confidenceBuckets.high} | Medium ${confidenceBuckets.medium} | Low ${confidenceBuckets.low}`,
|
||||
``,
|
||||
`AFFECTED PROCESSES:`,
|
||||
...(affectedProcesses.length > 0
|
||||
? affectedProcesses.map(p => `- ${p.label} - BROKEN at step ${p.minStep ?? '?'} (${p.hits} symbols, ${p.stepCount ?? '?'} steps)`)
|
||||
: ['- None found']),
|
||||
``,
|
||||
`AFFECTED CLUSTERS:`,
|
||||
...(affectedClusters.length > 0
|
||||
? affectedClusters.map(c => `- ${c.label} (${c.impact}, ${c.hits} symbols)`)
|
||||
: ['- None found']),
|
||||
``,
|
||||
`RISK: ${risk}`,
|
||||
`- Direct callers: ${directCount}`,
|
||||
`- Processes affected: ${processCount}`,
|
||||
`- Clusters affected: ${clusterCount}`,
|
||||
``,
|
||||
];
|
||||
|
||||
|
|
@ -767,7 +1322,6 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
};
|
||||
|
||||
// Depth 1 - Critical (with call site snippets)
|
||||
const depth1 = byDepth.get(1) || [];
|
||||
if (depth1.length > 0) {
|
||||
const header = direction === 'upstream'
|
||||
? `d=1 (Directly DEPEND ON ${target}):`
|
||||
|
|
@ -786,7 +1340,6 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
}
|
||||
|
||||
// Depth 2 - High impact
|
||||
const depth2 = byDepth.get(2) || [];
|
||||
if (depth2.length > 0) {
|
||||
const header = direction === 'upstream'
|
||||
? `d=2 (Indirectly DEPEND ON ${target}):`
|
||||
|
|
@ -798,7 +1351,6 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
}
|
||||
|
||||
// Depth 3 - Transitive
|
||||
const depth3 = byDepth.get(3) || [];
|
||||
if (depth3.length > 0) {
|
||||
lines.push(`d=3 (Deep impact/dependency):`);
|
||||
depth3.slice(0, 5).forEach(n => lines.push(formatNode(n)));
|
||||
|
|
@ -811,15 +1363,11 @@ MATCH (n:Function {id: emb.nodeId}) RETURN n`,
|
|||
lines.push(`⚠️ Optional: grep("${target}") for dynamic patterns`);
|
||||
lines.push(``);
|
||||
|
||||
// Add the marker for UI highlighting
|
||||
const marker = `[BLAST_RADIUS:${allNodeIds.join(',')}]`;
|
||||
lines.push(marker);
|
||||
|
||||
return lines.join('\n');
|
||||
},
|
||||
{
|
||||
name: 'blastRadius',
|
||||
description: `Analyze the blast radius (impact) of changing a function, class, or file.
|
||||
name: 'impact',
|
||||
description: `Analyze the impact of changing a function, class, or file.
|
||||
|
||||
Use when users ask:
|
||||
- "What would break if I changed X?"
|
||||
|
|
@ -838,7 +1386,12 @@ Confidence: 100% = certain, <80% = fuzzy match (may be false positive)
|
|||
|
||||
relationTypes filter (optional):
|
||||
- Default: CALLS, IMPORTS, EXTENDS, IMPLEMENTS (usage-based)
|
||||
- Can add CONTAINS, DEFINES for structural analysis`,
|
||||
- Can add CONTAINS, DEFINES for structural analysis
|
||||
|
||||
Additional output sections:
|
||||
- Affected processes (with step impact)
|
||||
- Affected clusters (direct/indirect)
|
||||
- Risk summary (based on direct callers, processes, clusters)`,
|
||||
schema: z.object({
|
||||
target: z.string().describe('Name of the function, class, or file to analyze'),
|
||||
direction: z.enum(['upstream', 'downstream']).describe('upstream = what depends on this; downstream = what this depends on'),
|
||||
|
|
@ -859,7 +1412,8 @@ relationTypes filter (optional):
|
|||
cypherTool,
|
||||
grepTool,
|
||||
readTool,
|
||||
highlightTool,
|
||||
blastRadiusTool,
|
||||
overviewTool,
|
||||
exploreTool,
|
||||
impactTool,
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -177,15 +177,13 @@ export interface ToolCallInfo {
|
|||
* Now supports step-based streaming where each step is a distinct message
|
||||
*/
|
||||
export interface AgentStreamChunk {
|
||||
type: 'reasoning' | 'tool_call' | 'tool_result' | 'content' | 'highlight' | 'error' | 'done';
|
||||
type: 'reasoning' | 'tool_call' | 'tool_result' | 'content' | 'error' | 'done';
|
||||
/** LLM's reasoning/thinking text (shown as a step) */
|
||||
reasoning?: string;
|
||||
/** Final answer content (streamed token by token) */
|
||||
content?: string;
|
||||
/** Tool call information */
|
||||
toolCall?: ToolCallInfo;
|
||||
/** Node IDs to highlight in the graph */
|
||||
highlightNodeIds?: string[];
|
||||
/** Error message */
|
||||
error?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1042,10 +1042,10 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Parse blast radius marker from tool results
|
||||
const blastMatch = tc.result.match(/\[BLAST_RADIUS:([^\]]+)\]/);
|
||||
if (blastMatch) {
|
||||
const rawIds = blastMatch[1].split(',').map((id: string) => id.trim()).filter(Boolean);
|
||||
// Parse impact marker from tool results
|
||||
const impactMatch = tc.result.match(/\[IMPACT:([^\]]+)\]/);
|
||||
if (impactMatch) {
|
||||
const rawIds = impactMatch[1].split(',').map((id: string) => id.trim()).filter(Boolean);
|
||||
if (rawIds.length > 0 && graph) {
|
||||
const matchedIds = new Set<string>();
|
||||
const graphNodeIds = graph.nodes.map(n => n.id);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export const NODE_COLORS: Record<NodeLabel, string> = {
|
|||
Type: '#a78bfa', // Violet light
|
||||
CodeElement: '#64748b', // Slate - muted
|
||||
Community: '#818cf8', // Indigo light - cluster indicator
|
||||
Process: '#f43f5e', // Rose - execution flow indicator
|
||||
};
|
||||
|
||||
// Node sizes by type - clear visual hierarchy with dramatic size differences
|
||||
|
|
@ -39,6 +40,7 @@ export const NODE_SIZES: Record<NodeLabel, number> = {
|
|||
Type: 3, // Type alias - small
|
||||
CodeElement: 2, // Generic small
|
||||
Community: 0, // Hidden by default - metadata node
|
||||
Process: 0, // Hidden by default - metadata node
|
||||
};
|
||||
|
||||
// Community color palette for cluster-based coloring
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { GraphNode, GraphRelationship, KnowledgeGraph } from '../core/graph/types';
|
||||
import { CommunityDetectionResult } from '../core/ingestion/community-processor';
|
||||
import { ProcessDetectionResult } from '../core/ingestion/process-processor';
|
||||
|
||||
export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'heritage' | 'communities' | 'enriching' | 'complete' | 'error';
|
||||
export type PipelinePhase = 'idle' | 'extracting' | 'structure' | 'parsing' | 'imports' | 'calls' | 'heritage' | 'communities' | 'processes' | 'enriching' | 'complete' | 'error';
|
||||
|
||||
export interface PipelineProgress {
|
||||
phase: PipelinePhase;
|
||||
|
|
@ -20,6 +21,7 @@ export interface PipelineResult {
|
|||
graph: KnowledgeGraph;
|
||||
fileContents: Map<string, string>;
|
||||
communityResult?: CommunityDetectionResult;
|
||||
processResult?: ProcessDetectionResult;
|
||||
}
|
||||
|
||||
// Serializable version for Web Worker communication
|
||||
|
|
|
|||
171
todo_live_check_feature.md
Normal file
171
todo_live_check_feature.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Feature Specification: GitNexus "Guardian" (Live Impact Check)
|
||||
|
||||
## 1. Overview
|
||||
The "Guardian" is an active, background monitoring system that provides real-time feedback to developers as they modify code. It leverages the deterministic Knowledge Graph (KuzuDB) to perform instant "Impact Analysis" and "Architecture Linting" without incurring LLM token costs.
|
||||
|
||||
**Goal:** Provide a "Safety Net" that catches breaking changes, side effects, and architectural violations *before* a commit is made.
|
||||
|
||||
## 2. Core Capabilities
|
||||
|
||||
### A. Live "Blast Radius" Detection
|
||||
* **Trigger:**
|
||||
* **Manual:** File Save / Debounced Keystroke.
|
||||
* **AI-Aware Heuristic:** "Burst Write Cooldown" (See Section 3).
|
||||
* **Logic:**
|
||||
1. Identify modified symbols (Functions, Classes) via incremental Tree-sitter parsing.
|
||||
2. Execute Graph Query (Cypher) to find dependents.
|
||||
* `MATCH (modified)<-[:CALLS*1..5]-(affected) RETURN affected`
|
||||
3. Filter "affected" nodes that are outside the current file.
|
||||
* **User Experience:**
|
||||
* **Toast/Status Bar:** "⚠️ Modification affects 12 external files."
|
||||
* **Panel:** List of affected files/functions (e.g., "Breaks `PaymentService.process()`").
|
||||
* **"Fix Prompt" Generator:** Button to copy a prompt for the AI agent (e.g., "Check `AuthService.ts` for regressions caused by my changes to `Login.tsx`").
|
||||
* **Cost:** **Zero Tokens.** (Pure Graph Traversal).
|
||||
|
||||
### B. Architecture "Linting"
|
||||
* **Trigger:** File Save / New Import Added.
|
||||
* **Logic:**
|
||||
1. Detect new `IMPORTS` or `CALLS` edges in the graph.
|
||||
2. Check against defined "Layer Rules" (e.g., defined in `.gitnexus/rules.yaml`).
|
||||
* *Rule Example:* `Frontend` cannot import `Database`.
|
||||
3. Execute Graph Query:
|
||||
* `MATCH (source)-[:IMPORTS]->(target) WHERE source.layer = 'Frontend' AND target.layer = 'Database' RETURN source, target`
|
||||
* **User Experience:**
|
||||
* **Inline Warning:** "❌ Architectural Violation: UI component cannot directly access Database types."
|
||||
* **Cost:** **Zero Tokens.** (Rule-based Graph Matching).
|
||||
|
||||
### C. "Smart" Explanation (On Demand)
|
||||
* **Trigger:** User clicks "Explain Risk" on a warning.
|
||||
* **Logic:**
|
||||
1. Gather context: Source code of the change + Signatures of affected functions.
|
||||
2. Send structured prompt to LLM (Small model: GPT-4o-mini / Local Llama).
|
||||
3. *Prompt:* "The user modified `calculateTotal()`. This function is called by `InvoiceGenerator`. Explain potential risks."
|
||||
* **User Experience:**
|
||||
* Natural language summary: "Changing the return type of `calculateTotal` will cause a compilation error in `InvoiceGenerator` which expects a number."
|
||||
* **Cost:** **Low.** (Only on user request, highly targeted context).
|
||||
|
||||
## 3. Technical Architecture
|
||||
|
||||
### The "Watcher" Loop
|
||||
1. **File Watcher:** Listens for `change` events in the workspace.
|
||||
2. **AI Detection Heuristic (Burst Mode):**
|
||||
* *Logic:* Detect rapid file writes (3+ files in <2s) typical of AI Agents.
|
||||
* *Action:* Suppress immediate checks (don't spam). Wait for "Cooldown" (e.g., 5s silence).
|
||||
* *Trigger:* After cooldown, run "Full Impact Scan" and present summary.
|
||||
3. **Incremental Parser:** Updates the KuzuDB graph for the specific file (milliseconds).
|
||||
* *Note:* Uses Tree-sitter's incremental parsing to be extremely fast.
|
||||
3. **Graph Engine:** Runs pre-compiled Cypher queries against the updated graph.
|
||||
4. **Client Interface (MCP/Extension):**
|
||||
* Push notifications to VS Code / Cursor.
|
||||
* Update "Impact" panel in the browser UI.
|
||||
|
||||
## 4. Agent Integration (Mandatory Workflow)
|
||||
|
||||
To ensure AI agents (Cursor, Claude, etc.) utilize GitNexus, we leverage "Prompt Engineering via Context" and Tool Definitions.
|
||||
|
||||
### A. The `.cursorrules` Protocol
|
||||
* **Mechanism:** A `.cursorrules` file in the project root enforces agent behavior.
|
||||
* **Content:**
|
||||
```markdown
|
||||
# GitNexus Safety Protocol
|
||||
You have access to `@gitnexus` tools.
|
||||
|
||||
**MANDATORY WORKFLOW:**
|
||||
1. **PLANNING:** Before writing code, use `gitnexus_get_dependencies` to map the blast radius.
|
||||
2. **VERIFICATION:** After EVERY code modification, you MUST run `gitnexus_detect_impact`.
|
||||
3. **REPORTING:** If risks are found, fix them or report to user.
|
||||
```
|
||||
|
||||
### B. "Bossy" Tool Descriptions
|
||||
* **Mechanism:** Update MCP Tool definitions to be imperative.
|
||||
* **Description:** "CRITICAL SAFETY TOOL. MUST be used after modifying code. Scans graph for broken dependencies. Returns affected files."
|
||||
|
||||
### C. "Fix Prompt" Generator
|
||||
* **Mechanism:** If the Agent ignores the tools and the "Guardian" detects a break, the popup offers a "Copy Fix Prompt" button.
|
||||
* **Prompt:** "Your changes to X broke Y. Use `gitnexus_detect_impact` to verify and fix."
|
||||
|
||||
## 5. Distributed Knowledge Graph (Git-Native Architecture)
|
||||
|
||||
To enable "B2B / Team" features without a central server, we use Git itself as the synchronization mechanism for the Knowledge Graph. This is the **"Git-Native Knowledge Graph"** architecture.
|
||||
|
||||
### A. The Core Concept: "Graph Manifest"
|
||||
You cannot commit the raw KuzuDB database files (binaries) to Git. Instead, we use a lightweight, diff-friendly **Graph Manifest**.
|
||||
|
||||
* **File Path:** `.gitnexus/graph-state.jsonl.gz`
|
||||
* **Content:** A compressed JSON Lines dump of the Nodes and Edges (semantic data only).
|
||||
* **Purpose:** Acts as the "Transport Layer" for the graph between machines.
|
||||
|
||||
### B. The Workflow
|
||||
|
||||
#### Step 1: The "Write" Op (Local Dev)
|
||||
1. **Code Change:** Developer modifies `User.ts`.
|
||||
2. **Local Indexing:** GitNexus CLI updates local KuzuDB instantly (Incremental Update).
|
||||
3. **Pre-Commit Hook:**
|
||||
* Trigger: `git commit`
|
||||
* Action: GitNexus dumps the current KuzuDB state to `.gitnexus/graph-state.jsonl.gz`.
|
||||
* Optimization: Only dumps semantic data (e.g., "Func A calls Func B"), not the full AST, keeping it small.
|
||||
|
||||
#### Step 2: The "Transport" (Git Sync)
|
||||
* `git push` uploads the Code + Graph Manifest.
|
||||
* **Crucial:** The graph version is now cryptographically tied to the commit hash. No "drift" between code and graph.
|
||||
|
||||
#### Step 3: The "Read" Op (Teammate Pull)
|
||||
1. **Git Pull:** Teammate receives new code + new manifest.
|
||||
2. **Post-Merge Hook / Hydration:**
|
||||
* Trigger: Git detects change in `.gitnexus`.
|
||||
* Action: GitNexus CLI reads the manifest and **bulk-inserts** it into the local KuzuDB.
|
||||
* **Result:** Teammate has a fully queried graph in seconds (vs. minutes of re-parsing).
|
||||
|
||||
### C. Conflict Resolution: "Discard and Rebuild"
|
||||
What happens if two devs change the graph simultaneously?
|
||||
|
||||
* **Scenario:** Merge conflict in `.gitnexus/graph-state.jsonl.gz`.
|
||||
* **Strategy:**
|
||||
1. GitNexus detects the conflict in the manifest file.
|
||||
2. It **discards** the conflicted manifest.
|
||||
3. It runs the **Parser** locally on the *merged* source code (Source of Truth).
|
||||
4. It generates a **fresh, correct** manifest.
|
||||
* **Philosophy:** The Graph is a *derivative* of the Code. We never manually merge the graph; we regenerate it from the source.
|
||||
|
||||
### D. Architecture Diagram (Mermaid)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Developer A (Write)"
|
||||
CodeA[User.ts] -->|Parser| DB_A[(Local KuzuDB)]
|
||||
DB_A -->|Pre-Commit Export| ManifestA[.gitnexus/graph-state.gnx]
|
||||
ManifestA -->|git push| GitHub
|
||||
end
|
||||
|
||||
subgraph "GitHub / Git Server"
|
||||
GitHub -->|git pull| DevB_Repo
|
||||
end
|
||||
|
||||
subgraph "Developer B (Read)"
|
||||
DevB_Repo[Code + Manifest] -->|Hydration Hook| DB_B[(Local KuzuDB)]
|
||||
ManifestA -->|Bulk Insert| DB_B
|
||||
DB_B -->|Instant Query| Cursor_B[Cursor / IDE]
|
||||
end
|
||||
|
||||
subgraph "Enterprise Hub (Monetization)"
|
||||
GitHub -->|Webhook| HubServer[Node.js Hub]
|
||||
HubServer -->|Download Manifest| CentralDB[(Neo4j / Postgres)]
|
||||
CentralDB -->|Analytics API| Dashboard[CTO Dashboard]
|
||||
end
|
||||
```
|
||||
|
||||
### E. B2B / Enterprise "Hub" Integration
|
||||
The "Hub" is a lightweight server that monetizes this architecture.
|
||||
1. **Action:** Subscribes to the repo's webhooks.
|
||||
2. **Ingestion:** Downloads *only* the manifest file (not the source code).
|
||||
3. **Storage:** Loads it into a centralized DB (Neo4j/Postgres) for organization-wide queries.
|
||||
4. **Security:** "We don't see your code, only your graph structure."
|
||||
|
||||
|
||||
## 7. Implementation Roadmap
|
||||
1. **Phase 1:** Implement `FileWatcher` in CLI + Incremental Graph Update.
|
||||
2. **Phase 2:** Create `ImpactQuery` engine (Cypher queries for dependents).
|
||||
3. **Phase 3:** Build MCP Tool `get_live_impact` for Editor integration and "Bossy" descriptions.
|
||||
4. **Phase 4:** Implement Git-Native Graph Sync (Manifest generation + Pre-commit hook).
|
||||
5. **Phase 5:** Add Architecture Rule definition schema (`.gitnexus/rules.yaml`).
|
||||
|
||||
Loading…
Add table
Reference in a new issue