pipe/docs/rules.md
trvon fb055c9978
Some checks failed
Lint Commits / commitlint (pull_request) Successful in 19s
Tests / Static Analysis (pull_request) Has been cancelled
Tests / Tests (pull_request) Has been cancelled
merge: bring main into the audit and state-map work
main gained six PRs (#1-#6) while the audit fixes and the state-map seam
were in progress. Four of main's commits fixed the same bugs this branch
did, independently; where they overlap, the more complete version wins:

- Fingerprints: main's (a0844bb) - catchSite (`member#index` among all
  catches), union/owner as identity, a legacy fingerprint so version-1
  baselines still match, baseline "version": 2. This branch's `catch`
  ordinal is dropped; its fingerprint_test still guards the result.
- verify --rule: main's verifyAnalysis (cf9e0b7), which also reports
  findings in skipped files as NOT SCANNED.
- Unreadable SDK: both kept - this branch's up-front probe gives the
  early, specific error; main's catch around the analyzer (d6b6bcb) is
  the backstop.
- Riverpod: main's "a ref.listen is not a render" (f6411dc) and this
  branch's AsyncError patterns / stored-value tracking compose; the
  fixture keeps both sets of cases.

Ported onto the model-only rules (the seam), since main edited code this
branch had moved into front-end facts:
- excerpt.dart's literal-free, bounded quoting (a prompt-injection
  guard) now also covers front-end facts: SwallowedFailure.handler and
  NonSubscribingRead.excerpt, and the C++ front end elides string
  literals in handler evidence the same way.
- ScanCoverage: runChecks stays source-free; the model carries coverage
  (`scanCoverage` in the state JSON, written by both front ends), so
  `check --model` states coverage like a source check.
- main's softened wording (rendered-not-emitted "probably unreachable",
  the remove-policy dead-handler text, notifier-field-unread) and the
  backticked flag message.

Docs: main's README grew the Firestore query inventory and the output
contract; the README stays short, the inventory moves to
docs/queries.md, and the contract, exit codes, verify and baseline
details join docs/usage.md.

Checked against origin/main on resolved mirrors of the dogfood apps:
nuwayze (102), dex (0) and word_alive (0) identical; tick differs only
by the intended riverpod stored-value fix. On the fixture every
difference is one of this branch's fixes. 214 tests pass.
2026-09-24 07:33:53 -06:00

83 lines
4.7 KiB
Markdown

# Rules
Every rule states a factual, whole-program claim ("no UI code matches this
state"), scoped to the code that was scanned. Evidence is `path:line:` plus a
bounded source excerpt with string literals elided. Precision below is
**factual** precision: how often the claim holds, not how often it is worth
acting on. A rule that cannot be validated is demoted or removed.
## Evidence per rule
Measured on the dogfood apps (nuwayze, dex, word_alive, tick).
| Rule | Evidence |
| --- | --- |
| `emitted-not-rendered` | 79/79 claims factual on nuwayze main (adjudicator with passing controls). One finding per unobserved union and per unobserved marker class. Drops to info when the UI reads the union's payload through base-class fields. |
| `rendered-not-emitted` | Fixture contract; suppressed with a reason when the owner emits through helper methods. Worded as "probably unreachable": only the scanned code is known. |
| `loading-not-reset` | Fixture contract; the dogfood apps guard correctly. |
| `loading-flag-not-reset` | 7/7 on dex, hand-adjudicated. |
| `event-without-handler` | Fixture contract; 168 dispatch sites on nuwayze all map to a handler. Follows the event hierarchy (`on<Base>` covers subtypes) and blocs sharing an event type. |
| `dead-handler` | 77/77 on nuwayze (adjudicator). The message names the directories scanned; events built only in tests are not features. |
| `riverpod-async-gap` | 10 hand-adjudicated findings (word_alive, tick). Recognises `.when`, `AsyncError`/`AsyncLoading` patterns and values stored in locals. `bool` providers are downgraded to info. |
| `notifier-never-notifies` | Fixture contract only; no real-code true positives yet. Limited to observable fields; lifecycle hooks excluded. |
| `notifier-inert-consumer` | Fixture contract only. |
| `provider-read-in-build` | 1 finding from 447 `context.read` calls on nuwayze. Reads inside closures are excluded by design. |
| `notifier-field-unread` | 3 on nuwayze; medium confidence, since a name match elsewhere can mask an unread member. |
| `provider-created-in-build` | Info, once per build method: the claim is factual, but impact depends on how often the widget rebuilds. |
## A factual finding is not always a bug
`emitted-not-rendered` at high confidence can be deliberate: dormant scaffolding
or a retired feature. Those findings are one decision per union, and the
remediation asks to check the repo's docs and action contracts before deleting
anything.
## How precision is measured
`tool/adjudicate.py <repo>` re-derives the two highest-volume claims
(`emitted-not-rendered`, `dead-handler`) with its own regex evidence, and prints
controls first: it must find evidence for every variant pipe calls rendered and
every event pipe calls dispatched. Controls are three-way (found /
unattributable / missing), and only *missing* fails. A corpus with no control
material reports **NO SIGNAL** and undefined precision. The script exits
non-zero whenever it cannot certify.
Limits: it is only partly independent (the list of variants, events and owners
to check comes from pipe's `state` output), it judges the factual claim only,
not severity, and it needs a clean working tree.
## Recall
1. **Rule contracts.** `test/rule_contract_test.dart` requires each rule to fire
on a positive control and stay silent on an adjacent negative control. A rule
without both fails the build.
2. **Labelled real bugs.** `pipe recall` evaluates `recall/manifest.json`. An
`open` case must be found; a `fixed` case must stay quiet, and reappearing is
a **REGRESSION**. Repos missing from the machine are *skipped*, never passed.
Results reflect each repo's working tree.
The labelled set is small: it bounds the risk of false negatives, it does not
prove their absence.
## provider and ChangeNotifier
Modelled: every provider-package declaration type (`Provider`,
`ChangeNotifierProvider`, `ListenableProvider`, `ValueListenableProvider`,
`FutureProvider`, `StreamProvider`, `StateProvider`, `ProxyProvider` and
variants, `.value`), and `Consumer`, `Selector`, `ListenableBuilder` and
`AnimatedBuilder` as consumers. Declarations are told apart from Riverpod by
`create:`/`update:`/`value:` arguments, not by import, so an unrelated class
named `Provider` taking `create:` would be miscounted.
Not modelled: `ProxyProvider` `update` functions returning unchanged values, and
rebuild frequency.
## Other limits
Not modelled: `bloc_concurrency` transformers, freezed unions whose generated
file is absent, and `ref.read` call sites.
pipe deliberately does not reimplement per-file checks the ecosystem already
does well: `unawaited_futures`, `use_build_context_synchronously`,
`cancel_subscriptions`, `close_sinks`, `bloc_lint`, or declaration-level dead
code (`undead`, `dallow`).