@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,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core-only verified folder setup orchestration.
|
|
3
|
+
*
|
|
4
|
+
* @module src/core/folder-setup
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Collection } from "../config";
|
|
8
|
+
import type { SyncService } from "../ingestion";
|
|
9
|
+
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
10
|
+
|
|
11
|
+
import { DEFAULT_EXCLUDES, loadConfig } from "../config";
|
|
12
|
+
import { defaultSyncService, withContentTypeRules } from "../ingestion";
|
|
13
|
+
import { verifyLexicalActivation } from "./activation-verifier";
|
|
14
|
+
import { applyConfigChange } from "./config-mutation";
|
|
15
|
+
import {
|
|
16
|
+
type CollectionSelection,
|
|
17
|
+
type FolderSetupError,
|
|
18
|
+
type FolderSetupErrorCode,
|
|
19
|
+
normalizeSetupExcludes,
|
|
20
|
+
preflightFolder,
|
|
21
|
+
resolveSetupFolder,
|
|
22
|
+
resolveSetupStoreIndex,
|
|
23
|
+
selectFolderCollection,
|
|
24
|
+
setupError,
|
|
25
|
+
setupExcludesMatch,
|
|
26
|
+
setupFilterDisagreement,
|
|
27
|
+
setupInjectedFailure,
|
|
28
|
+
validateSetupOutputPaths,
|
|
29
|
+
} from "./folder-setup-planning";
|
|
30
|
+
import {
|
|
31
|
+
createSetupReceipt,
|
|
32
|
+
failSetupStage,
|
|
33
|
+
type FolderSetupReceipt,
|
|
34
|
+
getSetupReceiptPath,
|
|
35
|
+
passSetupStage,
|
|
36
|
+
persistSetupReceipt,
|
|
37
|
+
setupFingerprint,
|
|
38
|
+
type SetupFailure,
|
|
39
|
+
type SetupStageName,
|
|
40
|
+
startSetupStage,
|
|
41
|
+
} from "./setup-receipt";
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
FolderSetupError,
|
|
45
|
+
FolderSetupErrorCode,
|
|
46
|
+
} from "./folder-setup-planning";
|
|
47
|
+
|
|
48
|
+
export type FolderSetupFailurePoint =
|
|
49
|
+
| "after_config_save"
|
|
50
|
+
| "after_store_sync"
|
|
51
|
+
| "after_lexical_index"
|
|
52
|
+
| "after_lexical_proof";
|
|
53
|
+
|
|
54
|
+
export interface FolderSetupOptions {
|
|
55
|
+
folder: string;
|
|
56
|
+
store: SqliteAdapter;
|
|
57
|
+
configPath: string;
|
|
58
|
+
dataDir: string;
|
|
59
|
+
indexName?: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
exclude?: string[];
|
|
62
|
+
secretRiskAuthorized?: boolean;
|
|
63
|
+
/** Test-only deterministic interruption hook. */
|
|
64
|
+
failureInjection?: FolderSetupFailurePoint;
|
|
65
|
+
/** Test-only concurrency seam before the serialized config boundary. */
|
|
66
|
+
beforeConfigBoundary?: () => Promise<void>;
|
|
67
|
+
/** Test-only receipt persistence seam. */
|
|
68
|
+
receiptWriter?: (receipt: FolderSetupReceipt) => Promise<void>;
|
|
69
|
+
syncService?: SyncService;
|
|
70
|
+
now?: () => Date;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type FolderSetupResult =
|
|
74
|
+
| { ok: true; receipt: FolderSetupReceipt }
|
|
75
|
+
| {
|
|
76
|
+
ok: false;
|
|
77
|
+
error: FolderSetupError;
|
|
78
|
+
receipt: FolderSetupReceipt | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
class SetupAbort extends Error {
|
|
82
|
+
readonly result: FolderSetupResult;
|
|
83
|
+
|
|
84
|
+
constructor(result: FolderSetupResult) {
|
|
85
|
+
super("Folder setup aborted");
|
|
86
|
+
this.result = result;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function nowIso(options: FolderSetupOptions): string {
|
|
91
|
+
return (options.now ?? (() => new Date()))().toISOString();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function stageFailure(
|
|
95
|
+
stage: SetupStageName,
|
|
96
|
+
error: FolderSetupError
|
|
97
|
+
): SetupFailure {
|
|
98
|
+
return { stage, ...error };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function writeReceipt(
|
|
102
|
+
receipt: FolderSetupReceipt,
|
|
103
|
+
stage: SetupStageName,
|
|
104
|
+
options: FolderSetupOptions
|
|
105
|
+
): Promise<FolderSetupResult | null> {
|
|
106
|
+
try {
|
|
107
|
+
await (options.receiptWriter ?? persistSetupReceipt)(receipt);
|
|
108
|
+
return null;
|
|
109
|
+
} catch {
|
|
110
|
+
const error = setupError(
|
|
111
|
+
"receipt_write_failed",
|
|
112
|
+
`Failed to persist setup receipt: ${receipt.paths.receipt}`,
|
|
113
|
+
"Check local data-directory permissions and retry."
|
|
114
|
+
);
|
|
115
|
+
failSetupStage(receipt, stageFailure(stage, error), nowIso(options));
|
|
116
|
+
return { ok: false, error, receipt };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function persistFailure(
|
|
121
|
+
receipt: FolderSetupReceipt,
|
|
122
|
+
stage: SetupStageName,
|
|
123
|
+
error: FolderSetupError,
|
|
124
|
+
options: FolderSetupOptions
|
|
125
|
+
): Promise<FolderSetupResult> {
|
|
126
|
+
failSetupStage(receipt, stageFailure(stage, error), nowIso(options));
|
|
127
|
+
const writeFailure = await writeReceipt(receipt, stage, options);
|
|
128
|
+
return writeFailure ?? { ok: false, error, receipt };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function setupFolder(
|
|
132
|
+
options: FolderSetupOptions
|
|
133
|
+
): Promise<FolderSetupResult> {
|
|
134
|
+
const resolved = await resolveSetupFolder(options.folder);
|
|
135
|
+
if ("error" in resolved) {
|
|
136
|
+
return { ok: false, error: resolved.error, receipt: null };
|
|
137
|
+
}
|
|
138
|
+
const folder = resolved.folder;
|
|
139
|
+
|
|
140
|
+
const storeIdentity = await resolveSetupStoreIndex({
|
|
141
|
+
store: options.store,
|
|
142
|
+
requestedIndexName: options.indexName,
|
|
143
|
+
});
|
|
144
|
+
if ("code" in storeIdentity) {
|
|
145
|
+
return { ok: false, error: storeIdentity, receipt: null };
|
|
146
|
+
}
|
|
147
|
+
const receiptPath = getSetupReceiptPath({
|
|
148
|
+
dataDir: options.dataDir,
|
|
149
|
+
indexName: storeIdentity.indexName,
|
|
150
|
+
folderRealpath: folder,
|
|
151
|
+
});
|
|
152
|
+
const configLockPath = `${options.configPath}.setup.lock`;
|
|
153
|
+
const unsafeOutput = await validateSetupOutputPaths(folder, [
|
|
154
|
+
{ label: "Data directory", path: options.dataDir },
|
|
155
|
+
{ label: "Setup receipt", path: receiptPath },
|
|
156
|
+
{ label: "Config", path: options.configPath },
|
|
157
|
+
{ label: "Config lock", path: configLockPath },
|
|
158
|
+
{ label: "Index database", path: storeIdentity.dbPath },
|
|
159
|
+
]);
|
|
160
|
+
if (unsafeOutput) {
|
|
161
|
+
return { ok: false, error: unsafeOutput, receipt: null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const requestedExcludes = normalizeSetupExcludes(
|
|
165
|
+
options.exclude?.length ? options.exclude : DEFAULT_EXCLUDES
|
|
166
|
+
);
|
|
167
|
+
const loaded = await loadConfig(options.configPath);
|
|
168
|
+
let initialSelection: CollectionSelection | FolderSetupError | null = null;
|
|
169
|
+
let effectiveExcludes = requestedExcludes;
|
|
170
|
+
if (loaded.ok) {
|
|
171
|
+
initialSelection = await selectFolderCollection(
|
|
172
|
+
loaded.value,
|
|
173
|
+
folder,
|
|
174
|
+
options.name,
|
|
175
|
+
requestedExcludes
|
|
176
|
+
);
|
|
177
|
+
if (
|
|
178
|
+
!("code" in initialSelection) &&
|
|
179
|
+
initialSelection.disposition === "reused"
|
|
180
|
+
) {
|
|
181
|
+
if (
|
|
182
|
+
options.exclude !== undefined &&
|
|
183
|
+
!setupExcludesMatch(
|
|
184
|
+
requestedExcludes,
|
|
185
|
+
initialSelection.collection.exclude
|
|
186
|
+
)
|
|
187
|
+
) {
|
|
188
|
+
initialSelection = setupFilterDisagreement(initialSelection.collection);
|
|
189
|
+
} else {
|
|
190
|
+
effectiveExcludes = normalizeSetupExcludes(
|
|
191
|
+
initialSelection.collection.exclude
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const receipt = createSetupReceipt({
|
|
198
|
+
now: nowIso(options),
|
|
199
|
+
folder,
|
|
200
|
+
indexName: storeIdentity.indexName,
|
|
201
|
+
requestedName: options.name,
|
|
202
|
+
excludes: effectiveExcludes,
|
|
203
|
+
secretRiskAuthorized: options.secretRiskAuthorized ?? false,
|
|
204
|
+
configPath: options.configPath,
|
|
205
|
+
dataDir: options.dataDir,
|
|
206
|
+
});
|
|
207
|
+
startSetupStage(receipt, "preflight", nowIso(options));
|
|
208
|
+
let writeFailure = await writeReceipt(receipt, "preflight", options);
|
|
209
|
+
if (writeFailure) {
|
|
210
|
+
return writeFailure;
|
|
211
|
+
}
|
|
212
|
+
if (initialSelection && "code" in initialSelection) {
|
|
213
|
+
return persistFailure(receipt, "preflight", initialSelection, options);
|
|
214
|
+
}
|
|
215
|
+
const preflightError = await preflightFolder(
|
|
216
|
+
folder,
|
|
217
|
+
effectiveExcludes,
|
|
218
|
+
options.secretRiskAuthorized ?? false
|
|
219
|
+
);
|
|
220
|
+
if (preflightError) {
|
|
221
|
+
return persistFailure(receipt, "preflight", preflightError, options);
|
|
222
|
+
}
|
|
223
|
+
passSetupStage(receipt, "preflight", nowIso(options));
|
|
224
|
+
|
|
225
|
+
startSetupStage(receipt, "config_saved", nowIso(options));
|
|
226
|
+
writeFailure = await writeReceipt(receipt, "config_saved", options);
|
|
227
|
+
if (writeFailure) {
|
|
228
|
+
return writeFailure;
|
|
229
|
+
}
|
|
230
|
+
if (!loaded.ok) {
|
|
231
|
+
return persistFailure(
|
|
232
|
+
receipt,
|
|
233
|
+
"config_saved",
|
|
234
|
+
setupError(
|
|
235
|
+
"config_load_failed",
|
|
236
|
+
loaded.error.message,
|
|
237
|
+
"Initialize or repair the selected GNO config, then retry."
|
|
238
|
+
),
|
|
239
|
+
options
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await options.beforeConfigBoundary?.();
|
|
244
|
+
let activeSelection: CollectionSelection | null = null;
|
|
245
|
+
let selectedCollection: Collection | undefined;
|
|
246
|
+
let activeConfig = loaded.value;
|
|
247
|
+
let boundaryError: FolderSetupError | null = null;
|
|
248
|
+
try {
|
|
249
|
+
const mutation = await applyConfigChange(
|
|
250
|
+
{
|
|
251
|
+
store: options.store,
|
|
252
|
+
configPath: options.configPath,
|
|
253
|
+
writeLockPath: configLockPath,
|
|
254
|
+
onConfigUpdated: (config) => {
|
|
255
|
+
activeConfig = config;
|
|
256
|
+
},
|
|
257
|
+
afterConfigSaved: async (config) => {
|
|
258
|
+
activeConfig = config;
|
|
259
|
+
const selected = activeSelection;
|
|
260
|
+
if (!selected) {
|
|
261
|
+
throw new Error("Setup collection selection was not established");
|
|
262
|
+
}
|
|
263
|
+
receipt.collection = {
|
|
264
|
+
name: selected.collection.name,
|
|
265
|
+
path: folder,
|
|
266
|
+
disposition: selected.disposition,
|
|
267
|
+
};
|
|
268
|
+
receipt.fingerprints.config = setupFingerprint({
|
|
269
|
+
version: config.version,
|
|
270
|
+
ftsTokenizer: config.ftsTokenizer,
|
|
271
|
+
collection: selected.collection,
|
|
272
|
+
});
|
|
273
|
+
passSetupStage(receipt, "config_saved", nowIso(options));
|
|
274
|
+
const configReceiptFailure = await writeReceipt(
|
|
275
|
+
receipt,
|
|
276
|
+
"config_saved",
|
|
277
|
+
options
|
|
278
|
+
);
|
|
279
|
+
if (configReceiptFailure) {
|
|
280
|
+
throw new SetupAbort(configReceiptFailure);
|
|
281
|
+
}
|
|
282
|
+
if (options.failureInjection === "after_config_save") {
|
|
283
|
+
throw new Error("INJECTED_AFTER_CONFIG_SAVE");
|
|
284
|
+
}
|
|
285
|
+
startSetupStage(receipt, "store_synced", nowIso(options));
|
|
286
|
+
const storeReceiptFailure = await writeReceipt(
|
|
287
|
+
receipt,
|
|
288
|
+
"store_synced",
|
|
289
|
+
options
|
|
290
|
+
);
|
|
291
|
+
if (storeReceiptFailure) {
|
|
292
|
+
throw new SetupAbort(storeReceiptFailure);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
async (config) => {
|
|
297
|
+
const fresh = await selectFolderCollection(
|
|
298
|
+
config,
|
|
299
|
+
folder,
|
|
300
|
+
options.name,
|
|
301
|
+
effectiveExcludes
|
|
302
|
+
);
|
|
303
|
+
if ("code" in fresh) {
|
|
304
|
+
boundaryError = fresh;
|
|
305
|
+
return { ok: false, error: fresh.message, code: fresh.code };
|
|
306
|
+
}
|
|
307
|
+
if (
|
|
308
|
+
fresh.disposition === "reused" &&
|
|
309
|
+
!setupExcludesMatch(fresh.collection.exclude, effectiveExcludes)
|
|
310
|
+
) {
|
|
311
|
+
boundaryError = setupFilterDisagreement(fresh.collection);
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
error: boundaryError.message,
|
|
315
|
+
code: boundaryError.code,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
activeSelection = fresh;
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
config: fresh.config,
|
|
322
|
+
value: fresh.collection,
|
|
323
|
+
skipSave: fresh.disposition === "reused",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
if (!mutation.ok) {
|
|
328
|
+
if (boundaryError) {
|
|
329
|
+
return persistFailure(receipt, "config_saved", boundaryError, options);
|
|
330
|
+
}
|
|
331
|
+
return persistFailure(
|
|
332
|
+
receipt,
|
|
333
|
+
receipt.stages.config_saved.status === "passed"
|
|
334
|
+
? "store_synced"
|
|
335
|
+
: "config_saved",
|
|
336
|
+
setupError(
|
|
337
|
+
mutation.code === "SYNC_ERROR"
|
|
338
|
+
? "store_sync_failed"
|
|
339
|
+
: "config_save_failed",
|
|
340
|
+
mutation.error,
|
|
341
|
+
"Fix config/data-directory permissions and rerun setup."
|
|
342
|
+
),
|
|
343
|
+
options
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
activeConfig = mutation.config;
|
|
347
|
+
selectedCollection = mutation.value;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
if (error instanceof SetupAbort) {
|
|
350
|
+
return error.result;
|
|
351
|
+
}
|
|
352
|
+
if (
|
|
353
|
+
error instanceof Error &&
|
|
354
|
+
error.message === "INJECTED_AFTER_CONFIG_SAVE"
|
|
355
|
+
) {
|
|
356
|
+
return persistFailure(
|
|
357
|
+
receipt,
|
|
358
|
+
"config_saved",
|
|
359
|
+
setupInjectedFailure("after_config_save"),
|
|
360
|
+
options
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return persistFailure(
|
|
364
|
+
receipt,
|
|
365
|
+
receipt.stages.config_saved.status === "passed"
|
|
366
|
+
? "store_synced"
|
|
367
|
+
: "config_saved",
|
|
368
|
+
setupError(
|
|
369
|
+
"config_save_failed",
|
|
370
|
+
error instanceof Error ? error.message : "Config mutation failed",
|
|
371
|
+
"Retry after the selected config write lock is available."
|
|
372
|
+
),
|
|
373
|
+
options
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const collection = selectedCollection;
|
|
378
|
+
if (!collection) {
|
|
379
|
+
return persistFailure(
|
|
380
|
+
receipt,
|
|
381
|
+
"store_synced",
|
|
382
|
+
setupError(
|
|
383
|
+
"store_sync_failed",
|
|
384
|
+
"Store projection completed without a selected collection",
|
|
385
|
+
"Retry setup against a healthy config and index store."
|
|
386
|
+
),
|
|
387
|
+
options
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
passSetupStage(receipt, "store_synced", nowIso(options));
|
|
391
|
+
writeFailure = await writeReceipt(receipt, "store_synced", options);
|
|
392
|
+
if (writeFailure) {
|
|
393
|
+
return writeFailure;
|
|
394
|
+
}
|
|
395
|
+
if (options.failureInjection === "after_store_sync") {
|
|
396
|
+
return persistFailure(
|
|
397
|
+
receipt,
|
|
398
|
+
"store_synced",
|
|
399
|
+
setupInjectedFailure("after_store_sync"),
|
|
400
|
+
options
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
startSetupStage(receipt, "lexical_indexed", nowIso(options));
|
|
405
|
+
writeFailure = await writeReceipt(receipt, "lexical_indexed", options);
|
|
406
|
+
if (writeFailure) {
|
|
407
|
+
return writeFailure;
|
|
408
|
+
}
|
|
409
|
+
const syncService = options.syncService ?? defaultSyncService;
|
|
410
|
+
const sync = await syncService.syncCollection(
|
|
411
|
+
collection,
|
|
412
|
+
options.store,
|
|
413
|
+
withContentTypeRules({ runUpdateCmd: false }, activeConfig)
|
|
414
|
+
);
|
|
415
|
+
const indexedCount =
|
|
416
|
+
sync.filesAdded + sync.filesUpdated + sync.filesUnchanged;
|
|
417
|
+
if (indexedCount === 0) {
|
|
418
|
+
return persistFailure(
|
|
419
|
+
receipt,
|
|
420
|
+
"lexical_indexed",
|
|
421
|
+
setupError(
|
|
422
|
+
"lexical_index_failed",
|
|
423
|
+
`No document reached the lexical index (${sync.filesErrored} errors, ${sync.filesSkipped} skipped)`,
|
|
424
|
+
"Inspect converter errors or add an indexable text document, then retry."
|
|
425
|
+
),
|
|
426
|
+
options
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
passSetupStage(receipt, "lexical_indexed", nowIso(options));
|
|
430
|
+
writeFailure = await writeReceipt(receipt, "lexical_indexed", options);
|
|
431
|
+
if (writeFailure) {
|
|
432
|
+
return writeFailure;
|
|
433
|
+
}
|
|
434
|
+
if (options.failureInjection === "after_lexical_index") {
|
|
435
|
+
return persistFailure(
|
|
436
|
+
receipt,
|
|
437
|
+
"lexical_indexed",
|
|
438
|
+
setupInjectedFailure("after_lexical_index"),
|
|
439
|
+
options
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
startSetupStage(receipt, "lexical_proved", nowIso(options));
|
|
444
|
+
writeFailure = await writeReceipt(receipt, "lexical_proved", options);
|
|
445
|
+
if (writeFailure) {
|
|
446
|
+
return writeFailure;
|
|
447
|
+
}
|
|
448
|
+
const proof = await verifyLexicalActivation(options.store, collection.name);
|
|
449
|
+
if (!proof.ok || !proof.value.ready) {
|
|
450
|
+
const message = proof.ok
|
|
451
|
+
? `Lexical activation was not proven (${proof.value.stages.lexical.code ?? proof.value.stages.index.code ?? "unknown"})`
|
|
452
|
+
: proof.error.message;
|
|
453
|
+
if (proof.ok) {
|
|
454
|
+
receipt.activation = proof.value;
|
|
455
|
+
receipt.fingerprints.index = proof.value.fingerprint;
|
|
456
|
+
}
|
|
457
|
+
return persistFailure(
|
|
458
|
+
receipt,
|
|
459
|
+
"lexical_proved",
|
|
460
|
+
setupError(
|
|
461
|
+
"lexical_proof_failed",
|
|
462
|
+
message,
|
|
463
|
+
"Fix the lexical corpus/index state, then rerun setup."
|
|
464
|
+
),
|
|
465
|
+
options
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
receipt.activation = proof.value;
|
|
469
|
+
receipt.fingerprints.index = proof.value.fingerprint;
|
|
470
|
+
passSetupStage(receipt, "lexical_proved", nowIso(options));
|
|
471
|
+
writeFailure = await writeReceipt(receipt, "lexical_proved", options);
|
|
472
|
+
if (writeFailure) {
|
|
473
|
+
return writeFailure;
|
|
474
|
+
}
|
|
475
|
+
if (options.failureInjection === "after_lexical_proof") {
|
|
476
|
+
return persistFailure(
|
|
477
|
+
receipt,
|
|
478
|
+
"lexical_proved",
|
|
479
|
+
setupInjectedFailure("after_lexical_proof"),
|
|
480
|
+
options
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
startSetupStage(receipt, "completed", nowIso(options));
|
|
485
|
+
passSetupStage(receipt, "completed", nowIso(options));
|
|
486
|
+
receipt.status = "completed";
|
|
487
|
+
receipt.pending = [];
|
|
488
|
+
writeFailure = await writeReceipt(receipt, "completed", options);
|
|
489
|
+
return writeFailure ?? { ok: true, receipt };
|
|
490
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared trust boundary for caller-supplied project affinity.
|
|
3
|
+
*
|
|
4
|
+
* Raw roots and hints stop here. Retrieval pipelines receive only the resolved,
|
|
5
|
+
* redaction-safe scoring input.
|
|
6
|
+
*
|
|
7
|
+
* @module src/core/project-affinity-surface
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Config, ProjectAffinityInput } from "../config/types";
|
|
11
|
+
import type { ProjectAffinityScoringInput } from "../pipeline/project-affinity";
|
|
12
|
+
|
|
13
|
+
import { resolveProjectAffinity } from "./project-affinity";
|
|
14
|
+
|
|
15
|
+
export const MAX_PROJECT_AFFINITY_INPUTS = 16;
|
|
16
|
+
|
|
17
|
+
export interface CliProjectAffinityRequest {
|
|
18
|
+
projectAffinityDisabled?: boolean;
|
|
19
|
+
projectRoots?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ProjectAffinityInputError extends Error {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "ProjectAffinityInputError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const compareCodeUnits = (left: string, right: string): number =>
|
|
30
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
31
|
+
|
|
32
|
+
export const normalizeProjectAffinityValues = (
|
|
33
|
+
values: readonly string[] | undefined,
|
|
34
|
+
label: "project hints" | "project roots"
|
|
35
|
+
): string[] => {
|
|
36
|
+
if (values === undefined) return [];
|
|
37
|
+
if (
|
|
38
|
+
!Array.isArray(values) ||
|
|
39
|
+
values.length > MAX_PROJECT_AFFINITY_INPUTS ||
|
|
40
|
+
values.some((value) => typeof value !== "string")
|
|
41
|
+
) {
|
|
42
|
+
throw new ProjectAffinityInputError(
|
|
43
|
+
`${label} must contain at most ${MAX_PROJECT_AFFINITY_INPUTS} strings`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const normalized = [
|
|
47
|
+
...new Set(values.map((value) => value.normalize("NFC").trim())),
|
|
48
|
+
].sort(compareCodeUnits);
|
|
49
|
+
if (normalized.some((value) => value.length === 0)) {
|
|
50
|
+
throw new ProjectAffinityInputError(
|
|
51
|
+
`${label} must not contain empty values`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const scoringInput = async (
|
|
58
|
+
input: ProjectAffinityInput,
|
|
59
|
+
config: Config,
|
|
60
|
+
channel: "local" | "remote"
|
|
61
|
+
): Promise<ProjectAffinityScoringInput> => ({
|
|
62
|
+
enabled: config.projectAffinity?.enabled,
|
|
63
|
+
contribution: config.projectAffinity?.contribution,
|
|
64
|
+
resolution: await resolveProjectAffinity(input, config.collections, {
|
|
65
|
+
channel,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const resolveCliProjectAffinity = async (
|
|
70
|
+
config: Config,
|
|
71
|
+
options: {
|
|
72
|
+
cwd: string;
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
projectRoots?: readonly string[];
|
|
75
|
+
}
|
|
76
|
+
): Promise<ProjectAffinityScoringInput | undefined> => {
|
|
77
|
+
const projectRoots = normalizeProjectAffinityValues(
|
|
78
|
+
options.projectRoots,
|
|
79
|
+
"project roots"
|
|
80
|
+
);
|
|
81
|
+
if (options.disabled && projectRoots.length > 0) {
|
|
82
|
+
throw new ProjectAffinityInputError(
|
|
83
|
+
"--no-project-affinity cannot be combined with --project-root"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (options.disabled || config.projectAffinity?.enabled === false) return;
|
|
87
|
+
|
|
88
|
+
const roots =
|
|
89
|
+
projectRoots.length > 0
|
|
90
|
+
? projectRoots.map((path) => ({
|
|
91
|
+
path,
|
|
92
|
+
source: "cli_explicit" as const,
|
|
93
|
+
}))
|
|
94
|
+
: [{ path: options.cwd, source: "cli_cwd" as const }];
|
|
95
|
+
return scoringInput({ roots }, config, "local");
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const resolveRemoteProjectAffinity = async (
|
|
99
|
+
config: Config,
|
|
100
|
+
projectHints: readonly string[] | undefined
|
|
101
|
+
): Promise<ProjectAffinityScoringInput | undefined> => {
|
|
102
|
+
const hints = normalizeProjectAffinityValues(projectHints, "project hints");
|
|
103
|
+
if (hints.length === 0 || config.projectAffinity?.enabled === false) return;
|
|
104
|
+
return scoringInput(
|
|
105
|
+
{
|
|
106
|
+
roots: hints.map((hint) => ({
|
|
107
|
+
hint,
|
|
108
|
+
source: "remote_hint" as const,
|
|
109
|
+
})),
|
|
110
|
+
},
|
|
111
|
+
config,
|
|
112
|
+
"remote"
|
|
113
|
+
);
|
|
114
|
+
};
|