@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.
- package/README.md +21 -17
- package/assets/skill/SKILL.md +8 -5
- package/package.json +2 -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,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
|
+
};
|