mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
* fix(mobile): persist a fresh manifest onto the generation on disk A route-grant edit on the desktop moves no asset, so the bundle is published under the build id it already had: the cached generation holds the right bytes under a manifest an edit behind, and that stored manifest is the whole of an unreachable host's verdict. The store gains one operation for it. Refused unless the manifest names exactly the bytes on disk — same build id, and the same asset list read through the contract's own serializer, which is the string the id is a digest of. Written beside and renamed over, so a write that fails leaves the manifest the assets were downloaded with. A refusal is a return value, never a throw: it costs freshness, never the generation. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the manifest through on a same-build cache hit The same-build hit in `onManifestRead` opens the cached generation and now also asks the store to rewrite the manifest beside it, and the generation the session holds carries the fresh routes from that point. Without it every offline verdict lagged a grant edit: `onCacheRead` reads the stored routes, and nothing on this path wrote them. The manifest travels whole on `manifest-read`, because the store compares its asset list and a projection rebuilt from the fields a transition reads would name other bytes. The fallback fixture that read a newer manifest under the cached build id now uses a build of its own, which is what makes the download it is about happen at all. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): every same-build manifest read persists, whatever the route verdict (round 1) A verdict about this route is not a verdict about the manifest. A fresh list that takes this screen native, or that names a bundle this shell cannot open, still grants or revokes the other routes the same assets serve — and what is stored beside those assets is the whole of the next offline verdict. Both returns left it unwritten, so an offline entry kept grants the desktop had already taken away. The same-build read is now taken before the route verdict: the native and wall returns carry the fresh routes on the generation they hold and emit the persist, exactly as the open does. The different-build arms are untouched, wall included, which still fetches nothing. `readMobileWebShellReachability` moves to `mobile-web-shell-reachability.ts`, the module its test was already named for: the reducer was one line under the 300-line cap and this fold needed the room. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make the manifest swap crash-safe at the store (round 2) `moveFile` deletes the destination before it moves, because expo offers no atomic replace, so a failure after that delete left a generation with no manifest at all — which reads back as "no activation" and drops the host's cache. A phone whose host is unreachable loses its whole workspace to one interrupted rename. The store's fake threw before that delete, so the suite never exercised the ordering the adapter really has. The write is now three steps inside the generation directory: the fresh manifest to `manifest-next.json`, the old one away, the fresh one over it. The old manifest is never deleted before the whole of the fresh one is on disk, and `readActive` settles every window it leaves — both files present finishes the swap, a pending manifest alone is adopted, and one that is torn or names another build is discarded with the old one kept. A pending file that cannot be settled deletes nothing, because it may be the only manifest left and the next read can still adopt it. The asset-path guard refuses the pending name too, or an asset could be read back as an activation. `joinUri` moves to a module that imports nothing: taking it from the file-system module pulled `expo-file-system` into everything that addresses the cache, which is the import the store's testability rests on not having. The refusal rule now names where the id-to-assets refine actually lives. It is on the host's strict schema, not on the loose reader the phone parses with, so the store's asset comparison is the phone's own check rather than a restatement of one already made. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): a walled same-build manifest is not persisted (round 2) Round 1 wrote every same-build manifest through, the walled ones included. That is the one read where the fresh manifest must not reach disk: an offline entry skips the compat check, so a stored manifest this shell has just declared it cannot read would have the next offline entry open a page under the grants of that bundle. What is on disk stays the last manifest this shell accepted, and the held generation keeps its routes with it, so the session's record still matches what was written. The native-route arm keeps the write: a route this build cannot serve is not a bundle it cannot read, and the fresh list still governs the other routes those same assets serve. Its round-1 test is now a control that pins the wall persisting nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): the phone refuses a manifest whose build id is not its asset digest (round 3) The id is a cache key and a claim about content at once, and only the host checked the two against each other. The phone read the same document loosely and never recomputed the digest, so a stale or forged id reached the shell — which treats an id it already holds as the same bytes and opens the generation on disk without paging one. The reader now carries the host's refine, so a manifest that reaches the reducer under the cached build id names the same serialized asset list by construction. The two checks in that reader are one `superRefine` with the cheapest first and the first issue returning: the digest is the only one that hashes, and a manifest already over the allocation ceiling must not be hashed to be refused. The store's comparison stays, as the second reading of one rule rather than a rule of its own: its argument is a plain object, and nothing in the type says which parse it came from. The fixtures that published a literal id now derive it from the assets they name, which is what the host does. The one test that needs a single id over two asset lists still names it, because that collision is what it is about. Every one of the 13 manifests in the golden corpus was already digest-correct; the replay suites pass unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
12 lines
534 B
TypeScript
12 lines
534 B
TypeScript
/**
|
|
* One spelling of the cache's path arithmetic.
|
|
*
|
|
* Its own module, and deliberately importing nothing: `generation-store-file-system.ts` owns the
|
|
* same uri dialect but loads `expo-file-system` for the adapter, and the store's whole testability
|
|
* rests on never importing that. A value taken from there would pull the native module into every
|
|
* module that addresses the cache.
|
|
*/
|
|
export function joinUri(...segments: readonly string[]): string {
|
|
return segments.map((segment) => segment.replace(/\/+$/, '')).join('/')
|
|
}
|