mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
* test(mobile): ratchet the 201 unchecked RPC reply readers Step 4 moved every call-site cast into an RpcOperation's `read`, but 201 of those readers still answer `compatible: true` for any payload: `rpcUncheckedPayloadReader` (163), `rpcReadUnchecked` (26 outside its own module) and `rpcUncheckedMemberReader` (12), across 42 files. The cast moved; it did not become true. Held as data with an AST boundary test, shaped on the raw-request-port ratchet: a file that is not listed fails, a listed file that no longer has one fails, and a count that rises fails. Only a call counts, so an import is not a reader and prose never is. No behaviour change: this commit adds a list and a test. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): validate the source-control domain's RPC replies at arrival Replaces all 17 unchecked readers in mobile/src/source-control/ with `rpcResultVariant(variant, schema)`, so a malformed reply is an `RpcIncompatibleReplyError` naming the operation instead of a TypeError three frames downstream. The inventory drops 201 -> 184 and the five source-control operations files leave it entirely. This is a behaviour change, scoped to malformed replies. Six reply-matrix goldens move; every named-scenario golden and every `normal` partition is byte-identical, which is the parity claim. Schemas live one module per reply domain, beside the operations that read them: git-status, git-compare, git-history, hosted-review and worktree-metadata. A member is required only where a consumer reads it unguarded, and each schema records the consumer line that justifies it. Nothing is `.strict()`; every reply a consumer publishes verbatim keeps `z.looseObject` so an undeclared host member still passes through. Six replies have no reader anywhere in mobile and get `z.unknown()`, which is the honest schema for them, not a holdout. Three readers stay total by construction, because their contract is that an unreadable reply is a value rather than an error: the `git.status` projection (a null status three screens route on), the `session.tabs.list` reveal (a null list means poll again) and the generated commit message (a screen's copy, never a decode error in a text field). They gain the salvage report, not a verdict. Consumers take the schema's output type, so `MobileGitStatusResult` and the branch-compare aliases now name what mobile reads rather than the desktop aggregate, and seven call-site casts are gone. Three requirements came from the goldens, not from the host types: `git.history` sends `timestamp: null`, `hostedReview.getCreationEligibility` sends a `reviewLookupOutcome` the shared union does not list, and the `git.status` projection writes an absent member as a present `undefined`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): re-record the six source-control reply-matrix goldens step 7 moves Six goldens, all on malformed partitions. Every named-scenario golden and every `normal` partition is unchanged, which is the parity claim for this step. git.history-read / git.history#1 result-absent, result-null, inner-ok-missing, inner-false-string-error, inner-false-object-error: the load rejected with a TypeError reading 'items' or 'map' off undefined/null; it now rejects with `incompatible_reply: git.history-page (git.history)`. hostedReview.eligibility + create-intent / hostedReview.getCreationEligibility result-absent, result-null, inner-ok-*: the fetch fulfilled with the error envelope itself, re-typed as an eligibility and published into the compose prefill; it now rejects, and both callers already route that to the same "eligibility unavailable" state a null answer produced. hostedReview.create-chain + create-intent / hostedReview.create result-absent, result-null, inner-ok-missing, inner-false-object-error: the create form showed the raw TypeError text "Cannot read properties of undefined (reading 'ok')"; it now shows the incompatible-reply message. Every header digest is unchanged -- baseline, recorder, adapter, scenario and lockfile all match -- so the diff is the behaviour and nothing else. Recorded from this branch into a scratch directory and copied in, because there is no scoped honest alternative: scripts/rpc-recording.mts refuses to run unless the product tree equals the pinned baseline, and the README's remedy for an intended behaviour change is to repin, which rewrites the `baseline` header of all 667 goldens. So these six now carry a pin whose tree no longer produces them. That is a real gap in the oracle's design for behaviour changes, not a detail of this step, and it needs a decision before this lands. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the four reply-schema properties the goldens found Each of these cost a reply-matrix golden while writing the source-control schemas, and none of them follows from reading the consumers or the host types: a newer host's undeclared members must still decode, `git.history` sends `timestamp: null`, `hostedReview.getCreationEligibility` sends a `reviewLookupOutcome` the shared union does not list, and the `git.status` projection writes an absent member as a present `undefined`. The `.strict()` case is the one worth stating twice: at the top level it rejects the reply, and on the entry it drops the row, which shows a dirty worktree an empty Changes list. The fifth test pins the salvage report that makes such a drop visible instead of silent. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give an unreadable reply a message a user can read `RpcIncompatibleReplyError` put `incompatible_reply: <op> (<method>)` in `message`, and `message` is what the screens hand to a toast. Step 7 is the first change that can reach this error at all, so the token would have shipped to users as its own error copy. Fixed at the boundary rather than per site: `message` is now plain copy, and the machine token moved to `code` (`incompatible_reply`) and `name` (`RpcIncompatibleReplyError`), both readable by callers. The cross-bundle fallback in `isRpcIncompatibleReplyError` matched on the old message prefix, so it now matches on `name`, which a foreign copy of the module still carries. No existing test pinned the old text. Two new ones pin the copy, the token and the foreign-copy match. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to this branch and re-record Commitadeb5f9531recorded the six moved goldens into a scratch directory and copied them back, which left them pinned to `e7206f62`, a tree that no longer produces them. That is the one claim the `baseline` header exists to make, so this replaces it with the README's remedy done in full. `baseline` is nowf741b2ea82, the last commit on this branch that touches a fenced path, so the recording fence passes in place and every golden is pinned to the tree that produced it. All 667 were re-recorded through `scripts/rpc-recording.mts --record`; none were hand-edited. Decoding every value pool against the branch pointb8d4cde09fsorts the corpus into 661 header-only moves where `baseline` is the only key that moved, 6 whose body moved as well, 0 added and 0 deleted. The 6 are the disclosed step-7 delta, unchanged at 69 moved observation fields across malformed reply partitions, plus the readable incompatible-reply copy fromf741b2ea82. No `normal` partition and no named-scenario golden moved. `scenarioSha256` hashes the derived scenarios, not the manifest, so the repin moves no other header key; the README section this adds records that, the scratch-copy failure mode, and the follow-up repin main needs after a squash merge. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): narrow the incompatible-reply error by instanceof, not by cast The two new tests inf741b2ea82read the error through `as` casts, which the changed-code casting gate rejects. An `instanceof` guard narrows the same value and checks the class at the same time. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to the branch tip and re-record71d8c6a1e2touched a fenced path (`mobile/src`), so the pin from5f3f184fdfno longer named the tree that produces these goldens. The fence compares the whole of `mobile/src`, and a test file is inside it, so the pin follows the last commit that touches a fenced path rather than the commit whose behaviour moved. Re-recorded all 667 in place through `scripts/rpc-recording.mts --record`. Decoding every value pool against the branch pointb8d4cde09fstill gives 661 header-only moves with `baseline` the only moved key, 6 body moves, 0 added and 0 deleted; the six and their 69 moved observation fields are unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): record the four source-control reads that had no oracle git.status (host payload), git.branchCompare, git.commitCompare and git.branchDiff were migrated to checked readers with no recording observing them, so a required member a host omits would have surfaced only in production. Three families mount the owners rather than the senders, because each reply is only visible in what the owner then publishes: the Changes screen's loader hook (git.status, and the base-ref chain and git.branchCompare it triggers), the history list screen (git.history and the per-commit git.commitCompare), and the committed-diff opener hook (git.branchDiff). Ten goldens: three pilot recordings and seven reply matrices. Two adapter capabilities this needed. An inert FlatList never calls `renderItem`, so the history adapter renders one row through the screen's own callback, both to reach the handler that expands a commit and to read the file list back; without that the commit-compare reply changes nothing observable. And `lowlight` joins `react` and `zod` as a real library rather than a refusing proxy, because the branch diff highlights on its success arm before the preview reaches state, so the shipped text arm was otherwise unrecordable. No golden recorded its absence, so only `recorderSha256` moves. Recording the same scenarios against4b0009d414, the pre-refactor tree, is the before column. Decoding every value pool across the two gives 11 body moves and 666 header-only, 0 added, 0 deleted: the 6 already disclosed, plus the 5 new matrices at 63 moved observation fields. What moved is the point. A malformed git.status used to leave Changes `ready` over the malformed payload and go on to fetch a branch compare; it now says the host sent a reply it could not read. An absent git.branchDiff result used to put "Cannot read properties of undefined (reading 'kind')" on the screen. An unreadable git.commitCompare used to spin the expanded commit forever; it now says "No file changes". Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to the merge commit and re-record The merge is the last commit touching a fenced path, so it is the only tree the recorder's fence can match. Every golden moves `baseline` and picks up main's `recorderSha256` from #20920; the six the checked readers changed are the only bodies that move against main. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs(mobile): note the merge-commit pin and unwrap the recipe's record command `format:check` from `mobile/` caught the wrapped inline command the recipe had been carrying since it landed; pointing at the command above removes the duplicate and the wrap together. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): open the source-control reply enums so a newer host's arm degrades A closed `z.enum` in a reply schema is a version claim, and it refused replies every declared reader could have rendered: a `git.branchCompare` summary status of 'shallow-base' failed the whole Changes compare, a 'codeberg' provider failed the whole eligibility, and a 'typechange' entry status dropped the row. Main passed all three through. `openEnum` in zod-salvage declares the arm set open: an unrecognised arm reads as a member the consumers already handle, while absence and a non-string stay fatal. Not `.catch()`, which would swallow those two as well. `area` stays closed and says why: every arm grants stage, unstage or commit, so there is no member to degrade to that would not offer an action against a row this build cannot place. Main rendered such a row in no section either. Also drops two claims the code does not back. Nothing reads the salvage report, so the two comments promising a dropped entry "arrives as salvage.droppedPaths" are gone, and `hostKind` on the non-text diff arm had no reader. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * docs: write down the open-enum rule and the header keys a branch moves Rule 4 in the wire-compatibility page, beside the three rules it belongs with: an enum arm set is a wire surface, unknown arms degrade rather than reject, and leaving one closed is a decision to state where the schema is declared. The recorder recipe's step 4 said `baseline` would be the only moved header key, which is only true of a branch that never touched the recorder. It now names the three digests a branch's own edits move, so a reader recognises a clean result. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop the recorder's own timeout killing a full re-record The corpus records in ~110s warm and 160s under load, against a 120s budget, so a full re-record was killed roughly half the time. A killed run wrote a partial reporter banner and exited 1, which reads as a failing scenario rather than as a run that never finished — it cost two investigations here. The budget is now ten minutes, and a killed run says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to the open-enum commit and re-record `baseline` is the only header key that moves and no golden body moves: no matrix partition scripts an unknown enum arm, so the corpus cannot see this change. The eight schema unit tests are its only oracle. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop an unresolvable eligibility claiming the branch is not ready Both fallback prefills set `canCreate: false`, which is a determination nobody made. It short-circuits getMobilePrCreateBlockMessage before reviewLookupOutcome is read, so a malformed, refused or rejected eligibility told the user "This branch is not ready for a pull request yet." instead of asking them to retry. Dropping it leaves `canCreate` undefined, which is what "unproven" means here. Only a host that determined `canCreate: false` still gets the blocked copy. `area` now degrades to absent rather than staying closed. Dropping the row also dropped it from the unresolved-conflict gate, which grants create on a conflicted worktree; absent withholds stage, unstage and commit while keeping the row, since every area reader is an equality check. Its four consumers narrow explicitly: the diff-review queue filters unplaceable rows, the opener withholds the route, and the commit-failure prompt pins 'staged' where its own filter already did. `git.branchCompare` entries are nullish, matching the `?? []` its consumers use. Deletions: `MobileGitStatusProjection` and `uncheckedReaderCount` lose `export`, the boundary test drops its dead inventory self-file (the AST counter finds zero calls there, only prose), and `isRpcIncompatibleReplyError` is gone — it had no caller in mobile, desktop or e2e. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * style(mobile): formatting and a thrown rejection in the round-2 tests Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to the round-2 tip and re-record The round-2 eligibility fix is a behaviour change, so the corpus has to be re-recorded at a pin that includes it. Four goldens move body: the two create-intent eligibility matrices on every non-normal partition, and the two prefill scenarios that lose the fallback's `canCreate: false`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to the main merge and re-record The merge is now the last commit touching a fenced path, so the corpus has to carry its sha. No body moves against the pre-merge corpus: main's engine change shifts `recorderSha256` on every golden and nothing else, and main's fifteen step-6 goldens re-record byte-identical. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): admit the three unchecked readers #20954 landed The ratchet is a ceiling against this branch adding readers, not a claim about what main may land. #20954 brought `notification-stream-closed`, `native-chat-session-page` and `terminal-buffer-cleared`, so the merge has to raise those lines and say where they came from. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repin the recording baseline to the inventory commit and re-record The ratchet inventory is a fenced path, so admitting #20954's three readers moved the fence head again. Baseline only; no body moves against the merge re-record. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): send the host's own provider token back instead of a fallback `provider` is not a member mobile only reads. The eligibility reply names it and the create call returns it, so `openEnum(..., 'unsupported')` did not soften a reading — it rewrote the bytes, and a host that had just named `codeberg` refused its own provider as unsupported. The action-sheet Create path has no provider gate, so nothing caught it. Passes the token through as a string from the reply to the create params. The allow-list that decides whether mobile may create stays supportsHostedReviewCreation(), which already answers no for a token this build does not know; its parameter widens to `string`, since answering for an unknown token is the whole job. The worktree-link switch gains a default, which also fixes an older hole: an unrecognised provider used to fall out of the switch as `undefined` params. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the provider pass-through in the corpus Repins to the provider fix and records `sc-create-intent-unlisted-provider`, whose eligibility reply names `codeberg` and whose recorded `hostedReview.create` params carry it back unchanged. Restoring the old enum fallback fails that golden on `Request params mismatch: hostedReview.create#1` and nothing else. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb