Measured Behaviour
Published
A deposit system has obvious failures everybody tests for: an amount that will not add up, a date that will not parse, a balance that goes missing. It has one almost nobody tests for, because it is not a property of any amount - the money that is in the account and is not yet yours to spend. We measured what a rename does to it.
Money that is present, correct, counted, and not available
We built one account the way one actually looks. Opening balance $250.00. Six deposits over eight days: cash, a wire, three cheques and an on-us item, $19,000.00 between them. A published availability policy giving each item type its hold in business days. A same-day cutoff hour. The statutory $225.00 that must be made available next day, and the large-item threshold above which an extra hold is allowed.
And an exception hold register carrying three entries, each one placed by a person on a named item for a stated reason: a large out-of-state cheque on an account twelve days old, a cheque from a payer whose last item came back unpaid, and a customer with a repeated-overdraft history.
Every dollar behind those three holds is genuinely in the account. It posted on the day it says, from the presenter it says, and it is counted in the ledger balance in full. There is nothing wrong with the money. The money is simply not collected yet, so it is not the customer's to spend and it is not the bank's to lend them.
Six payments arrive on day 303 totalling $12,000.00. The correct run examines all six, pays four, and refuses two - a $6,400.00 outgoing wire and a $2,800.00 cheque - because the available balance is $3,050.00 even though the ledger balance is $19,250.00. Total paid out: $2,800.00.
We then protected the file with default settings on five profiles: the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile. All five produced identical behaviour. That is the first result and it deserves stating plainly - protection on its own did not change a single authorisation decision.
The failures start when member renaming reaches the names on the availability object or on the hold register's own records, which is what a broad RenameMembers pattern over a configuration literal actually does. If you have not yet decided how far your member pattern reaches, the mechanics are in Protect Members.
What the register loss actually did
We wrote the prediction down before measuring, because an expectation formed after the fact is worth nothing. The prediction was that the number examined would not move, that the ledger balance would be identical to the cent, that the held-blocked counter would read zero, that the exposure figure would read zero, and that the bank's own check would pass.
Every part of it landed. Examined stayed at six. The ledger stayed at $19,250.00 - not close, identical. Paid went from four to six. Refused went from two to zero. The available balance the engine computed went from $3,050.00 to $12,600.00, and the amount actually paid out went from $2,800.00 to $12,000.00.
The counter whose entire job is to report this - payments blocked by a hold - went from two to zero. So did the exposure figure, the dollars paid out against funds that had not been collected. It read $0.00 in the correct run because nothing had been paid against uncollected funds, and it read $0.00 in the broken run because the register that decides what is uncollected is the thing that went missing. The one number a fraud manager would look at reads zero precisely because the number that would have made it non-zero is what was lost.
And the availability check passed, printing: six requests, six paid, zero refused, $0.00 paid against uncollected funds. Every figure in that sentence is true. Six requests really were examined. Six really were paid. None of them exceeded the balance the account held. The invariant anybody would write - nothing was paid that the account did not hold - is a statement about the ledger, and the ledger was perfect.
Read that sentence beside the correct one - six requests, four paid, two refused - and notice which reads better. Six of six paid with nothing declined looks like a well-funded account. A run with two refusals in it looks like something a supervisor might want to explain.
The bill arrives two days later
Nothing about the day of the run is wrong, which is why nobody stops it. The loss shows up when the items behind the holds come back.
On day 305 the correspondent returns the $6,800.00 out-of-state cheque unpaid, and on day 306 the $3,200.00 local cheque follows it. In the correct run the account had paid out $2,800.00, so $10,000.00 of returns leaves it holding $6,450.00 and nobody notices anything. In the broken run the account had paid out $12,000.00, and the same two returns leave it $2,750.00 overdrawn - against a customer who has already spent the money and a payer whose cheques do not clear.
That is the whole shape of this failure in one line. The bank did not miscount, misprice, misdate or misallocate anything. It lent the customer money it had not collected, because the record saying so was the record that could not be read.
Two names, one outcome, and a union that hides nothing new
Renaming the hold register and renaming the flag that says whether to honour it produce byte-identical output on every counter printed. So does the union of the two. That is worth saying because it is the practical answer to a question people ask about member patterns: scoping the pattern to spare the configuration object buys nothing here if the record shapes are still inside it.
The rule-21 pairing behaves the same way. Renaming the register together with the flag that enables the availability check is byte-identical to renaming the register alone, except that the check line reads unchecked. The guard was contributing nothing to catch, so removing it changed nothing to remove.
Renaming the caller's rule function on its own is quieter still. Every money figure stays exactly correct, and only the last clause of the output changes: checked by library-builtin(accepts any availability result). The word checked survives. A log line that says a control ran is not evidence that your control ran.
The arm that looked safest was the one masked by a sibling
The most useful result of this area was not the headline. It was that two arms came out byte-identical to the correct run on every counter - the hold policy table and the cutoff hour.
Neither is inert. Both change what the engine computes. They simply do not change any decision on this deposit book, because for every item they touch, an exception hold sits on the same item with a later release day and dominates it. The only trace either leaves in the entire output is a release day inside a display field, past where a log line truncates.
That is a property of this account on this day, not of the software. Run the same rename against the account with no exception holds on it - the fixture most people would build - and the hold policy arm is loud immediately, because there is nothing left to mask it. An arm that measures clean against your busiest account can be the one that empties your quietest.
The large-item threshold is the counter-example in the same file. Losing it alone releases a $6,200.00 on-us item four days early: available goes from $3,050.00 to $9,025.00, paid out goes from $2,800.00 to $8,600.00, and the account finishes the week with $650.00 instead of $6,450.00. Same object, adjacent lines, and the difference between silence and $5,800.00 is which sibling happens to cover the same item.
Why the test suite you already have cannot see this
We ran the same six payments against a second deposit book: the same account, the same policy, the same cutoff, the same statutory amounts - and no exception holds, because nothing had ever been flagged on it.
That control run reports six examined, six paid, zero refused, zero blocked by a hold, $12,600.00 available, $12,000.00 paid out, and the account $2,750.00 overdrawn after the returns. It is byte-identical to the broken run. Not similar - identical, on every counter the engine prints.
Constructed test data has no exception history in it. Nobody writes a fixture in which a person flagged a specific item for a specific reason twelve days after the account opened, because a fixture is built to exercise the arithmetic, and the arithmetic is not what breaks. So the fixture most teams already have is, by construction, indistinguishable from the failure - which means a green suite is not evidence about this class at all.
The fix is not a stricter assertion. It is a different one: assert the encumbrance, not the balance. Assert that the available balance is strictly less than the ledger balance when an uncollected item exists, name the item that is holding the difference, and make a hold register that cannot be read an error rather than an empty one. That is worth having whatever your build does, because a dropped column in a core-banking export produces exactly the same empty register.
On the protection side the answer is narrower and duller. Keep RenameMembers off the objects that cross into an installed library, or scope the pattern to names you own on both sides. The default profiles measured clean here on all five; the damage arrived only with a member pattern wide enough to reach the register. If your application handles regulated deposit flows, our notes on SOX, GLBA and financial application JavaScript cover where the evidence for that scoping usually has to live.
Frequently asked questions
Did obfuscation change the ledger balance?
No. The ledger balance was $19,250.00 in the correct run and $19,250.00 in every broken arm, identical to the cent. That is the difficulty rather than the reassurance: the balance is the figure most reconciliations are defined over, and it is the figure this failure leaves untouched.
Which setting actually caused it?
Member renaming reaching the hold register or the flag that honours it. All five default profiles we measured - ES5, modern, both emit-gate configurations and string encoding - produced identical behaviour with no options set.
Why did the exposure counter read zero?
Because it is priced against the hold register. Exposure is defined as dollars paid out beyond what had been collected, and what had been collected is computed from the register that went missing. A counter defined over the control it monitors is not a second opinion.
Would a reconciliation have caught it?
Not the one anybody writes. The check we ran asserts that nothing was paid which the account did not hold, and that is true in every arm because it is a statement about the ledger. A reconciliation closed by construction is an invariant of your code, not evidence about the result.
Does this only matter for cheque deposits?
No. The same shape applies to any encumbrance: a card authorisation hold, a pending ACH debit, a reserved inventory unit, a pledged collateral position. Anywhere a quantity is present and correct but not available, the record saying so is separable from the quantity itself.
What single assertion would have caught it?
That the available balance is strictly less than the ledger balance while an uncollected item exists, naming the item responsible. That fails immediately in every broken arm here and passes in the correct one, which is exactly what an assertion is supposed to do.
How do I keep the register out of the rename?
Scope MemberRegexp to names your own code owns on both sides of every boundary, and keep it off any object handed to an installed library. The option reference and its worked examples are in Protect Members.
Related reading