Skip to content

Store each cloud lineup as one row - #236

Open
SunkenInTime wants to merge 4 commits into
t3code/unify-cloud-mainfrom
lineups-one-row
Open

SunkenInTime wants to merge 4 commits into
t3code/unify-cloud-mainfrom
lineups-one-row

Conversation

@SunkenInTime

@SunkenInTime SunkenInTime commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #234. Merge this into #234's branch before #234 merges, so they deploy together.

A lineup used to sync as three kinds of cloud row (its standing spot, its landing spot, and the link with its details), each with references to the others. The server's conflict unit was one part, while what the user saved needed all three. That mismatch produced the run of lineup edge cases: a link arriving before its landing, "end in use" and "end missing" refusals, delete ordering, held-back and unsyncable rows, rows hydration wouldn't draw, endpoints brought back from the dead.

Now each lineup is one self-contained row: its name, notes, video and images, plus full copies of its standing spot and landing spot, each with its own id. Sharing is derived. Two lineups from the same spot carry the same spot id, and hydration draws that spot once. When copies disagree (only after concurrent edits), every client picks the copy from the highest-revision row, then the greatest publicId. Editing a shared spot patches every lineup that uses it, each against its own revision. A teammate's lineup added to a spot you delete keeps its own copy and survives. No row can depend on another, so a lineup conflict is an ordinary element conflict.

The local model, the UI, undo, Hive and .ica don't change.

Why now: production has zero lineup rows (read-only check on 2026-10-01), so nothing needs migrating. Once desktop users get cloud sync, a change like this would need a production migration.

Protocol 5: a protocol-4 tab can't read the new row, so the server now asks those tabs to reload. Protocol 4 never shipped publicly; the live web build is on 3, which #234 already refuses.

Net: about 600 lines out of the client and 330 out of the server; the lineup sync tests are rewritten for the new model.

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

Not safe to merge until the shared-landing move is preserved. The delayed independent edit is non-blocking.

Findings

  1. P1 Unrelated edit hides moved spot ▶
  2. P2 Holds spread across lineups ▶

Summary

Storing each cloud lineup as one row introduces two collaboration problems. A concurrent notes edit can hide a saved move of a shared landing; this must be fixed before merging. Holding one spot can also delay an independent teammate edit elsewhere in a connected lineup chain.

Reviews (1) · Last reviewed commit: "Sync each lineup as one row and draw sha..."

@coderabbitai

coderabbitai Bot commented Oct 1, 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: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 16c461d4-4388-4c5e-874a-ab05fd14dd44

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
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

Comment thread lib/collab/cloud_lineup_rows.dart Outdated
Comment on lines +77 to +81
bool outranks(CloudLineupRow row, CloudLineupRow? drawn) =>
drawn == null ||
row.revision > drawn.revision ||
(row.revision == drawn.revision &&
row.publicId.compareTo(drawn.publicId) > 0);

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.

P1 Unrelated edit hides moved spot

When two lineups share a landing, one teammate can move it while another edits only the notes in the other lineup. Both rows can reach the same revision, but this tie-break selects the notes row’s stale landing position when its public ID is greater. The saved move then disappears from the canvas. Preserve the move before merging.

Artifacts

Authored Dart reproduction source

  • The authored test generates both row states and invokes cloud row hydration, showing the precise comparison performed.

Hydration before independent edits

  • The executed baseline test shows both rows and the visible landing at (30, 40).

Hydration after independent edits

  • The executed edited-state test shows row `a` moved to (99, 99) but the visible landing remains (30, 40), confirming the hidden move.

Cloud row implementation comparison

  • The captured command compares the prior separate-landing-row representation with the current whole-row revision and publicId tie-break at lines 77–81.

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +57 to +62
if (heldIds.contains(link.id) ||
heldIds.contains(link.originId) ||
heldIds.contains(link.landingId)) {
held.add(link.id);
heldIds.addAll([link.originId, link.landingId]);
grew = true;

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.

P2 Holds spread across lineups

Holding o1 in a chain o1 → l1, o2 → l1, o2 → l2 also holds the last lineup because each newly held link adds both endpoints to the hold. A teammate’s edit to that last lineup stays off the canvas until o1 is released, even though the last lineup does not touch o1. This is a non-blocking delay to an otherwise independent update.

Artifacts

Authored three-link merge test

  • The authored Flutter test calls the real merge function and checks the visible k3 name and held IDs for both conditions, establishing the comparison.

Authored before-and-after test command

  • The authored command temporarily installs a non-recursive control, runs both tests, and restores the PR source, making the comparison reproducible.

Test output with non-recursive hold propagation

  • The executed control test reports k3 visible with the teammate’s edited name and not held, showing the comparison behavior.

Test output with PR hold propagation

  • The executed PR-code test reports k3 visible with its original name and held, confirming the delayed edit.

View artifacts

T-Rex Ran code and verified through T-Rex

SunkenInTime and others added 4 commits October 1, 2026 19:45
A lineup synced as an origin row, a landing row and a link row, and the
server checked across them: a link needed live ends (LINEUP_LINK_END_MISSING),
an end could not go while a link used it (LINEUP_END_IN_USE), and clients
opted in to both checks with applyBatch flags. The conflict unit was one
part while the user's lineup needs all three.

A lineup row now holds the whole lineup, kind "lineup", keyed by its id:
name, video, notes, images, and full copies of its origin and landing.
Lineups sharing a spot each carry their own copy; the client draws them as
one spot again. Every row stands on its own, so the cross-row checks, their
flags and error codes, and the endpoint index go. Page ownership
(LINEUP_PAGE_MISMATCH) and strategy-scoped keys stay.

Duplicating a strategy copies each row under fresh ids from one shared map,
so copied lineups that shared a spot still share it. The agent summary
counts each origin once per page, read from live lineups through a new
by_strategyId_and_deleted index. Image references read data.images of any
lineup row.

Protocol 5: a client on 4 cannot read the new rows and writes rows the
server no longer takes, so it is refused until it reloads. Production holds
no lineup rows (checked 2026-10-01) and dev's lineups table is empty, so
the narrowed schema needs no backfill.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The client side of one row per lineup. Live sync turns the page's graph
into one desired row per link, each carrying its origin and landing as
they are now, so moving a shared spot patches every lineup on it, each as
its own op against its own revision, and deleting a spot deletes the rows
of the lineups on it. Hydration rebuilds the graph from the rows: links
from rows, origins and landings deduplicated by id. When copies of one
spot disagree (only after concurrent edits) every client draws the copy
from the row with the highest revision, ties to the greatest publicId, and
live sync compares rows in that drawn form so the choice never authors a
write. The user's own pending rows outrank the server until they land.

Removed with the split rows: kind-prefixed keys and the drawn-row join,
held-back and unsyncable lineups, endpoint restore, undrawn remote rows,
endpoint-first and link-first outbox scheduling, the missing-end Keep mine
path and its messages, the sync status's unsyncable source, the graph id
rewrite on upload, and the Paranoia correction for landings (lineup rows
are written at the in-game size and are never corrected).

A remote merge no longer holds the whole graph while the user holds any
part of it: only lineups touching what is held, closed over the spots they
share, keep their screen copy; the rest of the page's lineups update.

An outbox record from an older build can still hold an origin, landing or
link op. It is never sent: it waits in attention with its own reason until
the user discards it, and Keep mine leaves it there.

The local model, UI, Hive, .ica and undo are unchanged.

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

Copies of a shared spot can drift apart (a move that reached only some of
its lineups), and the row revision is no measure of which copy is newest.
Each end of a lineup row now carries its spot's version, an integer from
1; clients draw the copy with the highest version, then the greatest
lineup id. Duplicate copies versions as they are, so a copy draws every
spot where the original does.

A patch or reorder of a deleted element or lineup was acked onto the
tombstone and the edit vanished on the next load. It is now rejected with
the new reason "deleted"; an add expecting the tombstone's revision
brings the row back. A write that changes nothing on a live row stays a
noop whatever revision it expects, so two clients healing the same row
never conflict.

Clients on protocol 4 still send checkLineupLinkEnds/checkLineupEndDeletes
and graph rows. The args are accepted and ignored, and the old kinds pass
argument validation to be refused per op in the handler, so such a client
gets CLIENT_UPGRADE_REQUIRED rather than a validation error. Storage takes
only lineups.

The agent summary is refreshed only after a batch that touched a page, an
agent element or a lineup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live sync compared lineup rows in their drawn form, so a fan-out patch
still queued for one row looked satisfied once the others landed, and its
record was dropped; deleting the lineup holding the drawn copy then showed
the stale one. Desired rows now carry each spot's drawn value and version
(unchanged keeps the drawn version, a change goes one past it, a new spot
starts at 1), versions taken from the rows the canvas was hydrated from,
and are compared with the rows as stored. Any row holding another copy is
patched until it really holds the drawn one, and a page is healed as soon
as it is drawn. Versions never reach the canvas, Hive or .ica.

Outbox records holding protocol 4 lineup ops are put in attention when the
outbox loads, whatever state an older build left them in, so reconciling
the canvas never removes them.

Keep mine after a teammate's delete sends an add over the tombstone with
the user's payload; a change it cannot bring back says so.

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

This branch has not been deployed

No deployments
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.

1 participant