PP-4020 Regression Report

Palace iOS 1.2.8 vs 2.2.5 — Side-by-side regression testing

9 sessions | April 8-16, 2026 | A1QA Test Library + 4 additional libraries

3
Regressions
30
Bugs Fixed
20
Improvements
15
Pre-existing
10
PRs (on refactor branch)

Side-by-Side Comparison

Hardened post-refactor build (left) vs 1.2.8 baseline (right). The left build includes all 9 PRs shipped during this sprint, currently on the refactor branch pending additional hardening before merge to develop.

Regressions (3)

Functionality that worked in 1.2.8 but is broken or degraded in 2.2.5.

F-001 PP-4105minorRegression

Account screen lost nav-bar title "Account"

1.2.8

Account screen displays "Account" as centered nav-bar title.

2.2.5

Nav bar shows only "< Libraries" back button; title missing.

Steps: Settings → Libraries → A1QA → Observe nav-bar
Notes: VoiceOver users lose screen name. Missing navigationTitle().
1.2.8
2.2.5
F-002 PP-4106majorRegression

Library card sign-up flow lost explanatory context

1.2.8

"Don't have a library card?" heading + location-check explanation + "Create Card" button.

2.2.5

Heading and paragraph gone. Only "Sign up for a library card" row.

Steps: Settings → Libraries → A1QA (signed out) → Scroll below form
Notes: Most user-visible regression. Recommend restoring copy before shipping.
1.2.8
2.2.5
F-032 PP-4107minorRegression

Unexpected login screen on library switch to A1QA

1.2.8

Switching between libraries does not prompt re-authentication.

2.2.5

Switching to A1QA presented a login screen once. Dismissing it allowed the flow to continue — borrow/return/playback all worked.

Steps: Libraries picker → Switch to A1QA → Observe login prompt
Notes: Intermittent — happened once. May be a 401 response triggering auth UI from recent session/token handling changes. Investigate TPPAccountSignInViewController presentation triggers. Could also be a backend session expiry edge case.

Pre-existing Bugs (15)

Bugs present in both versions. Not caused by the refactor.

F-012 PP-4112majorPre Existing

Revoke endpoint returns XML but client parses as JSON

1.2.8

403 XML response on revoke — JSON parse fails. Error hidden from user.

2.2.5

Same bug in both versions.

Steps: Borrow → Return → observe 403 handling
Notes: PRE-EXISTING. Worth fixing. XML/JSON content-type handling needed.
F-013 PP-4122blockerPre Existing

Local registry cleanup runs even when server return fails

1.2.8

Failed return still removes book locally — state desync.

2.2.5

Same bug in both versions.

Steps: Borrow → Return during instability → observe local vs server
Notes: PRE-EXISTING. Release-blocker class. Guard cleanup behind success.
F-033majorPre Existing

Credential bleed: A1QA credentials shown on Main Street account + sign-out loop

1.2.8

Switching libraries shows correct per-library credentials.

2.2.5

Initially showed A1QA credentials (01230000000237) on Main Street account screen. Attempting sign-out presented a modal sign-out screen repeatedly in a loop. Navigating away and back cleared the stale credentials — correct Main Street credentials shown after that. Borrow/return/playback worked fine once resolved.

Steps: Switch to Main Street → Settings → Account → Observe barcode field; attempt sign-out
Notes: ROOT CAUSE FOUND — see F-034. TOCTOU (Time-of-Check-Time-of-Use) race condition — a thread-safety bug where the credential read and the account switch happen at different times, allowing the wrong credentials to be returned in TPPUserAccount singleton since April 2020. Reclassified from regression to pre-existing: same vulnerable code in 1.2.8. Fix committed on modernize/whole-shot.
F-034 PP-4121blockerPre Existing PR #822

Cross-account credential contamination via TOCTOU (Time-of-Check-Time-of-Use) race in TPPUserAccount singleton

1.2.8

Same vulnerable code since 2020-04-08 commit 73eca3ed. Race window narrower pre-async/await.

2.2.5

Switching accounts while requests in-flight writes wrong credentials to keychain via TOCTOU (Time-of-Check-Time-of-Use) race. Causes persistent 401 loop + stacked sign-in modals with no escape.

Steps: Sign into 2 libraries → switch while catalog loads → observe 401 cascade
Notes: 6-year-old latent bug. Fix: use credentialSnapshot(for:) atomic reads + modal deduplication guard. 5 files changed. All tests pass. Backport to hotfix/release_2.2.5 recommended.
F-035 PP-4113majorPre Existing

My Books does not auto-refresh on navigation

1.2.8

My Books does not refresh on tab navigation — stale data until manual pull-to-refresh.

2.2.5

Same behavior — no auto-refresh on tab navigation.

Steps: Borrow a book → Navigate to My Books tab → Observe whether new loan appears without manual refresh
Notes: PRE-EXISTING in both versions. Should fix — users expect current state on tab switch. Add viewWillAppear/onAppear sync trigger.
F-062 PP-4116minorPre Existing

Double book presentation on rapid Read button taps

1.2.8

Also present — visible in side-by-side video comparison.

2.2.5

Tapping Read button quickly in succession can present the book reader twice — double presentation.

Steps: My Books → Tap Read rapidly (2+ taps) → Observe if reader presents twice
Notes: PRE-EXISTING. Visible on both versions in side-by-side video. Missing debounce guard on Read button action.
F-063 PP-4114majorPre Existing

Download buttons become stale/unresponsive on network loss

1.2.8

Download button becomes unresponsive after network loss.

2.2.5

Same behavior — buttons recover on next app foreground.

Steps: Toggle airplane mode during downloads → Try tapping Download
Notes: PRE-EXISTING. Both versions. Recovery on foreground is implicit not explicit.
F-065 PP-4115majorPre Existing

Holds state not syncing in search/list view after cancel

1.2.8

Cancel hold from detail view does not update search/list view state.

2.2.5

Same bug in both versions.

Steps: Search → Find held title → Cancel from detail → Return to list → Observe stale state
Notes: PRE-EXISTING. State update notification not reaching search list cells.
1.2.8
2.2.5
F-067 PP-4117minorPre Existing

Network loss during login shows invalid credentials error instead of network error

1.2.8

Network loss during sign-in shows invalid credentials message.

2.2.5

Same misleading error in both versions.

Steps: Enable airplane mode → Attempt sign in → Observe error
Notes: PRE-EXISTING. Error classification doesn't distinguish network from auth failure.
1.2.8
2.2.5
F-071 PP-4109majorPre Existing PR #828

Push notification routing reads wrong field — works by accident via keyword fallback

1.2.8

Reads "type" field from payload but CM backend sends "event_type".

2.2.5

Same code but also checks "event_type" after fix in PR #828.

Steps: Send FCM push with event_type field → Check if routing works
Notes: PRE-EXISTING since 2022. Found by reading CM backend source. Fix in PR #828.
F-072 PP-4109majorPre Existing PR #828

Push notification tap navigation fails — weak router reference goes nil when backgrounded

1.2.8

Router reference becomes nil when app is backgrounded — notification tap does nothing.

2.2.5

Fixed with pending navigation pattern in PR #828.

Steps: Background app → Send push → Tap notification → Observe navigation
Notes: PRE-EXISTING. Weak reference to router goes nil during background. Fix: pending navigation stored and executed on foreground.
F-079 PP-4110majorPre Existing PR #833

Empty device ID breaks cross-device bookmark sync for non-Adobe-DRM users

1.2.8

Annotations posted with device='' (empty string) when Adobe DRM not activated.

2.2.5

Same bug — code is identical in both versions.

Steps: API-level test: curl annotations endpoint — 23 of 255 annotations have empty device field.
Notes: Found via PP-4020 API sync test (scripts/test-sync.sh). Fix in PR #833.
F-081majorPre Existing PR #843

Overdrive borrow-from-hold fails with "wrong headers" (Code=609)

1.2.8

processOverdriveDownload passes the post-borrow /borrow URL straight to OverdriveAPIExecutor.fulfillBook() with no guard. CM returns a 200 OPDS atom entry (no x-overdrive-scope / x-overdrive-patron-authorization headers) → Code=609.

2.2.5

Same code path, same bug. No deferral guard.

Steps: Place hold on an Overdrive audiobook → wait for availability → tap Borrow → download silently fails with "wrong headers" in logs
Notes: Pre-existing since at least 1.2.8. Fix on modernize/whole-shot adds shouldDeferOverdriveFulfillment(for:state:) + deferOverdriveFulfillment(for:) — releases the download slot, triggers bookRegistry.sync(), and surfaces loanAlreadyExistsAlertMessage. Mirrors the existing guard in processRegularDownload. 6 unit tests cover the truth table (OverdriveDeferredFulfillmentTests.swift).
F-082minorPre Existing

ImageCache disk write fails on launch — "The folder doesn't exist"

1.2.8

Error logged repeatedly at launch: Cache disk write failed: The folder "urnuuidXXX" doesn't exist. Book-cover JPEGs fail to cache.

2.2.5

Same bug: TPPAppDelegate:44 calls clearCacheOnUpdate() which wipes Library/Caches/ImageCache/. The ImageCache.shared singleton caches the URL once at init, so later writes fail because the parent dir was deleted after init.

Steps: Install fresh build (or bump bundle version) → launch → observe repeated "Cache disk write failed" entries in console on every book-cover cache attempt
Notes: Pre-existing, found while dogfooding SpecterQA 13.1.0 on modernize/whole-shot. Fix: saveToDisk now recreates cacheDirectory if missing. Committed 90187ceeb. Runtime verification: 10+ errors → 0 on clean launch. Regression test added (testSet_afterExternalDirectoryDeletion_recreatesAndSucceeds).
F-083minorPre Existing

System URLCache SQLite open failures at launch

1.2.8

NSURLStorageURLCacheDB deleteAllResponses: ... Truncate Database failed: unable to open database file ErrCode: 14 + NetworkStorageDB:_openDBWriteConnections: failed to open write connection on every launch.

2.2.5

Same cascade of errors. Root cause: clearAllCaches() in GeneralCache also removed Library/Caches/<bundle-id>/ — the directory CFNetwork uses for the default URLCache's Cache.db.

Steps: Fresh install / version bump → launch → check console for NSURLStorage / NetworkStorage SQLite errors
Notes: Pre-existing. Related to F-082 (same clearAllCaches function). Fix: preserve Bundle.main.bundleIdentifier directory in the preserve list. Committed 90187ceeb. Regression test added (testClearAllCaches_preservesBundleIDDirectory).

Bugs Fixed (30)

Issues from 1.2.8 resolved in 2.2.5.

F-017majorFixed

PP-3704 GCS credential leak fixed — LicensesService uses ephemeral session

1.2.8

LicensesService shared URLSession could leak GCS credentials across requests.

2.2.5

LicensesService uses ephemeral URLSession — credentials never persist in cache.

Steps: Code review of LicensesService changes between 1.2.8 and 2.2.5
Notes: Security fix. Not user-visible but eliminates credential leak vector.
F-018minorFixed

Catalog covers float above section headings in 1.2.8

1.2.8

Covers render above section heading — overlap/misalignment.

2.2.5

Lanes render cleanly: heading on top covers below.

Steps: Catalog tab → Scroll to any lane
Notes: Layout bug fixed in 2.2.5.
1.2.8
2.2.5
F-029minorFixed

Book description raw HTML tags fixed

1.2.8

Raw <p> </p> tags visible as literal text in description.

2.2.5

Clean prose with proper paragraph breaks.

Steps: Search → Tap book → Read DESCRIPTION
Notes: Pre-existing 1.2.8 bug fixed. Visible on every HTML description.
1.2.8
2.2.5
F-037minorFixed

EPUB reader brightness slider fixed

1.2.8

Brightness slider in visual adjustments does not respond — no effect on screen brightness.

2.2.5

Brightness slider works correctly — adjusts screen brightness as expected.

Steps: My Books → Read EPUB → Tap page → Font/display settings (TT icon) → Adjust brightness slider
Notes: Pre-existing 1.2.8 bug fixed in 2.2.5. Evidence of refactor value.
F-039minorFixed

EPUB search results order fixed — now ascending by page

1.2.8

Search results displayed in reverse/descending page order (e.g. 41→14→34).

2.2.5

Search results displayed in correct ascending page order (e.g. 29→29→14→34).

Steps: Open EPUB → Search → Enter query → Observe result order
Notes: Pre-existing 1.2.8 bug fixed. Tested on Palace Bookshelf DRM-free EPUB.
1.2.8
2.2.5
F-040minorFixed

EPUB reader view no longer shifts when nav bar toggled

1.2.8

Content area shifts/resizes when nav bar and bottom bar appear — visible layout jump.

2.2.5

Nav bar overlays content without shifting — no layout jump.

Steps: Open EPUB → Tap to show/hide nav bar → Observe content position
Notes: Layout bug fixed in 2.2.5. Better reading experience.
1.2.8
2.2.5
F-041minorFixed

Catalog facet switching improved but still slow

1.2.8

Switching between catalog facets (All/Ebooks/Audiobooks) causes a flash of the catalog and noticeably slow loading.

2.2.5

Facet switching is smoother — no flash of catalog content. Still loads a bit slower than ideal.

Steps: Catalog tab → Tap between All / Ebooks / Audiobooks facets → Observe transition speed
Notes: Improved in 2.2.5 but further optimization possible. Consider caching facet results or prefetching adjacent facets.
F-043minorFixed

Book detail view gained Back button

1.2.8

No back button on book detail view — user must swipe back or use tab bar to navigate away.

2.2.5

Back button shown in top-left corner for explicit navigation.

Steps: Tap any book → Observe top-left nav area
Notes: Improves discoverability. Swipe-back still works as well.
1.2.8
2.2.5
F-045cosmeticFixed

Search results button styling now consistent with rest of app

1.2.8

Search result buttons (Download/Return/Get) use a different style — small outlined buttons inconsistent with buttons elsewhere in the app.

2.2.5

Search result buttons (Listen/Return/Borrow) use the same rounded button styling as detail view and My Books — consistent across the app.

Steps: Catalog → Search → Compare button styling to My Books and detail view
Notes: Design debt resolved. Unified button language across the app.
1.2.8
2.2.5
F-046majorFixed

Audiobook skip-forward 30s jumps backward

1.2.8

Skip forward 30s button repeatedly jumps to earlier points in the chapter instead of advancing.

2.2.5

Skip forward works correctly — advances 30s as expected.

Steps: Listen to audiobook → Tap skip forward 30s repeatedly → Observe playback position
Notes: Pre-existing 1.2.8 bug. Evidence of refactor value.
1.2.8
2.2.5
F-047minorFixed

Audiobook TOC icon invisible in nav bar

1.2.8

TOC icon is invisible in the nav bar — functional but impossible to see. Appears intermittently unresponsive.

2.2.5

TOC icon visible and responsive.

Steps: Listen to audiobook → Look for TOC icon in nav bar
Notes: Likely a tint/color issue on 1.2.8 rendering icon same color as background.
1.2.8
2.2.5
F-048majorFixed

Audiobook download hangs near end of title

1.2.8

Download progress appears to hang/freeze near the end of the title download.

2.2.5

Download completes smoothly.

Steps: My Books → Download audiobook → Observe progress near completion
Notes: Pre-existing 1.2.8 bug fixed.
1.2.8
2.2.5
F-049majorFixed

Audiobook bookmarks page inaccessible

1.2.8

Cannot access the bookmarks page — unable to verify if bookmarks work at all.

2.2.5

Bookmarks accessible and functional.

Steps: Listen to audiobook → Attempt to access bookmarks
Notes: Pre-existing 1.2.8 bug fixed.
1.2.8
2.2.5
F-050minorFixed

Audiobook playback starts before player UI presents

1.2.8

Audio playback begins before the player view is visible — UI-blocking background actions cause premature playback start.

2.2.5

Playback starts after player view presents as expected.

Steps: My Books → Tap Listen → Observe if audio starts before player appears
Notes: Threading/presentation order issue in 1.2.8. Fixed in 2.2.5.
1.2.8
2.2.5
F-051cosmeticFixed

Audiobook player UI and animations improved

1.2.8

Player transitions and animations are rough. UI less polished.

2.2.5

All animations and transitions are smooth and clean. UI is crisper.

Steps: Listen to audiobook → Navigate around player → Observe transitions
Notes: Significant polish improvement in 2.2.5.
1.2.8
2.2.5
F-052minorFixed

Audiobook resumes from last played position

1.2.8

Does not resume from last played location — restarts from beginning.

2.2.5

Picks up last played location on re-entry.

Steps: Listen → Exit player → Re-open same audiobook → Observe playback position
Notes: Important UX improvement. Users expect position persistence.
1.2.8
2.2.5
F-053majorFixed

Audiobook slow initial load on Overdrive titles

1.2.8

Very slow to load on first start — significant delay before playback begins.

2.2.5

Loads and starts playback promptly.

Steps: My Books → Tap Listen on Overdrive audiobook → Observe load time
Notes: Pre-existing 1.2.8 bug. Same skip/garble/TOC issues as L1 (F-046, F-047) also present on Overdrive titles.
1.2.8
2.2.5
F-054majorFixed

Audiobook garbles sound during skips

1.2.8

Audio garbles/distorts during skip forward and back operations.

2.2.5

Skips are clean with no audio artifacts.

Steps: Listen to audiobook → Skip forward/back repeatedly → Listen for audio distortion
Notes: Pre-existing 1.2.8 bug. Not observed on 2.2.5.
1.2.8
2.2.5
F-055cosmeticFixed

Audiobook slider transitions choppy

1.2.8

Scrubbing/slider transitions are choppy and jerky.

2.2.5

Slider transitions are smooth.

Steps: Listen to audiobook → Drag progress slider → Observe animation
Notes: Pre-existing 1.2.8 bug fixed.
1.2.8
2.2.5
F-056minorFixed

Audiobook bookmarks sync across devices

1.2.8

Unable to verify bookmark sync — bookmarks page inaccessible (see F-049).

2.2.5

Bookmarks sync correctly across devices.

Steps: Create bookmark on Device A → Open same audiobook on Device B → Check bookmarks
Notes: New capability in 2.2.5. Cross-device sync working for both position (F-052) and bookmarks.
1.2.8
2.2.5
F-057majorFixed

LCP audiobook instant checkout + background download with streaming

1.2.8

Checkout and download behavior less responsive. No streaming during download.

2.2.5

Checks out immediately. Background download begins automatically. Playback available immediately via streaming while download continues.

Steps: Borrow LCP audiobook → Observe checkout speed and playback availability
Notes: Significant UX improvement — no waiting for full download before playback.
1.2.8
2.2.5
F-058majorFixed

LCP audiobook same player bugs as L1/L2 on 1.2.8

1.2.8

Same choppy navigation/seeking. No TOC/Bookmark access from nav bar. Does not respect cross-device position sync.

2.2.5

Playback skipping and seeking flawless. Bookmarks and all functionality works. Cross-device position sync works.

Steps: Listen to LCP audiobook → Skip/seek → Check bookmarks → Check cross-device sync
Notes: Pre-existing 1.2.8 bugs consistent across all audiobook types (L1/L2/L3). All fixed in 2.2.5. Confirms the audiobook player refactor delivered value across all DRM types.
1.2.8
2.2.5
F-059minorFixed

Background download failure recovery improved

1.2.8

Download sometimes fails when app is backgrounded. No automatic recovery — user must manually retry.

2.2.5

Download also fails when backgrounded but auto-restarts and completes when app is re-opened.

Steps: Borrow title → Tap Download → Lock screen / background app → Re-open app → Observe download state
Notes: Both versions fail during background download (pre-existing limitation — likely iOS background execution limits). 2.2.5 improves by auto-resuming on foreground. Full background download support may require URLSession background configuration.
1.2.8
2.2.5
F-060minorFixed

Audiobook resumes playback on Bluetooth reconnect

1.2.8

Bluetooth reconnect does not always resume playback.

2.2.5

Playback resumes automatically after Bluetooth reconnect.

Steps: Listen → Disconnect BT → Reconnect → Observe playback
Notes: Both resume but 2.2.5 more reliable. Tested with AirPods Pro.
1.2.8
2.2.5
F-064cosmeticFixed

Inconsistent Reserve/Holds terminology unified

1.2.8

Mix of "Reserve" and "Hold" terminology.

2.2.5

Consistent "Hold" terminology throughout.

Steps: Browse holds-eligible titles → Compare button labels
Notes: Intentional cleanup of library terminology.
1.2.8
2.2.5
F-066 PP-4108majorFixed PR #825

Return button on search/list view not presenting confirmation alert

1.2.8

Return button on search/list view presents confirmation alert before cancelling hold.

2.2.5

Return button on search/list view appears inoperable — confirmation alert is not being presented. Likely a SwiftUI alert presentation issue in the list context.

Steps: Search for held title → Tap Return in list view → Observe if confirmation alert appears
Notes: Was regression in initial 2.2.5 build. Fixed in PR #825 (holds UI fix). Verified resolved in hardened build.
F-068minorFixed

Push notification: hold-available received and triggers sync

1.2.8

Hold-available notification received. No deep-link navigation.

2.2.5

Notification received and triggers Holds tab navigation + sync.

Steps: Send hold-available FCM push → Observe delivery and navigation
Notes: Automated test via Firebase service account. 2.2.5 adds Holds navigation.
F-069minorFixed

Push notification: loan-expiry warning received

1.2.8

Loan-expiry notification received and displayed.

2.2.5

Same — both versions receive and display correctly.

Steps: Send loan-expiry FCM push → Observe delivery
Notes: Both versions handle correctly. No regression.
F-070minorFixed

Push notification: deep-link navigates to Holds tab on tap

1.2.8

1.2.8 receives notification but no navigation on tap.

2.2.5

2.2.5 navigates to Holds tab on notification tap.

Steps: Send hold-available push → Tap notification → Observe navigation
Notes: New capability in 2.2.5. 1.2.8 had no deep-link routing.
1.2.8
2.2.5
F-080majorFixed

Sign-out hangs on 1.2.8 — requires force-quit to recover

1.2.8

Sign-out from A1QA hangs indefinitely. App becomes unresponsive. Requires force-quit and relaunch to recover.

2.2.5

Sign-out completes normally.

Steps: Settings → Libraries → A1QA → Sign Out → Observe if app responds
Notes: Found during side-by-side session 8 (2026-04-15). Pre-existing 1.2.8 bug, fixed in 2.2.5. Force-quit + relaunch recovered the session.

Improvements (20)

New features and intentional UX changes in 2.2.5. Each item is tagged as either a new feature or a behavior change.

F-004minorBehavior Change

Bottom tab "Reservations" → "Holds"

1.2.8

Tab: Catalog | My Books | Reservations | Settings

2.2.5

Tab: Catalog | My Books | Holds | Settings

Steps: Any screen with tab bar visible
Notes: Standard library terminology — intentional.
1.2.8
2.2.5
F-009cosmeticBehavior Change

Sync Bookmarks copy grammatically awkward when toggle on

1.2.8

"Save your reading position and bookmarks to all your other devices."

2.2.5

"Toggle on sync bookmarks to save..." — reads as command when already on.

Steps: Sign in → Settings → Libraries → A1QA → Sync Bookmarks
Notes: Low priority. Consider state-aware string.
1.2.8
2.2.5
F-011minorBehavior Change

Content Licenses visibility depends on signed-in state

1.2.8

Content Licenses only visible when signed in.

2.2.5

Visible in both signed-out and signed-in states.

Steps: Sign out → check → Sign in → check
Notes: Probably improvement — licenses should be reachable pre-auth.
1.2.8
2.2.5
F-019cosmeticBehavior Change

Catalog header consolidated — library name as nav title

1.2.8

"Catalog" title + separate library row below pills.

2.2.5

Library name + logo as nav title. Row eliminated.

Steps: Catalog tab → Compare nav bar
Notes: More compact. Intentional redesign — more compact header.
1.2.8
2.2.5
F-025minorNew Feature

Search gained live/auto-search

1.2.8

Nothing until search button tapped. Blank screen while typing.

2.2.5

Results appear as user types. Pre-search browsable content.

Steps: Catalog → Search → Begin typing
Notes: Significant UX improvement. Modern standard.
1.2.8
2.2.5
F-026minorNew Feature

Search results gained format filter pills

1.2.8

No format filter in search.

2.2.5

All / Ebooks / Audiobooks pills below search bar.

Steps: Catalog → Search → Search mixed-format term
Notes: Valuable for patrons seeking specific formats.
1.2.8
2.2.5
F-027cosmeticBehavior Change

Search button "Get" → "Borrow"

1.2.8

"Get" button on results.

2.2.5

"Borrow" button on results.

Steps: Catalog → Search → Compare button label
Notes: Correct library terminology. Intentional change.
1.2.8
2.2.5
F-028cosmeticBehavior Change

Search navigation "Back" → "Cancel"

1.2.8

"< Back" top-left.

2.2.5

"Cancel" top-right.

Steps: Catalog → Search → Observe dismiss control
Notes: Standard iOS search pattern.
1.2.8
2.2.5
F-030cosmeticBehavior Change

Borrow flow shows cancel option + sheet actions

1.2.8

Borrow starts immediately. No cancel option.

2.2.5

Sheet shows Cancel during borrow then Listen + Return on completion.

Steps: Tap Borrow → Observe sheet
Notes: Minor improvement — adds user control.
2.2.5
F-031minorNew Feature

Detail view auto-dismisses after return

1.2.8

User stays on detail screen with Borrow button — dead end.

2.2.5

Auto-dismisses back to catalog after return.

Steps: Borrowed title → Return → Return Loan → Observe nav
Notes: Eliminates dead-end screen. Clean navigation.
1.2.8
2.2.5
F-036minorBehavior Change

EPUB reader hides nav bar on open

1.2.8

EPUB reader opens with nav bar visible — back button + search + TOC + settings + bookmark icons shown.

2.2.5

EPUB reader opens with nav bar hidden — full immersive view. Tap to toggle.

Steps: My Books → Tap Read on EPUB (A Passion's Scourge) → Observe nav bar on initial open
Notes: Arguably better for reading immersion but changes discoverable navigation. Intentional — immersive reading default.
1.2.8
2.2.5
F-038minorBehavior Change

Content protection error on stale loan after return on another device

1.2.8

Stale loan remains readable after return on another device — no DRM check on open. Book removed after manual refresh.

2.2.5

Stale loan shows content protection error when attempting to read — DRM license check catches revoked loan. Book cleared after manual refresh.

Steps: Return book on Device B → On Device A open My Books (no refresh) → Tap Read on returned title
Notes: 2.2.5 is more correct (validates license). RECOMMENDED FIX: implement auto-refresh on My Books (F-035) — stale loans get cleared on tab appearance before user can tap Read, eliminating this error entirely. If auto-refresh is not feasible, improve error message to 'This book has been returned' instead of generic content protection error.
F-042minorBehavior Change

Download progress indicator removed from My Books list view

1.2.8

Download shows a black overlay with a progress indicator over the book cover in list view.

2.2.5

Download button shows loading state inline — no overlay and no progress indicator.

Steps: My Books → Tap Download on a borrowed title → Observe download feedback
Notes: Cleaner look — no layout shift during downloads (old version expanded a card that pushed content down). Downloads appear concurrent rather than sequential. Consider adding a progress ring or percentage to the button state for large downloads.
1.2.8
2.2.5
F-044cosmeticBehavior Change

Book detail view color gradient and button styling changed

1.2.8

Dark brown/black gradient behind cover. Read/Return buttons are outlined (white text/border on dark background).

2.2.5

Lighter warm brown gradient. Read/Return buttons are filled white with dark text.

Steps: Tap any book → Observe header gradient and button styling
Notes: Intentional visual refresh. Buttons now adapt color dynamically based on background for optimal contrast/readability. Better accessibility.
1.2.8
2.2.5
F-061minorBehavior Change

Download error handling improved but half-sheet sometimes hangs

1.2.8

Download errors show as inline text with Retry button.

2.2.5

Half-sheet error presentation — cleaner but occasionally doesn't dismiss.

Steps: Trigger download failure → Observe error UI
Notes: 2.2.5 improves error messaging but half-sheet can get stuck. Consider adding a Close button.
F-073cosmeticBehavior Change

U1: Catalog header redesigned — library name as nav title

1.2.8

'Catalog' centered nav title + library name/logo as separate row below filter pills. 3-tab bar (Catalog / My Books / Settings).

2.2.5

Library name + logo inline as nav title. Filter pills directly below. 4-tab bar (Catalog / My Books / Holds / Settings). Cleaner layout.

Steps: Compare U1-1.2.8-catalog-home.png vs U1-2.2.5-catalog-home.png
Notes: DISCOVERY: automated BrowserStack screenshot walker. Confirms F-019. Intentional redesign.
1.2.8
2.2.5
F-074cosmeticBehavior Change

U1: My Books header and sort control redesigned

1.2.8

'My Books' centered nav title. 'Sort By:' label + 'Title' pill left-aligned. Library logo centered below nav bar.

2.2.5

Library name as nav title with logo. Sort icon top-right ('Title'). No separate library logo row. 4-tab bar.

Steps: Compare U1-1.2.8-my-books.png vs U1-2.2.5-my-books.png
Notes: DISCOVERY: automated BrowserStack screenshot walker. Consistent with catalog redesign pattern.
1.2.8
2.2.5
F-075minorBehavior Change

U1: Settings gained section headers and Wi-Fi toggle

1.2.8

Flat list: Libraries / About App / Privacy Policy / User Agreement / Software Licenses. Compact centered title.

2.2.5

Sectioned layout: LIBRARIES / DOWNLOADS (Download Only on Wi-Fi toggle) / ABOUT AND LEGAL. Large title 'Settings'. 4-tab bar.

Steps: Compare U1-1.2.8-settings.png vs U1-2.2.5-settings.png
Notes: DISCOVERY: automated BrowserStack screenshot walker. New feature: Wi-Fi-only downloads toggle.
1.2.8
2.2.5
F-077cosmeticBehavior Change

U1: Book detail gained Back button and dark gradient header

1.2.8

'< Back' text button top-left. Teal/dark gradient. 'Borrow' + 'Preview' pills centered.

2.2.5

'< Back' with chevron icon. Darker gradient adapts to cover art. 'Borrow' button centered. Clean description rendering.

Steps: Compare U1-1.2.8-book-detail.png vs U1-2.2.5-book-detail.png
Notes: DISCOVERY: automated BrowserStack screenshot walker. Confirms F-043 and F-044.
1.2.8
2.2.5
F-078minorBehavior Change

U1: Lane list view redesigned — compact filters and Borrow buttons

1.2.8

'Newly Added for Adults' centered title. Filter pills row. 'Get' buttons. 3-tab bar.

2.2.5

Library name as nav title. Compact filter row. 'Borrow' + 'Preview' buttons per book. Larger cover art. 4-tab bar.

Steps: Catalog → More... on lane
Notes: Confirms F-027 (Get→Borrow). Filter UX improved.
1.2.8
2.2.5

Performance Profiling

6 Instruments traces on iPhone 17 Pro Max (Moes Max). Manual + automated walker flows. Fix shipped in PR #831.

Allocations — Headline Numbers

-81%
Heap Allocations
500 MiB → 93 MiB
-38%
Total Allocations
19.9M → 12.5M
-23%
Persistent Objects
600K → 460K

Allocations Comparison

Metric 1.2.8 2.2.5 Delta
Persistent heap allocations500 MiB93 MiB-81%
Persistent objects599,734459,694-23%
Total allocations19.9M12.5M-38%
Malloc 16-byte94.2 MiB3.7 MiB-96%
Stack VM6.95 MiB2.73 MiB-61%
CG raster data (decoded covers)15.5 MiB806 MiB+52x
Heap + Anonymous VM (total)674 MiB1.17 GiB+74%

CG Raster Data — Before & After Fix (PR #831)

The 52x CG raster spike was caused by decoded cover images not being evicted. PR #831 reduced max decode dimension from 1024px to 384px and added preparingThumbnail.

Metric Before Fix After Fix Improvement
Peak CG raster data589 MB363 MB-38%
Peak total footprint1,095 MB800 MB-27%
CG raster at idle~150 MB5 MB-97%
Compressed cache (ImageIO)50 MB204 MB+308% (expected)
Note: Compressed cache increase is expected — more images stay in JPEG form instead of being fully decoded. Net improvement: 295 MB less total footprint at peak.

Memory Footprint Over Time (After Fix)

Per-phase footprint during stress test: catalog scroll (25x), 3 filter switches, My Books, Settings, stress scroll (30x).

800MB
600MB
400MB
200MB
192
Idle
192
Scroll 10x
332
Audio
496
Ebooks
585
All
779
My Books
785
Settings
800
Stress
Peak at 800 MB after 30x stress scroll across all catalog filters. Memory stabilizes — no unbounded growth.

What Improved

Wins from SwiftUI migration

  • 81% fewer persistent heap allocations
  • 96% reduction in Malloc 16-byte (ObjC message passing overhead eliminated)
  • 61% less stack VM (fewer deep UIKit call stacks)
  • 38% fewer total allocations (less churn)
  • Retain cycles fixed in PR #811 (AudiobookDataManager, SpeechSynthesizer, PDFMetadata)

Found & fixed

  • CG raster 52x spike → fixed in PR #831 (-38% peak, -97% idle)
  • Decoded images not evicted on scroll → preparingThumbnail + 384px max
  • Async disk→memory cache promotion for instant cover display
  • Library switch now evicts decoded images immediately
  • My Books reload deferred when tab is offscreen

Crashlytics & Growth Trends

Firebase Crashlytics data from the 90-day window (Jan 11 - Apr 10, 2026). ~59,000 monthly active users, 99.59% crash-free rate. Live data updated weekly via Firebase MCP (see Live Update card below).

User Growth Through 2.x Releases

The 2.x releases did not cause user churn. Monthly active users grew 34% from the 1.2.8 era to today.

Period Version Monthly Active Users
Jul 20251.2.844,000
Oct 20252.0.047,600
Jan 20262.2.x54,900
Apr 20262.2.459,000

Fatal Crash Trend — 92% Reduction Across 2.x Releases

Each patch addressed crashers. User base grew during this period, so per-user crash rate dropped even faster.

Version Fatal Crashes Notes
2.2.01,495
Highest crash count
2.2.2890
-40%
2.2.4115
Current App Store release
2.2.5*4
Internal testing only
* 2.2.5 has not shipped — 4 fatals are from internal testing only.

Top Fatal Crash Issues

Issue Fatals Users Origin Status
recursive_mutex lock failed1,218200Pre-existingDRM thread-safety — since 1.2.4
HTMLTextView.makeAttributedString6873742.x newFixed
Failed to determine nav direction673416Pre-existingReadium bug — since 1.1.4
X509 CRL decoding (Botan DRM)48948Pre-existingDRM cert validation
TPPBookRegistry EXC_BAD_ACCESS3243012.x newPartially fixed
AdobeCertificate EXC_BREAKPOINT1271102.x newNeeds investigation

Non-Fatal Errors — Real vs Better Logging

The 2.x non-fatal spike is largely better instrumentation, not more bugs. Many errors logged in 2.x were happening in 1.x but silently swallowed.

Category Events Assessment
Bookmark cross-contamination115,612Tied to F-034 credential race. Fix in PR #822.
Audiobook open failures25,6442.x regression. Partially fixed (token refresh + LCP session).
Error posting annotation (400)210,063Expected — server returns 400 for returned books. 1.x silently ignored.
URLSession taskInfo lifecycle~128,000Normal HTTP/2 behavior. Now logged at DEBUG level.
Request cancellation21,072Intentional — safety mechanism during account switches.
Network/server errors~150,000Server errors, timeouts, feed parsing. Not client bugs.

F-034 Impact — Crashlytics Evidence

The credential contamination bug (F-034, fixed in PR #822) has indirect but strong evidence of affecting real users at scale:

115,612
bookmark errors
(wrong account context)
7,120
sign-in errors
(wrong credentials sent)
10,984
profile failures
(wrong account token)
Combined, these represent a substantial portion of non-fatal volume. The per-account fix (PR #822) should reduce all three post-deployment. Monitoring recommended for first 2 weeks.

Platform Distribution

56%
iOS (iPhone)
32%
iPadOS
12%
macOS (Catalyst)
macOS at 12% of fatals is disproportionate to its likely user share. May indicate Mac-specific DRM or layout instability.

Live Crashlytics Update

Fetched 2026-04-16 · 2026-04-09 to 2026-04-16
232
Fatal Crashes (7d)
62,583
Non-Fatal Events (7d)
9
Active Fatal Issues

Fatal Crashes by Version

Version Fatals
2.2.4 (454)196
2.2.2 (446)28
3.0.0 (453)8
2.2.5 (451)0
2.2.5 (453)0

Top Fatal Issues

Issue Fatals Users Origin Versions
X509 CRL decoding (Botan DRM) Repetitive887Pre Existing1.2.4 – 2.2.4
HTMLTextView.makeAttributedString 68562.X New2.2.0 – 2.2.4
recursive_mutex lock failed Repetitive6530Pre Existing1.2.4 – 3.0.0
Failed to determine nav direction 66Pre Existing1.1.4 – 2.2.4
TPPAppDelegate EXC_BREAKPOINT 51Internal Testing3.0.0 – 3.0.0
objc_release EXC_BAD_ACCESS Repetitive42Pre Existing1.0.37 – 3.0.0
AudioEngine chapter cache EXC_BREAKPOINT 432.X New2.2.2 – 3.0.0
AudiobookBookmarkBusinessLogic EXC_BAD_ACCESS Fresh332.X New2.2.4 – 2.2.4
PalaceAudiobookToolkit EXC_BAD_ACCESS Fresh222.X New2.2.4 – 2.2.4

Platform Distribution (Fatal)

31%
iOS (iPhone)
(73 events)
47%
iPadOS
(109 events)
22%
macOS (Catalyst)
(52 events)

Top Non-Fatal Issues

Issue Events Users
Error posting annotation (902)31,0574,424
Image decode failure (1501)14,0521,220
Network request failed: Problem Document (912)7,3472,774
Failed to parse data as XML (604)2,5481,557
Audiobook failed to open (401)2,3561,177
Request Cancelled (911)1,348556
Data source: Firebase Crashlytics MCP. Updated automatically during Claude Code sessions (weekly cadence). Snapshot: assets/data/crashlytics-2026-04-16.json

Engineering Governance

All work on this project runs through ForgeOS — an automated governance platform that enforces gates, tracks decisions, and builds institutional knowledge across sessions.

Gate Pipeline

Every changeset is risk-scored and routed through a 6-stage pipeline. High-risk changes (auth, concurrency, DRM) require all gates with full review coverage. Low-risk changes get a lighter path.

Intent
Scope defined
Initiative exists
Design
Architect review
(if risk > low)
Implementation
Build success
Lint pass
Verification
Tests attached
QA review
Hardening
Security review
Perf review
Release
Docs
Release notes
Gate promotion is blocking — PRs cannot be created until all required gates pass. This was made mandatory in April 2026 after the October-November rapid patch cycle showed the cost of shipping without systematic verification.

Risk-Based Review Routing

Changesets are automatically scored 0-100 based on which modules they touch. Higher risk triggers more reviewers and stricter evidence requirements.

Risk Level Score Required Reviews Example from this sprint
Low0-25Docs only
Medium26-50QA + DocsCrawler optimizations (risk: 35)
High51-75Architect + QA + Specialized
Critical76-100All 7 rolesF-034 credential fix (risk: 80)
The F-034 credential contamination fix scored 80 (critical) because it touched auth, concurrency, database, and security modules simultaneously — triggering architect, QA, security, performance, reliability, accessibility, and docs reviews before it could ship.

Shared Mind — Institutional Knowledge

Every lesson learned, architecture decision, and anti-pattern discovered is recorded to a persistent knowledge base. Future AI sessions query it automatically before writing code, so institutional knowledge compounds across sessions instead of being rediscovered.

292
Total Observations
11
Knowledge Domains
10
Architecture Decisions
14
Changesets Tracked

Knowledge domains:

Authentication Annotations Account Switching Accounts Notifications Performance Crashlytics Regression Testing OverDrive

Examples of recorded knowledge from this sprint:

Anti-pattern: "Never use a singleton with mutable identity for credential storage" — from F-034 TOCTOU discovery
Anti-pattern: "Never depend on DRM-specific state for general app functionality" — from F-079 device ID discovery
Fix recipe: "When debugging notification issues, always cross-reference the CM backend payload format" — from F-071/F-072
Lesson: "Always verify against the backend before classifying a symptom as a client bug" — from F-META-1
Lesson: "Separate instrumentation noise from genuine errors when analyzing Crashlytics trends" — from Crashlytics analysis

Release History — 1,450 Commits Analyzed

Full git history from 1.2.8 through 2.2.5 mined and recorded in the Shared Mind. Every major decision, bug introduction, and fix is traceable.

Release Commits Key Changes Outcome
1.2.8 → 2.0.0403Full UIKit/ObjC → SwiftUI/Swift. LCP streaming (3 prototypes). Audiobook player rewrite.81% heap reduction. Introduced HTML crash (687 fatals) + registry corruption (324 fatals).
2.0.2 → 2.0.4144Rapid patch cycle. Mixed crash fixes with new infra. Actor-based concurrency added then deleted.Anti-pattern: reactive patching without systematic testing. Motivated PP-4020.
2.1.0 → 2.1.1222Test infra investment (Cucumberish → XCTest). SAML overhaul (5+ fix commits). CI hardening.BDD framework added then fully replaced. SAML revealed as systemic architecture problem.
2.1.1 → 2.2.0207CarPlay prototype. Stale-while-revalidate caching. Xcode 26 upgrade.CarPlay caused duplicate player instances. Audiobook toolkit updated 5 times.
2.2.0 → 2.2.1177Readium 3.2→3.6. 112+ accessibility fixes (WCAG 2.1 AA). Top 5 Crashlytics crashes fixed.10+ force-unwrap removals. Thread-safety fixes for registry, HTMLTextView, recursive_mutex.
2.2.3 → 2.2.4141Search format facets (added, reverted, re-added). Pixelated covers fixed at 3 levels. Wi-Fi toggle.12+ test flakiness fixes (sleep → expectations). Accessibility hotfix cut.
ModernizationObjC elimination (-2,132 LOC). God class decomposition. DI migration. Actors + async/await.Per-account credentials. 0→5,735 tests. ForgeOS blocking gates.
PP-4020 Sprint11 PRs73-finding regression report. F-034 credential fix. F-079 sync fix. Image cache fix. 3 regression fixes. SAML arch refactor (#837). Catalog state machine (#836).92% fatal crash reduction validated. 34% user growth confirmed. SAML root cause eliminated. Testing posture documented.

Cross-Release Anti-Patterns Identified

Patterns extracted from 1,450 commits that the governance system now guards against:

Audiobook toolkit churn: Submodule updated 25+ times across releases with frequent regressions requiring reverts. Highest-risk dependency in the project.
SAML auth recurrence: Required fixes in every major release from 2.0.4 through 2.2.1. Root cause: 5 structural defects (force-unwrapped DI, no cookie validation, boolean flag instead of state machine, coupled UI, polluted state). Fixed in PR #837 — protocol-based DI, expired cookie filtering, credentialsStale state machine, SAML state isolation. 29 TDD tests.
CI rebuild cycle: 10-20% of commits per release were CI/CD fixes. Test infrastructure rebuilt multiple times before settling on XCTest-only.
Force unwrap debt: Accumulated through 2.0.0-2.1.1, batch-fixed in 2.2.0-2.2.1. Now banned by codebase policy (PR #830).
Thread safety in production: Dictionary access, registry sync, notification services, and cache managers all had thread-safety crashes discovered in production, not testing. Now addressed by Swift actors + structured concurrency.

Root Cause Analysis — Why Early 2.x Releases Were Rough

This sprint found 3 regressions and 30 bug fixes — a 10:1 improvement ratio. Fatal crashes dropped 92%. User base grew 34%. The 2.x rewrite made Palace a measurably better app. But the early releases (2.0.0 through 2.0.5) were rough — 5 patches in 30 days. The production crashers were not caused by the rewrite being too aggressive. They were caused by four systemic factors that existed before the rewrite began:

1
Minimal test infrastructure. 38 test files with ~13 mocks existed pre-rewrite, covering sign-in logic, bookmarks, and OPDS parsing. But most production paths (downloads, DRM, catalog, concurrency) had no test coverage and no injectable seams. The test foundation wasn't broad enough to catch regressions in the layers the rewrite actually changed. Deeper investment didn't come until the modernization sprint (38 → 367 files, 5,823 tests).
2
The platform was architecturally untestable. ObjC singletons with mutable global state (TPPUserAccount, TPPBookRegistry), GCD-based concurrency with no isolation guarantees, no dependency injection. You couldn't write meaningful tests even if you wanted to — every class reached directly into global state.
3
The rewrite exposed latent bugs, not new ones. The credential TOCTOU race (F-034, 6 years old since 2020), the recursive_mutex DRM crash (since 1.2.4), the empty device ID sync bug (F-079) — all pre-existing. The rewrite widened race windows by introducing async/await, making latent bugs manifest as visible crashes. They were always there; they just crashed silently or infrequently on the old synchronous code paths.
4
Legacy ObjC conflicting with Swift concurrency. ObjC methods called from async Swift contexts have no thread-safety enforcement. The ObjC/Swift boundary was the crash site for recursive_mutex (1,218 fatals) and registry corruption (324 fatals). These crash classes disappeared entirely after ObjC elimination — proof that the language boundary, not the rewrite logic, was the failure point.
10:1
fixes to regressions
(30 fixed vs 3 regressed)
12 of 12
pre-existing bugs
confirmed in both versions
0
production incidents
from modernization sprint

The correct sequence (what the modernization sprint did):

1. Make the platform testable first — expand DI from ~13 injection points to full protocol coverage, actors, AppContainer
2. Build comprehensive tests — 38 → 356 test files, 5,823 tests covering concurrency, credentials, network, SAML, catalog
3. Eliminate the ObjC/Swift boundary — port all 42 ObjC files to Swift (-2,132 LOC)
4. Validate via systematic regression testing (this sprint)

Result: zero production incidents from the modernization sprint. The 3 regressions found are minor UI issues (nav title, sign-up copy, login prompt) — no crashes, no data loss, no security impact. All fixed in PR #834.

Retroactive Release Ledger — 1,458 Commits Analyzed

Every release from 1.2.8 through 2.2.5 has been retroactively registered in the governance system with module classification and institutional knowledge extraction. 14 changesets, 15 Shared Mind observations across 10 knowledge domains.

Release Commits Key Theme
1.2.8 → 1.2.9205CI hardening, 11 reverts
1.2.9 → 2.0.0202UIKit→SwiftUI rewrite. Introduced HTML crash + registry corruption.
2.0.0 → 2.0.110Hotfix — crash + branch confusion
2.0.1 → 2.0.2405 crash fixes, auth flow repair
2.0.2 → 2.0.4144Actor infra (later removed), DispatchQueue modernization
2.0.4 → 2.0.55Pinless login restore — fragile auth code
2.0.5 → 2.1.029Stability — audiobook races, deadlocks
2.1.0 → 2.1.1222Largest release — test infra rewrite, SAML overhaul
2.1.1 → 2.2.0207CarPlay, half-sheet downloads, +39K LOC
2.2.0 → 2.2.1177Readium 3.6, 112 a11y fixes, 582 files changed
2.2.1 → 2.2.267VoiceOver churn (4 reverts), hold crash fix
2.2.2 → 2.2.34Version bump only
2.2.3 → 2.2.4141Search facet churn, audiobook manifest fix
2.2.4 → 2.2.55Crashlytics logging — PP-4020 candidate build
14 releases shipped without governance or regression testing. The rewrite (2.0.0, 202 commits) was followed by 5 patch releases in rapid succession to fix crashes and auth issues it introduced. This sprint established the regression testing process and governance hooks that will gate future releases.

Enforcement — Hard Hooks on Every Git Operation

As of this sprint, governance is no longer advisory. Three hooks fire automatically via the Claude Code harness and block operations that don't meet governance requirements:

Operation Hook Enforcement
git commitPreToolUse → exit 2Blocked without ForgeOS changeset on branch
git pushPreToolUse → exit 2Blocked unless all ForgeOS gates pass
gh pr createPreToolUse → exit 2Blocked unless all ForgeOS gates pass
Session startSessionStartGovernance workflow injected into agent context
Exit code 2 is a hard block — the git operation does not execute. The error message tells the agent exactly what to run to satisfy the requirement. If ForgeOS is unreachable, hooks warn but allow (to prevent total lockout).

Regression Report Toolchain

This sprint's retro identified 7 process gaps. All are now implemented as scripts in the repo:

generate-regression-report.pyCSV → HTML report. --validate checks integrity, --strict refuses unverified findings. generate-jira-tickets.pyCSV → Jira ticket drafts with severity/type mapping. Text, JSON, or markdown output. regression-report.shFull workflow orchestrator: init CSV, run sync tests, mutation testing, generate report. forgeos-session.shCollects 5 evidence types: unit_test, lint (+ test quality), coverage (vs floors), mutation (kill rate), a11y audit. verify-pr.shPre-PR verification battery: build, tests, lint, coverage floors, mutation, accessibility. JSON report output.

Every finding now requires a verified column — starts false, set to true only after human confirmation on device. The report generator renders a pulsing warning badge on unverified findings and refuses to build in strict mode.

Methodology

How this testing was conducted.

Test Environment

1.2.8 (baseline)

Moes Tester — iPhone 13 Pro Max (physical, local)
Palace-1.2.8.362.ipa re-signed with dev cert
BrowserStack — iPhone 16 / iOS 18

2.2.5 build 459 (test build)

Moes Max — iPhone 17 Pro Max / iOS 26 (physical, local)
Built from hotfix/release_2.2.5 branch
BrowserStack — iPhone 16 / iOS 18

Build note: All side-by-side comparison testing (sessions 1-7) was performed against 2.2.5 build 459. The final side-by-side video and final performance profiling were performed against the hardened post-refactor build (modernize/whole-shot with all 9 PRs applied), which includes fixes discovered during this sprint. All PRs are on the refactor branch pending additional hardening before merge to develop. Session 9 (April 16) added the SAML architecture refactor (PR #837) and testing posture documentation.

Coverage — 40+ Test Areas

Functional

Authentication (sign-in, sign-out, error states, multi-library)
Reading (EPUB: DRM-free, Adobe, LCP; PDF)
Listening (Findaway, Overdrive, LCP, open-access)
Catalog (search, borrow, hold, cancel, return, browse)
Multi-account (add, switch, isolation)

Non-functional

Offline (browse, mid-session loss, recovery)
Downloads (queue, cancel, pause/resume, background)
Background audio (lock screen, calls, Bluetooth, AirPods)
Push notifications (hold-available, loan-expiry, deep-link)
Persistence (bookmarks, positions, cross-device sync via API)
Performance (Instruments allocations + leaks, 6 traces)
UI completeness (15 screens automated side-by-side)

Testing Modalities & Effectiveness

This sprint combined six testing modalities. The bulk of bug discovery came from deep diagnostics, logging analysis, SpecterQA automation, code review, and API-level testing — these found the pre-existing bugs, notification payload issues, and the annotation sync defect. Manual side-by-side testing on physical devices was essential for a different reason: it verified every improvement and behavior change through screenshots, caught the 4 regressions that only surface through human observation, and provided the classification judgment (regression vs intentional change) that no automated tool can make.

Diagnostics & loggingDeep investigation of error states, network failures, state desync, and timing issues on physical devices. Primary bug discovery modality.~20 findings BrowserStack + automationAutomated screenshot walker for UI completeness (15 screens). Push notification delivery via Firebase service account.15 findings Manual side-by-sidePhysical device comparison for regressions + verification of all improvements via screenshots. Essential for classification judgment.7 found + 27 verified Code review & CM backendSource analysis of circulation manager push payload format, LicensesService credential handling. Found F-071, F-017.2 findings API-level sync testCustom curl script against annotations endpoint. Queried 255 annotations, found F-079 (empty device ID).1 finding Instruments profiling6 Allocations + Leaks traces. Found 81% heap reduction but 52x CG raster data increase (PR #831).perf data

No single modality was sufficient. Automated tools provided breadth and caught issues at scale, but manual testing caught the regressions that matter most to users and provided the human judgment needed to correctly classify 72 findings.

Testing Posture & Capabilities (as of 2026-04-16)

The 2.x modernization refactor (modernize/whole-shot) was the prerequisite for this testing posture. Before the refactor, the codebase had a modest test foundation — 38 test files, ~13 shared mocks, and protocol-based injection for a handful of interfaces (network executor, DRM authorizer, book registry). This covered sign-in logic, bookmarks, and OPDS parsing, but left most production paths untestable due to singleton coupling, force-unwraps, ObjC/Swift boundary issues, and god classes with hundreds of direct dependencies. The refactor expanded injection points across the entire codebase (38 → 367 test files, ~300 → 5,823 tests), eliminated 42 ObjC files, decomposed god classes (BookRegistry, DownloadCenter, BusinessLogic), and added AppContainer — creating the seams that the deeper layers (auth flows, downloads, DRM, catalog, concurrency) now rely on for testing. The hermetic layer (NoNetworkURLProtocol), mutation testing, coverage enforcement, and chaos/fuzz/security suites were all built on top of those new seams. Full details in docs/Testing/TESTING_POSTURE.md.

Unit tests (XCTest)5,735 methods across 620 classes in 356 files. Hermetic (NoNetworkURLProtocol via NSPrincipalClass). 23 shared mocks.CI-gated
Mutation testingpalace_mutate.py: comparison, boolean, boundary, return-value operators. 50% kill rate threshold.Evidence collected
Test quality lintlint-test-quality.py detects fluff, shallow, and missing-assert patterns. 0 violations in new files.Evidence collected
Coverage floorsPer-module thresholds (46% overall, 30-50% per module) enforced in CI.CI-gated
SpecterQA E2E25 journey YAMLs + 41 replay recordings. MCP-driven iOS simulator testing (v13.0.0).Manual trigger
Contract/API tests23 JSON/XML fixtures from real CM responses (OPDS 1.x/2.0, auth documents, patron profiles, holds, annotations). 5 scenario fixtures.CI-gated
Security testsCredential privacy, DRM adversarial, auth flow security — all hermetic.CI-gated
Chaos & fuzzFault injection harness + OPDS/annotation/LCP parser fuzzing with 9-file corpus.CI-gated
Visual regression (SpecterQA)Replaced pixel-based snapshots with E2E simulator testing via SpecterQA. Covers the same screens plus interactive flows.Manual trigger
Performance profilingInstruments traces via perf-walker-device.py (WebDriverAgent on physical iPhone).Manual
CM contract monitorpalace-cm-monitor MCP checks Circulation Manager for API drift weekly.Non-blocking

Confidence Matrix

Catalog browsing, search, account switching, credential isolationHigh Basic/OAuth/OIDC auth, book borrowing, holdsHigh SAML auth (29 new unit tests, pending E2E)Medium Accessibility (VoiceOver, touch targets)Medium EPUB/PDF reading (Readium WKWebView invisible to XCTest)Low Audiobook playback, downloads, push notificationsLow DRM fulfillment (Adobe/LCP), CarPlay, offline modeVery Low

Governance Pipeline

Every code change follows ForgeOS governance: forge_initforge_propose_changeset (before coding) → evidence collection (unit_test, lint, coverage, mutation, a11y) → gate promotion (review → testing → release) → forge_release_check. Enforcement hooks block git commit/push/PR without governance. Pre-PR verification via scripts/verify-pr.sh.

Known Gaps (cannot automate)

EPUB/PDF rendering (Readium WKWebView outside accessibility tree), DRM fulfillment (requires license servers), audiobook playback quality (audio output), background audio interruptions (phone calls, Siri), CarPlay (requires head unit), push notification delivery (requires APNs + device). These require manual device testing per the checklist in docs/Testing/TESTING_POSTURE.md.

F-META-1Research

A Note on Attribution

During testing, 4 separate symptoms initially attributed to iOS code regressions were systematically investigated and every one dissolved into non-iOS causes: a stuck server-side loan, deleted GCS bucket files, vendor routing misconfiguration, and intermittent backend issues.

This matters because the support tickets that motivated PP-4020 face the same attribution challenge. Without full diagnostic context, end users cannot distinguish iOS regressions from backend or vendor issues. A meaningful share of the tickets interpreted as evidence that "the refactor introduced bugs" may actually be environmental issues that have nothing to do with the iOS code.

What We've Done to Improve Future Attribution

The 2.x modernization and this sprint specifically added instrumentation to make root-cause attribution faster and more accurate in future incidents:

Enhanced Crashlytics metadataDevice ID, account UUID, library name, and credential state are now attached to every crash and non-fatal event. Previously, crashes arrived with no context about which library or account was active. Error origin taggingNon-fatal errors now carry an origin tag (network, parsing, DRM, sync, auth) so backend vs client issues can be filtered in the Crashlytics dashboard without reading stack traces. Structured loggingThe 2.x logging layer uses structured fields (book ID, library ID, HTTP status, endpoint) instead of free-text messages. Logs can be queried and correlated across sessions. App crash detection on launch2.x detects prior-session crashes and logs them with the last-known state (active library, screen, network status). This was invisible in 1.x. Request cancellation trackingIntentional cancellations (account switch cleanup) are now tagged separately from unexpected failures, eliminating a major source of false-alarm non-fatals. Credential state snapshotsAuth-related errors now include whether credentials were present, stale, or missing at the time of failure — directly supporting diagnosis of credential contamination issues like F-034.

These improvements mean that the next time a symptom surfaces — whether from a support ticket or Crashlytics alert — the data needed to attribute it to client code, backend, or vendor is already attached to the event. The 4 symptoms that took hours to dissolve during this sprint should take minutes to triage going forward.

Future Regressions — Automated Process

This sprint's process has been systematized into a repeatable, tool-assisted workflow for all future regression testing. The entire suite is checked into the repo — any team member with Claude Code can run /regression to start a guided regression pass.

/regression PP-XXXXClaude Code slash command that walks QA through the full 7-phase workflow: setup → automated sweep → manual testing (area by area) → finding review → report generation → Jira tickets → publishing.New
scripts/regression-report.shOrchestrator with subcommands: setup (workspace + CSV template + test matrix), auto (sync tests, mutation, push notifications), report (HTML generation), tickets (Jira creation), checklist.New
scripts/generate-regression-report.pyCSV → interactive HTML report. Dark theme, collapsible sections, Jira badges, side-by-side evidence. --validate checks screenshot refs. --strict refuses unverified findings.New
scripts/generate-jira-tickets.pyCSV → Jira Bug tickets via REST API. Priority mapping, parent linking, Done transitions for fixed bugs. --dry-run for preview. --update-csv writes ticket keys back.New
docs/Testing/REGRESSION_TEST_MATRIX.mdStanding checklist of 40+ test areas across P0 (auth, borrow, DRM), P1 (reading, playback, sync), and P2 (UI polish, edge cases) priority tiers.New

Key improvements over the PP-4020 process:

Verified column — findings default to false until human confirms on device. The --strict flag blocks report generation with unverified findings. Eliminates the 9.6% false positive rate from this sprint.
CSV backup on every write — automatic timestamped backup before any programmatic modification. Prevents the mid-session corruption that nearly lost data during this sprint.
One-shot report generationgenerate-regression-report.py replaces the 15+ inline Python regenerations with a single deterministic script.
Guided manual testing — the /regression skill walks testers through each area one at a time, tracking progress and helping log findings in the correct CSV format.
Jira integration with dry-run — preview all tickets before creation. Automatic priority mapping, component assignment, parent linking, and Done transitions for fixed bugs.
Media compression rules — screenshots at 800px max, video at 720p. The 1.8GB → 111MB compression from this sprint is now the default, not a late fix.

For the full retro and process documentation, see RETRO-FINAL.md in the regression workspace and CLAUDE.md § Regression Testing Protocol in the repo.

Full Test Transcript

Every test area from the PP-4020 sweep, with its outcome and linked findings. Source of truth: HANDOFF.md. Checked areas were exercised manually or via automation; "no differences" means tested and validated with no regression found.

AreaTestOutcomeFindings
Authentication
A1Sign in via Settings screenDoneF-001…F-011
A2Sign in via just-in-time (JIT) popupDoneF-032, F-033, F-034 + SQ-005/7/8 (PR #825)
A3Sign outDoneF-080 (pre-existing hang in 1.2.8, fixed in 2.2.5)
A4Sign-in error statesDoneF-067 (network-loss→misleading error)
Reading (EPUB / PDF)
R1Read EPUB — DRM-free (Palace Bookshelf)DoneF-036, F-037, F-039, F-040
R2Read EPUB — Adobe DRMNo differences
R3Read EPUB — LCP DRM (Palace Marketplace / De Marque)DoneF-036, F-037, F-038
R4Read PDFNo differences
Listening (Audiobook)
L1Listen — Findaway/AudioEngineDoneF-046…F-052
L2Listen — OverdriveDoneF-053…F-056
L3Listen — LCP audiobook (Dungeon Crawler Carl)DoneF-057, F-058
L4Listen — open-accessDoneCovered by L2
Catalog / Lifecycle
C1SearchDoneF-025…F-028, F-045
C2BorrowDoneF-029, F-030
C3Place a holdDoneF-064
C4Cancel a holdDoneF-065, F-066
C5Convert hold to loan (when available)PartialOriginally deferred — requires waiting for hold to fire. Today (2026-04-17): Overdrive path covered by F-081 / PR #843 (pre-existing Code=609 bug + fix). Adobe/LCP conversion still requires a waited-out manual run.
C6Return earlyDoneF-031
C7Browse catalog / lanesDoneF-018, F-019, F-041
Multi-library
M1Add a second libraryNo differences
M2Switch between librariesDoneF-032
M3Per-library isolationDoneF-033, F-034
M4Remove a libraryN/AFeature doesn't exist
Network / Offline
N1Browse downloaded content offlineNo differences
N2Online → offline mid-sessionDoneF-061, F-063
N3Offline → onlineBoth handle well
N4App state restoration after force quitBoth recover
Downloads
D1Queue multiple downloadsDoneF-042
D2Cancel in-progress downloadDone
D3Pause/resume downloadDone
D4Background download completionDoneF-059
Background / Audio Interruptions
B1Lock screen controlsNo differences
B2Phone call interruptionNo differences
B3Other audio app interruptionNo differences
B4Bluetooth headset connect/disconnectDoneF-060
B5AirPods auto-pauseDoneCovered by B4
B6AirPods reconnectionDoneCovered by B4
Notifications (completed 2026-04-14)
NT1Hold-available push receivedDoneF-068
NT2Push payload routing (event_type field)DoneF-071 (pre-existing, fixed in PR #828)
NT3Deep-link navigation to Holds tab on tapDoneF-070, F-072 (fixed in PR #828)
UI Completeness (completed 2026-04-14)
U115 screens side-by-side via BrowserStack + manualDoneF-073…F-078 — see U1-report.md
Persistence & Sync (completed 2026-04-15)
P1EPUB bookmark cross-device syncAPI-verifiedSee P1-P5-results.md, scripts/test-sync.sh
P2EPUB reading position cross-device syncAPI-verifiedVerified across Marketplace/LCP/ODL distributors
P3Audiobook bookmark cross-device syncAPI-verified
P4Audiobook position cross-device syncAPI-verifiedAnimal Farm LCP — LocatorAudioBookTime format
P5Multi-device sync (checkouts / holds / positions)API-verifiedF-079 (empty device ID bug — fixed in PR #833). OverDrive skipped — org access lost.
Performance (completed earlier)
PR1-PR5Performance profiling — 6 Instruments tracesDone81% fewer heap allocations in 2.2.5. Image cache fix in PR #831. See profiling/PR5-final-comparison.md.
Advanced / Deferred
ADV-1Rapid account switch during active network loadDeferredCovered by general performance testing; F-034 fix (PR #822) eliminates the race.
ADV-2Sign-in modal deduplication under 401 stormDeferredNot reproduced in any manual session.
ADV-3Sign-out with stale/invalid credentialsDeferredNot tested.
Open Investigations
SQ-007Stale sign-in modal on Download for signed-in basic authInvestigatingTwo systemic guards committed (4c7775548). One untraced call site remains. See SESSION-2026-04-11.md for candidate call sites.
Today's Additions (2026-04-17)
Overdrive borrow-from-hold end-to-endDoneF-081 (pre-existing Code=609, fixed in PR #843)
Launch-time cache integrity (ImageCache + URLCache)DoneF-082, F-083 (pre-existing, fixed in commit 90187ceeb)

Known environmental issues (not iOS bugs)

  • Cryptonomicon on A1QA is in a stuck license-pool state — do not use for testing.
  • Overdrive titles on Lyrasis Reads fail with "Not a valid HPLD card" — EXPECTED per Lyrasis ops.
  • Cover images from storage.googleapis.com/rua-uplo/ return 404 — publisher deleted files from GCS bucket.
  • Staging backend may be unreliable during off-hours deploy windows.

All Findings (76)

68 active findings + 7 superseded + F-META-1. [REG] [PRE] [FIX] [NEW] [CHG] [SUP]

F-001 [REG] Account screen lost nav-bar title "Account" F-002 [REG] Library card sign-up flow lost explanatory context F-003 [SUP] Password field gained Show reveal toggle F-004 [CHG] Bottom tab "Reservations" → "Holds" F-005 [SUP] SUPERSEDED — see F-007 F-006 [SUP] Password field no visual feedback while typing in 1.2.8 F-007 [SUP] Content Licenses row added to Account screen F-008 [SUP] Advanced settings row added F-009 [CHG] Sync Bookmarks copy grammatically awkward when toggle on F-010 [SUP] Password Show toggle visible while signed in F-011 [CHG] Content Licenses visibility depends on signed-in state F-012 [PRE] Revoke endpoint returns XML but client parses as JSON F-013 [PRE] Local registry cleanup runs even when server return fails F-017 [FIX] PP-3704 GCS credential leak fixed — LicensesService uses ephemera F-018 [FIX] Catalog covers float above section headings in 1.2.8 F-019 [CHG] Catalog header consolidated — library name as nav title F-025 [NEW] Search gained live/auto-search F-026 [NEW] Search results gained format filter pills F-027 [CHG] Search button "Get" → "Borrow" F-028 [CHG] Search navigation "Back" → "Cancel" F-029 [FIX] Book description raw HTML tags fixed F-030 [CHG] Borrow flow shows cancel option + sheet actions F-031 [NEW] Detail view auto-dismisses after return F-032 [REG] Unexpected login screen on library switch to A1QA F-033 [PRE] Credential bleed: A1QA credentials shown on Main Street account + F-034 [PRE] Cross-account credential contamination via TOCTOU (Time-of-Check- F-035 [PRE] My Books does not auto-refresh on navigation F-036 [CHG] EPUB reader hides nav bar on open F-037 [FIX] EPUB reader brightness slider fixed F-038 [CHG] Content protection error on stale loan after return on another de F-039 [FIX] EPUB search results order fixed — now ascending by page F-040 [FIX] EPUB reader view no longer shifts when nav bar toggled F-041 [FIX] Catalog facet switching improved but still slow F-042 [CHG] Download progress indicator removed from My Books list view F-043 [FIX] Book detail view gained Back button F-044 [CHG] Book detail view color gradient and button styling changed F-045 [FIX] Search results button styling now consistent with rest of app F-046 [FIX] Audiobook skip-forward 30s jumps backward F-047 [FIX] Audiobook TOC icon invisible in nav bar F-048 [FIX] Audiobook download hangs near end of title F-049 [FIX] Audiobook bookmarks page inaccessible F-050 [FIX] Audiobook playback starts before player UI presents F-051 [FIX] Audiobook player UI and animations improved F-052 [FIX] Audiobook resumes from last played position F-053 [FIX] Audiobook slow initial load on Overdrive titles F-054 [FIX] Audiobook garbles sound during skips F-055 [FIX] Audiobook slider transitions choppy F-056 [FIX] Audiobook bookmarks sync across devices F-057 [FIX] LCP audiobook instant checkout + background download with streami F-058 [FIX] LCP audiobook same player bugs as L1/L2 on 1.2.8 F-059 [FIX] Background download failure recovery improved F-060 [FIX] Audiobook resumes playback on Bluetooth reconnect F-061 [CHG] Download error handling improved but half-sheet sometimes hangs F-062 [PRE] Double book presentation on rapid Read button taps F-063 [PRE] Download buttons become stale/unresponsive on network loss F-064 [FIX] Inconsistent Reserve/Holds terminology unified F-065 [PRE] Holds state not syncing in search/list view after cancel F-066 [FIX] Return button on search/list view not presenting confirmation ale F-067 [PRE] Network loss during login shows invalid credentials error instead F-068 [FIX] Push notification: hold-available received and triggers sync F-069 [FIX] Push notification: loan-expiry warning received F-070 [FIX] Push notification: deep-link navigates to Holds tab on tap F-071 [PRE] Push notification routing reads wrong field — works by accident v F-072 [PRE] Push notification tap navigation fails — weak router reference go F-073 [CHG] U1: Catalog header redesigned — library name as nav title F-074 [CHG] U1: My Books header and sort control redesigned F-075 [CHG] U1: Settings gained section headers and Wi-Fi toggle F-076 [SUP] U1: Holds tab is now a dedicated 4th tab F-077 [CHG] U1: Book detail gained Back button and dark gradient header F-078 [CHG] U1: Lane list view redesigned — compact filters and Borrow button F-079 [PRE] Empty device ID breaks cross-device bookmark sync for non-Adobe-D F-080 [FIX] Sign-out hangs on 1.2.8 — requires force-quit to recover F-081 [PRE] Overdrive borrow-from-hold fails with "wrong headers" (Code=609) F-082 [PRE] ImageCache disk write fails on launch — "The folder doesn't exist" F-083 [PRE] System URLCache SQLite open failures at launch F-META-1 [RSQ] RESEARCH: Backend artifacts misattributed to iOS regressions