@mr-jones123/toji 0.1.1
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/README.md +158 -0
- package/package.json +47 -0
- package/packages/toji-comms/README.md +71 -0
- package/packages/toji-comms/src/cli/agents.ts +121 -0
- package/packages/toji-comms/src/cli/mmx.ts +65 -0
- package/packages/toji-comms/src/cli/subprocess.ts +47 -0
- package/packages/toji-comms/src/comms/orchestrator.ts +92 -0
- package/packages/toji-comms/src/comms/prompt.ts +84 -0
- package/packages/toji-comms/src/comms/store.ts +145 -0
- package/packages/toji-comms/src/comms/types.ts +94 -0
- package/packages/toji-comms/src/db/connection.ts +58 -0
- package/packages/toji-comms/src/db/migrations.ts +69 -0
- package/packages/toji-comms/src/index.ts +368 -0
- package/packages/toji-comms/src/mcp/client.ts +71 -0
- package/packages/toji-comms/src/mcp/server.ts +81 -0
- package/packages/toji-mem/README.md +52 -0
- package/packages/toji-mem/grammars/manifest.json +9 -0
- package/packages/toji-mem/grammars/tree-sitter-cpp.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-dart.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-java.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-javascript.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-python.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-tsx.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-typescript.wasm +0 -0
- package/packages/toji-mem/src/db/connection.ts +58 -0
- package/packages/toji-mem/src/db/migrations.ts +181 -0
- package/packages/toji-mem/src/index.ts +326 -0
- package/packages/toji-mem/src/indexer/file-walker.ts +45 -0
- package/packages/toji-mem/src/indexer/index-project.ts +277 -0
- package/packages/toji-mem/src/indexer/parsers/cpp.ts +81 -0
- package/packages/toji-mem/src/indexer/parsers/dart.ts +91 -0
- package/packages/toji-mem/src/indexer/parsers/java.ts +83 -0
- package/packages/toji-mem/src/indexer/parsers/python.ts +84 -0
- package/packages/toji-mem/src/indexer/parsers/registry.ts +28 -0
- package/packages/toji-mem/src/indexer/parsers/tree-sitter-loader.ts +39 -0
- package/packages/toji-mem/src/indexer/parsers/types.ts +48 -0
- package/packages/toji-mem/src/indexer/parsers/typescript.ts +105 -0
- package/packages/toji-mem/src/standards/store.ts +52 -0
- package/packages/toji-mem/src/tools/blast-radius.ts +98 -0
- package/packages/toji-mem/src/tools/graph-explore.ts +186 -0
- package/packages/toji-mem/src/tools/project-overview.ts +102 -0
- package/packages/toji-mem/src/tools/query-memory.ts +105 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Language, Parser, type Tree } from "web-tree-sitter";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
let initialized = false;
|
|
7
|
+
const languageCache = new Map<string, Language>();
|
|
8
|
+
|
|
9
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
10
|
+
|
|
11
|
+
export async function loadTreeSitterLanguage(grammarFile: string): Promise<Language> {
|
|
12
|
+
if (!initialized) {
|
|
13
|
+
await Parser.init();
|
|
14
|
+
initialized = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const grammarPath = join(packageRoot, "grammars", grammarFile);
|
|
18
|
+
const cached = languageCache.get(grammarPath);
|
|
19
|
+
if (cached) return cached;
|
|
20
|
+
|
|
21
|
+
if (!existsSync(grammarPath)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Missing tree-sitter grammar ${grammarFile}. Run: bun --cwd packages/toji-mem run download-grammars`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const language = await Language.load(grammarPath);
|
|
28
|
+
languageCache.set(grammarPath, language);
|
|
29
|
+
return language;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function parseWithGrammar(grammarFile: string, content: string): Promise<Tree> {
|
|
33
|
+
const language = await loadTreeSitterLanguage(grammarFile);
|
|
34
|
+
const parser = new Parser();
|
|
35
|
+
parser.setLanguage(language);
|
|
36
|
+
const tree = parser.parse(content);
|
|
37
|
+
if (!tree) throw new Error(`Failed to parse content with ${grammarFile}`);
|
|
38
|
+
return tree;
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type SymbolKind = "function" | "class" | "method" | "interface" | "type" | "variable";
|
|
2
|
+
|
|
3
|
+
export interface ParseInput {
|
|
4
|
+
projectId: number;
|
|
5
|
+
filePath: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ParsedSymbol {
|
|
10
|
+
name: string;
|
|
11
|
+
kind: SymbolKind;
|
|
12
|
+
startLine: number;
|
|
13
|
+
endLine: number;
|
|
14
|
+
exported: boolean;
|
|
15
|
+
signature?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ParsedImport {
|
|
19
|
+
importedName?: string;
|
|
20
|
+
source: string;
|
|
21
|
+
startLine: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ParsedCall {
|
|
25
|
+
callerName?: string;
|
|
26
|
+
calleeName: string;
|
|
27
|
+
startLine: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ParsedRelation {
|
|
31
|
+
fromName: string;
|
|
32
|
+
toName: string;
|
|
33
|
+
edgeType: "symbol_extends_symbol" | "symbol_implements_symbol";
|
|
34
|
+
startLine: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ParsedFile {
|
|
38
|
+
symbols: ParsedSymbol[];
|
|
39
|
+
imports: ParsedImport[];
|
|
40
|
+
calls: ParsedCall[];
|
|
41
|
+
relations: ParsedRelation[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LanguageParser {
|
|
45
|
+
language: string;
|
|
46
|
+
extensions: string[];
|
|
47
|
+
parseFile(input: ParseInput): Promise<ParsedFile>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Query } from "web-tree-sitter";
|
|
2
|
+
|
|
3
|
+
import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
|
|
4
|
+
import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
|
|
5
|
+
|
|
6
|
+
const typeScriptQuerySource = `
|
|
7
|
+
(function_declaration name: (identifier) @function.name) @function.definition
|
|
8
|
+
(class_declaration name: (type_identifier) @class.name) @class.definition
|
|
9
|
+
(method_definition name: (property_identifier) @method.name) @method.definition
|
|
10
|
+
(interface_declaration name: (type_identifier) @interface.name) @interface.definition
|
|
11
|
+
(type_alias_declaration name: (type_identifier) @type.name) @type.definition
|
|
12
|
+
(lexical_declaration (variable_declarator name: (identifier) @variable.name value: [(arrow_function) (function_expression)])) @variable.definition
|
|
13
|
+
(import_statement source: (string) @import.source) @import.statement
|
|
14
|
+
(call_expression function: (identifier) @call.name) @call.expression
|
|
15
|
+
(call_expression function: (member_expression property: (property_identifier) @call.member_name)) @call.expression
|
|
16
|
+
(class_declaration name: (type_identifier) @heritage.from (class_heritage (extends_clause value: (identifier) @extends.to)))
|
|
17
|
+
(class_declaration name: (type_identifier) @heritage.from (class_heritage (implements_clause (type_identifier) @implements.to)))
|
|
18
|
+
(interface_declaration name: (type_identifier) @heritage.from (extends_type_clause (type_identifier) @extends.to))
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const javaScriptQuerySource = `
|
|
22
|
+
(function_declaration name: (identifier) @function.name) @function.definition
|
|
23
|
+
(class_declaration name: (identifier) @class.name) @class.definition
|
|
24
|
+
(method_definition name: (property_identifier) @method.name) @method.definition
|
|
25
|
+
(lexical_declaration (variable_declarator name: (identifier) @variable.name value: [(arrow_function) (function_expression)])) @variable.definition
|
|
26
|
+
(import_statement source: (string) @import.source) @import.statement
|
|
27
|
+
(call_expression function: (identifier) @call.name) @call.expression
|
|
28
|
+
(call_expression function: (member_expression property: (property_identifier) @call.member_name)) @call.expression
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const symbolKinds = new Map<string, SymbolKind>([
|
|
32
|
+
["function.name", "function"],
|
|
33
|
+
["class.name", "class"],
|
|
34
|
+
["method.name", "method"],
|
|
35
|
+
["interface.name", "interface"],
|
|
36
|
+
["type.name", "type"],
|
|
37
|
+
["variable.name", "variable"],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export function createTypeScriptParser(language: string, grammarFile: string, extensions: string[]): LanguageParser {
|
|
41
|
+
return {
|
|
42
|
+
language,
|
|
43
|
+
extensions,
|
|
44
|
+
async parseFile(input): Promise<ParsedFile> {
|
|
45
|
+
const tree = await parseWithGrammar(grammarFile, input.content);
|
|
46
|
+
const grammar = await loadTreeSitterLanguage(grammarFile);
|
|
47
|
+
const query = new Query(grammar, language === "javascript" || language === "jsx" ? javaScriptQuerySource : typeScriptQuerySource);
|
|
48
|
+
const captures = query.captures(tree.rootNode);
|
|
49
|
+
|
|
50
|
+
const symbols: ParsedSymbol[] = [];
|
|
51
|
+
const imports: ParsedImport[] = [];
|
|
52
|
+
const calls: ParsedCall[] = [];
|
|
53
|
+
const relations: ParsedRelation[] = [];
|
|
54
|
+
let heritageFrom: string | undefined;
|
|
55
|
+
|
|
56
|
+
for (const capture of captures) {
|
|
57
|
+
const kind = symbolKinds.get(capture.name);
|
|
58
|
+
if (kind) {
|
|
59
|
+
const parent = capture.node.parent;
|
|
60
|
+
symbols.push({
|
|
61
|
+
name: capture.node.text,
|
|
62
|
+
kind,
|
|
63
|
+
startLine: capture.node.startPosition.row + 1,
|
|
64
|
+
endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
|
|
65
|
+
exported: Boolean(parent?.parent?.type.includes("export")),
|
|
66
|
+
signature: parent?.text.split("\n")[0]?.trim(),
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (capture.name === "import.source") {
|
|
72
|
+
imports.push({
|
|
73
|
+
source: capture.node.text.replaceAll("'", "").replaceAll('"', ""),
|
|
74
|
+
startLine: capture.node.startPosition.row + 1,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (capture.name === "call.name" || capture.name === "call.member_name") {
|
|
80
|
+
calls.push({
|
|
81
|
+
calleeName: capture.node.text,
|
|
82
|
+
startLine: capture.node.startPosition.row + 1,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (capture.name === "heritage.from") {
|
|
88
|
+
heritageFrom = capture.node.text;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if ((capture.name === "extends.to" || capture.name === "implements.to") && heritageFrom) {
|
|
93
|
+
relations.push({
|
|
94
|
+
fromName: heritageFrom,
|
|
95
|
+
toName: capture.node.text,
|
|
96
|
+
edgeType: capture.name === "extends.to" ? "symbol_extends_symbol" : "symbol_implements_symbol",
|
|
97
|
+
startLine: capture.node.startPosition.row + 1,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { symbols, imports, calls, relations };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TojiDatabase } from "../db/connection";
|
|
2
|
+
|
|
3
|
+
export interface AddStandardInput {
|
|
4
|
+
scope: "global" | "project";
|
|
5
|
+
projectPath?: string;
|
|
6
|
+
language?: string;
|
|
7
|
+
framework?: string;
|
|
8
|
+
rule: string;
|
|
9
|
+
rationale?: string;
|
|
10
|
+
priority?: "low" | "medium" | "high";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function addStandard(database: TojiDatabase, input: AddStandardInput): number {
|
|
14
|
+
const result = database
|
|
15
|
+
.query<{ id: number }, [string, string | null, string | null, string | null, string, string | null, string]>(
|
|
16
|
+
`INSERT INTO standards (scope, project_path, language, framework, rule, rationale, priority)
|
|
17
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
18
|
+
RETURNING id`,
|
|
19
|
+
)
|
|
20
|
+
.get(
|
|
21
|
+
input.scope,
|
|
22
|
+
input.projectPath ?? null,
|
|
23
|
+
input.language ?? null,
|
|
24
|
+
input.framework ?? null,
|
|
25
|
+
input.rule,
|
|
26
|
+
input.rationale ?? null,
|
|
27
|
+
input.priority ?? "medium",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!result) throw new Error("Failed to add standard");
|
|
31
|
+
|
|
32
|
+
database
|
|
33
|
+
.query<unknown, [number, string, string, string, string]>(
|
|
34
|
+
`INSERT INTO standards_fts (standard_id, rule, rationale, language, framework) VALUES (?, ?, ?, ?, ?)`,
|
|
35
|
+
)
|
|
36
|
+
.run(result.id, input.rule, input.rationale ?? "", input.language ?? "", input.framework ?? "");
|
|
37
|
+
|
|
38
|
+
return result.id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getStandards(database: TojiDatabase, language?: string, framework?: string): unknown[] {
|
|
42
|
+
return database
|
|
43
|
+
.query(
|
|
44
|
+
`SELECT id, scope, project_path, language, framework, rule, rationale, priority
|
|
45
|
+
FROM standards
|
|
46
|
+
WHERE (? IS NULL OR language IS NULL OR language = ?)
|
|
47
|
+
AND (? IS NULL OR framework IS NULL OR framework = ?)
|
|
48
|
+
ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, created_at DESC
|
|
49
|
+
LIMIT 20`,
|
|
50
|
+
)
|
|
51
|
+
.all(language ?? null, language ?? null, framework ?? null, framework ?? null);
|
|
52
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { TojiDatabase } from "../db/connection";
|
|
2
|
+
|
|
3
|
+
export type BlastRadiusDirection = "incoming" | "outgoing" | "both";
|
|
4
|
+
|
|
5
|
+
export interface BlastRadiusOptions {
|
|
6
|
+
target: string;
|
|
7
|
+
filePath?: string;
|
|
8
|
+
depth?: number;
|
|
9
|
+
direction?: BlastRadiusDirection;
|
|
10
|
+
edgeTypes?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getBlastRadius(database: TojiDatabase, options: BlastRadiusOptions): unknown[] {
|
|
14
|
+
const depth = options.depth ?? 2;
|
|
15
|
+
const direction = options.direction ?? "both";
|
|
16
|
+
const edgeTypeFilter = buildEdgeTypeFilter(options.edgeTypes);
|
|
17
|
+
const outgoingJoin = direction === "incoming" ? "" : outgoingTraversal(edgeTypeFilter.sql);
|
|
18
|
+
const incomingJoin = direction === "outgoing" ? "" : incomingTraversal(edgeTypeFilter.sql);
|
|
19
|
+
const startFileFilter = options.filePath ? "AND f.path = ?" : "";
|
|
20
|
+
|
|
21
|
+
const sql = `WITH RECURSIVE start_nodes(node_type, node_id, node_name) AS (
|
|
22
|
+
SELECT 'symbol', s.id, s.name
|
|
23
|
+
FROM symbols s
|
|
24
|
+
JOIN files f ON f.id = s.file_id
|
|
25
|
+
WHERE (s.name = ? OR s.canon_name = ?)
|
|
26
|
+
${startFileFilter}
|
|
27
|
+
UNION
|
|
28
|
+
SELECT 'file', f.id, f.path
|
|
29
|
+
FROM files f
|
|
30
|
+
WHERE f.path = ? OR f.name = ?
|
|
31
|
+
),
|
|
32
|
+
traversed(node_type, node_id, node_name, distance, via_edge, direction) AS (
|
|
33
|
+
SELECT node_type, node_id, node_name, 0, 'target', 'target' FROM start_nodes
|
|
34
|
+
${outgoingJoin}
|
|
35
|
+
${incomingJoin}
|
|
36
|
+
)
|
|
37
|
+
SELECT DISTINCT
|
|
38
|
+
t.node_type,
|
|
39
|
+
t.node_id,
|
|
40
|
+
COALESCE(s.name, f.name, t.node_name) AS name,
|
|
41
|
+
COALESCE(s.kind, 'file') AS kind,
|
|
42
|
+
t.distance,
|
|
43
|
+
t.via_edge,
|
|
44
|
+
t.direction,
|
|
45
|
+
COALESCE(sf.path, f.path, t.node_name) AS path,
|
|
46
|
+
COALESCE(s.language, f.language, '') AS language,
|
|
47
|
+
CASE WHEN t.via_edge = 'file_imports_file' THEN 'conservative_file' ELSE 'precise_symbol' END AS scope
|
|
48
|
+
FROM traversed t
|
|
49
|
+
LEFT JOIN symbols s ON t.node_type = 'symbol' AND s.id = t.node_id
|
|
50
|
+
LEFT JOIN files sf ON sf.id = s.file_id
|
|
51
|
+
LEFT JOIN files f ON t.node_type = 'file' AND f.id = t.node_id
|
|
52
|
+
ORDER BY t.distance, path, name
|
|
53
|
+
LIMIT 100`;
|
|
54
|
+
|
|
55
|
+
return database.query(sql).all(...buildParams(options, depth, direction, edgeTypeFilter.params));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function outgoingTraversal(edgeTypeSql: string): string {
|
|
59
|
+
return `UNION
|
|
60
|
+
SELECT e.to_type, COALESCE(e.to_id, -e.id), COALESCE(e.to_name, ''), traversed.distance + 1, e.edge_type, 'outgoing'
|
|
61
|
+
FROM traversed
|
|
62
|
+
JOIN edges e ON e.from_type = traversed.node_type AND e.from_id = traversed.node_id
|
|
63
|
+
WHERE traversed.distance < ?
|
|
64
|
+
${edgeTypeSql}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function incomingTraversal(edgeTypeSql: string): string {
|
|
68
|
+
return `UNION
|
|
69
|
+
SELECT e.from_type, e.from_id, '', traversed.distance + 1, e.edge_type, 'incoming'
|
|
70
|
+
FROM traversed
|
|
71
|
+
JOIN edges e ON e.to_type = traversed.node_type AND e.to_id = traversed.node_id
|
|
72
|
+
WHERE traversed.distance < ?
|
|
73
|
+
${edgeTypeSql}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildEdgeTypeFilter(edgeTypes?: string[]): { sql: string; params: string[] } {
|
|
77
|
+
if (!edgeTypes || edgeTypes.length === 0) return { sql: "", params: [] };
|
|
78
|
+
return {
|
|
79
|
+
sql: `AND e.edge_type IN (${edgeTypes.map(() => "?").join(", ")})`,
|
|
80
|
+
params: edgeTypes,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildParams(
|
|
85
|
+
options: BlastRadiusOptions,
|
|
86
|
+
depth: number,
|
|
87
|
+
direction: BlastRadiusDirection,
|
|
88
|
+
edgeTypeParams: string[],
|
|
89
|
+
): Array<string | number> {
|
|
90
|
+
const params: Array<string | number> = [options.target, options.target];
|
|
91
|
+
if (options.filePath) params.push(options.filePath);
|
|
92
|
+
params.push(options.target, options.target);
|
|
93
|
+
|
|
94
|
+
if (direction !== "incoming") params.push(depth, ...edgeTypeParams);
|
|
95
|
+
if (direction !== "outgoing") params.push(depth, ...edgeTypeParams);
|
|
96
|
+
|
|
97
|
+
return params;
|
|
98
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { TojiDatabase } from "../db/connection";
|
|
4
|
+
import { getBlastRadius, type BlastRadiusDirection } from "./blast-radius";
|
|
5
|
+
|
|
6
|
+
export interface GraphExploreOptions {
|
|
7
|
+
intent: string;
|
|
8
|
+
seedQueries?: string[];
|
|
9
|
+
maxSeeds?: number;
|
|
10
|
+
depth?: number;
|
|
11
|
+
direction?: BlastRadiusDirection;
|
|
12
|
+
projectPath?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SeedSymbol {
|
|
16
|
+
name: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
language: string;
|
|
19
|
+
path: string;
|
|
20
|
+
signature?: string;
|
|
21
|
+
score: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function graphExplore(database: TojiDatabase, options: GraphExploreOptions): Record<string, unknown> {
|
|
25
|
+
const maxSeeds = options.maxSeeds ?? 8;
|
|
26
|
+
const queries = buildQueries(options.intent, options.seedQueries);
|
|
27
|
+
const projectId = options.projectPath ? findProjectId(database, options.projectPath) : undefined;
|
|
28
|
+
const seeds = findSeeds(database, queries, maxSeeds, projectId);
|
|
29
|
+
|
|
30
|
+
if (seeds.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
intent: options.intent,
|
|
33
|
+
queries,
|
|
34
|
+
projectId,
|
|
35
|
+
seeds: [],
|
|
36
|
+
blastRadius: [],
|
|
37
|
+
hint: "No seed symbols found. Try concrete seedQueries such as adapter, registry, runner, or run toji_project_overview first.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
const blastRadius = [];
|
|
43
|
+
for (const seed of seeds) {
|
|
44
|
+
const rows = getBlastRadius(database, {
|
|
45
|
+
target: seed.name,
|
|
46
|
+
filePath: seed.path,
|
|
47
|
+
depth: options.depth ?? 2,
|
|
48
|
+
direction: options.direction ?? "both",
|
|
49
|
+
}) as Array<Record<string, unknown>>;
|
|
50
|
+
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const key = `${String(row.path)}:${String(row.kind)}:${String(row.name)}:${String(row.distance)}:${String(row.direction)}`;
|
|
53
|
+
if (seen.has(key)) continue;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
blastRadius.push({ ...row, seed: `${seed.name} (${seed.path})` });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
intent: options.intent,
|
|
61
|
+
queries,
|
|
62
|
+
projectId,
|
|
63
|
+
seeds,
|
|
64
|
+
blastRadius,
|
|
65
|
+
likelyFiles: summarizeFiles(blastRadius),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildQueries(intent: string, seedQueries?: string[]): string[] {
|
|
70
|
+
const explicit = (seedQueries ?? []).map((item) => item.trim()).filter(Boolean);
|
|
71
|
+
const tokens = intent
|
|
72
|
+
.split(/[^A-Za-z0-9_]+/)
|
|
73
|
+
.map((item) => item.trim())
|
|
74
|
+
.filter((item) => item.length >= 3 && !stopWords.has(item.toLowerCase()));
|
|
75
|
+
return [...new Set([...explicit, ...tokens])].slice(0, 16);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findSeeds(database: TojiDatabase, queries: string[], maxSeeds: number, projectId?: number): SeedSymbol[] {
|
|
79
|
+
const scores = new Map<string, SeedSymbol>();
|
|
80
|
+
|
|
81
|
+
for (const [queryIndex, query] of queries.entries()) {
|
|
82
|
+
const rows = findSeedRows(database, query, projectId);
|
|
83
|
+
|
|
84
|
+
for (const row of rows) {
|
|
85
|
+
const key = `${row.path}:${row.kind}:${row.name}`;
|
|
86
|
+
const previous = scores.get(key);
|
|
87
|
+
const score = (previous?.score ?? 0) + 100 - queryIndex;
|
|
88
|
+
scores.set(key, { ...row, score });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [...scores.values()]
|
|
93
|
+
.sort((left, right) => right.score - left.score || rankKind(left.kind) - rankKind(right.kind) || left.path.localeCompare(right.path))
|
|
94
|
+
.slice(0, maxSeeds);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findSeedRows(database: TojiDatabase, query: string, projectId?: number): SeedSymbol[] {
|
|
98
|
+
if (projectId !== undefined) return findSeedRowsByLike(database, query, projectId);
|
|
99
|
+
|
|
100
|
+
const ftsRows = database
|
|
101
|
+
.query<SeedSymbol, [string]>(
|
|
102
|
+
`SELECT name, kind, language, path, signature
|
|
103
|
+
FROM symbol_fts
|
|
104
|
+
WHERE symbol_fts MATCH ?
|
|
105
|
+
LIMIT 20`,
|
|
106
|
+
)
|
|
107
|
+
.all(query);
|
|
108
|
+
if (ftsRows.length > 0) return ftsRows;
|
|
109
|
+
|
|
110
|
+
return findSeedRowsByLike(database, query);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function findSeedRowsByLike(database: TojiDatabase, query: string, projectId?: number): SeedSymbol[] {
|
|
114
|
+
const like = `%${query}%`;
|
|
115
|
+
const projectFilter = projectId === undefined ? "" : "AND s.project_id = ?";
|
|
116
|
+
const params = projectId === undefined ? [like, like, query, like, like] : [like, like, projectId, query, like, like];
|
|
117
|
+
return database
|
|
118
|
+
.query<SeedSymbol, Array<string | number>>(
|
|
119
|
+
`SELECT s.name, s.kind, s.language, f.path, s.signature
|
|
120
|
+
FROM symbols s
|
|
121
|
+
JOIN files f ON f.id = s.file_id
|
|
122
|
+
WHERE (s.name LIKE ? OR f.path LIKE ?)
|
|
123
|
+
${projectFilter}
|
|
124
|
+
ORDER BY CASE
|
|
125
|
+
WHEN s.name = ? THEN 0
|
|
126
|
+
WHEN s.name LIKE ? THEN 1
|
|
127
|
+
WHEN f.path LIKE ? THEN 2
|
|
128
|
+
ELSE 3
|
|
129
|
+
END,
|
|
130
|
+
f.path,
|
|
131
|
+
s.name
|
|
132
|
+
LIMIT 20`,
|
|
133
|
+
)
|
|
134
|
+
.all(...params);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function findProjectId(database: TojiDatabase, projectPath: string): number | undefined {
|
|
138
|
+
return database
|
|
139
|
+
.query<{ id: number }, [string]>(`SELECT id FROM projects WHERE root_path = ?`)
|
|
140
|
+
.get(resolve(projectPath))?.id;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function summarizeFiles(rows: Array<Record<string, unknown>>): Array<{ path: string; hits: number; minDistance: number }> {
|
|
144
|
+
const files = new Map<string, { path: string; hits: number; minDistance: number }>();
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
const path = String(row.path ?? "");
|
|
147
|
+
if (!path) continue;
|
|
148
|
+
const distance = typeof row.distance === "number" ? row.distance : Number(row.distance ?? 99);
|
|
149
|
+
const current = files.get(path) ?? { path, hits: 0, minDistance: distance };
|
|
150
|
+
current.hits += 1;
|
|
151
|
+
current.minDistance = Math.min(current.minDistance, distance);
|
|
152
|
+
files.set(path, current);
|
|
153
|
+
}
|
|
154
|
+
return [...files.values()].sort((left, right) => left.minDistance - right.minDistance || right.hits - left.hits || left.path.localeCompare(right.path));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rankKind(kind: string): number {
|
|
158
|
+
if (kind === "class") return 0;
|
|
159
|
+
if (kind === "function") return 1;
|
|
160
|
+
if (kind === "method") return 2;
|
|
161
|
+
return 3;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const stopWords = new Set([
|
|
165
|
+
"the",
|
|
166
|
+
"and",
|
|
167
|
+
"for",
|
|
168
|
+
"with",
|
|
169
|
+
"what",
|
|
170
|
+
"where",
|
|
171
|
+
"when",
|
|
172
|
+
"would",
|
|
173
|
+
"could",
|
|
174
|
+
"should",
|
|
175
|
+
"like",
|
|
176
|
+
"add",
|
|
177
|
+
"new",
|
|
178
|
+
"need",
|
|
179
|
+
"needed",
|
|
180
|
+
"files",
|
|
181
|
+
"change",
|
|
182
|
+
"changes",
|
|
183
|
+
"affected",
|
|
184
|
+
"impact",
|
|
185
|
+
"impacted",
|
|
186
|
+
]);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { TojiDatabase } from "../db/connection";
|
|
4
|
+
|
|
5
|
+
export interface ProjectOverviewOptions {
|
|
6
|
+
projectPath?: string;
|
|
7
|
+
projectId?: number;
|
|
8
|
+
includeSymbols?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getProjectOverview(database: TojiDatabase, options: ProjectOverviewOptions = {}): Record<string, unknown> {
|
|
12
|
+
const project = findProject(database, options);
|
|
13
|
+
if (!project) {
|
|
14
|
+
return {
|
|
15
|
+
error: "No indexed project found. Run toji_index_project or /toji-index first.",
|
|
16
|
+
projectPath: options.projectPath ? resolve(options.projectPath) : undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const counts = {
|
|
21
|
+
files: scalarCount(database, "files", project.id),
|
|
22
|
+
symbols: scalarCount(database, "symbols", project.id),
|
|
23
|
+
imports: scalarCount(database, "imports", project.id),
|
|
24
|
+
calls: scalarCount(database, "calls", project.id),
|
|
25
|
+
edges: scalarCount(database, "edges", project.id),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const overview: Record<string, unknown> = {
|
|
29
|
+
project,
|
|
30
|
+
counts,
|
|
31
|
+
languages: database
|
|
32
|
+
.query(`SELECT language, count(*) AS files FROM files WHERE project_id = ? GROUP BY language ORDER BY files DESC`)
|
|
33
|
+
.all(project.id),
|
|
34
|
+
directories: database
|
|
35
|
+
.query(
|
|
36
|
+
`SELECT CASE WHEN instr(path, '/') = 0 THEN '(root)' ELSE substr(path, 1, instr(path, '/') - 1) END AS directory,
|
|
37
|
+
count(*) AS files
|
|
38
|
+
FROM files
|
|
39
|
+
WHERE project_id = ?
|
|
40
|
+
GROUP BY directory
|
|
41
|
+
ORDER BY files DESC, directory
|
|
42
|
+
LIMIT 50`,
|
|
43
|
+
)
|
|
44
|
+
.all(project.id),
|
|
45
|
+
edgeTypes: database
|
|
46
|
+
.query(`SELECT edge_type, count(*) AS count FROM edges WHERE project_id = ? GROUP BY edge_type ORDER BY count DESC`)
|
|
47
|
+
.all(project.id),
|
|
48
|
+
files: database.query(`SELECT path, language FROM files WHERE project_id = ? ORDER BY path LIMIT 200`).all(project.id),
|
|
49
|
+
tests: database
|
|
50
|
+
.query(`SELECT path, language FROM files WHERE project_id = ? AND (path LIKE 'test/%' OR path LIKE 'tests/%' OR path LIKE '%test%') ORDER BY path LIMIT 100`)
|
|
51
|
+
.all(project.id),
|
|
52
|
+
topSymbols: database
|
|
53
|
+
.query(
|
|
54
|
+
`SELECT s.name, s.kind, s.language, f.path, s.signature
|
|
55
|
+
FROM symbols s
|
|
56
|
+
JOIN files f ON f.id = s.file_id
|
|
57
|
+
WHERE s.project_id = ? AND s.kind IN ('class', 'function', 'method', 'interface', 'type')
|
|
58
|
+
ORDER BY CASE s.kind WHEN 'class' THEN 0 WHEN 'interface' THEN 1 WHEN 'function' THEN 2 WHEN 'method' THEN 3 ELSE 4 END,
|
|
59
|
+
f.path,
|
|
60
|
+
s.start_line
|
|
61
|
+
LIMIT 120`,
|
|
62
|
+
)
|
|
63
|
+
.all(project.id),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (options.includeSymbols) {
|
|
67
|
+
overview.symbolsByFile = database
|
|
68
|
+
.query(
|
|
69
|
+
`SELECT f.path,
|
|
70
|
+
group_concat(s.kind || ':' || s.name, ', ') AS symbols
|
|
71
|
+
FROM files f
|
|
72
|
+
LEFT JOIN symbols s ON s.file_id = f.id
|
|
73
|
+
WHERE f.project_id = ?
|
|
74
|
+
GROUP BY f.id
|
|
75
|
+
ORDER BY f.path
|
|
76
|
+
LIMIT 200`,
|
|
77
|
+
)
|
|
78
|
+
.all(project.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return overview;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findProject(database: TojiDatabase, options: ProjectOverviewOptions): { id: number; root_path: string; name: string; indexed_at: string } | undefined {
|
|
85
|
+
if (options.projectId !== undefined) {
|
|
86
|
+
return database.query<{ id: number; root_path: string; name: string; indexed_at: string }, [number]>(`SELECT id, root_path, name, indexed_at FROM projects WHERE id = ?`).get(options.projectId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (options.projectPath) {
|
|
90
|
+
return database
|
|
91
|
+
.query<{ id: number; root_path: string; name: string; indexed_at: string }, [string]>(`SELECT id, root_path, name, indexed_at FROM projects WHERE root_path = ?`)
|
|
92
|
+
.get(resolve(options.projectPath));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return database
|
|
96
|
+
.query<{ id: number; root_path: string; name: string; indexed_at: string }, []>(`SELECT id, root_path, name, indexed_at FROM projects ORDER BY indexed_at DESC, id DESC LIMIT 1`)
|
|
97
|
+
.get();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scalarCount(database: TojiDatabase, table: string, projectId: number): number {
|
|
101
|
+
return database.query<{ count: number }, [number]>(`SELECT count(*) AS count FROM ${table} WHERE project_id = ?`).get(projectId)?.count ?? 0;
|
|
102
|
+
}
|