@gmickel/gno 1.15.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.
Files changed (46) hide show
  1. package/README.md +21 -17
  2. package/assets/skill/SKILL.md +8 -5
  3. package/package.json +2 -1
  4. package/src/app/context-agent-projection.ts +303 -0
  5. package/src/app/context-format.ts +249 -0
  6. package/src/app/context-runtime-contract.ts +325 -0
  7. package/src/app/context-runtime-input.ts +362 -0
  8. package/src/app/context-runtime-types.ts +65 -0
  9. package/src/app/context-runtime.ts +170 -0
  10. package/src/app/context-surface.ts +145 -0
  11. package/src/cli/commands/context-build.ts +149 -0
  12. package/src/cli/commands/context-verify.ts +90 -0
  13. package/src/cli/options.ts +4 -0
  14. package/src/cli/program.ts +178 -0
  15. package/src/core/context-budget.ts +461 -0
  16. package/src/core/context-capsule-index-schema.ts +15 -0
  17. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  18. package/src/core/context-capsule-schema.ts +473 -0
  19. package/src/core/context-capsule-validation.ts +416 -0
  20. package/src/core/context-capsule-verification.ts +218 -0
  21. package/src/core/context-capsule.ts +439 -0
  22. package/src/core/context-compiler.ts +513 -0
  23. package/src/core/context-evidence-metadata.ts +33 -0
  24. package/src/core/context-evidence.ts +495 -0
  25. package/src/core/context-facets.ts +163 -0
  26. package/src/core/context-guidance.ts +69 -0
  27. package/src/core/context-scope.ts +32 -0
  28. package/src/core/context-verifier-canonical.ts +90 -0
  29. package/src/core/context-verifier-input.ts +66 -0
  30. package/src/core/context-verifier.ts +447 -0
  31. package/src/core/sections.ts +63 -0
  32. package/src/mcp/server.ts +10 -4
  33. package/src/mcp/tools/context.ts +229 -0
  34. package/src/mcp/tools/index.ts +27 -0
  35. package/src/pipeline/chunk-lookup.ts +33 -0
  36. package/src/pipeline/hybrid.ts +79 -57
  37. package/src/pipeline/types.ts +14 -0
  38. package/src/sdk/client.ts +68 -6
  39. package/src/sdk/index.ts +21 -0
  40. package/src/sdk/types.ts +24 -0
  41. package/src/serve/background-runtime.ts +1 -0
  42. package/src/serve/context-capsule.ts +136 -0
  43. package/src/serve/context.ts +10 -1
  44. package/src/serve/routes/api.ts +2 -0
  45. package/src/serve/server.ts +23 -0
  46. package/src/store/sqlite/adapter.ts +38 -20
@@ -0,0 +1,495 @@
1
+ /** Strict indexed-evidence loading for Context Capsule compilation. */
2
+
3
+ import type { SearchResults } from "../pipeline/types";
4
+ import type {
5
+ ActivationIndexSnapshot,
6
+ ContextRow,
7
+ DocumentRow,
8
+ StorePort,
9
+ StoreResult,
10
+ } from "../store/types";
11
+ import type {
12
+ ContextCanonicalProjection,
13
+ MaterializedContextCandidate,
14
+ } from "./context-budget";
15
+ import type { ContextCapsulePayloadV1 } from "./context-capsule-schema";
16
+ import type {
17
+ ContextCanonicalPlanDraft,
18
+ ContextCompilerInput,
19
+ ContextEvidencePlan,
20
+ ContextMaterialization,
21
+ ContextRetrievalCandidate,
22
+ ContextRetrievalRequest,
23
+ } from "./context-compiler";
24
+
25
+ import { decorateUriForIndex, deriveDocid, parseUri } from "../app/constants";
26
+ import { canonicalizeIndexName } from "../app/index-name";
27
+ import {
28
+ chunkMatchesCanonicalContent,
29
+ createChunkLookup,
30
+ } from "../pipeline/chunk-lookup";
31
+ import { SEARCH_RESULT_PLANNER_METADATA } from "../pipeline/types";
32
+ import {
33
+ contextCapsuleEvidenceIdentity,
34
+ sha256Text,
35
+ } from "./context-capsule-validation";
36
+ import { planContextEvidence } from "./context-compiler";
37
+ import { projectContextEvidenceMetadata } from "./context-evidence-metadata";
38
+ import {
39
+ extractInclusiveLines,
40
+ extractSections,
41
+ headingForLine,
42
+ } from "./sections";
43
+
44
+ type ContextEvidenceStore = Pick<
45
+ StorePort,
46
+ | "getActivationIndexSnapshot"
47
+ | "getChunksBatch"
48
+ | "getCollections"
49
+ | "getContexts"
50
+ | "getDocumentsByDocids"
51
+ > &
52
+ Required<Pick<StorePort, "getContentBatch">>;
53
+
54
+ export type ContextEvidenceErrorCode =
55
+ | "chunk_coordinate_mismatch"
56
+ | "chunk_load_failed"
57
+ | "collection_load_failed"
58
+ | "content_load_failed"
59
+ | "context_changed_during_compile"
60
+ | "context_load_failed"
61
+ | "document_load_failed"
62
+ | "index_changed_during_compile"
63
+ | "index_snapshot_failed"
64
+ | "index_snapshot_mismatch"
65
+ | "stored_provenance_mismatch";
66
+
67
+ export class ContextEvidenceError extends Error {
68
+ readonly code: ContextEvidenceErrorCode;
69
+
70
+ constructor(
71
+ code: ContextEvidenceErrorCode,
72
+ message: string,
73
+ cause?: unknown
74
+ ) {
75
+ super(message, cause === undefined ? undefined : { cause });
76
+ this.name = "ContextEvidenceError";
77
+ this.code = code;
78
+ }
79
+ }
80
+
81
+ export interface ContextEvidenceSnapshot {
82
+ collections: string[];
83
+ contexts: ContextRow[];
84
+ documents: ContextEvidenceSnapshotDocument[];
85
+ contextFingerprint: string;
86
+ indexFingerprint: string;
87
+ }
88
+
89
+ /** Content-free active document identity captured with an evidence snapshot. */
90
+ export interface ContextEvidenceSnapshotDocument {
91
+ collection: string;
92
+ uri: string;
93
+ sourceHash: string;
94
+ mirrorHash: string | null;
95
+ }
96
+
97
+ export interface ContextEvidenceValue {
98
+ collection: string;
99
+ title: string | null;
100
+ heading: string | null;
101
+ modifiedAt: string | null;
102
+ documentDate: string | null;
103
+ observedAt: null;
104
+ contextIds: string[];
105
+ trust: "untrusted";
106
+ egress: "unavailable";
107
+ }
108
+
109
+ export type ContextEvidenceCompilerInput = Omit<
110
+ ContextCompilerInput,
111
+ "contextSnapshot" | "observedAt"
112
+ >;
113
+
114
+ export interface ContextEvidenceProjectionContext {
115
+ contextFingerprint: string;
116
+ indexFingerprint: string;
117
+ }
118
+
119
+ export interface ContextEvidenceCompilerDeps<P> {
120
+ store: ContextEvidenceStore;
121
+ retrieve: (request: ContextRetrievalRequest) => Promise<SearchResults>;
122
+ projectCanonical: (
123
+ draft: ContextCanonicalPlanDraft<ContextEvidenceValue>,
124
+ fingerprints: ContextEvidenceProjectionContext
125
+ ) => ContextCanonicalProjection<P> | null;
126
+ }
127
+
128
+ export interface CompiledContextEvidence<P> extends ContextEvidencePlan<
129
+ ContextEvidenceValue,
130
+ P
131
+ > {
132
+ snapshot: ContextEvidenceProjectionContext;
133
+ }
134
+
135
+ const compareCodeUnits = (left: string, right: string): number => {
136
+ if (left < right) return -1;
137
+ if (left > right) return 1;
138
+ return 0;
139
+ };
140
+
141
+ const hashJson = (value: unknown): string => sha256Text(JSON.stringify(value));
142
+
143
+ const unwrapStore = <T>(
144
+ result: StoreResult<T>,
145
+ code: ContextEvidenceErrorCode,
146
+ operation: string
147
+ ): T => {
148
+ if (result.ok) return result.value;
149
+ throw new ContextEvidenceError(
150
+ code,
151
+ `${operation}: ${result.error.message}`,
152
+ result.error.cause
153
+ );
154
+ };
155
+
156
+ const canonicalContexts = (contexts: ContextRow[]) =>
157
+ contexts
158
+ .map(({ scopeType, scopeKey, text }) => ({ scopeType, scopeKey, text }))
159
+ .sort(
160
+ (left, right) =>
161
+ compareCodeUnits(left.scopeType, right.scopeType) ||
162
+ compareCodeUnits(left.scopeKey, right.scopeKey) ||
163
+ compareCodeUnits(left.text, right.text)
164
+ );
165
+
166
+ export const fingerprintContextRows = (contexts: ContextRow[]): string =>
167
+ hashJson(canonicalContexts(contexts));
168
+
169
+ const canonicalIndexSnapshot = (
170
+ collection: string,
171
+ snapshot: ActivationIndexSnapshot
172
+ ) => ({
173
+ collection,
174
+ identity: {
175
+ indexName: snapshot.identity.indexName,
176
+ schemaVersion: snapshot.identity.schemaVersion,
177
+ ftsTokenizer: snapshot.identity.ftsTokenizer,
178
+ ftsStateHash: snapshot.identity.ftsStateHash,
179
+ activeDocumentCount: snapshot.identity.activeDocumentCount,
180
+ ftsSynchronized: snapshot.identity.ftsSynchronized,
181
+ },
182
+ documents: [...snapshot.documents]
183
+ .map(({ uri, sourceHash, mirrorHash, active }) => ({
184
+ uri,
185
+ sourceHash,
186
+ mirrorHash,
187
+ active,
188
+ }))
189
+ .sort(
190
+ (left, right) =>
191
+ compareCodeUnits(left.uri, right.uri) ||
192
+ compareCodeUnits(left.sourceHash, right.sourceHash) ||
193
+ compareCodeUnits(left.mirrorHash ?? "", right.mirrorHash ?? "")
194
+ ),
195
+ });
196
+
197
+ /** Capture one strict, content-free index/context snapshot before or after work. */
198
+ export const captureContextEvidenceSnapshot = async (
199
+ store: ContextEvidenceStore,
200
+ indexNameInput: string,
201
+ requestedCollections: string[]
202
+ ): Promise<ContextEvidenceSnapshot> => {
203
+ const contexts = unwrapStore(
204
+ await store.getContexts(),
205
+ "context_load_failed",
206
+ "Failed to load configured contexts"
207
+ ).map((row) => ({ ...row }));
208
+ const collections =
209
+ requestedCollections.length > 0
210
+ ? [...new Set(requestedCollections)].sort(compareCodeUnits)
211
+ : [
212
+ ...new Set(
213
+ unwrapStore(
214
+ await store.getCollections(),
215
+ "collection_load_failed",
216
+ "Failed to load indexed collections"
217
+ ).map((row) => row.name)
218
+ ),
219
+ ].sort(compareCodeUnits);
220
+ const indexName = canonicalizeIndexName(indexNameInput);
221
+ const indexSnapshots: ReturnType<typeof canonicalIndexSnapshot>[] = [];
222
+ const documents: ContextEvidenceSnapshotDocument[] = [];
223
+ for (const collection of collections) {
224
+ const snapshot = unwrapStore(
225
+ await store.getActivationIndexSnapshot(collection),
226
+ "index_snapshot_failed",
227
+ `Failed to load index snapshot for ${collection}`
228
+ );
229
+ if (canonicalizeIndexName(snapshot.identity.indexName) !== indexName) {
230
+ throw new ContextEvidenceError(
231
+ "index_snapshot_mismatch",
232
+ `Index snapshot for ${collection} belongs to ${snapshot.identity.indexName}, not ${indexName}`
233
+ );
234
+ }
235
+ indexSnapshots.push(canonicalIndexSnapshot(collection, snapshot));
236
+ for (const document of snapshot.documents) {
237
+ if (!document.active) continue;
238
+ documents.push({
239
+ collection,
240
+ uri: decorateUriForIndex(document.uri, indexName),
241
+ sourceHash: document.sourceHash,
242
+ mirrorHash: document.mirrorHash,
243
+ });
244
+ }
245
+ }
246
+ return {
247
+ collections,
248
+ contexts,
249
+ documents: documents.sort(
250
+ (left, right) =>
251
+ compareCodeUnits(left.uri, right.uri) ||
252
+ compareCodeUnits(left.sourceHash, right.sourceHash) ||
253
+ compareCodeUnits(left.mirrorHash ?? "", right.mirrorHash ?? "")
254
+ ),
255
+ contextFingerprint: fingerprintContextRows(contexts),
256
+ indexFingerprint: hashJson(indexSnapshots),
257
+ };
258
+ };
259
+
260
+ const referenceDocument = (
261
+ documents: DocumentRow[],
262
+ candidate: ContextRetrievalCandidate,
263
+ indexName: string
264
+ ): DocumentRow => {
265
+ const result = candidate.result;
266
+ const matches = documents.filter(
267
+ (document) =>
268
+ document.docid === result.docid &&
269
+ decorateUriForIndex(document.uri, indexName) === result.uri
270
+ );
271
+ if (matches.length !== 1) {
272
+ throw new ContextEvidenceError(
273
+ "stored_provenance_mismatch",
274
+ `Expected one active indexed document for ${result.uri}; found ${matches.length}`
275
+ );
276
+ }
277
+ const document = matches[0];
278
+ const parsedUri = parseUri(result.uri);
279
+ const sourceHash = result.source.sourceHash;
280
+ const mirrorHash = result.conversion?.mirrorHash;
281
+ if (
282
+ !document ||
283
+ !document.active ||
284
+ !document.mirrorHash ||
285
+ parsedUri?.collection !== document.collection ||
286
+ result.source.relPath !== document.relPath ||
287
+ sourceHash !== document.sourceHash ||
288
+ mirrorHash !== document.mirrorHash ||
289
+ deriveDocid(document.sourceHash) !== document.docid
290
+ ) {
291
+ throw new ContextEvidenceError(
292
+ "stored_provenance_mismatch",
293
+ `Stored source identity drifted for ${result.uri}`
294
+ );
295
+ }
296
+ return document;
297
+ };
298
+
299
+ /** Materialize all planned candidates with exactly one batch per store layer. */
300
+ export const materializeContextEvidenceCandidates = async (
301
+ store: ContextEvidenceStore,
302
+ candidates: ContextRetrievalCandidate[],
303
+ indexNameInput: string
304
+ ): Promise<ContextMaterialization<ContextEvidenceValue>[]> => {
305
+ if (candidates.length === 0) return [];
306
+ const indexName = canonicalizeIndexName(indexNameInput);
307
+ const docids = [...new Set(candidates.map(({ result }) => result.docid))];
308
+ const documents = unwrapStore(
309
+ await store.getDocumentsByDocids(docids, { activeOnly: true }),
310
+ "document_load_failed",
311
+ "Failed to batch-load Context evidence documents"
312
+ );
313
+ const alignedDocuments = candidates.map((candidate) =>
314
+ referenceDocument(documents, candidate, indexName)
315
+ );
316
+ const mirrorHashes = [
317
+ ...new Set(
318
+ alignedDocuments.map((document) => document.mirrorHash as string)
319
+ ),
320
+ ];
321
+ const [contentResult, chunksResult] = await Promise.all([
322
+ store.getContentBatch(mirrorHashes),
323
+ store.getChunksBatch(mirrorHashes),
324
+ ]);
325
+ const contentByHash = unwrapStore(
326
+ contentResult,
327
+ "content_load_failed",
328
+ "Failed to batch-load Context evidence mirrors"
329
+ );
330
+ const chunksByHash = unwrapStore(
331
+ chunksResult,
332
+ "chunk_load_failed",
333
+ "Failed to batch-load Context evidence chunks"
334
+ );
335
+ const getChunk = createChunkLookup(chunksByHash);
336
+ const sectionsByHash = new Map<string, ReturnType<typeof extractSections>>();
337
+ const validatedMirrors = new Set<string>();
338
+ const validatedChunks = new Set<string>();
339
+
340
+ return candidates.map((candidate, index) => {
341
+ const result = candidate.result;
342
+ const document = alignedDocuments[index];
343
+ const metadata = result[SEARCH_RESULT_PLANNER_METADATA];
344
+ const snippetRange = result.snippetRange;
345
+ if (!document?.mirrorHash || !metadata || !snippetRange) {
346
+ throw new ContextEvidenceError(
347
+ "chunk_coordinate_mismatch",
348
+ `Hybrid result lacks exact chunk coordinates for ${result.uri}`
349
+ );
350
+ }
351
+ const content = contentByHash.get(document.mirrorHash);
352
+ const chunk = getChunk(document.mirrorHash, metadata.seq);
353
+ const chunkKey = `${document.mirrorHash}:${metadata.seq}`;
354
+ if (
355
+ !content ||
356
+ metadata.mirrorHash !== document.mirrorHash ||
357
+ !chunk ||
358
+ chunk.mirrorHash !== document.mirrorHash ||
359
+ snippetRange.startLine !== chunk.startLine ||
360
+ snippetRange.endLine !== chunk.endLine
361
+ ) {
362
+ throw new ContextEvidenceError(
363
+ "chunk_coordinate_mismatch",
364
+ `Stored chunk identity drifted for ${result.uri}`
365
+ );
366
+ }
367
+ if (!validatedMirrors.has(document.mirrorHash)) {
368
+ if (sha256Text(content) !== document.mirrorHash) {
369
+ throw new ContextEvidenceError(
370
+ "chunk_coordinate_mismatch",
371
+ `Stored mirror bytes drifted for ${result.uri}`
372
+ );
373
+ }
374
+ validatedMirrors.add(document.mirrorHash);
375
+ }
376
+ if (!validatedChunks.has(chunkKey)) {
377
+ if (!chunkMatchesCanonicalContent(chunk, content)) {
378
+ throw new ContextEvidenceError(
379
+ "chunk_coordinate_mismatch",
380
+ `Stored chunk coordinates drifted for ${result.uri}`
381
+ );
382
+ }
383
+ validatedChunks.add(chunkKey);
384
+ }
385
+ const text = extractInclusiveLines(content, chunk.startLine, chunk.endLine);
386
+ if (!text) {
387
+ throw new ContextEvidenceError(
388
+ "chunk_coordinate_mismatch",
389
+ `Canonical line range is unavailable for ${result.uri}`
390
+ );
391
+ }
392
+ let sections = sectionsByHash.get(document.mirrorHash);
393
+ if (!sections) {
394
+ sections = extractSections(content);
395
+ sectionsByHash.set(document.mirrorHash, sections);
396
+ }
397
+ return {
398
+ ok: true,
399
+ candidate: {
400
+ uri: result.uri,
401
+ docid: document.docid,
402
+ startLine: chunk.startLine,
403
+ endLine: chunk.endLine,
404
+ text,
405
+ sourceHash: document.sourceHash,
406
+ mirrorHash: document.mirrorHash,
407
+ value: {
408
+ collection: document.collection,
409
+ title: projectContextEvidenceMetadata(document.title),
410
+ heading: projectContextEvidenceMetadata(
411
+ headingForLine(sections, chunk.startLine)
412
+ ),
413
+ modifiedAt: document.sourceMtime ?? null,
414
+ documentDate: document.frontmatterDate ?? null,
415
+ observedAt: null,
416
+ contextIds: [...candidate.contextIds],
417
+ trust: "untrusted",
418
+ egress: "unavailable",
419
+ },
420
+ },
421
+ };
422
+ });
423
+ };
424
+
425
+ /** Compile one evidence plan and discard it if either source snapshot drifted. */
426
+ export const compileContextEvidence = async <P>(
427
+ input: ContextEvidenceCompilerInput,
428
+ deps: ContextEvidenceCompilerDeps<P>
429
+ ): Promise<CompiledContextEvidence<P>> => {
430
+ const before = await captureContextEvidenceSnapshot(
431
+ deps.store,
432
+ input.indexName,
433
+ input.collections
434
+ );
435
+ const fingerprints = {
436
+ contextFingerprint: before.contextFingerprint,
437
+ indexFingerprint: before.indexFingerprint,
438
+ };
439
+ const plan = await planContextEvidence(
440
+ { ...input, contextSnapshot: before.contexts, observedAt: null },
441
+ {
442
+ retrieve: deps.retrieve,
443
+ materializeCandidates: (candidates) =>
444
+ materializeContextEvidenceCandidates(
445
+ deps.store,
446
+ candidates,
447
+ input.indexName
448
+ ),
449
+ projectCanonical: (draft) => deps.projectCanonical(draft, fingerprints),
450
+ }
451
+ );
452
+ const after = await captureContextEvidenceSnapshot(
453
+ deps.store,
454
+ input.indexName,
455
+ input.collections
456
+ );
457
+ if (before.indexFingerprint !== after.indexFingerprint) {
458
+ throw new ContextEvidenceError(
459
+ "index_changed_during_compile",
460
+ "Index changed while Context evidence was being compiled"
461
+ );
462
+ }
463
+ if (before.contextFingerprint !== after.contextFingerprint) {
464
+ throw new ContextEvidenceError(
465
+ "context_changed_during_compile",
466
+ "Configured contexts changed while Context evidence was being compiled"
467
+ );
468
+ }
469
+ return { ...plan, snapshot: fingerprints };
470
+ };
471
+
472
+ /** Project one selected candidate into the frozen Capsule evidence contract. */
473
+ export const toContextCapsuleEvidence = (
474
+ candidate: MaterializedContextCandidate<ContextEvidenceValue>,
475
+ selectionRank: number
476
+ ): ContextCapsulePayloadV1["evidence"][number] => {
477
+ const identity = {
478
+ uri: candidate.uri,
479
+ docid: candidate.docid,
480
+ startLine: candidate.startLine,
481
+ endLine: candidate.endLine,
482
+ sourceHash: candidate.sourceHash,
483
+ mirrorHash: candidate.mirrorHash,
484
+ passageHash: candidate.passageHash,
485
+ };
486
+ return {
487
+ evidenceId: contextCapsuleEvidenceIdentity(identity),
488
+ ...identity,
489
+ ...candidate.value,
490
+ text: candidate.text,
491
+ retrievalRank: candidate.retrievalRank,
492
+ selectionRank,
493
+ facets: [...candidate.facets],
494
+ };
495
+ };
@@ -0,0 +1,163 @@
1
+ /** Deterministic, generation-free Context Capsule facet derivation. */
2
+
3
+ import type { QueryModeInput, SearchResult } from "../pipeline/types";
4
+
5
+ import { isWithinTemporalRange } from "../pipeline/temporal";
6
+
7
+ const COMPARISON_PATTERN = /\b(?:vs\.?|versus|compared?\s+(?:to|with))\b/iu;
8
+ const COMPARISON_LEAD_PATTERN = /^\s*(?:compare|comparison\s+of)\s+/iu;
9
+ const QUOTED_PATTERN = /["“]([^"”]{2,512})["”]/gu;
10
+ const ENTITY_PATTERN = /\p{Lu}[\p{L}\p{N}._-]*(?:\s+\p{Lu}[\p{L}\p{N}._-]*)*/gu;
11
+ const TEMPORAL_SIGNALS = [
12
+ "today",
13
+ "yesterday",
14
+ "this week",
15
+ "last week",
16
+ "this month",
17
+ "last month",
18
+ "latest",
19
+ "newest",
20
+ "most recent",
21
+ "recent",
22
+ ] as const;
23
+ const TEMPORAL_PATTERN =
24
+ /\b(?:today|yesterday|this week|last week|this month|last month|latest|newest|most recent|recent)\b/giu;
25
+ const STOP_WORDS = new Set([
26
+ "a",
27
+ "an",
28
+ "and",
29
+ "are",
30
+ "as",
31
+ "at",
32
+ "be",
33
+ "by",
34
+ "compare",
35
+ "find",
36
+ "for",
37
+ "from",
38
+ "how",
39
+ "in",
40
+ "is",
41
+ "last",
42
+ "latest",
43
+ "month",
44
+ "most",
45
+ "newest",
46
+ "of",
47
+ "on",
48
+ "or",
49
+ "the",
50
+ "this",
51
+ "today",
52
+ "to",
53
+ "vs",
54
+ "what",
55
+ "week",
56
+ "when",
57
+ "where",
58
+ "which",
59
+ "who",
60
+ "with",
61
+ "yesterday",
62
+ ]);
63
+
64
+ export interface ContextDerivedFacet {
65
+ value: string;
66
+ matchText: string;
67
+ temporal: boolean;
68
+ }
69
+
70
+ const compareCodeUnits = (left: string, right: string): number => {
71
+ if (left < right) return -1;
72
+ if (left > right) return 1;
73
+ return 0;
74
+ };
75
+
76
+ export const normalizeContextText = (value: string): string =>
77
+ value.replace(/\r\n?/g, "\n").normalize("NFC").trim().replace(/\s+/g, " ");
78
+
79
+ const normalizedFacet = (value: string): string =>
80
+ normalizeContextText(value).toLocaleLowerCase("und").slice(0, 512);
81
+
82
+ const addFacet = (
83
+ facets: Map<string, ContextDerivedFacet>,
84
+ value: string,
85
+ temporal = false
86
+ ): void => {
87
+ const normalized = normalizedFacet(value);
88
+ if (!normalized || facets.has(normalized) || facets.size >= 128) return;
89
+ facets.set(normalized, {
90
+ value: normalized,
91
+ matchText: normalized,
92
+ temporal,
93
+ });
94
+ };
95
+
96
+ const comparisonOperands = (goal: string): string[] => {
97
+ const explicit = goal.split(COMPARISON_PATTERN);
98
+ if (explicit.length > 1) {
99
+ return explicit.map((operand) =>
100
+ operand
101
+ .replace(COMPARISON_LEAD_PATTERN, "")
102
+ .replace(TEMPORAL_PATTERN, "")
103
+ .trim()
104
+ );
105
+ }
106
+ if (!COMPARISON_LEAD_PATTERN.test(goal)) return [];
107
+ return goal
108
+ .replace(COMPARISON_LEAD_PATTERN, "")
109
+ .split(/\s+and\s+/iu)
110
+ .map((operand) => operand.replace(TEMPORAL_PATTERN, "").trim());
111
+ };
112
+
113
+ export const deriveContextFacetPlan = (
114
+ goal: string,
115
+ queryModes: readonly QueryModeInput[] = []
116
+ ): ContextDerivedFacet[] => {
117
+ const facets = new Map<string, ContextDerivedFacet>();
118
+ for (const match of goal.matchAll(QUOTED_PATTERN))
119
+ addFacet(facets, match[1] ?? "");
120
+ for (const mode of queryModes) addFacet(facets, mode.text);
121
+ for (const operand of comparisonOperands(goal)) addFacet(facets, operand);
122
+ for (const entity of goal.match(ENTITY_PATTERN) ?? []) {
123
+ const withoutLead = entity.replace(COMPARISON_LEAD_PATTERN, "");
124
+ if (!STOP_WORDS.has(normalizedFacet(withoutLead)))
125
+ addFacet(facets, withoutLead);
126
+ }
127
+ const lowerGoal = normalizedFacet(goal);
128
+ for (const signal of TEMPORAL_SIGNALS) {
129
+ if (lowerGoal.includes(signal)) addFacet(facets, signal, true);
130
+ }
131
+ const words = lowerGoal.match(/[\p{L}\p{N}][\p{L}\p{N}._-]*/gu) ?? [];
132
+ for (const word of words) {
133
+ if (word.length > 1 && !STOP_WORDS.has(word)) addFacet(facets, word);
134
+ }
135
+ if (facets.size === 0) addFacet(facets, goal);
136
+ return [...facets.values()].sort((left, right) =>
137
+ compareCodeUnits(left.value, right.value)
138
+ );
139
+ };
140
+
141
+ export const deriveContextFacets = (
142
+ goal: string,
143
+ queryModes: readonly QueryModeInput[] = []
144
+ ): string[] =>
145
+ deriveContextFacetPlan(goal, queryModes).map((facet) => facet.value);
146
+
147
+ export const candidateMatchesContextFacet = (
148
+ facet: ContextDerivedFacet,
149
+ result: SearchResult,
150
+ text: string,
151
+ temporalRange: { since?: string; until?: string }
152
+ ): boolean => {
153
+ if (facet.temporal) {
154
+ const timestamp = result.source.documentDate ?? result.source.modifiedAt;
155
+ return Boolean(
156
+ timestamp && isWithinTemporalRange(timestamp, temporalRange)
157
+ );
158
+ }
159
+ const haystack = normalizeContextText(
160
+ `${result.title ?? ""}\n${text}`
161
+ ).toLocaleLowerCase("und");
162
+ return haystack.includes(facet.matchText);
163
+ };