Skip to content

Add the editor configuration and a formatting leg with its own name - #68

Merged
iderex merged 3 commits into
mainfrom
format/editorconfig-and-leg
Aug 9, 2026
Merged

iderex merged 3 commits into
mainfrom
format/editorconfig-and-leg

Conversation

@iderex

@iderex iderex commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Closes #23.

Two halves, as the issue asks. .editorconfig so the common cases are right
before anybody runs anything, and a check in the gate so the rule is refused
rather than requested. The check is its own leg with its own name.

What decides what

gofmt decides Go. It ships with the toolchain, decisions/means.md names it,
and re-implementing any part of it here would be a fight with the formatter the
compiler agrees with.

internal/format decides everything gofmt does not reach, which is most of the
tree: the HTML, the JSON, the workflow YAML and the prose. Three properties, all
of them ones the tree already satisfies rather than a preference imposed on it: a
final newline, no trailing whitespace, and no tab indent outside Go.

git ls-files | sed 's/.*\.//' | sort | uniq -c | sort -rn
     20 md
      6 yml
      2 html
      1 mod
      1 json
      1 go
      ...

Run at 5995c3e. Four of those types have no formatter that does not bring a
runtime with it, which is why the answer is a check plus an editor
configuration rather than a formatter.

It is Go for the reason decisions/means.md gives. Each property is a function
reading bytes and returning a verdict, which means it can be run against a
planted input; the same rule as a shell fragment in a workflow step has no suite
and can only be shown to bite by breaking the tree on purpose. No dependency is
added, so go.mod is untouched and there is still no go.sum.

It reads the files git tracks rather than whatever the working directory holds,
so an untracked scratch file in somebody's clone cannot red the leg.

The correction this branch carries

The first version of this change put its own workflow file in the tree,
.github/workflows/format.yml, with a gofmt step and a step running the
internal/format suite. Its comment said what it was waiting for: the single
entry point of #18, and calling that instead once it existed. #18 has since
landed on main as go run . gate, so the branch was carrying a promise it could
now keep, and merging it unkept would have added a second gofmt run to every
pull request under a name the gate's own suite does not read.

The rules did not move. internal/format, .editorconfig and the
.gitattributes widening are the same bytes as before. What moved is where the
whitespace half runs: it is the editorconfig leg of the entry point, with a
job in gate.yml like every other leg, and format.yml is deleted.

go run .
usage: go run . gate [leg...]
       go run . sources

the legs, in order: build, test, format, editorconfig, tests-reach-nothing, no-hardcoded-names, site-fetches-nothing-outside

Two legs rather than one widened leg, because a Leg runs one command and the
two halves fail for different reasons: one is the toolchain's formatter, the
other is three properties .editorconfig states. A red says which.

The line-ending half, which is the part worth getting right first

The issue names it: a formatting gate that reds only on one operating system
pushes contributors away for a reason they cannot see. The two halves get there
differently and only one of them is self-sufficient.

internal/format normalises CRLF before judging, and a test compares the
verdicts on both spellings of the same content so it cannot quietly become an
operating-system check:

go test ./internal/format -count=1 -v
--- PASS: TestCheckFileRefusesAMissingFinalNewline (0.00s)
--- PASS: TestCheckFileRefusesTrailingWhitespace (0.00s)
--- PASS: TestCheckFileRefusesATabIndentOutsideGo (0.00s)
--- PASS: TestCheckFileAllowsATabIndentInGo (0.00s)
--- PASS: TestCheckFileGivesTheSameVerdictOnCRLF (0.00s)
--- PASS: TestCheckFileIgnoresBinary (0.00s)
--- PASS: TestCheckFileAllowsAnEmptyFile (0.00s)
--- PASS: TestTrackedTreeIsFormatted (0.42s)
PASS
ok      flowfin.dev/hub/internal/format  0.783s

gofmt does not normalise, and this was measured rather than assumed. On a
working copy left with CRLF it lists files whose content is correct:

file internal/format/format.go
internal/format/format.go: ASCII text, with CRLF line terminators
gofmt -l .
internal\format\format.go
internal\format\format_test.go

What keeps that from being a Windows-only red is .gitattributes, which widens
from the JSON files to the whole tree with eol=lf. A checkout under the new
attributes is LF in the working copy as well as in the object store, whatever
core.autocrlf says locally, and gofmt then agrees with itself:

rm internal/format/format.go internal/format/format_test.go
git checkout -- internal/format/
file internal/format/format.go
internal/format/format.go: ASCII text
git ls-files --eol internal/format/
i/lf    w/lf    attr/text=auto eol=lf  internal/format/format.go
i/lf    w/lf    attr/text=auto eol=lf  internal/format/format_test.go
gofmt -l . ; echo "exit=$?"
exit=0

All run at 5995c3e on Windows with core.autocrlf true, which is the checkout
that would have shown the problem. Widening .gitattributes renormalised
nothing, because every tracked blob was already stored with LF. Nothing under
.gitattributes or internal/format has changed since, so those outputs are
still about the bytes this branch lands. The runs below are at the head.

The leg refusing something

Trailing whitespace planted on the first line of README.md, then the leg:

go run . gate editorconfig
...
FAIL    flowfin.dev/hub/internal/format  1.757s

gate examined 1 of 7 legs.
  build                         not asked for
  test                          not asked for
  format                        not asked for
  editorconfig                  FAILED: go test ./internal/format -run TestTrackedTreeIsFormatted -count=1: exit status 1
  tests-reach-nothing           not asked for
  no-hardcoded-names            not asked for
  site-fetches-nothing-outside  not asked for
gate refused: editorconfig
exit=1

The job deleted from gate.yml, which is what makes the leg visible to the
gate's own suite rather than to nobody:

go test ./internal/gate -run TestWorkflowDeclaresOneJobPerLeg -count=1
--- FAIL: TestWorkflowDeclaresOneJobPerLeg (0.00s)
    gate_test.go:235: leg editorconfig has no job in .github/workflows/gate.yml reporting as "Gate: editorconfig"
--- FAIL: TestEveryGateJobRunsTheEntryPointAndNothingElse (0.00s)
    gate_test.go:256: .github/workflows/gate.yml does not carry a step running "run: go run . gate editorconfig"
    gate_test.go:272: .github/workflows/gate.yml has 6 toolchain steps and 7 legs; a leg run twice or a check inlined in a job is a second procedure
FAIL
exit=1

Both run at a9b41a5 against a working copy restored afterwards; neither break is
in the diff. Each of the three whitespace rules also has a unit leg that plants
it in memory, so a rule cannot pass because the tree happens not to exercise it.

The whole gate, clean

go run . gate
...
gate examined 7 of 7 legs.
  build                         passed
  test                          passed
  format                        passed
  editorconfig                  passed
  tests-reach-nothing           passed
  no-hardcoded-names            passed
  site-fetches-nothing-outside  passed
exit=0

Run at a9b41a5.

What this does not do

It does not format anything. There is no formatter for HTML, YAML or Markdown
here, and adding one means adding a runtime decisions/means.md ruled out. The
check refuses; the editor configuration is what fixes it before you get there.

It does not judge a lone carriage return or an indent width. The first is not in
the tree and the second cannot be judged without parsing each language, which is
a formatter and not a whitespace rule.

Requiring the leg on main is #48 and is not done here.

No second person has read this change. What stands in place of a review is the
evidence above: the gate passing, two planted breaks refused, the line-ending
property measured in both directions, each with the command that produced it.

Closes #23.

The tree holds HTML, YAML, JSON, Go and prose, and nothing decided whitespace in
any of them. Review time goes on it and diffs carry changes nobody made.

.editorconfig is the request, and it records what the tree already is rather
than a preference imposed on it: LF, a final newline, no trailing whitespace,
four spaces except where an ecosystem uses two and except Go, which follows
gofmt rather than arguing with it.

internal/format is the refusal, and it is Go for the reason decisions/means.md
gives: a rule in a workflow step has no suite and cannot be run against a
planted input, and every property here is one function reading bytes and
returning a verdict. It judges three things gofmt does not reach, over the files
git tracks rather than whatever the working directory holds, so a scratch file
nobody committed cannot red the leg.

The Formatting workflow is the leg, with its own name, so a red says formatting
rather than something in the build and so it can be required on its own when #48
arrives.

The line-ending trap is the part worth getting right rather than discovering
later. internal/format normalises CRLF before judging, and a test compares the
verdicts on both spellings of the same content, so it cannot become an
operating-system check by accident. gofmt does not normalise and lists a Go file
whose working copy has CRLF, which is why .gitattributes widens from the JSON
files to the whole tree with eol=lf: a fresh clone is LF in the working copy
whatever core.autocrlf says locally, so gofmt agrees with itself on every
platform. Measured rather than assumed, on a working copy that had CRLF and a
fresh checkout that did not.

CONTRIBUTING.md names the format command and says which half handles line
endings which way, including what to do when gofmt lists a file nobody touched.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
iderex and others added 2 commits August 9, 2026 01:34
This branch was written before #18 landed and said so: it added
.github/workflows/format.yml with its own gofmt step and its own
`go test ./internal/format` step, and its comment promised to call the
single entry point once one existed. One exists on main now, and the
promise was still unkept, so merging as it stood would have put a second
gofmt run on every pull request under a different name and left the
whitespace rules in a job the gate's own suite cannot see.

What was wrong is the shape rather than the rules: internal/format,
.editorconfig and the .gitattributes widening are unchanged. The
whitespace half is now the `editorconfig` leg of `go run . gate`, with a
job in gate.yml like every other leg, and format.yml is gone.

This prevents two failures. A leg living outside the entry point drifts
from what a contributor runs before pushing, which is the thing #18
exists against. And a check outside gate.yml is invisible to
TestWorkflowDeclaresOneJobPerLeg, so deleting its job would have been
green.

Found by reading the branch against main before merging.

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex
iderex merged commit 4617086 into main Aug 9, 2026
20 checks passed
@iderex
iderex deleted the format/editorconfig-and-leg branch August 18, 2026 15: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.

Add an editor configuration and a formatting check with its own name

1 participant