Skip to content
Ritesh FirodiyaGet in touch

Work / Chitragupt / Wiki / Decisions

2026-07-04-temporal-anchor-per-category

Decisioncanonicalverified 2026-07-04

DECISION.2026-07-04.TEMPORAL-ANCHOR-PER-CATEGORY

Temporal 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 — new TemporalKind, TemporalSchema, REQUIRED_TEMPORAL map, FINANCE_ROUTED_CATEGORIES set, hasRequiredTemporal() predicate, MISSING_TEMPORAL_MESSAGE copy map. DocumentRecordSchema gains a temporal field.
  • packages/parser/src/ay.ts — new deriveAyFromDate(iso). AYSourceSchema gains "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 accepts event_date_iso, valid_from_iso, valid_until_iso, as_of_date_iso. shouldRunLlmFallback escalates when a finance-routed doc has neither AY nor a period boundary.
  • apps/functions/src/triggers/firestore/on-document-confirmed.ts — resolves target AY via temporal.ay → derived_ay → deriveAyFromDate(period_end | event_date); routes by FINANCE_ROUTED_CATEGORIES instead of the silent if (ay) guard; loud logger.error when a finance doc slips through with no AY.
  • apps/functions/src/inbox/documents.ts — confirmDocument throws failed-precondition with the category-specific message from MISSING_TEMPORAL_MESSAGE when hasRequiredTemporal(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-run recomputeAfterConfirm for 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 is ay.
  • 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_TEMPORAL is the source of truth for the category → anchor map

Every project of mine is written down like this.

Read the résumé