Measured, not asserted
Published
A vested percentage is not a fact about a participant. It is derived from a count of years, and the count is derived from a rule about what makes a year count. This test renamed that rule -- one option key naming an hours requirement -- and a participant who was 40% vested in a $12,400.00 employer match became 60% vested. The plan paid out $7,440.00 where it owed $4,960.00, forfeited $4,960.00 where it should have forfeited $7,440.00, and its own service check passed and printed the wrong figure as its evidence.
The percentage is three derivations deep, and only the last one looks like money
Every recordkeeping system does the same work when somebody terminates. Take the participant's plan years. Decide which of them count as a year of service. Count them. Look the count up in the plan's vesting schedule to get a percentage. Multiply the employer match by it. Whatever is left is forfeited back to the plan.
Only the last step is arithmetic on money. The three before it are policy: what makes a year count, how many years each step of the schedule requires, and what percentage each step grants. A reviewer checking a settlement statement sees dollars, and the dollars are the output of all three.
The fixture is configured the way a plan document would configure it: six-year graded vesting at 20/40/60/80/100, the hours-counting method of crediting service, and a 1,000-hour requirement per plan year. Participant P-40118 was on the payroll for four plan years but was credited with 1,000 hours in only three of them -- 2022 was a part year at 820 hours, which is exactly the case the hours-counting method exists to handle. Three years of service is 40% vested. Participant P-40266 has seven full years and is 100% vested.
The vesting engine is copied in unprotected, as an installed dependency would be. Its own defaults are the permissive ones any library ships when it has not been told a plan's rules: no schedule at all, elapsed-time service, and no hours requirement.
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, with 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 plan's own code and the installed vesting engine read. That is a configuration question, not a verdict on protection.
A real plan year, correctly named, that should not have counted
Renaming the hours requirement reverted the engine to its default of zero hours, so every plan year on file counted. P-40118 went from three credited years to four, from 40% vested to 60%, and the settlement reads credited=4y [2021/2022/2023/2024] vested=60% match=$12400.00 keeps=$7440.00 forfeited=$4960.00 => FORFEITURE POSTED.
Read that line the way a benefits analyst would. A forfeiture was posted, so the mechanism is visibly working. The match balance is right. The percentage is a real step on the plan's own schedule. The credited years are named individually and every one of them is a plan year this participant genuinely worked -- 2022 included, because they were on the payroll for it. Nothing is undefined, nothing is NaN, and no figure on the line is implausible.
The only thing wrong is that 2022 should not have counted, and the reason it should not have counted -- 820 hours against a 1,000-hour requirement -- is not on the line at all. $2,480.00 left the plan that should have stayed in it.
The plan's own rule of record passed and said so: checked by service-check(4y credited => 60% of $12400.00). That sentence is true. Four years were credited and 60% is what four years buys. The check was written to refuse a percentage that was not derived from a credited-service count, and a count was derived; it was simply derived under a rule that had reverted.
Two independent names reach the same wrong answer
Renaming the service-crediting method rather than the hours requirement produced a byte-identical result: elapsed-time service counts every plan year on file, so P-40118 is credited with four years and 60% again. Two different names, in two different positions on the same options object, arrive at the same overstatement by different routes.
That matters for how you scope a rename pattern. Excluding one of these names and not the other buys nothing at all. The pair has to be treated as one surface, and the surface is "everything that decides what counts", not "the option that looks like a policy".
Adding the enabling flag in front of the plan's check to either pattern changed nothing, which is worth stating precisely because it is the opposite of what four previous passes in this series measured. Here the check could not have caught it either way: the check is defined over whether a count exists, and the count exists. A guard written over the presence of a value is blind to a change in the rule that produced it.
The schedule's own sub-keys fail loudly, and take a fully vested participant with them
The vesting schedule is a list of steps, and each step is an object with a service requirement and a percentage. Renaming those two sub-keys -- years and pct, either separately or together -- leaves the schedule visibly present in the configuration dump and matching nothing. Every participant drops to 0% vested.
P-40118 forfeits the whole $12,400.00. P-40266, who has seven years of service and is unambiguously 100% vested, forfeits the whole $31,875.00. That is loud: a fully vested long-service participant losing an entire match balance is the kind of result a plan sponsor hears about within a day.
Renaming the schedule itself goes the other way and is quieter. An empty schedule has no step to fail, so the engine falls back to full vesting -- the only safe answer a library can give when it has not been told a plan's rules -- and P-40118 reads vested=100% forfeited=$0.00 => FULLY VESTED, NOTHING FORFEITED. The $7,440.00 the plan was owed simply does not appear.
So one options object contains a name whose loss over-vests by 20 points, a name whose loss over-vests to 100%, and two names whose loss zeroes everybody. The direction is decided by where each default points, not by how important the option sounds.
What failed safely, and what lost only its evidence
The plain numbers failed closed. Renaming the match balance threw a TypeError at the point the plan's own rule formatted it, so nothing settled at all. Renaming the hours on the service record credited zero years and forfeited everything, loudly. Renaming the rule's own ok flag rejected every settlement -- a permission that goes missing reads as absent, which is the direction a permission should fail in.
One arm is worth separating out because it costs nothing and proves nothing. Renaming the plan-year field left the settlement exactly correct -- three credited years, 40%, $4,960.00 kept, $7,440.00 forfeited -- and printed credited=3y [undefined/undefined/undefined]. The money is right and the record of which years earned it is gone. A participant disputing a forfeiture disputes a year, and that is the one thing the statement can no longer tell them.
The same shape appears in the identity fields: renaming the participant identifier or the last four digits blanks them and leaves every dollar correct. These are not dangerous arms. They are the ones that make the dangerous arms hard to find, because a reviewer who greps the output for undefined finds these and not the 60%.
What this means if you protect a benefits or recordkeeping front end
The exposure is narrow and nameable. Renaming inside your own module is not the problem. The problem is a rename pattern wide enough to reach the keys of an options object you hand to an installed library, or the fields of a record that library reads.
Three things follow. First, keep member renaming off the boundary: use RenameMembers with a MemberRegexp that matches your internal names and excludes every option key and record field a dependency reads. Second, rank the names by what they decide rather than by what they are called -- the hours requirement and the service method decide the answer, and neither of them contains a dollar sign or a percentage. Third, assert the outcome: a smoke test that settles one known termination and asserts the forfeited amount to the cent fails in every arm in this article, and a test that checks the schedule is configured passes in most of them.
None of this argues against protecting a recordkeeping front end, and the same reasoning underpins the control narrative in SOX and GLBA controls for JavaScript. The protected build settled both participants correctly on all five presets. It argues for knowing which names in the file are shared with somebody who is not rebuilt with you.
Frequently asked questions
Did obfuscation change any vested percentage 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 vesting engine also reads.
What exactly happened when the hours requirement was renamed?
The engine reverted to its default of zero hours, so a plan year the participant worked only 820 hours counted as a full year of service. Credited service went from three years to four, vesting from 40% to 60%, and $2,480.00 of employer match was paid out instead of forfeited back to the plan.
Would the settlement statement look wrong?
No. A forfeiture is still posted, the match balance is right, 60% is a real step on the plan's schedule, and every credited plan year named on the statement is a year the participant genuinely worked. The 820-hour fact that disqualified one of them is not printed anywhere.
Did the plan's own service check catch it?
No, and the reason generalises. The check refuses a percentage that was not derived from a credited-service count. A count was derived -- it was simply derived under a rule that had reverted. A guard defined over whether a value exists is blind to a change in the rule that produced it.
Which names failed loudly?
The schedule's sub-keys. Renaming the service requirement or the percentage inside each step left the schedule visibly present in the configuration dump and matching nothing, so every participant dropped to 0% -- including one with seven years of service who forfeited an entire $31,875.00 match.
Is there an arm that changed nothing but the paperwork?
Yes, and it is worth knowing about. Renaming the plan-year field left the settlement exactly correct and printed the credited years as undefined. The money is right and the record of which years earned it is gone, which is precisely what a participant disputing a forfeiture needs.
What is the cheapest test that would have caught all of this?
Settle one known termination end to end and assert the forfeited amount to the cent, plus the list of credited plan years. That fails in every arm in this article. A test that checks the vesting schedule is configured passes in most of them.
Related reading