diff --git a/.changeset/fallback-emoji-highlights.md b/.changeset/fallback-emoji-highlights.md new file mode 100644 index 0000000..6248de2 --- /dev/null +++ b/.changeset/fallback-emoji-highlights.md @@ -0,0 +1,5 @@ +--- +"webgpu-search": patch +--- + +Preserve whole emoji ZWJ sequences and regional-indicator flag pairs in highlight ranges when Intl.Segmenter is unavailable. diff --git a/.gitignore b/.gitignore index 1fa6cb0..ecb130b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ logs benchmark_*.json # Phase 5 frozen baseline (Issue #11): the checked-in headless matrix is the # exception to the benchmark_*.json ignore above. -!benchmark_snapshot_matrix.json +!docs/benchmarks/benchmark_snapshot_matrix.json fuzzy-benchmark-results.json chart_*.png *chart*.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c164d1a..966bd29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -110,7 +110,7 @@ on `127.0.0.1:5173` before running `scripts/test-regression.ts`. restore, CRC, caps, getters, hooks, and profile guards in `bun run test:snapshot`. 4. For benchmark fixture changes, bump `BENCHMARK_FIXTURE_VERSION`, regenerate - `benchmark_snapshot_matrix.json` + `.md`, and update `docs/benchmarks.md` §3. + `docs/benchmarks/benchmark_snapshot_matrix.json` + `.md`, and update `docs/benchmarks.md` §3. 5. Run the relevant gates before opening a PR (at minimum `check:shaders`, `typecheck`, `build`, `test:mock`, plus any suite covering your area). diff --git a/apps/benchmark/index.html b/apps/benchmark/index.html index ba1df17..785108f 100644 --- a/apps/benchmark/index.html +++ b/apps/benchmark/index.html @@ -22,13 +22,7 @@

1. Choose a run

-
Dataset sizes
- - - - - -
+
Dataset sizes
diff --git a/apps/benchmark/src/benchmark.ts b/apps/benchmark/src/benchmark.ts index 18a753d..01fcb54 100644 --- a/apps/benchmark/src/benchmark.ts +++ b/apps/benchmark/src/benchmark.ts @@ -1,3 +1,4 @@ +import { BENCHMARK_DATASET_SIZES } from '../../../scripts/bench-data/dataset-sizes'; /** * Full Matrix Benchmark Engine & Performance Characterization Runner. * @@ -282,7 +283,7 @@ export class BenchmarkRunner { * execution, median + p95 statistics, and real UI thread telemetry. */ async runBenchmark( - sizes: number[] = [10_000, 100_000, 500_000, 1_000_000, 2_000_000], + sizes: number[] = [...BENCHMARK_DATASET_SIZES], query: string = 'AuthController', mode: 'substring' | 'fuzzy' = 'substring', iterationsOrOptions?: number | BenchmarkOptions, diff --git a/apps/benchmark/src/main.ts b/apps/benchmark/src/main.ts index cdd3a45..684ea85 100644 --- a/apps/benchmark/src/main.ts +++ b/apps/benchmark/src/main.ts @@ -1,3 +1,4 @@ +import { BENCHMARK_DATASET_SIZES } from '../../../scripts/bench-data/dataset-sizes'; import { CPUEngine, WebGPUEngine } from 'webgpu-search'; import './style.css'; import { BenchmarkRunner, type BenchmarkRowResult } from './benchmark'; @@ -24,6 +25,18 @@ const exportCsv = document.querySelector('#export-csv'); const exportMarkdown = document.querySelector('#export-markdown'); const exportChart = document.querySelector('#export-chart'); +document.querySelector('.size-options')?.replaceChildren(...BENCHMARK_DATASET_SIZES.map(size => { + const label = document.createElement('label'); + const input = document.createElement('input'); + input.type = 'checkbox'; + input.name = 'size'; + input.value = String(size); + input.checked = size <= 100_000; + const text = size >= 1_000_000 ? `${size / 1_000_000}M` : `${size / 1000}k`; + label.append(input, ` ${text}`); + return label; +})); + let currentRows: BenchmarkRowResult[] = []; let currentMode: 'substring' | 'fuzzy' = 'substring'; let currentCorpus: CorpusType = 'ascii'; diff --git a/apps/site/index.html b/apps/site/index.html index 937808a..c12549c 100644 --- a/apps/site/index.html +++ b/apps/site/index.html @@ -64,7 +64,7 @@

Find the right thing.
Keep the UI fast.
WebGPU vs CPU vs popular libs
live

- +
Ready.7 timed searches / engine
diff --git a/apps/site/src/corpus.ts b/apps/site/src/corpus.ts index a014140..fb06231 100644 --- a/apps/site/src/corpus.ts +++ b/apps/site/src/corpus.ts @@ -2,11 +2,10 @@ import './fonts.css'; import './style.css'; import { normalizeText, scoreExactMatches, type SearchResultItem } from 'webgpu-search'; import { corpusRow } from './corpus-gen'; +import { SITE_DATASET_SIZES } from '../../../scripts/bench-data/dataset-sizes'; const PAGE_SIZE = 100; const MAX_MATCHES_SHOWN = 1000; -/** Sizes the homepage benchmark offers (apps/site/index.html #qb-size). */ -const ALLOWED_SIZES = [100000, 200000, 500000, 1000000, 2000000] as const; const DEFAULT_SIZE = 100000; const BUILD_CHUNK = 50000; @@ -15,8 +14,8 @@ function parseSize(raw: string | null): number { if (raw === null || raw.trim() === '') return DEFAULT_SIZE; const requested = Number(raw); if (!Number.isFinite(requested) || requested <= 0) return DEFAULT_SIZE; - let size: number = ALLOWED_SIZES[0]; - for (const allowed of ALLOWED_SIZES) if (allowed <= requested) size = allowed; + let size: number = SITE_DATASET_SIZES[0]!; + for (const allowed of SITE_DATASET_SIZES) if (allowed <= requested) size = allowed; return size; } diff --git a/apps/site/src/main.ts b/apps/site/src/main.ts index 1f7865e..15e4864 100644 --- a/apps/site/src/main.ts +++ b/apps/site/src/main.ts @@ -1,3 +1,4 @@ +import { SITE_DATASET_SIZES } from '../../../scripts/bench-data/dataset-sizes'; import './fonts.css'; import './style.css'; import { formatMs } from './format'; @@ -5,6 +6,13 @@ import { formatMs } from './format'; const copyButton = document.querySelector('#copy-install'); const qbSize = document.querySelector('#qb-size'); +if (qbSize) { + qbSize.replaceChildren(...SITE_DATASET_SIZES.map(size => { + const option = new Option(`${size.toLocaleString('en-US')} records`, String(size)); + option.selected = size === 500_000; + return option; + })); +} const qbQuery = document.querySelector('#qb-query'); const qbRun = document.querySelector('#qb-run'); const qbStatus = document.querySelector('#qb-status'); diff --git a/docs/IMPROVEMENT-PLAN.md b/docs/IMPROVEMENT-PLAN.md new file mode 100644 index 0000000..4ccbe2c --- /dev/null +++ b/docs/IMPROVEMENT-PLAN.md @@ -0,0 +1,121 @@ +# Post-1.0 Improvement Plan + +> **Created**: 2026-10-11 +> **Baseline**: `webgpu-search@1.0.1` (commit `bee5fda`) +> **Status**: Proposed + +This plan lists the next improvements, most valuable first. It comes from a quick look at the repo layout, tests, open issues and recent commits, not a full audit. + +--- + +## Phase 1: Quick cleanup (≈ half a day) + +Low-risk housekeeping that clears the way for the bigger phases. + +| # | Task | Done when | +|---|------|-----------| +| 1.1 | **Implemented:** Keep one shared dataset-size list in `scripts/bench-data`, consumed by `apps/site` and `apps/benchmark`. | Only one definition exists; issue #38 closes when the cleanup PR merges. | +| 1.2 | **Done:** Move the benchmark snapshot JSON and Markdown to `docs/benchmarks/` and update the runner, checks, and docs. | Both artifacts live under `docs/benchmarks/`. | +| 1.3 | **Done:** Delete the two finished implementation plans and remove stale references. | Remaining documentation references resolve. | + +--- + +## Phase 2: Tests for the 1.0 promises (≈ 2–3 days) + +There are about 300 tests, but several core modules have none of their own. This phase covers the guarantees that 1.0 commits to. + +### 2.1 Tests that GPU and CPU return the same scores +- Add `test/scoring-parity.test.ts`. It runs a fixed set of queries through the mock WebGPU path and through `cpu-reference.ts` / `exact-scorer.ts`. +- Check that both return the same `SearchResultItem[]`: same order, same integer scores, scores in descending order. +- Cover ASCII, CJK, emoji/ZWJ, mixed case, and queries at the maximum length. +- **Why:** AGENTS.md §2.4 requires the two paths to score the same, and nothing enforces it today. + +### 2.2 Snapshot codec tests +- Add `test/snapshot-codec.test.ts`: + - Saving an index and loading it back gives identical search results. + - Bad input is rejected with the right typed error: wrong magic bytes, wrong version, truncated data, a corrupted checksum, a mismatched text profile. + - A checked-in snapshot from 1.0.x still loads (a "golden" fixture). +- **Why:** the format in `docs/snapshot-format.md` is a public promise to keep old snapshots loading. + +### 2.3 Text pipeline tests +- **Existing coverage verified:** normalization and case folding pass 98 checks in both default and Turkish locales; the highlighting suite covers NFC/NFD, folding expansions, surrogate pairs, and emoji. +- **Fallback bug fixed:** without `Intl.Segmenter`, highlights could split joined emoji and flags. Shared fallback boundaries now preserve ZWJ sequences and regional-indicator pairs; regression cases run with segmentation disabled before the normal suite. See `docs/public-api.md` §2.5. +- Add `test/text-normalization.test.ts`, covering NFC, case folding with `CaseFolding-16.0.0` (ß→ss, the Turkish dotless i, Greek final sigma), surrogate pairs, and `u32` packing. +- Add `test/highlight.test.ts`, checking that highlight ranges land on whole characters in multi-byte text. + +### 2.4 Run in CI +- Make sure `bun run test:mock` in `.github/workflows/ci.yml` picks up the new files. + +--- + +## Phase 3: Public results page for the collected benchmarks (≈ 3–4 days) + +Benchmark results already go to Upstash automatically (the opt-in `api/submit-benchmark.ts` intake), but nobody can see them. + +| # | Task | +|---|------| +| 3.1 | Add a read-only, cached API route in `api/` that returns aggregated results (median speedup grouped by GPU type, browser and dataset size). Return only aggregates, never raw rows. | +| 3.2 | Add a results section or page to `apps/site`: a chart of WebGPU vs CPU vs uFuzzy vs Fuse.js, with integrated and discrete GPUs shown separately. | +| 3.3 | Use the data to set the default `threshold` (currently 30,000) from measured crossover points. Consider a separate default for integrated GPUs. | +| 3.4 | Show a minimum sample count on each group, and hide groups with too few samples. | + +**Done when:** the page is live, and the README links to it in place of the old benchmark numbers. + +--- + +## Phase 4: Break up the largest files (≈ 3–5 days) + +`document-index.ts` has 3,331 lines, almost 30% of the library. `webgpu-engine.ts` has 1,093. + +### 4.1 Split `document-index.ts` +Proposed modules under `src/document/`: +- `build.ts`: ingesting records and building the index +- `query.ts`: running searches and merging results +- `filter-facet.ts`: wiring in the code from `filtering/` and `faceting/` +- `snapshot.ts`: saving and loading with `snapshot-codec.ts` +- `document-index.ts`: a thin public class that delegates to the above + +### 4.2 Split `webgpu-engine.ts` +- Move buffer allocation and resizing, pipeline/shader setup, and dispatch/readback into separate modules. + +### Rules for this phase +- Behavior must not change. The Phase 2 tests must pass before and after. +- The public API in `src/index.ts` must not change. Compare `dist/index.d.ts` before and after. +- Run `bun run check:shaders`, `typecheck`, `build` and `test:mock`, then do the subagent review that AGENTS.md requires. + +--- + +## Phase 5: Bundle size (≈ 1–2 days) + +| # | Task | +|---|------| +| 5.1 | Add `size-limit` (or a small script) to CI with a budget for each entry point (`.` and `./worker`). | +| 5.2 | Measure how much `case-fold-table.ts` and `fold-table.ts` add to the bundle. | +| 5.3 | If they are large, load the full Unicode tables only for the `unicode-default` profile, so ASCII-only users don't pay for them. This must not change the snapshot format. | + +--- + +## Phase 6: New features (later, one at a time) + +Rank these again once Phases 1–5 are done. + +1. **Add and remove records without a full rebuild.** This would need GPU buffers that can grow and a way to mark deleted records ("tombstones"). It needs its own design doc first. +2. **Official framework bindings** (`webgpu-search/react`, `/vue`), built from the recipes in `examples/`. +3. **A Node example on the `webgpu` npm package (Dawn)**, to back up the claim that search runs on the server too. Add it to `docs/support-matrix.md`. + +--- + +## Order and dependencies + +``` +Phase 1 ──► Phase 2 ──► Phase 4 (refactor needs the tests first) + │ + └──────► Phase 5 +Phase 3 can run in parallel with everything else +Phase 6 starts after Phase 4 +``` + +## Release notes +- Phases 1–3 and 5 are patch or minor releases (add a changeset). +- Phase 4 should produce a `patch` changeset with no public API changes. +- Phase 6 items are minor releases, each with its own changeset. diff --git a/docs/ISSUE-11-PLAN.md b/docs/ISSUE-11-PLAN.md deleted file mode 100644 index dbc1fc3..0000000 --- a/docs/ISSUE-11-PLAN.md +++ /dev/null @@ -1,189 +0,0 @@ -# Issue #11 — 1.0 Lock Compatibility, Reliability, and the Embedding Ecosystem: Implementation Plan - -> **Issue Reference**: [GitHub Issue #11: 1.0: lock compatibility, reliability, and the embedding ecosystem](https://github.com/codewarnab/webgpu-search/issues/11) -> **Target Milestone**: `1.0.0` -> **Status**: Phases 0–6 complete -> **Post-1.0 note (2026-09-23)**: the `apps/docs-search`, -> `apps/monaco-palette`, and `apps/log-viewer` proof apps were removed; -> benchmark fixture generators moved to `scripts/bench-data/`, and -> `scripts/test-proof-apps.ts` / `scripts/assemble-site.ts` were deleted. -> Phase 4/5 prose below referencing those paths is historical. -> **Predecessors**: v0.1.1 (#8), v0.2 (#7), v0.3 (#9), v0.4 (#10) -> **Roadmap**: Issue #12 (embeddable search infrastructure through 1.0) - ---- - -## 0. Executive Summary & Outcome - -Ship a stable embeddable-search contract that other web products can depend on across browser, device, worker, and CPU fallback environments. - -The package is an embeddable, private, offline-capable search engine for large datasets already inside a web app. It is not a hosted search platform, a UI product, a crawler, or a replacement for server-side search operations. - -Success means a host can build or restore an index off the main thread, update it incrementally, query and cancel safely, highlight original text, observe fallback/recovery, and dispose resources without accessing private buffers. - ---- - -## 1. Release Requirements (from Issue #11) - -- [x] Freeze and document public TypeScript APIs, result ordering, versioning, and deprecation policy. (Done: `docs/public-api.md`, `scripts/check-public-api.ts` + `scripts/public-api-surface.txt` 187-export allowlist, `check:public-api` + `attw` CI gates.) -- [x] Publish a browser/OS/GPU support matrix and a tested CPU-only baseline. (Done: `docs/support-matrix.md`, `scripts/test-cpu-baseline.ts`, `test:compatibility`.) -- [x] Prove device-loss rebuild, resource cleanup, concurrent-query isolation, and deterministic fallback. (Done: `rebuildGpu()` on both index types, `docs/reliability.md`, `scripts/test-reliability.ts` 58 checks, parity Part 7, contracts §13, wired into `test:compatibility` + `test:all`.) -- [x] Define index-format compatibility and migration support across supported releases. (Done: `docs/snapshot-format.md` §§1–9 migration table + reject taxonomy, `snapshot-codec` schema/header agreement guard, `DocumentIndex` ctor/`fromSnapshotData`/`applySnapshotData` `ProfileMismatchError` guards, `scripts/test-snapshot.ts` §§14–17 hookIds/worker-version/all-caps/profile+live-format pins.) -- [x] Maintain integration examples for web IDE search, local logs/data grids, and offline docs. (Done: `apps/monaco-palette`, `apps/log-viewer`, `apps/docs-search`, `scripts/test-proof-apps.ts` §§1–12 incl. teardown + public-API-only + recipe + DOM-free audits.) -- [x] Publish reproducible benchmark fixtures, environments, median/p95 results, memory, build/upload, and restore costs. (Done: `docs/benchmarks.md` frozen fixtures `1.0.0`, `scripts/benchmark-fixtures.ts` versioned corpora/queries spec, `bench-snapshot-matrix.ts` per-scenario median/p95 + `vramBytes`/`ramBytes` + build/upload + serialize/restore bytes/time with headless-non-comparable label, checked-in `benchmark_snapshot_matrix.json` + `.md` baselines, `scripts/test-benchmarks.ts` 82-check pin wired into `test:compatibility` + `test:all`.) -- [x] Provide package-size budgets, tree-shaking guidance, CSP/worker setup, SSR-safe imports, and accessibility guidance for example UIs. (Done: per-entry gzip budgets — 90 KB index / 76 KB worker — in `scripts/check-bundle-size.ts`, `"sideEffects": false`, `docs/packaging.md`.) -- [x] Document diagnostics, issue-report data, security/privacy boundaries, and unsupported configurations. (Done: `docs/diagnostics.md`, `.github/ISSUE_TEMPLATE/webgpu-search-issue.md`, `docs/security-privacy.md`, unsupported list in `docs/support-matrix.md` §6.) - -### Acceptance boundary - -- A documented compatibility suite passes on every supported browser path. -- GPU unavailability or loss does not change documented match semantics. -- A persisted index can be restored or rejected with a specific migration reason, never read under the wrong format/profile. -- Package examples consume only public APIs and include teardown and error handling. -- Major semantic or storage changes require a major version. - ---- - -## 2. Current Baseline & Gaps - -| Area | Current state | Gap to 1.0 | -| --- | --- | --- | -| Public API | `packages/webgpu-search/src/index.ts:1` exports `SearchIndex`, `DocumentIndex`, `SearchWorkerClient`, engines, packing, ranking, hooks, diagnostics, snapshots | Unfrozen; no `Public` vs `Internal` split; no versioning/deprecation doc | -| Result ordering | `src/ranking.ts` five-tier tie-breakers, `SearchResultItem` normalized integer scores in `src/types.ts:63` | Ordering contract not frozen in a public doc | -| Snapshot format | Canonical v4 + legacy v3 read-only, fail-closed `IncompatibleIndexError`; `docs/snapshot-format.md:1`, `src/text-profile.ts:14` | No cross-release migration table; reject reasons not standardized for hosts | -| Device loss | `GpuDevicePool.onDeviceLost` → CPU fallback + explicit `rebuildGpu()` on `SearchIndex` / `DocumentIndex` (`fallbackReason:'device-lost'`); headless simulation via `GpuDevicePool.simulateDeviceLoss`; proven in `docs/reliability.md` §1 | Done (Phase 2) | -| Lifecycle | Idempotent `destroy()` + `[Symbol.dispose]` on both index types, single device-loss subscription, `WebGPUEngine` unmap-before-destroy + shared-pool release, worker `DESTROY` lifecycle; multi-index leak gate in `scripts/test-reliability.ts` §2 | Done (Phase 2) | -| Proof apps | `apps/monaco-palette`, `apps/log-viewer`, `scripts/test-proof-apps.ts:1`, `workspace:*` linked | Missing third proof app: offline docs / API search | -| Framework recipes | `examples/react`, `examples/vue`, `examples/svelte`, `examples/vanilla` | No public-API-only + teardown lint gate | -| Benchmarks | Headless CPU matrix `scripts/bench-snapshot-matrix.ts:1`, browser runner `scripts/run-all-benchmarks.ts:1` | No frozen fixtures, env table, memory/build/upload/restore costs | -| Packaging | `packages/webgpu-search/tsup.config.ts:1` (esm+cjs, treeshake), `scripts/check-bundle-size.ts:1` (82KB gzip budget) | No published tree-shaking / CSP / worker / SSR / a11y guidance | -| Diagnostics | `src/diagnostics.ts`, `QueryDiagnostics` in `src/types.ts:791`, `FallbackReason` in `src/types.ts:268` | No diagnostics / issue-report / security-privacy docs | - ---- - -## 3. Phases - -### Phase 0 — Freeze Public API + Versioning Policy ✅ DONE - -Scope: `src/index.ts`, `src/types.ts:12`, `src/errors.ts`, `src/diagnostics.ts`. - -Shipped: `docs/public-api.md` (entry points, Stable/Power-user/Internal tiers, class contracts, frozen ordering + echoes, errors, version constants, breaking-change + deprecation rules), `scripts/check-public-api.ts` + `scripts/public-api-surface.txt` (187-export allowlist diff, `INTERNAL_*` guard, version/tie-breaker/echo/alias/exports-map pins) wired as `bun run check:public-api` and CI (`Check Public API Surface` + `attw --pack`). - -Tasks (all complete): - -1. Audit every export in `src/index.ts:1`; classify `Public` vs `Internal` (`INTERNAL_WORKER_ID_KEY`, packing internals, fold tables, ranking helpers). -2. Add `docs/public-api.md`: exported symbols, `SearchIndex` / `DocumentIndex` / `SearchWorkerClient` contracts, `SearchResultItem` ordering (score DESC + tie-breakers), `SearchResponse` echoes (`profileId`, `scoringVersion`, `cpuScorer`, `fallbackReason`). -3. Document breaking-change rule: major version for any scoring or storage semantic change; minor for additive APIs. -4. Document deprecation policy: removal window, alias handling (`limit` vs `maxResults` as template). -5. Gates: `attw` check, API surface diff check in CI, `bun run typecheck`, `bun run build`. - -Exit: hosts can pin `1.x` and rely on frozen ordering + echoes. - -### Phase 1 — Support Matrix + CPU Baseline ✅ DONE - -Tasks: - -1. [x] Add `docs/support-matrix.md`: Chrome / Edge / Safari / Firefox × Windows / macOS / Linux / Android, WebGPU vs CPU-only, Worker / Node.js / SSR. (Shipped; §5 maps each path to its named suite.) -2. [x] Define tested CPU-only baseline: `preferGpu:false` + `cpuScorer:'exact'` as the reference contract. (Pinned by `scripts/test-cpu-baseline.ts`, 39 checks, wired as `bun run test:cpu-baseline` and into `test:all` + CI.) -3. [x] Mark `cpuScorer:'ufuzzy'` as explicit opt-in, non-conforming, CPU-only. (`docs/support-matrix.md` §2; enforced fail-closed in `search-index.ts` / `document-index.ts`; pin covers echo `cpu-algorithm-requested` + all conflict throws.) -4. [x] Wire documented compatibility suite: `test:mock`, `test:contracts`, `test:parity`, `test:search-modes`, `test:browser` across supported paths. (Plus new `test:cpu-baseline`; one-command `bun run test:compatibility` for the headless set; `test:browser` stays the separate hardware gate; CI runs each suite incl. the pin as a named step.) - -Exit: every supported path has a named passing suite; CPU baseline is reproducible without a GPU. - -### Phase 2 — Reliability Proofs ✅ DONE - -Scope: `src/document-index.ts:184`, `src/search-index.ts:72`, `src/gpu-device-pool.ts:31`, `src/webgpu-engine.ts:164`, `src/worker/worker-client.ts`. - -Tasks: - -1. [x] Device-loss rebuild: explicit `rebuildGpu()` on `SearchIndex` / `DocumentIndex` (re-acquire + re-upload retained corpora, identical-match-semantics; `false` for CPU-by-design/empty, throws only when destroyed) alongside existing `restore` / re-`init` paths. Headless simulation via `GpuDevicePool.simulateDeviceLoss()`. (Pinned by `scripts/test-reliability.ts` §1, 58 checks; worker indexes rebuild via `restore()` / re-`init()`; documented in `docs/reliability.md` §1.) -2. [x] Resource cleanup: audited idempotent `destroy()`, buffer disposal (unmap-before-destroy), `GpuDevicePool` release (injected no-op, shared refcount clamp), worker `DESTROY`; added 8+8 multi-index create/destroy leak gate (`getListenerCount()` returns to baseline) + `[Symbol.dispose]` on both index types. (Pinned by `scripts/test-reliability.ts` §2 + `test:contracts` §13; `docs/reliability.md` §2.) -3. [x] Concurrent-query isolation: direct paths proven independent (per-query echo + exact-oracle match, `AbortSignal` siblings isolated, mutation `generation` abort); `SearchWorkerClient` monotonic sequencing (rapid typing aborts superseded, last wins) + caller `signal`. (Pinned by `scripts/test-reliability.ts` §3 alongside existing `test:worker` §5 and `test:parity` Part 5; `docs/reliability.md` §3.) -4. [x] Deterministic fallback: GPU-unavailable and GPU-lost queries match the documented CPU contract (same matches/order/echoes; `prefer-cpu` / `unsupported-mode` / `device-lost` / `gpu-execution-error` reasons; `AbortError` never converts to fallback). Extended `scripts/test-parity-harness.ts` (Part 7) and `scripts/test-contracts.ts` (§13). (Pinned by `scripts/test-reliability.ts` §4; `docs/reliability.md` §4.) -5. [x] Gates: `bun run test:mock`, `bun run test:worker`, `bun run test:browser` all green; plus new `bun run test:reliability` wired into `test:compatibility` + `test:all` and `docs/support-matrix.md` §5. - -Exit: loss, cleanup, and concurrency are proven, not just handled. - -### Phase 3 — Index-Format Compatibility + Migration ✅ DONE - -Scope: `src/snapshot-codec.ts`, `src/snapshot-idb.ts`, `src/persistence.ts`, `src/idb-storage.ts`, `docs/snapshot-format.md:1`. - -Shipped: `docs/snapshot-format.md` §§1–9 (v3 → v4 → 1.0 table, reject taxonomy, `hasGetter`/`hookIds` rules, profile guarantee, live-format `4`, IDB caps, suites), codec agreement guard, `DocumentIndex` `ProfileMismatchError` guards, `test-snapshot` §§14–17. Gates: `test:snapshot` §§1–17, `test:records`, `bench:snapshot-matrix` all green; plus `check:shaders`, `typecheck`, `build`, `test:mock`, `check:public-api` (187 pinned). - -Tasks (all complete): - -1. [x] Extend `docs/snapshot-format.md` with a v3 → v4 → 1.0 compatibility + migration table. -2. [x] Standardize reject reasons: `IncompatibleIndexError` (version/profile/checksum/shape), `IncompatibleHookError` (missing hooks), `ProfileMismatchError` (case/profile). -3. [x] Cover in tests: legacy restore, corrupt CRC, oversize caps (`MAX_SNAPSHOT_*`), missing `getter` (`hasGetter`), missing `hookIds`, worker `serialize` / `restore` version guard. -4. [x] Guarantee: never read under wrong format/profile; `getStats().formatVersion` semantics stay live-format. -5. [x] Gates: `bun run test:snapshot`, `bun run test:records`, `bun run bench:snapshot-matrix`. - -Exit: any persisted index restores or rejects with an actionable migration reason. - -### Phase 4 — Integration Examples (Public-API Only) ✅ DONE - -Shipped: `apps/docs-search` (offline documentation / API search: `DocsEngine`, versioned `generateDocsRecords` corpus, section/version filters + facets, IDB snapshot bundle, `rebuildGpu` recovery hook, `pagehide` teardown), `pagehide` teardown on all three proof-app UIs, `test-proof-apps` §§8–12 (docs-engine functional incl. cancel/recovery/snapshot/XSS pins, teardown lint, public-API-only import lint, framework-recipe audit, DOM-free engine/data audit). Gates: `test:proof-apps` §§1–12, `typecheck` (7 tasks), `build`, `test:mock` all green. - -Tasks (all complete): - -1. [x] Keep `apps/monaco-palette` — Monaco-style file / symbol / command search. -2. [x] Keep `apps/log-viewer` — large local log viewer / data-grid quick find. -3. [x] Add `apps/docs-search` — offline documentation / API search (the missing third proof app). -4. [x] Enforce per app: public-API-only imports from `webgpu-search`, off-main-thread build/restore via worker, incremental mutations, query cancel, original-text highlights, fallback/recovery observability, `destroy()` + error paths. -5. [x] Extend `scripts/test-proof-apps.ts` to cover all three + teardown lint + cross-platform safety audit (zero unguarded DOM globals). -6. [x] Check framework recipes (`examples/react`, `examples/vue`, `examples/svelte`, `examples/vanilla`) for worker-first + abort-safe + destroy-on-unmount patterns. - -Exit: three proof apps pass using only public APIs. - -### Phase 5 — Reproducible Benchmarks ✅ DONE - -Scope: `scripts/bench-snapshot-matrix.ts`, `scripts/run-all-benchmarks.ts`, `scripts/run-fuzzy-benchmark.ts`, `apps/benchmark`. - -Shipped: `scripts/benchmark-fixtures.ts` (`BENCHMARK_FIXTURE_VERSION 1.0.0`, frozen monaco-5k / logs-10k/50k/100k corpora, queries/modes/filters/facets, 19 expected rows, deterministic `BENCHMARK_LOG_BASE_TIME_MS` anchor threaded through a backwards-compatible `generateStructuredLogs(count, startId?, baseTimeMs?)` param), `bench-snapshot-matrix.ts` per-scenario `index-build` rows + `vramBytes`/`ramBytes`/`buildMs`/`uploadMs` on every row + snapshot bytes (`snapshotBytes`/`schemaBytes`/`columnarBytes`/`docsBytes`) and restore costs (`restoredMatches`/`restoreTimeMs`) + `fixturesVersion`/`fixtures` in JSON + sibling markdown summary with the headless-non-comparable label, checked-in `benchmark_snapshot_matrix.json` + `benchmark_snapshot_matrix.md` baselines (`.gitignore` exception), `docs/benchmarks.md` (§§1–5 fixtures/env-table/per-scenario table/repro/re-baseline rule), `scripts/test-benchmarks.ts` 82-check pin wired into `test:compatibility` + `test:all` and `docs/support-matrix.md` §5. Gates: `bench:snapshot-matrix`, `test:benchmarks`, `check:shaders`, `typecheck`, `build`, `test:mock`, `check:public-api`, `check:bundle-size`, `test:proof-apps` all green. - -Tasks (all complete): - -1. [x] Freeze fixtures: version `generateMonacoRecords` and `generateStructuredLogs` corpora; record sizes, queries, modes, filters, facets. -2. [x] Publish per-scenario table: environment (browser / GPU / driver / runtime), median/p95 with samples, memory (`vramBytes` / `ramBytes`), build/upload, serialize/restore bytes + time. -3. [x] Keep headless-CPU and browser-WebGPU reports separate; label headless numbers non-comparable to GPU runs. -4. [x] Check in `benchmark_snapshot_matrix.json` baselines + markdown summary. - -Exit: performance claims trace to named fixtures and environments. - -### Phase 6 — Packaging + Maintenance Docs ✅ DONE - -Scope: `packages/webgpu-search/package.json`, `packages/webgpu-search/tsup.config.ts`, `scripts/check-bundle-size.ts`. - -Shipped: per-entry gzip budgets (90 KB `index.js`/`index.cjs`, 76 KB `worker.js`/`worker.cjs`, 40 KB index delta cap + rationale log in `check-bundle-size.ts`), `"sideEffects": false`, `docs/packaging.md` (tree-shaking, CSP, worker setup, SSR-safe imports, example-UI a11y), `docs/diagnostics.md` (telemetry, warnings, budgets, issue-report data), `.github/ISSUE_TEMPLATE/webgpu-search-issue.md`, `docs/security-privacy.md` (IDB PII scope, snapshot trust boundary, CRC vs authenticity). Gates: `check:shaders`, `typecheck`, `build`, `test:mock`, `check:bundle-size`, `check:public-api` all green. - -Tasks (all complete): - -1. Keep per-file gzip budget gate; add per-entry (`index` vs `worker`) budget + rationale log. -2. Publish tree-shaking guidance: entry choice, side-effects, what pulls in WGSL / uFuzzy. -3. Publish setup guides: CSP (worker blob, no-WASM notes), worker setup (`SearchWorkerClient`, `startSearchWorker`), SSR-safe imports (no unguarded `window` / `document`). -4. Publish example-UI accessibility guidance: keyboard, focus, live regions, reduced motion. -5. Add `docs/diagnostics.md` (telemetry fields, warnings, budgets), issue-report template (snapshot header, `getStats()`, timings, `fallbackReason`), `docs/security-privacy.md` (IDB PII scope, snapshot trust boundary, CRC vs authenticity), unsupported configurations list. -6. Gates: `bun run check:shaders`, `bun run typecheck`, `bun run build`, `bun run test:mock`. - -Exit: hosts can install, bundle, sandbox, debug, and report issues without reading source. - ---- - -## 4. Suggested Execution Order - -```text -0 API freeze → 3 format compat → 2 reliability → 1 matrix → 4 examples → 5 benchmarks → 6 packaging -``` - -Rationale: correctness and storage contracts (0, 3, 2) before ecosystem proof (1, 4, 5, 6), mirroring roadmap sequencing rules in Issue #12. - ---- - -## 5. Verification Per Phase - -- `bun run check:shaders` -- `bun run typecheck` -- `bun run build` -- `bun run test:mock` -- Phase-specific: `test:contracts`, `test:parity`, `test:snapshot`, `test:worker`, `test:proof-apps`, `test:browser`, `bench:snapshot-matrix`, `check:bundle-size`, `test:cpu-baseline`, `test:compatibility` -- Subagent verification before sign-off per `AGENTS.md`: contract/regression check, portability audit (no unguarded DOM), clean-diff audit. diff --git a/docs/SITE-REBUILD-PLAN.md b/docs/SITE-REBUILD-PLAN.md deleted file mode 100644 index baae3bd..0000000 --- a/docs/SITE-REBUILD-PLAN.md +++ /dev/null @@ -1,180 +0,0 @@ -# webgpu-search Website Rebuild Plan - -**Status:** Shipped with post-1.0 revisions (2026-09-23). Homepage uses the -wireframe blue accent (`--blue: #1769e0`) with an embedded `#benchmark` quick -comparison (WebGPU vs exact CPU vs uFuzzy vs Fuse.js). `/benchmark/` and -`/compare/` redirect to `/#benchmark` (see `vercel.json`); there is no -separate full-matrix page in production. The `apps/docs-search`, -`apps/monaco-palette`, and `apps/log-viewer` example apps were removed and -benchmark fixture generators moved to `scripts/bench-data/`; `examples/` -holds framework recipes only. The §4 route table and §§5/7 app lists below -are historical and no longer reflect the tree. -**Design update (2026-09-23):** monochrome direction superseded — homepage uses the wireframe blue accent (`--blue: #1769e0`) and single-column hero with full-width `quick-start.ts` code window. Benchmarks consolidated: one quick WebGPU-vs-CPU benchmark embedded on the homepage (`#benchmark`, shared `apps/site/src/quick-benchmark.ts`), full matrix only at `/benchmark/`; `/compare/` retired with a Vercel redirect to `/benchmark/`. The homepage demo doubles as entry point, with an optional collapsed playground for custom text. -**Owner intent:** Replace the current benchmark dashboard UI with a polished product homepage, while keeping browser-based comparison and full benchmark testing available as clear, separate pages on the same site. -**Primary deployment:** Vercel, currently configured to publish `apps/benchmark/dist`. -**Plan date:** 2026-09-23 - -**Latest direction confirmation:** The current benchmark UI is to be discarded, not retained as the homepage. The full benchmark remains available as a redesigned page on this site. The user approved implementation after this plan was written. - -## 1. Product goal - -Turn the current deployed benchmark dashboard into an approachable product website for `webgpu-search`. - -The first impression should explain what the library does and let a visitor take one obvious next step. Visitors who want to experiment can compare search in their own browser. Visitors who want rigorous numbers can open a dedicated full benchmark page on the same site. Example apps should be directly testable from the site. - -The existing benchmark engine and data-generation work are useful; the current benchmark page's visual design and information layout are not. Retire that UI completely and redesign the benchmark experience rather than serving it unchanged under a different link. - -## 2. Approved design direction - -- **Visual style:** calm, bright, minimal, Vercel-inspired; white or warm-white canvas, near-black primary text, soft-gray supporting text and borders. -- **Color:** black/white/gray for the interface. No blue decorative text, blue primary buttons, or saturated metric tiles. Restrained syntax colors are acceptable inside code samples only. -- **Typography:** clean system sans-serif for reading; monospace only for code, queries, and compact technical values. -- **Density:** generous spacing, short explanations, a few clear metrics, and progressive disclosure for technical detail. -- **Language:** plain words first. Explain GPU, CPU fallback, cold upload, and benchmark qualification beside the relevant result rather than in a wall of documentation. -- **Responsive and accessible:** keyboard usable, named controls, visible focus, adequate contrast, semantic headings, and mobile layouts. - -The Here.now prototype was an initial visual sketch. Its blue accents are superseded by this monochrome direction; the screenshot of the old dashboard is the anti-reference for density and visual noise. - -## 3. Site map and visitor journeys - -### `/` — Product homepage - -1. **Header:** brand, Features, Examples, Docs/Source, and a clear “Try it in your browser” action. -2. **Hero:** one-sentence value proposition, concise explanation of WebGPU with CPU fallback, and two primary actions: “Try search” and “Compare performance.” -3. **Short code sample:** familiar `SearchIndex` usage, with syntax highlighting confined to the snippet. -4. **Interactive search demo:** small local sample dataset, immediate results, simple fuzzy/substring choice, and a clear note that it is a small demonstration—not a performance claim. -5. **Feature summary:** hybrid routing, fuzzy and substring search, Unicode handling, and Worker support; no exhaustive API inventory. -6. **Example app cards:** direct “Open test app” links for Docs Search, Code Palette, and Log Viewer. -7. **Closing install and navigation:** package install command and links to the comparison, full benchmark, source, and npm package. - -### `/compare/` — Simple browser comparison - -An approachable, real comparison of WebGPU and exact CPU search using the visitor’s own browser and the same generated data/query. - -- Default to a safe, useful dataset size; offer a small and a larger option without exposing a long matrix of settings. -- Limit direct GPU/CPU comparison to the supported WebGPU modes (fuzzy and substring) so both sides use the same search contract. -- Run both engines against the same records and query, warm up, collect repeated samples, and report median search latency. -- Separate index/data preparation time from warm-search latency. -- Show each engine’s result count, elapsed time, and whether the top results agree. Report actual values; never promise the GPU will always win. -- Detect WebGPU availability and display CPU-only/fallback status honestly. If the GPU path did not run, do not label CPU timings as GPU timings or show a speedup. -- Keep results in the browser. The page must not upload a visitor’s queries or data. -- Explain in one sentence that device, browser, dataset size, and query affect results. -- Provide a visible link to `/benchmark/` for users who want the full test matrix. - -### `/benchmark/` — Full benchmark, redesigned - -Keep the full benchmark capability available on the same website, but replace the current dense dashboard UI with a new, calmer interface. - -- Reuse the established `BenchmarkRunner`, dataset generator, parity/reference logic, and export functions where they remain correct. -- Present the run as a short flow: choose corpus and search mode, choose dataset sizes, run, then read the summary. -- Keep a sensible default run. Large sizes (up to the supported 2M rows) remain opt-in and display a note that runtime and memory use depend on the visitor’s device. -- Put the main answer first: which engine ran, typical search latency, result-count agreement, and hardware/fallback status. -- Show median and p95 clearly. Keep cold upload/index preparation distinct from retained, repeated searches. -- Put detailed per-engine timings, VRAM estimates, charts, and the full result table in collapsible “Details” sections. -- Preserve corpus options (ASCII, CJK, and emoji/mixed script) and both substring/fuzzy benchmark modes. -- Preserve useful export actions (CSV, chart image, and concise Markdown summary), but keep them secondary to reading the result. -- Clearly label software/mock or otherwise non-qualified hardware results; avoid presenting them as dedicated-GPU performance. - -## 4. Example app test links - -The repository already contains these working apps: - -| Site route | Workspace app | Purpose | -| --- | --- | --- | -| `/examples/docs-search/` | `apps/docs-search` | Offline documentation search, filters/facets, suggestions, highlighting, and snapshots | -| `/examples/code-palette/` | `apps/monaco-palette` | Keyboard-first file/symbol search and preview | -| `/examples/log-viewer/` | `apps/log-viewer` | Large log search, filtering, live stream, and facets | - -The deployed site should serve these apps on the same origin so the homepage links open runnable examples rather than source-code pages. Their Vite asset bases must match their mounted paths. The site build will stage each example build into the site’s static output under the corresponding route. - -## 5. Implementation architecture - -### Site shell - -- Add a dedicated `apps/site` workspace for the new landing page and simple comparison page. -- Keep the site static and client-side; no backend service or remote search API is introduced. -- Use the `webgpu-search` workspace package for the interactive comparison, with CPU and WebGPU indexes over the same data. -- Keep the landing and comparison sources separate from the benchmark runner UI so each page remains focused. - -### Full benchmark app - -- Continue using `apps/benchmark` as the full benchmark execution workspace, but replace its current `index.html`, `src/style.css`, and old dashboard-oriented `src/main.ts` UI. -- Retain and reuse benchmark modules such as `src/benchmark.ts`, `src/dataset.ts`, `src/export.ts`, and the worker where practical. -- Build the redesigned full benchmark with its Vite base set to `/benchmark/`. -- The benchmark workspace may still be named `benchmark` internally; that name does not dictate the deployed home page. - -### One Vercel deployment - -- Change `vercel.json` so the deployed output is `apps/site/dist`. -- Make the site build depend on the benchmark and example-app builds, then stage their static output under `apps/site` before the site Vite build. -- Set each mounted app’s Vite base to its final same-origin route, so scripts, styles, workers, and assets resolve under the correct path. -- Verify Vercel serves `/`, `/benchmark/` (with `/compare/` redirecting there), and all three `/examples/.../` routes from this single deployment. - -## 6. Existing benchmark UI retirement - -The user explicitly wants the current dashboard design discarded. Implementation will: - -1. Replace its current benchmark-console page as the deployed root homepage. -2. Remove obsolete console markup/styles/DOM wiring that are not needed by the redesigned `/benchmark/` page. -3. Reuse the benchmark logic, not the current visual treatment or information hierarchy. -4. Keep the full benchmark available as `/benchmark/` on the same site. -5. Keep source/data/benchmark modules only where they serve the redesigned full benchmark or tests; remove dead UI code after checking references. - -## 7. Likely files and areas to change - -- `docs/SITE-REBUILD-PLAN.md` — this durable plan and status. -- `apps/site/` — new homepage, comparison page, shared styles, comparison logic, build/staging script, and app metadata. -- `apps/benchmark/index.html`, `apps/benchmark/src/main.ts`, `apps/benchmark/src/style.css` — replace the old dashboard UI with the new full-benchmark UX. -- `apps/benchmark/vite.config.ts` — benchmark route asset base. -- `apps/docs-search/vite.config.ts`, `apps/monaco-palette/vite.config.ts`, `apps/log-viewer/vite.config.ts` — mounted asset bases for same-origin example routes. -- `apps/*/package.json` as needed — workspace build dependencies/order. -- `vercel.json` — publish the new site output. -- `README.md` — point the live demo and feature links at the new homepage and pages after the routes are implemented. - -Exact file changes may be adjusted to fit existing Vite/Turbo conventions; the user-facing routes and requirements above are the contract. - -## 8. Build sequence - -1. **Record direction and inspect contracts:** keep this plan current; confirm index options, response timing fields, worker behavior, and benchmark runner inputs before wiring UI. -2. **Create the site workspace:** implement the monochrome responsive shell, homepage, feature summary, install block, and route navigation. -3. **Implement browser comparison:** build a reproducible generated corpus, run exact CPU and WebGPU-compatible fuzzy/substring queries, surface real engine/fallback status, and show simple readable results. -4. **Redesign the full benchmark page:** replace the old dashboard UI, reuse its benchmark core, make the result summary readable, and progressively disclose detailed tables/charts/exports. -5. **Mount examples on the same origin:** set their Vite bases, add build ordering/staging, and add working homepage cards for each deployed route. -6. **Switch the Vercel root:** point production output at `apps/site/dist`; confirm the old dashboard is no longer the home page and full benchmark is reachable under `/benchmark/`. -7. **Verify and refine:** run required repo checks, test GPU-available and CPU-fallback paths, inspect desktop/mobile layouts and all routes, and fix any issues before publishing a review preview. - -## 9. Acceptance criteria - -- [ ] `/` is a new clean product homepage, not the current benchmark dashboard. -- [ ] Primary UI is light, calm, and readable with the approved blue accent (`#1769e0`); code highlighting may use restrained colors. -- [ ] The homepage has a compact working search demo, an embedded quick benchmark, an optional custom-text playground, and clear links to the full benchmark and example apps. -- [ ] The homepage `#benchmark` section runs a genuine in-browser WebGPU-versus-exact-CPU comparison over the same dataset and query (`/compare/` retired; redirects to `/benchmark/`). -- [ ] Comparison reports actual timings, result counts, and fallback/device state without fabricated values or unsupported speedup claims. -- [ ] `/benchmark/` runs the full matrix locally in the visitor’s browser and uses a redesigned, less overwhelming UI. -- [ ] Full benchmark retains useful detailed statistics, multi-corpus coverage, qualification notes, and exports. -- [ ] Docs Search, Code Palette, and Log Viewer work at their same-origin routes on the deployed site. -- [ ] Visitors’ searches/datasets remain local to their browser. -- [ ] All routes work after Vite build and Vercel static deployment, including workers and nested assets. -- [ ] `bun run check:shaders`, `bun run typecheck`, `bun run build`, and `bun run test:mock` pass. -- [ ] No old-dashboard-only DOM hooks, styles, debug logs, or dead route assets remain. - -## 10. Verification and review - -For completion, run the required repository gates: - -```bash -bun run check:shaders -bun run typecheck -bun run build -bun run test:mock -``` - -Then verify the built site routes and the comparison/benchmark behaviors in a real browser where possible. Exercise WebGPU unavailable/fallback presentation as well as the WebGPU-ready path. Check that results are local, no benchmark claim is hard-coded as measured data, mobile layouts remain usable, and all visible test links point to working pages. Repository policy also requires a verification subagent review before sign-off. - -## 11. Scope boundaries - -- No change to the core library’s public search contract is planned. -- No WGSL shader edit is planned unless a defect is discovered; any such edit must keep source and inlined shader exports synchronized and pass shader checks. -- No server-side compute, account system, or analytics pipeline is needed for the website. -- The existing full benchmark capability is retained, but its current UI is not. -- A public Vercel deployment should happen only after local checks and review; a Here.now preview can be used for design review if requested/appropriate. diff --git a/docs/benchmarks.md b/docs/benchmarks.md index f155239..07815b1 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -1,10 +1,9 @@ # Reproducible Benchmarks (Phase 5) -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 5. > Fixtures: `scripts/benchmark-fixtures.ts` (`BENCHMARK_FIXTURE_VERSION = 1.0.0`). > Runner: `scripts/bench-snapshot-matrix.ts` (`bun run bench:snapshot-matrix`). > Pin: `scripts/test-benchmarks.ts` (`bun run test:benchmarks`). -> Baseline: `benchmark_snapshot_matrix.json` + `benchmark_snapshot_matrix.md` (repo root). +> Baseline: `docs/benchmarks/benchmark_snapshot_matrix.json` + `docs/benchmarks/benchmark_snapshot_matrix.md` (under `docs/benchmarks/`). Performance claims trace to named fixtures and environments. There are two report families — headless-CPU and browser-WebGPU — and they are never @@ -52,7 +51,7 @@ interpolation, samples checked in). `index-build` rows are single-shot | Report | Command | Engine | Runtime / browser / GPU | Memory | Comparability | | --- | --- | --- | --- | --- | --- | -| Headless CPU baseline (this doc, §§3–4) | `bun run bench:snapshot-matrix -- --out benchmark_snapshot_matrix.json` | CPU (`preferGpu: false`, exact scorer) | Bun/Node version + `platform` recorded in JSON `environment` | `vramBytes: 0` (no GPU upload), `ramBytes` from `getStats()`, `uploadMs: 0` | Headless CPU numbers are **NOT comparable to browser/WebGPU runs** | +| Headless CPU baseline (this doc, §§3–4) | `bun run bench:snapshot-matrix -- --out docs/benchmarks/benchmark_snapshot_matrix.json` | CPU (`preferGpu: false`, exact scorer) | Bun/Node version + `platform` recorded in JSON `environment` | `vramBytes: 0` (no GPU upload), `ramBytes` from `getStats()`, `uploadMs: 0` | Headless CPU numbers are **NOT comparable to browser/WebGPU runs** | | Browser WebGPU matrix | `bun run test:benchmark` / `test:benchmark:fuzzy` (`scripts/run-all-benchmarks.ts`, `apps/benchmark`) | WebGPU retained buffers vs CPU parity vs uFuzzy vs JS native | Chrome version, adapter (`vendor`/`device`/`architecture`), driver, OS recorded per row; software adapters report `pending-hardware` | Per-buffer VRAM (`records`/`offsets`/`query` 512 B/`output` 65,544 B) + packing pipeline (`normalizeMs`/`packMs`/`uploadMs`) | Browser numbers compare only against same-hardware runs; `pending-hardware` cells never qualify | Browser reports (`benchmark_results.json` / `benchmark_summary.md` from the @@ -65,8 +64,8 @@ not checked in as the 1.0 frozen baseline. Generated `2026-09-22T13:35:10.488Z`, `runtime=bun/1.4.2`, `platform=linux-arm64`, `engine=cpu`, `preferGpu=false`, `headless=true`. -Full samples + `environment` + `fixtures` in `benchmark_snapshot_matrix.json`; -same table in `benchmark_snapshot_matrix.md`. +Full samples + `environment` + `fixtures` in `docs/benchmarks/benchmark_snapshot_matrix.json`; +same table in `docs/benchmarks/benchmark_snapshot_matrix.md`. | Scenario | Docs | Operation | Median (ms) | p95 (ms) | Costs | |---|---|---|---:|---:|---| @@ -101,8 +100,8 @@ WebGPU reports carry real `vramBytes` per §2 and are not mixed into this table. ```bash # Full headless matrix (several minutes: 100k build + 10 samples/op). -# Writes benchmark_snapshot_matrix.json + benchmark_snapshot_matrix.md. -bun run bench:snapshot-matrix -- --out benchmark_snapshot_matrix.json +# Writes docs/benchmarks/benchmark_snapshot_matrix.json + docs/benchmarks/benchmark_snapshot_matrix.md. +bun run bench:snapshot-matrix -- --out docs/benchmarks/benchmark_snapshot_matrix.json # CI shape (throwaway copy, does not touch the checked-in baseline): bun scripts/bench-snapshot-matrix.ts --out /tmp/benchmark_snapshot_matrix.json diff --git a/benchmark_snapshot_matrix.json b/docs/benchmarks/benchmark_snapshot_matrix.json similarity index 100% rename from benchmark_snapshot_matrix.json rename to docs/benchmarks/benchmark_snapshot_matrix.json diff --git a/benchmark_snapshot_matrix.md b/docs/benchmarks/benchmark_snapshot_matrix.md similarity index 100% rename from benchmark_snapshot_matrix.md rename to docs/benchmarks/benchmark_snapshot_matrix.md diff --git a/docs/diagnostics.md b/docs/diagnostics.md index a286736..b88db94 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -1,6 +1,5 @@ # Diagnostics, Budgets & Issue-Report Data -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 6. > API freeze: `docs/public-api.md` §§2.5/5. Field types: `src/types.ts`, > policy: `src/diagnostics.ts`, guards: `src/guard.ts`. diff --git a/docs/packaging.md b/docs/packaging.md index 13ee4a6..4ea91b0 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -1,6 +1,5 @@ # Packaging, Bundle Budgets, Setup & Example-UI Accessibility -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 6. > Status: frozen guidance for `1.x`. Budgets are enforced by > `bun run check:bundle-size` (`scripts/check-bundle-size.ts`). diff --git a/docs/public-api.md b/docs/public-api.md index 64c7a05..063079a 100644 --- a/docs/public-api.md +++ b/docs/public-api.md @@ -1,6 +1,5 @@ # Public API Freeze (1.0 Contract) -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 0. > Status: frozen for `1.x`. Hosts can pin `^1.0.0` and rely on everything > documented here: entry points, class contracts, result ordering, response > echoes, errors, version constants, and the breaking-change / deprecation rules. @@ -168,6 +167,20 @@ Ranking (`src/ranking.ts:32`): `DEFAULT_TIE_BREAKERS`, `normalizeTieBreakers`, `compareRanked`, `sortRanked`, `compareIdsAsc`, `isExactTokenMatch`. Highlight (`src/highlight.ts`): `alignHighlights`, `normalizeWithSourceMap`, `guardClusterBoundary`, `mergeHighlightRanges`, `renderHighlightedText`. +Highlight ranges use UTF-16 offsets in the original string. With +`Intl.Segmenter`, source mapping uses the runtime's grapheme segmentation. +Without it, the dependency-free fallback preserves combining marks, skin-tone +modifiers, emoji ZWJ sequences (such as 👩‍💻), and regional-indicator flag +pairs (such as 🇺🇸). Matching a component highlights the whole sequence: +searching for 💻 in 👩‍💻 renders `👩‍💻`. Adjacent flags remain +separate. Source mapping and end-boundary guards share this fallback so neither +end of a range splits these sequences. + +The fallback covers these cases; it does not implement every rule in +[Unicode grapheme segmentation](https://www.unicode.org/reports/tr29/). +`bun run test:highlight` checks these boundaries with `Intl.Segmenter` disabled, +then restores it for the normal highlighting suite. + Hooks (`src/hooks.ts`): `defaultTokenizer`, `codeTokenizer`, `normalizeSearchHooks`, `resolveEffectiveHooks`, `hasAnyHook`, `getHookId`, `collectHookIds`, `assertHooksSatisfied`, `tokenizeWithHook`, diff --git a/docs/reliability.md b/docs/reliability.md index aefe0bb..585871f 100644 --- a/docs/reliability.md +++ b/docs/reliability.md @@ -1,6 +1,5 @@ # Reliability Proofs (Phase 2) -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 2. > Proves device-loss rebuild, resource cleanup, concurrent-query isolation, > and deterministic fallback — not just handling. diff --git a/docs/security-privacy.md b/docs/security-privacy.md index bb30e51..c7c307e 100644 --- a/docs/security-privacy.md +++ b/docs/security-privacy.md @@ -1,6 +1,5 @@ # Security & Privacy Boundaries -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 6. > Wire compat: `docs/snapshot-format.md`; runtime matrix: > `docs/support-matrix.md` §§3–4; packaging: `docs/packaging.md`. diff --git a/docs/snapshot-format.md b/docs/snapshot-format.md index d66cf2b..de49f0a 100644 --- a/docs/snapshot-format.md +++ b/docs/snapshot-format.md @@ -1,6 +1,5 @@ # Snapshot Format + Index-Format Compatibility (1.0 Contract) -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 3. > Status: frozen for `1.x`. The write path is canonical v4 only; the read > path accepts v3 (legacy, read-only migration) + v4. Any persisted index > restores or rejects with an actionable migration reason — never reads diff --git a/docs/support-matrix.md b/docs/support-matrix.md index b3216eb..f1aa584 100644 --- a/docs/support-matrix.md +++ b/docs/support-matrix.md @@ -1,6 +1,5 @@ # Support Matrix + CPU Baseline -> Plan ref: `docs/ISSUE-11-PLAN.md` Phase 1. > 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. @@ -148,7 +147,7 @@ The browser suite is the executing-hardware 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 `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 | +| 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 | @@ -190,8 +189,7 @@ browser regression gate on stable Chrome. ## 7. What this phase does not claim -Migration tables, the third proof app, and packaging guides are earlier -phases in `docs/ISSUE-11-PLAN.md` and are not claimed by -this matrix. Frozen benchmark fixtures live in `docs/benchmarks.md` +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`. diff --git a/packages/webgpu-search/src/highlight.ts b/packages/webgpu-search/src/highlight.ts index a408b96..f01d5f6 100644 --- a/packages/webgpu-search/src/highlight.ts +++ b/packages/webgpu-search/src/highlight.ts @@ -61,9 +61,104 @@ export interface RenderHighlightOptions { escapeHtml?: boolean; } -const COMBINING_MARK_REGEX = /^\p{M}/u; -const EXTEND_OR_JOINER_REGEX = /^[\p{M}\p{Sk}\u200D\uFE0F]/u; -const JAMO_V_OR_T_REGEX = /^[\u1160-\u11FF\uD7B0-\uD7FB]/u; +const STICKY_EXTEND_MODIFIER_REGEX = /(?:\p{M}|\p{Sk}|\uFE0F)/yu; +const STICKY_JAMO_V_OR_T_REGEX = /[\u1160-\u11FF\uD7B0-\uD7FB]/yu; +const STICKY_EMOJI_MODIFIER_REGEX = /[\u{1F3FB}-\u{1F3FF}]/yu; +const REGIONAL_INDICATOR_REGEX = /^\p{Regional_Indicator}/u; +const PICTOGRAPHIC_REGEX = /^\p{Extended_Pictographic}/u; + +/** + * Shared fallback boundaries for source maps and end guards. Consume the + * pictograph after a ZWJ as well as the joiner itself; otherwise a match on 💻 + * inside 👩‍💻 gets a start offset in the middle of the displayed emoji. + * Pair regional indicators from the start of each run (UAX #29 GB12/GB13), + * so adjacent flags remain separate. This is a targeted fallback, not a full + * implementation of the Unicode grapheme segmentation algorithm. + */ +function fallbackClusterEnd(raw: string, start: number): number { + const first = String.fromCodePoint(raw.codePointAt(start)!); + let end = start + first.length; + if (REGIONAL_INDICATOR_REGEX.test(first) && REGIONAL_INDICATOR_REGEX.test(raw.slice(end))) { + end += 2; + } + const pictographic = PICTOGRAPHIC_REGEX.test(first); + // GB9a: an emoji modifier (U+1F3FB..U+1F3FF) extends its base even with no + // ZWJ, so 👍 + 🏽 cluster together and queries never split the tone. + if (pictographic) { + STICKY_EMOJI_MODIFIER_REGEX.lastIndex = end; + if (end < raw.length && STICKY_EMOJI_MODIFIER_REGEX.test(raw)) { + end += 2; // modifiers are astral (one surrogate pair) + } + } + // GB11 core: consume Extend* ZWJ before checking for a trailing pictograph, + // so 👩‍💻 and 👩🏽‍💻 cluster as one even with a modifier before the joiner. + // Grapheme_Extend covers marks, ZWJ-adjacent glue (U+FE0F), and the + // emoji modifier range U+1F3FB..U+1F3FF (General_Category=Sk). ASCII + // Sk symbols like ` and ^ are excluded by the \x80 gate below so + // code identifiers such as "a`b" keep 1:1 source spans. Sticky regexes + // test one code point at `lastIndex` without slicing. + for (;;) { + while (end < raw.length) { + const cu = raw.charCodeAt(end); + if (cu < 0x80) break; // ASCII fast path: ASCII never extends a cluster + STICKY_EXTEND_MODIFIER_REGEX.lastIndex = end; + STICKY_JAMO_V_OR_T_REGEX.lastIndex = end; + if (!STICKY_EXTEND_MODIFIER_REGEX.test(raw) && !STICKY_JAMO_V_OR_T_REGEX.test(raw)) break; + end += String.fromCodePoint(raw.codePointAt(end)!).length; + } + if (end < raw.length && raw[end] === '\u200D' && pictographic) { + const afterJoiner = end + 1; + let scan = afterJoiner; + while (scan < raw.length) { + const scu = raw.charCodeAt(scan); + if (scu < 0x80) break; + STICKY_EXTEND_MODIFIER_REGEX.lastIndex = scan; + STICKY_JAMO_V_OR_T_REGEX.lastIndex = scan; + if (!STICKY_EXTEND_MODIFIER_REGEX.test(raw) && !STICKY_JAMO_V_OR_T_REGEX.test(raw)) break; + scan += String.fromCodePoint(raw.codePointAt(scan)!).length; + } + const tail = String.fromCodePoint(raw.codePointAt(scan) ?? 0x10ffff); + if (scan < raw.length && PICTOGRAPHIC_REGEX.test(tail)) { + end = scan + tail.length; + continue; + } + } + break; + } + return end; +} + +/** + * True when the piece at `pos` continues the cluster headed at `head`: + * a modifier/tone tail, a ZWJ tail, or a pictograph reached through + * Extend* ZWJ from a pictographic head (GB11 core). Used only to group + * shared source spans in the no-Segmenter fallback. + */ +function isClusterContinuation(raw: string, head: number, pos: number): boolean { + const headFirst = String.fromCodePoint(raw.codePointAt(head)!); + if (REGIONAL_INDICATOR_REGEX.test(headFirst)) { + return pos === head + headFirst.length && REGIONAL_INDICATOR_REGEX.test(raw.slice(pos)); + } + if (!PICTOGRAPHIC_REGEX.test(headFirst)) return false; + let scan = head + headFirst.length; + if (scan === pos) { + STICKY_EMOJI_MODIFIER_REGEX.lastIndex = scan; + if (scan < raw.length && STICKY_EMOJI_MODIFIER_REGEX.test(raw)) return true; + } + for (;;) { + while (scan < raw.length && scan < pos) { + const cu = raw.charCodeAt(scan); + if (cu < 0x80) return false; + STICKY_EXTEND_MODIFIER_REGEX.lastIndex = scan; + STICKY_JAMO_V_OR_T_REGEX.lastIndex = scan; + if (!STICKY_EXTEND_MODIFIER_REGEX.test(raw) && !STICKY_JAMO_V_OR_T_REGEX.test(raw)) return false; + scan += String.fromCodePoint(raw.codePointAt(scan)!).length; + } + if (scan !== pos) return scan > pos; + if (scan < raw.length && raw[scan] === '\u200D') return true; + return scan > pos; + } +} let cachedSegmenter: Intl.Segmenter | null = null; function getSegmenter(): Intl.Segmenter | null { @@ -210,32 +305,22 @@ export function normalizeWithSourceMap(raw: string, normalized: boolean): Source appendSegmentTokens(segStr, normalized, spanStart, spanEnd, allTokens, allStarts, allEnds); } } else { - // Fallback grapheme iteration for environments without Intl.Segmenter + // Fallback grapheme iteration for environments without Intl.Segmenter. + // Modifier-continuation pieces (tone/VS16 after a base, ZWJ tail after a + // joiner) share the cluster head's span, mirroring Intl.Segmenter: a + // component query then highlights the whole displayed sequence. let i = 0; const wfLen = wellFormed.length; + let clusterHead = 0; while (i < wfLen) { const segStart = i; - const cp = wellFormed.codePointAt(i) as number; - i += cp > 0xffff ? 2 : 1; - - // Extend across following combining marks, modifiers, joiners, and Jamo - while (i < wfLen) { - const nextCp = wellFormed.codePointAt(i) as number; - const nextLen = nextCp > 0xffff ? 2 : 1; - const slice = wellFormed.slice(i, i + nextLen); - if ( - COMBINING_MARK_REGEX.test(slice) || - EXTEND_OR_JOINER_REGEX.test(slice) || - JAMO_V_OR_T_REGEX.test(slice) - ) { - i += nextLen; - } else { - break; - } - } + i = fallbackClusterEnd(wellFormed, segStart); + const continuesCluster = + segStart > clusterHead && isClusterContinuation(wellFormed, clusterHead, segStart); + if (!continuesCluster) clusterHead = segStart; const segStr = wellFormed.slice(segStart, i); - const spanStart = leadingTrimOffset + segStart; + const spanStart = leadingTrimOffset + clusterHead; const spanEnd = leadingTrimOffset + i; appendSegmentTokens(segStr, normalized, spanStart, spanEnd, allTokens, allStarts, allEnds); } @@ -285,17 +370,18 @@ export function guardClusterBoundary(raw: string, end: number): number { return cur; } - // Regex fallback for environments without Intl.Segmenter - while (cur < len) { - const cp = raw.codePointAt(cur); - if (cp === undefined || cp < 0x0300) break; - const charLen = cp > 0xffff ? 2 : 1; - const slice = raw.slice(cur, cur + charLen); - if (EXTEND_OR_JOINER_REGEX.test(slice) || JAMO_V_OR_T_REGEX.test(slice)) { - cur += charLen; - } else { - break; - } + // ASCII cannot extend an emoji/mark cluster. Keep ordinary fuzzy highlights + // from rescanning their prefix for every matched token in the fallback. + // 0x80 is the ASCII boundary (0x7f is the highest ASCII unit); charCodeAt + // here is an ASCII-only fast path (see parity-lint auto-allow rule). + if (raw.charCodeAt(cur) < 0x80) return cur; + + // Use the same boundaries as source mapping, including flag-pair parity. + let start = 0; + while (start < cur) { + const clusterEnd = fallbackClusterEnd(raw, start); + if (cur < clusterEnd) return clusterEnd; + start = clusterEnd; } return cur; } @@ -485,7 +571,11 @@ export function alignHighlights( return [{ start, end }]; } - // Fuzzy mode: greedy forward subsequence matching (identical to scoreFuzzyTokens & fuzzy.wgsl) + // Fuzzy mode: greedy forward subsequence matching (identical to scoreFuzzyTokens & fuzzy.wgsl). + // Per-token ranges use the token's own source span; the trailing boundary is + // cluster-guarded while the start is not rewound, so a component query + // (e.g. laptop inside woman-technologist) never swallows its whole cluster. + // Range merging keeps adjacent cluster pieces contiguous. let q = 0; const strLen = sourceMap.tokenCount; const rawRanges: HighlightRange[] = []; diff --git a/scripts/bench-data/dataset-sizes.ts b/scripts/bench-data/dataset-sizes.ts new file mode 100644 index 0000000..f1505f1 --- /dev/null +++ b/scripts/bench-data/dataset-sizes.ts @@ -0,0 +1,5 @@ +/** Shared interactive benchmark sizes; frozen snapshot fixtures are versioned separately. */ +export const DATASET_SIZES = [10_000, 100_000, 200_000, 500_000, 1_000_000, 2_000_000] as const; + +export const SITE_DATASET_SIZES = DATASET_SIZES.filter(size => size >= 100_000); +export const BENCHMARK_DATASET_SIZES = DATASET_SIZES.filter(size => size !== 200_000); diff --git a/scripts/bench-snapshot-matrix.ts b/scripts/bench-snapshot-matrix.ts index d23c30a..dda1011 100644 --- a/scripts/bench-snapshot-matrix.ts +++ b/scripts/bench-snapshot-matrix.ts @@ -15,7 +15,7 @@ * Frozen fixtures live in `scripts/benchmark-fixtures.ts` * (`BENCHMARK_FIXTURE_VERSION`, corpora, sizes, queries, modes, filters, * facets). Normative doc: `docs/benchmarks.md`. Checked-in baseline: - * `benchmark_snapshot_matrix.json` + `benchmark_snapshot_matrix.md`. + * `docs/benchmarks/benchmark_snapshot_matrix.json` + `docs/benchmarks/benchmark_snapshot_matrix.md`. * * Environment: headless CPU only (`preferGpu: false`) for determinism. * Numbers are NOT comparable to browser/WebGPU runs. The JSON report records @@ -88,8 +88,8 @@ interface MatrixRow { } function resolveOutPath(raw: string | undefined): string { - const fallback = 'benchmark_snapshot_matrix.json'; - if (!raw) return fallback; + const fallback = 'docs/benchmarks/benchmark_snapshot_matrix.json'; + if (!raw || raw === fallback) return fallback; // Allow explicit absolute paths under cwd or /tmp (CI uses /tmp), otherwise // sanitize to basename inside cwd to avoid arbitrary writes (e.g. /etc/passwd). if (raw.startsWith('/tmp/') || raw.startsWith(`${process.cwd()}/`)) return raw; @@ -308,7 +308,9 @@ async function main(): Promise { console.log(''); if (outPath) { - const { writeFileSync } = await import('node:fs'); + const { mkdirSync, writeFileSync } = await import('node:fs'); + const { dirname } = await import('node:path'); + mkdirSync(dirname(outPath), { recursive: true }); const generatedAt = new Date().toISOString(); const environment = { engine: 'cpu', diff --git a/scripts/benchmark-fixtures.ts b/scripts/benchmark-fixtures.ts index fa72647..69dc9cd 100644 --- a/scripts/benchmark-fixtures.ts +++ b/scripts/benchmark-fixtures.ts @@ -7,7 +7,7 @@ * versioned corpora, record sizes, queries, modes, filters, and facets * cannot drift between the runner, the checked-in baseline, and the docs. * - * Plan ref: `docs/ISSUE-11-PLAN.md` Phase 5. Normative doc: + * Normative doc: * `docs/benchmarks.md`. * * Portability: DOM-free (no `window` / `document`); safe in workers, diff --git a/scripts/check-parity-lint.ts b/scripts/check-parity-lint.ts index 239b5f6..6251736 100644 --- a/scripts/check-parity-lint.ts +++ b/scripts/check-parity-lint.ts @@ -107,13 +107,15 @@ for (const rel of strictTargets) { }); if (ccHits.length > 0) { const allowed = new Set(charCodeAtAllowlist[name] ?? []); - // Auto-allow vetted unit accesses: ASCII fast path (0x7f/0x41) and the + // Auto-allow vetted unit accesses: ASCII fast path (0x7f/0x80 boundary, + // 0x41 case-fold) and the // lone-surrogate last-resort scan, which must inspect UTF-16 units // (0xd800/0xdbff/0xdc00) to detect lone surrogates on pre-2020 engines. const autoOk = ccHits.filter((ln) => { const ctx = lines.slice(Math.max(0, ln - 6), ln + 2).join('\n'); return ( ctx.includes('0x7f') || + ctx.includes('0x80') || ctx.includes('0x41') || ctx.includes('isAscii') || ctx.includes('asciiToTokens') || diff --git a/scripts/run-all-benchmarks.ts b/scripts/run-all-benchmarks.ts index d1d0727..359f662 100644 --- a/scripts/run-all-benchmarks.ts +++ b/scripts/run-all-benchmarks.ts @@ -1,4 +1,5 @@ import puppeteer from 'puppeteer-core'; +import { BENCHMARK_DATASET_SIZES } from './bench-data/dataset-sizes'; import fs from 'fs'; import path from 'path'; import { @@ -20,7 +21,7 @@ async function main() { }; const isFast = args.includes('--fast'); - const sizesArg = getArg('sizes') || (isFast ? '10000,100000' : '10000,100000,500000,1000000,2000000'); + const sizesArg = getArg('sizes') || (isFast ? '10000,100000' : BENCHMARK_DATASET_SIZES.join(',')); const corpusArg = getArg('corpus') || 'ascii'; const queryArg = getArg('query'); const warmupsArg = getArg('warmups') || (isFast ? '2' : '5'); diff --git a/scripts/run-fuzzy-benchmark.ts b/scripts/run-fuzzy-benchmark.ts index 74586ae..dc3d86e 100644 --- a/scripts/run-fuzzy-benchmark.ts +++ b/scripts/run-fuzzy-benchmark.ts @@ -1,4 +1,5 @@ import puppeteer from 'puppeteer-core'; +import { BENCHMARK_DATASET_SIZES } from './bench-data/dataset-sizes'; import fs from 'fs'; import path from 'path'; import { @@ -19,7 +20,7 @@ async function main() { }; const isFast = args.includes('--fast'); - const sizesArg = getArg('sizes') || (isFast ? '10000,100000' : '10000,100000,500000,1000000,2000000'); + const sizesArg = getArg('sizes') || (isFast ? '10000,100000' : BENCHMARK_DATASET_SIZES.join(',')); const corpusArg = getArg('corpus') || 'ascii'; const queryArg = getArg('query'); const warmupsArg = getArg('warmups') || (isFast ? '2' : '5'); diff --git a/scripts/test-benchmarks.ts b/scripts/test-benchmarks.ts index d0b17be..a24915a 100644 --- a/scripts/test-benchmarks.ts +++ b/scripts/test-benchmarks.ts @@ -9,7 +9,7 @@ * - Frozen query shapes serve live on small smoke indexes (ordering, * filters, facets, autocomplete, snapshot serialize/restore with * bytes + time, memory `vramBytes` / `ramBytes`, build costs). - * - Checked-in baseline `benchmark_snapshot_matrix.json` exists, matches the + * - Checked-in baseline `docs/benchmarks/benchmark_snapshot_matrix.json` exists, matches the * frozen version, covers every (scenario, operation) pair, and keeps * headless-CPU / browser-WebGPU reports separate (non-comparable label). * - `docs/benchmarks.md` publishes the per-scenario table + environments. @@ -232,8 +232,8 @@ async function main(): Promise { // ------------------------------------------------ 4. Checked-in baseline console.log('4. Checked-in baseline artifacts'); - const jsonPath = path.join(rootDir, 'benchmark_snapshot_matrix.json'); - const mdPath = path.join(rootDir, 'benchmark_snapshot_matrix.md'); + const jsonPath = path.join(rootDir, 'docs/benchmarks/benchmark_snapshot_matrix.json'); + const mdPath = path.join(rootDir, 'docs/benchmarks/benchmark_snapshot_matrix.md'); check('baseline JSON checked in', fs.existsSync(jsonPath)); check('baseline markdown checked in', fs.existsSync(mdPath)); if (fs.existsSync(jsonPath)) { diff --git a/scripts/test-highlight.ts b/scripts/test-highlight.ts index 502e8a0..ac725b2 100644 --- a/scripts/test-highlight.ts +++ b/scripts/test-highlight.ts @@ -39,6 +39,50 @@ interface DocItem { async function runM3Tests() { console.log('--- Running Unicode-Safe Highlighting Engine Tests ---'); + // Run before any normal highlighting call can cache Intl.Segmenter. + console.log('0. Testing emoji boundaries without Intl.Segmenter...'); + const segmenterDescriptor = Object.getOwnPropertyDescriptor(Intl, 'Segmenter'); + Object.defineProperty(Intl, 'Segmenter', { value: undefined, configurable: true }); + try { + const cases = [ + { raw: 'Dev 👩‍💻 at work', query: '💻', start: 4, end: 9 }, + { raw: 'Dev 👩‍💻 at work', query: '👩', start: 4, end: 9 }, + { raw: ' 👨‍👩‍👧‍👦!', query: '👧', start: 2, end: 13 }, + { raw: 'Flag 🇺🇸 here', query: '🇸', start: 5, end: 9 }, + { raw: '🇺🇸🇨🇦!', query: '🇨', start: 4, end: 8 }, + { raw: '🇺🇸🇨!', query: '🇨', start: 4, end: 6 }, + { raw: '👍🏽!', query: '🏽', start: 0, end: 4 }, + { raw: '👍🏽!', query: '👍🏽', start: 0, end: 4 }, + { raw: '👩🏽‍💻!', query: '💻', start: 0, end: 7 }, + { raw: '👩🏽‍💻!', query: '👩🏽‍💻', start: 0, end: 7 }, + { raw: 'Press 1️⃣ now', query: '1️⃣', start: 6, end: 9 }, + { raw: 'I ❤️ WS', query: '❤️', start: 2, end: 4 }, + { raw: 'Fly 🏴‍☠️!', query: '🏴‍☠️', start: 4, end: 9 }, + { raw: 'Café!', query: 'é', start: 3, end: 5 }, + { raw: '👩‍💻💡!', query: '💡', start: 5, end: 7 }, + { raw: 'a\u200Db', query: 'b', start: 2, end: 3 }, + { raw: 'plain text', query: 'text', start: 6, end: 10 } + ]; + for (const { raw, query, start, end } of cases) { + for (const mode of ['substring', 'fuzzy'] as const) { + const ranges = alignHighlights(raw, query, { mode }); + assert.deepStrictEqual(ranges, [{ start, end }], `${mode}: ${raw} / ${query}`); + assert.strictEqual(renderHighlightedText(raw, ranges, 'mark'), + `${raw.slice(0, start)}${raw.slice(start, end)}${raw.slice(end)}`); + } + } + assert.strictEqual(guardClusterBoundary('👍🏽!', 2), 4); + assert.strictEqual(guardClusterBoundary('👩🏽‍💻!', 2), 7); + assert.strictEqual(guardClusterBoundary('👩‍💻!', 2), 5); + assert.strictEqual(guardClusterBoundary('🇺🇸🇨🇦!', 2), 4); + assert.strictEqual(guardClusterBoundary('🇺🇸🇨🇦!', 4), 4); + assert.strictEqual(guardClusterBoundary('🇺🇸🇨🇦!', 6), 8); + } finally { + if (segmenterDescriptor) Object.defineProperty(Intl, 'Segmenter', segmenterDescriptor); + else Reflect.deleteProperty(Intl, 'Segmenter'); + } + + // ========================================================================= // 1. Indentation Trim Compensation (leadingTrimOffset) // ========================================================================= diff --git a/scripts/test-reliability.ts b/scripts/test-reliability.ts index 9ca10c3..3c8499a 100644 --- a/scripts/test-reliability.ts +++ b/scripts/test-reliability.ts @@ -1,5 +1,5 @@ /** - * Phase 2 reliability proofs (docs/ISSUE-11-PLAN.md): + * Reliability proofs (docs/reliability.md): * device-loss rebuild, resource cleanup, concurrent-query isolation, * deterministic fallback. *