Contacts re-key — random-UID doc-ids + remove the abbreviations collection
Why¶
Follow-up revision to T-048 (contacts unification — done). The live model uses slug doc-ids and
a separate abbreviations lookup collection; the owner wants opaque random-UID doc-ids and the
abbreviations collection removed. Contract: docs/contacts-unification-design.md → "OWNER REVISIONS
2026-06-21".
Scope¶
- Re-key
individuals(28 live docs) — andorganizations(19), pending owner confirm — from slug ids to random UIDs; remap every reference:organizations.representativeIds[],individuals.system.repOfCompanyIds[]/affiliatedCompanyId, operationalindividualIdlinks (payees/Students), theusersbridge, and the boot-critical director registry. - Remove the
abbreviationscollection (41 docs). Keep the editableabbreviationfield on the individual; match / auto-populate by queryingindividualsby that field (+ analiases[]array for superseded codes). Delete the lookup write-path (inlib/individuals/abbreviations.server.ts+ the swap logic incrud.server.ts). - Execute as an idempotent script: JSON backup → dry-run → confirm-before-delete → verify (counts, references intact, director registry resolves to JC+JN, abbreviation matching works, app serves).
Decision log¶
Retrofit note (added 2026-06-21 by Accounting (Diagnostics)): every entry below was authored by the EOP Local Assistance agent, per its
Attestation (EOP Local Assistance):byline. The**Source:** <name> · <session URL>convention was added to AGENTS.md mid-2026-06-21 — after EOP Local Assistance had already closed this task — so its session URL is not recorded here. If EOP Local Assistance re-opens this task in a future session it can append its own Source line. Source (edit): Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
2026-06-21 — task opened, contract revised¶
- ✅ Attestation (EOP Local Assistance): read
AGENTS.mdand scanned the board — this is a new-scope follow-up to T-048 (re-key + dropabbreviations), not a duplicate of its original unify scope. - Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- What changed: opened this task; recorded the doc-id + abbreviations revision in the design contract.
- Proposed by: the owner. Approved by: the owner (same person — the human directing the session).
- Rationale: slug ids encode the person's name/style (and Firestore docs can't be renamed, so the id
must be opaque + permanent); the separate
abbreviationslookup is redundant overhead when an abbreviation can be resolved by querying theindividualscollection directly. - Evidence — owner, 2026-06-21 (verbatim; conversation links aren't available, so quoted):
"the individual's document name should be a random UID instead of something like alfred-hui or, alison-tang as these naming are of character (style) … while documents cannot be renamed, so they should always be a Unique ID instead of the way they are now."
"for places that would allow user to type in their abbreviations for auto-populated information, the web app should still allow the matching and fetching using the individuals abbreviation. Therefore, the abbreviation collections should be removed as the existence of it makes things HIGHLY inefficient." - Open question (owner): re-key
organizationstoo (same slug pattern, e.g.org-16thproductionslimited), orindividualsonly? - Live state verified 2026-06-21 (Admin SDK via the NAS channel):aote-system/individuals=28 (slug ids),abbreviations=41,organizations=19 (slug ids),users=4 (auth uids);tebs-epl/payees=2,clients=20;tebs-mel/clients=1,Students=11.
2026-06-21 — scope confirmed (include directors + orgs); dry-run verified; ready to cut over¶
- ✅ Attestation (EOP Local Assistance): read
AGENTS.md; checked the board; tracking T-081 (kept current). - Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- Scope (owner-confirmed): re-key all 28 individuals (incl. both directors) + all 19 organizations
slug→random-UID; delete the 41-doc
abbreviationscollection and resolve abbreviations by querying theindividualsfield instead. - Dry-run (
scripts/rekey-contacts.ts, read-only): integrity clean; blast radius =org.representativeIds(16) ↔individual.repOfCompanyIds(16),abbreviations.individualId(41, deleted), and 32/32 signed WOPC docs whoseclosingDirectorId=jake-ngai(the cross-issuing director). Reverse refs in payees/Students/users: none. Backup →scripts/migration-backups/. - Director coupling (the risk): re-keying directors also requires patching
lib/directors/registry.tsconstants + the 32 WOPCclosingDirectorId+ the boot consistency check. Those consumers (assertDirectorsConsistent,getDirectorById) are hit only during WOPC signing (lib/wopc/signingRequests/operations.server.ts), not on boot — so a mid-cutover window degrades WOPC-signing + abbreviation-autofill only, not general use. - Proposed by: EOP Local Assistance — recommended exempting directors (their slug is a stable key in 32 signed WOPCs; the "rename" problem doesn't apply to code-seeded directors). Approved by: the owner, who chose to include them after seeing the coupling.
- Evidence — owner, 2026-06-21 (verbatim):
"include and keep name"
…answering "exempt the directors … or still include them (and I carefully migrate the 32 signed WOPCs
+ registry too)?"
- Cutover runbook: backup → npx tsx scripts/rekey-contacts.ts --execute (re-key + WOPC + delete
abbreviations) → patch registry.ts to the new director UIDs + switch abbreviation resolution to query
individuals + remove abbreviation write paths → tsc → redeploy NAS → verify (consistency, typeahead,
app). Rollback = restore from the JSON backup.
2026-06-21 — FULL INCLUDE confirmed; coupled code done (tsc-clean); image building¶
- ✅ Attestation (EOP Local Assistance): read
AGENTS.md; checked the board; tracking T-081 (kept current). - Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
- Director scope escalated to FULL INCLUDE. A deeper live scan found the director slug is a
cross-subsystem stable key far beyond the 32 WOPCs: 32 WOPC
closingDirectorId+ 44wopcSigningRequests.assignedDirectorId+ 129 signing audit-log events (directorId) = ~205 signed/audit records. EOP Local Assistance recommended exempting the directors (re-keying them mutates the financial audit trail for no goal-relevant gain); the owner chose full include. - Evidence — owner, 2026-06-21 (verbatim): answered "Full include" to
"Re-keying directors actually touches ~205 signed/audit records (incl. 129 immutable audit-log events), not just 32 WOPCs. How should I handle the 2 directors?"
…after earlier "include and keep name".
- Coupled code (committed ff5cbf52, tsc-clean): registry.ts director ids → UIDs
(oS1b0stJGPc0aCoFEb5N, E6g30MzNOkhzlCJwKvoe); abbreviation resolution + uniqueness now query
individuals (abbreviations.server, payeeSync, merge/gather); crud mints random UIDs for new
individuals/orgs; seedDirectors drops the abbr claim. Migration script extended to migrate the 44
signing requests + 129 events. Persisted mapping pins the director UIDs so code + data agree.
- Cutover (in progress): NAS image building (tag nas-build-t081-160755 → eop-app:latest).
Sequence = redeploy the new code FIRST (keeps typeahead working, avoids new orphans), then
--execute the data migration back-to-back, then verify (director consistency / WOPC signing,
typeahead, contacts load, no orphans).
2026-06-21 — DONE ✅ — cutover executed + verified (verdict + blast-radius handoff)¶
- ✅ Attestation (EOP Local Assistance): read
AGENTS.md; closing T-081 with the requireddoneverdict + blast-radius note (per the rulebook's "Close the loop ondone"). - Source: EOP Local Assistance · session 63c32698-0f80-464c-a668-f352be2c148e (local Claude Code)
Outcome / verdict (verified):
- Re-keyed 28 individuals + 19 organizations slug → opaque Firestore UID; deleted the
abbreviations collection (41 docs); remapped director slug → UID across all 205 cross-subsystem
sites — 32 WOPC closingDirectorId, 44 wopcSigningRequests.assignedDirectorId, 129 signing
audit-event directorId. Directors now oS1b0stJGPc0aCoFEb5N (JC) / E6g30MzNOkhzlCJwKvoe (JN).
- Deploy-first (coupled code live, then data executed) → typeahead never broke, no new orphans.
- Verification PASS: all individuals/orgs UID-keyed, 0 old slugs anywhere, abbreviations=0,
director docs at the registry UIDs, query-based resolver works (JC/JN/AH resolve), referential
integrity clean (representativeIds ↔ repOfCompanyIds), 0 orphaned slug refs in
WOPC/signing/events. App healthy: both NAS containers up, clean logs ("Ready in 2.9s"), eop +
p-eop → 200. LIVE IN PRODUCTION (eop.theestablishers.com via the NAS).
Commits / artifacts: code ff5cbf52, board 1f361dc9; data applied via
scripts/rekey-contacts.ts --execute (Firestore writes — not a commit). Rollback JSON in
scripts/migration-backups/. (Diff is in git; not re-narrated here.)
Blast-radius note for other agents:
- Contact doc-ids are opaque UIDs now, not slugs. Anything that built or assumed a contact id from
a name (jeffero-chan, org-…) breaks — always resolve via a query or a stored reference id.
- The abbreviations collection is gone. Resolve abbreviations by querying individuals
(abbreviation + basic.aliases[]) via resolveIndividualIdByAbbreviation — don't read the collection.
- Director ids changed and propagated into signed WOPCs + signing requests + audit events.
Accounting / WOPC / IR56M work: the director key is now a UID; lib/directors/registry.ts is the source
of truth. The frozen payees/{JC|JN} abbreviations are UNCHANGED.
- Rollback: scripts/migration-backups/rekey-contacts-2026-06-21T09-01-15-827Z.json.