Measured Behaviour
Published
A length is not a length until you say where you measured from. A cod measured to the fork of its tail is about eight per cent shorter than the same cod measured to the tip, and a minimum size is written in one of those two and not the other. Which one is a string.
Everything correct except one word
We built one market morning: twelve boxes off two vessels, four species, 795 fish. Published minimum sizes to the millimetre - cod 300mm, haddock 270mm, plaice 240mm, whiting 230mm - written, as this authority writes them, in fork length. Some crews measure to the fork and some to the tip, so each lot records which. The conversion between the two is a published constant of 1.08 and the code applies it correctly, in the right direction, in every arm we measured.
The correct run finds five undersize lots of twelve: 519kg of immature fish, over the authority's zero-tolerance allowance, and the market check fails. Projected over a season of 9,400 lots that is 406,550kg.
Protection alone changed nothing on five profiles, measured twice. Then we renamed the one string that says which datum the table is written in - which is what a broad RenameMembers pattern over a configuration literal reaches, and the mechanics are in Protect Members - and the library substituted its own default, which is total length.
Undersize lots went from five to zero. Undersize kilograms went from 519 to zero. The season projection went from 406,550kg to nothing. The market check passed. And the published limits still printed exactly as before: COD:300mm HAD:270mm PLE:240mm WHG:230mm. The only thing anywhere in the output that moved, apart from the verdicts, is the word fork-length becoming total-length.
Why every fish passed
Take the three fork-measured lots that were undersize. A cod at 282mm to the fork is undersize against a 300mm fork limit. Read the limit as a total length and the code converts the measurement onto that datum - 282 times 1.08 is 305mm - and it clears. Haddock at 258mm becomes 279mm against 270. Plaice at 229mm becomes 247mm against 240.
The two tip-measured lots that were undersize go the other way and land in the same place. A cod at 310mm to the tip is 287mm to the fork and undersize; read as a total-length limit it is compared directly against 300 and clears outright. Plaice at 258mm total is 239mm to the fork, undersize by a single millimetre, and clears at 258 against 240.
Nothing is out of range. Nothing is NaN. No counter reads zero because a value went missing - the counters read zero because the answer really was zero for the comparison that was actually performed. Every measurement in the output is a real measurement of a real fish, taken correctly, converted correctly, and compared against a published limit that is correct to the millimetre.
The negative control makes the testing consequence exact. We ran the same twelve lots with every fish comfortably over its limit on either datum - the fixture almost everybody writes, because a legal length is the one you can type without opening the regulation. That control is byte-identical on every counter to the arm that loses the datum.
The other arms, and one that is quieter than it looks
Renaming the tolerance took undersize lots from five to four. This authority forgives nothing; the vendor forgives a centimetre, and the plaice lot that was undersize by one millimetre disappeared into it. The check still failed, so this one is survivable - but note that it removed exactly the marginal case, which is the case that decides whether a measurement regime is worth having.
Renaming the allowance did nothing to the verdicts here, because five of twelve is over a five per cent allowance as well as over a zero per cent one. Renaming the minimum size table emptied it: no species has a limit, no lot is undersize, and the configuration screen says so plainly.
The landing obligation is the interesting one. It decides whether an undersize fish comes over the quay and enters a record, or goes back over the side. Renaming it took landed weight from 1,233kg to 714kg - the 519kg of undersize fish simply stop being recorded - while the undersize count stayed at five and the check still failed. The fish are just as dead in both arms. In one of them there is a number.
And renaming the quota-treatment flag took the against-quota figure from 519kg to zero while changing nothing else at all: the undersize fish are still found, still counted, still rejected, and no longer drawn against anything.
The loud arm, as in every area we measure, is the value arm. Renaming the measured-length field left nothing measurable and the engine refused before grading anything: no measurable lot(12 of 12) - refused before grading anything. Losing a number fails closed. Losing the word that says what the number means fails open.
The option and the record field that share a name
The configuration carries a key saying which datum the table is written in. Each lot carries a field saying which datum that lot was measured in. The two names differ by one letter, they are read within three lines of each other, and they do not fail the same way.
Renaming the configuration key gives the library's default of total length, so every fork measurement is converted up and all five undersize lots pass. Renaming the record field leaves the engine unable to tell which datum a lot used - so neither conversion branch matches and every measurement is compared as-is. That happens to be correct for the eight fork-measured lots and too generous for the four tip-measured ones, and it lands at three undersize instead of five or zero.
So the two sides of what looks like one concept produce three different answers - five, three and zero - and only the middle one is arrived at by a route that is right about most of the data. A table and its switch are byte-equivalent only when nothing downstream operates on the result; here a conversion does, and the halves separate.
Two more record fields are worth naming. Renaming the species field means no lot matches any row of the size table, so nothing is undersize and the check passes - while the four published limits still print correctly on the configuration line. And renaming the lot-weight field takes landed weight and undersize weight both to zero while the undersize count stays at five and the check still fails: the verdict survives, the quantities do not, and a report built on the kilograms would show a clean morning.
What to do about it
Scope the member pattern away from configuration keys and record field names, which removes every arm above.
Then apply the rule this failure keeps teaching in domain after domain: never let a unit or a datum live in a different key from the quantity it describes. Carry the measurement and its datum together as one value, or normalise at the boundary where the measurement enters the system, so there is no separate string left to lose. A hand-edited config, a dropped key over a serialisation hop and a library upgrade all produce the same substitution.
Then assert the property rather than the verdict. A grading run over a real market morning that finds zero undersize lots is not, on its own, good news, and the check that says so cannot tell you otherwise. What does separate the runs is the distribution: if you know roughly what fraction of a real landing is marginal, an undersize count of zero is an alarm.
Regulated measurement software carries obligations that sit outside the bundle entirely; medical device JavaScript compliance works through the same separation in a domain where the measurement rules are written down even more strictly.
Frequently asked questions
Does obfuscation break minimum landing sizes?
Not on its own. Five protection profiles produced identical output on the same fixture, twice. What broke it was member renaming reaching the string that names which datum the size table is written in.
What does losing the datum actually do?
The library substitutes its own default. Our table is written in fork lengths and the default is total length, so every fork measurement is converted up by eight per cent before comparison. Five undersize lots became zero, 519kg of immature fish became none, and the season projection fell from 406,550kg to nothing.
Was anything in the output wrong or out of range?
No. Every measurement is a real fish measured correctly, the conversion constant is right and applied in the right direction, and the published limits still print exactly as before. The only text that changed is the word naming the datum.
Which arms failed loudly?
Renaming the measured-length field left nothing measurable and the engine refused before grading anything. Renaming the size table emptied it visibly. Losing a number fails closed; losing the word that says what the number means fails open.
What did the landing obligation change?
Whether an undersize fish enters a record. Renaming it took landed weight from 1,233kg to 714kg because the undersize fish go back over the side instead of over the quay. The undersize count stayed at five and the check still failed - the fish are equally dead in both arms, but only one has a number.
Would our test suite catch this?
Only if your fixtures contain fish near the limit on both datums. Twelve lots of comfortably adult fish produce output byte-identical on every counter to the arm that loses the datum.
How do we protect grading software safely?
Scope the member pattern away from configuration and record names, carry a measurement and its datum as one value rather than two keys, and treat an undersize count of zero over a real landing as an alarm rather than a pass. Protect Members documents how the regexp is applied and what a safe scope looks like.
Related reading