Measured Behaviour
Published
A rate deck is the artefact an auditor asks to see, and in every arm we measured it was complete, correct and exactly as published. That is the problem. A classification failure does not corrupt the deck; it moves which row of it a real vehicle reaches, and every toll charged remains a genuine published rate for a class that genuinely exists.
Eight vehicles, one gantry, one perfect rate deck
We built one morning at one gantry the way one actually looks. Eight passages: a car, a car towing a caravan, a van with no tag, a three-axle rigid over twelve tonnes, an articulated lorry at thirty-eight tonnes, a coach, a car with a single-axle box trailer, and one passage the roadside axle sensor could not classify at all.
The tariff behind them is ordinary: axle count maps to a class, each class has a published rate from $3.25 to $19.50, a vehicle over twelve tonnes gross moves up one class, towed axles count, and a passage with no transponder carries a $2.50 video surcharge because it costs more to collect. A passage the sensor cannot classify is billed at the HIGHEST class - which is the only posture that does not pay a driver to obscure their axles.
The correct run collects $77.75 across the eight, an average of $9.72 a passage. At eighteen thousand passages a day that lane is worth about $63.9m a year.
Protection alone changed nothing. Five profiles - the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile - all produced behaviour identical to the unprotected run. The failures start when member renaming reaches the names on the classification object, which is what a broad RenameMembers pattern over a configuration literal does; the mechanics are in Protect Members.
The quiet arm: every rate published, three vehicles undercharged
countTrailerAxles is one boolean saying that a towed axle is on the road, carries load, wears the pavement and is billed. The vendor default is FALSE. Lose the key and three of the eight vehicles change class: the car with a caravan drops from CLASS-4 to CLASS-2, the articulated lorry from CLASS-6 to CLASS-4, and the car with a box trailer from CLASS-3 to CLASS-2.
Collection falls from $77.75 to $58.25 - from $9.72 a passage to $7.28, or roughly $47.8m a year on that lane instead of $63.9m. A sixteen million dollar annual hole in a facility whose configuration screen is entirely correct.
Because it is entirely correct. The rate deck in the broken run reads CLASS-2:$3.25 CLASS-3:$6.50 CLASS-4:$9.75 CLASS-5:$14.25 CLASS-6:$19.50, identical to the correct run, complete and exactly as published. The class table reads 2ax:CLASS-2 3ax:CLASS-3 and so on, also identical. Every toll charged is a real published rate for a class that really exists. Every receipt is defensible. A spot check on the tariff passes, because the tariff was never what moved.
The authority's own rule passed too, printing 8 passage(s), 8 billed at published rates, 1 unclassified, $58.25 collected. Every figure there is true. The invariant it asserts - that every passage was billed at a rate appearing in the published deck - is a statement about the deck, and the deck was fine.
weightClassKg is the same shape one line away: lose the twelve-tonne upgrade threshold and the rigid and the coach each drop a class, $77.75 becomes $71.25, and again nothing in the deck moves.
The disposition of the unknown, and why it is self-targeting
One passage in the eight is the one the option unclassifiedClass exists for: the loop reported nothing, the camera got the plate. Our operator sends it to CLASS-5, the highest, and with the video surcharge it costs $16.75. The vendor default sends it to CLASS-2, the lowest.
Lose the key and that passage costs $5.75. Collection falls to $66.75, the mix on the report still looks like a plausible morning, and the unclassified counter still reads 1 - so the system is correctly reporting that it could not identify the vehicle, while quietly giving it the cheapest rate on the deck.
Read who receives that discount. Not a random vehicle: the discount goes precisely and only to the vehicle whose axles could not be counted. A driver with an obscured or defeated axle signature is exactly the population this option exists to price, and a lost name hands them the car rate. The error is not distributed across your traffic; it is aimed at the part of it you were most careful about.
Two sides of one decision that are NOT the same
Elsewhere in this pass, losing a table and losing the flag that says whether to apply it produced byte-identical output four separate times. Here it did not, and the difference is worth understanding.
Losing classTable sends every passage to the disposition of the unknown: eight unclassified, all CLASS-5, $119.00 collected. Losing honourAxleSensor does the same thing to the classification - but the weight upgrade still runs on top of the result, so three heavy vehicles move from CLASS-5 to CLASS-6 and the total is $134.75 instead.
So a table and its switch are byte-equivalent only when nothing downstream operates on the result. Add one transformation after the lookup and the two halves separate by $15.75 on eight vehicles. You cannot generalise from one area to the next; you have to run both halves in the file you actually ship.
Both of those arms are loud in the customer-facing direction - cars billed as lorries generate disputes the same week. The quiet arms are the ones above, where heavy vehicles are billed as cars and nobody complains at all.
The negative control is a gantry that only sees cars
We ran the same eight passages with every vehicle replaced by a two-axle car with a tag and nothing towed. Collection: $26.00, eight CLASS-2 passages, and every arm in this article that touches trailer axles, the weight upgrade or the disposition of the unknown is completely inert.
That is the fixture most teams have, because constructed traffic is cars. It exercises the lookup, it exercises the deck, it proves the arithmetic, and it is blind by construction to every finding above.
The mirror is one field away and it is loud. Renaming passedMinute, or removing the rate deck outright, produced no priceable passage(8 of 8) -- refused before tolling. Losing the value fails closed. Losing what the values MEAN - which class a real vehicle belongs to - fails open, silently, against your heaviest traffic.
What we would actually do about it
Reconcile the class MIX, not the rate deck. The deck is correct in every arm above; the mix is what moved. A gantry whose CLASS-6 count halves overnight has told you something the revenue total takes a quarter to say, and the mix is already in your reports.
Assert the classification against a second observation. An axle count from a loop and a vehicle silhouette from a camera disagreeing is a real, ordinary event worth alarming on. It also happens to catch every arm in this article.
Make an unreadable class table an error rather than a route to the disposition of the unknown. Sending every passage on the road to the unclassified branch is not a classification failure the operator can see; it is a classification result the system reports with a straight face.
And decide the disposition of the unknown deliberately, then test that the value you chose is the value in force. It is the one option in this file whose loss is aimed squarely at the traffic you were trying to catch.
Roadside and in-vehicle software carries the same configuration objects to places you cannot patch quickly - ISO 21434, Type Approval and Your Vehicle Interface JavaScript covers what that means for the build you ship into a vehicle or a gantry controller.
Frequently asked questions
Does obfuscation change how vehicles are classified?
Not on its own. Five profiles - the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile - all produced behaviour identical to the unprotected run: eight passages, $77.75 collected, the same class for every vehicle.
What did break it?
Member renaming reaching the classification configuration object. The library reads its options by name; a name it cannot find is a key you never supplied, so the library substitutes its own default. The vendor default for 'do towed axles count' is FALSE.
Our rate deck is audited every quarter. Does that not cover it?
No, and this is the trap. The rate deck is complete, correct and identical in every arm we measured except the one that removes it outright, which fails loudly and immediately. What moved is which row of a correct deck a real vehicle reaches. A populated, reconciled deck is evidence that the rates exist, never that any vehicle found the right one.
How much money is actually at stake?
On our eight-passage sample, losing the towed-axle rule took the average from $9.72 to $7.28 a passage. At eighteen thousand passages a day that is roughly $47.8m a year against $63.9m - about a sixteen million dollar gap on one lane, with no error visible on any receipt.
Which vehicle gets the discount when the sensor fails?
Whichever one your unclassified-class option names, and the vendor default names the cheapest. That means the discount is aimed precisely at the vehicles nobody could identify - the exact population that option exists to price. Losing the key took our unclassifiable passage from $16.75 to $5.75.
Would our tests catch this?
Not if your fixture is cars. We ran the same eight passages as two-axle cars with tags: every arm touching towed axles, the weight upgrade or the unknown disposition went completely inert. Constructed traffic is cars, and cars are blind to this whole class.
How do we protect tolling software safely?
Scope the member pattern so it cannot reach configuration keys or record field names, and reconcile the class mix rather than the rate deck. Protect Members documents how the regexp is applied and what a safe scope looks like.
Related reading