fix(review): the recovery sentence survives an explicit load, and a refusal retraction says only that refreshes are accepted again - #2702
Conversation
…round poll The #2630 recovery sentence was retired by the next successful read of any kind, and explicit loads take the same recordQueueRefreshSuccess path: a post-decision reload, a batch composable, the board-filter watcher or the pre-decision barrier landing a few hundred milliseconds after the recovering poll blanked the polite live region before it could be spoken (#2638 item 2). recordQueueRefreshSuccess now takes the read kind and retires only when a BACKGROUND poll success belongs to a later read than the one that raised the sentence, counted by a background-read ordinal on the composable. The degraded and refusal onsets still retire it, because there the sentence is false rather than merely old. Explicit loads still RAISE one when they end a degraded state, and the #2694 list-scoped raise and its recoveryAlreadyRaised guard are unchanged, as are #2445's composite semantics for the transient counter. The signal also carries WHICH disclosure it retracts (degraded or refused), so the surfaces can say the narrower thing for a refusal retraction. Refs #2638, #2214
A refusal disclosure is retracted the moment the LIST read answers, on a tick
whose composite read can still fail at the deep-link leg and return before the
queue is replaced. Both skins announced that with the queue sentence, whose
second clause ('Showing current proposals') was therefore unproven for up to
two further poll intervals (#2214, recorded from PR #2694's round 2).
review.queue.refused.recovered says only that the server is accepting refreshes
again, in en, it and es. Each skin's hoisted sr-only recovery region picks the
key from the kind the composable reports; the regions stay always mounted and
hoisted above the branch pair exactly as #2630 left them.
Refs #2638, #2214
Composable, red first against the unmodified sources: a poll recovery followed by an explicit load inside the interval left the sentence retired (expected false to be true, twice, including the case where a failed background tick sits between the two), and the refusal retraction had no kind to report (queueRefreshRecoveredKind undefined, twice). 4 failed / 161. Views, red first the same way: the Legacy refusal-retraction case rendered the queue sentence where the refusal one belongs, and the new Paper case did too (each 'expected ... to be undefined', the key not existing yet). Green after the fix: 5 files / 450 across the composable spec, both view specs, the Paper language spec and the catalog parity guard. Refs #2638, #2214
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Review record (alpha product-trust lane, review-and-ship round 1 at head Reviewer: one fresh-context independent reviewer subagent (read-only; Codex credits exhausted, SC-9), given the worktree at the head and the merge-base diff. Verdict: SHIP, no merge-blocking items; one MEDIUM fixed in round 2 because it is the defect class this PR closes, with the roles swapped. Lenses that found nothing: the read counter is incremented exactly once per started background read, before any await, with the in-flight flag held synchronously, and never on the explicit path ( Findings, triaged once:
Round count: 2 after the fix push. Merge gate: |
Round-2 review finding, MEDIUM: raiseQueueRecovery stamped every raise with the ordinal of the last STARTED background read. That is the running read for a poll raise, but for the #2630 explicit-clear path (loadProposalsWithOutcome -> recordQueueRefreshSuccess) it names a read that already finished, so the very next tick satisfied count > stamp and retired the sentence. A post-decision reload that ends the degraded state 14.9 s into a 15 s cycle was blanked 100 ms later: the defect this PR exists to close, with the roles swapped. An explicit raise is now stamped with the read that has not started yet, so it survives the next poll success and retires on the one after. Either raise therefore gets at least one full interval, and the comment states both halves instead of claiming one interval for all of them. The onsets still retire immediately for both, because there the sentence is false rather than old. 'source' is threaded through recordQueueListReadSucceeded so a refusal retraction is stamped by the same rule. Red first against the round-1 head: 'gives an EXPLICIT-load recovery a full interval before a poll can retire it' failed 'expected false to be true' at the first poll tick, 1 failed / 163. The companion onset pin was green before and after, which is the point of it. Also two comment-only round-2 LOWs: the queue-sentence upgrade is justified by the degraded state having ended rather than by the read completing, with the refusal-only case named as a deliberate under-claim; and the retirement comment now names the reachability-limited case where a pin leg failing every tick with 405/410/409 (which this route does not emit) leaves a sentence standing until an onset, now that an explicit success no longer bounds it. Refs #2638, #2214
|
Round-2 record (alpha product-trust lane; fix head Fixed: Red-first: with the round-1 implementation parked and the new spec in place, "gives an explicit-load recovery a full interval before a poll can retire it" failed at the assertion after the first poll tick (the round-1 head retired it on the very next tick); the companion onset pin was green before and after by design. Verified at the fix head: the five named suites 5 files, 452 passed (run before and after the merge of Not verified: no browser or screen-reader run; the it/es wording is the lane's (#1770). Merge gate remaining: |
|
Round-2 verification record (scoped to the fix diff Verdict: SHIP for the fix diff, no CRITICAL, HIGH or MEDIUM. What it checked: the stamp arithmetic (an explicit raise between reads N and N+1 stamps N+1, survives read N+1's success and retires at N+2; a poll raise in read N still retires at N+1); the in-flight case cannot lengthen the life beyond two intervals because an explicit load supersedes the in-flight background read synchronously, so the next success is N+2 and retirement N+3, the same envelope as an explicit load landing right after a fast read; the poll cannot start between an explicit load's start and its raise because the loading flag blocks it before the counter bump, so "count + 1" always names a read that has not started; the refusal retraction's source parameter is required with both call sites explicit and the default source is the safe direction; both onsets retire unconditionally; the new spec is a genuine regression guard (on the round-1 code the stamp would be 3 and read 4 would retire), the onset case is a fresh run to the threshold, and the four round-1 recovery and refusal cases stay green with the new stamps; the comments match the branches; the round-2 diff is the two files and the PR's full diff is the nine files. Findings, triaged once at the two-round ceiling, both recorded on #2638:
Round count: 2. Merge gate remaining: |
Summary
The review queue's recovery sentence was retired by the next successful read of any kind. Explicit loads take the same
recordQueueRefreshSuccesspath and are common right after a recovery (post-decision reloads inuseReviewActions, the batch composables, the board-filter watcher,dismissSettledElsewhereNotice, the pre-decision refresh barrier), so a reviewer's already-clicked Approve landing a few hundred milliseconds after the recovering poll emptied the polite live region before it could be spoken. The sentence is now retired only by a later background poll success, or by the degraded or refusal onset, which is where the sentence becomes false rather than merely old.The refusal retraction also gets its own sentence. It is raised the moment the LIST read answers, on a tick whose composite read can still fail at the deep-link leg and return before the queue is replaced, so the shared #2630 sentence's second clause ("Showing current proposals") was unproven for up to two further poll intervals.
review.queue.refused.recoveredsays only that the server is accepting refreshes again and nothing about the queue's contents.Refs #2638 (item 2; items 1 and 3 are parked on the D-15 layout decision and are not touched here), Refs #2214 (the refusal-retraction copy follow-up recorded from PR #2694's round 2), Refs PRs #2630, #2694.
Changes
Commit 1, the retirement rule (
src/composables/useReviewProposals.ts):recordQueueRefreshSuccesstakessource: 'poll' | 'explicit', defaulting to 'explicit' so every caller that is not the poll is safe by construction, and a background-read ordinal (backgroundQueueReadCount, incremented at the top ofrefreshProposals) records which read raised the standing sentence.recordQueueListReadSucceededand therecoveryAlreadyRaisedguard keep working; [Frontend][Review] Reset interrupted transient poll streaks #2445's composite semantics for the transient counter are untouched.queueRefreshRecoveredbecomes a computed over a newqueueRefreshRecoveredKindref ('degraded' | 'refused' | null), so existing consumers read it exactly as before.Commit 2, the refusal sentence (
src/locales/{en,it,es}/review.ts,src/views/paper/PaperReviewView.vue,src/views/LegacyReviewView.vue):review.queue.refused.recoveredin the three catalogs, with a translator note saying the clause about the proposals being current belongs todegraded.recoveredand must not be repeated here.review.queue.refused.recoveredwhen the kind is 'refused' and keepreview.queue.degraded.recoveredotherwise. The regions stay always mounted, keep their role, aria-live and aria-atomic, and keep the fix(review): localize the degraded-queue warning, announce recovery, keep it visible #2630 hoisting above the branch pair; nothing else in either view moved.Commit 3, the specs (
useReviewProposals.spec.ts,ReviewView.spec.ts,PaperReviewView.spec.ts).Behaviour deliberately kept: when a fully successful read ends a visible degraded state, the queue sentence wins over a 'refused' kind raised earlier in that same read, because that read did replace the rendered rows.
Test plan
Verified, from
frontend/taskdeck-web:npx vitest --run --maxWorkers=2 src/tests/composables/useReviewProposals.spec.ts: 4 failed / 161. "is not retired by an explicit load inside the poll interval" and "is not retired by an explicit load that follows a FAILED background tick" both failed atexpect(rp.queueRefreshRecovered.value).toBe(true)after the explicit load (expected false to be true, the defect itself); "announces the retraction with the refusal sentence, not the queue sentence" and "says nothing about the queue when the pin leg strands the composite read" failed onrp.queueRefreshRecoveredKindbeing undefined.ReviewView.spec.ts > discloses a list read the server keeps refusingand the newPaperReviewView.spec.ts > announces a refusal retraction with the refusal sentence, not the queue oneeach failed with the region rendering "This review queue is up to date again. Showing current proposals." where the refusal sentence is expected. 6 failed / 450 across the five files.npx vitest --run --maxWorkers=2 src/tests/composables/useReviewProposals.spec.ts src/tests/views/paper/review/PaperReviewView.spec.ts src/tests/views/ReviewView.spec.ts src/tests/views/paper/review/PaperReviewView.language.spec.ts src/tests/i18n/catalogs.spec.ts: Test Files 5 passed, Tests 450 passed. The catalog parity guard covers the new key, the Paper language spec pins the Italian degraded recovery unchanged, and the fix(review): disclose refused poll refreshes, name a malformed link, and make the explicit-path outcome consistent #2694 explicit-path outcome matrix stays green.ReviewView.coverage.spec.ts,PaperReviewActiveProposalStability.spec.ts,PaperReviewMembershipFilter.spec.ts,ReviewQueueRail.spec.ts,workspaceReviewRoute.spec.ts,nativeBrowserDialogs.spec.ts,primaryActionGuards.spec.ts.npm run typecheckclean,npx eslintclean on the nine changed files,git diff --checkclean.NOT verified: Playwright and a real screen-reader run, so the claim that the sentence now survives long enough to be spoken rests on the composable's state, not on an announcement observed by assistive technology. The Italian and Spanish wording for the new key is this lane's, not native-reviewed (OUTSTANDING_TASKS #1770). No browser check of either skin.
Boundaries and risks
Owned and touched:
useReviewProposals.ts, the three review catalogs (review.queue.*only),useReviewProposals.spec.ts, and the two review views plus their specs for the region's key alone. Not touched: the sticky layout seam (issue items 1 and 3, D-15), the receipt recorders, the diff pane,useReviewActions.ts(its reload calls are consumers of the fix, not part of it).Risks: a recovery sentence now stands until a later background read succeeds, so if the poll is stopped or suspended while a sentence is up, it stays up until the surface unmounts; the visible warnings are unaffected and the sentence is gated on
!queueAccessRevokedin both skins. Two live regions can still speak within one interval when a refusal and a degraded state resolve in sequence, which is the #2214 double-announcement item already tracked there.Round 2
One MEDIUM from the fresh-context review, fixed at
32c46790c.raiseQueueRecoverystamped every raise with the ordinal of the last STARTED background read. For a poll raise that is the read currently running, which is correct, but the #2630 explicit-clear path (loadProposalsWithOutcomeintorecordQueueRefreshSuccess) raises BETWEEN ticks, so the stamp named a read that had already finished and the very next tick satisfiedcount > stamp. A post-decision reload that ended the degraded state 14.9 s into a 15 s cycle was therefore blanked about 100 ms later: the same defect this PR exists to close, with the roles swapped. Not a regression against main, but the comment claimed a mechanism that only held for poll-raised sentences.The stamping rule is now explicit and set by source. A poll raise is stamped with the read it was raised in, so it survives that read and retires on the next later poll success. An explicit raise is stamped with the read that has not started yet, so it survives the next poll success and retires on the one after. Either raise gets at least one full poll interval, and the comment at
backgroundQueueReadCountstates both halves. The degraded and refusal onsets still retire immediately for both stamps, because there the sentence is false rather than merely old.sourceis threaded throughrecordQueueListReadSucceededso a refusal retraction is stamped by the same rule.Two comment-only LOWs taken in the same commit. The queue-sentence upgrade is now justified by a visible degraded state having ended rather than by the read completing, and the refusal-only case (a fully successful poll clearing a refusal with no degraded state keeps the refusal sentence) is named as a deliberate under-claim that says nothing false. The retirement comment now names the reachability-limited gap: a hash-pinned by-id read failing every tick with a status that is neither transient nor a pin-level outcome (405, 410 or 409, which this route is documented not to emit) records no further composite success, so only an onset retires a standing sentence now that an explicit success no longer bounds it.
Red first against the round-1 head,
npx vitest --run --maxWorkers=2 src/tests/composables/useReviewProposals.spec.ts: 1 failed / 163. "gives an EXPLICIT-load recovery a full interval before a poll can retire it" failed with "expected false to be true" at the assertion after the FIRST poll tick, which is the defect. Its companion pin, "still retires an explicit-load recovery immediately at a degraded onset", was green before and after by design: it guards the onset path against this fix.Green at the round-2 head: the same five-spec command as above gives Test Files 5 passed, Tests 452 passed, run before and again after merging
origin/main27b0e9191(PR #2700, the inbox composer, disjoint from every file here).npm run typecheckclean,npx eslintclean on the nine changed files,git diff --checkclean.NOT verified is unchanged from round 1: no Playwright, no screen-reader run, no browser check, and the it/es wording for the new key is this lane's rather than native-reviewed.