Skip to content

Read a root file a document links, which the paths leg could not see - #165

Merged
iderex merged 1 commit into
mainfrom
invariants/a-root-file-a-document-links-and-the-tree-does-not-hold
Aug 22, 2026
Merged

Read a root file a document links, which the paths leg could not see#165
iderex merged 1 commit into
mainfrom
invariants/a-root-file-a-document-links-and-the-tree-does-not-hold

Conversation

@iderex

@iderex iderex commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #162.

What this changes

The invariants paths leg gains a second reading, and the pattern it already had
is untouched.

pathInProse requires a leading segment naming a directory of this tree, so a
file at the root was never read as a path at all, wherever a document wrote it.
The repair is not a wider pattern. What separates a path from a word at the root
is the parentheses: a name inside a link is somebody saying they expect a file to
be there, and the same name in a sentence is a word.

So check.LinkTargetsWithoutADirectory reads the markdown link targets that name
a file beside the document, the leg joins each one to the directory its own
document sits in, and both readings feed the one refusal site that was already
there. A document that writes a path in a sentence and links it as well is one
dead pointer and is refused once.

Joining to the document's own directory rather than to the root is what this tree
requires rather than a nicety. docs/privacy.md links supply-chain.md and means
docs/supply-chain.md; resolving that against the root would refuse the file that
is there and pass the one that is not.

What failure it prevents

A green run over README.md read as every pointer in that file resolving, and it
was not that. The leg was exact on a path one directory deep and blind on a path
at the root, and the difference was invisible in its output.

That is a removal this branch's own history carries. README.md linked LICENSE,
both went in one commit, and had only the link survived nothing here would have
said so.

The measurement from the issue, run on this branch with one line appended to
README.md and taken off again afterwards:

[a root file that is not here](A-FILE-THAT-IS-NOT-HERE.md)

go test ./internal/invariants -run TestThisRepositorySatisfiesTheInvariants -count=1 -v
          paths this repository's own documents name: 34 examined
        1 refused
          ..\..\README.md: it names A-FILE-THAT-IS-NOT-HERE.md, which is not in this tree (document-names-a-path-that-does-not-resolve)
--- FAIL: TestThisRepositorySatisfiesTheInvariants (0.12s)

The proof that it bites

Deleted and watched go red. With the link reading taken out of the leg and
everything else in place:

go test ./internal/invariants -count=1
--- FAIL: TestCases (0.01s)
    --- FAIL: TestCases/a-document-linking-a-root-file-that-is-not-there (0.00s)
        invariants_test.go:159: expected refusal not produced: document-names-a-path-that-does-not-resolve
FAIL

Three cases, which are the three the issue asks for.
a-document-linking-a-root-file-that-is-not-there refuses exactly
document-names-a-path-that-does-not-resolve and no other property.
a-document-linking-a-root-file-that-is-there is its near neighbour, the same
tree with the file added, and refuses nothing.
a-root-file-name-in-a-sentence-rather-than-a-link names the same two files in a
sentence, neither of them in the tree, and refuses nothing.

The third case earns its place by being one character pair away from a refusal.
Putting parentheses around one of those names turns it red:

-this tree yet and no CHANGELOG.md either
+this tree yet and no [LICENSE](LICENSE) either

go test ./internal/invariants -count=1 -run TestCases
--- FAIL: TestCases/a-root-file-name-in-a-sentence-rather-than-a-link (0.01s)
    invariants_test.go:159: refusal produced that no case expected: document-names-a-path-that-does-not-resolve

internal/check/paths_test.go covers the arms no fixture reaches: a fragment, a
scheme, an address, a target under a directory, a backslash, an empty target and
a parenthetical following a bracketed phrase. Each of those arms fails silently
if it stops working, which is the same outcome as an arm that never existed.

One fixture changed and why

no-intended-use-notice is a tree with no NOTICE.md whose readme pointed at it
with a link. Once the leg can see that link, that tree trips two rules, which is
correct behaviour and is not something a case declaring exactly one property can
carry. Its readme now names the notice without linking it. The readme half of the
notice leg asks that the readme names the notice rather than links it, so the case
still exercises what it always exercised.

What was run

Everything CONTRIBUTING.md names, at 2639376a04a9771cf4d7fe0d99c08db676cd5b9f,
with the working tree clean.

go build ./cmd/... ./internal/...
go vet ./cmd/... ./internal/...
gofmt -l cmd internal
(no output from any of the three)

go test -count=1 ./cmd/... ./internal/...
ok  github.com/Flowfin/lab/cmd/contexts     0.452s
ok  github.com/Flowfin/lab/cmd/lab          1.652s
ok  github.com/Flowfin/lab/cmd/notices      6.182s
ok  github.com/Flowfin/lab/cmd/pullrequest  0.462s
ok  github.com/Flowfin/lab/internal/check   0.720s
ok  github.com/Flowfin/lab/internal/contexts        0.431s
ok  github.com/Flowfin/lab/internal/hardware        0.459s
ok  github.com/Flowfin/lab/internal/invariants      0.823s
ok  github.com/Flowfin/lab/internal/notices 0.422s
ok  github.com/Flowfin/lab/internal/prose   0.483s
ok  github.com/Flowfin/lab/internal/pullrequest     0.463s

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-22T04:03:09Z
0 refused

The suite was also run with -v. The integration-hardware harness reports that
it was not asked for and nothing in it ran, which is unchanged by this and is the
ordinary state of a run on this machine.

Read by one pair of eyes

This board has no second reader for this change tonight. The two runs above, the
deletion that reddens the suite and the one-character near miss stand in place of
one.

What this does not do

A link target carrying a directory is left to the pattern that was already there.
That pattern reads such a path wherever a document writes it out and never
resolves it against the document's own directory, so a link from inside docs/
whose target starts with docs/ is read as the path it spells rather than as the
place a reader lands. That is the state this leg was already in and this change
neither widens nor repairs it.

A target with a scheme in it is somebody else's server. Nothing here can say
whether a file on one exists, and a refusal for a document on somebody else's
machine is the annoyance that gets a check switched off.

Nothing about the licence declaration moves. DeclaredLicence in
internal/invariants/invariants.go is still the empty string and the licence leg
still reports that it was not asked, which is #47.

Closes #162.

The invariants leg holds this repository's own documents to the paths they name,
and it could not see a file at the root. pathInProse requires a leading segment
naming a directory of this tree, so LICENSE, README.md, NOTICE.md, DCO, go.mod
and the dotfiles were never read as paths at all, wherever a document wrote
them. The leg says at itself that it holds documents to the paths they name and
said nothing about the paths it could not see, so a green run over README.md
read as every pointer in it resolving, and it was not that.

That is the shape of a removal already on this branch's history. README.md
linked LICENSE, both left the default branch in one commit, and had only the
link survived nothing here would have said so.

What separates a path from a word at the root is the parentheses. A name inside
a link is somebody saying they expect a file to be there; the same name in a
sentence is a word, and a pattern general enough to read A-FILE.md as a path
also reads a capitalised word, a version string and the last segment of a URL as
one. So the leg gains a second reading rather than a widened pattern: a link
target with no directory in it is joined to the directory its own document sits
in and resolved, and a bare word is left to the reading that was already there.
Joining to the document's own directory rather than to the root is what the
tree requires: docs/privacy.md links supply-chain.md and means
docs/supply-chain.md.

Both readings feed one refusal site, so a document that writes a path in a
sentence and links it as well is one dead pointer and is refused once.

Proved by deleting it. With the link reading removed and everything else in
place:

    go test ./internal/invariants -count=1
    --- FAIL: TestCases/a-document-linking-a-root-file-that-is-not-there
        invariants_test.go:159: expected refusal not produced: document-names-a-path-that-does-not-resolve

Its near neighbour is the same tree with the file added, and the third case is
the same names in a sentence rather than in a link. Putting the parentheses
around one of those names is what turns that case red, so the two differ by the
link alone.

One fixture had to change to keep proving what it proves.
no-intended-use-notice is a tree with no NOTICE.md whose readme pointed at it,
and that readme now names the notice without linking it. The tree it described
trips two rules once this one can see it, which is correct behaviour and not
something a single-property fixture can carry. The readme half of the notice leg
asks that the readme names the notice rather than links it, so the case is
unchanged in what it exercises.

What this does not do. A link target carrying a directory is left to the pattern
that was already there, which reads such a path wherever a document writes it
out rather than resolving it against the document's own directory. A target with
a scheme in it is somebody else's server and nothing here can say whether a file
on one exists.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit 5afb7f2 into main Aug 22, 2026
25 checks passed
@iderex
iderex deleted the invariants/a-root-file-a-document-links-and-the-tree-does-not-hold branch August 22, 2026 04:07
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.

A document naming a root file that is not in the tree is not refused

1 participant