1. Centralize Tab Identifiers
Parent status: [x] Done
Problem: Tab IDs ('metrics', 'breakdown', 'bills', 'loans', 'taxes', 'savings', 'other-income') are scattered as raw string literals across ~15 files. Renaming or adding a tab requires hunting through search modules, PlanDashboard routing, PlanTabs, and test fixtures.
Files affected:
Tasks:
Done definition:
2. Centralize Frequency Constants
Parent status: [x] Done
Problem: Pay frequency, bill frequency, savings frequency, and view mode strings are used as discriminators in 30+ switch/if-else statements across 12+ files. The types exist in src/types/frequencies.ts and src/types/viewMode.ts, but every function that branches on them uses raw string literals like 'bi-weekly' or 'monthly'.
Files affected:
- src/utils/payPeriod.ts — 5+ switch statements on frequency/view mode strings (
getPaychecksPerYear, getDisplayModeLabel, getDisplayModeOccurrencesPerYear, getPayFrequencyViewMode, formatPayFrequencyLabel)
- src/utils/payCalendar.ts — 5 separate case statements on pay frequency
- src/utils/frequency.ts —
PAY_FREQUENCY_OCCURRENCES, BILL_FREQUENCY_OCCURRENCES, SAVINGS_FREQUENCY_OCCURRENCES maps with hardcoded keys
- src/utils/billFrequency.ts —
convertBillToMonthly, formatBillFrequency switch on frequency strings
- Component files — UI option arrays for frequency dropdowns duplicate the same strings
Tasks:
Done definition:
3. Extract Field Error Types to Shared Location
Parent status: [x] Done
Problem: Every tab-view component defines its own *FieldErrors type locally. There are 7 independent definitions that all follow the same pattern ({ fieldName?: string }) but share no common base and can't be imported by tests or other modules.
Current definitions (all local to their component):
Tasks:
Done definition:
4. Centralize Reallocation Source Type Metadata
Parent status: [x] Done
Problem: Reallocation source types ('bill', 'deduction', 'custom-allocation', 'savings', 'investment', 'retirement') and their metadata (display labels, pause-only vs adjustable classification, section ordering) are defined independently in multiple files. Adding a new source type requires coordinated changes across 4+ files with no compiler guidance.
Files affected:
Tasks:
Done definition:
5. Centralize Loan and Retirement Type Definitions
Parent status: [x] Done
Problem: Loan types ('mortgage', 'auto', 'student', etc.) and retirement plan types are defined as UI dropdown option arrays inside component files rather than shared constants. The type union in src/types/obligations.ts separately lists the same strings.
Files affected:
Tasks:
Done definition:
6. Remove Component Import from BudgetContext
Parent status: [x] Done
Problem: BudgetContext.tsx imports ErrorDialog from src/components/_shared. This is a dependency-inversion violation — the data/state layer should not depend on presentation components. It makes BudgetContext harder to test in isolation and creates a potential circular dependency risk.
Tasks:
Done definition:
7. Consolidate Inline Types in PayBreakdown
Parent status: [x] Done
Problem: PayBreakdown.tsx defines 5+ large local types (AllocationCategory, AllocationAccount, AccountFunding, ValidationMessage, ReallocationUndoSnapshot, ReallocationSummaryMeta). AllocationCategory alone has 12 optional boolean/count flags suggesting it conflates multiple concerns.
Tasks:
Done definition:
8. Improve Logic Gate Robustness
Parent status: [x] Done
Problem: Many switch statements across the codebase use string literal cases with no default exhaustiveness check. If a new value is added to a union type, the compiler won't flag the missing case — it silently falls through.
Key locations:
Tasks:
Done definition:
1. Centralize Tab Identifiers
Parent status:
[x] DoneProblem: Tab IDs (
'metrics','breakdown','bills','loans','taxes','savings','other-income') are scattered as raw string literals across ~15 files. Renaming or adding a tab requires hunting through search modules, PlanDashboard routing, PlanTabs, and test fixtures.Files affected:
TabIdtype union defined here, but IDs are hardcoded stringsactiveTabrouting,openTabFromLink, keyboard handlerstabId: 'bills') with no shared constantTasks:
TAB_IDSconstant object insrc/constants/tabIds.ts(e.g.TAB_IDS.bills,TAB_IDS.metrics) and derive theTabIdtype from it.TAB_IDS.*references.TabIdtype is derived from the constant (type TabId = typeof TAB_IDS[keyof typeof TAB_IDS]) so adding a new tab is a single-location change.Done definition:
src/constants/tabIds.tsand test assertions.2. Centralize Frequency Constants
Parent status:
[x] DoneProblem: Pay frequency, bill frequency, savings frequency, and view mode strings are used as discriminators in 30+ switch/if-else statements across 12+ files. The types exist in
src/types/frequencies.tsandsrc/types/viewMode.ts, but every function that branches on them uses raw string literals like'bi-weekly'or'monthly'.Files affected:
getPaychecksPerYear,getDisplayModeLabel,getDisplayModeOccurrencesPerYear,getPayFrequencyViewMode,formatPayFrequencyLabel)PAY_FREQUENCY_OCCURRENCES,BILL_FREQUENCY_OCCURRENCES,SAVINGS_FREQUENCY_OCCURRENCESmaps with hardcoded keysconvertBillToMonthly,formatBillFrequencyswitch on frequency stringsTasks:
src/constants/frequencies.tsexporting constant objects for each frequency family (e.g.PAY_FREQUENCIES.biWeekly,BILL_FREQUENCIES.semiAnnual) and derive the union types from them.payPeriod.ts,payCalendar.ts,frequency.ts, andbillFrequency.tsto use the constants instead of raw strings.ViewModestrings — create aVIEW_MODESconstant object and derive the type.Done definition:
3. Extract Field Error Types to Shared Location
Parent status:
[x] DoneProblem: Every tab-view component defines its own
*FieldErrorstype locally. There are 7 independent definitions that all follow the same pattern ({ fieldName?: string }) but share no common base and can't be imported by tests or other modules.Current definitions (all local to their component):
PaySettingsFieldErrorsin PaySettingsModal.tsxBillFieldErrors,BenefitFieldErrorsin BillsManager.tsxLoanFieldErrorsin LoansManager.tsxSavingsFieldErrors,RetirementFieldErrorsin SavingsManager.tsxOtherIncomeFieldErrorsin OtherIncomeManager.tsxTasks:
src/types/fieldErrors.tsand move all*FieldErrorstypes there.Done definition:
src/types/fieldErrors.ts.*FieldErrorstype definition.4. Centralize Reallocation Source Type Metadata
Parent status:
[x] DoneProblem: Reallocation source types (
'bill','deduction','custom-allocation','savings','investment','retirement') and their metadata (display labels, pause-only vs adjustable classification, section ordering) are defined independently in multiple files. Adding a new source type requires coordinated changes across 4+ files with no compiler guidance.Files affected:
ReallocationProposalSourceTypeunion type, filtering logic iterates all 6 types as string literalsPAUSE_ONLY_TYPES,ADJUSTABLE_TYPES,SECTION_ORDERall hardcoded with the same stringsTasks:
src/constants/reallocationSourceTypes.tswith a single metadata array/object that defines each source type's ID, label,isPauseOnlyflag, and display order.ReallocationProposalSourceTypefrom the metadata so adding a type is a one-line change.PAUSE_ONLY_TYPES,ADJUSTABLE_TYPES, andSECTION_ORDERin ReallocationReviewModal to be computed from the shared metadata.reallocationPlanner.tsfiltering logic to iterate the metadata instead of hardcoding each type.Done definition:
5. Centralize Loan and Retirement Type Definitions
Parent status:
[x] DoneProblem: Loan types (
'mortgage','auto','student', etc.) and retirement plan types are defined as UI dropdown option arrays inside component files rather than shared constants. The type union insrc/types/obligations.tsseparately lists the same strings.Files affected:
Loan.typeis a string union of 6 valuesLOAN_TYPESarray andLOAN_PAYMENT_FREQUENCIESarray defined locallyRETIREMENT_PLAN_OPTIONSandgetRetirementPlanDisplayLabelLOAN_TYPE_LABELSrecord duplicates label data from LoansManagerTasks:
src/constants/loanTypes.ts— single-source loan type metadata (value + label); derive theLoan.typeunion from it.LOAN_TYPESandLOAN_PAYMENT_FREQUENCIESfrom LoansManager to shared constants.LOAN_TYPE_LABELSin HistorySnapshotCard — import from the shared constants instead.retirement.tsfor the same pattern and consolidate if needed.Done definition:
LOAN_TYPE_LABELSmap exists outside the shared constants file.6. Remove Component Import from BudgetContext
Parent status:
[x] DoneProblem: BudgetContext.tsx imports
ErrorDialogfromsrc/components/_shared. This is a dependency-inversion violation — the data/state layer should not depend on presentation components. It makes BudgetContext harder to test in isolation and creates a potential circular dependency risk.Tasks:
ErrorDialog.ErrorDialogrendering to a higher-level component (e.g. a provider wrapper or PlanDashboard) that subscribes to BudgetContext error state.Done definition:
src/contexts/BudgetContext.tsxhas zero imports fromsrc/components/.7. Consolidate Inline Types in PayBreakdown
Parent status:
[x] DoneProblem: PayBreakdown.tsx defines 5+ large local types (
AllocationCategory,AllocationAccount,AccountFunding,ValidationMessage,ReallocationUndoSnapshot,ReallocationSummaryMeta).AllocationCategoryalone has 12 optional boolean/count flags suggesting it conflates multiple concerns.Tasks:
AllocationCategory,AllocationAccount,AccountFunding) tosrc/types/.AllocationCategoryshould be decomposed or use a discriminated union instead of 12 optional flags.Done definition:
src/types/and importable by services or tests.AllocationCategoryis refactored to avoid the flag-per-concern pattern.8. Improve Logic Gate Robustness
Parent status:
[x] DoneProblem: Many switch statements across the codebase use string literal cases with no
defaultexhaustiveness check. If a new value is added to a union type, the compiler won't flag the missing case — it silently falls through.Key locations:
getPaychecksPerYear,getDisplayModeLabel, etc.)PayFrequencyconvertBillToMonthly,formatBillFrequencyTasks:
default: neverchecks (or use a helper likeassertNever()) to all switch statements that branch on union types.Record<PayFrequency, number>) so missing keys are caught by the type system at definition time rather than at each call site.Done definition:
assertNever()default.