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 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.