feat(collab): de transportloze samenwerklaag (COLLABORATION.md Fase 0) #988

Merged
brenno merged 4 commits from collab/deck-op into main 2026-07-30 11:45:26 +00:00
Owner

De basis van live samenwerken (COLLABORATION.md Fase 0, #975) — het niet-afgewezen spoor B (de vergaderadapters van spoor A blijven afgewezen per #985). Puur Dart, geen netwerk, geen Flutter-import; de hele laag draait door een loopback in de tests, zonder infrastructuur. Nog niet in de interface.

Wat erin zit

  • lib/collab/deck_op.dart — het getypte operatiemodel (§5.1). Een DeckOp is één gezaghebbende wijziging aan het in-memory Deck, op volgorde van version: InsertSlide/RemoveSlide/ReorderSlide en de field-level SetSlideField/SetDeckMeta over de SlideField/DeckMetaField-enums. Ops werken op het getypte model, nooit op herparste Markdown (P5). applyOp is puur en fail-closed: een ontbrekende slide, een index buiten bereik of een waarde van het verkeerde type gooit — een stille no-op zou de op-stroom laten desynchroniseren (P3). Enums i.p.v. de door het ontwerp genoemde String field maken de switch exhaustief: de compiler, niet een test, garandeert dat élk syncbaar veld wordt afgehandeld.
  • lib/collab/collab_transport.dart — het CollabTransport-contract (P6) + LoopbackHub/LoopbackTransport. Een domme pijp: vervoert gevormde events, ordent en versiet niet. LockEvent draagt een forced-vlag voor de autoritaire ForceUnlock (§5.4).
  • lib/collab/collab_session.dart — de autoriteit-statemachine en lock-tabel (§5.3–5.4). Precies één sessie is autoriteit (P3) en kent de monotone versie toe aan élke op — eigen bewerkingen én ontvangen intenties — en herzendt de gezaghebbende op; iedereen past in versievolgorde toe en reproduceert zo hetzelfde deck.
  • TabInfo.collabSession (§5.7) — de naad waar een genetwerkte fase de live sessie aan een tabblad hangt; null op elk tabblad vandaag. Bewust nog géén provider en géén app_shell-override: die horen bij de fase waarin er een echte sessie ís.

Onderweg meegenomen: fix(test) — de sjabloontest no stray documents gebruikte een dubbel-ontsnapte regex (\\. in een raw string), matchte nóóit en liet main rood sinds #986. Eén backslash i.p.v. twee.

v1-grens (bewust, in de code gemarkeerd)

Een volger-bewerking gaat heen-en-weer (als intentie verzonden, toegepast bij de gezaghebbende echo) i.p.v. optimistisch onder een lock — de latentieverbergende helft van §5.4. Owner-drop-handover (§5.3), snapshot-herbasering voor laatkomers (§5.2) en de nullable/clear-flag-velden zijn vervolg. De enums en de TabInfo-seam zijn de naden waar dat bijkomt.

Waarborgen

  • make check groen (coverage-vloer 34%, 0 bestanden eronder), make check-secrets (0), make sast (0 findings).
  • Volledig getest over de loopback: convergentie van autoriteit- én volger-bewerkingen, versievolgorde, de lock-tabel en forceUnlock; en een data-gedreven tabel die élk SlideField/DeckMetaField uitoefent met een compleetheidsassertie.
  • Bewaker-overweging: dit raakt géén bestandsformaat, opslag, afhankelijkheid of uitgaand verkeer. Het is een in-memory laag; de .md blijft de waarheid (P2). Netwerk (Matrix) en de ketenkeuring van de matrix-SDK komen apart (#976/#977).

Sluit #975.

🤖 Generated with Claude Code

De basis van live samenwerken (COLLABORATION.md Fase 0, #975) — het niet-afgewezen spoor B (de vergaderadapters van spoor A blijven afgewezen per #985). Puur Dart, geen netwerk, geen Flutter-import; de hele laag draait door een loopback in de tests, zonder infrastructuur. Nog niet in de interface. ## Wat erin zit - **`lib/collab/deck_op.dart`** — het getypte operatiemodel (§5.1). Een `DeckOp` is één gezaghebbende wijziging aan het in-memory `Deck`, op volgorde van `version`: `InsertSlide`/`RemoveSlide`/`ReorderSlide` en de field-level `SetSlideField`/`SetDeckMeta` over de `SlideField`/`DeckMetaField`-enums. Ops werken op het getypte model, nooit op herparste Markdown (P5). `applyOp` is puur en **fail-closed**: een ontbrekende slide, een index buiten bereik of een waarde van het verkeerde type gooit — een stille no-op zou de op-stroom laten desynchroniseren (P3). Enums i.p.v. de door het ontwerp genoemde `String field` maken de switch exhaustief: de compiler, niet een test, garandeert dat élk syncbaar veld wordt afgehandeld. - **`lib/collab/collab_transport.dart`** — het `CollabTransport`-contract (P6) + `LoopbackHub`/`LoopbackTransport`. Een domme pijp: vervoert gevormde events, ordent en versiet niet. `LockEvent` draagt een `forced`-vlag voor de autoritaire `ForceUnlock` (§5.4). - **`lib/collab/collab_session.dart`** — de autoriteit-statemachine en lock-tabel (§5.3–5.4). Precies één sessie is autoriteit (P3) en kent de monotone versie toe aan élke op — eigen bewerkingen én ontvangen intenties — en herzendt de gezaghebbende op; iedereen past in versievolgorde toe en reproduceert zo hetzelfde deck. - **`TabInfo.collabSession`** (§5.7) — de naad waar een genetwerkte fase de live sessie aan een tabblad hangt; `null` op elk tabblad vandaag. Bewust nog géén provider en géén app_shell-override: die horen bij de fase waarin er een echte sessie ís. Onderweg meegenomen: **`fix(test)`** — de sjabloontest `no stray documents` gebruikte een dubbel-ontsnapte regex (`\\.` in een raw string), matchte nóóit en liet main rood sinds #986. Eén backslash i.p.v. twee. ## v1-grens (bewust, in de code gemarkeerd) Een volger-bewerking gaat heen-en-weer (als intentie verzonden, toegepast bij de gezaghebbende echo) i.p.v. optimistisch onder een lock — de latentieverbergende helft van §5.4. Owner-drop-handover (§5.3), snapshot-herbasering voor laatkomers (§5.2) en de nullable/clear-flag-velden zijn vervolg. De enums en de `TabInfo`-seam zijn de naden waar dat bijkomt. ## Waarborgen - `make check` groen (coverage-vloer 34%, 0 bestanden eronder), `make check-secrets` (0), `make sast` (0 findings). - Volledig getest over de loopback: convergentie van autoriteit- én volger-bewerkingen, versievolgorde, de lock-tabel en forceUnlock; en een data-gedreven tabel die élk `SlideField`/`DeckMetaField` uitoefent met een compleetheidsassertie. - Bewaker-overweging: dit raakt géén bestandsformaat, opslag, afhankelijkheid of uitgaand verkeer. Het is een in-memory laag; de `.md` blijft de waarheid (P2). Netwerk (Matrix) en de ketenkeuring van de matrix-SDK komen apart (#976/#977). Sluit #975. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Eerste bouwsteen van de samenwerklaag (COLLABORATION.md Fase 0, #975):
een DeckOp is één gezaghebbende wijziging aan het in-memory Deck, op
volgorde van version. Ops werken op het getypte Deck/Slide-model, nooit
op herparste Markdown (P5) — een co-auteurbewerking gaat als een
field-level intentie over de lijn, niet als een tekstdiff.

- Structurele ops: InsertSlide, RemoveSlide, ReorderSlide.
- SetSlideField / SetDeckMeta over getypte SlideField/DeckMetaField-enums.
  Enums i.p.v. de door het ontwerp genoemde 'String field': de switch in
  applyOp is dan exhaustief, dus een nieuw veld zonder case is een
  compileerfout — de compiler bewaakt de volledigheid, niet een test.
- applyOp is puur (muteert het deck niet) en FAIL-CLOSED: een ontbrekende
  slide, een index buiten bereik of een waarde van het verkeerde type
  gooit ArgumentError i.p.v. een stille no-op, want een stille no-op zou
  de op-stroom laten desynchroniseren (P3).

Nullable velden met een clear-flag (Slide.privacy, viewLimit) zijn
bewust buiten v1: een platte waarde kan 'zet op null' niet uitdrukken en
de locking maakt de gedekte veldset voldoende (§5.4, §12 Q2). De enums
zijn de naad waar latere velden bijkomen.

Puur Dart, geen netwerk; volledig getest via een data-gedreven tabel die
élk SlideField/DeckMetaField uitoefent, met een compleetheidsassertie.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bouwt de samenwerklaag verder op deck_op (COLLABORATION.md Fase 0, #975)
en hangt hem via TabInfo in de app, zodat de hele laag bereikbaar is.

- collab_transport.dart: het CollabTransport-contract (P6) + LoopbackHub/
  LoopbackTransport. De transport is een domme pijp: hij vervoert
  gevormde events tussen deelnemers, ordent en versiet niet. LockEvent
  krijgt een forced-vlag voor de autoritaire ForceUnlock (§5.4).
- collab_session.dart: de autoriteit-statemachine (§5.3-5.4). Precies één
  sessie is autoriteit (P3) en kent de monotone versie toe aan élke op —
  eigen bewerkingen én ontvangen intenties — en herzendt de gezaghebbende
  op; iedereen past in versievolgorde toe en reproduceert zo hetzelfde
  deck. Plus de lock-tabel (acquire/release/forceUnlock).
- TabInfo krijgt een CollabSession?-veld (§5.7: "Store the active
  SessionRef on TabInfo"): null = dit tabblad werkt niet samen, wat elk
  tabblad vandaag is. De naad staat er vanaf het begin; een genetwerkte
  fase (Matrix, §6) hangt hier de live sessie aan. Bewust nog géén
  Riverpod-provider en géén app_shell-override: die horen bij de fase
  waarin er een echte sessie ís om per tab te tonen.
- deck_op krijgt copyWithVersion: de autoriteit stempelt de canonieke
  versie op een intentie (version 0) vóór het herzenden.

v1-grens, bewust: een volger-bewerking gaat heen-en-weer (als intentie
verzonden, toegepast bij de gezaghebbende echo) i.p.v. optimistisch onder
een lock — de latentieverbergende helft van §5.4, plus owner-drop-handover
(§5.3) en snapshot-herbasering voor laatkomers (§5.2), zijn vervolg.

Volledig over de loopback getest: convergentie van autoriteit- én
volger-bewerkingen, versievolgorde, de lock-tabel en forceUnlock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(test): dubbel-ontsnapte regex in de sjabloontest (main stond rood)
All checks were successful
scans / scans (pull_request) Successful in 3m28s
67e20710be
De 'no stray documents'-poort gebruikte r'^(.+)\\.(nl|en|es|el)\\.md$':
in een raw string is \\ een letterlijke backslash, dus de regex eiste een
backslash in de bestandsnaam en matchte nóóit — elk sjabloonbestand faalde
op 'match isNotNull', councilProposal.es.md als eerste in de listing.
Binnengekomen met #986 (Griekse sjablonen), zonder groene make check
gemerged, dus main was sindsdien rood. Eén backslash i.p.v. twee.

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