mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
cc9b258c8d6e2ac5ec0664c90ed203cbf753eef7
11368
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cc9b258c8d | test(editor): select rich copy ranges through Playwright | ||
|
|
ebbf1d11a2 | test(editor): skip source-mode HTML validation probes | ||
|
|
cb0e9336fc | perf(editor): avoid HTML round trips in source mode | ||
|
|
4d30ed2c29 | fix(editor): match parser fence whitespace | ||
|
|
482b03cc45 | fix(editor): match fence suffix whitespace rules | ||
|
|
8157e9e148 | Merge branch 'nwparker/markdown-better' into nwparker/markdown-fallback-preview | ||
|
|
653fcafabf | Merge branch 'nwparker/markdown-fence-consumers' into nwparker/markdown-better | ||
|
|
667ff6564f | perf(editor): scan fence lines without substring allocations | ||
|
|
bcf4aded0d | fix(editor): include fallback preview label in its PR layer | ||
|
|
60ebab837e | Merge branch 'nwparker/markdown-better' into nwparker/markdown-fallback-preview | ||
|
|
f154eb43ca | fix(editor): preserve copied blocks and literal Markdown attributes | ||
|
|
39fe876231 | Merge branch 'nwparker/markdown-better' into nwparker/markdown-fallback-preview | ||
|
|
8b33fe853c | fix(editor): retain CR line endings during save recovery | ||
|
|
fa15a05c67 | feat(editor): open fallback preview from unsaved Markdown drafts | ||
|
|
e238aa0e72 | Merge branch 'nwparker/markdown-fence-consumers' into nwparker/markdown-better | ||
|
|
f2b8892ff4 | test(editor): assert highlighted lines across Monaco token spans | ||
|
|
92ef32a480 | fix(editor): share fence boundaries for Markdown links | ||
|
|
ab51dbef20 | fix(editor): preserve escaped link labels through repeated edits | ||
|
|
030a1e0c77 |
fix(relay): stop holding a cell row across the whole control accept (#21563)
* fix(relay): stop holding a cell row across the whole control accept The cell accept path took the host's cell row FOR UPDATE at its first supersession statement and held it to COMMIT across a dozen round trips, which capped a cell far from Postgres at a couple of accepts a second. Fold every cell-row change on the path into one conditional delta write issued last, so the contended row is held only across the commit. * fix(relay): give relay_cells one global row lock order, taken last Moving the accept's cell-row write to the end of its transaction put it after the host's relay_control_connection_reservations rows, while every director path that reads the inventory took those rows the other way round. Pin one order for both roles -- host rows, then the shared cell row -- by locking the host's reservation rows before the inventory in the nine director paths that take both, document the tiers next to CellInventoryLockMode, and add a census that fails on a new path taking relay_cells first. |
||
|
|
73c49416b7 | Merge remote-tracking branch 'origin/main' into nwparker/markdown-better | ||
|
|
545f526c31 |
refactor(editor): land shared Markdown scanners on main (#21565)
* refactor(editor): introduce shared Markdown code scanners (#21554) * refactor(editor): add shared Markdown scanners * test(editor): verify standalone scanner boundaries * fix(editor): preserve line endings and fenced code boundaries * fix(editor): keep bare dash lines out of table scanning |
||
|
|
bbfbfcaee3 | test(editor): verify formatted selection copy in Electron | ||
|
|
e8fe75bb45 | Merge branch 'nwparker/markdown-scanners-main' into nwparker/markdown-better | ||
|
|
f52b5105b9 |
fix(editor): serialize copied rich selections as Markdown (#21529)
* test(editor): cover bounded table output * fix(editor): preserve markdown on clipboard copy |
||
|
|
695f4c407d | fix(editor): keep bare dash lines out of table scanning | ||
|
|
2038376d8e |
fix(terminal): a park must not discard the only copy of a remote pane's scrollback (#21285)
* fix(terminal): keep a client copy of a parked remote pane's scrollback
A remote-runtime pty's bytes never transit the client's main process, so the pane's
xterm buffer is the only client-side copy. The ordinary cold-park unmounted that pane
without capturing it, licensed by TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY — a static
build string that says nothing about whether the host retained this pty's buffer. On
reveal, a host that answers 'no-serializable-buffer' (or stays silent past the request
timeout) collapses to a null snapshot and the pane paints blank: tabs and splits survive,
the scrollback is gone.
Capture before every park, not only the retention-budget force-park, so the reveal has a
copy to replay when the host cannot answer. An unverifiable host answer is not proof the
pane was empty; keep the buffer, never discard it.
Adds ORCA_E2E_FORCE_REMOTE_TERMINAL_SNAPSHOT_UNAVAILABLE so an e2e can reproduce the
host-retains-nothing state, mirroring the existing forced-truncation lever.
* test(terminal): prove a parked remote pane survives a host that answers nothing
The oracle is a token the test types into the terminal before the park and the fixture
echoes back. Nothing replays stdin, so a respawned command cannot reproduce that line —
only the pre-park buffer can. An earlier argv marker passed vacuously for exactly that
reason.
The control ('host retains the buffer') is insensitive to the fix and fails if the harness
never parks, never reveals, or never echoed the token, so the regression case cannot be
green for a harness reason.
* refactor(terminal): validate the paired host terminal RPC shape instead of casting it
The merge-commit consistent-type-assertions gate flags every new `as`. Two were fixture
shapes that a type annotation states directly, and the third hid an unchecked RPC payload —
readCreatedTerminalTab now fails with the shape named rather than surfacing later as an
undefined surface id.
* fix(terminal): let a park capture survive an unhydrated repo catalog
Reading state.repos unguarded threw out of the cold-park effect whenever the catalog was
absent, which would break parking itself. Capture is best-effort evidence; an empty catalog
also fails open in shouldPreserveTerminalScrollbackBuffers, the safe direction for a park.
* docs(terminal): pin why the two unhydrated-catalog fallbacks point opposite ways
shouldPreserveTerminalScrollbackBuffers fails open toward 'remote' because a worktree wrongly
judged local parks with no copy at all. worktree-runtime-owner.ts resolves the same unhydrated
catalog to 'local', which is safe there and would be data loss here. A reader pattern-matching
'fail open' across the two gets one of them backwards.
* fix(terminal): keep a parked pane's scrollback across a reconnect merge
The direct-SSH pull replaces a replaced tab's layout wholesale, and a park capture does not
bump tab.generation — so a just-parked tab is not in locallyPreservedTabIds and the only
client-side copy of its remote scrollback went with the layout it replaced. That is the same
data loss this branch already fixes, one layer down, and it is the layer that decides whether
the fix survives the app update the user actually performed.
Carry the client's leaf-keyed scrollback into the host's layout, filtered to the host's own
root leaves. Structure stays the host's verbatim, so a split it added while we were away still
wins and a leaf it retired still drops its bytes. Local wins a conflict: neither copy is then
the only one, but remote-wins would overwrite the tail captured since the last upload and
propagate that backwards on the next replace-session patch.
Not a generation bump: the pane key is `${tab.id}-${tab.generation}`, so bumping would remount
the pane and destroy the very buffer the capture just serialized, lift the recovery-storm
ledger ceiling, and let a stale local ptyId win through preserveNewerLocalTerminalFields.
* fix(terminal): carry a parked pane's scrollback through the mirrored-layout rebuild
Found in review of this PR by rc-ssh-remoting. chooseRemoteTerminalLayout rebuilds a
mirrored tab's layout from the host's picture and never carried buffersByLeafId or
scrollbackRefsByLeafId forward, though it already receives existingLayout. The host
publishes no scrollback of its own, so ANY session-inventory frame landing between park and
reveal dropped the only client-side copy: the rebuild is bufferless, terminalLayoutEqual
compares buffers so the write is not bailed out, and apply-terminal-records assigns it
wholesale.
Measured before the fix: 336 bytes captured at park, 0 after one forced frame, blank pane on
reveal. After: 411 bytes survive the frame and the reveal repaints.
The e2e passed either way because no frame happened to land in its window, so it was not
covering the destroying event. It now forces one inside the park -> reveal window and asserts
the capture survives it.
An identical fix was written and reverted earlier in this branch as 'no measurable effect' —
that measurement ran on a harness deleting the client profile between launches, so nothing
downstream of persistence could register. It was never actually tested.
* feat(session): add a local-only home for ordinary-park scrollback
localOnlyScrollbackByTabId is a top-level session field, tabId -> leafId -> buffer, that never
rides the remote projection: exportRemoteWorkspaceSession is an explicit allowlist of named
top-level fields, so a new one is omitted for free, whereas anything added to
TerminalLayoutSnapshot is copied whole. It is also outside the two records the mirrored-tab apply
rewrites, so a host inventory frame cannot wipe it.
Registered in every exhaustive session registry ('tabKeyed'), hydrated and scoped like the layout
map, dropped with its tab on close/removal/purge/repo removal/mirrored retirement, copied on profile
transfer, emitted by the incremental patch builder, and capped by pruneLocalTerminalScrollbackBuffers
alongside the shared home — with a per-home test so an uncapped path cannot go unnoticed.
Known ceiling, not widened here: the field routes through the partition router that falls back to
'local' when the repo catalog is unknown at write time (#21295).
* fix(terminal): keep ordinary-park scrollback off the upload, and read both homes through one resolver
The ordinary cold park fires on every workspace hide. Its capture now splits: structure (root,
ptyIds, titles) stays in the shared layout, bytes go to localOnlyScrollbackByTabId. Force-park,
hibernate, sleep and shutdown keep writing buffersByLeafId, because that copy is what a second
desktop cold-restores from; a shared capture clears the local copy so the two homes never hold two
versions of one leaf.
resolveLeafScrollbackBuffers is the only read across the two homes (local wins a conflict: it is
the later write by construction). restoreTerminalPaneLayout no longer reads buffersByLeafId
directly, the capture's merge prior comes from the resolver, and the post-replay release covers
both homes.
Measured with the projection at 20 tabs x 2 panes at the per-leaf cap: the shared-layout shape
exports ~22 MiB per replace-session; the local-only shape exports the bufferless baseline.
* test(sync): pin that the mirrored rebuild carries the client scrollback refs
The carry-through added in
|
||
|
|
5bd59968e4 | test(editor): use parser-compatible fence indentation | ||
|
|
6219b4f1d1 | Merge branch 'nwparker/markdown-scanners-main' into nwparker/markdown-better | ||
|
|
23aab087f3 | fix(editor): preserve line endings and fenced code boundaries | ||
|
|
923cc06d3b | Merge branch 'nwparker/markdown-scanners-main' into nwparker/markdown-better | ||
|
|
c6f12e0ee6 |
refactor(editor): introduce shared Markdown code scanners (#21554)
* refactor(editor): add shared Markdown scanners * test(editor): verify standalone scanner boundaries |
||
|
|
3355d9f127 | test(editor): verify save and reopen preserve Markdown source bytes | ||
|
|
e2afb5eef9 |
feat(mobile): the page reads this host and keeps the app's pins (OTA phase C, C1.4) (#21503)
* feat(mobile): the page mounts on the shell's init, with the client injected (OTA phase C, C1.1) The Route A entry built no client and mounted the route tree immediately, so the web provider minted its own: it read the page channel, built `BridgeRpcClient` and fell back to a placeholder that rejected every call. A tree that mounts before `init` reads synchronous getters against a client that knows no host, no state and no build, and the first render it records is the wrong one. The entry now owns the page's one client. It builds it from the channel at module scope, mounts nothing until `onReady` fires, and stamps the session and build ids `getShellSession()` returns on the document beside the mount state, so a screenshot, the render check and a device console read the same three facts. `client-context.web.tsx` takes that client by injection and serves it from `acquire()` for every hostId, because the bridge protocol names no host; the placeholder and its `BridgeTransportUnavailableError` are gone, along with the entry that pointed at them in the unvalidated-port inventory. A document with no channel is not inside the shell, so it says `unbridged` and stops rather than waiting out a backoff nobody answers. The render check gains a shell double that answers `ready` with `init`, reads the stamped session back off the document, and proves the gate is real by opening the same route with no double and finding an empty `#root`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the shell names the screen, and the page routes to it (OTA phase C, C1.2) The shell serves its document at `/` and refuses every other path, so the page's own location matches no route in the tree it carries and expo-router paints Unmatched. Nothing in the document can tell it otherwise, so the screen has to cross the bridge. `init` gains an optional `route: { pathname, params }`. The pathname is held to what a path may be rather than to what a screen may want: rooted, single-slash, no query and no fragment. A protocol-relative `//host` would make `history.replaceState` throw a cross-origin SecurityError and take the mount down with it, and the params are a field of their own so neither side parses a URL. The shell route supplies it, the screen passes it to B4's hook, and the hook holds it for the life of one host: the page routes once, before its first render, so a route that changed afterwards has nothing left to change. The page writes that URL into its history and then mounts. It also hands the same URL to `ExpoRoot` as its `location`, because `ExpoRoot` snapshots `window.location.href` when its module is imported, which is before any frame has crossed the bridge: without it the router reads the `/` the shell served and replaces the page's own path right back. A shell too old to name a route leaves the page with nothing to open, so it paints a panel saying to update the app, built as elements outside React because the route tree is exactly what cannot mount there. Both platforms stop reading the document's URL to decide a load finished. The page rewrites its own path before its first render, so a document that committed at `/` reports finishing at `/h/<hostId>`; reading the path withheld `ready` forever and left the Android WebView hidden behind it. What is left is whether the load committed, which is the question the state machine already answers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the desktop lists a page route, the shell honours it or stays native (OTA phase C, C1.3) The worktree list now renders from the desktop's bundle, and which routes do is negotiated rather than decided on one side. The manifest gains `routes: [{ pathname, grants }]`, written from one declared list the builder checks against the tree it bundled, so a declaration naming a screen with no module fails the build instead of reaching a phone as a page that paints Unmatched. The field is additive because the phone reads the manifest loosely and pins no schema version; the desktop's own writer stays `.strict()`, and the stale comment saying there was no additive path is corrected. The shell answers for what it can do. A route the bundle does not list, or lists needing a grant this app does not implement, settles as `native-route` and downloads nothing; so does a desktop that ships no bundle at all, which is the one blocked verdict that is not a wall, because a desktop with no bundle declares no page route and there is no workspace to refuse. The route is answered before the compat verdict for the same reason: a bundle this shell cannot open is not a reason to refuse a screen it was never going to open. `app/h/[hostId]/index.tsx` mounts the shell when the flag is on and takes the native list back as the fallback, and both routes read the flag through one hook so the census stays the whole census. A tap on a worktree row still opens the native session screen. The page posts `notify { name: 'navigate', href }` behind the `navigate` grant, which is not a convention: `notify` is a closed union, so an older shell refuses the whole frame and the page checks the grant before it posts. The shell pushes the target over the still-mounted view, so Back reveals the page with nothing reloaded. `route-handoff.ts` and its web sibling are the seam, router-shaped so the list's own hook and the recorder's adapter are untouched and no golden moves: the web file wraps the three members that leave the document and hands back any target outside the page routes `init` named. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the page reads this host and keeps the app's pins (OTA phase C, C1.4) Three gaps the census named, and the last of them is why the page needed a second grant. `expo-secure-store` is `{}` on web, so the page's own `loadHosts()` answered with an empty array and the list painted "Host not found" over the host the shell had just opened it for. `init` gains an optional `host`, and `host-store.web.ts` serves it: the profile the screens read, with no device token and no public key, because the bridge already carries the connection those would have opened. The native writes it cannot make — pairing, renaming, recency — settle rather than throw, since recency orders a list the page never shows. AsyncStorage's web build is `window.localStorage`, and the page has none worth having: Android turns DOM storage off and on iOS the origin host is the session id, so a pin set in the page was gone on the next remount. The builder aliases the module to a page store whose values are the app's own — `init` primes the allowlisted keys, a write is applied locally and posted over a new `storage` grant, and the app is where it lands. The allowlist is two keys and is the whole fence: everything the app stores shares one namespace, the hybrid shell flag included, so a page that could write any of it could turn the feature on for a build that never offered it. A key outside the list is refused and, crucially, not kept locally either — a pin that looks set and is not is the failure the grant exists to avoid. The bridge host is built only once both have been read, because `init` is answered once per `ready` and carries them: a host that started without them would have to be torn down to carry them, and the list would already have mounted against a host it could not name. `Alert.alert` on a failed host removal is a silent no-op in React Native Web, so inside the page that failure had no surface at all. It routes to the error the list already shows, on both platforms. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the page can tell the shell it faulted (OTA phase C, C1.1) A page that throws where it renders has nowhere to report it: the shell sees a document that loaded and a view that never painted, so it waits on a blank page forever. This adds the one frame that says so. `notify { name: 'fault' }` carries the capture an `error` frame already carries, so both directions share one bound and one reader. It rides a grant because `notify` is a closed list on both sides: a page served by a newer desktop into an older shell would have the whole frame refused, so the page asks `init.grants.native` first and stays quiet on a no. The shell answers it as `document-load-failed`, which is what happened. That reason drops the generation and downloads once, so a page broken by bytes this host has since replaced recovers, and one broken by its own code stops at the failure screen rather than a blank one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): give the bridge's notifications and the host's errors their own modules The fault report took both files over the 300-line cap, so each gives up the group that was already separable. The page's one-way members move to `bridge-client-notifications.ts`, which is also where the two policies that split them can be stated: the two the native contract declares throw before a session, and the fault report never throws at all. The host's three error classes move to `bridge-host-errors.ts`, the mirror of the page's own `bridge-client-errors.ts`. No behaviour changes. The commit before this one is over the cap on its own, which a forward-only history is the reason to say rather than hide. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): one boundary under the page's root, reporting to the shell (OTA phase C, C1.1) Nothing in `app/h/**` exports an `ErrorBoundary` and `ExpoRoot` provides no global one, so a throw while a route renders — or a route module that rejects once the manifest is lazy — unmounts the tree and leaves a blank document. The shell sees a load that finished and waits on it forever. The entry now wraps what it mounts on `init` in one boundary that posts the throw over the bridge. Above `ExpoRoot`, not inside its wrapper: a route that cannot be resolved throws where the router renders it, and a boundary below the router never sees that. It renders nothing and offers nothing to press. The generation is on disk and was hash-checked before the view loaded it, so the same bytes throw again and a retry here would only throw twice; recovery belongs to the shell, which drops the generation on the report. The render check now grants the fault and collects what the page posts into the errors every case already asserts empty, because a throw the boundary caught paints nothing and logs nothing a `pageerror` listener would hear. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the page-fault callback ref after the commit, not during render React may replay or discard a render, so the write belongs in the commit phase. Layout, not passive, and declared above the host's effect: a native frame can arrive between a commit and a passive effect, and the host must already hold this render's callback. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the route ref after the commit, not during render Same class as the page-fault ref: render must stay pure because React can replay or discard it. Folded into the one commit-phase effect above the host's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the page-route and navigate refs after the commit Same class again: the last two writes this branch adds join the commit-phase effect, so nothing this hook holds is written while React is rendering. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the storage-write ref after the commit The last render-phase ref write in this hook joins the commit-phase effect. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): name the confirm setter this callback already calls in its deps A `useState` setter is stable, so the identity of the callback is unchanged; the list now says what the body reads. Reported on the line this branch rewrote. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): take the boundary test to C0.5's fake-client pair `createBridgePortPair` is generic over the shell client now; the fake-client form this test wants is `createFakeBridgePortPair`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): bound the wait for a page that never says a word (OTA phase C, C1.1) A route module that throws while the bundle is evaluated takes the entry with it. The document still commits and the WebView still reports it loaded, but no boundary mounts, no fault is posted and no frame is ever sent, so the session sat in `ready` behind a blank view forever. The native view's finished load starts a clock; the page's first `ready` stops it; expiry is `document-load-failed`, which deletes the generation and fetches once. Nothing cancels the timer — a `ready` that lands first makes the expiry a no-op — so the runner owns a clock and the reducer owns every decision. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(config): make a route chunk throw, so the render check proves the boundary reports The check folded page faults into its errors but nothing ever produced one, so a boundary that stopped reporting would have stayed green. The server now serves one real route chunk with a throw in front of it: the module still links, so the failure is an evaluation throw where the router renders, which is exactly what the boundary is for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make the host enforce the grants it issued, and hear nothing before ready `forwardNotify` acted on any frame that parsed, including a `fault` from a page that had never asked for a session and therefore held no grant. Both refusals now go through one rule the host shares with the frame it sends, so the list a page is told about and the list it will be served cannot drift. Inert while every page is offered `fault`; the ungranted arm is what C1.3 needs the moment a grant belongs to a route rather than to the protocol. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refuse a route no page can open, rather than blanking the WebView (OTA phase C, C1.2) `sendInit` put `options.route` straight on the wire and only the page's decoder checked it, so an out-of-contract pathname made the page refuse the whole `init`, ask again on its 2 s backoff forever, and the shell un-hide a view that would never paint. The only trace was a `console.warn` inside the WebView. Three changes, one failure mode. The host parses the route at construction and serves no session at all when it will not do, reporting it as a shell failure. The pathname rule refuses empty segments, dot segments and backslashes anywhere, because `replaceState` normalises `/../../etc` to `/etc` and `/h/a\b` to `/h/a/b` and the page then renders whatever came out. And the producer encodes the host id it interpolates, which is how one carrying a query, a fragment or whitespace got there. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make a handoff mean the shell took it, not that a frame left (OTA phase C, C1.3) `handOff` returned `client.notifyNavigate(href)`, which answers whether the frame left the page and never whether the shell accepted it. Two hrefs the app builds today were posted, answered true and suppressed the local fallback, so the tap did nothing at all: the Connection-log link's object form, which `String` turns into `[object Object]`, and any href carrying a fragment, because the pathname is stripped to match and the whole href is what goes on the wire. Object hrefs now resolve the way the router resolves them, and the string is checked against the envelope's own pattern and cap before it is posted; anything that fails falls through to the local router, which is the policy this module already states. Whether a target names a screen that exists is shape's business no longer, and the comment says C1.7 owns it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep a failed action off the whole screen and the page's store honest (OTA phase C, C1.4) Five, from one round of review. A removal that fails no longer writes the identity error: that one is an early return over the header, the list and the overlays, with nothing to dismiss it and nothing left to render the confirm it re-opens. It goes to a dismissible line above the list instead, on both platforms, cleared by the next confirmed refresh. `init` reads the allowlisted keys on every answer rather than capturing them at mount, so a document that reloads inside one mount is primed from after its own writes. The read stays synchronous: the page refuses every member until `init` lands and the golden recorder mounts a screen in the same turn it drains one, so a promise here moves the first render of every bridged replay. A profile read that rejects is now a shell failure with a diagnostic instead of a `ready` session with no host behind it and a page asking forever. The page bounds a value by the envelope's own constant rather than caching what the wire drops. And a write is held to the keys this page was handed, so one host's page cannot rewrite another's pinned list. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): state the two test fixtures' types instead of asserting them The casting gate reads a `SAFETY:` rationale off the line directly above the assertion, and a wrapped comment puts a comment there instead. Two of the four were not assertions worth keeping at all: a hoisted fixture says its own type, and the router comes from the mock the file already installs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): assert the two fixtures in place, not through a widened binding `const x: unknown = …` followed by `x as T` is the widen-then-assert the anti-slop gate refuses, and rightly: the evidence is discarded and then invented again. The assertion belongs at the literal, with its rationale on the line above it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): start a new flow when the shell view remounts A remount cleared `pageReady` but left the flow alone, so the wait the retired document armed still matched. It expired onto the page that replaced it, took a ready workspace to `document-load-failed`, and deleted the generation on the way. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say which page notification the bridge refused and why A refused `notify` fell through to the line about a view outliving its host, which is a different fault and names neither the notification nor the reason. The two refusals now get a line each, so a page that was told nothing cannot bury one reaching past what it was told. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the ready deadline to the page's own retry ceiling The margin was stated in a comment and asserted against itself, so changing either number left the suite green. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say what was wrong with the screen a refused shell named C1.1's per-kind log lands on a branch that also refuses a route, and that diagnostic was still falling through to the line about a view outliving its host. It names the shell's own bug now, and carries the issue. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refuse a dot segment however the route spells it A URL parser percent-decodes a path before it resolves it, so `/h/%2e%2e/x` climbed out of the `/h/` prefix exactly as `/h/../x` does and landed the page on a screen nobody asked for, with no refusal anywhere. The one segment rule both patterns share now reads the encoded spellings as the dot segments they are, and still lets an escape inside a name through. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): name routes in the manifest field list the builder emits C1.3 added `routes` to every manifest this builder writes, and the Phase A contract test still listed eight keys, which is what went red in CI. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): hold a navigate target to the same segment rule as the shell's The href pattern is built from the segment source C1.2 tightened, and nothing said so: a spelling one pattern refused while the other took it would be a hole with a `notify` already pointed at it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): name the key a refused page write reached for The last diagnostic still falling through to the line about a view outliving its host, on the branch that added it. The key is the evidence: it says which host's pinned list the page was reaching into. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the ref-refresh probe the navigations this branch added C1.1's new case builds its own probe, and on this branch a probe also collects the hrefs the page hands back. The file stopped typechecking on the merge, which the tests ratchet caught. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the ref-refresh probe this branch's storage writes too Same merge, one branch further: a probe here also collects what the page asked the screen to write. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): format the web shell route entry Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): hand the page the app's storage as it stands, not one init late The page is handed its allowlisted keys on every `init`, built synchronously, and the app writes two of them from its own native screens. The shell's map was only re-read after a ready, so a native write between two readies reached the init after next: the drawer opened on the repo the user left. The map is now module-scoped and every writer of an allowlisted key notes it as it writes, so the init that answers a ready already carries it. The store read only seats the map, and a read that started before a write no longer puts the older value back. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): find the banner's dismiss without an assertion Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): keep the storage mirror in the storage tree The app's own writers had to reach into `src/mobile-web-shell/` to note a write, which is the dependency the wrong way round: the shell is what is built on the app's storage, not the other way. The mirror moves to `src/storage/` and no longer knows which keys the page is allowed; the caller names them on every read and every seat, so the allowlist stays where it is enforced. No behaviour change. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): split the bridge frame suite along the modules the merge created `bridge-rpc-client-frames.test.ts` reached 835 counted lines once C0.8 and C1.1 both added cases to it, over the 800 the lint allows. The split follows the two modules those changes extracted, so each suite now names the module it covers. `bridge client page faults` moves to `bridge-client-notifications.test.ts` (the outbound notify surface) and `bridge client refusals and send failures` to `bridge-client-inbound-frames.test.ts` (the reader, including the refused-event release that cancels at the shell). The seven suites that exercise the client as a whole stay put. The fake port all three drive moves to `bridge-page-client-test-harness.ts` rather than being copied three times. No case changed and none was dropped: 48 `it` cases before, 37 + 4 + 7 after, and all nine `describe` bodies compare byte-identical to their originals. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the shared init fixture as the member a case reads The harness exported `INIT` as `BridgeHostMessage`. While it was a module-local const, control flow narrowed it to the `init` member at each use, so `INIT.grants` read fine. An imported binding keeps its declared type instead, so the same read lost `grants` to the union and the tests ratchet went red. Declared as the init member, which is what every case already treats it as. No cast: the object literal is checked against the narrower type directly. `INIT` was the only exported fixture with this shape. `CONNECTION` is `as const`, `GRANTS` is inferred, and nothing reads a member off an `eventFrame` result. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
56e5cd5e34 | fix(editor): render CJK-adjacent emphasis in Markdown preview | ||
|
|
d8cfe374a3 |
refactor(usage): share attribution and scope filtering
Readiness checklist review: PASS. No proven release-blocking findings. Codex and usage tests: 116 passed; all required CI checks green. |
||
|
|
ac4dc6599b |
feat(mobile): the desktop lists a page route, the shell honours it or stays native (OTA phase C, C1.3) (#21502)
* feat(mobile): the page mounts on the shell's init, with the client injected (OTA phase C, C1.1) The Route A entry built no client and mounted the route tree immediately, so the web provider minted its own: it read the page channel, built `BridgeRpcClient` and fell back to a placeholder that rejected every call. A tree that mounts before `init` reads synchronous getters against a client that knows no host, no state and no build, and the first render it records is the wrong one. The entry now owns the page's one client. It builds it from the channel at module scope, mounts nothing until `onReady` fires, and stamps the session and build ids `getShellSession()` returns on the document beside the mount state, so a screenshot, the render check and a device console read the same three facts. `client-context.web.tsx` takes that client by injection and serves it from `acquire()` for every hostId, because the bridge protocol names no host; the placeholder and its `BridgeTransportUnavailableError` are gone, along with the entry that pointed at them in the unvalidated-port inventory. A document with no channel is not inside the shell, so it says `unbridged` and stops rather than waiting out a backoff nobody answers. The render check gains a shell double that answers `ready` with `init`, reads the stamped session back off the document, and proves the gate is real by opening the same route with no double and finding an empty `#root`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the shell names the screen, and the page routes to it (OTA phase C, C1.2) The shell serves its document at `/` and refuses every other path, so the page's own location matches no route in the tree it carries and expo-router paints Unmatched. Nothing in the document can tell it otherwise, so the screen has to cross the bridge. `init` gains an optional `route: { pathname, params }`. The pathname is held to what a path may be rather than to what a screen may want: rooted, single-slash, no query and no fragment. A protocol-relative `//host` would make `history.replaceState` throw a cross-origin SecurityError and take the mount down with it, and the params are a field of their own so neither side parses a URL. The shell route supplies it, the screen passes it to B4's hook, and the hook holds it for the life of one host: the page routes once, before its first render, so a route that changed afterwards has nothing left to change. The page writes that URL into its history and then mounts. It also hands the same URL to `ExpoRoot` as its `location`, because `ExpoRoot` snapshots `window.location.href` when its module is imported, which is before any frame has crossed the bridge: without it the router reads the `/` the shell served and replaces the page's own path right back. A shell too old to name a route leaves the page with nothing to open, so it paints a panel saying to update the app, built as elements outside React because the route tree is exactly what cannot mount there. Both platforms stop reading the document's URL to decide a load finished. The page rewrites its own path before its first render, so a document that committed at `/` reports finishing at `/h/<hostId>`; reading the path withheld `ready` forever and left the Android WebView hidden behind it. What is left is whether the load committed, which is the question the state machine already answers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the desktop lists a page route, the shell honours it or stays native (OTA phase C, C1.3) The worktree list now renders from the desktop's bundle, and which routes do is negotiated rather than decided on one side. The manifest gains `routes: [{ pathname, grants }]`, written from one declared list the builder checks against the tree it bundled, so a declaration naming a screen with no module fails the build instead of reaching a phone as a page that paints Unmatched. The field is additive because the phone reads the manifest loosely and pins no schema version; the desktop's own writer stays `.strict()`, and the stale comment saying there was no additive path is corrected. The shell answers for what it can do. A route the bundle does not list, or lists needing a grant this app does not implement, settles as `native-route` and downloads nothing; so does a desktop that ships no bundle at all, which is the one blocked verdict that is not a wall, because a desktop with no bundle declares no page route and there is no workspace to refuse. The route is answered before the compat verdict for the same reason: a bundle this shell cannot open is not a reason to refuse a screen it was never going to open. `app/h/[hostId]/index.tsx` mounts the shell when the flag is on and takes the native list back as the fallback, and both routes read the flag through one hook so the census stays the whole census. A tap on a worktree row still opens the native session screen. The page posts `notify { name: 'navigate', href }` behind the `navigate` grant, which is not a convention: `notify` is a closed union, so an older shell refuses the whole frame and the page checks the grant before it posts. The shell pushes the target over the still-mounted view, so Back reveals the page with nothing reloaded. `route-handoff.ts` and its web sibling are the seam, router-shaped so the list's own hook and the recorder's adapter are untouched and no golden moves: the web file wraps the three members that leave the document and hands back any target outside the page routes `init` named. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the page can tell the shell it faulted (OTA phase C, C1.1) A page that throws where it renders has nowhere to report it: the shell sees a document that loaded and a view that never painted, so it waits on a blank page forever. This adds the one frame that says so. `notify { name: 'fault' }` carries the capture an `error` frame already carries, so both directions share one bound and one reader. It rides a grant because `notify` is a closed list on both sides: a page served by a newer desktop into an older shell would have the whole frame refused, so the page asks `init.grants.native` first and stays quiet on a no. The shell answers it as `document-load-failed`, which is what happened. That reason drops the generation and downloads once, so a page broken by bytes this host has since replaced recovers, and one broken by its own code stops at the failure screen rather than a blank one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): give the bridge's notifications and the host's errors their own modules The fault report took both files over the 300-line cap, so each gives up the group that was already separable. The page's one-way members move to `bridge-client-notifications.ts`, which is also where the two policies that split them can be stated: the two the native contract declares throw before a session, and the fault report never throws at all. The host's three error classes move to `bridge-host-errors.ts`, the mirror of the page's own `bridge-client-errors.ts`. No behaviour changes. The commit before this one is over the cap on its own, which a forward-only history is the reason to say rather than hide. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): one boundary under the page's root, reporting to the shell (OTA phase C, C1.1) Nothing in `app/h/**` exports an `ErrorBoundary` and `ExpoRoot` provides no global one, so a throw while a route renders — or a route module that rejects once the manifest is lazy — unmounts the tree and leaves a blank document. The shell sees a load that finished and waits on it forever. The entry now wraps what it mounts on `init` in one boundary that posts the throw over the bridge. Above `ExpoRoot`, not inside its wrapper: a route that cannot be resolved throws where the router renders it, and a boundary below the router never sees that. It renders nothing and offers nothing to press. The generation is on disk and was hash-checked before the view loaded it, so the same bytes throw again and a retry here would only throw twice; recovery belongs to the shell, which drops the generation on the report. The render check now grants the fault and collects what the page posts into the errors every case already asserts empty, because a throw the boundary caught paints nothing and logs nothing a `pageerror` listener would hear. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the page-fault callback ref after the commit, not during render React may replay or discard a render, so the write belongs in the commit phase. Layout, not passive, and declared above the host's effect: a native frame can arrive between a commit and a passive effect, and the host must already hold this render's callback. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the route ref after the commit, not during render Same class as the page-fault ref: render must stay pure because React can replay or discard it. Folded into the one commit-phase effect above the host's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the page-route and navigate refs after the commit Same class again: the last two writes this branch adds join the commit-phase effect, so nothing this hook holds is written while React is rendering. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): take the boundary test to C0.5's fake-client pair `createBridgePortPair` is generic over the shell client now; the fake-client form this test wants is `createFakeBridgePortPair`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): bound the wait for a page that never says a word (OTA phase C, C1.1) A route module that throws while the bundle is evaluated takes the entry with it. The document still commits and the WebView still reports it loaded, but no boundary mounts, no fault is posted and no frame is ever sent, so the session sat in `ready` behind a blank view forever. The native view's finished load starts a clock; the page's first `ready` stops it; expiry is `document-load-failed`, which deletes the generation and fetches once. Nothing cancels the timer — a `ready` that lands first makes the expiry a no-op — so the runner owns a clock and the reducer owns every decision. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(config): make a route chunk throw, so the render check proves the boundary reports The check folded page faults into its errors but nothing ever produced one, so a boundary that stopped reporting would have stayed green. The server now serves one real route chunk with a throw in front of it: the module still links, so the failure is an evaluation throw where the router renders, which is exactly what the boundary is for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make the host enforce the grants it issued, and hear nothing before ready `forwardNotify` acted on any frame that parsed, including a `fault` from a page that had never asked for a session and therefore held no grant. Both refusals now go through one rule the host shares with the frame it sends, so the list a page is told about and the list it will be served cannot drift. Inert while every page is offered `fault`; the ungranted arm is what C1.3 needs the moment a grant belongs to a route rather than to the protocol. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refuse a route no page can open, rather than blanking the WebView (OTA phase C, C1.2) `sendInit` put `options.route` straight on the wire and only the page's decoder checked it, so an out-of-contract pathname made the page refuse the whole `init`, ask again on its 2 s backoff forever, and the shell un-hide a view that would never paint. The only trace was a `console.warn` inside the WebView. Three changes, one failure mode. The host parses the route at construction and serves no session at all when it will not do, reporting it as a shell failure. The pathname rule refuses empty segments, dot segments and backslashes anywhere, because `replaceState` normalises `/../../etc` to `/etc` and `/h/a\b` to `/h/a/b` and the page then renders whatever came out. And the producer encodes the host id it interpolates, which is how one carrying a query, a fragment or whitespace got there. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make a handoff mean the shell took it, not that a frame left (OTA phase C, C1.3) `handOff` returned `client.notifyNavigate(href)`, which answers whether the frame left the page and never whether the shell accepted it. Two hrefs the app builds today were posted, answered true and suppressed the local fallback, so the tap did nothing at all: the Connection-log link's object form, which `String` turns into `[object Object]`, and any href carrying a fragment, because the pathname is stripped to match and the whole href is what goes on the wire. Object hrefs now resolve the way the router resolves them, and the string is checked against the envelope's own pattern and cap before it is posted; anything that fails falls through to the local router, which is the policy this module already states. Whether a target names a screen that exists is shape's business no longer, and the comment says C1.7 owns it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): start a new flow when the shell view remounts A remount cleared `pageReady` but left the flow alone, so the wait the retired document armed still matched. It expired onto the page that replaced it, took a ready workspace to `document-load-failed`, and deleted the generation on the way. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say which page notification the bridge refused and why A refused `notify` fell through to the line about a view outliving its host, which is a different fault and names neither the notification nor the reason. The two refusals now get a line each, so a page that was told nothing cannot bury one reaching past what it was told. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the ready deadline to the page's own retry ceiling The margin was stated in a comment and asserted against itself, so changing either number left the suite green. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say what was wrong with the screen a refused shell named C1.1's per-kind log lands on a branch that also refuses a route, and that diagnostic was still falling through to the line about a view outliving its host. It names the shell's own bug now, and carries the issue. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refuse a dot segment however the route spells it A URL parser percent-decodes a path before it resolves it, so `/h/%2e%2e/x` climbed out of the `/h/` prefix exactly as `/h/../x` does and landed the page on a screen nobody asked for, with no refusal anywhere. The one segment rule both patterns share now reads the encoded spellings as the dot segments they are, and still lets an escape inside a name through. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): name routes in the manifest field list the builder emits C1.3 added `routes` to every manifest this builder writes, and the Phase A contract test still listed eight keys, which is what went red in CI. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): hold a navigate target to the same segment rule as the shell's The href pattern is built from the segment source C1.2 tightened, and nothing said so: a spelling one pattern refused while the other took it would be a hole with a `notify` already pointed at it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): give the ref-refresh probe the navigations this branch added C1.1's new case builds its own probe, and on this branch a probe also collects the hrefs the page hands back. The file stopped typechecking on the merge, which the tests ratchet caught. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): format the web shell route entry Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): split the bridge frame suite along the modules the merge created `bridge-rpc-client-frames.test.ts` reached 835 counted lines once C0.8 and C1.1 both added cases to it, over the 800 the lint allows. The split follows the two modules those changes extracted, so each suite now names the module it covers. `bridge client page faults` moves to `bridge-client-notifications.test.ts` (the outbound notify surface) and `bridge client refusals and send failures` to `bridge-client-inbound-frames.test.ts` (the reader, including the refused-event release that cancels at the shell). The seven suites that exercise the client as a whole stay put. The fake port all three drive moves to `bridge-page-client-test-harness.ts` rather than being copied three times. No case changed and none was dropped: 48 `it` cases before, 37 + 4 + 7 after, and all nine `describe` bodies compare byte-identical to their originals. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the shared init fixture as the member a case reads The harness exported `INIT` as `BridgeHostMessage`. While it was a module-local const, control flow narrowed it to the `init` member at each use, so `INIT.grants` read fine. An imported binding keeps its declared type instead, so the same read lost `grants` to the union and the tests ratchet went red. Declared as the init member, which is what every case already treats it as. No cast: the object literal is checked against the narrower type directly. `INIT` was the only exported fixture with this shape. `CONNECTION` is `as const`, `GRANTS` is inferred, and nothing reads a member off an `eventFrame` result. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
084e101328 |
fix(session-search): keep the title filter while full-text search is off (#21550)
* fix(session-search): keep the title filter while full-text search is off The search box switched to index search the moment the query was non-blank, so on a computer with indexing off the panel showed only the consent card and hid every session. Derive the mode once in useAiVaultPanelSearch: while local consent is pending the box stays the legacy title filter and the consent card becomes an offer above the filtered results. * test(session-search): cover the legacy title filter while indexing is off The panel test fails on the pre-fix code with no session rows rendered. * refactor(session-search): drop Not now and name the query flag queried The dismiss button's only remaining job was wiping the user's live title filter. Keep localConsent and expose queried so the panel reads one flag per fact, and assert the post-enable and empty-box states. * refactor(session-search): guard first and name the search-mode flags for what they mean |
||
|
|
b8f3b1ec00 |
feat(mobile): the shell names the screen, and the page routes to it (OTA phase C, C1.2) (#21501)
* feat(mobile): the page mounts on the shell's init, with the client injected (OTA phase C, C1.1) The Route A entry built no client and mounted the route tree immediately, so the web provider minted its own: it read the page channel, built `BridgeRpcClient` and fell back to a placeholder that rejected every call. A tree that mounts before `init` reads synchronous getters against a client that knows no host, no state and no build, and the first render it records is the wrong one. The entry now owns the page's one client. It builds it from the channel at module scope, mounts nothing until `onReady` fires, and stamps the session and build ids `getShellSession()` returns on the document beside the mount state, so a screenshot, the render check and a device console read the same three facts. `client-context.web.tsx` takes that client by injection and serves it from `acquire()` for every hostId, because the bridge protocol names no host; the placeholder and its `BridgeTransportUnavailableError` are gone, along with the entry that pointed at them in the unvalidated-port inventory. A document with no channel is not inside the shell, so it says `unbridged` and stops rather than waiting out a backoff nobody answers. The render check gains a shell double that answers `ready` with `init`, reads the stamped session back off the document, and proves the gate is real by opening the same route with no double and finding an empty `#root`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the shell names the screen, and the page routes to it (OTA phase C, C1.2) The shell serves its document at `/` and refuses every other path, so the page's own location matches no route in the tree it carries and expo-router paints Unmatched. Nothing in the document can tell it otherwise, so the screen has to cross the bridge. `init` gains an optional `route: { pathname, params }`. The pathname is held to what a path may be rather than to what a screen may want: rooted, single-slash, no query and no fragment. A protocol-relative `//host` would make `history.replaceState` throw a cross-origin SecurityError and take the mount down with it, and the params are a field of their own so neither side parses a URL. The shell route supplies it, the screen passes it to B4's hook, and the hook holds it for the life of one host: the page routes once, before its first render, so a route that changed afterwards has nothing left to change. The page writes that URL into its history and then mounts. It also hands the same URL to `ExpoRoot` as its `location`, because `ExpoRoot` snapshots `window.location.href` when its module is imported, which is before any frame has crossed the bridge: without it the router reads the `/` the shell served and replaces the page's own path right back. A shell too old to name a route leaves the page with nothing to open, so it paints a panel saying to update the app, built as elements outside React because the route tree is exactly what cannot mount there. Both platforms stop reading the document's URL to decide a load finished. The page rewrites its own path before its first render, so a document that committed at `/` reports finishing at `/h/<hostId>`; reading the path withheld `ready` forever and left the Android WebView hidden behind it. What is left is whether the load committed, which is the question the state machine already answers. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the page can tell the shell it faulted (OTA phase C, C1.1) A page that throws where it renders has nowhere to report it: the shell sees a document that loaded and a view that never painted, so it waits on a blank page forever. This adds the one frame that says so. `notify { name: 'fault' }` carries the capture an `error` frame already carries, so both directions share one bound and one reader. It rides a grant because `notify` is a closed list on both sides: a page served by a newer desktop into an older shell would have the whole frame refused, so the page asks `init.grants.native` first and stays quiet on a no. The shell answers it as `document-load-failed`, which is what happened. That reason drops the generation and downloads once, so a page broken by bytes this host has since replaced recovers, and one broken by its own code stops at the failure screen rather than a blank one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): give the bridge's notifications and the host's errors their own modules The fault report took both files over the 300-line cap, so each gives up the group that was already separable. The page's one-way members move to `bridge-client-notifications.ts`, which is also where the two policies that split them can be stated: the two the native contract declares throw before a session, and the fault report never throws at all. The host's three error classes move to `bridge-host-errors.ts`, the mirror of the page's own `bridge-client-errors.ts`. No behaviour changes. The commit before this one is over the cap on its own, which a forward-only history is the reason to say rather than hide. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): one boundary under the page's root, reporting to the shell (OTA phase C, C1.1) Nothing in `app/h/**` exports an `ErrorBoundary` and `ExpoRoot` provides no global one, so a throw while a route renders — or a route module that rejects once the manifest is lazy — unmounts the tree and leaves a blank document. The shell sees a load that finished and waits on it forever. The entry now wraps what it mounts on `init` in one boundary that posts the throw over the bridge. Above `ExpoRoot`, not inside its wrapper: a route that cannot be resolved throws where the router renders it, and a boundary below the router never sees that. It renders nothing and offers nothing to press. The generation is on disk and was hash-checked before the view loaded it, so the same bytes throw again and a retry here would only throw twice; recovery belongs to the shell, which drops the generation on the report. The render check now grants the fault and collects what the page posts into the errors every case already asserts empty, because a throw the boundary caught paints nothing and logs nothing a `pageerror` listener would hear. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the page-fault callback ref after the commit, not during render React may replay or discard a render, so the write belongs in the commit phase. Layout, not passive, and declared above the host's effect: a native frame can arrive between a commit and a passive effect, and the host must already hold this render's callback. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the route ref after the commit, not during render Same class as the page-fault ref: render must stay pure because React can replay or discard it. Folded into the one commit-phase effect above the host's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): take the boundary test to C0.5's fake-client pair `createBridgePortPair` is generic over the shell client now; the fake-client form this test wants is `createFakeBridgePortPair`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): bound the wait for a page that never says a word (OTA phase C, C1.1) A route module that throws while the bundle is evaluated takes the entry with it. The document still commits and the WebView still reports it loaded, but no boundary mounts, no fault is posted and no frame is ever sent, so the session sat in `ready` behind a blank view forever. The native view's finished load starts a clock; the page's first `ready` stops it; expiry is `document-load-failed`, which deletes the generation and fetches once. Nothing cancels the timer — a `ready` that lands first makes the expiry a no-op — so the runner owns a clock and the reducer owns every decision. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(config): make a route chunk throw, so the render check proves the boundary reports The check folded page faults into its errors but nothing ever produced one, so a boundary that stopped reporting would have stayed green. The server now serves one real route chunk with a throw in front of it: the module still links, so the failure is an evaluation throw where the router renders, which is exactly what the boundary is for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make the host enforce the grants it issued, and hear nothing before ready `forwardNotify` acted on any frame that parsed, including a `fault` from a page that had never asked for a session and therefore held no grant. Both refusals now go through one rule the host shares with the frame it sends, so the list a page is told about and the list it will be served cannot drift. Inert while every page is offered `fault`; the ungranted arm is what C1.3 needs the moment a grant belongs to a route rather than to the protocol. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refuse a route no page can open, rather than blanking the WebView (OTA phase C, C1.2) `sendInit` put `options.route` straight on the wire and only the page's decoder checked it, so an out-of-contract pathname made the page refuse the whole `init`, ask again on its 2 s backoff forever, and the shell un-hide a view that would never paint. The only trace was a `console.warn` inside the WebView. Three changes, one failure mode. The host parses the route at construction and serves no session at all when it will not do, reporting it as a shell failure. The pathname rule refuses empty segments, dot segments and backslashes anywhere, because `replaceState` normalises `/../../etc` to `/etc` and `/h/a\b` to `/h/a/b` and the page then renders whatever came out. And the producer encodes the host id it interpolates, which is how one carrying a query, a fragment or whitespace got there. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): start a new flow when the shell view remounts A remount cleared `pageReady` but left the flow alone, so the wait the retired document armed still matched. It expired onto the page that replaced it, took a ready workspace to `document-load-failed`, and deleted the generation on the way. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say which page notification the bridge refused and why A refused `notify` fell through to the line about a view outliving its host, which is a different fault and names neither the notification nor the reason. The two refusals now get a line each, so a page that was told nothing cannot bury one reaching past what it was told. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the ready deadline to the page's own retry ceiling The margin was stated in a comment and asserted against itself, so changing either number left the suite green. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say what was wrong with the screen a refused shell named C1.1's per-kind log lands on a branch that also refuses a route, and that diagnostic was still falling through to the line about a view outliving its host. It names the shell's own bug now, and carries the issue. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): refuse a dot segment however the route spells it A URL parser percent-decodes a path before it resolves it, so `/h/%2e%2e/x` climbed out of the `/h/` prefix exactly as `/h/../x` does and landed the page on a screen nobody asked for, with no refusal anywhere. The one segment rule both patterns share now reads the encoded spellings as the dot segments they are, and still lets an escape inside a name through. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): format the web shell route entry Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): split the bridge frame suite along the modules the merge created `bridge-rpc-client-frames.test.ts` reached 835 counted lines once C0.8 and C1.1 both added cases to it, over the 800 the lint allows. The split follows the two modules those changes extracted, so each suite now names the module it covers. `bridge client page faults` moves to `bridge-client-notifications.test.ts` (the outbound notify surface) and `bridge client refusals and send failures` to `bridge-client-inbound-frames.test.ts` (the reader, including the refused-event release that cancels at the shell). The seven suites that exercise the client as a whole stay put. The fake port all three drive moves to `bridge-page-client-test-harness.ts` rather than being copied three times. No case changed and none was dropped: 48 `it` cases before, 37 + 4 + 7 after, and all nine `describe` bodies compare byte-identical to their originals. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the shared init fixture as the member a case reads The harness exported `INIT` as `BridgeHostMessage`. While it was a module-local const, control flow narrowed it to the `init` member at each use, so `INIT.grants` read fine. An imported binding keeps its declared type instead, so the same read lost `grants` to the union and the tests ratchet went red. Declared as the init member, which is what every case already treats it as. No cast: the object literal is checked against the narrower type directly. `INIT` was the only exported fixture with this shape. `CONNECTION` is `as const`, `GRANTS` is inferred, and nothing reads a member off an `eventFrame` result. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
7080eb0604 |
fix(relay): bound the idle-rehome candidate poll to a window of decisions (#21557)
* fix(relay): bound the idle-rehome candidate poll to a window of decisions The director's idle-regional-rehome poll built every (eligible host x target cell in its preferred region) pair, applied the cohort predicate downstream of that fan-out, sorted the lot, and took LIMIT 100 OFFSET n. Its cost was set by the size of the fleet and the width of the cohort, so raising the cohort from 10% to 100% pushed it past the serving pool's 5 s statement_timeout and the rollout stalled at 0.37 hosts/min. The poll now resolves the cell inventory once (tens of rows), takes a bounded window of decision rows in primary-key order from a keyset cursor with the cohort, freshness and cross-region predicates applied first, verifies only that window against the host-side gates, and ranks targets in the process. Same candidates in the same priority order; the work per poll no longer depends on the cohort or the fleet. Adds a once-a-minute aggregated poll summary so an operator can tell a poll gated by the dispatch budget from one that found nobody to move. Co-Authored-By: Claude <noreply@anthropic.com> * fix(relay): pin the rehome verification to the window's exact keys The window read and the verification read take separate snapshots. The verification repeated the window's predicate with its own LIMIT, so a decision that turned eligible between the two reads shifted that LIMIT and pushed the window's last host out of it -- while the cursor still advanced past that host, skipping it for a whole sweep. The verification now names the keys the window returned. Its LIMIT stays as the optimisation fence that stops Postgres flattening the subquery, but can no longer truncate a key set that is at most one window long. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
742fa638f3 | Update README downloads badge | ||
|
|
d043cfbbee |
feat(mobile): the page mounts on the shell's init, with the client injected and a fault boundary (OTA phase C, C1.1) (#21500)
* feat(mobile): the page mounts on the shell's init, with the client injected (OTA phase C, C1.1) The Route A entry built no client and mounted the route tree immediately, so the web provider minted its own: it read the page channel, built `BridgeRpcClient` and fell back to a placeholder that rejected every call. A tree that mounts before `init` reads synchronous getters against a client that knows no host, no state and no build, and the first render it records is the wrong one. The entry now owns the page's one client. It builds it from the channel at module scope, mounts nothing until `onReady` fires, and stamps the session and build ids `getShellSession()` returns on the document beside the mount state, so a screenshot, the render check and a device console read the same three facts. `client-context.web.tsx` takes that client by injection and serves it from `acquire()` for every hostId, because the bridge protocol names no host; the placeholder and its `BridgeTransportUnavailableError` are gone, along with the entry that pointed at them in the unvalidated-port inventory. A document with no channel is not inside the shell, so it says `unbridged` and stops rather than waiting out a backoff nobody answers. The render check gains a shell double that answers `ready` with `init`, reads the stamped session back off the document, and proves the gate is real by opening the same route with no double and finding an empty `#root`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): the page can tell the shell it faulted (OTA phase C, C1.1) A page that throws where it renders has nowhere to report it: the shell sees a document that loaded and a view that never painted, so it waits on a blank page forever. This adds the one frame that says so. `notify { name: 'fault' }` carries the capture an `error` frame already carries, so both directions share one bound and one reader. It rides a grant because `notify` is a closed list on both sides: a page served by a newer desktop into an older shell would have the whole frame refused, so the page asks `init.grants.native` first and stays quiet on a no. The shell answers it as `document-load-failed`, which is what happened. That reason drops the generation and downloads once, so a page broken by bytes this host has since replaced recovers, and one broken by its own code stops at the failure screen rather than a blank one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): give the bridge's notifications and the host's errors their own modules The fault report took both files over the 300-line cap, so each gives up the group that was already separable. The page's one-way members move to `bridge-client-notifications.ts`, which is also where the two policies that split them can be stated: the two the native contract declares throw before a session, and the fault report never throws at all. The host's three error classes move to `bridge-host-errors.ts`, the mirror of the page's own `bridge-client-errors.ts`. No behaviour changes. The commit before this one is over the cap on its own, which a forward-only history is the reason to say rather than hide. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): one boundary under the page's root, reporting to the shell (OTA phase C, C1.1) Nothing in `app/h/**` exports an `ErrorBoundary` and `ExpoRoot` provides no global one, so a throw while a route renders — or a route module that rejects once the manifest is lazy — unmounts the tree and leaves a blank document. The shell sees a load that finished and waits on it forever. The entry now wraps what it mounts on `init` in one boundary that posts the throw over the bridge. Above `ExpoRoot`, not inside its wrapper: a route that cannot be resolved throws where the router renders it, and a boundary below the router never sees that. It renders nothing and offers nothing to press. The generation is on disk and was hash-checked before the view loaded it, so the same bytes throw again and a retry here would only throw twice; recovery belongs to the shell, which drops the generation on the report. The render check now grants the fault and collects what the page posts into the errors every case already asserts empty, because a throw the boundary caught paints nothing and logs nothing a `pageerror` listener would hear. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the page-fault callback ref after the commit, not during render React may replay or discard a render, so the write belongs in the commit phase. Layout, not passive, and declared above the host's effect: a native frame can arrive between a commit and a passive effect, and the host must already hold this render's callback. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): take the boundary test to C0.5's fake-client pair `createBridgePortPair` is generic over the shell client now; the fake-client form this test wants is `createFakeBridgePortPair`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): bound the wait for a page that never says a word (OTA phase C, C1.1) A route module that throws while the bundle is evaluated takes the entry with it. The document still commits and the WebView still reports it loaded, but no boundary mounts, no fault is posted and no frame is ever sent, so the session sat in `ready` behind a blank view forever. The native view's finished load starts a clock; the page's first `ready` stops it; expiry is `document-load-failed`, which deletes the generation and fetches once. Nothing cancels the timer — a `ready` that lands first makes the expiry a no-op — so the runner owns a clock and the reducer owns every decision. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(config): make a route chunk throw, so the render check proves the boundary reports The check folded page faults into its errors but nothing ever produced one, so a boundary that stopped reporting would have stayed green. The server now serves one real route chunk with a throw in front of it: the module still links, so the failure is an evaluation throw where the router renders, which is exactly what the boundary is for. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make the host enforce the grants it issued, and hear nothing before ready `forwardNotify` acted on any frame that parsed, including a `fault` from a page that had never asked for a session and therefore held no grant. Both refusals now go through one rule the host shares with the frame it sends, so the list a page is told about and the list it will be served cannot drift. Inert while every page is offered `fault`; the ungranted arm is what C1.3 needs the moment a grant belongs to a route rather than to the protocol. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): start a new flow when the shell view remounts A remount cleared `pageReady` but left the flow alone, so the wait the retired document armed still matched. It expired onto the page that replaced it, took a ready workspace to `document-load-failed`, and deleted the generation on the way. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): say which page notification the bridge refused and why A refused `notify` fell through to the line about a view outliving its host, which is a different fault and names neither the notification nor the reason. The two refusals now get a line each, so a page that was told nothing cannot bury one reaching past what it was told. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the ready deadline to the page's own retry ceiling The margin was stated in a comment and asserted against itself, so changing either number left the suite green. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): split the bridge frame suite along the modules the merge created `bridge-rpc-client-frames.test.ts` reached 835 counted lines once C0.8 and C1.1 both added cases to it, over the 800 the lint allows. The split follows the two modules those changes extracted, so each suite now names the module it covers. `bridge client page faults` moves to `bridge-client-notifications.test.ts` (the outbound notify surface) and `bridge client refusals and send failures` to `bridge-client-inbound-frames.test.ts` (the reader, including the refused-event release that cancels at the shell). The seven suites that exercise the client as a whole stay put. The fake port all three drive moves to `bridge-page-client-test-harness.ts` rather than being copied three times. No case changed and none was dropped: 48 `it` cases before, 37 + 4 + 7 after, and all nine `describe` bodies compare byte-identical to their originals. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the shared init fixture as the member a case reads The harness exported `INIT` as `BridgeHostMessage`. While it was a module-local const, control flow narrowed it to the `init` member at each use, so `INIT.grants` read fine. An imported binding keeps its declared type instead, so the same read lost `grants` to the union and the tests ratchet went red. Declared as the init member, which is what every case already treats it as. No cast: the object literal is checked against the narrower type directly. `INIT` was the only exported fixture with this shape. `CONNECTION` is `as const`, `GRANTS` is inferred, and nothing reads a member off an `eventFrame` result. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
cd81725d70 |
feat(terminal): configure URL click and middle-click behavior (#21438)
* feat(terminal): configure URL click behavior * fix(i18n): include terminal link setting title * fix(i18n): localize terminal click controls * fix(settings): update terminal URL click title |
||
|
|
b0808741ec | refactor(editor): pair padding masks with their restore token | ||
|
|
90a6822544 | Merge branch 'nwparker/markdown-scanners' into nwparker/markdown-better | ||
|
|
b3b98cfdaf | test(editor): verify standalone scanner boundaries | ||
|
|
f2fd18c820 |
test(mobile): make the bridged recording-corpus replay a CI gate and pin the C1 page closure (OTA phase C, C1.6) (#21533)
* test(mobile): the bridged corpus replay is a gate, not an opt-in flag (OTA phase C, C1.6) `rpc-recording-through-bridge.test.ts` replays all 787 goldens through the page bridge and pins how far that bridge is from byte-identical. It only ran when `RPC_FOUNDATION_BRIDGE=1` was set, which CI did in a step of its own. A gate whose CI job has to remember to set a variable is opt-in, and a branch that widened the divergence with that step edited away would have been measured by nobody. The suite now runs by default and `RPC_FOUNDATION_BRIDGE=0` skips it, for a local run that does not want the three minutes. `BRIDGED_PARITY_OFF` names the one value that skips, so an unset or mistyped variable still runs the gate. The dedicated CI step goes with it: `pnpm test` collects the file already, so keeping the step would have run the same 788 tests twice in one job. Vitest gives the file a worker beside the rest of the suite, so the marginal wall time is a fraction of the ~3 min it takes alone. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the bridged verdict of every C1-page-closure golden by id (OTA phase C, C1.6) The bridged replay certifies the corpus in counts over 787 goldens, and a count is the wrong instrument for the claim C1 needs. C1 moves one domain to the web: `app/h/_layout.tsx`, `app/h/[hostId]/index.tsx` and their import closure. A golden recorded at a call site in there that stopped replaying byte-identically is paid for by any of the other 684 that started, and every existing assertion still passes. `c1-page-closure.ts` names the 22 families and 103 goldens of that closure and pins each one to the verdict it gives: 54 byte-identical, 49 in four of the five classes the suite already excludes, all of them recorder observation artifacts whose wire bytes C0.5 and C0.8 proved identical. Membership is checked per family, not against the flat id list, so a golden newly derived into a family this domain owns arrives as a finding instead of being missed for never having been pinned. A closure golden may only be excluded into a class that carries a reason in `BRIDGED_PARITY_EXCLUSIONS`. Two full-corpus assertions go with it. `identical` was a floor; it is now the exact 787 minus the excluded classes. And the run's own size is pinned to the corpus: every class is an upper bound, so without that a corpus that lost goldens outside the identical set satisfied all of them. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): move a closure golden to a verdict it is not already pinned to (OTA phase C, C1.6) The drift test took whichever golden the pin lists first and moved it to `params-undefined`. Nothing said that golden was not pinned to that class already, and the day one is, the test asserts a change it never made. Pick the first golden whose verdict is something else, and assert the pair of verdicts the line reports rather than just the new one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin `identical` to its baseline, not to the run's own exclusions (OTA phase C, C1.6) `identical: pinned - excludedCount` took `excludedCount` from the run, and the sum assertion above it already forces `total(counts) === excludedCount`, so the pair reduced to `corpus === pinned`: only the size of the corpus was pinned. With every per-class check an upper bound, a golden moving out of an excluded class into `identical` satisfied all of them. `bridgedParityTallyDrift` pins every number to `BRIDGED_PARITY_BASELINE` exactly, `identical` among them, and its own tests encode the mutation: one `result-absent-settlement` golden reported `identical` is two drift lines, where before it was a green run. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): the observation class counts 3, not 7 (OTA phase C, C1.6) The number in the prose predates the baseline it describes; `result-absent-observation` is 3 in `BRIDGED_PARITY_BASELINE` and in the run, and the line above it already says 341 / 3 / 6 / 33 / 8. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): name the checks that are still there, not the bound that is gone (OTA phase C, C1.6) Three comments described the per-class upper bound the tally replaced. What the exact pin is exact *against* is now `unclassified`, the exclusion sum and the membership pins, so say those. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): drop the closure-exclusion check the cheap suite already makes (OTA phase C, C1.6) `c1PageClosureExclusions()` reads `C1_PAGE_CLOSURE` and nothing the run produced, so asserting on it inside the three-minute replay bought nothing the cheap suite next door does not already buy. `c1-page-closure.test.ts` makes the same assertion with a presence precondition on top, which is the stronger of the two. The comment beside the tally counted two checks above it; there are three, and a golden that moved out of an excluded class into `identical` is invisible to all of them: `result-absent-settlement` is past the nameable cap, so membership drift never looks at it, and lowering it lowers the exclusion sum with it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
129807f795 | Merge branch 'nwparker/markdown-scanners' into nwparker/markdown-better | ||
|
|
24cbbd6c2b | refactor(editor): add shared Markdown scanners | ||
|
|
a9d6e3a5bb | fix(editor): support CJK emphasis in preview | ||
|
|
3132c48fdc | fix(editor): preserve code contents and validate reference candidates |