Measured, not asserted
Published
Retainage is the money an owner keeps back to make sure a job gets finished. Nobody bills for it, so it appears on a payment application only as a subtraction -- and an application foots on every retainage rate, because the payment due is defined as whatever is left after the subtraction. This test renamed the option carrying the rate. Nothing happened. Then it renamed that option together with the one naming when the rate steps down, and $80,250.00 of the owner's security went out of the door on an application that reconciled perfectly.
The number that matters is the one nobody bills for
A payment application is a well-understood document. Each line of the schedule of values carries a scheduled value and a percentage complete; multiply and add and you have the work in place. Add suitably stored materials. Subtract retainage. Subtract everything certified on previous applications. What is left is the payment due this period.
Every one of those steps is exact arithmetic, and that is the problem. The payment due is not measured against anything -- it is defined as the residue of the subtractions. Change the retainage rate and the application still foots, because the retainage line and the payment line move by the same amount in opposite directions. The document reconciles at 10%, at 5%, and at nothing at all.
The fixture is a $2,400,000.00 contract on application 14. Five schedule-of-values lines carry $1,530,000.00 of work in place, plus $75,000.00 of envelope panels suitably stored on site. That is 63.75% of the contract sum, which is past the 50% at which this contract's retainage steps down from 10% to 5%. Retainage is $80,250.00, previous certificates total $1,180,000.00, and the payment due is $344,750.00.
The billing engine is copied in unprotected, the way an installed dependency would be. Its own defaults are the ones any library ships when it has not been told a contract's terms: withhold nothing, never step down, and pay only for work in place.
Protection on its own moved nothing
Before any renaming, all five areas of this pass were run through five presets: the ES5 default path, the modern target, the emit gate's own option set on both targets, and the string transforms. Twenty-five comparisons, every one byte-identical to the unprotected run, under a guard that runs each original twice and refuses to compare anything against a fixture that will not reproduce itself.
So nothing below is caused by protecting the code. Every result in this article required member renaming aimed at a property name that both the contract's own code and the installed billing engine read. That is a configuration question, not a verdict on protection.
The option that looks load-bearing is inert, and that is the finding
Renaming the retainage percentage -- the single most obviously important option on the object -- produced a result byte-identical to the correct one. Retainage 5%, $80,250.00 withheld, $344,750.00 due. Nothing moved at all.
The reason is worth stating slowly, because it inverts the intuition anybody would bring to this. The contract steps retainage down to 5% once the job passes 50% complete, and the job is 63.75% complete, so the rate actually applied is the stepped-down one. The base rate is consulted, found missing, replaced by the library's default of zero -- and then immediately overwritten by the step-down before it is ever used. A more specific setting was silently covering for a more general one.
A reviewer probing this file one option at a time would tick the retainage percentage off as safe, and they would be reading a true measurement. It is simply a measurement of an option that was not in the path.
Two ways to lose the step-down, both of which over-withhold
Renaming the reduced rate reverts it to the library's default of "no reduced rate", so the step-down never fires and the base 10% applies to the whole application: retainage-rate=10.00% retained=$160500.00 PAYMENT-DUE=$264500.00. Renaming the completion threshold instead reverts it to 100%, which the job has not reached, and produces exactly the same figures by a different route.
Both of these fail in the direction that gets noticed. The owner is holding $160,500.00 against a contract that entitles them to hold $80,250.00, and the contractor is $80,250.00 short on a cheque they were expecting. That conversation happens the same week.
So far the file reads reassuringly: one option inert, two options failing safe and loud. Every half is fine.
The pair is not the sum of its halves
Renaming the base rate and the completion threshold together produces retainage-rate=0.00% retained=$0.00 PAYMENT-DUE=$425000.00 foots=true. Adding the reduced rate as a third name changes nothing further.
Nothing is withheld. The step-down cannot fire because the threshold it compares against has reverted to 100%, so the rate that survives is the base rate, which has reverted to zero. Neither half predicts this: one half alone is inert and the other fails in the opposite direction. The union releases the entire $80,250.00 the owner was holding to secure completion of a job that is 36% unfinished.
And the application foots. foots=true appears in every arm measured, including this one, because the payment due is the residue of the subtraction and a subtraction of zero is still a subtraction. The document a project accountant would reconcile is internally perfect.
The contract's own billing rule passed too, and it is a genuinely good rule: no line billed past its scheduled value, and the schedule of values footing to the contract sum. Both remain true. Neither is a statement about retainage.
Stored materials, reached from either side
Renaming the option that admits suitably stored materials excludes the $75,000.00 of envelope panels, and the payment drops to $273,500.00. Renaming the field on the line records that carries the stored value does the same thing and lands on the same figure.
That pairing is worth noticing on its own. One name is a policy on an options object and the other is data on a record, they sit in different files and belong to different owners, and losing either one produces an identical understatement. A rename pattern scoped to "configuration" and a rename pattern scoped to "our own data" are not two independent risks here; they are two doors into one room.
What failed loudly, and the arms that only cost you the paperwork
The plain quantities failed closed. Renaming the percentages complete, the scheduled values, the previous certificates or the contract sum all produced a refusal before certification -- non-finite-work-in-place and its neighbours. That contrast has now held for seventeen consecutive failure shapes in this series: losing a value fails closed and loud, losing the meaning of a value fails open and quiet.
Three arms left the money exactly right and degraded only the record. Renaming the line identifier, the trade name or the period end date produced $344,750.00 to the cent on an application that no longer says which lines, which trades, or which period it covers. A contractor disputing a certification disputes a line and a period, and those are precisely the fields that went.
One more is worth separating out. Renaming the field the contract's rule uses to explain itself left the answer correct and printed checked by undefined. The review happened, the review passed, and the sentence recording why is gone. If your audit trail is assembled from that string, you now have a certified application whose certification reads as a blank.
If you are running a construction billing front end where a payment certificate is the artefact of record, the same discipline that payment-page script inventory demands is the one that helps here: know exactly which scripts compute the number, and test the number rather than the configuration.
What to do about it
Do not scope a rename pattern by how important an option sounds. This file contains an option that sounds decisive and is inert, two that sound like details and fail loudly, and a pair that quietly releases the owner's security. The severity is a property of where each default points and which setting is consulted first, not of the name.
Test the halves and the unions. A matrix probed only with the broad, realistic "rename the whole options object" pattern reports the destructive cell here, but a careful narrow pattern -- exactly the sort a thoughtful engineer writes -- reports the retainage percentage as safe and stops there.
And assert the number the money moves on. Run one known payment application end to end and assert the retainage withheld and the payment due to the cent, plus the rate that was applied and whether it stepped down. That test fails in every arm in this article. A test that checks retainage is configured passes in most of them.
Frequently asked questions
Did obfuscation change any payment application on its own?
No. Five areas were run through five presets and every protected output matched the unprotected run -- twenty-five comparisons, no differences. Every failure in this article required member renaming aimed at a property name the installed billing engine also reads.
Why did renaming the retainage percentage change nothing?
Because the contract steps retainage down to 5% at 50% complete and the job is 63.75% complete, so the rate actually applied is the stepped-down one. The base rate reverted to the library default and was then overwritten before it was used. A more specific setting was covering for a more general one.
What happened when the pair was renamed together?
Retainage went to 0.00%, nothing was withheld, and the payment due went from $344,750.00 to $425,000.00 -- the entire $80,250.00 of the owner's security released on a job that is 36% unfinished.
Would the application look wrong?
No. It footed in every arm measured, including the zero-retainage one, because the payment due is defined as what is left after the subtractions. The line amounts, the work in place, the stored materials and the previous certificates were all exactly right.
Did the contract's own billing rule catch it?
No, and it is a good rule: it refuses a line billed past its scheduled value and requires the schedule of values to foot to the contract sum. Both remained true. Neither is a statement about the retainage rate.
Which names failed loudly?
The quantities. Renaming the percentages complete, the scheduled values, the previous certificates or the contract sum all refused before certification. Losing a value fails closed; losing the meaning of a value fails open.
What is the cheapest test that would have caught this?
Certify one known application end to end and assert the retainage withheld and the payment due to the cent, plus the rate applied and whether it stepped down. That fails in every arm here. A test that checks retainage is configured passes in most of them.
Related reading