@gmickel/gno 1.16.0 → 1.18.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.
Files changed (86) hide show
  1. package/README.md +34 -18
  2. package/assets/skill/SKILL.md +25 -6
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +2 -2
  5. package/src/app/context-agent-projection.ts +303 -0
  6. package/src/app/context-format.ts +249 -0
  7. package/src/app/context-runtime-contract.ts +325 -0
  8. package/src/app/context-runtime-input.ts +362 -0
  9. package/src/app/context-runtime-types.ts +65 -0
  10. package/src/app/context-runtime.ts +170 -0
  11. package/src/app/context-surface.ts +145 -0
  12. package/src/cli/commands/context-build.ts +149 -0
  13. package/src/cli/commands/context-verify.ts +90 -0
  14. package/src/cli/commands/daemon.ts +69 -2
  15. package/src/cli/commands/models/pull.ts +13 -3
  16. package/src/cli/commands/status.ts +2 -0
  17. package/src/cli/detach.ts +37 -20
  18. package/src/cli/options.ts +4 -0
  19. package/src/cli/program.ts +252 -27
  20. package/src/config/index.ts +3 -0
  21. package/src/config/types.ts +37 -0
  22. package/src/core/context-budget.ts +461 -0
  23. package/src/core/context-capsule-index-schema.ts +15 -0
  24. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  25. package/src/core/context-capsule-schema.ts +473 -0
  26. package/src/core/context-capsule-validation.ts +416 -0
  27. package/src/core/context-capsule-verification.ts +218 -0
  28. package/src/core/context-capsule.ts +439 -0
  29. package/src/core/context-compiler.ts +513 -0
  30. package/src/core/context-evidence-metadata.ts +33 -0
  31. package/src/core/context-evidence.ts +495 -0
  32. package/src/core/context-facets.ts +163 -0
  33. package/src/core/context-guidance.ts +69 -0
  34. package/src/core/context-scope.ts +32 -0
  35. package/src/core/context-verifier-canonical.ts +90 -0
  36. package/src/core/context-verifier-input.ts +66 -0
  37. package/src/core/context-verifier.ts +447 -0
  38. package/src/core/job-manager.ts +19 -0
  39. package/src/core/mutation-generations.ts +33 -0
  40. package/src/core/sections.ts +63 -0
  41. package/src/llm/cache.ts +13 -3
  42. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  43. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  44. package/src/mcp/context.ts +161 -0
  45. package/src/mcp/http-security.ts +477 -0
  46. package/src/mcp/http-session.ts +272 -0
  47. package/src/mcp/http-transport.ts +370 -0
  48. package/src/mcp/resources/index.ts +141 -134
  49. package/src/mcp/server.ts +28 -82
  50. package/src/mcp/tools/add-collection.ts +3 -1
  51. package/src/mcp/tools/capture.ts +3 -0
  52. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  53. package/src/mcp/tools/context.ts +230 -0
  54. package/src/mcp/tools/embed.ts +62 -52
  55. package/src/mcp/tools/index-cmd.ts +88 -74
  56. package/src/mcp/tools/index.ts +49 -2
  57. package/src/mcp/tools/remove-collection.ts +2 -0
  58. package/src/mcp/tools/status.ts +11 -0
  59. package/src/mcp/tools/sync.ts +16 -14
  60. package/src/mcp/tools/workspace-write.ts +7 -3
  61. package/src/pipeline/chunk-lookup.ts +33 -0
  62. package/src/pipeline/hybrid.ts +79 -57
  63. package/src/pipeline/types.ts +14 -0
  64. package/src/sdk/client.ts +68 -6
  65. package/src/sdk/index.ts +21 -0
  66. package/src/sdk/types.ts +24 -0
  67. package/src/serve/background-runtime.ts +12 -211
  68. package/src/serve/context-capsule.ts +136 -0
  69. package/src/serve/context.ts +10 -1
  70. package/src/serve/embed-scheduler.ts +74 -43
  71. package/src/serve/index.ts +9 -0
  72. package/src/serve/jobs.ts +78 -80
  73. package/src/serve/public/components/HealthCenter.tsx +74 -1
  74. package/src/serve/public/globals.built.css +1 -1
  75. package/src/serve/public/pages/Dashboard.tsx +1 -0
  76. package/src/serve/resident-admission.ts +159 -0
  77. package/src/serve/resident-background-work.ts +39 -0
  78. package/src/serve/resident-request.ts +55 -0
  79. package/src/serve/resident-runtime.ts +490 -0
  80. package/src/serve/resident-status.ts +96 -0
  81. package/src/serve/routes/api.ts +265 -167
  82. package/src/serve/routes/mcp.ts +69 -0
  83. package/src/serve/server.ts +212 -35
  84. package/src/serve/status-model.ts +51 -0
  85. package/src/serve/status.ts +5 -0
  86. package/src/store/sqlite/adapter.ts +64 -29
@@ -0,0 +1,362 @@
1
+ /** Strict shared input normalization for every Context Capsule surface. */
2
+
3
+ import type { QueryModeInput } from "../pipeline/types";
4
+ import type { ContextCapsuleBuildInput } from "./context-runtime-types";
5
+
6
+ import { isValidLanguageHint } from "../config/types";
7
+ import { normalizeTag, validateTag } from "../core/tags";
8
+ import { resolveTemporalRange } from "../pipeline/temporal";
9
+ import { buildUri, parseUri } from "./constants";
10
+ import { ContextRuntimeError } from "./context-runtime-types";
11
+ import { canonicalizeIndexName } from "./index-name";
12
+
13
+ const COLLECTION_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
14
+ const QUERY_MODES = new Set(["term", "intent", "hyde"]);
15
+ const MAX_FILTER_VALUES = 128;
16
+ const MAX_FILTER_LENGTH = 256;
17
+ const MAX_TEXT_LENGTH = 16_384;
18
+ const DEFAULT_LIMIT = 20;
19
+ const DEFAULT_CANDIDATE_LIMIT = 40;
20
+
21
+ const compareCodeUnits = (left: string, right: string): number =>
22
+ left < right ? -1 : left > right ? 1 : 0;
23
+
24
+ const canonicalStrings = (
25
+ values: readonly string[] | undefined,
26
+ label: string
27
+ ): string[] => {
28
+ if (
29
+ values !== undefined &&
30
+ (!Array.isArray(values) ||
31
+ values.some((value) => typeof value !== "string"))
32
+ ) {
33
+ throw new ContextRuntimeError("invalid_filter", `${label} must be strings`);
34
+ }
35
+ return [
36
+ ...new Set((values ?? []).map((value) => value.normalize("NFC").trim())),
37
+ ].sort(compareCodeUnits);
38
+ };
39
+
40
+ const canonicalFilters = (
41
+ values: readonly string[] | undefined,
42
+ label: string
43
+ ): string[] => {
44
+ const normalized = canonicalStrings(values, label);
45
+ if (
46
+ normalized.length > MAX_FILTER_VALUES ||
47
+ normalized.some(
48
+ (value) =>
49
+ value.length === 0 ||
50
+ value.length > MAX_FILTER_LENGTH ||
51
+ value.includes("\r")
52
+ )
53
+ ) {
54
+ throw new ContextRuntimeError(
55
+ "invalid_filter",
56
+ `${label} contains an invalid value`
57
+ );
58
+ }
59
+ return normalized;
60
+ };
61
+
62
+ const canonicalTagFilters = (
63
+ values: readonly string[] | undefined,
64
+ label: string
65
+ ): string[] => {
66
+ if (
67
+ values !== undefined &&
68
+ (!Array.isArray(values) ||
69
+ values.some((value) => typeof value !== "string"))
70
+ ) {
71
+ throw new ContextRuntimeError("invalid_filter", `${label} must be strings`);
72
+ }
73
+ const normalized = [
74
+ ...new Set((values ?? []).map((value) => normalizeTag(value))),
75
+ ].sort(compareCodeUnits);
76
+ if (
77
+ normalized.length > MAX_FILTER_VALUES ||
78
+ normalized.some(
79
+ (value) =>
80
+ value.length > MAX_FILTER_LENGTH ||
81
+ value.includes("\r") ||
82
+ !validateTag(value)
83
+ )
84
+ ) {
85
+ throw new ContextRuntimeError(
86
+ "invalid_filter",
87
+ `${label} contains an invalid tag`
88
+ );
89
+ }
90
+ return normalized;
91
+ };
92
+
93
+ const positiveSafeInteger = (
94
+ value: number,
95
+ label: string,
96
+ code: "invalid_budget" | "invalid_filter" = "invalid_budget"
97
+ ): number => {
98
+ if (!Number.isSafeInteger(value) || value < 1) {
99
+ throw new ContextRuntimeError(
100
+ code,
101
+ `${label} must be a positive safe integer`
102
+ );
103
+ }
104
+ return value;
105
+ };
106
+
107
+ const canonicalQueryModes = (
108
+ values: QueryModeInput[] | undefined
109
+ ): QueryModeInput[] => {
110
+ if (values === undefined) return [];
111
+ if (!Array.isArray(values) || values.length > MAX_FILTER_VALUES) {
112
+ throw new ContextRuntimeError("invalid_filter", "Invalid query modes");
113
+ }
114
+ const normalized = values.map((value) => {
115
+ if (
116
+ value === null ||
117
+ typeof value !== "object" ||
118
+ !QUERY_MODES.has(value.mode) ||
119
+ typeof value.text !== "string"
120
+ ) {
121
+ throw new ContextRuntimeError("invalid_filter", "Invalid query mode");
122
+ }
123
+ const text = value.text.normalize("NFC").trim();
124
+ if (!text || text.length > 4096 || text.includes("\r")) {
125
+ throw new ContextRuntimeError(
126
+ "invalid_filter",
127
+ "Invalid query mode text"
128
+ );
129
+ }
130
+ return { mode: value.mode, text };
131
+ });
132
+ if (normalized.filter((value) => value.mode === "hyde").length > 1) {
133
+ throw new ContextRuntimeError(
134
+ "invalid_filter",
135
+ "Only one hyde query mode is allowed"
136
+ );
137
+ }
138
+ return normalized;
139
+ };
140
+
141
+ const validateUriPrefix = (
142
+ value: string | null | undefined,
143
+ indexName: string,
144
+ collections: readonly string[]
145
+ ): string | null => {
146
+ if (value === undefined || value === null) return null;
147
+ if (typeof value !== "string") {
148
+ throw new ContextRuntimeError("invalid_uri", "URI prefix must be a string");
149
+ }
150
+ const parsed = parseUri(value);
151
+ if (
152
+ !parsed ||
153
+ parsed.collection.length === 0 ||
154
+ (parsed.indexName !== undefined &&
155
+ canonicalizeIndexName(parsed.indexName) !== indexName) ||
156
+ (collections.length > 0 && !collections.includes(parsed.collection))
157
+ ) {
158
+ throw new ContextRuntimeError(
159
+ "invalid_uri",
160
+ "URI prefix must be a canonical GNO reference inside the requested index and collections"
161
+ );
162
+ }
163
+ if (buildUri(parsed.collection, parsed.path, { indexName }) !== value) {
164
+ throw new ContextRuntimeError(
165
+ "invalid_uri",
166
+ "URI prefix must use its canonical indexed GNO representation"
167
+ );
168
+ }
169
+ return value;
170
+ };
171
+
172
+ export const normalizeContextBuildInput = (
173
+ input: ContextCapsuleBuildInput,
174
+ defaultIndexName: string | undefined,
175
+ now: Date,
176
+ configuredCollectionNames?: readonly string[]
177
+ ) => {
178
+ if (!input || typeof input !== "object" || typeof input.goal !== "string") {
179
+ throw new ContextRuntimeError("invalid_goal", "Context goal is required");
180
+ }
181
+ const rawQuery = input.query ?? input.goal;
182
+ const goal = input.goal.normalize("NFC").trim();
183
+ const query =
184
+ typeof rawQuery === "string" ? rawQuery.normalize("NFC").trim() : "";
185
+ if (
186
+ !goal ||
187
+ !query ||
188
+ goal.length > MAX_TEXT_LENGTH ||
189
+ query.length > MAX_TEXT_LENGTH ||
190
+ goal.includes("\r") ||
191
+ query.includes("\r")
192
+ ) {
193
+ throw new ContextRuntimeError(
194
+ "invalid_goal",
195
+ "Context goal and query must be non-empty canonical text"
196
+ );
197
+ }
198
+ let indexName: string;
199
+ try {
200
+ indexName = canonicalizeIndexName(
201
+ input.indexName ?? defaultIndexName ?? "default"
202
+ );
203
+ } catch (cause) {
204
+ throw new ContextRuntimeError(
205
+ "invalid_filter",
206
+ "Context index name is invalid",
207
+ cause
208
+ );
209
+ }
210
+ const collections = canonicalStrings(input.collections, "collections");
211
+ if (
212
+ collections.length > MAX_FILTER_VALUES ||
213
+ collections.some((value) => !COLLECTION_PATTERN.test(value))
214
+ ) {
215
+ throw new ContextRuntimeError(
216
+ "invalid_filter",
217
+ "Invalid collection filter"
218
+ );
219
+ }
220
+ if (configuredCollectionNames) {
221
+ const configured = new Set(configuredCollectionNames);
222
+ const unknown = collections.find(
223
+ (collection) => !configured.has(collection)
224
+ );
225
+ if (unknown) {
226
+ throw new ContextRuntimeError(
227
+ "invalid_filter",
228
+ `Collection not found: ${unknown}`
229
+ );
230
+ }
231
+ const prefixCollection =
232
+ input.uriPrefix === undefined || input.uriPrefix === null
233
+ ? null
234
+ : parseUri(input.uriPrefix)?.collection;
235
+ if (prefixCollection && !configured.has(prefixCollection)) {
236
+ throw new ContextRuntimeError(
237
+ "invalid_filter",
238
+ `Collection not found: ${prefixCollection}`
239
+ );
240
+ }
241
+ }
242
+ const uriPrefix = validateUriPrefix(input.uriPrefix, indexName, collections);
243
+ const budgetTokens = positiveSafeInteger(
244
+ input.budgetTokens,
245
+ "Context token budget"
246
+ );
247
+ const budgetBytes =
248
+ input.budgetBytes === undefined
249
+ ? Math.min(Number.MAX_SAFE_INTEGER, budgetTokens * 4)
250
+ : positiveSafeInteger(input.budgetBytes, "Context byte budget");
251
+ const safetyMarginTokens = input.safetyMarginTokens ?? 0;
252
+ const safetyMarginBytes = input.safetyMarginBytes ?? 0;
253
+ if (
254
+ !Number.isSafeInteger(safetyMarginTokens) ||
255
+ !Number.isSafeInteger(safetyMarginBytes) ||
256
+ safetyMarginTokens < 0 ||
257
+ safetyMarginBytes < 0 ||
258
+ safetyMarginTokens >= budgetTokens ||
259
+ safetyMarginBytes >= budgetBytes
260
+ ) {
261
+ throw new ContextRuntimeError(
262
+ "invalid_budget",
263
+ "Context safety margins must be non-negative and smaller than their budgets"
264
+ );
265
+ }
266
+ if (
267
+ (input.since !== undefined && typeof input.since !== "string") ||
268
+ (input.until !== undefined && typeof input.until !== "string")
269
+ ) {
270
+ throw new ContextRuntimeError(
271
+ "invalid_filter",
272
+ "Context date filters are invalid"
273
+ );
274
+ }
275
+ const temporalRange = resolveTemporalRange(
276
+ query,
277
+ input.since,
278
+ input.until,
279
+ now
280
+ );
281
+ if (
282
+ (input.since !== undefined && temporalRange.since === undefined) ||
283
+ (input.until !== undefined && temporalRange.until === undefined) ||
284
+ (temporalRange.since !== undefined &&
285
+ temporalRange.until !== undefined &&
286
+ temporalRange.since > temporalRange.until)
287
+ ) {
288
+ throw new ContextRuntimeError(
289
+ "invalid_filter",
290
+ "Context date filters are invalid or reversed"
291
+ );
292
+ }
293
+ const depthPolicy = input.depthPolicy ?? "balanced";
294
+ if (!["fast", "balanced", "thorough"].includes(depthPolicy)) {
295
+ throw new ContextRuntimeError(
296
+ "invalid_filter",
297
+ "Invalid Context depth policy"
298
+ );
299
+ }
300
+ const limit = input.limit ?? DEFAULT_LIMIT;
301
+ const candidateLimit =
302
+ input.candidateLimit ??
303
+ (depthPolicy === "thorough"
304
+ ? DEFAULT_CANDIDATE_LIMIT * 2
305
+ : DEFAULT_CANDIDATE_LIMIT);
306
+ positiveSafeInteger(limit, "Context result limit", "invalid_filter");
307
+ positiveSafeInteger(
308
+ candidateLimit,
309
+ "Context candidate limit",
310
+ "invalid_filter"
311
+ );
312
+ if (input.graph !== undefined && typeof input.graph !== "boolean") {
313
+ throw new ContextRuntimeError(
314
+ "invalid_filter",
315
+ "Context graph flag must be boolean"
316
+ );
317
+ }
318
+ const author =
319
+ typeof input.author === "string"
320
+ ? input.author.normalize("NFC").trim()
321
+ : null;
322
+ const lang =
323
+ typeof input.lang === "string" ? input.lang.normalize("NFC").trim() : null;
324
+ if (
325
+ (input.author !== undefined &&
326
+ (!author || author.length > MAX_FILTER_LENGTH)) ||
327
+ (input.lang !== undefined && (!lang || !isValidLanguageHint(lang)))
328
+ ) {
329
+ throw new ContextRuntimeError(
330
+ "invalid_filter",
331
+ "Context author or language filter is invalid"
332
+ );
333
+ }
334
+ return {
335
+ ...input,
336
+ goal,
337
+ query,
338
+ indexName,
339
+ collections,
340
+ uriPrefix,
341
+ queryModes: canonicalQueryModes(input.queryModes),
342
+ tagsAll: canonicalTagFilters(input.tagsAll, "tagsAll"),
343
+ tagsAny: canonicalTagFilters(input.tagsAny, "tagsAny"),
344
+ categories: canonicalFilters(input.categories, "categories"),
345
+ author,
346
+ lang,
347
+ since: temporalRange.since,
348
+ until: temporalRange.until,
349
+ graph: input.graph ?? false,
350
+ limit,
351
+ candidateLimit,
352
+ budgetTokens,
353
+ budgetBytes,
354
+ safetyMarginTokens,
355
+ safetyMarginBytes,
356
+ depthPolicy,
357
+ };
358
+ };
359
+
360
+ export type NormalizedContextBuildInput = ReturnType<
361
+ typeof normalizeContextBuildInput
362
+ >;
@@ -0,0 +1,65 @@
1
+ import type { Config } from "../config/types";
2
+ import type { ContextCapsuleV1 } from "../core/context-capsule";
3
+ import type { ContextEvidenceCompilerDeps } from "../core/context-evidence";
4
+ import type { ContextVerifierDeps } from "../core/context-verifier";
5
+ import type { EmbeddingPort, RerankPort } from "../llm/types";
6
+ import type { QueryModeInput } from "../pipeline/types";
7
+ import type { StorePort } from "../store/types";
8
+ import type { VectorIndexPort } from "../store/vector";
9
+
10
+ export type ContextDepthPolicy = "fast" | "balanced" | "thorough";
11
+
12
+ export interface ContextCapsuleBuildInput {
13
+ goal: string;
14
+ query?: string;
15
+ indexName?: string;
16
+ collections?: string[];
17
+ uriPrefix?: string | null;
18
+ queryModes?: QueryModeInput[];
19
+ tagsAll?: string[];
20
+ tagsAny?: string[];
21
+ categories?: string[];
22
+ author?: string;
23
+ lang?: string;
24
+ since?: string;
25
+ until?: string;
26
+ graph?: boolean;
27
+ limit?: number;
28
+ candidateLimit?: number;
29
+ budgetTokens: number;
30
+ budgetBytes?: number;
31
+ safetyMarginTokens?: number;
32
+ safetyMarginBytes?: number;
33
+ depthPolicy?: ContextDepthPolicy;
34
+ }
35
+
36
+ export interface ContextCapsuleRuntimeDeps {
37
+ store: StorePort &
38
+ ContextEvidenceCompilerDeps<ContextCapsuleV1>["store"] &
39
+ ContextVerifierDeps["store"];
40
+ config: Config;
41
+ indexName?: string;
42
+ vectorIndex?: VectorIndexPort | null;
43
+ embedPort?: EmbeddingPort | null;
44
+ rerankPort?: RerankPort | null;
45
+ countTokens?: (accountingJson: string) => number;
46
+ tokenizerFingerprint?: string | null;
47
+ resolveCurrentRanks?: ContextVerifierDeps["resolveCurrentRanks"];
48
+ }
49
+
50
+ export type ContextRuntimeErrorCode =
51
+ | "invalid_goal"
52
+ | "invalid_budget"
53
+ | "invalid_filter"
54
+ | "invalid_uri"
55
+ | "retrieval_failed";
56
+
57
+ export class ContextRuntimeError extends Error {
58
+ readonly code: ContextRuntimeErrorCode;
59
+
60
+ constructor(code: ContextRuntimeErrorCode, message: string, cause?: unknown) {
61
+ super(message, cause === undefined ? undefined : { cause });
62
+ this.name = "ContextRuntimeError";
63
+ this.code = code;
64
+ }
65
+ }
@@ -0,0 +1,170 @@
1
+ /** Shared application boundary for Context Capsule build and verification. */
2
+
3
+ import type {
4
+ ContextCapsuleV1,
5
+ ContextCapsuleVerification,
6
+ } from "../core/context-capsule";
7
+ import type {
8
+ ContextCapsuleBuildInput,
9
+ ContextCapsuleRuntimeDeps,
10
+ } from "./context-runtime-types";
11
+
12
+ import {
13
+ canonicalContextCapsuleJson,
14
+ ContextCapsuleContractError,
15
+ } from "../core/context-capsule";
16
+ import { compileContextEvidence } from "../core/context-evidence";
17
+ import {
18
+ canonicalContextCapsuleVerificationJson,
19
+ parseCanonicalContextCapsuleForVerification,
20
+ verifyContextCapsule,
21
+ } from "../core/context-verifier";
22
+ import { searchHybrid } from "../pipeline/hybrid";
23
+ import {
24
+ currentContextFingerprints,
25
+ projectContextCapsule,
26
+ } from "./context-runtime-contract";
27
+ import { normalizeContextBuildInput } from "./context-runtime-input";
28
+ import { ContextRuntimeError } from "./context-runtime-types";
29
+ import { canonicalizeIndexName } from "./index-name";
30
+
31
+ export type {
32
+ ContextCapsuleBuildInput,
33
+ ContextCapsuleRuntimeDeps,
34
+ ContextDepthPolicy,
35
+ ContextRuntimeErrorCode,
36
+ } from "./context-runtime-types";
37
+ export { ContextRuntimeError } from "./context-runtime-types";
38
+
39
+ /** Build one strict Capsule through the shared compiler composition. */
40
+ export const buildContextCapsule = async (
41
+ input: ContextCapsuleBuildInput,
42
+ deps: ContextCapsuleRuntimeDeps
43
+ ): Promise<ContextCapsuleV1> => {
44
+ const now = new Date();
45
+ const normalized = normalizeContextBuildInput(
46
+ input,
47
+ deps.indexName,
48
+ now,
49
+ deps.config.collections.map((collection) => collection.name)
50
+ );
51
+ const noRerank = normalized.depthPolicy === "fast";
52
+ const plan = await compileContextEvidence<ContextCapsuleV1>(
53
+ {
54
+ goal: normalized.goal,
55
+ query: normalized.query,
56
+ indexName: normalized.indexName,
57
+ collections: normalized.collections,
58
+ uriPrefix: normalized.uriPrefix,
59
+ queryModes: normalized.queryModes,
60
+ tagsAll: normalized.tagsAll,
61
+ tagsAny: normalized.tagsAny,
62
+ categories: normalized.categories,
63
+ author: normalized.author ?? undefined,
64
+ lang: normalized.lang ?? undefined,
65
+ since: normalized.since,
66
+ until: normalized.until,
67
+ graph: normalized.graph,
68
+ limit: normalized.limit,
69
+ candidateLimit: normalized.candidateLimit,
70
+ temporalNow: now,
71
+ limits: {
72
+ requestedBytes: normalized.budgetBytes,
73
+ requestedTokens: normalized.budgetTokens,
74
+ safetyMarginBytes: normalized.safetyMarginBytes,
75
+ safetyMarginTokens: normalized.safetyMarginTokens,
76
+ },
77
+ },
78
+ {
79
+ store: deps.store,
80
+ retrieve: async (request) => {
81
+ const requestNoRerank = noRerank || request.noRerank === true;
82
+ const result = await searchHybrid(
83
+ {
84
+ store: deps.store,
85
+ config: deps.config,
86
+ vectorIndex: deps.vectorIndex ?? null,
87
+ embedPort: deps.embedPort ?? null,
88
+ expandPort: null,
89
+ rerankPort: requestNoRerank ? null : (deps.rerankPort ?? null),
90
+ },
91
+ request.query,
92
+ { ...request, noRerank: requestNoRerank }
93
+ );
94
+ if (!result.ok) {
95
+ throw new ContextRuntimeError(
96
+ "retrieval_failed",
97
+ result.error.message,
98
+ result.error.cause
99
+ );
100
+ }
101
+ return result.value;
102
+ },
103
+ projectCanonical: (draft, snapshots) =>
104
+ projectContextCapsule(draft, snapshots, normalized, deps),
105
+ }
106
+ );
107
+ if (!plan.projection) {
108
+ const budgetExhausted = plan.omissions.some(
109
+ (item) => item.reason === "global_budget"
110
+ );
111
+ throw new ContextCapsuleContractError(
112
+ budgetExhausted ? "invalid_budget" : "no_evidence",
113
+ budgetExhausted
114
+ ? "No evidence fit the requested Context Capsule budget"
115
+ : "No in-scope evidence was available for the Context Capsule"
116
+ );
117
+ }
118
+ return plan.projection.value;
119
+ };
120
+
121
+ /** Verify one Capsule through the same runtime fingerprint boundary. */
122
+ export const verifyContextCapsuleRuntime = async (
123
+ input: unknown,
124
+ deps: ContextCapsuleRuntimeDeps
125
+ ): Promise<ContextCapsuleVerification> => {
126
+ // Parse before any store access. verifyContextCapsule repeats this guard to
127
+ // retain its standalone fail-closed contract.
128
+ const capsule = parseCanonicalContextCapsuleForVerification(input, {
129
+ countTokens: deps.countTokens,
130
+ tokenizerFingerprint: deps.tokenizerFingerprint,
131
+ });
132
+ if (
133
+ deps.indexName !== undefined &&
134
+ canonicalizeIndexName(deps.indexName) !== capsule.scope.indexName
135
+ ) {
136
+ throw new ContextRuntimeError(
137
+ "invalid_filter",
138
+ `Context Capsule index ${capsule.scope.indexName} does not match runtime index ${deps.indexName}`
139
+ );
140
+ }
141
+ return verifyContextCapsule(input, {
142
+ store: deps.store,
143
+ currentFingerprints: currentContextFingerprints(capsule, deps),
144
+ resolveCurrentRanks: deps.resolveCurrentRanks,
145
+ countTokens: deps.countTokens,
146
+ tokenizerFingerprint: deps.tokenizerFingerprint,
147
+ });
148
+ };
149
+
150
+ export const canonicalBuiltContextCapsuleJson = (
151
+ capsule: ContextCapsuleV1
152
+ ): string => canonicalContextCapsuleJson(capsule);
153
+
154
+ export const canonicalVerifiedContextCapsuleJson = (
155
+ receipt: ContextCapsuleVerification
156
+ ): string => canonicalContextCapsuleVerificationJson(receipt);
157
+
158
+ /** Pure validation used by CLI before opening the selected store. */
159
+ export const validateContextCapsuleBuildInput = (
160
+ input: ContextCapsuleBuildInput,
161
+ defaultIndexName?: string,
162
+ configuredCollectionNames?: readonly string[]
163
+ ): void => {
164
+ normalizeContextBuildInput(
165
+ input,
166
+ defaultIndexName,
167
+ new Date(),
168
+ configuredCollectionNames
169
+ );
170
+ };