Second step of the state-map seam. The five ChangeNotifier/provider rules
walked resolved units from runProviderRules. Their recognisers move to
the Dart front end (dart_provider_facts.dart) and emit a NotifierModel
of neutral facts (facts.dart):
- NotifierFact / NotifierMethod / FieldMutation: an observable object
that must announce its own changes, its methods, what each calls,
mutates, and whether it notifies directly. Deliberately general enough
for a Qt QObject or a hand-rolled observer subject.
- consumer sites, provider declarations, member uses per file.
- NonSubscribingRead: every read made while building without
subscribing; whether the type is observable is now the rule's filter,
not the recogniser's.
- ProvidersCreatedInBuild: providers constructed in a build method.
The transitive notify closure, the observable/lifecycle/wrapper
judgements and all messages stay in provider_rules.dart, which no longer
imports the analyzer.
runChecks now takes only a StateFlow: no Project, no FileOutcome. The
scanned directories ride on the model (StateFlow.directories) for the
dead-handler scope claim. facts.dart is exported publicly.
No behaviour change, checked byte for byte: `check --format json` is
identical on the fixture and on resolved mirrors of nuwayze, tick, dex
and word_alive. 128 tests pass.
Two ways the event rules reported handled events as unhandled (error
severity, high confidence):
1. A handler on the event base or an intermediate group. `on<SyncEvent>`
with a switch inside handles every subclass, but handlers were matched
to dispatches by exact name, so every dispatched subclass was reported
by event-without-handler and the base handler itself by dead-handler.
Variants now record the supertype they were enumerated through, and
coverage follows it: an event is handled when it or an ancestor is
registered, and a handler is live when anything at or below it is
dispatched or constructed. A group handler still does not cover a
sibling outside the group (UploadAbandoned stays reported).
2. Blocs sharing an event type. Only the first owner of an event type was
kept, so a second bloc's handlers were ignored: an event only it handles
was reported unhandled, and its dead handlers were never examined. There
is now one event union per owning bloc. A dispatch does not record which
bloc it targets, so an event any sharing bloc handles counts as handled,
and an unhandled event is reported once per shared union rather than
once per bloc. Dead handlers are still judged per bloc.
Reproduced first on base_event.dart: 4 false findings before
(SyncStarted, UploadRetryNow unhandled; SyncEvent, UploadRetry dead) and
the shared case (ShareDismissed falsely unhandled, ShareBetaBloc's dead
ShareExpired missed). Each is now a contract control.
No change on nuwayze (102 findings) or tick (2): byte-identical before
and after, since neither uses these patterns.
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).
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.