pipe/docs/design.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

Design

Motivation

The state bugs users feel (a spinner that never stops, an error never shown, a tap that does nothing) are whole-program facts: a state emitted in one file and never matched in any other, an event dispatched with no handler anywhere. A per-file linter cannot express them, so pipe resolves the whole project and reasons over all of it at once.

pipe is a CLI on package:analyzer, not a custom_lint or analyzer plugin: plugins have no project-wide index, flutter analyze does not report new-system plugin diagnostics, and a CLI can pin its own analyzer version.

Architecture

bin/pipe.dart                      entrypoint
lib/src/cli.dart                   commands and flags
lib/src/errors.dart                PipeException (message + remedy)
lib/src/project.dart               package validation, source discovery
lib/src/sdk.dart                   SDK discovery (Flutter-aware)
lib/src/analysis.dart              analysis contexts, per-file resolution, scan
                                   coverage, unresolved references
lib/src/pool.dart                  bounded-concurrency map over files
lib/src/excerpt.dart               bounded, literal-free source excerpts
lib/src/index.dart                 resolution health, timing, diagnostics
lib/src/model.dart                 declaration model (`graph`)
lib/src/state_flow.dart            Dart front end: the state model and its JSON
lib/src/dart_facts.dart            Dart front end: loading and construction facts
lib/src/dart_provider_facts.dart   Dart front end: ChangeNotifier / provider facts
lib/src/facts.dart                 language-neutral facts (JSON round-trip)
lib/src/rules.dart                 rules, findings, baselines (model only)
lib/src/provider_rules.dart        ChangeNotifier / provider rules (model only)
lib/src/pipeline.dart              analyse -> model -> rules
lib/src/verify.dart                baseline verification
lib/src/suggest.dart               fix order
lib/src/guide.dart                 agent brief, markdown report, yams emission
lib/src/recall.dart                labelled-bug recall
lib/src/firestore_facts.dart       Firestore query inventory (no rules)
lib/src/gateway_facts.dart         adapters for declarative query APIs
tool/adjudicate.py                 precision adjudicator
tool/cpp_front.py                  experimental C++ front end
tool/build_release.sh              native AOT build

Resolution mirrors Flutter's own tooling: the context collection is rebuilt when package_config.json changes, file changes go through changeFile and applyPendingFileChanges, and an InconsistentAnalysisException gets a fresh session.

The state model

Rules never read a syntax tree. A front end turns code into a state model: unions and variants, where each is emitted and matched, events dispatched and handled, busy paths whose failure is swallowed, and observable objects with whether their mutations are announced. runChecks takes only that model.

The model is plain JSON, and a model read back from JSON reports exactly what the in-process one does (test/state_map_roundtrip_test.dart):

pipe state . --format json > model.json   # or any front end that writes it
pipe check --model model.json             # every rule, baseline and gate

A front end for another language only has to write that JSON.

C++ front end (experimental)

tool/cpp_front.py reads a compile database with libclang (Homebrew LLVM) and maps each project enum class to a union:

  • emission: a reference that produces the value (assignment, atomic store, argument, return, initialiser); a cast from an integer is an indirect emission, which makes "never emitted" unprovable for that enum;
  • observation: a case, == or !=;
  • swallowed failure: a busy enumerator written to the method's own state, then a top-level catch that neither writes state nor rethrows.

Identity comes from clang's resolved AST, so two enums with the same name are never confused. Sources the build does not compile are recorded as unresolved.

tool/cpp_front.py <repo> --build-dir <repo>/build/debug -o model.json
pipe check --model model.json

For C++, read rendered-not-emitted as "branched on but never produced" (a dead branch, or a state a state machine never enters) and emitted-not-rendered as "produced but never branched on" (computed and ignored, or reaching output only through a name table or reflection). Rule messages still say "UI".

On yams (375 translation units, about two minutes) a hand check of 13 findings found 12 factually correct and 1 false positive, which the front end now excludes. That sample bounds the false-positive rate; it does not measure it. Controls live in test/fixtures/cpp_state and test/cpp_front_test.dart, skipped when LLVM is absent.