The problem with fixing the data
Instruments lie consistently. A masthead unit that goes back on 3° out after the rig comes out stays 3° out all season. A paddlewheel reads two percent slow. A heel sensor mounted the wrong way up reads the mirror image of the truth. These are not random errors — they are fixed, systematic, and almost always discovered after the sailing.
The obvious fix is to correct the recorded numbers: go through the season’s telemetry and rewrite every affected reading. It is also the wrong fix, for a reason that has nothing to do with how hard it is:
That last point matters most, because calibrating a boat is an iterative workflow. You try three degrees, look at the tacks, try three and a half. Every one of those steps has to be reversible, and none of them can be allowed to build on the one before.
What a lens is instead
So Nordic Stars stores the correction and not its effect. A calibration entry names a channel, a correction, and the instant it takes effect from. It changes nothing on disk. Every time the platform reads that channel, it applies the correction on the way out.
That sentence is the whole design, and it is worth reading twice for what it makes cheap. Delete an entry: every chart, every tile, every percentage of target returns to exactly the numbers it served before the entry existed — not approximately, not after a rebuild, but immediately and exactly, because the numbers underneath never moved. It is what makes it safe to try one.
It also means an entry is deletable, which is unusual in this platform — most history here is append-only. A calibration entry earns the exception because it does not record something that happened. It changes what every later read means, and a person must be able to take that back.
1 The correction
An entry is linear: a scale and an offset. Nothing curved, nothing conditional, nothing learned.
Read as a sentence: stretch the reading by the scale, then shift it by the offset — in that order.
The order matters and is fixed: scale first. A paddlewheel two percent slow is scale 1.02. A sensor wired backwards is scale −1, which is legal on purpose — an inverted sensor is a real boat, not a typo. A scale of zero is refused: it would map every reading to the same number, which is an erasure rather than a correction, and the platform says so rather than accepting it.
Angles are different, and the form says so
An angular channel — wind direction, heading, a masthead bearing — is corrected by rotation, and only by rotation:
Read as a sentence: turn the reading by the offset, and let it wrap. 359° plus three degrees reads 2°, not 362°.
There is no scale on an angular channel. Angles rotate; they do not scale — a scaled angle is not an angle, it is a number that happens to have a degree sign after it. So the platform refuses a scale other than exactly 1 on an angular channel, the form hides the field rather than greying it out, and the correction routine ignores a scale even if one somehow reached it. Three layers saying the same thing, because the cheapest wrong answer here looks entirely plausible on a chart.
Whether a channel is angular is not a guess and not a name-match: it is a property of the channel itself in the platform’s channel registry — the same one the import mapper, the chart explorer and the heads-up display all read. One answer, everywhere.
2 Why it costs nothing
Here is the objection that should occur to you. A season of one-per-second telemetry is tens of millions of readings. The debrief player does not read them one at a time — it reads precomputed summaries: for each second and each minute, how many readings there were, what they summed to, the smallest, the largest. If a correction is applied at read time, and the reads come from summaries computed before the correction existed, surely every summary has to be rebuilt?
No — and the reason is one line of algebra.
Averages ride the stored sums
An average is a sum divided by a count. Correct every reading first, then add them up:
Read as a sentence: correcting a thousand readings and adding them up gives the same answer as adding them up once and correcting the total.
Which means the platform never has to touch the readings. It takes the sum it already stored, scales it, and adds the offset once for each reading counted — and the count itself is untouched, because a correction moves values; it never adds or drops a measurement. Divide, and you have the corrected average. The stored summary is unchanged, and the number served is exact.
Smallest and largest map through — and swap
The smallest and largest readings in a summary are corrected by correcting them directly. With one trap:
Read as a sentence: a negative scale turns the number line around, so the smallest reading becomes the largest.
Multiplying by a negative number reverses order. The lowest heel angle of a minute, run through scale −1, is the highest corrected value in that minute. Miss the swap and every summary quietly reports a range that runs backwards — the classic bug in this piece of code, which is why the platform’s tests check not only that the answer is right but that it is not the un-swapped one.
Smallest and largest are internal machinery here: the debrief serves averages, so this rule is what keeps a summary accurate underneath rather than something you will see on a chart.
Angles: rotate the arrow, not the numbers
Angles cannot be summed — that is the subject of circular statistics, and the short version is that a summary of an angular channel stores two sums rather than one: the total of the sines and the total of the cosines. Together those are a single arrow, whose direction is the average heading.
Rotating every reading by θ and re-summing turns out to be the same as rotating that one arrow by θ:
Read as a sentence: turn the arrow by the offset; its length does not change.
Write the stored pair as an arrow of length R pointing at μ. The two lines above are exactly the angle-addition formulas, so the result is an arrow of length R pointing at μ + θ. The mean heading moved by the offset; the strength of the average — how tightly the readings agreed — is untouched, which is right, because a rotation does not make a wandering wind steadier.
3 Proved, not asserted
Everything above is a theorem, so the platform’s tests do not check it against numbers somebody typed in. They compute it both ways and compare:
The tests carry their own independent implementation of the summarising, so the lens is never checked against itself; the angular case is checked a third way as well (average first, add the offset after, wrap). The readings chosen for the angular test straddle 0° on purpose — 350, 355, 2, 10, 359 — because that is where a careless implementation answers 180° and is spectacularly wrong. The routes agree to within a billionth.
And the same comparison is repeated end to end against a real database: seed readings through the ordinary ingest path, apply a correction, read the summaries back, and check them against the arithmetic done from first principles — then check that the stored summary is still exactly the one that was written.
4 Which correction applies
A boat accumulates entries over a season — the vane was 3° out until the mast came out in June, and 0.5° out after. So an entry carries an instant it takes effect from, and each reading is corrected by the entry in force at the moment that reading was taken.
- The latest entry at or before the reading wins. A reading taken at exactly the effective instant is corrected — the boundary is inclusive.
- Before the first entry, nothing is corrected. No entry reaches backwards, and nothing is interpolated between two entries: a correction is a step, not a ramp.
- Two entries for the same instant: the later-created wins. The earlier one now applies to nothing — and it is still listed, dimmed, rather than quietly disappearing, because a row that has stopped mattering is different from a row that was never there.
There is a deliberate use for an entry that corrects nothing — offset 0, scale 1. It is how a crew stops an earlier correction applying to later sailing without deleting the history of having made it. The platform labels such an entry as passing reads through, rather than treating it as a mistake.
Wherever the debrief prints a number a correction touched, the number carries a small cal badge — so nobody has to remember which readings are raw. The badge travels with the data it describes, which is why it can never outlive it.
What the lens does not do
Three limits, named here rather than discovered later:
- Derived channels are not recomputed. Correcting boat speed does not re-derive true wind speed from the wind triangle. Those derived values came off your instrument system, and re-deriving a season of them would be exactly the retrospective rewrite this whole design refuses. Correct the channel you measured; the derived channel keeps what your instruments computed at the time.
- An error that changes sign with tack cannot be corrected this way. Leeway, current and a vane that leans with heel all push one way on port and the other on starboard. That is real, and no single linear entry fixes it — which is precisely what guided calibration’s cross-check exists to detect and tell you about.
- Race Control shows raw. On the boat, the tablet displays what the instruments say and the uplink sends what the instruments said. Calibration is an analytical lens over recorded data, not a filter between the crew and their own instruments.
The correction is applied inside the platform, on the way out of a read — not in your browser. There is one implementation of this arithmetic and every path that serves data goes through it, which is how a chart, a tile and an export cannot disagree about what a corrected number is.