@gmickel/gno 2.12.2 → 2.13.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 +1 -1
- package/assets/skill/SKILL.md +1 -1
- package/assets/skill/cli-reference.md +23 -10
- package/assets/skill/mcp-reference.md +2 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.12.2.zip → gno-browser-clipper-v2.13.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.13.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +66 -5
- package/spec/mcp.md +96 -1
- package/spec/output-schemas/convert.schema.json +97 -0
- package/spec/output-schemas/mcp-job-status.schema.json +17 -1
- package/spec/output-schemas/mcp-sync-result.schema.json +9 -2
- package/spec/output-schemas/search-results.schema.json +7 -0
- package/src/cli/commands/index-cmd.ts +50 -12
- package/src/cli/commands/query.ts +21 -8
- package/src/cli/commands/search.ts +11 -0
- package/src/cli/commands/vsearch.ts +17 -4
- package/src/cli/options.ts +19 -0
- package/src/cli/program.ts +79 -10
- package/src/core/collection-scope.ts +105 -0
- package/src/core/errors.ts +5 -0
- package/src/core/job-manager.ts +238 -19
- package/src/core/reindex-paths.ts +167 -0
- package/src/core/retrieval-trace-request.ts +1 -0
- package/src/ingestion/convert-file.ts +181 -0
- package/src/llm/registry.ts +20 -0
- package/src/mcp/http-egress.ts +21 -2
- package/src/mcp/tools/collection-scope.ts +25 -0
- package/src/mcp/tools/index-cmd.ts +66 -48
- package/src/mcp/tools/index.ts +74 -10
- package/src/mcp/tools/job-status.ts +7 -0
- package/src/mcp/tools/query.ts +28 -7
- package/src/mcp/tools/search.ts +4 -0
- package/src/mcp/tools/sync.ts +68 -60
- package/src/mcp/tools/vsearch.ts +9 -3
- package/src/pipeline/filters.ts +18 -3
- package/src/pipeline/graph-retrieval.ts +1 -0
- package/src/pipeline/hybrid.ts +15 -2
- package/src/pipeline/hydration.ts +7 -2
- package/src/pipeline/owner-fusion.ts +1 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +7 -0
- package/src/pipeline/vsearch.ts +10 -5
- package/src/sdk/client.ts +39 -11
- package/src/sdk/convert.ts +27 -0
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +1 -1
- package/src/serve/jobs.ts +4 -1
- package/src/serve/public/components/IndexingProgress.tsx +25 -3
- package/src/serve/routes/api.ts +164 -24
- package/src/store/sqlite/adapter.ts +97 -27
- package/src/store/sqlite/eligibility.ts +4 -0
- package/src/store/types.ts +7 -0
- package/src/store/vector/sqlite-vec.ts +67 -15
- package/src/store/vector/variant-search.ts +41 -4
- package/browser-extension/artifacts/gno-browser-clipper-v2.12.2.zip.sha256 +0 -1
package/src/sdk/client.ts
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
import { publishCapture } from "../core/capture-publish";
|
|
111
111
|
import { projectCollectionEgressPolicy } from "../core/collection-egress-policy-projection";
|
|
112
112
|
import { CollectionEgressPolicyService } from "../core/collection-egress-policy-service";
|
|
113
|
+
import { resolveCollectionScope } from "../core/collection-scope";
|
|
113
114
|
import { applyConfigChange } from "../core/config-mutation";
|
|
114
115
|
import { getDocumentCapabilities } from "../core/document-capabilities";
|
|
115
116
|
import { EgressAuditService } from "../core/egress-audit";
|
|
@@ -196,7 +197,7 @@ import {
|
|
|
196
197
|
} from "../llm/inference-scope";
|
|
197
198
|
import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
|
|
198
199
|
import { resolveDownloadPolicy } from "../llm/policy";
|
|
199
|
-
import { resolveModelUri } from "../llm/registry";
|
|
200
|
+
import { modelCollectionForList, resolveModelUri } from "../llm/registry";
|
|
200
201
|
import {
|
|
201
202
|
answerTraceTerminalStatus,
|
|
202
203
|
generateGroundedAnswer,
|
|
@@ -476,6 +477,27 @@ class GnoClientImpl implements GnoClient {
|
|
|
476
477
|
}
|
|
477
478
|
}
|
|
478
479
|
|
|
480
|
+
/** Normalize and validate a search/vsearch/query `collections` allowlist. */
|
|
481
|
+
private withCollectionScope<
|
|
482
|
+
T extends { collection?: string; collections?: string[] },
|
|
483
|
+
>(options: T): T {
|
|
484
|
+
const scope = resolveCollectionScope(options, this.config.collections);
|
|
485
|
+
if (!scope.ok) throw sdkError("VALIDATION", scope.message);
|
|
486
|
+
return scope.collections === undefined
|
|
487
|
+
? options
|
|
488
|
+
: { ...options, collections: scope.collections };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private modelCollection(
|
|
492
|
+
type: "embed" | "expand" | "rerank",
|
|
493
|
+
options: { collection?: string; collections?: string[] }
|
|
494
|
+
): string | undefined {
|
|
495
|
+
return (
|
|
496
|
+
options.collection ??
|
|
497
|
+
modelCollectionForList(this.config, type, options.collections)
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
479
501
|
private getCollections(collection?: string) {
|
|
480
502
|
if (!collection) {
|
|
481
503
|
return this.config.collections;
|
|
@@ -541,6 +563,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
541
563
|
answer?: boolean;
|
|
542
564
|
rerank?: boolean;
|
|
543
565
|
collection?: string;
|
|
566
|
+
collections?: string[];
|
|
544
567
|
requiredEmbed?: boolean;
|
|
545
568
|
requiredExpand?: boolean;
|
|
546
569
|
requiredAnswer?: boolean;
|
|
@@ -551,9 +574,9 @@ class GnoClientImpl implements GnoClient {
|
|
|
551
574
|
rerankModel?: string;
|
|
552
575
|
}): Promise<RuntimePorts> {
|
|
553
576
|
this.assertOpen();
|
|
554
|
-
const egressCollections =
|
|
555
|
-
|
|
556
|
-
: ("all" as const);
|
|
577
|
+
const egressCollections =
|
|
578
|
+
options.collections ??
|
|
579
|
+
(options.collection ? [options.collection] : ("all" as const));
|
|
557
580
|
|
|
558
581
|
let embedPort: EmbeddingPort | null = null;
|
|
559
582
|
let expandPort: GenerationPort | null = null;
|
|
@@ -567,7 +590,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
567
590
|
this.config,
|
|
568
591
|
"embed",
|
|
569
592
|
options.embedModel,
|
|
570
|
-
options
|
|
593
|
+
this.modelCollection("embed", options)
|
|
571
594
|
),
|
|
572
595
|
{
|
|
573
596
|
egressCollections,
|
|
@@ -612,7 +635,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
612
635
|
this.config,
|
|
613
636
|
"expand",
|
|
614
637
|
options.expandModel ?? options.genModel,
|
|
615
|
-
options
|
|
638
|
+
this.modelCollection("expand", options)
|
|
616
639
|
),
|
|
617
640
|
{
|
|
618
641
|
egressCollections,
|
|
@@ -665,7 +688,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
665
688
|
this.config,
|
|
666
689
|
"rerank",
|
|
667
690
|
options.rerankModel,
|
|
668
|
-
options
|
|
691
|
+
this.modelCollection("rerank", options)
|
|
669
692
|
),
|
|
670
693
|
{
|
|
671
694
|
egressCollections,
|
|
@@ -735,6 +758,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
735
758
|
options: GnoSearchOptions
|
|
736
759
|
): Promise<SearchResults> {
|
|
737
760
|
this.assertOpen();
|
|
761
|
+
options = this.withCollectionScope(options);
|
|
738
762
|
let traceSession: RetrievalTraceSession | null = null;
|
|
739
763
|
try {
|
|
740
764
|
const { projectHints, ...searchOptions } = options;
|
|
@@ -789,6 +813,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
789
813
|
options: GnoVectorSearchOptions
|
|
790
814
|
): Promise<SearchResults> {
|
|
791
815
|
this.assertOpen();
|
|
816
|
+
options = this.withCollectionScope(options);
|
|
792
817
|
|
|
793
818
|
let ports: RuntimePorts | null = null;
|
|
794
819
|
let traceSession: RetrievalTraceSession | null = null;
|
|
@@ -805,7 +830,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
805
830
|
this.config,
|
|
806
831
|
"embed",
|
|
807
832
|
options.model,
|
|
808
|
-
options
|
|
833
|
+
this.modelCollection("embed", options)
|
|
809
834
|
);
|
|
810
835
|
traceSession = unwrapStore(
|
|
811
836
|
await startRetrievalTraceRequest({
|
|
@@ -823,6 +848,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
823
848
|
requiredEmbed: true,
|
|
824
849
|
embedModel: options.model,
|
|
825
850
|
collection: options.collection,
|
|
851
|
+
collections: options.collections,
|
|
826
852
|
});
|
|
827
853
|
if (!ports.embedPort || !ports.vectorIndex) {
|
|
828
854
|
throw sdkError(
|
|
@@ -882,6 +908,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
882
908
|
options: GnoQueryOptions
|
|
883
909
|
): Promise<SearchResults> {
|
|
884
910
|
this.assertOpen();
|
|
911
|
+
options = this.withCollectionScope(options);
|
|
885
912
|
|
|
886
913
|
const normalizedInput = normalizeStructuredQueryInput(
|
|
887
914
|
query,
|
|
@@ -905,14 +932,14 @@ class GnoClientImpl implements GnoClient {
|
|
|
905
932
|
this.config,
|
|
906
933
|
"embed",
|
|
907
934
|
options.embedModel,
|
|
908
|
-
options
|
|
935
|
+
this.modelCollection("embed", options)
|
|
909
936
|
);
|
|
910
937
|
const expandUri = expandRequested
|
|
911
938
|
? resolveModelUri(
|
|
912
939
|
this.config,
|
|
913
940
|
"expand",
|
|
914
941
|
options.expandModel ?? options.genModel,
|
|
915
|
-
options
|
|
942
|
+
this.modelCollection("expand", options)
|
|
916
943
|
)
|
|
917
944
|
: undefined;
|
|
918
945
|
const rerankUri = rerankRequested
|
|
@@ -920,7 +947,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
920
947
|
this.config,
|
|
921
948
|
"rerank",
|
|
922
949
|
options.rerankModel,
|
|
923
|
-
options
|
|
950
|
+
this.modelCollection("rerank", options)
|
|
924
951
|
)
|
|
925
952
|
: undefined;
|
|
926
953
|
let ports: RuntimePorts | null = null;
|
|
@@ -956,6 +983,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
956
983
|
genModel: options.genModel,
|
|
957
984
|
rerankModel: options.rerankModel,
|
|
958
985
|
collection: options.collection,
|
|
986
|
+
collections: options.collections,
|
|
959
987
|
});
|
|
960
988
|
return attachRetrievalTraceMetadata(
|
|
961
989
|
this.decorateSearchResults(
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK conversion of one file to the Markdown GNO would index (no client,
|
|
3
|
+
* config or index needed).
|
|
4
|
+
*
|
|
5
|
+
* @module src/sdk/convert
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ConvertFileResult,
|
|
10
|
+
convertFile as convertOneFile,
|
|
11
|
+
} from "../ingestion/convert-file";
|
|
12
|
+
import { sdkError } from "./errors";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert one file to the Markdown GNO would index for it; nothing is
|
|
16
|
+
* persisted. Failures throw `GnoSdkError` with `details.reason` set to the
|
|
17
|
+
* same reason `gno convert` reports.
|
|
18
|
+
*/
|
|
19
|
+
export async function convertFile(path: string): Promise<ConvertFileResult> {
|
|
20
|
+
const result = await convertOneFile(path);
|
|
21
|
+
if (result.ok) return result.value;
|
|
22
|
+
const { reason, message, validation, details } = result.error;
|
|
23
|
+
let code: "NOT_FOUND" | "VALIDATION" | "RUNTIME" = "RUNTIME";
|
|
24
|
+
if (reason === "NOT_FOUND") code = "NOT_FOUND";
|
|
25
|
+
else if (validation) code = "VALIDATION";
|
|
26
|
+
throw sdkError(code, message, { details: { reason, ...details } });
|
|
27
|
+
}
|
package/src/sdk/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type { IndexStatus } from "../store/types";
|
|
|
27
27
|
|
|
28
28
|
export { GnoSdkError, sdkError } from "./errors";
|
|
29
29
|
export { createGnoClient } from "./client";
|
|
30
|
+
export { convertFile } from "./convert";
|
|
31
|
+
export type { ConvertFileResult } from "../ingestion/convert-file";
|
|
30
32
|
export type {
|
|
31
33
|
DocumentSection,
|
|
32
34
|
GnoAskOptions,
|
package/src/sdk/types.ts
CHANGED
|
@@ -179,7 +179,7 @@ export type GnoQueryOptions = Omit<
|
|
|
179
179
|
GnoProjectHintOptions;
|
|
180
180
|
export type GnoAskOptions = Omit<
|
|
181
181
|
AskOptions,
|
|
182
|
-
"contentTypeRules" | "projectAffinity"
|
|
182
|
+
"collections" | "contentTypeRules" | "projectAffinity"
|
|
183
183
|
> &
|
|
184
184
|
GnoModelOverrides &
|
|
185
185
|
GnoProjectHintOptions;
|
package/src/serve/jobs.ts
CHANGED
|
@@ -16,11 +16,13 @@ export interface JobProgress {
|
|
|
16
16
|
export interface JobStatus {
|
|
17
17
|
id: string;
|
|
18
18
|
type: JobType;
|
|
19
|
-
status: "running" | "completed" | "failed";
|
|
19
|
+
status: "queued" | "running" | "completed" | "failed";
|
|
20
20
|
createdAt: number;
|
|
21
21
|
progress?: JobProgress;
|
|
22
22
|
result?: SyncResult;
|
|
23
23
|
error?: string;
|
|
24
|
+
/** Reindex jobs: the collection (null = all) and merged paths (null = whole). */
|
|
25
|
+
scope?: JobRecord["scope"];
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface StartJobSuccess {
|
|
@@ -49,6 +51,7 @@ const fromResidentRecord = (record: JobRecord): JobStatus => ({
|
|
|
49
51
|
progress: record.progress,
|
|
50
52
|
result: record.result,
|
|
51
53
|
error: record.error,
|
|
54
|
+
...(record.scope ? { scope: record.scope } : {}),
|
|
52
55
|
});
|
|
53
56
|
|
|
54
57
|
function cleanupExpiredJobs(now = Date.now()): void {
|
|
@@ -24,7 +24,7 @@ import { Badge } from "./ui/badge";
|
|
|
24
24
|
export interface JobStatus {
|
|
25
25
|
id: string;
|
|
26
26
|
type: "add" | "sync";
|
|
27
|
-
status: "running" | "completed" | "failed";
|
|
27
|
+
status: "queued" | "running" | "completed" | "failed";
|
|
28
28
|
createdAt: number;
|
|
29
29
|
result?: SyncResult;
|
|
30
30
|
error?: string;
|
|
@@ -138,7 +138,7 @@ export function IndexingProgress({
|
|
|
138
138
|
setStatus(data);
|
|
139
139
|
setFetchError(null);
|
|
140
140
|
|
|
141
|
-
if (data.status === "running") {
|
|
141
|
+
if (data.status === "queued" || data.status === "running") {
|
|
142
142
|
pollTimeout = setTimeout(() => {
|
|
143
143
|
void poll();
|
|
144
144
|
}, 1000);
|
|
@@ -187,6 +187,20 @@ export function IndexingProgress({
|
|
|
187
187
|
|
|
188
188
|
// Compact mode
|
|
189
189
|
if (compact) {
|
|
190
|
+
if (status?.status === "queued") {
|
|
191
|
+
return (
|
|
192
|
+
<div
|
|
193
|
+
aria-live="polite"
|
|
194
|
+
className={cn("flex items-center gap-2 text-sm", className)}
|
|
195
|
+
>
|
|
196
|
+
<Loader2Icon className="size-4 animate-spin text-muted-foreground" />
|
|
197
|
+
<span className="text-muted-foreground">
|
|
198
|
+
Queued... {formatElapsed(elapsed)}
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
if (status?.status === "running") {
|
|
191
205
|
return (
|
|
192
206
|
<div
|
|
@@ -237,6 +251,14 @@ export function IndexingProgress({
|
|
|
237
251
|
{/* Status header */}
|
|
238
252
|
<div className="flex items-center justify-between">
|
|
239
253
|
<div className="flex items-center gap-2">
|
|
254
|
+
{status?.status === "queued" && (
|
|
255
|
+
<>
|
|
256
|
+
<Loader2Icon className="size-5 animate-spin text-muted-foreground" />
|
|
257
|
+
<span className="font-medium">
|
|
258
|
+
Queued behind another indexing job
|
|
259
|
+
</span>
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
240
262
|
{status?.status === "running" && (
|
|
241
263
|
<>
|
|
242
264
|
<Loader2Icon className="size-5 animate-spin text-primary" />
|
|
@@ -264,7 +286,7 @@ export function IndexingProgress({
|
|
|
264
286
|
<div className="flex items-center gap-1.5 text-muted-foreground text-sm">
|
|
265
287
|
<TimerIcon className="size-4" />
|
|
266
288
|
<span>
|
|
267
|
-
{status?.status === "running"
|
|
289
|
+
{status?.status === "queued" || status?.status === "running"
|
|
268
290
|
? formatElapsed(elapsed)
|
|
269
291
|
: formatDuration(status?.result?.totalDurationMs ?? elapsed)}
|
|
270
292
|
</span>
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -16,7 +16,6 @@ import type {
|
|
|
16
16
|
EgressPolicy,
|
|
17
17
|
ModelPreset,
|
|
18
18
|
} from "../../config/types";
|
|
19
|
-
import type { JobManager } from "../../core/job-manager";
|
|
20
19
|
import type { ProjectAffinityScoringInput } from "../../pipeline/project-affinity";
|
|
21
20
|
import type {
|
|
22
21
|
AskResult,
|
|
@@ -57,6 +56,7 @@ import {
|
|
|
57
56
|
type EgressRelaxationConfirmation,
|
|
58
57
|
} from "../../core/collection-egress-policy-service";
|
|
59
58
|
import { parsePolicySetBody } from "../../core/collection-egress-policy-validation";
|
|
59
|
+
import { resolveCollectionScope } from "../../core/collection-scope";
|
|
60
60
|
import {
|
|
61
61
|
type ConnectorVerificationCode,
|
|
62
62
|
getConnectorVerificationRemediation,
|
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
planCreateFolder,
|
|
83
83
|
planDuplicateRefactor,
|
|
84
84
|
} from "../../core/file-refactors";
|
|
85
|
+
import { JobError, type JobManager } from "../../core/job-manager";
|
|
85
86
|
import {
|
|
86
87
|
MemoryError,
|
|
87
88
|
type MemoryErrorCode,
|
|
@@ -111,6 +112,7 @@ import {
|
|
|
111
112
|
resolveRemoteProjectAffinity,
|
|
112
113
|
} from "../../core/project-affinity-surface";
|
|
113
114
|
import { projectRecordEvidenceMetadata } from "../../core/record-metadata";
|
|
115
|
+
import { resolveReindexScope } from "../../core/reindex-paths";
|
|
114
116
|
import {
|
|
115
117
|
isLeaseBusy,
|
|
116
118
|
localRequestLedger,
|
|
@@ -303,6 +305,8 @@ export interface SearchRequestBody {
|
|
|
303
305
|
limit?: number;
|
|
304
306
|
minScore?: number;
|
|
305
307
|
collection?: string;
|
|
308
|
+
/** Collection allowlist (1-256 names); not combined with `collection`. */
|
|
309
|
+
collections?: string[];
|
|
306
310
|
intent?: string;
|
|
307
311
|
exclude?: string;
|
|
308
312
|
since?: string;
|
|
@@ -323,6 +327,8 @@ export interface QueryRequestBody {
|
|
|
323
327
|
limit?: number;
|
|
324
328
|
minScore?: number;
|
|
325
329
|
collection?: string;
|
|
330
|
+
/** Collection allowlist (1-256 names); not combined with `collection`. */
|
|
331
|
+
collections?: string[];
|
|
326
332
|
lang?: string;
|
|
327
333
|
intent?: string;
|
|
328
334
|
candidateLimit?: number;
|
|
@@ -496,6 +502,7 @@ export interface ImportPreviewRequestBody {
|
|
|
496
502
|
export interface SyncRequestBody {
|
|
497
503
|
collection?: string;
|
|
498
504
|
gitPull?: boolean;
|
|
505
|
+
paths?: string[];
|
|
499
506
|
}
|
|
500
507
|
|
|
501
508
|
export interface CreateDocRequestBody {
|
|
@@ -651,6 +658,42 @@ function parseRestMetadataFilter(
|
|
|
651
658
|
return { ok: true, filter: normalizeMetadataPredicate(parsed.data) };
|
|
652
659
|
}
|
|
653
660
|
|
|
661
|
+
/**
|
|
662
|
+
* Resolve a search/query `collections` allowlist: 400 VALIDATION for malformed
|
|
663
|
+
* input or `collection` with `collections`, 404 NOT_FOUND for unknown names.
|
|
664
|
+
*/
|
|
665
|
+
async function parseRestCollectionScope(
|
|
666
|
+
body: { collection?: unknown; collections?: unknown },
|
|
667
|
+
configured: readonly { name: string }[] | null,
|
|
668
|
+
store: SqliteAdapter
|
|
669
|
+
): Promise<
|
|
670
|
+
| { ok: true; collections: string[] | undefined }
|
|
671
|
+
| { ok: false; response: Response }
|
|
672
|
+
> {
|
|
673
|
+
if (body.collections === undefined)
|
|
674
|
+
return { ok: true, collections: undefined };
|
|
675
|
+
let names = configured;
|
|
676
|
+
if (!names) {
|
|
677
|
+
const stored = await store.getCollections();
|
|
678
|
+
if (!stored.ok) {
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
response: errorResponse("RUNTIME", stored.error.message, 500),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
names = stored.value;
|
|
685
|
+
}
|
|
686
|
+
const scope = resolveCollectionScope(body, names);
|
|
687
|
+
if (scope.ok) return { ok: true, collections: scope.collections };
|
|
688
|
+
return {
|
|
689
|
+
ok: false,
|
|
690
|
+
response:
|
|
691
|
+
scope.kind === "validation"
|
|
692
|
+
? errorResponse("VALIDATION", scope.message)
|
|
693
|
+
: errorResponse("NOT_FOUND", scope.message, 404),
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
654
697
|
function fileRefactorHttpErrorResponse(error: FileRefactorHttpError): Response {
|
|
655
698
|
return errorResponse(error.code, error.message, error.status, error.details);
|
|
656
699
|
}
|
|
@@ -1780,6 +1823,9 @@ export async function handleSync(
|
|
|
1780
1823
|
if (body.gitPull !== undefined && typeof body.gitPull !== "boolean") {
|
|
1781
1824
|
return errorResponse("VALIDATION", "gitPull must be a boolean");
|
|
1782
1825
|
}
|
|
1826
|
+
if (body.paths !== undefined && !Array.isArray(body.paths)) {
|
|
1827
|
+
return errorResponse("VALIDATION", "paths must be an array of strings");
|
|
1828
|
+
}
|
|
1783
1829
|
|
|
1784
1830
|
// Get collections to sync (case-insensitive matching)
|
|
1785
1831
|
const collectionName = body.collection?.toLowerCase();
|
|
@@ -1801,31 +1847,95 @@ export async function handleSync(
|
|
|
1801
1847
|
return errorResponse("VALIDATION", "No collections to sync");
|
|
1802
1848
|
}
|
|
1803
1849
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
const result = await defaultSyncService.syncAll(
|
|
1809
|
-
collections,
|
|
1810
|
-
store,
|
|
1811
|
-
withContentTypeRules(
|
|
1812
|
-
{
|
|
1813
|
-
gitPull: body.gitPull,
|
|
1814
|
-
runUpdateCmd: true,
|
|
1815
|
-
},
|
|
1816
|
-
ctxHolder.config
|
|
1817
|
-
)
|
|
1818
|
-
);
|
|
1819
|
-
recordContentMutation(result, ctxHolder.markContentMutation);
|
|
1820
|
-
if (hasContentMutation(result)) {
|
|
1821
|
-
if (ctxHolder.scheduler) {
|
|
1822
|
-
await ctxHolder.scheduler.triggerNow();
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
return result;
|
|
1850
|
+
const options = withContentTypeRules(
|
|
1851
|
+
{
|
|
1852
|
+
gitPull: body.gitPull,
|
|
1853
|
+
runUpdateCmd: true,
|
|
1826
1854
|
},
|
|
1827
|
-
ctxHolder.
|
|
1855
|
+
ctxHolder.config
|
|
1828
1856
|
);
|
|
1857
|
+
let paths: string[] | null;
|
|
1858
|
+
try {
|
|
1859
|
+
// A path-level sync runs no update command; only git pull conflicts.
|
|
1860
|
+
paths = await resolveReindexScope(
|
|
1861
|
+
body.collection ? (collections[0] ?? null) : null,
|
|
1862
|
+
body.paths,
|
|
1863
|
+
{ gitPull: body.gitPull }
|
|
1864
|
+
);
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1867
|
+
const separator = message.indexOf(": ");
|
|
1868
|
+
const code = separator > 0 ? message.slice(0, separator) : "";
|
|
1869
|
+
return errorResponse(
|
|
1870
|
+
code === "INVALID_PATH" ? "INVALID_PATH" : "VALIDATION",
|
|
1871
|
+
separator > 0 ? message.slice(separator + 2) : message
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// Captured now: a queued pass runs after this request's context is gone.
|
|
1876
|
+
const config = ctxHolder.config;
|
|
1877
|
+
const runSync = async (
|
|
1878
|
+
scopedPaths: string[] | null,
|
|
1879
|
+
passOptions: { gitPull?: boolean; runUpdateCmd?: boolean }
|
|
1880
|
+
): Promise<SyncResult> => {
|
|
1881
|
+
const collection = collections[0];
|
|
1882
|
+
const result =
|
|
1883
|
+
scopedPaths && collection
|
|
1884
|
+
? wrapCollectionSync(
|
|
1885
|
+
await defaultSyncService.syncPaths(
|
|
1886
|
+
collection,
|
|
1887
|
+
store,
|
|
1888
|
+
scopedPaths,
|
|
1889
|
+
withContentTypeRules({}, config)
|
|
1890
|
+
)
|
|
1891
|
+
)
|
|
1892
|
+
: await defaultSyncService.syncAll(
|
|
1893
|
+
collections,
|
|
1894
|
+
store,
|
|
1895
|
+
withContentTypeRules({ ...passOptions }, config)
|
|
1896
|
+
);
|
|
1897
|
+
recordContentMutation(result, ctxHolder.markContentMutation);
|
|
1898
|
+
if (hasContentMutation(result)) {
|
|
1899
|
+
if (ctxHolder.scheduler) {
|
|
1900
|
+
await ctxHolder.scheduler.triggerNow();
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return result;
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
const manager = ctxHolder.jobManager;
|
|
1907
|
+
if (manager) {
|
|
1908
|
+
try {
|
|
1909
|
+
const accepted = await manager.submitReindex({
|
|
1910
|
+
type: "sync",
|
|
1911
|
+
collection: body.collection ? (collections[0]?.name ?? null) : null,
|
|
1912
|
+
paths,
|
|
1913
|
+
// A whole sync over REST always runs the collection's update command.
|
|
1914
|
+
options: { gitPull: body.gitPull ?? false, runUpdateCmd: true },
|
|
1915
|
+
run: async (scope) => ({
|
|
1916
|
+
kind: "sync",
|
|
1917
|
+
value: await runSync(scope.paths, scope.options),
|
|
1918
|
+
}),
|
|
1919
|
+
});
|
|
1920
|
+
return jsonResponse(
|
|
1921
|
+
{ jobId: accepted.jobId, status: accepted.status },
|
|
1922
|
+
202
|
|
1923
|
+
);
|
|
1924
|
+
} catch (error) {
|
|
1925
|
+
if (error instanceof JobError && error.code === "QUEUE_FULL") {
|
|
1926
|
+
return errorResponse("QUEUE_FULL", error.message, 429);
|
|
1927
|
+
}
|
|
1928
|
+
if (error instanceof JobError) {
|
|
1929
|
+
return errorResponse("CONFLICT", error.message, 409, {
|
|
1930
|
+
activeJobId: manager.getActiveJob()?.id ?? "",
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
throw error;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Standalone fallback (unit fixtures without a resident job manager).
|
|
1938
|
+
const jobResult = startJob("sync", () => runSync(paths, options));
|
|
1829
1939
|
|
|
1830
1940
|
if (!jobResult.ok) {
|
|
1831
1941
|
return jobConflictResponse(jobResult);
|
|
@@ -1834,6 +1944,20 @@ export async function handleSync(
|
|
|
1834
1944
|
return jsonResponse({ jobId: jobResult.jobId }, 202);
|
|
1835
1945
|
}
|
|
1836
1946
|
|
|
1947
|
+
function wrapCollectionSync(
|
|
1948
|
+
result: Awaited<ReturnType<typeof defaultSyncService.syncPaths>>
|
|
1949
|
+
): SyncResult {
|
|
1950
|
+
return {
|
|
1951
|
+
collections: [result],
|
|
1952
|
+
totalDurationMs: result.durationMs,
|
|
1953
|
+
totalFilesProcessed: result.filesProcessed,
|
|
1954
|
+
totalFilesAdded: result.filesAdded,
|
|
1955
|
+
totalFilesUpdated: result.filesUpdated,
|
|
1956
|
+
totalFilesErrored: result.filesErrored,
|
|
1957
|
+
totalFilesSkipped: result.filesSkipped,
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1837
1961
|
/**
|
|
1838
1962
|
* GET /api/docs
|
|
1839
1963
|
* Query params: collection, limit (default 20), offset (default 0), tagsAll, tagsAny
|
|
@@ -4587,6 +4711,13 @@ export async function handleSearch(
|
|
|
4587
4711
|
const parsedFilter = parseRestMetadataFilter(body.filter);
|
|
4588
4712
|
if (!parsedFilter.ok) return parsedFilter.response;
|
|
4589
4713
|
const filter = parsedFilter.filter;
|
|
4714
|
+
const collectionScope = await parseRestCollectionScope(
|
|
4715
|
+
body,
|
|
4716
|
+
context?.config.collections ?? null,
|
|
4717
|
+
store
|
|
4718
|
+
);
|
|
4719
|
+
if (!collectionScope.ok) return collectionScope.response;
|
|
4720
|
+
const { collections } = collectionScope;
|
|
4590
4721
|
let projectAffinity: ProjectAffinityScoringInput | undefined;
|
|
4591
4722
|
try {
|
|
4592
4723
|
projectAffinity = context
|
|
@@ -4606,6 +4737,7 @@ export async function handleSearch(
|
|
|
4606
4737
|
limit: Math.min(body.limit || 10, 50),
|
|
4607
4738
|
minScore: body.minScore,
|
|
4608
4739
|
collection: body.collection,
|
|
4740
|
+
collections,
|
|
4609
4741
|
intent: body.intent?.trim() || undefined,
|
|
4610
4742
|
exclude,
|
|
4611
4743
|
tagsAll,
|
|
@@ -4785,6 +4917,13 @@ export async function handleQuery(
|
|
|
4785
4917
|
const parsedFilter = parseRestMetadataFilter(body.filter);
|
|
4786
4918
|
if (!parsedFilter.ok) return parsedFilter.response;
|
|
4787
4919
|
const filter = parsedFilter.filter;
|
|
4920
|
+
const collectionScope = await parseRestCollectionScope(
|
|
4921
|
+
body,
|
|
4922
|
+
ctx.config.collections,
|
|
4923
|
+
ctx.store
|
|
4924
|
+
);
|
|
4925
|
+
if (!collectionScope.ok) return collectionScope.response;
|
|
4926
|
+
const { collections } = collectionScope;
|
|
4788
4927
|
let projectAffinity: ProjectAffinityScoringInput | undefined;
|
|
4789
4928
|
try {
|
|
4790
4929
|
projectAffinity = await resolveRemoteProjectAffinity(
|
|
@@ -4804,6 +4943,7 @@ export async function handleQuery(
|
|
|
4804
4943
|
limit: Math.min(body.limit ?? 20, 50),
|
|
4805
4944
|
minScore: body.minScore,
|
|
4806
4945
|
collection: body.collection,
|
|
4946
|
+
collections,
|
|
4807
4947
|
lang: body.lang,
|
|
4808
4948
|
intent: body.intent?.trim() || undefined,
|
|
4809
4949
|
candidateLimit:
|