Skip to content

Refuse a record slug that disagrees with its directory - #178

Merged
iderex merged 1 commit into
mainfrom
check/a-record-slug-that-disagrees-with-its-directory
Aug 24, 2026
Merged

iderex merged 1 commit into
mainfrom
check/a-record-slug-that-disagrees-with-its-directory

Conversation

@iderex

@iderex iderex commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Closes #54.

What this does

The record format fixes a header, and until now nothing compared the Slug
field with the directory the record sits in. The slug is how a reader gets
from a quoted result back to the experiment that produced it, and the
promotion section makes that walk matter to somebody on another board. A
disagreement broke that walk silently, because both halves existed and only
their agreement was missing.

record-slug-disagrees-with-its-directory refuses it. The message names both
strings, because which of the two is wrong decides the repair and nothing in
the tree can tell a mistyped field from a misnamed directory:

go run ./cmd/lab check testdata/cases/a-record-slug-that-names-another-directory/tree
examined testdata/cases/a-record-slug-that-names-another-directory/tree
1 experiment directory walked, 1 record read
no docs/decisions directory in this tree
0 decision records read
the time this run read is 2026-08-24T18:49:00Z
1 refused
  testdata\cases\a-record-slug-that-names-another-directory\tree\experiments\timing-test\EXPERIMENT.md: its Slug is "throughput-test" and it sits in a directory named "timing-test", so a reader walking back from the slug reaches another experiment or nothing (record-slug-disagrees-with-its-directory)
exit status 1

That is the last of the three refusals #54 asks for. The two date refusals
are already on the default branch, and I read them at this head rather than
carrying the claim over:

go run ./cmd/lab check testdata/cases/record-with-a-question-date-that-is-not-a-date/tree | tail -1
  ...EXPERIMENT.md: its Question-Written is "March 2026", and record 0008 writes a date as YYYY-MM-DD, for example 2026-01-02 (record-header-date-is-not-a-date)

go run ./cmd/lab check testdata/cases/record-still-asking-with-an-answer-date/tree | tail -1
  ...EXPERIMENT.md: it says asking and carries an Answer-Written of 2026-02-02, so it has been edited towards a state it is not in (record-answer-date-disagrees-with-the-record)

go run ./cmd/lab check testdata/cases/record-answered-before-it-was-asked/tree | tail -1
  ...EXPERIMENT.md: its Answer-Written is 2026-02-02 and its Question-Written is 2026-03-01, so the answer is dated before the question (record-answer-date-disagrees-with-the-record)

What was retired, deliberately and by name

two-experiments-share-a-slug is gone, together with the expectation its
fixture carried. It refused two experiments declaring one slug once case was
ignored, and the agreement rule subsumes it: two directories under
experiments/ cannot share a name, so two experiments can only answer to one
slug when at least one of their records declares a slug that is not its own
directory's name. Keeping both would have made every tree that tripped the
sharing rule trip the agreement rule as well, and a fixture tripping two
rules proves neither cleanly. That collision is what stopped this refusal
being written when the date rules landed, and the choice between retiring the
sharing rule, giving it precedence and folding the two into one property was
taken on #54 on 2026-08-24.

Where the subsumption stops, written at the function rather than only
here. The comparison is between two slugs, so it is made only where the
directory name and the declared field are both legal slugs. Where either is
not, the tree is already refused for that shape and this rule says nothing,
so a pair answering to one slug out of a directory that is not a slug is
refused for the directory's name and never for the sharing. The tree is red
either way and the message points somewhere else, which is the whole of what
the retirement cost.

Case is no longer folded and the retired rule folded it. A legal slug carries
no upper case, so two legal slugs cannot differ by case alone, and an exact
comparison between two strings that have both passed refuseSlug is the same
comparison a folded one would make.

The fixtures

a-record-slug-that-names-another-directory trips exactly this refusal and
no other, with a-record-slug-that-is-a-slug as the near neighbour that
differs by the smallest legal change and refuses nothing.

two-experiments-answering-to-one-slug is kept and re-pointed at this
property, because it is the evidence for the subsumption the retirement rests
on: the tree the old rule was written for is still refused, twice, once per
record.

The proof that it bites

Three deletions were executed against the guard and each reddened the suite
for the reason it names.

Removing the refusal site:

--- FAIL: TestCases/a-record-slug-that-names-another-directory (0.00s)
--- FAIL: TestCases/two-experiments-answering-to-one-slug (0.00s)
        check_test.go:46: expected refusal not produced: record-slug-disagrees-with-its-directory

Widening the comparison to every declared slug, which reddened twenty-eight
cases including both near neighbours:

--- FAIL: TestCases/a-record-slug-that-is-a-slug (0.00s)
--- FAIL: TestCases/two-experiments-answering-to-their-own-slugs (0.00s)
        check_test.go:46: refusal produced that no case expected: record-slug-disagrees-with-its-directory

Removing the clause that compares only two legal slugs, which is the
collision the clause exists against:

--- FAIL: TestCases/a-record-slug-that-is-not-a-slug (0.00s)
        check_test.go:46: refusal produced that no case expected: record-slug-disagrees-with-its-directory

The tree was restored between each one and the suite is green at this head.

The gate

Run at this head, in the order CONTRIBUTING.md names:

go build ./cmd/... ./internal/...        (no output)
go vet ./cmd/... ./internal/...          (no output)
gofmt -l cmd internal                    (no output)
go test -count=1 -v ./cmd/... ./internal/...
ok  	github.com/Flowfin/lab/cmd/contexts	0.849s
ok  	github.com/Flowfin/lab/cmd/lab	9.176s
ok  	github.com/Flowfin/lab/cmd/notices	41.490s
ok  	github.com/Flowfin/lab/cmd/pullrequest	0.880s
ok  	github.com/Flowfin/lab/internal/check	1.103s
ok  	github.com/Flowfin/lab/internal/contexts	0.839s
ok  	github.com/Flowfin/lab/internal/hardware	0.889s
ok  	github.com/Flowfin/lab/internal/invariants	1.181s
ok  	github.com/Flowfin/lab/internal/notices	0.846s
ok  	github.com/Flowfin/lab/internal/prose	0.885s
ok  	github.com/Flowfin/lab/internal/pullrequest	0.882s

go run ./cmd/lab check .
examined .
1 experiment directory walked, 1 record read
18 decision records read
the time this run read is 2026-08-24T18:47:44Z
0 refused

The integration-hardware harness was not asked for and produced nothing here.
Its tests are behind a build constraint and the default run does not compile
them, so nothing above is a measurement about hardware.

The means

Go, because the refusal is one more arm on an operator that already exists in
internal/check, it is proved by the ledger of fixtures the package already
carries, and it adds no language, runtime or dependency to the tree.

What this does not do

It does not judge whether a slug describes its experiment, and it does not
judge which of the two strings is the correct one. It holds the agreement,
and a record whose field and directory agree on a poor name passes.

No second person has read this change. The evidence above stands in place of
one: the three executed deletions, the runs they produced, and the gate at
this head.

The slug is how a reader gets from a quoted result back to the experiment
that produced it, and the promotion section makes that walk matter to
somebody on another board. Until now nothing compared the Slug field with
the directory the record sits in, so a disagreement broke that walk
silently: both halves existed and only their agreement was missing.

record-slug-disagrees-with-its-directory refuses it, and the message names
both strings, because which of the two is wrong decides the repair and
nothing in the tree can tell a mistyped field from a misnamed directory.

WHAT WAS RETIRED AND WHY. two-experiments-share-a-slug is gone, deliberately
and by name, together with the expectation its fixture carried. It refused
two experiments declaring one slug once case was ignored, and the agreement
rule subsumes it: two directories under experiments/ cannot share a name, so
two experiments can only answer to one slug when at least one of their
records declares a slug that is not its own directory's name. Keeping both
would have made every tree that tripped the sharing rule trip the agreement
rule as well, and a fixture tripping two rules proves neither cleanly. That
collision is what stopped this refusal being written when the two date rules
landed; the choice between retiring the sharing rule, giving it precedence
and folding the two into one property was taken on issue #54 on 2026-08-24.

WHERE THE SUBSUMPTION STOPS is written at the function rather than only
here. The comparison is between two slugs, so it is made only where the
directory name and the declared field are both legal slugs. Where either is
not, the tree is already refused for that shape and this rule says nothing,
so a pair answering to one slug out of a directory that is not a slug is
refused for the directory's name and never for the sharing. The tree is red
either way and the message points somewhere else, which is the whole of what
the retirement cost.

Case is no longer folded and the retired rule folded it. A legal slug carries
no upper case, so two legal slugs cannot differ by case alone, and an exact
comparison between two strings that have both passed refuseSlug is the same
comparison a folded one would make.

Two fixtures prove it. a-record-slug-that-names-another-directory trips
exactly this refusal and no other, with a-record-slug-that-is-a-slug as the
near neighbour that differs by the smallest legal change and refuses
nothing. two-experiments-answering-to-one-slug is kept and re-pointed at
this property, because it is the evidence for the subsumption the retirement
rests on: the tree the old rule was written for is still refused.

Three deletions were executed against the guard and each reddened the suite
for the reason it names. Removing the refusal site left both fixtures
reporting the expected refusal not produced. Widening the comparison to
every declared slug reddened twenty-eight cases including both near
neighbours. Removing the clause that compares only two legal slugs reddened
a-record-slug-that-is-not-a-slug with a refusal no case expected, which is
the collision the clause exists against.

Refs #54

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex added the enhancement New feature or request label Aug 24, 2026
@iderex iderex self-assigned this Aug 24, 2026
@iderex iderex added this to the 3. The question and the answer milestone Aug 24, 2026
@iderex
iderex merged commit 228a8cd into main Aug 24, 2026
25 checks passed
@iderex
iderex deleted the check/a-record-slug-that-disagrees-with-its-directory branch August 24, 2026 18:55
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.

Refuse a record header that disagrees with itself

1 participant