Guidance for automated agents (and humans) working in this repository.
polymorph:websocket: a WIT interface for WebSocket client connections plus
multiple implementations that run the same guest component against real
WebSocket stacks. It is a sibling of
polymorph:webcrypto and
polymorph:webrtc-datachannels
and deliberately mirrors their architecture — prefer clarity and
correctness over features, and keep the implementations behaviourally in
sync (the cross-implementation conformance suite is the gate). See
README.md for the design.
The repository follows the siblings' conventions: the root justfile is
the single entry point (just check for the fast gate, just ci for the
exact CI mirror) with component-scoped module justfiles; scripts/setup.sh
is the idempotent dependency setup CI reuses verbatim; conformance is
driven by a shared guest suite on the polymorph:test harness with
a per-target driver and targets.toml declaring target facts.
Layout (each directory's justfile module in parentheses):
wit/— the one copy of thepolymorph:websocketpackage;wit/README.mdis the package contract document item docs reference by section name.rust/wasmtime/— thewasmtime-websockethost crate. Its knobs (the connect/close bounds, the inbound-buffer bound) live onWasiWebsocketCtx; the crate reads no ambient environment.js/polyengine/(just polyengine-module-check) —websocket.ts, THE browser-first JS host module, over polyengine's (formerlydeltic) embedder conventions (typed streams,ComponentException) for its runtime-linked, no-transpile legs (conformance/driver-ct/polyengine/, the WPT parity round trip, the demo). Its knobs are exported functions (configure,setMaxInboundBufferBytes,setConnectTimeoutMs,setCloseTimeoutMs); the module reads no ambient configuration. Since the A22 protocol/runtime split, this published package (@polymorph/websocket) depends on@polyengine/protocolonly (js/polyengine/deno.json, caret-pinned^0.3.1) — no@polyengine/runtimeimport anywhere in it; value recognition is brand-based (@polyengine/protocolpredicates, notinstanceof), so its copy of@polyengine/protocolis harmless even in a multi-copy graph. The runtime-linked application legs (conformance/driver-ct/polyengine/, the WPT parity round trip, the demo) separately exact-pin the@polyengine/{runtime,translator,wasi, ct-runner}lockstep family (0.6.3) plus@polyengine/protocol(0.3.1) inconformance/driver-ct/polyengine/deno.json; the root justfile'sexam-polyenginerecipe (CI-wired) asserts one resolved runtime-family version, one resolved protocol version, and thatjs/polyenginenames no@polyengine/runtimespecifier. Bump procedure:conformance/driver-ct/polyengine/README.md.js/componentize/(just wpt::…) —websocket.js, the WHATWG-API shim for componentize-js guests (deviations registry in its header), and the WPT parity gate (wpt/README.mdis the vendoring policy; losses ratchet inwpt/parity/losses.js).conformance/(just conformance-ct) — the conformance suite on thepolymorph:testharness:guest-ct/(the suite component; the committedtests.lockis the corpus inventory),driver-ct/(wasmtime + composed + polyengine-deno + polyengine-browser legs,targets.tomlwith the expected-fail mechanism, the committedmatrix.md), andserver/(echod;server/PROTOCOL.mdis its wire contract,server/echod.mjsthe shared Node spawn helpers). The suite crates consume the harness as rev-pinned git dependencies (the two[workspace.dependencies]entries in the rootCargo.toml), the polyengine-browser driver consumes its JS runner core from JSR (@jsr/polymorph__testindriver-ct/polyengine/package.json), and thecomponent-testCLI is cargo-installed at the rev Cargo.lock records (conformance-ct::_ct-tools). Both sides of a bump name one polymorph-test release; the rootCargo.tomlcomment is the bump checklist. There is no transpiler dependency anywhere: the JS host legs are runtime-linked by the pinned polyengine JSR prereleases (single pin site:driver-ct/polyengine/{deno.json, deno.lock}).examples/(just demo::…) — the echo-demo guest and its host runners.
Checks to run before committing, by what changed: WIT or wit/README.md →
just validate-wit then just conformance-ct (a surface change is
co-dependent across every implementation); either implementation →
just conformance-ct; Rust → just check; conformance machinery →
just conformance-ct; justfiles/CI → just ci.
Changing any interface or resource identifier means updating everyone who names it as a string; nothing catches these at build time except the places listed failing at run time. The sites:
- the WIT worlds: root
wit/,rust/wasmtime/wit/world.wit,rust/guest-provider/wit/world.wit,conformance/wit/world.wit,examples/echo-demo/wit/world.wit; bindgen!configs (interface paths appear in per-function import overrides andwith:maps):rust/wasmtime/src/bindings.rs,examples/wasmtime-demo/src/lib.rs;wit_bindgen::generate!in the suite (conformance/guest-ct/src/lib.rs) and its import paths inguest-ct/src/body.rs;- the polyengine imports records, keyed by verbatim versioned WIT id and
failing at run time rather than build time:
js/polyengine/websocket.ts(CONNECTIONS_INTERFACE,websocketImports), the parity carrier (js/componentize/wpt/parity/polyengine-carrier.ts), and the exported export ids inexamples/polyengine-demo/run.ts; - the polyengine host module's exported resource-class names, which the
embedder maps by resource name:
js/polyengine/websocket.ts.
Before designing WIT or touching async/stream plumbing, consult
lann/wasm-component-starter
(especially OUTLINE.md) — treat it as a living knowledge base and re-read
it rather than relying on a cached summary.
These come from the README's design notes; changing one is a design decision to record, not a refactor.
- One copy of the shared WIT package. The
polymorph:websocketpackage is defined exactly once, at the rootwit/. Components pull it in throughwit/depssymlinks back to the root. Do not copy the package into a component or replace those symlinks with real directories. - The JS host must stay browser-compatible.
js/polyengine/websocket.tsuses only the standardWebSocketAPI — nonode:modules, no Deno-only APIs: the same file must load in a browser unchanged (the polyengine-browser conformance row and the Chromium parity leg both depend on it). - The browser API bounds the portable surface. Capabilities the browser
WebSocketcannot serve (headers, client certs, ping/pong, trust decisions) do not appear on the ungated surface. Divergence between implementations is resolved, never accumulated — apply the webcrypto sibling's portability ladder in order: design it out; enhance the deficient implementation; narrow uniformly; record latitude at the definition site; isolate behind a gate or withheld export. A divergence with no artifact is a bug. - Message boundaries are preserved. The interface is message-oriented; do not flatten it into a byte stream.
- Client-only. A listener surface is additive, if ever wanted; do not let server-side concerns shape the client resource.
- The in-guest provider's TLS posture stays off the shared surface.
wss:in-guest comes from composingpolymorph:tlswith explicit, fail-closed trust anchors (seerust/guest-provider/README.md); trust decisions never appear on the WIT.
Requests arrive with a reason attached. The reason is a claim about the code, and it can be false while the request still points at something real. Establish that it holds before writing the change, and if it does not, say so first. A contradiction turned up while researching is a result to report, not an obstacle to route around. Separate what is wrong with the code now from what the proposed remedy fixes — they are often both true of different problems; name which property the change actually buys.
Every WIT comment is a doc comment: bindings generators project it into
library documentation, so its audience is the package's consumers, not
this repository's contributors. Package-wide contracts (streaming, close
semantics, error contract) live in a wit/README.md, referenced by name
from item docs — never restated in full at a use site, never living only
inside one item's doc. Basic usage first, critical caveats never buried
mid-paragraph behind mechanics. Use Simplified Technical English as
guidance: short sentences, active voice, one instruction per sentence,
consistent terms. No repository-internal content (implementations, test
harnesses, design history) on the package surface.
Code comments describe what something is or does, not the process by which it was arrived at. Rationale like "we removed X because Y" belongs in commit messages or PR descriptions. Comment what a reader could not predict: an invariant, a hazard, a deliberate departure from the obvious choice, a constraint imposed from outside the file — never a defence of the presence of ordinary code. Answers to an objection belong where the objection was raised (the pull request), not in source. Guards are the exception: a test or assertion exists because of the failure it prevents, so saying what it catches describes what it is.
Docs state invariants, not inventories. Never embed values a build or test run computes; if a number matters, a gate asserts it.
Three factors, binding in order:
- Necessity. Changes that cannot land separately without leaving
mainworse between them go in one PR, whatever that does to its size. Once conformance gates all implementations against one behavior, a change to the package surface is co-dependent across the WIT and every implementation by construction — name the co-dependence in the description. - Cohesion. One decision per PR: a single ruling plus its consequences. "And also" is the tell that two PRs are sharing a branch.
- Review time. Within what the first two allow, smaller is better — except that many nearly identical changes are one PR, not many, because near-identical diffs review sublinearly. The test is textual similarity of the diffs, not thematic similarity of the work.
Open findings and design decisions live in this repository's GitHub issue
tracker (gh issue list), not in a TODO file. Before starting work that
touches an area, search the open issues — some encode contract decisions
the change should resolve, not work around. Close issues through PRs with
closing-keyword lines (Fixes #N); when a PR resolves only part of an
issue, tick the resolved items and comment naming the PR. File new issues
for new findings rather than adding TODO comments or files.