# Reference patch — 02-start-without-the-tap

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

1. There is **no contract delta**. This curveball changes what the machine allows, not what it is made
   of, so the candidate's contract folder is untouched.
2. **Overlay** `TripRules.java` (in this directory) onto `reference/src/TripRules.java`. That is the
   entire patch.

Measured: `reference_diff = 3` — `diff_budget = max(3, 3 + min(ceil(3/2), 6)) = 5`.

The curveball suite (11 tests) and the base suite (18 tests) are both green after the patch.

## The whole change

```java
new Edge(TripState.MATCHED, TripStarted.class, TripState.IN_PROGRESS)
```

One row appended to `TripRules.standard()`, plus the comma the row above it now needs. Three lines
measured, one of which is punctuation.

**Nothing else was opened.** `Trip`, `TransitionTable`, `Edge`, `TripProjection`, `TripJournal`,
`InMemoryJournal`, `EffectRunner`, `RiderNotification`, `TripFactory` and `Demo` are byte-for-byte what
they were. This is the cheapest curveball in the problem, and that is the finding rather than a
disappointment: **a change that is purely "which (state, event) pairs are legal" should cost only
rows,** and in a design where the rules are data it does.

## Why the base suite stays green, checked rather than hoped for

This curveball makes a pair legal that used to be refused, so it is worth being explicit that no base
assertion covered that pair. The base suite asserts these refusals:

| Base test | Pairs it asserts are refused | Still refused? |
|---|---|---|
| `refusesSkippingAhead` | `REQUESTED` + `TripStarted` / `TripCompleted` / `DriverArrived` | yes — the rule was about having no driver, not about the arrival |
| `refusesDuplicateEvent` | `MATCHED` + `Matched`, `DRIVER_ARRIVED` + `DriverArrived` | yes — untouched |
| `refusesCancellationInProgress` | `IN_PROGRESS` + `RiderCancelled` | yes — untouched |
| `refusesAfterTerminal`, `oneExceptionRoot` | everything on a terminal trip | yes — the terminal check runs before the table |
| `refusalChangesNothing` | `REQUESTED` + `TripStarted` / `TripCompleted` | yes |

`MATCHED + TripStarted` is not among them, so the one pair this requirement legalises was genuinely
free. That was a deliberate choice when picking the requirement — see the note at the end.

## What this curveball is actually measuring

The shape of the change is a **shortcut edge**: a second, shorter path through the same graph, so
`DRIVER_ARRIVED` becomes a state a trip may pass through rather than one it must. That is a different
stress from curveball 01, which added a *backward* edge and a whole new event type.

- **A table-driven design pays 3 lines** and never opens a method.
- **A `switch (state)` design pays for the shape.** The `MATCHED` arm has to grow a second accepted
  event beside `DriverArrived`, which means an inner `switch` or `instanceof` chain inside an arm that
  previously handled exactly one case, and the `IN_PROGRESS` arm has to keep refusing `TripStarted`
  while the `MATCHED` arm now accepts it — the same event with two answers in two places, kept
  consistent by hand.
- **A design that encoded the lifecycle as an ordinal** — `state.next()`, or a `TripState[]` walked in
  order, which is a tempting shortcut when the happy path is a straight line — cannot express this at
  all without abandoning the idea. That is the trap this curveball is set for, and the base suite does
  not catch it, because a straight-line machine passes all 18 base tests. This is the design defect
  the problem's tier-3 placement is really for.

The `history()` requirement is the second thing being measured, and it is a modelling question rather
than a mechanical one: a trip that skipped the tap has **no `DRIVER_ARRIVED` entry**, and the
requirement says explicitly not to invent one. A design that keeps the log as the single source of
truth gets this for free — the log records the event it was given. A design that maintains a separate
state field and reconstructs a history for display is exactly where a helpful synthetic arrival entry
gets added, and then a kerb-wait report silently counts trips that never had one.

## Note on choosing this requirement

Two other rows-only requirements were considered and rejected in favour of this one:

- **A driver swap** (`Matched` legal from `MATCHED`, replacing the driver) is a genuine self-loop and
  would have been a nice test of "a state that can be re-entered". It was rejected because it directly
  contradicts a base assertion — `refusesDuplicateEvent` asserts `MATCHED + Matched` throws — so the
  base suite would have gone to 17 of 18. There is precedent in the corpus for a curveball that
  knowingly invalidates a base assertion (`logger/02-collapse-repeats`, `elevator/03-home-floor`), but
  a green base suite is the stronger signal when the requirement can be chosen freely, because it keeps
  "did the change break something that used to work" as a live question rather than an expected
  failure the grader has to remember to excuse.
- **Re-entering `REQUESTED`** is already what curveball 01 does, so it would have measured the same
  property twice.
