a73x

4e7623ee

docs: verify.js is where a page rule gets pinned, not a browser

a73x   2026-08-26 12:30

Commit message
docs: verify.js is where a page rule gets pinned, not a browser

decisions.md said a real-browser pass was the only gate mux.js could
have. It is the gate for pixels and keys; web/verify.js runs the page
under `zig build test` and is where state rules belong — three of them
now live there.

Each field's rationale moves beside the field it explains, which is the
comment class it was in before the struct existed and keeps docscheck's
budget at zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

docs/decisions.md
Old New
@@ -6909,5 +6909,12 @@ nothing in `test/` can see it. `wsclient` stands in for the browser on the wire
6909 but attaches `0 0` and never executes `mux.js`, so every rule that lives in the 6909 but attaches `0 0` and never executes `mux.js`, so every rule that lives in the
6910 page — this one, the `up` re-attach, the badge vocabulary — is invisible to the 6910 page — this one, the `up` re-attach, the badge vocabulary — is invisible to the
6911 e2e suite by construction. Faking a leg with `wsclient` would assert the 6911 e2e suite by construction. Faking a leg with `wsclient` would assert the
6912 fixture's behavior, not the page's. A real-browser pass is the gate for mux.js 6912 fixture's behavior, not the page's.
6913 behavior, and this entry is the reason to keep paying for one. 6913
6914 The gate for those rules is `web/verify.js`, which runs `mux.js` under
6915 `zig build test` (gated on `node`): it constructs a real `Tile`, feeds it wire
6916 bytes, and calls `sendAttach`. State rules belong there — `verifyStatusShell`
6917 pins this latch, the refusal that must keep re-attaching, `setStatus`'s
6918 precedence and both badge words, and each check was watched failing against the
6919 mutation that removes its rule. A real-browser pass stays the gate for what only
6920 a browser has: rendering, input, and the platform APIs the shell talks to.
src/webhub.zig
Old New
@@ -698,10 +698,6 @@ fn drainBrowser(
698 698
699 /// False means the tile is over. Close is idempotent, so a failed 699 /// False means the tile is over. Close is idempotent, so a failed
700 /// re-dial leaves the caller's deferred close with nothing to do. 700 /// re-dial leaves the caller's deferred close with nothing to do.
701 ///
702 /// Takes the Restore because its per-dial half is reset HERE rather than
703 /// at the three call sites: one of them forgot, and a forgotten reset is
704 /// silent until a torn transport turns the next refusal into an ending.
705 fn redial( 701 fn redial(
706 alloc: std.mem.Allocator, 702 alloc: std.mem.Allocator,
707 transport: *client.Transport, 703 transport: *client.Transport,
@@ -711,6 +707,9 @@ fn redial(
711 live: *Liveness, 707 live: *Liveness,
712 restore: *Restore, 708 restore: *Restore,
713 ) bool { 709 ) bool {
710 // The per-dial reset belongs here, not at the three call sites: one of
711 // them forgot, and a forgotten reset stays silent until a torn
712 // transport turns the next refusal into an ending.
714 restore.onRedial(); 713 restore.onRedial();
715 ws.writeMessage(controlMessage(.reconnecting), .binary) catch return false; 714 ws.writeMessage(controlMessage(.reconnecting), .binary) catch return false;
716 transport.close(); 715 transport.close();
@@ -720,37 +719,33 @@ fn redial(
720 } 719 }
721 720
722 /// The restore rule's state, one value rather than three locals so that 721 /// The restore rule's state, one value rather than three locals so that
723 /// `redial` can own the per-dial reset. A dial that skips it inverts the 722 /// `redial` can own the per-dial reset.
724 /// next exit_status: `saw_grid` carried over reads the REFUSAL that
725 /// follows as "the shell exiting", latches `ended`, and disables the
726 /// heal for the rest of the pump's life — silently, since a tear is the
727 /// only way to reach it.
728 ///
729 /// Only `saw_grid` is per-DIAL — it is the browser's own discriminator
730 /// (mux.js, the exit_status case): before any grid an exit_status is the
731 /// daemon refusing the attach, after one it is the shell exiting. The
732 /// other two outlive the connection ON PURPOSE, because every refusal
733 /// closes it: `serviceObserver` answers an unseated attach with
734 /// exit_status and then `dropObserver`, so a flag cleared on `.closed`
735 /// bounds nothing at all.
736 ///
737 /// `ended` is wallview's "exited stays exited" (its pump ENDS on an
738 /// exit_status rather than redialing). Without it a user typing `exit`
739 /// gets a new shell: the daemon reaps the session and closes, the hub
740 /// redials, mux.js re-attaches on `up`, the attach is refused because
741 /// the session is gone — and that is indistinguishable from the restore
742 /// case unless the pump remembers it already watched this session die.
743 ///
744 /// `birth_tried` bounds a daemon that refuses the birth too (a full
745 /// table, a name it will not make) to ONE attempt per healthy period: it
746 /// is cleared by a grid arriving, not by a re-dial, so a second daemon
747 /// restart still heals while a refuse/redial spin cannot fork a process
748 /// per turn.
749 const Restore = struct { 723 const Restore = struct {
724 // The only per-DIAL flag, and the browser's own discriminator (mux.js,
725 // the exit_status case): before any grid an exit_status is the daemon
726 // refusing the attach, after one it is the shell exiting. A dial that
727 // keeps it inverts the next exit_status — the refusal after a tear
728 // reads as the shell exiting, latches `ended`, and disables the heal
729 // for the rest of the pump's life, silently, because a tear is the
730 // only way to reach it.
750 saw_grid: bool = false, 731 saw_grid: bool = false,
732 // Bounds a daemon that refuses the birth too (a full table, a name it
733 // will not make) to ONE attempt per healthy period: cleared by a grid
734 // arriving, not by a re-dial, so a second daemon restart still heals
735 // while a refuse/redial spin cannot fork a process per turn.
751 birth_tried: bool = false, 736 birth_tried: bool = false,
737 // wallview's "exited stays exited" (its pump ENDS on an exit_status
738 // rather than redialing). Without it a user typing `exit` gets a new
739 // shell: the daemon reaps the session and closes, the hub redials,
740 // mux.js re-attaches on `up`, the attach is refused because the
741 // session is gone — indistinguishable from the restore case unless
742 // the pump remembers it already watched this session die.
752 ended: bool = false, 743 ended: bool = false,
753 744
745 // Only `saw_grid` resets. The other two outlive the connection ON
746 // PURPOSE, because every refusal closes it: `serviceObserver` answers
747 // an unseated attach with exit_status and then `dropObserver`, so a
748 // flag cleared on the close bounds nothing at all.
754 fn onRedial(self: *Restore) void { 749 fn onRedial(self: *Restore) void {
755 self.saw_grid = false; 750 self.saw_grid = false;
756 } 751 }
@@ -763,9 +758,9 @@ const Restore = struct {
763 if (t == .exit_status and self.saw_grid) self.ended = true; 758 if (t == .exit_status and self.saw_grid) self.ended = true;
764 } 759 }
765 760
766 /// Read AFTER onFrame: an exit_status this pump has not answered and 761 // Read AFTER onFrame: an exit_status this pump has not answered and
767 /// cannot read as an ending. Whether the target may be recreated at 762 // cannot read as an ending. Whether the target may be recreated at all
768 /// all is the caller's half (`client.hydratedCreates`). 763 // is the caller's half (`client.hydratedCreates`).
769 fn wantsBirth(self: Restore, t: proto.MsgType) bool { 764 fn wantsBirth(self: Restore, t: proto.MsgType) bool {
770 return t == .exit_status and !self.saw_grid and !self.ended and !self.birth_tried; 765 return t == .exit_status and !self.saw_grid and !self.ended and !self.birth_tried;
771 } 766 }