@yuaone/core 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-loop.d.ts +62 -0
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +705 -18
- package/dist/agent-loop.js.map +1 -1
- package/dist/background-agent.d.ts +110 -0
- package/dist/background-agent.d.ts.map +1 -0
- package/dist/background-agent.js +255 -0
- package/dist/background-agent.js.map +1 -0
- package/dist/coding-standards.d.ts +45 -0
- package/dist/coding-standards.d.ts.map +1 -0
- package/dist/coding-standards.js +1152 -0
- package/dist/coding-standards.js.map +1 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -6
- package/dist/constants.js.map +1 -1
- package/dist/context-manager.d.ts +6 -0
- package/dist/context-manager.d.ts.map +1 -1
- package/dist/context-manager.js +23 -4
- package/dist/context-manager.js.map +1 -1
- package/dist/index.d.ts +28 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -2
- package/dist/index.js.map +1 -1
- package/dist/llm-client.d.ts +8 -3
- package/dist/llm-client.d.ts.map +1 -1
- package/dist/llm-client.js +64 -13
- package/dist/llm-client.js.map +1 -1
- package/dist/plugin-auto-loader.d.ts +108 -0
- package/dist/plugin-auto-loader.d.ts.map +1 -0
- package/dist/plugin-auto-loader.js +743 -0
- package/dist/plugin-auto-loader.js.map +1 -0
- package/dist/plugin-registry.d.ts +112 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +319 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/plugin-types.d.ts +388 -0
- package/dist/plugin-types.d.ts.map +1 -0
- package/dist/plugin-types.js +8 -0
- package/dist/plugin-types.js.map +1 -0
- package/dist/plugin-validator.d.ts +54 -0
- package/dist/plugin-validator.d.ts.map +1 -0
- package/dist/plugin-validator.js +129 -0
- package/dist/plugin-validator.js.map +1 -0
- package/dist/repo-knowledge-graph.d.ts +112 -0
- package/dist/repo-knowledge-graph.d.ts.map +1 -0
- package/dist/repo-knowledge-graph.js +561 -0
- package/dist/repo-knowledge-graph.js.map +1 -0
- package/dist/role-registry.js +1 -1
- package/dist/role-registry.js.map +1 -1
- package/dist/self-debug-loop.d.ts +257 -0
- package/dist/self-debug-loop.d.ts.map +1 -0
- package/dist/self-debug-loop.js +870 -0
- package/dist/self-debug-loop.js.map +1 -0
- package/dist/skill-learner.d.ts +136 -0
- package/dist/skill-learner.d.ts.map +1 -0
- package/dist/skill-learner.js +382 -0
- package/dist/skill-learner.js.map +1 -0
- package/dist/skill-loader.d.ts +90 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +309 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/specialist-registry.d.ts +132 -0
- package/dist/specialist-registry.d.ts.map +1 -0
- package/dist/specialist-registry.js +413 -0
- package/dist/specialist-registry.js.map +1 -0
- package/dist/sub-agent-prompts.d.ts +45 -0
- package/dist/sub-agent-prompts.d.ts.map +1 -0
- package/dist/sub-agent-prompts.js +177 -0
- package/dist/sub-agent-prompts.js.map +1 -0
- package/dist/sub-agent-router.d.ts +75 -0
- package/dist/sub-agent-router.d.ts.map +1 -0
- package/dist/sub-agent-router.js +174 -0
- package/dist/sub-agent-router.js.map +1 -0
- package/dist/sub-agent.d.ts +48 -0
- package/dist/sub-agent.d.ts.map +1 -1
- package/dist/sub-agent.js +108 -5
- package/dist/sub-agent.js.map +1 -1
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +177 -7
- package/dist/system-prompt.js.map +1 -1
- package/dist/task-classifier.d.ts +25 -1
- package/dist/task-classifier.d.ts.map +1 -1
- package/dist/task-classifier.js +171 -1
- package/dist/task-classifier.js.map +1 -1
- package/dist/tool-planner.d.ts +160 -0
- package/dist/tool-planner.d.ts.map +1 -0
- package/dist/tool-planner.js +501 -0
- package/dist/tool-planner.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/world-state.d.ts.map +1 -1
- package/dist/world-state.js +8 -1
- package/dist/world-state.js.map +1 -1
- package/package.json +2 -1
- package/plugins/git/patterns/branch-patterns.json +101 -0
- package/plugins/git/patterns/commit-patterns.json +186 -0
- package/plugins/git/plugin.yaml +128 -0
- package/plugins/git/skills/branch-strategy.md +172 -0
- package/plugins/git/skills/commit-conv.md +178 -0
- package/plugins/git/skills/conflict-resolve.md +159 -0
- package/plugins/git/skills/history-clean.md +199 -0
- package/plugins/git/skills/pr-review.md +196 -0
- package/plugins/git/strategies/conflict-resolve.json +244 -0
- package/plugins/git/strategies/release-flow.json +292 -0
- package/plugins/git/validators/rules.json +348 -0
- package/plugins/react/patterns/anti-patterns.json +88 -0
- package/plugins/react/patterns/components.json +80 -0
- package/plugins/react/patterns/hooks.json +72 -0
- package/plugins/react/plugin.yaml +229 -0
- package/plugins/react/skills/bugfix.md +208 -0
- package/plugins/react/skills/component-gen.md +206 -0
- package/plugins/react/skills/hook-extract.md +208 -0
- package/plugins/react/skills/ssr.md +256 -0
- package/plugins/react/skills/test.md +273 -0
- package/plugins/react/strategies/build-fix.json +43 -0
- package/plugins/react/strategies/hook-loop-fix.json +36 -0
- package/plugins/react/strategies/hydration-fix.json +42 -0
- package/plugins/react/validators/rules.json +92 -0
- package/plugins/typescript/patterns/best-practices.json +25 -0
- package/plugins/typescript/patterns/common-errors.json +32 -0
- package/plugins/typescript/plugin.yaml +74 -0
- package/plugins/typescript/skills/debug.md +23 -0
- package/plugins/typescript/skills/migration.md +24 -0
- package/plugins/typescript/skills/refactor.md +22 -0
- package/plugins/typescript/skills/strict-mode.md +23 -0
- package/plugins/typescript/strategies/strict-migration.json +37 -0
- package/plugins/typescript/strategies/type-error-fix.json +37 -0
- package/plugins/typescript/validators/rules.json +28 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module repo-knowledge-graph
|
|
3
|
+
* @description Repo Knowledge Graph — Code structure as a queryable graph.
|
|
4
|
+
*
|
|
5
|
+
* Nodes: File, Class, Function, Variable, Interface, Type, Module
|
|
6
|
+
* Edges: imports, calls, extends, implements, depends_on, exports, contains
|
|
7
|
+
*
|
|
8
|
+
* Enables: impact analysis, refactor safety, dependency tracing, dead code detection.
|
|
9
|
+
*
|
|
10
|
+
* Uses lightweight regex-based parsing (not full AST) for speed.
|
|
11
|
+
*/
|
|
12
|
+
export type NodeType = "file" | "class" | "function" | "variable" | "interface" | "type" | "module";
|
|
13
|
+
export type EdgeType = "imports" | "calls" | "extends" | "implements" | "depends_on" | "exports" | "contains";
|
|
14
|
+
export interface GraphNode {
|
|
15
|
+
/** Unique: "file:src/index.ts" or "fn:src/index.ts:myFunc" */
|
|
16
|
+
id: string;
|
|
17
|
+
type: NodeType;
|
|
18
|
+
name: string;
|
|
19
|
+
filePath: string;
|
|
20
|
+
line?: number;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface GraphEdge {
|
|
24
|
+
from: string;
|
|
25
|
+
to: string;
|
|
26
|
+
type: EdgeType;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export interface ImpactReport {
|
|
30
|
+
/** Directly affected nodes */
|
|
31
|
+
direct: GraphNode[];
|
|
32
|
+
/** Transitively affected (2+ hops) */
|
|
33
|
+
transitive: GraphNode[];
|
|
34
|
+
/** Total impact radius */
|
|
35
|
+
radius: number;
|
|
36
|
+
/** Risk level based on affected node count */
|
|
37
|
+
risk: "low" | "medium" | "high" | "critical";
|
|
38
|
+
/** Files that need to be checked/updated */
|
|
39
|
+
affectedFiles: string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* RepoKnowledgeGraph — queryable code structure graph.
|
|
43
|
+
*
|
|
44
|
+
* Builds a lightweight graph from TypeScript/JavaScript source files
|
|
45
|
+
* using regex-based parsing. Supports impact analysis, dead code detection,
|
|
46
|
+
* dependency tracing, and incremental updates.
|
|
47
|
+
*/
|
|
48
|
+
export declare class RepoKnowledgeGraph {
|
|
49
|
+
private nodes;
|
|
50
|
+
private edges;
|
|
51
|
+
private storagePath;
|
|
52
|
+
constructor(projectRoot: string);
|
|
53
|
+
/**
|
|
54
|
+
* Load persisted graph from disk.
|
|
55
|
+
*/
|
|
56
|
+
init(): Promise<void>;
|
|
57
|
+
/** Add a node to the graph */
|
|
58
|
+
addNode(node: GraphNode): void;
|
|
59
|
+
/** Add an edge (deduplicates) */
|
|
60
|
+
addEdge(edge: GraphEdge): void;
|
|
61
|
+
/** Find all callers of a function/method */
|
|
62
|
+
findCallers(nodeId: string): GraphNode[];
|
|
63
|
+
/** Find all dependents of a file/module */
|
|
64
|
+
findDependents(nodeId: string): GraphNode[];
|
|
65
|
+
/** Analyze impact of changing a node (BFS traversal) */
|
|
66
|
+
analyzeImpact(nodeId: string, maxDepth?: number): ImpactReport;
|
|
67
|
+
/**
|
|
68
|
+
* Find dead code — nodes with no incoming edges (except file nodes).
|
|
69
|
+
* File nodes are always "alive". Only exported symbols that are never
|
|
70
|
+
* imported elsewhere are considered dead.
|
|
71
|
+
*/
|
|
72
|
+
findDeadCode(): GraphNode[];
|
|
73
|
+
/** Get subgraph for a specific file */
|
|
74
|
+
getFileGraph(filePath: string): {
|
|
75
|
+
nodes: GraphNode[];
|
|
76
|
+
edges: GraphEdge[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Build graph from TypeScript source files (regex-based parsing).
|
|
80
|
+
* Scans .ts/.tsx files under projectRoot, extracts imports, exports,
|
|
81
|
+
* classes, functions, interfaces, and types.
|
|
82
|
+
*/
|
|
83
|
+
buildFromProject(projectRoot: string, filePatterns?: string[]): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Incrementally update graph for changed files.
|
|
86
|
+
* Removes old nodes/edges for the file, then re-indexes.
|
|
87
|
+
*/
|
|
88
|
+
updateFiles(changedFiles: string[]): Promise<void>;
|
|
89
|
+
/** Persist graph to disk */
|
|
90
|
+
save(): Promise<void>;
|
|
91
|
+
/** Get graph statistics */
|
|
92
|
+
getStats(): {
|
|
93
|
+
nodes: number;
|
|
94
|
+
edges: number;
|
|
95
|
+
files: number;
|
|
96
|
+
};
|
|
97
|
+
/** Get a node by ID */
|
|
98
|
+
getNode(id: string): GraphNode | undefined;
|
|
99
|
+
/** Get all nodes */
|
|
100
|
+
getAllNodes(): GraphNode[];
|
|
101
|
+
/** Get all edges */
|
|
102
|
+
getAllEdges(): GraphEdge[];
|
|
103
|
+
private load;
|
|
104
|
+
private collectFiles;
|
|
105
|
+
private indexFile;
|
|
106
|
+
private parseImports;
|
|
107
|
+
private parseDeclarations;
|
|
108
|
+
private getLineNumber;
|
|
109
|
+
private findNodeByName;
|
|
110
|
+
private inferNodeType;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=repo-knowledge-graph.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repo-knowledge-graph.d.ts","sourceRoot":"","sources":["../src/repo-knowledge-graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module repo-knowledge-graph
|
|
3
|
+
* @description Repo Knowledge Graph — Code structure as a queryable graph.
|
|
4
|
+
*
|
|
5
|
+
* Nodes: File, Class, Function, Variable, Interface, Type, Module
|
|
6
|
+
* Edges: imports, calls, extends, implements, depends_on, exports, contains
|
|
7
|
+
*
|
|
8
|
+
* Enables: impact analysis, refactor safety, dependency tracing, dead code detection.
|
|
9
|
+
*
|
|
10
|
+
* Uses lightweight regex-based parsing (not full AST) for speed.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile, writeFile, readdir, stat, mkdir } from "node:fs/promises";
|
|
13
|
+
import { join, relative, dirname, extname, basename } from "node:path";
|
|
14
|
+
// ─── Regex Patterns for Parsing ───
|
|
15
|
+
const IMPORT_PATTERN = /^import\s+(?:(?:type\s+)?(?:\{[^}]*\}|[\w*]+(?:\s+as\s+\w+)?|\*\s+as\s+\w+)(?:\s*,\s*(?:\{[^}]*\}|[\w*]+))*\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
16
|
+
const EXPORT_NAMED_PATTERN = /^export\s+(?:type\s+)?(?:interface|type|class|function|const|let|var|enum|abstract\s+class)\s+(\w+)/gm;
|
|
17
|
+
const EXPORT_DEFAULT_PATTERN = /^export\s+default\s+(?:class|function|abstract\s+class)\s+(\w+)/gm;
|
|
18
|
+
const CLASS_PATTERN = /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s]+))?/gm;
|
|
19
|
+
const FUNCTION_PATTERN = /^(?:export\s+)?(?:async\s+)?function\s+(\w+)/gm;
|
|
20
|
+
const INTERFACE_PATTERN = /^(?:export\s+)?interface\s+(\w+)(?:\s+extends\s+([\w,\s]+))?/gm;
|
|
21
|
+
const TYPE_ALIAS_PATTERN = /^(?:export\s+)?type\s+(\w+)\s*=/gm;
|
|
22
|
+
const CONST_EXPORT_PATTERN = /^(?:export\s+)?(?:const|let|var)\s+(\w+)/gm;
|
|
23
|
+
// ─── RepoKnowledgeGraph ───
|
|
24
|
+
/**
|
|
25
|
+
* RepoKnowledgeGraph — queryable code structure graph.
|
|
26
|
+
*
|
|
27
|
+
* Builds a lightweight graph from TypeScript/JavaScript source files
|
|
28
|
+
* using regex-based parsing. Supports impact analysis, dead code detection,
|
|
29
|
+
* dependency tracing, and incremental updates.
|
|
30
|
+
*/
|
|
31
|
+
export class RepoKnowledgeGraph {
|
|
32
|
+
nodes = new Map();
|
|
33
|
+
edges = [];
|
|
34
|
+
storagePath;
|
|
35
|
+
constructor(projectRoot) {
|
|
36
|
+
this.storagePath = join(projectRoot, ".yuan", "knowledge-graph.json");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load persisted graph from disk.
|
|
40
|
+
*/
|
|
41
|
+
async init() {
|
|
42
|
+
await this.load();
|
|
43
|
+
}
|
|
44
|
+
/** Add a node to the graph */
|
|
45
|
+
addNode(node) {
|
|
46
|
+
this.nodes.set(node.id, node);
|
|
47
|
+
}
|
|
48
|
+
/** Add an edge (deduplicates) */
|
|
49
|
+
addEdge(edge) {
|
|
50
|
+
const exists = this.edges.some((e) => e.from === edge.from && e.to === edge.to && e.type === edge.type);
|
|
51
|
+
if (!exists) {
|
|
52
|
+
this.edges.push(edge);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Find all callers of a function/method */
|
|
56
|
+
findCallers(nodeId) {
|
|
57
|
+
const callerIds = this.edges
|
|
58
|
+
.filter((e) => e.to === nodeId && e.type === "calls")
|
|
59
|
+
.map((e) => e.from);
|
|
60
|
+
return callerIds
|
|
61
|
+
.map((id) => this.nodes.get(id))
|
|
62
|
+
.filter((n) => n !== undefined);
|
|
63
|
+
}
|
|
64
|
+
/** Find all dependents of a file/module */
|
|
65
|
+
findDependents(nodeId) {
|
|
66
|
+
const depIds = new Set();
|
|
67
|
+
for (const edge of this.edges) {
|
|
68
|
+
if (edge.to === nodeId && (edge.type === "imports" || edge.type === "depends_on")) {
|
|
69
|
+
depIds.add(edge.from);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return Array.from(depIds)
|
|
73
|
+
.map((id) => this.nodes.get(id))
|
|
74
|
+
.filter((n) => n !== undefined);
|
|
75
|
+
}
|
|
76
|
+
/** Analyze impact of changing a node (BFS traversal) */
|
|
77
|
+
analyzeImpact(nodeId, maxDepth = 3) {
|
|
78
|
+
const direct = [];
|
|
79
|
+
const transitive = [];
|
|
80
|
+
const visited = new Set();
|
|
81
|
+
visited.add(nodeId);
|
|
82
|
+
const queue = [];
|
|
83
|
+
// Seed with direct dependents
|
|
84
|
+
for (const edge of this.edges) {
|
|
85
|
+
if (edge.to === nodeId &&
|
|
86
|
+
(edge.type === "imports" ||
|
|
87
|
+
edge.type === "depends_on" ||
|
|
88
|
+
edge.type === "calls" ||
|
|
89
|
+
edge.type === "extends" ||
|
|
90
|
+
edge.type === "implements")) {
|
|
91
|
+
if (!visited.has(edge.from)) {
|
|
92
|
+
visited.add(edge.from);
|
|
93
|
+
queue.push({ id: edge.from, depth: 1 });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
while (queue.length > 0) {
|
|
98
|
+
const item = queue.shift();
|
|
99
|
+
const node = this.nodes.get(item.id);
|
|
100
|
+
if (!node)
|
|
101
|
+
continue;
|
|
102
|
+
if (item.depth === 1) {
|
|
103
|
+
direct.push(node);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
transitive.push(node);
|
|
107
|
+
}
|
|
108
|
+
// Continue BFS if within depth limit
|
|
109
|
+
if (item.depth < maxDepth) {
|
|
110
|
+
for (const edge of this.edges) {
|
|
111
|
+
if (edge.to === item.id &&
|
|
112
|
+
!visited.has(edge.from) &&
|
|
113
|
+
(edge.type === "imports" ||
|
|
114
|
+
edge.type === "depends_on" ||
|
|
115
|
+
edge.type === "calls")) {
|
|
116
|
+
visited.add(edge.from);
|
|
117
|
+
queue.push({ id: edge.from, depth: item.depth + 1 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Collect unique affected files
|
|
123
|
+
const affectedFiles = new Set();
|
|
124
|
+
for (const n of [...direct, ...transitive]) {
|
|
125
|
+
affectedFiles.add(n.filePath);
|
|
126
|
+
}
|
|
127
|
+
const totalAffected = direct.length + transitive.length;
|
|
128
|
+
let risk;
|
|
129
|
+
if (totalAffected === 0)
|
|
130
|
+
risk = "low";
|
|
131
|
+
else if (totalAffected <= 3)
|
|
132
|
+
risk = "medium";
|
|
133
|
+
else if (totalAffected <= 10)
|
|
134
|
+
risk = "high";
|
|
135
|
+
else
|
|
136
|
+
risk = "critical";
|
|
137
|
+
return {
|
|
138
|
+
direct,
|
|
139
|
+
transitive,
|
|
140
|
+
radius: visited.size - 1,
|
|
141
|
+
risk,
|
|
142
|
+
affectedFiles: Array.from(affectedFiles),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Find dead code — nodes with no incoming edges (except file nodes).
|
|
147
|
+
* File nodes are always "alive". Only exported symbols that are never
|
|
148
|
+
* imported elsewhere are considered dead.
|
|
149
|
+
*/
|
|
150
|
+
findDeadCode() {
|
|
151
|
+
const nodesWithIncoming = new Set();
|
|
152
|
+
for (const edge of this.edges) {
|
|
153
|
+
nodesWithIncoming.add(edge.to);
|
|
154
|
+
}
|
|
155
|
+
const dead = [];
|
|
156
|
+
for (const [id, node] of this.nodes) {
|
|
157
|
+
// Skip file/module nodes — they're always roots
|
|
158
|
+
if (node.type === "file" || node.type === "module")
|
|
159
|
+
continue;
|
|
160
|
+
if (!nodesWithIncoming.has(id)) {
|
|
161
|
+
dead.push(node);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return dead;
|
|
165
|
+
}
|
|
166
|
+
/** Get subgraph for a specific file */
|
|
167
|
+
getFileGraph(filePath) {
|
|
168
|
+
const fileNodes = [];
|
|
169
|
+
const fileEdges = [];
|
|
170
|
+
const nodeIds = new Set();
|
|
171
|
+
for (const [id, node] of this.nodes) {
|
|
172
|
+
if (node.filePath === filePath) {
|
|
173
|
+
fileNodes.push(node);
|
|
174
|
+
nodeIds.add(id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const edge of this.edges) {
|
|
178
|
+
if (nodeIds.has(edge.from) || nodeIds.has(edge.to)) {
|
|
179
|
+
fileEdges.push(edge);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { nodes: fileNodes, edges: fileEdges };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build graph from TypeScript source files (regex-based parsing).
|
|
186
|
+
* Scans .ts/.tsx files under projectRoot, extracts imports, exports,
|
|
187
|
+
* classes, functions, interfaces, and types.
|
|
188
|
+
*/
|
|
189
|
+
async buildFromProject(projectRoot, filePatterns) {
|
|
190
|
+
const extensions = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
191
|
+
const ignorePatterns = [
|
|
192
|
+
"node_modules",
|
|
193
|
+
"dist",
|
|
194
|
+
".git",
|
|
195
|
+
".next",
|
|
196
|
+
"coverage",
|
|
197
|
+
"__pycache__",
|
|
198
|
+
];
|
|
199
|
+
const files = await this.collectFiles(projectRoot, extensions, ignorePatterns);
|
|
200
|
+
// Apply file pattern filter if provided
|
|
201
|
+
const filtered = filePatterns
|
|
202
|
+
? files.filter((f) => filePatterns.some((p) => {
|
|
203
|
+
try {
|
|
204
|
+
return new RegExp(p).test(f);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return f.includes(p);
|
|
208
|
+
}
|
|
209
|
+
}))
|
|
210
|
+
: files;
|
|
211
|
+
for (const filePath of filtered) {
|
|
212
|
+
await this.indexFile(projectRoot, filePath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Incrementally update graph for changed files.
|
|
217
|
+
* Removes old nodes/edges for the file, then re-indexes.
|
|
218
|
+
*/
|
|
219
|
+
async updateFiles(changedFiles) {
|
|
220
|
+
for (const filePath of changedFiles) {
|
|
221
|
+
// Remove old nodes for this file
|
|
222
|
+
const oldNodeIds = [];
|
|
223
|
+
for (const [id, node] of this.nodes) {
|
|
224
|
+
if (node.filePath === filePath) {
|
|
225
|
+
oldNodeIds.push(id);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const id of oldNodeIds) {
|
|
229
|
+
this.nodes.delete(id);
|
|
230
|
+
}
|
|
231
|
+
// Remove old edges involving these nodes
|
|
232
|
+
const oldIdSet = new Set(oldNodeIds);
|
|
233
|
+
this.edges = this.edges.filter((e) => !oldIdSet.has(e.from) && !oldIdSet.has(e.to));
|
|
234
|
+
// Determine project root from storage path
|
|
235
|
+
const projectRoot = dirname(dirname(this.storagePath)); // .yuan/../..
|
|
236
|
+
// Re-index if file still exists
|
|
237
|
+
try {
|
|
238
|
+
await stat(filePath);
|
|
239
|
+
await this.indexFile(projectRoot, filePath);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// File was deleted — removal above is sufficient
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Persist graph to disk */
|
|
247
|
+
async save() {
|
|
248
|
+
const dir = dirname(this.storagePath);
|
|
249
|
+
await mkdir(dir, { recursive: true });
|
|
250
|
+
const data = {
|
|
251
|
+
nodes: Array.from(this.nodes.values()),
|
|
252
|
+
edges: this.edges,
|
|
253
|
+
version: 1,
|
|
254
|
+
updatedAt: Date.now(),
|
|
255
|
+
};
|
|
256
|
+
await writeFile(this.storagePath, JSON.stringify(data), "utf-8");
|
|
257
|
+
}
|
|
258
|
+
/** Get graph statistics */
|
|
259
|
+
getStats() {
|
|
260
|
+
const files = new Set();
|
|
261
|
+
for (const node of this.nodes.values()) {
|
|
262
|
+
files.add(node.filePath);
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
nodes: this.nodes.size,
|
|
266
|
+
edges: this.edges.length,
|
|
267
|
+
files: files.size,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** Get a node by ID */
|
|
271
|
+
getNode(id) {
|
|
272
|
+
return this.nodes.get(id);
|
|
273
|
+
}
|
|
274
|
+
/** Get all nodes */
|
|
275
|
+
getAllNodes() {
|
|
276
|
+
return Array.from(this.nodes.values());
|
|
277
|
+
}
|
|
278
|
+
/** Get all edges */
|
|
279
|
+
getAllEdges() {
|
|
280
|
+
return [...this.edges];
|
|
281
|
+
}
|
|
282
|
+
// ─── Private: Persistence ───
|
|
283
|
+
async load() {
|
|
284
|
+
try {
|
|
285
|
+
const raw = await readFile(this.storagePath, "utf-8");
|
|
286
|
+
const data = JSON.parse(raw);
|
|
287
|
+
this.nodes.clear();
|
|
288
|
+
this.edges = [];
|
|
289
|
+
for (const node of data.nodes) {
|
|
290
|
+
this.nodes.set(node.id, node);
|
|
291
|
+
}
|
|
292
|
+
this.edges = data.edges ?? [];
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// No persisted graph — start fresh
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// ─── Private: File Collection ───
|
|
299
|
+
async collectFiles(dir, extensions, ignore) {
|
|
300
|
+
const results = [];
|
|
301
|
+
try {
|
|
302
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
if (ignore.includes(entry.name))
|
|
305
|
+
continue;
|
|
306
|
+
const fullPath = join(dir, entry.name);
|
|
307
|
+
if (entry.isDirectory()) {
|
|
308
|
+
const sub = await this.collectFiles(fullPath, extensions, ignore);
|
|
309
|
+
results.push(...sub);
|
|
310
|
+
}
|
|
311
|
+
else if (entry.isFile() && extensions.has(extname(entry.name))) {
|
|
312
|
+
results.push(fullPath);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Permission error or similar — skip
|
|
318
|
+
}
|
|
319
|
+
return results;
|
|
320
|
+
}
|
|
321
|
+
// ─── Private: Indexing ───
|
|
322
|
+
async indexFile(projectRoot, filePath) {
|
|
323
|
+
let content;
|
|
324
|
+
try {
|
|
325
|
+
content = await readFile(filePath, "utf-8");
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const relPath = relative(projectRoot, filePath);
|
|
331
|
+
const fileId = `file:${relPath}`;
|
|
332
|
+
// Add file node
|
|
333
|
+
this.addNode({
|
|
334
|
+
id: fileId,
|
|
335
|
+
type: "file",
|
|
336
|
+
name: basename(filePath),
|
|
337
|
+
filePath: relPath,
|
|
338
|
+
});
|
|
339
|
+
// Parse imports
|
|
340
|
+
this.parseImports(content, relPath, fileId, projectRoot);
|
|
341
|
+
// Parse exports and declarations
|
|
342
|
+
this.parseDeclarations(content, relPath, fileId);
|
|
343
|
+
}
|
|
344
|
+
parseImports(content, relPath, fileId, _projectRoot) {
|
|
345
|
+
// Reset lastIndex for global regex
|
|
346
|
+
IMPORT_PATTERN.lastIndex = 0;
|
|
347
|
+
let match;
|
|
348
|
+
while ((match = IMPORT_PATTERN.exec(content)) !== null) {
|
|
349
|
+
const importPath = match[1];
|
|
350
|
+
// Resolve relative imports to file nodes
|
|
351
|
+
if (importPath.startsWith(".")) {
|
|
352
|
+
const dir = dirname(relPath);
|
|
353
|
+
let resolved = join(dir, importPath);
|
|
354
|
+
// Normalize extension
|
|
355
|
+
if (!extname(resolved)) {
|
|
356
|
+
resolved += ".ts"; // Default assumption
|
|
357
|
+
}
|
|
358
|
+
// Remove leading ./ and normalize
|
|
359
|
+
resolved = resolved.replace(/\\/g, "/");
|
|
360
|
+
const targetId = `file:${resolved}`;
|
|
361
|
+
this.addEdge({
|
|
362
|
+
from: fileId,
|
|
363
|
+
to: targetId,
|
|
364
|
+
type: "imports",
|
|
365
|
+
metadata: { importPath },
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
// External module — create module node
|
|
370
|
+
const moduleId = `module:${importPath}`;
|
|
371
|
+
if (!this.nodes.has(moduleId)) {
|
|
372
|
+
this.addNode({
|
|
373
|
+
id: moduleId,
|
|
374
|
+
type: "module",
|
|
375
|
+
name: importPath,
|
|
376
|
+
filePath: "",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
this.addEdge({
|
|
380
|
+
from: fileId,
|
|
381
|
+
to: moduleId,
|
|
382
|
+
type: "imports",
|
|
383
|
+
metadata: { importPath },
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
parseDeclarations(content, relPath, fileId) {
|
|
389
|
+
const lines = content.split("\n");
|
|
390
|
+
// Track which patterns we're using with their node types
|
|
391
|
+
const patterns = [
|
|
392
|
+
{ regex: CLASS_PATTERN, nodeType: "class", prefix: "class" },
|
|
393
|
+
{ regex: FUNCTION_PATTERN, nodeType: "function", prefix: "fn" },
|
|
394
|
+
{ regex: INTERFACE_PATTERN, nodeType: "interface", prefix: "iface" },
|
|
395
|
+
{ regex: TYPE_ALIAS_PATTERN, nodeType: "type", prefix: "type" },
|
|
396
|
+
];
|
|
397
|
+
for (const { regex, nodeType, prefix } of patterns) {
|
|
398
|
+
regex.lastIndex = 0;
|
|
399
|
+
let match;
|
|
400
|
+
while ((match = regex.exec(content)) !== null) {
|
|
401
|
+
const name = match[1];
|
|
402
|
+
const nodeId = `${prefix}:${relPath}:${name}`;
|
|
403
|
+
const line = this.getLineNumber(content, match.index);
|
|
404
|
+
this.addNode({
|
|
405
|
+
id: nodeId,
|
|
406
|
+
type: nodeType,
|
|
407
|
+
name,
|
|
408
|
+
filePath: relPath,
|
|
409
|
+
line,
|
|
410
|
+
});
|
|
411
|
+
// File contains this declaration
|
|
412
|
+
this.addEdge({
|
|
413
|
+
from: fileId,
|
|
414
|
+
to: nodeId,
|
|
415
|
+
type: "contains",
|
|
416
|
+
});
|
|
417
|
+
// Handle class extends
|
|
418
|
+
if (nodeType === "class" && match[2]) {
|
|
419
|
+
const parentName = match[2].trim();
|
|
420
|
+
// Create a tentative edge — target might be resolved later
|
|
421
|
+
this.addEdge({
|
|
422
|
+
from: nodeId,
|
|
423
|
+
to: `class:*:${parentName}`,
|
|
424
|
+
type: "extends",
|
|
425
|
+
metadata: { parentName },
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
// Handle class implements
|
|
429
|
+
if (nodeType === "class" && match[3]) {
|
|
430
|
+
const ifaces = match[3]
|
|
431
|
+
.split(",")
|
|
432
|
+
.map((s) => s.trim())
|
|
433
|
+
.filter(Boolean);
|
|
434
|
+
for (const iface of ifaces) {
|
|
435
|
+
this.addEdge({
|
|
436
|
+
from: nodeId,
|
|
437
|
+
to: `iface:*:${iface}`,
|
|
438
|
+
type: "implements",
|
|
439
|
+
metadata: { interfaceName: iface },
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Handle interface extends
|
|
444
|
+
if (nodeType === "interface" && match[2]) {
|
|
445
|
+
const parents = match[2]
|
|
446
|
+
.split(",")
|
|
447
|
+
.map((s) => s.trim())
|
|
448
|
+
.filter(Boolean);
|
|
449
|
+
for (const parent of parents) {
|
|
450
|
+
this.addEdge({
|
|
451
|
+
from: nodeId,
|
|
452
|
+
to: `iface:*:${parent}`,
|
|
453
|
+
type: "extends",
|
|
454
|
+
metadata: { parentName: parent },
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// Parse exported constants/variables
|
|
461
|
+
CONST_EXPORT_PATTERN.lastIndex = 0;
|
|
462
|
+
let constMatch;
|
|
463
|
+
while ((constMatch = CONST_EXPORT_PATTERN.exec(content)) !== null) {
|
|
464
|
+
const name = constMatch[1];
|
|
465
|
+
// Only track exports (not internal variables)
|
|
466
|
+
if (constMatch[0].startsWith("export")) {
|
|
467
|
+
const nodeId = `var:${relPath}:${name}`;
|
|
468
|
+
const line = this.getLineNumber(content, constMatch.index);
|
|
469
|
+
this.addNode({
|
|
470
|
+
id: nodeId,
|
|
471
|
+
type: "variable",
|
|
472
|
+
name,
|
|
473
|
+
filePath: relPath,
|
|
474
|
+
line,
|
|
475
|
+
});
|
|
476
|
+
this.addEdge({
|
|
477
|
+
from: fileId,
|
|
478
|
+
to: nodeId,
|
|
479
|
+
type: "exports",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// Parse export default
|
|
484
|
+
EXPORT_DEFAULT_PATTERN.lastIndex = 0;
|
|
485
|
+
let defaultMatch;
|
|
486
|
+
while ((defaultMatch = EXPORT_DEFAULT_PATTERN.exec(content)) !== null) {
|
|
487
|
+
const name = defaultMatch[1];
|
|
488
|
+
const nodeId = `default:${relPath}:${name}`;
|
|
489
|
+
const line = this.getLineNumber(content, defaultMatch.index);
|
|
490
|
+
this.addNode({
|
|
491
|
+
id: nodeId,
|
|
492
|
+
type: "class",
|
|
493
|
+
name,
|
|
494
|
+
filePath: relPath,
|
|
495
|
+
line,
|
|
496
|
+
metadata: { isDefault: true },
|
|
497
|
+
});
|
|
498
|
+
this.addEdge({
|
|
499
|
+
from: fileId,
|
|
500
|
+
to: nodeId,
|
|
501
|
+
type: "exports",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
// Parse named exports
|
|
505
|
+
EXPORT_NAMED_PATTERN.lastIndex = 0;
|
|
506
|
+
let namedMatch;
|
|
507
|
+
while ((namedMatch = EXPORT_NAMED_PATTERN.exec(content)) !== null) {
|
|
508
|
+
const name = namedMatch[1];
|
|
509
|
+
// Check if we already have this node (from class/function parsing above)
|
|
510
|
+
const existingId = this.findNodeByName(relPath, name);
|
|
511
|
+
const nodeId = existingId ?? `export:${relPath}:${name}`;
|
|
512
|
+
if (!existingId) {
|
|
513
|
+
const line = this.getLineNumber(content, namedMatch.index);
|
|
514
|
+
this.addNode({
|
|
515
|
+
id: nodeId,
|
|
516
|
+
type: this.inferNodeType(namedMatch[0]),
|
|
517
|
+
name,
|
|
518
|
+
filePath: relPath,
|
|
519
|
+
line,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
this.addEdge({
|
|
523
|
+
from: fileId,
|
|
524
|
+
to: nodeId,
|
|
525
|
+
type: "exports",
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// ─── Private: Utilities ───
|
|
530
|
+
getLineNumber(content, index) {
|
|
531
|
+
let line = 1;
|
|
532
|
+
for (let i = 0; i < index; i++) {
|
|
533
|
+
if (content[i] === "\n")
|
|
534
|
+
line++;
|
|
535
|
+
}
|
|
536
|
+
return line;
|
|
537
|
+
}
|
|
538
|
+
findNodeByName(filePath, name) {
|
|
539
|
+
const prefixes = ["class", "fn", "iface", "type", "var", "default"];
|
|
540
|
+
for (const prefix of prefixes) {
|
|
541
|
+
const id = `${prefix}:${filePath}:${name}`;
|
|
542
|
+
if (this.nodes.has(id))
|
|
543
|
+
return id;
|
|
544
|
+
}
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
inferNodeType(declaration) {
|
|
548
|
+
if (/\bclass\b/.test(declaration))
|
|
549
|
+
return "class";
|
|
550
|
+
if (/\bfunction\b/.test(declaration))
|
|
551
|
+
return "function";
|
|
552
|
+
if (/\binterface\b/.test(declaration))
|
|
553
|
+
return "interface";
|
|
554
|
+
if (/\btype\b/.test(declaration))
|
|
555
|
+
return "type";
|
|
556
|
+
if (/\b(?:const|let|var)\b/.test(declaration))
|
|
557
|
+
return "variable";
|
|
558
|
+
return "variable";
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
//# sourceMappingURL=repo-knowledge-graph.js.map
|