board.py --set-row: replace the #398 whole-body-edit race with a compare-and-swap verb (design 006 S12) nf-mymf ← Beads

closed priority 2 task unassigned activesession:board-set-row

Twinned from https://github.com/cfsh/ninefold/issues/811 by tools/beads/import_github.py's reconcile pass.

Prescription, not a design question — the mechanism is decided below. Filed by the CTO off design 006 §5.6 (S12), which is background only; nothing here requires reading that doc.

The bug this replaces

The session board is issue #398's body — a markdown table, one row per live session, edited by having each session gh issue edit --body the whole thing back. gh issue edit --body is a whole-body replace, so two sessions editing within the same window silently clobber each other: whichever writes second wins, and the first session's row (or anyone else's) reverts to whatever it was before its own edit — no error, no merge, no signal.

Every session that has cared about this has hand-rolled the same fix independently: fetch the body, locate its own row, splice in the new content, re-fetch the body immediately before writing and abort if it changed since the first read. nf-dev-gh-upgrade named the consequence directly: *"every session must independently reinvent that, and any session that doesn't silently eats a peer's row."* This CTO session has done exactly that hand-rolled dance by hand something like eight times in the last day of orientation.

The fix is one verb that does the compare-and-swap once, correctly, so nobody reinvents it again.

What to build

tools/office/board.py, following this tier's conventions (tools/pr/*.py layout, unittest under tools/tests/, fakes over gh/git per design 001 — do not shell out to gh issue edit directly; extend nflib.gh.Gh with whatever body-read/body-write method it's still missing, the same way the rest of the tier talks to GitHub).

board.py --set-row <session> --row-file <path> [--board 398]

Inserts or replaces the row for <session> in the Sessions table of the board issue (#398 by default; --board overrides for tests/other repos, mirroring audit_sessions.py --board).

board.py --delete-row <session> [--board 398]

Same compare-and-swap mechanics, removes the row for <session> entirely rather than replacing it. Refuses (does nothing, clear message) if no row for that session exists. This is what a session's own wrap-up should call — per the board's own stated convention, *"a wrapped session's row is deleted, not greyed out."*

Both verbs
Tests — tools/tests/test_board.py

Against the existing gh fake (design 001), at minimum:

  1. Set on a session with no existing row appends a new row, table structure otherwise unchanged.
  2. Set on a session with an existing row replaces only that row, in place, byte-identical everywhere else.
  3. The race: fake the body changing between the tool's read and its write (this is the whole point — mutate the fake mid-call, or inject a second write between read and write in the test harness) — the tool must abort and must not write, and the row that "won" the race must survive untouched.
  4. Delete removes the row and nothing else changes; deleting a session with no row refuses cleanly rather than writing a no-op body.
  5. Matching is exact on the bolded name — a session name that is a prefix or substring of another's must not match it.
  6. Mutation-check each per this suite's standing rule (a test that cannot fail is unverified, not verified — #450).
Out of scope

Dependencies

None.

Comments

No comments.

Add a comment