fix(guide,suggest): word agent guidance no stronger than the evidence #2
4 changed files with 16 additions and 12 deletions
fix(guide): word each rule's one-liner no stronger than its confidence
Four legend epithets claimed more than their rules establish: - rendered-not-emitted (medium): "so it is dead code". The rule sees only the scanned code and suppresses owners whose helper-method emissions it cannot resolve; it now says the branch is probably unreachable. - riverpod-async-gap: "a failure shows a blank or stale screen". It can; the rule downgrades `bool` guard providers to info precisely because it often does not. - notifier-field-unread (medium): "can never affect what the user sees". The check is a by-name read search in the scanned code; the finding message and the epithet now say "likely". - provider-created-in-build: "on every rebuild and state is lost". The rule is info because rebuild frequency is unknown; it now says "if the widget rebuilds". The README rules table and the rendered-not-emitted and notifier-field-unread messages are aligned with the same wording.
commit
37d4864d54
|
|
@ -148,7 +148,7 @@ Baselines and reports are JSON, so they diff and review like any other file.
|
|||
| Rule | What it means | Verified precision (with n) |
|
||||
| --- | --- | --- |
|
||||
| `emitted-not-rendered` | A state the bloc emits that no UI code matches, so the user never sees it. | **79/79 (100%)** factual on a CLEAN nuwayze main via an independent adjudicator with passing controls (`tool/adjudicate.py`). Reported once per **root cause**, not once per variant: one finding per unobserved union (dormant feature) and one per marker class when the union is observed. When the UI reads the union's payload through **base-class fields** the finding drops to info, because the data does reach the user. On clean nuwayze main: 30 findings covering 79 variant-level claims. |
|
||||
| `rendered-not-emitted` | A UI branch no state can reach: dead UI. | n=0 on nuwayze (unions whose bloc emits through helper methods are suppressed with a reason). Fires on a fixture with a passing negative control. |
|
||||
| `rendered-not-emitted` | A UI branch for a variant its bloc never emits in the scanned code: probably dead UI. | n=0 on nuwayze (unions whose bloc emits through helper methods are suppressed with a reason). Fires on a fixture with a passing negative control. |
|
||||
| `loading-not-reset` | An in-flight state whose failure path never resolves, so the spinner sticks. | n=0 on both dogfood repos, both verified to guard correctly; proven able to fire by fixture. |
|
||||
| `loading-flag-not-reset` | The same consequence for `copyWith` + `bool loading` state, which has no variant to match on. | **7/7 (100%)** on dex after hand adjudication; 4 characterised false positives were eliminated by the current gate. n=0 on nuwayze, verified correct. |
|
||||
| `event-without-handler` | A dispatched event with no `on<Event>`, so the interaction does nothing. | n=0 across the corpus; verified non-vacuous (168 dispatch sites on nuwayze all map to a registered handler). |
|
||||
|
|
@ -157,8 +157,8 @@ Baselines and reports are JSON, so they diff and review like any other file.
|
|||
| `notifier-never-notifies` | A `ChangeNotifier` method that changes observable state without ever notifying listeners. | Fixture contract only: **zero true positives** on the corpus (nuwayze 30 observable mutators, word_alive 36 — all of which notify). Gated on observable fields with lifecycle hooks excluded, after the first run was 9/9 false positives. |
|
||||
| `notifier-inert-consumer` | A notifier consumed by a `Consumer`/`Selector` with no notifying path at all, so those consumers can never rebuild. | Fixture contract only; no real-code true positives yet. |
|
||||
| `provider-read-in-build` | A `Listenable` read with `context.read` / `Provider.of(listen: false)` **directly in a build body**, so the widget never subscribes and can render stale state. | 1 finding on nuwayze from **447** raw `context.read` calls; 0 on word_alive/dex. Hand-verified: the first 4 findings were all reads inside **closures** (a `BlocListener` callback, a `Selector` builder), so a closure guard was added and only the direct-in-build case survives. |
|
||||
| `notifier-field-unread` | A public getter on a consumed `ChangeNotifier` that nothing reads, so the state it exposes cannot affect what the user sees. | 3 on nuwayze (e.g. `UserProvider.isInitializing`, `AppLocaleProvider.preferredLocale`), 0 on word_alive/dex. Reported once per notifier, and only for notifiers the UI actually consumes. **medium** confidence: a name match elsewhere can mask a genuinely unread member. |
|
||||
| `provider-created-in-build` | A provider constructed inside `build` without `.value`, so `create` reruns on every rebuild and state is lost. | 1 finding on nuwayze — `AppProviders.build` constructs **45** providers — from 45 raw sites, because it is reported **once per build method**. Deliberately **info**, not warning: the claim is factual, but a root `StatelessWidget` rarely rebuilds, so impact depends on rebuild frequency. |
|
||||
| `notifier-field-unread` | A public getter on a consumed `ChangeNotifier` that nothing reads, so the state it exposes likely never reaches the UI. | 3 on nuwayze (e.g. `UserProvider.isInitializing`, `AppLocaleProvider.preferredLocale`), 0 on word_alive/dex. Reported once per notifier, and only for notifiers the UI actually consumes. **medium** confidence: a name match elsewhere can mask a genuinely unread member. |
|
||||
| `provider-created-in-build` | A provider constructed inside `build` without `.value`, so if the widget rebuilds, `create` reruns and its state is lost. | 1 finding on nuwayze — `AppProviders.build` constructs **45** providers — from 45 raw sites, because it is reported **once per build method**. Deliberately **info**, not warning: the claim is factual, but a root `StatelessWidget` rarely rebuilds, so impact depends on rebuild frequency. |
|
||||
|
||||
### How precision was measured
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ const Map<String, String> ruleEpithet = {
|
|||
ruleEmittedNotRendered:
|
||||
'A state the bloc emits that no UI code matches, so the user never sees it.',
|
||||
ruleRenderedNotEmitted:
|
||||
'A UI branch that no state can reach, so it is dead code.',
|
||||
'A UI branch for a variant its bloc never emits in the scanned code, so '
|
||||
'the branch is probably unreachable.',
|
||||
ruleLoadingNotReset:
|
||||
'A loading state whose failure path never resolves, so the spinner sticks.',
|
||||
ruleLoadingFlagNotReset:
|
||||
|
|
@ -44,7 +45,8 @@ const Map<String, String> ruleEpithet = {
|
|||
ruleDeadHandler: 'An `on<Event>` registration that nothing ever dispatches.',
|
||||
ruleRiverpodAsyncGap:
|
||||
'An async Riverpod provider read in the UI whose error and/or loading '
|
||||
'state is never rendered, so a failure shows a blank or stale screen.',
|
||||
'state is never rendered, so a failure can leave a blank or stale screen '
|
||||
'with no sign that anything went wrong.',
|
||||
ruleNotifierNeverNotifies:
|
||||
'A ChangeNotifier method that changes state without ever notifying '
|
||||
'listeners, so the UI keeps showing the old values.',
|
||||
|
|
@ -55,11 +57,11 @@ const Map<String, String> ruleEpithet = {
|
|||
'A Listenable read with `context.read` / `Provider.of(listen: false)` '
|
||||
'inside build, so the widget never subscribes and can go stale.',
|
||||
ruleNotifierFieldUnread:
|
||||
'A public getter on a consumed ChangeNotifier that nothing reads, so the '
|
||||
'state it exposes can never affect what the user sees.',
|
||||
'A public getter on a consumed ChangeNotifier that nothing in the scanned '
|
||||
'code reads by name, so the state it exposes likely never reaches the UI.',
|
||||
ruleProviderCreatedInBuild:
|
||||
'A provider constructed inside build without `.value`, so a new instance '
|
||||
'and listener are created on every rebuild and state is lost.',
|
||||
'A provider constructed inside build without `.value`, so if the widget '
|
||||
'rebuilds, a new instance and listener are created and its state is lost.',
|
||||
};
|
||||
|
||||
/// Fixes that read the same under either policy.
|
||||
|
|
|
|||
|
|
@ -821,8 +821,9 @@ String runProviderRules({
|
|||
confidence: 'medium',
|
||||
message:
|
||||
'${notifier.name} is consumed in the UI but ${unread.length} of '
|
||||
'its public member(s) are read nowhere outside the class, so '
|
||||
'nothing the user sees can depend on them: ${unread.join(', ')}.',
|
||||
'its public member(s) are read by name nowhere outside the class '
|
||||
'in the scanned code, so what the user sees likely does not depend '
|
||||
'on them: ${unread.join(', ')}.',
|
||||
file: notifier.file,
|
||||
line: notifier.line,
|
||||
evidence: [
|
||||
|
|
|
|||
|
|
@ -609,7 +609,8 @@ FindingReport runChecks({
|
|||
confidence: 'medium',
|
||||
message:
|
||||
'$variant is matched in UI code but ${coverage.union.owner} '
|
||||
'never emits it, so that branch is dead.',
|
||||
'never emits it in the scanned code, so that branch is probably '
|
||||
'unreachable.',
|
||||
file: sites.isEmpty ? null : sites.first.path,
|
||||
line: sites.isEmpty ? null : sites.first.line,
|
||||
evidence: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue