Work / Chitragupt / Wiki / Decisions
2026-07-09-mapper-rollup-contract
Decisioncanonicalverified 2026-07-09
DECISION.2026-07-09.MAPPER-ROLLUP-CONTRACTDecision
Fix the silent-orphan mapper contract between
triggers/firestore/ledger-mappers/*.ts and
packages/shared/src/rules/v1/income-summary.ts. Add a hard rule and
a contract test:
Every
entry_typea mapper emits must be either
- a key in
ENTRY_TYPE_TO_BUCKET(routed to a real tax bucket), or- on the
AUDIT_ONLY_ENTRY_TYPESwhitelist / an audit prefix.Anything else is a build-time failure.
The mapper-contract Jest test drives every registered mapper with a
fixture matching its schema, captures every emitted entry_type, and
fails if any classifies as unknown. This closes the failure mode
where a mapper's rename or a new form's field emits a value that
silently vanishes into unmapped_paise at review time.
Why
The 2026-07-09 fresh audit found the mapper→consumer contract broken in 10 places:
bank-interest-certemittedbank_interest_income+tds_bank_interest→ both silent. Bank-interest-cert income and TDS never reached the tax review.home-loan-interest-certemittedsection_24b_interest→ the bucket table only knewsection_24b_self_occupied/section_24b_let_out. §24(b) deduction was always zero.mf-casemitteddividend_income→ silent. MF dividends dropped.form-16aemittedtds_non_salary→ silent. Non-salary TDS (§194J, §194H, §194I) dropped.medical-insurance-premiumemittedsection_80d_preventive_checkup→ silent. ₹5k preventive sub-cap not credited.mf-casmf_stcg_realised/mf_ltcg_realised→ silent in the tax bucket table (they reached the CG rollup via a parallel path, but the tax review's own STCG / LTCG buckets missed them).crypto-statementtds_194s_vda→ silent. §194S crypto TDS dropped.professional-receipthad no mapper at all → freelance income contributed zero to every review.
The audit-only side had its own problem: unmapped_paise was polluted
by intentionally-audit entries (Form 12BA perquisites, TIS heads,
per-transaction bank-statement rows, salary-slip monthlies that would
double-count Form 16). That noise buried the true-orphan signal.
Impact
Ledger contract changes in packages/shared/src/rules/v1/income-summary.ts:
- Bucket table extended:
tds_non_salary,section_194ib_tds,mf_stcg_realised,mf_ltcg_realised,tds_194s_vda,professional_receipt_income. - New
AUDIT_ONLY_ENTRY_TYPESset +AUDIT_ONLY_PREFIXES(form_12ba_, form_12bb_, tis_) — enumerates entry_types that are documentation rows, not tax-affecting. summariseIncomenow excludes audit-only rows fromunmapped_paise, sounmapped_paise > 0means a true silent orphan in the contract.- New export
classifyEntryType(entryType)returns"bucket" | "audit_only" | "unknown"— used by the contract test.
Mapper edits (fixes to silent orphans):
bank-interest-cert→ routes throughinfo_code_savings_interestandinfo_code_tds_194a(same buckets AIS uses).home-loan-interest-cert→ emitssection_24b_self_occupiedas the V1 default. Comment notes a V2 property-type discriminator will route let-out interest.mf-cas→ emitsinfo_code_dividend_mffor dividends.form-16a→ drops thenon_salary_payment_grossemit entirely (would double-count vs 26AS/AIS). Keepstds_non_salaryas the single row.medical-insurance-premium→ routes preventive-checkup under the samesection_80d_self/section_80d_parentsentry_type as the base premium, sochapterVIABreakdowncaps the combined amount at the §80D limit.- New
professional-receiptmapper — emits base receipt amount asprofessional_receipt_income → other_income_paise. GST is subtracted when only the total is available.
New file: apps/functions/src/__tests__/mapper-contract.test.ts —
one test per registered mapper, 29 total, all green.
Status
Active.
Sources
- Fresh-perspective audit of the ledger emit-vs-consume matrix,
2026-07-09 (branch
production-readiness-audit-v2). - Related decisions: 2026-07-06-v1-launch-blockers (B5, B6, B7, B13 — earlier fixes that this decision generalises into a contract), 2026-07-09-dedup-content-hash-and-purge (Phase A of the same audit).
- Related concepts / entities: pillar-tax, pillar-expense, pillar-portfolio, upload-only.
Every project of mine is written down like this.
Read the résumé