# When not to make the table data

Start with what STANDARD v1.0 actually penalises at D3 level 0: *"Behaviour selected by `if`/`switch`
on a type field, **in more than one place**"*. Read the last four words. One `switch (state)` inside
one method is not the defect the standard names. It becomes one when a second method has to agree
with it.

Level 3 is the other end: *"The seam set is minimal — no speculative interface with a single
implementation and no foreseeable second one"*. The standard then says the symmetry out loud, that
level 3 *"penalises over-abstraction as much as level 0 penalises none"*, and names the failure tag
`over-engineered (premature interface)`. That tag routes to `lessons/B1` at its `when-not` stage.

So this file is not a caveat bolted onto the lesson. Three files of table machinery for a machine that
has one rule is the level 3 failure, exactly.

## The concrete bad example

A document is a draft until somebody publishes it. That is the whole requirement, and there is no
second one coming. Here it is behind the apparatus this lesson teaches. Nested in one file so it can
be compiled as it stands; `javac 21 -Xlint:all` prints nothing and exits 0.

```java
public final class DocFlow {

    public enum DocState { DRAFT, PUBLISHED }

    public sealed interface DocEvent {
        record Publish() implements DocEvent { }
    }

    public record DocEdge(DocState from, Class<? extends DocEvent> on, DocState to) { }

    public static final class DocRules {
        public static List<DocEdge> standard() {
            return List.of(new DocEdge(DocState.DRAFT, DocEvent.Publish.class, DocState.PUBLISHED));
        }
        private DocRules() { }
    }

    public static final class DocTransitionTable {
        private final Map<DocState, Map<Class<? extends DocEvent>, DocState>> edges;
        public DocTransitionTable(Collection<DocEdge> rules) { /* index them */ }
        public DocState next(DocState from, DocEvent event) { /* look one up, or throw */ }
    }
}
```

Two states, one event, one row, four types.

**What a reviewer sees.** A `Map` of `Map`s to hold a single fact. `DocRules.standard()` has no second
rule set and no second caller, so the indirection buys nothing that
`if (state == DRAFT) state = PUBLISHED;` did not already have. The next person adds a
`DocRulesProvider` so the flow can be configured, and now there is a configuration format for a
machine with one rule.

**The version worth writing instead.** A field, an enum, and a method that refuses when the document
is already published. Then a third state arrives, say review or scheduled publication. The table is a
twenty-minute change from there, and it will be shaped by three real requirements rather than by a
guess about one.

## The other overuse: a table with the types taken out

The second way this goes wrong looks more sophisticated and is worse.

```java
Map<String, String> transitions = Map.of("DRIVER_ARRIVED:trip_started", "IN_PROGRESS");
```

"Rules as data" reads as an invitation to make them strings, loadable from a config file, editable
without a deploy. What it costs is every check the compiler was doing. `"DRIVER_ARRVIED:trip_started"`
compiles, and so does a target state that no longer exists. The defect tag for this is
`stringly-typed`, from `rubric.mjs`'s closed vocabulary, and it routes to `lessons/A1`.

`Edge` keeps the enum and the `Class` for exactly this reason. Rules being data does not mean rules
being text.

## What the table costs, measured

Three prices, and the middle one is the expensive one.

**It costs code before any requirement arrives.** From `node lessons/B6/contrast/measure.mjs`:

```
size of a    4 file(s)  100 normalised lines
size of b    7 file(s)  140 normalised lines
```

Both directories are commented for teaching, so those counts include javadoc. Read the shape: three
more files, for behaviour that is identical.

**It costs more than no table when the change needs a third input.** Also measured, in
`contrast/curveball.md`:

| Change | A, rules in control flow | B, rules as data |
|---|---|---|
| one `(state, event)` pair becomes legal | **1** | **3** |
| the rules vary by market | **10** | **4** |
| a rule that depends on how long the trip has waited | **10** | **15** |

The 15 against 10 is the number that matters here, and the 3 against 1 is the one that keeps you
honest. A table is a bet that the rules will move along `(state, event)`. A losing bet is not neutral.

**It costs the compiler's help on new events.** `Edge` keys on `Class<? extends TripEvent>`, which is
not exhaustiveness-checked, so nothing forces a new event to have a row. Verified in the corpus:
`corpus/trip-state-machine/curveballs/01-driver-cancels/reference-patch/PATCH.md` records that adding
a sixth event broke three files by name, and `TripRules` was not one of them. The missing rows were
caught by the suite. An exhaustive `switch (state)` over the enum, by contrast, stops compiling when a
state is added.

## It also costs clock

`Edge`, `TransitionTable` and `TripRules` are about fifteen minutes of a forty-five-minute round, and
the interviewer sees no behaviour for the first ten of them. STANDARD v1.0 caps D2 at 0 when no `main`
exists, on the grounds that *"interviewers run it first"*, and D2 is 25% of the score against D3's
20%. Machinery nobody asked for, paid for with the driver, loses on both dimensions at once.

## The axis this lesson deliberately leaves alone

Two rules in `worked/` are not rows, and neither should be.

**The five-minute wait.** It lives in `WaitTimeout`, asked after the table and before the log append.
Putting it on `Edge` as a fourth component would make every other row grow a no-op guard, and it would
hand `TransitionTable.next` a clock. `contrast/curveball.md` prices that trade at 15 lines against 10.
It is still the right one: the second time-dependent rule is then a new implementation and no edit.

**"A finished trip is finished".** That is `TripState.terminal()`, a boolean on the enum, checked
before the table. It could have been expressed as the absence of outgoing rows, which is what it also
is. It stays explicit because callers need to tell "not yet" from "never again", and a missing row
cannot say which of the two it means.

And one from the second table in the corpus. `corpus/vending-machine/reference/src/Transitions.java`
allows `IDLE + SELECT`, with no money in the machine, and its own comment explains why: *"the price
objects, later, and that is a different concern in a different class."* Money is a third input. The
table stays a table.

## The threshold, from both sides

**Make the rules data** when the machine has at least three states and more than one entry point, or
when a requirement already names a second rule set. Two of those hold for
`corpus/trip-state-machine`: six states, and market rules the moment it ships in a second city.
`corpus/vending-machine` has four entry points. Its `TransitionTable` javadoc says the switch version
would spread the rules across four methods. Then *"each method grows a guard clause that has to be
kept consistent with three others by hand"*.

**Leave it as control flow** when the machine is a straight line with one entry point and no second
rule set in sight. `contrast/a/` is 4 files and passes the same seven tests in `BaseTest.java`. When the
table becomes right, converting is mechanical and the rows are already written down in the switch.

**Never put a rule in the table that is not a function of `(state, event)`.** Not because it is
expensive, but because it does not fit. A `WaitExpired` row says the pair is legal in principle. Only
the guard knows whether it is legal yet, and a design that pretends otherwise cancels trips one second
after the driver pulls up. `worked/` prints that, in block 5.
