Skip to content

Read the held-back date and show the hold in the listing - #201

Merged
iderex merged 1 commit into
mainfrom
listing/the-hold-and-the-date-it-started
Aug 26, 2026
Merged

iderex merged 1 commit into
mainfrom
listing/the-hold-and-the-date-it-started

Conversation

@iderex

@iderex iderex commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes #200

What this changes

The runner reads Held-back and the listing shows the hold.

internal/check/heldback.go is new and holds both halves of the field: the name
Held-back, and record-held-back-is-not-a-date, which refuses a value that is
not a date written the one way record 0008 names. It sits in its own file with
the check that reads it, which is the shape headerDateFields argues for at its
own list and which Measurement-Commit and Borrowed already took: a checker
built before a field is unaware of it, and a field with no check has nowhere to
hide. The dispatch is one line in the record walk.

internal/check/list.go reads the field into the entry, keeping the value as
written apart from the value as a date, and Report gains a block after the
table: a count, then one line per held-back record naming the slug and the date
the clock started.

docs/experiment-template.md gains a paragraph for the field, and its closing
sentence naming the records the format comes from gains 0022. That sentence
enumerated three records and the format now comes from four, so leaving it alone
would have made the template wrong in the same change that made it incomplete.

Three cases under testdata/cases/ and one listing tree under
testdata/listings/ are new. No path leaves the tree:

git diff --name-status origin/main...HEAD | cut -f1 | sort | uniq -c
     17 A
      5 M

What failure it prevents

An experiment held back under record 0010 reported by lab list as ordinary
unanswered work. Record 0022 decides that such a record stays in asking and
carries Held-back with the date of the report, and that the listing prints
that as its own dated line. Nothing on the default branch read the field, so the
verb whose whole purpose is to stop a tree misreporting what is running was
producing exactly the misreport 0022 chose a field over a fourth state to
prevent.

The refusal prevents the other half. A value nothing can read leaves the record
saying it is held back and saying nothing about since when, and the fact of a
hold with no date is waiting indefinitely arriving through a typo rather than
through a decision, which is the option 0022 rejected. A field written with
nothing after the colon is refused for the reason RecordHeaderDateIsNotADate
gives: under record 0013 absence is legal and an empty declaration is a
different statement.

What was run

At the commit being pushed, 9a38499e1d607f910c97cc2cae5d9b7a8ddb9478:

$ go build ./cmd/... ./internal/... && echo 'exit 0, no output'
exit 0, no output

$ go vet ./cmd/... ./internal/... && echo 'exit 0, no output'
exit 0, no output

$ gofmt -l cmd internal
(no output is the passing result)

$ go test -count=1 ./cmd/... ./internal/...
ok  	github.com/Flowfin/lab/cmd/contexts	0.513s
ok  	github.com/Flowfin/lab/cmd/lab	1.748s
ok  	github.com/Flowfin/lab/cmd/notices	6.719s
ok  	github.com/Flowfin/lab/cmd/pullrequest	0.567s
ok  	github.com/Flowfin/lab/internal/check	0.889s
ok  	github.com/Flowfin/lab/internal/contexts	0.526s
ok  	github.com/Flowfin/lab/internal/hardware	0.519s
ok  	github.com/Flowfin/lab/internal/invariants	0.922s
ok  	github.com/Flowfin/lab/internal/notices	0.518s
ok  	github.com/Flowfin/lab/internal/prose	0.560s
ok  	github.com/Flowfin/lab/internal/pullrequest	0.554s

$ go run ./cmd/lab check .
examined .
1 experiment directory walked, 1 record read
25 decision records read
the time this run read is 2026-08-26T12:59:08Z
0 refused

$ go run ./cmd/lab list .
examined .
1 experiment
the time this run read is 2026-08-26T12:59:08Z
  slug                       state     question written  waiting  needs
  reading-a-tree-of-records  answered  2026-08-11        -        none
0 records are held back

The suite above was run without -v, so the line saying what it did not cover
is not in that output. It was run separately over the package that prints it:

$ go test -count=1 -v ./internal/hardware/... | tail -5
the integration-hardware harness was not asked for and nothing in it ran.
asking costs a machine with the hardware each test names and an explicit request:
    go test -tags integration_hardware ./internal/hardware
with LAB_INTEGRATION_HARDWARE=1 in the environment. its results are about that machine and are not this suite's results.
ok  	github.com/Flowfin/lab/internal/hardware	0.449s

Every guard removed, and the suite watched going red

Each was reverted immediately afterwards, and the run above is the restored tree.

The refusal itself, with the not-a-date branch made to return nothing:

--- FAIL: TestCases (0.06s)
    --- FAIL: TestCases/record-held-back-with-nothing-after-the-colon (0.00s)
        check_test.go:46: expected refusal not produced: record-held-back-is-not-a-date
    --- FAIL: TestCases/record-held-back-with-a-value-that-is-not-a-date (0.00s)
        check_test.go:46: expected refusal not produced: record-held-back-is-not-a-date

The dispatch line deleted, which is the one missing line that removes a rule
from every run while the operator still holds it:

--- FAIL: TestCases (0.05s)
    --- FAIL: TestCases/record-held-back-with-a-value-that-is-not-a-date (0.00s)
        check_test.go:46: expected refusal not produced: record-held-back-is-not-a-date
    --- FAIL: TestCases/record-held-back-with-nothing-after-the-colon (0.00s)
        check_test.go:46: expected refusal not produced: record-held-back-is-not-a-date

An absent field read as a hold, which is the mistake that reports every record
in a tree as held back since nothing:

--- FAIL: TestTheListingKeepsAHoldApartFromASilence (0.00s)
    list_test.go:209: no-hold is held back true, want false
--- FAIL: TestTheReportPrintsEveryColumn (0.00s)
        4 records are held back
          oldest-question is held back and its Held-back is "", which is not a date

An unreadable value printed as a date, which prints the first day of year one as
the moment the clock started:

--- FAIL: TestTheReportPrintsTheHoldsAndTheirDates (0.00s)
          unreadable-hold is held back and the clock started 0001-01-01

The count line dropped, which is the line separating a tree with no hold from a
listing that never looked:

--- FAIL: TestTheReportCountsHoldsWhenThereAreNone (0.00s)
    list_test.go:280: the report does not count the holds it did not find:
--- FAIL: TestAListingOfATreeWithNoExperimentsStillCountsTheHolds (0.00s)
    list_test.go:293: the report does not count the holds it did not find:

There is no second reader on this board tonight, and this body carries the
evidence in place of one rather than implying a review happened.

What this does not do

It refuses no absent field, and it cannot. Record 0013 makes a field added
after it optional and lets a check over it refuse only what is present, and
record 0022 says so of this field in its own words: a record being held back
that carries no Held-back sits in asking with a question saying nothing, and
nothing here refuses it. That is the case the field exists for, and only the
person writing the record prevents it. Both the operator and the template say so
where a reader meets them.

It refuses no hold on a record that is not asking, though record 0022 fixes a
hold as a field beside asking. The listing prints the state on the line
instead, so the disagreement is visible rather than judged, and no property in
this tree covers it.

It refuses no held-back date later than the time the run read, so a window that
has not started yet passes. QuestionDatedLaterThanTheRun is the neighbouring
rule for the question's date and this change does not widen it.

It reads no clock against the window. Nothing here counts the 90 days, notices
one elapsing, or knows whether the single extension was granted. What the runner
does is read a date and print it; the window itself is record 0022 and the
paragraph in SECURITY.md, and both are held by people rather than by a check.

Two of the three legs the case harness compares are what these fixtures buy, and
the third is not available here: every comparison is over which properties were
refused and never over which line refused them, so the two refusing cases prove
the property bites and prove nothing about which of the operator's branches did
it. internal/check/harness_test.go states that bound and this change does not
move it.

Record 0022 decides that a held-back record stays in asking and carries a
Held-back field with the date of the report, and that the listing prints that as
its own dated line. Nothing on the default branch read the field, so an
experiment held back appeared in lab list as ordinary unanswered work. That is
the misreport 0022 chose a field over a fourth state to prevent, and it was
being produced by the verb whose whole purpose is to stop a tree misreporting
what is running.

Held-back arrives in internal/check/heldback.go with the check that reads it,
which is the shape headerDateFields argues for and which Measurement-Commit and
Borrowed already took: a checker built before a field is unaware of it, and a
field with no check has nowhere to hide.

The refusal is record-held-back-is-not-a-date, over a field that is present
only. A value nothing can read leaves the record saying it is held back and
saying nothing about since when, and the fact of a hold with no date is waiting
indefinitely arriving through a typo rather than through a decision. A field
written with nothing after the colon is refused for the reason
RecordHeaderDateIsNotADate gives: under record 0013 absence is legal and an
empty declaration is a different statement.

The listing prints lines rather than a sixth column, which is 0022's own word
for it and is what the table can carry without wrapping. The count is printed
whatever it is, so a tree with no hold and a listing that never looked are
different outputs. A value that is not a date is printed as written, and a hold
on a record that is not asking is printed with the state beside it, because a
report that dropped either would hide the disagreement instead of the subject.

Two shapes stay unrefused and both are disclosed at the operator: an absent
field, which record 0013 forbids refusing and 0022 says so of this field in its
own words, and a hold on a record that is not asking, which nothing here judges.

docs/experiment-template.md gains the field and names 0022 among the records the
format comes from. That sentence enumerated three records and the format now
comes from four, so leaving it would have made the template wrong in the same
change that made it incomplete.

Every refusal and every line was proved by removing it and watching the suite go
red: the refusal itself, the dispatch case, an absent field read as a hold, an
unreadable value printed as the zero time, and the count line dropped.

Closes #200

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex added this to the 3. The question and the answer milestone Aug 26, 2026
@iderex iderex added the enhancement New feature or request label Aug 26, 2026
@iderex iderex self-assigned this Aug 26, 2026
@iderex
iderex merged commit ffd3f7d into main Aug 26, 2026
25 checks passed
@iderex
iderex deleted the listing/the-hold-and-the-date-it-started branch August 26, 2026 13:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read the held-back date and show the hold in the listing

1 participant