Skip to content

Product review: a three-day meeting, end to end - #8

Merged
perfilev-dev merged 5 commits into
mainfrom
product-review-2026-08
Aug 4, 2026
Merged

Product review: a three-day meeting, end to end#8
perfilev-dev merged 5 commits into
mainfrom
product-review-2026-08

Conversation

@perfilev-dev

Copy link
Copy Markdown
Contributor

Walked one club's meeting from the registration desk to a published results page —
practice on 7 August, qualifying on the 8th, eliminations on the 9th, four classes
covering heads-up, an index class, a time window and a bracket — and fixed what got
in the way. Every pass in the practice day and all sixteen elimination pairs ran
over the bus through the operator panel; the qualifying ETs were written into the
log by hand and are stated as invented.

Five commits, each building and testing green on its own:

  • beam402 sheet names the day--id/--name/--date/--ref, so a meeting over
    three days is three days off one rulebook instead of three hand-edited copies of
    it, which drift.
  • Recording waits for the numbers — a bug, found by running a practice day: the
    staging machine goes Complete on the finish beam and the ET arrives 1.3 s later
    off the node (D25), and a press inside that beat wrote a real 11.850 down as
    -. Silently, because a pass with no time is a legitimate record — it is what a
    car that stops on the track gets.
  • A day in qualifying has a board — derived by Field::qualify, the draw's own
    function, so it cannot disagree with the ladder people end up in. A practice day
    used to produce no output at all, in the CLI or the API.
  • Every round is published, and everyone who is not in the field — a settled
    class used to be a final with no semi-final under it, and a facade could see that
    five entered and four qualified without being able to name the fifth.
  • CLAUDE.md says race control is written — it opened by saying the repository
    has no code and nothing to build, next to thirteen crates and 325 tests.

Still open, not in this PR

  • A day cannot be rehearsed. The simulator's call_up brings back the same
    pair, as it documents, so 19 cars × 3 passes give identical ETs and a club cannot
    test its own rulebook — field sizes, minimums, class windows — against the live
    loop. A scenario of several [[pass]] blocks would make one a rehearsal script.
    This is the most valuable of what is left: a rehearsal is the only way to find a
    wrong class rule before the event rather than during it.
  • [event] date is one string, so a meeting running 8–9 August publishes as the 8th.
  • Ties in seeding break on entry number, silently — two cars 0.0100 off their dials
    and the top seed went to the lower number.
  • The entry list from the desk does not show a class's window, cut, minimum or
    scoring attempts, so somebody checking their own entry cannot see the rules they
    will be judged by.
  • Pair order within a round reads 1 v 5, 4 bye, 2 bye, 3 bye — not a running order.

The facade half of this is product-review-2026-08 on Beam402/beam402-demo.

🤖 Generated with Claude Code

perfilev-dev and others added 5 commits August 4, 2026 20:26
A season skeleton is a rulebook a club keeps: format, seeding, ladder, field
sizes, the fouls an official may call. The one thing in it that is not a rule is
the particular Saturday in `[event]` — and `beam402 sheet` copied that through
verbatim, so a meeting running practice on Friday, qualifying on Saturday and
eliminations on Sunday needed three files. The skeleton's own comment said as
much: change the id and the date for each event.

Three copies of a rulebook drift. Somebody fixes a field size in one of them and
the class runs two ways in one weekend, which is the failure this exists to stop
rather than a tidiness argument.

So the day is an argument: `--id`, `--name`, `--date`, `--ref`. Absent fields keep
whatever the skeleton carries, so a club running one day at a time sees nothing
new, and the override goes through the sheet's own checks rather than around them
— a `--id` that cannot be a URL is refused at the desk, where **D34** says a
thing that is wrong should be found.

Rewritten line by line rather than through a TOML round trip, for the same reason
the entries are appended instead of re-emitted: the skeleton is a file the club
wrote and reads, and a parse-and-print hands it back with its comments gone. A
key the skeleton never had — an `id` on a season that was never uploaded — is
inserted rather than dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by running a practice day over the bus: the log took
`Q Unlimited 17 - - -` for a pass that had just run 11.850.

The staging machine calls a round complete when every car has been **seen** to
cross the finish beam. That is the right statement about the strip and not yet a
statement about the numbers — the ET is latched in the node and comes back on a
later poll (**D25**), 1.3 s later on the reference venue. `record` gated on the
phase alone, and the panel enabled the button for that whole beat.

It is the worst shape a defect can have here. A pass with no time is a
**legitimate** record — it is what a car that stops on the track gets, and it
seeds that driver at the back — so nothing refused, nothing warned, and a driver
leading their class was quietly put last.

So the gate is the round being **whole**: every lane that raced has its ET, or
there is no longer a reason to expect one. Two ways of the latter, and they
matter. A car that never reached the finish beam has nothing coming, which the
`Abandon` action already knows. And a node that stopped answering has nothing
coming either — so past a three-second grace, `-` is the honest record and gets
written deliberately rather than by beating the poll loop to a button. Refusing
forever would strand the day.

`next` takes the same gate, which is the other half of the same bug: clearing the
round does not stop the records arriving, so the previous car's ET landed in the
next pair's round and showed on the panel before that pair had staged. That was
visible as a 11.85 sitting under a `ready` phase with no reaction time beside it.

The state carries `settled` so the operator page stops offering a button that
would do this, instead of relying on the person not being quick.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A class that had not drawn its ladder printed one sentence — "qualifying, the
ladder has not been drawn" — and the API said nothing at all about it. So a
**practice day produced no output**, which is a strange thing for a feature the
README describes as an entry sheet nobody ever draws: two passes recorded, and the
only way to read them was the raw log. A qualifying session in progress was the
same, all day, for everybody at the track.

The board is derived and it is derived by [`Field::qualify`] — the function the
draw itself calls. That is the whole design of it. A second implementation of
"best pass" would eventually disagree with the ladder people are racing off, and
it would disagree in the direction nobody checks: quietly, between passes, on the
thing a driver is deciding their next run from.

The cut is **shown, not applied**. Cars below the line are in the list with no
seed, because they are still racing and a board that hid them would be hiding the
only thing they are looking at. Where a class has a cut, its size comes from the
real call rather than from a second copy of that one-line rule.

A car with no time yet gets **no place**. The draw does place those — they qualify
last, in entry order, because they entered and they paid — but entry order is not
a qualifying position, and the first version of this told a driver who had not run
that they were provisionally third. Likewise no cut line until somebody has a
time, or the line is a statement about the alphabet.

Scored by the class's own measure, so a bracket board shows how close to the dial
and the ET both: on the clock alone the top qualifier looks like the slowest car
there, which is exactly the sentence bracket racing exists to make possible.

It stops answering once the ladder is drawn. From that moment the field is the
answer and two orders for one class is one too many.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things a results page needs and the read contract could not carry.

**Every round.** `Progress` replaced the current round with the next one as a
class advanced, so a settled class was a final with nothing under it — no
semi-final, no quarter-final, no way to see how a winner got there, which is most
of what anybody opens a results page for. The rounds were never lost: they are `W`
and `B` lines in the log, and discarding them here only meant every reader had to
replay it again to get them back. So the state keeps them, `round` still points at
the one a class is on, and the API gains `rounds` beside it — **D35** says a shape
gains fields rather than changing what one means, and `round` is somebody's
dependency.

**Everyone else.** A facade holds a field and a count of entries; it can see that
five entered and four qualified and it cannot name the fifth. That name is the
difference between "seeded last" and "did not qualify", which the tower has printed
since the cut existed, and withdrawn is a third sentence again — so
`did_not_qualify` and `withdrawn` are separate keys rather than a flag on one.

Checked by running a meeting end to end: four classes, sixteen pairs off the
ladders, and the day read back with `quarter-final, semi-final, final` under the
class that had byes in the first round.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CLAUDE.md` opened with "the repository currently contains no code" and "there is
nothing to build, lint, or test — do not invent build commands", while `software/`
holds a Rust workspace of thirteen crates and `cargo test` runs 325 of them. It is
the first thing a contributor reads, and it was telling them not to look.

The claim it was reaching for is still true and worth keeping, so it is kept and
narrowed: **no hardware exists**, nothing has run against a beam, and no number any
of this prints was measured. Tests passing is not a subsystem working. What changed
is that the code is now findable, with the commands that exercise it, and
`software/` is in the file table beside the documents.

The ADR range was stale by one and the roadmap by five entries; both catch up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@perfilev-dev
perfilev-dev merged commit 51ebcae into main Aug 4, 2026
1 check failed
@perfilev-dev
perfilev-dev deleted the product-review-2026-08 branch August 4, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant