Codebase Map
Layout: explore packages under
android-app/app/src/main/java/com/zharif/autobudget/ directly; the
module-level map and layer diagram live in
overview.md. This page holds only what the
code can't tell you at a glance, plus extension recipes.
Orientation notes:
parser/and the money/export codecs incommon/are pure Kotlin — no Android imports; JVM-only unit tests depend on it.- Seeds:
data/seed/DefaultCategories.kt(10 default categories) andparser/banks/BankRuleSets.kt(62 production rules plus 12 debug-shell rules keyed oncom.android.shell; debug ones never ship in release). - Generated — do not edit by hand:
ui/components/MaterialIconCatalog.ktlists all 2083 Material "filled" icon names; regenerate withandroid-app/scripts/generate_icon_catalog.sh. - Exported schema JSON under
android-app/app/schemas/is the source of truth for migration tests.
Universal conventions live in AGENTS.md; behavior-level decisions trace to ADRs (index in overview.md).
Extension recipes
Add a parse rule for an existing bank
- Add a
ParserRuletoBankRuleSetsfor that package. Regex must use named groups(?<amount>...), optional(?<merchant>...)/(?<account>...). Direction:DEBIT/CREDIT/AUTO; status:AUTO/explicit. - Add golden fixtures:
app/src/test/resources/fixtures/<pkg>.json(or extend an existing file) with the expectedamountMillis, merchant, direction, status, confidence. - Run
./gradlew testDebugUnitTest—ParseGoldenTestmust pass and keep ≥90% HIGH confidence; update the starter-rule counts in parser.md (intro + package table). - Bump
RuleRepository.RULE_SEED_VERSION(1 → 2 → …). Existing installs reconcile once per version: new starter rules are inserted (keyed on the stableParserRule.id→rules.sourceId), legacy rows get theirsourceIdbackfilled by matchinglabel, and user-edited or deleted rules are never touched or re-added (ADR-0019).
Improve ruleset matching from captured notifications
Capture dumps arrive as base64+gzip JSON:
{version, notifications:[{sourcePackage, rawText, capturedAt, isMasked}]}.
- Decode (
base64 -d | gunzip); group samples bysourcePackage. - Simulate that package's current rules against each rawText BEFORE writing
any regex; note which miss and why. Match order: embedded JSON wins only
when both
merchantNameandtransactionAmountare non-blank — otherwise regex rules run. - Build patterns from the shared tokens in
BankRuleSets($anyCurrencyAmount,$merchantName,$accountRef). Known traps: masked accounts carry 2–6 asterisks (***5660) while$accountRefallows exactly 4; exclude'/’from the merchant class when the boundary is's; captures pass throughcutAtStopWord, so check trailing qualifiers trim cleanly. - Re-simulate: every failing rawText must hit and zero existing fixture
bodies may match. Verify detector verdicts per sample too — direction/
status flags (
received,refund,pending, …) scan the whole text before the rule's DEBIT/CREDIT/AUTO kind applies. - Finish with Add a parse rule for an existing bank:
golden fixtures from the real rawTexts,
RULE_SEED_VERSIONbump,ParseGoldenTest≥90% HIGH gate, parser.md counts.
Add a bank package
- Add
PACKAGE_*constant + rules inBankRuleSets. - Add the app to
ui/onboarding/BankCatalog.kt(KNOWN_BANK_APPS) with a display label. - Add
<package android:name="…"/>to the<queries>block inAndroidManifest.xmlso package detection works on Android 11+. - Fixtures + tests as above.
Add a currency
- Add one entry to
Currency.SPECS(code, minor digits, symbol) incommon/Currency.kt. - If the parser should detect its symbol/token in text, add to
Currency.TOKENS(loose, forAmountParser) andCurrency.DETECT(word-boundary-safe, for whole-text detection). supportedCodes()feeds the Settings currency picker automatically.- Add a
MoneyFormatterTest/AmountParserTestcase if the minor digits or symbol differ from the norm (e.g. JPY 0 digits).
Add a category keyword
- Edit
KeywordClassifier.DEFAULT_KEYWORDS(or a user's stored overrides). Note stored values replace defaults per category — they don't merge. - Existing installs: the stored
category_keywordsJSON wins; editing in Settings and saving persists the new list.
Change the category set / icons
- Defaults live in
data/seed/DefaultCategories.kt; icons are strings resolved byui/components/CategoryIcon.kt(categoryIconVector). Legacy emoji seeds are mapped too. CategoryRepository.ensureSeededonly seeds when the table is empty — existing installs keep their categories.
Category icons come from the full Material "filled" catalog (ADR-0023):
ui/components/MaterialIconCatalog.ktis generated — do not edit by hand. It lists all 2083 icon names and resolves"Filled.X"→ vector via chunkedwhens.- Regenerate after bumping
compose-material-icons-extended:android-app/scripts/generate_icon_catalog.sh [path-to-sources.jar](defaults to the newest sources jar in the Gradle cache). - New categories store the canonical
ImageVector.name(e.g."Filled.Pets"). Legacy short ids ("fastfood") and emoji still resolve via the legacywhenfallback incategoryIconVector. - The category editor's picker is
ui/components/IconPicker.kt(search + lazy grid in a bottom sheet). Search logicfilterIconNamesis a pure, JVM-testable function.
DB migration procedure
- Change entities/DAOs in
data/db/. - Bump
@Database(version = N+1)inAppDatabase. - Write
MIGRATION_N_N+1and register inaddMigrations(...)(DatabaseModule). Prefer SQL (ALTER TABLE …) overRoom.databaseBuilder(...).fallbackToDestructiveMigration()— data loss. - Inspect the exported schema JSON in
app/schemas/after build — it's the source of truth for migration tests. - Never change a released migration retroactively.
Add a screen
- Add a route constant to
Routesinui/navigation/AutoBudgetNavHost.kt. - Add
composable(route) { … }innavGraph(...)with args (navArgument+NavType). - Create the screen + ViewModel (
@HiltViewModel, inject repositories, expose oneStateFlow<UiState>viacombine+flatMapLatest+stateIn). - Reuse
ui/components/*(MonthSelector, EmptyState, TransactionRow…). - If it's a tab, add a
TabItemtoTABS.
Add a setting
- Add a
KEY_*constant toSettingsRepository. - Read via
settings.observe(KEY)(flow) orsettings.get(KEY)(one-shot); write viasettings.put(KEY, value). - The value lives in the encrypted
settingstable (Room) — no SharedPreferences for new settings.
Where decisions live
Every behavioral rule above traces to an ADR in docs/adr/; the index table
is in docs/architecture/overview.md.