Status: Complete
Date: 2026-09-22
Parent plan: Dynamic Feed Orchestrator Refactor Plan §5 Phase 0
This is the inventory Phase 0 asks for: current route-to-page mappings, IFeedTemplateContentFactory
registrations, search/favorites item-to-ViewModel paths, and existing unit-test coverage — captured
before any contract changes so later phases can diff against a known baseline.
| Page | Route(s) | Page key source | CMS-driven? |
|---|---|---|---|
DynamicFeedOrchestratorPage |
/dynamic-feed-orchestrator (?page={key}) |
Query param, normalized, passed to IHomeFeedQueries.GetPageAsync directly — no LocalizedDynamicPageLoader |
Yes |
MecHomePage |
/mec |
"{englishSiteKey}-home" → LocalizedDynamicPageLoader.LoadAsync |
Yes |
MecCommitteesPage |
/mec/committees |
"{englishSiteKey}-committees" → LocalizedDynamicPageLoader.LoadAsync |
Yes |
MecCommitteeDetailPage |
/mec/committee/{CommCd}, /mec/committees/{CommCd} |
pageKey query override, else "{englishSiteKey}-committees-{Slugify(comm_name)}" → LocalizedDynamicPageLoader.LoadAsync. The "Committee Information" block is a direct IMECQueries lookup, not feed content — mixed page. |
Partially |
LecPage |
/mec/{Mec}/lec/{LecId}, /mec/{Mec}/lec/{LecId}/{Page} |
"{mecSiteKey}-lec-{lecId}[-{page}]" → LocalizedDynamicPageLoader.LoadAsync |
Yes |
LecRosterPage |
/mec/{Mec}/lec/{LecId}/roster |
Not feed content — live IMECQueries calls. Borrows the LEC home page's CMS title for display only. |
No |
AlpaIHomePage |
/alpa |
Signed-in: const "alpa-home" via IHomeFeedQueries. Signed-out: const "global-home-loggedout" via IDynamicFeedQueries. No LocalizedDynamicPageLoader. |
Yes |
HydrationOrchestratorPage (dev/preview) |
/HydrationOrchestratorPage |
"{englishSiteKey}-home" / "{englishSiteKey}-fr-home" pair via IScaffoldContentQueries, side-by-side EN/FR preview |
Dev tool only |
DocumentsHub (embedded component, not routed) |
n/a | DocumentsPageKey property → IHomeFeedQueries.GetPageAsync |
Yes |
No other @page-routed file references PageDto/GetPageAsync/IDynamicFeedQueries/IHomeFeedQueries —
confirmed by repo-wide search (KCM, Jumpseat, Documents list, Advocacy, Notifications, Member Resources,
etc. are all non-CMS or bespoke-query pages).
Loader split: DynamicFeedOrchestratorPage and AlpaIHomePage call IHomeFeedQueries/
IDynamicFeedQueries + DynamicPageOrchestrator directly. MecHomePage, MecCommitteesPage,
MecCommitteeDetailPage, and LecPage all go through LocalizedDynamicPageLoader. This is the
existing split Phase 1's IDynamicPageLoader extraction needs to unify — the first two are already
effectively using what Phase 1 wants to formalize as the "generic" path.
IFeedTemplateContentFactory registrations| Factory | ContentType |
Registered in DI? |
|---|---|---|
LecLinkFactory |
"LecLink" |
Yes |
PilotCardFactory |
"PilotCard" |
Yes |
CommitteeListFactory |
"Committee" |
Yes |
CommitteeMembersFactory |
"ContactItem" |
Yes |
DocumentHeroFactory |
"DocumentItem" |
Yes |
PodcastEpisodeFactory |
"PodcastEpisode" |
Yes |
MecHotelDirectoryFactory |
"MecHotelDirectory" |
Yes |
DynamicListFactory |
"DynamicList" |
No — blocked on IItemsQueries having no production implementation. Only implementation repo-wide is a test double (FakeItemsQueries in DynamicListFactoryTests). This is the only unregistered factory; matches the plan's §8 risk row unchanged. |
Registrations live solely in ALPAMobile/HeadServiceCollectionExtensions.cs — no other
*ServiceCollectionExtensions.cs registers an IFeedTemplateContentFactory.
Search does not uniformly reuse the feed pipeline:
SearchViewModel.SearchAsync maps each raw Coveo result through CoveoSearchResultFactory.Create,
which builds a SearchResultRowViewModel directly from raw fields (title/excerpt/url/contact/date)
— its own mapping, independent of ItemComponentFactory.FeedItemJson),
CoveoSearchResultFactory deserializes it back into an ItemDto and calls the same
ItemComponentFactory the live feed uses, populating SearchResultRowViewModel.FeedComponentVm.SearchPage.razor renders <ComponentView Vm="@result.FeedComponentVm" /> only for that carve-out;
every other result type renders bespoke components (FlightCard, contact rows, etc.).This is a dual path, not a single shared one — Phase 7 ("Unify search rendering") has real work to do; it isn't starting from zero, but the "reuse" today is a narrow, JSON-round-trip special case, not the general pipeline.
Favorites has its own independent hydration/rendering pipeline, entirely separate from
ItemComponentFactory/ComponentView/ItemDto:
FavoritesViewModel.HydrateAsync switches on favorite.ItemTypeId and calls a dedicated card
factory or hand-built ViewModel per type: Document → DocumentHeroFactory.Create(document),
MenuItem → MenuItemCardFactory, DocumentSet → CategoryCardFactory, Link →
LinkCardFactory, Contact → hand-built ContactCardViewModel (walks IMECQueries and matches
alpa_id from scratch), Hotel → hand-built MecHotelRowViewModel (walks IMECQueries and
matches Hotel.StableId(), duplicating Hotel.IsInformationalOnly() logic that
MecHotelDirectoryFactory also computes for the live feed).FavoritesPage.razor dispatches on item.Kind/item.Card type and renders concrete components
directly (CardSmall, ContactCard, etc.) — zero references to <ComponentView>.Favorites reuses the same card-factory classes the feed pipeline uses internally
(DocumentHeroFactory, MenuItemCardFactory, CategoryCardFactory, LinkCardFactory) but calls
them directly against domain models, not through ItemDto. Contact and Hotel re-implement
matching/mapping logic from scratch rather than reusing CommitteeMembersFactory/
MecHotelDirectoryFactory. Phase 8 should prioritize Contact and Hotel — they're the only two
with duplicated logic rather than just a different entry point into an already-shared factory.
| Test file | Cases | Covers |
|---|---|---|
DynamicPageOrchestratorTests |
47 | Orchestrator resolve/progressive-resolve behavior |
ItemComponentFactoryTests |
67 | Component dispatch across discriminators, including DocumentItem |
FavoritesViewModelTests |
34 | Favorites hydration across all ItemTypeId branches |
CommitteeListFactoryTests |
22 | "Committee" resolver |
DynamicFeedRenderHelpersTests |
12 | Progressive-loading apply/cancel/stuck-container behavior (§2.1 of parent plan) |
PilotCardFactoryTests |
10 | "PilotCard" resolver |
CommitteeMembersFactoryTests |
7 | "ContactItem" resolver |
DynamicListFactoryTests |
6 | "DynamicList" resolver (against FakeItemsQueries; not exercised in production since it's unregistered) |
Architecture/RazorComponentResolutionTests |
— | Architecture-level check that every registered RCL tag resolves (lives under UnitTest/Architecture/, not the flat UnitTest/ root the parent plan's file list implies) |
Gap found: no dedicated test file exists for PodcastEpisodeFactory ("PodcastEpisode"). It has
zero references anywhere under UnitTest/. Every other registered factory has either a dedicated test
file or indirect coverage through ItemComponentFactoryTests/DynamicPageOrchestratorTests. Recommend
adding PodcastEpisodeFactoryTests before Phase 6 touches shared item-resolution behavior, since
there's currently no regression signal for that resolver at all.
Fixture coverage: there's no separate fixture-file directory (no fixtures/*.json anywhere in the
repo) — "fixtures" today means inline ItemDto/TemplateDto construction inside each test. Coverage
by content type: home (MecHomePageRefreshTests, AlpaIHomePageRefreshTests), committee
(CommitteeListFactoryTests), document (ItemComponentFactoryTests, DynamicPageOrchestratorTests),
contact (CommitteeMembersFactoryTests), link (LecLinkFactoryTests). All five content types the
Phase 0 checklist names already have representative inline fixtures; no new fixture files are needed
to satisfy that checklist item, only the PodcastEpisodeFactoryTests gap above.
dotnet test UnitTest/UnitTest.csproj --logger "trx;LogFileName=phase0-baseline.trx" run against this
baseline before any contract changes:
Passed! - Failed: 0, Passed: 2167, Skipped: 0, Total: 2167, Duration: 1m 35s — UnitTest.dll (net10.0)
Results: UnitTest/TestResults/phase0-baseline.trx. Clean baseline — 0 failures, 0 skips — to diff
future phase runs against.
PodcastEpisodeFactoryTests), not blocking since it's pre-existing
and unrelated to this plan's contract changes.Phase 0 exit criteria met. Ready to start Phase 1 (extract the common page loader).