7b8d9f33

Report <> used to concatenate strings or text

Ships `HsNoSemigroupOnText`, the sixth rule.

## What it reports

A chain of `<>` with a string literal as one of its operands. The rule was
filed as tier `types` and the types tier is not built, so what ships is the
literal-operand approximation `plan/plan.md` already sanctioned for M5. M7
widens it to the chains with no literal in them, which is where `T.pack x <> y`
and `show x <> y` live.

## Two choices worth reviewing

**Read off the parse tree, not the token stream.** Every other syntactic fact in
this tool comes from the tokens, and here that would be wrong: a token before an
operator is not an operand of it, so `text "a" <> b` has a string literal beside
the `<>` while concatenating whatever `text` returns. That shape is all over the
corpus (`chunk ("Building " <> x)`, `toHtml ("Status: " <> s)`,
`textValue ("#" <> slug)`), and a token scan calls all of it a violation.

**One finding per chain, not per operator.** Two findings inside one statement
cannot both be answered: the second suppression a reader wrote would be one that
suppresses nothing. So `"at " <> path <> ": " <> msg` reports once, and
parentheses are peeled before the operands are read so `("[" <> t) <> "]"` is
one chain as well.

## Corpus

275 findings over five repositories, and none in this one, so it lands here as a
guard and as a `ratchet` for anyone adopting it.

| | |
|---|---|
| smos | 98 of 190 total findings |
| centjes | 65 of 134 |
| sydtest | 59 of 139 |
| autodocodec | 45 of 92 |
| feedback | 8 of 12 |

A sample of fifty was `<>` on a `String` or a `Text` every time. The one class
where the rule asks for something it should not is the lazy representations: a
`<>` on a `Data.Text.Lazy.Builder` or a lazy `ByteString` appends a chunk rather
than copying, so the cost half of the argument does not hold and the operator is
the type's intended API. Five sites of the 275, named in `ruleWhy` as what the
suppression is for.

## What it cost outside the rule

47 lines for the rule, but it is the first one to want a fact extraction did not
produce, so it touched five Haskell files rather than the budgeted one:
`SemigroupChain` and the `ModuleContext` field, the extraction that fills them,
the generator, the spec over the generator, and the registry. It also brings
`syb`, because finding every `<>` in a module means walking the whole expression
tree and hand-writing that over `HsExpr` would have been most of the rule. The
budget section of `plan/plan.md` records this as the trade it predicted.

Misses, all accepted: the section `("a" <>)` and prefix `(<>) "a" x` forms.

`nix flake check` passes.
configure