v0.3.0-prerelease — Phase 2: Browser Runtime (live.js)
Pre-releaseVanilla-JS IIFE bundle (dist/live.js) for in-page picking, configure, generation,
and variant cycling. 31.30 kB minified — well under the 50 kB live.js budget.
Built by a 4-agent team (architect / coder / security / tester) with disjoint
file ownership. No React, no DOM-deps, no Node imports — Browser-target ES2020.
What landed
Contracts (src/contracts/browser.ts, 467 lines)
BrowserState5-kind discriminated union + 18-entrySTATE_TRANSITIONStablePickResult,Variant,ParameterBinding,Annotation,BridgeClientinterfaceSanitizeModuleinterface — single trust-boundary for user/agent stringsWispDesignGlobal/InitOptions/WispDesignHandlefor the IIFE boot API- Constants split into
src/browser/constants.ts(pure, no zod) to keep the
IIFE bundle free of zod side effects
Browser modules (src/browser/, ~2500 lines across 10 files)
state-machine.ts— pure reducer, invalid event returns input unchangedpicker.ts—document.elementFromPoint+pickable()(>=20x20,
excludes WISP UI / html / body / script / style / meta /visibility:hidden)multi-select.ts— Cmd/Ctrl-click-add toggle (Improvement #1 vs Impeccables single-select)floating-bar.ts— 3-mode bar (configure / generating / cycling)parameter-sliders.ts— zero-roundtrip CSS-var binding (Killer-Feature)annotations.ts— structured{targetId, kind, note}— NOT PNG flatten (Improvement #7)variant-render.ts—@scope-based 3-variant cycling; variant 0 = live
original (preserves listeners), variants 1+ are clonesindex.ts— IIFE entry,window.WispDesign.init({ token, bridgeUrl })bridge-client.ts— fetch POST + EventSource subscribe (Long-Poll fallback)
Security (src/browser/sanitize.ts, 302 lines)
escapeHtml,sanitizeFreeText(strips<script>/<style>/<iframe>/
<object>/<embed>/<svg>blocks + on*= handlers + js:/data:/vbscript:
prefixes + C0+DEL controls; word-boundary truncate at 75% threshold)trustedCssVar(rejectsurl(/@import/expression(/{/}/;/backslash)trustedSelector(deep:has()check rejects nested iframe/script)- Returns structured
SanitizeError{ code, message, detail? }
Tests (tests/browser/, 94 new tests across 7 files)
sanitize.test.ts(28) — XSS payloads, CSS-var + selector rejectionstate-machine.test.ts(19) — transitions, self-loops, invalid-event silencepicker.test.ts(15) —pickable()filter, selector chains, attribute subsetmulti-select.test.ts(6) — modifier toggle,[data-wisp-ui]exclusionvariant-render.test.ts(9) — wrapper, 3 siblings,@scoperule injectionbundle-size.test.ts(5) —dist/live.jsexists, < 50000 bytes, IIFE bannerconstants-drift.test.ts(12) — byte-equivalence guard between
src/browser/constants.tsandsrc/contracts/browser.ts(CI catches drift)
Cumulative: 171/171 tests green (94 browser + 77 bridge + 2 smoke).
Tooling
- tsup multi-entry: CLI ESM (
dist/index.js, 9.74 kB) + Browser IIFE
(dist/live.js, 31.30 kB,globalName: WispDesign, target es2020, minified,
noExternal: [/.*/],outExtensionstrips the.globalinfix) - vitest
environmentMatchGlobs:tests/browser/**runs jsdom, rest pure Node - jsdom +
@types/jsdomas devDeps
Notable engineering decision
Bundle was 91.17 kB initially — sanitize.ts value-imported
FREE_TEXT_MAX_LEN from contracts/browser.js, which triggered top-level
z.object(...) evaluation and inlined the full zod runtime (~60 kB overhead).
Fix: rewire sanitize.ts to import from src/browser/constants.ts (pure
constants, no zod). The other 9 browser modules already did this — only
sanitize.ts was the offender. constants-drift.test.ts now guards against
future drift between constants.ts and contracts/browser.ts.
Result: 31.30 kB. 60 kB reduction in a single import rewire.
Pinned behaviors
escapeHtmlis NOT idempotent (&→&amp;)textPreviewlength is 81 when truncated (…ellipsis appended after 80 char slice)sanitizeFreeTextstrips the leading whitespace inside<img on…=…>along with
the handler — result lacks a trailing space inside the tag- jsdom does not implement
@scopecascade — variant-render structural tests
assert the@scoperule is in the<style>textContent, not that styles apply
Quality gates
$ npm run check
typecheck: clean
test: 171 passed in 13 files (2.45s)
build: index.js 9.74 kB + live.js 31.30 kB
$ node dist/index.js doctor
7/7 OK
Next
Phase 3 — Source-Edit Engine (src/source/). Marker-bounded fs.writeFileSync
splice, byte-equivalent inject/remove roundtrip, carbonize CSS @scope →
permanent on accept, per-session .wisp/sessions/<id>.jsonl undo log
(Improvement #2 vs Impeccables no-undo), refuse-edit safety on generated files.