@flyingrobots/graft 0.4.0 → 0.5.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/ARCHITECTURE.md +386 -0
- package/CHANGELOG.md +47 -0
- package/CODE_OF_CONDUCT.md +65 -0
- package/README.md +153 -17
- package/bin/graft.js +4 -14
- package/docs/ADVANCED_GUIDE.md +49 -0
- package/docs/CLI.md +43 -0
- package/docs/GUIDE.md +321 -32
- package/docs/MCP.md +44 -0
- package/package.json +15 -4
- package/src/adapters/node-fs.ts +4 -0
- package/src/adapters/node-git.ts +47 -0
- package/src/adapters/node-process-runner.ts +27 -0
- package/src/cli/index-cmd.ts +75 -11
- package/src/cli/init.ts +808 -57
- package/src/cli/main.ts +437 -0
- package/src/contracts/capabilities.ts +341 -0
- package/src/contracts/causal-ontology.ts +622 -0
- package/src/contracts/causal-surface-next-action.ts +18 -0
- package/src/contracts/output-schemas.ts +1169 -0
- package/src/git/diff.ts +25 -21
- package/src/git/target-git-hook-bootstrap.ts +56 -0
- package/src/hooks/posttooluse-read.ts +21 -74
- package/src/hooks/pretooluse-read.ts +20 -56
- package/src/hooks/read-governor.ts +95 -0
- package/src/hooks/read-messages.ts +53 -0
- package/src/mcp/burden.ts +123 -0
- package/src/mcp/cache.ts +51 -0
- package/src/mcp/cached-file.ts +10 -8
- package/src/mcp/context.ts +65 -2
- package/src/mcp/daemon-control-plane.ts +554 -0
- package/src/mcp/daemon-job-scheduler.ts +279 -0
- package/src/mcp/daemon-repos.ts +216 -0
- package/src/mcp/daemon-server.ts +396 -0
- package/src/mcp/daemon-worker-pool.ts +310 -0
- package/src/mcp/daemon-worker-process.ts +52 -0
- package/src/mcp/metrics.ts +108 -1
- package/src/mcp/monitor-tick-job.ts +99 -0
- package/src/mcp/persisted-local-history.ts +1246 -0
- package/src/mcp/persistent-monitor-runtime.ts +549 -0
- package/src/mcp/policy.ts +84 -0
- package/src/mcp/receipt.ts +82 -12
- package/src/mcp/repo-concurrency.ts +318 -0
- package/src/mcp/repo-state.ts +777 -0
- package/src/mcp/repo-tool-job.ts +302 -0
- package/src/mcp/run-capture-config.ts +33 -0
- package/src/mcp/runtime-causal-context.ts +72 -0
- package/src/mcp/runtime-observability.ts +219 -0
- package/src/mcp/runtime-staged-target.ts +161 -0
- package/src/mcp/runtime-workspace-overlay.ts +255 -0
- package/src/mcp/semantic-transition-guidance.ts +60 -0
- package/src/mcp/semantic-transition-summary.ts +130 -0
- package/src/mcp/server.ts +696 -55
- package/src/mcp/stdio-server.ts +12 -0
- package/src/mcp/stdio.ts +2 -5
- package/src/mcp/tools/activity-view.ts +325 -0
- package/src/mcp/tools/causal-attach.ts +67 -0
- package/src/mcp/tools/causal-status.ts +58 -0
- package/src/mcp/tools/changed-since.ts +13 -11
- package/src/mcp/tools/code-find.ts +164 -0
- package/src/mcp/tools/code-refs.ts +466 -0
- package/src/mcp/tools/code-show.ts +252 -0
- package/src/mcp/tools/daemon-monitors.ts +14 -0
- package/src/mcp/tools/daemon-repos.ts +22 -0
- package/src/mcp/tools/daemon-sessions.ts +14 -0
- package/src/mcp/tools/daemon-status.ts +12 -0
- package/src/mcp/tools/doctor.ts +45 -2
- package/src/mcp/tools/explain.ts +4 -0
- package/src/mcp/tools/file-outline.ts +7 -3
- package/src/mcp/tools/git-files.ts +73 -0
- package/src/mcp/tools/graft-diff.ts +12 -4
- package/src/mcp/tools/map.ts +92 -38
- package/src/mcp/tools/monitor-pause.ts +18 -0
- package/src/mcp/tools/monitor-resume.ts +18 -0
- package/src/mcp/tools/monitor-start.ts +20 -0
- package/src/mcp/tools/monitor-stop.ts +18 -0
- package/src/mcp/tools/precision-match.ts +51 -0
- package/src/mcp/tools/precision-query.ts +127 -0
- package/src/mcp/tools/precision.ts +312 -0
- package/src/mcp/tools/run-capture.ts +126 -44
- package/src/mcp/tools/safe-read.ts +14 -12
- package/src/mcp/tools/since.ts +7 -2
- package/src/mcp/tools/state.ts +11 -3
- package/src/mcp/tools/stats.ts +5 -1
- package/src/mcp/tools/workspace-authorizations.ts +14 -0
- package/src/mcp/tools/workspace-authorize.ts +20 -0
- package/src/mcp/tools/workspace-bind.ts +25 -0
- package/src/mcp/tools/workspace-rebind.ts +25 -0
- package/src/mcp/tools/workspace-revoke.ts +18 -0
- package/src/mcp/tools/workspace-status.ts +12 -0
- package/src/mcp/warp-pool.ts +36 -0
- package/src/mcp/workspace-router.ts +984 -0
- package/src/operations/file-outline.ts +12 -2
- package/src/operations/graft-diff.ts +56 -10
- package/src/operations/safe-read.ts +27 -4
- package/src/operations/state.ts +6 -9
- package/src/parser/lang.ts +19 -3
- package/src/parser/outline.ts +191 -2
- package/src/parser/types.ts +9 -1
- package/src/policy/types.ts +4 -3
- package/src/ports/filesystem.ts +1 -0
- package/src/ports/git.ts +16 -0
- package/src/ports/process-runner.ts +22 -0
- package/src/release/security-gate.ts +102 -0
- package/src/session/tracker.ts +31 -0
- package/src/version.ts +3 -0
- package/src/warp/indexer.ts +171 -56
- package/src/warp/observers.ts +1 -1
- package/src/warp/open.ts +4 -3
- package/src/warp/plumbing.d.ts +5 -1
- package/src/warp/writer-id.ts +30 -0
package/src/mcp/tools/map.ts
CHANGED
|
@@ -3,26 +3,69 @@ import { z } from "zod";
|
|
|
3
3
|
import { extractOutline } from "../../parser/outline.js";
|
|
4
4
|
import { detectLang } from "../../parser/lang.js";
|
|
5
5
|
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
6
|
-
import {
|
|
6
|
+
import { GitFileQuery, listGitFiles } from "./git-files.js";
|
|
7
|
+
import { evaluateMcpRefusal, type McpPolicyRefusal } from "../policy.js";
|
|
8
|
+
import { collectSymbols, normalizeRepoPath } from "./precision.js";
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
class StructuralMapRequest {
|
|
11
|
+
readonly directory: string;
|
|
12
|
+
|
|
13
|
+
constructor(args: Record<string, unknown>, projectRoot: string) {
|
|
14
|
+
const rawPath = args["path"];
|
|
15
|
+
if (rawPath !== undefined && typeof rawPath !== "string") {
|
|
16
|
+
throw new Error("StructuralMapRequest: path must be a string when provided");
|
|
17
|
+
}
|
|
18
|
+
this.directory = rawPath !== undefined && rawPath.trim().length > 0
|
|
19
|
+
? normalizeRepoPath(projectRoot, rawPath)
|
|
20
|
+
: ".";
|
|
21
|
+
Object.freeze(this);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toGitFileQuery(projectRoot: string): GitFileQuery {
|
|
25
|
+
return GitFileQuery.project(projectRoot, this.directory === "." ? "" : this.directory);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class StructuralMapSymbol {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly kind: string;
|
|
32
|
+
readonly signature?: string;
|
|
33
|
+
readonly exported: boolean;
|
|
34
|
+
readonly startLine?: number;
|
|
35
|
+
readonly endLine?: number;
|
|
36
|
+
|
|
37
|
+
constructor(opts: {
|
|
38
|
+
name: string;
|
|
39
|
+
kind: string;
|
|
40
|
+
signature?: string;
|
|
41
|
+
exported: boolean;
|
|
42
|
+
startLine?: number;
|
|
43
|
+
endLine?: number;
|
|
44
|
+
}) {
|
|
45
|
+
this.name = opts.name;
|
|
46
|
+
this.kind = opts.kind;
|
|
47
|
+
this.exported = opts.exported;
|
|
48
|
+
if (opts.signature !== undefined) this.signature = opts.signature;
|
|
49
|
+
if (opts.startLine !== undefined) this.startLine = opts.startLine;
|
|
50
|
+
if (opts.endLine !== undefined) this.endLine = opts.endLine;
|
|
51
|
+
Object.freeze(this);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class StructuralMapFile {
|
|
9
56
|
path: string;
|
|
10
57
|
lang: string;
|
|
11
|
-
symbols:
|
|
12
|
-
}
|
|
58
|
+
symbols: readonly StructuralMapSymbol[];
|
|
13
59
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.trim().split("\n").filter((l) => l.length > 0);
|
|
24
|
-
} catch {
|
|
25
|
-
return [];
|
|
60
|
+
constructor(opts: {
|
|
61
|
+
path: string;
|
|
62
|
+
lang: string;
|
|
63
|
+
symbols: readonly StructuralMapSymbol[];
|
|
64
|
+
}) {
|
|
65
|
+
this.path = opts.path;
|
|
66
|
+
this.lang = opts.lang;
|
|
67
|
+
this.symbols = Object.freeze([...opts.symbols]);
|
|
68
|
+
Object.freeze(this);
|
|
26
69
|
}
|
|
27
70
|
}
|
|
28
71
|
|
|
@@ -36,45 +79,56 @@ export const mapTool: ToolDefinition = {
|
|
|
36
79
|
path: z.string().optional(),
|
|
37
80
|
},
|
|
38
81
|
createHandler(ctx: ToolContext): ToolHandler {
|
|
39
|
-
return (args) => {
|
|
40
|
-
const
|
|
82
|
+
return async (args) => {
|
|
83
|
+
const request = new StructuralMapRequest(args, ctx.projectRoot);
|
|
41
84
|
|
|
42
|
-
const filePaths =
|
|
43
|
-
const files:
|
|
85
|
+
const filePaths = (await listGitFiles(request.toGitFileQuery(ctx.projectRoot), ctx.git)).paths;
|
|
86
|
+
const files: StructuralMapFile[] = [];
|
|
87
|
+
const refused: McpPolicyRefusal[] = [];
|
|
44
88
|
|
|
45
89
|
for (const filePath of filePaths) {
|
|
46
|
-
const lang = detectLang(filePath);
|
|
47
|
-
if (lang === null) continue;
|
|
48
|
-
|
|
49
90
|
let content: string;
|
|
50
91
|
try {
|
|
51
|
-
content = ctx.fs.
|
|
92
|
+
content = await ctx.fs.readFile(path.join(ctx.projectRoot, filePath), "utf-8");
|
|
52
93
|
} catch {
|
|
53
94
|
continue;
|
|
54
95
|
}
|
|
55
96
|
|
|
97
|
+
const actual = {
|
|
98
|
+
lines: content.split("\n").length,
|
|
99
|
+
bytes: Buffer.byteLength(content),
|
|
100
|
+
};
|
|
101
|
+
const refusal = evaluateMcpRefusal(ctx, filePath, actual);
|
|
102
|
+
if (refusal !== null) {
|
|
103
|
+
refused.push(refusal);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lang = detectLang(filePath);
|
|
108
|
+
if (lang === null) continue;
|
|
109
|
+
|
|
56
110
|
const result = extractOutline(content, lang);
|
|
57
|
-
const symbols
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
signature:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
files.push({ path: filePath, lang, symbols });
|
|
111
|
+
const symbols = collectSymbols(result.entries, filePath, result.jumpTable ?? []).map((symbol) =>
|
|
112
|
+
new StructuralMapSymbol({
|
|
113
|
+
name: symbol.name,
|
|
114
|
+
kind: symbol.kind,
|
|
115
|
+
exported: symbol.exported,
|
|
116
|
+
...(symbol.signature !== undefined ? { signature: symbol.signature } : {}),
|
|
117
|
+
...(symbol.startLine !== undefined ? { startLine: symbol.startLine } : {}),
|
|
118
|
+
...(symbol.endLine !== undefined ? { endLine: symbol.endLine } : {}),
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
files.push(new StructuralMapFile({ path: filePath, lang, symbols }));
|
|
70
123
|
}
|
|
71
124
|
|
|
72
125
|
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
73
126
|
const totalSymbols = files.reduce((n, f) => n + f.symbols.length, 0);
|
|
74
127
|
|
|
75
128
|
return ctx.respond("graft_map", {
|
|
76
|
-
directory:
|
|
129
|
+
directory: request.directory,
|
|
77
130
|
files,
|
|
131
|
+
...(refused.length > 0 ? { refused } : {}),
|
|
78
132
|
summary: `${String(files.length)} files, ${String(totalSymbols)} symbols`,
|
|
79
133
|
});
|
|
80
134
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const monitorPauseTool: ToolDefinition = {
|
|
5
|
+
name: "monitor_pause",
|
|
6
|
+
description:
|
|
7
|
+
"Pause the repo-scoped background index monitor for an authorized workspace.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
},
|
|
11
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
12
|
+
return async (args) => {
|
|
13
|
+
return ctx.respond("monitor_pause", { ...await ctx.pauseMonitor({
|
|
14
|
+
cwd: args["cwd"] as string,
|
|
15
|
+
}) });
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const monitorResumeTool: ToolDefinition = {
|
|
5
|
+
name: "monitor_resume",
|
|
6
|
+
description:
|
|
7
|
+
"Resume a paused repo-scoped background index monitor for an authorized workspace.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
},
|
|
11
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
12
|
+
return async (args) => {
|
|
13
|
+
return ctx.respond("monitor_resume", { ...await ctx.resumeMonitor({
|
|
14
|
+
cwd: args["cwd"] as string,
|
|
15
|
+
}) });
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const monitorStartTool: ToolDefinition = {
|
|
5
|
+
name: "monitor_start",
|
|
6
|
+
description:
|
|
7
|
+
"Start or resume the repo-scoped background index monitor for an authorized workspace.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
pollIntervalMs: z.number().int().positive().optional(),
|
|
11
|
+
},
|
|
12
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
13
|
+
return async (args) => {
|
|
14
|
+
return ctx.respond("monitor_start", { ...await ctx.startMonitor({
|
|
15
|
+
cwd: args["cwd"] as string,
|
|
16
|
+
pollIntervalMs: args["pollIntervalMs"] as number | undefined,
|
|
17
|
+
}) });
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolDefinition, ToolContext, ToolHandler } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export const monitorStopTool: ToolDefinition = {
|
|
5
|
+
name: "monitor_stop",
|
|
6
|
+
description:
|
|
7
|
+
"Stop the repo-scoped background index monitor for an authorized workspace.",
|
|
8
|
+
schema: {
|
|
9
|
+
cwd: z.string(),
|
|
10
|
+
},
|
|
11
|
+
createHandler(ctx: ToolContext): ToolHandler {
|
|
12
|
+
return async (args) => {
|
|
13
|
+
return ctx.respond("monitor_stop", { ...await ctx.stopMonitor({
|
|
14
|
+
cwd: args["cwd"] as string,
|
|
15
|
+
}) });
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class PrecisionSymbolMatch {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly kind: string;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
readonly signature?: string;
|
|
6
|
+
readonly exported: boolean;
|
|
7
|
+
readonly startLine?: number;
|
|
8
|
+
readonly endLine?: number;
|
|
9
|
+
|
|
10
|
+
constructor(opts: {
|
|
11
|
+
name: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
path: string;
|
|
14
|
+
signature?: string;
|
|
15
|
+
exported: boolean;
|
|
16
|
+
startLine?: number;
|
|
17
|
+
endLine?: number;
|
|
18
|
+
}) {
|
|
19
|
+
if (opts.name.trim().length === 0) {
|
|
20
|
+
throw new Error("PrecisionSymbolMatch: name must be non-empty");
|
|
21
|
+
}
|
|
22
|
+
if (opts.kind.trim().length === 0) {
|
|
23
|
+
throw new Error("PrecisionSymbolMatch: kind must be non-empty");
|
|
24
|
+
}
|
|
25
|
+
if (opts.path.trim().length === 0) {
|
|
26
|
+
throw new Error("PrecisionSymbolMatch: path must be non-empty");
|
|
27
|
+
}
|
|
28
|
+
if (opts.startLine !== undefined && (!Number.isInteger(opts.startLine) || opts.startLine < 1)) {
|
|
29
|
+
throw new Error("PrecisionSymbolMatch: startLine must be an integer >= 1");
|
|
30
|
+
}
|
|
31
|
+
if (opts.endLine !== undefined && (!Number.isInteger(opts.endLine) || opts.endLine < 1)) {
|
|
32
|
+
throw new Error("PrecisionSymbolMatch: endLine must be an integer >= 1");
|
|
33
|
+
}
|
|
34
|
+
if (
|
|
35
|
+
opts.startLine !== undefined &&
|
|
36
|
+
opts.endLine !== undefined &&
|
|
37
|
+
opts.endLine < opts.startLine
|
|
38
|
+
) {
|
|
39
|
+
throw new Error("PrecisionSymbolMatch: endLine must be >= startLine");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.name = opts.name.trim();
|
|
43
|
+
this.kind = opts.kind.trim();
|
|
44
|
+
this.path = opts.path.trim();
|
|
45
|
+
this.exported = opts.exported;
|
|
46
|
+
if (opts.signature !== undefined) this.signature = opts.signature;
|
|
47
|
+
if (opts.startLine !== undefined) this.startLine = opts.startLine;
|
|
48
|
+
if (opts.endLine !== undefined) this.endLine = opts.endLine;
|
|
49
|
+
Object.freeze(this);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
import { PrecisionSymbolMatch } from "./precision-match.js";
|
|
3
|
+
|
|
4
|
+
type SymbolQueryMode = "glob" | "plain";
|
|
5
|
+
|
|
6
|
+
export class RankedPrecisionSymbolMatch {
|
|
7
|
+
readonly match: PrecisionSymbolMatch;
|
|
8
|
+
readonly score: number;
|
|
9
|
+
|
|
10
|
+
constructor(opts: {
|
|
11
|
+
match: PrecisionSymbolMatch;
|
|
12
|
+
score: number;
|
|
13
|
+
}) {
|
|
14
|
+
if (opts.score < 0) {
|
|
15
|
+
throw new Error("RankedPrecisionSymbolMatch: score must be >= 0");
|
|
16
|
+
}
|
|
17
|
+
this.match = opts.match;
|
|
18
|
+
this.score = opts.score;
|
|
19
|
+
Object.freeze(this);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class PrecisionSymbolQuery {
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly mode: SymbolQueryMode;
|
|
26
|
+
readonly #globMatcher?: (name: string) => boolean;
|
|
27
|
+
|
|
28
|
+
constructor(query: string) {
|
|
29
|
+
const normalizedQuery = query.trim();
|
|
30
|
+
if (normalizedQuery.length === 0) {
|
|
31
|
+
throw new Error("PrecisionSymbolQuery: query must be non-empty");
|
|
32
|
+
}
|
|
33
|
+
this.text = normalizedQuery;
|
|
34
|
+
if (picomatch.scan(normalizedQuery).isGlob) {
|
|
35
|
+
this.mode = "glob";
|
|
36
|
+
this.#globMatcher = picomatch(normalizedQuery, { nocase: true });
|
|
37
|
+
} else {
|
|
38
|
+
this.mode = "plain";
|
|
39
|
+
}
|
|
40
|
+
Object.freeze(this);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
score(name: string): number | null {
|
|
44
|
+
if (this.mode === "glob") {
|
|
45
|
+
return this.#globMatcher?.(name) === true ? 0 : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const loweredName = name.toLowerCase();
|
|
49
|
+
const loweredQuery = this.text.toLowerCase();
|
|
50
|
+
if (name === this.text) return 0;
|
|
51
|
+
if (loweredName === loweredQuery) return 1;
|
|
52
|
+
if (name.startsWith(this.text)) return 2;
|
|
53
|
+
if (loweredName.startsWith(loweredQuery)) return 3;
|
|
54
|
+
return loweredName.includes(loweredQuery) ? 4 : null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class PrecisionSearchRequest {
|
|
59
|
+
readonly exactName?: string;
|
|
60
|
+
readonly query?: PrecisionSymbolQuery;
|
|
61
|
+
readonly kind?: string;
|
|
62
|
+
readonly filePath?: string;
|
|
63
|
+
readonly pathPrefix?: string;
|
|
64
|
+
readonly ceiling?: number;
|
|
65
|
+
|
|
66
|
+
constructor(opts: {
|
|
67
|
+
exactName?: string;
|
|
68
|
+
query?: string;
|
|
69
|
+
kind?: string;
|
|
70
|
+
filePath?: string;
|
|
71
|
+
pathPrefix?: string;
|
|
72
|
+
ceiling?: number;
|
|
73
|
+
}) {
|
|
74
|
+
const exactName = opts.exactName?.trim();
|
|
75
|
+
const query = opts.query?.trim();
|
|
76
|
+
const kind = opts.kind?.trim().toLowerCase();
|
|
77
|
+
const filePath = opts.filePath?.trim();
|
|
78
|
+
const pathPrefix = opts.pathPrefix?.trim();
|
|
79
|
+
|
|
80
|
+
if ((exactName?.length ?? 0) === 0 && (query?.length ?? 0) === 0) {
|
|
81
|
+
throw new Error("PrecisionSearchRequest: exactName or query is required");
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
opts.ceiling !== undefined &&
|
|
85
|
+
(!Number.isInteger(opts.ceiling) || opts.ceiling < 1)
|
|
86
|
+
) {
|
|
87
|
+
throw new Error("PrecisionSearchRequest: ceiling must be an integer >= 1");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (exactName !== undefined && exactName.length > 0) this.exactName = exactName;
|
|
91
|
+
if (query !== undefined && query.length > 0) this.query = new PrecisionSymbolQuery(query);
|
|
92
|
+
if (kind !== undefined && kind.length > 0) this.kind = kind;
|
|
93
|
+
if (filePath !== undefined && filePath.length > 0) this.filePath = filePath;
|
|
94
|
+
if (pathPrefix !== undefined && pathPrefix.length > 0) this.pathPrefix = pathPrefix;
|
|
95
|
+
if (opts.ceiling !== undefined) this.ceiling = opts.ceiling;
|
|
96
|
+
Object.freeze(this);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
selectLens(): "file" | "exact" | "all" {
|
|
100
|
+
if (this.filePath !== undefined) return "file";
|
|
101
|
+
if (this.exactName !== undefined) return "exact";
|
|
102
|
+
return "all";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
rank(match: PrecisionSymbolMatch): RankedPrecisionSymbolMatch | null {
|
|
106
|
+
if (this.exactName !== undefined && match.name !== this.exactName) return null;
|
|
107
|
+
if (this.kind !== undefined && match.kind.toLowerCase() !== this.kind) return null;
|
|
108
|
+
if (this.filePath !== undefined && match.path !== this.filePath) return null;
|
|
109
|
+
if (this.pathPrefix !== undefined && !match.path.startsWith(this.pathPrefix)) return null;
|
|
110
|
+
if (this.query === undefined) {
|
|
111
|
+
return new RankedPrecisionSymbolMatch({ match, score: 0 });
|
|
112
|
+
}
|
|
113
|
+
const score = this.query.score(match.name);
|
|
114
|
+
return score === null ? null : new RankedPrecisionSymbolMatch({ match, score });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sort(matches: readonly RankedPrecisionSymbolMatch[]): PrecisionSymbolMatch[] {
|
|
118
|
+
return [...matches]
|
|
119
|
+
.sort((a, b) =>
|
|
120
|
+
a.score - b.score ||
|
|
121
|
+
(this.query?.mode === "plain" ? a.match.name.length - b.match.name.length : 0) ||
|
|
122
|
+
a.match.path.localeCompare(b.match.path) ||
|
|
123
|
+
a.match.name.localeCompare(b.match.name)
|
|
124
|
+
)
|
|
125
|
+
.map((entry) => entry.match);
|
|
126
|
+
}
|
|
127
|
+
}
|