feat(collab): Fase 0.5 — WebDAV-async-transport (#989) #995

Merged
brenno merged 6 commits from collab/webdav-async-transport into main 2026-07-30 14:19:22 +00:00
Owner

Fase 0.5 van COLLABORATION.md (#989): de eerste genetwerkte transportlaag — asynchroon co-auteuren over de Nextcloud/WebDAV die de gebruiker al heeft. Géén nieuwe afhankelijkheid, geen AGPL, geen Rust; de aanbevolen stap terwijl realtime-Matrix geparkeerd staat (#976/#991).

In de korrel van Fase 0 (#988): een puur bouwblok, geen UI.

Wat er landt

  • collab_codec.dart — de JSON-wire-(de)serialiser die §5.2 vraagt: alle vijf DeckOp-typen (incl. een volledige Slide voor InsertSlide), LockEvent, en de getypte veldwaarden. JSON by design, geen Markdown-round-trip (re-parsen zou slide-id's hergenereren, §5.5/P5). Elke decoder fail-closed. Volledigheid geborgd via een test over SlideField.values/DeckMetaField.values en een maximale-slide round-trip.
  • webdav_async_transport.dart + collab_log_store.dartWebdavAsyncTransport achter hetzelfde CollabTransport-contract als de loopback, dus de sessie-autoriteit stuurt hem ongewijzigd aan. Ops/locks landen als records in een append-only log-sidecar; een poll levert andermans records strikt op volgorde af en stopt bij een gat (past bij de versie-volgorde-apply, §5.3). Twee schrijvers botsen op een voorwaardelijke PUT (If-None-Match: *). Een deelnemer hoort zijn eigen verzending nooit. Geschreven tegen een CollabLogStore-naad → getest zonder server; WebdavCollabLogStore is de productiebinding (gedekt via een loopback-WebDAV).
  • collab.dart — module-barrel, zodat de laag bereikbaar is via de bestaande TabInfo.collabSession-naad.

Het .md blijft onaangeroerd (P2) — de log is een vergankelijke sidecar. Snapshot-herbaselijning voor late deelnemers (§5.2) en de interface-bedrading (§5.7) blijven vervolg, net als bij #988.

Poort: make check groen (7553 tests, dekking 87,4%, per-bestand-vloer gehaald; nieuwe bestanden 95%/100%/91%). Docs bijgewerkt (SOURCE_MAP, CHANGELOG, COLLABORATION.md §10).

Sluit #989.

Fase 0.5 van COLLABORATION.md (#989): de **eerste genetwerkte transportlaag** — asynchroon co-auteuren over de Nextcloud/WebDAV die de gebruiker al heeft. Géén nieuwe afhankelijkheid, geen AGPL, geen Rust; de aanbevolen stap terwijl realtime-Matrix geparkeerd staat (#976/#991). In de korrel van Fase 0 (#988): een puur bouwblok, geen UI. **Wat er landt** - `collab_codec.dart` — de JSON-wire-(de)serialiser die §5.2 vraagt: alle vijf `DeckOp`-typen (incl. een volledige `Slide` voor `InsertSlide`), `LockEvent`, en de getypte veldwaarden. JSON *by design*, geen Markdown-round-trip (re-parsen zou slide-id's hergenereren, §5.5/P5). Elke decoder fail-closed. Volledigheid geborgd via een test over `SlideField.values`/`DeckMetaField.values` en een maximale-slide round-trip. - `webdav_async_transport.dart` + `collab_log_store.dart` — `WebdavAsyncTransport` achter hetzelfde `CollabTransport`-contract als de loopback, dus de sessie-autoriteit stuurt hem ongewijzigd aan. Ops/locks landen als records in een append-only log-sidecar; een poll levert andermans records strikt op volgorde af en stopt bij een gat (past bij de versie-volgorde-apply, §5.3). Twee schrijvers botsen op een voorwaardelijke PUT (`If-None-Match: *`). Een deelnemer hoort zijn eigen verzending nooit. Geschreven tegen een `CollabLogStore`-naad → getest zonder server; `WebdavCollabLogStore` is de productiebinding (gedekt via een loopback-WebDAV). - `collab.dart` — module-barrel, zodat de laag bereikbaar is via de bestaande `TabInfo.collabSession`-naad. Het `.md` blijft onaangeroerd (P2) — de log is een vergankelijke sidecar. Snapshot-herbaselijning voor late deelnemers (§5.2) en de interface-bedrading (§5.7) blijven vervolg, net als bij #988. **Poort:** `make check` groen (7553 tests, dekking 87,4%, per-bestand-vloer gehaald; nieuwe bestanden 95%/100%/91%). Docs bijgewerkt (SOURCE_MAP, CHANGELOG, COLLABORATION.md §10). Sluit #989.
The dedicated model (de)serialiser COLLABORATION.md §5.2 calls for: all
five DeckOp types (including a full Slide for InsertSlide), LockEvent,
and the typed field values, to and from JSON. JSON by design, not a
Markdown round-trip — re-parsing would regenerate slide ids (§5.5) and
re-derive escaping the typed op model exists to avoid (P5).

Every decoder is fail-closed (mirrors applyOp): unknown discriminator,
missing field, wrong type or unknown enum name throws FormatException
rather than yielding a lossy op. The per-field value kinds are guarded
by a test over SlideField.values / DeckMetaField.values; the full-Slide
mapping by an exhaustive maximal-slide round-trip.

First building block of the WebDAV async transport (#989); the loopback
transport needs no serialisation, so this lives beside the transports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fase 0.5 (COLLABORATION.md §10): asynchronous co-authoring over the
Nextcloud/WebDAV a user already configured — no new dependency, no AGPL,
no Rust — the recommended step while real-time Matrix stays parked
(#976/#991). The first transport that connects two real machines.

WebdavAsyncTransport implements the same CollabTransport pipe as the
loopback, so collab_session.dart drives it unchanged. Sends append to a
numbered log; a poll delivers others' records strictly in sequence order
and stops at a gap, matching the authority's version-order apply (§5.3).
A participant never hears its own sends (tagged by participantId). The
transport is written against a CollabLogStore seam so tests drive it
with an in-memory store; WebdavCollabLogStore is the production binding,
appending each record with a conditional PUT (If-None-Match: *) so two
racers cannot both take one sequence number. The deck .md is untouched
(P2) — the log is a transient sidecar.

Snapshot re-baselining for late joiners (§5.2) and the per-tab provider
wiring (§5.7) stay follow-ups, as in Fase 0 (#988).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drives WebdavCollabLogStore end to end over a throwaway loopback WebDAV
server (the webdav_service_coverage_test pattern): listing parses and
sorts record filenames and ignores non-record siblings, a missing ops
directory reads as an empty log, read hits the zero-padded path, and
append carries If-None-Match:* — reporting success on 201 and failure on
412. Proves the conditional-PUT append works over real HTTP, not only the
in-memory store.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SOURCE_MAP gets collab_codec, collab_log_store and webdav_async_transport
with the store-seam rationale; CHANGELOG notes the first networked
transport; COLLABORATION.md §10 marks Fase 0.5 as transport+codec landed,
with snapshot re-baselining (§5.2) and the provider wiring (§5.7) as the
remaining follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The async transport, its codec and its store are reached from no
entrypoint on their own, so the dead-code gate rightly flagged them —
and the allowlist is deliberately not a parking spot for "not wired up
yet". A `collab.dart` barrel exports the whole module; tabs_provider,
which already depends on the layer for the TabInfo.collabSession seam,
imports the barrel instead of a single file, so the transports and codec
are reachable through the module they belong to. Registered in SOURCE_MAP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chore(collab): baseline the export-only barrel in the coverage tool (#989)
All checks were successful
scans / scans (pull_request) Successful in 3m26s
d26c33812f
collab.dart is pure export directives, so it has no executable lines and
never reaches the coverage denominator — exactly the "no executable
lines" case uncoveredBaseline documents. The files it re-exports are each
covered by their own tests (95%+).

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