Measured Behaviour

Does obfuscation break catch quota accounting?

A catch accounting system has obvious failures everybody tests for: a landing that will not save, a weight that will not parse, a balance that goes missing. It has one almost nobody tests for, because it is not a property of any landing - the ratio between the weight that went over the scale and the weight the quota is written in. We measured what a rename does to it.

A quantity that no record carries

We built one vessel's season the way one actually looks. Ten landings over two trips, three species, four presentations: whole, gutted, headed-and-gutted, filleted. A published quota in live weight - 42,000kg of North Sea cod, 18,000kg of haddock, 9,000kg of plaice - and a published conversion deck saying what each presentation stands for: whole 1.00, gutted 1.17, headed-and-gutted 1.44, filleted 2.65.

The correct run draws 58,874kg of live weight from 34,300kg of counted landings, exhausts the cod quota inside the season at 40,032kg of 42,000, and refuses the last two cod landings outright. Ten landings presented, eight counted, two refused.

Here is the part that decides everything else. There is no record anywhere in that system carrying the number that matters. Every landing records a species, a presentation and a weight, and every one of those three is true. The extra 24,574kg of live fish is not written on any landing; it is created by multiplying two things that are each individually correct. A per-landing audit that asks was this weight recorded correctly? answers yes, ten times out of ten, in every arm below.

We then protected the file with default settings on five profiles: the ES5 target, the modern target, both emit-gate configurations and the string-encoding profile. All five produced identical behaviour, and we re-ran the whole base column a second time to be sure. That is the first result and it deserves stating plainly - protection on its own did not change a single kilogram.

The failures start when member renaming reaches the names on the quota configuration object, which is what a broad RenameMembers pattern over a configuration literal actually does. If you have not yet decided how far your member pattern reaches, the mechanics are in Protect Members.

What losing the conversion deck actually did

A conversion factor is not a unit and it is not a policy. It is the ratio between the thing that was measured and the thing the limit is written in, and the library's fallback for a presentation it cannot find is 1.00.

That fallback is the whole problem, because 1.00 is a correct factor. It is the right answer for a whole fish. Nothing about it looks like an error, nothing about it is out of range, and nothing about it produces a value a validator would question.

Renaming the deck took the live weight from 58,874kg to 41,700kg - which is exactly the landed weight, to the kilogram, because every presentation now converts at one. The conversion uplift went from 24,574kg to 0kg. Counted landings went from eight to ten and refusals from two to zero, because the cod balance now reads 29,800kg against a 42,000kg quota: seventy-one per cent used, comfortably inside, keep fishing.

The vessel's own projection line moved in the direction that reads as good news. A 42,000kg quota counted correctly is spent after 0.71 trips like this one. Counted at 1.00 it lasts 1.01 trips - the same quota appearing to stretch about forty per cent further.

And the operator's own rule passed. It asserts that no stock was drawn beyond its quota, and no stock was. It printed 10 landing(s), 10 counted, 0 refused, 41700kg live drawn, no stock beyond its quota, and every figure in that sentence is true.

The result that decides how you should test this

We built a negative control alongside the real catch: the same ten landings, the same weights, the same vessel, the same days - with every one of them landed whole. It is the fixture almost everybody writes, because whole is the simplest row to type and a presentation code is a detail you add later.

That control run and the run whose conversion deck had been renamed are byte-identical on every counter. Ten landings, ten counted, none refused, 41,700kg landed, 41,700kg live, zero uplift, and the same three stock balances to the kilogram. The only difference anywhere in the output is the detail line naming each landing's presentation, which nobody reads.

So a test suite built on whole-fish fixtures cannot see this defect. Not because the assertions are weak, but because the correct answer for that data and the wrong answer for real data are the same number. If your fixtures do not contain a presentation whose factor is not 1.00, there is no assertion you can add that will help.

The rest of the matrix, and which arms are loud

We measured every option key and every record field on this object, plus the fields the port authority's weigh-note file writes and this bundle only reads. Not every arm is quiet, and the loud ones are worth naming because they are the ones that will not hurt you.

Losing the quota table itself is loud in the only place that matters - the configuration screen. It prints no quota table(no stock is limited), which is an obviously wrong state that a smoke test or a human glance will catch. Losing the species-to-stock map is similar: the balances come back keyed COD, HAD and PLE instead of COD-NS, HAD-NS and PLE-NS, and no row in the quota table matches, so nothing is limited. Both are bad, and both announce themselves.

The overage tolerance is quieter and more specific. This authority permits no overage; the vendor default permits ten per cent. Renaming it raised the cod ceiling to 46,200kg and let a balance of 42,032kg through - past the published quota, with the published quota still displayed correctly beside it.

The balance scope is the one that reads most innocuous. A quota belongs to a vessel for a season; the vendor's default starts the balance again every trip. Renaming that key does not weaken the limit, it removes it - the balances come back split per trip and nothing is ever exhausted, while the quota table on screen is still exactly right.

Two arms went the other way and refused loudly. Renaming the landed-weight field left the engine unable to read a single landing and it stopped before drawing down anything: no countable landing(10 of 10) - refused before drawing down any quota. Same for the species field. That is the pattern we have now measured in over thirty domains: losing a value fails closed and loudly, losing what a value means fails open and silently.

What to do about it

The first move is scope. A member-renaming pattern that reaches a configuration literal or a record field name is doing something you almost certainly did not intend. Rename your own internal function and variable names as widely as you like; exclude the objects whose keys are read by an installed library or written into a record.

The second move costs nothing and is worth having regardless of obfuscation, because a hand-edited config file and a dropped key over a serialisation hop produce the same substitution: never let a conversion factor default to a valid value. A missing factor should be an error, not a 1.00. The whole failure here rests on the library treating I do not know what this presentation converts at as it converts at one.

The third is a reconciliation that a per-record audit cannot give you. Every landing in the defective run is individually correct, so no amount of checking landings will find it. What does find it is checking the aggregate against something that was not computed the same way - a landed-weight total against a live-weight total, with the ratio between them asserted to be greater than one whenever the catch contains anything other than whole fish.

If you are shipping this kind of software as a Node service or a desktop bundle, the protection question and the correctness question are separable, and they should be answered separately. Protect Node.js source code covers the packaging side.

Frequently asked questions

Does obfuscation break catch quota accounting?

Not on its own. We protected the same catch accounting file on five profiles - ES5, modern, both emit-gate configurations and string encoding - and all five produced identical output, twice. What broke it was member renaming reaching the names on the quota configuration object.

What exactly went wrong when the conversion deck was renamed?

The library substituted its own fallback of 1.00 for every presentation. Live weight fell from 58,874kg to 41,700kg, the conversion uplift went from 24,574kg to zero, and two landings that should have been refused were accepted - all while every landing was recorded truthfully and every balance was exact.

Why is a factor of 1.00 harder to spot than a missing value?

Because 1.00 is a correct factor for whole fish. A missing weight gives you NaN and an engine that refuses to proceed. A missing conversion factor gives you a plausible number in the right unit that stands for a different quantity of fish, and nothing in the output is out of range.

Did the system's own compliance check catch it?

No, and it could not have. The check asserts that no stock was drawn beyond its quota. In the defective run no stock was drawn beyond its quota, so it passed - printing a sentence in which every figure was true.

Which arms failed loudly instead?

Renaming the landed-weight field or the species field left nothing countable and the engine refused before drawing down any quota. Losing the quota table or the species map printed an obviously empty configuration. Losing a value is loud; losing what a value means is not.

Would our test suite catch this?

Only if your fixtures contain a presentation whose factor is not 1.00. We ran the same ten landings with everything landed whole, and that control is byte-identical on every counter to the arm that loses the conversion deck. Whole-fish fixtures cannot see this.

How do we protect fisheries software safely?

Scope the member pattern so it cannot reach configuration keys or record field names, make a missing conversion factor an error rather than a 1.00, and reconcile landed weight against live weight rather than auditing landings one at a time. Protect Members documents how the regexp is applied and what a safe scope looks like.

Related reading