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
3 changed files with 41 additions and 1 deletions
Showing only changes of commit d1b430b795 - Show all commits

Merge branch 'main' into docs/honest-agent-guidance
Some checks failed
Lint Commits / commitlint (pull_request) Successful in 16s
Tests / Static Analysis (pull_request) Has been cancelled
Tests / Tests (pull_request) Has been cancelled

trevon 2026-09-22 21:19:33 +00:00

View file

@ -1334,7 +1334,13 @@ class RiverpodModel {
if (!provider.isAsync) continue;
final providerSites = sitesFor(provider.name);
if (providerSites.isEmpty) continue;
if (!providerSites.any((site) => site.readsValue)) continue;
// Only a `ref.watch` renders the value. A `ref.listen` that reads it
// drives a side effect and draws nothing, so it cannot leave a blank or
// stale screen; listeners still count below as places that handle
// error or loading.
if (!providerSites.any((site) => site.watches && site.readsValue)) {
continue;
}
final missing = <String>[];
if (!providerSites.any((site) => site.handlesError)) missing.add('error');
if (!providerSites.any((site) => site.handlesLoading)) {

View file

@ -33,6 +33,12 @@ class FutureProvider<T> {
class Ref {
AsyncValue<T> watch<T>(FutureProvider<T> provider) =>
throw UnimplementedError();
void listen<T>(
FutureProvider<T> provider,
void Function(AsyncValue<T>? previous, AsyncValue<T> next) listener,
) =>
throw UnimplementedError();
}
class SubscriptionTier {}
@ -46,6 +52,9 @@ final unhandledProvider =
/// Consumed with `when(data:, loading:, error:)`.
final handledProvider = FutureProvider<int>((ref) async => 1);
/// Consumed only by `ref.listen`, to mirror the value into a side effect.
final listenOnlyProvider = FutureProvider<bool>((ref) async => false);
class SubscriptionView {
SubscriptionView(this.ref);
@ -60,4 +69,14 @@ class SubscriptionView {
loading: () => -1,
error: (error, stack) => -2,
);
/// Not a render: a listener that mirrors the value into a side effect (for
/// example an analytics user property) draws nothing, so ignoring loading
/// and error here leaves no blank or stale screen.
void mirrorFlag(void Function(bool value) sink) {
ref.listen(listenOnlyProvider, (previous, next) {
final value = next.asData?.value;
if (value != null) sink(value);
});
}
}

View file

@ -68,4 +68,19 @@ void main() {
isNot(contains('handledProvider')),
);
});
test('a provider consumed only by ref.listen is not a render gap', () {
// A listener mirrors the value into a side effect and draws nothing, so
// it cannot leave a blank or stale screen. Seen when dogfooding on
// word_alive: an analytics user-property listener was reported as a UI
// read with unhandled loading and error.
final sites = analysis.flow.riverpod.sitesFor('listenOnlyProvider');
expect(sites, hasLength(1));
expect(sites.single.watches, isFalse);
expect(sites.single.readsValue, isTrue);
expect(
analysis.flow.riverpod.gaps.map((g) => g.provider.name),
isNot(contains('listenOnlyProvider')),
);
});
}