@gmickel/gno 1.16.0 → 1.17.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 +20 -16
- package/assets/skill/SKILL.md +8 -5
- package/package.json +1 -1
- package/src/app/context-agent-projection.ts +303 -0
- package/src/app/context-format.ts +249 -0
- package/src/app/context-runtime-contract.ts +325 -0
- package/src/app/context-runtime-input.ts +362 -0
- package/src/app/context-runtime-types.ts +65 -0
- package/src/app/context-runtime.ts +170 -0
- package/src/app/context-surface.ts +145 -0
- package/src/cli/commands/context-build.ts +149 -0
- package/src/cli/commands/context-verify.ts +90 -0
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +178 -0
- package/src/core/context-budget.ts +461 -0
- package/src/core/context-capsule-index-schema.ts +15 -0
- package/src/core/context-capsule-retrieval-schema.ts +81 -0
- package/src/core/context-capsule-schema.ts +473 -0
- package/src/core/context-capsule-validation.ts +416 -0
- package/src/core/context-capsule-verification.ts +218 -0
- package/src/core/context-capsule.ts +439 -0
- package/src/core/context-compiler.ts +513 -0
- package/src/core/context-evidence-metadata.ts +33 -0
- package/src/core/context-evidence.ts +495 -0
- package/src/core/context-facets.ts +163 -0
- package/src/core/context-guidance.ts +69 -0
- package/src/core/context-scope.ts +32 -0
- package/src/core/context-verifier-canonical.ts +90 -0
- package/src/core/context-verifier-input.ts +66 -0
- package/src/core/context-verifier.ts +447 -0
- package/src/core/sections.ts +63 -0
- package/src/mcp/server.ts +10 -4
- package/src/mcp/tools/context.ts +229 -0
- package/src/mcp/tools/index.ts +27 -0
- package/src/pipeline/chunk-lookup.ts +33 -0
- package/src/pipeline/hybrid.ts +79 -57
- package/src/pipeline/types.ts +14 -0
- package/src/sdk/client.ts +68 -6
- package/src/sdk/index.ts +21 -0
- package/src/sdk/types.ts +24 -0
- package/src/serve/background-runtime.ts +1 -0
- package/src/serve/context-capsule.ts +136 -0
- package/src/serve/context.ts +10 -1
- package/src/serve/routes/api.ts +2 -0
- package/src/serve/server.ts +23 -0
- package/src/store/sqlite/adapter.ts +38 -20
package/src/sdk/client.ts
CHANGED
|
@@ -22,6 +22,9 @@ import type {
|
|
|
22
22
|
GnoCreateFolderResult,
|
|
23
23
|
GnoCreateNoteOptions,
|
|
24
24
|
GnoCreateNoteResult,
|
|
25
|
+
GnoContextInput,
|
|
26
|
+
GnoContextResult,
|
|
27
|
+
GnoContextVerificationResult,
|
|
25
28
|
GnoClientInitOptions,
|
|
26
29
|
GnoDuplicateNoteOptions,
|
|
27
30
|
GnoEmbedOptions,
|
|
@@ -39,8 +42,21 @@ import type {
|
|
|
39
42
|
GnoVectorSearchOptions,
|
|
40
43
|
} from "./types";
|
|
41
44
|
|
|
42
|
-
import {
|
|
43
|
-
|
|
45
|
+
import {
|
|
46
|
+
decorateUriForIndex,
|
|
47
|
+
DEFAULT_INDEX_NAME,
|
|
48
|
+
getIndexDbPath,
|
|
49
|
+
} from "../app/constants";
|
|
50
|
+
import {
|
|
51
|
+
buildContextCapsule,
|
|
52
|
+
validateContextCapsuleBuildInput,
|
|
53
|
+
verifyContextCapsuleRuntime,
|
|
54
|
+
} from "../app/context-runtime";
|
|
55
|
+
import {
|
|
56
|
+
canonicalizeIndexName,
|
|
57
|
+
INDEX_NAME_REQUIREMENTS,
|
|
58
|
+
isValidIndexName,
|
|
59
|
+
} from "../app/index-name";
|
|
44
60
|
import {
|
|
45
61
|
ConfigSchema,
|
|
46
62
|
loadConfig,
|
|
@@ -108,7 +124,7 @@ interface OpenedClientState {
|
|
|
108
124
|
store: SqliteAdapter;
|
|
109
125
|
llm: LlmAdapter;
|
|
110
126
|
downloadPolicy: DownloadPolicy;
|
|
111
|
-
indexName
|
|
127
|
+
indexName: string;
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
interface RuntimePorts {
|
|
@@ -161,7 +177,10 @@ async function resolveClientState(
|
|
|
161
177
|
configSource = "file";
|
|
162
178
|
}
|
|
163
179
|
|
|
164
|
-
const
|
|
180
|
+
const indexName = canonicalizeIndexName(
|
|
181
|
+
options.indexName ?? DEFAULT_INDEX_NAME
|
|
182
|
+
);
|
|
183
|
+
const dbPath = options.dbPath ?? getIndexDbPath(indexName);
|
|
165
184
|
await mkdir(dirname(dbPath), { recursive: true });
|
|
166
185
|
|
|
167
186
|
const store = new SqliteAdapter();
|
|
@@ -179,7 +198,7 @@ async function resolveClientState(
|
|
|
179
198
|
llm: new LlmAdapter(config, options.cacheDir),
|
|
180
199
|
downloadPolicy:
|
|
181
200
|
options.downloadPolicy ?? resolveDownloadPolicy(process.env, {}),
|
|
182
|
-
indexName
|
|
201
|
+
indexName,
|
|
183
202
|
};
|
|
184
203
|
}
|
|
185
204
|
|
|
@@ -192,7 +211,7 @@ class GnoClientImpl implements GnoClient {
|
|
|
192
211
|
private readonly store: SqliteAdapter;
|
|
193
212
|
private readonly llm: LlmAdapter;
|
|
194
213
|
private readonly downloadPolicy: DownloadPolicy;
|
|
195
|
-
private readonly indexName
|
|
214
|
+
private readonly indexName: string;
|
|
196
215
|
private closed = false;
|
|
197
216
|
|
|
198
217
|
constructor(state: OpenedClientState) {
|
|
@@ -644,6 +663,49 @@ class GnoClientImpl implements GnoClient {
|
|
|
644
663
|
}
|
|
645
664
|
}
|
|
646
665
|
|
|
666
|
+
async context(input: GnoContextInput): Promise<GnoContextResult> {
|
|
667
|
+
this.assertOpen();
|
|
668
|
+
validateContextCapsuleBuildInput(
|
|
669
|
+
{ ...input, indexName: this.indexName },
|
|
670
|
+
this.indexName,
|
|
671
|
+
this.config.collections.map((collection) => collection.name)
|
|
672
|
+
);
|
|
673
|
+
const collection =
|
|
674
|
+
input.collections?.length === 1 ? input.collections[0] : undefined;
|
|
675
|
+
const useModels = input.depthPolicy !== "fast";
|
|
676
|
+
const ports = await this.createRuntimePorts({
|
|
677
|
+
embed: useModels,
|
|
678
|
+
rerank: useModels,
|
|
679
|
+
collection,
|
|
680
|
+
});
|
|
681
|
+
try {
|
|
682
|
+
return await buildContextCapsule(
|
|
683
|
+
{ ...input, indexName: this.indexName },
|
|
684
|
+
{
|
|
685
|
+
store: this.store,
|
|
686
|
+
config: this.config,
|
|
687
|
+
indexName: this.indexName,
|
|
688
|
+
vectorIndex: ports.vectorIndex,
|
|
689
|
+
embedPort: ports.embedPort,
|
|
690
|
+
rerankPort: ports.rerankPort,
|
|
691
|
+
}
|
|
692
|
+
);
|
|
693
|
+
} finally {
|
|
694
|
+
await this.disposeRuntimePorts(ports);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async verifyContext(
|
|
699
|
+
capsule: GnoContextResult
|
|
700
|
+
): Promise<GnoContextVerificationResult> {
|
|
701
|
+
this.assertOpen();
|
|
702
|
+
return verifyContextCapsuleRuntime(capsule, {
|
|
703
|
+
store: this.store,
|
|
704
|
+
config: this.config,
|
|
705
|
+
indexName: this.indexName,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
647
709
|
async get(ref: string, options: GnoGetOptions = {}) {
|
|
648
710
|
this.assertOpen();
|
|
649
711
|
const resolution = resolveEffectiveIndex([ref], this.indexName);
|
package/src/sdk/index.ts
CHANGED
|
@@ -31,6 +31,10 @@ export type {
|
|
|
31
31
|
GnoAskOptions,
|
|
32
32
|
GnoCaptureOptions,
|
|
33
33
|
GnoCaptureResult,
|
|
34
|
+
GnoContextErrorCode,
|
|
35
|
+
GnoContextInput,
|
|
36
|
+
GnoContextResult,
|
|
37
|
+
GnoContextVerificationResult,
|
|
34
38
|
GnoClient,
|
|
35
39
|
GnoClientInitOptions,
|
|
36
40
|
GnoEmbedOptions,
|
|
@@ -51,3 +55,20 @@ export type {
|
|
|
51
55
|
GnoUpdateOptions,
|
|
52
56
|
GnoVectorSearchOptions,
|
|
53
57
|
} from "./types";
|
|
58
|
+
export {
|
|
59
|
+
ContextCapsuleContractError,
|
|
60
|
+
type ContextCapsuleErrorCode,
|
|
61
|
+
type ContextCapsuleV1,
|
|
62
|
+
} from "../core/context-capsule";
|
|
63
|
+
export {
|
|
64
|
+
ContextEvidenceError,
|
|
65
|
+
type ContextEvidenceErrorCode,
|
|
66
|
+
} from "../core/context-evidence";
|
|
67
|
+
export {
|
|
68
|
+
ContextVerifierError,
|
|
69
|
+
type ContextVerifierErrorCode,
|
|
70
|
+
} from "../core/context-verifier";
|
|
71
|
+
export {
|
|
72
|
+
ContextRuntimeError,
|
|
73
|
+
type ContextRuntimeErrorCode,
|
|
74
|
+
} from "../app/context-runtime";
|
package/src/sdk/types.ts
CHANGED
|
@@ -4,8 +4,19 @@
|
|
|
4
4
|
* @module src/sdk/types
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type {
|
|
8
|
+
ContextCapsuleBuildInput,
|
|
9
|
+
ContextRuntimeErrorCode,
|
|
10
|
+
} from "../app/context-runtime";
|
|
7
11
|
import type { Config } from "../config/types";
|
|
8
12
|
import type { CaptureInput, CaptureReceipt } from "../core/capture";
|
|
13
|
+
import type {
|
|
14
|
+
ContextCapsuleErrorCode,
|
|
15
|
+
ContextCapsuleV1,
|
|
16
|
+
ContextCapsuleVerification,
|
|
17
|
+
} from "../core/context-capsule";
|
|
18
|
+
import type { ContextEvidenceErrorCode } from "../core/context-evidence";
|
|
19
|
+
import type { ContextVerifierErrorCode } from "../core/context-verifier";
|
|
9
20
|
import type { NoteCollisionPolicy } from "../core/note-creation";
|
|
10
21
|
import type { NotePresetId } from "../core/note-presets";
|
|
11
22
|
import type { DocumentSection } from "../core/sections";
|
|
@@ -65,6 +76,15 @@ export type GnoVectorSearchOptions = SearchOptions & {
|
|
|
65
76
|
model?: string;
|
|
66
77
|
};
|
|
67
78
|
|
|
79
|
+
export type GnoContextInput = Omit<ContextCapsuleBuildInput, "indexName">;
|
|
80
|
+
export type GnoContextResult = ContextCapsuleV1;
|
|
81
|
+
export type GnoContextVerificationResult = ContextCapsuleVerification;
|
|
82
|
+
export type GnoContextErrorCode =
|
|
83
|
+
| ContextRuntimeErrorCode
|
|
84
|
+
| ContextCapsuleErrorCode
|
|
85
|
+
| ContextEvidenceErrorCode
|
|
86
|
+
| ContextVerifierErrorCode;
|
|
87
|
+
|
|
68
88
|
export interface GnoGetOptions {
|
|
69
89
|
from?: number;
|
|
70
90
|
limit?: number;
|
|
@@ -185,6 +205,10 @@ export interface GnoClient {
|
|
|
185
205
|
): Promise<SearchResults>;
|
|
186
206
|
query(query: string, options?: GnoQueryOptions): Promise<SearchResults>;
|
|
187
207
|
ask(query: string, options?: GnoAskOptions): Promise<AskResult>;
|
|
208
|
+
context(input: GnoContextInput): Promise<GnoContextResult>;
|
|
209
|
+
verifyContext(
|
|
210
|
+
capsule: ContextCapsuleV1
|
|
211
|
+
): Promise<GnoContextVerificationResult>;
|
|
188
212
|
get(
|
|
189
213
|
ref: string,
|
|
190
214
|
options?: GnoGetOptions
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** REST adapters for the shared Context Capsule application boundary. */
|
|
2
|
+
|
|
3
|
+
import type { GnoContextErrorCode } from "../sdk/types";
|
|
4
|
+
import type { ServerContext } from "./context";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
formatContextCapsuleMarkdown,
|
|
8
|
+
formatContextCapsuleVerificationMarkdown,
|
|
9
|
+
} from "../app/context-format";
|
|
10
|
+
import {
|
|
11
|
+
buildContextCapsule,
|
|
12
|
+
canonicalBuiltContextCapsuleJson,
|
|
13
|
+
canonicalVerifiedContextCapsuleJson,
|
|
14
|
+
validateContextCapsuleBuildInput,
|
|
15
|
+
verifyContextCapsuleRuntime,
|
|
16
|
+
} from "../app/context-runtime";
|
|
17
|
+
import {
|
|
18
|
+
contextSurfaceError,
|
|
19
|
+
parseContextBuildSurfaceInput,
|
|
20
|
+
parseContextVerifySurfaceInput,
|
|
21
|
+
} from "../app/context-surface";
|
|
22
|
+
import { ContextCapsuleContractError } from "../core/context-capsule";
|
|
23
|
+
|
|
24
|
+
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
|
25
|
+
const MARKDOWN_HEADERS = {
|
|
26
|
+
"content-type": "text/markdown; charset=utf-8",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const parseJsonBody = async (request: Request): Promise<unknown> => {
|
|
30
|
+
try {
|
|
31
|
+
return await request.json();
|
|
32
|
+
} catch {
|
|
33
|
+
throw new ContextCapsuleContractError("invalid_input", "Invalid JSON body");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ContextRestErrorCode = GnoContextErrorCode | "runtime_error";
|
|
38
|
+
|
|
39
|
+
export const CONTEXT_REST_ERROR_STATUS = {
|
|
40
|
+
invalid_goal: 400,
|
|
41
|
+
invalid_budget: 400,
|
|
42
|
+
invalid_filter: 400,
|
|
43
|
+
invalid_uri: 400,
|
|
44
|
+
invalid_input: 400,
|
|
45
|
+
identity_mismatch: 400,
|
|
46
|
+
no_evidence: 404,
|
|
47
|
+
tokenizer_unavailable: 503,
|
|
48
|
+
chunk_coordinate_mismatch: 409,
|
|
49
|
+
stored_provenance_mismatch: 409,
|
|
50
|
+
index_snapshot_mismatch: 409,
|
|
51
|
+
index_changed_during_compile: 409,
|
|
52
|
+
context_changed_during_compile: 409,
|
|
53
|
+
capsule_mutated_during_verify: 409,
|
|
54
|
+
context_changed_during_verify: 409,
|
|
55
|
+
index_changed_during_verify: 409,
|
|
56
|
+
retrieval_failed: 500,
|
|
57
|
+
chunk_load_failed: 500,
|
|
58
|
+
collection_load_failed: 500,
|
|
59
|
+
content_load_failed: 500,
|
|
60
|
+
context_load_failed: 500,
|
|
61
|
+
document_load_failed: 500,
|
|
62
|
+
index_snapshot_failed: 500,
|
|
63
|
+
runtime_error: 500,
|
|
64
|
+
} as const satisfies Record<ContextRestErrorCode, number>;
|
|
65
|
+
|
|
66
|
+
export const contextRestStatusForCode = (code: string): number =>
|
|
67
|
+
code in CONTEXT_REST_ERROR_STATUS
|
|
68
|
+
? CONTEXT_REST_ERROR_STATUS[code as ContextRestErrorCode]
|
|
69
|
+
: 500;
|
|
70
|
+
|
|
71
|
+
const errorResponse = (error: unknown): Response => {
|
|
72
|
+
const publicError = contextSurfaceError(error);
|
|
73
|
+
return new Response(JSON.stringify({ error: publicError }), {
|
|
74
|
+
status: contextRestStatusForCode(publicError.code),
|
|
75
|
+
headers: JSON_HEADERS,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const handleContextBuild = async (
|
|
80
|
+
context: ServerContext,
|
|
81
|
+
request: Request
|
|
82
|
+
): Promise<Response> => {
|
|
83
|
+
try {
|
|
84
|
+
const { input, format } = parseContextBuildSurfaceInput(
|
|
85
|
+
await parseJsonBody(request),
|
|
86
|
+
context.indexName
|
|
87
|
+
);
|
|
88
|
+
validateContextCapsuleBuildInput(
|
|
89
|
+
input,
|
|
90
|
+
context.indexName,
|
|
91
|
+
context.config.collections.map((collection) => collection.name)
|
|
92
|
+
);
|
|
93
|
+
const capsule = await buildContextCapsule(input, {
|
|
94
|
+
store: context.store,
|
|
95
|
+
config: context.config,
|
|
96
|
+
indexName: context.indexName,
|
|
97
|
+
vectorIndex: context.vectorIndex,
|
|
98
|
+
embedPort: context.embedPort,
|
|
99
|
+
rerankPort: context.rerankPort,
|
|
100
|
+
});
|
|
101
|
+
return format === "md"
|
|
102
|
+
? new Response(formatContextCapsuleMarkdown(capsule), {
|
|
103
|
+
headers: MARKDOWN_HEADERS,
|
|
104
|
+
})
|
|
105
|
+
: new Response(canonicalBuiltContextCapsuleJson(capsule), {
|
|
106
|
+
headers: JSON_HEADERS,
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return errorResponse(error);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const handleContextVerify = async (
|
|
114
|
+
context: ServerContext,
|
|
115
|
+
request: Request
|
|
116
|
+
): Promise<Response> => {
|
|
117
|
+
try {
|
|
118
|
+
const { capsule, format } = parseContextVerifySurfaceInput(
|
|
119
|
+
await parseJsonBody(request)
|
|
120
|
+
);
|
|
121
|
+
const receipt = await verifyContextCapsuleRuntime(capsule, {
|
|
122
|
+
store: context.store,
|
|
123
|
+
config: context.config,
|
|
124
|
+
indexName: context.indexName,
|
|
125
|
+
});
|
|
126
|
+
return format === "md"
|
|
127
|
+
? new Response(formatContextCapsuleVerificationMarkdown(receipt), {
|
|
128
|
+
headers: MARKDOWN_HEADERS,
|
|
129
|
+
})
|
|
130
|
+
: new Response(canonicalVerifiedContextCapsuleJson(receipt), {
|
|
131
|
+
headers: JSON_HEADERS,
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return errorResponse(error);
|
|
135
|
+
}
|
|
136
|
+
};
|
package/src/serve/context.ts
CHANGED
|
@@ -19,6 +19,8 @@ import type { DocumentEventBus } from "./doc-events";
|
|
|
19
19
|
import type { EmbedScheduler } from "./embed-scheduler";
|
|
20
20
|
import type { CollectionWatchService } from "./watch-service";
|
|
21
21
|
|
|
22
|
+
import { DEFAULT_INDEX_NAME } from "../app/constants";
|
|
23
|
+
import { canonicalizeIndexName } from "../app/index-name";
|
|
22
24
|
import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
|
|
23
25
|
import { resolveDownloadPolicy } from "../llm/policy";
|
|
24
26
|
import { getActivePreset } from "../llm/registry";
|
|
@@ -64,6 +66,8 @@ export function resetDownloadState(): void {
|
|
|
64
66
|
export interface ServerContext {
|
|
65
67
|
store: SqliteAdapter;
|
|
66
68
|
config: Config;
|
|
69
|
+
/** Canonical identity of the already-open resident store. */
|
|
70
|
+
indexName: string;
|
|
67
71
|
vectorIndex: VectorIndexPort | null;
|
|
68
72
|
embedPort: EmbeddingPort | null;
|
|
69
73
|
expandPort: GenerationPort | null;
|
|
@@ -82,6 +86,7 @@ export interface ServerContext {
|
|
|
82
86
|
|
|
83
87
|
export interface CreateServerContextOptions {
|
|
84
88
|
offline?: boolean;
|
|
89
|
+
indexName?: string;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
/**
|
|
@@ -197,6 +202,7 @@ export async function createServerContext(
|
|
|
197
202
|
return {
|
|
198
203
|
store,
|
|
199
204
|
config,
|
|
205
|
+
indexName: canonicalizeIndexName(options.indexName ?? DEFAULT_INDEX_NAME),
|
|
200
206
|
vectorIndex,
|
|
201
207
|
embedPort,
|
|
202
208
|
expandPort,
|
|
@@ -242,5 +248,8 @@ export async function reloadServerContext(
|
|
|
242
248
|
options: CreateServerContextOptions = {}
|
|
243
249
|
): Promise<ServerContext> {
|
|
244
250
|
await disposeServerContext(ctx);
|
|
245
|
-
return createServerContext(ctx.store, newConfig ?? ctx.config,
|
|
251
|
+
return createServerContext(ctx.store, newConfig ?? ctx.config, {
|
|
252
|
+
...options,
|
|
253
|
+
indexName: options.indexName ?? ctx.indexName,
|
|
254
|
+
});
|
|
246
255
|
}
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -4509,6 +4509,7 @@ export async function routeApi(
|
|
|
4509
4509
|
return handleStatus({
|
|
4510
4510
|
store,
|
|
4511
4511
|
config,
|
|
4512
|
+
indexName: "default",
|
|
4512
4513
|
vectorIndex: null,
|
|
4513
4514
|
embedPort: null,
|
|
4514
4515
|
expandPort: null,
|
|
@@ -4544,6 +4545,7 @@ export async function routeApi(
|
|
|
4544
4545
|
current: {
|
|
4545
4546
|
config,
|
|
4546
4547
|
store,
|
|
4548
|
+
indexName: "default",
|
|
4547
4549
|
vectorIndex: null,
|
|
4548
4550
|
embedPort: null,
|
|
4549
4551
|
expandPort: null,
|
package/src/serve/server.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { ContextHolder } from "./routes/api";
|
|
10
10
|
|
|
11
11
|
import { startBackgroundRuntime } from "./background-runtime";
|
|
12
|
+
import { handleContextBuild, handleContextVerify } from "./context-capsule";
|
|
12
13
|
import { DocumentEventBus } from "./doc-events";
|
|
13
14
|
// HTML import - Bun handles bundling TSX/CSS automatically via routes
|
|
14
15
|
import homepage from "./public/index.html";
|
|
@@ -525,6 +526,28 @@ export async function startServer(
|
|
|
525
526
|
);
|
|
526
527
|
},
|
|
527
528
|
},
|
|
529
|
+
"/api/context": {
|
|
530
|
+
POST: async (req: Request) => {
|
|
531
|
+
if (!isRequestAllowed(req, port)) {
|
|
532
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
533
|
+
}
|
|
534
|
+
return withSecurityHeaders(
|
|
535
|
+
await handleContextBuild(ctxHolder.current, req),
|
|
536
|
+
isDev
|
|
537
|
+
);
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
"/api/context/verify": {
|
|
541
|
+
POST: async (req: Request) => {
|
|
542
|
+
if (!isRequestAllowed(req, port)) {
|
|
543
|
+
return withSecurityHeaders(forbiddenResponse(), isDev);
|
|
544
|
+
}
|
|
545
|
+
return withSecurityHeaders(
|
|
546
|
+
await handleContextVerify(ctxHolder.current, req),
|
|
547
|
+
isDev
|
|
548
|
+
);
|
|
549
|
+
},
|
|
550
|
+
},
|
|
528
551
|
"/api/query/diagnose": {
|
|
529
552
|
POST: async (req: Request) => {
|
|
530
553
|
if (!isRequestAllowed(req, port)) {
|
|
@@ -86,12 +86,17 @@ const WHITESPACE_REGEX = /\s+/;
|
|
|
86
86
|
const SINGLE_LINE_QUERY_PATTERN = /[\r\n]/;
|
|
87
87
|
const DOUBLE_QUOTE_PATTERN = /"/g;
|
|
88
88
|
const DOC_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
89
|
+
const SQLITE_SAFE_PARAMETER_BATCH_SIZE = 900;
|
|
89
90
|
const FTS5_FIELD_WEIGHTS = {
|
|
90
91
|
filepath: 1.5,
|
|
91
92
|
title: 4.0,
|
|
92
93
|
body: 1.0,
|
|
93
94
|
} as const;
|
|
94
95
|
|
|
96
|
+
const uniqueNonEmptyValues = (values: readonly string[]): string[] => [
|
|
97
|
+
...new Set(values.filter((value) => value.trim().length > 0)),
|
|
98
|
+
];
|
|
99
|
+
|
|
95
100
|
/** Content-free activation snapshot query; kept exported for contract tests. */
|
|
96
101
|
export const ACTIVATION_INDEX_SNAPSHOT_SQL = `SELECT
|
|
97
102
|
d.id,
|
|
@@ -1384,28 +1389,38 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1384
1389
|
mirrorHashes: string[]
|
|
1385
1390
|
): Promise<StoreResult<Map<string, string>>> {
|
|
1386
1391
|
try {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1389
|
-
if (mirrorHashes.length === 0) {
|
|
1392
|
+
const uniqueHashes = uniqueNonEmptyValues(mirrorHashes);
|
|
1393
|
+
if (uniqueHashes.length === 0) {
|
|
1390
1394
|
return ok(new Map());
|
|
1391
1395
|
}
|
|
1396
|
+
const db = this.ensureOpen();
|
|
1392
1397
|
|
|
1393
1398
|
interface DbContentRow {
|
|
1394
1399
|
mirror_hash: string;
|
|
1395
1400
|
markdown: string;
|
|
1396
1401
|
}
|
|
1397
1402
|
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
.
|
|
1403
|
+
const result = new Map<string, string>();
|
|
1404
|
+
for (
|
|
1405
|
+
let offset = 0;
|
|
1406
|
+
offset < uniqueHashes.length;
|
|
1407
|
+
offset += SQLITE_SAFE_PARAMETER_BATCH_SIZE
|
|
1408
|
+
) {
|
|
1409
|
+
const batch = uniqueHashes.slice(
|
|
1410
|
+
offset,
|
|
1411
|
+
offset + SQLITE_SAFE_PARAMETER_BATCH_SIZE
|
|
1412
|
+
);
|
|
1413
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
1414
|
+
const rows = db
|
|
1415
|
+
.query<DbContentRow, string[]>(
|
|
1416
|
+
`SELECT mirror_hash, markdown FROM content
|
|
1417
|
+
WHERE mirror_hash IN (${placeholders})`
|
|
1418
|
+
)
|
|
1419
|
+
.all(...batch);
|
|
1420
|
+
for (const row of rows) result.set(row.mirror_hash, row.markdown);
|
|
1421
|
+
}
|
|
1405
1422
|
|
|
1406
|
-
return ok(
|
|
1407
|
-
new Map(rows.map((row) => [row.mirror_hash, row.markdown] as const))
|
|
1408
|
-
);
|
|
1423
|
+
return ok(result);
|
|
1409
1424
|
} catch (cause) {
|
|
1410
1425
|
return err(
|
|
1411
1426
|
"QUERY_FAILED",
|
|
@@ -1493,9 +1508,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1493
1508
|
}
|
|
1494
1509
|
|
|
1495
1510
|
// Dedupe and filter empty strings
|
|
1496
|
-
const uniqueHashes =
|
|
1497
|
-
...new Set(mirrorHashes.filter((h) => h.trim().length > 0)),
|
|
1498
|
-
];
|
|
1511
|
+
const uniqueHashes = uniqueNonEmptyValues(mirrorHashes);
|
|
1499
1512
|
if (uniqueHashes.length === 0) {
|
|
1500
1513
|
return ok(new Map());
|
|
1501
1514
|
}
|
|
@@ -1505,11 +1518,16 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1505
1518
|
|
|
1506
1519
|
// SQLite SQLITE_LIMIT_VARIABLE_NUMBER defaults to 999
|
|
1507
1520
|
// Reserve 99 for potential future filter params (collection, language, etc.)
|
|
1508
|
-
const SQLITE_MAX_PARAMS = 900;
|
|
1509
|
-
|
|
1510
1521
|
// Batch queries to respect SQLite parameter limit
|
|
1511
|
-
for (
|
|
1512
|
-
|
|
1522
|
+
for (
|
|
1523
|
+
let i = 0;
|
|
1524
|
+
i < uniqueHashes.length;
|
|
1525
|
+
i += SQLITE_SAFE_PARAMETER_BATCH_SIZE
|
|
1526
|
+
) {
|
|
1527
|
+
const batch = uniqueHashes.slice(
|
|
1528
|
+
i,
|
|
1529
|
+
i + SQLITE_SAFE_PARAMETER_BATCH_SIZE
|
|
1530
|
+
);
|
|
1513
1531
|
const placeholders = batch.map(() => "?").join(",");
|
|
1514
1532
|
const sql = `SELECT * FROM content_chunks
|
|
1515
1533
|
WHERE mirror_hash IN (${placeholders})
|