Service-layer refactor: code review and handoff (2026-09-21)

TLDR

1. Handoff context

What was reviewed

The refactor moves DataManager, RestService and Authentication behind N-Tier ports: Infrastructure services, a shared authenticated transport, a shared cache query, and four narrow auth ports. It is delivered as one branch and one PR per phase, each branch containing all earlier phases. Every PR targets imp/blazor-hybrid-service-layer-decoupling (dca3123d), so the PR diff in Azure DevOps is cumulative (phases 0 to N). The final integration into imp/blazor-hybrid has not been opened.

PR Phase Branch Tip commit Files (incremental / cumulative) Verdict
2505 P0 safety baseline codex/service-layer-phase0 505ff93a 54 / 54 approve with changes
2509 P1 transport foundation codex/service-layer-phase1 dd855bf2 38 / 85 approve with changes
2510 P2 MEC pilot codex/service-layer-phase2 9751e6ab 27 / 101 approve
2511 P3 remaining domains and prefetch codex/service-layer-phase3 7d73ca93 99 / 173 approve with changes
2512 P4 documents codex/service-layer-phase4 19f852a7 54 / 208 approve with changes
2513 P5 authentication split codex/service-layer-phase5 4707790e 157 / 327 block
2514 P6 retire RestService codex/service-layer-phase6 14af5811 92 / 363 approve with changes
2515 P7 retire DataManager codex/service-layer-phase7 6633bb59 85 / 411 approve with changes

PR titles: 2505 "refactor: establish service-layer phase 0 safety baseline", 2509 "refactor: add service-layer phase 1 transport foundation", 2510 "refactor: extract MEC queries in service-layer phase 2", 2511 "refactor: extract remaining service domains in phase 3", 2512 "refactor: extract document services in phase 4", 2513 "refactor: decouple authentication lifecycle in phase 5", 2514 "refactor: retire REST aggregate in phase 6", 2515 "refactor: retire data facade in phase 7".

Source documents in the repo:

How the review was done

A multi-agent review run on 2026-09-21. The agents read files only through git show and git diff against the origin/codex/service-layer-phase* refs. Nothing was checked out, edited, posted or voted on.

  1. 45 finder agents: each phase reviewed through three lenses (behavior regression against the legacy code, concurrency and session-boundary safety, plan conformance with architecture rules and test quality), chunked by file count. Seven cross-cutting audits ran on the tip: DI composition, auth end-to-end, guard-test integrity, leftovers, upgrade compatibility, security, and plan drift. One agent reproduced the build and tests; one audited branch and PR state.
  2. 163 raw findings were merged into 118 unique findings.
  3. Each finding was attacked by skeptic agents trying to refute it: three lenses (code truth, existing guards and tests, regression versus deliberate intent) for critical, high and medium findings, one for the rest. Severity in this document is the verifiers' corrected severity; the filed severity is shown where it changed.
  4. F68 is the one finding with no verdict, because its verifier failed to return. It is kept and marked unverified.

The first workflow run hit a session limit partway through; it was resumed and completed, and this document is from the completed run.

Decisions and constraints the reader needs

  1. Fix F57 on the phase-5 branch together with F65, F64, F62 and F69, then rebase PRs 2514 and 2515 onto it.
  2. Fix F38 on phase 3 (or as a commit at the tip) and F73 on phase 6.
  3. Decide F50 (document download retry): add a bounded retry plus one forced-refresh replay, or record it as an accepted regression.
  4. Merge origin/imp/blazor-hybrid into the integration branch, resolve the two conflicting files at hunk level, and rerun the full unit suite and both native builds at the merged commit.
  5. Run the device walkthroughs listed in section 8 (F57, F65, F64, F62, F38, F12, F33, F50 first).
  6. Merge in numeric order, one PR at a time. The stack PRs have no auto-complete or policies, so each completion is manual.

2. How to read the rest of this document

Line numbers refer to the phase-7 ref (6633bb59) unless a phase is named. IDs are stable within this document only.

Confirmed findings: 89. Severity after verifier corrections: 1 high, 6 medium, 71 low, 11 nit. Refuted: 28. Unverified: 1 (F68).


PR Phase Verdict Reason
2505 P0 approve-with-changes Blanket OperationCanceledException rethrow in 23 DataManager catch blocks removes the offline-cache fallback on HTTP timeouts and Android TCP aborts (F01). Fixed for DataManager by P2 to P7, so P0 must not ship alone. Also open: F02, F03, F04 (all low, F02 and F04 fixed in P5).
2509 P1 approve-with-changes iOS ServiceLayerHttpClient drops the TimeoutIntervalForRequest=30 / Resource=60 config from Fix 1988 (F12). Needs a device check; two-line fix. F14 (ValidateOnStart never runs in the MAUI host), F15, F16, F13 are low.
2510 P2 approve Only low or nit findings (F27, F29, F28, F30, F31).
2511 P3 approve-with-changes Notification lists and push registration lost the legacy transient retry, and the test was renamed to pin Requests == 1 (F38, medium, 3/3). Also F33, F36 (low after correction), F45.
2512 P4 approve-with-changes Document download lost the 5s retry and the silent-login retry (F50, low by majority of verifiers, medium as filed). Either add a bounded retry / one 401 refresh replay or record it as an accepted regression.
2513 P5 block F57 (high, 3/3): JWT exchange lost all transport retry, so one network blip fails refresh, clears the token and arms the 30s cooldown. Also F65 (lost Session Expired / Account Locked dialog on cold start), F64 (logout awaits push POST inside the revocation window), F62 (theme epoch throw aborts logged-out /home), F69 (no test builds the real auth DI graph).
2514 P6 approve-with-changes F73 (medium): FTDT import maps the whole page in the adapter, so one unmappable record stops all import and the sync timestamp never advances. F74, F75, F76 are low.
2515 P7 approve-with-changes Code is clean apart from low items (F84, F85, F86, F87, F88). Blocked on process gates: device walkthroughs P7-A4 and P7-A5 are open, and origin/imp/blazor-hybrid (4774e079) now conflicts with the tip in ALPAMobile/HeadServiceCollectionExtensions.cs and ALPAMobile/Services/Authentication.cs.

Recommended merge and fix order:

  1. Fix F57 first, on the phase-5 branch, then rebase 2514 and 2515 onto it. Bundle F65, F64, F62 and F69 with it. These four were reproduced by two or three verifiers each and all sit in the same auth-lifecycle code.
  2. Land F38 (notification/registration retry) on the phase-3 branch or as a commit at the tip; land F73 (FTDT per-record mapping) on the phase-6 branch or at the tip. If the stack is merged as one unit, commits at the tip are equivalent.
  3. Merge in numeric order, one at a time: 2505, 2509, 2510, 2511, 2512, 2513, 2514, 2515. Each PR contains its predecessors, so the ADO diff for PR N shows phases 0 to N. No auto-complete, no policies and no reviewers exist on the integration branch (section 7), so each completion is a manual step.
  4. Before the final PR into imp/blazor-hybrid: merge origin/imp/blazor-hybrid into the integration branch, resolve the two conflicting files at hunk level, rerun the full unit suite and both native builds at the merged SHA, and run the open device walkthroughs (section 6).

4. Findings ranked by severity

Sev column: corrected severity (filed severity in parentheses where it changed). Votes: held / total verifiers. Fixed later: whether a later phase in the stack repairs it.

ID Sev PR / phase File:line Title Votes Fixed later
F57 high 2513 / P5 ALPAMobile.Infrastructure/Remote/Authentication/AuthRestClient.cs:15,23-26 JWT exchange lost all transport retry; one blip fails refresh, clears token, arms 30s cooldown 3/3 no
F38 medium 2511 / P3 ALPAMobile.Infrastructure/Remote/NotificationsApiClient.cs:58,68 Notification list reads and push registration lost legacy transient retry; test pins the regression 3/3 no
F62 medium 2513 / P5 ALPAMobile.Application/Theming/ThemeCoordinator.cs:109,194,350 ThemeCoordinator propagates ContextInvalidatedException and aborts whole page resolve 3/3 no
F64 medium 2513 / P5 ALPAMobile/Services/Authentication/LogoutParticipants.cs:134-142 Logout awaits push re-registration POST inside the revocation window 3/3 no
F65 medium 2513 / P5 ALPAMobile/Services/Authentication.Session.cs:267-285,411-431 Startup second logout bumps epoch and cancels the Session Expired / Account Locked dialog 3/3 no
F69 medium 2513 / P5 ALPAMobile/HeadServiceCollectionExtensions.cs:157-201 No test builds the real auth DI graph (P5-A1) 3/3 no
F73 medium 2514 / P6 ALPAMobile.Infrastructure/Remote/FlightTimeDutyTime/FtdtApiClientAdapter.cs:40 FTDT import maps whole page unguarded; one bad record aborts import 3/3 no
F01 low (high) 2505 / P0 ALPAMobile/Services/DataManager.cs:393 Blanket OCE rethrow removes offline cache fallback on timeouts 3/3 yes (P2-P7)
F02 low (medium) 2505 / P0 ALPAMobile/Services/Authentication.cs:932-1000 Logout cleanup reordered; cache or token-store failure aborts sign-out 2/3 yes (P5)
F03 low (medium) 2505 / P0 ALPAMobile/Services/DataManager.cs:1833-1866 Coordinator lock held across bulk insert, messenger send, file writes 2/3 no
F04 low (medium) 2505 / P0 ALPAMobile/Services/Authentication.cs:936-975 Token refresh not generation-guarded; can resurrect session after logout 2/3 yes (P5)
F06 low 2505 / P0 ALPAMobile.Infrastructure/Remote/DynamicFeedQueries.cs:67-90 ContextInvalidatedException treated as page failure by UI callers 1/1 no
F07 low (medium) 2505 / P0 ALPAMobile/Services/MenuService.cs:287,440,466 ContextInvalidatedException thrown into unprepared consumers; throws after successful favorites mutation 3/3 no
F12 low (medium) 2509 / P1 ALPAMobile/MauiProgram.cs:247-296 iOS ServiceLayerHttpClient drops NSUrlSession timeouts (Fix 1988) 3/3 no
F14 low 2509 / P1 ALPAMobile/HeadServiceCollectionExtensions.cs:164-207 ValidateOnStart on ApiEndpointOptions never runs in MAUI host 1/1 no
F15 low 2509 / P1 ALPAMobile.Infrastructure/Remote/AuthenticatedRequestProvider.cs:115,161 Failed/offline force-refresh reported as success; 401 replay reuses rejected token 1/1 no
F16 low 2509 / P1 ALPAMobile.Infrastructure/Caching/CachedRemoteQuery.cs:154-185 refresh:true joins in-flight fetch and returns pre-mutation data 1/1 no
F18 low (medium) 2509 / P1 ALPAMobile.Infrastructure/Caching/CachedRemoteQuery.cs:203-215 OCE catch precedes transient classifier; blocks stale fallback 2/3 no
F20 low cross docs/detail/SERVICE-LAYER-IMPLEMENTATION.html:34-48 Test evidence (TRX) not reproducible from repo 1/1 no
F21 low 2509 / P1 UnitTest/Authentication_TokenProviderTests.cs:64-71 InvalidatedContext_DoesNotReturnLateToken cannot fail on the generation guard 1/1 no
F22 low 2509 / P1 ALPAMobile.Infrastructure/Remote/AuthenticatedRequestProvider.cs:22-25,96-103 Default Optional bearer attaches JWT to any absolute URI, no host guard 1/1 no
F23 low 2509 / P1 AGENTS.md:72 Stale RestService.SetEndpoints() / KcmLegacyGatewayUrl references 1/1 partial
F24 low 2509 / P1 ALPAMobile.Infrastructure/Remote/RequestProvider.cs:105,112,143-144 Final transport and deserialization failures no longer logged 1/1 no
F27 low 2510 / P2 ALPAMobile.Infrastructure/Caching/CachedRemoteQuery.cs:186-207 EventsReadyMessage published from pool thread under coordinator lock 1/1 no
F29 low 2510 / P2 ALPAMobile.Infrastructure/Services/Mec/MecService.cs:62-84 Two independent generation captures can share a stale delegate 1/1 no
F31 low 2510-2511 / P2-3 ALPAMobile.Infrastructure/Remote/MecApiClient.cs:25-39 Routes and scope strings hard-coded inline 1/1 no
F33 low (medium) 2511 / P3 ALPAMobile.Application/Services/Caching/CacheContentService.cs:197-206 _backgrounded latch cleared only at end of resume; guest banner refresh skipped on session expiry 3/3 no
F34 low 2511 / P3 ALPAMobile.Application/Services/Caching/CacheContentService.cs:263-285 Removed in-flight guards let duplicate waves queue on _waveGate 1/1 no
F35 low 2511 / P3 ALPAMobile.Infrastructure/Remote/Content/ContentApiClient.cs:108-116 Content failures reported as synthetic InvalidOperationException 1/1 no
F36 low (medium) 2511 / P3 ALPAMobile.Application/Services/Caching/CacheContentService.cs:197,217,371-376 Logout no longer cancels in-flight authenticated prefetch wave 3/3 no
F37 low 2511 / P3 ALPAMobile.Application/Services/Caching/CacheContentService.cs:46,52,378-390 Readiness getters take the coordinator lock on the UI thread 1/1 no
F40 low 2511 / P3 ALPAMobile.Infrastructure/Services/LegacyCatalogFailure.cs:21-26 Reported exception lacks operation name and stack 1/1 no
F41 low 2511 / P3 ALPAMobile.Infrastructure/Remote/MemberApiClient.cs:59-70 Member-number lookup requires valid JSON body 1/1 no
F42 low 2511 / P3 ALPAMobile.Infrastructure/Remote/JumpseatApiClient.cs:42 Jumpseat catalogue parse coerces ISO-looking strings to dates 1/1 no
F44 low 2511 / P3 ALPAMobile.Infrastructure/Services/Notifications/NotificationsService.cs:30-38 Shared cache operation for flight notifications ignores audience set 1/1 no
F45 low (medium) 2511 / P3 UnitTest/Phase3ServiceCompositionTests.cs:17-38 P3-A1 scaffold routers never resolved in a test 3/3 no
F46 low 2511 / P3 ALPAMobile.Infrastructure/Persistence/Search/ContentPrefetchCatalog.cs:7-25 Prefetch catalog wiring untested 1/1 no
F47 low 2511 / P3 UnitTest/CacheContentService_BackgroundingTests.cs:44-47,105-106 Source-text and endpoint-toggle tests weakened 1/1 no
F50 low (medium) 2512 / P4 ALPAMobile.Infrastructure/Remote/Documents/DocumentsApiClient.cs:13-38 Document download lost all retry and silent-login / 401 recovery 3/3 no
F51 low 2512 / P4 ALPAMobile.Infrastructure/Services/Documents/DocumentCacheService.cs:35-40,99-109 Global lock held during file reads and catalogue scan 1/1 no
F55 low 2512 / P4 UnitTest/Phase4ServiceCompositionTests.cs:22-40 Container test overrides the registrations it should prove 1/1 no
F58 low 2513 / P5 ALPAMobile.Infrastructure/Remote/Authentication/AuthRestClient.cs:40 Real 500/502/504 mapping can raise modal Authentication Error on silent refresh 1/1 no
F60 low 2513 / P5 ALPAMobile.Infrastructure/Security/SecureTokenStore.cs:115,159-167 ClearAsync waits on unbounded write gate 1/1 no
F61 low 2513 / P5 ALPAMobile.Application/Services/FlightTimeDutyTime/FTDTDutyPeriodService.cs:53-56,93-98 IsSyncing stuck true after context change 1/1 no
F66 low 2513 / P5 ALPAMobile/Helpers/FTDTDataSyncHelper.cs:311-330 FTDT import self-heal saves server response wholesale (AB#2219 null-protection dropped) 1/1 no
F67 low 2513 / P5 ALPAMobile.Presentation/ViewModels/LoginPageViewModel.cs:287,351-352 Login returns silently when sign-in attempt is rejected 1/1 no
F70 low (medium) 2513 / P5 ALPAMobile/Services/Authentication/LegacyAuthBearerBridge.cs:4-9 P5-A5 bridge has no test; doc drops A5 3/3 code yes (P6); doc no
F71 low 2513 / P5 ALPAMobile.Application/Theming/ThemeCoordinator.cs Phase 5 carries unplanned refactors; "AuthenticateUser" Sentry transaction dropped 1/1 no
F72 low 2513 / P5 UnitTest/Architecture/head-consumers.json No ratchet test for IAuthentication consumers 1/1 no
F74 low 2514 / P6 ALPAMobile.Infrastructure/Remote/MobileMenuApiClient.cs:23-105 Retry diagnostics and Sentry transactions dropped 1/1 no
F75 low 2514 / P6 ALPAMobile.Infrastructure/Remote/MobileMenuApiClient.cs:45,115,140 Cancellation and ContextInvalidatedException reported as GeneralError 1/1 no
F76 low 2514 / P6 ALPAMobile.Infrastructure/Remote/DynamicContentApiClient.cs:53-62 Optional-bearer clients block on token refresh inside their deadline 1/1 no
F79 low 2514 / P6 UnitTest/TransientNetworkErrorClassifierTests.cs:56 Transient-profile test asserts a production-dead method 1/1 no
F80 low 2514 / P6 UnitTest/Architecture/LayerDependencyRulesTests.cs No no-leak architecture test for FTDT/MobileMenu generated types 1/1 no
F84 low 2515 / P7 ALPAMobile.Infrastructure/Caching/SignInContextReset.cs:28-37 Cache cleanup failure during sign-in escalates to credential-erasing logout 1/1 no
F85 low 2515 / P7 ALPAMobile.Infrastructure/Caching/SignInContextReset.cs:16-40 New member's token persisted before outgoing member's cache purge 1/1 no
F86 low 2515 / P7 ALPAMobile/Helpers/AppProperties.cs:430 Readers of ITokenStore.UserInfoJson bypass the published-identity snapshot 1/1 no
F87 low 2515 / P7 ALPAMobile/Services/Authentication.Session.cs:35-52,74-85,171 Abandoned explicit sign-in leaves _explicitSignInEpoch unpublished 1/1 no
F88 low (medium) 2515 / P7 UnitTest/Authentication_PublicationTests.cs:134-170 Real sign-in publication path never exercised end to end 3/3 no
F89 low 2515 / P7 ALPAMobile.Infrastructure/Caching/SignInContextReset.cs:29-38 Sign-in reset clears only API cache and document DB 1/1 no
F90 low 2513 / P5 ALPAMobile/HeadServiceCollectionExtensions.cs:170-199 Logout phase/order declared twice 1/1 no
F91 low pre-existing ALPAMobile/Services/SharedActionsService.cs:48 Resolves unregistered concrete ALPALogService; document-open event never logged 1/1 no
F92 low 2513 / P5 ALPAMobile/Services/Authentication.Session.cs:231-237,264 Superseded sign-in can leave new username with old password 1/1 no
F93 low 2511 / P3 (pre-existing) ALPAMobile/Services/FlightSearchPushNotificationService.cs:273 Raw push tokens and Installation logged despite doc claim 1/1 no
F94 low 2505 / P0 UnitTest/Architecture/SourceConsumerScanner.cs:57-70 Scanner alias detection is exact-match 1/1 no
F95 low 2509, 2513 / P1, P5 UnitTest/Architecture/HeadConsumerRatchetTests.cs:56-64 Ratchet allowlist grew twice; "only remove" rule not enforced 1/1 partial
F96 low (medium) cross docs/detail/SERVICE-LAYER-DECOUPLING-PLAN.html:1823,1947 Plan-committed raw-boundary architecture check does not exist 3/3 no
F97 low cross docs/detail/SERVICE-LAYER-PHASE-0-INVENTORY.html:80,82 P7-A3 inventory absent; Phase 0 inventory cites deleted test classes 1/1 no
F98 low 2514 / P6 scripts/gateway-readiness-check.py:51,1098 Gateway catalog drift guard silently no-ops after RestService.cs deletion 1/1 no
F99 low 2514-2515 / P6-7 docs/FTDT-DATA-MIGRATION-PLAN.md:147,154,310 Runbooks still point at removed RestService/DataManager symbols 1/1 no
F101 low 2513-2515 / P5-7 ALPAMobile.Application/Abstractions/Authentication/IAuthentication.cs:55 Production code kept alive only by tests 1/1 no
F102 low 2514 / P6 ALPAMobile/Services/Platform/LegacyRestNetworkErrorClassifier.cs:33 iOS NSURLError transient-code list duplicated in three places 1/1 no
F104 low 2509, 2514 / P1, P6 ALPAMobile.Presentation/Components/Pages/SettingsPage.razor.cs:153-185 Environment toggle flips endpoints before session is revoked 1/1 no
F106 low 2512 / P4 ALPAMobile/Services/Authentication/LogoutParticipants.cs:45-58 Downloaded PDFs not purged at sign-out 1/1 no
F108 low (medium) 2511 / P3 docs/detail/SERVICE-LAYER-DECOUPLING-PLAN.html:333,740 Plan 4.2 auth-fallback statement contradicts itself; Phase 0 matrix omits subscriptions and airports 3/3 no
F109 low (medium) 2513 / P5 docs/detail/SERVICE-LAYER-IMPLEMENTATION.html Phase 5 evidence table reuses plan IDs with different meanings 3/3 no
F112 low 2509 / P1 docs/detail/SERVICE-LAYER-IMPLEMENTATION.html:28 Doc says only GET retries; false at tip 1/1 no
F117 low cross docs/detail/SERVICE-LAYER-DECOUPLING-PLAN.html:327 Integration branch not synced with imp/blazor-hybrid 1/1 no
F10 nit (low) 2505 / P0 UnitTest/DataManager_CacheContextTests.cs:94 Late-response barrier tested for few of ~25 paths 1/1 partial
F13 nit (low) 2509 / P1 ALPAMobile.Infrastructure/Remote/AuthenticatedRequestProvider.cs:105-107 401 recovery disabled for GET with MaxRetries=0 1/1 no
F28 nit (low) 2510 / P2 ALPAMobile.Infrastructure/Remote/Mec/MecWireModels.cs:96-97 STJ MEC wire models lack Newtonsoft empty-string numeric leniency 1/1 no
F30 nit (low) 2510 / P2 UnitTest/MecCompatibilityTests.cs:251-271 Phase 2 acceptance evidence partly missing 1/1 moot (P7)
F52 nit 2512 / P4 ALPAMobile.Infrastructure/Caching/DocumentCacheCodecs.cs:19-21 Persisted "null" catalogue treated as corrupt each launch 1/1 no
F81 nit 2514 / P6 UnitTest/ApiEndpointOptionsTests.cs:143 Tautological assertion 1/1 no
F82 nit 2514-2515 / P6-7 ALPAMobile/Platforms/iOS/AppDelegate.cs:381 Stale DataManager/RestService names in messages, comments, tests 1/1 no
F83 nit 2515 / P7 ALPAMobile.Presentation/Components/Pages/JumpseatResultsPage.razor.cs:136-150 Comments attribute flight-search contract to the jumpseat service 1/1 no
F100 nit 2515 / P7 docs/component-specifications/card/pilot-card-property-mapping.html:130,170 Component specs still prescribe injecting DataManager 1/1 no
F103 nit (low) 2510-2511 / P2-3 ALPAMobile.Infrastructure/Remote/JumpseatApiClient.cs:27,37 Member JWT sent to legacy gateway host on token-free endpoint 1/1 no
F105 nit (low) 2511 / P3 ALPAMobile.Infrastructure/Remote/MemberApiClient.cs:34-38,51-55 Deserialization exception messages embed payload values 1/1 no

5. Detail per confirmed finding

Format: defect, failure scenario, evidence, fix, verifier reasoning. Severity corrections are the majority of verifier votes.

High

F57 (high, 3/3 held; votes medium / high / high -> high). P5, PR 2513. JWT exchange lost all transport retry.

Medium

F38 (medium, 3/3 held; votes medium x3). P3, PR 2511. Notification list reads and push registration lost transient retry.

F62 (medium, 3/3; votes medium / low / medium -> medium). P5, PR 2513. ThemeCoordinator aborts whole page resolve.

F64 (medium, 3/3; medium x3). P5, PR 2513. Logout awaits push re-registration POST inside the revocation window.

F65 (medium, 3/3; medium x3). P5, PR 2513. Startup second logout cancels the Session Expired / Account Locked dialog.

F68 (low, UNVERIFIED). P5. AuthSessionEffects.cs:37-44, FTDTDataSyncHelper.cs:124-140. StartCurrent calls effect() inside TryExecute, so the synchronous prefix of push registration / FTDT SyncAllAsync (LiteDB read, DI resolution, token acquisition) runs under the global coordinator lock, against the stated "never run work inside the coordinator" invariant. The verifier for this finding failed to return (infrastructure error), so it has not been independently checked. Fix: check under the lock, start the effect outside via Task.Run / Task.Yield.

F69 (medium, 3/3; medium x3). P5, PR 2513. No test builds the real auth DI graph (P5-A1).

F73 (medium, 3/3; medium x3). P6, PR 2514. FTDT import maps the whole page unguarded.

Low and nit (in id order)

F01 (low; filed high; votes low / low / medium -> low). P0, PR 2505. DataManager.cs:393 (23 sites), MenuService.cs:269, FavoritesService.cs:121. Phase 0 adds if (ex is OperationCanceledException) throw; after EnsureCacheContext(generation). None of those methods take a caller token, so the only OCEs reaching them are HttpClient timeouts (TaskCanceledException), Android TCP aborts (AB#2494) and ContextInvalidatedException; the base fell through to cached JSON. Failure at P0 through P6: poor network exhausts retries with TaskCanceledException; GetMemberAsync throws (contradicting AB#2396), the tab bar is not primed and Jumpseat/KCM/MEC/notification screens fail instead of showing cache. Only ContextInvalidatedException needed the rethrow, and EnsureCacheContext already throws for it. No test seeds a cache and throws a timeout. Fix: rethrow only ContextInvalidatedException or OCE with a cancelled supplied token. Verifiers: real at P0; at the tip DataManager is deleted and RequestProvider + LegacyCatalogFailure turn timeouts into TimeoutException / HttpRequestException, so CachedRemoteQuery falls back. Two verifiers say the tip MenuService:252 / FavoritesService rethrow only sees real caller cancellation; one says a refresh=true timeout inside the SDK path can still skip fallback (medium). Majority: low at tip, high in P0 to P6 intermediate states. Do not ship PR 2505 alone.

F02 (low; filed medium; votes low / low / refuted -> low; 2/3). P0, PR 2505. Authentication.cs:932-1000. ClearSessionCacheContextAsync now runs first; a ClearCaches() (LiteDB DeleteAll) throw exits before the sign-out flags flip; a _tokenStore.ClearAsync throw skips cookies, image cache, theme/site memos, FavoriteToggleContext.Invalidate and AppProperties.Clear. Needs a corrupt/locked LiteDB or keystore fault. Fixed by P5 StartLogout / CompleteLogoutAsync (flags flip first, every step in its own try/catch). Third verifier refuted (nit): legacy also aborted on a bare ClearCache() throw.

F03 (low; filed medium; votes low / refuted / low; 2/3). P0. CacheContextCoordinator holds one monitor for Generation, TryExecute and Invalidate; ClearDatabase, three BulkInsert*, DocumentsVersion++, synchronous WeakReferenceMessenger.Send and File.WriteAllBytes run inside it (still true for DocumentCatalogHydrator at the tip). Holding the lock across the writes is plan-mandated (plan lines 711, 739: cache write atomic with ClearCache); subscribers are fire-and-forget so the deadlock scenario is unsupported; no on-device measurement. Genuine residue: Generation could be a Volatile.Read. Fix: make Generation lock-free; keep the writes under the lock.

F04 (low; filed medium; votes low / low / refuted; 2/3). P0. Authentication.cs:936-975, 356-372, 1306-1314. Refresh and silent login apply results with no epoch/generation check; the final Invalidate does not repeat the bearer/authToken clear. Pre-existing race (base had no guard) and narrower than filed; fixed by P5 AuthSessionAttempt(Epoch, Generation) / IsAttemptCurrent.

F06 (low, 1/1). P0. DynamicFeedQueries.cs:67-90,139-176, AlpaIHomePage.razor.cs:321-331, Main.razor.cs:130-146. GetPageAsync/GetSiteAsync throw ContextInvalidatedException on generation change; callers catch (Exception), ex.Report(), set _pageNotFound. Sign-out during a feed fetch logs a benign error; the UI impact is mostly masked by the logged-out branch. Fix: catch ContextInvalidatedException / OCE before the generic catch.

F07 (low; filed medium; votes medium / low / low -> low). P0. MenuService.cs:287,440,466, FavoritesService.cs:231,282, TopNav.razor.cs:65,79. CommitContext throws ContextInvalidatedException on a menu request-version change (base returned fetched data uncommitted, AB#2380); the test was inverted. Reversal is deliberate (plan line 426, Phase 0 inventory line 99: "return an explicit context-invalidated outcome instead of delivering the old member's result"). Residue: FavoritesService.AddFavoriteAsync / RemoveFavoriteByIdAsync throw after the server call already succeeded, and TopNav.OnInitializedAsync awaits an unguarded menu load. Sentry claim overstated (Report() only logs). Fix: do not throw after a successful mutation; treat the exception as expected in AppShellViewModel / TopNav.

F10 (nit; filed low). P0. Late-response-after-logout tests cover notifications, documents, menu, favorites and site; the other ~20 paths are untested. All 35 CommitCacheContext sites are wrapped today, and the classes are replaced by CacheContextSafetyTests at the tip. Missing: a rule against raw SetCache. Fix: parameterized late-response test plus a source rule.

F12 (low; filed medium; votes low / medium / low -> low). P1, PR 2509. MauiProgram.cs:247-296, InfrastructureServiceCollectionExtensions.cs:67-68, RequestProvider.cs:102-120. Legacy DefaultHttpClient sets TimeoutIntervalForRequest=30 / Resource=60 (Fix 1988: HttpClient.Timeout ignored by NSUrlSessionHandler, 209s in Sentry). The new client uses bare DefaultSessionConfiguration, infinite HttpClient.Timeout, and only a linked CTS CancelAfter(30s) with no WaitAsync guard. Whether NSUrlSessionHandler honours the token is unverified (the repo's own view-model comment treats managed cancellation against it as functional; default request timeout of 60s still bounds an idle stall). Failure if the token is ignored: a degraded gateway outlives the 30s deadline across the whole migrated API surface. Fix: set the same two timeouts on the service-layer session configuration; verify on device. Not fixed at tip.

F14 (low, 1/1). P1. HeadServiceCollectionExtensions.cs:206. ValidateOnStart() never runs because MauiApp.Build() uses a plain provider and nothing calls IStartupValidator; validation is lazy on the first IOptionsMonitor.CurrentValue read, and ToggleEndpointEnvironment sets DevEnv before ApplyEndpoints validates. Impossible with today's hard-coded values. Fix: read CurrentValue after Build, or drop the fail-fast claim.

F15 (low, 1/1). P1. AuthenticatedRequestProvider.cs:115,161, Authentication.TokenProvider.cs:14. ForceRefreshAsync returns true for any non-empty token, including the unchanged current token on offline/exception paths; ProduceRefreshAsync caches (generation, T1) -> true, blocking later real refreshes until the token changes. Fix: require the re-read token to differ from key.Token.

F16 (low, 1/1). P1. CachedRemoteQuery.cs:154-185. GetAsync(refresh:true) joins any in-flight operation for the key regardless of start time and commits its result; concrete instance: flight notifications after an audience change. Fix: start a fresh operation when the existing one began earlier, or add Invalidate(key).

F18 (low; filed medium; votes low / refuted / low; 2/3). P1. CachedRemoteQuery.cs:203-215. First catch stores any OCE without checking operation.Token, so the transient-classifier catch is unreachable for OCE. In production every fetch goes through RequestProvider, which converts timeouts to Result failures (Timeout / TransportError, both fallback-eligible), so only an OCE from the token step (GetValidTokenAsync) can escape. Test ThrownCancellation_NeverBecomesTransportFallback pins the strict contract. Fix: filter with when (operation.Token.IsCancellationRequested || generation stale).

F20 (low, 1/1). cross. TRX files are git-ignored (test-logs/), zero .trx are tracked, the tested SHA is "recorded in the phase PR", phases 4, 5, 7 lack SHAs in the doc, and the phase 7 TRX finished at 07:05:39, before tip commit 6633bb59 (07:07:30). Counts match on-disk TRX. Note: section 5 reproduces 2,607 / 2,601 at the tip, which closes this for the tip. Fix: attach TRX summaries with tested SHA to each PR.

F21 (low, 1/1). P1. Authentication_TokenProviderTests.cs:64-71. The test sets IsLoggedIn false inside Invalidate, and the code also requires IsLoggedIn, so removing the generation guard still yields null. Fix: keep IsLoggedIn true across the invalidation.

F22 (low, 1/1). P1. AuthenticatedRequestProvider.cs:96-103, RequestProvider.cs:117-119. Default Bearer = Optional attaches the member JWT to any absolute URI; no host check against ApiEndpointOptions. All current callers use configured hosts. Fix: attach only for configured hosts, or default to None.

F23 (low, 1/1). P1. Stale refs to RestService.SetEndpoints() / KcmLegacyGatewayUrl: AGENTS.md:74, docs/detail/GATEWAY-QA-READINESS-CHECK.md:18,27, docs/mobile-content-api/index.html:167,172, tools/fault-injection/mitmproxy_dispose_repro.py:21. AGENTS.md:72 and ImageCachePolicy.cs:44 were fixed later.

F24 (low, 1/1). P1. RequestProvider.cs:135-136,171. Retry exhaustion and JSON failure no longer log (base logged both at error level); only retry warnings remain. Callers such as FlightSearchApiClient, JumpseatApiClient, NotificationsApiClient show no logging on those paths. Fix: log the final failure without the query string.

F27 (low, 1/1). P2. CachedRemoteQuery.cs:186-207, MecService.cs:78-85, MyPilotGroupPageViewModel.cs:93-110,286-350. ProduceAsync resumes on a pool thread (ConfigureAwait(false)) and sends EventsReadyMessage inside the coordinator lock; the handler enumerates Groups and sets list-item properties with no main-thread marshalling. Legacy sent from a finally on the caller's context. Failure: race with SetGroupedItems; exception swallowed by ex.Report(). Fix: send after releasing the lock via a main-thread dispatcher, or marshal in the handler.

F28 (nit; filed low). P2. MecWireModels.cs:96-97. decimal? latitude/longitude have no numeric-leniency converter; Newtonsoft mapped "" to null, STJ throws JsonException. No fixture or capture shows the gateway ever sends "". Fix: lenient nullable-number converter, or confirm with a gateway capture.

F29 (low, 1/1). P2. MecService.cs:62-84, CachedRemoteQuery.cs:52,167-183. MecService captures Generation and closes delegates over it; GetAsync captures again. A logout+login landing between the two captures installs the operation at the new generation with an old-generation delegate, so a current-session joiner gets ContextInvalidatedException. Window is a few statements; self-heals on the next call. Fix: pass the query's captured generation into the delegates.

F30 (nit; filed low). P2. Missing: DataManager forwarding test (moot at P7), MyPilotGroupPageViewModel EventsReadyMessage subscriber test (real gap), end-to-end MecService corrupt-row test (decoders are tested individually). ClearInMemoryCache no longer exists at the tip.

F31 (low, 1/1). P2-3. Inline route and scope strings in MecApiClient.cs:25-39, MecService.cs:49, NotificationsApiClient.cs:31,58, SubscriptionsApiClient.cs:18,36, ContentApiClient.cs:41-88; one route already lives in Constants (FlightNotificationListPath). Legacy RestService also inlined routes. Violates the "no magic strings" rule in CLAUDE.md. Fix: shared route constants; ask before changing the Phase 2 idiom.

F33 (low; filed medium; votes medium / low / low -> low). P3. CacheContentService.cs:198-204,321-341,639, App.xaml.cs:264-300. SuspendWaves sets _backgrounded; only RetryIncompleteWavesIfNeededAsync clears it (after a 1500ms delay and token refresh). The "refresh throws, latch never clears" path is effectively unreachable (GetJWTAuthTokenCore swallows exceptions). Reachable: on resume with an expired token, the rejected refresh raises IsAuthenticatedChanged(false) while the latch is set; the handler returns early, so FetchContentUnAuthorized() (guest banner refresh) is skipped and Retry does not replay it. Search index notifications still fire via CacheContentAsync. Fix: clear the latch at the start of Resumed or in finally; run the guest banner refresh on backgrounded sign-out.

F34 (low, 1/1). P3. CacheContentService.cs:263-285,425-431,648-655,698-704. Legacy skipped triggers while a wave ran; now waiters serialize on _waveGate and re-check only after a successful wave, so N queued handlers each replay a full failed wave. The orphaned connectivity CTS existed in legacy too. Fix: coalesce waiters onto the in-flight wave task or add a cooldown.

F35 (low, 1/1). P3. ContentApiClient.cs:108-116, ContentService.cs:170-172. Failures reported as new InvalidOperationException($"{operation}: {error.Message}") via ICrashReporter, bypassing ex.Report(): no ALPA Event_GeneralError, Sentry grouping changes, dedupe key bypassed. Other new services still use ex.Report(). Fix: report through a path that also writes the ALPA event.

F36 (low; filed medium; votes medium / low / low -> low). P3. CacheContentService.cs:197,217-218,371-376. StartLogout invalidates the generation before NotifySignedOut raises IsAuthenticatedChanged(false), so SynchronizeStateGeneration() zeroes the flags first and the if (prefetchingAuth || caching && ...) authCancellationSource?.Cancel() guard is never true; the outgoing wave keeps dispatching steps and holds _waveGate. Impact is smaller than filed: post-logout requests fail fast with auth-required (token already cleared), commits are generation-gated, the only image prefetch source (kcm-map) has ClearOnSignOut: false. No logout-during-wave test. Fix: capture pre-reset flags or cancel sources before synchronizing; check generation between steps.

F37 (low, 1/1). P3. CacheContentService.cs:46,52,378-390, SearchViewModel.cs:85,106. IsSearchIndexReady / IsFetchInProgress now take the coordinator lock, so a render overlapping Invalidate waits for the cache wipe. Brief hitch, no deadlock. Fix: volatile fields or an immutable snapshot.

F40 (low, 1/1). P3. LegacyCatalogFailure.cs:21-26. Default branch is new InvalidOperationException(error.Message) with no operation and no stack, then Report(); the caller logged is "Create". KCM airports and subscriptions both returning 500 collapse into one identical event. Fix: $"[{operation}] {error.Message}" as ContentService does.

F41 (low, 1/1). P3. MemberApiClient.cs:59-70. GetAsync<JsonElement> throws on a bare body (A12345, 00123456); legacy stripped quotes from raw text. Only reachable if the server sends unquoted text; tests cover "A12345" and 12345 only. Fix: read as raw text.

F42 (low, 1/1). P3. JumpseatApiClient.cs:42. JToken.Parse uses the default DateParseHandling.DateTime; an ISO-like string field becomes culture-formatted text (FlightSearchApiClient sets DateParseHandling.None). Fix: JsonTextReader with DateParseHandling.None.

F44 (low, 1/1). P3. NotificationsService.cs:30-38. The fetch closure captures per-call selected audiences but the operation is keyed by one registry key; a refresh joining an in-flight fetch returns and persists the first caller's audience set. Self-heals on the next fetch. Fix: include the audience hash in operation identity.

F45 (low; filed medium; votes low x3 -> low). P3. Phase3ServiceCompositionTests.cs:17-38. No test resolves any *QueriesRouter; plan row P3-A1 "scaffold routers retain source policy" cites per-slice DI tests. Typed router constructors and the global-port Check<> assertions partly guard it. Fix: resolve each router and assert live/mock delegates under the flag.

F46 (low, 1/1). P3. ContentPrefetchCatalog.cs:7-25. Seven key-to-query mappings untested; orchestration tests use a fake catalog; composition test does not resolve CacheContentService. All seven are correct today.

F47 (low, 1/1). P3. CacheContentService_BackgroundingTests.cs:105-106 loosened to Does.Contain("!prefetchedAuth") / "!cached" (source-text scans); ApiEndpointOptionsTests no longer checks the /v2 toggle for Notifications, Subscriptions, Jumpseat. The clients read CurrentValue per request today. Fix: behavioural tests.

F50 (low; filed medium; votes medium / low / low -> low). P4, PR 2512. DocumentsApiClient.cs:13-38, AuthenticatedRequestProvider.cs:106, RequestProvider.cs:105, DocumentCacheService.cs:64-70, UnitTest/DocumentsApiClientTests.cs:193. Legacy DownloadDocumentItem made up to three attempts (5s retry, then 5s + SilentLoginWithRetryAsync + retry). The new client POSTs once (MaxRetries = 2 is inert for POST; 401 replay is GET-only); prefetch loses retry too; user Retry calls GetValidTokenAsync(force:false), which reuses a token the gateway rejected but the client still considers valid. Test DownloadFailure_IsNeverReplayedOrUsedToForceRefresh pins it. /api/doc/downloadfile is read-only, so the "no write replay" rationale does not apply. Two verifiers lowered severity because it is documented as a deliberate refinement (implementation doc ~line 184; plan lines 797 and 1126) and only bites on revocation/clock skew or a transient 5xx. Recommendation stands: add a bounded read-POST retry plus one forced-refresh replay for downloadfile, or record it as an accepted regression.

F51 (low, 1/1). P4. DocumentCacheService.cs:35-40,60,99-109. File.ReadAllBytes and the catalogue-versus-disk scan (GetDocuments, File.Exists each, GetCategories) now run inside TryExecute; legacy held the lock only around WriteAllBytes. Milliseconds to tens of ms. Fix: do reads outside the lock with Ensure(generation).

F52 (nit). P4. DocumentCacheCodecs.cs:19-21, DocumentCatalogService.cs:71-75,121. ProduceAsync persists "null" and the codec fails it as Corrupt, so each cold start logs a warning and deletes the row. Only for an account whose catalogue is null. Fix: do not persist null.

F55 (low, 1/1). P4. Phase4ServiceCompositionTests.cs:22-40. After AddHeadServices() the test re-registers IDocumentPrefetchConfiguration, ISettingsService, IOptionsMonitor<PrefetchTagOptions>, so the head aliases and PrefetchTagOptionsConfigurator are never resolved. Fix: fake only platform leaves.

F58 (low, 1/1). P5. AuthRestClient.cs:40, Authentication.cs:647-648. Legacy Polly turned every >= 500 into 503 (silent branch); the new client delivers real 500/502/504, which reaches ShowAuthenticationErrorMessage during a silent background refresh. AuthUiMessageGate limits it to one dialog at a time. Fix: map to service-unavailable or suppress the dialog on silent refresh.

F60 (low, 1/1). P5. SecureTokenStore.cs:115,159-167, Authentication.Session.cs:177-213,343-380. ClearAsync waits on an unbounded SemaphoreSlim; if a keystore write hangs, CompleteLogoutAsync never decrements _tokenRevocations, so BeginSignInAttempt returns (-1,-1) until restart. Rare and hypothetical. Fix: bound the wait.

F61 (low, 1/1). P5. FTDTDutyPeriodService.cs:53-56,93-98. The finally resets IsSyncing only through TryExecute(generation), skipped after a context change; the wait loop has no timeout. The same DutyPeriod instance must survive logout and re-sign-in. Fix: reset IsSyncing unconditionally.

F66 (low, 1/1). P5. FTDTDataSyncHelper.cs:311-330. Import self-heal saves the SyncFDPAsync response wholesale, dropping the AB#2219 null-skipping merge that legacy SaveAndSyncAsync had. Narrow: freshly imported record; Results is the only locally computed field at risk. Fix: reuse the PushAsync merge.

F67 (low, 1/1). P5. LoginPageViewModel.cs:287,351-352. if (!await authentication.CompleteJwtSignInAsync(...)) return; gives the user no message, and an earlier success analytics event has already fired; sibling view models show "This sign-in attempt is no longer active." No unit tests for LoginPageViewModel.

F70 (low; filed medium; votes low x3). P5. LegacyAuthBearerBridge.cs:4-9. P5-A5 "bridge regression tests" has no test (only the AuthCoreBearer recording fake); the doc table lists A1 to A4 only and remaps A3 (FTDT guards) and A4 (build counts). The bridge was deleted in P6, so the mis-wiring scenario cannot occur; the doc gap remains at the tip.

F71 (low, 1/1). P5. Unplanned refactors in an "auth split": ThemeCoordinator / ThemeStore rewrite and IThemeStore.SaveAsync(ThemeResponse, long), ~490-line FTDTDataSyncHelper AsyncLocal generation change, SecureTokenStore gating, ExceptionReportedDataKey alias, larger port surface. The FTDT and theme generation changes are documented; not documented: the _tracer.StartTransaction("AuthenticateUser", ...) login-latency Sentry span was silently dropped (AuthRestClient has no tracer). Fix: restore the span or document.

F72 (low, 1/1). P5. The ratchet tracks only IRestService, DataManager, IRestEndpointProvider (HeadConsumerRatchetTests.cs:192-194); the plan's "IAuthentication referenced only by ..." exit has no enforcing test. At the tip it holds in code; [Obsolete] is an error in strict layers (EnableStrictWarnings) but only a warning in the head project and tests. The cited line 505 does not exist at the tip (the only reference is line 200). Fix: add IAuthentication to the ratchet.

F74 (low, 1/1). P6. MobileMenuApiClient.cs:23-105, RequestProvider.cs:127. RestService opened Sentry transactions for GetMenuItemsAsync / GetFavoriteItemTypesAsync / GetFavoritesAsync and RetryTransientAsync showed the "Network issue detected. Retrying..." snackbar plus an Event_GeneralDebug log; now only an ILogger warning without operation name. Not documented. Verified for menu and favorites only. Fix: retry observer plus ITracer spans, or document the removal.

F75 (low, 1/1). P6. MobileMenuApiClient.cs:45,57,69,93,101,115,140. Every adapter catch calls Report(error) unfiltered, including ContextInvalidatedException and cancellation, which stamps the exception so outer layers cannot filter. MemberApiClient already has catch (OperationCanceledException) { throw; }. Fix: add that catch before the general one.

F76 (low, 1/1). P6. DynamicContentApiClient.cs:53-62, LogApiClient.cs:19-24, AuthenticatedRequestProvider.cs:96-102. Optional-bearer clients now await GetValidTokenAsync inside their deadline (DynamicContent 15s), which can run a full refresh and raise session-recovery dialogs from background work (log flush). Bounded and transient. Fix: peek the current token without refresh, or use None for /api/log.

F79 (low, 1/1). P6. TransientNetworkErrorClassifierTests.cs:56 asserts LegacyRestNetworkErrorClassifier.IsTransient, which has no production caller; the real gate is the catch filter in RequestProvider.cs:130-132. Fix: drive the table through RequestProvider or delete the helper.

F80 (low, 1/1). P6. Plan P6-A2 ("FTDT generated types do not leak") has no architecture rule; LayerDependencyRulesTests has 10 rules, none for the generated namespaces; the package reference has no PrivateAssets. Code is clean today. P6-A4 native viewer walkthrough is pending. Fix: architecture test restricting generated namespaces to Infrastructure.

F81 (nit). P6. ApiEndpointOptionsTests.cs:143 asserts on a snapshot captured before the failing apply; cannot fail. The other two assertions are meaningful.

F82 (nit). P6-7. 22 "DataManager is null" / "not available" strings in non-test code (e.g. AppDelegate.cs:381), class DataManagerAirportLookup (HeadServiceCollectionExtensions.cs:246), UnitTest/DataManager_* file names and ~65 comments still name deleted types. Sentry messages point at a nonexistent class. Fix: reword to name the null port; rename the class and tests.

F83 (nit). P7. JumpseatResultsPage.razor.cs:136-150. Comments say "the jumpseat service preserves the AB#1883 rethrow contract"; the code is FlightSearchService.SearchFlightsAsync. Behaviour matches legacy.

F84 (low, 1/1). P7. SignInContextReset.cs:28-37, Authentication.Session.cs:143-152,266-269, Authentication.cs:274-292,864-876. SignInContextReset throws AggregateException on cache/LiteDB cleanup failure; PublishJwtAsync and the new ApplyAcquiredTokenAsync catch route it to HandleSecureStorageFailureAsync, which logs "SecureStorage failure" and starts a SessionExpired logout with ClearStoredCredentials: true. At P6 the MSAL path just reported and returned false. The wipe-on-storage-failure decision is a recorded owner ruling (AB#2220), but it covers store failures, not cache cleanup. Fix: revoke-only logout with the correct label for cleanup failures.

F85 (low, 1/1). P7. Authentication.Session.cs:239-260, SignInContextReset.cs:16-40. SetAuthTokenAsync / SetUserInfoJsonAsync for the incoming member run before PublishContextAsync waits on MutationGate and purges. A kill in that window hydrates B on relaunch with A's rows on disk, and same-member NeedsContextPublication skips the purge. Requires explicit sign-in over a still-signed-in member. Base cleared only on logout, so not a regression. Fix: write a durable purge-pending marker first.

F86 (low, 1/1). P7. AppProperties.cs:430, FlightSearchPushNotificationService.cs:245, PushNotificationHandler.cs:47 read _tokenStore.UserInfoJson, which SecureTokenStore.WriteAsync updates before guarded publication, while CurrentUser still returns the outgoing member; memberJson in PushNotificationHandler is an unused read. Fix: read via ISessionState.CurrentUser.

F87 (low, 1/1). P7. Authentication.Session.cs:48,74-85,171. BeginSignInAttempt sets _explicitSignInEpoch; _publishedSignInEpoch advances only on publication. A rejected explicit login while a session is retained leaves the epochs unequal, so the next silent refresh takes the context-publication path and wipes the API cache and document DB unnecessarily. No test. Fix: clear the explicit epoch on failure.

F88 (low; filed medium; votes low x3). P7. Authentication_PublicationTests.cs:134-170, AuthCoreTestPorts.cs:34-46. AuthCoreEffects.PublishContext re-implements the SignInContextReset barrier; the real class is tested only in isolation (SignInContextResetTests); nothing resolves IAuthSessionEffects from a container. DI registrations are statically sound and the phase 5 exit requires live verification, not a resolution test. Fix: composition test plus an Authentication test over the real reset.

F89 (low, 1/1). P7. SignInContextReset.cs:29-38. NeedsContextPublication fires on a changed UserName/MEC/LEC/UserType, but the reset clears only ICacheStore and the document DB. Feed and theme state are invalidated by the generation bump; favorites hearts (FavoriteToggleContext) and preferences run only at logout. Same as old DataManager, so not a regression.

F90 (low, 1/1). P5. HeadServiceCollectionExtensions.cs:170-199, LogoutParticipants.cs:13-28. The runner orders by LogoutParticipantRegistration.Order/Phase and never reads ILogoutParticipant.Phase/Order; both are declared. Values agree today. Fix: single source of truth or a test asserting agreement.

F91 (low, pre-existing). SharedActionsService.cs:46-48. GetService<ALPALogService>() returns null because only IALPALogService is registered (ApplicationServiceCollectionExtensions.cs:31), so Event_DocumentOpened is never logged. Same code at dca3123d. Flight.cs:1866,1927 have the same defect. Fix: resolve IALPALogService.

F92 (low, 1/1). P5. Authentication.Session.cs:231-237,264. SetUserNameAsync then IsAttemptCurrent then SetPasswordAsync; early returns skip any rollback (the only rollback is _tokenStore.ClearAsync), so a superseded attempt can leave B's username beside A's password, and the next silent login is rejected. Narrow; logout normally clears credentials afterward. Fix: write the pair as one operation.

F93 (low, pre-existing, 1/1). FlightSearchPushNotificationService.cs:273 logs the serialized Installation (UserId = user name, PushChannel = device token); MauiProgram.cs:525, FirebaseService.cs:33, AppDelegate.cs:308,312 log raw tokens. SERVICE-LAYER-IMPLEMENTATION.html:134 claims raw tokens are no longer logged; false. Identical at base. Fix: redact the lines or correct the doc.

F94 (low, 1/1). P0. SourceConsumerScanner.cs:57-70. Aliases register only on an exact target-name match, so global::-qualified or nested-type aliases used only in platform-conditional code evade the ratchet (confirmed by mutation). All five targets are deleted at the tip; the IL scanner is a second check.

F95 (low, 1/1). P1, P5. HeadConsumerRatchetTests.cs:56-64 checks only that JSON equals the scan. Phase 1 added 2 active entries (OptionsRestEndpointProvider), phase 5 added 2 (LegacyAuthBearerBridge); active counts p0..p7 = 54, 53, 51, 48, 48, 47, 6, 0. Both shims are migrated at the tip; the test was not hardened. Fix: pin the Phase 0 key set or assert only migrated transitions.

F96 (low; filed medium; votes low / low / medium -> low). cross. Plan row 1823 promises "raw-boundary checks" and criterion 1947 says "no service deserializes external JSON directly into domain models"; UnitTest/Architecture has no such test (e.g. AnonymousTabBarSeedProvider.cs:58 deserializes into the domain type). Not a per-phase acceptance gate. Fix: IL test forbidding generic Deserialize over Domain types in Infrastructure/Remote, or descope in the plan.

F97 (low, 1/1). cross. SERVICE-LAYER-PHASE-0-INVENTORY.html:80,82 cite DataManager_CacheContextTests and DataManager_AuthFallbackPolicyTests, both deleted; the plan's P7-A3 "test inventory comparison" was not produced; SERVICE-LAYER-IMPLEMENTATION.html:318 says only "rehomed". x.ratchet found 38 base test names gone at the tip, all rehomed or deliberately changed. Fix: check in a removed-to-rehomed mapping.

F98 (low, 1/1). P6. scripts/gateway-readiness-check.py:51,1098. --catalog-check scans RestService.cs only behind if RESTSERVICE_PATH.exists() with no else, so it now skips silently and prints gaps: 0; 64 endpoints.json rows point at the deleted file. Catalog is complete today. Fix: scan Infrastructure/Remote and Constants.cs; fail loudly on missing targets.

F99 (low, 1/1). P6-7. About 53 docs still name removed classes, e.g. docs/FTDT-DATA-MIGRATION-PLAN.md:149,153,154 ("bump ftdtClientVersion in RestService.cs"; the real switch is Version at FtdtApiClientAdapter.cs:19), GATEWAY-QA-READINESS-CHECK.md:18,41, tools/fault-injection/README.md:92. No historical banner. Fix: rewrite or banner.

F100 (nit). P7. 13 component specs still show DataManager (e.g. pilot-card-property-mapping.html:130 ctor sample). CLAUDE.md names them as the UI baseline. Fix: add a "DataManager retired; use IDocumentsQueries" note and stamp Updated.

F101 (low, 1/1). P5-7. [Obsolete] IAuthentication (IAuthentication.cs:55) has one non-test consumer (the DI alias at HeadServiceCollectionExtensions.cs:200); RunLogOutTaskAsync, ClearSessionCacheContextAsync and LegacyRestNetworkErrorClassifier.IsTransient are called only from tests; ~60 test files depend on the composite via FakeAuthentication. Doc says the composite stays "while generated-client bridges are retired in Phase 6"; P6 did not remove it. Fix: record a removal milestone.

F102 (low, 1/1). P6. iOS NSURLError codes duplicated: LegacyRestNetworkErrorClassifier.cs:32-36 (-1005, -1009, -1001, -1200), Authentication.cs:694 (same four), MauiProgram.cs:175 (three, no -1200; may differ deliberately). Authentication's copy pre-dates the stack. Fix: call IsTransientIosErrorCode from the first two.

F103 (nit; filed low). P2-3. JumpseatApiClient.cs:27,37. GetKcmAirportsAsync sends the member JWT (Bearer = Optional) to KcmLegacyGatewayUrl (gateway.alpa.org), an ALPA-owned host not switched by the QA/prod toggle. Base sent the same header via the shared client; the host choice is the documented AB#2717 workaround. Fix: BearerPolicy.None until AB#2717 is retired.

F104 (low, 1/1). P1, P6. SettingsPage.razor.cs:153-185. ToggleEndpointEnvironment sets DevEnv and calls ApplyEndpoints() (only cancels the change token), awaits two credential reads, then LogOutAsync. A request started in that window goes to the new host with the old environment's JWT. Diagnostic-Mode only; legacy had the same window; both hosts are ALPA-owned. Fix: revoke first.

F105 (nit; filed low). P3. MemberApiClient.cs:33-37,51-55. Newtonsoft conversion errors embed offending values and go to CaptureException and error.Message. MemberRaw.birthDt is string?, so the birth-date example in the filing does not apply; only typed PAC fields (amount, dates) can leak, and only if the backend breaks its contract. Pre-existing. Fix: value-free wrapper exception.

F106 (low, 1/1). P4. LogoutParticipants.cs:53-58, DocumentCacheService.cs:77,170. DocumentLogoutParticipant clears LiteDB rows only; PDFs written with File.WriteAllBytes to Path.Combine(CacheDirectory, document.FileName) remain after sign-out, and revoked documents are never purged. Legacy behaved the same. Next member sees a leftover file only for a FileID they are already entitled to. Fix: logout participant deleting downloaded files.

F108 (low; filed medium; votes low x3). P3. Plan line 333 ("protected-data auth.required never becomes a stale fallback") versus line 740 (auth fallback via a separate flag for "banners, Jumpseat policies, subscriptions and flight notifications"); the Phase 0 matrix (SERVICE-LAYER-PHASE-0-INVENTORY.html:111) omits subscriptions and airports, though SubscriptionsService.cs:17 and FlightSearchService.cs:19 set AllowAuthorizationFallback: true. Legacy did fall back; behaviour is preserved. Fix: add both to the matrix and reconcile the two plan lines.

F109 (low; filed medium; votes low x3). P5. The implementation doc reuses P5-A3 (FTDT guards) and P5-A4 (build counts) with different meanings than plan rows 1616-1640, omits P5-A5 and the account-locked walkthrough, and folds logout ordering into A2. The same row says "device login/logout/offline walkthrough remain pending" and the doc states "No device acceptance is claimed", so the device gap is disclosed. Fix: rewrite the table with plan IDs and Pass / Not run / Superseded.

F112 (low, 1/1). P1. SERVICE-LAYER-IMPLEMENTATION.html:28 says "Only GET requests retry automatically ... DELETE send once"; RequestProvider.cs:105-106 retries DELETE when RetryIdempotentDelete is set (MobileMenuApiClient.cs:25), and doc lines 123 and 278 mention read-POST and DELETE retries. Fix: append the exceptions.

F117 (low, 1/1). cross. origin/imp/blazor-hybrid was 12 commits ahead of dca3123d at 730daccc (24 files; overlap in DocumentCachePage.razor, JumpseatResultsPage.razor, JumpseatResultsPage.razor.cs; merge-tree clean). It is now at 4774e079, 24 commits ahead, and a merge into the tip conflicts in ALPAMobile/HeadServiceCollectionExtensions.cs and ALPAMobile/Services/Authentication.cs. All recorded test results predate those commits. Fix: sync, resolve at hunk level, rerun suite and both native builds at the merged SHA.


6. Refuted findings

ID Title Why refuted
F05 SetCache / SetJson capture Generation at write time Caller-side contract documented in ICacheStore.cs:9-13; every non-test writer at the tip wraps the write in TryExecute with a pre-fetch generation.
F08 Plan marks raw-DTO mapping seam as done Plan checkboxes are all unchecked; inventory documents the prose contract and defers other families to later phases; codecs exist at the tip.
F09 Document-list processed flag and 500ms poll removed Failure scenario unreachable: non-refresh reads return the memo or persisted cache; the old flag was never reset and was the latent bug.
F11 DataManager falls back to new MauiConnectivityService() No new DataManager exists; DI registers IConnectivityService; fallback never taken and deleted with DataManager in P7.
F17 Session identity keyed only by logout-advanced generation at P1 Every forced-expiry path runs logout (advancing generation) before another member can sign in; CachedRemoteQuery had no production consumer in P1; SignInContextReset added in P7.
F19 P1-A2 raw seam has no consumer and no LockedAccount/BadCredentials tests AuthRestClient consumes IRawRequestProvider at the tip and AuthRestClientTests covers both bodies; P1 has RawNoRetry_PreservesStatusAndBody.
F25 Scope creep in the P1 transport PR Stale-snapshot scenario cannot happen (both Settings paths publish); other items are planned P1 work.
F26 P2 MEC failure handling narrower than legacy HttpError/DeserializationFailed fallback and Sentry reporting restored in P3 (MecService.DefaultOptions, LegacyCatalogFailure); 401-no-fallback is the plan's documented rule (line 333, 740, 1823). Refuted 2 of 3.
F32 AppConfig and AppUpdate no longer send member bearer Endpoints are identity-independent app config; legacy sent no bearer pre-login; pinned by ConfigAndUpdate_ArePublicAndNeverAcquireABearer; plan line 796 lists app configuration as guest-safe. Refuted 2 of 3.
F39 401 on member/content/comms reads no longer falls back Deliberate and tested (GetMemberAsync_UnauthorizedRetainsProtectedTypedError, AdditionalAllowlistCannotOverrideAuthorizationGuard); plan lines 795-796, 1074, 1823.
F43 LoggedInChanged invalidation depends on lazy DataManager construction App still resolves CacheContentService, which injects IDocumentsQueries (registered as DataManager), so the subscription exists before any sign-in path.
F48 FlightSearch mapping re-implemented; legacy mappers are test oracle only Infrastructure cannot reference the head model; production uses ToCriteria(); parity test is a deliberate migration guard.
F49 RegisterPushTokenAsync still returns Task; layer crossing Plan's Result-plus-diagnostic contract is implemented at the port level; both crossings go through Application ports.
F53 Prefetch aborts on per-document OCE No OCE source reaches the loop: token path swallows exceptions and downloads convert timeouts to Result failures.
F54 Logout regression test for the document memo is vacuous DocumentLogoutParticipant is in the real logout path in that test; the assertion fails if the generation does not advance.
F56 P4-A5 only compile-verified; viewer failure dialog untested Doc discloses the device walkthrough as not run (P4-A5 row, P7-A4 open); awaited viewer behaviour is documented as deliberate.
F59 Logout no longer wipes SecureStorage (RemoveAll) SecureMsalCache.Register is never called and UseMSAL is false, so no MSAL key exists; base never removed the MSAL account either.
F63 FTDT push starts after a separate generation check Fixed in P6 (push started inside the same TryExecute); stuck flag sits on an outgoing in-memory instance.
F77 FTDT writes lost 3-attempt transient retry Documented single-send (implementation doc lines 254, 272), plan bans write retry without a server idempotency contract; local queue retry budget is retained. Refuted 2 of 3.
F78 Menu "count is zero" guard has no test Menu_EmptyWireResponseRemainsAFailureForDomainFallback covers [], null, "" at the adapter.
F107 Locked decision "NSwag clients stay generated" rewritten Recorded as plan Q6 "Phase 3 refinement" and in implementation doc lines 130-135; routes and bodies pinned by tests. Refuted 3 of 3.
F110 Dynamic-content pilot moved without both-simulator evidence Doc says the device walkthrough is pending and no simulator run is claimed; deadline and bearer behaviour are covered by transport tests. Refuted 2 of 3.
F111 Doc never records Pass/Fail/Not run per criterion Every device-gated row says "not run" or "remains pending"; P7-A4 and P7-A5 marked Open.
F113 Plan edits relax process items (single P3 PR, shared fake base) Documented deviation attributed to the implementation request; plan line 964 already permits shared fixtures.
F114 No CI or branch policy on any stack PR (filed high) Refuted 3 of 3: plan line 327 says to verify branch policy and use explicit review if none exists; PR 2505's body says auto-complete is disabled; CI runs on the final PR into imp/blazor-hybrid. The fact itself stands and is recorded in section 7.
F115 All PRs show cumulative diffs against a fixed target Plan line 966 mandates every phase PR opens into the integration branch; refuted 2 of 3.
F116 Merge order and strategy unconfigured Plan requires explicit completion when no policy exists; each PR body names the merge order; refuted 2 of 3.
F118 Local-only branches and untracked dirs (informational) Workspace housekeeping, not a defect; phase5-saved holds no unpushed commits; .agents/ and .codex/ do not show as untracked.

7. Build and test reproduction; branch and PR state audit

Reproduction at tip 6633bb59 (codex/service-layer-phase7)

Step Result
dotnet build UnitTest/UnitTest.csproj -c Debug --no-incremental Succeeded, 0 errors, 776 warnings.
dotnet test UnitTest/UnitTest.csproj -c Debug --no-build Failed 0, Passed 2607, Skipped 0, Total 2607. Matches the doc's 2,607.
dotnet test UnitTest/UnitTest.csproj -c Release Failed 0, Passed 2601, Skipped 0, Total 2601. Matches the doc's 2,601.
dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-ios -c Debug --no-incremental Succeeded, 0 errors, 675 warnings.
dotnet build ALPAMobile/ALPAMobile.csproj -f net10.0-android -c Debug --no-incremental Succeeded, 0 errors, 702 warnings (includes NU1903 for SQLitePCLRaw.lib.e_sqlite3.android 2.1.11, GHSA-2m69-gcr7-jv3q, unrelated to this refactor).

Branch and PR state


8. Coverage gaps

Not verifiable from a read-only static review, and needs a person or a device:

  1. Device walkthroughs are open per the implementation doc: P2-A4 (MEC live/offline and Search), P3-A4 (live wave and Search), P4-A5 (documents, favorites, native viewer), P5-A4 (login, forced expiry, logout, account switch, account-locked walkthrough), P6-A4 (native viewer), P7-A4 (both UI suites) and P7-A5 (final integration). Priority scenarios from this review:
  2. iOS and Android Release configurations were not built; only Debug native builds and net10.0 Debug/Release unit tests ran. Release-only trimming effects on ActivatorUtilities are unchecked.
  3. Whether NSUrlSessionHandler honours cancellation tokens and strips Authorization on cross-host redirects, and whether Sentry's HTTP handler applies to the named ServiceLayerHttpClient (F12, security lens).
  4. Backend contracts: whether getalpano can return unquoted text (F41), whether getmecairports can send "" numerics (F28), whether downloadfile and FTDT writes are server-idempotent (F50, plan wants an explicit contract).
  5. Files not read line by line: about 400 changed files were sampled; .razor/.cs Presentation pages, most UnitTest bodies (about 2,500), the FTDT mapper body (559-line move), MecStringJsonConverter, ContentWireModels and raw wire models. MSAL paths (UseMSAL=false) were read but not traced in depth.
  6. Findings that depend on timing (F62, F65, F64, F33) were established by code tracing and ordering reasoning, not by running the app.
  7. Phase-by-phase incremental fixes inside the stack were checked only for the files each finding named; x.security analysed the cumulative diff and tip files, so a bug introduced and fixed within the stack may not appear here.
  8. CI: pipelines 98, 99, 100 were never queued on any codex branch (read-only rules), so whether they pass at the tip is unknown.
  9. Base drift: imp/blazor-hybrid has moved to 4774e079 (includes PR 2493) since the stack was cut, so the final-integration merge conflicts described in F117 and section 3 are the first thing to resolve.