feat(collab): eigenaarsoverdracht bij wegvallen van de eigenaar (§5.3, #1004) #1006

Merged
brenno merged 8 commits from collab/owner-handover into main 2026-07-30 22:58:58 +00:00
Owner

Closes #1004.

Vervolg binnen Fase 0.5 (na #996/#999–#1003). Valt de eigenaar van een
samenwerksessie weg — verbinding kwijt of app dicht — dan stopte de sessie:
niemand kende nog versies toe en intents bleven onverwerkt in de async
WebDAV-log liggen. Dit laat een opvolger de sessie levend houden en draagt terug
over als de eigenaar terugkomt, zonder de single-authority-invariant (P3) in
steady state te breken. Het .md-bestandsformaat (P2) blijft ongemoeid: alle
handover-metadata leeft in de sidecar.

Ontwerp (COLLABORATION.md §5.3)

  • Beacon (beacon.json, last-write-wins naast de log): {authority, authorityIsOwner, tick}. De autoriteit bumpt tick elke poll — een heartbeat;
    een bevroren tick is het drop-signaal. Advisory: WebDAV-schrijfrecht is de
    enige poort.
  • Detectie in poll-tellingen, geen wandklok — deterministisch toetsbaar.
  • isOwner is een lanceerfeit (host=true, guest=false), niet uit de beacon
    afgeleid. Alleen de eigenaar persisteert. Terugkerende eigenaar her-host't
    (resume-aware: baseline overgenomen, niet gereset) en herwint autoriteit via de
    electie-route.
  • Overname-poorten: caught-up én sequence-steady — die tweede sluit het
    WebDAV list-lag-gat (geen cross-client read-your-writes). Versie hervat vanaf de
    hoogst bekende versie.
  • Deterministische opvolgvolgorde zonder roster: een stabiele hash-backoff.
  • Fail-closed: malformed/afwezige beacon, een read die gooit, een write die
    faalt → nooit autoriteit. Claim pas ná een geslaagde beacon-write; een
    incumbent die zijn heartbeat niet meer kan schrijven stapt terug.
  • Verloren intents worden bij een autoriteitswissel her-ingezonden via de
    bestaande deck-diff — alleen wanneer caught-up, zodat een achterlopend deck geen
    structurele op dupliceert.

Gedocumenteerde residuals (Fase 0.5, bewust)

  • Twee gelijktijdige autoriteiten in één poll-venster → dezelfde versie; elke
    replica dropt de duplicaat en convergeert, maar de verliezende autoriteit
    behoudt haar eigen op tot ze herlaadt. De gewone enkel-opvolger-route raakt dit
    niet. Volledige fix (mid-sessie re-baseline) rijdt mee met §5.2.
  • Heartbeat = liveness-zijkanaal. Beide in §5.3 + een dreigingsmodel-rij in
    SECURITY_DESIGN.md.
  • "Alleen de eigenaar bewaart" is een client-side coördinatiepoort, geen
    server-afdwinging — eerlijk gesteld in de dreigingsmodel-rij (na bewaker-review).
    Een gast zit niet vast: verlaten of een .ocideck-pakket exporteren blijft vrij.

Reikwijdte

CollabSession (muteerbare autoriteit), WebdavAsyncTransport
(isCaughtUp/knownMaxSeq), CollabLogStore (beacon), nieuwe
HandoverCoordinator, launch (CollabLaunch, resume-aware host), provider + UI
(save-poort in saveDeckWithDestination, tijdelijke-autoriteit-melding), l10n
(31 talen), docs.

Toetsing

Ontworpen na een drievoudige ontwerp-review (architectuur, beveiliging, test),
daarna een implementatie-correctheid-review en een bewaker-review vóór de PR —
elke blokkerende bevinding verwerkt (o.a. de list-lag-poort mutatie-vast getoetst,
de re-drive caught-up-gated, en de eerlijke ACL-formulering). ~40 nieuwe tests
incl. de list-lag-poort (store-double), fail-closed-paden, hand-back, terugkerende
eigenaar via electie, versie-hervatting, convergentie en de residual.

Poorten (deze machine — geen CI-runner op Forgejo): make check groen (87,4%
dekking), make l10n-check groen, make check-secrets en make sast schoon.
Goldens niet gedraaid: deze wijziging raakt geen rendering (backend + een
transient snackbar).

🤖 Generated with Claude Code

Closes #1004. Vervolg binnen Fase 0.5 (na #996/#999–#1003). Valt de eigenaar van een samenwerksessie weg — verbinding kwijt of app dicht — dan stopte de sessie: niemand kende nog versies toe en intents bleven onverwerkt in de async WebDAV-log liggen. Dit laat een opvolger de sessie levend houden en draagt terug over als de eigenaar terugkomt, zonder de single-authority-invariant (P3) in steady state te breken. Het `.md`-bestandsformaat (P2) blijft ongemoeid: alle handover-metadata leeft in de sidecar. ## Ontwerp (COLLABORATION.md §5.3) - **Beacon** (`beacon.json`, last-write-wins naast de log): `{authority, authorityIsOwner, tick}`. De autoriteit bumpt `tick` elke poll — een heartbeat; een bevroren tick is het drop-signaal. Advisory: WebDAV-schrijfrecht is de enige poort. - **Detectie in poll-tellingen, geen wandklok** — deterministisch toetsbaar. - **`isOwner` is een lanceerfeit** (host=true, guest=false), niet uit de beacon afgeleid. Alleen de eigenaar persisteert. Terugkerende eigenaar her-host't (resume-aware: baseline overgenomen, niet gereset) en herwint autoriteit via de electie-route. - **Overname-poorten**: caught-up **én** sequence-steady — die tweede sluit het WebDAV list-lag-gat (geen cross-client read-your-writes). Versie hervat vanaf de hoogst bekende versie. - **Deterministische opvolgvolgorde zonder roster**: een stabiele hash-backoff. - **Fail-closed**: malformed/afwezige beacon, een read die gooit, een write die faalt → nooit autoriteit. Claim pas ná een geslaagde beacon-write; een incumbent die zijn heartbeat niet meer kan schrijven stapt terug. - **Verloren intents** worden bij een autoriteitswissel her-ingezonden via de bestaande deck-diff — alleen wanneer caught-up, zodat een achterlopend deck geen structurele op dupliceert. ## Gedocumenteerde residuals (Fase 0.5, bewust) - Twee gelijktijdige autoriteiten in één poll-venster → dezelfde versie; elke replica dropt de duplicaat en convergeert, maar de *verliezende* autoriteit behoudt haar eigen op tot ze herlaadt. De gewone enkel-opvolger-route raakt dit niet. Volledige fix (mid-sessie re-baseline) rijdt mee met §5.2. - Heartbeat = liveness-zijkanaal. Beide in §5.3 + een dreigingsmodel-rij in SECURITY_DESIGN.md. - "Alleen de eigenaar bewaart" is een **client-side coördinatiepoort**, geen server-afdwinging — eerlijk gesteld in de dreigingsmodel-rij (na bewaker-review). Een gast zit niet vast: verlaten of een `.ocideck`-pakket exporteren blijft vrij. ## Reikwijdte `CollabSession` (muteerbare autoriteit), `WebdavAsyncTransport` (`isCaughtUp`/`knownMaxSeq`), `CollabLogStore` (beacon), nieuwe `HandoverCoordinator`, launch (`CollabLaunch`, resume-aware host), provider + UI (save-poort in `saveDeckWithDestination`, tijdelijke-autoriteit-melding), l10n (31 talen), docs. ## Toetsing Ontworpen na een drievoudige ontwerp-review (architectuur, beveiliging, test), daarna een implementatie-correctheid-review en een bewaker-review vóór de PR — elke blokkerende bevinding verwerkt (o.a. de list-lag-poort mutatie-vast getoetst, de re-drive caught-up-gated, en de eerlijke ACL-formulering). ~40 nieuwe tests incl. de list-lag-poort (store-double), fail-closed-paden, hand-back, terugkerende eigenaar via electie, versie-hervatting, convergentie en de residual. Poorten (deze machine — geen CI-runner op Forgejo): `make check` groen (87,4% dekking), `make l10n-check` groen, `make check-secrets` en `make sast` schoon. Goldens niet gedraaid: deze wijziging raakt geen rendering (backend + een transient snackbar). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
CollabSession.isAuthority becomes mutable via becomeAuthority()/stepDown();
new commits resume version assignment from the current version, so a successor
that took over at the highest observed version continues without a gap or
collision. Pure sync logic — the policy of *when* to flip lives one layer up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The handover coordinator gates a takeover on the successor having seen every
record. WebDAV-specific, so it stays off the neutral CollabTransport interface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
readBeacon/writeBeacon beside the snapshot (in-memory + WebDAV bindings). The
beacon (beacon.json) is an advisory last-write-wins blob carrying who assigns
versions and a liveness tick; a frozen tick is the owner-drop signal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The policy layer over the async transport (COLLABORATION.md §5.3). One atomic
poll+decide cycle drives: a beacon heartbeat; poll-counted staleness (no
wall-clock); a caught-up + sequence-steady takeover guard (the second closes the
WebDAV list-lag gap — no cross-client read-your-writes); a roster-free
deterministic backoff; hand-back to a returning owner; and fail-closed claiming
(write the beacon before acting; an incumbent that cannot heartbeat steps down).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hostCollabSession/joinCollabSession return a CollabLaunch {session, coordinator}
and the coordinator owns the poll loop; hostCollabSession is resume-aware for a
returning owner. The provider re-drives lost edits on an authority change (only
when caught up, so a lagging deck cannot duplicate a structural op) and surfaces
a temporary-authority indicator. Only the owner persists: saveDeckWithDestination
— the single save chokepoint — refuses a guest's save. +1 file-size baseline for
the one build() listen hook (behaviour extracted; the class stays under ceiling).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The save-block and temporary-authority snackbars, added via make add-l10n.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
COLLABORATION.md §5.3 gains the full Fase 0.5 mechanism and the two documented
residuals (transient dual-authority divergence for a losing writer; the heartbeat
liveness side-channel). SECURITY_DESIGN.md gains a threat-model row. USER_GUIDE,
SOURCE_MAP (registers handover_coordinator.dart) and CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs(collab): honest 'only owner saves' framing after core-values review (#1004)
All checks were successful
scans / scans (pull_request) Successful in 3m32s
cfe1b6b67d
The bewaker flagged that the threat-model row over-claimed the WebDAV ACL as a
persistence backstop: in a shared folder the .md sits beside the sidecar, so
'only the owner saves' is a client-side coordination gate, not server-enforced.
Reworded. Also softened the CHANGELOG no-collision phrasing (the failover
edge is a documented residual) and gave the USER_GUIDE a line on reloading after
a contested handover plus a guest's escape route (leave, or export a package).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brenno merged commit c13346657f into main 2026-07-30 22:58:58 +00:00
Sign in to join this conversation.
No description provided.