fix(guide,suggest): word agent guidance no stronger than the evidence #2

Merged
trevon merged 13 commits from docs/honest-agent-guidance into main 2026-09-22 21:19:58 +00:00
4 changed files with 16 additions and 12 deletions
Showing only changes of commit 37d4864d54 - Show all commits

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.
trvon 2026-09-22 12:00:14 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -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) | | 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. | | `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-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. | | `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). | | `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-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. | | `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. | | `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. | | `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 `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. | | `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 ### How precision was measured

View file

@ -33,7 +33,8 @@ const Map<String, String> ruleEpithet = {
ruleEmittedNotRendered: ruleEmittedNotRendered:
'A state the bloc emits that no UI code matches, so the user never sees it.', 'A state the bloc emits that no UI code matches, so the user never sees it.',
ruleRenderedNotEmitted: 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: ruleLoadingNotReset:
'A loading state whose failure path never resolves, so the spinner sticks.', 'A loading state whose failure path never resolves, so the spinner sticks.',
ruleLoadingFlagNotReset: ruleLoadingFlagNotReset:
@ -44,7 +45,8 @@ const Map<String, String> ruleEpithet = {
ruleDeadHandler: 'An `on<Event>` registration that nothing ever dispatches.', ruleDeadHandler: 'An `on<Event>` registration that nothing ever dispatches.',
ruleRiverpodAsyncGap: ruleRiverpodAsyncGap:
'An async Riverpod provider read in the UI whose error and/or loading ' '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: ruleNotifierNeverNotifies:
'A ChangeNotifier method that changes state without ever notifying ' 'A ChangeNotifier method that changes state without ever notifying '
'listeners, so the UI keeps showing the old values.', '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)` ' 'A Listenable read with `context.read` / `Provider.of(listen: false)` '
'inside build, so the widget never subscribes and can go stale.', 'inside build, so the widget never subscribes and can go stale.',
ruleNotifierFieldUnread: ruleNotifierFieldUnread:
'A public getter on a consumed ChangeNotifier that nothing reads, so the ' 'A public getter on a consumed ChangeNotifier that nothing in the scanned '
'state it exposes can never affect what the user sees.', 'code reads by name, so the state it exposes likely never reaches the UI.',
ruleProviderCreatedInBuild: ruleProviderCreatedInBuild:
'A provider constructed inside build without `.value`, so a new instance ' 'A provider constructed inside build without `.value`, so if the widget '
'and listener are created on every rebuild and state is lost.', 'rebuilds, a new instance and listener are created and its state is lost.',
}; };
/// Fixes that read the same under either policy. /// Fixes that read the same under either policy.

View file

@ -821,8 +821,9 @@ String runProviderRules({
confidence: 'medium', confidence: 'medium',
message: message:
'${notifier.name} is consumed in the UI but ${unread.length} of ' '${notifier.name} is consumed in the UI but ${unread.length} of '
'its public member(s) are read nowhere outside the class, so ' 'its public member(s) are read by name nowhere outside the class '
'nothing the user sees can depend on them: ${unread.join(', ')}.', 'in the scanned code, so what the user sees likely does not depend '
'on them: ${unread.join(', ')}.',
file: notifier.file, file: notifier.file,
line: notifier.line, line: notifier.line,
evidence: [ evidence: [

View file

@ -609,7 +609,8 @@ FindingReport runChecks({
confidence: 'medium', confidence: 'medium',
message: message:
'$variant is matched in UI code but ${coverage.union.owner} ' '$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, file: sites.isEmpty ? null : sites.first.path,
line: sites.isEmpty ? null : sites.first.line, line: sites.isEmpty ? null : sites.first.line,
evidence: [ evidence: [