pikelet-wasm 0.7.0 → 0.8.1
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/CHANGELOG.md +340 -4
- package/NOTICE +5 -0
- package/README.md +226 -475
- package/complete/builder.d.ts +4 -4
- package/complete/builder.mjs +26 -9
- package/complete/encoder-kernels/encoder.node.wasm +0 -0
- package/complete/encoder-kernels/encoder.wasm +0 -0
- package/complete/fusion.mjs +72 -0
- package/complete/index.d.ts +34 -7
- package/complete/index.mjs +258 -129
- package/complete/inline-transformer.mjs +119 -10
- package/complete/lexical.mjs +11 -11
- package/complete/retrieval-abstention.mjs +136 -31
- package/complete/sources.mjs +22 -1
- package/complete/wordpiece.mjs +22 -0
- package/dist/engine.js +1 -1
- package/dist/engine.scalar.js +1 -1
- package/dist/engine.scalar.wasm +0 -0
- package/dist/engine.wasm +0 -0
- package/package.json +47 -82
- package/{pancake-artifact-common.js → src/artifact/pikelet-artifact-common.js} +42 -42
- package/{pancake-artifact-range.js → src/artifact/pikelet-artifact-range.js} +43 -36
- package/{pancake-artifact-sketch.js → src/artifact/pikelet-artifact-sketch.js} +105 -68
- package/{pancake-artifact.d.ts → src/artifact/pikelet-artifact.d.ts} +11 -11
- package/{pancake-artifact.js → src/artifact/pikelet-artifact.js} +10 -10
- package/{pancake-core.js → src/core/pikelet-core.js} +141 -140
- package/{pancake-errors.js → src/errors/pikelet-errors.js} +9 -8
- package/{pancake.d.ts → src/pikelet.d.ts} +55 -55
- package/{pancake.js → src/pikelet.js} +47 -47
- package/{pancake.node.mjs → src/pikelet.node.mjs} +49 -49
- package/{pancake.web.d.ts → src/pikelet.web.d.ts} +13 -13
- package/{pancake.web.mjs → src/pikelet.web.mjs} +24 -24
- package/{pancake.workerd.mjs → src/pikelet.workerd.mjs} +22 -22
- /package/{pancake-loader.js → src/loader/pikelet-loader.js} +0 -0
package/complete/builder.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pikelet-wasm/complete/builder — Node-only assembly of complete .
|
|
2
|
+
* pikelet-wasm/complete/builder — Node-only assembly of complete .pikelet
|
|
3
3
|
* artifacts (spec/COMPLETE_PROFILE.md). The wire-format constants here are
|
|
4
4
|
* the same objects the reader uses (complete/format.mjs).
|
|
5
5
|
*/
|
|
@@ -15,8 +15,8 @@ export declare function canonicalJson(value: unknown): string;
|
|
|
15
15
|
export declare const align16: (n: number) => number;
|
|
16
16
|
|
|
17
17
|
/** Manifest profile strings and the header formatVersion each one implies. */
|
|
18
|
-
export declare const PROFILE_V1: '
|
|
19
|
-
export declare const PROFILE_V2: '
|
|
18
|
+
export declare const PROFILE_V1: 'pikelet-complete-v1';
|
|
19
|
+
export declare const PROFILE_V2: 'pikelet-complete-v2';
|
|
20
20
|
export declare const FORMAT_VERSIONS: Record<string, number>;
|
|
21
21
|
/** Corpus layout name for format 2 (per-record digests behind a page table). */
|
|
22
22
|
export declare const CORPUS_LAYOUT_V2: 'records-v2';
|
|
@@ -52,7 +52,7 @@ export declare function buildInlineTransformerEncoderSegment(input: {
|
|
|
52
52
|
* buildCorpusSegment() corpus); mismatches throw. Segments with a kind this
|
|
53
53
|
* spec does not name must carry an explicit kindNumber (readers skip them).
|
|
54
54
|
*/
|
|
55
|
-
export declare function
|
|
55
|
+
export declare function assemblePikeletFile(
|
|
56
56
|
manifestFields: Record<string, unknown> & { profile: string; corpus: { records: number } & Record<string, unknown> },
|
|
57
57
|
segments: Array<{ kind: string; bytes: Buffer | Uint8Array; kindNumber?: number }>,
|
|
58
58
|
outPath: string
|
package/complete/builder.mjs
CHANGED
|
@@ -81,8 +81,25 @@ export function buildQueryInterpSegment(kind, encoderBytes, calibrationBytes) {
|
|
|
81
81
|
// and skips a small function-word stopword list — readers need no
|
|
82
82
|
// coordination with the list, because an unindexed query token simply finds
|
|
83
83
|
// no postings.
|
|
84
|
+
//
|
|
85
|
+
// Interrogative words (how/does/what/who/why/where/when/which/do/did) are
|
|
86
|
+
// deliberately in this list, not just the base one: without them, a
|
|
87
|
+
// natural-language question's own scaffolding becomes real, indexable BM25
|
|
88
|
+
// content — a passage containing literal dialogue like "How does Georgiana
|
|
89
|
+
// get on, Darcy?" then wins a high lexical score against the query "How
|
|
90
|
+
// does Elizabeth initially react to Mr. Darcy?" purely from "how does" and
|
|
91
|
+
// "darcy" co-occurring, with zero topical relevance. Confirmed live: this
|
|
92
|
+
// pushed a genuinely correct chunk (ranked #2 by the vector search alone)
|
|
93
|
+
// completely out of the fused top-10, replaced by RRF-boosted noise. Not
|
|
94
|
+
// borrowed wholesale from calibrate.mjs's COVERAGE_STOPWORDS, which also
|
|
95
|
+
// excludes domain words ("tell", "explain", "overview", "history",
|
|
96
|
+
// "definition") that are legitimate lexical CONTENT on many real corpora
|
|
97
|
+
// (a docs site literally titled "History") — this list only adds the
|
|
98
|
+
// unambiguous interrogative set, since the lexical index (unlike coverage)
|
|
99
|
+
// also indexes passage text, not just the query.
|
|
84
100
|
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'
|
|
101
|
+
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',
|
|
102
|
+
'how', 'does', 'did', 'do', 'what', 'who', 'why', 'where', 'when', 'which']);
|
|
86
103
|
const lexTokenize = (text) => (String(text).toLowerCase().match(/[a-z0-9']+/g) || [])
|
|
87
104
|
.filter((w) => w.length >= 2 && w.length <= 32 && !LEXICAL_STOPWORDS.has(w));
|
|
88
105
|
|
|
@@ -228,7 +245,7 @@ export function buildLexicalSegment(texts) {
|
|
|
228
245
|
};
|
|
229
246
|
}
|
|
230
247
|
|
|
231
|
-
// Corpus layout v1 (format 1, profile
|
|
248
|
+
// Corpus layout v1 (format 1, profile pikelet-complete-v1): count + offsets +
|
|
232
249
|
// records, integrity by whole-segment digest only. Kept for producing
|
|
233
250
|
// format-1 files (compatibility fixtures, readers that predate format 2);
|
|
234
251
|
// new artifacts use buildCorpusSegment() below.
|
|
@@ -248,7 +265,7 @@ export function buildCorpusSegmentFromBuffers(records) {
|
|
|
248
265
|
return out;
|
|
249
266
|
}
|
|
250
267
|
|
|
251
|
-
// Corpus layout v2 (format 2, profile
|
|
268
|
+
// Corpus layout v2 (format 2, profile pikelet-complete-v2): per-record
|
|
252
269
|
// integrity that a reader can check on the single range read that hydrates
|
|
253
270
|
// a record, without fetching the rest of the segment (contract 4.1 / 7):
|
|
254
271
|
//
|
|
@@ -352,7 +369,7 @@ export async function measureRecommendedRerank({
|
|
|
352
369
|
async read(offset, length) { return bytes.subarray(offset, offset + length); },
|
|
353
370
|
async close() {},
|
|
354
371
|
};
|
|
355
|
-
const sketch = await artifactModule.
|
|
372
|
+
const sketch = await artifactModule.PikeletSketchArtifact.open(source);
|
|
356
373
|
try {
|
|
357
374
|
const count = sketch.count;
|
|
358
375
|
if (count === 0) throw new Error('cannot measure rerank on an empty sketch');
|
|
@@ -432,16 +449,16 @@ function selfQueriesFromSnapshot(graph, maxQueries) {
|
|
|
432
449
|
return queries;
|
|
433
450
|
}
|
|
434
451
|
|
|
435
|
-
// Container format version follows the profile string:
|
|
436
|
-
// files carry corpus layout v1 and header version 1;
|
|
452
|
+
// Container format version follows the profile string: pikelet-complete-v1
|
|
453
|
+
// files carry corpus layout v1 and header version 1; pikelet-complete-v2
|
|
437
454
|
// files carry corpus layout records-v2 (buildCorpusSegment) and header
|
|
438
455
|
// version 2, so readers that predate per-record integrity reject them
|
|
439
456
|
// explicitly instead of misreading the corpus tables.
|
|
440
|
-
export const PROFILE_V1 = '
|
|
441
|
-
export const PROFILE_V2 = '
|
|
457
|
+
export const PROFILE_V1 = 'pikelet-complete-v1';
|
|
458
|
+
export const PROFILE_V2 = 'pikelet-complete-v2';
|
|
442
459
|
export const FORMAT_VERSIONS = { [PROFILE_V1]: 1, [PROFILE_V2]: 2 };
|
|
443
460
|
|
|
444
|
-
export function
|
|
461
|
+
export function assemblePikeletFile(manifestFields, segments, outPath) {
|
|
445
462
|
const formatVersion = FORMAT_VERSIONS[manifestFields.profile];
|
|
446
463
|
if (!formatVersion) {
|
|
447
464
|
throw new Error(`manifest.profile must be ${PROFILE_V1} or ${PROFILE_V2}, got ${manifestFields.profile}`);
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Vector/lexical rank fusion for hybrid retrieval — the one definition the
|
|
2
|
+
// reader (index.mjs), the calibrator (pikelet/src/calibrate.mjs, which fits
|
|
3
|
+
// coverage on the passage fusion ranks first) and the BEIR ladder
|
|
4
|
+
// (benchmarks/beir/query-E.mjs) all call, so a fit-time or benchmark
|
|
5
|
+
// ranking is the ranking the reader serves.
|
|
6
|
+
//
|
|
7
|
+
// Reciprocal-rank fusion over the exact-reranked vector order and the BM25
|
|
8
|
+
// order: score = 1/(K + vRank) + lexicalWeight/(K + lexRank), lexical term
|
|
9
|
+
// present only for records BM25 returned. Every lexical candidate joins the
|
|
10
|
+
// vector pool (the reader reranks them exactly), so it always has a vRank.
|
|
11
|
+
//
|
|
12
|
+
// The vector-margin guard: when the vector top-1 leads the top-2 by at
|
|
13
|
+
// least guardMargin of its own distance, it keeps rank 1 regardless of
|
|
14
|
+
// fusion. Without it, with K=60 any record that appears in both lists
|
|
15
|
+
// outranks a vector-only rank 1 (1/(60+v) + w/(60+l) > 1/60 for any finite
|
|
16
|
+
// v when w=1), so a query whose terms match many chunks — a character name
|
|
17
|
+
// in a novel — fills the top ranks with term matches and buries the
|
|
18
|
+
// passage the encoder placed first. Measured on three sets with
|
|
19
|
+
// scripts/fusion-sim.mjs before any default changed.
|
|
20
|
+
|
|
21
|
+
export const FUSION_DEFAULTS = Object.freeze({
|
|
22
|
+
rrfK: 60,
|
|
23
|
+
lexicalWeight: 0.5,
|
|
24
|
+
guardMargin: 0.05,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Array<{id: number, distance: number}>} vectorOrder hits sorted by
|
|
29
|
+
* exact distance ascending (the rerank output), including every lexical
|
|
30
|
+
* candidate.
|
|
31
|
+
* @param {number[]} lexicalIds record ids in BM25 order (after the cutoff).
|
|
32
|
+
* @param {{rrfK?: number, lexicalWeight?: number, guardMargin?: number}} [options]
|
|
33
|
+
* @returns the same hit objects in fused order.
|
|
34
|
+
*/
|
|
35
|
+
export function fuseCandidates(vectorOrder, lexicalIds, options = {}) {
|
|
36
|
+
const rrfK = options.rrfK ?? FUSION_DEFAULTS.rrfK;
|
|
37
|
+
const lexicalWeight = options.lexicalWeight ?? FUSION_DEFAULTS.lexicalWeight;
|
|
38
|
+
const guardMargin = options.guardMargin ?? FUSION_DEFAULTS.guardMargin;
|
|
39
|
+
if (!(rrfK > 0) || !(lexicalWeight >= 0) || !(guardMargin >= 0)) {
|
|
40
|
+
throw new Error('fuseCandidates: rrfK must be > 0, lexicalWeight and guardMargin >= 0');
|
|
41
|
+
}
|
|
42
|
+
if (!lexicalIds.length) return vectorOrder.slice();
|
|
43
|
+
const lexRank = new Map();
|
|
44
|
+
lexicalIds.forEach((id, i) => { if (!lexRank.has(id)) lexRank.set(id, i); });
|
|
45
|
+
let fused = vectorOrder
|
|
46
|
+
.map((hit, vRank) => ({
|
|
47
|
+
hit,
|
|
48
|
+
score: 1 / (rrfK + vRank) + (lexRank.has(hit.id) ? lexicalWeight / (rrfK + lexRank.get(hit.id)) : 0),
|
|
49
|
+
}))
|
|
50
|
+
.sort((a, b) => (b.score - a.score) || (a.hit.distance - b.hit.distance))
|
|
51
|
+
.map((entry) => entry.hit);
|
|
52
|
+
if (guardMargin > 0 && vectorOrder.length > 1) {
|
|
53
|
+
const [first, second] = vectorOrder;
|
|
54
|
+
// The margin is relative to the top-1's own distance, which needs a
|
|
55
|
+
// positive denominator. first.distance === 0 is not a degenerate case
|
|
56
|
+
// to skip but the STRONGEST possible top-1 -- cosine distance is
|
|
57
|
+
// 1 - dot, so a query matching passage text verbatim scores exactly 0
|
|
58
|
+
// -- and gating on `first.distance > 0` disabled the guard for exactly
|
|
59
|
+
// the hit it exists to protect (a worse top-1 at 0.001 was guarded, a
|
|
60
|
+
// perfect one at 0 was not). Any second distance strictly greater than
|
|
61
|
+
// a zero first is an infinite relative lead, so the guard fires.
|
|
62
|
+
// Distances are non-negative here (cosine/L2 as returned by the
|
|
63
|
+
// rerank); a metric admitting negatives would need an absolute margin.
|
|
64
|
+
const lead = first.distance > 0
|
|
65
|
+
? (second.distance - first.distance) / first.distance
|
|
66
|
+
: (second.distance > 0 ? Infinity : 0);
|
|
67
|
+
if (lead >= guardMargin && fused[0] !== first) {
|
|
68
|
+
fused = [first, ...fused.filter((hit) => hit !== first)];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return fused;
|
|
72
|
+
}
|
package/complete/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pikelet-wasm/complete — reader for complete .
|
|
3
|
-
* (spec/COMPLETE_PROFILE.md, profile
|
|
2
|
+
* pikelet-wasm/complete — reader for complete .pikelet artifacts
|
|
3
|
+
* (spec/COMPLETE_PROFILE.md, profile pikelet-complete-v1). Opens any kind
|
|
4
4
|
* (1 student-inline, 2 declared-external, 3 inline-transformer) from a file
|
|
5
5
|
* path in Node or a range source anywhere.
|
|
6
6
|
*/
|
|
@@ -74,7 +74,7 @@ export interface CompleteSearch {
|
|
|
74
74
|
identity: string;
|
|
75
75
|
/** Container header formatVersion (1 or 2). */
|
|
76
76
|
formatVersion: number;
|
|
77
|
-
/** Manifest profile: '
|
|
77
|
+
/** Manifest profile: 'pikelet-complete-v1' | 'pikelet-complete-v2'. */
|
|
78
78
|
profile: string;
|
|
79
79
|
/**
|
|
80
80
|
* Human-readable pack name from the identity-verified manifest
|
|
@@ -158,6 +158,13 @@ export interface CompleteSearch {
|
|
|
158
158
|
parallelism?: number;
|
|
159
159
|
gap?: number;
|
|
160
160
|
maxRangeBytes?: number;
|
|
161
|
+
/**
|
|
162
|
+
* A matchQuality: 'none' verdict withholds results by default. Set
|
|
163
|
+
* true to see the raw retrieval anyway (e.g. to inspect why the
|
|
164
|
+
* classifier abstained) — matchQuality and confidence are unaffected,
|
|
165
|
+
* this only controls whether results is populated. Default false.
|
|
166
|
+
*/
|
|
167
|
+
showAbstained?: boolean;
|
|
161
168
|
}): Promise<CompleteQueryResult>;
|
|
162
169
|
/** Hydrate one corpus record by id (verified per record on format 2). */
|
|
163
170
|
record(id: number): Promise<Record<string, unknown>>;
|
|
@@ -166,7 +173,7 @@ export interface CompleteSearch {
|
|
|
166
173
|
close(): Promise<void>;
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
export declare function
|
|
176
|
+
export declare function openPikeletFile(
|
|
170
177
|
input: string | CompleteRangeSource,
|
|
171
178
|
options?: {
|
|
172
179
|
/**
|
|
@@ -238,6 +245,29 @@ export declare function openPancakeFile(
|
|
|
238
245
|
|
|
239
246
|
/** Header formatVersion -> manifest profile accepted by this reader. */
|
|
240
247
|
export declare const SUPPORTED_PROFILES: Readonly<Record<number, string>>;
|
|
248
|
+
|
|
249
|
+
/** Rank-fusion parameters shared by the reader, the calibrator and the BEIR ladder. */
|
|
250
|
+
export interface FusionOptions {
|
|
251
|
+
/** Reciprocal-rank-fusion constant (default 60). */
|
|
252
|
+
rrfK?: number;
|
|
253
|
+
/** Weight of the lexical (BM25) rank term relative to the vector term (default 0.5). */
|
|
254
|
+
lexicalWeight?: number;
|
|
255
|
+
/**
|
|
256
|
+
* Relative distance margin (top-2 minus top-1, over top-1) at or above which
|
|
257
|
+
* the vector top-1 keeps rank 1 (default 0.05; 0 disables the guard).
|
|
258
|
+
*/
|
|
259
|
+
guardMargin?: number;
|
|
260
|
+
}
|
|
261
|
+
export declare const FUSION_DEFAULTS: Readonly<Required<FusionOptions>>;
|
|
262
|
+
/**
|
|
263
|
+
* Fuse the exact-reranked vector order (every lexical candidate included) with the BM25 order.
|
|
264
|
+
* Returns the same hit objects, reordered.
|
|
265
|
+
*/
|
|
266
|
+
export declare function fuseCandidates<T extends { id: number; distance: number }>(
|
|
267
|
+
vectorOrder: T[],
|
|
268
|
+
lexicalIds: number[],
|
|
269
|
+
options?: FusionOptions,
|
|
270
|
+
): T[];
|
|
241
271
|
export declare const CORPUS_LAYOUT_V2: 'records-v2';
|
|
242
272
|
/**
|
|
243
273
|
* Check a host encoder against a kind-2 declaration's verification vectors
|
|
@@ -284,6 +314,3 @@ export declare function buildInlineTestVectors(embedder: unknown, texts?: string
|
|
|
284
314
|
text: string; windows: number; embedding: number[]; tolerance: number;
|
|
285
315
|
}>>;
|
|
286
316
|
export declare function verifyInlineTestVectors(embedder: unknown): Promise<{ checked: number }>;
|
|
287
|
-
|
|
288
|
-
/** Documented alias for openPancakeFile — the project renamed to Pikelet; the reader's original export stays for compatibility. */
|
|
289
|
-
export declare const openPikeletFile: typeof openPancakeFile;
|