docs: passer vision, readcap-and-nuri-model et l'incident en anglais
Le reste du dossier docs/ était déjà en anglais ; ces trois fichiers avaient été rédigés en français par erreur. Traduction fidèle, sans changement de fond : mêmes sections, mêmes tableaux, mêmes blocs de code. Le retour à la ligne dur à 78 colonnes est levé (une ligne par paragraphe, convention du projet). Marqueurs épistémiques préservés et rendus aussi visibles : VERIFIED / INFERRED / CORRECTED / DIRECTION / GAP. Les citations verbatim de commentaires amont restent intactes. Deux incohérences de FOND signalées par la traduction et corrigées ici — elles étaient invisibles tant qu'on lisait chaque section isolément : - readcap-and-nuri-model, section « Caveats / gaps » : elle listait encore le fetch keyless comme hypothèse INFÉRÉE à confirmer, alors que le bloc CORRIGÉ du §4bis la déclare fausse et non constructible. Contradiction interne née de ma correction partielle. Conservée barrée plutôt que supprimée : l'hypothèse est intuitive et se reformera sinon. - incident write-loss : l'intro affirmait en fait établi que « l'écriture n'atteint jamais durablement le broker », alors que la réserve épistémique plus bas dit explicitement que l'alternative (perte d'écriture vs réhydratation à froid) n'est pas tranchée. L'intro ne rapporte plus que le symptôme observé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -1,57 +1,57 @@
|
||||
# Perte d'écriture lors d'une mort de socket (`SerializationError`)
|
||||
# Write loss on socket death (`SerializationError`)
|
||||
|
||||
**Post-mortem — 2026-07-14 · Statut : OUVERT (non traité).**
|
||||
**Post-mortem — 2026-07-14 · Status: OPEN (not addressed).**
|
||||
|
||||
Une entité écrite juste avant une période d'inactivité peut être **perdue silencieusement** : l'écriture n'atteint jamais durablement le broker, et l'entité est absente à la reconnexion. Le **compte / l'identité survit** (pas de fork). Observé en conditions réelles (Festipod, Firefox) lors d'une pause après login/création.
|
||||
An entity written just before a period of inactivity can be **silently lost**: it is absent on reconnection. *(Whether the write never durably reached the broker, or reached it and is not read back on a cold reconnection, is **not settled** — see Epistemic caveat below. The wording here deliberately states only the observed symptom.)* The **account / identity survives** (no fork). Observed in real conditions (Festipod, Firefox) during a pause after login/creation.
|
||||
|
||||
## Symptôme
|
||||
## Symptom
|
||||
|
||||
1. L'utilisateur se connecte, l'app crée une entité (un événement Festipod).
|
||||
2. Une période d'inactivité suit (idle, onglet en arrière-plan…).
|
||||
3. Le socket broker meurt spontanément avec `SOCKET IS CLOSED Some(Left(SerializationError))`.
|
||||
4. À la reconnexion, l'entité créée a disparu ; l'app relit son propre scope **vide**.
|
||||
1. The user logs in, the app creates an entity (a Festipod event).
|
||||
2. A period of inactivity follows (idle, tab in the background…).
|
||||
3. The broker socket dies spontaneously with `SOCKET IS CLOSED Some(Left(SerializationError))`.
|
||||
4. On reconnection, the created entity has disappeared; the app reads back its own scope **empty**.
|
||||
|
||||
## Preuves (VÉRIFIÉ — logs Firefox en direct, verbatim)
|
||||
## Evidence (VERIFIED — live Firefox logs, verbatim)
|
||||
|
||||
```
|
||||
… REPLAY TOPIC NOT FOUND <topic> IN OVERLAY <overlay>
|
||||
… NEED REPLAY true
|
||||
… SENDING EVENTS FROM OUTBOX RETURNED: Err(TopicNotFound)
|
||||
[user1][polyfill] resolveAccount(user1) → 1 record ← le compte SURVIT (pas de fork)
|
||||
[user1][polyfill] readScopeIndex(…) → 0 entities ← mais le scope est VIDE
|
||||
[user1][polyfill] resolveAccount(user1) → 1 record ← the account SURVIVES (no fork)
|
||||
[user1][polyfill] readScopeIndex(…) → 0 entities ← but the scope is EMPTY
|
||||
… set reçu: 0 objets Event (public)
|
||||
… SOCKET IS CLOSED Some(Left(SerializationError)) [51, 3, 223, …]
|
||||
```
|
||||
|
||||
Lecture (**mécanisme plausible, non tranché**) : l'écriture a été poussée dans l'**outbox** local, mais le socket est mort avant qu'elle ne soit **flushée durablement** dans le topic broker ; à la reconnexion, le replay de l'outbox échoue (`Err(TopicNotFound)`) parce que le topic n'a **jamais été créé côté broker** → l'événement est abandonné. Le compte, lui, avait déjà été résolu durablement (`resolveAccount → 1 record`) : il n'est ni perdu ni forké.
|
||||
Interpretation (**plausible mechanism, not settled**): the write was pushed into the local **outbox**, but the socket died before it was **durably flushed** into the broker topic; on reconnection, the outbox replay fails (`Err(TopicNotFound)`) because the topic was **never created on the broker side** → the event is abandoned. The account, for its part, had already been durably resolved (`resolveAccount → 1 record`): it is neither lost nor forked.
|
||||
|
||||
> **Réserve épistémique.** Les preuves établissent le *symptôme* (perte + `Err(TopicNotFound)` + `readScopeIndex → 0`). Le *mécanisme* exact n'est pas tranché entre **(i) perte à l'écriture** (l'écriture n'atteint jamais durablement le broker) et **(ii) échec de réhydratation à froid** (l'écriture *est* sur le broker mais une session fraîche ne rouvre pas le scope propre). Le `Err(TopicNotFound)` sur le replay outbox penche pour **(i) dans ce cas Firefox**. Voir la repro @data ci-dessous, qui expose un symptôme voisin mais **ne tranche pas** (i) vs (ii).
|
||||
> **Epistemic caveat.** The evidence establishes the *symptom* (loss + `Err(TopicNotFound)` + `readScopeIndex → 0`). The exact *mechanism* is not settled between **(i) loss at write time** (the write never durably reaches the broker) and **(ii) cold-rehydration failure** (the write *is* on the broker but a fresh session does not reopen its own scope). The `Err(TopicNotFound)` on the outbox replay leans toward **(i) in this Firefox case**. See the @data repro below, which exhibits a neighboring symptom but **does not settle** (i) vs (ii).
|
||||
|
||||
## Chaîne causale (TRACÉ — lecture du core NextGraph, à re-vérifier)
|
||||
## Causal chain (TRACED — reading of the NextGraph core, to be re-verified)
|
||||
|
||||
- La `SerializationError` ferme le socket. Le core émet la déconnexion : `broker.rs` → `LocalBrokerMessage::Disconnected` → `disconnections_sender.send(...)` (≈ `broker.rs:1051`, à re-vérifier — numéro volatil, se repérer par le symbole).
|
||||
- Cette déconnexion est **poussée** aux abonnés via `disconnections_subscribe(cb)` (flux PUSH).
|
||||
- **La reconnexion NextGraph est un `// TODO` non implémenté** (≈ `broker.rs:1051-1076`) : rien ne rétablit le socket ni ne re-flushe l'outbox.
|
||||
- `user_connect` renvoie un **instantané** `{ server_id, server_ip, error, since }` au moment de l'appel — pas un flux, inutilisable pour détecter une chute ultérieure.
|
||||
- **Aucune API de confirmation de durabilité d'écriture** : un appelant ne peut pas `await` la garantie qu'une écriture a atteint le broker.
|
||||
- The `SerializationError` closes the socket. The core emits the disconnection: `broker.rs` → `LocalBrokerMessage::Disconnected` → `disconnections_sender.send(...)` (≈ `broker.rs:1051`, to be re-verified — volatile number, navigate by symbol).
|
||||
- This disconnection is **pushed** to subscribers via `disconnections_subscribe(cb)` (PUSH stream).
|
||||
- **NextGraph reconnection is an unimplemented `// TODO`** (≈ `broker.rs:1051-1076`): nothing re-establishes the socket nor re-flushes the outbox.
|
||||
- `user_connect` returns a **snapshot** `{ server_id, server_ip, error, since }` at call time — not a stream, unusable for detecting a later drop.
|
||||
- **No write-durability confirmation API**: a caller cannot `await` the guarantee that a write has reached the broker.
|
||||
|
||||
## Ce que le SDK expose mais ne consomme pas
|
||||
## What the SDK exposes but does not consume
|
||||
|
||||
`disconnections_subscribe` **se déclenche** sur cette panne — mais ni le polyfill (`@ng-eventually/client`) ni l'app consommateur ne s'y abonnent. Le signal existe, personne ne l'écoute ; côté app, aucun mécanisme ne re-tente ni n'avertit l'utilisateur.
|
||||
`disconnections_subscribe` **does fire** on this failure — but neither the polyfill (`@ng-eventually/client`) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.
|
||||
|
||||
## Portée & non-reproduit
|
||||
## Scope & not reproduced
|
||||
|
||||
- **Observé Firefox uniquement** à ce jour. Un test manuel sur un autre navigateur n'a pas déclenché la `SerializationError` ni ses conséquences.
|
||||
- **Reproduction @data (Chromium, broker réel) — 2026-07-14, décisive.** Le test de reconnexion @data existant (`reconnexion-meme-identite`) était **faux-vert** : il relisait les repos de A depuis l'**IndexedDB local** du profil persistant, jamais depuis le broker. Un lecteur **réellement à froid** (contexte non-persistant `freshBrowser`, **même** wallet/compte A, aucun état local — seedé du wallet capturé avant l'événement) lit **0** événement de A (`BARRIER timed-out (8000ms)`, `CONNECTION ESTABLISHED`). Signature **différente** du cas Firefox (pas de mort de socket ; l'`OUTBOX empty` est celui du lecteur, trivialement vide) et **ne tranche pas** (i) vs (ii) — un barrier vide est compatible avec les deux. Établi en revanche : **@data n'a jamais vérifié la durabilité broker des lectures propres de A**, et la réhydratation à froid depuis le broker échoue. Repro : `src/modules/event/features/reconnexion-froide-sans-local.feature` (Festipod).
|
||||
- **Pour trancher (i) vs (ii)** : vérifier indépendamment que l'écriture de A atteint le broker — p.ex. un lecteur *chaud* / une seconde identité lit le doc public de l'événement (le scénario d'isolation deux-identités). S'il le voit → l'écriture est durable → le 0 du lecteur à froid est un **(ii)** (réhydratation). Sinon → **(i)**.
|
||||
- **Observed on Firefox only** to date. A manual test on another browser did not trigger the `SerializationError` nor its consequences.
|
||||
- **@data reproduction (Chromium, real broker) — 2026-07-14, decisive.** The existing @data reconnection test (`reconnexion-meme-identite`) was a **false green**: it read A's repos back from the persistent profile's **local IndexedDB**, never from the broker. A **genuinely cold** reader (non-persistent `freshBrowser` context, the **same** wallet/account A, no local state — seeded from the wallet captured before the event) reads **0** events from A (`BARRIER timed-out (8000ms)`, `CONNECTION ESTABLISHED`). A **different** signature from the Firefox case (no socket death; the `OUTBOX empty` is the reader's, trivially empty) and it **does not settle** (i) vs (ii) — an empty barrier is compatible with both. Established on the other hand: **@data has never verified the broker durability of A's own reads**, and cold rehydration from the broker fails. Repro: `src/modules/event/features/reconnexion-froide-sans-local.feature` (Festipod).
|
||||
- **To settle (i) vs (ii)**: independently verify that A's write reaches the broker — e.g. a *warm* reader / a second identity reads the event's public doc (the two-identity isolation scenario). If it sees it → the write is durable → the cold reader's 0 is a **(ii)** (rehydration). Otherwise → **(i)**.
|
||||
|
||||
## Pistes de correction (non arbitré)
|
||||
## Fix leads (not arbitrated)
|
||||
|
||||
1. **Core** — corriger la `SerializationError` **et** implémenter le TODO de reconnexion (rétablir le socket + re-flusher l'outbox).
|
||||
2. **SDK / polyfill** — consommer `disconnections_subscribe` → reconnexion + re-flush outbox comme mitigation, indépendamment du core.
|
||||
3. **API de durabilité** — exposer une confirmation qu'une écriture a atteint le broker, pour que l'appelant puisse l'`await`.
|
||||
1. **Core** — fix the `SerializationError` **and** implement the reconnection TODO (re-establish the socket + re-flush the outbox).
|
||||
2. **SDK / polyfill** — consume `disconnections_subscribe` → reconnection + outbox re-flush as a mitigation, independently of the core.
|
||||
3. **Durability API** — expose a confirmation that a write has reached the broker, so that the caller can `await` it.
|
||||
|
||||
## Liens
|
||||
## Links
|
||||
|
||||
- `docs/nextgraph-current-state.md` — état courant du core (déconnexion / reconnexion à cross-référencer ici).
|
||||
- Impact produit + caveat côté consommateur : concept Festipod `data-layer` → `caveat_write-durability-across-disconnect`.
|
||||
- `docs/nextgraph-current-state.md` — current state of the core (disconnection / reconnection to be cross-referenced here).
|
||||
- Product impact + consumer-side caveat: Festipod concept `data-layer` → `caveat_write-durability-across-disconnect`.
|
||||
|
||||
Reference in New Issue
Block a user