@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
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/** Validation, projection, and fingerprint rules for Context runtime surfaces. */
|
|
2
|
+
|
|
3
|
+
import type { ContextCanonicalProjection } from "../core/context-budget";
|
|
4
|
+
import type { ContextCapsuleV1 } from "../core/context-capsule";
|
|
5
|
+
import type { ContextCapabilityState } from "../core/context-capsule-retrieval-schema";
|
|
6
|
+
import type { ContextCapsulePayloadV1 } from "../core/context-capsule-schema";
|
|
7
|
+
import type { ContextCanonicalPlanDraft } from "../core/context-compiler";
|
|
8
|
+
import type { ContextEvidenceValue } from "../core/context-evidence";
|
|
9
|
+
import type { NormalizedContextBuildInput } from "./context-runtime-input";
|
|
10
|
+
import type { ContextCapsuleRuntimeDeps } from "./context-runtime-types";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
ContextCapsuleContractError,
|
|
14
|
+
createContextCapsuleV1,
|
|
15
|
+
} from "../core/context-capsule";
|
|
16
|
+
import { sha256Text } from "../core/context-capsule-validation";
|
|
17
|
+
import {
|
|
18
|
+
fingerprintContextRows,
|
|
19
|
+
toContextCapsuleEvidence,
|
|
20
|
+
} from "../core/context-evidence";
|
|
21
|
+
import { canonicalVerifierJson } from "../core/context-verifier-canonical";
|
|
22
|
+
import { resolveModelUri } from "../llm/registry";
|
|
23
|
+
|
|
24
|
+
const fingerprint = (value: unknown): string =>
|
|
25
|
+
sha256Text(canonicalVerifierJson(value));
|
|
26
|
+
|
|
27
|
+
const configFingerprint = (deps: ContextCapsuleRuntimeDeps): string =>
|
|
28
|
+
fingerprint(deps.config);
|
|
29
|
+
|
|
30
|
+
const configuredContextFingerprint = (
|
|
31
|
+
deps: ContextCapsuleRuntimeDeps
|
|
32
|
+
): string =>
|
|
33
|
+
fingerprintContextRows(
|
|
34
|
+
(deps.config.contexts ?? []).map((context) => ({
|
|
35
|
+
...context,
|
|
36
|
+
syncedAt: "",
|
|
37
|
+
}))
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const retrievalFingerprint = (
|
|
41
|
+
capsule: Pick<
|
|
42
|
+
ContextCapsuleV1,
|
|
43
|
+
"goal" | "query" | "scope" | "retrieval" | "capabilities"
|
|
44
|
+
>,
|
|
45
|
+
contextFingerprint: string
|
|
46
|
+
): string =>
|
|
47
|
+
fingerprint({
|
|
48
|
+
capabilities: capsule.capabilities,
|
|
49
|
+
contextFingerprint,
|
|
50
|
+
goal: capsule.goal,
|
|
51
|
+
query: capsule.query,
|
|
52
|
+
retrieval: capsule.retrieval,
|
|
53
|
+
scope: capsule.scope,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const capabilityState = (
|
|
57
|
+
requested: boolean,
|
|
58
|
+
used: boolean,
|
|
59
|
+
unavailableReasons: string[],
|
|
60
|
+
usedReasons: string[] = []
|
|
61
|
+
): ContextCapabilityState => ({
|
|
62
|
+
requested,
|
|
63
|
+
attempted: requested,
|
|
64
|
+
outcome: used ? "used" : requested ? "unavailable" : "not_requested",
|
|
65
|
+
fallbackReasons: requested ? (used ? usedReasons : unavailableReasons) : [],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const capsuleCapabilityStates = (
|
|
69
|
+
draft: ContextCanonicalPlanDraft<ContextEvidenceValue>,
|
|
70
|
+
input: NormalizedContextBuildInput
|
|
71
|
+
) => ({
|
|
72
|
+
semanticSearch: capabilityState(
|
|
73
|
+
input.depthPolicy !== "fast",
|
|
74
|
+
draft.retrieval.semanticSearch,
|
|
75
|
+
["embedding_unavailable"]
|
|
76
|
+
),
|
|
77
|
+
reranking: capabilityState(
|
|
78
|
+
input.depthPolicy !== "fast",
|
|
79
|
+
draft.retrieval.reranked,
|
|
80
|
+
["reranking_unavailable"]
|
|
81
|
+
),
|
|
82
|
+
graphExpansion: capabilityState(
|
|
83
|
+
input.graph,
|
|
84
|
+
draft.retrieval.graphExpansion,
|
|
85
|
+
draft.retrieval.graphFallbackReasons.length > 0
|
|
86
|
+
? draft.retrieval.graphFallbackReasons
|
|
87
|
+
: ["graph_unavailable"],
|
|
88
|
+
draft.retrieval.graphFallbackReasons
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const capsuleCapabilities = (
|
|
93
|
+
states: ReturnType<typeof capsuleCapabilityStates>,
|
|
94
|
+
exactTokens: boolean,
|
|
95
|
+
configuredContext: boolean
|
|
96
|
+
) => ({
|
|
97
|
+
lexicalSearch: true as const,
|
|
98
|
+
semanticSearch: states.semanticSearch.outcome === "used",
|
|
99
|
+
reranking: states.reranking.outcome === "used",
|
|
100
|
+
graphExpansion: states.graphExpansion.outcome === "used",
|
|
101
|
+
exactTokenCount: exactTokens,
|
|
102
|
+
configuredContext,
|
|
103
|
+
egressPolicy: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const capsuleFallbacks = (
|
|
107
|
+
capabilities: ReturnType<typeof capsuleCapabilities>,
|
|
108
|
+
states: ReturnType<typeof capsuleCapabilityStates>
|
|
109
|
+
): ContextCapsulePayloadV1["fallbacks"] => [
|
|
110
|
+
...(states.semanticSearch.outcome === "unavailable"
|
|
111
|
+
? [
|
|
112
|
+
{
|
|
113
|
+
code: "embedding_unavailable" as const,
|
|
114
|
+
capability: "semantic_search" as const,
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
118
|
+
...(states.reranking.outcome === "unavailable"
|
|
119
|
+
? [
|
|
120
|
+
{
|
|
121
|
+
code: "reranking_unavailable" as const,
|
|
122
|
+
capability: "reranking" as const,
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
: []),
|
|
126
|
+
...(states.graphExpansion.outcome === "unavailable"
|
|
127
|
+
? [
|
|
128
|
+
{
|
|
129
|
+
code: "graph_unavailable" as const,
|
|
130
|
+
capability: "graph_expansion" as const,
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
: []),
|
|
134
|
+
...(capabilities.exactTokenCount
|
|
135
|
+
? []
|
|
136
|
+
: [
|
|
137
|
+
{
|
|
138
|
+
code: "tokenizer_unavailable" as const,
|
|
139
|
+
capability: "token_count" as const,
|
|
140
|
+
},
|
|
141
|
+
]),
|
|
142
|
+
{ code: "egress_policy_unavailable", capability: "egress_policy" },
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
export const projectContextCapsule = (
|
|
146
|
+
draft: ContextCanonicalPlanDraft<ContextEvidenceValue>,
|
|
147
|
+
snapshots: { indexFingerprint: string; contextFingerprint: string },
|
|
148
|
+
input: NormalizedContextBuildInput,
|
|
149
|
+
deps: ContextCapsuleRuntimeDeps
|
|
150
|
+
): ContextCanonicalProjection<ContextCapsuleV1> | null => {
|
|
151
|
+
if (draft.selection.selected.length === 0) return null;
|
|
152
|
+
const evidence = draft.selection.selected.map((candidate, index) =>
|
|
153
|
+
toContextCapsuleEvidence(candidate, index + 1)
|
|
154
|
+
);
|
|
155
|
+
const evidenceIdsByFacet = new Map<string, string[]>();
|
|
156
|
+
for (const item of evidence) {
|
|
157
|
+
for (const facet of item.facets) {
|
|
158
|
+
const ids = evidenceIdsByFacet.get(facet) ?? [];
|
|
159
|
+
ids.push(item.evidenceId);
|
|
160
|
+
evidenceIdsByFacet.set(facet, ids);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const exactTokens =
|
|
164
|
+
deps.countTokens !== undefined && deps.tokenizerFingerprint != null;
|
|
165
|
+
const capabilityStates = capsuleCapabilityStates(draft, input);
|
|
166
|
+
const capabilities = capsuleCapabilities(
|
|
167
|
+
capabilityStates,
|
|
168
|
+
exactTokens,
|
|
169
|
+
draft.configuredContexts.length > 0
|
|
170
|
+
);
|
|
171
|
+
const base = {
|
|
172
|
+
schemaVersion: "1.0" as const,
|
|
173
|
+
coordinateSpace: "canonical_mirror" as const,
|
|
174
|
+
goal: draft.goal,
|
|
175
|
+
query: draft.query,
|
|
176
|
+
scope: {
|
|
177
|
+
indexName: draft.indexName,
|
|
178
|
+
collections: draft.collections,
|
|
179
|
+
uriPrefix: draft.uriPrefix,
|
|
180
|
+
tagsAll: input.tagsAll,
|
|
181
|
+
tagsAny: input.tagsAny,
|
|
182
|
+
categories: input.categories,
|
|
183
|
+
since: input.since ?? null,
|
|
184
|
+
until: input.until ?? null,
|
|
185
|
+
},
|
|
186
|
+
retrieval: {
|
|
187
|
+
depthPolicy: input.depthPolicy,
|
|
188
|
+
facets: draft.retrieval.facets,
|
|
189
|
+
queryVariants: draft.retrieval.queryVariants,
|
|
190
|
+
expansionPolicy: "deterministic_only" as const,
|
|
191
|
+
request: {
|
|
192
|
+
author: input.author,
|
|
193
|
+
lang: input.lang,
|
|
194
|
+
queryModes: input.queryModes,
|
|
195
|
+
limit: input.limit,
|
|
196
|
+
candidateLimit: input.candidateLimit,
|
|
197
|
+
graphRequested: input.graph,
|
|
198
|
+
},
|
|
199
|
+
capabilityStates,
|
|
200
|
+
indexSnapshot: {
|
|
201
|
+
before: snapshots.indexFingerprint,
|
|
202
|
+
after: snapshots.indexFingerprint,
|
|
203
|
+
stable: true as const,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
capabilities,
|
|
207
|
+
};
|
|
208
|
+
const payload: ContextCapsulePayloadV1 = {
|
|
209
|
+
...base,
|
|
210
|
+
budget: {
|
|
211
|
+
authority: "canonical_json",
|
|
212
|
+
requestedTokens: input.budgetTokens,
|
|
213
|
+
requestedBytes: input.budgetBytes,
|
|
214
|
+
safetyMarginTokens: input.safetyMarginTokens,
|
|
215
|
+
safetyMarginBytes: input.safetyMarginBytes,
|
|
216
|
+
usedTokens: 1,
|
|
217
|
+
usedBytes: 0,
|
|
218
|
+
estimator: exactTokens ? "active_tokenizer" : "unicode_conservative",
|
|
219
|
+
tokenizerFingerprint: exactTokens
|
|
220
|
+
? (deps.tokenizerFingerprint ?? null)
|
|
221
|
+
: null,
|
|
222
|
+
},
|
|
223
|
+
fingerprints: {
|
|
224
|
+
config: configFingerprint(deps),
|
|
225
|
+
retrieval: retrievalFingerprint(base, snapshots.contextFingerprint),
|
|
226
|
+
embeddingModel: capabilities.semanticSearch
|
|
227
|
+
? sha256Text(deps.embedPort?.modelUri ?? "")
|
|
228
|
+
: null,
|
|
229
|
+
rerankModel: capabilities.reranking
|
|
230
|
+
? sha256Text(deps.rerankPort?.modelUri ?? "")
|
|
231
|
+
: null,
|
|
232
|
+
tokenizer: exactTokens ? (deps.tokenizerFingerprint ?? null) : null,
|
|
233
|
+
},
|
|
234
|
+
fallbacks: capsuleFallbacks(capabilities, capabilityStates),
|
|
235
|
+
guidance: {
|
|
236
|
+
extractiveOnly: true,
|
|
237
|
+
evidenceTrust: "untrusted_data",
|
|
238
|
+
instructionBoundary: "hard_delimited",
|
|
239
|
+
configuredContexts: draft.configuredContexts,
|
|
240
|
+
},
|
|
241
|
+
evidence,
|
|
242
|
+
coverage: {
|
|
243
|
+
complete: draft.selection.coverage.unresolvedFacets.length === 0,
|
|
244
|
+
requestedFacets: draft.retrieval.facets,
|
|
245
|
+
coveredFacets: draft.selection.coverage.coveredFacets.map((facet) => ({
|
|
246
|
+
facet,
|
|
247
|
+
evidenceIds: evidenceIdsByFacet.get(facet) ?? [],
|
|
248
|
+
})),
|
|
249
|
+
unresolvedFacets: draft.selection.coverage.unresolvedFacets,
|
|
250
|
+
gaps: draft.selection.coverage.gaps,
|
|
251
|
+
},
|
|
252
|
+
omissions: {
|
|
253
|
+
total: draft.selection.omissions.length,
|
|
254
|
+
items: draft.selection.omissions.slice(0, 20),
|
|
255
|
+
reasonCounts: draft.selection.reasonCounts,
|
|
256
|
+
truncated: draft.selection.omissions.length > 20,
|
|
257
|
+
},
|
|
258
|
+
truncated: draft.selection.omissions.some(
|
|
259
|
+
(item) => item.reason === "global_budget"
|
|
260
|
+
),
|
|
261
|
+
warnings: [
|
|
262
|
+
...(draft.selection.coverage.unresolvedFacets.length > 0
|
|
263
|
+
? [{ code: "incomplete_coverage" as const }]
|
|
264
|
+
: []),
|
|
265
|
+
...(draft.selection.omissions.length > 20
|
|
266
|
+
? [{ code: "omissions_truncated" as const }]
|
|
267
|
+
: []),
|
|
268
|
+
...(exactTokens ? [] : [{ code: "token_estimate_used" as const }]),
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
try {
|
|
272
|
+
const value = createContextCapsuleV1(payload, {
|
|
273
|
+
countTokens: deps.countTokens,
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
value,
|
|
277
|
+
usedBytes: value.budget.usedBytes,
|
|
278
|
+
usedTokens: value.budget.usedTokens,
|
|
279
|
+
};
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (
|
|
282
|
+
error instanceof ContextCapsuleContractError &&
|
|
283
|
+
error.code === "invalid_budget"
|
|
284
|
+
) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const currentContextFingerprints = (
|
|
292
|
+
capsule: ContextCapsuleV1,
|
|
293
|
+
deps: ContextCapsuleRuntimeDeps
|
|
294
|
+
) => ({
|
|
295
|
+
config: configFingerprint(deps),
|
|
296
|
+
retrieval: retrievalFingerprint(capsule, configuredContextFingerprint(deps)),
|
|
297
|
+
embeddingModel: capsule.capabilities.semanticSearch
|
|
298
|
+
? sha256Text(
|
|
299
|
+
resolveModelUri(
|
|
300
|
+
deps.config,
|
|
301
|
+
"embed",
|
|
302
|
+
undefined,
|
|
303
|
+
capsule.scope.collections.length === 1
|
|
304
|
+
? capsule.scope.collections[0]
|
|
305
|
+
: undefined
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
: null,
|
|
309
|
+
rerankModel: capsule.capabilities.reranking
|
|
310
|
+
? sha256Text(
|
|
311
|
+
resolveModelUri(
|
|
312
|
+
deps.config,
|
|
313
|
+
"rerank",
|
|
314
|
+
undefined,
|
|
315
|
+
capsule.scope.collections.length === 1
|
|
316
|
+
? capsule.scope.collections[0]
|
|
317
|
+
: undefined
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
: null,
|
|
321
|
+
tokenizer:
|
|
322
|
+
capsule.capabilities.exactTokenCount && deps.tokenizerFingerprint
|
|
323
|
+
? deps.tokenizerFingerprint
|
|
324
|
+
: null,
|
|
325
|
+
});
|
|
@@ -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
|
+
>;
|