Measured Behaviour
Published
A cost screening policy is three lists on one object, and only two of them are about whether a cost is allowable. The third decides whether a document had to exist before the money moved. We measured what a rename does to each, and the arm that got caught was not the one we expected.
Three lists, and the third is a different kind of thing
We built a voucher feed the way a real one looks: forty-two vouchers across a budget year, containing in-budget costs, costs the budget does not contain, costs barred outright on any federal award, costs needing prior approval, costs that walk a category through its cap, and six costs incurred after the period of performance closed.
The policy has an allowlist of nine budgeted categories, a denylist of four barred ones, three categories that need a prior approval on file, four category ceilings, and a period end on day 365. The correct run charges 279,300.00 across twenty vouchers and refuses 140,810.00: two costs not in the budget, nine barred, three with no approval on file, two over cap and six outside the period.
Protection alone, on all five profiles, produced identical behaviour. Everything below came from member renaming reaching the policy object, which is what a regexp like ^(allowableCategories)$ or a broad RenameMembers pattern over a configuration literal does; the scoping mechanics are in Protect Members.
Renaming the budget charged a cost the budget does not contain
An allowlist the library cannot read is indistinguishable from one you never supplied, and a screening library that has been shown no allowlist permits every category there is. It cannot do anything else - refusing every category it has not been told about would make it unusable on the first run.
Measured: charges went from 279,300.00 to 300,980.00, and 21,680.00 of consulting appeared in the charged totals. Consulting is not an unallowable cost. It is an ordinary, perfectly proper business expense that simply is not in this award's approved budget, which is exactly the kind of charge that becomes a questioned cost three years later.
The institution's own policy check passed. It looks for barred categories in the charged totals, and there were none - the denylist was still intact and still doing its job.
Renaming the barred list moved no money whatsoever
The denylist arm is the one worth dwelling on. Renaming it produced charges of 279,300.00 and refusals of 140,810.00 - identical to the correct run, to the cent, on both figures.
What moved was the reason. Costs refused as barred went from nine to zero, and costs refused as not in the budget went from two to eleven. The same nine vouchers were refused either way, because alcohol, lobbying, entertainment and fines are not in the approved budget either. One list was shadowing the other.
A finding that changes a reason code and no dollar is a finding reviewers close, and closing it is defensible: on this feed, in this arm, nothing was mischarged. The problem is that the conclusion does not survive the two lists failing together.
The pair was the only arm anything caught
Renaming both lists produced the one refusal in the whole file. The run was rejected outright: the institution's policy check reported 7,790.00 dollars of alcohol charged to a federal award and stopped it.
That is the inverse of the pattern this series usually measures. The narrow, careful rename is the silent one. The broad, sloppy rename is the one that gets caught - and the reason is precise. The institution's own rule names the barred categories as text in its own code, so it can only fire when a barred cost actually reaches the charged totals, and that needs both lists to fail at once.
A pattern that reaches one configuration key and not its neighbour is not a safer pattern here. It is the pattern that leaves the alarm switched off.
The approval list is not an allowability question at all
The third list decides whether a document had to exist before the money moved. Participant support costs on this award are allowable, budgeted, ordinary and under their cap - the sponsor simply has not yet agreed to them in writing, so they may not be charged yet.
Renaming the prior-approval list took charges from 279,300.00 to 298,600.00 and put 19,300.00 of participant support into the charged totals with no approval on file. Nothing about the cost is wrong. Nobody disputes it. It is a repayment at audit for a paperwork defect on a proper charge.
The detail that makes it quiet: the run also reports how many costs were charged without documentation, and that counter went from three to zero. The measure that exists to flag this went to zero in the same edit that caused it, because both are computed from the same list.
Renaming the flag that requires the document to be on file, rather than the list itself, charged the same 298,600.00 - but left the undocumented counter reading three. Same money, and one of the two arms leaves a trace.
The period of performance, and what to do about all of it
One more name decides more than it looks like it does. Renaming the budget period end took charges from 279,300.00 to 317,960.00 and costs refused as outside the period from six to zero. Costs incurred after an award closed are not costs of that award, however allowable they would have been inside it, and that is a single date.
The practical guidance is the same as ever and it is not complicated. Exclude the policy object from the member pattern. A list of budget categories is not a secret worth protecting, and it is the single most fragile thing in the file.
Then pin the counts, not just the totals. The correct run reports two not-allowable, nine barred, three no-approval, two over cap and six after period. A test that asserts that vector fails on every arm in this article, including both of the ones where the charged total did not move at all.
If your institution also handles student or minor records alongside its awards, the overlapping obligations are covered in COPPA, FERPA and the JavaScript You Ship.
Frequently asked questions
Did protecting the bundle change which costs were charged?
No. On all five profiles we tried, the protected bundle screened the same forty-two vouchers to the same result as the original. The differences all came from member renaming reaching the names on the screening policy object.
Why does losing an allowlist fail open rather than closed?
Because a list the library cannot read is indistinguishable from one you never supplied, and a screening library shown no allowlist has to permit everything. Refusing every category it has not been told about would make it unusable. The allowlist's fail-closed reputation applies to an unknown entry, not to an unread option.
Renaming the barred list changed nothing. Is that safe?
On this feed, in that arm, no money moved - charges and refusals were identical to the cent, and only the reason codes changed. It is not evidence of safety, because the same two lists renamed together charged 7,790.00 dollars of alcohol to the award. A half that looks inert can be the half that switches off the alarm.
Which arm did the institution's own policy check catch?
Only the one where both category lists were renamed together. It rejected the run and named the amount. In every other arm it passed, including the arm that charged 21,680.00 dollars of unbudgeted consulting, because it looks for barred categories and consulting is not barred - it is simply not in this budget.
What happened to prior approvals?
Renaming the prior-approval list charged 19,300.00 dollars of participant support with no approval on file, and the counter that reports costs charged without documentation went from three to zero in the same edit. The cost itself is allowable and undisputed; what is missing is the document the award required.
What is the cheapest test that would have caught these?
Assert the refusal counts as a vector, not just the charged total. The correct run refuses two as not allowable, nine as barred, three for no approval, two over cap and six outside the period. That assertion fails on every arm in this article.
How do we keep renaming away from the screening policy?
Scope the member regexp so the policy literal is excluded, and keep renaming pointed at logic rather than settings. The option and regexp mechanics are documented in Protect Members.
Related reading