# Portal browser port — rolling changelog

## Current target

Run the original `testchmb_a_00` in a browser using a custom Rust/WebAssembly engine and raw WebGL 2. The current development build is served at **http://localhost:8066**. The supplied retail files remain local; no game assets are downloaded or replaced with generated art.

**Fidelity status: early implementation; not retail-equivalent.** Loading original assets does not establish matching rendering, behavior, audio, or physics. Zero-exception parity remains the goal, not an achieved claim.

## Continued iteration

- Automatic work remains capped at **12 follow-up iterations**. After iteration6, a directly launched workflow runs the remaining six objectives serially, each with a fresh implementation worker and an independent verification worker. The unsupported 30-minute coordinator handoff has been cancelled.
- `ITERATION.md` tracks the count, ownership, stopping conditions, and priorities. Supported cover/door playback, dispenser collision, and the wake-up attachment camera/handoff are implemented. First-elevator track travel, walk-on boarding, and an upper-walkway exit are now playable; the missing static threshold collision was fixed in the follow-up below. The upper cycling-portal cube/button route now passes desktop/mobile interaction checks. Cleanser gameplay removal and authored replacement branches now pass desktop/mobile and real-time dialogue checks. Next is uninterrupted full-map verification and remaining presentation/physics gaps; ChangeLevel remains unsupported.
- The schedule requires this Grok session to remain running; it is not a claim of unattended work after the application exits.

## 2026-09-10 — Automatic launch recovery

- The lightweight scheduled coordinator lacked a workflow-launch tool and started no worker. Cancelled that schedule instead of leaving it to fail repeatedly.
- Created and launched `portal-continuation` directly from the parent session: at most six remaining objectives, each followed by a fresh independent verification worker, with no overlapping implementation or nested launches. It stops/pauses on missing evidence, failed verification, or worker failure. Maximum12 child calls for iterations7–12.
- Workflow smoke validation passed for the canned stop path; it is not proof of live game/test success. The real workflow has started with the continuous first-elevator checkpoint as its next task. Game source was unchanged by this orchestration repair.

## 2026-09-10 — Scheduled iteration 7: continuous first-elevator / upper-walkway checkpoint

- Extended `tools/verify-playthrough.mjs` from the accepted first-puzzle exit through ordinary W-key walk-on boarding over the original static threshold, the authored elevator delay and 65-unit/s ride, arrival-gated door opening, and walking off onto the upper walkway. The opening, vault crossing, natural cube release, pickup/drop and cube-held lower doors retain their existing assertions. No engine/compiler/web source, authored geometry, player step height or retail asset changed; no game defect was established.
- Chrome **1440×900 desktop passed twice** from a fresh page using only keyboard/mouse motion, look and E use. `__portal.state()` / `entities()` are read-only observations; there are no pose, step, advance, forced-I/O, pause or restart shortcuts. Each run records **11 checkpoints and 73 ride samples**. Checks cover stationary pre-departure boarding, continuous rider support, monotonic lift motion, original nodes **174 → 175 → 173 → 171**, exact scheduled arrival, collision-gated opening and upper-walkway dismount. Maximum sampled passenger-height drift was **0.00004578 units** in both runs.
- Final checkpoint: map time **121.33334 seconds**, position **[-1153.828, -863.835, 704.031]**, one portal crossing, no held entity and no train rider after dismount. First train stopped at **[-1472, -864, 792]**; the outgoing train stayed at its initial stop. Cube 323 remained on the lower button at **[-509.247, -922.911, 156.031]**, with both lower exit-door amounts **1.0**. Every checkpoint/sample had WebGL error 0; JavaScript/HTTP errors and train warnings were empty. Boarding, arrival and upper-walkway screenshots were inspected.
- Passed commands: `node --check tools/verify-playthrough.mjs`; `node tools/verify-playthrough.mjs` **twice**; `node tools/verify.mjs` (**7** desktop/mobile core checks); `node tools/verify-controls.mjs` (**4** ordinary-route checks); unchanged `node tools/verify-elevator.mjs` (**4** result groups: desktop/mobile walk-on, timing, arrival/exit, pause/restart, real-time ride and independently triggered outgoing train); `node tools/verify-upper.mjs` (**2** desktop/mobile result groups covering cube/button, portal cycling/traversal, exit fizzle and restart). Native/compiler tests and Wasm rebuild were not rerun for this test-only extension. A **30-file** source fingerprint comparison confirms protected engine/compiler/web files, existing elevator test and package scripts are unchanged.
- Evidence: `verification/playthrough.json`, `verification/playthrough-*.png`, `verification/results.json`, `verification/controls.json`, `verification/elevator.json`, and `verification/upper.json`. Both route results/screenshots, unique command logs, accepted-route backup, scoped diff, source/input audit and `acceptance-evidence.json` are retained in `target/iteration-07-elevator-route/`. Both attempts passed; iteration-6 failure evidence remains preserved in its original archive.
- **Limits / next checkpoint:** continuous coverage ends on the first elevator's upper walkway. Upper-puzzle completion, outgoing elevator and unsupported ChangeLevel are not continuous-playthrough certified. This route is desktop keyboard/mouse, not mobile continuous-play certification; existing mobile elevator/upper regressions retain their labeled inspection setup. The route remains separately invoked, outside `npm run verify`; no retail visual/physics parity claim is made. Next extend the ordinary-input route through the upper cycling-portal cube/button puzzle to a verified cube-held upper-door checkpoint.
- Reused the healthy **`tools/serve.py --port 8066`** process (PID12977), without launching a duplicate. At **19:38:23 UTC**, **http://localhost:8066/** and **/pkg/portal_browser_bg.wasm** both returned HTTP200. Completed count advances **6 → 7 / 12** only after the above checks passed; active ownership is cleared on normal exit.

## 2026-09-10 — Rate-limit recovery

- Iteration6 stopped with HTTP429 and remains **incomplete (5/12 completed)**. It added only an unfinished ordinary-keyboard/mouse playthrough harness; comparison against the pre-iteration snapshot found no engine/compiler/web source edits.
- Preserved `tools/verify-playthrough.mjs` and its failing checkpoint without adding it to the passing test command. The initial run stopped at the vault portal with the player outside the intended approach alignment. This requires controller/game diagnosis, not a full-map success claim.
- Restored the preview after its ten-hour server-task expiry; HTTP200 plus core desktop/mobile and ordinary-route browser control suites passed.
- Replaced the recurring implementation conversation with a lightweight coordinator launching **one fresh-context workflow worker per objective**. Workers have bounded tool/read budgets and preserve progress in files rather than growing one transcript. The workflow's canned-host smoke check passed; a real fresh run was launched. This reduces accumulated context but does not guarantee external API rate limits cannot recur.

## 2026-09-10 — Scheduled iteration 6: ordinary-input first-puzzle checkpoint

- Corrected the interrupted `tools/verify-playthrough.mjs` controller without changing engine physics. Movement now releases W, waits for horizontal speed below 0.5 units/s, and rechecks each waypoint within 7 units before turning or accepting arrival; input pulses and resting positions are recorded. Portal crossing releases W even on failure and asserts exactly one crossing. Checkpoints assert WebGL error 0 and no inspection-camera override.
- Fresh exported BSP evidence identified two bad draft waypoints, not engine defects: brush 389 blocks the direct southward route at Y=-416, and entry-frame brushes 837/838 obstruct the X=-544 approach. The controller now follows the passage around the vault and centers on X=-512 through the entry. No authored geometry, player step, portal aperture, or supplied asset changed.
- Chrome 1440×900 completed the bounded real-time route **twice**: wake-up control handoff, original 66.7-second vault-portal activation, one crossing, natural first-cube release, keyboard E pickup, carry/drop on the original button, then walk through both exit panels into the corridor. The final run ended at map time **98.7667 seconds**, position **[-1105.721, -863.972, 128.031]**; cube 323 remained at **[-506.019, -922.251, 156.031]**, unheld, with both exit-door amounts **1.0**. Six checkpoints had WebGL error 0; browser/HTTP errors were empty. Portal, button, and exit screenshots were inspected.
- The final test source passed `node --check tools/verify-playthrough.mjs`, `node tools/verify-playthrough.mjs`, `node tools/verify.mjs` (**7 core checks**, desktop 1440×900 / mobile 390×844), and `node tools/verify-controls.mjs` (**4 ordinary-route checks**). Core/controls also passed before the final rerun. The playthrough invokes only read-only `__portal.state()` / `entities()`; all route motion/look/use is Chrome keyboard/mouse, with no pose, step, advance, forced I/O, restart, or pause setup. Native/compiler suites were not rerun for this test-only edit; 13 archived engine/compiler/web source files compared unchanged against the iteration-06 snapshot.
- Evidence: `verification/playthrough.json`, `verification/playthrough-*.png`, `verification/results.json`, and `verification/controls.json`. Unique command logs, the original draft, both navigation failures, and the first passing result are retained under `target/iteration-06-controller/`. The unverified later draft route remains in `verify-playthrough.before.mjs`; the executable test now explicitly ends at the first-puzzle exit instead of claiming full-map certification.
- **Limits / next checkpoint:** continuous elevator boarding/ride, upper puzzle, outgoing elevator and unsupported ChangeLevel are not covered. The route is desktop keyboard/mouse on `?verify=1` with read-only observation, not a mobile continuous-playthrough certification; ordinary non-debug controls are separately tested. The playthrough remains a separately invoked test, not part of `npm run verify`. Existing renderer/physics approximations, missing cleanser/dissolve presentation and independent retail parity remain unresolved. No new game defect was established by the controller failures.
- Existing `tools/serve.py` service was reused, with no duplicate launched. Preview **http://localhost:8066/** and actual **/pkg/portal_browser_bg.wasm** returned HTTP 200. An initial manually guessed `portal_astra_bg.wasm` probe returned 404; correcting the probe required no server/source change. Completed iteration count is now **6 / 12**; ownership is cleared after recording this checkpoint.

## 2026-09-10 — Preview maintenance and iteration-5 acceptance

- Independently reran desktop/mobile cleanser behavior and real-time speech/completion checks; they passed. Missing field/dissolve visuals remain an explicit limitation.
- Refreshed the preview server and health monitor at 08:38 UTC before their original task lifetime limit. No duplicate services were left running; page and Wasm URLs returned HTTP 200. The scheduled implementation loop remains active.

## 2026-09-10 — Scheduled iteration 5: cleanser gameplay and spawned-cube I/O

### Implemented

- Added scheduler registration for spawned `prop_physics` rows using the original lossless template metadata, a fresh clone ID and independent output fire counts. Duplicate targetnames fan out, `!activator`/`!self` resolve clone IDs, and registration does not reset the clock/queue or rerun map spawn. Invalid/duplicate rows reject atomically. The independent scheduler worker's API/tests are incorporated; other dynamic entity classes remain unsupported.
- Template sources are removed from live scheduler routing while retained as immutable spawn metadata. Replacement makers select that source rather than an arbitrary hidden/deleted clone. Actual cloned cube pickup/drop outputs now reach hint/timer targets, and pickup/drop activators use the player identity. Empty-message `env_hudhint` clears its hint instead of showing generic pickup text.
- Both original `trigger_portal_cleanser` volumes now process eligible physics props and player contacts. Props are released if held, removed from simulation/rendering once, and dispatch `OnDissolve`; only objects named `box` dispatch `OnDissolveBox`. Players fizzle active linked portals. Existing trigger flags, filters, Enable/Disable and original output wiring remain authoritative.
- First-room cube removal disables the button cube trigger and enters the map's existing dialogue/quiet relay branches. The dialogue branch releases the retained cube at actual `ai_box_fried.vcd` completion, **10.776666641235352 seconds after scene start**; the quiet branch releases it at the authored **+4 seconds**. Natural dispenser exit then closes the cover and creates one retained replacement. The upper cleanser has no invented first-room replacement output.
- Added once-per-instance `OnOutOfWorld` for Source-style ±16384 coordinate / ±2000 velocity limits, preserving the original clone-targeted Kill and drop-logic outputs. Kill clears held-object state. Added machine-readable cleanser/removal and relevant gate/scene diagnostics plus `tools/verify-cleanser.mjs` in `npm run verify`.

### Verification and explicit gaps

- **95 native engine tests**, **55 compiler tests**, **3 inspector tests**, release Wasm build and scoped formatting/syntax checks passed. The **15 scheduler tests** include six dynamic-registration cases for fresh once-counts, duplicate-name routing, clone activators/self, kill isolation, atomic failures and nonzero-clock registration. Runtime tests cover both replacement times, actual button placement/repickup, source-only template routing, clone Wake/Kill/out-of-world outputs, both cleanser volumes, disable/re-enable, noncube removal and player portal fizzle.
- Chrome **1440×900 desktop / 390×844 mobile** passed real E/touch pickup/drop and W/touch approach to the first cleanser; removal happens once, held state clears, paused state freezes, and original dialogue/quiet branches release and replace correctly. Upper-cleanser and both player-fizzle cases pass, with no fabricated replacement when the original upper entity has none. Results/screenshots: `verification/cleanser.json`, `verification/cleanser-*.png`; desktop/mobile before/after renders inspected.
- A separate desktop **continuous real-time cleanser speech/completion** run passed: the supplied crate-vaporized WAV progresses in an audio element, then source-scene completion releases the cover without inspection time advancement during that wait. Human listening/retail audio parity was not established.
- All surrounding browser regressions passed: core/first puzzle, authored animation, natural dispenser, wake-up camera, walk-on elevator and continuous ride, upper puzzle, ordinary-route controls, capture, and the full **67-second real-time opening**. Completed results report no JavaScript/WebGL/HTTP errors. A new branch-test fixture initially held the cube short of the button; it was corrected to actually drop/repick the cube, preserving the branch assertions.
- **Not a retail dissolve presentation:** gameplay removal is immediate. The original `portal_cleanser` field particles and dissolve particles/animation are **not rendered**, and the on-screen hint explicitly notes the missing dissolve particles. No invented visual art or guessed dissolve duration was added. Trigger contact remains hull/convex overlap rather than swept VPhysics; fast tunneling, additional affected entity classes, general dynamic entity spawning and full scene-actor arbitration remain incomplete.
- These route tests use inspection player poses for setup, not an uninterrupted full-map playthrough. Remaining priorities include continuous two-puzzle/elevator progression, visible cleanser/dissolve presentation, full cube PHY/angular dynamics and campaign transition. Supplied assets are unchanged; no retail-equivalence claim is made. Preview remains **http://localhost:8066** with the original server/watcher. Reversible snapshot: `target/iteration-05-before.tar.gz`.

## 2026-09-10 — Scheduled iteration 4: upper cycling-portal puzzle

### Implemented

- Fixed the upper orange portal's blocked traversal: its BSP includes a backing brush exactly **8 units** behind the portal plane, which the previous strict `<8` cutout test excluded. The existing rectangular aperture now includes that boundary with a small numerical tolerance; walls outside the aperture remain solid.
- Portal disable/fizzle inputs now affect the linked color only when they target its current entity. Fizzling an inactive predecessor no longer overwrites an unrelated live portal. The original five-second blue cycle and **0.01-second unlinked gaps** are preserved; upper exit still fizzles **blue2 only** and leaves orange active.
- Added held-prop portal-space target tracing and position/velocity mapping. The cube can cross ahead of the player, remains held through the player's subsequent traversal, and can be dropped on the far side. Loose AABB props also cross supported linked apertures. Remote holding is released in place if the portal link closes/changes, avoiding a cross-map snap to the player's new link.
- Corrected the linked-view clip-plane bias so coplanar destination wall surfaces are excluded rather than covering the upper portal view. Rendering remains raw WebGL2 and one-level, with the existing provisional rim.
- Added bounded player hull exit clearance for the authored blue2 portal at a side-wall edge. A search limited to the portal's remaining horizontal aperture width (plus contact tolerance) chooses a nonpenetrating destination; if none exists, traversal fails closed. No map geometry or authored portal position was changed.
- The original upper cube can now be retrieved through blue3, carried back through orange and into blue1, placed on the upper button, and left holding both exit panels while the player returns through the cycling portal to blue2 and walks to the outgoing elevator approach. Added `tools/verify-upper.mjs` to `npm run verify`.

### Verification and limits

- **85 native engine tests**, **55 compiler tests**, and **3 inspector tests** passed; release Wasm build and scoped formatting/syntax checks passed. New native tests cover exact cycle gaps, inactive-target fizzle isolation, held/loose cube traversal, portal-change release, upper-button door retention, and safe blue2 wall-edge exit without penetration. An inspection fixture retained walking velocity during setup; it was corrected to match browser pose setup rather than weakening its cube-location assertion.
- Chrome **1440×900 desktop / 390×844 mobile** upper-puzzle interaction checks passed: player-button press/release, original blue cycling, actual E/touch upper-cube pickup/drop, cube crossing ahead of the player, held traversal through two links, cube-held doors, five player portal crossings, walk-through exit, blue-only fizzle/cycle shutdown, outgoing elevator opening, and restart. Results: `verification/upper.json`; inspected linked-view, remote-held cube, button and exit screenshots in `verification/upper-*.png`.
- Core, first-puzzle, animation, natural dispenser, camera, walk-on elevator/continuous real-time ride, ordinary controls, and **67-second real-time opening** suites passed with no JavaScript/WebGL/HTTP errors. The new route uses **player/camera inspection poses and time advancement for setup/waiting**, while cube movement uses pickup/drop and portal simulation; it is not an uninterrupted full-map playthrough or a retail comparison.
- **Explicit approximations remain:** rectangular collision aperture versus oval visual; bounded lateral player exit adjustment; far-side held cube clamped clear of the exit plane; no split/cloned prop mesh at the portal plane; no angular dynamics or orientation transform for nonsymmetric props; no verified Source pickup distances/physics or general recursive rendering. Cube lighting still comes from its exported initial lighting and can visibly differ across rooms.
- Cleansers/dissolve/replacement dialogue, full dynamic scheduler registration, continuous two-puzzle progression, final campaign transition, and independent retail fidelity verification remain unfinished. No supplied asset files changed and no generated substitute art was introduced. Preview remains **http://localhost:8066** using the existing server/watcher. Reversible snapshot: `target/iteration-04-before.tar.gz`.

## 2026-09-10 — Elevator walk-on collision follow-up

- Fixed the jump-only boarding issue by restoring **missing original static-prop collision**, not by changing player step height, train placement, or model geometry.
- The authored `elevator_caps` PHY includes a threshold at world Z **136.745737** between corridor Z128 and elevator floor Z148.529462. The resulting **8.75-unit and 11.78-unit** rises both fit the unchanged 18-unit step. The threshold had been rendered but its collision was absent.
- Exported the retail elevator-cap and shaft-wall PHY for all eight solid6 static instances, including upper and outgoing landings: **100 convexes / 1,072 collision triangles**. Added parser shape/volume and runtime threshold/walk-on regression assertions.
- The independent investigation confirmed the train's initial +2-unit track alignment and child-parent transform were already correct. Those transforms remain unchanged.
- Desktop/mobile Chrome now boards with ordinary W/touch movement, rides to the upper walkway, exits, pauses/resumes, and restarts without jumping. The continuous real-time ride and outgoing-track checks also passed. Removed the obsolete lip `knownGaps` entry and replaced its jump workaround with required walk-on assertions.
- Final checks passed: **80 native engine tests, 55 compiler tests, 3 inspector tests**, plus the complete desktop/mobile browser suite including wake-up, dispenser, first puzzle, both elevator states/routes, ordinary controls, continuous real-time ride and 67-second opening. No JavaScript/WebGL/HTTP errors were reported.
- This correction covers the observed entry defect. Complete VPhysics, upper-puzzle and cleanser behavior, visual parity, and campaign transitions remain unfinished.

## 2026-09-10 — Scheduled iteration 3: first elevator travel

### Implemented

- Added Rust tracktrain movement for the map's unparented, fixed-orientation, instantaneous-speed `path_track` chains. Initial train origins align to p1 (`214 → 216` for the first train); `StartForward` uses authored `startspeed=65`, Stop preserves position, and every reached node emits chronological `OnPass` without replacing travel with a timer. The first route is **Z 216 → 305.390015 → 487.600006 → 792**, 576 units of travel (576/65 seconds at this implemented constant speed).
- Exported original elevator PHY collision: each body has **20 terminal convexes / 408 triangles**, each door **2 convexes / 48 triangles**. Body and door geometry/collision follow train parent translations. The retail train brush's passable spawnflag is honored; actual support comes from the authored body PHY rather than an invented platform. Door `EnableCollision`/`DisableCollision` follows original relays.
- Added ground-contact passenger displacement and vertical lift velocity inheritance on leaving/jumping, plus rising-floor contact recovery for relanding. A loose AABB cube can ride the floor. Static corridor collision remains in passenger sweeps; unsupported crush/block cases emit a diagnostic. Train-mounted children, including nested child offsets, move together. Stationary trains skip support-query work and only dynamic collision is rebuilt while this map's passable trains move.
- First-elevator entry keeps the authored **+5-second StartForward** delay. P4's actual arrival output stops the train and schedules door opening/chime at **+1 second**. Player can ride to and walk out onto the upper walkway **within testchmb_a_00**; no map transition is faked.
- The outgoing train keeps its separate name/path and original copy/paste-looking Stop target. Its isolated endpoint test emits an explicit unsupported `ChangeLevel testchmb_a_01` notice; campaign transition is not implemented. Added `verify-elevator.mjs` to `npm run verify`.

### Verification and remaining limits

- **79 native engine tests**, **54 compiler tests**, and **3 inspector tests** passed; release asset export, release Wasm build, formatting/syntax checks passed. Tests cover actual elevator PHY support/closed-door collision, source track positions, stop/start timing, four ordered node passes, parent offsets, upper exit, reset, coarse/fine scheduler times within 1e-10 seconds, jump velocity/relanding without penetration, and a loose cube's preserved relative floor height. Existing PHY volume checks now include both elevator models within the existing two-parts-per-million tolerance.
- Chrome **1440×900 desktop / 390×844 mobile** passes cover real keyboard/touch jump boarding, delayed start, constant-speed travel, frozen rider/geometry while paused, all four node passes, arrival-gated opening, independent outgoing-train state, upper-walkway exit and restart. A separate **continuous real-time desktop ride**, with mid-travel pause/resume and no inspection time advancement during the ride, passed. Results/screenshots: `verification/elevator.json` and `verification/elevator-*.png`; desktop/mobile arrival and exit screenshots inspected.
- Core, first-puzzle, authored animation, natural dispenser, wake-up camera, ordinary-route controls, capture and full **67-second real-time opening** regressions passed. These tests use inspection poses for initial setup; this is not an uninterrupted two-puzzle/full-map playthrough. Final browser results report no JavaScript/WebGL/HTTP errors.
- **Reproduced entry mismatch:** the decoded elevator floor/lip is about **20.5 units above the corridor**, exceeding the current 18-unit player step. Ordinary walking stops at the lip; a normal jump boards successfully. No retail shape was lowered and no larger step was invented to hide this. This remains in `verification/elevator.json.knownGaps` and needs further Source placement/step reference investigation before claiming walk-on equivalence.
- Track motion is a bounded subset, not full Source train simulation: reverse/alternate/looped tracks, acceleration/orientation blending, 0.1-second Source lookahead event semantics, crush/unblockable behavior, complete moving-surface impulses, rotational props and general parent transforms remain incomplete. Elevator lighting/beam/shake rendering and full sound fidelity are still incomplete. Upper portal-cycle puzzle, cleansers, final progression and campaign remain unverified/unfinished.
- Public Valve SDK `trains.cpp`, `trains.h` and `pathtrack.cpp` informed flags/speed/path semantics; no independent running-retail comparison was performed. Preview remains **http://localhost:8066** using the existing server and health watcher; supplied assets were preserved. Reversible source snapshot: `target/iteration-03-before.tar.gz`.

## 2026-09-10 — Scheduled iteration 2: authored wake-up camera

### Implemented

- Exported the original MDL attachment table with bounded reads, checksum/version validation, finite matrix checks and bone-index validation. `blackin`'s `vehicle_driver_eyes` is attached to bone 2, `Blackout.view`; the runtime composes entity × animated bone × attachment matrices, **without applying inverse bind a second time**.
- Rust/Wasm now follows the authored opening camera position, forward and up vectors, including roll. Raw WebGL2 main and linked-portal view matrices accept camera up; no JavaScript camera simulation or generated motion was added.
- `point_viewcontrol` Enable/Disable and `SetParentAttachment` handle the opening's takeover. Keyboard/mouse/touch movement, look, jump, use and noclip are blocked while this camera owns control. The player remains at the original spawn until the existing animation completion enables the original `trigger_teleport`; actual hull contact teleports to `(-544, -368.776, 160)`, yaw 190, and the authored output disables the camera. Camera/model removal still uses the authored +0.2-second outputs. Handoff is not driven by a standalone timer.
- Added the opening `env_fade` hold/decay path as a Rust-controlled raw WebGL2 fullscreen blend: commentary black at map spawn, intro fade at 0.2 s, three-second hold, four-second fade-in ending at 7.2 s. Pause and restart preserve/reset camera and fade state.
- Map-spawn I/O is drained only after scene/audio metadata is installed, fixing native scene-start regressions exposed by initializing the camera early. Restart resets the camera, animation, teleport count, and fade without starting paused audio.
- Added explicit `?verify=1` `inspectView` override for fixed-camera prop screenshots; ordinary gameplay does not use it. Prop-animation/capture harnesses opt into it; camera tests run with it off. Existing controls regressions now wait for the authored control handoff instead of expecting movement during the cutscene. Added `verify-camera.mjs` to `npm run verify`.

### Verification and limits

- **74 native engine tests**, **54 compiler tests**, **3 inspector tests**, release Wasm build, release asset export and scoped formatting checks passed. Attachment tests cover malformed offsets/bones/nonfinite matrices and compare the runtime camera against independently skinned retail bone probes at five fractional cycles within **0.0002 Source units**.
- Native tests verify original initial eye position approximately `(-486.636, -380.546, 190.299)`, blocked controls, changing authored pose, actual teleport contact, camera cleanup, delayed bed closing, and restart. Moving the player outside the trigger deliberately prevents handoff even after the animation ends, guarding against a guessed completion timer.
- Chrome **1440×900 desktop / 390×844 mobile** camera interactions passed: black/hold/fade, changing position and roll, frozen pause, blocked keyboard/touch/mouse input, animation/contact handoff, restored keyboard/touch movement, bed follow-up and restart. The pre-handoff eye differs by under two Source units from the standing eye; no extra smoothing was invented. Screenshots inspected: `verification/camera-*.png`; results: `verification/camera.json`.
- Core, first-puzzle, bed/dispenser/both-elevator animation, natural dispenser retention/release/replacement, ordinary-route pointer-lock controls and capture regressions passed. The full **67-second opening ran in real time** with additional camera pause/handoff assertions plus existing bed closing, radio/speech, portal activation and restart checks. All completed suites report no JavaScript/WebGL/HTTP errors (`verification/opening.json` and existing result files).
- Camera/fade/parent semantics were checked against public Valve Source SDK 2013 `triggers.cpp`, `baseentity.cpp`, `EnvFade.cpp` and `view_effects.cpp`; these are implementation evidence, **not** independent Portal retail execution. Full general viewcontrol paths/targets/FOV, landmark teleport transforms, modulated/per-player/multiple fades, Source message quantization and rendering parity remain unsupported. Tonemap/bloom, player body, exact lighting/glass and other visual effects remain incomplete; blackin's tiny helper mesh stays hidden.
- No elevator train travel, cleansers or full-map/campaign completion was added. Preview remains **http://localhost:8066** with the existing server/watcher. Supplied asset directories were preserved. Reversible source snapshot: `target/iteration-02-before.tar.gz`.

## 2026-09-10 — Dispenser collision follow-up

### Fixed

- Reproduced the cube passing through the closed dispenser, then decoded the **original retail PHY collision** instead of adding a release timer or guessed retaining plane.
- Added a dependency-free Rust PHY parser with checksum validation, bounded tree/ledge reads, outward convex triangles, native unit conversion, and independently checked MDL bone bindings. The cover exports **20 bone-bound convexes / 288 triangles**; the body exports **8 terminal convexes / 128 triangles**. Internal approximation hulls are excluded so the dispenser opening stays open.
- The renderer and collision system now consume the same authored bone poses. Runtime collision transforms bone-local PHY vertices directly, generates face/axis/edge separating planes for existing swept hulls, and replaces only the dynamic collision portion when poses change.
- The original cube now rests on closed panels, falls only as the real opening animation clears its shape, activates the original exit trigger, closes the cover, and creates exactly one replacement retained by the reclosed panels. Native and browser tests exercise this without moving the cube through an inspection hook.
- Replaced the previous soft `knownGaps` report in the animation test with required natural-close/replacement assertions. Added `verify-dispenser.mjs` to the normal `npm run verify` command.

### Verification

- **70 native engine tests**, including natural retail dispenser retention/release/replacement and open-gap geometry tests, passed. **52 compiler tests**, including eight PHY tests, passed. The parser validates authored volumes within two parts per million. **3 inspector tests** passed.
- Desktop **1440×900** and mobile **390×844** Chrome passes cover: naturally spawned cube retention for ten extra seconds; entry-triggered animation and release; automatic close and exactly one retained replacement; paused-state stability; E/touch pickup/drop; restart resetting cube count and retention. Results/screenshots: `verification/dispenser.json` and `verification/dispenser-*.png`; screenshots inspected.
- Existing core, first-puzzle, bed/dispenser/both-elevator animation, ordinary pointer-lock controls and real-time 67-second opening browser regressions passed with no JavaScript/WebGL/HTTP errors. The first-puzzle test still places the released cube on its button and traverses the exit.
- PHY extraction and runtime integration remain scoped to this dispenser body/cover. Other prop collisions, cube angular dynamics, moving-body impulses, continuous animation sweeps, contact/friction parity, wake-up camera and elevator travel remain unfinished. This fixes the observed failure, **not** full Source VPhysics or retail parity.

## 2026-09-09 — Scheduled iteration 1: authored skeletal playback

### Implemented

- Added validated `sample_sequence_frames` in the Rust model compiler. It exports original local bone positions/quaternions, hierarchy, inverse binds, vertex influences, source FPS/frame count and loop flags for the bed cover, dispenser cover, both elevator door instances and `blackin`. Supplied asset directories are unchanged; generated metadata is in `web/assets/chamber.json` (about 2.85 MB).
- Added Rust/Wasm local-position interpolation and quaternion slerp **before** hierarchical skinning. Raw WebGL2 updates only changed prop vertex ranges; UVs/materials/topology are retained. This is not vertex-frame interpolation or a guessed hinge transform. Pause freezes animation and restart restores the initial rendered pose.
- `SetAnimation` and `SetDefaultAnimation` now drive the supported props. Nonlooping completions fire `OnAnimationDone` once at `(frame_count - 1) / fps`, chronologically within the existing I/O scheduler; replacing/killing playback cancels the old completion. Unsupported requested sequences produce explicit diagnostics rather than invented timings.
- Preserved the map's unusual bed input: at **5.2 s** it selects the one-frame looping `open` pose, not the separate `opening` sequence. `blackin` starts at 4.2 s; its source-duration completion drives bed `closing` at **11.533333 s**, lasting 31 frames at 30 FPS (1 s). Camera/teleport behavior is not implemented by this timing support.
- Dispenser and elevator door `open`/`close` clips each use 20 authored frames at 30 FPS (19/30 s). Elevator close returns to the authored `idle_closed` default; blank default after open holds the endpoint. No train movement or map transition was added.

### Verification and discovered limits

- **65 native engine tests** passed with retail-dependent tests enabled; **44 compiler tests** passed, including 18 model tests and runtime/compiler agreement at five fractional cycles for every exported sequence. Positions/normals agree within 0.0001 Source units/components, also under nonzero instance origin/angles. **3 inspector tests**, formatting checks, release asset export and release Wasm build passed. Existing unused-metadata warnings remain.
- Chrome animation checks passed at **1440×900 desktop / 390×844 mobile**: exact bed input times, skeletal midpoint/end geometry, screenshot changes, frozen pause, pixel-identical initial-pose restart, E/touch cube pickup/drop, real dispenser trigger close/replacement, and independent open/close/default states for both elevator door sets. Results/screenshots: `verification/animations.json` and `verification/animation-*.png`; inspected desktop/mobile bed and dispenser renders.
- Core, first-puzzle, ordinary-route controls and capture regressions passed (`results.json`, `interactions.json`, `controls.json`). The **67-second opening ran in real time**, now additionally checking pause/resume during the closing animation and completion, while retaining radio/speech/portal/restart checks (`opening.json`). Completed suites reported no JavaScript/WebGL/HTTP errors. The independent compiler worker's changes and tests were incorporated before completion.
- **Explicit pre-existing failure exposed by the new test:** no animated prop collision means the first cube falls through the closed dispenser cover before `trigger_drop_new_box` is enabled, so natural close/replacement does not occur. This was reproduced and is retained in `animations.json.knownGaps`; it was not replaced with a timer. Close/replacement is verified separately with actual E/touch pickup/drop and an **inspection pose** placing the cube within the real trigger. Natural dispensing/retention remains unfinished; the original first-puzzle regression still passes.
- Opening camera/attachment/fade/handoff, animated collision/PHY, train movement/base velocity, full-map completion, sequence transitions/blends/embedded animation events, dynamic lighting updates on skinned vertices, and general Source animation support remain incomplete. Blackin is still hidden. Glass/lighting remain visibly approximate. No independent running-retail comparison or 1:1 fidelity claim is made.
- Preview remains **http://localhost:8066**, using the existing server and health watcher without duplicate services. Reversible source snapshot: `target/iteration-01-before.tar.gz` (compiler sampler is independently scoped).

## 2026-09-09 — Playable first-puzzle baseline

### Implemented

- Custom Rust/WebAssembly engine, direct WebGL 2 draw calls and shaders, original Source-coordinate world geometry. JavaScript supplies browser input, image/network loading, and audio transport only.
- Native Rust asset compiler: BSP, embedded ZIP, shared VPK dependencies, VMT patches, VTF decoding, MDL/VVD/VTX meshes, supported initial skeletal poses, Source overlays, lightmaps, and native static-prop vertex lighting.
- Current export: **151,512 triangles, 247 batches, 194 materials, 100 full-resolution textures, 2,360 lightmap charts, 91 prop instances, 53 native-lit static props, 323 entities, 1,139 collision brushes, and 27 referenced sound files**. The full first BSP includes the vault, first puzzle, and upper portal-cycling section; rendering that geometry does not mean all sections are playable.
- Raw player hull sweeps, sliding, steps, grounded movement, jump, and crouch. Fixed 120 Hz host simulation. Source-like movement values remain unverified against retail.
- One-level linked portal views and transformed player traversal at the authored vault portal positions. Map I/O activates the pair at 66.7 seconds. Bounded rectangular collision cutouts replace an earlier unsafe whole-brush removal approach.
- Entity scheduler preserves duplicate output keys, target fanout, relay delays, timers, trigger lifecycles, and first-button player/cube filters. Doors move their parented visual meshes and brush collision together.
- Template-relative cube instances, swept AABB gravity/sliding, pickup/drop, player-versus-prop collision-mask separation, and first-button doors. The first puzzle interaction test places a dispensed cube on the button and walks through the opened exit.
- Five recovered retail scenes with 26 timed events; original entry dialogue, radio, button/bed sounds, chronological scene outputs and completion callbacks. Browser pause/resume also pauses/resumes audio. No synthesized dialogue.
- Responsive development menu, mute/restart/pause, keyboard/mouse pointer lock, touch inspection, engine diagnostics, and explicit inspection time advancement.

### Browser-driven corrections

- Fixed missing bed/button geometry: their base-texture alpha is a self-illumination mask, not transparency. Opaque materials now retain zero-alpha texels and apply the authored self-illumination mask.
- Corrected near-black dynamic props: Source ambient-cube vector samples use a different scale from lightmap texture samples. Direct lighting and better ambient lookup remain pending.
- Fixed cube sticking on dispenser slope, invalid playerclip collisions for props, and an initially overlapping upper cube hull. The replacement uses basic AABB resolution, not retail VPhysics parity.
- Fixed first-button `OnTrigger` dispatch, last-contact release, invisible button-brush travel bounds, and child mesh transforms.
- Fixed missed very short keyboard/touch presses by retaining press edges until a simulation step.
- Exported shared retail sound dependencies to remove verified button-audio 404 responses.

### Verified baseline

- **57 native engine tests**, including actual compiled-world and first-button tests, passed with asset-dependent tests explicitly enabled.
- **34 asset/compiler tests** passed; the model parser sweep covers 596 matching retail MDL/VVD/VTX triplets. **3 scene-inspector tests** passed.
- Chrome checks passed: load, enter, ordinary-route pointer lock and mouse look, keyboard movement, jump/crouch, wall collision, pause with frozen simulation, resume, mute, diagnostics, restart, portal activation/traversal, player-button press/release, dispensed-cube E pickup/drop, cube-held doors, and walking through the first exit.
- Desktop 1440×900 / 1366×768 and mobile 390×844 checked, with touch interactions. Generated screenshots were inspected, including the vault, first room, and upper room. No JavaScript/WebGL errors or missing files in the completed core/control/puzzle suites.
- The **67-second opening ran in real time** in Chrome: radio and original speech audio elements loaded and advanced, pause stopped playback, resume continued it, all seven opening speech files were requested, and the portal pair activated. That test passed with no errors; it verifies playback/timing, not human listening quality or retail DSP equivalence. Results: `verification/opening.json`.
- Automated puzzle setup uses inspection poses; this is **not** an uninterrupted full-map playthrough or a comparison to retail execution.
- Machine-readable results and screenshots are in `verification/`. This verifies implemented browser behavior, not zero-exception retail equivalence.

### Remaining fidelity and campaign gaps

- Opening animated camera/attachment, bed/dispenser/elevator animation playback, trains/base velocity, cleansers, final transitions, save/load, and full first-map completion are still incomplete.
- Prop motion uses simple AABBs, contact sliding and spawn-overlap recovery; no angular VPhysics, decoded PHY shapes, prop-prop/player collision, or complete dynamic-entity I/O registration. Held-object portal behavior remains incomplete.
- Portal renderer is one level deep with a provisional rim; recursion, full clipping fidelity, player avatar and collision equivalence remain unfinished.
- Material/refraction/reflection/bump/Phong effects, direct/dynamic prop lighting/shadows, lightstyle changes, particles, color correction/bloom, clock and indicator proxies are incomplete. Current glass and prop brightness differ visibly from retail.
- Audio is not yet spatialized and has no full soundscape, surface footsteps, pitch/DSP, busy-actor arbitration, or extracted retail captions. `Doors.FullClose8` is an explicit unresolved sound-script dependency.
- Later campaign maps have not been implemented or verified. The native compiler is reusable, but loading a map is not campaign support.
- Retail comparison is blocked by lack of a verified running compatible reference; supplied executable is 32-bit macOS. Source asset evidence and browser tests cannot establish exact fidelity alone.

## 2026-09-09 — Initial implementation and first render

- Confirmed original BSP campaign maps, unpacked Portal materials/models/sounds, and shared Half-Life 2 VPK archives are present.
- Confirmed Rust, the `wasm32-unknown-unknown` target, Node, and Chrome are installed.
- Started concurrent work on the Rust BSP/material/lightmap compiler, MDL/VVD/VTX prop loader, convex-brush player collision, and first-chamber entity scripting.
- Engine rendering and simulation are authored directly in Rust; JavaScript is limited to browser startup, input, asset transport, and browser audio access. No Three.js or game engine is used.
- The supplied retail executable is Mach-O i386 (32-bit). Running the original as a reference on this Mac has not been verified.

### Verification record

- First Rust/Wasm build compiled successfully and is served at **http://localhost:8066**.
- First BSP-only export: 2,541 rendered map faces, 8,842 triangles, 154 material records, 61 decoded retail textures, 2,360 native lightmap charts, 1,139 collision brushes, 323 entities, and 71 overlays.
- Chrome end-to-end pass: enter chamber, keyboard movement, pause with frozen state, resume, mute, diagnostics, restart, ground collision, jump, crouch, wall containment, map-timed portal activation, and orange-to-blue traversal. Portal timing/traversal assertions use explicitly enabled inspection hooks; ordinary keyboard movement is tested separately.
- Desktop 1440×900 and mobile 390×844 layouts inspected; touch entry, jump, pause, and diagnostics exercised. No JavaScript exceptions or HTTP errors in that pass.
- Screenshots and machine-readable results are in `verification/`. This is browser behavior verification, **not** side-by-side retail parity verification.
- Native tests initially exposed an 18-inch step/contact-margin assertion; this was subsequently resolved and the full test suite passed.

### Gaps at the first-render checkpoint (superseded by the entry above)

- The first verified export contained BSP/overlay geometry only; authored props were integrated in the later baseline.
- Opening camera animation is not implemented: the initial view uses `info_player_start`, not the animated retail wake-up attachment.
- Portal views currently render one linked view, not recursive views; the rim is a provisional shader rather than the full retail portal particle/material effect.
- Glass/material effects, prop lighting, countdown animation, cube/button sequencing, and complete audio behavior remain incomplete. The first puzzle is not yet certified completable.

### Known outstanding requirements

- World mesh/material/lightmap rendering and authored props.
- Correct spawn, camera, player movement, collision, crouch, jumping, interactions, cube dynamics.
- Recursive portal views and matching traversal/momentum; first chamber scripted portals.
- Entity input/output timing, triggers, buttons, doors, elevators, transitions, save state.
- Retail voiceover, positional audio, ambient sounds, footsteps, sound processing, subtitles.
- Source material shader behavior, animated materials, decals, glass, particles, dynamic shadows, postprocessing.
- Animation, skinning, physics meshes, prop collision, player representation and reflection.
- Browser end-to-end and regression verification; desktop/mobile display checks.
- Side-by-side retail reference verification and subsequent campaign maps.
