Measured, not asserted

Does obfuscation break laytime and demurrage calculation?

Laytime is a subtraction, and the interesting thing about it is that the operands are chosen by a word rather than by a number. A basis decides which of the hours on a statement of facts are laytime at all. This test renamed that word and watched one port call go from $1,500 of demurrage to $18,000 while a second went from $7,500 of despatch earned to $4,500 -- opposite directions from a single rename, with every recorded hour still correct, the configuration screen still naming the charter party's basis, and the owner's own arithmetic check still passing on both.

Laytime is a subtraction whose operands are chosen by a word

Every demurrage desk does the same arithmetic. Take the moment a valid notice of readiness has run, take the moment cargo operations completed, subtract to get the elapsed hours. Then subtract again: the excepted periods, the hours the charter party says do not count. Compare what is left against the allowance. If the allowance is exceeded the vessel is on demurrage at a daily rate; if it is beaten the charterer earns despatch, usually at half the rate.

Only two of those inputs are plain numbers. The allowance is a number and the rates are numbers. Everything else is a decision: whether the notice of readiness had to run for six hours before laytime could start, and above all which BASIS the charter is on. On running hours every hour after the notice counts. On weather working days the rain, the port holidays and the time spent shifting berth come out first.

The fixture used here carries exactly that: a weather-working-days basis, a table of excepted periods, a six-hour notice validity, a seventy-two hour allowance, an $18,000 per day demurrage rate and a $9,000 despatch rate. Two port calls sit on opposite sides of the allowance so that anything which moves the boundary moves them in opposite directions. A slow discharge at Rotterdam runs 96 elapsed hours with 22 of them excepted, which is 74 counted against 72 allowed -- two hours on demurrage, $1,500. A fast load at Singapore runs 60 elapsed hours with 8 excepted, 52 counted, twenty hours inside the allowance -- $7,500 of despatch earned.

The engine that does the arithmetic is copied in unprotected, as an installed dependency would be. It has no view about which hours ought to count. It is told, by basis and by exception table, and it does what it is told.

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. That includes the guard which runs each original twice and refuses to compare anything against a fixture that does not reproduce itself first.

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 demurrage desk's own code and the installed calculation engine read. That is the failure mode this series exists to map, and it is a configuration question rather than a verdict on protection.

One word, and the same voyage moved in two directions

Renaming the basis reverted it to the library's default of running hours. Nothing else in the file was touched. Rotterdam went from $1,500 to ON DEMURRAGE $18,000.00, because the 22 excepted hours stopped being subtracted and 96 counted hours ran 24 hours past the allowance. Singapore went from $7,500 to DESPATCH EARNED $4,500.00, because the same lost subtraction pushed its counted hours from 52 to 60 and halved the time it had beaten the allowance by.

One rename, one line of configuration, two customers moved in opposite directions. That is the property which makes a spot check useless here: whichever account somebody happens to look at, the error has a plausible sign. Rotterdam is more expensive and Singapore is cheaper, and neither looks like a fault. They look like a slow port and a fast one.

The exception TABLE produced the same money by a different route, and it is the quieter of the two. With excludedPeriods renamed, the engine reverted to an empty exception set -- and the configuration line still printed basis=weather-working-days. The charter party's basis is displayed, correctly, as being in force. Zero hours are being excepted under it. A demurrage clerk checking that the right basis is configured gets a true answer to the wrong question.

The reconciliation is closed by construction, so it passed every time

The owner's rule of record in this fixture is a real check, not a decoration. It refuses a statement of facts whose elapsed hours do not fit inside the span of the call, taken from the port log rather than from the hours being billed. It is the kind of assertion a careful operator writes.

It printed sof-check(96h elapsed inside a 102h call, 74h counted) in the correct run and it printed a true sentence of the same shape in every arm measured, including both arms above. It could not be otherwise. The check is defined over the SPAN, and the span never moved; what moved was which of the hours inside that span were treated as laytime. A reconciliation defined over a total cannot see a reclassification within it.

That is the same shape a previous pass found in cost allocation, where an allocated total equalled the billed total on every basis including bases nobody chose. It is worth stating as a design rule rather than an obfuscation one: an invariant your own code closes by construction is a property of the code, not evidence about the answer. To check laytime you have to check the counted hours against the port log line by line, not the elapsed hours against the call.

What failed loudly, and why the loud half is the reassuring half

Every plain number in the area failed closed and said so. Renaming the notice-of-readiness timestamp or the completion timestamp produced non-finite-port-call(refused before settlement) on both calls. Renaming the commencement hour produced non-finite-start(refused before settlement). Nothing is billed, somebody notices within the day.

The allowance failed loudly in the other direction: reverted to the library's default of zero free hours, Rotterdam settled at $55,500.00 and Singapore's despatch became ON DEMURRAGE $39,000.00. Grotesque, and grotesque is safe -- the charterer disputes it the same week.

The two rates are the interesting pair, because they fail silently in opposite commercial directions and each one only touches one party. With the demurrage rate renamed to the library's default of zero, Rotterdam settled at $0.00 while Singapore's despatch was untouched. With the despatch rate renamed, Singapore settled at $0.00 and Rotterdam was untouched. An owner reviewing demurrage revenue sees nothing wrong with the despatch ledger and vice versa, because each error is invisible from the other desk.

The notice validity sat in between: reverted to zero hours, laytime started four hours earlier, Rotterdam went to $4,500.00 and Singapore to $6,000.00. Both wrong, both entirely plausible.

A name your own code owns on both sides keeps working

The excepted hours themselves are worth a paragraph because they measured SAFE, and the reason generalises. The rain, holiday and shifting hours are written on the port-call record by this file and read back through keys that live in this file's own exception table. Renaming moved the write and the read together, so the money did not change at all -- $1,500 and $7,500, exactly as before.

What did change is the printout. The engine, which is not protected, displayed the excepted periods as _0x1=14h _0x2=6h _0x3=2h. The arithmetic was right and the audit line was unreadable. That is the honest description of a field that never crosses a boundary: robust against renaming, and worth nothing as evidence to anybody outside the bundle.

The moment one of those keys is read by anything else -- a laytime statement exported to the charterer, a claim submitted to a P&I club, a spreadsheet a broker maintains -- the same rename stops being harmless, because only one side of the comparison moves.

What this means if you protect a chartering or demurrage front end

The exposure is narrow and it is 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.

In practice that means three things. 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, treat the basis, the exception table and the rates as the highest-value names in the file, not the timestamps -- the timestamps fail loudly and the basis does not. Third, assert the outcome rather than the configuration: a smoke test that settles one known port call and compares the dollar figure catches every arm in this article, and a test that checks the basis is configured catches none of them.

None of this is an argument against protecting a chartering front end, and the same reasoning underpins the control narrative in SOX, GLBA and your financial application JavaScript. The protected build settled both port calls correctly on all five presets. It is an argument for knowing which names in the file are shared with somebody who is not being rebuilt with you.

Frequently asked questions

Did obfuscation change any demurrage figure 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 calculation engine also reads.

What exactly happened when the laytime basis was renamed?

The engine reverted to its own default of running hours, so the excepted periods stopped being subtracted. One port call went from $1,500 of demurrage to $18,000 and another went from $7,500 of despatch earned to $4,500. One rename, two accounts, opposite directions.

Would the configuration screen show that something was wrong?

Not in the arm that renamed the exception table. The configuration line still printed the charter party's basis as weather working days while zero hours were being excepted under it. The basis was displayed correctly and applied to nothing.

Why did the owner's own reconciliation not catch it?

Because it is defined over the span of the port call, and the span never moved. What moved was which hours inside that span counted as laytime. It printed a true sentence in every arm measured. A reconciliation your own code closes by construction is an invariant of the code, not evidence about the result.

Which names failed safely?

The plain numbers. Renaming the notice timestamp or the completion timestamp refused the settlement outright with a non-finite port call. Renaming the allowance produced a $55,500 invoice on a call that owed $1,500. Those are caught in a day. The basis, the exception table and the two rates are the quiet ones.

Do the demurrage and despatch rates fail the same way?

They fail identically in mechanism and invisibly to each other. Renaming the demurrage rate zeroed the demurrage invoice and left despatch untouched; renaming the despatch rate did the reverse. Each error is invisible from the other desk, which is why reviewing one ledger proves nothing about the other.

What is the cheapest test that would have caught all of this?

Settle one known port call end to end and assert the dollar figure, not the configuration. A test that checks the basis is set passes in every arm in this article. A test that checks Rotterdam owes $1,500 fails in six of them.

Related reading