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.