Service layer decoupling — Phase 0 inventory

Baseline source: dca3123dd9c80b89586cb6de89afb387b01c23a1 (imp/blazor-hybrid after the plan documentation merged). Implementation destination: imp/blazor-hybrid-service-layer-decoupling. Plan and phase acceptance criteria.

Executable dependency ratchet

The machine-readable checklist is head-consumers.json. Every consumer/dependency pair has an owning phase and a disposition. The baseline retains migrated entries with migration evidence; deleting a caller without recording its disposition fails the ratchet.

HeadConsumerRatchetTests scans all five production assemblies, including method bodies, fields, constructor parameters, generic service-location calls, nested messages, async/lambda bodies and generated Razor properties. Compiler-generated types are attributed to their source owner. Its Roslyn source complement identifies each declaring type and resolves references across Debug/Release and net10/iOS/Android preprocessor branches, so a native-only dependency cannot hide behind the neutral test target. Comments, literal text and unused imports are not consumers. Unresolved target identifiers fail with their source location.

Each run attaches head-consumers.actual.json to the NUnit/TRX result with member or source-line evidence. PR validation publishes the TRX attachments. Compare that report with the checked-in checklist before changing a phase entry. At Phase 0, 54 active dependency pairs remain (27 compiled type pairs and 27 source file/type pairs); eight retired pairs record four classifier consumers migrated in this phase. The current IL must come from a fresh build: stale bin assemblies are not an acceptable baseline.

Owning phase Consumer migration
0 Remove concrete classifier dependencies from CacheContentService and the two Jumpseat ViewModels. Keep equivalent shared classification behavior.
1 Pilot DynamicContentApiClient only when transport preserves its serializer, route, timeout and error contracts. Start the options-backed endpoint compatibility adapter.
2 Move the nested MEC/LEC readiness message to Application with its existing scope; migrate MyPilotGroupPageViewModel.
3 Replace App's concrete update-check dependency and migrate content, notifications, KCM and FTDT consumers behind their existing ports.
4 Move Menu/Favorites transport dependencies and native document URL construction. SharedActionsService currently resolves IRestService in a method body.
5 Replace Authentication's broad REST dependency using the existing JWT port and the ordered session/logout contracts.
6 Move all remaining endpoint consumers, Settings service location and logging; retire REST implementation/registration. Preserve binary/XML exceptions below.
7 Remove facade aggregate/interface registrations only when every live alias/router consumer has migrated. Composition-root rows can span earlier domain migrations; phase 7 owns their final disappearance.

IRestEndpointProvider is a temporary Application compatibility port. Its existence is permitted until phase 6; the ratchet prevents new consumers from prolonging it. Referencing a target's implementation/interface is recorded as an entry; references from a target to itself are excluded. Test fakes and documentation are outside the production inventory.

Route and serializer dispositions

Caller Current contract to preserve Owning phase / disposition
DynamicContentApiClient GET /api/mobilecontent/dynamic/site/{siteKey}/page/{pageKey}, site and theme routes; AlpaWireJson.Options; 15-second linked deadline; page-ID repair; dynamiccontent.* error codes; reads the current endpoint on every request 1, conditional transport pilot with parity tests before switching
RequestProvider JSON transport; current System.Text.Json web options; retries/cancellation must be characterized by operation before the new decorator owns retry policy 1, transport foundation
RestService generated clients Existing generated wire contracts, base paths and mutation semantics; preserve non-success JWT response bodies such as locked/bad-credential responses 2–6 adapters; 6 deletes the aggregate implementation
PodcastFeedClient XML stream Retains IHttpClientFactory; explicit non-JSON exception
ImageCacheService Binary image bytes and cache policy Retains IHttpClientFactory; explicit binary exception
BannerImageConverter Android image stream and downsampling Separate follow-up after the image-cache port exposes the required stream/decode operation
AutoLoginWebViewContentView Cookie-authenticated PDF GET, PDF/content validation and native viewer navigation 6 migrates endpoint lookup; binary/cookie transport remains explicit
SentryEventProcessor Inspects exception/event data; does not send HTTP requests No transport migration
RestServiceLogEventSink Queue owns retries; a send is one attempt 6 migrates sink while preserving queue scheduling and recursion protection

Persisted JSON and raw mapping decisions

Phase 0 preserves every existing cache key and persisted schema. A future HTTP DTO is an input to a pure CreateFromRaw mapper. It is not automatically the object serialized into the existing cache. Infrastructure owns the transport-specific DTO/factory/codec; Application exposes the existing Domain-shaped contract. No mapper resolves services or reads options; the surrounding service supplies needed configuration.

The compatibility reader continues accepting today's persisted Domain-shaped JSON. A future codec must write that same shape (including names, casing, nulls and collections) and read legacy fixtures before any domain extraction can pass acceptance. Do not make a schema migration or purge users' caches as an incidental part of service movement. Byte-preservation applies to unchanged raw payload storage; equivalent object round-trips also require schema and Search parity, rather than assuming property ordering alone proves correctness.

Current cache keys / family Persisted shape and reader Extraction owner
GetMECEventsAsync, GetLECEventsAsync Newtonsoft ObservableCollection<MECLECEvent> JSON arrays; EventSearchExtractor reads the cached shape 2
GetMECRepsAsync, GetLECRepsAsync Newtonsoft ObservableCollection<Representative> arrays; RepresentativeSearchExtractor 2
GetMECHotelAirportsAsync Newtonsoft ObservableCollection<MECAirports> array; HotelSearchExtractor 2
GetMECCommitteesAsync Newtonsoft ObservableCollection<Committee> array; CommitteeSearchExtractor 2
Notification and flight-notification keys Newtonsoft ObservableCollection<NotificationCenterMessage> arrays; notification search readers 3
KCM, Jumpseat, airports, company, directory, resource/CTA and accident keys Existing Domain/legacy model arrays; current per-key registry extractor 3
GetMemberAsync, GetAppConfigSettingsAsync, GetPACMemberInfoAsync Existing Newtonsoft object shape; excluded from Search where the registry says so 3
GetPageBannersAsync, GetAllSubscriptionsForUserAsync Existing Newtonsoft arrays; registry exclusions unchanged 3
GetDocumentListAsync Newtonsoft List<ScopeItem> hierarchy, including groups/categories/documents; DocumentSearchExtractor and document hydration 4
GetMenuItems Newtonsoft ObservableCollection<MenuItem> tree and compatibility reader; MenuItemSearchExtractor 4
DynamicFeedSite:{siteKey} System.Text.Json with AlpaWireJson.Options, not the generic Newtonsoft codec; dynamic-feed search reader Existing specialized store; preserve during 0/1
ALPALogCache Existing durable logging queue payload 6; queue storage is not a cached remote query

LegacyCachePayloadTests holds six fixed synthetic legacy payloads, one per MEC/LEC partition. It verifies byte preservation through the real store, complete Newtonsoft model/schema round-trips (including nulls, computed properties and nested collections), Search identity, and security trimming. Six malformed-payload cases pin the current behavior: the opaque store retains the string, the legacy reader throws, Search removes the previous rows and emits none, and explicit Delete evicts the corrupt payload. This does not claim that every legacy query automatically evicts corrupt data; the future compatibility reader must adopt that behavior explicitly.

The four existing MEC/LEC SearchContentRegistry.Keys constants retain their original string values. Four MEC query methods address six separate cache partitions. Auth/JWT and push-registration registry placeholders are documentation-only and never persisted.

Search security trimming remains an independent per-key policy. The generation barrier rejects old-context publication for all coordinated member work; a reference-data Search classification is not permission to publish an old member's in-flight result into a new session.

Implementation and validation record

Implementation branch: codex/service-layer-phase0; PR target: imp/blazor-hybrid-service-layer-decoupling. This section is the historical Phase 0 safety baseline. Later phases extracted the domains and retired the aggregates; the test migration map identifies the current fixtures for the original evidence below.

Criterion Implemented evidence What it demonstrates
P0-A1 HeadConsumerRatchetTests, SourceConsumerScanner, head-consumers.json, LayerDependencyRulesTests Executable consumers and owning phases, including native conditional source, with no new unclassified consumer. Infrastructure remains independent of MAUI, Presentation and the head assembly.
P0-A2 CacheContextSafetyTests, DataManager_CacheContextTests, Authentication_LogoutCacheContextTests, SecureTokenStore_ClearFailureTests, CacheContextRegistrationTests Real isolated LiteDB/cache/file stores with controllable fetches: old work cannot restore API/Search/document rows, memos, readiness or downloaded files; document/site single-flight separates generations; current-context writes and caller-only cancellation still work.
P0-A3 LegacyCachePayloadTests, existing registry/security-trim tests, site and document corruption tests Six hardcoded MEC/LEC fixtures preserve key bytes, persisted schema and Search mapping. Documents/sites evict corrupt payloads; legacy MEC readers still throw on malformed payloads until an extracted codec deliberately changes that behavior.
P0-A4 MauiConnectivityServiceTests, TransientNetworkErrorClassifierTests, DataManager_AuthFallbackPolicyTests Constrained Internet and Debug-only force-offline policy; shared/REST/iOS/TLS classification; existing per-domain auth policy, cancellation, and concurrent MEC/LEC partitions.

Validation completed on 2026-09-19:

Check Result
Full Debug unit + architecture suite 2,080 passed; phase0-final-debug.trx
Phase 0 Release contract suite 122 passed; phase0-final-release.trx
iOS Debug simulator build (iossimulator-arm64, code signing disabled) Passed; compilation only
Android Debug build Passed; compilation only
Standalone DocsTests suite 4 passed; phase0-docs.trx
Navigation freshness, local HTML links/fragments, Markdown lint Passed

Validation commands and results are also recorded with this phase's PR. Every test run uses a separate TRX filename; the full suite includes the generated inventory attachment. Native builds check platform compilation. Device login/logout/offline smoke has not been executed by this change; no live feature acceptance is claimed.

Deliberate safety behavior changes

Preserved auth and classifier policies

Case Phase 0 behavior
MEC auth guard fails Throw the existing authentication-required signal; no stale protected fallback.
Ordinary notification auth guard fails Return null before fallback.
Banners, Jumpseat policies/airlines, subscriptions, airport reference data, flight notifications Preserve their existing per-domain cached fallback when auth fails; no global auth-policy rewrite in this phase.
Cancellation or changed session Reject the operation; never use stale fallback.
Shared exception classifier Remains in Application because Presentation already consumes that seam; no outward dependency is introduced.
Data-query classifier Infrastructure extends the common classifier with the existing TLS EOF case.
REST classifier Head adapter retains disposed-content and native iOS codes; bare TLS EOF is still not added to REST's retry policy.

Limits and later markers

The barrier can wait for synchronous indexing or document hydration that already owns it. Controlled tests prove ordering; they do not measure UI responsiveness on a device. Theme styling/SecureStorage theme publication and a late JWT refresh reinstalling session tokens remain Phase 5 session-lifecycle work. The durable log queue remains the explicit non-query-cache exception.

Phase 1 still owns transport/retry/options validation. Phase 2 remains the first extracted-feature acceptance marker. Integrated device and UAT validation must pass on the dedicated implementation branch before its final PR into imp/blazor-hybrid.