From 95e0ab8afa8de3949c2baa0c0953a20c26b460a3 Mon Sep 17 00:00:00 2001 From: PJ Date: Sat, 15 Aug 2026 21:11:47 +0530 Subject: [PATCH] docs(folio): say why the merged card key cannot be made injective Folio rejects a duplicate account name, so the twin the drop rule guards against is two names the web key cannot tell apart, not two accounts sharing a name. State what closing the rest would cost and what the tree would have to carry to close it properly. --- examples/folio/sanderling/predicates.ts | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/examples/folio/sanderling/predicates.ts b/examples/folio/sanderling/predicates.ts index fe94ee5..db36849 100644 --- a/examples/folio/sanderling/predicates.ts +++ b/examples/folio/sanderling/predicates.ts @@ -417,13 +417,26 @@ export function homeAccountsOf(cards: readonly CardReading[]): Account[] | null // // A name carried by more than one card is left out for the same reason, the // rule createdAccountHasNonZeroBalance applies with `matches.length === 1`: -// nothing here can say which of them a count came from. Folio accepts the same -// account name twice and Home lists whatever fits the viewport, so a reading -// that saw one Travel card and a later one that saw two would otherwise -// subtract two DIFFERENT accounts' counts and convict a healthy app of -// double-submitting. The twin does not have to be readable to spoil the -// identity, so duplicates are counted over every card, not just the usable -// ones. Dropping a card can only ever cost a detection. +// nothing here can say which of them a count came from. Two accounts never +// share a NAME (Accounts.name is UNIQUE and Repository.createAccount rejects +// one already taken, NOCASE), but they can share a KEY, because web's key is +// the card text in front of the digit run, which is the name with any trailing +// digits shaved off it: "Travel1" holding 25 transactions and "Travel12" +// holding 6 both key to "TRTravel" and both read a three-digit run, so +// subtracting one from the other subtracts two unrelated counting series. The +// twin does not have to be readable to spoil the identity, so duplicates are +// counted over every card, not just the usable ones. Dropping a card can only +// ever cost a detection. +// +// What this cannot see is a twin that never shares a reading with its pair, and +// Home lists only what fits the viewport. Nothing computed from the card text +// can: the two cards' text is identical character for character ("TRTravel1" +// followed by "25 transactions" and "TRTravel12" followed by "6 transactions" +// are one string), so no key derived from it separates them. Refusing every run +// that could hide a name's own digits would, at the price of the evidence web +// convicts on today, whose measured witness is a count of 12 rising to 14. +// Separating them needs something the tree does not carry: the account's id on +// the card, or a separator in front of the count. export function homeTxnCountsOf(cards: readonly CardReading[]): Record | null { const cardsPerName = new Map(); for (const card of cards) cardsPerName.set(card.name, (cardsPerName.get(card.name) ?? 0) + 1);