refactor(comments): retirer les raisonnements sur l'état de NextGraph du code app

Application de la règle qu'on vient de durcir : l'app IGNORE entièrement l'état
d'implémentation de NextGraph. Le CODE était légitime — `inbox.readSynced` est
une surface SDK exportée ; ce sont les JUSTIFICATIONS qui fautaient, en
expliquant les choix par des internes du cœur.

Réécrit en termes de CONTRAT :
- registration.ts / FestipodDataContext : « barrier-gated read, le repo d'inbox
  n'est pas encore ouvert dans le verifier, un read ancré renverrait 0 » devient
  « `read` rend ce qui est connu localement maintenant, `readSynced` rend une
  fois les dépôts synchronisés visibles ; ce site a besoin du second parce qu'il
  lit depuis une session froide ».
- ngBootstrap : « le verifier sérialise les créations » devient « `docCreate` est
  un aller-retour qui ne recouvre pas le suivant, donc le coût du seed croît
  LINÉAIREMENT avec le nombre de documents ». Le ~2s mesuré est conservé, mais
  explicitement comme une observation, pas comme un contrat.
- entityWrites : description de lecture périmée (ORM fan-out, ngSet couplé au
  scope) remplacée par la vue réactive.

La distinction read/readSynced vit désormais là où elle est légitime :
knowledge_sdk-surface, avec le critère de choix (`read` dans une session qui
observe déjà l'inbox, `readSynced` dès que la justesse dépend d'une session
froide voyant le dépôt d'une autre identité). knowledge_context-internals cesse
d'expliquer le fix par `ensureRepoOpen`/premier `State` et pointe le contrat.

Laissé tel quel : `src/shared/support/hooks.ts` et les steps e2e — le harness de
test connaît légitimement la plomberie ; la règle vise l'app. Et le « no
cross-account fan-out » de FestipodDataContext, qui décrit le périmètre de l'app
et non NextGraph.

tsc : 0 erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
Sylvain Duchesne
2026-07-28 17:17:47 +02:00
parent 7459d49e83
commit 05ee576d7d
6 changed files with 26 additions and 29 deletions
@@ -1,6 +1,6 @@
---
type: knowledge
summary: Internal pitfalls of FestipodDataContext — currentUserId = a stable principal derived from the identifier, TWO id spaces joined through the normalized identifier (resolveParticipantUser / USER_PRINCIPAL_PREFIX), OPT-IN auto-seed (FESTIPOD_AUTO_SEED, OFF by default), Option-B derived participantCount (reliable at the owner's connection through a barrier-gated inbox read; single source = event.participantCount), session reset on identity change (overlay + caps), useShapeQuery instrumentation (spinner + timing) + identity-first logs, mutations that are no-ops in local mode despite the toast
summary: Internal pitfalls of FestipodDataContext — currentUserId = a stable principal derived from the identifier, TWO id spaces joined through the normalized identifier (resolveParticipantUser / USER_PRINCIPAL_PREFIX), OPT-IN auto-seed (FESTIPOD_AUTO_SEED, OFF by default), Option-B derived participantCount (reliable at the owner's connection because it reads under the synced-view contract; single source = event.participantCount), session reset on identity change (overlay + caps), useShapeQuery instrumentation (spinner + timing) + identity-first logs, mutations that are no-ops in local mode despite the toast
last_checked: 2026-07-27
---
@@ -52,7 +52,7 @@ When it is enabled, the auto-seed fires if events AND users are both empty — *
## `participantCount` — derived and owned by the owner (Option B)
> ✅ **CORRECTED (2026-07-13).** The requirement is **"reliable at the owner's NEXT CONNECTION"** (the creator processes their inbox when they connect), NOT a live real-time cross-user notification. The bug was: the owner-materializer materialized **too early** (before the participant's deposit had synced) → read `active=0` → wrote 0 → **memoized that 0** → never re-processed. Fix: (1) a **barrier-gated inbox read** — `inbox.readSynced` (= `ensureRepoOpen(doc)` waits for the first `State`, THEN `read`, like `discovery.readIndex`) instead of `inbox.read`, so an already-synced deposit IS seen on connection; (2) the materializer fires **directly on connection** (`[ready, ownedKey]`), no longer only on a push; (3) `materializedCountRef` no longer locks in a premature 0 (its sole role = loop guard: only write when the derived value changes); (4) **the single source of the NUMBER = `event.participantCount`** (the `participantCount: 1` literal in `CreateEventScreen` is removed → it starts at 0; the display no longer computes a local number). Kept GREEN (on a fresh profile) by `event/e2e-multibrowser.feature` « Le compteur converge chez le propriétaire à sa prochaine connexion » (un-`@wip`'d). No polling ([[rule_no-broker-polling]]).
> ✅ **CORRECTED (2026-07-13).** The requirement is **"reliable at the owner's NEXT CONNECTION"** (the creator processes their inbox when they connect), NOT a live real-time cross-user notification. The bug was: the owner-materializer materialized **too early** (before the participant's deposit had synced) → read `active=0` → wrote 0 → **memoized that 0** → never re-processed. Fix: (1) read under the **synced-view contract** — `inbox.readSynced` instead of `inbox.read`, so a deposit already synced by another identity IS seen from a cold session (the two differ by contract, see [[knowledge_sdk-surface]]); (2) the materializer fires **directly on connection** (`[ready, ownedKey]`), no longer only on a push; (3) `materializedCountRef` no longer locks in a premature 0 (its sole role = loop guard: only write when the derived value changes); (4) **the single source of the NUMBER = `event.participantCount`** (the `participantCount: 1` literal in `CreateEventScreen` is removed → it starts at 0; the display no longer computes a local number). Kept GREEN (on a fresh profile) by `event/e2e-multibrowser.feature` « Le compteur converge chez le propriétaire à sa prochaine connexion » (un-`@wip`'d). No polling ([[rule_no-broker-polling]]).
**Since Option B (2026-07-07)**: `participantCount` is no longer mutated in place by the participant. The flow is inbox-deposit → owner-materialization:
- `joinEvent`/`leaveEvent` **no longer** write `participantCount` on the event's doc (that would be an isolation violation — the participant writing someone else's doc; NextGraph writes are membership-bound, with no append). The participant only writes their **own** participation doc (protected), then **deposits** a marker into the event's inbox (`depositRegistration` on join, `depositLeave` on leave, `src/shared/data/registration.ts`).
@@ -47,7 +47,7 @@ May assume: the SDK owns NURI construction and placement. May not assume: that t
## Inbox — delivery to an identity
**`inbox.post(targetInbox, { payload, from?, ts? })`** deposits into a document's inbox. `from` omitted defaults to the current identity; **`from: null` is an explicit anonymous deposit**, and naming another identity is rejected as a spoof. **`inbox.read(targetInbox)`** returns every `Deposit` (`{ from, payload, ts }`) sorted by ascending `ts`. **`inbox.watch(targetInbox, onDeposits)`** fires once on the initial state and again on every change; it returns an unsubscribe. **`inbox.readSynced`** is the read that waits for the document to be current. `inbox.materialize` is an alias of `read`.
**`inbox.post(targetInbox, { payload, from?, ts? })`** deposits into a document's inbox. `from` omitted defaults to the current identity; **`from: null` is an explicit anonymous deposit**, and naming another identity is rejected as a spoof. **`inbox.read(targetInbox)`** returns every `Deposit` (`{ from, payload, ts }`) sorted by ascending `ts`. **`inbox.watch(targetInbox, onDeposits)`** fires once on the initial state and again on every change; it returns an unsubscribe. **`inbox.readSynced`** is the same read under a stronger contract: it returns once the deposits synced to that inbox are visible, where `read` returns what is known locally right now. **Choose by need, not by habit**: `read` inside a session already watching the inbox, `readSynced` whenever correctness depends on a cold session seeing another identity's deposit. `inbox.materialize` is an alias of `read`.
May assume: