mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* test(mobile): pin the browser pane's golden families The half pin for C6: 4 families, 15 goldens, every verdict the one C2's rule predicts. Measured per family with vitest `-t` over the full 787-golden corpus, with C1's 103 reproduced golden-for-golden as the control: 6 byte-identical, 9 result-absent-settlement. No composed `c6-page-closure.ts`: a composed table is pinned against a route and the browser is a pane, so C7's route is what composes this with C1's. The derivation census does not wait for that route. `mobileWebAppRoute- Closure` becomes one case of `mobileWebAppModuleClosure`, which takes any entries, so the pane's own closure can be read from the module. Two cases: the pane alone reaches exactly the pinned four, and the pane beside `app/h/_layout` adds exactly those four and no other, with the layout reproducing C1's 22 as the control for the difference. Closure at this base: 48 local modules alone, 34 beyond the layout, 30 under `src/browser` and four through the web siblings. The design said 23, all under `src/browser`; it was measured before C6.2 and C6.3 added those siblings, so the pin carries the re-measured number. `browser.screencast` has no golden at all, so this certifies the input path and says nothing about the frame path. Red first: with `browser.wheel` dropped from the table, both census cases fail naming the missing family; restored, the file's 10 cases pass and the parity suite reports "15 goldens in 4 families, 6 byte-identical". Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the frame budget against the shell's real frame Ruling 2's pin. `binaryEventEnvelopeBytes()` sizes the mobile view's device scale from a skeleton it builds itself, and until now its only check was another skeleton of the same shape in the same file: two copies of one assumption agreeing with each other. This measures the real thing. A frame with CDP's nine metadata fields and a real `Page.screencastFrame` timestamp, encoded by C6.1's `encodeBridgeScreencastFrame` and serialized by the real `BridgeHostSubscriptions`, posted through the host harness: 303 bytes besides the image, against a bound of 516. Held above is not enough on its own — 213 bytes of slack is room for the shell to grow the envelope by a field the page never hears about — so the bound is reconstructed exactly instead. Every byte of that slack is a number this frame prints narrower than a double can; adding those back gives 516 on the nose. The budget cases run a generated noise image at the budgeted scale, not a committed fixture: the worst case is the image JPEG compresses least, and a photograph sits a tenth of the way to it. 901,161 px at 0.545 bytes per pixel is 491,132 bytes, which the shell posts at 654,857 of the 655,360-byte cap. One envelope more and the shell drops it, which is ruling 1 read from the budget's side. Red first, two ways. Drop the metadata widening from the bound and three cases fail, the sharpest being the real shell answering the frame the page thought it could send with zero posts. Add a field to the shell's own envelope and the reconstruction fails at 516 against 548, where the existing suite stays green on all 14 — which is the drift this file exists for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): record the measured frame bytes, correctinge7cd24ef10The previous commit message says the shell posts 654,857 bytes for a frame at the budgeted area. That number was not measured; I wrote it from the budget arithmetic instead of reading it off the harness. The measured value is 655,147, which is 213 under the cap rather than 503. Nothing in the assertions changes — they compare against the cap and the bound, never against a literal — but the figure now lives in the file where it was measured rather than only in a message that has it wrong. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): render the browser pane in a page and paint a real frame The only place C6's whole frame path runs. Every other check reads one half: the shell suites drive the host with no page, the page suites drive the hooks with no shell, and the parity pin certifies the input path from a recording. Ruling 4: no route is added. `bundleMobileWebApp` already takes an `appDir`, so this builds a one-route tree of its own and nothing under `mobile/app` moves. The shell double grows a screencast lane to serve it: it accepts a subscribe, posts `event.binary`, and prices each frame the way `BridgeHostSubscriptions` does, so an over-cap frame is dropped where the page can watch the stream survive it. Six cases: the pane subscribes with `wantsBinary` and paints the frame it is handed; a second frame flips the double buffer; an over-cap frame is dropped and the next one paints on the same subscription; the grant withheld produces the update-the-app copy and no subscribe at all; a tap issues one `browser.mouseClick` at the centre of the source viewport; and no request leaves the bundle's own origin. Measured. The frame the pane asks this viewport for is 390x698, which as noise is 201,924 base64 characters. The phone's mobile-mode frame is 780x1424 and encodes to 811,168, which is 124% of the cap and the reason the area budget exists; the over-cap case uses 2400x2160 at 3,761,580, 574% of it. The tap maps to (194, 356) against a 390x712 source, one device pixel off centre because the rendered width is 382.33 CSS pixels for 390 source pixels. One finding, recorded rather than fixed because it is not the pane's. The page files a CSP `script-src` violation on every load, on any route: Zod 4 feature-detects its compiled path with `new Function('')`, the shell's `script-src 'self'` blocks it, Zod catches the throw and takes the interpreted path. The page is correct and the report is filed anyway. One case names it so a second `eval` is visible, and every other case asserts no violation beyond it. Red first, twice, both by reverting behaviour C6.2 landed. Stub out the decode probe in `whenBrowserFrameDisplayable` and the flip case fails on two identical frame digests. Point `updateBrowserImageSource` at the host element instead of the surface child and the paint and flip cases both fail. The first case's comment is corrected by the first of those: it claimed a visible layer proved the decode-then-flip, and the frame still paints with the probe gone, so the flip case is what proves it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): sweep the worst-case JPEG cost instead of taking one point The reviewer is right, and it is worse than the report says. At 0.545 bytes per pixel, 90 of 143 viewports posted a frame over the cap, and 59 of the 111 the budget claims to fit were dropped outright by the real shell — 390x712 at scale 1.8 among them. The old number came from one 2400x2160 frame. A single large frame is the cheapest per pixel in the whole range, so a worst case measured there is not a worst case anywhere else. Swept 143 viewports, widths 320 to 1400 and heights 480 to 1600, each encoded by Chromium at the scale the real budget picks for it. Across the 111 the budget fits, the cost ranges 0.54470 to 0.55351 bytes per pixel. The constant is now 0.56: that maximum plus 0.00649, about 1.2%, for the encoder version it was not swept on. The docstring carries the sweep, the range, the margin and the date. 0.56 is a fixed point, not a guess. Raising the constant shrinks the budget, which lowers the scale, which moves the cost; 0.555, 0.56 and 0.565 all leave the same 31 viewports over the cap, and every one of those sits at the scale floor of 1, where the module already declines to go blurrier and C6 ruling 1's drop rule is the protection. The new test asserts both halves: nothing the budget fits goes over, and the largest viewport it cannot fit is dropped by the real shell. The sweep lives in `config/scripts` because it needs Chromium: the frames are CDP screencast frames, so Chromium's encoder is the oracle and a Node JPEG library would calibrate against the wrong bytes. It drives the real budget, the real scale function and the real `BridgeHostSubscriptions`, and runs in about 4 seconds. Ruling 2's block in `browser-screencast-budget-at-the-shell.test.ts` now says plainly what it measures. It feeds `noise(area * theConstant)`, a byte count the constant itself produced, so it can falsify the expansion and the drop rule but never the constant. It read as if it validated the worst case, and it did not. Red first: put 0.545 back and the sweep fails with 59 viewports, each naming its scale and reporting `null` — the real shell dropping the frame rather than posting it over the cap. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): turn Zod's JIT probe off for the page, before any module The page filed a CSP `script-src` violation on every load: Zod decides whether it may compile by constructing `new Function('')` and reading the throw as "no JIT here", the shell's `script-src 'self'` is exactly that throw, and the browser reports it before Zod catches it. Zod's own source gates the probe on `jitless` for this case. `z.config({ jitless: true })` at the entry does not work, and the reviewer's suggestion of putting it there was measured losing the race. `$ZodObject` reads `allowsEval` when a schema is constructed, not when one is parsed, so the first module-scope `z.object(...)` in the bundle fires the probe — and esbuild evaluates the chunk holding zod and its callers before the chunk holding any module of ours that imports zod. A Function-constructor trap in the page put the call under `new ZodObject` ahead of the entry's first statement. `globalConfig` is `globalThis.__zod_globalConfig`, which zod adopts with `??=` rather than replacing, so the banner can set the flag before any module runs. That is where it now lives, beside the `process` shim and under the same `MOBILE_WEB_APP_SHIMS` contract, which asserts it is applied. Nothing is lost: the compiled path was never reachable in a page under this policy. The render check's `newCsp()` filter is gone. It dropped violations by `blockedURI === 'eval'`, which would have hidden a real one, and every case now asserts zero. The first case walks load and first paint, which is where the second of the two reports fired. The dead `violations` array is deleted. Red first: blank the banner constant and four of the six cases fail, each naming a `blockedUri: 'eval'` the filter used to swallow. Finding, not fixed here and reported instead: the page bundles two copies of zod, mobile's 4.4.3 and the repo root's 4.5.4, because `src/shared/zod-salvage.ts` resolves upward. That is 808 KB of duplicate source. Aliasing `zod` to one copy in the builder fixes it and was measured working, but it changes which zod shared code runs in the shipped page, which is a call to make on its own rather than inside a CSP fix. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the shell double the whole canCarry rule and the acks The double reproduced one arm of `BridgeHostSubscriptions.canCarry`, the message cap, and silently carried anything the other two would have refused: a window already holding its maximum frames, and a window whose bytes the frame would push past the limit. It also ignored the page's `ack` frames, so its window never reopened — which was invisible only because no case streamed far enough to close it. Both arms are in now, and the `ack` arm consumes the page's acks exactly as the host does. The three caps are read out of `bridge-caps.ts` and `bridge-host-subscriptions.ts` rather than retyped, the same way the harness already reads the protocol version and the CSP, so a double carrying a stale number is not possible. The render check's own `640 * 1024` is gone with them. One case for it: thirty frames of about 200 KB, roughly 6 MB through a 4 MiB window, nothing over the message cap, so a drop can only come from the window. Every frame posts, nothing is dropped, and the page's ack seqs are read back to show the window stayed open because the page acked rather than because the double was generous. The file docstring said the double answers no RPC. It serves a screencast stream now, so it says that instead, and says what it still is not: it decides no domain behaviour. The dead `violations` array is gone, folded with the CSP commit. Red first: make the `ack` arm inert, as it was before this commit, and the case fails with `Set{'posted','dropped'}` against `Set{'posted'}`. A first attempt at that mutation left the byte subtraction in place and stayed green, which is the mutation being wrong rather than the case. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * ci(mobile): run the whole mobile-web-app family, not a list that goes stale The `mobile_web_app` job hand-listed ten files. The render check this chain added was not among them, so it would have skipped in CI — and it was not the first: three landed censuses were already unlisted, and their closure blocks only run with `ORCA_MOBILE_WEB_APP_DEPS_REQUIRED=1`, so they are green in the sharded `test` job whether or not they ever ran here. Nobody could see it. The list is now two vitest filename filters, `config/scripts/mobile-web- app-` and the one builder test outside that prefix. Quoted, because vitest matches a positional as a substring against the discovered files rather than expanding a glob: `mobile-web-app-*.test.mjs` finds nothing, and it fails by reporting no test files rather than by running fewer. Both forms were tried before this one was written. It runs 18 files and 205 cases, against 10 files before. With mobile dependencies absent, 111 of those 205 skip, which is the measure of what only this job runs. Per file, cases CI has never run: browser-pane-render 7 of 7 (this chain) source-control-external-links 9 of 9 source-control-keyboard 6 of 6 source-control-text-inputs 6 of 19 (C4.2) frame-budget-sweep 4 of 4 (this chain) route-manifest 2 of 17 Two more files the filter adds run fully in the sharded job already and change nothing here: `browser-pane-text-inputs` (C6.4 — it censuses a hand-written closure and never bundles, so unlike the report it was not skipping) and `external-link-seam`. The sweep is renamed into the family for the same reason. As `mobile-browser-frame-budget-sweep.test.ts` it matched neither the job's filter nor `pr-code-change-scope.mjs`'s `config/scripts/mobile-web-app-` prefix, so a change to it alone would not have run the job that runs it. It is also gated on the dependency check now: it needs no react-native-web, but it launches Chromium, and that flag is what tells the job with a browser from the one without. Unguarded it would have failed the sharded `test` job outright. That filter is a prefix match. `config/scripts/mobile-web-app-` and `mobile/src/` both fire this job, and `.github/workflows/pr.yml` is in GLOBAL_FORCE_PREFIXES, so this commit runs everything. Also: `postedFrame` in the ruling-2 pin and in the sweep both reached the binary lane through `?.`, so a subscribe that opened no stream read as zero posts — indistinguishable from a dropped frame, which is the verdict both files are about. They throw now. The render check's restated `640 * 1024` went with the window caps in c73b405f81; the cap is read from `bridge-caps.ts`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * ci(mobile): record what the mobile-web-app job costs to run The filter that replaced the hand list runs 18 files where the list ran 10, so the step's cost is now a function of what anyone names into the family rather than of what a reviewer remembered to add. Measured on this machine: 25-30s wall for the whole step, of which the frame-budget sweep is 2.5s. The sweep is the one part whose cost is a choice. It encodes 111 noise JPEGs in Chromium, one per viewport the budget fits, so adding rows to that set is a decision about this job's runtime and the comment says so where someone would make it. Found, not fixed, and reported for its own PR rather than folded here: the page bundles two copies of zod, mobile's 4.4.3 and the repo root's 4.5.4, reached through `src/shared/zod-salvage.ts`, which resolves upward while `mobile/src/` resolves to mobile's. That is 808 KB of duplicate source and two module instances in the shipped page. Aliasing `zod` in `mobileWebAppBuildOptions` fixes it and was measured working during this chain; it is reverted and stays reverted, because it changes which zod shared code runs in the page and that is not a call to make inside a CI commit. The CSP fix in71254ab3a9does not depend on it: `globalConfig` lives on `globalThis`, so the banner covers both copies. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): load the frame-budget sweep's mobile modules after the dependency guard vite transforms every file under mobile/ against mobile/tsconfig.json, which extends expo/tsconfig.base.json; the sharded test job installs no mobile dependencies, so the sweep's static imports failed the file at load before describe.skip ran. Type-only imports stay static; the values load in beforeAll behind mobileWebAppDependenciesPresent(). Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): certify the frame budget at the quality the pane ships Round 2: the sweep and the render check encoded fixtures at a retyped 0.72; both now read BROWSER_FRAME_QUALITY (the sweep from the module, the render check through the harness reader), so a quality change fails the certification instead of leaving it green. Every render case now asserts zero CSP violations; the sweep pins the 32 viewports left at scale 1; three references to a renamed file and a file that never existed are corrected; a shim count comment is made count-agnostic. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): assert the frame-budget sweep against the constant, not the measured maximum The margin above the measured 0.55351 is what an encoder drift is allowed to spend; pinning the measurement made a drift inside the margin fail a budget that still held. The number stays in the docstring as the sweep's record. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): move the sweep's measured-maximum note beside the assertion it explains Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb