Skip to content

refactor(leaderboard): the #540 review: MemberDevices, DeviceRequest, typed labels and keys, ISO times - #1

Merged
LunarECL merged 5 commits into
LunarECL:feat/devices-slice13from
tddworks:refactor/pr540-devices
Oct 11, 2026
Merged

LunarECL merged 5 commits into
LunarECL:feat/devices-slice13from
tddworks:refactor/pr540-devices

Conversation

@hanrw

@hanrw hanrw commented Oct 11, 2026 •

Copy link
Copy Markdown

Why

The review on #540 asked for the label count, the time form and three domain-design points. This is one way to do them, on top of your 4e14e21c, in five commits, so you can take it, change it, or close it. Nothing lands on your branch unless you merge this.

What changed

  • DeviceLabel counts 1–40 in UTF-16 units, as the server's readLabel does. A test with a family emoji (11 units) pins it.

  • ServerTime reads only ISO 8601, with or without milliseconds (the server's toISOString()). SQLite and Unix-time forms are refused. Your Export round-trip test now uses ISO inputs.

  • MemberDevices (new) owns which device joined, isInFirstWeek(_:at:), added(since:), removing(_:by:at:) and adding(_:), each tested directly in MemberDevicesTests. Device.removed(by:at:) marks a removal. The membership asks it instead of comparing fields.

  • Joining: a private JoiningState carries the key, the interval and the approved summary in each step, replacing joiningKey, joiningInterval and approvedSummary. The public Joining is unchanged, so no key reaches a view.

  • Types: Device, DeviceRef and PendingDevice labels are DeviceLabel (a label the server would never send fails the answer). removedBy and LeaderboardError.removed(by:) keep the DeviceRef.

  • design.md:

    • §2: MemberDevices and joining in the tree, and a MemberDevices section.
    • §2a: the flow chart and the joining states.
    • §4: the owners.
    • §5: times and labels.
    • §7: MachineIdentity in the diagram.
  • DeviceKey (second commit) names a device by the key it signs with: Device, DeviceRef, MemberDevices and shownDevices use it. It is text only on the wire (the LeaderboardAPI calls, the JSON) and in settings.json, whose format is unchanged.

  • The wait between a new device's asks is injected as sleep, like now. A test pins that it waits the server's interval between asks.

  • DeviceRequest (third commit, the review's point 4). A design change, so the docs changed first, and I'd like your view on it. A Mac is either asking to be added to a member or a member's device, never both, so the request is its own aggregate, as device_requests is on the server, not a state of the membership:

    • DeviceRequest owns the code, the unsaved key, the wait at the server's interval, expiry and decline(). Its stage is waiting, approved(member), joined, expired or declined.
    • membership.requestToJoin(label:) answers the request, and is refused while this Mac is a member (alreadyJoined). membership.join(through:sharing:) replaces confirmJoining. joining and JoiningState leave the membership.
    • design.md: §2's tree and a DeviceRequest section, §2a's stages, §3's tells, §4's owner and §7's pieces. Also CANONICAL_MODEL's Leaderboard row.
    • The fourth commit keeps each object to its own state. join(through:) checks its own rules, then tells the request handOver(), which moves the request to joined itself. The request owns its wait (default Task.sleep), so the membership no longer carries one. DeviceRequestTests builds the request directly.
  • Abilities, not rules (fifth commit). membership.removals(of:) answers the dialog's choices (Removal: keepingItsDays, deletingItsDays; none for a removed device), and remove(_:_:) takes one, refusing what it didn't offer (notOffered). It replaces offersDeletingDays(whenRemoving:) plus the deletingDays flag, so a view picks from what the domain offers instead of asking and passing a Bool. MemberDevices' rules and Device.removed(by:at:) are internal: a view lists devices and asks the membership.

 membership.requestToJoin ─▶ waiting ─▶ approved(member) ─▶ membership.join(through:) ─▶ joined
                               │ ▲ 202       │
                               └─┘           └─ request.decline() ─▶ declined
                               └─ 401 ─▶ expired

How it was verified

  • swift test --filter LeaderboardTests: 193 tests passed.
  • xcodebuild test -scheme ClaudeBar on macOS: TEST SUCCEEDED.
  • Each rule was broken on purpose (characters instead of UTF-16 units, Unix time read again, the joined device held to a first week, removed devices counted as added, a member asking to be added, an expiry not recorded, a hand-over that doesn't end the request, a removal that wasn't offered), and each made a test fail.
  • check-docs --strict passes.
  • Not run on Windows.

🤖 Generated with Claude Code

…SO times (#540)

From the review on #540:
- DeviceLabel counts 1–40 in UTF-16 units, as the server's readLabel does,
  so a label with emoji isn't refused as 400 badLabel.
- ServerTime reads only ISO 8601, the server's toISOString(); SQLite and
  Unix-time forms are refused rather than guessed.
- MemberDevices owns which device joined, the first week and what was
  added since a device; Device.removed(by:at:) marks a removal.
- Joining's steps carry their key, interval and summary, so the parallel
  optionals are gone; the public Joining is unchanged.
- Device, DeviceRef and PendingDevice labels are DeviceLabel; removedBy
  and LeaderboardError.removed(by:) keep the DeviceRef.
- design.md: §2 tree and a MemberDevices section, §2a's flow and joining
  states, §4 owners, §5 times and labels, §7 MachineIdentity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ae800723-df4b-49c0-bcbe-fa3f39e0dfaa

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

hanrw and others added 2 commits October 11, 2026 11:15
…cted wait (#540)

The rest of the #540 review's point 3 and its small things:
- DeviceKey names a device by the key it signs with: Device, DeviceRef,
  MemberDevices and shownDevices use it. Text only on the wire (the
  LeaderboardAPI calls, the JSON) and in settings.json, which is unchanged.
- LeaderboardMembership takes the wait between a new device's asks as
  `sleep`, like `now`; a test pins that it waits the server's interval.
- design.md: shownDevices as Set<DeviceKey>; DeviceKey in §7's pieces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Mac is either asking to be added to a member or a member's device,
never both, so the request is its own aggregate, as device_requests is
on the server, not a state of the membership (the #540 review's point 4).

- DeviceRequest owns the code, the unsaved key, the wait at the server's
  interval, expiry and decline(); its stage is waiting, approved(member),
  joined, expired or declined.
- LeaderboardMembership.requestToJoin(label:) answers the request, and is
  refused while this Mac is a member (alreadyJoined);
  join(through:sharing:) replaces confirmJoining. Joining and its
  JoiningState leave the membership.
- design.md: the request in §2's tree and its own section, §2a's stages,
  §3's tells, §4's owner, §7's pieces; CANONICAL_MODEL's Leaderboard row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hanrw hanrw changed the title refactor(leaderboard): device rules on MemberDevices, typed labels, ISO times (#540 review) refactor(leaderboard): the #540 review: MemberDevices, DeviceRequest, typed labels and keys, ISO times Oct 11, 2026
hanrw and others added 2 commits October 11, 2026 11:38
#540)

- DeviceRequest.handOver() answers the member, the key and the member's
  /me answer and moves itself to joined, once; refused before approval.
  LeaderboardMembership.join(through:sharing:) checks its own rules, then
  tells it, instead of reading `approval` and setting `markJoined()`.
- DeviceRequest's wait defaults to Task.sleep; the membership no longer
  carries a `sleep` it never used.
- DeviceRequestTests builds the request directly: the wait, the member,
  expiry, decline and the hand-over.
- design.md: DeviceRequest's laws and tells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… internal device rules (#540)

- LeaderboardMembership.removals(of:) answers the dialog's choices
  (Removal: keepingItsDays, deletingItsDays; none for a removed device),
  and remove(_:_:) takes one, refusing what it didn't offer (notOffered).
  It replaces offersDeletingDays(whenRemoving:) and the deletingDays flag.
- MemberDevices' rules (joined, isInFirstWeek, added(since:), removing,
  adding, device(withKey:)) and Device.removed(by:at:) are internal: a view
  lists the devices and asks the membership, never the rules.
- design.md: §2's tells and answers, §3's tells, §4's owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LunarECL
LunarECL merged commit 9d7df8c into LunarECL:feat/devices-slice13 Oct 11, 2026
1 check passed
@hanrw
hanrw deleted the refactor/pr540-devices branch October 11, 2026 04:26
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