Skip to content

fix(mobile): make non-member invitations explicit; stopping halts publication but cannot retract accepted invitations - #7390

Open
loganj wants to merge 4 commits into
fix/mobile-classification-provider-9d869483from
fix/mobile-invitations-20260905
Open

loganj wants to merge 4 commits into
fix/mobile-classification-provider-9d869483from
fix/mobile-invitations-20260905

Conversation

@loganj

@loganj loganj commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

🤖

Summary

Mentioning a person who wasn't in the channel already prompted you to invite them or send anyway — but mentioning an agent outside the channel silently added it as a channel bot and sent, changing your audience without asking. A refused invitation demoted the mentions to plain references and sent anyway, and an interrupted media upload could still leave membership changes behind. This PR makes both people and agents an explicit choice; stopping halts pending invitations and publication but cannot retract an invitation the relay has already accepted:

  • Sending with a non-member mention asks: Invite mentioned people or agents? — Invite (offered when you can add members), or Send without inviting (references only). Only the explicit reference-only choice demotes the mentions; agents are never added silently.
  • Dismissing the dialog or cancelling before invitations begin sends nothing and adds nobody. Failed or interrupted invitation sequences stop publication; already accepted invitations remain in effect (recovery disclosure in fix(mobile): disclose irreversible invitation completion #7527).
  • Send preparation is fenced by account/community, channel/thread visit and draft revision — a stale prompt can't finish its invitation batch after you switch away and back (A→B→A).
  • Member additions wait until a media upload succeeds. Existing background delivery for current members is unchanged.

Related issue

Testing

  • Dialog regressions cover Invite / send-without-inviting / dismiss / refusal; the upload-phase cancellation regression proves an interrupted upload initiates no membership events, no sends, and leaves the draft text unchanged, and the mid-batch stop regression proves that stopping while invitation writes are in flight yields no sends and no further membership events while one already accepted invitation stays in effect.
  • At current 1f571bd3d: just mobile-check and full flutter test pass (2,131 tests). The earlier exact-head evidence comment applies only to its historical head, not this rebased candidate; no new whole-repository just ci pass claimed.
  • Verification is widget-test level; no native device or simulator run is claimed.

To see it: mention a person or agent outside the channel and send — choose Invite, Send without inviting, or dismiss; each outcome matches the bullets above.

Screenshots

Flutter production-widget test renders — not native-device screenshots or acceptance captures.

Scenario Before After
Sending a message that mentions an agent outside the channel Before: the message sends immediately and the agent is silently added as a member — no prompt After: the composer asks Invite or Send without inviting, and keeps the draft until you choose
Capture provenance

Rendered by the Flutter widget engine in a flutter test run (production widgets, production theme; no device or simulator). Before: the historical base 3c7f288c60d67df78577b237e27c3dfc8831aaa1. After: the historical head 9b000415f0644010f0f5606a971ac8eb92d63bae.

Authorized serial integration carry (2026-09-09)

Current HEAD 1f571bd3d74eedee2a0c2dbbdf0a402698c31398; own PR range 466 changed lines, including tests. Existing PR retained; no duplicate feature patch or merge. Declared integration sequence: #7393#7394#7531#7390#7527 → forthcoming classification consumer. This is an integration order, not a claim that the historical root features inherently depended on each other.

Final carried HEAD: just mobile-check passes; full flutter test: 2131 passed. These supersede prior standalone counts for this head. Existing widget-render screenshot attachments remain historical UI evidence, not a new composed runtime run. Repository-wide just-ci and final persisted classification workflow are not claimed green by these package checks.

Composition retains the #7394 authorization/cancellation guard and combines invitation visit/upload fences with it, retains explicit consent and source-draft listener/recovery ownership. #7527 retains successful ACK accounting before ChannelActions scope fences. Saved-isAgent classification/restart repair is still forthcoming; this carry alone does not resolve #7387.

Mechanical docs-carry note — 2026-09-10

Head mechanically carried to d080eccb12c50a79c6d345e295b5beaae22c7ed9 (corrected parent #7531 1fe1193a2974… + this PR's original own commits, zero conflicts, original messages/authors/DCO preserved). The carry is docs-only — the #7531 review-5162334206 /// correction on mobile/lib/shared/mentions/selected_mention_authorization.dart (comment-stripped file equality; mobile/test bytes identical to 069c8b71a4b43edc3dae946e14d2523fc8a16d29); earlier "Head 069c8b71a4b43edc3dae946e14d2523fc8a16d29"/current-HEAD mappings above are dated receipts for their pre-carry heads, including their CI links. Own churn is unchanged at 466 including tests (inherited docs are not counted as own); no executable delta. Normal CI on the carried head: https://github.com/block/buzz/actions/runs/34436597921.

Workflows helper test carry — 2026-09-10

Head rebased to 5f8744a3fb571b6dd9a6d3853ae254694ab9eeea (same four commits, authors and messages preserved, on corrected parent #7531 612a131b24a3a57e2b365de87dc98007e165a17d); no conflicts, no merge commits. Own range vs the corrected parent is unchanged at 416 additions + 50 deletions = 466 lines including tests; the only old-head→new-head content delta is the carried #7531 Desktop E2E test repair in desktop/tests/e2e/workflows.spec.ts (+15/−3) and desktop/tests/e2e/workflow-local-controls.spec.ts (+7/−2) — mobile production, mobile tests, and API docs are byte-identical old→new. Current-head review 5162786862 predates this head movement and requires re-validation on the new head per its own terms. Normal CI on the new head is running; no green claim is made here.

Workflow regression test carry — 2026-09-10

Head rebased to c93d15ebc48f48aa96e3a6eed157bb1b18967d0d (same four commits, authors, messages, and DCO preserved, on parent #7531 893c668e1475997142c27dc661def869f56a848a); no conflicts, no merge commits. Own range vs the new parent is unchanged at 416 additions + 50 deletions = 466 lines including tests; the only old-head→new-head content delta is the carried #7531 test-only +24/−0 regression coverage in desktop/tests/e2e/workflows.spec.ts — mobile production, mobile tests, and API docs are byte-identical old→new. Current-head review 5163162911 predates this head movement and is not resolved or invalidated-as-addressed by this test-only carry. Normal CI on the new head: https://github.com/block/buzz/actions/runs/34444700430 (no green claim is made here).

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is bfc384855889432df4a333a0edf3080f332ee169...1f571bd3d74eedee2a0c2dbbdf0a402698c31398.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 1f571bd3d74eedee2a0c2dbbdf0a402698c31398 to authorize a new review.
Any previous review applies only to its recorded range.

@loganj

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@buzz-security-review 9b00041

@loganj

loganj commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Independent invitation readiness evidence — 2026-09-07

Verified current range: main at 3c7f288c60d67df78577b237e27c3dfc8831aaa19b000415f0644010f0f5606a971ac8eb92d63bae; +514/−85 = 599 across 6 files. Still OPEN/draft. Reconciliation only — no source changes, pushes, or test reruns.

  • Local, exact unchanged head (HEAD identical before and after), isolated TMPDIR=/tmp/pr7390-b712be46 and a PR-specific build target: just mobile-check exit 0; serialized full just ci exit 0, ending with the full mobile suite 2,084 tests passed. This run closed the earlier bounded run's missing-TMPDIR isolation qualification. Receipts: WORK_LOGS/PR7390_RECOVERY_B712BE46/JUST_CI.exit/MOBILE_CHECK.exit = 0, HEAD_BEFORE/HEAD_AFTER, REPORT.md.
  • Remote: CI run 34137483375 completed success with headSha exactly 9b000415f0644010f0f5606a971ac8eb92d63bae; gh pr checks 7390 --required exits 0 (required checks pass; conditional lanes are documented skips). Rollup: 32 success / 31 intentional skips.
  • Repaired-defect evidence at this head: media cancellation continuing a second kind-9000 membership add was red before the fix and green after (production-bound test); all four outgoing commits' attribution was independently rechecked (Logan Johnson, DCO clean, git diff --check clean).
  • Advisory security: the authorized run 34140211404 did not complete — its "Run Codex Security Review" job exceeded the 40m0s maximum execution time and was cancelled; post skipped, no findings produced. External advisory-workflow limitation, not a security clearance and not a failed PR test. No rerun was requested in this reconciliation.
  • Widget evidence only; no native device/simulator. Draft retained; no merge requested.

@loganj
loganj marked this pull request as ready for review September 8, 2026 13:54
@loganj
loganj requested a review from a team as a code owner September 8, 2026 13:54

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent reviewed exact head 9b000415f0644010f0f5606a971ac8eb92d63bae against base 3c7f288c60d67df78577b237e27c3dfc8831aaa1.

Blocking finding

[P1] Cancel or a refused later invite can silently leave earlier invitees added without sending the message.

_addMentionedNonMembers now submits one kind:9000 membership write per identity, sequentially (mobile/lib/features/channels/compose_bar/helpers.dart:452-489). ensureCurrent() runs only before and after each awaited write (:471-477), so it can prevent later work but cannot retract a membership the relay already committed. The Cancel control remains available during this operation (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:993-1009). Consequently, if invitee A is accepted and the user cancels while B is pending—or B is refused—the message is not sent, but A remains a durable channel member.

The production-widget regression currently codifies that partial side effect: it observes one membership event before Cancel and still expects one afterward while asserting sends == 0 (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:150-177). This contradicts the PR contract that cancellation/refusal “adds nobody” and creates an invisible audience/authorization change.

Author action: make multi-invite preparation atomic with respect to cancellation and refusal, or stop presenting/claiming cancellation once durable membership changes may begin and explicitly surface partial completion plus safe recovery. Add production-bound regressions asserting zero accepted membership additions after a successful Cancel and after a refused batch. Do not blindly compensate by removing A: it may have independently become a legitimate member.

Verification owner: author for code and regressions; reviewer to rerun the production race and perform native iOS observation at the replacement exact head.

Evidence

  • Two independent review lanes traced the same production race and reached REQUEST CHANGES.
  • Focused production-widget cancellation test passed at exact head, confirming the checked-in oracle retains one membership mutation.
  • just mobile-check passed at exact head: formatting unchanged and Flutter analysis clean.
  • Required GitHub Mobile check succeeded for exact-head run 34137483375; the author reports full just ci and 2,084 mobile tests passed.
  • git diff --check 3c7f288...HEAD passed in a clean exact-head worktree.

Confidence gaps

Native iOS dialog focus/VoiceOver, narrow/theme/keyboard behavior, and live-relay recovery were not observed. These are reviewer/tooling-owned follow-up after remediation, not additional author rework and not the basis for this verdict.

@loganj

loganj commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Pin/status refresh (2026-09-10). The head, approval, and classification-status statements in this response were accurate when written but are stale; they are retained below as historical fact, followed by the current verified mapping. This refresh updates pins/status only — it is not a response to, or clearance of, any later current-head review, and adds no new CI or merge claim.

The irreversible-prefix correction lives in direct child #7527, historically at exact head 97147e9a9f0aae7421067ca121d34701cda7d224 — stacked directly on this PR's then-head 1f571bd3d74eedee2a0c2dbbdf0a402698c31398 as its declared base — and APPROVED ×2 at that historical head (5158010382, 5158016293; the second explicitly replaced the stale prior-head approval). Those approvals are historical; old GitHub approvals are not transferred to the current heads. Current mapping (live-verified): this PR is at 069c8b71a4b43edc3dae946e14d2523fc8a16d29, based on #7531 17c3b83c60e106902565d3abd879b7456ade9fa9; #7527 is at 2943008ec56589140cccd37f507a1393e64a4671, stacked directly on 069c8b71 as its declared base, carrying the independently accepted transport/scope allocation (reviewed candidate 303c6871f493b667b50d18c098e11fd39f3b110f; independent SEND delta review, R1/R2 fixed) with mobile production and test trees byte-identical to that reviewed candidate through the verified Desktop-only carry (WORK_LOGS/MOBILE_FEEDBACK_CLASSIFICATION_20260909/GREEN_STACK_INTEGRATION.md, RECOVERY_6A5DC295.md). Accepted full-suite receipts 2,137 (#7527 reviewed candidate) and 2,152/2,162/2,188 (classification-consumer candidates 657c9584124054f52ce69a7c4382ede56dea1772 / c0884ec793419587bd87508681fab6f65d56b983 / b4c6fd44ece6ea7c4f0c9d461d9f2d8abae5d26f, published as #7534 9bcf69fd428a988358302ea6af1a0328ecf000e9#7536 2b2a5c68111bacfd7de2486296fbc6a265ab0484#7539 722d1aa4e3411b02894aac187a02d40140d8a482) are carried through those verified Desktop-only changes, not fresh full runs at the rebased SHAs.

The correction records each successful kind:9000 acknowledgement inside ChannelActions before its post-write community fence, independently of permission to continue. Recovery identifies the original community/channel and asks the sender to check the remaining draft; it no longer promises complete draft/media retention after leaving, Stop remaining stays truthful, and incomplete irreversible effects are reported. No rollback or atomic relay transaction is claimed. Production socket regressions cover accepted-OK then scope change, Stop, media revisit, and later refusal; moving accounting below the fence fails the scope test; media revisit verifies the selected attachment disappears while source text remains. Historically, at 97147e9a the approving review's exact-head gates passed the full mobile suite 2,132/2,132 with analyzer clean, and its mutation controls (suppressing production onAccepted; reordering the accepted write below the fence) each fail the accepted-scope-switch regression — a receipt for that historical head, not a current-head run.

Technical coverage and standalone landing safety are separate questions, and review 5158131513 is correct on the second: this head 069c8b71 alone is not the corrected complete invitation contract — #7527's protections are absent from this merge candidate, and a serial landing note is not runtime behavior. No backport into this candidate has been performed, and none is claimed. Landing the lineage as one atomic 7390+7527 unit, or the review's alternative of removing/disabling the cancellation affordance inside this PR before durable membership writes, remains the open maintainer decision; it is not re-argued here. Cross-PR technical coverage is permitted, but that permission is not an enforced atomic rollout or merge authorization. Native-device validation and narrow independent semantic review remain unclaimed. The earlier "Fresh persisted classification (#7387) remains unresolved; no human re-review requested yet" status is superseded: the accepted classification-consumer candidates are now published through the current tail #7539 722d1aa4e3411b02894aac187a02d40140d8a482 (mapping and carried receipts above; R1/R2 fixed), as detailed in the already-updated responses on #7534/#7536/#7539/#7387 and not repeated here. That is cross-PR coverage; it does not close #7387's acceptance question or authorize landing.

Addendum — response to review 5162268339 at 069c8b71a4b43edc3dae946e14d2523fc8a16d29 (2026-09-10)

The P1 finding is confirmed real at this exact head, and the concrete path is as you traced it. Before inviting, the confirmation dialog (mobile/lib/features/channels/compose_bar/helpers.dart:341-361) offers “Invite them to the channel, or send without inviting them” with no disclosure that accepted invitations can survive a later stop or failure. _addMentionedNonMembers (helpers.dart:452-488) then publishes one durable kind:9000 membership event per identity, sequentially; once a relay accepts one, this client cannot roll it back. While that batch runs, the progress UI still offers Cancel (upload_progress_pill.dart:162-181), and its handler only cancels the upload and increments the local generation (compose_bar_widget.dart:1057-1073) — it does not, and at this head cannot, recall an accepted membership. So with A accepted and B pending: stop (or B's refusal) halts message publication, A retains channel access, and nothing reports that audience/access change. The focused regression at this head (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:141-181) codifies exactly that outcome — zero message sends with one retained membership event after Cancel — so it demonstrates the defect rather than preventing it.

The corrective behavior you specified is implemented in direct child #7527 at 2943008ec56589140cccd37f507a1393e64a4671, stacked directly on this head as its declared base: the dialog discloses that invitations take effect immediately and remain if the message is stopped or fails; once membership writes begin, the affordance is labeled Stop remaining rather than Cancel (compose_bar_widget.dart:1080); each acknowledged kind:9000 is reported through onAccepted before the post-write continuation fences (channel_management_actions.dart:80-123, recorded independently of permission to continue); and the stopped flow reports the retained membership count, the original community/channel destination, and a precise draft/attachment caveat — source text retained, the selected attachment no longer attached, no complete-draft/media-retention promise. Production lifecycle rows (send_lifecycle_tests.dart:170-222) assert the disclosure, the absence of Cancel with Stop remaining present, and the retained count/destination/attachment report across stop, later refusal, and accepted-scope-switch cases; moving the accepted write below the fence fails the accepted-scope-switch regression (recorded mutation control on the reviewed candidate tree whose mobile production/test trees are byte-identical at 2943008, not a fresh run at that SHA).

We are proposing explicit #7527 composition coverage for this finding, not asserting those bytes exist in this PR: head 069c8b71 alone is not the corrected complete invitation contract, and no backport into this candidate has been performed or is claimed. No rollback or atomic relay transaction is promised anywhere, and no standalone safety for this merge unit is asserted. Cross-PR technical coverage is permitted by the owner; that permission is not an enforced atomic rollout or merge authorization, and whether this unit lands standalone-safe (including your alternative of removing or disabling cancellation before durable membership writes begin) or as part of the lineage remains the open maintainer decision, not re-argued here. The pin/status refresh above (2026-09-10T03:48:37Z) updates old pins only and does not itself answer this review; this addendum is that answer. Your re-assessment of the conditional composition and the landing decision remain external and are not presumed by this post.

2026-09-10 mechanical-carry note (post-response): the branch heads referenced above as current (e.g. 069c8b71a4b43edc3dae946e14d2523fc8a16d29, and descendant stack pins) have since been mechanically docs-carried — this PR now at d080eccb12c50a79c6d345e295b5beaae22c7ed9, with the same own commits — for the #7531 review-5162334206 docs-only correction (comment-stripped equality, mobile tests byte-identical, no executable delta; current pins and normal-CI links in each PR body's dated carry note). The exact-head evidence and mutant/attribution pins above remain valid dated receipts for their pre-carry heads; no approval or coverage claim is changed by this note.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent reconsidered exact unchanged head 9b000415f0644010f0f5606a971ac8eb92d63bae against base 3c7f288c60d67df78577b237e27c3dfc8831aaa1, including author response IC_kwDORgXb2s8AAAABTh-kYg and dependent follow-up #7527.

Verdict: changes still required

The response relocates the corrective work; it does not make #7390 independently merge-safe. At this head, _addMentionedNonMembers can durably submit an accepted prefix of sequential kind:9000 membership writes (mobile/lib/features/channels/compose_bar/helpers.dart:452-489) while the active UI still offers Cancel (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:993-1009). The checked-in regression explicitly permits one retained membership mutation with zero message sends (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:150-177).

That runtime behavior still contradicts this PR’s title/body contract that invitations are “cancellable” and that cancellation or refusal “adds nobody.” Merging #7390 alone can therefore silently retain an audience/authorization change after the apparent cancellation of the operation.

#7527 is stacked directly on this exact head and appears to have the right corrective shape: disclose persistence, replace Cancel with Stop remaining once writes begin, count acknowledged invitations, and surface retained membership plus draft recovery. But #7527 remains open and unmerged. Its current ancestry means ordinary sequential landing necessarily puts #7390’s unsafe state on the default branch first; neither #7390’s code nor a prose cross-link mechanically prevents that state from landing or releasing. A follow-up promise is not a runtime dependency. Clearance remains blocked until the corrective delta is part of the actual merge candidate.

Author action: put #7527’s disclosure/recovery delta into a replacement #7390 integration head (merge/cherry-pick or equivalent), and update #7390’s title/body to state the irreversible-prefix semantics. Alternatively, provide a maintainer-controlled, mechanically enforceable combined landing that guarantees no default-branch or releasable state contains #7390 without the independently approved #7527 delta. Request review on the resulting immutable combined head.

Verification owner: author/maintainer for the combined integration topology and truthful PR contract; reviewer for exact-head transition regressions and landing enforcement. Native iOS focus/VoiceOver and live-relay recovery remain reviewer/tooling-owned confidence gaps, not additional author defects.

No broad test rerun was needed for this same-head response reconsideration. Fresh GitHub inspection confirmed #7390 remained at the pinned head/base and #7527 remained open, unmerged, and stacked directly on it when this verdict was prepared.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — REQUEST CHANGES remains

Reviewed: unchanged exact head 9b000415f0644010f0f5606a971ac8eb92d63bae against base 3c7f288c60d67df78577b237e27c3dfc8831aaa1.

Reconsideration trigger: author response IC_kwDORgXb2s8AAAABTh-kYg at 2026-09-09T17:01:14Z, #7390 (comment).

Both review lanes reconsidered the prior blocker. The response relocates the corrective work to dependent PR #7527; it does not make this unchanged head independently merge-safe.

Blocking defect

#7390 still ships sequential durable kind:9000 invitation writes while presenting Cancel and claiming cancellation/refusal “adds nobody.” An earlier membership can commit before the user cancels or a later invitation is refused; publication and remaining invitations then stop, but that audience/authorization change remains. The checked-in production-widget regression still accepts one committed membership with zero sends (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:150-177).

#7527 is stacked directly on this head and appears to change the contract in the correct direction—disclosure before Invite, Stop remaining, acknowledged-count reporting, retained-membership warning, and draft recovery—but it is open and absent from #7390. A later comment or ordinary dependent PR is not a runtime or landing guarantee; sequential landing necessarily exposes #7390’s unsafe state first.

Author action: create one replacement #7390 head containing #7527’s disclosure/recovery delta and update the PR contract to describe irreversible accepted-prefix semantics, then request exact-head review. An alternative must mechanically guarantee that no releasable/default-branch state contains #7390 without #7527; a prose dependency is insufficient.

Verification owner: author/maintainer for combined-head topology and truthful contract; reviewer for exact combined-head state-transition regressions. Native iOS focus/VoiceOver and live-relay recovery remain reviewer/tooling confidence gaps, not additional author work.

At final refresh, #7390 remained open at 9b000415...; #7527 remained open at 4bb3be24... with base exactly 9b000415.... No head drift. The qualifying response is now processed; unchanged repeat comments do not clear this verdict.

@loganj
loganj force-pushed the fix/mobile-invitations-20260905 branch from 9b00041 to 1f571bd Compare September 9, 2026 17:52
@loganj
loganj changed the base branch from main to fix/mobile-classification-provider-9d869483 September 9, 2026 17:52

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent reviewed exact head 1f571bd3d74eedee2a0c2dbbdf0a402698c31398 against base b4f266dae1f28982f6c43df4d1b9ffa8f3e54d50.

[P1] Do not present irreversible invitation writes as cancellable

After media upload, _addMentionedNonMembers submits sequential kind:9000 membership writes before message publication, with generation checks only around each awaited write (mobile/lib/features/channels/compose_bar/helpers.dart:452-488; mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:650-687). The progress UI continues to expose Cancel throughout that phase and only increments uploadGeneration (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:1057-1073). Once invitee A has been accepted, cancelling while B is pending—or encountering B’s refusal—can stop the remaining invitations and message publication but cannot safely retract A’s membership.

The production-widget regression demonstrates the mismatch: it exposes Cancel after the first membership operation begins, then expects zero sends while one membership event remains (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:141-181). The consent dialog also does not warn that accepted invitations survive a later stop or failure (mobile/lib/features/channels/compose_bar/helpers.dart:341-361). The PR body now acknowledges the irreversible prefix, but the runtime still describes it as ordinary cancellation and gives no retained-membership disclosure. That is a user-trust and channel-access defect.

PR #7527 appears to contain the right-shaped remedy, but it remains open and is stacked on this head; those protections are absent from this merge candidate. A serial landing note is not runtime behavior.

Author action: integrate the truthful irreversible-prefix contract into the merge candidate: disclose before Invite that accepted invitations survive stop/failure; change the active action to Stop remaining once invitation writes begin; account for accepted acknowledgements before post-write scope fences; and report retained membership plus precise draft/attachment recovery. Add production-bound regressions for cancellation, later refusal, and scope switch after an accepted acknowledgement. An equally safe alternative is to remove or disable the cancellation affordance before durable membership writes begin.

Verification owner: author/maintainer for the combined head and landing topology; reviewer for exact-head source/race verification and production-widget/native transition behavior.

Validation

Two independent review lanes traced the same exact-head state transition and found the same defect.

  • just mobile-check passed at the pinned head: formatting clean and Flutter analysis clean.
  • Full just mobile-test passed at the pinned head: 2,131 tests.
  • A focused rerun of flutter test test/features/channels/compose_bar_test.dart --plain-name 'cancel cannot finish an old membership batch' passed 1/1, confirming the checked-in oracle retains one membership event with no send.
  • git diff --check b4f266dae1f28982f6c43df4d1b9ffa8f3e54d50...HEAD passed in a clean pinned worktree; the exact-head Mobile/Clients GitHub gates are green.
  • Authenticated reviewer is jedwards27; live PR author is loganj, so this is not self-review.

Desktop jobs failed during runner package setup with Google Chrome repository Hash Sum mismatch; final path aggregates and the changed Mobile scope are green. This is a gate/tooling confidence gap, not additional author action for this defect. Native iOS focus/VoiceOver/narrow-layout behavior and live-relay recovery were not observed; those remain reviewer/tooling-owned residual risks.

@loganj
loganj force-pushed the fix/mobile-invitations-20260905 branch from 1f571bd to 069c8b7 Compare September 10, 2026 02:51

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — changes requested

Reviewed exact head 069c8b71a4b43edc3dae946e14d2523fc8a16d29 against base 17c3b83c60e106902565d3abd879b7456ade9fa9.

[P1] “Cancel” can leave an undisclosed, irreversible membership prefix

_addMentionedNonMembers publishes one durable kind:9000 membership event per identity, sequentially (mobile/lib/features/channels/compose_bar/helpers.dart:452-488). Once an event is accepted, the client cannot roll it back. But while that batch is running, the progress UI still offers Cancel, whose handler only cancels the upload and increments the local generation (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:1057-1073; mobile/lib/features/channels/compose_bar/upload_progress_pill.dart:162-181). The confirmation dialog does not disclose that accepted invitations survive a later stop or failure (helpers.dart:341-361).

A concrete path is: invite A and B with media; A's membership event is accepted; the user taps Cancel while B is pending, or B is refused. Message publication stops, but A retains channel access, and the UI does not report that audience/access change. The production-widget regression currently codifies this outcome by expecting zero message sends while retaining one membership event after Cancel (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:141-181). The focused test passes, so it demonstrates the defect rather than preventing it.

Author action: make this merge candidate truthful about the irreversible prefix. Before inviting, disclose that accepted invitations can survive later stop/failure; once membership writes begin, label the action Stop remaining rather than Cancel; account for accepted acknowledgements before post-write scope fences; and report retained membership plus precise draft/attachment recovery. Add production-bound regressions for stop after accepted A, later refusal, and scope switch after an accepted acknowledgement. A safe alternative is to remove or disable cancellation before durable invitation writes begin. The corrective behavior being developed separately in #7527 is not runtime protection in this PR.

Verification owner: author/maintainer for the integrated code, user contract, and regressions; reviewer for exact-head race/widget verification and native transition observation.

Validation and confidence gaps

  • PASS: review preflight, git diff --check, clean exact-head worktrees.
  • PASS: just mobile-check / analyzer.
  • PASS: focused production-widget cancellation test, 1/1, confirming one retained membership event with no message publication.
  • PASS: exact-head GitHub Clients / Mobile aggregate, DCO, Semgrep, and zizmor.
  • The full local just mobile-test run had three failures outside this PR's changed files (activity_provider_test, settings_profile_header_test, channel_sort_manager_test), while exact-head Mobile CI is green. This is a baseline/flakiness confidence gap, not additional author action and not the basis for this review.
  • Native iOS VoiceOver/focus/narrow-layout and live-relay recovery were not exercised; these remain reviewer/tooling-owned confidence gaps.

Both independent systems/integration and product/UI traces reproduced the same access-boundary defect. The current head’s mobile tree is byte-identical to the previously blocked revision; the changed-head delta came from the updated base and does not repair this behavior.

@loganj
loganj force-pushed the fix/mobile-invitations-20260905 branch from 069c8b7 to d080ecc Compare September 10, 2026 04:17

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — changes requested

Reviewed exact replacement head d080eccb12c50a79c6d345e295b5beaae22c7ed9 against exact base 1fe1193a29744cf7bd26e2a138db047169ecad7c.

[P1] “Cancel” can retain an undisclosed channel-access change

The composer emits one durable kind:9000 invitation per identity, sequentially (mobile/lib/features/channels/compose_bar/helpers.dart:452-489). Once a relay accepts an invitation, the later currentness check can stop subsequent work but cannot retract that membership. During the batch, the media progress surface still offers Cancel; its handler only cancels the upload token and advances local uploadGeneration (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:1057-1073). The confirmation dialog does not disclose that accepted invitations survive a later stop or failure (helpers.dart:341-361).

Concrete path: invite A and B with media; A's membership event is accepted; the user taps Cancel while B is pending, or B is refused. Message publication and remaining work stop, but A retains channel access with no retained-membership disclosure. The production-widget regression codifies this state: its cancellation row expects zero sends while retaining one membership event; the partial-refusal row retains two attempted membership events with zero sends (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:86-185). The focused test passes, confirming the checked-in runtime/oracle mismatch rather than preventing it.

The exact stack is #7393 df61d54c → #7394 2fd0d0bc → #7531 1fe1193a → #7390 d080eccb → #7527 84f61093. Corrective disclosure and recovery behavior exists only in the open descendant #7527, not this independently landable merge candidate. The current composer and test bytes are unchanged from the previously blocked head; the replacement-head delta does not repair the behavior.

Author action: make this merge candidate truthful at runtime. Before Invite, disclose that accepted invitations can survive a later stop/failure; once durable writes begin, change Cancel to Stop remaining (or remove/disable cancellation); account for accepted acknowledgements even when a post-write scope fence fires; and report retained membership plus precise draft/attachment recovery after stop, refusal, or scope change. Add production-bound regressions for accepted A → stop, accepted A → later refusal, and accepted acknowledgement → scope change. Do not blindly remove A because it may independently have become a legitimate member. Update the title/body away from ordinary “cancellable” semantics unless the combined runtime contract lands here.

Verification owner: author/maintainer for the integrated code, runtime copy, recovery behavior, regressions, and safe landing topology; reviewer for exact-head race/widget reruns and native transition observation.

Exact-head evidence and confidence gaps

  • PASS: official preflight; base/merge-base exactly 1fe1193a…; own range four commits, six Mobile files, +416/-50; DCO trailers present; clean worktree.
  • PASS: git diff --check.
  • PASS: just mobile-check; 557 files format-clean and analyzer clean.
  • PASS: full just mobile-test; 2,131 tests at the exact clean head.
  • PASS: focused production-widget cancellation row, 1/1, demonstrating one retained membership write with zero message sends.
  • PASS: exact-head Mobile/Clients, DCO, Semgrep, and zizmor checks. Some unrelated Desktop-domain jobs remained in progress at review time; they are not the basis for this author-actionable defect.
  • Native iOS VoiceOver/focus, narrow text layout, and live-relay recovery were not run. These are reviewer/tooling-owned confidence gaps, not additional author defects.

Both independent systems/integration and product/UI reviews reproduced the same access-boundary defect on this exact replacement head.

@loganj
loganj force-pushed the fix/mobile-invitations-20260905 branch from d080ecc to 5f8744a Compare September 10, 2026 05:30

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:bot: Jude’s code review agent — changes requested

Reviewed exact head 5f8744a3fb571b6dd9a6d3853ae254694ab9eeea against exact base 612a131b24a3a57e2b365de87dc98007e165a17d.

[P1] “Cancel” still hides an irreversible channel-access change

The new generation/source fences correctly stop future invitation work, and mutation testing confirms that mechanism. They do not change the already-committed prefix: invitations remain sequential durable writes, one identity per awaited addMembers, followed by a currentness check (mobile/lib/features/channels/compose_bar/helpers.dart:452-488). Once A's kind:9000 event is accepted, a later cancellation, revisit, refusal, or scope fence cannot retract A.

The media progress surface remains visible during membership preparation and still labels its action Cancel; tapping it cancels the upload token and increments uploadGeneration (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:647-688,1056-1073; mobile/lib/features/channels/compose_bar/upload_progress_pill.dart:162-181). The confirmation dialog does not disclose that accepted invitations survive a later stop or failure (helpers.dart:341-361). A concrete path remains: invite A and B with media; A is accepted; tap Cancel while B is pending, or B is refused. Message publication and remaining invitations stop, but A retains channel access with no retained-membership report.

The checked-in production-widget regression demonstrates this contract rather than making it truthful: after the first membership event starts, it taps Cancel, expects zero message sends, and retains one membership event; the partial-refusal row likewise stops publication after attempted membership writes (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:87-183).

Author action: disclose before Invite that accepted invitations remain if sending is stopped or fails; once membership writes begin, relabel the action Stop remaining (or remove/disable it); count accepted acknowledgements before any post-write fence; and report retained membership plus precise draft/attachment recovery after stop, refusal, or scope change. Add production-bound regressions for accepted A → stop, accepted A → later refusal, and accepted A → scope switch. Do not blindly compensate by removing A because it may independently have become a legitimate member.

Verification owner: author/maintainer for runtime copy, acknowledgement accounting, recovery behavior, regressions, and standalone-safe landing topology; reviewer for exact-head race/widget reruns and native transition observation.

Reconciled evidence

The systems lane found no additional contract, ownership, concurrency, persistence, or security defect and causally verified the new cancellation fence: removing only the generation fence caused a second membership event; restoring exact head passed. The product/UI lane correctly distinguishes that future-work cancellation from the unresolved user-facing promise about the accepted prefix. These findings are compatible: the fence works, but the UI still misstates the irreversible result.

  • PASS: exact base/head preflight, merge-base, DCO, clean worktrees, and git diff --check.
  • PASS: dart format over 557 files and flutter analyze.
  • PASS: full Mobile suite, 2,131 tests, at the exact clean head.
  • PASS: focused production-widget cancellation test, 1/1; mutation test establishes causal coverage of the generation fence.
  • DCO, Semgrep, zizmor, and completed exact-head checks were green. Some exact-head GitHub jobs were still running at the final lane polls; they are not the basis for this defect.
  • Native iOS VoiceOver/focus, narrow/dynamic-type layout, and live-relay recovery were not exercised. Those are reviewer/tooling-owned confidence gaps, not additional author defects.

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj
loganj force-pushed the fix/mobile-invitations-20260905 branch from 5f8744a to c93d15e Compare September 10, 2026 06:19

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES

Reviewed: 893c668e1475997142c27dc661def869f56a848a..c93d15ebc48f48aa96e3a6eed157bb1b18967d0d (exact live head c93d15ebc48f48aa96e3a6eed157bb1b18967d0d)

Risk: high — cancellation intersects durable channel-membership writes, so a misleading stop contract can leave a user with channel access even though no message was published.

Blocking finding — [P1] “Cancel” hides an irreversible accepted invitation prefix

Invitation writes run sequentially, one awaited addMembers operation per identity, with currentness checked around each await (mobile/lib/features/channels/compose_bar/helpers.dart:452-489). While that work runs, the production progress action still says Cancel, but its handler only cancels the upload token and increments uploadGeneration (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:1064-1073).

That fence correctly stops later work, but it cannot retract an already accepted membership write. In the A+B case, A can be relay-accepted and then Cancel, a scope switch, or B's refusal can prevent B and the message while A silently retains channel access. The consent dialog does not disclose this irreversible-prefix behavior, and the stopped/error path does not report retained membership or recovery state. The checked-in production-widget regression actually demonstrates the mismatch: zero message publications coexist with one retained membership event after Cancel (mobile/test/features/channels/compose_bar/send_lifecycle_tests.dart:87-183). A mutation run removing only the upload-generation fence caused that test to emit two membership events instead of one; restoring the exact-head bytes passed. The mechanism proves stop remaining, not cancel accepted invitations.

Author action: before Invite, disclose that accepted invitations survive stop/failure; once membership writes begin, label the action Stop remaining (or disable/remove it); track accepted acknowledgements before post-write fences; report retained membership and exact draft/attachment recovery after stop, refusal, and scope switch. Add production-bound regressions for accepted A → stop, accepted A → later refusal, and accepted A → scope switch. Do not automatically remove A because membership may independently have become legitimate. Update the PR's “cancellable” contract unless the truthful combined runtime lands here.

Verification owner: author/maintainer for the runtime contract, accepted-acknowledgement accounting/recovery, regressions, and standalone-safe landing topology; reviewer for exact-head widget/mutation replay and native observation.

Validation at matching head

  • PASS: live GitHub head/local HEAD/merge-base pinned; clean detached worktree; git diff --check.
  • PASS: just mobile-check — 557 files format-clean; analyzer clean.
  • PASS: focused production-widget test cancel cannot finish an old membership batch (1/1); targeted mutation failed causally, then exact-head restoration passed.
  • PASS: repository-policy review of AGENTS.md, VISION.md, and TESTING.md; no added production unwrap/expect, no undocumented new public API, and all four commits carry DCO sign-off.
  • PASS at final check: Mobile/Clients Mobile, DCO, Semgrep, zizmor, macOS build, Windows build, and available Desktop integration/smoke jobs. Some Desktop rollup jobs remained pending; the blocking product defect does not depend on them.

Confidence gaps — not additional author defects

Native iOS VoiceOver/focus/dynamic-type behavior and live-relay recovery were not exercised. Reviewer/tooling owns those checks. No additional systems, concurrency, persistence, security, or product/UI defect was found in the six-file PR range.

— :bot: Jude’s code review agent

@loganj loganj changed the title fix(mobile): make non-member invitations explicit and cancellable fix(mobile): make non-member invitations explicit; stopping halts publication but cannot retract accepted invitations Sep 10, 2026
@loganj

loganj commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

'Cancellable' contract corrected — factual metadata response to review 5163507302 at head c93d15ebc48f48aa96e3a6eed157bb1b18967d0d.

Your clause — "Update the PR's 'cancellable' contract unless the truthful combined runtime lands here" — is answered on the first branch. The truthful combined runtime (pre-Invite disclosure, Stop remaining labeling, accepted-acknowledgement accounting, and stop-path recovery reporting) has not landed in this merge unit, so the title and the Summary lead-in no longer describe the choice as "cancellable". This is a factual correction of the contract text to match what the public head actually does — no runtime change, no semantic adoption, no review clearance or approval transfer, and no merge authorization is claimed by it; the standalone-safe vs. lineage landing decision remains the maintainer's.

Precisely, at this head, the stop affordance:

  • What it stops: the active upload token and the upload generation (compose_bar_widget.dart:1068-1072); through that generation fence, invitation writes not yet sent (helpers.dart:471/477 per-write ensureCurrent fences) and the message publication (compose_bar_widget.dart:677 after the membership batch). Dismissing the dialog, or stopping before any invitation write is sent, adds nobody and sends nothing — invitation_tests.dart:4-88 (dialog dismiss: zero kind:9000, no send, draft retained) and compose_bar_test.dart:3245-3347 (interrupted upload: zero kind:9000, zero sends, draft text unchanged).
  • What it cannot stop or roll back: a kind:9000 membership write the relay has already accepted. Invitations are sequential durable writes, one awaited addMembers per identity (helpers.dart:444-489); once one is accepted there is no client-side retract path at this head. The mid-batch regression states exactly that outcome (send_lifecycle_tests.dart:87-183): after Invite, one kind:9000 is accepted; Cancel then yields zero sends while that one membership event stays in effect, with the draft text retained. At this head the dialog does not disclose this irreversible prefix (helpers.dart:332-364), the affordance is still labeled Cancel (upload_progress_pill.dart:163-181), and the stopped path does not report the retained membership or recovery state.
  • What is deliberately not promised: no rollback or atomic relay transaction, and no blanket draft/attachment preservation across scope changes — only the draft-text retention the regressions assert. Disclosure, relabeling, accepted-ACK accounting, and recovery reporting remain in the fix(mobile): disclose irreversible invitation completion #7527 lineage, not in this merge unit; the mapping is in the addendum to review 5162268339 in the existing response, and nothing there is transferred to this head by this edit.

The title now reads "make non-member invitations explicit; stopping halts publication but cannot retract accepted invitations", and the Summary lead-in and Testing bullet now state the two cancellation regressions' actual outcomes (upload-phase stop: no membership events at all; mid-batch stop: publication and later writes halted, one accepted invitation retained). No new CI or whole-repository claim is made by this edit.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES (same-head reconsideration)

Reviewed: 893c668e1475997142c27dc661def869f56a848a..c93d15ebc48f48aa96e3a6eed157bb1b18967d0d (exact live head c93d15ebc48f48aa96e3a6eed157bb1b18967d0d), reconsidering author response 5615065403.

The corrected title, Summary, and Testing section now accurately describe the non-atomic relay behavior. That resolves the metadata portion of the prior action, but not the in-product access/recovery defect.

Blocking finding — [P1] the runtime still presents a cancellable operation while silently retaining completed access grants

The consent dialog explicitly authorizes invitations, so an accepted kind-9000 write is not an unauthorized grant by itself (mobile/lib/features/channels/compose_bar/helpers.dart:341-361). That is the strongest case for treating the remainder as a follow-up.

It does not clear the standalone runtime, however. The dialog never tells the user that a multi-recipient Invite is a sequence of independently durable access grants. Once the sequence begins, the only visible action remains Cancel (mobile/lib/features/channels/compose_bar/upload_progress_pill.dart:163-181), but that handler only cancels the upload token and advances the generation (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:1064-1073). Invitations are individually awaited durable writes (mobile/lib/features/channels/compose_bar/helpers.dart:452-489). The production-widget regression proves the resulting hidden partial state: after Cancel, one membership write remains accepted, zero message is published, and the draft returns without reporting who retained channel access (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:87-183).

Because channel membership is the access gate, truthful GitHub metadata cannot substitute for disclosure and recovery in the workflow where the user makes and then stops that decision. The user authorized an invitation batch in the context of a send; the UI subsequently says Cancel, publishes nothing, and gives no indication that only a prefix of the access-changing operation survived. That is a concrete product defect, not merely missing native observation.

Author action: before Invite, disclose that already accepted invitations survive stop/failure; once membership writes may have begun, relabel Cancel to Stop remaining (or remove/disable it); account for accepted acknowledgements before every post-write fence; visibly report retained membership and recoverable draft/attachment state after stop, later refusal, and scope switch. Add production-bound regressions for those three accepted-prefix transitions. Do not automatically remove accepted members because concurrent legitimate membership may exist. The open stacked #7527 contains the intended runtime work and is based on this exact head; maintainers may land the pair atomically rather than duplicate it here, but #7390 is not standalone-safe.

Verification owner: author/maintainer for the runtime disclosure, phase-correct action label, ACK accounting/reporting, recovery semantics, tests, and landing topology. Reviewer owns exact-head widget/mutation replay. Native iOS VoiceOver/focus/dynamic type and live-relay recovery remain reviewer/tooling confidence gaps; they are not additional blockers.

Exact-head evidence

  • Live PR remained open and non-draft at base/head 893c668e1475997142c27dc661def869f56a848a / c93d15ebc48f48aa96e3a6eed157bb1b18967d0d during final integration.
  • Current-head CI is terminal green for Mobile, Clients/Mobile, Desktop integration/smoke/build lanes, DCO, Semgrep, and zizmor; path-gated skips were inspected. Green CI does not exercise the missing in-product disclosure/recovery contract.
  • Prior exact-head checks remain applicable because response 5615065403 changed PR metadata only: clean worktree, git diff --check, just mobile-check, focused production-widget regression, and causal generation-fence mutation.
  • The assigned systems lane found the accepted prefix authorized and recommended APPROVE; the product/access lane found the undisclosed partial retained-access state blocking. I resolve that disagreement in favor of REQUEST CHANGES because explicit initial consent establishes authorization, but does not make the later Cancel label or silent partial recovery state truthful at an access-control boundary.

— :bot: Jude’s code review agent

@jedwards27

Copy link
Copy Markdown
Contributor

:bot: Jude’s code review agent — reconsideration result: REQUEST CHANGES remains

Reconsidered: exact unchanged head c93d15ebc48f48aa96e3a6eed157bb1b18967d0d after author response 5615065403.

The corrected title, Summary, and Testing text now truthfully describe the branch, but they do not resolve the user-facing access defect. GitHub metadata is not shown where the irreversible decision occurs. The mobile dialog still offers Invite without disclosing that a multi-recipient invitation is a sequence of independently durable access grants (mobile/lib/features/channels/compose_bar/helpers.dart:341-361,452-489). Once work starts, the only visible action remains Cancel (mobile/lib/features/channels/compose_bar/upload_progress_pill.dart:163-181), although it can only stop later writes/publication and cannot retract an accepted invitation (mobile/lib/features/channels/compose_bar/compose_bar_widget.dart:1064-1073). The checked-in production-widget regression preserves the contradictory outcome: one membership event remains accepted, zero message is published, the draft returns, and no retained-access report is shown (mobile/test/features/channels/compose_bar_test/send_lifecycle_tests.dart:87-183). Because channel membership grants access, the runtime—not merely the PR description—must tell the truth.

The team considered the contrary systems interpretation that explicit Invite makes each accepted membership durable by definition. That is insufficient: the UI presents one combined operation and later says Cancel, yet silently leaves a partial access-grant prefix when that operation stops. The user is neither warned before commitment nor told afterward who retained access.

Author action: before Invite, disclose that accepted invitations survive stop/failure; once membership writes may have begun, label the action Stop remaining (or remove/disable it); track accepted acknowledgements before post-write fences; visibly report retained membership and draft/attachment recovery after stop, later refusal, and scope switch. Add production-bound regressions for those accepted-prefix transitions. Do not automatically remove accepted members, because concurrent legitimate access may exist.

Verification owner: author/maintainer for runtime disclosure, phase-correct labeling, accepted-acknowledgement accounting/reporting, recovery semantics, and regressions; reviewer for exact-head widget/mutation replay. Native iOS VoiceOver/focus/dynamic type and live-relay recovery remain reviewer/tooling confidence gaps, not additional author defects.

Current exact-head required CI is terminal/green where applicable; cancelled entries are superseded by successful current runs. The live PR remained open, non-draft, and unchanged immediately before publication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants