Samenwerken: een gewijzigde paneelzoom reist niet mee, en niets bewaakt die richting #1803

Closed
opened 2026-08-27 12:25:46 +00:00 by brenno · 3 comments
Owner

Wat er misgaat

Werken twee mensen samen aan één deck, dan reist een gewijzigde paneelzoom
niet mee. De een sleept de zoom van een bulletsImage- of twoImages-afbeelding
naar 140%, de ander blijft 0% zien. Er komt geen melding, geen conflict, geen
spoor — de twee schermen lopen stil uiteen en blijven zo.

De asymmetrie maakt het extra verwarrend: een nieuwe dia draagt de zoom wél
mee (die gaat als hele dia over de lijn), en een hersynchronisatie herstelt
hem ook. Alleen het wijzigen van de zoom op een dia die beide kanten al hebben,
komt niet aan. Wie het probeert te reproduceren door een verse dia te maken, ziet
dus niets.

Reproductie

  1. Twee cliënten in dezelfde sessie, beide op hetzelfde deck.
  2. Cliënt A maakt een bulletsImage-dia met een afbeelding en synchroniseert.
  3. Cliënt A zet de paneelzoom op bijvoorbeeld 140%.
  4. Cliënt B ziet de zoom onveranderd op 0%. Beiden slaan op → twee verschillende
    bijsnijdingen, afhankelijk van wie het laatst schreef.

Waar het zit

imageZoom staat niet in SlideField (lib/collab/deck_op.dart), terwijl
zijn eigen buren er wél in staan: imageSize, imageFocalX, imageFocalY,
imageFocalX2, imageFocalY2. collab_deck_diff.dart loopt bij een bewerking
op een bestaande dia letterlijk over SlideField.values, dus wat daar niet in
staat wordt niet gediffed en er is geen terugval op een hele-dia-operatie.
collab_codec.dart draagt imageZoom wél (regels 202 en 279), en dát is precies
waarom insert en snapshot het goed doen en de bewerking niet.

Dit is geen bewuste grens maar een vergeten veld, en de datums laten dat zien:

  • f62d4abe (30-07-2026) zette het getypte operatiemodel neer, mét de
    focal-velden en imageSize;
  • 5f926d28 (12-08-2026) voegde imageZoom toe aan het model, twee weken later;
  • imageZoom heeft nooit in deck_op.dart gestaan.

Waarom geen enkele test dit ving

Er staan twee pariteitscontroles, en ze kijken allebei dezelfde kant op:

  • test/deck_op_test.dart:236 — elke SlideField heeft een testgeval;
  • test/collab_codec_test.dart:379 — de codec beeldt elke SlideField af.

Beide bewaken "alles wat ín SlideField staat wordt afgehandeld". Geen van beide
bewaakt de andere richting: "elk synchroniseerbaar veld van Slide staat in
SlideField"
. Een veld toevoegen aan Slide haalt dus geen enkele poort
omlaag, en dat is exact hoe dit erdoorheen kwam.

De bredere waarneming — en waarom een losse reparatie niet genoeg is

imageZoom is niet de enige sleutel die de hele-dia-codec wel kent en
SlideField niet. Er zijn er zestien meer: tableRows, bulletMarkerOverride,
improvementLayout, viewLimit, renderPage, privacy, quality,
contentRedacted, mediaRedacted, aiAssistedFields, findingRole,
timelineLayout, timelineReveal, timelineAnimationMs, timelineCurrentIndex
(en id, dat terecht geen veldbewerking is).

Een deel daarvan is met opzet buiten de synchroniseerbare oppervlakte
gelaten — de doc-comment bovenaan collab_deck_diff.dart noemt tabelrijen,
annotaties en het zegel bij name. Maar aan de code is niet te zien wélke van de
zestien bewust zijn en welke vergeten. Dat onderscheid bestaat alleen in iemands
hoofd, en dát is het eigenlijke defect: de volgende keer gaat het op precies
dezelfde manier mis.

Voorstel

  1. imageZoom toevoegen aan SlideField, aan slideFieldValue en aan de
    apply-kant, met een regressietest die eerst rood staat: twee cliënten, zoom
    gewijzigd op een bestaande dia, waarde komt aan.
  2. Een pariteitspoort in de ontbrekende richting: elk veld van Slide staat
    in SlideField, óf op een expliciete uitsluitingslijst met een reden erbij.
    Dan is "bewust niet gesynchroniseerd" een geschreven keuze in plaats van een
    afwezigheid, en faalt een nieuw veld de poort tot iemand kiest.
  3. Bij het opstellen van die lijst de zestien langslopen en per veld vastleggen
    welke van de twee het is. Dat is het echte werk; punt 1 is een regel.

Waarom dit nu boven kwam

Gevonden tijdens het ontwerp voor #1801 (beeldverwijzingen). Een callout wijst
naar een plek in de afbeelding, en die plek wordt uitgerekend over de bijsnijding
— dus zoom en focal. Lopen twee mensen uiteen op de zoom, dan wijst dezelfde
callout bij hen naar iets anders. Dit is daar slice 1 van, maar het staat op
zichzelf: het gaat vandaag al mis, zonder dat callouts bestaan.

**Wat er misgaat** Werken twee mensen samen aan één deck, dan reist een gewijzigde **paneelzoom** niet mee. De een sleept de zoom van een `bulletsImage`- of `twoImages`-afbeelding naar 140%, de ander blijft 0% zien. Er komt geen melding, geen conflict, geen spoor — de twee schermen lopen stil uiteen en blijven zo. De asymmetrie maakt het extra verwarrend: een **nieuwe** dia draagt de zoom wél mee (die gaat als hele dia over de lijn), en een **hersynchronisatie** herstelt hem ook. Alleen het *wijzigen* van de zoom op een dia die beide kanten al hebben, komt niet aan. Wie het probeert te reproduceren door een verse dia te maken, ziet dus niets. **Reproductie** 1. Twee cliënten in dezelfde sessie, beide op hetzelfde deck. 2. Cliënt A maakt een `bulletsImage`-dia met een afbeelding en synchroniseert. 3. Cliënt A zet de paneelzoom op bijvoorbeeld 140%. 4. Cliënt B ziet de zoom onveranderd op 0%. Beiden slaan op → twee verschillende bijsnijdingen, afhankelijk van wie het laatst schreef. **Waar het zit** `imageZoom` staat **niet** in `SlideField` (`lib/collab/deck_op.dart`), terwijl zijn eigen buren er wél in staan: `imageSize`, `imageFocalX`, `imageFocalY`, `imageFocalX2`, `imageFocalY2`. `collab_deck_diff.dart` loopt bij een bewerking op een bestaande dia letterlijk over `SlideField.values`, dus wat daar niet in staat wordt niet gediffed en er is geen terugval op een hele-dia-operatie. `collab_codec.dart` draagt `imageZoom` wél (regels 202 en 279), en dát is precies waarom insert en snapshot het goed doen en de bewerking niet. Dit is geen bewuste grens maar een vergeten veld, en de datums laten dat zien: - `f62d4abe` (30-07-2026) zette het getypte operatiemodel neer, mét de focal-velden en `imageSize`; - `5f926d28` (12-08-2026) voegde `imageZoom` toe aan het model, twee weken later; - `imageZoom` heeft **nooit** in `deck_op.dart` gestaan. **Waarom geen enkele test dit ving** Er staan twee pariteitscontroles, en ze kijken allebei dezelfde kant op: - `test/deck_op_test.dart:236` — elke `SlideField` heeft een testgeval; - `test/collab_codec_test.dart:379` — de codec beeldt elke `SlideField` af. Beide bewaken "alles wat ín `SlideField` staat wordt afgehandeld". Geen van beide bewaakt de andere richting: **"elk synchroniseerbaar veld van `Slide` staat in `SlideField`"**. Een veld toevoegen aan `Slide` haalt dus geen enkele poort omlaag, en dat is exact hoe dit erdoorheen kwam. **De bredere waarneming — en waarom een losse reparatie niet genoeg is** `imageZoom` is niet de enige sleutel die de hele-dia-codec wel kent en `SlideField` niet. Er zijn er zestien meer: `tableRows`, `bulletMarkerOverride`, `improvementLayout`, `viewLimit`, `renderPage`, `privacy`, `quality`, `contentRedacted`, `mediaRedacted`, `aiAssistedFields`, `findingRole`, `timelineLayout`, `timelineReveal`, `timelineAnimationMs`, `timelineCurrentIndex` (en `id`, dat terecht geen veldbewerking is). Een deel daarvan is **met opzet** buiten de synchroniseerbare oppervlakte gelaten — de doc-comment bovenaan `collab_deck_diff.dart` noemt tabelrijen, annotaties en het zegel bij name. Maar aan de code is niet te zien wélke van de zestien bewust zijn en welke vergeten. Dat onderscheid bestaat alleen in iemands hoofd, en dát is het eigenlijke defect: de volgende keer gaat het op precies dezelfde manier mis. **Voorstel** 1. `imageZoom` toevoegen aan `SlideField`, aan `slideFieldValue` en aan de apply-kant, met een regressietest die eerst rood staat: twee cliënten, zoom gewijzigd op een bestaande dia, waarde komt aan. 2. Een **pariteitspoort in de ontbrekende richting**: elk veld van `Slide` staat in `SlideField`, óf op een expliciete uitsluitingslijst met een reden erbij. Dan is "bewust niet gesynchroniseerd" een geschreven keuze in plaats van een afwezigheid, en faalt een nieuw veld de poort tot iemand kiest. 3. Bij het opstellen van die lijst de zestien langslopen en per veld vastleggen welke van de twee het is. Dat is het echte werk; punt 1 is een regel. **Waarom dit nu boven kwam** Gevonden tijdens het ontwerp voor #1801 (beeldverwijzingen). Een callout wijst naar een plek in de afbeelding, en die plek wordt uitgerekend over de bijsnijding — dus zoom en focal. Lopen twee mensen uiteen op de zoom, dan wijst dezelfde callout bij hen naar iets anders. Dit is daar slice 1 van, maar het staat op zichzelf: het gaat vandaag al mis, zonder dat callouts bestaan.
Author
Owner

Opgepakt. Tak: fix/1803-collab-imagezoom-pariteit.

Verwachte reikwijdte: lib/collab/deck_op.dart (het veld plus slideFieldValue en de apply-kant), een pariteitspoort in de ontbrekende richting met een expliciete, beredeneerde uitsluitingslijst, en tests in test/deck_op_test.dart / test/collab_deck_diff_test.dart. Begin met de regressietest die eerst rood staat.

Opgepakt. Tak: `fix/1803-collab-imagezoom-pariteit`. Verwachte reikwijdte: `lib/collab/deck_op.dart` (het veld plus `slideFieldValue` en de apply-kant), een pariteitspoort in de ontbrekende richting met een expliciete, beredeneerde uitsluitingslijst, en tests in `test/deck_op_test.dart` / `test/collab_deck_diff_test.dart`. Begin met de regressietest die eerst rood staat.
Author
Owner

Opgelost en gemerged: PR #1805.

Punt 1 — het veld. imageZoom staat nu in SlideField, slideFieldValue,
de apply-kant en de codec-soortenkaart. De regressietest stond eerst rood tegen
de onherstelde code, met precies de goede reden: Expected: an object with length of <1>, Actual: [] — de diff maakte er nul ops van.

Die test leest bewust de wáárde ná toepassing en niet of een hervergelijking leeg
is. De bestaande expectReproduces-helper, in het bestand omschreven als "the
strongest check", is voor deze klasse fout blind: een veld dat de diff niet kent
is in béíde richtingen onzichtbaar, dus de hervergelijking komt leeg terug
terwijl er niets is overgekomen.

Punt 2 — de poort. make check-collab-field-parity stelt nu de vraag die
niemand stelde. Elk veld van Slide staat in SlideField, óf op een
uitsluitingslijst mét de reden ernaast, óf op een schuldlijst die alleen mag
krimpen. Getoetst in twee richtingen: stil op de echte repo, luid op een geplante
overtreding. De schuldlijst rijdt mee in make ratchets, zodat stilstand
zichtbaar wordt.

Punt 3 — de classificatie, deels. Vijf velden staan nu als bewuste
uitsluiting vastgelegd, elk met de reden: id (de ops sleutelen erop),
mediaRedacted, contentRedacted en renderPage (voorbijgaande
projectie-/renderstand, zo benoemd in slideToJson) en tableRows (een
vastgelegde v1-grens uit de doc-comment van collab_deck_diff.dart).

De overige achttien staan als schuld, niet als besluit. Dat is met opzet: ik
heb alleen geclassificeerd waar geschreven bewijs voor was. Of een tijdlijninstelling
of een privacydispositie hoort mee te reizen is een keuze, geen constatering, en
die verzin ik hier niet — de poort zorgt er nu voor dat het een keuze blíjft in
plaats van een gat.

Wat er onderweg groter bleek. Zeven van die achttien zitten helemáál niet in
de samenwerklaag: anchor, nextAnchor, ganttScale, ganttSections,
menuLayout, tableColumnAlignments, tableNumberColumns. Niet in
slideToJson, niet in slideFromJson — nagekeken, in beide richtingen nul
treffers. Een codec-rondgang verliest ze, dus ook een nieuwe dia komt bij de
ander aan met die velden op hun standaardwaarde. Dat breekt een vastgelegde
belofte: de doc-comment van slideToJson zegt "Every field is carried so the
receiver reproduces the slide exactly (P3)".

Dat is een andere reparatie met eigen tests en zit bewust niet in deze PR. Het
verdient een eigen issue; die leg ik apart voor.

Poorten: make check volledig groen (dekking 87,1%, per-bestandsvloer nul
eronder), check-secrets en sast schoon, beide CI-poorten groen.

Opgelost en gemerged: [PR #1805](https://pawprint.vigilis.online/LibreKAT/Ocideck/pulls/1805). **Punt 1 — het veld.** `imageZoom` staat nu in `SlideField`, `slideFieldValue`, de apply-kant en de codec-soortenkaart. De regressietest stond eerst rood tegen de onherstelde code, met precies de goede reden: `Expected: an object with length of <1>, Actual: []` — de diff maakte er nul ops van. Die test leest bewust de wáárde ná toepassing en niet of een hervergelijking leeg is. De bestaande `expectReproduces`-helper, in het bestand omschreven als "the strongest check", is voor deze klasse fout blind: een veld dat de diff niet kent is in béíde richtingen onzichtbaar, dus de hervergelijking komt leeg terug terwijl er niets is overgekomen. **Punt 2 — de poort.** `make check-collab-field-parity` stelt nu de vraag die niemand stelde. Elk veld van `Slide` staat in `SlideField`, óf op een uitsluitingslijst mét de reden ernaast, óf op een schuldlijst die alleen mag krimpen. Getoetst in twee richtingen: stil op de echte repo, luid op een geplante overtreding. De schuldlijst rijdt mee in `make ratchets`, zodat stilstand zichtbaar wordt. **Punt 3 — de classificatie, deels.** Vijf velden staan nu als bewuste uitsluiting vastgelegd, elk met de reden: `id` (de ops sleutelen erop), `mediaRedacted`, `contentRedacted` en `renderPage` (voorbijgaande projectie-/renderstand, zo benoemd in `slideToJson`) en `tableRows` (een vastgelegde v1-grens uit de doc-comment van `collab_deck_diff.dart`). De overige achttien staan als schuld, **niet als besluit**. Dat is met opzet: ik heb alleen geclassificeerd waar geschreven bewijs voor was. Of een tijdlijninstelling of een privacydispositie hoort mee te reizen is een keuze, geen constatering, en die verzin ik hier niet — de poort zorgt er nu voor dat het een keuze blíjft in plaats van een gat. **Wat er onderweg groter bleek.** Zeven van die achttien zitten helemáál niet in de samenwerklaag: `anchor`, `nextAnchor`, `ganttScale`, `ganttSections`, `menuLayout`, `tableColumnAlignments`, `tableNumberColumns`. Niet in `slideToJson`, niet in `slideFromJson` — nagekeken, in beide richtingen nul treffers. Een codec-rondgang verliest ze, dus ook een *nieuwe* dia komt bij de ander aan met die velden op hun standaardwaarde. Dat breekt een vastgelegde belofte: de doc-comment van `slideToJson` zegt "Every field is carried so the receiver reproduces the slide exactly (P3)". Dat is een andere reparatie met eigen tests en zit bewust niet in deze PR. Het verdient een eigen issue; die leg ik apart voor. Poorten: `make check` volledig groen (dekking 87,1%, per-bestandsvloer nul eronder), `check-secrets` en `sast` schoon, beide CI-poorten groen.
brenno 2026-08-27 13:19:35 +00:00
Author
Owner

Vervolg vastgelegd. De poort uit deze PR bracht de hele oppervlakte in kaart; wat daar uit kwam staat nu als losse issues in plaats van als schuldlijst-zonder-eigenaar:

  • #1807 — zeven velden reizen helemáál niet mee, ook niet bij een nieuwe dia (breekt de slideToJson-belofte "Every field is carried … (P3)")
  • #1808 — elf velden komen bij invoegen wel over en bij bewerken niet; per veld een besluit, niet elf issues
  • #1809 — tabelbewerkingen bereiken de ander niet, en nergens staat dat; raakt élk tabelgedragen slidetype
  • #1811expectReproduces heet "the strongest check" maar is blind voor precies deze klasse fout

De schuldlijst unsyncedBaseline blijft de teller: hij mag alleen krimpen, en make ratchets laat stilstand zien.

Vervolg vastgelegd. De poort uit deze PR bracht de hele oppervlakte in kaart; wat daar uit kwam staat nu als losse issues in plaats van als schuldlijst-zonder-eigenaar: - #1807 — zeven velden reizen helemáál niet mee, ook niet bij een nieuwe dia (breekt de `slideToJson`-belofte "Every field is carried … (P3)") - #1808 — elf velden komen bij invoegen wel over en bij bewerken niet; per veld een besluit, niet elf issues - #1809 — tabelbewerkingen bereiken de ander niet, en nergens staat dat; raakt élk tabelgedragen slidetype - #1811 — `expectReproduces` heet "the strongest check" maar is blind voor precies deze klasse fout De schuldlijst `unsyncedBaseline` blijft de teller: hij mag alleen krimpen, en `make ratchets` laat stilstand zien.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#1803
No description provided.