@gmickel/gno 1.22.0 → 1.24.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/README.md +33 -12
- package/assets/skill/SKILL.md +41 -19
- package/package.json +1 -1
- package/spec/cli.md +127 -20
- package/spec/evals-agentic.md +35 -0
- package/spec/evals.md +6 -0
- package/spec/mcp.md +18 -0
- package/spec/output-schemas/query-diagnose-v1.schema.json +123 -0
- package/spec/output-schemas/query-diagnose.schema.json +89 -2
- package/spec/output-schemas/setup-activation-result.schema.json +456 -0
- package/spec/output-schemas/setup-command-result.schema.json +93 -0
- package/spec/output-schemas/setup-receipt.schema.json +258 -0
- package/spec/output-schemas/setup-semantic-receipt.schema.json +195 -0
- package/src/app/context-runtime-types.ts +3 -0
- package/src/app/context-runtime.ts +1 -0
- package/src/app/context-surface.ts +4 -2
- package/src/cli/commands/ask.ts +31 -20
- package/src/cli/commands/completion/scripts.ts +2 -0
- package/src/cli/commands/context-build.ts +17 -7
- package/src/cli/commands/embed.ts +7 -2
- package/src/cli/commands/query.ts +58 -37
- package/src/cli/commands/search.ts +29 -19
- package/src/cli/commands/setup-activation.ts +324 -0
- package/src/cli/commands/setup-semantic.ts +591 -0
- package/src/cli/commands/setup.ts +410 -0
- package/src/cli/commands/vsearch.ts +31 -22
- package/src/cli/options.ts +39 -0
- package/src/cli/program.ts +112 -0
- package/src/cli/setup-semantic-worker.ts +177 -0
- package/src/config/defaults.ts +10 -1
- package/src/config/types.ts +71 -0
- package/src/core/config-mutation.ts +94 -64
- package/src/core/file-lock.ts +70 -31
- package/src/core/folder-setup-planning.ts +453 -0
- package/src/core/folder-setup.ts +490 -0
- package/src/core/project-affinity-surface.ts +114 -0
- package/src/core/project-affinity.ts +330 -0
- package/src/core/setup-activation.ts +309 -0
- package/src/core/setup-receipt.ts +321 -0
- package/src/core/validation.ts +20 -1
- package/src/mcp/tools/ask.ts +10 -1
- package/src/mcp/tools/context.ts +18 -0
- package/src/mcp/tools/index.ts +13 -2
- package/src/mcp/tools/query.ts +12 -0
- package/src/mcp/tools/search.ts +7 -0
- package/src/mcp/tools/vsearch.ts +7 -0
- package/src/pipeline/diagnose.ts +48 -3
- package/src/pipeline/explain.ts +54 -13
- package/src/pipeline/hybrid.ts +100 -59
- package/src/pipeline/project-affinity.ts +162 -0
- package/src/pipeline/search.ts +76 -10
- package/src/pipeline/types.ts +9 -0
- package/src/pipeline/vsearch.ts +117 -91
- package/src/sdk/client.ts +80 -20
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +20 -7
- package/src/serve/connectors.ts +29 -2
- package/src/serve/context-capsule.ts +18 -1
- package/src/serve/routes/api.ts +69 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical, privacy-bounded receipts for resumable folder setup.
|
|
3
|
+
*
|
|
4
|
+
* @module src/core/setup-receipt
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// node:fs/promises provides private file creation, directory permissions, and atomic rename APIs that Bun does not expose.
|
|
8
|
+
import { chmod, mkdir, open, rename, unlink } from "node:fs/promises";
|
|
9
|
+
// node:path has no Bun equivalent.
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import type { ActivationVerificationReceipt } from "../store/types";
|
|
13
|
+
|
|
14
|
+
import { canonicalizeIndexName } from "../app/index-name";
|
|
15
|
+
|
|
16
|
+
export const SETUP_RECEIPT_SCHEMA_VERSION = "1.0" as const;
|
|
17
|
+
|
|
18
|
+
export const SETUP_STAGE_NAMES = [
|
|
19
|
+
"preflight",
|
|
20
|
+
"config_saved",
|
|
21
|
+
"store_synced",
|
|
22
|
+
"lexical_indexed",
|
|
23
|
+
"lexical_proved",
|
|
24
|
+
"completed",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type SetupStageName = (typeof SETUP_STAGE_NAMES)[number];
|
|
28
|
+
export type SetupStageStatus = "pending" | "in_progress" | "passed" | "failed";
|
|
29
|
+
|
|
30
|
+
export interface SetupStageReceipt {
|
|
31
|
+
status: SetupStageStatus;
|
|
32
|
+
token: string | null;
|
|
33
|
+
startedAt: string | null;
|
|
34
|
+
completedAt: string | null;
|
|
35
|
+
code: string | null;
|
|
36
|
+
remediation: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SetupFailure {
|
|
40
|
+
stage: SetupStageName;
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
remediation: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FolderSetupReceipt {
|
|
47
|
+
schemaVersion: typeof SETUP_RECEIPT_SCHEMA_VERSION;
|
|
48
|
+
status: "in_progress" | "failed" | "completed";
|
|
49
|
+
generatedAt: string;
|
|
50
|
+
input: {
|
|
51
|
+
folder: string;
|
|
52
|
+
folderFingerprint: string;
|
|
53
|
+
indexName: string;
|
|
54
|
+
requestedName: string | null;
|
|
55
|
+
excludes: string[];
|
|
56
|
+
secretRiskAuthorized: boolean;
|
|
57
|
+
};
|
|
58
|
+
fingerprints: {
|
|
59
|
+
input: string;
|
|
60
|
+
config: string | null;
|
|
61
|
+
index: string | null;
|
|
62
|
+
};
|
|
63
|
+
collection: {
|
|
64
|
+
name: string | null;
|
|
65
|
+
path: string;
|
|
66
|
+
disposition: "pending" | "created" | "reused";
|
|
67
|
+
};
|
|
68
|
+
paths: {
|
|
69
|
+
config: string;
|
|
70
|
+
receipt: string;
|
|
71
|
+
};
|
|
72
|
+
stages: Record<SetupStageName, SetupStageReceipt>;
|
|
73
|
+
pending: string[];
|
|
74
|
+
failure: SetupFailure | null;
|
|
75
|
+
activation: ActivationVerificationReceipt | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type CanonicalJson =
|
|
79
|
+
| boolean
|
|
80
|
+
| null
|
|
81
|
+
| number
|
|
82
|
+
| string
|
|
83
|
+
| CanonicalJson[]
|
|
84
|
+
| { [key: string]: CanonicalJson };
|
|
85
|
+
|
|
86
|
+
function canonicalize(value: unknown): CanonicalJson {
|
|
87
|
+
if (
|
|
88
|
+
value === null ||
|
|
89
|
+
typeof value === "boolean" ||
|
|
90
|
+
typeof value === "number" ||
|
|
91
|
+
typeof value === "string"
|
|
92
|
+
) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value)) {
|
|
96
|
+
return value.map((item) => canonicalize(item));
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "object") {
|
|
99
|
+
const output: Record<string, CanonicalJson> = {};
|
|
100
|
+
for (const key of Object.keys(value).sort()) {
|
|
101
|
+
const item = (value as Record<string, unknown>)[key];
|
|
102
|
+
if (item !== undefined) {
|
|
103
|
+
output[key] = canonicalize(item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
throw new TypeError(`Unsupported canonical JSON value: ${typeof value}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function serializeSetupReceipt(receipt: FolderSetupReceipt): string {
|
|
112
|
+
return `${JSON.stringify(canonicalize(receipt), null, 2)}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function setupFingerprint(value: unknown): string {
|
|
116
|
+
return new Bun.CryptoHasher("sha256")
|
|
117
|
+
.update(JSON.stringify(canonicalize(value)))
|
|
118
|
+
.digest("hex");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function setupRootFingerprint(folderRealpath: string): string {
|
|
122
|
+
return new Bun.CryptoHasher("sha256").update(folderRealpath).digest("hex");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function getSetupReceiptPath(input: {
|
|
126
|
+
dataDir: string;
|
|
127
|
+
indexName: string;
|
|
128
|
+
folderRealpath: string;
|
|
129
|
+
}): string {
|
|
130
|
+
const indexIdentity = canonicalizeIndexName(input.indexName);
|
|
131
|
+
const rootFingerprint = setupRootFingerprint(input.folderRealpath);
|
|
132
|
+
return join(
|
|
133
|
+
input.dataDir,
|
|
134
|
+
"setup-receipts",
|
|
135
|
+
indexIdentity,
|
|
136
|
+
`${rootFingerprint}.json`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function createSetupReceipt(input: {
|
|
141
|
+
now: string;
|
|
142
|
+
folder: string;
|
|
143
|
+
indexName: string;
|
|
144
|
+
requestedName?: string;
|
|
145
|
+
excludes: string[];
|
|
146
|
+
secretRiskAuthorized: boolean;
|
|
147
|
+
configPath: string;
|
|
148
|
+
dataDir: string;
|
|
149
|
+
}): FolderSetupReceipt {
|
|
150
|
+
const indexName = canonicalizeIndexName(input.indexName);
|
|
151
|
+
const folderFingerprint = setupRootFingerprint(input.folder);
|
|
152
|
+
const receiptPath = getSetupReceiptPath({
|
|
153
|
+
dataDir: input.dataDir,
|
|
154
|
+
indexName,
|
|
155
|
+
folderRealpath: input.folder,
|
|
156
|
+
});
|
|
157
|
+
const stages = Object.fromEntries(
|
|
158
|
+
SETUP_STAGE_NAMES.map((stage) => [
|
|
159
|
+
stage,
|
|
160
|
+
{
|
|
161
|
+
status: "pending",
|
|
162
|
+
token: null,
|
|
163
|
+
startedAt: null,
|
|
164
|
+
completedAt: null,
|
|
165
|
+
code: null,
|
|
166
|
+
remediation: null,
|
|
167
|
+
},
|
|
168
|
+
])
|
|
169
|
+
) as Record<SetupStageName, SetupStageReceipt>;
|
|
170
|
+
const requestedName = input.requestedName?.trim().toLowerCase() || null;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
schemaVersion: SETUP_RECEIPT_SCHEMA_VERSION,
|
|
174
|
+
status: "in_progress",
|
|
175
|
+
generatedAt: input.now,
|
|
176
|
+
input: {
|
|
177
|
+
folder: input.folder,
|
|
178
|
+
folderFingerprint,
|
|
179
|
+
indexName,
|
|
180
|
+
requestedName,
|
|
181
|
+
excludes: [...new Set(input.excludes)].sort(),
|
|
182
|
+
secretRiskAuthorized: input.secretRiskAuthorized,
|
|
183
|
+
},
|
|
184
|
+
fingerprints: {
|
|
185
|
+
input: setupFingerprint({
|
|
186
|
+
folder: input.folder,
|
|
187
|
+
indexName,
|
|
188
|
+
requestedName,
|
|
189
|
+
excludes: [...new Set(input.excludes)].sort(),
|
|
190
|
+
secretRiskAuthorized: input.secretRiskAuthorized,
|
|
191
|
+
}),
|
|
192
|
+
config: null,
|
|
193
|
+
index: null,
|
|
194
|
+
},
|
|
195
|
+
collection: {
|
|
196
|
+
name: null,
|
|
197
|
+
path: input.folder,
|
|
198
|
+
disposition: "pending",
|
|
199
|
+
},
|
|
200
|
+
paths: {
|
|
201
|
+
config: input.configPath,
|
|
202
|
+
receipt: receiptPath,
|
|
203
|
+
},
|
|
204
|
+
stages,
|
|
205
|
+
pending: [],
|
|
206
|
+
failure: null,
|
|
207
|
+
activation: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function startSetupStage(
|
|
212
|
+
receipt: FolderSetupReceipt,
|
|
213
|
+
stage: SetupStageName,
|
|
214
|
+
now: string
|
|
215
|
+
): void {
|
|
216
|
+
receipt.status = "in_progress";
|
|
217
|
+
receipt.generatedAt = now;
|
|
218
|
+
receipt.failure = null;
|
|
219
|
+
receipt.stages[stage] = {
|
|
220
|
+
status: "in_progress",
|
|
221
|
+
token: setupFingerprint({
|
|
222
|
+
receipt: receipt.input.folderFingerprint,
|
|
223
|
+
stage,
|
|
224
|
+
startedAt: now,
|
|
225
|
+
}),
|
|
226
|
+
startedAt: now,
|
|
227
|
+
completedAt: null,
|
|
228
|
+
code: null,
|
|
229
|
+
remediation: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function passSetupStage(
|
|
234
|
+
receipt: FolderSetupReceipt,
|
|
235
|
+
stage: SetupStageName,
|
|
236
|
+
now: string
|
|
237
|
+
): void {
|
|
238
|
+
receipt.generatedAt = now;
|
|
239
|
+
receipt.stages[stage] = {
|
|
240
|
+
...receipt.stages[stage],
|
|
241
|
+
status: "passed",
|
|
242
|
+
completedAt: now,
|
|
243
|
+
code: null,
|
|
244
|
+
remediation: null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function failSetupStage(
|
|
249
|
+
receipt: FolderSetupReceipt,
|
|
250
|
+
failure: SetupFailure,
|
|
251
|
+
now: string
|
|
252
|
+
): void {
|
|
253
|
+
const current = receipt.stages[failure.stage];
|
|
254
|
+
receipt.status = "failed";
|
|
255
|
+
receipt.generatedAt = now;
|
|
256
|
+
receipt.failure = failure;
|
|
257
|
+
if (current.status !== "passed") {
|
|
258
|
+
receipt.stages[failure.stage] = {
|
|
259
|
+
...current,
|
|
260
|
+
status: "failed",
|
|
261
|
+
token:
|
|
262
|
+
current.token ??
|
|
263
|
+
setupFingerprint({
|
|
264
|
+
receipt: receipt.input.folderFingerprint,
|
|
265
|
+
stage: failure.stage,
|
|
266
|
+
startedAt: now,
|
|
267
|
+
}),
|
|
268
|
+
startedAt: current.startedAt ?? now,
|
|
269
|
+
completedAt: now,
|
|
270
|
+
code: failure.code,
|
|
271
|
+
remediation: failure.remediation,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function persistSetupReceipt(
|
|
277
|
+
receipt: FolderSetupReceipt
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
const receiptDir = dirname(receipt.paths.receipt);
|
|
280
|
+
await mkdir(receiptDir, { recursive: true, mode: 0o700 });
|
|
281
|
+
await chmod(receiptDir, 0o700);
|
|
282
|
+
|
|
283
|
+
const tempPath = `${receipt.paths.receipt}.tmp.${crypto.randomUUID()}`;
|
|
284
|
+
let tempFile: Awaited<ReturnType<typeof open>> | null = null;
|
|
285
|
+
try {
|
|
286
|
+
tempFile = await open(tempPath, "wx", 0o600);
|
|
287
|
+
await tempFile.writeFile(serializeSetupReceipt(receipt), "utf8");
|
|
288
|
+
await tempFile.sync();
|
|
289
|
+
await tempFile.close();
|
|
290
|
+
tempFile = null;
|
|
291
|
+
await rename(tempPath, receipt.paths.receipt);
|
|
292
|
+
await chmod(receipt.paths.receipt, 0o600);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
await tempFile?.close().catch(() => {
|
|
295
|
+
/* best-effort temporary receipt handle cleanup */
|
|
296
|
+
});
|
|
297
|
+
await unlink(tempPath).catch(() => {
|
|
298
|
+
/* best-effort temporary receipt cleanup */
|
|
299
|
+
});
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function loadSetupReceipt(
|
|
305
|
+
path: string
|
|
306
|
+
): Promise<FolderSetupReceipt | null> {
|
|
307
|
+
const file = Bun.file(path);
|
|
308
|
+
if (!(await file.exists())) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const value: unknown = await file.json();
|
|
312
|
+
if (
|
|
313
|
+
typeof value !== "object" ||
|
|
314
|
+
value === null ||
|
|
315
|
+
!("schemaVersion" in value) ||
|
|
316
|
+
value.schemaVersion !== SETUP_RECEIPT_SCHEMA_VERSION
|
|
317
|
+
) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return value as FolderSetupReceipt;
|
|
321
|
+
}
|
package/src/core/validation.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { realpath } from "node:fs/promises";
|
|
|
9
9
|
// node:os for homedir (no Bun os utils)
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
// node:path for path utils (no Bun path utils)
|
|
12
|
-
import { isAbsolute, join, posix as pathPosix } from "node:path";
|
|
12
|
+
import { isAbsolute, join, posix as pathPosix, relative, sep } from "node:path";
|
|
13
13
|
|
|
14
14
|
import { toAbsolutePath } from "../config/paths";
|
|
15
15
|
|
|
@@ -58,6 +58,25 @@ export function validateRelPath(relPath: string): string {
|
|
|
58
58
|
return normalized;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Return whether candidate is equal to or nested beneath parent.
|
|
63
|
+
*
|
|
64
|
+
* Inputs must already be canonical absolute paths. `relative()` keeps this
|
|
65
|
+
* segment-safe, unlike string-prefix checks (`/project` vs `/project-old`).
|
|
66
|
+
*/
|
|
67
|
+
export function isCanonicalPathContained(
|
|
68
|
+
parent: string,
|
|
69
|
+
candidate: string
|
|
70
|
+
): boolean {
|
|
71
|
+
const relativePath = relative(parent, candidate);
|
|
72
|
+
return (
|
|
73
|
+
relativePath === "" ||
|
|
74
|
+
(relativePath !== ".." &&
|
|
75
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
76
|
+
!isAbsolute(relativePath))
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
export async function validateCollectionRoot(
|
|
62
81
|
inputPath: string
|
|
63
82
|
): Promise<string> {
|
package/src/mcp/tools/ask.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ToolContext } from "../server";
|
|
|
8
8
|
import type { ToolResult } from "./index";
|
|
9
9
|
|
|
10
10
|
import { buildVerifiedAsk } from "../../app/verified-ask";
|
|
11
|
+
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
11
12
|
import {
|
|
12
13
|
finishRetrievalTraceAfterError,
|
|
13
14
|
retrievalTraceFilters,
|
|
@@ -30,6 +31,7 @@ const queryModeSchema = z
|
|
|
30
31
|
export const askInputSchema = z
|
|
31
32
|
.object({
|
|
32
33
|
query: z.string().trim().min(1),
|
|
34
|
+
projectHints: z.array(z.string()).max(16).optional(),
|
|
33
35
|
verify: z.literal(true),
|
|
34
36
|
collection: z.string().optional(),
|
|
35
37
|
limit: z.number().int().min(1).max(100).default(5),
|
|
@@ -153,8 +155,14 @@ export const handleAsk = (
|
|
|
153
155
|
);
|
|
154
156
|
if (!normalized.ok) throw new Error(normalized.error.message);
|
|
155
157
|
const query = normalized.value.query;
|
|
158
|
+
const { projectHints, ...askInput } = args;
|
|
159
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
160
|
+
context.config,
|
|
161
|
+
projectHints
|
|
162
|
+
);
|
|
156
163
|
const options = {
|
|
157
|
-
...
|
|
164
|
+
...askInput,
|
|
165
|
+
projectAffinity,
|
|
158
166
|
queryModes:
|
|
159
167
|
normalized.value.queryModes.length > 0
|
|
160
168
|
? normalized.value.queryModes
|
|
@@ -210,6 +218,7 @@ export const handleAsk = (
|
|
|
210
218
|
embedPort: modelPorts.embedPort,
|
|
211
219
|
rerankPort: modelPorts.rerankPort,
|
|
212
220
|
genPort: modelPorts.genPort,
|
|
221
|
+
projectAffinity,
|
|
213
222
|
traceSession,
|
|
214
223
|
});
|
|
215
224
|
const finished = await traceSession?.finish(
|
package/src/mcp/tools/context.ts
CHANGED
|
@@ -25,6 +25,11 @@ import {
|
|
|
25
25
|
parseContextVerifySurfaceInput,
|
|
26
26
|
} from "../../app/context-surface";
|
|
27
27
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
28
|
+
import { ContextCapsuleContractError } from "../../core/context-capsule";
|
|
29
|
+
import {
|
|
30
|
+
ProjectAffinityInputError,
|
|
31
|
+
resolveRemoteProjectAffinity,
|
|
32
|
+
} from "../../core/project-affinity-surface";
|
|
28
33
|
import {
|
|
29
34
|
finishRetrievalTraceAfterError,
|
|
30
35
|
startRetrievalTraceRequest,
|
|
@@ -224,6 +229,18 @@ export const handleContext = (
|
|
|
224
229
|
null;
|
|
225
230
|
let traceSession: RetrievalTraceSession | undefined;
|
|
226
231
|
try {
|
|
232
|
+
let projectAffinity;
|
|
233
|
+
try {
|
|
234
|
+
projectAffinity = await resolveRemoteProjectAffinity(
|
|
235
|
+
context.config,
|
|
236
|
+
parsed.projectHints
|
|
237
|
+
);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error instanceof ProjectAffinityInputError) {
|
|
240
|
+
throw new ContextCapsuleContractError("invalid_input", error.message);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
227
244
|
const traceStart = await startRetrievalTraceRequest({
|
|
228
245
|
store: context.store,
|
|
229
246
|
config: context.config,
|
|
@@ -261,6 +278,7 @@ export const handleContext = (
|
|
|
261
278
|
vectorIndex: modelPorts?.vectorIndex ?? null,
|
|
262
279
|
embedPort: modelPorts?.embedPort ?? null,
|
|
263
280
|
rerankPort: modelPorts?.rerankPort ?? null,
|
|
281
|
+
projectAffinity,
|
|
264
282
|
traceSession,
|
|
265
283
|
});
|
|
266
284
|
const finalized = await traceSession?.finish("completed");
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -131,13 +131,22 @@ export const MCP_WRITE_TOOL_NAMES = new Set([
|
|
|
131
131
|
// Shared Input Schemas
|
|
132
132
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
133
|
|
|
134
|
-
const
|
|
134
|
+
const projectHintsInputSchema = z
|
|
135
|
+
.array(z.string())
|
|
136
|
+
.max(16)
|
|
137
|
+
.optional()
|
|
138
|
+
.describe(
|
|
139
|
+
"Opaque caller project hints for cross-surface parity; remote hints never inspect server paths"
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
export const searchInputSchema = z.object({
|
|
135
143
|
query: z
|
|
136
144
|
.string()
|
|
137
145
|
.min(1, "Query cannot be empty")
|
|
138
146
|
.describe(
|
|
139
147
|
"Exact keyword, identifier, filename, error text, or phrase to match with BM25"
|
|
140
148
|
),
|
|
149
|
+
projectHints: projectHintsInputSchema,
|
|
141
150
|
collection: z
|
|
142
151
|
.string()
|
|
143
152
|
.optional()
|
|
@@ -378,13 +387,14 @@ const duplicateNoteInputSchema = z.object({
|
|
|
378
387
|
name: z.string().optional(),
|
|
379
388
|
});
|
|
380
389
|
|
|
381
|
-
const vsearchInputSchema = z.object({
|
|
390
|
+
export const vsearchInputSchema = z.object({
|
|
382
391
|
query: z
|
|
383
392
|
.string()
|
|
384
393
|
.min(1, "Query cannot be empty")
|
|
385
394
|
.describe(
|
|
386
395
|
"Natural-language concept to match semantically; use gno_search for exact error text or identifiers"
|
|
387
396
|
),
|
|
397
|
+
projectHints: projectHintsInputSchema,
|
|
388
398
|
collection: z
|
|
389
399
|
.string()
|
|
390
400
|
.optional()
|
|
@@ -455,6 +465,7 @@ export const queryInputSchema = z.object({
|
|
|
455
465
|
.describe(
|
|
456
466
|
"Primary user query; combine with intent or queryModes for ambiguous requests"
|
|
457
467
|
),
|
|
468
|
+
projectHints: projectHintsInputSchema,
|
|
458
469
|
collection: z
|
|
459
470
|
.string()
|
|
460
471
|
.optional()
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
normalizeContentTypes,
|
|
27
27
|
} from "../../config";
|
|
28
28
|
import { resolveDepthPolicy } from "../../core/depth-policy";
|
|
29
|
+
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
29
30
|
import {
|
|
30
31
|
finishRetrievalTraceAfterError,
|
|
31
32
|
retrievalTraceFilters,
|
|
@@ -49,6 +50,7 @@ import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
|
49
50
|
|
|
50
51
|
interface QueryInput {
|
|
51
52
|
query: string;
|
|
53
|
+
projectHints?: string[];
|
|
52
54
|
collection?: string;
|
|
53
55
|
limit?: number;
|
|
54
56
|
minScore?: number;
|
|
@@ -227,6 +229,10 @@ export function handleQuery(
|
|
|
227
229
|
hasStructuredModes,
|
|
228
230
|
});
|
|
229
231
|
const { noExpand, noRerank } = depthPolicy;
|
|
232
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
233
|
+
ctx.config,
|
|
234
|
+
args.projectHints
|
|
235
|
+
);
|
|
230
236
|
const expandUri =
|
|
231
237
|
!noExpand && !hasStructuredModes
|
|
232
238
|
? resolveModelUri(ctx.config, "expand", undefined, args.collection)
|
|
@@ -253,6 +259,7 @@ export function handleQuery(
|
|
|
253
259
|
queryModes,
|
|
254
260
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
255
261
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
262
|
+
projectAffinity,
|
|
256
263
|
};
|
|
257
264
|
|
|
258
265
|
try {
|
|
@@ -440,6 +447,10 @@ export function handleQueryDiagnose(
|
|
|
440
447
|
hasStructuredModes,
|
|
441
448
|
});
|
|
442
449
|
const { noExpand, noRerank } = depthPolicy;
|
|
450
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
451
|
+
ctx.config,
|
|
452
|
+
args.projectHints
|
|
453
|
+
);
|
|
443
454
|
|
|
444
455
|
if (!args.fast) {
|
|
445
456
|
const embedResult = await llm.createEmbeddingPort(embedUri, {
|
|
@@ -524,6 +535,7 @@ export function handleQueryDiagnose(
|
|
|
524
535
|
queryModes,
|
|
525
536
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
526
537
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
538
|
+
projectAffinity,
|
|
527
539
|
contentTypeRules,
|
|
528
540
|
contentTypeRulesFingerprint:
|
|
529
541
|
fingerprintContentTypeRules(contentTypeRules),
|
package/src/mcp/tools/search.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { SearchResult, SearchResults } from "../../pipeline/types";
|
|
|
11
11
|
import type { ToolContext } from "../server";
|
|
12
12
|
|
|
13
13
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
14
|
+
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
14
15
|
import {
|
|
15
16
|
finishRetrievalTraceAfterError,
|
|
16
17
|
retrievalTraceFilters,
|
|
@@ -22,6 +23,7 @@ import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
|
22
23
|
|
|
23
24
|
interface SearchInput {
|
|
24
25
|
query: string;
|
|
26
|
+
projectHints?: string[];
|
|
25
27
|
collection?: string;
|
|
26
28
|
limit?: number;
|
|
27
29
|
minScore?: number;
|
|
@@ -113,6 +115,10 @@ export function handleSearch(
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
119
|
+
ctx.config,
|
|
120
|
+
args.projectHints
|
|
121
|
+
);
|
|
116
122
|
const options = {
|
|
117
123
|
limit: args.limit ?? 5,
|
|
118
124
|
minScore: args.minScore,
|
|
@@ -126,6 +132,7 @@ export function handleSearch(
|
|
|
126
132
|
author: args.author,
|
|
127
133
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
128
134
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
135
|
+
projectAffinity,
|
|
129
136
|
};
|
|
130
137
|
let traceSession: RetrievalTraceSession | undefined;
|
|
131
138
|
try {
|
package/src/mcp/tools/vsearch.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ToolContext } from "../server";
|
|
|
12
12
|
|
|
13
13
|
import { decorateUriForIndex, parseUri } from "../../app/constants";
|
|
14
14
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
15
|
+
import { resolveRemoteProjectAffinity } from "../../core/project-affinity-surface";
|
|
15
16
|
import {
|
|
16
17
|
finishRetrievalTraceAfterError,
|
|
17
18
|
retrievalTraceFilters,
|
|
@@ -34,6 +35,7 @@ import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
|
34
35
|
|
|
35
36
|
interface VsearchInput {
|
|
36
37
|
query: string;
|
|
38
|
+
projectHints?: string[];
|
|
37
39
|
collection?: string;
|
|
38
40
|
limit?: number;
|
|
39
41
|
minScore?: number;
|
|
@@ -135,6 +137,10 @@ export function handleVsearch(
|
|
|
135
137
|
undefined,
|
|
136
138
|
args.collection
|
|
137
139
|
);
|
|
140
|
+
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
141
|
+
ctx.config,
|
|
142
|
+
args.projectHints
|
|
143
|
+
);
|
|
138
144
|
const options = {
|
|
139
145
|
limit: args.limit ?? 5,
|
|
140
146
|
minScore: args.minScore,
|
|
@@ -147,6 +153,7 @@ export function handleVsearch(
|
|
|
147
153
|
author: args.author,
|
|
148
154
|
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
149
155
|
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
156
|
+
projectAffinity,
|
|
150
157
|
};
|
|
151
158
|
let traceSession: RetrievalTraceSession | undefined;
|
|
152
159
|
const traceStart = await startRetrievalTraceRequest({
|
package/src/pipeline/diagnose.ts
CHANGED
|
@@ -19,6 +19,12 @@ import { resolveDocRef } from "../core/ref-parser";
|
|
|
19
19
|
import { err, ok } from "../store/types";
|
|
20
20
|
import { evaluateQueryTargetFilters } from "./filters";
|
|
21
21
|
import { searchHybrid } from "./hybrid";
|
|
22
|
+
import {
|
|
23
|
+
getProjectAffinityMetadata,
|
|
24
|
+
type ProjectAffinityScoringInput,
|
|
25
|
+
type ProjectAffinityScoreMetadata,
|
|
26
|
+
scoreProjectAffinity,
|
|
27
|
+
} from "./project-affinity";
|
|
22
28
|
|
|
23
29
|
export type QueryDiagnoseTargetStatus =
|
|
24
30
|
| "not_found"
|
|
@@ -46,7 +52,7 @@ export interface QueryDiagnoseStage {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export interface QueryDiagnoseResult {
|
|
49
|
-
schemaVersion: "1.0";
|
|
55
|
+
schemaVersion: "1.0" | "1.1";
|
|
50
56
|
query: string;
|
|
51
57
|
target: {
|
|
52
58
|
ref: string;
|
|
@@ -65,6 +71,7 @@ export interface QueryDiagnoseResult {
|
|
|
65
71
|
filterReasons: string[];
|
|
66
72
|
};
|
|
67
73
|
stages: QueryDiagnoseStage[];
|
|
74
|
+
affinity?: ProjectAffinityScoreMetadata;
|
|
68
75
|
chunk: {
|
|
69
76
|
seq: number | null;
|
|
70
77
|
startLine: number | null;
|
|
@@ -155,6 +162,15 @@ function findTargetCandidate(
|
|
|
155
162
|
);
|
|
156
163
|
}
|
|
157
164
|
|
|
165
|
+
const hasTrustedProjectAffinityInput = (
|
|
166
|
+
input: ProjectAffinityScoringInput | undefined
|
|
167
|
+
): boolean =>
|
|
168
|
+
input?.enabled !== false &&
|
|
169
|
+
Boolean(
|
|
170
|
+
input?.resolution.matches.length ||
|
|
171
|
+
input?.resolution.roots.some((root) => root.source !== "remote_hint")
|
|
172
|
+
);
|
|
173
|
+
|
|
158
174
|
export async function diagnoseQueryTarget(
|
|
159
175
|
deps: HybridSearchDeps,
|
|
160
176
|
query: string,
|
|
@@ -276,8 +292,28 @@ export async function diagnoseQueryTarget(
|
|
|
276
292
|
chunks.find((chunk) => chunk.seq === firstMatched?.seq) ??
|
|
277
293
|
chunks[0] ??
|
|
278
294
|
null;
|
|
295
|
+
const targetResult = searchResult.value.results.find(
|
|
296
|
+
(result) => result.uri === doc.uri
|
|
297
|
+
);
|
|
298
|
+
const lastMatched = trace?.stages
|
|
299
|
+
.toReversed()
|
|
300
|
+
.flatMap((stage) => stage.candidates)
|
|
301
|
+
.find(
|
|
302
|
+
(candidate) =>
|
|
303
|
+
candidate.mirrorHash === doc.mirrorHash && targetSeqs.has(candidate.seq)
|
|
304
|
+
);
|
|
305
|
+
const affinity =
|
|
306
|
+
(targetResult ? getProjectAffinityMetadata(targetResult) : undefined) ??
|
|
307
|
+
(lastMatched && options.projectAffinity
|
|
308
|
+
? scoreProjectAffinity(
|
|
309
|
+
lastMatched.score,
|
|
310
|
+
doc.collection,
|
|
311
|
+
options.projectAffinity,
|
|
312
|
+
{ kind: "hybrid_blended", score: lastMatched.score }
|
|
313
|
+
)
|
|
314
|
+
: null);
|
|
279
315
|
|
|
280
|
-
|
|
316
|
+
const baseResult: QueryDiagnoseResult = {
|
|
281
317
|
...buildBaseResult(query, options.target, "diagnosed", doc, {
|
|
282
318
|
graphHints,
|
|
283
319
|
chunkCount: chunks.length,
|
|
@@ -298,5 +334,14 @@ export async function diagnoseQueryTarget(
|
|
|
298
334
|
totalResults: searchResult.value.meta.totalResults,
|
|
299
335
|
queryModes: searchResult.value.meta.queryModes,
|
|
300
336
|
},
|
|
301
|
-
}
|
|
337
|
+
};
|
|
338
|
+
return ok(
|
|
339
|
+
affinity && hasTrustedProjectAffinityInput(options.projectAffinity)
|
|
340
|
+
? {
|
|
341
|
+
...baseResult,
|
|
342
|
+
schemaVersion: "1.1",
|
|
343
|
+
affinity,
|
|
344
|
+
}
|
|
345
|
+
: baseResult
|
|
346
|
+
);
|
|
302
347
|
}
|