From 8d6f17ad59382ee17591bcd65388d5e5430596c8 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:50:18 +0200 Subject: [PATCH] Refuse a record slug that disagrees with its directory 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> --- internal/check/slug.go | 106 +++++++++++------- .../expected | 4 + .../expected-refusals | 1 + .../near-neighbour | 1 + .../experiments/timing-test/EXPERIMENT.md | 13 +++ .../expected-refusals | 2 +- 6 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 testdata/cases/a-record-slug-that-names-another-directory/expected create mode 100644 testdata/cases/a-record-slug-that-names-another-directory/expected-refusals create mode 100644 testdata/cases/a-record-slug-that-names-another-directory/near-neighbour create mode 100644 testdata/cases/a-record-slug-that-names-another-directory/tree/experiments/timing-test/EXPERIMENT.md diff --git a/internal/check/slug.go b/internal/check/slug.go index 3983665..3e335a8 100644 --- a/internal/check/slug.go +++ b/internal/check/slug.go @@ -21,11 +21,17 @@ const ( // out of another experiment and half edited. RecordSlugIsNotALegalSlug = "record-slug-is-not-a-legal-slug" - // TwoExperimentsShareASlug refuses two experiments declaring one slug once - // case is ignored. A slug is what a reader holds when they walk back from - // a quoted result to the experiment that produced it, and two experiments - // answering to it means that walk lands in the wrong place or nowhere. - TwoExperimentsShareASlug = "two-experiments-share-a-slug" + // RecordSlugDisagreesWithItsDirectory refuses a record whose Slug names a + // directory other than the one 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 breaks the walk silently, because both + // halves exist and only their agreement is missing. + // + // The message names both strings. Which of the two is wrong decides the + // repair, and nothing in the tree can tell whether the field was mistyped + // or the directory was misnamed. + RecordSlugDisagreesWithItsDirectory = "record-slug-disagrees-with-its-directory" ) // legalSlug is the shape record 0014 fixes: lower case letters, digits and @@ -57,60 +63,76 @@ func refuseSlug(slug string) string { return "" } -// refuseSlugs holds every experiment to the shape and to being the only one -// answering to its slug. +// refuseSlugs holds every experiment to the shape and holds a record's Slug to +// naming the directory the record sits in. // -// WHERE THE THIRD RULE CAN AND CANNOT BITE, because a green run is otherwise -// read as more than it is. Slugs are compared with case ignored, and under the -// shape above two legal slugs can never differ by case alone, so the comparison -// bites today only on two experiments declaring the same slug exactly. The -// case-folding half is a guard for a pair where at least one side is already -// refused for its shape, and for the day the shape is argued again. That is why -// the fixture proving this rule is a pair of records declaring one slug rather -// than a pair differing in case: a pair differing in case trips two rules, and -// a fixture tripping two proves neither cleanly. +// WHAT THE THIRD RULE REPLACED. Until this function was rewritten, the third +// rule here was two-experiments-share-a-slug, which refused two experiments +// declaring one slug once case was ignored. It is retired rather than kept +// beside the agreement rule, because 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, which is what this refuses. Keeping +// both would have meant every tree that tripped the sharing rule tripping the +// agreement rule as well, and a fixture tripping two rules proves neither +// cleanly. The retirement is deliberate and by name; issue #54 is where it was +// argued and decided. +// +// WHERE THE SUBSUMPTION STOPS, because a green run is otherwise read as more +// than it is. The comparison below 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 from 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 not folded here and the retired rule folded it. Under the shape +// above a legal slug carries no upper case, so two legal slugs can never 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. Folding +// would only reach a pair where one side is already refused for its shape. func refuseSlugs(experiments []experiment) []Refusal { var refusals []Refusal - declared := make(map[string]experiment) for _, exp := range experiments { - if reason := refuseSlug(exp.directory); reason != "" { + directoryReason := refuseSlug(exp.directory) + if directoryReason != "" { refusals = append(refusals, Refusal{ Property: ExperimentDirectoryIsNotALegalSlug, Subject: exp.path, - Detail: fmt.Sprintf("its directory is named %q and %s", exp.directory, reason), + Detail: fmt.Sprintf("its directory is named %q and %s", exp.directory, directoryReason), }) } - if exp.declaresSlug { - if reason := refuseSlug(exp.slug); reason != "" { - refusals = append(refusals, Refusal{ - Property: RecordSlugIsNotALegalSlug, - Subject: exp.record, - Detail: fmt.Sprintf("its %s is %q and %s", FieldSlug, exp.slug, reason), - }) - } + // An absent field is never a refusal, which record 0013 fixes, and a + // record declaring none answers to its directory name and cannot + // disagree with it. + if !exp.declaresSlug { + continue } - // The slug an experiment answers to is the one its record declares, - // and the directory name where it declares none. Both are the same - // string in a record that is in order, and the comparison has to work - // on one that is not. - answered := exp.directory - if exp.declaresSlug { - answered = exp.slug - } - folded := strings.ToLower(answered) - if first, taken := declared[folded]; taken { + slugReason := refuseSlug(exp.slug) + if slugReason != "" { refusals = append(refusals, Refusal{ - Property: TwoExperimentsShareASlug, - Subject: exp.path, - Detail: fmt.Sprintf("it answers to the slug %q and so does %s, so a reader holding that slug cannot tell which of them produced a result", - answered, first.path), + Property: RecordSlugIsNotALegalSlug, + Subject: exp.record, + Detail: fmt.Sprintf("its %s is %q and %s", FieldSlug, exp.slug, slugReason), }) + } + + if directoryReason != "" || slugReason != "" { continue } - declared[folded] = exp + + if exp.slug != exp.directory { + refusals = append(refusals, Refusal{ + Property: RecordSlugDisagreesWithItsDirectory, + Subject: exp.record, + Detail: fmt.Sprintf("its %s is %q and it sits in a directory named %q, so a reader walking back from the slug reaches another experiment or nothing", + FieldSlug, exp.slug, exp.directory), + }) + } } return refusals diff --git a/testdata/cases/a-record-slug-that-names-another-directory/expected b/testdata/cases/a-record-slug-that-names-another-directory/expected new file mode 100644 index 0000000..466fe70 --- /dev/null +++ b/testdata/cases/a-record-slug-that-names-another-directory/expected @@ -0,0 +1,4 @@ +directories 1 +records 1 +experiments present +decisions absent diff --git a/testdata/cases/a-record-slug-that-names-another-directory/expected-refusals b/testdata/cases/a-record-slug-that-names-another-directory/expected-refusals new file mode 100644 index 0000000..c538782 --- /dev/null +++ b/testdata/cases/a-record-slug-that-names-another-directory/expected-refusals @@ -0,0 +1 @@ +record-slug-disagrees-with-its-directory diff --git a/testdata/cases/a-record-slug-that-names-another-directory/near-neighbour b/testdata/cases/a-record-slug-that-names-another-directory/near-neighbour new file mode 100644 index 0000000..e714134 --- /dev/null +++ b/testdata/cases/a-record-slug-that-names-another-directory/near-neighbour @@ -0,0 +1 @@ +a-record-slug-that-is-a-slug diff --git a/testdata/cases/a-record-slug-that-names-another-directory/tree/experiments/timing-test/EXPERIMENT.md b/testdata/cases/a-record-slug-that-names-another-directory/tree/experiments/timing-test/EXPERIMENT.md new file mode 100644 index 0000000..6163e4d --- /dev/null +++ b/testdata/cases/a-record-slug-that-names-another-directory/tree/experiments/timing-test/EXPERIMENT.md @@ -0,0 +1,13 @@ +Slug: throughput-test +State: asking +Question-Written: 2026-01-01 + +## Question + +Does the walk hold a record's slug to the directory it sits in? + +## Method + +It was pointed at this tree. + +## Answer diff --git a/testdata/cases/two-experiments-answering-to-one-slug/expected-refusals b/testdata/cases/two-experiments-answering-to-one-slug/expected-refusals index 1c1e0e6..c538782 100644 --- a/testdata/cases/two-experiments-answering-to-one-slug/expected-refusals +++ b/testdata/cases/two-experiments-answering-to-one-slug/expected-refusals @@ -1 +1 @@ -two-experiments-share-a-slug +record-slug-disagrees-with-its-directory