eaaad3ed

Say what a path is, and add the rule that holds us to it

Stacked on #14, which typed the paths this holds the repository to. Review that first; this diff is against it.

`HsNoFilePath`. Everything it reports is a module's own signature naming the type where `path`'s `Path` would say whether it holds a file or a directory and whether it is absolute or relative.

11 files, 154 lines. 181 tests, 3 of them new.

## Read off the tokens

Not the parse tree. The token stream is where a name the code wrote is already told apart from the same word elsewhere: a comment saying `-- a FilePath is a String`, the string literal of the same sentence, and the tail of `import System.FilePath` are each their own token. `good.hs` holds all three and its golden is empty.

That is also why this is not a grep.

## Every finding is suppressible where it is

Each one is scoped to the declaration it is inside, so the report offers a suppression for that declaration and nothing wider:

```
[error HsNoFilePath]: The type FilePath. Say what the path is with Path from path.
     ╭──▶ hopinion-gen/test/Hopinion/AnnotationSpec.hs@28:16-28:24
  28 │ resourceDir :: FilePath
     •                ┬───────
     │ Hint: To suppress, write this directly above line 28 of ... : -- [allow:HsNoFilePath] <reason>
```

Converting at the edge is not reported at all. A library that wants a `String` gets `readFile (toFilePath file)`, and that is correct.

## The fact it reads is new

Nothing recorded which capitalised names a module writes, so `NameFact` arrives with it: the `conid` pass in `Parse`, the field on `ModuleContext`, and the declaration lookup in `Extract` that says where an annotation about one would have to go. Three files, 74 lines. That is the whole of the machinery, and a name-occurrence fact is reusable by other rules in the catalogue.

## What it does not touch

The repository already passes, because #14 converted it. Nothing here changes existing code beyond the one line that registers the rule.

## What is deliberately not here

The same rule over `import System.FilePath` and `import System.Directory`. An import is not a declaration, so the comment attachment pass has nowhere to attach a suppression above one, and the only thing left is a file-scoped suppression, which would exempt every other finding in that file. A rule whose findings cannot be suppressed where they are breaks the rule #10 is built on: a suppression the report offers must be one the parser accepts. That wants an `AttachedToImport` first, and it is its own change.

`filepath` therefore survives in two places, both genuine gaps in `path`: cabal hands module names back as `[String]`, so `joinPath` is needed at that boundary, and turning a directory's name into a file's name has no typed equivalent. Those two are exactly where a dependency-based rule would want a per-site suppression.

One thing to settle: this cites `haskell-style.md`, "Preferred libraries", which does not currently say it. The guide wants the line, or the rule is citing something that is not there.

## What it costs elsewhere

57 findings in nix-ci, 47 in sydtest, 10 in centjes. None of them is the exception the rule stays quiet about, so all three are real conversions rather than suppressions to write.

nix-ci turns the rule off in a new `hopinion.yaml` until those 57 are converted, which is NorfairKing/nix-ci#527. That is a decision, not a workaround: 57 suppressions would reach a passing check dishonestly, because a suppression says this one is deliberate and none of these is.

## Review loop

```
nix flake check
nix develop --command cabal test hopinion-test --test-options="--ai-executor"
```

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 53s 1s 1s 54s 53s
Eval 1m08s 10s 10s 1m18s 13s
Build 1m18s 0s 1m17s 2m36s 1m17s
Suite 53s 11s 1m42s 2m36s 2m24s

Timeline

0s1m10s