Suppress a finding, and turn a rule off Replaces #6, which GitHub will not let me reopen: I merged it by mistake and then took it back off master. #9 is merged, so this is against master and the diff here is this PR alone. Adds `CommentBareTodo` from `comments.md`, and with it the two things that make a checker adoptable at all: a way to say no to a finding, and a way to turn a rule off. 109 files, 3,380 lines. 234 tests. ## The keystone **A suppression that answers for nothing is an error.** That is what makes on-by-default with unlimited local escapes safe, and it is why there is no config file of exceptions that nobody reads. All of these fail the run in their own right: - one that has outlived the finding it was written for - one that answers for more than one finding at once - one naming a rule nothing answers to, or a rule this repository turned off - one with no reason - one written where it cannot attach to anything - one in a file no cabal component claims, which is watched by nobody **A suppression the report offers must be one the parser accepts.** The report tells you what to write and where. If the next run then rejected it, the tool would have lied. ## Which rules run `hopinion.yaml` at the repository root, not a flag, and there is no flag anywhere. The development loop and CI read the same file, so they cannot disagree about which rules they ran. Every way of getting that file wrong is refused rather than ignored, because each one reads as a rule turned off and behaves as a rule still running: an unknown key, a name that is not a rule id, a rule nothing answers to, a document that is not a mapping. **The whole file goes through the codec.** Nothing here reads a key or a value for itself. Even the list of keys the file may hold comes off `jsonObjectSchemaViaCodec` rather than being written down beside the codec, because a second list is a second answer: a field added to one and not the other is a setting the codec accepts and the reader refuses. A property asserts that whatever the codec writes, the reader reads back, and blanking that derived key list fails it. `ChoicesError` says which of the four things went wrong; `ChoicesFileError` pairs it with the file it is about, and one renderer turns that into the sentence a reader is shown. `parseChoices` is handed bytes and no path, so what a file means cannot depend on what it is called. ## What this rests on The comment attachment pass, computed here in an explicit pass over spans rather than taken from where exact-print annotations put a comment. That placement answers "where must this be reprinted", not "what is this about", and the two differ exactly where it would matter. The most consequential decision in it: **a blank line between a comment and the code below means the comment is not attached to that code.** ## Still not here One level, no fact store, nothing reads what the compiler wrote down. Neither rule needs any of it. weeder removed `readChoicesFrom` on the way in, because the flag it exists for belongs to a command that does not exist yet. ## Review loop ``` nix flake check nix develop --command cabal test hopinion-test --test-options="--ai-executor" ```