hotfix(3.2.4): blank Library line on support reports (PP-5078) + LCP audiobook streaming (PP-4957) - #1455
hotfix(3.2.4): blank Library line on support reports (PP-5078) + LCP audiobook streaming (PP-4957)#1455mauricecarrier7 wants to merge 13 commits into
Conversation
…078)
`generateBody` rendered `Library: \(accountsManager.currentAccount?.name ?? "")`.
`currentAccount` resolves through the library registry and is nil until that
registry has loaded the selected account, so a patron who reports a problem
before it settles sends a report whose Library line is empty — while the patron
ID, a separate per-library lookup, resolves normally.
Real ticket 18864 (1 Sep 2026, app 3.2.3) reached support as:
Palace Version: 3.2.3
Library:
Patron ID: 21467001510417
The agent triaging it recorded the summary as "Sign in prompt - no library?" —
the blank actively implied the patron had no library configured. The patron had
written that they were a member of Park Ridge Public Library.
The line is now produced by `libraryFieldValue(name:uuid:)`, which yields three
outcomes a reader can tell apart:
* a real name — the common case, passed through verbatim
* the identifier — the app knows WHICH library but cannot name it yet;
support can resolve a UUID, and cannot resolve a blank
* "(none selected)" — the app genuinely has no library
A blank could equally have meant the line was lost in mail transit. None of
these can. Whitespace-only names fall back too, since a space renders in the
email exactly like nothing at all.
Intermittent, not universal: of the recent problem reports sampled in HelpSpot,
most carried a library name and a minority did not, which is why this has been
easy to miss.
**Scope:** the Library line in the problem-report body, plus its tests. The
patron-ID resolution, the report's other fields, and registry loading are all
untouched.
**Not done — the XCTest suite was NOT run.** It could not be: on this branch
`xcodebuild` refuses with "an out-of-date resolved file was detected at
Package.resolved", a pre-existing condition of main's committed lockfile rather
than anything this change introduces, and forcing resolution instead pulls a
newer swift-toolkit that conflicts on sqlite.swift. CI runs on a clean
environment and is the verification of record here. What WAS verified locally is
the decision itself, executed standalone against all four cells: the old
expression returns an empty string for a nil name (the shipped defect,
reproduced), and each new cell returns what it should. That proves the logic,
not the wiring into `generateBody`.
**Not done:** no build-number bump. This is deliberate — bumping now would
presume this ships alone, and un-bumping after a merge is worse than adding it.
Bump `CURRENT_PROJECT_VERSION` (492 on main today) before cutting the build.
**Deferred:** develop has since extracted this into
`AccountsManager+ProblemReportContext`, where the name still comes from
`currentAccount?.name` while the patron ID comes from the requested library — so
develop additionally mislabels a report filed about library A while B is
selected. The forward-port needs to carry the fallback into that extraction
rather than apply this diff verbatim; the file's own header comment records the
current behaviour as an intentional replication of the pre-extraction code.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bumps MARKETING_VERSION 3.2.3 -> 3.2.4 and CURRENT_PROJECT_VERSION 492 -> 493 across the four app-target configs (Palace + Palace-noDRM, Debug/Release). The two 1.0 entries belong to a non-app target and are untouched. 3.2.4 carries, relative to shipped 3.2.3: * PP-5078 — problem reports arrived with a blank Library line The LCP archive-download fix is already in this base: build 492 (cfe530b, "stop the doubled archive download, show its progress, and stop offering Listen before the audio exists") is the commit this branch was cut from, so it is inherited rather than needing a port. LCP audiobook STREAMING is a separate thing and is deliberately NOT here. That feature does not exist on this line at all — 3.2.3 has zero references to `lcpAudiobookStreaming`, against five files on develop — and it depends on the swift-toolkit `fork/3.11.0-issue-579` pin, where this branch is on exactVersion 3.9.0. Back-porting it would be a release, not a hotfix. **Scope:** version strings only. No code change in this commit. **Not done:** this branch does not build. `xcodebuild` fails before compiling with "Could not resolve package dependencies: swift-toolkit depends on sqlite.swift 0.16.0..<1.0.0 and root depends on 0.15.0..<0.16.0" — recorded during the 3.3.0 regression as F-INFRA-006, "the shipped 3.2.3 tag no longer builds from a clean checkout". swift-toolkit is pinned at exactVersion 3.9.0, so the fork's own dependencies appear to have moved under a fixed tag. That blocks shipping ANY 3.2.4 build until it is resolved, and it is not caused by anything on this branch. **Deferred:** the likely remedy is moving swift-toolkit to the fork revision develop already uses (58413f868), which is a known-good combination with its sqlite range — but that is a dependency bump on a release line and wants its own change and its own verification, not a quiet addition to a support-copy fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ace Readium fork
`xcodebuild` on this line fails before compiling anything:
Could not resolve package dependencies:
'swift-toolkit' depends on 'sqlite.swift' 0.16.0..<1.0.0
and root depends on 'sqlite.swift' 0.15.0..<0.16.0
3.2.3 pins `readium/swift-toolkit` at exactVersion 3.9.0 — upstream, not our
fork — and that tag's own dependency range has since moved to require
sqlite.swift 0.16+, while this project caps sqlite at 0.15.x. A fixed version
tag is not a fixed dependency graph, so a release that built at cut time stopped
building without a single commit landing on it.
Recorded during the 3.3.0 regression as F-INFRA-006: "THE SHIPPED 3.2.3 TAG NO
LONGER BUILDS FROM A CLEAN CHECKOUT."
This moves the same two references develop already carries:
swift-toolkit readium/swift-toolkit @ exactVersion 3.9.0
-> ThePalaceProject/swift-toolkit @ revision 58413f868
SQLite.swift upToNextMinorVersion 0.15.0 -> 0.16.0
`58413f868` is `fork/3.11.0-issue-579`, the Palace fork carrying "Do not report
cancelled HTTP reads as loading errors" — the Readium regression that broke LCP
audiobook streaming, which is why the fork exists. It is also the revision
develop has run on since PP-4957, so this is a known-good combination rather
than a new one, and no later fork bump exists to chase.
Verified: `-resolvePackageDependencies` succeeds, and the app target reaches
`** BUILD SUCCEEDED **`.
**Scope:** two package references and the resulting lockfile. No source change.
**Not done — the TEST target does not compile after this.** 14 test files use
`ManifestJSON` and `Manifest.from`, which this line has no definition for;
develop gained them in `PalaceTests/Audiobook/ManifestFixture.swift` (added by
050c941, a later toolkit bump) and that fixture was never brought back to
main. The next commit ports it. Splitting them keeps this commit to the single
question of whether the branch can resolve at all.
**Deferred:** nothing here stops the same class recurring. An `exactVersion` pin
on a fork whose maintainers can retag will drift again; only a `revision` pin is
immune, which is what this now uses for swift-toolkit but not for the other
version-pinned packages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es again
After the Readium fork bump, 14 test files on this line stopped compiling:
error: cannot find type 'ManifestJSON' in scope
error: type 'Manifest' has no member 'from'
Both symbols were previously reachable from the audiobook toolkit's own test
target. develop replaced that arrangement with a local fixture,
`PalaceTests/Audiobook/ManifestFixture.swift`, added by 050c941 (a toolkit
bump) — 95 lines defining `enum ManifestJSON` and
`extension Manifest { static func from(jsonFileName:bundle:) }`. It was never
brought back to main, so this line had no definition for either.
Ported verbatim from develop. It imports only Foundation and XCTest, so it
carries none of the decomposition that separates the two branches.
Verified: the test target compiles and `ProblemReportEmailTests` runs
12 tests / 0 failures — the first time the four PP-5078 cases have executed on
this branch at all. Before this they could not be built, only reasoned about.
**Scope:** one test fixture plus its two pbxproj registrations. No production
code.
**Not done:** the other 13 files that reference these symbols were not
individually run; the fixture is what they were missing, but only
ProblemReportEmailTests was executed here. A full-suite pass on this branch is
still owed and will surface anything else the fork bump moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… helper SoD review (qa_test) found the gap: the four cases added in 8044e27 pin `libraryFieldValue` itself, and measured properly — reverting it to `name ?? ""` is killed by three of them by name. But nothing pinned that `generateBody` CALLS it. Reverting the call site to `currentAccount?.name ?? ""` left every test green while shipping the exact defect of ticket 18864. That is the "test the helper, not the producer" trap, and it is the more dangerous half here: the helper is new code nobody had a reason to change, while the call site is a one-line interpolation inside a 60-character string that a merge could plausibly clobber. This asserts on the emitted body, because the bare `Library:` line IS the artifact support receives — a return value would be one indirection away from the thing that was wrong. It also covers the whitespace and end-of-body forms, which render identically in an email. Proven to bite: reverting the call site fails `testPP5078_generateBody_neverEmitsABareLibraryLine` by name. 13 tests green with the fix in place. Deliberately asserts a PROPERTY (never empty) rather than an expected value, because what the accounts manager resolves in a test environment is not fixed — pinning "(none selected)" would make this a test of the environment. **Scope:** one test. No production change. **Not done:** the producer is still only exercised through whatever the real `AppContainer.production().accountsManager` returns under test. Driving both the resolved-name and unresolved-name branches through `generateBody` needs an injectable accounts manager; `generateBody` takes the concrete `AccountsManager` class and no test double exists on this line. The helper's four cases cover that matrix directly, so the gap is in wiring coverage, not decision coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports the streaming path to the 3.2.x line. LCP audiobook streaming worked until a Readium change made cancelled HTTP reads report as loading errors, which broke streaming-from-license; the Palace fork of swift-toolkit carries the fix (fork/3.11.0-issue-579, "Do not report cancelled HTTP reads as loading errors"). Restoring streaming here therefore means moving BOTH Readium pins — the app's, done in b444854, and the audiobook toolkit's own, done here — plus the Palace-side gate that decides when to stream. Behaviour is unchanged until the flag is on. With `lcp_audiobook_streaming_enabled` false, an LCP audiobook downloads the full `.lcpa` before playback exactly as it does today. With it true, the book is playable on its `.lcpl` license alone and the player streams on demand. Ported from develop's a0968e6. The four core files applied cleanly; the flag plumbing did not, because develop has since extracted `PalaceFeatureFlags` into its own package and split `DeveloperSettingsViewModel` out — neither exists on this line. The flag is therefore hand-wired into this line's shapes: `RemoteFeatureFlags.FeatureFlag` gains the case, the local-override key and the `isLCPAudiobookStreamingEnabled` accessor (local override > Firebase > false), and `FirebaseManager` gains the `RemoteConfigKey` case plus its `setDefaults` entry. The `managerKey` mapping is included deliberately: without it `isFeatureEnabled` falls through to the in-app default and the flag reads false forever no matter what Firebase says — an inert flag that looks wired. Submodule ios-audiobooktoolkit 7a53098c -> e446a8e, the earliest revision whose project points Readium at the fork rather than upstream 3.9.0. That is 33 commits, not the 106 to develop's pin, and it was chosen for that reason. **This raises the deployment target from iOS 16.0 to 17.0**, on all ten app configs, because e446a8e's toolkit requires it. That is the real cost of this port and it is not reversible within it: iOS 16 devices will not receive 3.2.4. Flagged explicitly because a floor raise in a patch release is not something anyone should discover from a build log. Verified: `** BUILD SUCCEEDED **`, and 43 tests pass across LCPFulfillmentHandlerTests, AudiobookContentGateTests and ProblemReportEmailTests. The flag chain was traced end to end — wire key, managerKey mapping, and five consumer call sites. **Scope:** the streaming gate, its flag plumbing, the toolkit pin, and the deployment-target raise that pin forces. **Not done — streaming itself is NOT verified on this line.** The tests prove the gate's decisions and that everything compiles and links; they do not prove an LCP audiobook actually streams. That needs a device, an LCP title, and the flag on. On develop it was proven live on sim (borrow -> instant Listen -> `.lcpa` stays 0 bytes -> plays); no equivalent run has happened here. **Not done:** no full-suite pass on this branch. Three classes were run, chosen because they cover the ported code; the other ~8,500 tests have not been executed against the new toolkit and Readium fork on this line. **Deferred, and it matters before shipping:** Firebase currently has `lcp_audiobook_streaming_enabled` set to TRUE at 100% (published 2026-08-24). It is inert today because no shipped 3.2.x build has the flag. The moment 3.2.4 ships, those users get streaming ON immediately — the in-app default of false is overridden by the remote value. If that is not intended for this cohort, the Firebase value or a condition must be set BEFORE release, not after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts consumer
SoD review (architect and qa_test, independently) blocked the streaming port for
a real defect, confirmed before acting on it.
76f0206ed ported the streaming PROVIDER without its CONSUMER.
`MyBooksDownloadCenter:258` declared `lcpStreamingEnabledProvider` and nothing
read it. On develop the consumer lives in
`MyBooksDownloadCenter+RegistryDownloadServicing.swift`; that file does not
exist on this line, where the equivalent logic is inline in
`BookRegistrySync.contentPresence` — and that was not flag-aware.
The consequence, with streaming ON:
fulfillment writes .lcpl only -> .downloadSuccessful
relaunch -> contentPresence = .licenseOnly
armDecision -> .downloadNeeded (every .licenseOnly arm)
redownload suppressed by LocalBookContentService's streaming guard
-> book stranded permanently: Listen gone after first relaunch, never heals
Not hypothetical: Firebase has `lcp_audiobook_streaming_enabled` TRUE at 100%,
so this is the shipping path, and the failure is silent and unrecoverable from
the app.
`contentPresence` now returns `.present` for a license-only LCP audiobook when
streaming is on, so load-time reconciliation keeps the book playable across
launches. The provider is injected as a closure, matching the other providers on
this type, so the flag is read at CALL time rather than construction — this
object is built during `TPPBookRegistry.init`, before Remote Config has
necessarily fetched.
Verified by executing the decision chain both ways:
streaming ON, license-only on disk
without fix: .licenseOnly -> .downloadNeeded -> STRANDED
with fix: .present -> .downloadSuccessful -> plays
streaming OFF (must not change)
without fix: .licenseOnly -> .downloadNeeded
with fix: .licenseOnly -> .downloadNeeded (identical)
`** BUILD SUCCEEDED **`; 86 tests pass across LCPFulfillmentHandlerTests,
AudiobookContentGateTests, ProblemReportEmailTests and BookRegistrySyncTests —
the last chosen because it covers the file this changes.
**Scope:** the one `.licenseOnly` branch in `contentPresence`, plus the provider
that feeds it. Flag OFF is byte-identical to before.
**Not done:** no regression test for the strand itself. The decision chain was
proven by executing it standalone, not through `BookRegistrySync`, because
reaching `contentPresence` needs a book on disk, an LCP-capable fixture and the
`#if LCP` path — a fixture this line has no precedent for. A test that drives
`load()` twice with a license-only file and asserts the state survives is the
right shape and is owed.
**Not done:** develop's `LocalBookContentServiceTests` (+41) and
`RegistryDownloadServicingSeamTests` (+62) are still unported; the architect
flagged the first as the test that would have caught this. `LocalBookContentServiceTests`
exists on this line and could take the port.
**Deferred:** streaming still unverified end to end on this line — that needs a
device, an LCP title and the flag on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he dead provider Second round of SoD review. Three findings, all correct, all acted on. **1. The strand had no regression test, and my reason for that was false.** I wrote that reaching `contentPresence` needed an LCP fixture "this line has no precedent for". It does: `BookRegistrySyncTests.test_load_licenseWithoutContent_schedulesTheContentRedownload` already writes an `.lcpl`, builds a book via `makeLCPAudiobook`, drives `load()` and asserts the resulting state — in a suite I had already run. The new test is its mirror with streaming ON: the book must stay `.downloadSuccessful` and NO re-download may be scheduled. Without the flag-aware branch it downgrades to `.downloadNeeded` and is stranded, because the heal is suppressed by the streaming guard. **2. Tests were reading the live Firebase flag.** `LocalBookContentServiceTests` built its service without a `streamingEnabledProvider`, so ~14 tests consulted `RemoteFeatureFlags.shared` — whose Firebase value is TRUE at 100%. Three would fail and three would pass vacuously, and which depended on a remote config rather than on this code. Pinned in both `makeService` and `setUp`, defaulting OFF, matching what develop already does. Same pin applied to `BookRegistrySyncTests` (its `setUp` instance to a literal false; its scheduling factory threads the new parameter). **3. `MyBooksDownloadCenter.lcpStreamingEnabledProvider` was dead** — declared, read by nothing, and carrying a comment asserting a `RegistryDownloadServicing.contentPresence` seam that does not exist on this line. That false claim is what produced the original block. Removed; the real consumer lives in `BookRegistrySync.contentPresence`. **The previous "86 tests green" was measured against the WRONG toolkit and is withdrawn.** `harness test` runs `worktree doctor` and self-heals a reported drift by re-running `setup-ios`, which re-copies the toolkit from the CANONICAL checkout — release/3.3.0, pinning ca0f4cac. The doctor always reports drift for a hand-provisioned copy because it reads the module git DB rather than the copy, so every `harness test` invocation silently replaced the correctly-pinned e446a8e tree. I told the reviewer their finding was wrong; it was not, and their method (hashing the toolkit's pbxproj) was better than my content-marker check, which was true when run and overwritten afterwards. Re-verified properly: toolkit provisioned at e446a8e and confirmed by pbxproj hash 38a0cd7bf1bd BEFORE and AFTER the run, built with raw xcodebuild so no self-heal could intervene. **110 tests, 0 failures**, across BookRegistrySync, LocalBookContentService, LCPFulfillmentHandler, AudiobookContentGate and ProblemReportEmail. An intermediate run showed 7 failures. Those were operator contamination, not a defect: a stale `RemoteFeatureFlags.lcpAudiobookStreamingLocalOverride = 1` left on the simulator by an earlier manual experiment forced streaming ON in the test host. Cleared, and the same command then passed. Recorded because "my own leftover state" is a real explanation that looks exactly like a code failure. **Scope:** test pinning, one regression test, one dead declaration removed. **Not done:** the full suite still has not run on this branch — 110 of ~8,600. The five suites chosen are the ones covering changed code, but a toolkit move, a Readium fork repin and an iOS floor raise all have reach beyond them. **Not done:** streaming is still not verified end to end. The device pass is next and is the evidence that matters; PP-4957 was closed on sim evidence with this pass explicitly still owed. **Deferred:** 20 other `BookRegistrySync` constructions across 9 test files do not pin the flag. They are only exposed if a test builds an LCP audiobook with an `.lcpl` on disk, which none currently does — but the production default reading `RemoteFeatureFlags.shared` means that is a latent coupling, not a safe one. The `harness test` self-heal clobbering a hand-pinned submodule copy is a harness defect and is not fixed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s measured
Round 3 blocked on two findings. The architect reached both by reading; the QA
reviewer reached both by MEASURING against deleted production branches, which is
the stronger evidence and the reason these are fixed rather than argued.
(1) The streaming guard in `LocalBookContentService.redownloadLCPContentFile`
had no test at flag ON. `makeService(streamingEnabled:)` gained the parameter in
the same commit that was supposed to cover it, and nothing ever passed `true` —
so deleting `if streamingEnabledProvider() { return }` left the suite green.
That is the round-1 defect's shape exactly: a seam with no consumer. The new
test is differential rather than one-sided, because the ON half alone is
satisfied by a service that never fulfills at all; the OFF half is what proves
the seed, the spy and the call site are live, and therefore that the ON half's
zero is the guard's doing.
(2) `BookRegistrySyncTests` asserted `XCTAssertFalse(scheduledRedownload)`
immediately off `load`'s completion, but scheduling is
`main.asyncAfter(+contentRedownloadDelay)`. The absence was guaranteed, not
earned — QA confirmed the assertion does not fire even when the mutant DOES
schedule. This file already carries a comment recording the same trap being
measured on the in-flight test; this now uses the same barrier.
**Scope:** test-only. No production code changed.
**Not done:** the full suite has still not run on this branch. The
`review:qa_test` gate is absent from this branch's `plan.required`, so the QA
verdict that found both of these would not have been consulted at merge — a
gate that cannot fail. Tracking separately; not fixed here.
**Deferred:** `AccountsManager(defaults:suiteName)` injection for the
host-conditional ProblemReportEmail mutant (QA: does not block).
Build 493 was NOT free. `Palace-3.3.0.493.zip` already exists in ios-binaries, so the hotfix was carrying a build number a shipped 3.3.0 artifact had already used. Two different binaries under one number is exactly the confusion a build number exists to prevent. 500 chosen because it is the first number above the global maximum, verified from two independent sources that agree: the highest CURRENT_PROJECT_VERSION across every remote branch and tag is 499 (origin/release/3.3.0), and the highest build in ios-binaries is likewise 499 (Palace-3.3.0.499.zip). Note that this project numbers builds as ONE monotonic sequence spanning all version trains — 3.2.3.492, 3.3.0.493, 3.4.0.498 — not per-version. That is why `scripts/ios-check-version.sh` would not have caught this: it tests the exact `<version>.<build>` filename, so it sees `3.2.4.493` as absent and reports free while `3.3.0.493` sits beside it. **Scope:** version only — 4 CURRENT_PROJECT_VERSION lines, no other change. **Not done:** App Store Connect was not consulted; both sources here are git and ios-binaries. A build uploaded without its number ever being committed would be invisible to both, though nothing in this project's history works that way.
QA round 4 found a surviving mutant: replacing `lcpStreamingEnabledProvider()` with a literal `false` at AudiobookSessionManager.swift:2056 left this suite green. The provider was wired and nothing consumed it — the third instance of that shape on this branch. Fixing it surfaced something worse. Run unmutated at 25d57af this suite is **13 executed, 7 failures**. It has been red, and the reason no one saw it is instructive: every construction site here left `lcpStreamingEnabledProvider` defaulted, so the tests read `RemoteFeatureFlags.shared`, which is TRUE at 100% in Firebase. The suite asserts download-first behaviour, so a live TRUE fails it. QA's mutant run reported "13 executed, 0 failures" — but that zero was produced BY the mutation, which forced the flag off and accidentally supplied the pinning the suite was missing. The mutant looked survived and the suite looked green off one run, and both readings were artifacts of the same defect. All three construction sites are now pinned to `false` — `setUp` via a `makeManager` factory, and the two contract-snapshot tests, which build their own manager and whose recorded call ORDER only exists with streaming off. Result: 14 executed, 0 failures. The added test is differential for the reason the other two on this branch are: the ON half alone is satisfied by a gate that does nothing, so the OFF half asserts the trigger DID fire, making the ON half's empty trigger list attributable to the flag rather than to a dead test. Falsified both directions. Baseline 14/0. Mutating the argument to `false` fails 3 named assertions in `testGate_contentMissing_streamingOn_…` (:190 the await must not be reached, :194 contentUnavailable != proceed, :196 the trigger list must stay empty). Production file restored and verified clean after. **Scope:** test-only. No production code changed. **Not done:** the other suites' pins were added earlier in this branch; this commit does not audit the whole test tree for further unpinned reads of `RemoteFeatureFlags.shared`. That census is worth doing and is not done here.
… 16 back
The hotfix pinned the toolkit at e446a8e — 33 commits past the 3.2.3 base. Two
of them are the LCP streaming fix. The other 31 are features and fixes that have
no bearing on either thing 3.2.4 exists to do, and the full suite proved they
were not free: PlaybackRateTests (5 tests) and AudiobookLoaderTests failed 3 of
3 iterations, green on every other branch, so this branch introduced them.
The cause was a real product change riding in on the bump. PP-4518 moved the
playback ladder from presets [0.75, 1.0, 1.2, 1.5, 2.0] to
[0.50, 1.0, 1.5, 2.0, 2.50, 3.0] and steps from 0.75×–2.0× to 0.50×–3.0×. That
is a user-visible change to how audiobooks play, and a hotfix for a blank
support-report field is not where it belongs.
Narrowed to the 3.2.3 toolkit base 7a53098 plus exactly the two streaming
commits, cherry-picked with no conflicts. Both are dependency-pin only and say
so: 06afaac bumps Readium 3.9.0 → 3.11.0 ("no source changes needed for the
3.10/3.11 API deltas"), e446a8e repins to ThePalaceProject/swift-toolkit @
58413f868 = 3.11.0 + fix-issue-579 ("No source changes"). Toolkit now
a408d718 on hotfix/3.2.4-streaming-only.
**iOS 16 comes back, and that is the bigger win.** The 17.0 floor on this branch
was not a decision — it was a consequence. The wide pin's toolkit requires 17.0;
the narrowed one requires 13.0. All ten configs return to 16.0, matching 3.2.3,
so this hotfix no longer drops iOS 16 patrons. Verified by building: the app
source compiles clean at 16.0 and the suites run.
Deliberately NOT carried: the F1/F2 download-durability fixes. They are real
patron-facing bugs present in 3.2.3 — per-launch-random background session
identifiers meant a track finishing while the app was killed could never be
finalized, leaving the book manifest-only against expiring signed URLs (the
-1008 class). They are excluded because all five are already ancestors of the
3.3.0 toolkit and 3.3.0 is at RC, they touch OverDrive/OpenAccess rather than
the LCP path this hotfix fixes, and cb284b9 exists only because F2 introduced a
retain cycle that architect SoD caught — an area subtle enough to want a release
cycle rather than a hotfix.
Verified: 99 executed, 0 failures across PlaybackRate, AudiobookLoader,
AudiobookContentGate, BookRegistrySync and LocalBookContentService. The 18
assertions in the two previously-failing suites all pass.
**Scope:** toolkit pin + deployment target. No app source changed.
**Not done:** the full CI-parity suite has not been re-run since this change —
the run that found the failures is now invalid and the verdict must be re-earned.
**Deferred:** hotfix/3.2.4-streaming-only exists only on this machine. It must
be pushed to ThePalaceProject/ios-audiobooktoolkit before this branch can
resolve for CI or anyone else.
🏗️ CodeAtlas Ledger Analysis✅ All Checks Passed♿ Accessibility (via AccessLint)
✅ No accessibility issues detected 🧪 Test Coverage (via QAAtlas)
🏛️ Architecture Analysis
🔍 Reachability Analysis
✅ No dead code detected 📊 0 files analyzed | 📦 Download Full Report Powered by CodeAtlas Ledger |
…o ledger entry `tooling-integrity` failed on this PR, correctly. Two separate problems. The gate reads only `state.version` from Package.resolved. This hotfix pins Readium by REVISION — ThePalaceProject/swift-toolkit @ 58413f868, the fix-issue-579 series — because the streaming fix is in no upstream release. A revision pin has no `version` key, so the gate asserted "swift-toolkit pin not found in Package.resolved" while the pin sat right there in the file. That message describes a missing dependency; the reality was a pin it could not read. It now accepts either identifier and still requires the ledger to name whichever one is in use, so a fork pin is held to the same standard rather than exempted. The second problem is the one the gate exists for, and it was a real finding: `readium-money-path-validation.md` had no entry for this pin. Its own 3.9.0 section anticipated exactly this move — it records LCP streaming as `fail` pending "the unmerged fix-issue-579 branch" — so the ledger was asking for this entry by name. Added, with the evidence actually gathered rather than an assertion of success: two LCP audiobooks borrowed on Moes Max produced `.lcpl` licenses and ZERO `.lcpa`, and survived SIGKILL + cold launch as `download-successful` while the other ten registry records read `download-needed` — a control proving reconciliation ran. Paths not exercised are recorded as `not validated`, not as passes: EPUB/PDF, Findaway and OverDrive. Audio actually SOUNDING is called out as still owed, because no artifact here demonstrates it. **Scope:** one gate script and one ledger doc. No production code. **Not done:** the EPUB/PDF reader paths were not device-exercised against this pin. The suite is green, which is weaker evidence than a device pass, and the ledger says so rather than rounding it up.
🧪 Unit Test Results📊 View Full Interactive Report ❌ 2 TESTS FAILED7434 tests | 7321 passed | 2 failed | 111 skipped | ⏱️ 18m 35s | 📊 98.5% | 📈 49.1% coverage Tests by Class — 839 classes, 2 with failures
Failed Tests (click to expand)📊 Testing Coverage BreakdownUnit Test Line Coverage (testable surfaces): 49.1% Total coverage incl. UI/lifecycle: 47.2% (18 files excluded from testable denominator — see
🔗 Interactive HTML Report | CI Run Details 📦 Downloadable Artifacts
|
3.2.4 carries two patron-facing fixes and nothing else.
PP-5078 — support reports arrived with a blank Library line
Patrons tapped Report an Issue and the report reached support with
Library:and nothing after it, so nobody could tell which library the person was writing about.The line was built from the library's display name alone. The app learns that name from the server, and there is a window — fresh install, slow connection, just after switching libraries — where it knows which library is selected but not yet its name. In that window it printed an empty string: no "unknown", no fallback. The information was never actually missing; the app held the library's unique ID the whole time and simply was not writing it down. That is also why it looked random rather than reproducible — it depended on timing, not on the patron or the library.
The field now falls back through what it actually knows: name → unique ID →
(none selected). Support will no longer receive a blank library line.PP-4957 — LCP audiobook streaming, back-ported
Streaming from an LCP license broke in a Readium update; the org fork restores it. The back-port initially carried the streaming provider without one of its consumers, which both reviewers blocked: on the 3.2.x line there is no
MyBooksDownloadCenter+RegistryDownloadServicing.swift, so the equivalent logic lives inline inBookRegistrySync.contentPresence, which was not flag-aware. A.lcpl-only book would resolve.licenseOnly→.downloadNeeded, and the re-download that would heal it is suppressed by the streaming guard — stranding the book permanently, with Listen gone after the first relaunch and never recovering.contentPresenceis now flag-aware.The toolkit pin is deliberately narrow
The first pass pinned the toolkit 33 commits past the 3.2.3 base. The full suite proved that was not free:
PlaybackRateTests(×5) andAudiobookLoaderTestsfailed 3-of-3 iterations here while green on every other branch. PP-4518 had changed the playback ladder from presets[0.75, 1.0, 1.2, 1.5, 2.0]to[0.50, 1.0, 1.5, 2.0, 2.50, 3.0]— a user-visible change to how audiobooks play, which is not what a hotfix for a blank support field should carry.The pin is now the 3.2.3 base plus exactly the two dependency-pin commits the streaming fix needs (
ThePalaceProject/ios-audiobooktoolkit@a408d718). Both are pin-only and say so.iOS 16 is preserved. The 17.0 floor on the first pass was not a decision — the wide pin's toolkit requires 17.0 while the narrowed one requires 13.0. All ten configs return to 16.0, matching 3.2.3, so this hotfix does not drop iOS 16 patrons.
Deliberately not carried: the F1/F2 download-durability fixes. They are real bugs present in 3.2.3, but all five are already ancestors of the 3.3.0 toolkit and 3.3.0 is at RC; they touch OverDrive/OpenAccess rather than the LCP path this hotfix fixes; and one of them exists only to fix a retain cycle another introduced — an area subtle enough to want a release cycle.
Verification
scripts/xcode-test-optimized.sh) — 0 infra errors, 0 timeouts. Sampled ≈0.99×, with one retry-masked load-shaped flake (TPPBookRegistryLargeCorpusTests, failed 126s / passed 16s, green in CI history at 8–26s)..lcpllicenses and zero.lcpa. After SIGKILL + cold launch both stayeddownload-successful, while the other ten records in the same registry readdownload-needed— a control showing reconciliation ran, so the survivors are not an artifact ofload()never firing.contentPresencestreaming arm fails 2 named assertions; deleting theLocalBookContentServiceguard fails 2; mutatingAudiobookSessionManager:2056fails 3. A green test that was never capable of red proves nothing.AudiobookContentGateTestswas 13/7 RED before this branch and nobody knew: every construction site left the flag provider defaulted, so the tests read live Firebase (TRUE at 100%) while asserting download-first behaviour. All three sites are now pinned; 14/0.Build 500. 493 was not free —
Palace-3.3.0.493.ziphad already shipped. 500 verified against two independent sources that agree: the highest build across every git ref is 499, and the highest in ios-binaries is likewise 499.Merge policy
Per
CLAUDE.md, hotfix →mainuses a regular merge commit (--no-ff), never squash, and this branch must then be forward-ported todevelopwith its original SHAs. Squashing is what produced the 296-conflict storm on 3.1.0.Not done
LocalBookContentServicestreaming guard is unit-tested only; withcontentPresencereturning.present, no re-download is scheduled, so that guard is not reached on the device path.🤖 Generated with Claude Code