prime-agent-dsh 0.2.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.
@@ -0,0 +1,99 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ import {
4
+ AttachmentError,
5
+ admitEncodedImages,
6
+ type AttachmentStore,
7
+ type EncodedImageAttachment,
8
+ type ImageAttachmentRef,
9
+ type ImageMediaType,
10
+ } from "@deepseek-ai/dsh-attachment";
11
+ import type { ContentBlock } from "@deepseek-ai/dsh-llm";
12
+ import { LocalAttachmentStore, type Config as LocalAttachmentConfig } from "@deepseek-ai/dsh-attachment-local";
13
+
14
+ /** Inline image block used by Prime's model context. */
15
+ export interface PrimeInlineImage {
16
+ readonly data: string;
17
+ readonly mimeType: string;
18
+ readonly name?: string;
19
+ }
20
+
21
+ /** Ordered image block in a Prime user turn. */
22
+ export type PrimeTurnImage = PrimeInlineImage & { readonly type: "image" };
23
+
24
+ /** Resolved inline bytes suitable for a Prime image content block. */
25
+ export interface ResolvedPrimeImage {
26
+ readonly data: string;
27
+ readonly mimeType: ImageMediaType;
28
+ }
29
+
30
+ /** Strongly typed image boundary used by the Prime/DSH context converter. */
31
+ export interface DshImageAttachmentGateway {
32
+ admitPrimeImages(images: readonly PrimeInlineImage[]): Promise<readonly ImageAttachmentRef[]>;
33
+ resolveDshImage(attachment: ImageAttachmentRef, signal?: AbortSignal): Promise<ResolvedPrimeImage>;
34
+ }
35
+
36
+ const IMAGE_MEDIA_TYPES: readonly ImageMediaType[] = ["image/png", "image/jpeg", "image/webp", "image/gif"];
37
+
38
+ function imageMediaType(value: string): ImageMediaType {
39
+ const supported = IMAGE_MEDIA_TYPES.find((candidate) => candidate === value);
40
+ if (!supported) throw new AttachmentError(`Image type ${value} is not accepted by DSH.`, "UNSUPPORTED_IMAGE_TYPE");
41
+ return supported;
42
+ }
43
+
44
+
45
+ /** Ordered Prime user-turn content accepted by the pooled provider. */
46
+ export type PrimeTurnContent =
47
+ | { readonly type: "text"; readonly text: string }
48
+ | PrimeTurnImage;
49
+
50
+ /**
51
+ * Admit every inline image through DSH's authoritative batch gate, then build
52
+ * immutable-reference content blocks without changing the caller's ordering.
53
+ */
54
+ export async function admitPrimeTurnContent(
55
+ attachments: AttachmentStore,
56
+ content: readonly PrimeTurnContent[],
57
+ ): Promise<ContentBlock[]> {
58
+ const images = content.filter((block): block is PrimeTurnImage => block.type === "image");
59
+ const admitted = await admitEncodedImages(attachments, images.map((image) => ({
60
+ data: image.data,
61
+ mediaType: imageMediaType(image.mimeType),
62
+ ...(image.name === undefined ? {} : { name: image.name }),
63
+ })));
64
+ let imageIndex = 0;
65
+ return content.map((block): ContentBlock => block.type === "text"
66
+ ? { type: "text", text: block.text }
67
+ : { type: "image", attachment: admitted[imageIndex++] });
68
+ }
69
+
70
+ /**
71
+ * Real DSH-backed attachment admission and resolution.
72
+ *
73
+ * Admission uses DSH's public canonical-base64 batch API and the local durable,
74
+ * content-addressed store. Resolution re-reads through the store, which verifies
75
+ * the durable reference and digest before exposing bytes to Prime.
76
+ */
77
+ export class LocalDshImageAttachments implements DshImageAttachmentGateway {
78
+ readonly context: Context;
79
+ readonly store: LocalAttachmentStore;
80
+
81
+ constructor(config: LocalAttachmentConfig = {}) {
82
+ this.context = new Context();
83
+ this.store = new LocalAttachmentStore(this.context, config);
84
+ }
85
+
86
+ async admitPrimeImages(images: readonly PrimeInlineImage[]): Promise<readonly ImageAttachmentRef[]> {
87
+ const encoded: EncodedImageAttachment[] = images.map((image) => ({
88
+ data: image.data,
89
+ mediaType: imageMediaType(image.mimeType),
90
+ ...(image.name === undefined ? {} : { name: image.name }),
91
+ }));
92
+ return admitEncodedImages(this.store, encoded);
93
+ }
94
+
95
+ async resolveDshImage(attachment: ImageAttachmentRef, signal?: AbortSignal): Promise<ResolvedPrimeImage> {
96
+ const stored = await this.store.readImage(attachment, signal);
97
+ return { data: Buffer.from(stored.data).toString("base64"), mimeType: stored.ref.mediaType };
98
+ }
99
+ }
@@ -0,0 +1,233 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstatSync, readFileSync, realpathSync, readdirSync } from "node:fs";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { sessionEntryToContextMessages, type SessionEntry } from "@earendil-works/pi-coding-agent";
5
+
6
+ const SHA256 = /^[a-f0-9]{64}$/;
7
+ const COMMIT_VERSION = "prime-agent-dsh/derived-commit-v1";
8
+ const OBJECT_VERSION = "prime-agent-dsh/derived-object-v3-reference";
9
+ const STORE_VERSION = "prime-agent-dsh/durable-store-v3-reference";
10
+
11
+ type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
12
+ type RecordValue = Record<string, unknown>;
13
+ export type QueryMode = "literal" | "regex" | "full-text";
14
+ export type QueryScope = "effective" | "source";
15
+
16
+ export interface DurableContextQueryOptions {
17
+ readonly root: string;
18
+ readonly sessionId: string;
19
+ readonly primeSessionFile: string;
20
+ readonly maxCheckpoints?: number;
21
+ readonly maxResults?: number;
22
+ readonly maxQueryBytes?: number;
23
+ readonly maxScannedEntries?: number;
24
+ }
25
+ export interface CheckpointFilter {
26
+ readonly branchIds?: readonly string[];
27
+ readonly generations?: { readonly min?: number; readonly max?: number };
28
+ readonly commitDigests?: readonly string[];
29
+ }
30
+ export interface ContextQueryRequest {
31
+ readonly query: string;
32
+ readonly mode?: QueryMode;
33
+ readonly scope?: QueryScope;
34
+ readonly regexFlags?: string;
35
+ readonly filter?: CheckpointFilter;
36
+ readonly limit?: number;
37
+ readonly cursor?: string;
38
+ }
39
+ export interface ProvenanceTrace {
40
+ readonly sessionId: string;
41
+ readonly primeSessionFile: string;
42
+ readonly bindingDigest: string;
43
+ readonly head: string;
44
+ readonly commitDigest: string;
45
+ readonly generation: number;
46
+ readonly parentCommitDigest: string | null;
47
+ readonly objectDigest: string;
48
+ readonly sourceDigest: string;
49
+ readonly effectiveDigest: string;
50
+ readonly branchId: string;
51
+ readonly scope: QueryScope;
52
+ readonly entryIndex: number;
53
+ readonly entryDigest: string;
54
+ /** True when the returned value reconstructs the referenced entry exactly. */
55
+ readonly exactBody: boolean;
56
+ }
57
+ export interface ContextQueryHit {
58
+ readonly text: string;
59
+ readonly value: Json | undefined;
60
+ readonly score: number | undefined;
61
+ readonly truncated: boolean;
62
+ readonly trace: ProvenanceTrace;
63
+ }
64
+ export interface ContextQueryPage {
65
+ readonly hits: readonly ContextQueryHit[];
66
+ readonly nextCursor?: string;
67
+ readonly scannedEntries: number;
68
+ readonly skippedCorruptCheckpoints: number;
69
+ readonly snapshotGeneration: number;
70
+ /** Effective entries that cannot be reconstructed losslessly from Prime source. */
71
+ readonly unavailableEffectiveEntries: number;
72
+ }
73
+ export interface ContextCheckpoint {
74
+ readonly commitDigest: string; readonly generation: number; readonly branchId: string;
75
+ readonly parentCommitDigest: string | null; readonly objectDigest: string;
76
+ readonly sourceDigest: string; readonly effectiveDigest: string; readonly observedAt: number;
77
+ }
78
+
79
+ function obj(value: unknown): RecordValue | undefined { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as RecordValue : undefined; }
80
+ function canonical(value: unknown, seen = new Set<object>()): string {
81
+ if (value === null) return "null";
82
+ if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
83
+ if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(Object.is(value, -0) ? 0 : value);
84
+ if (Array.isArray(value)) { if (seen.has(value)) throw new TypeError("cyclic JSON"); seen.add(value); const out = `[${value.map(v => canonical(v, seen)).join(",")}]`; seen.delete(value); return out; }
85
+ const record = obj(value); if (record) { if (seen.has(record)) throw new TypeError("cyclic JSON"); seen.add(record); const out = `{${Object.keys(record).sort().map(k => `${JSON.stringify(k)}:${canonical(record[k], seen)}`).join(",")}}`; seen.delete(record); return out; }
86
+ throw new TypeError("not JSON");
87
+ }
88
+ function hashText(value: string): string { return createHash("sha256").update(value).digest("hex"); }
89
+ function digest(value: unknown): string { return hashText(canonical(value)); }
90
+ function safeText(path: string, maximum = 32 * 1024 * 1024): string { const stat = lstatSync(path); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > maximum) throw new Error(`unsafe or oversized query file: ${path}`); return readFileSync(path, "utf8"); }
91
+ function parse(path: string): unknown { return JSON.parse(safeText(path)); }
92
+ function semanticText(value: unknown): string {
93
+ const r = obj(value); if (r) { if (typeof r.text === "string") return r.text; if (typeof r.content === "string") return r.content; const m = obj(r.message); if (typeof m?.content === "string") return m.content; if (typeof m?.summary === "string") return m.summary; }
94
+ return canonical(value);
95
+ }
96
+ function tokens(text: string): string[] { return text.normalize("NFKC").toLocaleLowerCase("en-US").match(/[\p{L}\p{N}_]+/gu) ?? []; }
97
+ function encode(value: unknown): string { return Buffer.from(canonical(value)).toString("base64url"); }
98
+ function decode(value: string): RecordValue { if (Buffer.byteLength(value) > 4096) throw new Error("invalid query cursor"); try { const parsed = obj(JSON.parse(Buffer.from(value, "base64url").toString("utf8"))); if (parsed) return parsed; } catch { /* below */ } throw new Error("invalid query cursor"); }
99
+ function validPositive(value: number): boolean { return Number.isSafeInteger(value) && value > 0; }
100
+
101
+ interface Loaded { head: string; commitDigest: string; commit: RecordValue; object: RecordValue; branchId: string; source: Json[]; effective: Json[]; effectiveExact: boolean[] }
102
+ interface Candidate { hit: ContextQueryHit; sort: readonly (string | number)[] }
103
+
104
+ /**
105
+ * Read-only query adapter for DurableContextStore publications.
106
+ * Prime JSONL is canonical. This adapter validates and derives every view from immutable
107
+ * store commits, so any optional external FTS index can be deleted and rebuilt.
108
+ */
109
+ export class DurableContextQuery {
110
+ readonly root: string;
111
+ readonly sessionId: string;
112
+ readonly primeSessionFile: string;
113
+ readonly bindingDigest: string;
114
+ private readonly maxCheckpoints: number; private readonly maxResults: number;
115
+ private readonly maxQueryBytes: number; private readonly maxScannedEntries: number;
116
+
117
+ constructor(options: DurableContextQueryOptions) {
118
+ if (!isAbsolute(options.root) || !isAbsolute(options.primeSessionFile)) throw new Error("query paths must be absolute");
119
+ if (!/^[A-Za-z0-9._-]{1,128}$/.test(options.sessionId)) throw new Error("invalid session id");
120
+ this.root = realpathSync(options.root); this.primeSessionFile = realpathSync(options.primeSessionFile); this.sessionId = options.sessionId;
121
+ this.maxCheckpoints = options.maxCheckpoints ?? 128; this.maxResults = options.maxResults ?? 100;
122
+ this.maxQueryBytes = options.maxQueryBytes ?? 4096; this.maxScannedEntries = options.maxScannedEntries ?? 100_000;
123
+ if (![this.maxCheckpoints, this.maxResults, this.maxQueryBytes, this.maxScannedEntries].every(validPositive)) throw new Error("query limits must be positive integers");
124
+ this.bindingDigest = digest({ sessionId: this.sessionId, primeSessionFile: this.primeSessionFile });
125
+ const binding = obj(parse(join(this.root, "BINDING")));
126
+ if (!binding || binding.version !== STORE_VERSION || binding.sessionId !== this.sessionId || binding.primeSessionFile !== this.primeSessionFile || binding.bindingDigest !== this.bindingDigest) throw new Error("durable query is bound to another Prime session");
127
+ }
128
+
129
+ private load(cutoff = Number.MAX_SAFE_INTEGER): { values: Loaded[]; corrupt: number } {
130
+ const headsPath = join(this.root, "heads"); const stat = lstatSync(headsPath); if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("unsafe heads directory");
131
+ const names = readdirSync(headsPath).filter(n => /^\d{16}-[a-f0-9]{64}$/.test(n) && Number(n.slice(0, 16)) <= cutoff).sort().reverse();
132
+ const values: Loaded[] = []; let corrupt = 0;
133
+ for (const head of names) try {
134
+ const commitDigest = head.slice(17); if (safeText(join(headsPath, head)).trim() !== commitDigest) throw new Error();
135
+ const commitRaw = safeText(join(this.root, "commits", `${commitDigest}.json`)).trim(); if (hashText(commitRaw) !== commitDigest) throw new Error();
136
+ const commit = obj(JSON.parse(commitRaw)); if (!commit || commit.version !== COMMIT_VERSION || commit.bindingDigest !== this.bindingDigest || commit.generation !== Number(head.slice(0, 16)) || typeof commit.object !== "string" || !SHA256.test(commit.object) || typeof commit.sourceDigest !== "string" || !SHA256.test(commit.sourceDigest) || typeof commit.effectiveDigest !== "string" || !SHA256.test(commit.effectiveDigest) || (commit.parent !== null && (typeof commit.parent !== "string" || !SHA256.test(commit.parent)))) throw new Error();
137
+ const objectRaw = safeText(join(this.root, "objects", `${commit.object}.json`)).trim(); if (hashText(objectRaw) !== commit.object) throw new Error();
138
+ const object = obj(JSON.parse(objectRaw)); const compatibility = obj(object?.compatibility);
139
+ if (!object || object.version !== OBJECT_VERSION || object.bindingDigest !== this.bindingDigest || object.sourceDigest !== commit.sourceDigest || object.effectiveDigest !== commit.effectiveDigest || !Array.isArray(object.sourceEntryDigests) || !(object.sourceEntryDigests as unknown[]).every(v => typeof v === "string" && SHA256.test(v)) || !Array.isArray(object.effectiveEntryDigests) || !(object.effectiveEntryDigests as unknown[]).every(v => typeof v === "string" && SHA256.test(v)) || !compatibility || compatibility.version !== STORE_VERSION || compatibility.sessionId !== this.sessionId || compatibility.revision !== commit.generation || compatibility.sourceDigest !== commit.sourceDigest || compatibility.effectiveDigest !== commit.effectiveDigest || typeof compatibility.branchId !== "string" || !Array.isArray(compatibility.entries) || typeof compatibility.cropped !== "boolean") throw new Error();
140
+ if ("effective" in object || compatibility.messages !== undefined || !Array.isArray(object.sourceLocators)
141
+ || !Array.isArray(object.effectiveReferences) || object.sourceLocators.length !== object.sourceEntryDigests.length
142
+ || object.effectiveReferences.length !== object.effectiveEntryDigests.length || compatibility.entries.length !== 0) throw new Error();
143
+ const prime = readFileSync(this.primeSessionFile);
144
+ const source = object.sourceLocators.map((raw, index) => {
145
+ const locator = obj(raw); const expected = (object.sourceEntryDigests as string[])[index];
146
+ if (!locator || locator.index !== index || !Number.isSafeInteger(locator.byteOffset) || (locator.byteOffset as number) < 0
147
+ || !Number.isSafeInteger(locator.byteLength) || (locator.byteLength as number) <= 0 || !Number.isSafeInteger(locator.line)
148
+ || (locator.line as number) <= 0 || locator.entryDigest !== expected || (locator.entryId !== undefined && typeof locator.entryId !== "string")) throw new Error();
149
+ const start = locator.byteOffset as number, end = start + (locator.byteLength as number);
150
+ if (end > prime.length || (start > 0 && prime[start - 1] !== 0x0a)
151
+ || (end < prime.length && prime[end] !== 0x0a && !(prime[end] === 0x0d && prime[end + 1] === 0x0a))) throw new Error();
152
+ const value = JSON.parse(prime.subarray(start, end).toString("utf8")) as Json;
153
+ if (digest(value) !== expected || (locator.entryId !== undefined && obj(value)?.id !== locator.entryId)) throw new Error();
154
+ return value;
155
+ });
156
+ if (digest(source) !== object.sourceDigest) throw new Error();
157
+ const effectiveExact: boolean[] = [];
158
+ const effective = object.effectiveReferences.map((raw, index) => {
159
+ const reference = obj(raw); const expected = (object.effectiveEntryDigests as string[])[index];
160
+ if (!reference || reference.entryDigest !== expected || (reference.role !== undefined && typeof reference.role !== "string")
161
+ || (reference.sourceIndex !== null && (!Number.isSafeInteger(reference.sourceIndex) || (reference.sourceIndex as number) < 0 || (reference.sourceIndex as number) >= source.length))) throw new Error();
162
+ if (reference.sourceIndex === null) { effectiveExact.push(false); return null; }
163
+ const entry = source[reference.sourceIndex as number]!;
164
+ if (digest(entry) === expected) { effectiveExact.push(true); return entry; }
165
+ const nested = obj(entry)?.message;
166
+ if (nested !== undefined && digest(nested) === expected) { effectiveExact.push(true); return nested as Json; }
167
+ const messages = sessionEntryToContextMessages(entry as unknown as SessionEntry);
168
+ const message = messages[0];
169
+ effectiveExact.push(false); // exact DSH conversion is intentionally not persisted
170
+ return message === undefined ? null : JSON.parse(canonical(message)) as Json;
171
+ });
172
+ if (compatibility.messageCount !== effective.length) throw new Error();
173
+ values.push({ head, commitDigest, commit, object, branchId: compatibility.branchId, source, effective, effectiveExact });
174
+ if (values.length >= this.maxCheckpoints) break;
175
+ continue;
176
+ } catch { corrupt++; }
177
+ return { values, corrupt };
178
+ }
179
+
180
+ listCheckpoints(filter: CheckpointFilter = {}): readonly ContextCheckpoint[] {
181
+ return this.load().values.filter(v => this.matches(v, filter)).map(v => ({ commitDigest: v.commitDigest, generation: v.commit.generation as number, branchId: v.branchId, parentCommitDigest: v.commit.parent as string | null, objectDigest: v.commit.object as string, sourceDigest: v.commit.sourceDigest as string, effectiveDigest: v.commit.effectiveDigest as string, observedAt: typeof v.commit.observedAt === "number" ? v.commit.observedAt : 0 }));
182
+ }
183
+ private matches(v: Loaded, filter: CheckpointFilter): boolean {
184
+ if (filter.branchIds && !filter.branchIds.includes(v.branchId)) return false;
185
+ if (filter.commitDigests && !filter.commitDigests.includes(v.commitDigest)) return false;
186
+ const generation = v.commit.generation as number;
187
+ if (filter.generations?.min !== undefined && generation < filter.generations.min) return false;
188
+ if (filter.generations?.max !== undefined && generation > filter.generations.max) return false;
189
+ return true;
190
+ }
191
+
192
+ query(request: ContextQueryRequest): ContextQueryPage {
193
+ if (typeof request.query !== "string" || Buffer.byteLength(request.query) > this.maxQueryBytes) throw new Error("query exceeds configured bound");
194
+ const mode = request.mode ?? "literal", scope = request.scope ?? "effective";
195
+ if (!["literal", "regex", "full-text"].includes(mode) || !["effective", "source"].includes(scope)) throw new Error("invalid query mode or scope");
196
+ const limit = request.limit ?? Math.min(20, this.maxResults); if (!validPositive(limit) || limit > this.maxResults) throw new Error("query result limit exceeds configured bound");
197
+ const requestKey = digest({ query: request.query, mode, scope, regexFlags: request.regexFlags ?? "", filter: request.filter ?? {} });
198
+ let cutoff = Number.MAX_SAFE_INTEGER, offset = 0; let cursorSnapshot: string | undefined; let cursorCeiling: string | undefined;
199
+ if (request.cursor) { const c = decode(request.cursor); if (c.v !== 2 || c.key !== requestKey || !Number.isSafeInteger(c.cutoff) || !Number.isSafeInteger(c.offset) || (c.offset as number) < 0 || typeof c.snapshot !== "string" || !SHA256.test(c.snapshot) || typeof c.ceiling !== "string" || !SHA256.test(c.ceiling)) throw new Error("query cursor does not match request"); cutoff = c.cutoff as number; offset = c.offset as number; cursorSnapshot = c.snapshot; cursorCeiling = c.ceiling; }
200
+ const loaded = this.load(cutoff); if (!request.cursor) cutoff = Math.max(0, ...loaded.values.map(v => v.commit.generation as number));
201
+ const snapshotIdentity = digest(loaded.values.map(v => ({ head: v.head, commit: v.commitDigest, object: v.commit.object })).sort((a,b) => a.head.localeCompare(b.head)));
202
+ const ceilingIdentity = loaded.values.find(v => v.commit.generation === cutoff)?.commitDigest ?? digest([]);
203
+ if (request.cursor && (snapshotIdentity !== cursorSnapshot || ceilingIdentity !== cursorCeiling)) throw new Error("query cursor snapshot is no longer available");
204
+ const selected = loaded.values.filter(v => this.matches(v, request.filter ?? {}));
205
+ let regex: RegExp | undefined; if (mode === "regex") { const flags = request.regexFlags ?? "iu"; if (!/^(?!.*(.).*\1)[imu]*$/.test(flags)) throw new Error("unsupported regex flags"); try { regex = new RegExp(request.query, flags); } catch { throw new Error("invalid regular expression"); } }
206
+ const needle = request.query.trim().split(/\s+/u).map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
207
+ const literal = mode === "literal" ? new RegExp(needle, "iu") : undefined;
208
+ const queryTokens = tokens(request.query); if (mode === "full-text" && queryTokens.length === 0) throw new Error("full-text query has no searchable terms");
209
+ const docs: { loaded: Loaded; index: number; text: string; value: Json | undefined; digest: string; truncated: boolean; exact: boolean }[] = [];
210
+ let unavailableEffectiveEntries = 0;
211
+ for (const v of selected) {
212
+ if (scope === "effective") v.effective.forEach((value, index) => { if (value !== null) docs.push({ loaded: v, index, text: semanticText(value), value, digest: (v.object.effectiveEntryDigests as string[])[index], truncated: false, exact: v.effectiveExact?.[index] ?? true }); else unavailableEffectiveEntries++; });
213
+ else v.source.forEach((value, index) => docs.push({ loaded: v, index, text: semanticText(value), value, digest: (v.object.sourceEntryDigests as string[])[index], truncated: false, exact: true }));
214
+ if (docs.length > this.maxScannedEntries) throw new Error("query scan exceeds configured bound");
215
+ }
216
+ const df = new Map<string, number>(); if (mode === "full-text") for (const d of docs) for (const t of new Set(tokens(d.text))) df.set(t, (df.get(t) ?? 0) + 1);
217
+ const avg = docs.length ? docs.reduce((n, d) => n + tokens(d.text).length, 0) / docs.length : 1;
218
+ const candidates: Candidate[] = [];
219
+ for (const d of docs) {
220
+ let score: number | undefined; let match: boolean;
221
+ if (literal) match = literal.test(d.text); else if (regex) { regex.lastIndex = 0; match = regex.test(d.text); }
222
+ else { const ts = tokens(d.text), counts = new Map<string, number>(); for (const t of ts) counts.set(t, (counts.get(t) ?? 0) + 1); score = 0; for (const q of queryTokens) { const tf = counts.get(q) ?? 0; if (!tf) continue; const idf = Math.log(1 + (docs.length - (df.get(q) ?? 0) + .5) / ((df.get(q) ?? 0) + .5)); score += idf * tf * 2.2 / (tf + 1.2 * (.25 + .75 * ts.length / avg)); } match = score > 0; score = Number(score.toFixed(12)); }
223
+ if (!match) continue;
224
+ const v = d.loaded, generation = v.commit.generation as number;
225
+ const trace: ProvenanceTrace = { sessionId: this.sessionId, primeSessionFile: this.primeSessionFile, bindingDigest: this.bindingDigest, head: v.head, commitDigest: v.commitDigest, generation, parentCommitDigest: v.commit.parent as string | null, objectDigest: v.commit.object as string, sourceDigest: v.commit.sourceDigest as string, effectiveDigest: v.commit.effectiveDigest as string, branchId: v.branchId, scope, entryIndex: d.index, entryDigest: d.digest, exactBody: d.exact };
226
+ const hit = { text: d.text, value: d.value, score, truncated: d.truncated, trace };
227
+ candidates.push({ hit, sort: mode === "full-text" ? [-(score ?? 0), -generation, d.index, v.commitDigest] : [-generation, d.index, v.commitDigest] });
228
+ }
229
+ candidates.sort((a, b) => { for (let i = 0; i < a.sort.length; i++) { const x=a.sort[i], y=b.sort[i]; if (x < y) return -1; if (x > y) return 1; } return 0; });
230
+ const hits = candidates.slice(offset, offset + limit).map(c => c.hit); const next = offset + limit < candidates.length ? encode({ v: 2, key: requestKey, cutoff, snapshot: snapshotIdentity, ceiling: ceilingIdentity, offset: offset + limit }) : undefined;
231
+ return { hits, ...(next ? { nextCursor: next } : {}), scannedEntries: docs.length, skippedCorruptCheckpoints: loaded.corrupt, snapshotGeneration: cutoff, unavailableEffectiveEntries };
232
+ }
233
+ }