Commit graph

104 commits

Author SHA1 Message Date
trvon
10db6fbb57
chore: apply dart format to the files this branch changed
The commits above were made without the pre-commit hook, which could not
run dart at the time.
2026-09-22 12:22:02 -06:00
trvon
17c9f9afab
fix(suggest): break score ties by file, line and rule
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.
2026-09-22 12:05:12 -06:00
trvon
ae412a7964
fix(cli): reject a missing --dir and report filesystem errors cleanly
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.
2026-09-22 12:05:12 -06:00
trvon
e2d84f5e75
fix(guide): persist the yams report with its caution and provenance
`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.
2026-09-22 12:05:12 -06:00
trvon
4a1c9cdd3e
fix(guide): quote analysed source as bounded, delimited data
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.
2026-09-22 12:05:12 -06:00
trvon
39b309af97
fix(guide,suggest): stop packing at the first entry that does not fit
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.
2026-09-22 12:05:11 -06:00
trvon
749aa53348
feat(report): state scan coverage in every check, guide and suggest output
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.
2026-09-22 12:05:11 -06:00
trvon
ac7cce1f76
fix(cli): keep status messages off stdout so JSON output stays parseable
`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.
2026-09-22 11:58:20 -06:00
trvon
a0844bb6f0
fix(baseline): key every fingerprint on identity, not on its line
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`.
2026-09-22 11:58:20 -06:00
trvon
9eb3d98d4d
fix(verify): a finding in an excluded file is not scanned, not resolved
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.
2026-09-22 11:58:20 -06:00
trvon
cf9e0b7e57
fix(verify): judge only the baselined findings of the rules that ran
`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.
2026-09-22 11:48:34 -06:00
trvon
f0fc57e341
fix(dead-handler): stop claiming more than the scan supports
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-21 07:09:47 -06:00
trvon
1ff0dc02e1
fix(recall): keep guarding cases whose evidence is readable
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 22:07:27 -06:00
trvon
ddf1be0442
fix(verify,recall): a partially readable project is not a fixed one
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 22:04:52 -06:00
trvon
a05d971130
fix(scan): a project of unreadable files reported a clean result
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 22:02:20 -06:00
trvon
74b5aa95e3
style: apply dart format to the two tests added in 4c65166
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:59:32 -06:00
trvon
4c65166347
fix: close two more ways a check could pass without checking anything
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:58:57 -06:00
trvon
7c5812b609
fix(verify): stop reporting unscanned findings as resolved
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
`--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.
2026-09-20 21:55:31 -06:00
trvon
51929e2107
fix(scan): stop reporting success for a scan that found nothing
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:53:49 -06:00
trvon
5c82518c57
docs: document the command-specific flags the execution sweep found undocumented
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:50:41 -06:00
trvon
639c2d168e
fix(cli): reject malformed numeric options instead of silently defaulting
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:48:14 -06:00
trvon
1193e061f9
test(cli): exercise graph end to end, not just its parser
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:44:03 -06:00
trvon
03df9d01de
fix(cli): repair graph, which crashed on every invocation
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
`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.
2026-09-20 21:43:36 -06:00
trvon
1e7d7b72d5
fix(providers): drop the dangling "by type" clause when there are no declarations
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:34:42 -06:00
trvon
f1c0e56a1a
docs(fixtures): record why the UI fixture must stay dependency-free
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:30:56 -06:00
trvon
347f2ef51d
fix(adjudicate): stop the oracle from manufacturing contradictions
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 21:30:14 -06:00
trvon
3c91db2946
fix: act on the external audit - 5 defects, 5 vacuous tests, a real fixture exclusion bug
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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).
2026-09-20 20:45:58 -06:00
trvon
883c661d7b
feat: default remediation policy is to WIRE IT UP, not to delete
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 18:41:07 -06:00
trvon
c391171e6a
fix: attribute typed variable patterns, and stop treating base-field reads as unrendered
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 18:10:56 -06:00
trvon
8244959773
docs: label the provider-created-in-build negative controls in the fixture
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 18:00:46 -06:00
trvon
88ed85b43a
feat: add notifier-field-unread and provider-created-in-build, bound the brief header
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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).
2026-09-20 17:59:54 -06:00
trvon
2c7bea9ef0
feat: provider package support - model all provider declaration types, add provider-read-in-build
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 17:46:30 -06:00
trvon
d87c0bf8e0
perf: group never-matched state markers by marker class instead of one finding per variant
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 17:37:06 -06:00
trvon
5d0a704bc2
perf: collapse a fully unobserved state union into one decision-level finding
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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).
2026-09-20 17:18:51 -06:00
trvon
567a0b8a45 chore: adopt the yams repo layout
Some checks failed
Tests / Static Analysis (push) Has been cancelled
Tests / Tests (push) Has been cancelled
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.
2026-09-20 17:03:37 -06:00
trvon
4df7cc12b7 fix: track the rule fixture package_config so a fresh clone can run the tests
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.
2026-09-20 17:03:35 -06:00
pipe-loop
e9a29077cd perf: AOT build script + measured perf (6.4s -> 0.2s startup, nuwayze 23-29s -> 10.4s) 2026-09-20 16:24:19 -06:00
pipe-loop
b83fca1e30 feat: suggest command - ranked fix order per file with transparent scoring 2026-09-20 16:22:29 -06:00
pipe-loop
3cdb32f89d feat: provider rules (notifier-never-notifies, notifier-inert-consumer) with observable-field gate and coverage notes 2026-09-20 16:19:02 -06:00
pipe-loop
3ebc78d1a7 feat: add verify command - prove fixes, not just find problems 2026-09-20 16:07:09 -06:00
pipe-loop
aae2d5e43b test: adjudicate all 159 nuwayze R001/R006 findings with a validated checker (82/82, 77/77) 2026-09-20 16:05:00 -06:00
pipe-loop
f2949dfbfd feat: rule contracts + recall measurement (5/5 real bugs) + real --fail-on gate 2026-09-20 16:00:10 -06:00
pipe-loop
a773407684 fix: do not claim space pressure when there are simply no rules 2026-09-20 14:42:19 -06:00
pipe-loop
9b4707b5c3 fix: accurate state-owner summary, no-findings brief, skip empty legend 2026-09-20 14:41:42 -06:00
pipe-loop
513ce76150 feat: baseline/ratchet support, CI gate over new findings, README with precision table 2026-09-20 14:40:17 -06:00
pipe-loop
73fdcfeb95 feat: Riverpod support - provider model, riverpod-async-gap rule, tiered by value type 2026-09-20 14:35:40 -06:00
pipe-loop
6859d7e78c feat: phase 5 dogfood - model flag-based state, add loading-flag-not-reset, fixture tests 2026-09-20 14:29:21 -06:00
pipe-loop
bd667093ee feat: name first consumer per finding, add cleanly-deletable dead-handler section 2026-09-20 14:15:17 -06:00
pipe-loop
34a7921b66 feat: dense edit anchors, reserve legend space, derive legend from shown groups 2026-09-20 14:13:56 -06:00
pipe-loop
97f99d88a1 feat: decide wire-vs-delete from owner references, fix legend set, add by-rule aggregate 2026-09-20 14:05:53 -06:00