# The curveball, and what each design paid to absorb it

## What the interviewer says, ten minutes in

Finance signed off on a new tariff this morning and it is live from now on. The first started
hour is free, for every vehicle. Hours two and three are charged at the standard rate: motorbike
500, car 1000, truck 2000. From the fourth started hour onward it is the long-stay rate:
motorbike 1500, car 3000, truck 6000.

Rounding does not change. A car staying three hours and one minute has started four hours: free,
then 1000, then 1000, then 3000. It pays 5000.

That is `corpus/parking-lot/curveballs/02-tiered-pricing/REQUIREMENT-CHANGE.md`, near enough
verbatim, and `CurveballTest` asserts its worked example directly.

## The two designs

`a/ExitGate.java` decides the fee itself, in a `switch` inside a private method. This is what a
competent engineer writes under a 12-minute clock, and on the day it is written it is the right
call: one class, one file, nothing to follow.

`b/ExitGate.java` asks a `PricingPolicy` it was handed, and `b/FlatHourlyPricing.java` is today's
answer. `PricingPolicy.java` and `FlatHourlyPricing.java` are
`corpus/parking-lot/reference/src/` files with a provenance line added at the top and nothing else
changed. Both gates are the billing half of that reference's `ParkingLot`, with the allocator half
left out so the pricing seam is the only variable.

Design A is not weakened to make a point. It has one private `feeMinor`, so no rate table is
duplicated, and its rounding helper is the same arithmetic the reference uses. Before the
curveball lands the two designs are behaviourally identical, and `BaseTest` cannot tell them
apart:

```
a  / BaseTest        [ 6 tests successful ][ 0 tests failed ]
b  / BaseTest        [ 6 tests successful ][ 0 tests failed ]
a  / CurveballTest   [ 0 tests successful ][ 6 tests failed ]
b  / CurveballTest   [ 0 tests successful ][ 6 tests failed ]
```

Six failures each, with the same messages, starting with
`expected: <5000> but was: <8000>`. A grader looking only at behaviour has nothing to separate
these designs, which is why STANDARD v1.0 measures extensibility (D4) apart from functionality
(D2).

## Both designs go green

`a-after/` and `b-after/` are the same two designs with the change absorbed. Both pass the
curveball suite, and both fail the same four base-suite tests, because the tariff the base suite
asserts has been replaced. That is expected here, and the corpus patch notes say so.

```
a-after / CurveballTest   [ 6 tests successful ][ 0 tests failed ]
b-after / CurveballTest   [ 6 tests successful ][ 0 tests failed ]
a-after / BaseTest        [ 2 tests successful ][ 4 tests failed ]
b-after / BaseTest        [ 2 tests successful ][ 4 tests failed ]
```

Identical outcomes. The whole difference is in what it cost to get there.

## The measurement

Run from the repository root:

```bash
node -e "import('./server/lib/diff.mjs').then(async (m) => {
  const { readdirSync, readFileSync } = await import('node:fs');
  const f = (d) => readdirSync('lessons/B1/contrast/' + d)
    .filter((n) => n.endsWith('.java'))
    .map((n) => ({ path: n, content: readFileSync('lessons/B1/contrast/' + d + '/' + n, 'utf8') }));
  for (const [before, after] of [['a', 'a-after'], ['b', 'b-after'], ['a', 'b']]) {
    const r = m.measureChange(f(before), f(after));
    console.log(before, '->', after, JSON.stringify({ diffLines: r.diffLines,
      touchedFiles: r.touchedFiles, newFiles: r.newFiles, perFile: r.perFile }));
  }
})"
```

| Change | `diffLines` | pre-existing files edited | new files |
|---|---|---|---|
| A absorbs the new tariff | **26** | 1 · `a/ExitGate.java` (+21 −5) | 0 |
| B absorbs the new tariff | **2** | 1 · `b/ExitGate.java` (+1 −1) | 1 · `TieredPricing.java` |
| Extracting the policy later, behaviour unchanged | 32 | 1 · `ExitGate.java` (+13 −19) | 2 |

`measureChange` returns `diffLines`, and lines in brand-new files are free, because the cost being
measured is being forced back into code that already existed. `TieredPricing.java` is 45 lines and
charges nothing.

**B's 2 is not a number this lesson invented.**
`corpus/parking-lot/curveballs/02-tiered-pricing/budget.json` records `reference_diff: 2` for the
same change against the full reference, with the same note: one new file, plus the one
convenience-constructor line that names today's tariff. The seam reproduces its cost at two
different scales.

Two lines is D4 level 2, not level 3. Level 3 wants **zero** lines changed in pre-existing files,
and there are real cases that reach it.
`corpus/lru-cache/curveballs/01-least-frequently-used/budget.json` records `reference_diff: 0`
for an entire new eviction policy, because the wiring line lives in the contract rather than in
`reference/src`. `corpus/logger/curveballs/03-rolling-appender/budget.json` also records `0`:
`Appender.write(String)` already is the seam a rolling file destination needs, so
`reference/src` is byte-identical before and after.

## The difference the line count does not show

After A's edit, last month's tariff is gone. Rebilling a July stay for an audit needs a class
that no longer exists:

```
Audit.java:8: error: cannot find symbol
        System.out.println(new ExitGate(new FlatHourlyPricing())
                                            ^
  symbol:   class FlatHourlyPricing
  location: class Audit
1 error
```

The same file compiles against `b-after/` and prints `8000`. `FlatHourlyPricing` was left in
place rather than edited, which is what the reference patch does and why:

> `FlatHourlyPricing` is left in place rather than edited — the old tariff is still a fact about
> last month's revenue, and deleting it would be a bigger diff than keeping it.

A tariff you can still instantiate is a tariff you can still test and still reconcile against. It
is also the answer when an interviewer asks what happens to stays that were already open at the
moment the rate changed.

## The number that argues for the other side

Extracting the policy later cost 32 lines in one file, which is more than absorbing this tariff in
place cost. Nineteen of those 32 are lines that moved out of `ExitGate` into the two new files,
rather than lines rewritten. The base suite stays green throughout, so this is a refactor and not
a rewrite. Read it as a one-time price you pay when you know you need the seam. Once paid, each
tariff after it costs 2.
