Conversation
b135ec3 to
25e77e7
Compare
An Expo SDK 57 app under Continuous Native Generation: `ios/` and `android/` are prebuild output and are not committed, so every native fact lives in `app.config.ts` or in one of the three config plugins. `expo prebuild` in SDK 57 clears and regenerates both directories by default, which makes a committed native tree a second copy that can silently disagree with the plugin that is supposed to own it. Four facts in the app config are release blockers, and each is pinned by a test because none of them is visible until a build is in somebody's hands: the bundle id is `app.cuetracker` on both platforms, because on Play a different `applicationId` is a different app rather than an upgrade; the orientation set and tablet support are the shipping app's; and the colour scheme follows the system, because the app has three themes. `BUILD_NUMBER` and `APP_VERSION` come from the environment, which collapses the two mechanisms the Capacitor line uses into the one prebuild already applies to both projects. `with-ios-scene-lifecycle` adopts the UIKit scene life cycle, without which an app linked against the iOS 27 SDK installs and then refuses to launch. It is the spike's plugin with the two corrections its review named: the scene cold launch reads `connectionOptions.urlContexts` and `.userActivities`, and it reconstructs real launch options for `startReactNative`, without which `Linking.getInitialURL()` answers null and every cold-start deep link is discarded. `with-android-privacy` re-establishes `allowBackup="false"` and the app's own data-extraction rules. Android's default is `true`, so a prebuilt app silently opts back in and turns a claim PRIVACY.md, docs/index.html and README.md all make into a false one. `verify-apk.sh` now reads the result out of the built APK rather than trusting the plugin: backup off, the rules resolved through the resource table and their contents checked, no `<include>`, and the merged permission set pinned exactly per line. The Expo line's set is measured off a release prebuild and differs from the Capacitor line's, which is why the script takes the line as an argument instead of pretending one list covers both. `with-android-build-memory` raises the Gradle JVM heap from the template's `-Xmx2048m` to `-Xmx4g`. D8 runs out of heap merging this app's dex archives at the template's size, and the failure is an `OutOfMemoryError` inside `:app:mergeDexRelease` rather than anything the app's own code answers for. It is a plugin rather than a checked-in `gradle.properties` for the same reason as the other two: `android/` is prebuild output, so a hand-written value does not survive the next `expo prebuild --clean`. The release build is the assertion: without it, `assembleRelease` fails. Twenty-five permissions the dependency tree adds are blocked in the app config and each group is explained there: Expo's four optional template permissions, expo-secure-store's biometric pair, and expo-notifications' push receive, Play install-referrer binding and per-OEM launcher badge set. A TV tracker asking eight launcher vendors for shortcut access is exactly what the permission gate exists to stop. Three pnpm overrides come with the package. `expo` is a runtime dependency, so `pnpm audit --prod` walks its build tooling too, and `@expo/cli` and `@expo/metro-config` pull transitive `brace-expansion`, `nanoid` and `postcss` versions with high-severity advisories against them. Each override is written as a version range rather than a bare name, so it rewrites only the versions the advisory names and leaves every other copy in the tree alone. `pnpm audit --prod --audit-level=high` is clean with them and reports five highs without them. The package joins every gate in this commit rather than later: biome, dprint, cspell, `pnpm -r typecheck`, knip, jscpd, the sixteen dependency-cruiser rules, and jest-expo under both the ios and the android preset. `pnpm check` runs the native suite, and ci.yml gains a job per platform that generates the projects and compiles them, so a config plugin that no longer matches the Expo template fails in CI rather than on somebody's machine. The release gate's required list and its deadline move with them. One gate found its own bug immediately: `react` was declared at two versions across the workspace, pnpm hoisted one and nested the other, and a Radix package's peer bound the web app's component library to a different React instance than its renderer. `workspace-versions.test.ts` pins one range per package across every manifest.
Every member of `CueRuntime` on this target, and almost no new logic: the write queue, the op-log restore, the startup reconcile, the activities freshness gate, the persisted query cache, the sign-out teardown and the dead-token exit all arrive with `@cue/core` and are wired here rather than rewritten. The spike's parallel context, six members of the port under a different name, is not carried forward in any form. The module carries two native classes because both platforms need both, and an Expo local module is one native compilation unit. `CueHaptics` is the seven-verb vocabulary the Capacitor shells already ship, ported to Swift and Kotlin: generators built once and kept warm on iOS, action-oriented `HapticFeedbackConstants` with their per-API-level fallbacks on Android. `expo-haptics` cannot be the answer here: it exposes no `prepare()`, so a threshold tick arrives late enough to read as belonging to some other gesture, and its Android side funnels everything into a raw `Vibrator` waveform, which is what Android's own haptics guidance tells you not to use for touch feedback. `CueLegacyPreferences` reads Capacitor Preferences through each platform's own key shape, which are not the same shape: iOS prefixes the storage group onto the key inside `UserDefaults`, Android makes the group the shared-preferences file name and leaves the key alone, and a migration that assumes one reads nothing on the other platform. Two key-value stores behind one interface. The Keychain holds the token and nothing else, at `WHEN_UNLOCKED_THIS_DEVICE_ONLY`, which is what finally closes the iOS half of the storage caveat: the token no longer rides in a store every device backup includes. `expo-sqlite/kv-store` holds everything durable, and because it and the preferences now share one database, preferences are written under `pref.` rather than `cue.`: a sign-out that cleared `cue.` would take the durable write queue, the freshness baseline and the install marker with it, and losing the install marker makes the next launch look like a fresh install to the purge, which then clears the token. Two things run before the runtime is built, in that order, because both change what the token store holds. The reinstall purge clears the Keychain when the bulk store carries no install marker: Expo documents that SecureStore items survive an uninstall on iOS, so without this a user who deletes Cue and reinstalls it comes back apparently signed in with a stale token and none of their local state. Then the Capacitor migration, which is pure over a port and therefore a unit test rather than a device session: it adopts the legacy token on every boot and leaves it in place while a Capacitor build is still a rollback target, takes the legacy op log away on read because a replayed op is a duplicate play, parses that op log through a schema because it was written by another build at an unknown version, and seeds the first-mark caption as seen. The Web Crypto surface the shared OAuth code is written against is installed from a plain function rather than from an import side effect, so organize-imports cannot move it, and rather than from a React component, so it is not a global write at a moment nothing controls. `TextEncoder` is deliberately not shimmed: a hand-written UTF-8 encoder is right until it meets a non-latin1 input, and a crash at first sign-in is better than a challenge that hashes the wrong bytes. The `KeyValueStore` contract suite is `@cue/core`'s, run over both native backends rather than transcribed, which is what the `vitest` shim under `__tests__/support` is for. The composition root is rendered under RNTL for the one property nothing else can check: which of the two branches the gate takes after the purge and the migration have had their say. `neverRejects` moves the "none of these ever rejects" half of the reminders port into the port itself, so the two adapters state the policy once between them instead of once each.
…modal
Four tabs, fixed at four for every user, on four distinct paths. Fixed, because
media visibility is a reversible preference two screens away in Settings and a
navigation bar that changes item count when a switch flips is the least
predictable thing a shell can do: Apple's tab-bar guidance names the case
outright and Material's navigation bar says destinations do not change, so a
movies-only user keeps four tabs and the two with nothing in them say why.
Distinct paths, because the native-tabs navigator does not forward
`initialRouteName`, so four groups all serving `/` land on whichever is
alphabetically first; a path per tab works around that and makes every tab
deep-linkable besides.
Show detail, movie detail and the episode sheet are written once, in an
array-group directory that expo-router duplicates into each of the four groups.
The tab bar stays visible on push and each tab keeps its own back stack, which is
what a `UINavigationController` inside a tab does natively and what the per-tab
duplication would otherwise cost.
Two facts about that shape were found by running it rather than by reading about
it, and both are recorded where they bite. `unstable_settings`' per-group keys
carry no parentheses, because `matchLastGroupName` strips them before the
lookup, and with a parenthesised key every group silently falls back to the first
route in the tree: four tab stacks opening on `show/[showId]` with no id, four
GETs for `/shows/NaN` on every launch, and nothing on screen to show for it. And
even with the key right, `unstable_settings` had no effect on a tab press, so the
layout that ships declares each tab's own root first and relies on declaration
order, which is what the navigator actually falls back to.
The account area is one full-screen modal stack over the tabs rather than a route
inside whichever tab happened to be selected: a modal always has a real parent
and always dismisses back to where the user was, and full-screen rather than the
default because `"modal"` resolves to a page sheet on iOS and this is a task area
with three screens and its own back stack.
The episode route stays a child of the show route and presents as a `formSheet`
at the web app's own two detents, so a cold deep link paints the show underneath
and dismissing is one pop, with UIKit owning the physics.
The URL state the web app validates is validated here by the same two parsers,
because a route parameter is untrusted text on both targets. Path ids get the
same treatment in `route-params.ts`: `Number("")` is `NaN` rather than an error,
so a link that goes nowhere reaches the not-found screen instead of becoming a
request for `/shows/NaN`.
The screens are placeholders that render real data through the shared hooks, and
carry no styling: what they have to prove first is that the read path, the
persisted cache and the write queue already work here.
25e77e7 to
5b9f030
Compare
Run repository lint and the aggregate check with error-on-warnings so stale suppression diagnostics block the gate.
Enable Biome cognitive complexity errors at the default threshold and document each existing exception at the function that owns its inherent branching.
Measure the initial load and all JavaScript and CSS with the size-limit file plugin after a clean web build. Keep both budgets at the measured baseline plus twenty-five percent.
Persist the adopted token digest in bulk storage so each legacy token is migrated once. Cover relaunch after sign-out, changed legacy credentials, and rollback key preservation.
Render a Done action in the profile header and dismiss the full-screen account stack through Expo Router. Cover the initial route and dismissal behavior on iOS and Android.
Export both native platforms and measure their raw Hermes bytecode against scoped size-limit budgets in the native Android job.
Run Biome at threshold one over an unsuppressed product-source shadow and emit deterministic aggregate metrics. Fail if Biome output no longer matches the parser contract.
Classify tracked product TypeScript lines as code, comments, or blank and report Sonar-style density per package and in total without enforcing a threshold.
Build web and Expo outputs in a selected tree, apply the head size definition, and emit size-limit JSON without allowing a missing artifact to become a successful zero.
Append optional bundle size, cognitive complexity, and comment density tables to the footprint report while preserving its existing output when metrics are absent.
Install the pinned toolchain, build both worktrees in one run, collect size, complexity, and comment metrics, and pass the combined results to the existing single-comment report.
Assert the release APK byte size immediately after assembly and before manifest verification, using Google Play's 200 MB compressed download ceiling.
Parse every variant in the Xcode thinning report and reject the signed build before upload when its largest compressed app size exceeds 200 MB.
Add the npm ecosystem at the workspace root so Dependabot proposes lockfile updates for every pnpm package.
Analyze JavaScript and TypeScript source plus GitHub Actions on pull requests, main pushes, and a weekly schedule with pinned CodeQL actions.
Add the CodeQL context to the shipped-commit gate and read that context from its separate workflow check suite while keeping CI job matching suite-scoped.
Keep release path filters, architecture rules, spelling, required-check tests, and the persisted source witness aligned with the new non-shipping gates and reports.
Give each language matrix leg a distinct required context and run both contexts on release branches so every mobile release can satisfy the shipped-commit gate.
Every failure the app has to survive is a property of the server, so the mock now models them rather than leaving each test to intercept requests: a rate limit that closes the window with Retry-After, a 5xx, a slow answer, a connection that hangs, a connection that dies, and a write Trakt accepts but never applies. Armed by MOCK_TRAKT_FAULTS at boot or POST /__fault at runtime. Also expose the response headers the app reads. Retry-After and X-Pagination-* are not CORS-safelisted, so cross-origin the browser was handing the app a response with them stripped, and the mock silently stopped modelling pagination and backoff at all.
Every read turned its failure into a bare Error, so a screen could tell that something went wrong and nothing else. A rate limit, an outage and a 5xx take different copy and different retry policy, and the app was calling all of them the same thing. unwrapRead is now the one place a read becomes a throw, and it throws the failure. The shared rate-limit pause becomes observable for the same reason: it is the app's answer to what is happening and when it retries, so the UI can read it instead of inferring an outage from a query that happens to be failing. It is also raised for the 429 that spends a read's retry budget, since the window is still closed whether or not that particular read had attempts left.
Two things the owner sees are the app describing its own state wrongly. A rate
limit reads as "Trakt unreachable" over data that is on the screen and fine,
because every read failure collapsed into one boolean and a query never retried
itself. And a marked row stays green for as long as its write stays undelivered,
because the check waited for a progress re-read that a deferred write never
triggers, so several rows sit green at once and the app looks stuck.
The rules now live in one pure module both apps read from:
- A rate limit is not an outage. It says so, says when reads resume, and
offers no Retry, because the retry is automatic. Once the window reopens
and the read has given up, it says that instead, with a Retry.
- A failed refresh over cached content is a note, never the screen's error.
With nothing cached the strip stays silent and the screen carries it, so
"showing your cached data" is never printed over an empty screen.
- Reads retry themselves, waiting exactly as long as Trakt asked, on the same
backoff ladder the write queue already uses.
- A mark advances its row on the clock. The durable queue guarantees delivery,
so nothing about the row waits for the round trip. Green is the undo window
and nothing longer; past it the row reads as advanced, and a mark still on
its way is a quiet indicator rather than a green check.
The three view hooks that hand-copied the query status fields now extend the
shared shape, which is what queryStatus was for.
The strip takes its screen's read status and renders whatever the contract returns, deciding nothing. The queue rows and the marquee take their grammar from the shared hook, so the check has a third state: advancing, the row that has moved on while Trakt names its next episode. It is dimmed rather than green, because green means take-back-able and this no longer is, and it carries a quiet dot once the mark is past its undo window and still on its way. A screen with nothing cached now says what actually failed under its title, instead of blaming the connection for a rate limit.
The one native screen that marks anything takes its check grammar and its ambient line from the same module the web screens do, so the contract is proved consumable on this target before the visual layer exists.
The mock lane gains the two the owner reported: a 429 burst that must not read as an outage and must clear itself, and a mark that must advance its row in the frame of the tap and clear its pending note when the write lands. Both are driven by arming the fake Trakt rather than by intercepting requests, because both are properties of Trakt's answers and the native app has to survive the same ones. The hermetic specs move with the behaviour. Two of them failed a single read and expected an error state; a single failed read is now absorbed by the retry, so one becomes the case that proves the blip never surfaces and the others fail for as long as it takes the budget to run out. Retry appears only once the app has stopped trying on its own.
Fastlane runs shell steps from the fastlane directory, so the relative IPA path never resolved and the version check failed after a successful archive. The lane now checks the absolute path gym returns.
|
Merged: the Capacitor shells removed, 4,300 lines deleted and 147 added (net -4,153). Bundles: web initial load -3.3 kB (135.8 kB), web all js and css -5.2 kB (225.6 kB), Expo iOS bundle -43 B (5.03 MB), Expo Android bundle -23 B (5.06 MB), Play download -87 B (16.85 MB). Functions over cognitive complexity 15 unchanged at 11, worst 41, all in the web UI. The Capacitor iOS and Android shells, their five packages, the platform seams, the ios and android CI jobs and the Capacitor release lanes are gone; the Expo app is the only native codebase. The reader that migrates the old app's data on first launch stays until its sunset. |
Expo substitutes public environment values when Metro builds the JavaScript bundle. Supplying the client id only during prebuild left the shipped bundle empty and made startup throw.
A single Expo interface keeps product behavior and release gates focused on the UI that ships.
Headless checks against the fake Trakt preserve transport guarantees without coupling them to a rendered interface.
Removing dependencies and exports without a remaining consumer keeps the native workspace and its quality budgets honest.
|
Merged: the Android launch crash fixed, 118 lines added and 3 deleted. Bundles: web unchanged, Expo iOS bundle -99 B, Expo Android bundle +8 B, Play download -11 B. Build 1201 died at boot with "EXPO_PUBLIC_TRAKT_CLIENT_ID is not set". The release workflow set the variable on the prebuild step only, and Metro inlines it while bundling inside the Gradle and Xcode builds, which ran without it; both platforms shipped an empty client id. The Fastlane steps and the CI Android bundle now receive it, a workflow test requires it on every bundle-producing step, and a new android-e2e job installs the signed CI build on an API 35 emulator and fails on any fatal exception at launch. |
…ve-web-ui # Conflicts: # .dependency-cruiser.cjs # .github/workflows/ci.yml
The merge brought two release steps that read the old repository variable name beside two that read the new one. The web-only tsconfig the dependency-cruiser options pointed at left with the web app, so the option and its import go too.
|
Merged: the web app removed, 30,797 lines deleted and 891 added (net -29,906). Bundles: Expo iOS bundle -1.8 kB (5.03 MB), Expo Android bundle -1.9 kB (5.06 MB), Play download -100 B (16.85 MB); the web size ceilings left with the web build. Functions over cognitive complexity 15: 0 (was 11, all in the web UI); worst function now 15. The DOM application, its unit tests, its Playwright suite and its Vite tooling are gone; the Expo app is the only UI over the shared core. Twelve network-behaviour checks that only the browser suite exercised (per-flow request budgets from the fake Trakt's journal, the fault modes, token refresh, the rate-limit pause) now run headless in the core's harness against the same fake Trakt, each proven by a mutation. The repository-level CI tests moved to a root test project. Coverage percentages rose in every category. |
Reads need reusable options for screens, prefetches, cache writes, and tests without React subscriptions. Move pure values and module state to their owning layers, and gate the boundary so new wrappers cannot rebuild the old shape.
The home screen should compose its own query data while behavior remains in focused hooks. Scoped mark selectors keep unrelated rows out of record updates, and the performance baseline now exercises the real subscription path.
Native owns the show and episode compositions, while core keeps only reusable query factories. Mark feedback now presents from the write controller that owns each outcome.
Library grouping is pure reusable logic, while the native route owns its query subscriptions and screen-specific composition.
Screens and the reminder driver now subscribe through shared calendar query factories, while calendar slicing and visibility remain pure domain functions.
Search keeps only its interaction state as a core hook. Native screens now subscribe through the search and movie query factories they render.
The diary owns its infinite query and grouping, while exact-play removal remains a tested core write controller. Profile reads now use their query factories directly.
Every included core source directory now has a deterministic coverage floor. The final thin library-entry wrapper is replaced by a direct selection from the shared snapshot.
|
Merged: the core's read layer reshaped, 1,662 lines deleted and 1,403 added (net -259; core source -787, native screens and tests +528). Bundles: Expo iOS bundle -10.1 kB, Expo Android bundle -10.0 kB, Play download -3.0 kB. Reads are query option factories under the core's |
|
Merged: the startup-time gate, 348 lines added and 12 deleted. Bundles: Expo iOS bundle +478 B, Expo Android bundle +567 B, Play download +155 B. The simulator lane now measures a returning user's launch: the app publishes the time from process start to its idle marker (React Native's own startup timing) in the accessibility tree, and a second-launch Maestro flow reads it and asserts it against a ceiling kept in a ratchet file that carries its measurement, run and date and can only be edited downward. The ceiling is 600 ms from a measured 466.5 ms plus the observed run-to-run band; a planted three-second block at boot failed the flow on a throwaway PR, and raising the ceiling fails the ratchet test. The runner-timed launch-to-idle duration is reported in the job summary. |
Diff footprint
Product lines: code +9997 / -0 (net +9997), comments +2997 / -0, blank +1122 / -0 Bundle size
Complexity and comments
The merge base does not contain the measured packages, so its columns read n/a. Expo Atlas top contributors
|
Cue becomes a native app. iOS and Android ship as one React Native app built with Expo,
with platform navigation, gestures, haptics and notifications, over a shared TypeScript core.
The web app is retired with the shells; a PWA, if kept, is Expo's web export of the same screens. When this merges the repository reads as an
Expo project: the Capacitor shells, the web UI and their plumbing are gone,
packages/coreholds the domainand the Trakt data layer once, and
packages/nativeis the app.Status
Every user-visible change lands with its screenshots and recordings attached to its merge
comment below, and each screen ships to TestFlight and Firebase App Distribution from
release/expoas it lands, so the app is tested on a phone, not from clips.Landed, each part verified before merge (root
pnpm check, native jest on both platforms,Playwright, the fake Trakt lane twice, the native launch flow on a signed simulator build):
@cue/coreextracted with zero cache busts for shipping users; the corecannot import an app, and it typechecks and tests itself without one.
the platform adapters, a local Swift and Kotlin haptics module, device-code OAuth with
PKCE, migration of the token and the pending write queue from the previous shell.
Retry-After, an observable rate-limit pause, a
retryingstate, the mark control's threestates, one episode mark costing three requests instead of nine on the seeded account.
downward-only suppression baseline (core at zero), zero type suppressions, zero clones,
size ceilings that can only be lowered plus a per-PR size delta gate, the Play download
estimate at Play's reference density, the asset allowlist, CodeQL, and a footprint comment
recreated on every push with line, bundle, complexity and attribution deltas.
and Material's scales, snackbar hosts, the accessibility id vocabulary gated both ways.
fault endpoints, a deterministic native build with its framework linkage asserted.
view-status shape, one write-outstanding state, the write lock keyed per show.
"On the way", the History footer, the lapsed drawer, every state, light, dark and the
largest text size; media in its merge comment.
release/expo, version 2.0.0.movie detail, History, Profile, Settings, Onboarding.
Type passes; per-episode local notifications for the shows being watched.
How to read the footprint comment
The merge base has no
packages/tree, so the bundle and complexity base columns read n/a onthis PR. Merge-scoped runs (base pinned to the previous tip) are posted in the comments below
as each part lands.
The workspace split
packages/webis the Vite app.packages/coreis@cue/core: the domain, theTrakt data layer, the durable write queue, the runtime and composition root, the
auth store, the hooks, the stores, the preferences and the URL parsers, plus the
ports each app fills (key-value storage, preference storage, token storage,
haptics, reminders, connectivity, app visibility, the OAuth redirect handoff).
It is TypeScript source with no build step, reached through one wildcard subpath
(
@cue/core/domain/up-next), and it contains no.tsxand no.css.The rules are enforced rather than agreed. dependency-cruiser grew from 8 rules
to 16 over
packages, every one of them exercised against a planted violation:the core cannot import an app, the domain cannot reach the data layer, a port
cannot grow an implementation, the web app owns the DOM and the native app owns
Expo, and neither app may import the other.
pnpm check:core-portableassertsthe core's file types out of git rather than out of a config, and biome bans
eight browser and node globals inside it.
Behaviour-preserving for the web app, proven
The persisted query cache is keyed on a buster that used to be a Vite
definehashing three source trees by path, so the workspace move alone would have
dropped every shipping user's cache, and the extraction would have dropped it
again.
scripts/write-buster.mjsreplaces it with a path-independent shapewitness: it hashes the trees that define every persisted shape, in source order,
with each import statement's specifier collapsed to the digest of the module it
resolves to, so a file that moves or an import that is respelled produces the
same witness, while a field added to a type does not.
pnpm buster:checkfailsthe build while the committed witness and the computed one disagree.
The buster literal is seeded with the value main's own build already produces, so
this branch ships zero cache busts:
PERSIST_BUSTERvite build --mode testd0c3d97c58b8vite build --mode testd0c3d97c58b8Both were read out of the built
dist/assets/index-*.js, and main's value alsorecomputes from source with the old function's own logic.
buster:checkat thetip reads
shape b76d12c06a3e, buster d0c3d97c58b8: the two differ, which iswhat a mechanism change with no shape change looks like.
packages/nativeAn Expo app on expo-router, running the shared core through its own composition
root. What is in it:
expo-sqlitekey-value store,
expo-secure-storefor the token, a synchronous preferencestorage over the same database in its own
pref.namespace,expo-networkfor connectivity,
AppStatefor visibility,expo-notificationsfor thereminders planner (inexact triggers, since the exact-alarm permission is
blocked), and
expo-applicationfor the version.seven-verb haptics port with each platform's own system feedback, plus the
read side of the previous shell's stored preferences.
expo-cryptoratherthan a hand-rolled encoder, because a phone has no page to redirect.
pending write queue is read once and removed, so an upgrade in place lands
signed in with its undelivered writes intact and can never replay them twice.
the account area as a full-screen modal.
app.config.ts, which blocks 25 permissions thedependency tree would otherwise ask for and adds none of its own; the release
APK asks for 7.
ios/andandroid/are generated rather than committed, so both projects arerebuilt from
app.config.tson every build.What is deliberately not here
The screens. Everything under
packages/native/src/screensis a placeholderthat renders real data from the shared hooks with no styling: rows of text, no
artwork, no theme. They land next, on this same branch, as it is worked. The
scaffolding is complete: routing, the composition root, the ports, auth,
migration and the gates are all in place and exercised on a device, so the
screens are the remaining work rather than the risky part.
Also not here, and tracked as the next steps after the screens: the art pipeline
(the shared hook still hands
Elementto its consumers), the theme port, asnackbar host on the native side, and the Maestro launch flow.
Verification
pnpm checkat the root: biome, dprint, cspell, three tsc programs,dependency-cruiser over 507 modules and 1,741 dependencies, knip, jscpd,
buster:check,verify-bundle, and vitest with coverage: 106 files, 875tests. Then jest-expo: 8 suites, 70 tests across the ios and android projects.
packages/web: chromium 239 passed, mobile-chromium 40 passed,and the mock-mode equivalence lane 16 passed, which drives 15 scripted flows
against the local fake Trakt with no interception and fails if any of the six
write paths stops being sent.
assembleDebugplusverify-apk.sh(9.9.9 (42), 5 permissions, backup off, every storage domain excluded from both
channels), and the native app's
assembleReleaseplus the same check on theexpo line (7 permissions, same privacy assertions).
xcodebuildand run on an iPhone 17 Prosimulator against the local fake Trakt. It signed in through the device-code
grant (the request log carries
POST /oauth/device/codewith an S256challenge, then
POST /oauth/device/tokenwith the matching 43-characterverifier) and painted Up Next from the shared
useUpNexthook with the fournative tabs beneath it.
The numbers
507 files changed, 13,502 insertions, 1,946 deletions. Of those, 338 are renames
(153 byte-identical, 185 carrying an edit), 133 files are new, 23 are modified in
place and 12 are deleted: most of the diff is the move and the import rewrite,
not new code.
The web app's 24,211 product lines become 11,212 in
@cue/coreand 13,548 inpackages/web, so 45 percent of the product code now runs on both targets, at acost of about 550 lines for the ports and seams.
packages/nativeis 2,621tracked lines: 1,016 of composition root and adapters, 378 of route tree, 323 of
local module (236 of them Swift and Kotlin), 441 of tests and 223 of config
plugins.