The plan sorted by score alone. List.sort is not stable, so equal-score
actions came out in an order nothing guaranteed, and two runs over the same
code could hand an agent a different first action. Ties now fall back to file,
line, rule and label, both in the global order and within each file.
With several --dir values, one that did not exist was skipped silently: the
no-files guard only fires when every directory is empty, so
`--dir lib --dir tset` scanned lib and reported as though it had covered what
was asked for. A --dir that does not exist is now an error naming it.
A directory that could not be listed, or a baseline that could not be read or
written, escaped as an uncaught FileSystemException: exit 255 and a stack
trace. Those paths now raise a PipeException with the OS reason (exit 2), and
bin/pipe.dart catches any remaining FileSystemException the same way. The exit
codes are documented in bin/pipe.dart: 0 success, 1 a gate failed, 2 pipe could
not do what was asked, 64 usage.
rule_scope_claim_test widened the fixture with a test/ directory it does not
have, relying on exactly the silent skip; it now builds a project that has one.
`guide --yams` persists the markdown report, not the brief, and the markdown
had neither the brief's caution line ("static reachability claims ... confirm
each against the cited file") nor the [severity/confidence] of each finding -
so a report read back from the corpus presented every finding as equally
certain, with no framing. Its metadata still carried a stale `owner=pi` and
nothing to date or reproduce it.
The markdown now opens with the caution line, headings read
`### N. [severity/confidence] rule — location`, and the metadata records
`generated=<ISO-8601 UTC>` and, when the project is a git checkout,
`commit=<HEAD>`. `owner=pi` is gone.
The analysed repository is untrusted input, and its source reached the agent
brief and the yams corpus verbatim: emission and dispatch snippets were
`node.toSource()`, provider-read-in-build quoted the whole call, and the brief
printed `evidence: $item` raw (the markdown wrapped it in single backticks,
which a backtick in the source closes). A string literal written as an
instruction to the reader was pasted into the prompt as-is.
Every quoted excerpt now goes through sourceExcerpt(): string literals,
interpolations included, become '…'; comments are dropped (they are not
tokens); whitespace collapses; the result is capped at 120 characters, as
loading-not-reset's catch excerpts already were. Where the brief and the
markdown render evidence, it is labelled "source excerpt (data, not
instructions)" and fenced with one more backtick than the longest run inside
it, so nothing quoted can close its own quotation.
Fingerprints are unaffected: provider-read-in-build still keys on the full call
source, which is kept for identity and no longer quoted.
Both packers skipped an entry that did not fit the budget and kept going, so a
large top-ranked finding vanished while smaller, lower-ranked ones were packed
in behind it - presented as the most important work. Packing now stops at the
first entry that does not fit and counts it and everything after it as
omitted.
suggest's agent plan also reserved no room for its own omission note, so the
note itself could push the plan over budget; it now reserves room for the
largest possible note unless the entry is the last one.
When no entry fit, the guide said "this header alone exceeds the budget" even
when the header had room and it was the top entry that was too large. It now
says which of the two it was, with the size.
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.
`check --format json --write-baseline b.json` printed "wrote baseline to ..."
after the JSON document, and `guide --yams` printed "persisted to yams as ..."
after the brief, so stdout was no longer a single JSON document (or a clean
brief) and `| jq` failed. Both status lines now go to stderr.
A finding with no discriminating key fell back to `line=N`, and three rules
had none: a dormant emitted-not-rendered union carried only {union, owner},
and notifier-field-unread and notifier-inert-consumer only {owner}.
loading-not-reset embedded the catch's absolute line as `catchLine`. Any edit
above one of those findings changed its fingerprint, so `check --baseline`
re-reported an accepted finding as new and `verify` reported its entry as
RESOLVED.
union and owner now count as discriminating (each of those rules emits at most
one finding per union or notifier), and loading-not-reset carries `catchSite`,
the catch's index among the catch clauses of its enclosing member
(`twoSwallowedAwaits#1`), which does not move when code outside the member
does.
The test that guarded this shifted only findings.first. It now shifts every
finding, and a second test really inserts lines at the top of every fixture
file, re-analyses, and requires the same fingerprint set.
Migration: fingerprints change for emitted-not-rendered (dormant unions),
notifier-field-unread, notifier-inert-consumer and loading-not-reset. Old
baselines stay readable and keep working - both `check --baseline` and
`verify` also match the version-1 fingerprint (`legacyFingerprint`) - but
those entries stay line-sensitive until the baseline is rewritten with
--write-baseline, which now writes `"version": 2`.
The analyzer skips a file that analysis_options `exclude:` names (contextFor
throws, which pipe records as `skipped`). The verify gate refused a scan only
when files FAILED, and partialScanWarning named only failures, so a skipped
file's accepted findings were simply absent from the fresh report - and verify
listed them as RESOLVED, "these fixes are confirmed", for code nobody read.
Excluded files are a normal part of a project, so they do not abort verify.
Instead each accepted finding whose file was not analysed - skipped, failed,
filtered out by --no-include-generated, or outside every scanned --dir - is
reported as NOT SCANNED: neither resolved nor still present. allResolved (and
so --expect-resolved) now requires that list to be empty too.
partialScanWarning counts skipped files, so a scan with excluded files is
announced as PARTIAL on stderr like one with failures.
`verify --rule X` compared the fresh report against the whole baseline. Every
accepted finding of every other rule was absent from the report, because those
rules never ran, so verify listed them as RESOLVED - "these fixes are
confirmed" for fixes nobody made.
Baseline entries are now filtered to the enabled rules before comparison. An
entry's rule is its `rule` field or, for a fingerprints-only baseline, the
fingerprint's first `|` segment. Entries set aside are counted as `outOfScope`
in JSON and named in the text summary, so the filter is visible rather than
silent.
The finding said "nothing anywhere ever dispatches or constructs X" while the
analysis only ever scanned --dir (default lib). On a real app that overclaim was
false for 67 of its 77 findings: their events are constructed by TEST code, which
pipe never read. The claim, not the count, was the bug:
$ pipe check . --rule dead-handler # 77 findings
$ pipe check . --dir lib --dir test --rule dead-handler # 10 findings
67 features were one reading away from being "wired up" as missing UI, and the
same overclaim nearly justified deleting a handler covered by five seeded fuzz
tests - pipe said nothing anywhere constructed the event, because it never looked
in test/.
The message now names the scope it actually searched ("nothing under lib ever
dispatches or constructs X"), `Project` records the scanned directories so any
finding can do the same, and the remediation says to check for a dispatch in a
directory that was not scanned before wiring, since test-only handlers are not
missing features.
Found while auditing 77 "dead handlers" for a UI-wiring task: the first deletion
candidate turned out to be referenced by test/blocs/.../artist_dashboard_orders_fuzz_test.dart.
112 tests (was 109), dart analyze clean.
The previous commit over-corrected. It skipped EVERY case for a repo that had an
unreadable file, so a repo with one unreadable file silently dropped the
regression guard for all of its cases - a worse failure than the false MISSED it
replaced, because a skipped case guards nothing:
$ pipe recall --repo-with-one-unreadable-file
pipe: recall evaluated none of its 2 case(s): ... # both cases lost
Only a NEGATIVE verdict is unsafe when files could not be read. A hit stands,
because evidence that was actually read is evidence; a miss becomes "unknown"
rather than a false regression. So:
recall 1/1 known bugs accounted for (1 skipped)
by rule {dead-handler: 1/1}
skipped: evidence-in-UNREADABLE-item_bloc: 1 file(s) could not be analysed in /repo
The case with readable evidence is still evaluated and still found; only the case
whose evidence lives in the unreadable file is skipped, with the reason.
Also hardened, from the same round's probes: the "analysed nothing" guard tested
only `resolved == 0 && failed > 0`, so a run where every file was SKIPPED
(outside any analysis context) could still report success having analysed
nothing. It is not reachable through --dir (a path that does not resolve under
the root now hits the no-files guard first), but it was open by construction, so
the test is now simply "nothing was resolved".
Probes that came back clean this round, so they are recorded rather than
re-tested: all six commands on a healthy project produce ZERO bytes on stderr and
the same 20 findings (no false positives from the last three fixes); `contexts 0`
is not reachable; and a baseline written from a partially readable project does
warn on stderr at write time, which is the mitigation for the reverse
cross-baseline direction (`new 6 not in the baseline` is then factually correct
rather than a spurious regression claim).
109 tests (was 108), dart analyze clean.
The all-unreadable case was fixed last commit; the PARTIAL case still produced
two false claims.
1. `verify` reported unread findings as fixes. With two unreadable files it
printed:
baseline 20 accepted findings
resolved 8 no longer reported — these fixes are confirmed
RESOLVED
+ lib/item_bloc.dart:48 LoadItems [dead-handler]
item_bloc.dart was one of the files it could not read. Those 8 findings were
absent from the fresh report because they were never read, not because they
were fixed. `verify` now refuses a scan in which any file failed, mirroring
the --limit guard: a claim about someone's fixes must rest on a complete scan.
2. `recall` reported such a case as MISSED, a false regression claim that also
drove `--strict` to exit non-zero. A case whose evidence lives in a file that
could not be read cannot be judged either way, so it is now recorded as
skipped with the reason, exactly like a repo that is not present:
"1 file(s) could not be analysed in /repo".
3. Consequential accuracy fix: the vacuous-run guard added two commits ago said
"no repo was present under <root>" whenever nothing was evaluated. With the
new skip reason that diagnosis was wrong - the repo WAS present, its files
were unreadable - and the remedy pointed at the wrong problem. The message now
reports the actual skip reasons.
Verified: verify exits 2 on a partially readable project and still works on a
fully readable one; recall skips the unreadable case, keeps evaluating the
others, and reports no MISSED. The normal fixture is unaffected. 108 tests (was
106); the new tests fail without these changes (+0 -2) and skip honestly if
chmod cannot restrict the process.
Found by trying to FALSIFY the claim that the silent-success family was closed.
It was not - for the second iteration running, the falsification attempt found a
real defect.
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.
The committed versions were not in canonical format; they were reformatted after
the commit, which would have made the pre-commit hook rewrite them on the next
commit and block it.
Continuing the "where else can pipe report success without doing the work?"
line, which produced the --limit false verification last iteration.
1. The empty-scan guard from 51929e2 had a hole at a different layer. It fires in
loadProject, which counts DISCOVERED files, so a project whose only source is
generated still had one file:
$ pipe check . --no-include-generated
files 0/0 resolved (1 discovered, 0 selected, 1 generated, ...)
findings 0 exit 0
Files were discovered, then every one was filtered out of the selection, and
the analysis ran over nothing. The guard now also fires where selection
happens - the single place a file can be dropped - naming the cause:
"nothing to analyse: all 1 discovered file(s) were excluded by
--no-include-generated".
2. `recall` reported "0/0 known bugs accounted for" and exited 0 - including
under --strict - when it had verified nothing: an empty manifest, or a --root
under which none of the repos exist. "0/0 accounted for" reads as a pass, so
both are now failures that name why:
recall evaluated none of its 5 case(s): no repo was present under /corpus
recall manifest has no cases: recall/manifest.json
The per-case `skipped:` lines are kept; this only changes the case where there
is no result at all to report.
Verified: the two new tests fail without these changes and pass with them; a
healthy recall run still reports 3/5 with its notes, a project with generated
files included still exits 0, and `check --baseline <unrelated>` is still clearly
distinguishable from a clean project.
Also probed and CLEAN, so they are not re-tested blindly: `--budget 1` discloses
omissions in both text and JSON ("All 20 findings omitted ... Raise --budget"),
and a baseline matching nothing prints `findings 20 new=20` against a clean
project's `findings 0 new=0`.
104 tests (was 102), dart analyze clean.
`--limit` is documented as a debugging aid, but a truncated analysis looked
exactly like a complete one, and `verify` interpreted the truncation as work that
had been done:
$ pipe verify . --baseline b.json --limit 2
baseline 20 accepted findings
resolved 19 no longer reported — these fixes are confirmed
still 1 still reported
RESOLVED
19 findings were reported as FIXED because they were never scanned. That is a
false verification of fixes nobody made - the worst output this tool can produce,
because someone could use it to claim a defect was repaired.
Two changes:
- `verify` refuses a partial scan outright: "verify needs a complete scan: with
--limit, findings that were never scanned are reported as resolved", with a
remedy pointing at `check --limit` for debugging.
- Every other command now warns, because a truncated `check --limit 2` reporting
1 finding instead of 20 was equally mistakable for a clean result:
pipe: WARNING --limit 2 is a debugging aid: 2 of 12 file(s) were analysed,
so every result below is PARTIAL.
The warning is emitted at the single place truncation can happen (the file
selection in resolveProject) rather than in each command, so a future command
cannot forget it - forgetting to wire something up is precisely the failure mode
this whole iteration kept finding.
Found by asking whether any other path could report success without doing the
work, after the empty-scan fix. 102 tests (was 100), dart analyze clean.
Analysing zero files exited 0 with no message, so the tool passed while having
checked nothing:
$ pipe check . --dir libs # typo: analysed 0 files, exit 0
$ pipe check . --dir <empty dir> # 0 files, exit 0
That is the same defect class as a test that cannot fail, at the level of the
whole tool: a CI job running `pipe check` "passed" having inspected no code at
all. A scan that finds no Dart files is now an actionable error:
pipe: no Dart files found under libs in /project; pass --dir to choose which
directories to scan
This also covers the nonexistent `--dir` case, which was previously skipped
silently by the directory walk.
Second defect: `--no-include-generated` printed
files 12/12 resolved (13 discovered, 1 generated, 0 skipped, 0 failed)
The 13-versus-12 gap could not be reconciled from the line - it read as though a
file had vanished or failed silently. `filesSelected` existed in the model and in
the JSON all along; the text line simply omitted it. It now prints `12 selected`
whenever it differs from the discovered count, so the arithmetic resolves, and
stays quiet when nothing was filtered.
Found by running the remaining unexercised flags (`--dir`, `--include-generated`)
end to end, as the execution-first pass did for the rest.
100 tests (was 95), dart analyze clean.
An execution sweep comparing README flags against what each command accepts found
no documented flag that the CLI rejects, but five working flags the README never
mentioned: index --fail-on-error, suggest --max-actions/--budget,
guide --max-findings/--yams-name, recall --manifest. Each is now described with
its verified behaviour (e.g. index exits 1 with analyzer errors, 0 without), plus
a note that numeric flags reject non-positive-integer values instead of falling
back to a default.
Eight sites read a numeric option as `int.tryParse(...) ?? <default>`, so a typo
was answered with a different number and a success exit:
$ pipe check . --top 2o # showed 25 rows, exit 0
$ pipe guide . --budget abc # used 6000 chars, exit 0
Meanwhile --concurrency and --limit already rejected bad input with a clear
message, so the CLI disagreed with itself about whether a bad number is an error.
Every numeric option now goes through readPositiveInt(): --top, --budget,
--max-actions and --max-findings included. This is the same principle as the rest
of the tool - do not silently substitute a value the user did not ask for.
Second, related defect in `suggest`: each numeric option was read inside the one
output branch that uses it (--top in the text branch, --max-actions in the agent
branch, --budget in the agent branch), so a malformed value was ignored entirely
on the other paths - `suggest --top abc --format agent` exited 0. All three are
now read and validated before the format switch, so the value is rejected
regardless of which branch runs.
Found by executing every command with deliberately bad flag values, which is part
of the execution-first sweep that also turned up the broken `graph`. Two external
read-audits reported no defects in this area.
95 tests (was 91), dart analyze clean, determinism re-confirmed byte-identical
across -c 1/4/8 for check/state/graph/suggest.
Registration is not execution: a command can register every option and still
fail at run time. GraphCommand did exactly that, throwing
`ArgumentError: Could not find an option named "--dir"`. This runs the real
command against the fixture and asserts it completes.
`pipe graph` was completely non-functional. GraphCommand had no constructor, so
it never called _addCommonOptions, yet its body reads --dir, --sdk,
--concurrency, --include-generated, --limit and --format. It therefore registered
only --help:
$ pipe graph lib
Unhandled exception:
Invalid argument(s): Could not find an option named "--dir". (exit 255)
$ pipe graph lib --format json
Could not find an option named "--format". (exit 64)
So a documented command died on an unhandled exception instead of a clean CLI
error, and its JSON output was unreachable. Fixed by registering the common
options like every other project command.
Found by smoke-testing every command, which is what the two external audit
rounds never did: this round's auditor listed "run pipe graph and pipe check" as
a NEXT area to audit and never ran graph, and reported no confirmed defects.
Guard: test/cli_options_test.dart asserts every project command registers every
common option it reads (recall is exempted - it deliberately targets a corpus
root and registers its own equivalents). It fails without this fix (+0 -2).
Also, from the same round's unconfirmed suspicions, both of which reproduced:
- recall: an unrecognised `status` is now rejected instead of silently read as
`open`. `status` decides whether a case is a bug pipe must find or a permanent
regression guard, so a typo like "fix" defaulted to `open`, printed MISSED, and
sent the reader hunting for a regression that was really a manifest typo.
- recall: MISSED and REGRESSED entries now print their `note`. The manifest
already carried the explanation for the two nuwayze misses ("wired up on branch
wire/nuwayze-* (unmerged)") but the report never showed it, so an expected miss
looked exactly like a regression in pipe.
Recorded as NOT a defect: `pipe verify` exiting 0 while listing "new" findings.
`--expect-resolved` is documented - and implemented - as failing only on
baselined findings that are still reported; `verify` fails correctly in that case
(exit 1). It has no fail-on-new flag, which is a gap for CI use rather than a
contract violation, so the documented behaviour was left alone.
90 tests pass (was 84), dart analyze clean.
A project with ChangeNotifier classes but no provider declarations printed:
provider rules examined 10 ChangeNotifier class(es) (0 consumed by a
Consumer/Selector), and 36 observable mutator(s) were checked for a notify
path. Provider declarations by type: .
That trailing "Provider declarations by type: ." is a dangling colon and full
stop. It reads as a truncated sentence rather than as "this app declares none",
which is how it surfaced on word_alive. The clause is now omitted when the list
is empty, and still lists kinds most-common-first when it is not.
Found by reading an external auditor's raw tool output after its run died on an
API rate limit before it could write a report - its observations were still
worth salvaging even though it never reached a conclusion.
The note is extracted into providerRuleNote() so both branches are directly
testable; before this the empty case was only reachable through a real project.
The fixture resolves with no pub get and no network, which is why
.dart_tool/package_config.json is committed by hand. Adding a package: import
would silently break every rule test that depends on it, so the constraint is
now stated where someone would violate it.
The independent precision adjudicator reported a pipe defect that did not exist.
Three defects in the checker, all found by chasing its own output against the
source:
1. It matched variant names by UNQUALIFIED name, so `is ExportSuccess` in
analytics_tab.dart was credited to ArtistStoreManagementState.ExportSuccess -
the very defect pipe's own per-union scoping had just fixed. Those three files
reference AdminAnalyticsState, ContentModerationState and
HelpCenterManagementState, and none of them mentions
ArtistStoreManagementState, so pipe's finding was correct and the checker's
"contradiction" was false. Evidence about a variant now requires a file that
also names the union or its owner - unless the name is declared by only one
union, in which case it is unambiguous and needs no scoping (event
construction sites carry no type annotation, and demanding one rejected sound
evidence: the dispatched control fell from 162/162 to 143/162).
2. Its notion of "rendered" was type-tests only, missing switch-expression arms
(`ChatListOperationInProgress value =>` has no `case` keyword in Dart 3) and
base-class payload reads (`state.upcomingBookings`, where BookingsLoaded
passes `required super.upcomingBookings`). Both are real ways a variant reaches
the screen; pipe models both.
3. `controls_pass` was `len(control1) == len(rendered) and ...`, which on a
corpus with no control material is `0 == 0` - a vacuous pass. word_alive
printed "controls pass: True" with 0/0 and a pristine-looking 100%. The control
is now three-way (found / unattributable / missing), only `missing` fails, and
an empty corpus exits NO SIGNAL with precision reported as UNDEFINED rather
than perfect.
Also fixed: the scoping pass looked for union names in `all_names`, which holds
variant names only, so `union in file_ids` could never be true and every control
silently read 0.
Result on clean nuwayze main: controls 131/131 rendered, 143/162 dispatched (19
unattributable, 0 missing) - controls pass, and precision is 79/79 + 77/77 with
zero contradictions. No pipe defect was found this round; the adjudicator's one
apparent finding was its own.
84 tests still pass; dart analyze clean.
An independent Gemini 3.8 Flash audit of this repo found real problems. Each was
reproduced before being fixed.
Confirmed defects:
- tool/adjudicate.py crashed with KeyError once R001 started grouping variants
under 'variants' instead of 'variant'. Fixing that exposed a second bug of my
own: it then divided verified VARIANTS by FINDINGS and reported 216% precision.
Counts are now coherent (findings vs variant-level checks) and a clean repo
reports 0/0 rather than a fake 100%.
- GuideCommand called _readPolicy but never registered --policy, so
exited 64 despite the README documenting it.
- loading-flag-not-reset treated a constructor-invoked method as inheriting the
loading flag without checking flag.defaultsTrue, so a default-FALSE flag was
reported as in flight.
- Finding.fingerprint embedded the line number for rules whose only identity was
, so moving code broke baseline entries. is now a
discriminating key; only loading-not-reset, which can emit two findings from one
method, carries an explicit catchLine.
- suggest grouped unrelated provider-read-in-build findings by file; the group key,
label and target list now use valueType/call.
Recall defect found while checking the auditor's own claim: UI variant mentions
were collected project-wide by unqualified name, so ExportSuccess - which appears
in FOUR unions on nuwayze - was cross-credited. Mentions are now scoped per union
to files that also name the state type or its owner, which surfaced
ArtistStoreManagementState[ExportSuccess] as a real finding.
Five negative controls could not fail (an event that is never handled can never be
reported as unhandled; a union with no render sites can never be reported as
rendered-not-emitted). Added fixture cases that make each one bite: HandledTap
(handled AND dispatched), PatternState render sites, GuardedCubit (guarded loading
path), QuietCubit._warmUp (constructor-invoked await on a default-false flag).
Root cause of a recurring mystery: this repo EXCLUDED test/fixtures/** from
analysis, which made any analyzer running in the repo context parse the fixture
without resolving its imports and report phantom undefined-class errors. I had
dismissed these as stale cache several times; they were my own exclusion. The
exclude is gone and the fixture governs itself. A new test asserts the fixture has
zero analyzer errors, not merely that it resolves, so IR distortion cannot hide.
84 tests (was 81).
An unreachable state, handler or control is usually an unfinished feature rather
than dead weight, so pipe now leads with making it reachable: render the missing
states, add the missing branch, dispatch the event from a control. Removal is the
exception and is worded as needing explicit approval instead of being offered as
an equal alternative.
New FixPolicy (wire-up default | remove), threaded through runChecks,
runProviderRules, analyzeProject and buildGuidance, with --policy on check, guide
and suggest. It changes every remediation that presents a choice, the brief's
decision line, and the dead-handler section heading ('Unreachable handlers - wire
up the control' vs 'Cleanly deletable').
It is a policy, not a finding: the same facts are reported either way.
81 tests, including 6 new ones asserting the default and that remove flips it.
Two real defects found by an audit of nuwayze, both confirmed against the code.
1. Recall: the switch 'case ChatListLoaded value => value' binds the type on a
DeclaredVariablePattern, not the object pattern ChatListLoaded() this matcher
looked for. An exhaustive switch was therefore invisible and two variants were
wrongly reported as never rendered. Both the mention collector and the branch
collector now handle declared variable patterns.
2. Precision: the UI often reads a union's payload off the BASE class
(state.currentBooking, state.isRefreshing), so the data reaches the user even
though the variant is never matched. StateUnion now records uiReadsBaseFields
and those findings drop to info with an explanation instead of warning. That
removed 5 false-positive warnings on nuwayze.
3. recall/manifest.json gains status=open|fixed. A fixed case that is reported
again is a REGRESSION, so repaired bugs become permanent guards against the
real code instead of deleted expectations. The three bugs fixed earlier today
(dex x2, word_alive) are now fixed-status guards:
recall --strict -> 5/5 accounted for, 0 regressions.
nuwayze: 111 findings, 11 warnings (was 16). 75 tests, fixture regression guard
patternView covers the typed-pattern case.
The two Provider<AppConfig>.value uses are the negative controls for
provider-created-in-build: the type is provided, but only through .value, which
passes an instance created once. Saying so in the fixture makes the control
readable instead of implicit.
Completes the provider/ChangeNotifier rule set:
- notifier-field-unread: a public getter on a notifier the UI consumes that
nothing reads, so the state it exposes cannot reach the user. Once per
notifier. nuwayze: 3 (UserProvider.isInitializing,
AppLocaleProvider.preferredLocale, and _AgentHubController with 8 members).
- provider-created-in-build: a provider constructed in build without .value, so
create reruns on every rebuild. Reported once per build method, not once per
provider: nuwayze's AppProviders.build constructs 45 of them and that is one
decision, not 45 bugs. Deliberately info, not warning, because a root
StatelessWidget rarely rebuilds and static analysis cannot tell rebuild
frequency apart.
Also fixes a real defect the budget test caught: the brief's 'by rule:' line
grew with the rule count and crowded out the findings it summarises. It is now
capped at the top 6 with a '+N more' suffix, the omission note is reserved for
up front so the artifact stays inside its budget, and the overflow message is
shorter.
75 tests. Verified the fixture with pipe's own resolver (0 errors) and validated
that check by injecting a type error into a copy (errors=1).
Adds the provider half of the state-management surface:
- collects every provider-package declaration type (Provider,
ChangeNotifierProvider, ListenableProvider, ValueListenableProvider,
FutureProvider, StreamProvider, StateProvider, ProxyProvider family) and
reports the counts by type in the coverage note
- new rule provider-read-in-build: a Listenable read with context.read or
Provider.of(listen: false) directly in a build body, so the widget never
subscribes and can render stale state
447 raw context.read calls in nuwayze reduce to 1 finding. The first four were
hand-checked and every one was a read inside a closure (a BlobListener-style
listener callback or a Selector builder), so the collector now refuses to
descend into nested functions; that costs recall on reads inside Builder and is
recorded in the README rather than hidden.
Also fixes a third fingerprint collision: valueType/call were missing from the
finding identity, so two provider-read-in-build findings in one file shared a
baseline fingerprint. Identity now requires a discriminating key and falls back
to the line only when there is none.
The union-observed-but-variant-never-matched tier was one finding per variant,
so 67 findings described the same fix across 21 unions. Group by marker class
instead: failures and loading markers stay warnings because an unrendered
failure means the user gets no feedback, other markers drop to info.
nuwayze: 146 findings -> 108, and the actionable core is now visible: 14
warnings (2 dormant decisions + 12 missing-feedback groups), down from 49.
Also records a negative result in the README: tiering these by 'variant declares
no fields beyond its base' was tried and rejected, because 67/67 qualified and a
signal true of the whole population cannot discriminate.
Dogfooding after fixing the three reported bugs showed that high-confidence
emitted-not-rendered findings can be deliberately dormant scaffolding rather
than bugs: on nuwayze both such blocs were already documented as unreachable
in the repository's own action contract, and the CommentsBloc consumer had
been deleted on purpose when a StreamBuilder UI replaced it.
Reporting nine near-identical findings for one product decision buries the
findings that are real regressions, so an unobserved union now yields a single
finding listing its variants, and the remediation says to check the
repository's docs and action contracts before deleting anything.
nuwayze: 159 findings -> 146 (15 variant findings collapse to 2 decisions).
Mirrors the conventions used in ../yams: a sectioned .gitignore, thin
.githooks dispatchers over runnable scripts/, release-please config and
manifest, conventional-commit linting, CODEOWNERS, a PR template with a
rule-precision checkbox, and tests + commitlint workflows. CI calls
scripts/lint.sh so local hooks and CI cannot drift.
The fixtures are standalone packages with a hand-written
.dart_tool/package_config.json, and the blanket .dart_tool/ ignore rule
excluded it. Everything passed locally only because the file happened to
exist on disk, so a clone would have failed the rule-contract and recall
tests. Re-include that one path and give the fixture its own
analysis_options so it does not inherit the repository include.