Scope: which browsers, operating systems, and runtimes are supported, what the tested CPU-only baseline is, where
cpuScorer: 'ufuzzy'fits, and which named suite proves each path.
The reference contract is reproducible without any GPU and is the behavior every other path is compared against.
import { SearchIndex, DocumentIndex } from 'webgpu-search';
// Reference baseline: explicit CPU, exact scorer.
const index = await SearchIndex.create(records, { preferGpu: false });
const res = await index.search('auth', {
mode: 'substring',
cpuScorer: 'exact', // default; may be omitted
});Contract pins:
| Item | Value |
|---|---|
| Index options | preferGpu: false (explicit CPU even on large corpora) |
| Scorer | cpuScorer: 'exact' (default; shared exact-scorer.ts pipeline) |
engine |
'cpu' |
fallbackReason (flat SearchIndex) |
'prefer-cpu' for fuzzy / substring; 'unsupported-mode' for token / prefix / typo-tolerant queries (exact-only WGSL kernels are CPU-by-design) |
fallbackReason (DocumentIndex) |
same rule (validateSearchOptions + routeEngine, document-index.ts) |
| Echoes | profileId: 'unicode-default', scoringVersion: 'parity-v1', cpuScorer: 'exact' |
| Ordering | descending normalized integer scores; deterministic tie-breakers (ranking.ts); exact order iff hasOverflow === false, else totalMatches + hasOverflow + score multiset |
| Limits | limit default 50, clamped 1..8192 (RESULT_LIMIT_MAX); maxResults is a backwards-compatible alias (limit wins) |
| Query cap | 128 post-normalization tokens (QUERY_TOKENS_MAX); over-limit throws QueryTooLongError unless onQueryTooLong: 'cpu-fallback' |
| Reproducibility | deterministic: identical corpus + query + mode + options produce identical index / score order; integer scores only |
preferGpu: false forces CPU even above threshold (default 30,000);
callers must not rely on undocumented threshold: Infinity to stay on CPU.
Omitting cpuScorer is identical to cpuScorer: 'exact' — the pin suite
asserts default-equals-explicit.
Normative text pipeline and version caps live in
docs/text-normalization.md; snapshot wire compat lives in
docs/snapshot-format.md.
// Opt-in only. CPU-only. Scores are explicitly non-conforming.
const res = await index.search('auth', {
mode: 'fuzzy', // or 'substring'
cpuScorer: 'ufuzzy',
});Rules (enforced fail-closed in search-index.ts / document-index.ts):
cpuScorer: 'ufuzzy'never routes to WebGPU and is excluded from the differential matrix (scripts/test-parity-harness.ts,packages/webgpu-search/src/cpu-engine.tsquarantine note).preferGpu: true + cpuScorer: 'ufuzzy'throwsIncompatibleOptionErroratsearch()— usepreferGpu: falseorcpuScorer: 'exact'.cpuScorer: 'ufuzzy'supports only exactfuzzy/substringwithout typo tolerance.token/prefixmodes ortypoTolerance.enabledthrowIncompatibleOptionError(Use cpuScorer:'exact').- Successful ufuzzy queries return
engine: 'cpu',cpuScorer: 'ufuzzy',fallbackReason: 'cpu-algorithm-requested'. - uFuzzy scores are fabricated rank scores (
1000 - rank * 2inCPUEngine.searchWithUFuzzy); substring-mode ufuzzy requests are served by the native scan, not uFuzzy ranking. Do not compare ufuzzy numbers againstexact/ GPU numbers. - Unknown
cpuScorervalues throwIncompatibleOptionError(normalizeCpuScorer,text-profile.ts). Removed aliases (cpuAlgorithm/'parity') throw with a migration message.
Hosts that need comparable, portable scores must use the baseline
(cpuScorer: 'exact').
GpuDevicePool.isSupported() (gpu-device-pool.ts) reports WebGPU
availability as typeof navigator !== 'undefined' && !!navigator.gpu.
All library imports are SSR-safe (guarded navigator / document access);
absence of WebGPU always falls back to the Section 1 CPU contract with a
documented fallbackReason (webgpu-unsupported, device-request-failed,
below-threshold, prefer-cpu, device-lost, gpu-execution-error,
unsupported-mode, cpu-algorithm-requested, query-too-long,
memory-budget-exceeded).
| Browser | Windows | macOS | Linux | Android / ChromeOS | WebGPU path | CPU fallback |
|---|---|---|---|---|---|---|
| Chrome (stable) | Supported (D3D12) | Supported (Metal) | Supported (Vulkan; headless via SwiftShader, see below) | Supported (Android); ChromeOS supported | preferGpu: true / large corpora route to WebGPU; device via GpuDevicePool.acquireDevice or injected options.device |
Section 1 baseline, always available |
| Edge (Chromium) | Supported (same engine as Chrome) | Supported | n/a (no Edge Linux WebGPU target) | Supported (Android) | Same as Chrome | Section 1 baseline |
| Safari 18+ | n/a | Supported (Metal) | n/a | iOS 18+ supported | WebGPU where navigator.gpu is present; older Safari reports webgpu-unsupported and serves CPU |
Section 1 baseline |
| Firefox (stable) | CPU-only baseline | CPU-only baseline | CPU-only baseline | CPU-only baseline | Not a supported WebGPU path (Nightly / flag-only builds are untested and treated as webgpu-unsupported) |
Section 1 baseline (the supported Firefox path) |
| Headless Chromium (CI, SwiftShader / LLVMpipe) | Covered by test:browser gate |
Covered by test:browser gate |
Covered by test:browser gate (primary CI path) |
n/a | API-present but software-rendered: numbers are pending-hardware, never compared against physical-GPU runs |
Section 1 baseline, bit-comparable headless |
Notes:
- Device acquisition must go through
GpuDevicePoolor an injectedoptions.device. Never callnavigator.gpu.requestDevice()per index; the pool multiplexes one shared device andpowerPreference('high-performance' | 'low-power', validated fail-closed even on CPU-only paths). powerPreferenceunknown values throwIncompatibleOptionErroron every path, including CPU-only and injected-device paths.- Oversize datasets fail closed to CPU (
memory-budget-exceeded); there is no GPU paging. Per-buffer budgets are checked againstdevice.limits(records/offsets/query/output). - Device loss falls back to CPU (
fallbackReason: 'device-lost'); explicit rebuild is proven indocs/reliability.md(rebuildGpu()on both index types; worker indexes rebuild viarestore()/ re-init()).
| Runtime | Status | Notes |
|---|---|---|
| Browser main thread | Supported | Direct SearchIndex / DocumentIndex; prefer a worker for large corpora to avoid frame drops |
Dedicated Worker (SearchWorkerClient, startSearchWorker) |
Supported | Off-main-thread build / restore / search / mutate; stringIsolated: true (default) returns compact { index, score } hits enriched on the main thread; function hooks cannot cross the boundary (hooks reject with IncompatibleHookError; empty {} is a no-op) |
| Node.js >= 18 / Bun >= 1.1 | Supported, CPU-only | No navigator.gpu; baseline (Section 1) is the path. Used by all headless suites |
| SSR (import-time) | Supported | No unguarded window / document at import; GpuDevicePool.isSupported() guards acquisition; isDedicatedWorker === false outside workers (test:contracts pins this) |
| IndexedDB persistence | Supported where IndexedDB exists | Raw snapshot bytes; PII scope and CRC-vs-authenticity notes in docs/snapshot-format.md |
Engines field (package.json): node >= 18, bun >= 1.1.
Headless suites run anywhere Bun/Node runs (no GPU, no browser). The browser suite is the executing-hardware release gate.
| Suite | Command | Proves | Paths covered |
|---|---|---|---|
| Mock device | bun run test:mock (scripts/test-vgpu-mock.ts) |
WGSL buffer layout (32 B uniform, 512 B query, 65,544 B output), packing / serialization round-trip + fail-closed rejects, CPU auto-routing, overflow at 8191/8192/8193, abort / destroy / re-init, ufuzzy conflict | All OS headless; mock (non-executing) GPU + CPU baseline |
| Public contracts | bun run test:contracts (scripts/test-contracts.ts) |
Snapshot constants, error hierarchy + worker serialization round-trip, DocumentIndex / SearchWorkerClient contracts, SSR worker guard, package.json exports, filter/facet/typo/ranking/autocomplete/hooks/diagnostics shapes |
Node/Bun/SSR; worker-client protocol |
| Differential parity | bun run test:parity (scripts/test-parity-harness.ts) |
Matrix corrections (a)-(n), CPU-wiring differential cells, echo contracts, failure-injection delta (compile/pipeline/OOM/mapAsync/device-loss), concurrency/abort, worker contract; exact GPU-order cells report pending-hardware on vgpu/mock (mock never executes WGSL) |
Headless differential; browser subset is the release gate |
| Search modes | bun run test:search-modes (packages/webgpu-search/test/search-modes.test.ts) |
Bounded typo tolerance, token AND/OR/quorum + proximity, prefix anchor + prefixLength / exactCase gates, 4-mode CPU wiring, DocumentIndex integration (filters/facets/highlights), GPU routing (token/prefix/typo rejected to CPU, WGSL exact-only), determinism, portability (no unguarded DOM) |
CPU reference on all runtimes; GPU routing via mock device |
| CPU baseline pin | bun run test:cpu-baseline (scripts/test-cpu-baseline.ts) |
Section 1 reproducibility without a GPU (default-equals-explicit exact, integer descending scores, 4-mode coverage, prefer-cpu vs unsupported-mode reasons, ufuzzy opt-in + conflicts, DocumentIndex echoes, SSR guard) |
Node/Bun/SSR, no GPU required |
| Reliability proofs | bun run test:reliability (scripts/test-reliability.ts) |
Device-loss rebuild (rebuildGpu identical semantics), multi-index create/destroy leak gate + worker DESTROY, direct + worker concurrent-query isolation under rapid typing, GPU-unavailable/lost deterministic fallback vs the CPU contract; see docs/reliability.md |
Node/Bun/SSR headless (mock device); browser subset stays the hardware gate |
| Benchmark fixtures pin | bun run test:benchmarks (scripts/test-benchmarks.ts) |
Frozen fixtures 1.0.0 (scripts/benchmark-fixtures.ts): generator determinism, frozen queries serve live on smoke indexes, checked-in docs/benchmarks/benchmark_snapshot_matrix.json covers every (scenario, operation) pair with median/p95/samples + memory/build/snapshot costs, headless label pinned; see docs/benchmarks.md |
Node/Bun headless, no GPU required |
| Browser regression (hardware gate) | bun run test:browser (scripts/test-regression.ts) |
Timings separation, broad-query top-K, overflow detection, unicode packing, ordered (index, score, text) CPU/GPU parity on executing hardware (Bun oracle vs Chrome subject, same-host) |
Chrome/Chromium + benchmark dev server (apps/benchmark); requires a Chrome binary (CHROME_BIN) |
| Aggregator | bun run test:compatibility |
Runs the headless compatibility set in one command: test:mock + test:contracts + test:parity + test:search-modes + test:cpu-baseline + test:reliability + test:benchmarks. The browser gate stays separate (test:browser) because it needs Chrome + WebGPU |
CI + local |
Run:
# Headless compatibility set (no GPU, no browser needed)
bun run test:compatibility
# Executing-hardware release gate (needs Chrome; starts benchmark server)
bun run test:browser
# Full repo gate (includes compatibility suites plus records/highlight/
# mutations/worker/snapshot/observability/normalization/
# filtering/faceting/diagnostics)
bun run test:allCI (.github/workflows/ci.yml) runs each compatibility suite as a named
step — including the CPU baseline pin (test:cpu-baseline) — plus the
browser regression gate on stable Chrome.
- Firefox stable as a WebGPU path (use the CPU baseline).
- Safari < 18 as a WebGPU path (use the CPU baseline).
- Node.js / Bun / SSR as WebGPU paths (CPU-only by design).
cpuScorer: 'ufuzzy'as a conforming / comparable scorer (opt-in only).- Cross-scorer score comparisons (
exactvsufuzzyvs GPU pool on overflow): facetisApproximateis engine-relative, not cross-engine. - Server-side GPU execution claims from headless SwiftShader numbers
(always labeled
pending-hardware). - Function
filterpredicates +facetsoverSearchWorkerClient(facets drop fail-closed; closures cannot cross the worker boundary).
Migration tables, the third proof app, and packaging guides are outside
the scope of this matrix. Frozen benchmark fixtures live in docs/benchmarks.md
(Phase 5). Reliability proofs (rebuild, leak/concurrency gates) live in
docs/reliability.md.