Work / Chitragupt / Wiki / Decisions
2026-07-09-dedup-content-hash-and-purge
Decisioncanonicalverified 2026-07-09
DECISION.2026-07-09.DEDUP-CONTENT-HASH-AND-PURGEDecision
Rebuild the document-dedup foundation on two invariants:
- Tier 1 is byte-identical content, hashed from the raw upload bytes.
content_hash = sha256(buffer). Not derived fromstoragePathordocId. Two independent user sessions that upload the same PDF from disk now collide correctly. - Tier 2 is per-form logical identity via
documentInstanceKey. Persisted on the doc row asinstance_key(nullable — envelope forms like 26AS/AIS/TIS/MF CAS/ITR-V returnnulland match on(form_type, ay)alone). Broker CG usesbroker_name; Form 16 uses employer TAN; bank statements useaccount_no | period_start | period_end; etc.
And add the onDocumentPurged Firestore trigger — the missing
counterpart to onDocumentConfirmed. It fires on delete or on a
transition from confirmed to one of superseded / rejected_owner_denied / rejected_transfer_denied / rejected_duplicate, and:
- batch-deletes
ledger_entries where document_id == docId - batch-deletes
portfolio_positions where document_id == docId - calls
recomputeAfterConfirmfor the AY
Why
The 2026-07-06 fresh audit surfaced three related correctness holes that together made the "same document uploaded twice does not double-count" property false:
- Fingerprint was path-hashed, not byte-hashed — every fresh
upload got a new
storagePathand therefore a new fingerprint, making Tier 1 dedup near-dead code. documentInstanceKeywas defined but never called — Tier 2 dedup used(form_type, ay, account_id, period_start, period_end)withaccount_idoften null for brokers, causing cross-broker CG uploads to silently supersede each other.- No purge trigger existed — a stale
deleteDocumentcomment claimed a "Phase 5onDocumentDeletedtrigger" that never landed. Superseded and deleted docs stranded theirledger_entriesin place, andtax-review-enginereads ledger by AY with no join todocuments.status. Every re-upload of a confirmed doc double-counted its ledger contributions on the tax review.
Impact
Schema changes on documents/{docId}:
fingerprint: z.string()removed (no legacy — this branch has no customer data).content_hash: z.string().nullable()added — full 64-char SHA-256 of the raw bytes. Nullable only for Free-tierview_onlydocs whose bytes are downloaded later by the backfill worker.instance_key: z.string().nullable()added — result ofdocumentInstanceKey(formType, fields).
Runtime changes:
ingest-vault-document.ts— hashes the downloaded buffer, computesinstance_keyfrom the parsed fields, passes both tofindDedupLinks, and persists them on the doc row.parser/run-and-persist.ts— after LLM fallback resolves the form_type, re-stampscontent_hashandinstance_keyso rescan / backfill agree with initial ingest._lib/dedup.ts— rewritten. Tier 1 querieswhere("content_hash", "==", …). Tier 2 querieswhere("form_type", …).where("ay", …) .where("instance_key", …)— Firestore null-equality collapses envelope forms.- New
triggers/firestore/on-document-purged.tsandhandlers/purge-document-fanout.ts. - New composite index in
firebase/firestore.indexes.jsonfor the Tier-2 dedup query.
The deleteDocument callable's stale "Phase 5 trigger" comment is
now accurate.
Status
Active.
Sources
- Fresh-perspective audit of the document lifecycle, 2026-07-09
(branch
production-readiness-audit-v2). - Related decisions: 2026-07-06-v1-launch-blockers (B3 wired dedup
into ingest but on the broken fingerprint semantics; this decision
fixes the semantics), 2026-07-04-temporal-anchor-per-category
(feeds the Tier-2
ayaxis). - Related concepts / entities: upload-only, money-in-paise, pillar-tax, pillar-expense, pillar-portfolio.
Every project of mine is written down like this.
Read the résumé