Dynamic Feed Orchestrator Refactor Plan

Status: Phases 0–6 complete; PR #2640 merged into imp/blazor-hybrid 2026-09-29. Phase 7 withdrawn (2026-09-24) — handed to Jose Castro's overlapping AB#2877/2878/2879 work. Phase 8 not started and likely superseded by the same work — check with Jose before starting it. Phase 9 (obsolete-code/doc cleanup) in progress, worktree AB2855-DynFeedCleanup01 / branch chore/AB2855-dyn-feed-cleanup-01. Phase 10 not started.

Date: 2026-09-15

Scope: Dynamic feed loading, page hosting, template/content adapters, RCL/component packaging, CMS authoring, search rendering, and favorites rendering

Related: Dynamic Content Admin and Mobile Architecture expands section 4.6–4.10 into the full CMS/mobile boundary.

Update (2026-09-24) — pivot to a single consolidated integration branch: the original §7.1 plan (one PR per phase, stacked against imp/blazor-hybrid) was superseded before any of those PRs beyond baseline landed. Instead, imp/blazor-hybrid picked up an unrelated large change first — PR #2543, the Service Layer Decoupling integration, which obsoleted the monolithic IAuthentication interface in favor of ISessionState/IAuthTokenProvider/ICredentialStore/IAuthSession — so every already-built phase branch (all of which predate that PR) needed reconciling against current imp/blazor-hybrid before any of them could land cleanly. Rather than doing that reconciliation once per phase PR, a new worktree/branch AB2845-DynFeedIntegration01 / chore/AB2845-dyn-feed-integration-01 was created from latest imp/blazor-hybrid, and every phase branch (0 through 6) was merged into it in sequence, resolving conflicts once each — see §7.1 for the merge log and conflict details. That integration branch is the current source of truth; it will go to imp/blazor-hybrid as one PR rather than nine. Phase 7 (search rendering) was originally merged in too, but was withdrawn and dropped from the integration branch the same day — see Phase 7's own section for why. Phases 8–10 are otherwise unaffected by this pivot and remain to be built the normal way once the consolidated PR lands.

Update (2026-09-21): Progressive per-container loading landed on feat/ProgressiveContainerLoading (merged here) ahead of this plan's phases — see the new §2.1 and the callouts in §3.1, §4.1, Phase 1, and Phase 2 below. IItemsQueries/DynamicListFactory registration is still blocked as originally documented (§8 risk row unchanged). Phase 5's ContentResolutionContext.User should sequence after the auth-port split described in Service Layer Decoupling Plan §9 — that document currently references a DynamicContentHost.razor component that does not exist at this branch's tip; treat that specific claim as stale until re-verified, but the sequencing recommendation (resolvers should adopt ISessionState once that plan introduces it, rather than the plan's current UserInfo?) still applies. Resolved 2026-09-24 — PR #2543 merged into imp/blazor-hybrid, landing the ISessionState/IAuthTokenProvider split; the integration branch above builds on it directly.

1. Purpose

Restructure dynamic content consumption so that JSON, search results, and favorites can use the same presentation pipeline and reusable Razor component library (RCL) components.

The refactor has two related goals:

  1. Reduce feature-specific adapters that combine data access, business policy, and presentation decisions.
  2. Reduce feature-specific Razor pages that only load and display CMS-driven dynamic content.

The intended result is one shared dynamic page host and one shared item-to-RCL pipeline, with localization and feature-specific behavior expressed as optional policies.

The refactor must also establish a package contract for the web-based CMS. The CMS should author against versioned component capabilities and payload schemas, while the mobile app remains the runtime that executes adapters, resolves data, and renders RCL components.

2. Current State

The current dynamic feed path is:

PageDto
  -> DynamicPageOrchestrator
  -> PageFactory / ContainerFactory
  -> ItemComponentFactory
  -> ComponentView
  -> RCL component

Template adapters are dispatched through IFeedTemplateContentFactory. They return ItemDto objects, but many adapters also own feature-specific behavior such as:

The current page layer duplicates the dynamic loading shell across components such as:

LocalizedDynamicPageLoader already centralizes page selection and language behavior, but it also performs the common page loading and orchestration work that should be reusable by non-localized pages.

Search and favorites have partial reuse of RCL components, but their data paths still contain source-specific hydration and rendering decisions. A logical item can therefore take different paths depending on whether it came from a feed, search, or favorites.

The current payload documentation already identifies Admin-only fields that are not represented consistently in the renderer, including token overrides, roster tokens, view-all tokens, and icon styling tokens. These fields need to become part of an explicit authoring contract rather than remaining undocumented additions to the JSON bag.

2.1 Progressive container loading (landed ahead of this plan)

Branch note (2026-09-22): this machinery lives on feat/ProgressiveContainerLoading, which is not yet merged into imp/blazor-hybrid — confirmed via git merge-base --is-ancestor origin/feat/ProgressiveContainerLoading origin/imp/blazor-hybrid. Any phase whose code references ContainerResolution, ResolveProgressivelyAsync, or DynamicFeedRenderHelpers must branch from feat/ProgressiveContainerLoading (or a descendant), not from imp/blazor-hybrid directly, until that branch merges — see the Phase 1 PR (§7.1) for the concrete example of this tripping up a branch-from-imp/blazor-hybrid worktree.

Before Phase 1 began, LocalizedDynamicPageLoader and DynamicPageOrchestrator gained a second, progressive loading path alongside the original all-at-once LoadAsync/ResolveAsync:

This is currently wired into DynamicFeedOrchestratorPage, AlpaIHomePage, MecHomePage, MecCommitteesPage, MecCommitteeDetailPage, and LecPage directly against LocalizedDynamicPageLoader/DynamicPageOrchestrator. It predates and is not yet expressed by the non-localized IDynamicPageLoader this plan proposes (§4.1) or the shared page host (Phase 2) — both need to carry a progressive variant, not just the plan's original synchronous DynamicPageLoadResult, or this behavior regresses when those pages migrate.

3. Target Architecture

Route or caller DynamicPageRequest Localization required? No Yes LocalizedDynamicPageLoader optional page-selection policy DynamicPageLoader PageDto / published dynamic JSON DynamicPageOrchestrator ItemResolutionPipeline dispatch resolved item contentType resolver adapters ContainerFactory / ItemComponentFactory ComponentView Reusable RCL component

The default path is DynamicPageLoader. LocalizedDynamicPageLoader is an optional page-selection policy that selects the appropriate page before delegating to the common loader. Both paths converge on the same orchestrator, resolver pipeline, ViewModel mapping, and RCL components.

Route or caller
  -> DynamicPageRequest
  -> DynamicPageLoader
       ^
       |
  LocalizedDynamicPageLoader
  (optional page-selection policy)
  -> DynamicPageOrchestrator
  -> ItemResolutionPipeline
  -> ItemComponentFactory
  -> ComponentView
  -> reusable RCL component

3.1 Page loading responsibilities

DynamicPageLoader owns the common workflow:

LocalizedDynamicPageLoader owns only localization policy:

Localization must not be required by ordinary dynamic pages.

3.2 Page hosting responsibilities

A shared dynamic page host owns:

Feature pages should provide route and policy inputs only. They should not duplicate the page rendering shell.

3.3 Content resolution responsibilities

Feature content resolvers own:

They must not own Razor markup, page shells, or decisions that belong to ComponentView.

The shared item pipeline owns:

4. Design Decisions

4.1 Add a non-localized page loader

Introduce a neutral result type:

public sealed record DynamicPageLoadResult(
    bool PageFound,
    string RequestedPageKey,
    string SelectedPageId,
    IReadOnlyList<ComponentViewModel> Components,
    string? Title,
    string? TitleToken);

Add a common loader contract:

public interface IDynamicPageLoader
{
    Task<DynamicPageLoadResult> LoadAsync(
        string pageKey,
        bool forceRefresh = false,
        CancellationToken cancellationToken = default);
}

DynamicPageLoader should be the default path. LocalizedDynamicPageLoader should use the same result type and delegate final page loading to the common implementation.

IDynamicPageLoader must also expose a progressive counterpart mirroring ILocalizedDynamicPageLoader.LoadProgressivelyAsync (§2.1) — e.g. a LoadProgressivelyAsync returning a DynamicPageLoadResult-shaped ShellComponents/ContainerTasks pair — since that path already ships on several pages this plan intends to migrate onto the common loader. Treating it as a later add-on risks a regression where migrated pages lose per-container skeletons and SourceUnavailable handling that non-migrated pages keep.

4.2 Preserve stable routes through route policy

Keep stable routes such as /mec/committees, but resolve them to dynamic page keys through a route policy rather than a dedicated page component.

Example requests:

/mec
  -> {mec}-home
  -> non-localized unless the route requires otherwise

/mec/committees
  -> {mec}-committees
  -> localized

/dynamic-feed-orchestrator?page={ual-media}
  -> ual-media
  -> non-localized

Introduce a request model or resolver capable of carrying page-level policy such as:

4.3 Keep ItemDto initially as the intermediate contract

Do not replace ItemDto with a new presentation model in the first migration. The existing ItemComponentFactory and ComponentView pipeline already provides the desired RCL boundary.

First make all sources produce consistent ItemDto values. Introduce a separate ResolvedContentItem only if ItemDto cannot represent the common contract without feature leakage.

4.4 Widen the resolver context

The current factory contract receives only TemplateDto. This causes the orchestrator to copy outer-item metadata back onto factory-created items.

Evolve the contract toward a context object:

public sealed record ContentResolutionContext(
    ItemDto SourceItem,
    TemplateDto Template,
    UserInfo? User,
    string? PageId,
    string? ContainerType);

Use the existing IFeedTemplateContentFactory name initially unless the broader contract makes IContentResolver clearer during implementation.

4.5 Prefer composition for item resolution

Use inheritance only for page-level policies. Use composition for template resolution, mapping, and shared stamping.

The preferred dependency direction is:

LocalizedDynamicPageLoader
  -> DynamicPageLoader
       -> DynamicPageOrchestrator
            -> content resolvers

Avoid separate orchestrator subclasses that duplicate visibility, token, favorite, or component dispatch behavior.

4.6 Package authoring metadata separately from runtime code

The CMS needs a versioned component catalog package, not a copy of the mobile application's adapters or compiled RCL assemblies.

The package should contain:

The package must not contain mobile runtime implementation code such as:

The boundary is:

CMS component package
  -> validates and authors JSON
  -> publishes versioned PageDto content

Mobile runtime
  -> loads PageDto content
  -> runs adapters/resolvers
  -> maps to ViewModels
  -> renders RCL components

RCL components, ViewModels, adapters, and styles remain part of the mobile runtime package. The CMS package describes the public contract those runtime assets implement.

The admin tool does need its own authoring-time data adapter layer. This is separate from the mobile runtime adapters. It lets the CMS preview and configure dynamic content such as documents, categories, or other non-member-specific records by calling approved external APIs.

An authoring-time adapter should provide:

The admin adapter must run through the admin backend or a controlled gateway. Browser code should not receive mobile service credentials or call arbitrary URLs supplied by an administrator.

The first authoring-time adapters should target non-member-specific sources, for example:

The admin tool must not attempt to resolve member-context data such as:

Those remain mobile runtime capabilities. The CMS can configure their resolver definitions and preview a neutral placeholder or explicitly supplied test context, but it must never require a real member account to author or preview a page.

The two adapter layers should share schemas and resolver keys where possible, while keeping implementations separate:

CMS authoring adapter
  -> approved admin API/gateway
  -> normalized preview data
  -> CMS component canvas

Mobile runtime resolver
  -> mobile/application port
  -> member-aware data and visibility policy
  -> ItemDto / ViewModel
  -> RCL component

The component catalog should mark each resolver capability as one of:

MemberContext capabilities must not be exposed as executable authoring adapters in the CMS.

4.7 Establish a component registry

Create a registry as the single source of truth for the supported authoring surface. Each entry should identify:

ComponentDefinition
  key
  packageVersion
  wireType
  renderType
  category
  payloadSchema
  editableProperties
  allowedChildren
  requiredCapabilities
  dataCapabilities
  themeTokens
  assets
  favoriteSupport
  deprecation

The registry should be consumable in two forms:

  1. A build-time or checked-in catalog used by the mobile renderer and architecture tests.
  2. A published schema/catalog artifact consumed by the web CMS.

The registry must not be inferred by scanning Razor filenames. Razor tags can fail silently, and file names do not describe JSON compatibility, required services, or authoring constraints.

4.8 Separate visual components from data resolvers

The catalog should distinguish between:

For example, Committee should not be presented to the CMS as if it were a visual component. The CMS should author a generic list or contact/card composition with a resolver configuration that the mobile runtime knows how to execute. This keeps committee-specific data policy out of the RCL contract and allows the same visual composition to be used by search and featured lists.

4.9 Package styles as tokens and assets

Styles should be packaged in layers:

  1. Stable component CSS: shipped with the RCL and owned by the app/runtime.
  2. Theme token catalog: names, types, allowed values, fallback values, and accessibility constraints exposed to the CMS.
  3. Site theme values: CMS-authored values resolved by the mobile theme provider.
  4. Media assets: CMS-managed image/icon references with validation and cache metadata.

The CMS should select token names and approved values rather than author arbitrary CSS. This keeps the app's layout and interaction behavior stable while still allowing site customization.

Every token used by a payload should be resolvable by the runtime. Unknown tokens should produce a diagnostic and a documented fallback, not a broken page. Token catalogs need their own version and compatibility checks.

4.10 Version the package contract

Use independent versions for:

Published content should record the catalog version it was authored against. The mobile runtime should advertise the catalog versions and capabilities it supports. Publishing should be blocked when content requires an unsupported component, property, token, resolver, or interaction.

Compatibility rules should define:

5. Implementation Phases

Phase 0: Baseline and inventory

Full inventory: Phase 0 Baseline.

Exit criteria: The current behavior and migration targets are documented, and baseline tests pass. Met.

Phase 1: Extract the common page loader

Tests (UnitTest/DynamicPageLoaderTests.cs):

Exit criteria: Existing generic and localized routes behave as before, with one shared result type and no duplicated orchestration logic. Met — dotnet build ALPAMobile.sln clean, dotnet test 2175/2175 passed (2167 baseline + 8 new), 2026-09-22.

Phase 2: Create the shared dynamic page host

Exit criteria: At least one existing dynamic page uses the shared host without visual or navigation regression. Met — MecHomePage migrated onto DynamicContentHost; dotnet build clean, dotnet test 2175/2175 passed, 2026-09-22. Not yet validated live on-device (see repo testing policy — UI validation is still owed before this is fully trusted for a visible route).

Scope note: DynamicFeedOrchestratorPage's FixedContent slot (hotel-directory filter bar) isn't supported by the host yet — that page stays on its own markup until a later phase widens the host or the hotel-directory case gets its own solution. Only MecHomePage was migrated in this phase.

Phase 3: Migrate MecCommitteesPage

Exit criteria: /mec/committees is served by the generic host and no longer requires a feature-specific Razor page. Code-complete — dotnet build clean, dotnet test 2175/2175 passed, 2026-09-22. Not yet device-validated (narrow/wide layout comparison, platform UI suites).

New reusable piece: LocalizedDynamicRoutePage (Pages/DynamicFeed/) — generalizes the MecHomePage/MecCommitteesPage load-orchestration shape (localized progressive load, pull-to-refresh, hydration-language reactivity, document-catalog-ready reconciliation) behind a PageKeySuffix parameter, so any future stable {mec}-{suffix} route (Phase 4 candidates) can become a one-line shim instead of duplicating this logic again.

Phase 4: Migrate remaining dynamic page shells

Migrate one page at a time:

Retain dedicated pages only when they contain behavior that cannot be expressed by page metadata, route policy, or reusable RCL components, such as maps, document viewers, complex forms, or interactive search state.

Exit criteria: CMS-driven pages use one host; dedicated pages have an explicit reason to remain. Met — every candidate page from Phase 0 was assessed. MecHomePage and LecPage use DynamicContentHost; AlpaIHomePage and LecPage stay dedicated pages with concrete, source-confirmed reasons (not just "too hard to check"); LecRosterPage was never in scope. dotnet build clean, dotnet test 2175/2175 passed for the LecPage migration, 2026-09-22. Not device/UI-suite validated (see repo testing policy) — this is a stated limitation of this environment, not a skipped step.

Phase 5: Introduce the shared resolver context

Sequencing: the Service Layer Decoupling Plan is introducing narrower auth ports (ISessionState, IAuthTokenProvider, ICredentialStore, IAuthSession) in its own Phase 5 and recommends this plan's resolvers consume ISessionState once that port exists, rather than the UserInfo? field below. Resolved (2026-09-23): that plan's own risk table explicitly anticipates this plan building ContentResolutionContext.User on IAuthentication before its Phase 5 lands ("Dynamic-feed plan builds ContentResolutionContext.User on IAuthentication before Phase 5" — mitigation: "Sequence rule in §9"). That is the expected order, not a race condition to avoid — build on IAuthentication/ UserInfo? now; the ISessionState swap is a planned fast-follow once PR #2543 (the integration PR carrying that port into imp/blazor-hybrid) merges — it was still Active, blocked on an unrelated logout content-paint defect, as of 2026-09-23.

Exit criteria: Factories receive the information they need directly and no longer depend on implicit orchestrator post-processing. Mostly met — the context now exists and reaches every factory; the "no longer depend on implicit post-processing" half is intentionally deferred (see the unchecked item above) since forcing it now would remove working behavior for no benefit. dotnet build clean, dotnet test 2178/2178 passed (2175 baseline + 3 new), 2026-09-23. Not device-validated.

Phase 6: Extract shared item resolution behavior

Candidate resolvers include:

Exit criteria: Feature resolvers fetch and map content, while the shared pipeline controls presentation metadata and component dispatch. Partially met — the two behaviors that were actually duplicated (render-type stamping, favorite-flag default) are now shared; action/route resolution and loading/empty-result behavior were already centralized elsewhere and needed no further extraction. dotnet build clean, dotnet test 2178/2178 passed (identical count to Phase 5 — a pure, behavior-preserving refactor, confirmed by zero test changes needed), 2026-09-23. Not device-validated.

Phase 7: Unify search rendering — removed from this plan's scope, 2026-09-24

Withdrawn — handed to Jose Castro's AB#2877/2878/2879 stack. While this phase's documents/links work was in flight, Jose independently started the same alignment goal — search results (and, per AB#2879, favorites) rendering with the feed's own look — via a different mechanism: DynamicPageOrchestrator.ResolveTemplateFieldsInPlace(item, template, userInfo) applied at CoveoSearchResultFactory.BuildFeedComponentVm, shared at the orchestrator level, plus a reworked ItemComponentFactory image/glyph-policy pipeline (WithDefaultImagePolicy/WithDefaultGlyphPolicy) and a shared FeedItemContent.ShownText helper. His stack (PRs 2557→2558→2559→2560→2565, all Active against imp/blazor-hybrid as of 2026-09-24) touches the exact same files this phase's documents/links work touched (CoveoSearchResultFactory.cs, ItemComponentFactory.cs, DynamicPageOrchestrator.cs). Rather than land two competing implementations of the same goal, this phase is withdrawn: PR #2550 (documents pilot) was abandoned, the links sub-migration (never PR'd) is left uncommitted in its own worktree, and the documents/links merge commits were dropped from the consolidated integration branch (chore/AB2845-dyn-feed-integration-01), which now ends at Phase 6. The findings below (design notes, the DocumentItem.ParsedPublishDate bug, per-content-type investigation) are kept for reference — some of it may still be useful if Jose's approach doesn't end up covering documents/links, but it is not active work.

Original Phase 7 content (historical — withdrawn, see above)

Scoping note (2026-09-23): this phase is architecturally larger than Phases 1–6 — ALPAMobile.Infrastructure (where every search extractor lives) is not allowed to reference ALPAMobile.Presentation (where DocumentHeroFactory/ItemComponentFactory live), a rule the codebase already documents and works around per content type (see DocumentSearchExtractor.OpenRoute's route-literal duplication, and MenuItemSearchExtractor's tap-time DeepLinkResolver resolution in docs/detail/SEARCH-CONTENT-ONBOARDING.md). Piloted one result type (documents) first rather than migrating all four at once, given no simulator/live-search access in this environment to catch a subtle rendering mismatch.

  • [~] Migrate contacts, documents, committees, and links incrementally. (documents and links done — PRs pending; contacts and committees investigated and deliberately deferred — see below)

Per-type findings (2026-09-23): investigated all four before writing code for any of them. Only documents and links turned out to be safely migratable without a larger design change:

  • Documents — done. See design note below.
  • Links (ResourceLinkSearchExtractor) — done. CoveoSearchResultFactory builds a CardSmallViewModel from Title + NotificationContentRouter.NormalizeHttpUrl(ClickUri), the exact same two-field mapping MemberResourcesPage.razor.cs already builds inline for this identical raw AppLink data — so a resource link's search result renders as the same card shape as its Member Resources row. Deliberately does not reuse LinkCardFactory: that factory resolves hrefs through DeepLinkResolver/FavoriteKey.NormalizeLinkRawId, built for internal ALPA route/favorite conventions, whereas a Resource Link's AppLink.URL is a plain author-entered external URL — confirmed by the extractor's own remarks and by SearchPage.OpenResultAsync already normalizing this exact data with NormalizeHttpUrl, not DeepLinkResolver. No new raw-JSON field needed: Title/Route were already carried by SearchIndexEntry, gated on the existing ContentTypeName/Source discriminator ("Resource Link") rather than adding a new field for what's already a stable, tested string.
  • Committees (CommitteeSearchExtractor) and contacts (Representative/Company Contact extractors) — deferred, not migrated. Both don't render through ItemComponentFactory/ComponentView at all today; they deliberately open a search-only "contact card modal" (SearchIndexEntry.OpensContactModal) because their real MEC destination page was retired under AB#2500. The extractor's own comment says this is interim, pending "the MEC surface coming back as dynamic-feed-hydrated RCL components" — separate, larger work outside this refactor's scope. Migrating either now would mean either building a second, throwaway RCL-based rendering path ahead of that larger effort, or reverse-engineering interim UI parity with the modal — neither is a safe, proportionate change here.

Design note — why documents don't use FeedItemJson: the existing FeedItemJson/BuildFeedComponentVm mechanism (§2.1-adjacent, AB#2561) only works for content that's already ItemDto-shaped in the dynamic-feed cache. Documents aren't dynamic-feed content — DocumentSearchExtractor reads a DocumentItem from the documents API cache, not a SiteDto. Building an ItemDto for it in DocumentSearchExtractor (Infrastructure) would mean duplicating DocumentHeroFactory's (Presentation) mapping logic in a second location with no compiler-enforced way to keep them in sync — a materially larger duplication than the one-line OpenRoute literal the codebase already tolerates. Instead: DocumentSearchExtractor serializes the raw DocumentItem into a new SearchIndexEntry.DocumentJson field (threaded through RawCoveoSearchResultItem.DocumentJson), and CoveoSearchResultFactory (Presentation) deserializes it and calls DocumentHeroFactory.Create(DocumentItem) directly — the same pure, already-shared mapper Favorites uses. Same shape as the MenuItemSearchExtractor/DeepLinkResolver precedent already in the codebase (defer the Presentation-dependent step to Presentation, carry only raw data through Infrastructure), extended from a route string to a full card.

Bug found and fixed during the documents work: DocumentItem.ParsedPublishDate is a computed property that throws FormatException on the default empty PublishDate string. JsonSerializer.Serialize evaluates every public getter, so serializing a DocumentItem with no publish date (a real, confirmed-live condition) crashed the whole document's indexing. Fixed with a fail-soft TrySerializeDocument wrapper in DocumentSearchExtractor — a row that can't serialize keeps its existing Title/Metadata/Route fields (search still works) and just skips the reconstructed-card enhancement, same "skip the enhancement, never the row" rule used everywhere else in this pipeline. This is a pre-existing latent bug in DocumentItem that this phase's new serialization path exposed, not a bug in the new code itself — caught by the existing DocumentSearchExtractor test suite failing after this phase's change, not by manual review. This bug fix is worth porting to Jose's approach independently of the rest of this phase, since it's a real latent crash unrelated to which rendering approach wins.

Exit criteria: Search results reuse the same RCL component and visual contract as feed results wherever the content type is shared. Partially met — documents and links now render through real, already-shared mappers instead of bespoke SearchResultRowViewModel fields; committees and contacts remain on the interim OpensContactModal shape, deliberately not touched, pending the separate MEC-surface-retirement work. dotnet build clean, dotnet test 2187/2187 passed (2183 baseline + 4 new for links), 2026-09-23. Device-validated for both: deployed to a running Android emulator for documents and again for links (adb-confirmed process running each time, screenshots captured — including the HomeSkeleton loading state rendering correctly for the links build). Neither specific search-rendering path was exercised signed-in with a live query — that requires real ALPA credentials not available in this environment; clearing app data to force a fresh search index lost an already-authenticated test session that couldn't be reproduced.

Phase 8: Unify favorites rendering — likely superseded, needs a check-in before starting

Not started, and probably shouldn't be started as written. Jose's AB#2879 ("Favorites: open offline and keep the look of the card that was hearted," PR #2560, Active) already does a large part of what this phase describes — new IFavoriteLookStore/FavoriteLookStore, and rework of FavoriteToggleContext/FavoriteLooks so a favorited card keeps the same look it had when hearted. Talk to Jose before picking this phase up: either his work already covers it and this phase should be struck, or there's a narrower remaining slice worth defining once his stack lands.

Phase 8: Unify favorites rendering

Exit criteria: Favorites reuse the same ViewModels and RCL components as other content sources without losing favorite identity or navigation behavior.

Phase 9: Remove obsolete code and document the architecture

Exit criteria: No feature-specific page or adapter remains without a documented responsibility. Met — obsolete-code audit, doc updates, and the ComponentView dispatch-test gap are all closed.

Phase 10: Package the CMS authoring contract

The first package should cover the existing dynamic-feed surface, including:

Exit criteria: The admin tool can validate and preview a page using the published package, and the mobile runtime can reject or safely fall back from content that requires unsupported package capabilities.

6. Testing Strategy

Unit tests

Add or update tests for:

Relevant existing tests include:

UI validation

Run UI tests after:

Validate both supported layout widths for structural changes. Do not claim device validation unless the app was deployed and the rendered behavior was inspected on the device.

7. Validation Gates

After each phase:

dotnet build ALPAMobile.sln
dotnet test UnitTest/UnitTest.csproj --logger "trx;LogFileName=test.trx"

For route or UI migrations, also run the applicable platform UI suite:

./run-ui-tests.sh ios
./run-ui-tests.sh android

Read the generated test result before re-running a failed suite so the failing test identity is preserved.

7.1 PR sequencing — superseded 2026-09-24, see the status banner above

The plan below (one PR per phase, each stacked on the previous phase's branch) was the original intent and is preserved here for the historical record of how each phase was actually built and individually gated. It stopped being how the work reaches imp/blazor-hybrid once PR #2543 (Service Layer Decoupling) landed there first: reconciling nine already-built, pre-#2543 phase branches one PR at a time would have meant resolving the same IAuthentication fallout repeatedly. Instead every phase branch below was merged, once each, into a new consolidated worktree/branch AB2845-DynFeedIntegration01 / chore/AB2845-dyn-feed-integration-01, created from latest imp/blazor-hybrid (already carrying #2543). That branch is the current source of truth and will go to imp/blazor-hybrid as one PR.

Merge log (chore/AB2845-dyn-feed-integration-01, 2026-09-24, in order — each step built, tested full-suite, and committed before the next):

# Branch merged Conflict? Resolution
1 docs/AB2846-dyn-feed-baseline-01 (Phase 0) Clean —
2 feat/AB2847-dyn-feed-loader-01 (Phase 1) Real DynamicFeedOrchestratorPage had diverged two ways since this branch was cut: imp/blazor-hybrid grew a load-coalescing gate and the IAuthentication→ISessionState/IAuthTokenProvider split, while this branch introduced IDynamicPageLoader. Kept the loader abstraction (this phase's actual goal) and preserved both newer behaviors — git's 3-way merge resolved the .cs body correctly on its own; only the .razor @inject lines needed a manual union.
3 feat/AB2848-dyn-feed-host-01 (Phase 2) Doc only Plan-doc add/add conflict — took the branch's completed Phase 2 section over HEAD's stale unchecked one.
4 feat/AB2849-dyn-feed-mec-committees-01 (Phase 3) Real LocalizedDynamicRoutePage needed both the coalescing-gate fields (still used by the body) and the branch's new generalization parameters; dropped a FavoriteToggleContext injection made redundant by DynamicContentHost now owning it. Also caught a stale IAuthentication reference that had slipped through with zero conflict markers in a cleanly-added new file (MecCommitteesPage.razor.cs had been renamed away; MecCommitteesPage.razor resolved by taking the branch's thin-shim version entirely).
5 feat/AB2850-dyn-feed-lec-01 (Phase 4) Real LecPage.razor — same IAuthentication→ISessionState/IAuthTokenProvider split.
6 feat/AB2851-dyn-feed-resolver-context-01 (Phase 5) Clean —
7 feat/AB2852-dyn-feed-resolver-shared-01 (Phase 6) Real CommitteeMembersFactory.cs — combined this phase's own shared render-type-resolution refactor (its actual purpose) with a newer DisplayName field imp/blazor-hybrid had separately picked up from an unrelated contact-name-formatting fix.

The recurring pattern: PR #2543 obsoleted the monolithic IAuthentication interface mid-refactor, splitting it into ISessionState/IAuthTokenProvider/ICredentialStore/IAuthSession. Every phase branch predating that PR still targeted the old API in any file it touched — surfacing either as a direct conflict (rows 2, 4, 5 above) or, more subtly, as silent fallout in a whole new file that auto-merged cleanly with nothing to flag it (row 4's MecCommitteesPage.razor.cs/ LocalizedDynamicRoutePage.razor case). Every merge step above was followed by grep IAuthentication across its changed files regardless of whether git reported a conflict, specifically because of that second failure mode.

Test results (through Phase 6): dotnet build ALPAMobile.sln clean (0 errors) after every step. Full suite at the Phase 6 checkpoint: 3116/3116 passing, up from 3113 at the Phase 0 baseline. Two timing-sensitive tests — WatchForStuckContainers_GivesUpAStillLoadingContainer and Subscribe_UsesCallerProvidedNowOnEachTick — each flaked once under the full suite's parallel execution load but passed reliably in isolation; confirmed non-regressions, not real bugs (and per Phase 7's withdrawal note, Jose has since opened PR #2880 fixing the give-up-timer test's flakiness directly).

Phase 7 withdrawn, 2026-09-24: rows 8–9 originally merged feat/AB2853-dyn-feed-search-documents-01 and feat/AB2853-dyn-feed-search-links-01 (both real IAuthentication-split conflicts, same pattern as above) on top of row 7, taking the full suite to 3125/3125. Both merge commits were then dropped — git reset --hard back to row 7's commit, since the branch had never been pushed — once it became clear Jose Castro's AB#2877/2878/2879 stack (PRs 2557–2565, Active against imp/blazor-hybrid) covers the same search/favorites-alignment goal via a different, incompatible mechanism. PR #2550 (the documents pilot's own PR) was abandoned for the same reason. See Phase 7's section above for the full explanation and what's still salvageable (the DocumentItem.ParsedPublishDate bug fix).

Staying current with imp/blazor-hybrid, 2026-09-24–25: with Phase 7 dropped, the integration branch is kept up to date by periodically merging origin/imp/blazor-hybrid back in — the same "pull latest, resolve, build, test" step, just in the opposite direction from the phase merges above. Three such merges so far, all clean (no conflicts):

Downstream dependents worth tracking, not yet acted on:

Work items: Feature AB#2845 "Dynamic Feed Orchestrator Refactor" (standalone — not parented to Epic AB#2081, which is already Resolved), with one child Task per phase below.

Original per-phase PR plan (historical — not how the work actually landed; see merge log above)

Each phase's exit criteria (§5) doubles as its PR boundary: land one phase, run the §7 gate, merge, then branch the next phase from an up-to-date imp/blazor-hybrid. This mirrors the precedent already set by the sibling Service Layer Decoupling Plan, whose branch policy states "documentation can land independently; refactor implementation stays isolated until thoroughly tested" and which ships one branch per phase (codex/service-layer-phase0 … phase7).

Two phases are coarser than a good PR and should split further — migrate one page or one resolver per PR, not the whole phase at once, so a regression bisects to a single component:

Phase PR scope Worktree / branch Depends on
0 Baseline inventory (docs/tests only, no runtime change) AB2846-DynFeedBaseline01 / docs/AB2846-dyn-feed-baseline-01 none — PR #2530 open against imp/blazor-hybrid
1 IDynamicPageLoader + DynamicPageLoadResult (sync and progressive), LocalizedDynamicPageLoader delegation AB2847-DynFeedLoader01 / feat/AB2847-dyn-feed-loader-01 Phase 0 for context; compiles only against feat/ProgressiveContainerLoading (§2.1) — PR #2531 open against that branch, not imp/blazor-hybrid; retarget once it merges
2 Shared dynamic page host AB2848-DynFeedHost01 / feat/AB2848-dyn-feed-host-01 Phase 1 — PR #2532 open against feat/AB2847-dyn-feed-loader-01
3 Migrate MecCommitteesPage to the generic host AB2849-DynFeedMecCommittees01 / feat/AB2849-dyn-feed-mec-committees-01 Phase 2
4 One PR per page — see split above Done. MecHomePage: no code change, Phase 2 already covered it. LecPage: PR #2535. AlpaIHomePage/LecRosterPage: assessed and retained/excluded, doc-only, folded into PR #2535. Phase 3
5 ContentResolutionContext AB2851-DynFeedResolverContext01 / feat/AB2851-dyn-feed-resolver-context-01 Phase 4 (built on IAuthentication, not blocked on the ISessionState port — see §2's Phase 5 sequencing note)
6 Shared item-resolution extraction, then one PR per resolver — see split above AB2852-DynFeedResolverShared01, AB2852-DynFeedResolverCommittee01, … Phase 5
7 One PR per search result type — see split above AB2853-DynFeedSearchContacts01, AB2853-DynFeedSearchDocuments01, … Phase 6
8 Unify favorites rendering AB2854-DynFeedFavorites01 / feat/AB2854-dyn-feed-favorites-01 Phase 7
9 Remove obsolete code, update docs AB2855-DynFeedCleanup01 / chore/AB2855-dyn-feed-cleanup-01 Phases 1–8 merged
10 CMS component catalog package (mobile-side capability advertisement; see Dynamic Content Admin and Mobile Architecture) AB2856-DynFeedCatalog01 / feat/AB2856-dyn-feed-catalog-01 Phases 1–9 substantially complete — the companion doc's runtime pipeline assumes IDynamicPageLoader already exists

Current state (2026-09-29): phases 0 through 6, built on chore/AB2845-dyn-feed-integration-01 (see merge log above), landed in imp/blazor-hybrid via PR #2640, merged 2026-09-29. Phase 7 is withdrawn (handed to Jose — see Phase 7's own section). Phase 8 is on hold pending a check-in with Jose. AB#2840/2885 (Eric's MultiCommitteeRoster/contact-list sorting work) is downstream and paused on this plan, not yet started. Phase 9 (obsolete-code removal, doc updates, component-resolution test coverage) is now underway, branched from imp/blazor-hybrid directly per this note's own prior guidance — worktree AB2855-DynFeedCleanup01 / branch chore/AB2855-dyn-feed-cleanup-01. Phase 10 has not started.

8. Risks and Mitigations

Risk Mitigation
Generic loader loses localized fallback behavior Keep localization in a delegating loader and preserve dedicated tests before migration.
Stable routes break during page removal Add route-resolution tests and migrate one route at a time.
Feature-specific favorite identity is lost Treat identity as explicit resolver output and test it independently of visual component type.
Search/favorites expose different fields than feed items Use an incremental common contract and retain source-specific payload data until all consumers migrate.
RCL component dispatch silently fails Maintain RazorComponentResolutionTests and add coverage for every new ViewModel mapping.
A base orchestrator becomes a large inheritance hierarchy Use composition for item resolution and inheritance only for page policy.
DynamicList remains unavailable Track IItemsQueries implementation and registration as a separate prerequisite.
CMS authors content the mobile runtime cannot render Publish a catalog version with runtime capabilities and reject unsupported content before publication.
CMS adapter leaks member-specific data or credentials Run authoring adapters behind an admin backend, classify capabilities by execution owner, and prohibit real member context in preview.
CMS preview uses a different response shape than mobile Share normalized schemas and fixtures while keeping CMS and mobile adapter implementations separate.
Admin styling bypasses the design system Expose approved theme tokens and assets, not arbitrary CSS or implementation selectors.
Preview differs from mobile rendering Use shared payload schemas, fixtures, and compatibility tests for CMS preview and mobile runtime.
UI regressions are hidden by successful builds Validate migrated routes with UI tests and compare narrow and wide layouts.

9. Completion Criteria

The refactor is complete when: