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

4.7 KiB

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).