Product review: a three-day meeting, end to end - #8
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 sheetnames the day —--id/--name/--date/--ref, so a meeting overthree days is three days off one rulebook instead of three hand-edited copies of
it, which drift.
staging machine goes
Completeon the finish beam and the ET arrives 1.3 s lateroff 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 acar that stops on the track gets.
Field::qualify, the draw's ownfunction, 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.
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.mdsays race control is written — it opened by saying the repositoryhas no code and nothing to build, next to thirteen crates and 325 tests.
Still open, not in this PR
call_upbrings back the samepair, 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] dateis one string, so a meeting running 8–9 August publishes as the 8th.and the top seed went to the lower number.
scoring attempts, so somebody checking their own entry cannot see the rules they
will be judged by.
1 v 5, 4 bye, 2 bye, 3 bye— not a running order.The facade half of this is
product-review-2026-08onBeam402/beam402-demo.🤖 Generated with Claude Code