Found by dogfooding on nuwayze main: `pipe verify` against the ratchet
baseline printed
resolved 1 no longer reported - these fixes are confirmed
+ service_browse_bloc.dart:20 LoadServicesByArtist [dead-handler]
but nothing was fixed. nuwayze commit ccca9bd1e ("delete dead firestore
query code") deleted the event and its handler. Under pipe's default
wire-up policy, deleting an unreachable handler is the exception that
needs approval, so calling it a confirmed fix overstated what happened.
verify now separates the two. A gone finding is REMOVED when its
subject no longer exists:
- its file is not among the project's files;
- for rules labelled by a declaration (the event/state/union/provider or
owner class), that name is no longer declared anywhere;
- for dead-handler, the on<Event> registration itself is gone.
Everything else stays RESOLVED. Member-labelled rules (a method or
build) are classed by file only, never guessed. Removed entries still
count as gone for --expect-resolved; they are only named honestly, in
text ("removed N gone because the code was deleted - not wired up",
a REMOVED section) and JSON (removed, removedEntries).
On nuwayze main the entry now reads REMOVED. Tests: a unit test over
hand-built entries (a wired handler stays resolved, a deleted handler
and a deleted file are removed, a member label is not guessed), and an
end-to-end test that deletes a fixture's dead handler and runs
verifyAnalysis. 216 tests pass.
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.
check never looked at analyzer errors, so a project that resolved only
partly was analysed as if it were whole. pipe reads names off the syntax
tree, and an unresolved `Foo()` is a method call rather than a
construction, so a dispatch, emission or construction in such a file
simply disappears - and every finding is an absence claim that the
missing evidence could refute.
Found live during this work: with the Flutter SDK unreadable, nuwayze
resolved with 40,079 analyzer errors (package:flutter unresolvable) and
check still reported 102 findings at full confidence, with no warning.
Now, when any analysed file has an unresolved-reference diagnostic
(uri_does_not_exist, undefined_*, creation_with_non_type, ...):
- check warns on stderr and adds a report note naming the first one;
- a finding is lowered one confidence step, with
data.confidenceLowered = unresolved-references, when an unresolved file
mentions one of its identity names (event, variant, union, owner,
provider, member). pipe's evidence is name-based, so a file that never
names X cannot hide evidence about X; lowering everything for one stray
error would bury the signal (a first cut did exactly that, taking all
102 nuwayze findings down for a single unresolved file).
Validated on mirrors of the dogfood repos with a readable SDK (0 analyzer
errors): nuwayze 102 findings identical to the original main, dex 0,
word_alive 1 and tick 1 (the intended riverpod change). The fixture
test adds an unresolved file that constructs NeverDispatched: its
dead-handler finding drops to medium while UnhandledTap, which that file
never names, stays high.
An SDK directory that exists but cannot be read passed discovery, and the
analyzer then died with a raw FileSystemException stack trace. This
happens for real under macOS privacy protection: a Flutter SDK under
~/Documents stats fine but every open fails, and every app whose
package_config.json names that flutterRoot crashed:
Unhandled exception:
FileSystemException(path=.../dart-sdk/lib/_internal/allowed_experiments.json; ...)
The chosen SDK is now probed before the analysis collection is built, and
an unreadable one is a PipeException naming the path, where it was
discovered from, and the --sdk remedy.
The last-resort fallback to the running Dart SDK took the executable's
directory (<sdk>/bin) as the SDK root, which never contains
lib/libraries.json, so it could never succeed. sdkFromExecutable now
maps <sdk>/bin/dart to <sdk>.
ae412a7 routed filesystem failures to exit 2, but the analyzer throws its own
FileSystemException (package:analyzer/file_system), which the dart:io catch
in bin/pipe.dart never matched. A project whose package_config still named a
moved Flutter SDK therefore crashed with exit 255 and a stack trace. Found
by dogfooding on word_alive after its SDK moved.
Analyzer.create now turns that into a PipeException naming the SDK path, with
the remedy to re-run flutter pub get or pass --sdk.
A report listed findings without saying how much of the project they were
drawn from, so "0 findings" from 3 of 40 files looked like a clean project -
and an agent handed a brief had no way to know otherwise.
check, guide and suggest JSON now carry a `coverage` block
{discovered, selected, resolved, failed, skipped, complete}. The guide brief,
the markdown report, the suggest agent plan and both text outputs open with a
one-line coverage note, which says PARTIAL whenever a discovered file was not
analysed.
The note lives in the brief's header, which budget trimming never touches. The
"Coverage (what was actually examined)" caveats section was the first thing
dropped under a tight budget, silently; when it no longer fits, a one-line
pointer now says how many caveats were left out.
An unreadable file is handed back by the analyzer as a ResolvedUnitResult with
EMPTY content and zero declarations - not an error, not a failure. pipe counted
that as a successful resolution, so a project whose files could not be read
produced:
$ pipe index /tmp/unreadable
files 12/12 resolved (12 discovered, 0 generated, 0 skipped, 0 failed)
$ pipe check /tmp/unreadable
findings 0 exit 0
Verified directly: the same 12 files give `contentLen=0 decls=0` when
unreadable and `contentLen=722 decls=2` when readable, with `failure=null` in
both cases. So `check` reported a clean bill of health for a project it had
extracted nothing from - the same silent-success family as the empty scan and
the truncated scan.
Two changes, both at points where the information exists exactly once:
- the resolution loop detects a unit with empty content for a file that has bytes
on disk (a genuinely empty file is still fine) and records it as a failure, so
`filesFailed` becomes reachable and `index` accounts for it:
`11/12 resolved (12 discovered, 0 generated, 0 skipped, 1 failed)`;
- the outcome summary warns when some files failed and REFUSES when every file
failed, so a partial result can never be mistaken for a complete one:
pipe: WARNING 1 file(s) could not be analysed (11 resolved), so results are PARTIAL.
pipe: no file could be analysed: all 12 file(s) failed to resolve
The normal fixture is byte-for-byte unaffected (no warning, same 20 findings),
so this adds no false positive on readable projects.
Found by probing whether an all-files-fail run was reachable; a chmod-000 corpus
made it reachable and exposed that it had been silently scored as success.
106 tests (was 104), dart analyze clean.