Conversation
25e77e7 to
5b9f030
Compare
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
|
Merged: engine hardening for the native scaffold. The legacy token is adopted only when the previous shell actually stored one; the account modal gained its Done item with a router-level test that the modal closes; PlistBuddy failures in the iOS privacy script surface through the script's own exit path and the aps-environment key follows the build configuration; the expo-crypto digest shim has its own test. Verified with root pnpm check, native jest on both platforms, and a full CI run. |
|
Merged: the deterministic checks. Biome runs with warnings as errors and a cognitive complexity gate at 15, every suppression carrying a written reason. size-limit budgets cover the web initial load, all web JS and CSS, and both Hermes bundles. The release lanes assert the APK and the exported IPA against download ceilings out of the built artifacts. CodeQL scans source and workflows and gates the mobile release. The footprint job measures base and head in one run and recreates its comment on every push. Every gate was shown to fail under a planted violation before it was kept. |
|
Merged: the sync contract. Read failures are typed rather than collapsed to one flag. The read pool owns rate limits, honouring Retry-After and publishing one observable pause that every screen reads; the query layer owns transport and 5xx retries, so each failure kind has exactly one bounded ladder. The strip's states, in precedence: offline, rate limited with a live countdown, retrying, unreachable with the cause named and a Retry action, pending. A mark is green for the 5 second undo window, the same number the snackbar uses, then advancing, checked and disabled with a quiet dot while the write is queued, until Trakt confirms and the next episode takes the row. The fake Trakt gained fault modes (429 with Retry-After, 5xx, delay, hold, drop) and a reset control, and the mock lane drives both defects end to end. Merge-scoped footprint against the previous tip: product +1173, tests +1789, web initial load +1.2 kB, Hermes bundles +47.8 kB iOS and +32.2 kB Android, complexity profile unchanged. |
|
Merged: the native design foundation. packages/native/src/ui holds the tokens (dynamic colour pairs on iOS, scheme-resolved on Android, gated token for token against the web stylesheet's two theme blocks), eleven type roles per platform bound to Dynamic Type styles and Material 3 tokens with a test anchored to Apple's Large sizes and the Material scale, the snackbar hosts for the root, the sheet and the account modal, the app-idle marker, the accessibility id vocabulary, and the first primitives (row, check control, section header, empty state, sync strip). The splash stays up until the stores and fonts settle. Screen readers get the longer snackbar window. |
|
Two fixed behaviours, recorded on the web app running against the repository's fake Trakt ( Marking an episodecue-marking-an-episode.mp4First take: the check on the queue row is tapped and turns green, and the snackbar offers Undo ("Midnight Cartography S2 E3 marked"). The row advances to S2 E4 in the same frame, on the clock rather than on a round trip. The write is held by the fake for 7 seconds, so the undo window ( The sync strip under faultscue-sync-strip-under-faults.mp4Healthy is silence: no strip. One 429 with Two things in the recordings are worth a look before the native screens copy them:
|
|
Merged: size and quality budgets anchored to published norms. A committed quality baseline ratchets the count of cognitive-complexity suppressions (21 today) and the worst measured complexity (71 today) downward only, with zero suppressions allowed under the native app, and the check refuses a baseline that outruns the measurement. TypeScript suppressions are rejected across every package's source, tests, app routes and modules. Android release builds now ship with R8 and resource shrinking; CI builds the App Bundle, estimates the Play download for an arm64 xxhdpi device with bundletool (17.4 MB today, gated at 20 MB), gates the bundle itself, and derives the universal APK for the permission gate. The old 200 MB universal-APK ceiling is gone. The footprint comment reports comment density per package. Two gates written for the deleted web app (Vite size budgets, Lighthouse against vite preview) were dropped before merge, since the web UI is retired with the shells. |
|
Merged: the Maestro launch harness and the fake Trakt's missing controls. The fake Trakt gains six selectable seed states behind its reset control and the fault modes the parity flows need (a one-shot 401, a refused refresh, a 429 mid fan-out, a held and a dropped write, a failing history page, a pre-seeded op-log), each proven observable from a client. A Maestro launch flow asserts the onboarding screen, the connect button, the device code and the arrival on Up Next, with a test that every id a flow names exists in the app's id vocabulary. CI's iOS job now uploads the simulator app and a new native-e2e job on macOS installs it, boots a simulator, starts the fake and runs the flow. Getting that job green found and fixed a real launch defect: an unsigned simulator binary carries no entitlements, so every Keychain call failed and the auth store never left loading; the build is signed for the simulator now, the packaging step asserts the entitlements section is present, a token store that cannot answer drops to onboarding with a message instead of hanging, and the boot gates render a one-point frame so a hierarchy dump names whichever gate is holding. |
|
One mark no longer refetches the whole library. Marking an episode used to invalidate the Up A read's A mark never names an episode that has not aired. The optimistic advance projects the next Requests are bounded, and the fake can let go. Every Trakt request now aborts after 15 Cloudflare's answers stop reading as your connection. A 429 or 403 issued in front of the API The exact-alarm claim is corrected, not just deleted. The sync strip wraps. At phone width "Can't reach Trakt. Showing your cached data." was cut |
|
Merged Android. The release bundle is measured with bundletool at the device configuration Play Console itself reports against, XXXHDPI ARMv8, which means screen density 640 rather than the 480 that was there before. A second run measures every SDK, ABI, density and language combination and takes the largest. Both are held under 20 MB. On the merged tip CI reports 16,904,619 bytes at Play's reference configuration and 17,657,277 bytes across every configuration. The old ceiling on the App Bundle's own bytes is gone: every Play limit is stated on the compressed download, so an upload ceiling had no anchor, and the all-dimensions run does the job it was given properly. The font nobody imported. Expo Router's Android tab icons pulled in Budgets that can only go down. Every
The Play download budget is a decision rather than an anchor: 20 MB sits between Expo's minimal-app floor and the comparables, and Google's published curve prices the gap at roughly a point of install conversion per 6 MB. It re-anchors against the Play Console peer group once there is a production release. Growth needs a reason. The footprint job compares the merge base and the head for both Hermes bundles and the Play estimate, and fails above 64,000 bytes of growth. The only way past it is a Assets are enumerated. Every asset in the built export is listed with its bytes in Attribution. The footprint comment carries an Expo Atlas table of the largest contributing packages per platform, and the Atlas file uploads as an artifact, so a failing delta names its cause instead of leaving you to guess.
Two things worth knowing from the review before the merge. iOS has no download gate yet. Xcode documents its thinning export option as applying to non App Store exports only, so the App Thinning Size Report the branch tried to assert on is never produced by the release lane's App Store export, and every iOS build would have failed on the missing file. That assertion is removed and the IPA smoke ceiling stays. A real number needs either an ad hoc provisioning profile in CI for a second export of the same archive, or App Store Connect's App File Sizes after the first upload. The generated Android tab icons were invisible. Material Symbols path data is authored in the SVG viewBox |
|
The core's cognitive-complexity debt is retired. Nine functions carried a suppression; all nine are
What was split into what:
Behaviour is unchanged: every test that covered these paths passed before each split and passes now. Comments: 415 comment lines removed, 71 added, net -344 across the eight files, taking core Item 22's other target, core comment density at or under 14 percent, is not met and stays open: this Verified before merge: root
|
|
Merged: a deterministic simulator build. The native end-to-end job had gone red on every branch: React Native decides at pod install, through a live request to Maven, whether to use its prebuilt core, and when that request fails it silently builds from source without embedding React.framework while the Expo module frameworks still link it, so the app died in the dynamic linker at launch. Expo's precompiled modules are now off so the two halves are always built the same way, and the packaging step walks every embedded framework's load commands and refuses an artifact whose transitive frameworks are not in the bundle. |
|
Merged: Up Next on the native app. The queue rows are built on the foundation primitives with the three mark states (unwatched, just marked for the undo window, advancing), swipe to mark at the 96 pt threshold with the threshold and success haptics, pull to refresh that ends immediately during a rate-limit pause, the sync strip in flow, the marquee, "On the way" capped at three rows, the History footer, the lapsed drawer, and the empty, loading, degraded and offline states, in light and dark and at the largest accessibility text size. Two build defects were found and fixed on the way (an Expo UI release that does not compile against the pinned SDK, now pinned level with a test; a snack message built from a DOM element inside the shared store, which crashes a native text tree), and five visual defects were found by operating the screen on the simulator and fixed with tests. The placeholder onboarding screen is now readable on both appearances. Clips and stills below. clip-1-mark-undo-swipe.mp4clip-2-refresh-and-strip.mp4 |
|
Merged The native app syncs on its own now. The core no longer needs browser globals. A 40-line module used to install The accessibility ids are one vocabulary again. Render counts are gated. Three surfaces (the queue row, the check control, the Up Next screen) are measured with Reassure on every CI run and pinned at 2 renders with zero allowed deviation, so an accidental extra state update fails the build instead of arriving as a stutter. Durations are printed but do not gate: one machine's timing is not a threshold. The tests that hold the core to its coverage floors live in the core. 26 test files and their support moved from Duplicated concepts collapsed. One Also: a browser-hidden response is its own failure kind rather than a 503 Trakt never sent; pagination without headers keeps going until a short page instead of stopping after page one; concurrent identical reads share one request; write retry pacing keeps a 100 ms margin under Trakt's limit; and 420, 423 and 426 are named permanent account failures. Numbers, from the footprint. Nothing regressed: worst cognitive complexity 41 with 11 functions over 15, unchanged. Web initial load 139.1 kB brotli against a 170 kB limit, all JavaScript and CSS 230.9 kB against 285 kB, iOS bundle 5.12 MB against 5250 kB, Android 5.15 MB against 5400 kB, Play download 17.30 MB against 20 MB. 1,001 vitest tests and 288 jest tests pass and every coverage floor holds. Still owed: a startup timing gate. The simulator was unreachable where this was built, so there is no launch measurement and therefore no defensible ceiling to ratchet. That is the last item from the performance work. |
|
Merged: the native release lane, and a settle wait in the launch flow. The mobile release workflow gains a line choice, expo by default and capacitor for rollback. The Expo line prebuilds both platforms in the workflow, then fastlane builds the workspace with the existing signing, asserts the built IPA's version and build number against the values the config wrote, checks TestFlight for a strictly greater build number within the marketing version, and uploads to the internal group; on Android it builds the release App Bundle with R8 and resource shrinking, derives the universal APK with bundletool for the permission gate and for Firebase App Distribution, and checks the latest Firebase release the same way. The Expo line starts at version 2.0.0 so it never collides with the Capacitor 1.x builds. The launch flow now waits explicitly for the device-code screen after the connect tap instead of relying on the driver's automatic settle, which raced the screen transition. |
|
Merged: the push entitlement removed, 235 lines deleted and 20 added. expo-notifications wrote an APNs push entitlement into the iOS build, and the distribution profile has no push capability, so the first iOS archive failed. The module is out of the native app until per-episode notifications are built (local notifications need no push entitlement), along with its plugin, its permission suppressions and the entitlement check in the privacy script. |
|
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. |
|
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. |
|
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. |
|
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
|
Maestro's HTTP client is documented for script files with a config object, and the inline expression form left both detail flows dead at their first step.
A row that stops under the floating tab bar is visible to Maestro but its centre tap lands on the Library tab.
Release tags now have one strict version shape and must match the version embedded in the app. Removing branch releases makes the path partition and its architecture mirror unnecessary.
The release trigger no longer partitions tracked files, so its test suite no longer needs picomatch or its type declarations.
…d roll it up in a tested script
… flow timeout basis
…shot, and a background download waited on at once
Run every flow on Android and publish screenshots from both platforms
…l-platform-pass # Conflicts: # .maestro/flows/episode-sheet.yaml # .maestro/flows/launch.yaml # .maestro/flows/library.yaml # .maestro/flows/search.yaml # packages/native/src/TabStack.tsx
The footprint gate failed pull request #70, which changed only Maestro flows: the Play download estimate measured 20144986 bytes against 20144985 on its base b81bf85, while the tester APK and every JavaScript bundle measured the same. Both runs restored the fingerprinted Android build and re-embedded the JavaScript with scripts/reembed-android.sh, which compiled $work/index.android.bundle with hermesc. Hermes writes the source path it is given into the bytecode, and $work comes from mktemp, so each run's base/assets/index.android.bundle carried six different random characters and a different trailing hash. The length stays fixed, so the tester APK, which stores the entry uncompressed, kept its size; the Play estimate compresses the split APKs, so the random bytes moved it by one. scripts/compile-hermes.sh compiles from the bundle's own directory with a relative source path, and both re-embed scripts use it. The test compiles the same source in two fresh temporary directories and requires identical bytes; it fails when the script passes the absolute path.
Compile re-embedded Hermes bundles from a fixed path
Give the post sign-in app idle wait the app's full read retry budget
…traversal-dismiss
…s on first launch
Make the tab shell follow each platform: splash, search, bars, Done, tabs, sheet
Dismiss the episode sheet from its grabber in the Maestro flows
Ready the iOS simulator before Maestro and restart a driver that exits on first launch
Clear TestFlight export compliance and wait for processing
















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.
Coverage-Rationale: measured against main, 474 lines of packages/core have no test because the shared core predates the changed-lines gate, which every child pull request into feat/expo-native has passed since it landed; covering or deleting those lines is a plan item before this pull request merges, and the count is reported here so it cannot be forgotten.