# Reference patch — 02-collapse-repeats

The answer key for how the reference absorbs this change, and the thing that makes
`budget.json` reproducible. **Never shipped to the candidate.**

1. **Replace** `reference/src/Logger.java` with the file in this directory. Nothing else in
   `reference/src` changes.
2. The whole change is a new field —

   ```java
   private final AtomicReference<String> lastKey = new AtomicReference<>();
   ```

   — and three lines at the very top of `log`, before the clock is read or a `LogRecord` is
   built:

   ```java
   String key = level.name() + " " + message;
   if (key.equals(lastKey.getAndSet(key))) {
       return;
   }
   ```

Measured: `reference_diff = 10` (10 added, 0 removed, one file touched), so
`diff_budget = max(3, 10 + ceil(10/2)) = 15`.

The curveball suite (4 tests) is green after the patch.

**The base suite is deliberately not expected to stay green here.** One of its fourteen tests,
`duplicateMessagesAreNotCollapsedByDefault` ("logging the same message twice writes it twice"),
asserts the exact behaviour this requirement replaces. Every other base test is unaffected,
because every other base test either logs distinct messages or logs the same message with
something else — a different level, a different destination configuration, a clock advance — in
between.

## Where the requirement landed, and why there

`getAndSet` runs before the clock is even consulted and before any destination is looked at, so
collapsing a duplicate costs nothing beyond the comparison itself — a genuine duplicate never
reaches `offer`, never gets a timestamp, and never touches the registries. That ordering is also
what makes "collapsing happens once, centrally" true rather than aspirational: there is exactly
one place a record can be judged a duplicate, and it runs before fan-out, so no destination can
ever disagree with another about whether something was suppressed.

**Why an `AtomicReference`, not two plain fields (`lastLevel`, `lastMessage`).** Two separate
fields would need to be read and written together to mean anything, and two threads racing on
the read of one field and the write of the other is exactly the kind of half-updated state this
project's stress suites exist to catch — E1/E2 territory even though this particular class has
no concurrency test of its own. Folding both into one string key turns "compare-then-replace"
into a single atomic operation with no window between the compare and the replace: `getAndSet`
always tells the calling thread, truthfully, what the previous key was at the instant of the
swap, so two threads logging the identical message at the identical moment still produce exactly
one delivered record between them, not zero and not two.

**Why the key is `(level, message)`, not `message` alone.** Stated in the requirement: the same
message at a different level is a different observation about the system, not a repeat of the
first one, and collapsing across levels would mean an `ERROR` could be silently absorbed by a
`DEBUG` line that happened to share its text.
