fix(export): één download per webexport, en alleen melden wat we weten (#1902) #1906

Merged
brenno merged 10 commits from fix/webdownload-succesmelding-1902 into main 2026-09-01 00:16:49 +00:00
Owner

Closes #1902.

Wat er mis was

Op web is elke uitgang een browserdownload. De app las "er viel geen
uitzondering" als succes, en dat is geen bewijs: FilePicker.saveFile geeft op
web altijd null terug, ook wanneer het goed ging. Vier paden meldden
daardoor onvoorwaardelijk succes — deck-export, documentexport, .ocideck-pakket
en auditdossier.

De scherpste vorm zat bij een geredigeerde export. Die bood drie bestanden
achter elkaar aan (rapport, commitments, verificatiesleutels), en browsers
houden de tweede automatische download op rij tegen. De auteur hield een
geredigeerd rapport over — zonder iets om ook maar één redactie mee na te
trekken — en las "geëxporteerd naar". Dat is precies wat de comment in
export_service.dart wilde voorkomen.

Wat er nu gebeurt

  • Eén export, één download. Nieuw: lib/services/download_delivery.dart.
    Eén bestand gaat als zichzelf; meer dan één gaat als één ZIP, genoemd naar het
    hoofdbestand plus .zip. Dat geldt ook voor de sessie-export, die per dia een
    bestand aanbood.
  • Alleen melden wat we weten. Op web staat er aangeboden als download in
    plaats van geëxporteerd naar. De pagina kan niet zien of het bestand in de
    downloadmap aankwam — een download-anker meldt niets terug — en die grens hoort
    in de tekst te staan, niet alleen in de code.
  • Een geweigerde download is een fout. ExportFailure.downloadNotStarted
    voor de deck-export (de dienst levert de reden, de schil de zin — #576), null
    voor documentexport, pakket en dossier, met een melding in de schil. De
    sessie-export onderscheidt nu ook afbreken van mislukken: alleen het tweede
    krijgt een melding.
  • De poort ziet de nieuwe route. deliverAsDownload staat tussen de
    artefact-primitieven van check_audience_boundary. Daardoor kwam
    downloadDeckAsFile boven water: dat liep via downloadTextFile, wat geen
    primitief was, dus was er nooit gevraagd aan welke kant van de projectiegrens
    het stond. Nu geregistreerd als source.

Hoe het bewezen is

Twaalf nieuwe toetsen plus twee bij export_failure_text. De hele webtak lag
buiten bereik van de suite (kIsWeb is op de VM altijd false); twee
@visibleForTesting-haken maken hem meetbaar — de download-sink en de
platformtak.

Rood geproefd door de afleverlaag terug te zetten op het oude gedrag (elk bestand
apart, succes ongeacht de uitkomst): vier toetsen vallen om, op precies de twee
claims uit het issue — drie downloads in plaats van één, en een export die succes
meldt terwijl er niets vertrok.

Meegekomen

make check viel op drie plafonds; alle drie opgelost door te splitsen, niet
door de basislijn te verhogen:

  • ExportFormat/ExportResult naar lib/services/export_result.dart (data, geen
    dienst) — export_service.dart 515 → 450 regels, plafond mee omlaag naar 485.
  • De webaflevering uit ExportService.export naar een top-level functie —
    131 → 116 regels.
  • _flatMembers en de twee bestandsnaam-helpers uit FileService naar
    top-level; de naamexpressie stond op drie plekken letterlijk uitgeschreven.

deliversByDownload telt mee als platformpoort in check_conventions (de
productiewaarde ís kIsWeb; alleen een test kan hem omzetten).

Documentatie

USER_GUIDE zei dat de twee manifestbestanden op web "in dezelfde downloadmap"
belanden — precies wat er niet gebeurde. Bijgewerkt in USER_GUIDE, FILE_FORMAT en
hun nl-varianten, met de reden erbij. Drie nieuwe l10n-strings, 31 talen.

Twee dingen die het issue niet noemde

  • Stijlprofiel-export en het RFC 3161-tijdstempelverzoek deden hetzelfde.
    Die tweede is de vervelendste: hij bewaarde de nonce ná de export, en de
    comment daar zegt met zoveel woorden dat dat pas mag als het verzoek de deur
    uit is — anders blijft er een nonce achter voor een verzoek dat nooit
    verstuurd is. Na deze PR staat er in lib/ geen enkele
    FilePicker.saveFile-aanroep meer.
  • Onder wasm koos de downloadlaag stilzwijgend de lege stub.
    if (dart.library.html) is onwaar voor dart2wasm. Nagemeten in de glue-module
    van een echte --wasm-build: met dart.library.html staat .download er
    0× in en revokeObjectURL 1×, met dart.library.js_interop 1× en 2×. De
    ankercode was daar dus werkelijk weg. Wasm is nog geen bouwdoel (#1734); dit
    is precies de val die dat later stil zou maken.

Bewaker

Dit raakt een publieke belofte (de bewoording in de interface en twee
documenten) en de vorm waarin het werk van de gebruiker aankomt, dus de
bewaker heeft ernaar gekeken.

  • Meenemen naar ander gereedschap? Ja. Een ZIP is een open container die elk
    besturingssysteem uitpakt; de leden erin zijn onveranderd (.pdf,
    -redactie.json, .md). Geen OciDeck-specifieke omhulling, geen nieuwe
    partij om te vertrouwen — archive zat er al in en het pakken gebeurt in de
    tab zelf.
  • Het .md? Onaangeraakt. Dit is uitsluitend de afleverweg.
  • Als OciDeck stopt? De gedownloade ZIP blijft met elk uitpakprogramma te
    openen.

De botsing, hardop: gemak tegen integriteit. Eén los .pdf is prettiger dan
een ZIP die je moet uitpakken. Daarom gaat één bestand nog steeds als zichzelf —
alleen bij meer dan één wordt het een ZIP. Daar wint integriteit: de vorige vorm
liet het redactiemanifest stilzwijgend achter, en dan is een geredigeerd rapport
niet meer na te trekken. Veiligheid staat op 1. Ik zou van gedachten
veranderen
zodra browsers meerdere bestanden kunnen afleveren mét een
betrouwbare uitslag per bestand; dan is los beter dan een ZIP.

De bewoording is waarde 4: "geëxporteerd naar" beweerde meer dan de pagina kan
weten. De nieuwe zin is zwakker en waar.

Niet meegenomen

Dat de multi-download-poort van de browser dit werkelijk triggert is niet in
een echte browser nagemeten
; de repro-stappen staan in #1902. De code-analyse
staat wel vast, en de reparatie is hoe dan ook de goede kant op: één download kan
die poort niet raken.

🤖 Generated with Claude Code

Closes #1902. ## Wat er mis was Op web is elke uitgang een browserdownload. De app las "er viel geen uitzondering" als succes, en dat is geen bewijs: `FilePicker.saveFile` geeft op web **altijd `null`** terug, ook wanneer het goed ging. Vier paden meldden daardoor onvoorwaardelijk succes — deck-export, documentexport, `.ocideck`-pakket en auditdossier. De scherpste vorm zat bij een geredigeerde export. Die bood drie bestanden achter elkaar aan (rapport, commitments, verificatiesleutels), en browsers houden de tweede automatische download op rij tegen. De auteur hield een geredigeerd rapport over — zonder iets om ook maar één redactie mee na te trekken — en las "geëxporteerd naar". Dat is precies wat de comment in `export_service.dart` wilde voorkomen. ## Wat er nu gebeurt - **Eén export, één download.** Nieuw: `lib/services/download_delivery.dart`. Eén bestand gaat als zichzelf; meer dan één gaat als één ZIP, genoemd naar het hoofdbestand plus `.zip`. Dat geldt ook voor de sessie-export, die per dia een bestand aanbood. - **Alleen melden wat we weten.** Op web staat er *aangeboden als download* in plaats van *geëxporteerd naar*. De pagina kan niet zien of het bestand in de downloadmap aankwam — een download-anker meldt niets terug — en die grens hoort in de tekst te staan, niet alleen in de code. - **Een geweigerde download is een fout.** `ExportFailure.downloadNotStarted` voor de deck-export (de dienst levert de reden, de schil de zin — #576), `null` voor documentexport, pakket en dossier, met een melding in de schil. De sessie-export onderscheidt nu ook afbreken van mislukken: alleen het tweede krijgt een melding. - **De poort ziet de nieuwe route.** `deliverAsDownload` staat tussen de artefact-primitieven van `check_audience_boundary`. Daardoor kwam `downloadDeckAsFile` boven water: dat liep via `downloadTextFile`, wat geen primitief was, dus was er nooit gevraagd aan welke kant van de projectiegrens het stond. Nu geregistreerd als `source`. ## Hoe het bewezen is Twaalf nieuwe toetsen plus twee bij `export_failure_text`. De hele webtak lag buiten bereik van de suite (`kIsWeb` is op de VM altijd `false`); twee `@visibleForTesting`-haken maken hem meetbaar — de download-sink en de platformtak. Rood geproefd door de afleverlaag terug te zetten op het oude gedrag (elk bestand apart, succes ongeacht de uitkomst): vier toetsen vallen om, op precies de twee claims uit het issue — drie downloads in plaats van één, en een export die succes meldt terwijl er niets vertrok. ## Meegekomen `make check` viel op drie plafonds; alle drie opgelost door te splitsen, niet door de basislijn te verhogen: - `ExportFormat`/`ExportResult` naar `lib/services/export_result.dart` (data, geen dienst) — `export_service.dart` 515 → 450 regels, plafond mee omlaag naar 485. - De webaflevering uit `ExportService.export` naar een top-level functie — 131 → 116 regels. - `_flatMembers` en de twee bestandsnaam-helpers uit `FileService` naar top-level; de naamexpressie stond op drie plekken letterlijk uitgeschreven. `deliversByDownload` telt mee als platformpoort in `check_conventions` (de productiewaarde ís `kIsWeb`; alleen een test kan hem omzetten). ## Documentatie USER_GUIDE zei dat de twee manifestbestanden op web "in dezelfde downloadmap" belanden — precies wat er niet gebeurde. Bijgewerkt in USER_GUIDE, FILE_FORMAT en hun nl-varianten, met de reden erbij. Drie nieuwe l10n-strings, 31 talen. ## Twee dingen die het issue niet noemde - **Stijlprofiel-export en het RFC 3161-tijdstempelverzoek** deden hetzelfde. Die tweede is de vervelendste: hij bewaarde de nonce ná de export, en de comment daar zegt met zoveel woorden dat dat pas mag als het verzoek de deur uit is — anders blijft er een nonce achter voor een verzoek dat nooit verstuurd is. Na deze PR staat er in `lib/` geen enkele `FilePicker.saveFile`-aanroep meer. - **Onder wasm koos de downloadlaag stilzwijgend de lege stub.** `if (dart.library.html)` is onwaar voor dart2wasm. Nagemeten in de glue-module van een echte `--wasm`-build: met `dart.library.html` staat `.download` er 0× in en `revokeObjectURL` 1×, met `dart.library.js_interop` 1× en 2×. De ankercode was daar dus werkelijk weg. Wasm is nog geen bouwdoel (#1734); dit is precies de val die dat later stil zou maken. ## Bewaker Dit raakt een **publieke belofte** (de bewoording in de interface en twee documenten) en de **vorm waarin het werk van de gebruiker aankomt**, dus de bewaker heeft ernaar gekeken. - *Meenemen naar ander gereedschap?* Ja. Een ZIP is een open container die elk besturingssysteem uitpakt; de leden erin zijn onveranderd (`.pdf`, `-redactie.json`, `.md`). Geen OciDeck-specifieke omhulling, geen nieuwe partij om te vertrouwen — `archive` zat er al in en het pakken gebeurt in de tab zelf. - *Het `.md`?* Onaangeraakt. Dit is uitsluitend de afleverweg. - *Als OciDeck stopt?* De gedownloade ZIP blijft met elk uitpakprogramma te openen. **De botsing, hardop:** gemak tegen integriteit. Eén los `.pdf` is prettiger dan een ZIP die je moet uitpakken. Daarom gaat één bestand nog steeds als zichzelf — alleen bij meer dan één wordt het een ZIP. Daar wint integriteit: de vorige vorm liet het redactiemanifest stilzwijgend achter, en dan is een geredigeerd rapport niet meer na te trekken. Veiligheid staat op 1. **Ik zou van gedachten veranderen** zodra browsers meerdere bestanden kunnen afleveren mét een betrouwbare uitslag per bestand; dan is los beter dan een ZIP. De bewoording is waarde 4: "geëxporteerd naar" beweerde meer dan de pagina kan weten. De nieuwe zin is zwakker en waar. ## Niet meegenomen Dat de multi-download-poort van de browser dit werkelijk triggert is **niet in een echte browser nagemeten**; de repro-stappen staan in #1902. De code-analyse staat wel vast, en de reparatie is hoe dan ook de goede kant op: één download kan die poort niet raken. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
brenno force-pushed fix/webdownload-succesmelding-1902 from dfe4e8530b
All checks were successful
scans / scans (pull_request) Successful in 6m18s
static-gate / static-gate (pull_request) Successful in 10m9s
to 330842ca35
All checks were successful
scans / scans (pull_request) Successful in 6m20s
web-gate / web-gate (pull_request) Successful in 7m53s
static-gate / static-gate (pull_request) Successful in 9m31s
2026-09-01 00:06:27 +00:00
Compare
brenno merged commit 1948e97c2a into main 2026-09-01 00:16:49 +00:00
Sign in to join this conversation.
No description provided.