pikelet-wasm 0.7.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,527 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import { expectedBlobBytes } from './inline-transformer.mjs';
4
+ import { MAGIC, HEADER_BYTES, TABLE_ENTRY_BYTES, KINDS, KIND_NAMES } from './format.mjs';
5
+
6
+ export { MAGIC, HEADER_BYTES, TABLE_ENTRY_BYTES, KINDS, KIND_NAMES };
7
+
8
+ // The inline-transformer kernel via the web glue with an explicitly read
9
+ // wasm binary — the form that works under both native ESM and jiti's CJS
10
+ // transform (the node glue's createRequire bootstrap breaks there). Node
11
+ // builders only; the reader picks its own kernel.
12
+ export async function loadInlineEncoderKernel() {
13
+ const { default: createEncoderModule } = await import('./encoder-kernels/encoder.mjs');
14
+ const wasmBinary = fs.readFileSync(new URL('./encoder-kernels/encoder.wasm', import.meta.url));
15
+ return () => createEncoderModule({ wasmBinary });
16
+ }
17
+
18
+ export const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest();
19
+ export const align16 = (n) => Math.ceil(n / 16) * 16;
20
+
21
+ // Canonical form = JSON.stringify semantics with sorted object keys:
22
+ // undefined / functions / symbols are omitted as object properties and
23
+ // become null as array elements, exactly as JSON.stringify treats them —
24
+ // previously they leaked through as the literal text `undefined`, producing
25
+ // output JSON.parse rejects.
26
+ export function canonicalJson(value) {
27
+ if (Array.isArray(value)) {
28
+ return `[${value.map((v) => canonicalJson(v) ?? 'null').join(',')}]`;
29
+ }
30
+ if (value && typeof value === 'object') {
31
+ const parts = [];
32
+ for (const k of Object.keys(value).sort()) {
33
+ const serialized = canonicalJson(value[k]);
34
+ if (serialized !== undefined) parts.push(`${JSON.stringify(k)}:${serialized}`);
35
+ }
36
+ return `{${parts.join(',')}}`;
37
+ }
38
+ return JSON.stringify(value);
39
+ }
40
+
41
+ export function buildQueryInterpSegment(kind, encoderBytes, calibrationBytes) {
42
+ const out = Buffer.alloc(16 + encoderBytes.length + calibrationBytes.length);
43
+ out.writeUInt32LE(1, 0);
44
+ out.writeUInt32LE(kind, 4);
45
+ out.writeUInt32LE(encoderBytes.length, 8);
46
+ out.writeUInt32LE(calibrationBytes.length, 12);
47
+ out.set(encoderBytes, 16); // set(), not copy(): inputs may be plain Uint8Arrays
48
+ out.set(calibrationBytes, 16 + encoderBytes.length);
49
+ return out;
50
+ }
51
+
52
+ // Lexical index segment (kind 5, OPTIONAL), layout bm25-v1: a static
53
+ // inverted index over the corpus records for hybrid retrieval. Terms are
54
+ // addressed by a 64-bit hash (two fnv1a-32 passes, seeds 0 and 0x9e3779b9)
55
+ // in a fixed-width table sorted by (hashHi, hashLo), so a reader can binary
56
+ // search without materializing term strings — lazy-friendly by
57
+ // construction, even though the phase-1 reader loads the segment eagerly.
58
+ // Postings are (docIdDelta, tf) varint pairs. Document length (indexed
59
+ // tokens per record) and the corpus token total ship for BM25
60
+ // normalization. Hash collisions merge two terms' postings; at 64 bits over
61
+ // realistic vocabularies the probability is negligible and the failure mode
62
+ // is one slightly-wrong lexical score, not corruption.
63
+ //
64
+ // [0,4) u32 version = 1
65
+ // [4,8) u32 docCount
66
+ // [8,12) u32 termCount
67
+ // [12,20) u64 totalTokens
68
+ // [20,24) u32 doclenOffset absolute within the segment
69
+ // [24,28) u32 termTableOffset
70
+ // [28,32) u32 postingsOffset
71
+ // [32,40) u64 postingsBytes
72
+ // [40,64) reserved, zero
73
+ // doclens u32[docCount]
74
+ // termTable termCount x 24 bytes: u32 hashLo, u32 hashHi,
75
+ // u64 postingsOffset (relative to postingsOffset), u32 bytes,
76
+ // u32 df
77
+ // postings per term: varint docId (first absolute, then deltas),
78
+ // varint tf, repeated df times
79
+ //
80
+ // Indexing tokenizes on [a-z0-9']+ lowercased, keeps tokens of 2..32 chars,
81
+ // and skips a small function-word stopword list — readers need no
82
+ // coordination with the list, because an unindexed query token simply finds
83
+ // no postings.
84
+ const LEXICAL_LAYOUT = 'bm25-v1';
85
+ const LEXICAL_STOPWORDS = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'of', 'to', 'in', 'on', 'at', 'by', 'for', 'with', 'from', 'into', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'it', 'its', 'this', 'that', 'these', 'those', 'as', 'their', 'they', 'them', 'his', 'her', 'has', 'have', 'had', 'not', 'can', 'will', 'if', 'we', 'you', 'your', 'i']);
86
+ const lexTokenize = (text) => (String(text).toLowerCase().match(/[a-z0-9']+/g) || [])
87
+ .filter((w) => w.length >= 2 && w.length <= 32 && !LEXICAL_STOPWORDS.has(w));
88
+
89
+ function fnv1a32(str, seed) {
90
+ let h = 0x811c9dc5 ^ seed;
91
+ for (let i = 0; i < str.length; i++) {
92
+ h ^= str.charCodeAt(i);
93
+ h = Math.imul(h, 0x01000193);
94
+ }
95
+ return h >>> 0;
96
+ }
97
+
98
+ function varintBytes(n) {
99
+ const out = [];
100
+ let v = n >>> 0;
101
+ do {
102
+ let b = v & 0x7f;
103
+ v >>>= 7;
104
+ if (v) b |= 0x80;
105
+ out.push(b);
106
+ } while (v);
107
+ return out;
108
+ }
109
+
110
+ // Counting-sort construction over typed arrays: the obvious
111
+ // Map-of-Maps build dies around tens of millions of posting entries
112
+ // (a 456k-chunk wiki corpus has ~30M), so terms are interned to dense
113
+ // indexes on a first tokenize pass (df counted per term), entry arrays
114
+ // are preallocated exactly, and a second tokenize pass scatters
115
+ // (docId, tf) pairs into per-term slots. Entries land doc-ascending per
116
+ // term by construction, which is what the delta varints need. Output
117
+ // bytes are identical to the original builder's.
118
+ export function buildLexicalSegment(texts) {
119
+ const docCount = texts.length;
120
+ const doclens = new Uint32Array(docCount);
121
+ const termIndex = new Map(); // "hashHi:hashLo" -> dense index
122
+ const hashLoArr = [];
123
+ const hashHiArr = [];
124
+ const dfArr = [];
125
+ let totalTokens = 0;
126
+ let entryCount = 0;
127
+
128
+ const tallyDoc = (id, onTerm) => {
129
+ const tokens = lexTokenize(texts[id]);
130
+ const tf = new Map(); // dense term index -> tf within this doc
131
+ for (const t of tokens) {
132
+ const key = `${fnv1a32(t, 0x9e3779b9)}:${fnv1a32(t, 0)}`;
133
+ let idx = termIndex.get(key);
134
+ if (idx === undefined) {
135
+ idx = hashLoArr.length;
136
+ termIndex.set(key, idx);
137
+ hashLoArr.push(fnv1a32(t, 0));
138
+ hashHiArr.push(fnv1a32(t, 0x9e3779b9));
139
+ dfArr.push(0);
140
+ }
141
+ tf.set(idx, (tf.get(idx) || 0) + 1);
142
+ }
143
+ for (const [idx, count] of tf) onTerm(idx, count);
144
+ return tokens.length;
145
+ };
146
+
147
+ for (let id = 0; id < docCount; id++) {
148
+ const tokens = tallyDoc(id, (idx) => { dfArr[idx] += 1; entryCount += 1; });
149
+ doclens[id] = tokens;
150
+ totalTokens += tokens;
151
+ }
152
+
153
+ const termCount = hashLoArr.length;
154
+ // Per-term slot offsets in the entry arrays (prefix sums of df).
155
+ const slotStart = new Uint32Array(termCount + 1);
156
+ for (let i = 0; i < termCount; i++) slotStart[i + 1] = slotStart[i] + dfArr[i];
157
+ const cursor = slotStart.slice(0, termCount);
158
+ const eDoc = new Uint32Array(entryCount);
159
+ const eTf = new Uint32Array(entryCount);
160
+ for (let id = 0; id < docCount; id++) {
161
+ tallyDoc(id, (idx, tf) => {
162
+ const at = cursor[idx]++;
163
+ eDoc[at] = id;
164
+ eTf[at] = tf;
165
+ });
166
+ }
167
+
168
+ // Serialize in (hashHi, hashLo) order. Sizes first, then one fill pass.
169
+ const order = Array.from({ length: termCount }, (_, i) => i)
170
+ .sort((a, b) => (hashHiArr[a] - hashHiArr[b]) || (hashLoArr[a] - hashLoArr[b]));
171
+ const varintLen = (n) => (n < 0x80 ? 1 : n < 0x4000 ? 2 : n < 0x200000 ? 3 : n < 0x10000000 ? 4 : 5);
172
+ let postingsCursor = 0;
173
+ const termPostingsOffset = new Float64Array(termCount); // by sorted position
174
+ const termPostingsBytes = new Uint32Array(termCount);
175
+ order.forEach((idx, pos) => {
176
+ let bytes = 0;
177
+ let prev = 0;
178
+ for (let at = slotStart[idx]; at < slotStart[idx + 1]; at++) {
179
+ bytes += varintLen(at === slotStart[idx] ? eDoc[at] : eDoc[at] - prev) + varintLen(eTf[at]);
180
+ prev = eDoc[at];
181
+ }
182
+ termPostingsOffset[pos] = postingsCursor;
183
+ termPostingsBytes[pos] = bytes;
184
+ postingsCursor += bytes;
185
+ });
186
+
187
+ const doclenOffset = 64;
188
+ const termTableOffset = doclenOffset + 4 * docCount;
189
+ const postingsOffset = termTableOffset + 24 * termCount;
190
+ const out = Buffer.alloc(postingsOffset + postingsCursor);
191
+ out.writeUInt32LE(1, 0);
192
+ out.writeUInt32LE(docCount, 4);
193
+ out.writeUInt32LE(termCount, 8);
194
+ out.writeBigUInt64LE(BigInt(totalTokens), 12);
195
+ out.writeUInt32LE(doclenOffset, 20);
196
+ out.writeUInt32LE(termTableOffset, 24);
197
+ out.writeUInt32LE(postingsOffset, 28);
198
+ out.writeBigUInt64LE(BigInt(postingsCursor), 32);
199
+ for (let id = 0; id < docCount; id++) out.writeUInt32LE(doclens[id], doclenOffset + 4 * id);
200
+ const writeVarint = (value, at) => {
201
+ let v = value >>> 0;
202
+ do {
203
+ let b = v & 0x7f;
204
+ v >>>= 7;
205
+ if (v) b |= 0x80;
206
+ out[at++] = b;
207
+ } while (v);
208
+ return at;
209
+ };
210
+ order.forEach((idx, pos) => {
211
+ const entry = termTableOffset + 24 * pos;
212
+ out.writeUInt32LE(hashLoArr[idx], entry);
213
+ out.writeUInt32LE(hashHiArr[idx], entry + 4);
214
+ out.writeBigUInt64LE(BigInt(termPostingsOffset[pos]), entry + 8);
215
+ out.writeUInt32LE(termPostingsBytes[pos], entry + 16);
216
+ out.writeUInt32LE(dfArr[idx], entry + 20);
217
+ let at = postingsOffset + termPostingsOffset[pos];
218
+ let prev = 0;
219
+ for (let slot = slotStart[idx]; slot < slotStart[idx + 1]; slot++) {
220
+ at = writeVarint(slot === slotStart[idx] ? eDoc[slot] : eDoc[slot] - prev, at);
221
+ at = writeVarint(eTf[slot], at);
222
+ prev = eDoc[slot];
223
+ }
224
+ });
225
+ return {
226
+ bytes: out,
227
+ meta: { layout: LEXICAL_LAYOUT, terms: termCount, docCount, totalTokens },
228
+ };
229
+ }
230
+
231
+ // Corpus layout v1 (format 1, profile pancake-complete-v1): count + offsets +
232
+ // records, integrity by whole-segment digest only. Kept for producing
233
+ // format-1 files (compatibility fixtures, readers that predate format 2);
234
+ // new artifacts use buildCorpusSegment() below.
235
+ export function buildCorpusSegmentFromBuffers(records) {
236
+ const count = records.length;
237
+ const prefix = 4 + 8 * (count + 1);
238
+ const total = prefix + records.reduce((sum, b) => sum + b.length, 0);
239
+ const out = Buffer.alloc(total);
240
+ out.writeUInt32LE(count, 0);
241
+ let cursor = prefix;
242
+ for (let id = 0; id < count; id++) {
243
+ out.writeBigUInt64LE(BigInt(cursor), 4 + 8 * id);
244
+ out.set(records[id], cursor); // set(), not copy(): records may be plain Uint8Arrays
245
+ cursor += records[id].length;
246
+ }
247
+ out.writeBigUInt64LE(BigInt(cursor), 4 + 8 * count);
248
+ return out;
249
+ }
250
+
251
+ // Corpus layout v2 (format 2, profile pancake-complete-v2): per-record
252
+ // integrity that a reader can check on the single range read that hydrates
253
+ // a record, without fetching the rest of the segment (contract 4.1 / 7):
254
+ //
255
+ // [0, 4) u32 count
256
+ // [4, 8) u32 pageRecords (P)
257
+ // [8, 8 + 8*(count+1)) u64 offsets[count+1] record i = [offsets[i], offsets[i+1])
258
+ // [A, A + 32*pages) pageSha256[pages] pages = ceil(count / P); page p is
259
+ // the digest of recordSha256[p*P .. min(count,(p+1)*P))
260
+ // [B, B + 32*count) recordSha256[count] digest of each record's bytes
261
+ // [C, ...) records offsets[0] == C
262
+ //
263
+ // The manifest carries pageTableSha256 (digest of the page table bytes), so
264
+ // the chain is identity -> page table (read at open, 32 bytes per P
265
+ // records) -> one page of record digests (read when a record in it is first
266
+ // hydrated) -> the record. Returns the segment bytes plus the manifest's
267
+ // corpus fields; callers spread them under manifest.corpus.
268
+ export const CORPUS_LAYOUT_V2 = 'records-v2';
269
+ export const DEFAULT_CORPUS_PAGE_RECORDS = 256;
270
+ export function buildCorpusSegment(records, options = {}) {
271
+ const count = records.length;
272
+ const pageRecords = options.pageRecords ?? DEFAULT_CORPUS_PAGE_RECORDS;
273
+ if (!Number.isInteger(pageRecords) || pageRecords < 1 || pageRecords > 65536) {
274
+ throw new Error('pageRecords must be an integer in [1, 65536]');
275
+ }
276
+ const pages = Math.ceil(count / pageRecords);
277
+ const offsetsAt = 8;
278
+ const pageTableAt = offsetsAt + 8 * (count + 1);
279
+ const recordDigestsAt = pageTableAt + 32 * pages;
280
+ const recordsAt = recordDigestsAt + 32 * count;
281
+ const total = recordsAt + records.reduce((sum, b) => sum + b.length, 0);
282
+ const out = Buffer.alloc(total);
283
+ out.writeUInt32LE(count, 0);
284
+ out.writeUInt32LE(pageRecords, 4);
285
+ let cursor = recordsAt;
286
+ for (let id = 0; id < count; id++) {
287
+ out.writeBigUInt64LE(BigInt(cursor), offsetsAt + 8 * id);
288
+ out.set(records[id], cursor); // set(), not copy(): records may be plain Uint8Arrays
289
+ out.set(sha256(records[id]), recordDigestsAt + 32 * id);
290
+ cursor += records[id].length;
291
+ }
292
+ out.writeBigUInt64LE(BigInt(cursor), offsetsAt + 8 * count);
293
+ for (let p = 0; p < pages; p++) {
294
+ const from = recordDigestsAt + 32 * p * pageRecords;
295
+ const to = recordDigestsAt + 32 * Math.min(count, (p + 1) * pageRecords);
296
+ sha256(out.subarray(from, to)).copy(out, pageTableAt + 32 * p);
297
+ }
298
+ const pageTableSha256 = sha256(out.subarray(pageTableAt, recordDigestsAt)).toString('hex');
299
+ return {
300
+ bytes: out,
301
+ corpus: {
302
+ records: count,
303
+ layout: CORPUS_LAYOUT_V2,
304
+ pageRecords,
305
+ pages,
306
+ recordDigest: 'sha256',
307
+ pageTableSha256,
308
+ },
309
+ };
310
+ }
311
+
312
+ export function buildInlineTransformerEncoderSegment({ declaration, vocabBytes, weightBytes }) {
313
+ if (declaration.layout) {
314
+ const expected = expectedBlobBytes(declaration.layout);
315
+ if (weightBytes.length !== expected) {
316
+ throw new Error(`inline-encoder weights are ${weightBytes.length} bytes but the declared layout implies ${expected}`);
317
+ }
318
+ }
319
+ const declarationBytes = Buffer.from(canonicalJson(declaration), 'utf8');
320
+ const out = Buffer.alloc(12 + declarationBytes.length + vocabBytes.length + weightBytes.length);
321
+ out.writeUInt32LE(declarationBytes.length, 0);
322
+ out.writeUInt32LE(vocabBytes.length, 4);
323
+ out.writeUInt32LE(weightBytes.length, 8);
324
+ out.set(declarationBytes, 12);
325
+ out.set(vocabBytes, 12 + declarationBytes.length); // set(), not copy(): inputs may be plain Uint8Arrays
326
+ out.set(weightBytes, 12 + declarationBytes.length + vocabBytes.length);
327
+ return out;
328
+ }
329
+
330
+ // The spec's measured operating point (SKETCH_PROFILE.md section 5, mandatory
331
+ // in the complete profile's evaluation segment): open the just-built sketch
332
+ // bytes in memory, replay a query set at increasing rerank C against the
333
+ // exact full-rerank top-k, and return the smallest C whose recall reaches
334
+ // targetRecall — plus the whole curve for the evaluation segment. Queries
335
+ // are the corpus's own float embeddings when the caller has them, else rows
336
+ // dequantized out of the snapshot.
337
+ export async function measureRecommendedRerank({
338
+ artifactModule,
339
+ sketchBytes,
340
+ queryVectors = null,
341
+ snapshotBytes = null,
342
+ k = 10,
343
+ targetRecall = 0.98,
344
+ maxQueries = 256,
345
+ sweep = null,
346
+ }) {
347
+ const bytes = sketchBytes instanceof Uint8Array ? sketchBytes : new Uint8Array(sketchBytes);
348
+ const source = {
349
+ size: bytes.length,
350
+ preferredParallelism: Infinity,
351
+ preferredGapBytes: 0,
352
+ async read(offset, length) { return bytes.subarray(offset, offset + length); },
353
+ async close() {},
354
+ };
355
+ const sketch = await artifactModule.PancakeSketchArtifact.open(source);
356
+ try {
357
+ const count = sketch.count;
358
+ if (count === 0) throw new Error('cannot measure rerank on an empty sketch');
359
+ const topK = Math.min(k, count);
360
+ let queries;
361
+ let querySource;
362
+ if (queryVectors && queryVectors.length > 0) {
363
+ queries = sampleEvenly(queryVectors, maxQueries);
364
+ querySource = 'corpus-embeddings';
365
+ } else {
366
+ queries = selfQueriesFromSnapshot(artifactModule.parseUint8Snapshot(snapshotBytes), maxQueries);
367
+ querySource = 'dequantized-rows';
368
+ }
369
+ const truth = [];
370
+ for (const q of queries) {
371
+ truth.push(new Set((await sketch.search(q, topK, { rerank: count })).results.map((r) => r.id)));
372
+ }
373
+ const ladder = (sweep || [10, 15, 20, 30, 40, 60, 80, 120, 160, 240, 320, 480, 640])
374
+ .filter((c) => c >= topK && c < count);
375
+ ladder.push(count);
376
+ const curve = [];
377
+ let recommendedRerank = null;
378
+ let recommendedRecall = null;
379
+ for (const C of ladder) {
380
+ let hits = 0;
381
+ let total = 0;
382
+ for (let i = 0; i < queries.length; i++) {
383
+ for (const r of (await sketch.search(queries[i], topK, { rerank: C })).results) {
384
+ if (truth[i].has(r.id)) hits++;
385
+ }
386
+ total += truth[i].size;
387
+ }
388
+ const recall = total > 0 ? Number((hits / total).toFixed(4)) : 1;
389
+ curve.push({ rerank: C, recall });
390
+ if (recommendedRerank === null && recall >= targetRecall) {
391
+ recommendedRerank = C;
392
+ recommendedRecall = recall;
393
+ }
394
+ }
395
+ if (recommendedRerank === null) {
396
+ recommendedRerank = count;
397
+ recommendedRecall = curve[curve.length - 1].recall;
398
+ }
399
+ return {
400
+ recommendedRerank,
401
+ recall: recommendedRecall,
402
+ k: topK,
403
+ targetRecall,
404
+ queries: queries.length,
405
+ querySource,
406
+ curve,
407
+ };
408
+ } finally {
409
+ await sketch.close();
410
+ }
411
+ }
412
+
413
+ function sampleEvenly(items, max) {
414
+ if (items.length <= max) return items;
415
+ const stride = items.length / max;
416
+ const out = [];
417
+ for (let i = 0; i < max; i++) out.push(items[Math.floor(i * stride)]);
418
+ return out;
419
+ }
420
+
421
+ function selfQueriesFromSnapshot(graph, maxQueries) {
422
+ const n = Math.min(graph.count, maxQueries);
423
+ const stride = graph.count / n;
424
+ const queries = [];
425
+ for (let i = 0; i < n; i++) {
426
+ const id = Math.floor(i * stride);
427
+ const v = new Float32Array(graph.dim);
428
+ const base = id * graph.dim;
429
+ for (let d = 0; d < graph.dim; d++) v[d] = graph.offsets[id] + graph.scales[id] * graph.qdata[base + d];
430
+ queries.push(v);
431
+ }
432
+ return queries;
433
+ }
434
+
435
+ // Container format version follows the profile string: pancake-complete-v1
436
+ // files carry corpus layout v1 and header version 1; pancake-complete-v2
437
+ // files carry corpus layout records-v2 (buildCorpusSegment) and header
438
+ // version 2, so readers that predate per-record integrity reject them
439
+ // explicitly instead of misreading the corpus tables.
440
+ export const PROFILE_V1 = 'pancake-complete-v1';
441
+ export const PROFILE_V2 = 'pancake-complete-v2';
442
+ export const FORMAT_VERSIONS = { [PROFILE_V1]: 1, [PROFILE_V2]: 2 };
443
+
444
+ export function assemblePancakeFile(manifestFields, segments, outPath) {
445
+ const formatVersion = FORMAT_VERSIONS[manifestFields.profile];
446
+ if (!formatVersion) {
447
+ throw new Error(`manifest.profile must be ${PROFILE_V1} or ${PROFILE_V2}, got ${manifestFields.profile}`);
448
+ }
449
+ const layout = manifestFields.corpus?.layout;
450
+ if (formatVersion === 2 && layout !== CORPUS_LAYOUT_V2) {
451
+ throw new Error(`${PROFILE_V2} requires manifest.corpus from buildCorpusSegment() (layout ${CORPUS_LAYOUT_V2})`);
452
+ }
453
+ if (formatVersion === 1 && layout !== undefined) {
454
+ throw new Error(`${PROFILE_V1} carries corpus layout v1 (buildCorpusSegmentFromBuffers); got layout ${layout}`);
455
+ }
456
+ if (!Number.isInteger(manifestFields.corpus?.records) || manifestFields.corpus.records < 0) {
457
+ throw new Error('manifest.corpus.records must be a non-negative integer');
458
+ }
459
+ // Format 2 additionally commits to the embedded sketch's 256-byte header
460
+ // under the identity. The header carries the sketch's metric/dim/count and
461
+ // its residentSha256 / vectorsSha256, so anchoring the header anchors the
462
+ // sketch's own integrity chain to the complete identity without forcing a
463
+ // whole-segment read at open (the index segment stays lazy).
464
+ let indexCommitment;
465
+ if (formatVersion >= 2) {
466
+ const index = segments.find((s) => s.kind === 'index');
467
+ if (!index || index.bytes.length < 256) {
468
+ throw new Error('index segment must carry a 256-byte sketch header');
469
+ }
470
+ indexCommitment = { ...manifestFields.index, headerSha256: sha256(index.bytes.subarray(0, 256)).toString('hex') };
471
+ }
472
+ const manifest = {
473
+ ...manifestFields,
474
+ ...(indexCommitment ? { index: indexCommitment } : {}),
475
+ segments: segments.map((s) => ({
476
+ kind: s.kind,
477
+ sha256: sha256(s.bytes).toString('hex'),
478
+ bytes: s.bytes.length,
479
+ })),
480
+ };
481
+ const manifestBytes = Buffer.from(canonicalJson(manifest), 'utf8');
482
+ const identity = sha256(manifestBytes);
483
+
484
+ const tableOffset = HEADER_BYTES + manifestBytes.length;
485
+ let cursor = align16(tableOffset + segments.length * TABLE_ENTRY_BYTES);
486
+ const table = Buffer.alloc(segments.length * TABLE_ENTRY_BYTES);
487
+ const placed = [];
488
+ for (let i = 0; i < segments.length; i++) {
489
+ const entry = i * TABLE_ENTRY_BYTES;
490
+ // Known kinds map by name; a producer may carry an extra segment under
491
+ // a name this spec revision does not define by giving its numeric kind
492
+ // explicitly (readers skip unknown kinds, spec 3.3).
493
+ const kindNumber = KINDS[segments[i].kind] ?? segments[i].kindNumber;
494
+ if (!Number.isInteger(kindNumber) || kindNumber < 1 || kindNumber > 0xffffffff) {
495
+ throw new Error(`segment ${i} (${segments[i].kind}) has no kind number`);
496
+ }
497
+ table.writeUInt32LE(kindNumber, entry);
498
+ table.writeBigUInt64LE(BigInt(cursor), entry + 8);
499
+ table.writeBigUInt64LE(BigInt(segments[i].bytes.length), entry + 16);
500
+ placed.push({ offset: cursor, bytes: segments[i].bytes });
501
+ cursor = align16(cursor + segments[i].bytes.length);
502
+ }
503
+ const last = placed[placed.length - 1];
504
+ const fileBytes = last.offset + last.bytes.length;
505
+
506
+ const header = Buffer.alloc(HEADER_BYTES);
507
+ header.writeUInt32LE(MAGIC, 0);
508
+ header.writeUInt32LE(formatVersion, 4);
509
+ header.writeUInt32LE(manifestBytes.length, 8);
510
+ header.writeUInt32LE(segments.length, 12);
511
+ header.writeBigUInt64LE(BigInt(fileBytes), 16);
512
+ identity.copy(header, 24);
513
+
514
+ const fd = fs.openSync(outPath, 'w');
515
+ try {
516
+ fs.writeSync(fd, header, 0, header.length, 0);
517
+ fs.writeSync(fd, manifestBytes, 0, manifestBytes.length, HEADER_BYTES);
518
+ fs.writeSync(fd, table, 0, table.length, tableOffset);
519
+ for (const { offset, bytes } of placed) {
520
+ fs.writeSync(fd, bytes, 0, bytes.length, offset);
521
+ }
522
+ fs.ftruncateSync(fd, fileBytes);
523
+ } finally {
524
+ fs.closeSync(fd);
525
+ }
526
+ return { outPath, fileBytes, formatVersion, identity: identity.toString('hex'), manifest };
527
+ }
@@ -0,0 +1,2 @@
1
+ async function Module(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["c"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("encoder.wasm")}return new URL("encoder.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var _encoder_forward,_malloc,_free,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_encoder_forward=Module["_encoder_forward"]=wasmExports["d"];_malloc=Module["_malloc"]=wasmExports["e"];_free=Module["_free"]=wasmExports["f"];memory=wasmMemory=wasmExports["b"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}
2
+ ;return moduleRtn}export default Module;
@@ -0,0 +1,2 @@
1
+ async function Module(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["c"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("encoder.node.wasm")}return new URL("encoder.node.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var _encoder_forward,_malloc,_free,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_encoder_forward=Module["_encoder_forward"]=wasmExports["d"];_malloc=Module["_malloc"]=wasmExports["e"];_free=Module["_free"]=wasmExports["f"];memory=wasmMemory=wasmExports["b"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}
2
+ ;return moduleRtn}export default Module;
@@ -0,0 +1,8 @@
1
+ // The PSF1 wire-format constants (spec/COMPLETE_PROFILE.md), shared by the
2
+ // reader (index.mjs) and the builder (builder.mjs) so the format has exactly
3
+ // one definition. Environment-neutral.
4
+ export const MAGIC = 0x31465350; // "PSF1"
5
+ export const HEADER_BYTES = 64;
6
+ export const TABLE_ENTRY_BYTES = 48;
7
+ export const KINDS = { index: 1, corpus: 2, 'query-interp': 3, evaluation: 4, lexical: 5 };
8
+ export const KIND_NAMES = Object.fromEntries(Object.entries(KINDS).map(([name, kind]) => [kind, name]));