Documentmodus: opslaan vanuit de visuele stand herschrijft de hele bron #1613

Closed
opened 2026-08-20 16:00:40 +00:00 by brenno · 0 comments
Owner

Een document opslaan vanuit de visuele stand schrijft niet de bron terug die je hebt bewerkt, maar de bron die uit de heen-en-terugweg naar de rijke-tekstlaag komt. Die weg is niet byte-getrouw, dus het bestand op schijf wordt geherstructureerd op plekken waar je niets hebt aangeraakt.

Wat er gebeurt

Een echte notitie van 24 KB (koppen, GFM-tabellen, genummerde lijsten, een <!-- timeline -->-marker) geopend in de visuele stand, één woord aan een kop toegevoegd, Cmd+S. Uitkomst: 68 diff-regels tegen het origineel. Wat er veranderde zonder dat het gevraagd was:

  • witregels vóór en na koppen verdwijnen, en verschijnen elders;
  • een kop raakt los van zijn alinea — # 1. Kern van deze notitie belandt ná de regel die eronder stond;
  • de scheidingsregel van een tabel wordt herschreven: |---|---| wordt | --- | --- |;
  • een hele genummerde lijst van vijf punten verhuist naar een andere plek in het document;
  • # <!-- timeline --> wordt <!-- timeline -->;
  • in een herstelmomentopname van dezelfde sessie was een regel (4. Impact) uit de bron in het geheugen helemaal verdwenen.

Dat laatste maakt dit meer dan een opmaakkwestie: er kan inhoud sneuvelen.

De drift stapelt bovendien. Elke opslag schrijft de dan-geldende geherstructureerde vorm weg, en de volgende ronde begint dáár weer. In een repository met versiebeheer levert dat een diff op waarin de echte wijziging tussen tientallen spookregels verdwijnt.

Waarom dit tegen de belofte in gaat

saveDocument (lib/services/file/file_service_open.dart) zegt het met zoveel woorden: "precies de bron, geen deck-scaffold, geen marp:-kop, geen normalisatie", byte-getrouw naar het gekozen pad. DOCUMENT_MODE.md §1.1/§3 belooft hetzelfde: de bron ís de waarheid.

De mechaniek eronder weet ook dat de weg niet getrouw is. _onQuillChanged (lib/widgets/markdown_editor/markdown_editor.dart) noteert het letterlijk: "De heen-en-terugweg naar Markdown levert niet byte-getrouw dezelfde bron op (witregels rond koppen en blokken schuiven)". Daar wordt het gebruikt om een cursorwissel niet als bewerking te tellen — maar zodra er wél iets verandert, gaat de volledige geherstructureerde bron alsnog naar de notifier, en dus naar schijf.

Er ís een poort die hierover gaat: markdownVisualLimitations (lib/utils/markdown_visual_compatibility.dart) heet in zijn eigen doc "Constructs that the current rich-text bridge cannot round-trip without changing the author's Markdown source" en valt voor zulke constructies terug op de brontekst-stand. Die poort kent er drie: rawHtml, escapedPunctuation, looseTableLine. Witregel- en volgordedrift zit er niet in, dus die glipt er langs. En de belofte in datzelfde bestand dat een GFM-tabel "round-trips losslessly" klopt niet op byte-niveau — zie de scheidingsregel hierboven.

Reproduceren

  1. Neem een .md met koppen, een GFM-tabel met een compacte scheidingsregel (|---|---|) en een genummerde lijst.
  2. Open hem in OciDeck (documenttabblad), stand Visueel.
  3. Typ één woord in een kop.
  4. Cmd+S.
  5. diff origineel opgeslagen.

Wat een oplossing moet doen

Twee richtingen, en de keuze is een ontwerpbesluit:

  • De weg getrouw maken voor alles wat markdownVisualLimitations toelaat: wat de poort doorlaat moet ongewijzigd terugkomen, en wat dat niet haalt hoort de poort te noemen (en dus terug te vallen op de brontekst-stand).
  • Alleen schrijven wat je veranderde: de bewerking terugschrijven op de plek waar hij hoort in plaats van het hele document te vervangen. Dat raakt de brug fundamenteler, maar het maakt de vraag "is de rondgang getrouw" irrelevant voor de rest van het bestand.

Wat er in elk geval bij hoort: een eigenschapstoets over een corpus van echte documentenmarkdownFromDocument(documentFromMarkdown(x)) == x voor alles wat de compatibiliteitspoort toelaat, met de bestanden uit docs/ en een handvol notities als invoer. Zonder zo'n toets is elke reparatie hier één regressie van het volgende blok verwijderd. De eerdere reparaties #1507, #1558 en #1561 waren stuk voor stuk één symptoom uit deze klasse.

Reikwijdte

Dit raakt het bestandsformaat en wat er in het .md terechtkomt — de bewaker hoort mee te kijken vóór de PR, niet erna.

Gevonden tijdens het onderzoek naar de niet-opgeslagen-stip (#1611); dat was een andere oorzaak en is los opgelost.

Een document opslaan vanuit de **visuele stand** schrijft niet de bron terug die je hebt bewerkt, maar de bron die uit de heen-en-terugweg naar de rijke-tekstlaag komt. Die weg is niet byte-getrouw, dus het bestand op schijf wordt geherstructureerd op plekken waar je niets hebt aangeraakt. ## Wat er gebeurt Een echte notitie van 24 KB (koppen, GFM-tabellen, genummerde lijsten, een `<!-- timeline -->`-marker) geopend in de visuele stand, **één woord** aan een kop toegevoegd, Cmd+S. Uitkomst: **68 diff-regels** tegen het origineel. Wat er veranderde zonder dat het gevraagd was: - witregels vóór en na koppen verdwijnen, en verschijnen elders; - een kop raakt los van zijn alinea — `# 1. Kern van deze notitie` belandt ná de regel die eronder stond; - de scheidingsregel van een tabel wordt herschreven: `|---|---|` wordt `| --- | --- |`; - een hele genummerde lijst van vijf punten verhuist naar een andere plek in het document; - `# <!-- timeline -->` wordt `<!-- timeline -->`; - in een herstelmomentopname van dezelfde sessie was een regel (`4. Impact`) uit de bron in het geheugen helemaal verdwenen. Dat laatste maakt dit meer dan een opmaakkwestie: er kan inhoud sneuvelen. De drift stapelt bovendien. Elke opslag schrijft de dan-geldende geherstructureerde vorm weg, en de volgende ronde begint dáár weer. In een repository met versiebeheer levert dat een diff op waarin de echte wijziging tussen tientallen spookregels verdwijnt. ## Waarom dit tegen de belofte in gaat `saveDocument` (`lib/services/file/file_service_open.dart`) zegt het met zoveel woorden: *"precies de bron, geen deck-scaffold, geen `marp:`-kop, geen normalisatie"*, byte-getrouw naar het gekozen pad. DOCUMENT_MODE.md §1.1/§3 belooft hetzelfde: de bron ís de waarheid. De mechaniek eronder weet ook dat de weg niet getrouw is. `_onQuillChanged` (`lib/widgets/markdown_editor/markdown_editor.dart`) noteert het letterlijk: *"De heen-en-terugweg naar Markdown levert niet byte-getrouw dezelfde bron op (witregels rond koppen en blokken schuiven)"*. Daar wordt het gebruikt om een cursorwissel niet als bewerking te tellen — maar zodra er wél iets verandert, gaat de volledige geherstructureerde bron alsnog naar de notifier, en dus naar schijf. Er ís een poort die hierover gaat: `markdownVisualLimitations` (`lib/utils/markdown_visual_compatibility.dart`) heet in zijn eigen doc *"Constructs that the current rich-text bridge cannot round-trip without changing the author's Markdown source"* en valt voor zulke constructies terug op de brontekst-stand. Die poort kent er drie: `rawHtml`, `escapedPunctuation`, `looseTableLine`. Witregel- en volgordedrift zit er niet in, dus die glipt er langs. En de belofte in datzelfde bestand dat een GFM-tabel *"round-trips losslessly"* klopt niet op byte-niveau — zie de scheidingsregel hierboven. ## Reproduceren 1. Neem een `.md` met koppen, een GFM-tabel met een compacte scheidingsregel (`|---|---|`) en een genummerde lijst. 2. Open hem in OciDeck (documenttabblad), stand **Visueel**. 3. Typ één woord in een kop. 4. Cmd+S. 5. `diff origineel opgeslagen`. ## Wat een oplossing moet doen Twee richtingen, en de keuze is een ontwerpbesluit: - **De weg getrouw maken** voor alles wat `markdownVisualLimitations` toelaat: wat de poort doorlaat moet ongewijzigd terugkomen, en wat dat niet haalt hoort de poort te noemen (en dus terug te vallen op de brontekst-stand). - **Alleen schrijven wat je veranderde**: de bewerking terugschrijven op de plek waar hij hoort in plaats van het hele document te vervangen. Dat raakt de brug fundamenteler, maar het maakt de vraag "is de rondgang getrouw" irrelevant voor de rest van het bestand. Wat er in elk geval bij hoort: **een eigenschapstoets over een corpus van echte documenten** — `markdownFromDocument(documentFromMarkdown(x)) == x` voor alles wat de compatibiliteitspoort toelaat, met de bestanden uit `docs/` en een handvol notities als invoer. Zonder zo'n toets is elke reparatie hier één regressie van het volgende blok verwijderd. De eerdere reparaties #1507, #1558 en #1561 waren stuk voor stuk één symptoom uit deze klasse. ## Reikwijdte Dit raakt het bestandsformaat en wat er in het `.md` terechtkomt — de bewaker hoort mee te kijken vóór de PR, niet erna. Gevonden tijdens het onderzoek naar de niet-opgeslagen-stip (#1611); dat was een andere oorzaak en is los opgelost.
brenno 2026-08-20 18:45:14 +00:00
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#1613
No description provided.