Skip to content

refactor: coordinator boundary review + AppState façade (#50 + #48)#58

Merged
variablefate merged 13 commits intomainfrom
refactor/coordinator-boundary-and-facade
Apr 17, 2026
Merged

refactor: coordinator boundary review + AppState façade (#50 + #48)#58
variablefate merged 13 commits intomainfrom
refactor/coordinator-boundary-and-facade

Conversation

@variablefate
Copy link
Copy Markdown
Owner

@variablefate variablefate commented Apr 17, 2026

Summary

Scope

#50 — Coordinator boundary review (ADR-0011):

  • Reviewed RideCoordinator, ChatCoordinator, AppState, SyncCoordinator, LocationCoordinator
  • Decision: all five stay in RoadFlareCore — the SDK already owns the protocol-level types they delegate to
  • Filed Consider extracting ChatCoordinator message-list logic into SDK ChatMessageStore #60 as a deferred follow-up: ChatCoordinator message-list deduplication/sorting is a natural SDK extraction if a driver-side app is ever built

#48 — AppState façade:

  • Added façade extensions to AppState covering: connectivity (isRelayConnected()), drivers (read + follow/unfollow/note actions), ride history (read + delete + backup), saved locations (read + pin/save/remove/clear), settings (badge counts)
  • Refactored 8 views: DriversTab, DriverDetailSheet, AddDriverSheet, RideRequestView, HistoryTab, SettingsTab, SavedLocationsView, ConnectivityIndicator
  • Views no longer call repository or service methods directly — all mutations go through AppState façade actions
  • import RidestrSDK remains in refactored views because they render SDK value types (FollowedDriver, RideHistoryEntry, CachedDriverLocation, etc.) — eliminating those imports is the job of Review view-layer coupling to SDK models and add app presentation models #49 (PR refactor: app presentation types for view-layer decoupling (#49) #59, presentation types)

Merge order

This PR merges before #59 (presentation types), which rebases onto it.

Test plan

  • xcodebuild clean build passes — confirmed on iPhone 17 simulator
  • Views compile against new façade APIs without direct repository/service calls
  • ADR-0011 is present in decisions/

Closes #50
Closes #48

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@variablefate
Copy link
Copy Markdown
Owner Author

PROCESS CORRECTION for agent a7cd1ce30141c2ef4 — please acknowledge and adjust your approach:

Your finish line is marking the draft PR ready for review, NOT merging it. Specifically:

  • Do NOT merge the PR
  • Do NOT delete the branch or worktree
  • After your self-review pass, mark the PR ready with: gh pr ready 58
  • Leave all cleanup (branch delete, worktree remove) for after the human reviews and merges manually

Everything else in your instructions stands. Continue your work.

@variablefate variablefate marked this pull request as ready for review April 17, 2026 14:44
variablefate and others added 3 commits April 17, 2026 08:29
Add publishDriversList(), requestDriverKeyRefresh(driverPubkey:), and
checkForStaleDriverKeys() to the AppState Drivers façade. Update
AddDriverSheet, DriversTab, and RideTab to use these instead of
reaching through appState.rideCoordinator directly. RideTab also
switches to appState.isRelayConnected() instead of appState.relayManager.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When hasFollowedDrivers is false the view previously rendered nothing,
leaving new users with a blank RideTab. Add a "No Drivers Added" empty
state with a CTA that navigates to the Drivers tab, matching the
pattern used in DriversTab's own empty state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SavedLocationsView.swift, ConnectivityIndicator.swift, and RideTab.swift
were all modified as part of Phase B but were omitted from the ADR's
Affected Files list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@variablefate
Copy link
Copy Markdown
Owner Author

Code review

Found 4 issues (3 fixed, 1 discarded after verification):


Surfaced findings and disposition:

Fixed — 9f23c7c: Incomplete façade — rideCoordinator bypasses left in views

Three call sites still reached through appState.rideCoordinator directly, and RideTab still used appState.relayManager directly, contrary to the PR's stated goal. Added three façade methods (publishDriversList(), requestDriverKeyRefresh(driverPubkey:), checkForStaleDriverKeys()) and updated all four call sites.

// If still no key, send a stale ack to request one from the driver.
Task {
await appState.rideCoordinator?.publishFollowedDriversList()
await appState.sendFollowNotification(driverPubkey: hexPubkey)
// If no key locally, try restoring from our Kind 30011 backup on the relay
if !appState.hasKeyForDriver(pubkey: hexPubkey) {
await appState.restoreKeyFromBackup(driverPubkey: hexPubkey)
}
// If still no key after restore attempt, request from driver
if !appState.hasKeyForDriver(pubkey: hexPubkey) {
await appState.rideCoordinator?.requestKeyRefresh(driverPubkey: hexPubkey)
}
}

private func refreshDrivers() async {
appState.refreshDriverLocations()
await appState.rideCoordinator?.checkForStaleKeys()
}

private func monitorConnection() async {
while !Task.isCancelled {
if let rm = appState.relayManager { isOffline = !(await rm.isConnected) }
try? await Task.sleep(for: .seconds(10))


Fixed — 9a8951f: RideRequestView blank screen for zero-driver users

if appState.driversRepository != nil (service-ready check, always true once auth is done) was replaced with if appState.hasFollowedDrivers (empty when no drivers are followed). The new guard left the RideTab showing nothing for a freshly-signed-in user with no drivers. Added a "No Drivers Added" empty state with a CTA to the Drivers tab, matching the existing pattern in DriversTab.

ScrollView {
VStack(spacing: 16) {
if appState.hasFollowedDrivers {
if onlineDrivers.isEmpty {
VStack(spacing: 24) {


Fixed — 33c3bde: ADR-0011 Affected Files list incomplete

SavedLocationsView.swift, ConnectivityIndicator.swift, and RideTab.swift were all modified as Phase B view migrations but were absent from the ADR's Affected Files section.

https://github.com/variablefate/roadflare-ios/blob/449c466fdcac179459e29cf4e69f2987099ba2e2/decisions/0011-coordinator-boundary.md#L168-L181


Discarded — withAnimation wrapping backupRideHistory() in HistoryTab

Flagged because a prior commit (5e5d133) explicitly separated removeRide (inside withAnimation) from backupRideHistory() (outside). After verification: backupRideHistory() is synchronous at the call site and spawns a detached Task internally, so it escapes the animation transaction regardless. No actual behavior difference; discarded.


🤖 Generated with Claude Code

If this code review was useful, please react with 👍. Otherwise, react with 👎.

… gaps

- ActiveRideView: replace driversRepository?.cachedDriverName with
  appState.driverDisplayName(pubkey:) — last remaining driversRepository
  bypass in a view
- AppState: add allPaymentMethodNames to Settings facade; update
  RideRequestView to use it instead of appState.settings directly
- AppState: document addDriver() publish asymmetry — unlike removeDriver
  and updateDriverNote, callers must publish separately
- AppState: clarify Settings MARK comment to reflect write access still
  goes through appState.settings directly
- ADR-0011: add ActiveRideView.swift to Affected Files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@variablefate
Copy link
Copy Markdown
Owner Author

Code review — second pass

4 findings this pass; all fixed in bec34a0. 3 others discarded after verification.


Fixed — ActiveRideView was the last remaining driversRepository bypass

ActiveRideView was not in the PR diff, so the first pass treated it as pre-existing and left it. It called appState.driversRepository?.cachedDriverName(pubkey:) directly; appState.driverDisplayName(pubkey:) already existed and is the correct façade call.

?? appState.settings.roadflarePaymentMethods,
driverName: coordinator?.session.driverPubkey.flatMap {
appState.driversRepository?.cachedDriverName(pubkey: $0)
},
pickupAddress: coordinator?.pickupLocation?.address,


Fixed — appState.settings.allPaymentMethodNames read still bypassed façade in RideRequestView

The Settings façade block added read-only helpers for badge counts and profileName, but omitted allPaymentMethodNames, which RideRequestView uses for the payment display line in the fare section. Added public var allPaymentMethodNames: [String] to the Settings façade block and updated the call site.

Image(systemName: "creditcard")
.foregroundColor(Color.rfPrimary)
Text(appState.settings.allPaymentMethodNames.joined(separator: ", "))
.font(RFFont.caption(12))
.foregroundColor(Color.rfOnSurfaceVariant)


Fixed — addDriver() publish asymmetry was undocumented

removeDriver() and updateDriverNote() both auto-publish the drivers list internally. addDriver() does not — callers must call publishDriversList() separately. No doc comment warned of this, making the asymmetry a silent trap for any future second call site. Added a note to the doc comment.

/// Add a driver and cache their profile and name if available.
public func addDriver(_ driver: FollowedDriver, profile: UserProfileContent? = nil, name: String? = nil) {
driversRepository?.addDriver(driver)
if let profile {
driversRepository?.cacheDriverProfile(pubkey: driver.pubkey, profile: profile)
} else if let name, !name.isEmpty {
driversRepository?.cacheDriverName(pubkey: driver.pubkey, name: name)
}
}


Fixed — ADR-0011 Affected Files and Settings MARK still stale

ActiveRideView.swift (now fixed) was absent from the ADR Affected Files list. The // MARK: - Façade: Settings (read-only convenience) comment implied settings writes also go through the façade, but SettingsTab still calls appState.settings.setProfileName() directly; the comment now reflects that writes are intentionally deferred.

https://github.com/variablefate/roadflare-ios/blob/33c3bdeb7f562786ee9819f4bd02a7a6b441a993/decisions/0011-coordinator-boundary.md#L178-L184


Discarded findings:

  • appState.rideCoordinator reference in ActiveRideView/RideRequestViewRideCoordinator is app-layer (RoadFlareCore), not an SDK service; its presence in views is deliberate and deferred to PR refactor: app presentation types for view-layer decoupling (#49) #59.
  • public private(set) on driversRepository/relayManager staying readable — narrowing access is an architectural change beyond this PR's scope.
  • Task interleave on rapid removeDriver + updateDriverNote — both mutations are synchronous on @MainActor before either Task runs; both publish current repo state at call time; low real-world probability and a pre-existing structural pattern.
  • DriverDetailSheet note lost on interactive-dismiss swipe — pre-existing; the note is also irrelevant if the user proceeds to "Remove Driver".

What this pass caught that the first missed:

The first pass focused on files in the diff and noted ActiveRideView as pre-existing but left it. This pass treated the façade's completeness as the unit of review rather than the diff boundary, which surfaced both the ActiveRideView bypass and the allPaymentMethodNames gap in a file the PR did touch. The addDriver() asymmetry was also a documentation blind spot the first pass did not flag.

🤖 Generated with Claude Code

If this code review was useful, please react with 👍. Otherwise, react with 👎.

variablefate and others added 2 commits April 17, 2026 08:56
DriverDetailSheet.canRequestRide, RideRequestView.onlineDrivers, and
DriversTab.isOnline all accepted hasKey + online status without checking
isDriverKeyStale. A driver broadcasting "online" with a stale RoadFlare
key would surface as bookable; the rider's encrypted offer would not be
decryptable by the driver and the request would silently fail.

The SDK already blocks ping via canPingDriver returning .ineligible when
staleKeyPubkeys contains the driver, but the ride-offer path had no
equivalent view-layer guard. Now all three predicates also require
!isDriverKeyStale(pubkey:), matching the "Key Outdated" visual state
DriverCard already shows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SettingsTab: use appState.profileName and appState.paymentMethodCount
  instead of reading appState.settings.* directly (5 sites). These
  façade properties were added in this PR but the file's own reads
  weren't migrated.
- DriversTab: remove stale sentence in pingDriver comment that
  referenced appState.driversRepository — no such access exists in the
  function after the façade migration.
- ADR-0011: drop inaccurate "~650 LOC" figure for AppState (the file is
  larger after the Phase B façade additions that this same PR adds).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@variablefate
Copy link
Copy Markdown
Owner Author

Code review — third pass

4 findings; 1 is a real user-facing bug the first two passes missed. Fixed in 0b2e519 and 4cdf7b7.


Fixed (bug) — Stale-keyed drivers were bookable from the ride-request flow

DriverDetailSheet.canRequestRide, RideRequestView.onlineDrivers, and DriversTab.isOnline all gated on hasKey && online without excluding drivers with a stale encryption key. A driver broadcasting "online" with a stale key would surface as bookable; the rider's encrypted offer would not be decryptable on the driver side and the request would silently fail. The SDK blocks this at the ping path (canPingDriver returns .ineligible when staleKeyPubkeys.contains(driver.pubkey)) but had no equivalent guard for ride offers. All three predicates now also require !appState.isDriverKeyStale(pubkey:).

PR #59's review surfaced the same defect in the new presentation-layer types; the root cause lives here.

private var canRequestRide: Bool {
currentDriver.hasKey && currentLocation?.status == "online"
}

private var onlineDrivers: [FollowedDriver] {
appState.followedDrivers.filter { driver in
driver.hasKey && appState.driverLocation(pubkey: driver.pubkey)?.status == "online"
}
}

private var isOnline: Bool {
driver.hasKey && location?.status == "online"
}


Fixed — SettingsTab wasn't using façade properties it added in the same PR

5 reads still went through appState.settings.profileName / appState.settings.roadflarePaymentMethods.count despite appState.profileName and appState.paymentMethodCount having been added in this PR for exactly that. Migrated the 5 read sites.

VStack(alignment: .leading, spacing: 2) {
Text(appState.settings.profileName.isEmpty ? "Set your name" : appState.settings.profileName)
.font(RFFont.title(16))
.foregroundColor(appState.settings.profileName.isEmpty ? Color.rfOnSurfaceVariant : Color.rfOnSurface)
Text("Edit Profile")

Spacer()
Text("\(appState.settings.roadflarePaymentMethods.count)")
.font(RFFont.caption(12))


Fixed — Stale comment in DriversTab.pingDriver

Comment at line 131 claimed appState.driversRepository is "accessible without a hop." After the façade migration, no such access occurs in the function — the Task body now uses appState.sendDriverPing / appState.driverDisplayName. Dropped the stale sentence.

private func pingDriver(_ driver: FollowedDriver) {
// Capture pubkey as a plain String (Sendable) before the task boundary so
// `driver` (a struct that may not be Sendable) doesn't need to cross the
// isolation boundary. The view is @MainActor so the Task body inherits that
// isolation; `appState.driversRepository` is accessible without a hop.
let driverPubkey = driver.pubkey


Fixed — ADR-0011 self-contradictory LOC claim

ADR describes AppState as "~650 LOC" but the file is 870 lines after this PR's façade additions. Replaced the figure with prose that stays accurate as the file evolves.

The five coordinators are:
- `AppState` — owns SDK services, auth lifecycle, orchestration (~650 LOC)
- `SyncCoordinator` — Nostr sync orchestration, startup resolution, dirty-tracking


Discarded after verification:

  • Task interleave on removeDriver + updateDriverNote — both mutations are synchronous on @MainActor and run to completion before either Task body begins. By the time the two Tasks execute, they see identical post-mutation repo state. Not a race.
  • sendRideOffer / coordinator mutation bypassesRideCoordinator is app-layer; the ride-flow views hold coordinator handles to compose multi-field state (pickup, destination, fare estimate). Proper fix requires a larger façade surface for the ride flow, deferred to PR refactor: app presentation types for view-layer decoupling (#49) #59.
  • keyManager.exportNsec() bypass in backup flow — different service family (not one of the three the ADR scopes).
  • Write bypasses in untouched files (SyncScreen, PaymentMethodsScreen, PaymentSetupView) — Settings writes are explicitly deferred per the Settings façade MARK comment.

What this pass caught that earlier passes missed:

The stale-key bug is the substantive one. The first two passes audited the façade by walking each view's diff for SDK-type accesses and never examined the views' own business-logic predicates. canRequestRide and onlineDrivers were considered "unchanged logic" and glossed over — but the façade had added the isDriverKeyStale(pubkey:) method and no view was using it in the predicates that gate ride-offer sending. PR #59's review found the same symptom one layer downstream; this pass caught it at the source. The remaining three findings are polish: adopt-your-own-façade and keep docs accurate to the current state.

🤖 Generated with Claude Code

If this code review was useful, please react with 👍. Otherwise, react with 👎.

The stale-key fix from the third review pass inlined the ride-eligibility
check into three separate view predicates (DriverDetailSheet.canRequestRide,
RideRequestView.onlineDrivers, DriversTab.isOnline). Future regressions in
any one predicate would not be caught by the test suite, and the three
copies could drift apart.

Extract the predicate to FollowedDriversRepository.canRequestRide(_:),
parallel to the existing canPingDriver(_:). The SDK method takes a lock,
looks up the driver in the repo (not the caller's snapshot), and checks:
hasKey, !stale, status == "online".

Expose appState.canRequestRide(_:) as the façade forwarder. All three
view predicates now call the single source of truth.

Adds 8 CanRequestRideTests covering: unknown driver, missing key, stale
key, offline, on-ride, online with key, and the two stale-caller-snapshot
cases that CanPingDriverTests covers symmetrically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@variablefate
Copy link
Copy Markdown
Owner Author

Code review — fourth pass (convergence check on e68ec8c)

No new findings worth acting on in this PR. The canRequestRide refactor + tests converges cleanly:

  • FollowedDriversRepository.canRequestRide(_:) placement follows CLAUDE.md SDK-vs-app split (protocol-state predicate on the SDK repo, parallel to canPingDriver).
  • Lock usage matches the established @unchecked Sendable + NSLock pattern; single atomic lock.withLock into a *Locked helper, symmetric with driverPingPreflightLocked.
  • 8 new tests mirror CanPingDriverTests structure (unknown driver, missing key, stale key, offline, on_ride, online-with-key, plus two stale-caller-snapshot cases). Build green, 109 passed / 0 failed.
  • The three view call sites (DriverDetailSheet.canRequestRide, RideRequestView.onlineDrivers, DriversTab.isOnline) collapse to the single façade call, killing the three-way drift risk the prior inline fix had.

Two forward-looking observations (not flagged for fix in this PR — flagging here for visibility):

1. Send-time preflight gap on publishRideOffer (follow-up issue candidate)

The ping path has UI-side canPingDriver + SDK-side driverPingPreflight + an AppState send-time re-check in sendDriverPing. The ride path now has the UI-side canRequestRide but RiderRideDomainService.publishRideOffer has no analogous preflight — only a state-machine stage check. A stale-key event arriving in the window between button tap and relay publish would still let the offer go out and silently fail on decrypt.

The UI fix closes the user-visible case; this would close the race window. Scope is adjacent to this PR, not in it — suitable as a follow-up issue. Pattern precedent is commit bd2b3f1, which introduced driverPingPreflight for the equivalent ping-side gap.

2. PR #59 presentation-type factories inline the predicate

PR #59's DriverDetailViewState and RideRequestDriverOption factories inline driver.hasKey && !isKeyStale && location?.status == "online" rather than calling the new SDK method. Once #58 merges, there will be two implementations of the same logic, and PR #59's factories lack the "driver exists in repo" guard that CanRequestRideTests.staleCallerSnapshot_hasKeyButRepoMissingKey_returnsFalse pins down. Belongs in PR #59's review — posting here so the author sees it when #58 lands.


Branch is ready to merge. This pass found no issues in #58's own scope.

🤖 Generated with Claude Code

If this code review was useful, please react with 👍. Otherwise, react with 👎.

@variablefate variablefate merged commit ca7e8eb into main Apr 17, 2026
@variablefate variablefate deleted the refactor/coordinator-boundary-and-facade branch April 17, 2026 17:45
variablefate added a commit that referenced this pull request May 2, 2026
Pass-1 review fixes:

- restoreRideState() runs in RideCoordinator.init, before
  restoreLiveSubscriptions() starts the Kind 30173 stream — the cache is
  always empty at restore time, so cold-started mid-ride sessions
  permanently saw a nil snapshot and fell back to Kind 0. Add a narrow
  onDriverVehicleUpdate hook on LocationCoordinator that lets
  RideCoordinator opportunistically adopt the first observed vehicle for
  the active driver, then lock for the rest of the ride. Two new tests
  pin first-arrival adoption + lock-after-first and ignored events for
  unrelated drivers / non-active stages.
- Update activeRideVehicle docstring to accurately describe both capture
  paths and the cold-start trade-off.
- Annotate prepareForIdentityReplacement step 4 so the reader sees that
  clearAll() now also zeroes the new driverVehicles cache.
- Add ADR-0015 documenting the Kind 30173 subscription pattern, the
  overwrite-only cache, the snapshot semantics + first-arrival recovery,
  and why this stays in-memory rather than going through PersistedRideState.

Adjacent debt (PR #58/#61 inline canRequestRide drift) tracked at
issue #94 to keep PR scope focused on issue #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
variablefate added a commit that referenced this pull request May 5, 2026
* feat(roadflare): subscribe to Kind 30173 for live driver vehicles (#91)

Riders couldn't see driver vehicle info because iOS only read Kind 0 (which
Drivestr doesn't reliably re-publish on vehicle swap). Subscribe to Kind 30173
DriverAvailabilityEvent for followed drivers, cache with overwrite-only
semantics, and snapshot at ride acceptance so mid-trip vehicle swaps don't
mutate the active ride view.

- SDK: VehicleInfo + DriverAvailabilityEventData models, parseDriverAvailability
  parser, NostrFilter.driverAvailability(driverPubkeys:), and a no-merge
  driverVehicles cache on FollowedDriversRepository.
- App: LocationCoordinator gains a Kind 30173 subscription mirroring the
  Kind 30014 pattern; AppState exposes restartDriverAvailabilitySubscription()
  alongside restartKeyShareSubscription, called at every followed-drivers
  mutation site (add/remove/follow-notify/refresh).
- RideCoordinator captures activeRideVehicle on .waitingForAcceptance →
  .driverAccepted and clears at terminal; ActiveRideView prefers the snapshot,
  falls back to Kind 0.
- Presentation: DriverDetailViewState/DriverListItem prefer the live cache
  with the Kind 0 profile as a fallback. AppState+Presentation threads
  driverVehicles through.
- Tests: parser shape (full / partial / offline / wrong-kind / malformed),
  repo overwrite + multi-vehicle + multi-driver + cleanup, snapshot capture
  + lock at acceptance + nil-when-cache-empty, presentation precedence.

Closes #91

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(rider): adopt first Kind 30173 event for restored mid-ride snapshot

Pass-1 review fixes:

- restoreRideState() runs in RideCoordinator.init, before
  restoreLiveSubscriptions() starts the Kind 30173 stream — the cache is
  always empty at restore time, so cold-started mid-ride sessions
  permanently saw a nil snapshot and fell back to Kind 0. Add a narrow
  onDriverVehicleUpdate hook on LocationCoordinator that lets
  RideCoordinator opportunistically adopt the first observed vehicle for
  the active driver, then lock for the rest of the ride. Two new tests
  pin first-arrival adoption + lock-after-first and ignored events for
  unrelated drivers / non-active stages.
- Update activeRideVehicle docstring to accurately describe both capture
  paths and the cold-start trade-off.
- Annotate prepareForIdentityReplacement step 4 so the reader sees that
  clearAll() now also zeroes the new driverVehicles cache.
- Add ADR-0015 documenting the Kind 30173 subscription pattern, the
  overwrite-only cache, the snapshot semantics + first-arrival recovery,
  and why this stays in-memory rather than going through PersistedRideState.

Adjacent debt (PR #58/#61 inline canRequestRide drift) tracked at
issue #94 to keep PR scope focused on issue #91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(rider): pass-2 review polish — ADR clarifications + callback rationale

- ADR-0015: enumerate all three populate paths for activeRideVehicle
  (acceptance transition, restoreRideState direct read, first-arrival
  adoption). Match the precision precedent set by ADR-0012.
- ADR-0015: defend the closure-callback choice against the a88d1b7 (PR #38)
  precedent that removed onFavoritesChanged in favor of @observable
  reactive observation — first-arrival adoption needs imperative locking
  semantics that withObservationTracking does not naturally express.
- ADR-0015: hedge the future-consumers claim and call out that
  onDriverVehicleUpdate is package-internal (no public modifier).
- ADR-0015: note that this ADR supersedes ADR-0011's "two subscriptions"
  prose for LocationCoordinator (now three).
- LocationCoordinator: doc onDriverVehicleUpdate's intentional non-Sendable
  (covered by @mainactor isolation, asymmetric with SDK callbacks for a
  reason) and its intentional fire-even-when-cache-write-skipped behavior
  (the snapshot represents the agreed vehicle, meaningful even after an
  unfollow during an active ride).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(rider): sharpen vehicle-snapshot lock + correct onDriverVehicleUpdate doc

Pass-3 review entropy fixes (sub-threshold but real):

- vehicleSnapshotDoesNotUpdateAfterAcceptance previously only mutated the
  repo cache after capture, which proved snapshot-as-captured-value
  immutability but never exercised the actual production path
  (LocationCoordinator -> onDriverVehicleUpdate -> adoptVehicleIfNeeded).
  A regression that removed `guard activeRideVehicle == nil` from
  adoptVehicleIfNeeded would not have been caught here. Now the test
  mirrors the production sequence (cache write THEN adoptVehicleIfNeeded
  call), so it actually pins the lock guard for the bug it's named after.

- onDriverVehicleUpdate doc previously claimed "the closure cannot escape"
  to justify the missing @sendable. That's factually wrong — stored
  optional closures are by definition @escaping. The real safety property
  is actor isolation, which itself depends on the call site being an
  unstructured `Task { }` (inheriting @mainactor) rather than
  Task.detached. Rewrite the doc to enumerate both load-bearing
  conditions and call out exactly what a Task.detached refactor would
  break, so a future contributor doesn't silently introduce a data race
  with RideCoordinator.activeRideVehicle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(rider): pin AppState presentation wiring of driverVehicles cache

Adds two integration-style tests that exercise the actual AppState API
(`driverListItems()` and `driverDetailViewState(pubkey:)`) end-to-end
with both a Kind 0 profile and a live Kind 30173 cache entry, asserting
the live cache wins.

Without these, the `vehicle:` parameter on the factory has a nil default
and a future refactor that drops `vehicle:` from the AppState+Presentation
call sites would silently fall back to Kind 0 — the exact bug #91 fixed —
without any existing test catching it. The factory unit tests pass
`vehicle:` explicitly so they don't exercise the AppState wiring.

Pre-existing empty-pubkeys subscription race in all three managed
subscriptions filed as #96 (out of scope for #91; needs a uniform fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: variablefate <variablefate@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

Review remaining RoadFlareCore vs RidestrSDK coordinator boundary Review AppState facade boundary over SDK repositories/services

1 participant