PLAN-followups
v1.6 polish: post-v1.5 follow-ups (#487)
Status (2026-06-27)
Three sub-items captured by v1.5 round 2 + round 3 deferrals; one (Sub-item C) landed, two (A, B) deferred with a substrate-blocker report.
- Sub-item C — wasm-tools component new per-artifact rationale: DONE.
- Sub-item A — bundle-cli SPI rewrite to dispatch-bridge: DEFERRED (substrate blocker; see analysis below).
- Sub-item B — JS polyfill SQL strings auto-sync from Rust constants: DEFERRED, downstream of A. If A retires the polyfill SQL strings entirely, B is moot.
Sub-item C: per-artifact wasm-tools rationale — DONE
Survey output:
| Artifact | Source | crate-type | wit-bindgen | Output kind | wasm-tools component new? |
|---|---|---|---|---|---|
sqlite_cli.wasm | cli/ | cdylib | macro | CORE module + custom | YES — kept |
sqlite_lib.wasm | submodule sqlite-wasm/sqlite-lib/ | cdylib | macro | CORE module + custom | YES — kept |
extensions/*/*.wasm | each extension cdylib | cdylib (+ rlib) | macro | mostly CORE; some auto-component | auto-detected per-artifact |
None of the three categories build via cargo component build,
so wasm-tools component new is always required for cargo-built
cdylibs. Some toolchain configurations produce a component
directly (auto-detect on wasm32-wasip2); the script branches.
v1.5 round 1 incorrectly assumed wasm32-wasip2 always emits
components and considered the step dead; round 2 corrected by
un-removing it for sqlite_cli.wasm and sqlite_lib.wasm
specifically. This sub-item documents the per-artifact decision
inline in each script so a future reader does not re-make round
1's mistake.
Scripts annotated with per-artifact rationale:
scripts/build-composed-runtime.sh—sqlite_cli.wasm→ core module → kept.scripts/build-composed-runtime-single-memory.sh— same.scripts/encode-extension-components.sh— auto-detect viawasm-tools printhead, copy-through if already a component elsewasm-tools component new. Comment captures the per- artifact branch rationale.sqlite-wasm/scripts/build-sqlite-lib-component.sh— lives in the sqlite-wasm submodule; left in place (already correct). If the comment-add lands separately, it is purely cosmetic.
Sub-item A: bundle-cli SPI rewrite — DEFERRED (substrate blocker)
Goal: rewire extensions/bundle-cli/src/lib.rs to call
sqlink:wasm/dispatch-bridge.bridged-execute-cas directly instead
of sqlite:extension/bundles, so the browser polyfill
(browser/src/extension-loader.js lines ~1898-2351, ~450 lines)
can retire its sqlite:extension/bundles typed shim.
The blocker: bundle-cli's WIT world (dotcmd-aware in
sqlite-wit/wit/sqlite-extension/world.wit) belongs to the
sqlite:extension@1.0.0 package, while dispatch-bridge lives in
sqlink:wasm@0.1.0 (defined in sqlite-wasm/wit/dispatch-bridge.wit).
The two packages are not currently mutually-importable in a way
that lets bundle-cli's wit_bindgen::generate! macro reach
sqlink:wasm/dispatch-bridge without cross-package wit-deps
restructuring.
Specifically:
-
WIT package boundary. sqlite-wit's worlds today only import interfaces in their own package (
sqlite:extension/{spi, session, bundles, ...}). Adding a cross-package import would require either:- extending sqlite-wit's
wit/deps/to vendor thesqlink:wasmpackage (mirrors what sqlite-wasm already does the other way atsqlite-wasm/wit/deps/sqlite-extension/), OR - giving bundle-cli a bespoke world definition outside sqlite-wit so its bindgen path can be a UNION of both wit dirs. Either path crosses a submodule + package-versioning boundary not previously crossed by any in-tree extension.
- extending sqlite-wit's
-
Native host's bundle-cli load path.
host/src/lib.rs'sloaded_dotcmd_awaremodule satisfies bundle-cli's imports viawasmtime::component::bindgenagainst the dotcmd-aware world. It implementssqlite:extension/bundles::Host(host/src/lib.rsline 4009) directly againstsqlite_cas_cache::bundles_exec. Switching bundle-cli to dispatch-bridge would require:- adding a
dispatch_bridge::Hostimpl onLoadedState(open the cas connection, runbridged_execute_cas(sql, params)throughsqlite_cas_cache::bundles_exec's shared SQL constants); - extending the
bindgen!macro'swith:map to include the new package's interfaces; - possibly adding the sqlink:wasm WITs to the host's wit path search. Net new native code: ~150-200 lines. Manageable, but downstream of (1).
- adding a
-
bundle-cli runtime rewrite. Once the WIT can import
bridged-execute-cas, the bundle-cli body itself needs every typed call (bundles::bundle_save(name, set_hash, members)) rewritten to:- compose the right SQL string from
bundles_exec::*_SQLconstants; - bind parameters as
sql-valuelist; - call
bridged_execute_cas(sql, params); - parse the returned
query-resultrows back into typed struct shapes. Net new wasm-side code: ~400-500 lines (replacing ~300 lines of typed-call sites). Doable but couples to (1)'s WIT shape.
- compose the right SQL string from
-
WAC composition wiring (browser path). The composed browser binary's current recipe (
composition-cli-sqlite-lib.wac) doesn't wire bundle-cli's imports through sqlite-lib's dispatch-bridge export — it just re-exports dispatch-bridge to the JS host. If bundle-cli (a SEPARATE in-cli component, loaded at runtime viaextension_loader.load-extension-from-bytes) imports dispatch-bridge, sqlite-lib's export needs to flow through to it. Today's loader runtime doesn't compose dynamically; it callsComponent::new(bytes)then satisfies imports through the host wit-bindgen path. The browser-side polyfill would need to expose dispatch-bridge as a runtime-bindgen-satisfiable import to bundle-cli — which means re-implementing the same bridge surface in JS that sqlite-lib already exports. That loops back to a polyfill, just under a different interface name; the polyfill removal Sub-item A targets does NOT cleanly follow.
Conclusion (the substrate blocker): of points (1)-(4), point
(4) is the deal-breaker for the polyfill-removal goal. Even with
the WIT cross-package work done, the runtime-bindgen path that
the JS host uses to satisfy bundle-cli's imports means dispatch-
bridge still has to be polyfilled JS-side; it just moves from
sqlite:extension/bundles (twelve typed methods) to
sqlink:wasm/dispatch-bridge.bridged-execute-cas (one method).
The MOVE itself reduces the polyfill from ~450 lines to ~30-50 lines (one execute-cas method plus the schema bootstrap), which is the genuine win Sub-item A aimed at. But this requires: * sqlite-wit deps restructuring (or a bundle-cli bespoke world); * native host's dotcmd-aware bindgen extended to satisfy sqlink:wasm/dispatch-bridge for bundle-cli; * bundle-cli rewrite (~500 lines); * polyfill restructuring (the bundles handler set goes from twelve typed methods to one execute-cas method); * acceptance test pass on composed-bundle.spec.js (including reload leg).
Estimated effort: 3-5 days of focused work, spanning two submodules + workspace + browser. NOT feasible in a single session that also covers Sub-items B and C cleanly.
Recommendation: split Sub-item A into its own follow-up task (#487a or a new task ID) with explicit phases: a. WIT cross-package import substrate (deps/ wiring, decide between vendoring vs bespoke world). b. Native host's dotcmd-aware bindgen extension. c. bundle-cli SPI call-site rewrite. d. Browser polyfill restructuring. e. Composed-bundle browser test pass; bundle-cli native test pass.
Until then the v1.5 round 2 architecture (bundle-cli imports the
typed bundles SPI; native host implements it directly; browser
polyfill emulates it through bridged-execute-cas) remains the
shipping shape. Both sides are tested and work end-to-end; the
v1.6 polish goal was code-shape cleanup, not functional fix.
Sub-item B: JS polyfill SQL strings auto-sync from Rust — DEFERRED
Downstream of Sub-item A. Two cases:
-
If A's polyfill restructuring lands: the remaining JS-side SQL surface is just the schema bootstrap statements; the typed-method SQL drops out entirely. B becomes a small win (auto-sync the schema bootstrap statements from
sqlite_cas_cache::schema::INSTALL_SCHEMAinstead of inline JS). Effort: 0.5 day, choose path β (shared text filesqlite-cas-cache/sql/install_schema.sqlimported by both Rustinclude_str!and the JS polyfillimport.meta.url- fetched at build time). -
If A stays unlanded: B's value is preserved (the ~12 typed-method SQL strings in the polyfill still need to be byte-aligned with
bundles_exec::*_SQL). Path α (build-time codegen) would parse the Rust consts into JSON via abuild.rsstep and emit abundle-cli-sql.jsthe browser imports. Effort: 1-1.5 days. Path β stays an option too but requires extracting the consts to a shared.sqlfile first, which mechanically changes ~150 lines of Rust.
Either way, B does not block any shipping path today (the hand-sync is currently honest — both sides drive the same end-to-end test surface — and v1.5 round 3's transcription is byte-aligned with the Rust constants as documented in the polyfill's leading comment). Recommend doing B together with A, after A's substrate clears.
Verification (this fork)
bash -n scripts/build-composed-runtime.sh scripts/build-composed-runtime-single-memory.sh scripts/encode-extension-components.sh— all three pass. (Doc-only changes; no behavior delta. End-to-end build requires the wasm32-wasip2 toolchain + wasi-sdk + composed- runtime prerequisites; not re-run in this fork because Sub-item C is documentation-only.)- Sub-item A + B verification deferred along with the work.
Honest deferrals
- Sub-item A: full rewrite deferred. Substrate-blocker report above. Recommend splitting into its own task.
- Sub-item B: deferred together with A.
- The doc comment in
sqlite-wasm/scripts/build-sqlite-lib- component.sh(submodule-side) is intentionally NOT modified in this fork to avoid an extra submodule bump for a purely cosmetic change; the script is already correct and is referenced from the v1.6 plan-doc summary above.
v1.2: Browser composed-cli auto-load via JS registry
Background
composed-bundle.spec.js + composed-prefix.spec.js were
un-skipped in #479 with the assumption that .bundle save /
.prefix add etc. would round-trip via ComposedDatabase. execDotCommand. Running them with npm test in browser/
shows the assumption was wrong:
auto-load core-dotcmd failed: extension 'core-dotcmd' not in JS
registry. Call db.loadExtension(name, bytes) first. (404).
auto-load bundle-cli failed: ...
auto-load prefix-cli failed: ...
[+9 cascading 404s for sqlink-meta-cli, sha3sum-cli, serialize-cli,
archive-cli, session-cli, sqlite-utils-{schema,fts,maint,data}]
The architectural mismatch
The cli's startup code in cli/src/lib.rs calls
extension_loader::load_extension_from_bytes("prefix-cli", PREFIX_CLI_BYTES, &options) for each of its 12 cli-family
extensions. The bytes are baked in at compile-time via
include_bytes!. The native loader honors the bytes.
The browser polyfill (browser/src/extension-loader.js)
ignores the bytes and looks the extension up by NAME in a
pre-registered JS registry (opts.embed map in
openDatabaseComposed). The registry is populated only from
browser/scripts/transpile-extensions.mjs's PICK list,
which currently has just 2 entries: bundle_cli, prefix_cli.
Why the prior #479 fix wasn't enough
#479 (commits aca8d484 / 3afee868 / 7ae1644f) added the
execDotCommand method + Playwright assertions but didn't
touch the polyfill registry or the transpile PICK list. Without
those, the cli's load_extension_from_bytes calls 404 inside
the polyfill and every cli-family extension stays unregistered;
the dot-cmd dispatch table is empty; .prefix list / .bundle list report "Unknown command".
The Playwright assertions that passed (e.g. addOut matched
/foaf/) did so coincidentally — the URL contained "foaf" and
the cli's "Unknown command: .prefix add foaf
Scope
Four pieces. Each can be its own commit; or bundled if small.
-
Expand the transpile PICK list in
browser/scripts/transpile-extensions.mjsfrom 2 to 12:core-dotcmd,sqlink-meta-cli,sha3sum-cli,serialize-cli,archive-cli,session-cli,sqlite-utils-schema,sqlite-utils-data,sqlite-utils-fts,sqlite-utils-maint, plus the existingbundle-cli,prefix-cli. Re-runnpm run pretestto regeneratebrowser/src/generated/index.jswith all 12 inEXTENSION_LOADERS. -
Pass all 12 in
opts.embedin the two test HTMLs (composed-bundle.html,composed-prefix.html): changeopenDatabaseComposed({})toopenDatabaseComposed({embed: ['core-dotcmd', 'sqlink-meta-cli', ..., 'prefix-cli']}). Without this, the registry stays empty even with PICK expanded. -
Verify the polyfill's
instantiateFromBytespath works.extension-loader.js:610hasregistry.instantiateFromBytes = (bytes) => instantiateExtensionFromBytes(bytes). If this path is functional, an alternative to the embed-by-name approach is to make the polyfill honor the bytes the cli passes. Cheaper than expanding the transpile pipeline if it works. -
Decide on the polyfill's contract. Two viable shapes:
- (a) Cli passes bytes → polyfill instantiates from bytes directly (matches native loader semantics).
- (b) Cli looks up by name → polyfill resolves against the
pre-transpiled module map (current behavior).
v1.2 should pick one + document it. If (a), retire the
transpile PICK list dance; if (b), generate the PICK list
from the cli's
include_bytes!list automatically.
Effort
1.5-2 days. Most of it is verifying option (a) works (probably half a day with debug iterations) + updating the two specs once the auto-load path stabilizes. Option (b)'s "expand PICK to 12" path is faster (a few hours) but leaves the architectural mismatch in place for v1.3 to address.
Dependencies
c558327in sqlite-wasm (WIT sync, landed) — without this,wac composeitself fails before the runtime even loads.aca8d484in sqlink (execDotCommandmethod, landed).
Out of scope (v1.3+)
- General dispatch-bridge for externally-loaded (
.load PATH) dot-cmd-aware extensions in the browser. Today.loadfrom the cli's interactive prompt is a separate code path; this v1.2 item only targets the auto-loaded built-in cli-family extensions.
Status
Foundation merged, deeper substrate gap surfaced.
Two commits landed (e9684ed6 polyfill bytes-path + a7dd537e
jspi async-imports marking). These unblock the bytes-path (no
more 404 "not in JS registry") and the async wiring (no more
_utf8AllocateAndEncode received [undefined]). The cli's
auto-load loop now reaches per-extension instantiation for all
12 cli-family bytes.
But each per-extension instantiation now fails at a SECOND
substrate gap: the cli-family extensions import SPI interfaces
the polyfill's buildExtensionAdditionalImports doesn't
provide. Runtime-bindgen instantiates each extension in
ISOLATION against the polyfill — which has no idea how to wire
back into the composed cli's own SPI implementations.
Missing SPI imports per extension:
| Extension | Missing import surface |
|---|---|
| core-dotcmd | sqlite:extension/cli-state (getBool) |
| sqlink-meta-cli | sqlite:extension/loader-bridge (loadExtensionFromBytes) |
| serialize-cli, archive-cli | sqlite:extension/cli-stdout (write) |
| sqlite-utils-maint | sqlite:extension/cli-state (getText) |
| bundle-cli | sqlite:extension/build (spawnBuild) |
| prefix-cli | sqlite:extension/loader-bridge (applyPrefixPin) |
The native loader satisfies these via the composed cli's own exports. The browser needs per-interface host handlers in the polyfill that proxy back to the cli runtime — non-trivial because each handler needs to access cli state from inside an extension's instantiation closure.
Tests still fail (Unknown command: .prefix list) because
prefix-cli's instantiation errors out at the loader-bridge gap;
same for bundle-cli. The foundation merge keeps the byte-path
viable but the round-trip end-to-end test continues to fail.
Captured by task #481. SPI bridging tracked separately as v1.3 (see next section).
v1.3: Browser cli-family SPI bridging
Background
v1.2 (#481) landed the byte-path + async wiring foundation but uncovered that cli-family extensions import 4 SPI interfaces the browser polyfill doesn't satisfy. Each handler needs to proxy back to the composed cli's own implementations:
sqlite:extension/cli-state— get/set cli session state (used by core-dotcmd, sqlite-utils-maint).sqlite:extension/cli-stdout— write to the cli's stdout pipe (used by serialize-cli, archive-cli).sqlite:extension/loader-bridge— load other extensions + apply prefix-pins (used by sqlink-meta-cli, prefix-cli).sqlite:extension/build— spawn-build for.bundle build(used by bundle-cli).
Three viable shapes (RE-RANKED after attempted execution)
The original v1.3 recommendation was option 2 (~3 days). A fork investigation found the estimate was wrong because the cli-family extensions IMPORT host-side interfaces that no WAC-composable component EXPORTS today:
| Imported by | Interface | Defined in | Exported by |
|---|---|---|---|
| prefix-cli, sqlink-meta-cli | sqlite:extension/loader-bridge | sqlite-wit/wit/sqlite-extension/loader-bridge.wit | (none — host impl only) |
| core-dotcmd, sqlite-utils-maint | sqlite:extension/cli-state | sqlite-wit/wit/sqlite-extension/dotcmd.wit:138 | (none) |
| serialize-cli, archive-cli | sqlite:extension/cli-stdout | sqlite-wit/wit/sqlite-extension/dotcmd.wit:90 | (none) |
| bundle-cli | sqlite:extension/build | sqlite-wit/wit/sqlite-extension/host-spi.wit | sqlite-lib (OK) |
The native sqlink-host implements loader-bridge / cli-state /
cli-stdout in host/src/lib.rs as wasmtime host impls — never
as wasm exports. WAC can't wire an import to "Rust code in a
wasmtime runtime"; it needs a peer component.
Re-ranked options:
-
Per-interface polyfill handlers — NOW THE CHEAPEST. Add JS handlers in the browser polyfill for
loader-bridge,cli-state,cli-stdoutthat proxy back to the cli runtime via shared state.buildis already partially in sqlite-lib. Estimated: ~1 week (1-2 days per interface). -
Extract cli-host-shim component + WAC recipe — was the original v1.3 plan; ACTUAL effort is ~10 days, not ~3:
- Carve
loader-bridge/cli-state/cli-stdoutimpls out ofhost/src/lib.rsinto a separatecli-host-shimcrate with proper WIT exports (~1 week of WIT design + Rust restructure). - Then the 14-side WAC recipe extension (~3 days).
- Plus the cli's
extension-loader.load-extension-from-bytescalls need to either be no-ops or route through the shim.
- Carve
-
Stub SPI surfaces with no-ops — ~1 day. Lets the test specs "pass" but with hollow assertions (dot-cmds that read state would return empty; stdout writes would be lost). Loses the round-trip coverage the specs are meant to provide. Counterproductive.
Recommendation
Option 1 (per-interface polyfill handlers) is now the cheapest path to closing the test specs. It keeps the host-impl boundary clean (Rust stays in Rust; JS gets browser-flavored proxies). Option 2 is architecturally cleaner but ~2× the effort and requires touching host crate internals.
Status
Stopped per substrate-gap escape hatch after attempt at option 2. Fork (a85fd7c8) found WAC can't compose the cli-family because the import surfaces have no exporting component. PLAN updated with corrected analysis. Captured by task #482.
Pick option 1 to actually close the test specs, or accept that the foundation merge (#481) is enough and the specs stay in their broken-since-#479 state.
Attempt 2 finding (2026-06-25) — option 1 list was incomplete
Branch feat/browser-spi-handlers (5 commits, in /tmp/sqlink-
spi-handlers worktree, unpushed) implemented option 1 end-to-end:
- Extended
EXTENSION_IMPORT_STUB_NAMESwithloader-bridge,cli-stdout,cli-stderr,cli-state,build,bundles. - Added
buildCliHostHandlers({registry, cliState, onStdout, onStderr})inbrowser/src/extension-loader.jswith real impls forloader-bridge(proxies through registry),cli-state(state Map + dotcmd.wit schema defaults),cli-stdout/cli-stderr(route to onStdout/onStderr). - Plumbed optional handlers through
buildExtensionAdditionalImports(handlers)+instantiateExtensionFromBytes(bytes, {handlers}). - Wired ComposedDatabase.open() to build the handlers AFTER
onStdout is defined, then patch
registry.instantiateFromBytesso cli-family bytes-instantiation gets real handlers. - Bonus: discovered the v1 polyfill's
extension-loader.dispatch_dot_commandwas a 404 stub; wired it to walk the registry, find the owning extension'sdot-command.invokeexport, route the call, and translate the invoke-result (sql-value deltas) back to the host dispatch-dot-command-result shape (json-encoded deltas).
After all that, the tests now show real extension invocation and real text output — but BOTH still fail because:
.prefix add foaf …→Error: sqlite: sqlink-browser scenario-3: sqlite:extension/spi.execute not implemented.bundle list→sqlite:extension/bundles.bundleList not implemented
Root cause: composition-cli-sqlite-lib.wac:45 wires
sqlite:extension/spi@0.1.0 INTERNALLY between cli and lib but
does NOT re-export it (line 49 only re-exports wasi:cli/run +
dispatch-bridge). Cli-family extensions loaded via JS-side
runtime-bindgen therefore see the JS stub for spi.execute, not
the composed binary's real impl. dispatch-bridge has only
register-trampoline entries (register-host-scalar/aggregate/etc.)
— no host-execute / spi-passthrough.
So option 1's 4-handler list was incomplete. spi.execute is the actual show-stopper for both round-trip specs, and it requires either:
- Adding a
bridged-executeentry to dispatch-bridge in sqlite-lib (host-side Rust + WIT change + rebuild), OR - Re-exporting
sqlite:extension/spifrom the composed binary (wac recipe change + rebuild composed wasm), OR - A JS-side parallel SQLite (sql.js) — defeats the round-trip purpose since the state wouldn't be visible to the user's SQL.
All three touch host-side substrate the option-1 framing
explicitly excluded. Per substrate-gap escape hatch, stopping
work here. The 5 commits sit on feat/browser-spi-handlers
unmerged — keep around for the cli-state / cli-stdout /
dispatch-dot-command wiring (those land cleanly whenever spi gets
exposed), but the branch shouldn't merge alone since neither test
passes.
Real path forward is option 2's substrate change: add a host-execute capability to dispatch-bridge so the JS host can route extension spi.execute calls back through the composed binary's internal connection. Estimate ~3-5 days (1 day WIT + sqlite-lib impl, 1 day JS routing, 1-2 days tests + polish). Tracked under task #482 (now in_progress, paused).
v1.4: Browser SPI bridging via dispatch-bridge.bridged-execute
Status (2026-06-25): DONE
composed-prefix.spec.js passes end-to-end with real round-trip
output:
.prefix add foaf http://xmlns.com/foaf/0.1/→prefix "foaf" -> "http://xmlns.com/foaf/0.1/" registered.prefix list→NAME EXPANSION LAST_USED DESCRIPTIONtable with the foaf row..prefix expansion foaf→http://xmlns.com/foaf/0.1/..prefix delete foaf→deleted prefix "foaf".- subsequent
.prefix list→(no prefixes registered).
What landed
sqlite-wasm: addedbridged-execute(sql, params) -> result<query- result, sqlite-error>tosqlink:wasm/dispatch-bridge+ the matchingDispatchBridgeGuest::bridged_executeimpl onSqliteLibthat proxies to the sameSpiGuest::executepath against the shared connection.composition-cli-sqlite-lib.wac: also re-exportsqlite:extension/types@0.1.0from sqlite-lib the dispatch-bridge alias-export uses sql-value + query-result, so the types interface has to be reachable through the composed binary's export graph or wac compose validation rejects with "instance not valid to be used as export".browser/src/extension-loader.js:buildCliHostHandlersnow returns ansqlite:extension/spihandler. Execute proxies throughdispatch-bridge.bridged-execute; other spi methods (execute-batch, list-vfs, ...) fall back to a structured "not bridged in v1.4" error so jco's runtime-bindgen probe doesn't trap. Added_setBridge(dispatchBridge)setter the consumer calls afterbindgen.instantiate(...).browser/src/sqlink-composed.js: wires the dispatch-bridge handle intocliHostHandlersright afterspiLoader._setBindgenResult, beforewasi:cli/run.run()starts.cli/src/sqlink_registry.rs:ensure_schemasnow bootstraps the__sqlink_prefix*tables alongside thesqlink_*ones. Native sqlink-host installs the prefix schema viaprefix_registry::install_schema(&conn)at session boot but the browser composed-cli scenario has no native host. Without this,.prefix addsurfacedError: no such table: __sqlink_prefix.
Side effect: composed-bundle.spec.js skipped
.bundle save/list/show/delete touches the host-resident
sqlite:extension/bundles cas-cache registry, not sqlite-lib's
SQLite connection. dispatch-bridge can't reach it; the polyfill
still returns the structured stub. Captured as v1.5 (bundles
registry bridging): either a JS cas-cache shim that satisfies the
import surface or a wasm-side bundles store with its own dispatch-
bridge entry. Bundle spec marked test.skip with a comment
pointing at the v1.5 gap.
Side effect: WAC compose recipe change is load-bearing
Adding bridged-execute (which uses sql-value + query-result from
sqlite:extension/types) made wac compose 0.10 reject the
dispatch-bridge alias-export with "instance not valid to be used
as export (at offset 0x4c5c76)". Resolved by also exporting
sqlite:extension/types@0.1.0 from the composed binary. Mentioned
here because future dispatch-bridge entries that import additional
types may hit the same trap.
v1.5 round 6: composed-cli wasm runtime in a dedicated Worker — DONE
Status (2026-06-26): DONE — non-empty bundle reload-leg passes
composed-bundle.spec.js's reload-leg now passes with a NON-EMPTY
bundle (3 user-loaded scalar extensions: aba/bic/crc → members=3 in
the cas db, surface in .bundle show myset after page navigation).
The OPFS cas db file is 61 KB and starts with SQLite format 3
magic header — a real SQLite db, openable by any external client.
Architecture change
Round 4's β-prime sync-cache held the entire cas db bytes in a
JS-side Uint8Array on the main thread; flushAll ran after each
execDotCommand. This worked for tiny data but had a synchronous
write hazard: a beforeunload flush had to use the async OPFS API
under a synchronous unload event, which Chromium sometimes dropped
silently. The fix attempt in round 5 (Worker + SAB + Atomics) hit a
hard Web-spec blocker: Atomics.wait is forbidden on the browser
main thread (Window context).
Round 6 sidesteps both problems by hosting the entire composed-cli wasm runtime in a dedicated module Worker. From the Worker:
FileSystemSyncAccessHandle.read/write/sync/truncate/...are legal (sync from JS, sync from wasm — perfect for the VFS).- The opfs-host wit-imports call SAH methods directly inline. No SAB, no Atomics, no separate OPFS worker.
- Same architecture @sqlite.org/sqlite-wasm uses for OPFS.
Main thread (sqlink-composed.js) becomes a ComposedDatabase
postMessage proxy. The user-facing API (open/exec/execScalar/
execDotCommand/loadExtension/close) is unchanged — every call
postMessages to the worker via a sequence-id pairing protocol.
What landed
browser/src/composed-cli-worker.js: new file (~480 LoC). Hosts the entire wasm runtime, builds the extension registry + cli-host handlers + spi-loader + opfs-host inside the worker, instantiates the composed cli wasm with JSPI, owns QueueInputStream + globalStdioState + sentinel framing.init/exec/execDotCommand/loadExtension/closehandlers.browser/src/composed-cli-worker.js#createWorkerOpfsHost: pre-opens SAHs for/sqlink/cas.dbAND/sqlink/cas.db-journalat init time (because the wit-importedopen()is SYNC from wasm's POV butcreateSyncAccessHandle()is ASYNC — we cannot open on demand).open(path, false)reports size=0 asnot-foundso SQLite's hot-journal probe interprets an unwritten journal as absent.delete()truncates to 0 (instead of unlinking) so the SAH stays alive across the next transaction.browser/src/sqlink-composed.js: rewritten as a thin postMessage proxy (~240 LoC, down from 760).loadExtensiontakes raw bytes (Uint8Array); the bare-name path fetches bytes from the<name>_extension.component.wasmsymlink inbrowser/public/.browser/src/host-imports.js:opts.opfsHostis now required (constructed by the worker, passed in). No longer constructscreateOpfsHost()internally.browser/src/opfs-host.js: deleted — the β-prime sync-cache impl is superseded by the worker's direct-SAH host.browser/src/opfs-worker.js: deleted — round 5's SAB+Atomics scaffolding is no longer needed (the cli IS the worker now).browser/scripts/link-composed-wasm.mjs: extended to symlink aba/bic/crc*_extension.component.wasmintobrowser/public/so the bundle test can fetch real bytes.browser/tests/composed-bundle.html: phase 1 loads aba+bic+crc viadb.loadExtension(name, bytes)before.bundle save myset --no-build, producing a 3-member non-empty bundle.browser/tests/composed-bundle.spec.js: assertsmembers=3in saveOut andmembers (3)in showOut on the reload leg — decisively proves bundle bytes survived the OPFS round-trip.
Departures from prior rounds
- Round 4's β-prime (sync-cache + flushAll) is retired entirely. Direct-SAH in the worker is simpler and more durable: every SQLite write hits OPFS synchronously through the same SAH the page reload re-opens.
- Round 5's SAB+Atomics α architecture is also retired entirely.
Atomics.waitis illegal on the main thread; round 6 sidesteps by moving wasm into the worker so the main thread doesn't need to block at all. loadExtension(name, transpiledModule)shape is no longer supported (the transpiled module would have to be cloneable across the worker boundary, which it isn't — it carries closures). UseloadExtension(name, bytes)instead; the worker handles runtime-bindgen on its side.
Substrate verification matrix (run before commit)
browser/playwright composed-bundle.spec.js(2 tests): both pass. Reload-leg: saveOut="members=3", showOut="members (3): aba bic crc", opfsHeader="SQLite format 3\0", opfsSize=61440 bytes.browser/playwright composed-prefix.spec.js: passes (no regression on the v1.4 round-trip — full proxy shape works).sqlite-cas-cache cargo test -- --test-threads=1: 91/91.sqlink-host cargo test --lib: 49/49.bundle-cli cargo test: 5/5.sqlite-vfs-tvm cargo test --tests: 17/17.
v1.5 round 4: OPFS-backed VFS for the cas connection — DONE
Status (2026-06-26): DONE — reload persistence verified end-to-end
composed-bundle.spec.js's reload-leg now passes. Phase 1 runs
.bundle save myset, the host writes the cas db to
/sqlink/cas.db in navigator.storage's origin private root, the
page navigates to phase 2 (fresh wasm runtime, brand-new
shared_cas_conn), and .bundle list / .bundle show surface
the bundle from phase 1.
Substrate verification (the spec's "v1.6 substrate verification"
item): the OPFS file's first 16 bytes are "SQLite format 3\\0"
— it IS a SQLite db, openable by any external SQLite client
(sqlite3 cli, @sqlite.org/sqlite-wasm). Not a snapshot blob.
What landed
sqlite-wasm/wit/opfs-host.wit: new interface declaringopen / read / write / truncate / sync / size / close / deleteon u64 file handles with primitive payloads. Imported by thesqlite-libraryworld.sqlite-wasm/sqlite-vfs-tvm/src/opfs.rs: new"opfs"VFS table that trampolines each IO method through a dynOpfsBackendtrait. Keeps sqlite-vfs-tvm free of wit-bindgen; sqlite-lib supplies the WIT-backed impl. Tests prove real SQLite drives the VFS for CREATE/INSERT/SELECT round-trips against a MemBackend fixture.sqlite-wasm/sqlite-lib/src/lib.rs: wasm32shared_cas_connopens throughvfs="opfs"at path/sqlink/cas.db.tvm_cold_tier_initregisters theWitOpfsBackend(wit-bindgen shim inopfs_backend.rs) and installs the VFS alongside the existingtvm-mem.browser/src/opfs-host.js: JS impl ofsqlink:wasm/opfs-host. Sync from the wasm guest's POV — bytes live in a JS-side Uint8Array cache; OPFS round-trips happen atpreload(before instantiate) andflushAll(after eachexecDotCommand). Architecture note: option β (suspend via JSPI) couldn't tunnel through thecli → dispatch-dot-command JS → bundle-cli wasm → bundles polyfill JS → bridgedExecuteCas → opfs VFScall chain (the JS frames break JSPI's suspension surface), and option α (Worker + SAB + Atomics) would have required >500 LoC of cross-thread plumbing plus COOP/COEP setup. The sync-cache architecture matches the call shape AND the bundle registry's data volume (~ few KiB per save) without complexity.browser/tests/composed-bundle.html: phase=1 saves a bundle; phase=2 lists + shows + reads the OPFS file's first 16 bytes for SQLite-magic verification.browser/tests/composed-bundle.spec.js: reload-leg un-skipped (wastest.skipin round 3). Asserts on phase 2's list/show outputs AND on the"SQLite format 3"magic header.
Departures from the round-3 spec
- Used JSPI-aware sync-cache (option β-prime) instead of
Worker+SAB+Atomics (option α). The round-3 spec mandated α;
this round chose β-prime after the polyfill's call shape
ruled out plain β AND α's complexity proved disproportionate
to the data volume. Trade-off documented in
opfs-host.js's header comment. - Persistence cadence:
flushAll()runs after everyexecDotCommand(so even a single.bundle savesurvives an unclean unload), not just at db.close(). Slightly more OPFS writes than minimal, but bounded by the dot-cmd rate (≤ tens per second under normal usage).
Substrate verification matrix (run before commit)
sqlite-vfs-tvm/tests/opfs_serves_real_sqlite.rs: real SQLite drives the opfs VFS via the OpfsBackend trait. Passes.sqlite-wasm/sqlite-vfs-tvm cargo test: all 11 tests pass.cargo test -p sqlite-cas-cache -p sqlink-host: native tests green (cas-cache opens against the host filesystem, not the opfs VFS — the wasm32-gated wiring doesn't affect them).browser/playwright composed-bundle.spec.js(2 tests): both pass.browser/playwright composed-prefix.spec.js: passes (no regression on the v1.4 round-trip).
v1.5 round 3: Browser bundles bridging via bridged-execute-cas
Status (2026-06-26): DONE — reload persistence delivered in round 4
composed-bundle.spec.js's main round-trip leg passes end-to-end
with real cas-cache output:
.bundle save myset --no-build->bundle 'myset' saved (id=1, set_hash=af1349b9..., members=0).bundle list-> themysetrow, members + binaries counts..bundle show myset-> set_hash + created_at + last_used..bundle delete myset-> deletion message.- subsequent
.bundle list->(no bundles).
What landed
sqlite-wasm: addedbridged-execute-cas(sql, params) -> result< query-result, sqlite-error>tosqlink:wasm/dispatch-bridge+ the matchingDispatchBridgeGuest::bridged_execute_casimpl onSqliteLib. Routes to a NEW thread-localSHARED_CAS_CONNthat's distinct from the user-dataSHARED_CONN. On native it opens~/.cache/sqlink/cas.dbwith the default VFS; on wasm32 it transitionally opens:memory:(see deferral below).browser/src/extension-loader.js:buildCliHostHandlersnow returns ansqlite:extension/bundleshandler that drivesbundleSave / bundleFindByName / bundleFindByHashPrefix / bundleList / bundleShow / bundleDelete / bundleRecordBinary / bundleTouch / bundleAddAlias / bundleRemoveAlias / bundleAliasesinline againstdispatch-bridge.bridged-execute-cas. SQL transcribed fromsqlite-cas-cache::bundles_exec'spub constconstants.bundleGcthrows a structured "not implemented" error (not on the test path).browser/src/extension-loader.js: schema bootstrap (BOOTSTRAP + INSTALL) runs lazily on first call. Split per-statement because sqlite-lib'sbridged-execute*uses sqlite3_prepare semantics (single statement per call), so the Rust crate's monolithic BEGIN/COMMIT INSTALL_SCHEMA cant be a single blob in JS.- Tests:
composed-bundle.spec.jsun-skipped for the main round- trip leg with tightened substrate greps that fail decisively on "not bridged / not implemented / no such table / no such vfs". Reload-leg added but staystest.skip(see deferral).
OPFS-backed VFS for the cas connection — delivered in round 4
See the "v1.5 round 4: OPFS-backed VFS for the cas connection —
DONE" section above. The round-3 deferral is closed; the cas
connection now opens through the "opfs" VFS on wasm32 and
composed-bundle.spec.js's reload-leg passes end-to-end.
The round-3 spec called for option α (Worker + SyncAccessHandle
- SharedArrayBuffer + Atomics); round 4 chose option β-prime
(sync-cache + explicit flushAll at execDotCommand boundaries)
because the polyfill's call shape ruled out the simpler β and
α's complexity was disproportionate to the bundle registry's
data volume. Architecture rationale lives in the
browser/src/opfs-host.jsheader comment.
Plan: v1 follow-ups — roadmap for outstanding post-v1 work
Status (2026-06-25)
Roadmap for outstanding work after the v1 shipping push (bundles + prefixes + cas-cache + cli + tests + CI + fuzz/mutation infra all landed and pushed to origin/main). Single document covering many small-to-medium follow-ups because each is too small for its own PLAN doc but the total picture (effort + sequencing + dependencies) matters for pacing.
Motivation
Multiple individual PLAN docs (PLAN-bundles, PLAN-prefixes, PLAN-cas-cache, PLAN-wal-archive) each captured a single feature end-to-end. Several smaller items surfaced during their implementation got noted as "v1.1" or "deferred" in the source docs. This roadmap consolidates them so:
- The total post-v1 effort is visible in one place.
- Priorities are explicit (not just "v1.1" without ranking).
- Dependencies between items are surfaced.
- Fan-out structure is clear when several can run in parallel.
This is NOT a feature plan; each item below references its originating PLAN-*.md or commit for the full context. Items here get a one-paragraph summary + effort + dependencies + scope cut.
Priority + sequencing
P0 (blocking) P1 (high value) P2 (medium) P3 (nice-to-have)
─────────────────────────────────────────────────────────────────────────────────────────
CI green-up ───► Prefix v1.1 sweep ───► Single-slot hook More fuzz targets
Live .prefix prefer audit recording Longer fuzz runs
Cosmetic warnings install_loaded_ext
refactor
Bundles browser
dispatch driver
Bundle multi-aliasing
.load auto-cache
HTTPS resolver mock
Hostile-fs fixtures
Migration-arm tests
cargo-mutants re-run
P0 blocks everything because a red CI hides real regressions in subsequent work. P1 items have high-value + low risk + no dependencies on each other and can fan out in parallel. P2 items mostly group around shared infrastructure (test harnesses) or shared file regions; some can parallelize.
P0: CI green-up
Background
The recent push triggers (8f82db5 prefixes hot-path merge, prior
241008b compression-multiplexer + cca5735 cargo fmt fixes) all
went red on real GitHub Actions despite the local act-based
verification. Three distinct things broke at different runs:
cargo fmt --checksyntax (fixed in cca5735, then regressed somehow — need to check).CARGO_BUILD_TARGET: ""corruptingcargo install(fixed in cca5735).- Workspace manifest load failing on missing compression-multiplexer (fixed in 241008b by publishing + submoduling).
Post-fix push (8f82db5) still shows failure in 17s on real CI.
Likely a fourth issue — possibly the prefix substrate's 217-file
Manifest{} sweep introduced fmt drift OR a workspace member dep
that wasn't covered by my earlier excludes.
Scope
- Pull
gh run view <ID> --log-failedon the failing run to see the actual error. - Fix whatever surfaces. Likely candidates:
cargo fmt --checkfinding drift in one of the 217 patched Cargo.toml files or the Manifest{} sweep.- Workspace member missing from the
--excludelist now that prefix-cli exists (mirror bundle-cli's treatment). - wasi-sdk download URL drift.
- Re-verify under
actbefore pushing.
Effort
0.5 day. First-CI-run-after-merge regressions are usually small mechanical fixes.
Dependencies
None.
Out of scope
- Adding new CI jobs (separate effort).
- act-specific edge cases (already documented in
scripts/ci-local.sh).
Status
Open. Run number 28187557158 is the latest red CI; needs
investigation.
P1: Prefixes v1.1 migration sweep
Background
PLAN-prefixes.md ships v1 with a deprecation-window fallback for
extensions that don't declare preferred-prefix +
prefix-expansion in their manifest. All 217 in-tree extensions
currently run on the synthetic-expansion fallback
(sqlink-internal://<crate>). v1.1 makes the manifest fields a
hard error.
Before v1.1 cuts over, every in-tree extension needs real
(prefix, expansion) pairs assigned. The substrate fork already
batch-patched the Manifest{} literals with None, None defaults
(wit-bindgen forces all fields) — this sweep replaces the Nones
with sensible real values.
Scope
- Categorize the 217 extensions by family (cli-family / sqlite-utils-* / dotcmd-* / vec-* / single-purpose scalars).
- Per-family prefix conventions (sketch):
- cli-family (
bundle-cli,prefix-cli,serialize-cli, etc.): prefix =sqlink-<short>, expansion =com.tegmentum.sqlink.cli.<short>. - sqlite-utils-*: prefix =
sqlite-<short>, expansion =org.sqlite.utils.<short>. - Generic single-purpose (
uuid,json1,csv, ...): prefix =<name>, expansion =org.<community>.<name>orcom.tegmentum.sqlink.ext.<name>for in-tree experimental. - vec-*: prefix =
vec, expansion =org.faiss.vec(or similar; check upstream identifiers).
- cli-family (
- Update each extension's Cargo.toml
[package.metadata.extension]block (or the equivalent — verify the actual location of these fields). - Re-run encode-extension-components.sh (the wit-skew guard auto- rebuilds).
- Re-run extension-smoke + prefix integration tests to confirm no regression.
Effort
1.5 days. Most of it is mechanical sed-style edits; a couple hours for category decisions; rest is verification.
Dependencies
None (the substrate already accepts both real and synthetic forms).
Out of scope
- Globally-unique-expansion registry (v2; see PLAN-prefixes.md "Out of scope").
- Hard-error on missing fields — that's the v1.1 release cut, not this sweep. Separate commit/PR.
Status
Not started.
P1: Live .prefix prefer (bare-name re-registration on pin write)
Background
PLAN-prefixes.md's .prefix prefer NAME EXTENSION writes a row to
__sqlink_prefix_pin but the bare-name re-registration only fires
on next session. v1 simplification documented in the dot-cmd help
text. Operators who want the pin to take effect today have to
restart the cli.
Scope
- Inside
.prefix prefer's implementation, after writing the__sqlink_prefix_pinrow:- Find every other extension that has registered
(function_name, n_args)at the same arity. - For each, look up the FUNCTION pointer / wasm dispatch ID
via the substrate's
Hostregistration cache. - Re-register the bare name with SQLite pointing at the pinned
extension's implementation (use the same
register_host_loaded_*path the register-X impls use).
- Find every other extension that has registered
prefix unprefer NAMEsimilarly re-registers per SQLite default (last-loaded wins) — easiest via re-applying the load order to the bare name.- Add an integration test (
prefix_prefer_live_swap) that verifies the bare-name dispatch changes within a single session after.prefix prefer.
Effort
0.5 day. Mostly plumbing through existing structures.
Dependencies
None.
Out of scope
- Cross-database pin synchronization (v2).
- Pin lock-in (preventing rename once functions registered; v2).
Status
Not started.
P1: Cosmetic warnings cleanup
Background
Two pre-existing warnings every fork mentions:
non_snake_caseinsqlink-hostbin (probably the binary's entry-point fn or a generated identifier).- Unused import at
cli/src/lib.rs:771(bindings::sqlite::extension::types::SqlValue).
Cosmetic only; not load-bearing. Worth cleaning so future forks don't keep flagging them.
Scope
- Fix the
non_snake_casesite (probably#[allow(non_snake_case)]with rationale or rename if the binding name doesn't matter). - Remove the unused import OR add
#[cfg(...)]if it's conditionally used. - Run
cargo clippy --workspaceto catch any other low-hanging warnings while in the file.
Effort
15 minutes — two 1-line fixes.
Dependencies
None.
Out of scope
- Workspace-wide clippy cleanup (separate effort).
- Auto-format pass.
Status
Not started.
P2: Single-slot hook audit-only recording
Background
PLAN-prefixes.md's substrate intentionally skipped single-slot
hooks (authorizer / update / commit / wal) — they have no name to
qualify and at most one per connection, so the prefix model
doesn't apply. v1.1 can add audit-only recording into
__sqlink_prefix_function for diagnostic visibility.
Scope
- In each of the four
register_X_hookhost impls (authorizer, update, commit, wal), record a synthetic row in__sqlink_prefix_function:function_name=__hook_<shape>(e.g.__hook_authorizer)n_args= sentinel value like-1extension_name= the registering extension
.prefix conflictsalready SELECTs byfunction_name— these synthetic hook rows would surface in the conflicts view if multiple extensions register the same hook (last-wins by SQLite default, but the operator can now see who's claimed the slot).- Add an integration test exercising this for at least one hook shape.
Effort
0.5 day.
Dependencies
None.
Out of scope
- Actually qualifying single-slot hooks (they have no name; not meaningful).
- Operator-resolvable hook collisions (would require deeper changes to SQLite's hook API; SQLite enforces last-wins natively).
Status
Not started.
P2: install_loaded_extension refactor (DRY)
Background
Recovery fork (commit d6256f5) extracted
Host::ensure_prefix_for_extension + Host::record_function_for_extension
helpers and wired them into the 4 register_X impls. But
install_loaded_extension (host/src/lib.rs:7197) still has its
own inline prefix-recording logic — left as-is because it has
"extra collision-warn + pin-lookup logic that doesn't shape
cleanly into the new helpers" (per the recovery fork's report).
Now that the helpers are stable + the integration tests are
passing, refactor install_loaded_extension to call them too.
Eliminates ~50 lines of duplicate logic.
Scope
- Read
install_loaded_extensioncarefully to identify the collision-warn + pin-lookup logic that didn't fit the helpers. - Either:
- Extend
record_function_for_extensionto return enough info thatinstall_loaded_extensioncan do its collision-warn inline, OR - Add a third helper
Host::warn_on_function_collision(...)that both code paths can use.
- Extend
- Replace install_loaded_extension's inline logic with helper calls.
- Verify:
cargo test -p sqlink-host --lib prefix_registry::testscargo test -p extension-smoke --test extension_smoke_prefixesboth still pass.
Effort
0.5 day.
Dependencies
Depends on the existing helpers landing (already on main).
Out of scope
- Renaming install_loaded_extension (it's pub; sqlink-native imports it).
Status
Not started.
P2: Bundles browser composed-cli dispatch_dot_command driver
Background
PLAN-bundles.md v1 noted: the browser composed-cli has no
dispatch_dot_command driver — extension-loader.js:12 returns
404 when bundle-cli's .bundle save etc. dispatch through it.
v1.1 polish fork added the bundle-cli to browser PICK list but
the actual end-to-end browser test (composed-bundle.spec.js)
uses test.skip() with a documented reason.
Same gating applies to .prefix in the browser if/when prefixes
land cleanly there.
Scope
- Build a dispatch driver in the composed cli's JS layer that
routes
.xxx CMD ARGScalls to the loaded extension'sdispatch_dot_commandexport. - Verify browser composed-cli round-trips a
.bundle save myset --no-buildthrough the dispatcher. - Update
composed-bundle.spec.jsto remove thetest.skip()and exercise the full round-trip. - Mirror in
composed-prefix.spec.js.
Effort
1-2 days. Browser-side wasi-cli plumbing has its own learning curve.
Dependencies
None (the wasm side is already in place).
Out of scope
- Streaming output for long-running dot-cmds (v2).
- Browser-side
.bundle build(intentionally errors out per the existing stub — wasm components can't spawn processes).
Status
DONE. The original framing ("add a dispatch_dot_command driver in
extension-loader.js") was a red herring. Investigation surfaced
that bundle-cli + prefix-cli are EMBEDDED in the composed cli
component via include_bytes! in cli/src/lib.rs and dispatch
INTERNAL to the wasm cli — the JS host-import path was never the
right surface. The actual gap was a public method on
ComposedDatabase (browser/src/sqlink-composed.js) to drive a
single dot-command through the existing sentinel-bounded stdin
pipe.
Resolved by ComposedDatabase.execDotCommand(line) (commit
aca8d484), the sibling of exec() that writes a raw dot-cmd line
- sentinel SELECT through the persistent stdin queue and returns
the cli's stdout window.
composed-bundle.spec.jsandcomposed-prefix.spec.jsnow exercise.bundle save / list / show / deleteand.prefix add / list / expansion / deleteend-to-end against the composed cli with no skip — assertions substring-match the cli's actual stdout.
P2: Bundle multi-name aliasing
Background
PLAN-bundles.md v1 supports a single name per set_hash via the
__cas_bundle.name UNIQUE constraint. True multi-name aliasing
needs a separate __cas_bundle_alias table so two names can
point at the same set_hash.
Scope
- New
__cas_bundle_aliastable:CREATE TABLE __cas_bundle_alias (name TEXT PRIMARY KEY,bundle_id INTEGER NOT NULL REFERENCES __cas_bundle(id) ON DELETE CASCADE,created_at INTEGER NOT NULL); - Migrate
__cas_bundle.namefrom PRIMARY KEY to nullable display-name; aliases move to the new table. - Update
bundle_save+bundle_find_by_nameto use the alias table. - Update
.bundle listto show all aliases per row. - Test: two distinct names pointing at the same set_hash.
Effort
1 day (schema migration + bundle-cli + cas-cache API + tests).
Dependencies
None.
Out of scope
- Cross-database alias sync (v2).
Status
Documented v1.1 in PLAN-bundles.md.
P2: .load auto-cache into cas-cache by content-hash
Background
Currently .load /path/to/foo.component.wasm doesn't push the
bytes into cas-cache by content-hash; it just opens the file
directly. After .load, --bundle-load referencing that
extension hits the cas-cache-miss error path because the
extension's bytes are only on the operator's filesystem, not in
the registry.
Scope
- After a successful
.load PATH, compute content-hash + insert into cas-cache via the existingcas_cache.put_bytesAPI. - Update
--bundle-loadresolution to check both cas-cache and the original PATH (if recorded somewhere). - Test: load → save bundle → restart cli →
--bundle-loadworks without manual cas-cache priming.
Effort
0.5 day.
Dependencies
None.
Out of scope
- Auto-pruning loaded-but-not-bundled extensions from cas-cache.
Status
Documented v1.1 in PLAN-bundles.md.
P2: HTTPS resolver mocking infrastructure
Background
cargo-mutants triage flagged 3 surviving mutants in
sqlite-cas-cache/src/resolver.rs:179, 187 (HttpsResolver::resolve,
delete !) because the tests don't have an HTTPS mock — they
either skip the network path entirely or run against the real
internet (flaky in CI).
Scope
- Add
mockito(orwiremock-rs) as a dev-dependency. - Spin up a mock HTTPS server in the test fixtures that returns controllable responses.
- Replace the network-path tests with mocked variants.
- Mutation re-run on resolver.rs to confirm the 3 mutants now die.
Effort
0.5 day.
Dependencies
None.
Out of scope
- Replacing the production HTTPS client (still uses reqwest).
Status
Not started.
P2: Hostile-filesystem test fixtures
Background
cargo-mutants flagged store::open_external (delete ! on a
file-existence check) as surviving because no test exercises the
error path with EACCES / read-only parent dir. Possible with a
tempfile::TempDir + std::os::unix::fs::PermissionsExt, but
flaky in CI runners with permission-stripping (containers
sometimes ignore chmod).
Scope
- Add
permissions-ci-skiphelper that conditionally skips when the runner can't enforce filesystem permissions. - Test: create a read-only parent dir, attempt
open_external, assert it fails gracefully. - Mutation re-run to confirm mutant dies.
Effort
0.5 day.
Dependencies
None.
Out of scope
- Adversarial-filesystem testing beyond permissions (NFS races etc.).
Status
Not started.
P2: Schema-migration test fixtures
Background
cargo-mutants flagged store::install_schema:274, 279 (the
v1→v2 and v2→v3 migration arms) as surviving — no test writes a
legacy schema-version row + opens to trigger the migration.
Scope
- Add a
legacy_v1_db_fixture()helper that creates a fresh cas-cache db with__cas_meta.schema_version = '1'+ the v1 schema only. - Test: open with current store, assert migration runs + schema_version updates + new tables exist.
- Same for v2.
- Mutation re-run to confirm both arms now die.
Effort
0.5 day.
Dependencies
None.
Out of scope
- Backward-migration (downgrade) tests — not supported.
Status
Not started.
P2: cargo-mutants re-run
Background
The mutation-testing round (commits 9ca5128, 7267a13, a0c95dd) added 6 new tests closing 8-10 mutants in sqlite-cas-cache. Estimated post-fix kill rate: 92-94% — but never re-measured. Worth running again after the architectural-mutant work above to confirm + get a fresh number.
Scope
cargo mutants -p sqlite-cas-cache --output mutants-out-cas-cache --timeout-multiplier 3 --in-place— ~22 min runtime.cargo mutants -p sqlink-extension— ~6 min runtime.- Report the actual caught/missed counts.
- Close any new mutants that surfaced from the architectural work.
Effort
0.5 day total (most is wall-clock for the runs).
Dependencies
The HTTPS-mocking + hostile-fs + migration-arm work above (so the re-run sees the improvement).
Out of scope
- Adding mutation testing to additional crates (sqlink-host baseline scan is ~80-150 mutants per the prior research; the initial scope was intentionally narrow).
Status
Not started.
P3: More fuzz targets
Background
The fuzz infra round added 5 targets:
policy_check_manifestcas_put_bytes_roundtripbundle_save_set_hashparse_durationparse_load_args
Three more were considered but not implemented:
spawn_buildpath validation (post-P0 security fixes — verify the validators reject the path-escape attacks they should).bundle-cliargv parser (untrusted operator input).sqlink-cli-argvparser (untrusted argv, already extracted as a native crate).
Scope
- One fuzz_targets/*.rs per target.
- Seed corpus from prior P0 attack patterns + existing argv fixtures.
- 5-min smoke runs in fuzz-smoke CI per target.
Effort
1 day (3 small targets).
Dependencies
None.
Out of scope
- Differential fuzzing across parsers (v2).
Status
Not started.
P3: Longer fuzz runs
Background
The extended-fuzz round (test/fuzz-cas-extended branch, merged)
ran cas_put_bytes_roundtrip + bundle_save_set_hash for 20 min
each with no crashes. Coverage plateaued at ~417/549 feature
edges. Sharply diminishing returns at this point — going to
hours per target would explore deeper paths but with no
guarantee of finding anything.
Scope
- One-shot extended run (4-6 hours per target, weekend wall-clock).
- Report coverage delta vs the 20-min runs.
- Triage any findings.
Effort
0.5 day human + ~12 hours wall-clock per target.
Dependencies
None.
Out of scope
- Continuous fuzzing infra (OSS-Fuzz integration — separate plan).
Status
Not started.
Fan-out strategy
Group items into rounds based on dependency + region:
Round 1 (sequential or single fork)
- P0 CI green-up — must precede anything else; pushes can't be trusted while CI is red.
Round 2 (parallel forks, no overlap)
- Fork A: P1 cosmetic warnings (15 min) + P1 live
.prefix prefer(0.5 day) — both small + low-risk. - Fork B: P1 prefix v1.1 migration sweep (1.5 days) — 217-extension Cargo.toml batch edit.
- Fork C: P2 install_loaded_extension refactor (0.5 day) — DRY-up.
- Fork D: P2 single-slot hook audit recording (0.5 day) — audit-only path.
Round 2 finishes ~1-2 wall-clock days assuming parallelism.
Round 3 (parallel forks)
- Fork E: P2 bundle multi-name aliasing (1 day) + .load auto-cache (0.5 day) — both touch cas-cache + bundle-cli.
- Fork F: P2 bundle browser dispatch driver (1-2 days) — composed-cli JS layer.
- Fork G: P2 HTTPS resolver mocking + hostile-fs + migration-arm test fixtures (1.5 days total) — test-infra trio.
Round 4 (sequential)
- P2 cargo-mutants re-run (depends on Round 3's Fork G).
- P3 more fuzz targets.
- P3 longer fuzz runs.
Estimated total effort
| Round | Items | Sequential effort | Parallel wall-clock |
|---|---|---|---|
| Round 1 | P0 CI | 0.5 day | 0.5 day |
| Round 2 | 4 P1 items | 3 days | 1-2 days |
| Round 3 | 4 P2 items | 4-5 days | 1-2 days |
| Round 4 | 3 mixed items | 1.5 days + wall-clock | 1 day + wall-clock |
| Total | ~12 items | ~9-10 days sequential | ~3-5 days parallel |
Parallel-fork pacing assumes the established commit-per-step discipline + worktree-per-fork pattern.
Out of scope (genuinely deferred)
These items appear in various PLAN-*.md "Out of scope" sections and are listed here only for visibility — no work planned in this roadmap:
- Cross-target builds (bundles): wasi-sdk / zig-cc / cross. Per-user setup; not per-bundle.
- Bundle registry/publishing: tegmentum-org bundle registry for sharing across machines.
- Bundle
--with-schema/--with-data: overlaps with wal-archive territory. - sqlink-host as rlib: production-install path so generated crates depend on rlib not workspace source.
- Per-query prefix overrides (prefixes):
SELECT prefix foaf=other; foaf__name(...)SQL syntax. - Prefix-scoped permissions: gate per-prefix at capability layer ("only operator can use system__*").
- Prefix registry hosting: org-wide registry verifying globally-unique expansions.
- Cross-database prefix sync.
- Bulk prefix import/export.
- OSS-Fuzz integration: continuous fuzzing infra (separate CI investment).
- Workspace-wide clippy cleanup: cosmetic but large.
References
PLAN-bundles.md,PLAN-prefixes.md,PLAN-cas-cache.md,PLAN-wal-archive.md— source plans for items derived from v1 deferrals.- Recent commits referenced inline (e.g.
d6256f5for the hot-path helpers,9ca5128/7267a13/a0c95ddfor the mutation test additions).