f1eaa343

Bootstrap: the spine, three checks, and the nix-ci integration

The bootstrap from `plan/bootstrap.md`: the whole spine, three checks, one per
level and per family shape, running over nix-ci, sydtest and itself.

`nix flake check` is green across 19 checks, 10 of them end to end over the Nix
builders. 192 tests pass. `scripts/corpus-run.sh` exits 0.

## The three checks

| Rule | Level | nix-ci | sydtest | Expected |
|---|---|---|---|---|
| `CommentBareTodo` | module | 33 | 2 | 33 and 3 |
| `HsGenValidInGenPackage` | package | 0 | 7 | 0 and 7 |
| `TestGenValidSpecPerGenValid` | project | 122 | 0 | about 115 |

Plus 114 modules abstained on nix-ci for Template Haskell, against an estimate
of about 100. Every number is explained in `plan/corpus.md`.

**39 findings audited by hand, 0 false positives.**

**sydtest is 2 rather than 3** for `CommentBareTodo`, and that is the one number
that disagrees. The comment hopinion stays quiet about heads a five-line block
whose later lines carry two documentation URLs. The survey pattern looks for a
reference on the marker's own line; hopinion looks in the whole comment block,
because a reason spanning three lines is one comment. Worth a look: the
disagreement is a judgement call, not a bug.

## Suppression syntax

Settled during review: `-- [allow:RuleId] reason`, with `[allow:file:RuleId]`
for a whole file. Rule ids are PascalCase and contain no colon, so the bracket
can grow a second colon-separated field later without the id ever being where
the ambiguity lands. The reason ends where the comment ends, so a long one goes
in a block comment.

`plan/annotation-syntax.md` carries the fourteen options this came from, and the
measurement behind the collision claim: marginalia claims `[check`, `[check:file`,
`[check:all`, `[check:tag`, `[check:ref`; tagref claims `[tag:` and `[ref:`; both
ignore everything else silently, including a rule id ending in `ref`. A pragma
form was ruled out because it costs `-Wunrecognised-pragmas`, which is `-Werror`
here.

The terminator answer came out of a defect the implementation hit. Without one,
`-- [allow:X] Because.` followed by `-- TODO other` is one comment: the reason
absorbs the TODO and the TODO stops existing as a comment any rule can see, so
it can neither be found nor suppressed nor weeded. A suppression is now exactly
one comment, so the marker both starts a comment block and ends one.

## Self-weeding

A suppression that has stopped being relevant fails the run. Every way one can
stop being relevant is enumerated in the README table with its exit code. The
one that is easy to miss: **exactly one suppression answers for each finding**,
so a second suppression over the same code is reported as suppressing nothing
rather than riding along on the first one's back. Also caught: a suppression in
a file no cabal component claims, which nobody was judging at all.

## What to review hardest

`Hopinion/Comment.hs` is the critical path and where the judgement calls
concentrate. `Hopinion/Extract.hs` is the other one: a fact that is wrong there
is wrong for every rule that will ever read it.

## What contact with real code settled

Ten things, listed in the README. The ones that changed the design rather than
confirming it:

- A deriving clause with no strategy is a seventh instance origin, not stock.
- `AttachedToStatement` has to carry the enclosing declaration, or the portable
  scope key is unrecoverable for a comment inside a declaration.
- An inserted suppression goes below any comment already there and directly
  above the code, because ormolu inserts a blank line above a Haddock comment
  and would detach it. That answers annotations.md's open question about
  surviving ormolu.
- A comment at the end of a `do` block falls outside its declaration's span.

## Deliberately not done

Fact fields no rule reads, the `RuleId` enum for all 125 rules, and the guide
files moving into this repository. All listed in the README with why.

## Trying it out

```
nix develop --command cabal test hopinion-test --test-options="--ai-executor"
nix flake check
scripts/corpus-run.sh
```

The nix-ci integration is on its `hopinion-bootstrap` branch: 15 lines of flake,
23 check derivations, and 155 suppressions inserted by `hopinion annotate`,
whose diff touches only comments and survives ormolu.

## Cleanup

A pass over the shape of it, after the fact.

**The file moved from the location to the span.** Both ends of a span are always
in the same file, so carrying it on each end made a disagreement between them
representable without making it mean anything. It was also most of what the fact
files weighed: nix-ci-leader's facts were 2.80 MB against 1.78 MB of source, and
over a third of that was one path written out again and again. Now 2.21 MB.
levels.md had named exactly this as the test the serialisation had to pass, and
it was failing it.

**The sum codecs are written the disjoint way**, as in nix-ci: a string for the
constructors that are only themselves, an object for the ones that carry
something. That removed the hand-rolled tag-plus-optional-field encoding from
six types, and made the golden files legible as a side effect.

**Each layer's report is built once.** The module, package and project layers
differ only in what they run over and where their suppressions live, and were
three copies of the same thirteen lines.

Net: 272 lines deleted, 238 added, with the fact files 21 percent smaller and
every number on the corpus unchanged.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 0s 2s 2s 2s 0s
Eval 2s 16s 16s 19s 0s
Build 8s 0s 10s 19s 0s
Suite 0s 19s 19s 19s 0s

Timeline

0s10s