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.
- package/CHANGELOG.md +868 -0
- package/LICENSE +191 -0
- package/README.md +590 -0
- package/complete/builder.d.ts +82 -0
- package/complete/builder.mjs +527 -0
- package/complete/encoder-kernels/encoder.mjs +2 -0
- package/complete/encoder-kernels/encoder.node.mjs +2 -0
- package/complete/encoder-kernels/encoder.node.wasm +0 -0
- package/complete/encoder-kernels/encoder.wasm +0 -0
- package/complete/format.mjs +8 -0
- package/complete/index.d.ts +289 -0
- package/complete/index.mjs +1124 -0
- package/complete/inline-transformer.mjs +205 -0
- package/complete/lexical.mjs +264 -0
- package/complete/retrieval-abstention.mjs +119 -0
- package/complete/sources.mjs +186 -0
- package/complete/student-abstention.mjs +102 -0
- package/complete/student-embedder.mjs +231 -0
- package/complete/wordpiece.mjs +68 -0
- package/dist/engine.js +2 -0
- package/dist/engine.scalar.js +2 -0
- package/dist/engine.scalar.wasm +0 -0
- package/dist/engine.wasm +0 -0
- package/package.json +168 -0
- package/pancake-artifact-common.js +561 -0
- package/pancake-artifact-range.js +799 -0
- package/pancake-artifact-sketch.js +1040 -0
- package/pancake-artifact.d.ts +109 -0
- package/pancake-artifact.js +31 -0
- package/pancake-core.js +1268 -0
- package/pancake-errors.js +40 -0
- package/pancake-loader.js +66 -0
- package/pancake.d.ts +691 -0
- package/pancake.js +254 -0
- package/pancake.node.mjs +259 -0
- package/pancake.web.d.ts +50 -0
- package/pancake.web.mjs +88 -0
- package/pancake.workerd.mjs +72 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Release notes for the two published packages in this repository,
|
|
4
|
+
`pikelet-wasm` (engine + artifact readers/builders) and
|
|
5
|
+
`create-pancake-search` (scaffolder, `doctor`, Docusaurus plugin). Both are
|
|
6
|
+
pre-1.0: a minor bump may carry breaking changes, and each entry lists them
|
|
7
|
+
first.
|
|
8
|
+
|
|
9
|
+
## Unreleased
|
|
10
|
+
|
|
11
|
+
### Breaking / compatibility
|
|
12
|
+
|
|
13
|
+
- **The project is renamed to Pikelet.** Packages: `pancake-wasm` →
|
|
14
|
+
`pikelet-wasm`, `create-pancake-search` → `pikelet` (one CLI:
|
|
15
|
+
`npx pikelet create|compile|doctor|mcp`; the old `npm create
|
|
16
|
+
pancake-search` flow becomes `npx pikelet create`). New compiles
|
|
17
|
+
default to a `.pikelet` extension. What deliberately does NOT change:
|
|
18
|
+
the wire format — magic bytes, manifest profile strings
|
|
19
|
+
(`pancake-complete-v2` and family), segment kinds, and reader error
|
|
20
|
+
text keep their pancake-era names, so every existing artifact
|
|
21
|
+
identity (the published wiki pack included) stays valid, and
|
|
22
|
+
`.pancake` files remain fully readable (readers dispatch on magic
|
|
23
|
+
bytes, never extension). Library compatibility: `openPancakeFile` and
|
|
24
|
+
the other exported API names are unchanged; `openPikeletFile` is the
|
|
25
|
+
documented alias. Env vars gain `PIKELET_ENCODER_WEIGHTS_URL` /
|
|
26
|
+
`PIKELET_EMBED_WORKERS` (old names still honored). The Docusaurus
|
|
27
|
+
plugin's surface renames with the package (default asset dir
|
|
28
|
+
`pikelet-search/`, mount attribute `data-pikelet-search`, global
|
|
29
|
+
`window.PikeletDocusaurusSearch`, debug attribute
|
|
30
|
+
`data-pikelet-debug`); sites upgrading from the pancake-named plugin
|
|
31
|
+
update their config and any custom CSS hooks. Release-asset URLs in
|
|
32
|
+
code and docs keep their original hosts/paths — GitHub redirects them
|
|
33
|
+
after the repository rename, and the reader resolves and pins
|
|
34
|
+
redirects. Benchmark backend labels in committed result sets
|
|
35
|
+
(`pancake-wasm-int8` and friends) are measurement lineage and stay.
|
|
36
|
+
|
|
37
|
+
- **The Docusaurus plugin's default output is now the complete kind-3
|
|
38
|
+
`.pancake`**, served by the widget over genuine HTTP range reads — the
|
|
39
|
+
sketch profile's execution model, verified in a real browser (zero
|
|
40
|
+
search bytes until the panel opens, then 206 range responses with the
|
|
41
|
+
kind-3 encoder prefetched in the background). Previously the default
|
|
42
|
+
was the deprecated `.pancake-range` plus a Python-trained student
|
|
43
|
+
encoder; the flagship integration shipped the geometry the project's
|
|
44
|
+
own measurements had retired, and the complete-profile mode downloaded
|
|
45
|
+
the whole artifact into memory. Zero-config installs now stage the
|
|
46
|
+
packaged encoder assets (vocab shipped, weights digest-pinned and
|
|
47
|
+
fetched once) — no Python in the toolchain. Opting out: `mode:
|
|
48
|
+
'student'` (or any student option) keeps the small-footprint student
|
|
49
|
+
path, `mode: 'artifact'` keeps the deprecated range output, and both
|
|
50
|
+
log what they are. The widget defers all loading to the first panel
|
|
51
|
+
open, renders section heading-path breadcrumbs on results, and hides
|
|
52
|
+
raw distances unless `data-pancake-debug="1"` is set. The in-repo
|
|
53
|
+
docs-site now exercises the zero-config default.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **`create-pancake-search mcp` serves `.pancake` knowledge packs over
|
|
58
|
+
the Model Context Protocol** — the first concrete piece of the
|
|
59
|
+
knowledge-pack direction: an LLM client (Claude Code, Claude Desktop,
|
|
60
|
+
agent frameworks) attaches compiled packs as a retrieval tool with one
|
|
61
|
+
config line and no vector database, embedding service, or retrieval
|
|
62
|
+
backend. `mcp --pack a.pancake --pack b.pancake` mounts any number of
|
|
63
|
+
packs (named from the manifest, collision-suffixed) and exposes three
|
|
64
|
+
tools over stdio: `search` (per-pack result sections — distances are
|
|
65
|
+
only comparable within one pack — each result carrying pack name,
|
|
66
|
+
immutable manifest identity, title, heading path, anchor, and source,
|
|
67
|
+
so citations can pin the exact knowledge state an answer used),
|
|
68
|
+
`list_packs`, and `get_record` (full integrity-verified chunk by id).
|
|
69
|
+
Calibrated abstention crosses the protocol intact: a pack that cannot
|
|
70
|
+
answer reports `matchQuality: "none"` with zero results and the tool
|
|
71
|
+
result says to admit it rather than guess. The protocol layer is
|
|
72
|
+
hand-rolled newline-delimited JSON-RPC (four methods; no new
|
|
73
|
+
dependency); tool failures return correctable `isError` results, not
|
|
74
|
+
protocol errors. Kind-2 packs (host-encoder) are refused at mount with
|
|
75
|
+
an explanation; kind-1/3 packs are self-contained. The complete
|
|
76
|
+
reader's `info()` now exposes the manifest's pack `name`. Verified
|
|
77
|
+
live against real packs (docs.astro.build + nodejs.org mounted
|
|
78
|
+
together: cross-pack grouping, off-domain abstention, honest
|
|
79
|
+
`unscored` from a pack whose calibration failed its gate) and covered
|
|
80
|
+
by scaffold-e2e checks driving the compiled fixture pack through the
|
|
81
|
+
actual protocol.
|
|
82
|
+
- **`--pack` takes a URL** — the format's native habitat, wired into the
|
|
83
|
+
mount: a URL pack is range-read off dumb HTTP, never downloaded whole.
|
|
84
|
+
One line attaches the 456k-passage Wikipedia pack; mounting transfers
|
|
85
|
+
the ~52 MiB resident slice (measured 6–8 s over the real internet,
|
|
86
|
+
identity-pinned) and each question costs ~127 range requests against
|
|
87
|
+
the 649 MiB file. Both URL and file mounts take `#<sha256>` to pin the
|
|
88
|
+
manifest identity — a mount that finds different bytes refuses to
|
|
89
|
+
serve. `--shelf <file-or-url>` mounts every pack on a static
|
|
90
|
+
`packs.json` listing (`packs/` in this repo — a registry that is also
|
|
91
|
+
just a file: name, location, identity, license per entry, no service,
|
|
92
|
+
no accounts). All packs warm in the background at mount (encoder,
|
|
93
|
+
kernels, first rerank round trips) so the first tool call pays for
|
|
94
|
+
retrieval, not staging. `httpRangeSource` learned two things the wiki
|
|
95
|
+
mount taught the hard way. First, redirect memoization: github.com
|
|
96
|
+
302s every release-asset hit to a signed CDN URL, so ~130 range reads
|
|
97
|
+
per query were charging GitHub's rate-limited web front door ~130
|
|
98
|
+
times — the actual source of the 429 storms, while the blob CDN
|
|
99
|
+
behind the redirect served parallel 206s happily. The reader now
|
|
100
|
+
resolves the redirect chain once, pins the final URL (never appending
|
|
101
|
+
the cache-key param to a signed query string), and re-resolves
|
|
102
|
+
automatically when the signed URL expires (401/403); the front door
|
|
103
|
+
is charged per mount, not per read, and the live wiki demo went from
|
|
104
|
+
unable to finish one query to 6 s mount + ~1 s warm queries on the
|
|
105
|
+
same rate-limited IP. Second, transient statuses (429/502/503/504)
|
|
106
|
+
retry with capped backoff honoring `Retry-After`; `stats.retries` and
|
|
107
|
+
`stats.redirects` count both mechanisms. New `verify_pack` tool runs the
|
|
108
|
+
tests a pack carries inside itself: `compile` now embeds a sample of
|
|
109
|
+
the calibration's retrieval-verified positives as golden queries
|
|
110
|
+
(`evaluation.goldenQueries`), and the wiki pack's abstention probes
|
|
111
|
+
run the same way — an agent audits a newly attached knowledge source
|
|
112
|
+
with tests stored in the file it is auditing (24/24 on the fixture
|
|
113
|
+
pack, over HTTP). `mcp install --client claude-code|claude-desktop`
|
|
114
|
+
writes the client config (merge-safe, `--force` to replace) instead of
|
|
115
|
+
documenting it. `compile --license <SPDX-id>` records a content
|
|
116
|
+
license in the pack manifest (`corpus.provenance.license`), surfaced
|
|
117
|
+
by `list_packs` and `info().license` — packs meant for redistribution
|
|
118
|
+
should carry one. Scaffold e2e grows to 46 checks (verify_pack
|
|
119
|
+
goldens, URL mount over a local range server, wrong-pin refusal,
|
|
120
|
+
install round-trip).
|
|
121
|
+
- **The MCP server is dual-era: current protocol revision 2026-07-28
|
|
122
|
+
served statelessly alongside the legacy initialize handshake.** Modern
|
|
123
|
+
requests carry their protocol version in `_meta` per request; the
|
|
124
|
+
server answers the mandatory `server/discover` (supported versions,
|
|
125
|
+
capabilities, pack-usage instructions, cache hints), decorates modern
|
|
126
|
+
results with `resultType` and server identity, returns
|
|
127
|
+
`UnsupportedProtocolVersionError` (-32022, with the supported list)
|
|
128
|
+
for unknown versions, and marks `tools/list` as a CacheableResult with
|
|
129
|
+
deterministic tool order. Legacy clients (2025-11-25 and earlier) get
|
|
130
|
+
the exact pre-modern shapes, initialize handshake included — era is
|
|
131
|
+
selected per request, as the spec's dual-era rules describe. A
|
|
132
|
+
protocol-only conformance suite (16 checks, stubbed pack, no compile
|
|
133
|
+
or network) runs in `npm test`, so interoperability is verified in CI,
|
|
134
|
+
both eras. Also: an identity pin (`--pack …#sha256`, shelf entries) is
|
|
135
|
+
now enforced by the reader itself via `openPancakeFile`'s new
|
|
136
|
+
`expectedIdentity` option — a mismatched artifact is refused after the
|
|
137
|
+
64-byte header read, before the manifest or any segment is touched
|
|
138
|
+
(conformance asserts exactly one read happens); `mcp install` pins the
|
|
139
|
+
runtime version it writes (`create-pancake-search@<version>`) so
|
|
140
|
+
content-addressed packs are not served by whatever npm publishes next
|
|
141
|
+
month; and `verify_pack` describes itself as self-verification —
|
|
142
|
+
proving the artifact is intact and behaves as built, not that its
|
|
143
|
+
content is true or its publisher trustworthy.
|
|
144
|
+
- **The complete reader's resident scan accelerates through the engine's
|
|
145
|
+
SIMD scan kernel — warm-query p50 at the 456k wiki scale drops from
|
|
146
|
+
~540 ms to ~129 ms** (measured A/B on a local file source; ~110 ms/query
|
|
147
|
+
end to end in the kind-3 acceptance run). This closes the honest
|
|
148
|
+
regression the 0.6.0 wiki-recovery entry named: the correct 192-dim
|
|
149
|
+
sketch geometry made the pure-JS `O(count × sketchDims)` scan the warm
|
|
150
|
+
bottleneck. `openPancakeFile` gains `sketchScanner`: undefined (the
|
|
151
|
+
default) auto-stages `createSketchScanner` in the background in Node
|
|
152
|
+
when the corpus crosses ~16M sketch cells — docs-scale artifacts scan
|
|
153
|
+
in single-digit milliseconds and never load the engine; `false` forces
|
|
154
|
+
the pure-JS scan; a scanner object or async factory
|
|
155
|
+
`(sketchArtifact) => scanner` is the injection point for browsers,
|
|
156
|
+
where the reader cannot import the Node engine entrypoint (the wiki
|
|
157
|
+
web demo injects one — Vite splits the engine glue and wasm into lazy
|
|
158
|
+
assets fetched only when the factory runs, verified staging in a real
|
|
159
|
+
browser). Queries never wait on staging: the JS scan serves until the
|
|
160
|
+
kernel is ready, and permanently if loading fails — acceleration only
|
|
161
|
+
ever changes speed, never results (conformance asserts identical
|
|
162
|
+
`[id, distance]` output, and the wiki recall gates reproduce 96.0% /
|
|
163
|
+
95.2% exactly with the scanner active). `info().residentScan` reports
|
|
164
|
+
`'engine'` or `'js'`. Hardening in the sketch reader: a query whose
|
|
165
|
+
candidate pool C exceeds a scanner's creation-time buffers
|
|
166
|
+
(`maxRerank`) falls back to the JS scan for that query instead of
|
|
167
|
+
silently truncating the pool.
|
|
168
|
+
|
|
169
|
+
### Fixed
|
|
170
|
+
|
|
171
|
+
- **Both benchmark-reproduction defects a fresh clone would hit are
|
|
172
|
+
fixed.** `test-inline.mjs` still pointed its download URL at the
|
|
173
|
+
never-uploaded `artifact-wiki-inline-v3` release asset (its identity
|
|
174
|
+
and size pins were already v4) — a fresh clone's acceptance run
|
|
175
|
+
deleted nothing but 404'd; the URL now matches the pins. And the
|
|
176
|
+
committed churn decay curve (`benchmark_results/churn_scale_100k.json`,
|
|
177
|
+
measured 2026-07-11) still showed recall collapsing 96% → 7.2% over
|
|
178
|
+
five population turnovers — behavior the churn-recovery work of
|
|
179
|
+
2026-08-07 eliminated. Re-measured on the current engine: recall holds
|
|
180
|
+
92.0–96.8% through all five turnovers with a full top-10 at every
|
|
181
|
+
round (compaction 98.8%, fresh-build reference 96.8%); the committed
|
|
182
|
+
result set and its README paragraph now describe the engine as it is.
|
|
183
|
+
|
|
184
|
+
## pikelet-wasm 0.6.0 / create-pancake-search 0.6.0 — 2026-08-31
|
|
185
|
+
|
|
186
|
+
### Breaking / compatibility
|
|
187
|
+
|
|
188
|
+
- **`create-pancake-search` rejects `--runtime complete`** on create and
|
|
189
|
+
rebuild instead of silently building a snapshot project (the generated
|
|
190
|
+
Worker templates only serve snapshot and artifact runtimes; `complete`
|
|
191
|
+
passed validation but was dropped by the config builder). The error
|
|
192
|
+
points at the new command below.
|
|
193
|
+
- **`--include`/`--exclude` against a URL source now error** instead of
|
|
194
|
+
being silently ignored (they never applied to crawls). URL sources get
|
|
195
|
+
their own filters: `--include-url`/`--exclude-url`, matched against the
|
|
196
|
+
URL path with `*` as the only wildcard, and aggregate pages such as
|
|
197
|
+
mdBook's `print.html` are excluded from crawls by default (they
|
|
198
|
+
duplicate a whole site's content past exact-text chunk dedupe).
|
|
199
|
+
`--include-url`/`--exclude-url` against a folder source error
|
|
200
|
+
symmetrically.
|
|
201
|
+
- **The `.pancake-range` profile is deprecated** (contract §9.2). The
|
|
202
|
+
sketch profile superseded it in every measured regime — depth-1
|
|
203
|
+
execution ~5x faster end-to-end over real networks at roughly a third
|
|
204
|
+
of the artifact size — and the complete `.pancake` profile is the
|
|
205
|
+
product surface. Nothing breaks in this release: readers
|
|
206
|
+
(`PancakeRangeArtifact`, `openRangeArtifactFile`) stay supported for
|
|
207
|
+
existing artifacts, and the scaffold's `--runtime artifact` still
|
|
208
|
+
works. The builders (`buildRangeArtifact[File]`) are marked
|
|
209
|
+
`@deprecated` in the type declarations, the scaffold logs a deprecation
|
|
210
|
+
warning when the artifact runtime builds, and no further range format
|
|
211
|
+
revisions are planned. The Docusaurus plugin's default output still
|
|
212
|
+
uses the profile; its `completeProfile` option is the recommended
|
|
213
|
+
configuration.
|
|
214
|
+
- **Crawler fixes from a five-site independent-docs sweep** (Sphinx,
|
|
215
|
+
MkDocs Material, Docusaurus, VitePress, Jekyll): URL filters now gate
|
|
216
|
+
the queue instead of the dequeue, so filtered links no longer consume
|
|
217
|
+
the `--max-pages` budget (a large excluded section — e.g. a versioned
|
|
218
|
+
doc tree — used to starve discovery: docusaurus.io yielded 30 pages of
|
|
219
|
+
a 70-page budget, 69 after the fix); self-recall validation tolerates
|
|
220
|
+
near-duplicate content (original within top 5 passes as degraded with a
|
|
221
|
+
logged hint toward `--exclude-url`, instead of hard-failing the build —
|
|
222
|
+
versioned doc trees put several near-identical chunks at the same point
|
|
223
|
+
in embedding space, where rank 1 among them is arbitrary); crawls that
|
|
224
|
+
discover fewer than 5 pages warn that client-side-rendered navigation
|
|
225
|
+
cannot be followed; the filter-skip log names which filters did the
|
|
226
|
+
skipping.
|
|
227
|
+
|
|
228
|
+
### Added
|
|
229
|
+
|
|
230
|
+
- **The wiki pack's layout and sketch geometry are recovered — recall@10
|
|
231
|
+
rises from 82.8% to 95.2% and requests/query drop from ~458 to ~127.**
|
|
232
|
+
The pack pipeline's blessed build directory (`data-perm`: k-means
|
|
233
|
+
cluster-ordered rows, 192-dim 2:1-pooled sketch) had been deleted from
|
|
234
|
+
disk, and `compile-wiki.mjs` was written against `data-full` — whose
|
|
235
|
+
sketch was 96-dim 4:1 pooling, the geometry the pack's own measurements
|
|
236
|
+
had rejected (−11pt candidate capture). Every complete wiki artifact
|
|
237
|
+
since 2026-08-14 embedded the degraded sketch in unpermuted row order;
|
|
238
|
+
the range-storage ledger's request-count column exposed it. Recovered
|
|
239
|
+
by regenerating `data-perm` from the surviving `layout-perm.npy`
|
|
240
|
+
(ground truth mapped through the inverse permutation and
|
|
241
|
+
lockstep-verified), rebuilding the pack (format-2 sketch, measured
|
|
242
|
+
`recommendedRerank: 200`), and rebuilding the inline artifact. Three
|
|
243
|
+
causes, three fixes:
|
|
244
|
+
1. `compile-wiki.mjs` now prefers `data-perm` and warns loudly when
|
|
245
|
+
building from the unpermuted source;
|
|
246
|
+
2. the correct sketch geometry reaches target recall at C=200 instead
|
|
247
|
+
of C=600 — the same root cause was suppressing recall and inflating
|
|
248
|
+
request counts;
|
|
249
|
+
3. cluster-ordered rows only coalesce at a gap matched to the cluster
|
|
250
|
+
geometry, which nothing ever passed (even the original demo ran at
|
|
251
|
+
the 2 KiB default): the manifest MAY now carry `recommendedGap`
|
|
252
|
+
(spec §4 fetch hints) and the reader honors it unless the caller
|
|
253
|
+
overrides — the wiki artifact ships 16 KiB.
|
|
254
|
+
Measured on the rebuilt artifact (identity `1b180adf…`): augmented
|
|
255
|
+
recall@10 95.2% at C=200 / 97.0% at C=600 (titles 94.8%, hand-written
|
|
256
|
+
98.5%) with the artifact's own inline kernel — the true fp32 reference
|
|
257
|
+
is 96.0% (kind-2, precomputed vectors; `options.encodeQuery` is
|
|
258
|
+
ignored on kind-3 artifacts, so kernel-vs-fp32 comparisons need the
|
|
259
|
+
kind-2 build), a ~0.8pt quantization tax; ~127 range
|
|
260
|
+
requests/query over HTTP (≈ 88 sketch + 24 lexical + scoring), cutting
|
|
261
|
+
R2 cost from ~$165 to ~$46 per million queries. Verified along the
|
|
262
|
+
way: a reverse-Cuthill-McKee ordering extracted from the real graph is
|
|
263
|
+
equal-or-worse than plain corpus order for the rerank access pattern
|
|
264
|
+
in every (C, gap) cell — RCM served only the deprecated traversal
|
|
265
|
+
profile and is not worth porting forward. Honest regressions of the
|
|
266
|
+
correct geometry: open transfer grows to ~52 MiB (the true resident
|
|
267
|
+
sketch) and warm-query p50 roughly doubles to ~500 ms (192-dim
|
|
268
|
+
pure-JS scan — the WASM-scanner integration is now the top follow-up).
|
|
269
|
+
The augmented-vs-vector recall gap shrinks to +0.3pt: correct
|
|
270
|
+
candidate geometry absorbed most of what lexical rescue had been
|
|
271
|
+
compensating for.
|
|
272
|
+
- **Search ledger benchmark** (`scripts/benchmark-search-ledger.mjs`):
|
|
273
|
+
renders artifact performance on the axes a general audience can price —
|
|
274
|
+
time to first answer from nothing, per-query requests/KiB and dollars
|
|
275
|
+
per million queries on parameterized object-store pricing (R2 and S3
|
|
276
|
+
defaults), queries per GB of egress, storage cost, warm-session
|
|
277
|
+
latency, and an infrastructure column that reads zero. Composes
|
|
278
|
+
benchmark-range-storage across named distance presets
|
|
279
|
+
(city/continent/world as injected per-response delays, labeled as a
|
|
280
|
+
model) and emits a Markdown report with the reproduction commands
|
|
281
|
+
embedded. Steady-state economics exclude the first query's one-time
|
|
282
|
+
costs (the deferred encoder download) — charging those to every query
|
|
283
|
+
overstated cost ~50× in the first draft. Representative output at
|
|
284
|
+
docs scale: 42 KiB and 6 requests per query — $2.19 per million
|
|
285
|
+
queries on R2, 23,375 queries per GB, $0.0004/month to store.
|
|
286
|
+
- **Wiki-scale hybrid search: the lexical path now works at 456k chunks,
|
|
287
|
+
with an `augmented` retrieval mode that lifts the pack's headline
|
|
288
|
+
recall.** Three pieces:
|
|
289
|
+
- `buildLexicalSegment` is rebuilt as a counting-sort over typed arrays
|
|
290
|
+
(terms interned to dense indexes, entry arrays preallocated from df
|
|
291
|
+
prefix sums, one scatter pass) — the Map-of-Maps build died in the
|
|
292
|
+
tens of millions of posting entries; the wiki corpus (456,153
|
|
293
|
+
records, ~30M postings, 784,693 terms, 71.2 MiB segment) now builds
|
|
294
|
+
in one pass without heap pressure. Output bytes are unchanged.
|
|
295
|
+
- Lexical segments above 8 MiB open lazily (`openLexicalIndexLazy`):
|
|
296
|
+
only the 64-byte header and the doclen array (4 bytes/record) become
|
|
297
|
+
resident, and term lookup exploits the format's own structure — fnv
|
|
298
|
+
hashes are uniform and the table is sorted by them, so it is an
|
|
299
|
+
implicit interpolation index: predict the entry position from the
|
|
300
|
+
hash, read one ~512-entry window, re-aim for the rare tail. A query
|
|
301
|
+
term costs ~1 window read plus 1 postings read, parallel across
|
|
302
|
+
terms. Verified equivalent to the eager reader entry-for-entry in
|
|
303
|
+
conformance. Lazy reads carry the transitional integrity stance
|
|
304
|
+
(committed via the manifest digest, not individually verified — the
|
|
305
|
+
format-1 sketch-row stance); `info().lexical.lazy` reports it.
|
|
306
|
+
- `retrieval: 'augmented'` joins the mode set: BM25 candidates enter
|
|
307
|
+
the exact rerank but results keep pure distance order. Measured on
|
|
308
|
+
the wiki pack's 200-query pre-registered eval (456k records, exact
|
|
309
|
+
fp32 ground truth): vector 82.8% recall@10 (titles 80.8%), augmented
|
|
310
|
+
**85.3%** (titles 83.6%) — a new best on that eval at identical
|
|
311
|
+
latency — while RRF-ordered hybrid scores 45.9% on this metric, by
|
|
312
|
+
definition: RRF deliberately promotes lexical matches over exact
|
|
313
|
+
nearest neighbors (reciprocal ranks make lexical-rank-8 outscore
|
|
314
|
+
vector-rank-9), which labeled-relevance bakeoffs reward and
|
|
315
|
+
exact-NN-overlap metrics punish. On the docs-scale bakeoffs
|
|
316
|
+
augmented is identical to vector (the sketch's top-C misses nothing
|
|
317
|
+
at 2k chunks), confirming the decomposition: candidate augmentation
|
|
318
|
+
is what pays at scale, rank fusion is what pays on
|
|
319
|
+
keyword-vs-semantic relevance. `hybrid` remains the default;
|
|
320
|
+
`compile-wiki.mjs` now builds the lexical segment into wiki
|
|
321
|
+
artifacts. Range-storage at 456k over loopback HTTP with 25 ms
|
|
322
|
+
injected delay: the 597 MiB artifact opens on 30.0 MiB in 10 range
|
|
323
|
+
GETs (lazy lexical keeps its 71 MiB out of open); first-pass hybrid
|
|
324
|
+
queries fetch ~680 KiB in ~436 ranges (p50 746 ms at parallelism
|
|
325
|
+
64), repeat queries 62 KiB / 5.9 ranges with p50 ~320 ms dominated
|
|
326
|
+
by the pure-JS sketch scan — wiring the WASM sketch scanner into the
|
|
327
|
+
complete reader is the identified next lever.
|
|
328
|
+
- **Kind-3 artifacts open lazily: the inline encoder no longer blocks (or
|
|
329
|
+
bloats) open.** The first range-storage measurements showed cold open
|
|
330
|
+
transferring ~25 MiB in every kind-3 artifact regardless of corpus size
|
|
331
|
+
— the eagerly-read inline-encoder region — which at docs scale meant
|
|
332
|
+
open was effectively a full download. The reader now defers a
|
|
333
|
+
query-interpretation segment above 4 MiB: the 16-byte header and the
|
|
334
|
+
small calibration region are read at open, the encoder region
|
|
335
|
+
downloads in the background from the moment open resolves
|
|
336
|
+
(`prefetchEncoder: false` defers it to the first query), and the first
|
|
337
|
+
query awaits it. Integrity stance is unchanged in substance: the
|
|
338
|
+
whole-segment digest is verified when the bytes arrive, and the
|
|
339
|
+
header/calibration slices used at open are byte-compared against the
|
|
340
|
+
verified segment before any query can return — a mismatch fails every
|
|
341
|
+
query. Until then `info().encoder` serves the identity-verified
|
|
342
|
+
manifest declaration (marked `deferred`) and `encoderVerified` is null
|
|
343
|
+
(unknown, not unverified). Kinds 1/2 and small segments keep the
|
|
344
|
+
eager one-read path byte for byte; the spec's open semantics (§4)
|
|
345
|
+
document the deferral. Measured over loopback HTTP with 25 ms
|
|
346
|
+
injected delay: open drops from 25.15 MiB / ~860 ms to 0.63 MiB /
|
|
347
|
+
~200 ms, with the encoder's ~25 MiB paid by the first query (~900 ms)
|
|
348
|
+
or overlapped with user think-time under the default prefetch.
|
|
349
|
+
Retroactive: every published kind-3 artifact benefits without
|
|
350
|
+
recompiling. The bench gains `--no-prefetch-encoder` and a first-query
|
|
351
|
+
line so the open/first-query split is visible.
|
|
352
|
+
- **HTTP range-storage benchmark** (`npm run bench:range-storage`):
|
|
353
|
+
serves an artifact from a loopback server implementing HEAD + byte
|
|
354
|
+
ranges and opens it through `httpRangeSource()` — the same 206 path a
|
|
355
|
+
CDN or object store serves — measuring what the storage model actually
|
|
356
|
+
costs, separate from retrieval relevance. Reports per artifact: open
|
|
357
|
+
transfer (bytes/ranges/time before the first query), first-pass
|
|
358
|
+
per-query bytes, range counts, and latency percentiles, a repeat pass
|
|
359
|
+
isolating cache effects, and RSS/heap deltas; each artifact runs in a
|
|
360
|
+
fresh child process so memory numbers do not accumulate across corpus
|
|
361
|
+
sizes, `--server-delay-ms` injects a fixed per-response delay as a
|
|
362
|
+
rough RTT model, and the server cross-checks that bytes requested by
|
|
363
|
+
the range source equal bytes served. `--json`/`--csv` emit
|
|
364
|
+
machine-readable results. First measurements (kind-3 docs artifacts,
|
|
365
|
+
20 queries, hybrid): open transfer is ~25 MiB in 9 range GETs
|
|
366
|
+
regardless of corpus size — the eager inline-encoder segment dominates
|
|
367
|
+
cold start; first-pass queries cost 40–127 KiB in 5–16 parallel
|
|
368
|
+
ranges (p50 78–100 ms with 25 ms injected delay, so roughly 2–3
|
|
369
|
+
sequential request waves); repeat queries transfer zero bytes — the
|
|
370
|
+
row/record caches absorb everything, p50 ~20 ms of pure compute.
|
|
371
|
+
- **Ingestion/anchor conformance suite** (`test/ingestion/`, in npm
|
|
372
|
+
test): fixture corpora covering nested and duplicate headings, explicit
|
|
373
|
+
`{#custom-id}` anchors, headings inside fenced code, Unicode and setext
|
|
374
|
+
headings, inline code/links in headings, snake_case identifiers,
|
|
375
|
+
frontmatter titles/slugs, MDX components around headings, oversized
|
|
376
|
+
section splitting, and HTML pages with authored `id` attributes — each
|
|
377
|
+
asserting the full chain a search result depends on: document URL,
|
|
378
|
+
headingPath, anchor, final href, chunk text. Anchor generation is
|
|
379
|
+
parity-tested against the real github-slugger (the library Docusaurus
|
|
380
|
+
renders with) via a recorded 26-heading battery plus a live comparison
|
|
381
|
+
when the library is installed; two divergences are documented as
|
|
382
|
+
deliberate (multi-space heading runs, JSX tags stripped by MDX).
|
|
383
|
+
Building the suite surfaced and fixed four real defects: the slugger
|
|
384
|
+
was ASCII-only (github-slugger keeps Unicode letters — "Über uns" must
|
|
385
|
+
slug to "über-uns"), dash runs were wrongly collapsed ("C++ & C#" slugs
|
|
386
|
+
to "c--c"), heading underscores were stripped (destroying snake_case
|
|
387
|
+
identifiers like ACTION_QUERY_PARAMS), and setext headings
|
|
388
|
+
(`Title\n===`) were not recognized at all. Compiling nodejs.org's API
|
|
389
|
+
docs surfaced a fifth: minified HTML with unquoted attributes
|
|
390
|
+
(`href=fs.html`, `id=fsreadfilesync…`) was invisible to the link and
|
|
391
|
+
anchor extractors — the crawl found 2 pages of 30.
|
|
392
|
+
- **The v1 spec no longer lists hybrid search as a non-goal**:
|
|
393
|
+
docs/CREATE_PANCAKE_SEARCH_SPEC.md's non-goals entry for "Hybrid BM25 +
|
|
394
|
+
vector scoring" is struck through with a pointer to the shipped lexical
|
|
395
|
+
segment (spec §3.8) and the `query({ retrieval })` modes.
|
|
396
|
+
- **Section-aware ingestion and chunking.** Markdown/MDX documents are
|
|
397
|
+
parsed into their heading structure before chunking — heading lines are
|
|
398
|
+
recognized only outside code fences, explicit `{#custom-id}` heading ids
|
|
399
|
+
win over derived slugs, and duplicate headings get GitHub-style `-1`
|
|
400
|
+
suffixes — and HTML crawls section on `h1`–`h6` tags, preserving the
|
|
401
|
+
page's own `id` anchors. Chunking keeps a section together when it fits
|
|
402
|
+
(up to 1.6x the target size), subdivides at paragraph boundaries when it
|
|
403
|
+
does not, and merges sub-25-token sections into their neighbor. Every
|
|
404
|
+
chunk now carries the fields the corpus schema always reserved but
|
|
405
|
+
nothing populated: `headingPath` (ancestor headings ending in the
|
|
406
|
+
section's own) and `anchor`, so results deep-link to the exact section
|
|
407
|
+
(`Routing > Dynamic routes > Rest parameters`, `#rest-parameters`)
|
|
408
|
+
instead of pointing at a page. A leading h1 that restates the document
|
|
409
|
+
title stays out of heading paths. Plain-text files keep the flat token
|
|
410
|
+
windows.
|
|
411
|
+
- **Retrieval modes and a retrieval-quality bakeoff.** `query()` accepts
|
|
412
|
+
`retrieval: 'hybrid' | 'vector' | 'lexical'` (hybrid remains the
|
|
413
|
+
default; the single-signal modes exist for measurement and debugging —
|
|
414
|
+
abstention scores identically in every mode), and
|
|
415
|
+
`scripts/bakeoff-retrieval.mjs` runs a query set with expected sections
|
|
416
|
+
through all three modes, reporting success@1/@3 and MRR@5 split into
|
|
417
|
+
semantic questions and exact lookups. Measured on a 120-page Astro docs
|
|
418
|
+
crawl (2,123 section chunks, 24 queries): exact lookups score
|
|
419
|
+
success@3 0.67 vector-only vs 1.00 BM25-only vs 0.92 hybrid; semantic
|
|
420
|
+
questions 0.75 / 0.67 / 0.83 — hybrid is the only mode competitive on
|
|
421
|
+
both classes. A 9-doc repo-docs corpus agrees on exact lookups (hybrid
|
|
422
|
+
success@1 0.71 vs 0.57 for either single mode) with semantic within
|
|
423
|
+
noise at that sample size.
|
|
424
|
+
- **Hybrid search: complete artifacts carry a BM25 lexical index**
|
|
425
|
+
(segment kind 5, layout `bm25-v1`, spec §3.8) and the reader fuses
|
|
426
|
+
lexical and vector retrieval. The builder writes a static inverted
|
|
427
|
+
index over the corpus records (hash-addressed fixed-width term table,
|
|
428
|
+
varint postings, ~113 KiB for a 132-record docs corpus); `compile`
|
|
429
|
+
includes it by default. At query time the BM25 top matches join the
|
|
430
|
+
sketch's exact rerank as extra candidates — scored by true vector
|
|
431
|
+
distance, which is what rescues known-item lookups the sketch scan's
|
|
432
|
+
top-C misses — and the final result order fuses the distance and BM25
|
|
433
|
+
rankings by reciprocal rank (RRF, constant 60, untuned). Lexical hits
|
|
434
|
+
under a third of the best lexical score are dropped before fusion: idf
|
|
435
|
+
collapses common query terms into a near-tied mass that carries no
|
|
436
|
+
ranking information. Abstention is unaffected — it scores the
|
|
437
|
+
distance-sorted top-k exactly as calibrated; fusion changes result
|
|
438
|
+
order only. Readers that predate the segment kind skip it (spec §3.3)
|
|
439
|
+
and serve vector-only queries from the same file; artifacts without
|
|
440
|
+
the segment behave exactly as before (`info().lexical` reports which).
|
|
441
|
+
`PancakeSketchArtifact.search` gains `extraCandidates` (external row
|
|
442
|
+
ids joining the exact rerank) and `fullRerankOutput` (return every
|
|
443
|
+
reranked candidate, not the top k) to support this. The format is
|
|
444
|
+
lazy-friendly by construction (absolute offsets, hash-sorted
|
|
445
|
+
fixed-width term table) so a range-reading opener for wiki-scale packs
|
|
446
|
+
needs no format revision; the phase-1 reader loads the segment eagerly
|
|
447
|
+
and verifies its digest at open. Covered by 9 new conformance checks
|
|
448
|
+
including a deterministic rescue case (a known-item query whose
|
|
449
|
+
encoder vector is pure noise still surfaces the right record) and
|
|
450
|
+
lexical-segment tamper rejection.
|
|
451
|
+
- **The crawler follows the seed's own redirects** — HTTP and meta-refresh,
|
|
452
|
+
bounded at 5 hops, each hop logged — and the final URL defines the crawl
|
|
453
|
+
origin. Sites routinely send their root to a canonical host or localized
|
|
454
|
+
landing page (docs.astro.build's root is an HTTP 200 whose only content
|
|
455
|
+
is `<meta http-equiv="refresh" ...>`); the crawl previously discovered
|
|
456
|
+
one empty page and failed with an error blaming the content. Every other
|
|
457
|
+
fetch keeps the strict redirect-skip, and a contentless meta-refresh
|
|
458
|
+
page found mid-crawl is now followed through the normal frontier filters
|
|
459
|
+
instead of consuming page budget as an empty document. Exercised by a
|
|
460
|
+
new scaffold-e2e check against a local server chaining a 301 into a
|
|
461
|
+
meta refresh.
|
|
462
|
+
- **Grounding coverage weighs word informativeness and scores more
|
|
463
|
+
passages**: corpus-common words (chunk document frequency above ~5%,
|
|
464
|
+
shipped as a bloom in `asset.coverage.commonBloom`) count at 1/3 weight
|
|
465
|
+
— a docs corpus's "templates"/"support" no longer ground a query on
|
|
466
|
+
their own, which was letting roadmap-style questions score strong — and
|
|
467
|
+
coverage is the max over the top 5 passages instead of the top 1, so a
|
|
468
|
+
paraphrased query gets more chances to find the passage sharing its
|
|
469
|
+
vocabulary while recombination negatives' source chunks still each
|
|
470
|
+
ground only their own half. Hard-negative CV AUC across the three test
|
|
471
|
+
corpora: docs repro 0.808 → 0.831, wiki 0.918 → 0.924, Astro docs
|
|
472
|
+
crawl 0.877 → 0.887, with recombination-kind AUCs at 0.91–0.98.
|
|
473
|
+
- **The abstention model gains a grounding feature** (`self-templates-v3`):
|
|
474
|
+
the fraction of the query's content words present in the top retrieved
|
|
475
|
+
passage's text, fit as a fifth term in the same logistic model. Every
|
|
476
|
+
prior feature measures topic similarity (three distances plus membership
|
|
477
|
+
in a corpus-wide bloom), so "the corpus discusses this area" and "this
|
|
478
|
+
passage answers this question" were indistinguishable by construction —
|
|
479
|
+
a query can retrieve topically-adjacent content at a closer distance
|
|
480
|
+
than genuine controls while none of its terms appear in the returned
|
|
481
|
+
passage. The term is serialized as a supplemental `asset.coverage` field
|
|
482
|
+
rather than a `features[]` entry, so readers that predate it score the
|
|
483
|
+
topic-only model against the same thresholds (a conservative
|
|
484
|
+
degradation) instead of feeding an unknown feature name NaN into the
|
|
485
|
+
logistic — and the reader now degrades any non-finite score to
|
|
486
|
+
`unscored` rather than letting NaN compare false against both
|
|
487
|
+
thresholds and answer everything. Scoring an artifact that carries the
|
|
488
|
+
term hydrates the top-1 record before the verdict (the page is hot for
|
|
489
|
+
result hydration when answered; an abstained query costs one extra
|
|
490
|
+
read); word rules ship in the asset so builder and reader cannot drift.
|
|
491
|
+
Measured on the hard-negative repro corpus: hard-negative CV AUC 0.772
|
|
492
|
+
→ 0.808, recombination-kind AUC 0.891, all five same-field near-domain
|
|
493
|
+
probes now abstain outright, and the calibration summary adds
|
|
494
|
+
`cvAucHardByKind`. A/B-verified on a second, structurally opposite
|
|
495
|
+
corpus (250 distinct-topic wiki articles, low vocabulary overlap):
|
|
496
|
+
hard-negative CV AUC 0.853 without the term → 0.907 with it, pooled
|
|
497
|
+
0.903 → 0.937, with paraphrased no-title-word controls staying strong
|
|
498
|
+
under both. Known residual limits: a single-content-word query can be
|
|
499
|
+
spuriously grounded by a topically adjacent passage containing that
|
|
500
|
+
word, and attribute questions about present entities (entity words
|
|
501
|
+
grounded, attribute absent) still score strong under both models.
|
|
502
|
+
- **compile's self-calibration now trains against hard negatives**
|
|
503
|
+
(`self-templates-v2`), fixing artifacts that confidently answered
|
|
504
|
+
unanswerable in-domain questions. The v1 calibrator's negatives were all
|
|
505
|
+
easy — off-domain bank and gibberish, low known-token fraction, distant
|
|
506
|
+
retrieval — so the fitted model handed the vocabulary feature a dominant
|
|
507
|
+
weight (+4.1 standardized) and scored any in-domain query answerable:
|
|
508
|
+
measured on a docs corpus, 6 of 8 unanswerable in-domain probes returned
|
|
509
|
+
`strong` at p 0.75–0.94 while the pooled CV AUC read 0.998. v2 adds two
|
|
510
|
+
hard-negative classes with high known-token fractions: held-out
|
|
511
|
+
documents (excluded from calibration searches via `searchFiltered`, then
|
|
512
|
+
asked about — the wiki calibrator's held-out-shard trick made
|
|
513
|
+
corpus-generic) and cross-chunk recombinations (corpus words no single
|
|
514
|
+
document lexically supports). Hard-negative labels are verified by
|
|
515
|
+
retrieval (a held-out topic covered by a near-duplicate document is
|
|
516
|
+
dropped, not mislearned); overlapping off-domain queries are kept as
|
|
517
|
+
eval-only rows and reported instead of silently discarded. The gate now
|
|
518
|
+
also requires hard-negative CV AUC ≥ 0.75 — the pooled number is
|
|
519
|
+
dominated by the easy classes and cannot see this failure — and the
|
|
520
|
+
`weak` threshold rises to the hard-negative ceiling, so overlapped
|
|
521
|
+
queries surface with a caveat instead of full confidence. Same probes
|
|
522
|
+
after the change: 0 of 8 unanswerable queries score `strong` (6 weak,
|
|
523
|
+
2 none), answerable controls unchanged at `strong`. The calibration
|
|
524
|
+
summary adds per-class counts and `cvAucEasy`/`cvAucHard`.
|
|
525
|
+
- **`create-pancake-search compile`** builds a complete kind-3 `.pancake`
|
|
526
|
+
artifact from `--source <path|url>` and stops — no project, no Worker,
|
|
527
|
+
no Cloudflare. It chunks and embeds the corpus with the packaged inline
|
|
528
|
+
MiniLM encoder (auto-fetched, digest-pinned, when not present), measures
|
|
529
|
+
the rerank operating point, and writes one file (`--out`, default
|
|
530
|
+
`search.pancake`) openable with `pikelet-wasm/complete` on any runtime.
|
|
531
|
+
Takes `--name`, `--max-pages`, `--include`/`--exclude`, `--force`.
|
|
532
|
+
Exercised by the scaffold e2e (compile → open → query, plus the
|
|
533
|
+
`--runtime complete` rejection).
|
|
534
|
+
- **compile self-calibrates abstention** (`src/calibrate.mjs`), so compiled
|
|
535
|
+
artifacts answer `matchQuality` strong/weak/none out of the box instead
|
|
536
|
+
of "unscored". Positives are templated title questions plus content-word
|
|
537
|
+
queries, each verified by retrieval before it counts; negatives are a
|
|
538
|
+
built-in off-domain bank (overlap-dropped against the positive median
|
|
539
|
+
d0) and bloom-checked gibberish; the fit is the wiki calibrator's
|
|
540
|
+
logistic model over (d0, margin, mean10, known_frac). Thresholds are
|
|
541
|
+
placed on percentiles (5th of positives, 95th of negatives) rather than
|
|
542
|
+
the wiki calibrator's raw extremes — auto-generated queries put junk in
|
|
543
|
+
both tails and the saturated fit collapses geometric means of them —
|
|
544
|
+
with hard a quarter of the way up the gap: a false abstain hides
|
|
545
|
+
results, a false weak shows them with a caveat, so the weak verdict
|
|
546
|
+
owns the uncertain band. Skips with a logged reason (and ships unscored)
|
|
547
|
+
when positives or negatives run short or the cross-validated AUC misses
|
|
548
|
+
the 0.85 gate — the gate uses a deterministic 5-fold cross-validation
|
|
549
|
+
over the generated queries, since the fit AUC is in-sample; the embedded
|
|
550
|
+
asset records method, query counts, and both numbers (`fitAuc`,
|
|
551
|
+
`cvAuc`). New
|
|
552
|
+
flags: `--calibration <file>` embeds a prebuilt retrieval-signals-v1
|
|
553
|
+
asset, `--skip-calibration` opts out. The Docusaurus plugin default is
|
|
554
|
+
unchanged (auto-calibration is opt-in via `runtime.calibration: 'auto'`
|
|
555
|
+
outside compile).
|
|
556
|
+
- **Inline-transformer passage embedding runs on a worker pool** past 32
|
|
557
|
+
chunks (one WASM kernel + weight blob per worker, chunks dealt by index
|
|
558
|
+
so builds stay deterministic — pool and sequential builds return
|
|
559
|
+
bit-identical distances). 571 chunks: 13 min → 3 min on 8 cores.
|
|
560
|
+
`PANCAKE_SEARCH_EMBED_WORKERS` overrides the worker count (0/1 forces
|
|
561
|
+
sequential); environments without worker support fall back to the
|
|
562
|
+
sequential path with a logged reason.
|
|
563
|
+
|
|
564
|
+
## pikelet-wasm 0.5.0 / create-pancake-search 0.5.0 — 2026-08-26
|
|
565
|
+
|
|
566
|
+
### Breaking / compatibility
|
|
567
|
+
|
|
568
|
+
- **Sketch artifact format 2 (per-row integrity) is now what the builders
|
|
569
|
+
write by default.** The vectors region is laid out as interleaved blocks
|
|
570
|
+
— a digest page of truncated per-row SHA-256s (16 rows × 16 bytes by
|
|
571
|
+
default) ahead of each block's rows — anchored by a 32-byte-per-block
|
|
572
|
+
page-hash table at the end of the resident prefix, so `residentSha256`
|
|
573
|
+
(and, in a complete artifact, `index.headerSha256` and the identity)
|
|
574
|
+
covers it. `fetchRows` verifies every page against the table and every
|
|
575
|
+
row against its digest slot **on the read that fetches it**, in both the
|
|
576
|
+
full and staged micro tiers; tampered rows fail the query instead of
|
|
577
|
+
silently altering results. Geometry per the 2026-08-25 measurement:
|
|
578
|
+
~0% query-latency overhead, +4.2% file size. Readers ≤ 0.4.0 reject
|
|
579
|
+
format-2 sketches ("Unsupported sketch artifact version"); the new
|
|
580
|
+
reader opens format 1 and 2 (`verify: false` opts out, reported).
|
|
581
|
+
`exportSketchArtifact(..., { rowIntegrity: false })` still emits
|
|
582
|
+
format 1; `rowsPerBlock` / `rowDigestBytes` tune the geometry.
|
|
583
|
+
- Complete artifacts embedding a format-2 sketch report
|
|
584
|
+
`info().indexRowIntegrity: 'per-row-sha256'`; with that, every byte that
|
|
585
|
+
influences a query is authenticated per read and the spec's transitional
|
|
586
|
+
integrity stance applies only to format-1-sketch artifacts.
|
|
587
|
+
- New `test/sketch_row_integrity.js` in `npm test`.
|
|
588
|
+
- `create-pancake-search` has no behavior changes of its own; generated
|
|
589
|
+
projects now depend on `pikelet-wasm@^0.5.0` so the artifacts the
|
|
590
|
+
scaffolder builds (format-2 sketches included) open on the installed
|
|
591
|
+
reader.
|
|
592
|
+
|
|
593
|
+
## pikelet-wasm 0.4.0 / create-pancake-search 0.4.0 — 2026-08-25
|
|
594
|
+
|
|
595
|
+
### Breaking / compatibility
|
|
596
|
+
|
|
597
|
+
- **Complete artifact format 2** (`pancake-complete-v2`, header
|
|
598
|
+
`formatVersion` 2) is now what the builder writes by default
|
|
599
|
+
(`buildCorpusSegment`, `PROFILE_V2`): the corpus segment carries one
|
|
600
|
+
SHA-256 per record behind a page table the manifest commits to, so every
|
|
601
|
+
hydrated record is verified on its own range read. Readers ≤ 0.3.0 reject
|
|
602
|
+
format-2 files explicitly ("unsupported .pancake format version 2"); the
|
|
603
|
+
0.4.0 reader opens format 1 and 2 and reports the stance in
|
|
604
|
+
`info().corpusIntegrity`. `create-pancake-search` complete-mode builds
|
|
605
|
+
(`search.pancake`) and `examples/05` compiles now produce format 2;
|
|
606
|
+
released wiki artifacts stay format 1 and remain readable.
|
|
607
|
+
- `openPancakeFile` on kind-2 artifacts now runs `options.encodeQuery`
|
|
608
|
+
against the declaration's verification vectors **at open** and refuses
|
|
609
|
+
the open on disagreement (dimension or tolerance); declarations without
|
|
610
|
+
vectors are refused unless `allowUnverifiedEncoder: true`. Previously the
|
|
611
|
+
host encoder was trusted.
|
|
612
|
+
- Reader hardening: every artifact-derived read is validated (safe
|
|
613
|
+
integers, overflow, `fileBytes`, source size, per-open `maxReadBytes`
|
|
614
|
+
budget — default 256 MiB, `Infinity` → 2 GiB backstop — and
|
|
615
|
+
`maxRecordBytes` per record, default 16 MiB) and must return exactly the
|
|
616
|
+
bytes requested; u64 fields above 2^53−1, unknown query-interp versions,
|
|
617
|
+
duplicate segment kinds, and inconsistent offsets tables are rejected.
|
|
618
|
+
Unknown segment kinds are skipped (spec 3.3). Files that previously
|
|
619
|
+
opened only because a check was missing may now be refused with a
|
|
620
|
+
specific error.
|
|
621
|
+
|
|
622
|
+
### Added
|
|
623
|
+
|
|
624
|
+
- `info().formatVersion`, `profile`, `encoderVerified`, `corpusIntegrity`;
|
|
625
|
+
`record(id)`; `verifyHostEncoder()`, `SUPPORTED_PROFILES`,
|
|
626
|
+
`CORPUS_LAYOUT_V2` exports; `assemblePancakeFile` accepts unknown kinds
|
|
627
|
+
via `kindNumber` and returns `formatVersion`.
|
|
628
|
+
- `test/complete_profile.mjs` — deterministic complete-profile reader
|
|
629
|
+
conformance (77 checks: hostile input, verification, integrity, format-1
|
|
630
|
+
compatibility, kind-1 goldens from the committed 03 assets), run by
|
|
631
|
+
`npm test` and therefore CI.
|
|
632
|
+
- spec/COMPLETE_PROFILE.md Draft 2.
|
|
633
|
+
- `create-pancake-search --help` / `-h` print usage (previously "Missing
|
|
634
|
+
value for --help").
|
|
635
|
+
|
|
636
|
+
### Fixed (artifact layer hardening, from the 2026-08-21 external review)
|
|
637
|
+
|
|
638
|
+
- `PancakeRangeArtifact.openFile` / `PancakeSketchArtifact.openFile` close
|
|
639
|
+
their file descriptor when the open is rejected (previously every corrupt
|
|
640
|
+
artifact leaked one fd); `NodeFileRangeSource` releases the fd if `fstat`
|
|
641
|
+
fails after `open`, and refuses reads after `close()`.
|
|
642
|
+
- Range and sketch `search()` apply the core engine's query contract:
|
|
643
|
+
plain-array coordinates must be numbers (no `'1'` coercion), every
|
|
644
|
+
component must be finite for every metric (L2 no longer returns
|
|
645
|
+
`distance: NaN`), dimension checked; `k`, `rerank`, `efSearch` must be
|
|
646
|
+
positive integers and `k` / the sketch candidate pool are capped to the
|
|
647
|
+
row count before sizing any allocation (`search(q, 1e9)` over 300 rows no
|
|
648
|
+
longer attempts a 1e9-slot allocation).
|
|
649
|
+
- `parseUint8Snapshot` validates `M`/`M0`, bounds each level's adjacency
|
|
650
|
+
count by `M0`/`M`, checks the bytes it claims are present, and range-checks
|
|
651
|
+
neighbor ids — all before allocating that level's edge array.
|
|
652
|
+
- `httpRangeSource`: after the one-time full-download fallback, reads are
|
|
653
|
+
served from memory with no further requests (previously every read still
|
|
654
|
+
issued a fetch).
|
|
655
|
+
- Complete reader: `id` and `distance` are reserved result names and always
|
|
656
|
+
come from the search (a record's own fields no longer overwrite them);
|
|
657
|
+
`close()` is idempotent (no `EBADF` on a second call) and queries after
|
|
658
|
+
close are refused; `query({ k })` rejects 0 / negative / fractional `k`
|
|
659
|
+
instead of silently defaulting to 5.
|
|
660
|
+
- `create-pancake-search` loaders prefer the in-repo `pikelet-wasm` when
|
|
661
|
+
running inside the monorepo (a stale installed copy higher in the tree
|
|
662
|
+
no longer shadows the checkout); npm consumers are unaffected.
|
|
663
|
+
- Packaging: the native benchmark baselines (`faiss-node`, `hnswlib-node`,
|
|
664
|
+
`usearch`) and the `@emnapi/*` pins are no longer `optionalDependencies`
|
|
665
|
+
of `pikelet-wasm` — consumers no longer attempt those native builds; they
|
|
666
|
+
live in `benchmarks/package.json` (`cd benchmarks && npm install`). The
|
|
667
|
+
unused self devDependency on `pikelet-wasm` is removed.
|
|
668
|
+
- Sketch `search()` with a candidate pool equal to the row count (small
|
|
669
|
+
artifact, or `k`/`rerank` ≥ count) no longer runs the resident selection
|
|
670
|
+
scan — every row is a candidate for the exact rerank — instead of the
|
|
671
|
+
2·count² slot operations the selection loop degenerated to.
|
|
672
|
+
- `parseUint8Snapshot` reads version-1 uint8 payloads with their 4-byte
|
|
673
|
+
edges (id only), matching the engine's deserializer; v1 payloads were
|
|
674
|
+
previously misparsed as the v2 {id, distance} layout.
|
|
675
|
+
- `rerank: 0` / `efSearch: 0` keep their historical "use the default"
|
|
676
|
+
meaning (`undefined`, `null`, `0` → default; anything else must be a
|
|
677
|
+
positive integer).
|
|
678
|
+
- `parseUint8Snapshot` bounds `M`/`M0` to the engine's own envelope
|
|
679
|
+
(`M ∈ [2,128]`, `M ≤ M0 ≤ 256`).
|
|
680
|
+
- Complete reader: a kind-3 encoder's WASM buffers are released when the
|
|
681
|
+
open fails after the encoder was created; a digest-page read that fails
|
|
682
|
+
for a transport reason is not cached as a rejection (the next hydration
|
|
683
|
+
retries). `httpRangeSource` refuses a 206 whose `Content-Range` is not the
|
|
684
|
+
requested slice.
|
|
685
|
+
- **Format 2 manifests now commit to the embedded sketch's 256-byte header
|
|
686
|
+
(`index.headerSha256`, required)**, and the reader verifies it at open:
|
|
687
|
+
the sketch's metric/dim/count and its `residentSha256`/`vectorsSha256`
|
|
688
|
+
are anchored to the artifact identity instead of living only in the
|
|
689
|
+
segment's own (attacker-rewritable) header. Previously a flipped metric
|
|
690
|
+
word changed search semantics under an unchanged identity. On every
|
|
691
|
+
format the reader also cross-checks the sketch metric against the
|
|
692
|
+
manifest. Format-2 identities change (format 2 was never released).
|
|
693
|
+
- `httpRangeSource`: a 206 without a valid `Content-Range` naming the
|
|
694
|
+
requested slice is refused (RFC 9110 requires the header) and the body
|
|
695
|
+
length must match it; the full-download fallback cap is enforced on
|
|
696
|
+
bytes actually received (a chunked 200 aborts as soon as it streams past
|
|
697
|
+
the cap) instead of trusting `Content-Length`/HEAD; new `cacheKeyParam`
|
|
698
|
+
option (`null` for signed-URL hosts keeps the URL untouched, default
|
|
699
|
+
`'r'` keeps the Chromium cache-lock workaround).
|
|
700
|
+
- Stable external ids are bounded to the uint32 the v3 snapshot stores:
|
|
701
|
+
`add()` refuses (INDEX_LIMIT) before the engine mutates and `addBatch()`
|
|
702
|
+
preflights the whole batch, instead of assigning ids the snapshot would
|
|
703
|
+
truncate into an unrestorable export.
|
|
704
|
+
- `canonicalJson()` follows JSON semantics for unsupported values
|
|
705
|
+
(undefined/function object properties omitted, array entries become
|
|
706
|
+
null) instead of emitting invalid JSON.
|
|
707
|
+
- `evaluation()` is typed `Promise<Record<string, unknown> | null>` (it
|
|
708
|
+
returns null with no evaluation segment) and refuses a non-object
|
|
709
|
+
segment at runtime.
|
|
710
|
+
- Encoder verification validates the *expected* embeddings too: a
|
|
711
|
+
verification vector whose expectation is malformed (strings, null, NaN,
|
|
712
|
+
wrong length, missing) now fails verification instead of passing
|
|
713
|
+
vacuously — `x - "x"` is NaN and `NaN > maxDiff` is false, so such
|
|
714
|
+
vectors previously compared nothing while still reporting
|
|
715
|
+
`encoderVerified: true`. Applies to kind-2 host verification and kind-3
|
|
716
|
+
inline verification (shared `validateExpectedEmbedding`, exported).
|
|
717
|
+
- Lazy rerank rows: new `verifyIndexVectors: true` open option and
|
|
718
|
+
`verifyVectors()` method on the complete reader run the full pass
|
|
719
|
+
against the identity-anchored `vectorsSha256`; `info().vectorsVerified`
|
|
720
|
+
reports the state. Documented prominently (README, spec section 6):
|
|
721
|
+
without the pass, rows that feed reranking are committed but not
|
|
722
|
+
verified per read; per-row commitments are spec section 8 question 6.
|
|
723
|
+
- Builder inputs may be plain `Uint8Array`s, as the typings always said:
|
|
724
|
+
`buildCorpusSegment(FromBuffers)`, `buildQueryInterpSegment`, and
|
|
725
|
+
`buildInlineTransformerEncoderSegment` no longer call Buffer-only
|
|
726
|
+
`.copy()` on caller-supplied bytes.
|
|
727
|
+
- New `test/artifact_hardening.js` (66 checks) in `npm test`; 41 more checks
|
|
728
|
+
in `test/complete_profile.mjs`.
|
|
729
|
+
|
|
730
|
+
### Internal
|
|
731
|
+
|
|
732
|
+
- `pancake-artifact.js` is now a thin entry over `pancake-artifact-common.js`,
|
|
733
|
+
`pancake-artifact-range.js`, `pancake-artifact-sketch.js` (same exports,
|
|
734
|
+
same typings; the three files ship in the tarball).
|
|
735
|
+
- `create-pancake-search/src/cli.mjs` is split into `common`, `ingest`,
|
|
736
|
+
`embed`, `complete-build`, `scaffold`, `build` modules; `cli.mjs` keeps
|
|
737
|
+
the commands and re-exports `buildSearchAssets`, `fetchInlineEncoderWeights`,
|
|
738
|
+
`CliError` for the Docusaurus plugin. See the README's "Package layout".
|
|
739
|
+
|
|
740
|
+
## pikelet-wasm 0.3.0 — 2026-08-20
|
|
741
|
+
|
|
742
|
+
### Breaking / compatibility
|
|
743
|
+
|
|
744
|
+
- `buildRangeArtifact` now emits `.pancake-range` **format v3** (whole-segment
|
|
745
|
+
SHA-256 digests for the id map, router, and base segments stamped in the
|
|
746
|
+
header). Readers in 0.2.1 and earlier only accept v1/v2 and reject v3 files
|
|
747
|
+
with `SNAPSHOT_INVALID` ("Unsupported Pancake range artifact version").
|
|
748
|
+
The 0.3.0 reader still opens v1/v2 files (structurally validated,
|
|
749
|
+
unverified — they carry no digests).
|
|
750
|
+
- Snapshots whose payload declares an unknown future format version are
|
|
751
|
+
rejected at import instead of being parsed as the newest known layout
|
|
752
|
+
(JS and engine). Every version a released writer has emitted still
|
|
753
|
+
imports.
|
|
754
|
+
- `Pancake.create()` rejects configurations whose eager arena estimate
|
|
755
|
+
exceeds the 1.5 GiB wasm32 budget with `WASM_ALLOCATION_FAILED` (estimate,
|
|
756
|
+
budget, and backend formula in `details`) instead of aborting the WASM
|
|
757
|
+
instance from inside the engine.
|
|
758
|
+
- Types: `SketchArtifactBuildManifest.file` is now optional (absent on the
|
|
759
|
+
bytes-in/bytes-out builder).
|
|
760
|
+
|
|
761
|
+
### Added
|
|
762
|
+
|
|
763
|
+
- **`pikelet-wasm/complete`** — the one-file `.pancake` reader
|
|
764
|
+
(`openPancakeFile`, `httpRangeSource`, the inline-transformer host and its
|
|
765
|
+
verification-vector API), and **`pikelet-wasm/complete/builder`** — Node
|
|
766
|
+
assembly (`assemblePancakeFile`, segment builders,
|
|
767
|
+
`measureRecommendedRerank`, `loadInlineEncoderKernel`). Typed via
|
|
768
|
+
`complete/index.d.ts` / `complete/builder.d.ts`. The PSF1 wire format has one
|
|
769
|
+
definition (`complete/format.mjs`) shared by reader and builder; the encoder
|
|
770
|
+
kernels (`complete/encoder-kernels/`) ship here.
|
|
771
|
+
- Kind-3 (inline transformer) declarations are validated against the
|
|
772
|
+
compiled kernel before the first forward (layout, dim, exact blob length),
|
|
773
|
+
and their verification vectors (contract §4.4 mode 1, including a
|
|
774
|
+
>128-token windowed-pool probe) run automatically on open; `verify: false`
|
|
775
|
+
opts out.
|
|
776
|
+
- Range artifacts: `integrity` digests, `segmentVerified`, `verify` open
|
|
777
|
+
option, `verifyBaseSegment()`; sketch artifacts: `vectorsVerified`,
|
|
778
|
+
`verifyVectors()`, `clearCache()`, `buildSketchArtifactBytes()` (no
|
|
779
|
+
filesystem).
|
|
780
|
+
- Layered read budgets: `maxReadBytes` (open-path reads, default 256 MiB,
|
|
781
|
+
strict) and `maxRangeBytes` (coalesced query-path reads split at 16 MiB
|
|
782
|
+
record/row-aligned boundaries; also accepted by `prefetch()`). The 2 GiB
|
|
783
|
+
backstop remains. Segment verification streams through Node's incremental
|
|
784
|
+
SHA-256 in bounded chunks.
|
|
785
|
+
|
|
786
|
+
### Changed / fixed
|
|
787
|
+
|
|
788
|
+
- Complete-artifact `open()` fetches its post-segment-table extents as one
|
|
789
|
+
wave: 8 dependent range rounds down to 5.
|
|
790
|
+
- `manifest.corpus.records` is validated before it sizes a read; the
|
|
791
|
+
full-download fallback refuses when the content length is unknown
|
|
792
|
+
(previously an unbounded read on Range-ignoring hosts).
|
|
793
|
+
- AVX-512 native builds no longer drop `uint8_dot` to scalar (the kernel's
|
|
794
|
+
AVX2 branch is now taken on AVX-512 targets); the native addon and an ISA
|
|
795
|
+
compile sweep now run in CI.
|
|
796
|
+
- Both engine variants rebuilt (`dist/`).
|
|
797
|
+
|
|
798
|
+
## create-pancake-search 0.3.1 — 2026-08-21
|
|
799
|
+
|
|
800
|
+
### Fixed
|
|
801
|
+
|
|
802
|
+
- Generated projects depended on `pikelet-wasm: ^0.2.0`, which `npm install`
|
|
803
|
+
resolves to 0.2.1 — a reader that rejects the format-v3 `.pancake-range`
|
|
804
|
+
this version's scaffolder builds. Every `--runtime artifact` project
|
|
805
|
+
generated by 0.3.0 therefore answered `/search` with `SNAPSHOT_INVALID`
|
|
806
|
+
("Unsupported Pancake range artifact version") until its dependency was
|
|
807
|
+
bumped by hand. The generated `package.json` now inherits this package's
|
|
808
|
+
own `pikelet-wasm` range (`^0.3.0`) instead of carrying a second hardcoded
|
|
809
|
+
one. Projects already generated by 0.3.0: change the dependency to
|
|
810
|
+
`^0.3.0` and `npm install`.
|
|
811
|
+
|
|
812
|
+
## create-pancake-search 0.3.0 — 2026-08-20
|
|
813
|
+
|
|
814
|
+
Requires `pikelet-wasm >= 0.3.0` (uses `pikelet-wasm/complete`).
|
|
815
|
+
|
|
816
|
+
### Breaking / compatibility
|
|
817
|
+
|
|
818
|
+
- The 24.3 MiB MiniLM encoder weights are no longer in the tarball
|
|
819
|
+
(package size ~170 kB, was ~25.7 MB). The CLI and the Docusaurus plugin
|
|
820
|
+
download them once on first kind-3 use from the `inline-encoder-v1`
|
|
821
|
+
release asset, verify the pinned SHA-256, and write them to the configured
|
|
822
|
+
`weightsPath`; `PANCAKE_ENCODER_WEIGHTS_URL` points at a mirror.
|
|
823
|
+
Custom-named weights are never fetched.
|
|
824
|
+
- Kind-3 declarations now state `maxTokens: 128` (the embedded kernel's
|
|
825
|
+
real limit; earlier builds declared the teacher's 256) and
|
|
826
|
+
`longInputs: 'windowed-mean-pool'`, and carry verification vectors.
|
|
827
|
+
Artifacts built by 0.2.0 remain readable; rebuild to get the corrected
|
|
828
|
+
declaration.
|
|
829
|
+
|
|
830
|
+
### Added
|
|
831
|
+
|
|
832
|
+
- `create-pancake-search doctor <url>` — probes an artifact hosting URL
|
|
833
|
+
(Range/206, Range with a cache-key query string, negotiated protocol
|
|
834
|
+
h1/h2/h3, ETag, median RTT, artifact magic/identity) and exits 1 on any
|
|
835
|
+
failing check.
|
|
836
|
+
- Complete profile (kind 3) output: `runtime.mode: "complete"` /
|
|
837
|
+
`profile: "kind3"` with `embedding.mode: "inline-transformer"` emits a
|
|
838
|
+
single `search.pancake`. Docusaurus plugin option `completeProfile`
|
|
839
|
+
(`enabled`, optional `vectors`, `vocab`, `weights`, `calibration`,
|
|
840
|
+
`model`, `maxTokens`) writes `build/pancake-search/search.pancake` and
|
|
841
|
+
serves it with a bundled kind-3 browser reader; without precomputed
|
|
842
|
+
vectors the build embeds chunks through the packaged encoder.
|
|
843
|
+
- Docusaurus plugin `sourcePath` / `sourceRouteBase`: index the site's
|
|
844
|
+
markdown/MDX sources instead of rendered HTML, with front-matter `slug`
|
|
845
|
+
honored and `NN-` sidebar prefixes stripped in result routes.
|
|
846
|
+
- Generated Worker README and config schema updated for the new modes.
|
|
847
|
+
|
|
848
|
+
### Fixed
|
|
849
|
+
|
|
850
|
+
- `pikelet-wasm/artifact` and `pikelet-wasm/complete` resolve from the site,
|
|
851
|
+
the plugin, or the monorepo sibling, with an actionable error otherwise
|
|
852
|
+
(npm consumers of the Docusaurus plugin previously failed at bundle time).
|
|
853
|
+
|
|
854
|
+
## Earlier releases (summary)
|
|
855
|
+
|
|
856
|
+
- **pikelet-wasm 0.2.1 / create-pancake-search 0.2.0 — 2026-08-12.**
|
|
857
|
+
pancake-wasm: hostile-input hardening on deserialize, `packVersion`
|
|
858
|
+
stamping. create-pancake-search: first publish of the Docusaurus plugin and
|
|
859
|
+
`--mode student` (corpus-specific PSTU query encoder bundled in the
|
|
860
|
+
Worker, no Workers AI binding).
|
|
861
|
+
- **pikelet-wasm 0.2.0 / create-pancake-search 0.1.0 — 2026-07-30.**
|
|
862
|
+
Pancake 0.2 contract: `restore`, `inspectSnapshot`, per-query `efSearch`,
|
|
863
|
+
`PancakeError` / `PANCAKE_ERROR_CODES`; mutable-ef and matrix helpers
|
|
864
|
+
removed; `pikelet-wasm/artifact` search-artifact readers/builders.
|
|
865
|
+
create-pancake-search: initial scaffolder (snapshot and artifact
|
|
866
|
+
runtimes).
|
|
867
|
+
- **pikelet-wasm 0.1.0 — 2026-06-27.** Initial release: HNSW engine in WASM
|
|
868
|
+
(float and uint8 backends) for Node, browsers, and Cloudflare Workers.
|