Navigation must be rooted in aggregates/masters, not flattened into one menu item per backend entity. A child entity, a configuration sub-object, or a pipeline stage is reached by drilling into its parent (a detail tab/drawer), never by a separate top-level menu item.
Why: the user flagged "Signatory Master" as a separate Banking menu item — but a signatory is a child of a bank account. A CA thinks "I'm working on this bank account," not "now I go to the Signatory module and re-find the account." One-menu-item-per-table produces menu sprawl, the wrong mental model, and data drift (the two surfaces had diverged onto different account ids/banks — same concept, two divergent models). Pairs with [[feedback_detail_views_in_drawer]] and [[project_mtp_nav_depth]] (rail → group → page → in-page tab).
How to apply:
- Top-level menu = workspaces a user navigates to: masters (Bank Accounts, Chart of Accounts), registers, reports, and action-workflows (Cheque Issuance, Fund Transfer — verbs, not children).
- Children / config sub-objects / pipeline stages → a detail tab or drawer on the parent.
- One data model per concept; don't let a child get a second divergent fixture/page.
- Precedent already in the code:
routes/pages.tsmapsfiscalYear/periodClose/periodDashboard→ onePeriodDashboardpage.
Pinned as an ADR: mtp-web/docs/adr/0035-aggregate-rooted-navigation.md (the principle +
the 6 dependency-ordered consolidations + decision rules). Grounded in platform ADR-0011 (Finance
= 10 modules, one posting kernel, reads are projections). Principle is app-wide (HR/Students/
Exam follow it).
Progress (2026-05-30, mtp-web):
- Signatory Master → Bank Accounts › Signatories (
b09973a). - ITC Ledger + e-Way Bill → GST tabs (
614352d) — both are facets of theGstReturnaggregate. - Statement Import + Match → Bank Reconciliation tabs (
cfee1ed). - Fiscal Year + Period Close + Period Dashboard → one "Periods" (
fe4891e). - Deferred (need review-while-merging, not unattended): Outstanding → Aging (
?tab=param collision + two heavy virtualized pages); Cost Projects + Budget → Cost Centres (ThreePane hub + resolve the duplicate Setup "Budgets"); Purchase Invoice → Purchase Register (two mature pages, combined Purchases workspace). See ADR-0035 §Progress.
Mechanics that worked: fold X into Y as a ?tab= panel when Y already drives off
useSearchParams (GST, Bank Rec did); remove X from nav.ts + routes/pages.ts; keep X's data
layer/mock/key (now consumed by Y); reconcile Y's own derived fields (counts, audit) so the merged
surface has one data model.