Work / Chitragupt / Wiki / Decisions
2026-07-04-temporal-anchor-per-category
Decisioncanonicalverified 2026-07-04
DECISION.2026-07-04.TEMPORAL-ANCHOR-PER-CATEGORYTemporal anchor per vault category (not AY-everywhere)
Decision
Every confirmed document must carry a category-shaped temporal anchor — not an assessment year. Each VAULT_CATEGORIES value maps to one required TemporalKind (ay | span | as_of | event | validity | none), enforced at the confirmDocument callable and at the onDocumentConfirmed fan-out. Finance-routed categories with no printed AY derive one from period_end (or event_date) via deriveAyFromDate — Indian FY = Apr 1 → Mar 31, AY = FY start year + 1.
Why
Documents flipping to confirmed with ay: null silently broke the pillar recompute: expense_reviews/{ay}, portfolio_reviews/{ay}, tax_reviews/{ay} never got written, and the Tax / Expense / Portfolio pages showed empty state for users who had confirmed multiple uploads. The regex parser only reads explicit "Assessment Year YYYY-YY" strings, so bank statements, broker CG, MF CAS — which print periods, not AYs — hit the if (ay) guard at on-document-confirmed.ts and were dropped from the fan-out.
Fixing this as "AY is required everywhere" would break the categories the vault already supports but hasn't wired UI for yet — health documents carry event dates, identity documents carry validity windows, insurance policies span multiple AYs. AY is the wrong universal anchor.
Impact
packages/shared/src/schemas/document.ts— newTemporalKind,TemporalSchema,REQUIRED_TEMPORALmap,FINANCE_ROUTED_CATEGORIESset,hasRequiredTemporal()predicate,MISSING_TEMPORAL_MESSAGEcopy map.DocumentRecordSchemagains atemporalfield.packages/parser/src/ay.ts— newderiveAyFromDate(iso).AYSourceSchemagains"derived-from-period"and"llm".apps/functions/src/parser/run-and-persist.ts—composeTemporal()builds the temporal object from parser + LLM + field extractors; derives AY for finance-routed docs that came in null.apps/functions/src/parser/llm-fallback.ts— system prompt gets worked FY→AY examples and per-category temporal instructions; tool schema acceptsevent_date_iso,valid_from_iso,valid_until_iso,as_of_date_iso.shouldRunLlmFallbackescalates when a finance-routed doc has neither AY nor a period boundary.apps/functions/src/triggers/firestore/on-document-confirmed.ts— resolves target AY viatemporal.ay → derived_ay → deriveAyFromDate(period_end | event_date); routes byFINANCE_ROUTED_CATEGORIESinstead of the silentif (ay)guard; loudlogger.errorwhen a finance doc slips through with no AY.apps/functions/src/inbox/documents.ts—confirmDocumentthrowsfailed-preconditionwith the category-specific message fromMISSING_TEMPORAL_MESSAGEwhenhasRequiredTemporal(category, temporal)is false.apps/functions/src/inbox/backfill-temporal-recompute.ts— self-service one-shot to project temporal onto pre-existing confirmed docs, derive AY where possible, and re-runrecomputeAfterConfirmfor each distinct AY touched.apps/website/src/components/inbox/DocConfirmView.tsx— disables Confirm and surfaces the plain-English missing-anchor banner; reuses the AY picker when the missing anchor isay.- Affects pillar-inbox, inbox-document-types, pillar-tax, pillar-expense, pillar-portfolio.
- No mobile app impact — mobile is paused for V1 per 2026-06-27-mobile-end-user-only.
Status
Active.
Sources
- .context/wiki/decisions/2026-05-31 hybrid-parsing-haiku-fallback (LLM fallback surface this decision extends)
packages/shared/src/schemas/document.ts—REQUIRED_TEMPORALis the source of truth for the category → anchor map
Every project of mine is written down like this.
Read the résumé