# Codetrial Web

> The CodeTrial browser half and the boundary it talks across - why web/ has no build step, how a file reaches the browser embedded or from disk, the vendored checksum-pinned assets, the data-channel topics that have a producer in web/lib.js and a consumer in src/agent.rs, and the wire fixtures that keep the two agreeing. Use when editing anything under web/, adding or changing a data-channel message, touching src/web/, or wondering why a browser change passed every Rust test and still broke the interview.

- Skill: `sysprog21/codetrial-web` (Agent Skill)
- Install (CLI): `npx skillmds@latest add sysprog21/codetrial-web`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sysprog21/codetrial-web/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: sysprog21 (https://skillmd.com/u/sysprog21)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/sysprog21/codetrial-web

---


# The CodeTrial browser boundary

`web/` is plain ES modules loaded by the page. There is no bundler, no
transpile step and no framework, which is what makes an edit in the checkout
visible on reload. Keep it that way: a build step here would turn every browser
fix into a build artifact somebody has to remember to regenerate.

## How a file reaches the browser

`rust-embed` compiles `web/` into the binary, so a release build serves a
complete application with no `web/` directory beside it. The fallback is per
file rather than wholesale: a file present on disk wins, and one that is
missing is filled in from the embedded copy. That is why a partially populated
tree serves rather than 404s, and why running the binary from a checkout picks
up your edit with nothing set. `--web-dir` or `CODETRIAL_WEB_DIR` points that
root somewhere else.

The consequence worth remembering: a debug build that reads from disk and a
release build that serves embedded bytes can disagree, and only the second is
what ships. `scripts/test.sh` covers this, and CI proves the binary serves with
the web tree deleted.

## Vendored assets

`web/vendor/` holds third-party bytes that are downloaded rather than
committed: the LiveKit client, Pyodide, the MediaPipe face detector. They are
pinned by a `SHA256SUMS` beside each of them; `scripts/fetch-vendor.sh` fetches them and
`scripts/verify-vendor.sh` checks them, both wired into the gate. Never edit a
file under `web/vendor/`, never lint it, and never add a dependency by
dropping a file there by hand: the checksum is the whole mechanism. The avatar
model is not even vendored, the browser fetches it from a pinned upstream URL
against a pinned hash, and the interview falls back to a voice-only panel when
it is unreachable.

## The data channel is two implementations

Every topic on the LiveKit data channel has a producer in `web/lib.js` and a
consumer in `src/agent.rs`. Each side has its own tests, and a change that
tests only its own side is exactly the shape that once let the integrity hash
diverge and silently empty the evidence section of every camera interview
while the gate stayed green.

`tests/fixtures/*.json` are the answer: generated by
`node scripts/gen-wire-fixtures.mjs` from the real producer, consumed by the
Rust tests, and held to `--check` in the gate. So when you change a message:

1. Change the producer in `web/lib.js`.
2. Regenerate the fixtures and commit them with the change.
3. Change the consumer in `src/agent.rs` against the new fixture.

Never hand-edit a fixture. Everything in the generator is deterministic,
timestamps included, because a fixture that changes on every run cannot be
checked.

## Generated pages

`web/problems/`, `web/judges/` and the problem cards inside `web/index.html`
are generated from `problem-bank/`. Edit the bank and rerun the generators; see
codetrial-verify.

## The lint gate

`eslint.config.mjs` is narrow on purpose: every rule in it fails only where the
code cannot mean what it says, so the gate never becomes something people learn
to argue with. `no-undef` is the reason it exists, since a typo'd identifier in
`web/interview.js` reaches a candidate's browser while the same mistake in
`src/web/mod.rs` never leaves the terminal. Three recommended rules are excluded
with the reasoning written above them. Adding an `eslint-disable` is a stronger
claim than it looks: there are none today, and the Rust half carries four
`#[allow]`s in 45k lines, each with its reason on the same line.

Browser behavior gets a Node test in `tests/browser/*.test.js` and, where it
needs a real browser, `scripts/browser-check.sh` with Playwright and Chromium.

