Skip to content

Refuse a removal the pull-request body never names - #198

Merged
iderex merged 1 commit into
mainfrom
gate/a-removal-names-itself
Aug 26, 2026
Merged

iderex merged 1 commit into
mainfrom
gate/a-removal-names-itself

Conversation

@iderex

@iderex iderex commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Closes #155

What this changes

path-removed-without-being-named refuses a change that takes a tracked path
out of the tree when the pull-request body never names that path. Removing stays
allowed. Removing in silence is what ends, and the refusal message says so,
because a rule that reads as forbidding removals is a rule somebody argues with
instead of satisfying.

It lives in internal/pullrequest/ because that is the only place it can. Every
other check on this board reads a checkout: the record checks walk
experiments/, the invariants read tracked text, the prose rules read the bytes
around the words. All of them read what the tree became, and a result that
compiles and passes says nothing about what it replaced. This package holds both
ends of the range, which is what makes a removal visible at all.

The boundaries are written at the rule rather than left to be discovered. The
source path of a move that git reported as a rename is not judged, because the
entry carries both paths and the change already says where the content went. A
move nothing reported as one, which is what a rename made together with a
rewrite looks like, is an ordinary removal here and has to be named. A path
added and removed inside one branch never reaches the rule, because the range is
read from base to head. And the comparison asks whether the body carries the
path, never whether what it says about it is true.

Four cases that already existed removed a path under a body that never named it,
so those bodies now name it. The single field separating each of them from a
case that refuses nothing is unchanged, and each still refuses exactly its own
property.

.github/pull_request_template.md asks for the sentence where an author writes
it. A rule met for the first time at a red gate is a rule that reads as an
obstacle, and this one is one sentence away from satisfied.

Means: Go, in the package that already holds both ends of the range and already
reads the body, judged by the table-driven harness the other rules in it use. It
adds no language, no runtime and no dependency, and it needs no new reader,
because read.go already reports a removed path and reports a rename as one
entry carrying both paths.

What failure it prevents

The incident this issue records, exactly. A branch was cut from an older state
of the default branch and pushed on top of a newer one. The merge replaced seven
paths with what they had said earlier, taking a licence, a guard and four
hundred lines of a document, and the body described a change to how one workflow
pin is commented, because that is what its author believed the change did.
Nothing was red afterwards, because almost everything removed was prose and the
one test that went with it was the only thing reading the code path it covered.

What the rule converts is that a removal nobody wrote down becomes a removal
somebody had to write down. It cannot tell a true sentence from a false one, and
that bound is stated at the rule. It is worth having anyway, because the case
where nothing at all was written is the case that happened, and it is the case a
string comparison can separate.

What was run

At 43620deeadc6cd4f81f2467fc7c41bef7cde0b67, from the root of the checkout:

$ go build ./cmd/... ./internal/...

$ go vet ./cmd/... ./internal/...

$ gofmt -l cmd internal

$ 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-26T02:57:59Z
0 refused

$ go test -count=1 ./cmd/... ./internal/...
ok  	github.com/Flowfin/lab/cmd/contexts	3.951s
ok  	github.com/Flowfin/lab/cmd/lab	17.654s
ok  	github.com/Flowfin/lab/cmd/notices	46.721s
ok  	github.com/Flowfin/lab/cmd/pullrequest	3.941s
ok  	github.com/Flowfin/lab/internal/check	4.220s
ok  	github.com/Flowfin/lab/internal/contexts	3.199s
ok  	github.com/Flowfin/lab/internal/hardware	3.259s
ok  	github.com/Flowfin/lab/internal/invariants	3.572s
ok  	github.com/Flowfin/lab/internal/notices	3.197s
ok  	github.com/Flowfin/lab/internal/prose	3.252s
ok  	github.com/Flowfin/lab/internal/pullrequest	3.241s

The guard was proved by deleting it, at both places it can be lost. With the
refusal site disabled and nothing else changed:

$ go test ./internal/pullrequest -count=1
--- FAIL: TestJudge/a_change_that_removes_a_tracked_path_and_never_names_it
        refusal expected and not produced: path-removed-without-being-named
--- FAIL: TestJudge/a_change_that_removes_two_paths_and_names_only_one
        refusal expected and not produced: path-removed-without-being-named
--- FAIL: TestJudge/a_rename_nothing_reported_as_one
        refusal expected and not produced: path-removed-without-being-named
--- FAIL: TestEveryPropertyHasACaseThatRefusesIt
        no case refuses path-removed-without-being-named, so nothing in this suite has seen it bite
--- FAIL: TestARemovalRefusalNamesThePathAndTheRepair
        expected one refusal, got 0
--- FAIL: TestNamingOneRemovedPathDoesNotCoverAnother
        expected exactly one refusal, got 0
FAIL	github.com/Flowfin/lab/internal/pullrequest	1.024s

and with the refusal site intact and the one dispatch line in Judge removed
instead, which is the way a rule is lost while the store still declares it:

$ go test ./internal/pullrequest -count=1
--- FAIL: TestJudge/a_change_that_removes_a_tracked_path_and_never_names_it
        refusal expected and not produced: path-removed-without-being-named
--- FAIL: TestJudge/a_change_that_reads_no_body_and_removes_a_path
        skip expected and not produced: path-removed-without-being-named
FAIL	github.com/Flowfin/lab/internal/pullrequest	1.128s

Both were restored and the suite above is the run after restoring them. Each
refusing case has a near neighbour that refuses nothing and differs from it by
the body alone, so what the fixtures prove is the rule rather than the diff.

This change removes no tracked path.

What this does not do

It does not ask whether the sentence in the body is true, or whether it is about
the removal at all. A body that names the path while claiming the opposite
passes, and so does one that names it for an unrelated reason. That is the same
bound the issue-reference rules in this package already carry, and a reviewer is
what stands behind the rest.

It does not see a removal that arrives without passing this gate. A direct push
is refused by the ruleset on the default branch, which requires a pull request,
and an edit made through the web interface or history rewritten on the branch is
outside what this reads. Those are named at the rule so a green run is not taken
for more than it is.

It does not restore anything. The six path groups this issue is also about were
restored earlier under the issues that argued them, and the per-path comparison
recording that is in this issue's own comments.

It does not narrow what may be removed. Nothing here forbids a removal, and the
existing rules over a record that was already on the branch are untouched: those
still refuse a removed or renamed record outright, for their own reason, and a
change doing that now refuses two properties rather than one.

This board has no second reader tonight. What stands in place of one is the
deliberate breaking of the guard at both sites above, the near neighbour beside
every refusing case, and the full suite at the pushed commit.

One merge on the default branch removed landed work on seven paths under
a message describing a change to how one workflow pin is commented. A
licence, a guard and four hundred lines of a document went and nothing
was red, because every other check here reads what the tree became and a
result that compiles and passes says nothing about what it replaced.
#155 records that incident, and the decision it stood open on was taken
on 2026-08-24: removing stays allowed, removing in silence ends.

path-removed-without-being-named refuses a path the range takes out of
the tree that the pull-request body never names. The repair is a
sentence rather than putting the file back, and the message says so,
because a rule that reads as forbidding removals is one somebody argues
with instead of satisfying.

This package is where the rule can live at all. Every other check reads
a checkout; this one holds both ends of the range, which is what makes a
removal visible.

The boundaries are at the rule rather than left to be discovered. The
source path of a move git reported as a rename is not judged, because
the entry carries both paths and the change says where the content went.
A move nothing reported as one is an ordinary removal here and has to be
named, which is red for a reason a reader can act on. A path added and
removed inside one branch never reaches the rule. And what the check can
separate is a body that names the path from one that does not, never
whether what it says is true.

The guard was proved by deleting it. Removing the refusal site leaves
three table cases and both tests over the message failing with the
refusal not produced, and the coverage test saying nothing in the suite
has seen the property bite; removing the one dispatch line leaves twelve
failures including the skip. Each refusing case has a near neighbour
that refuses nothing and differs by the body alone.

Four cases that already existed removed a path under a body that never
named it, so they now name it. The one field that separates each of them
from a case refusing nothing is unchanged, and the sets they compare are
still exactly one property.

The template asks for the sentence where an author writes it, because a
rule met for the first time at a red gate is a rule that reads as an
obstacle.

Means: Go, in the package that already holds both ends of the range and
already reads the body, judged by the same table-driven harness the
other rules use. It adds no language, no runtime and no dependency.

Closes #155.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex self-assigned this Aug 26, 2026
@iderex
iderex merged commit e2b57c0 into main Aug 26, 2026
25 checks passed
@iderex
iderex deleted the gate/a-removal-names-itself branch August 26, 2026 03:04
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.

Landed work on seven paths left the default branch under a commit that describes a workflow pin comment

1 participant