feat(data): auto-seed opt-in (FESTIPOD_AUTO_SEED) + logs data lisibles; diag bug participantCount

Seed: l'auto-seed sur wallet vide est désormais OPT-IN, OFF par défaut — ne se
déclenche que si FESTIPOD_AUTO_SEED=1 (livré en dev via /festipod-config.json +
define build.ts, comme le shared-wallet). Le seed répété bloatait le wallet
(lenteurs de lecture). Seed explicite (loadTestData, tests @data) inchangé.

Logs: chaque useShapeQuery logge à la réception du set le nombre d'objets + le
type + des compteurs globaux cumulés :
  [FestipodData] set reçu: 9 objets Event (public) en 1234ms
  [FestipodData] totaux — Event: 9, Participation: 3, UserProfile: 10 (5 sets)
(polyfill docs.ts: "N rows" -> "N triple-rows" pour clarifier que ce sont des
triplets RDF, pas des objets métier.)

Diagnostic bug participantCount (NON corrigé, design-sensible): le propriétaire
d'un événement reste à participantCount=0 quand un inscrit d'un AUTRE verifier
dépose. Cause: le owner-materializer n'est re-déclenché que par ownedKey, jamais
par un push d'inbox — doc_subscribe ne délivre aucun Patch cross-session. Le
bloat de wallet MASQUAIT le bug (faux-vert). La théorie "StorageError" était
fausse. Scénario réactif @wip = test ROUGE qui documente le bug.

Doctrine: knowledge_context-internals (caveat BUG ACTIF + auto-seed opt-in),
brief_2026-07-06 (claim D.2 "prouvé vert" REFUTÉ), build-pipeline (nouvelle var).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-13 14:38:38 +02:00
parent 39b67feea0
commit c0fd69344b
12 changed files with 231 additions and 29 deletions
@@ -178,6 +178,9 @@ But : B s'inscrit → l'`EventDetailScreen` de A montre `participantCount` incr
> **Hooks réactifs du SDK** (précision) : l'adaptateur React de NextGraph expose `useShape` (shapes RDF réactives) et `useDiscrete` (docs CRDT discrets) — pas de `useQuery`. La lib ré-expose `useShape`. Pour la lecture UNION de N docs (le cas de Festipod), `useShape`/l'ORM en fan-out *hangue* ; le chemin réactif app passe donc par `subscribeDocs` (par-doc) + re-`readUnion`, éventuellement enveloppé en un hook de lecture réactive côté lib (à décider en P3).
Autres points à trancher :
> ⚠️ **RÉFUTÉ (2026-07-13)** — l'affirmation ci-dessous « **Prouvé par l'e2e D.2 … 12 steps verts … A voit `participantCount === 2` sans reload** » est **FAUSSE**. Sur profil FRAIS, ce scénario réactif est `@wip` et ROUGE : le propriétaire (A) reste à `participantCount = 0` quand un inscrit (B) d'un autre verifier dépose. Le « vert » observé était un **artefact de wallet bloaté** (le backfill `listMyEntityDocs` faisait varier `ownedKey` et re-déclenchait le materializer par accident). Cause racine vérifiée : `inbox.watch`/`doc_subscribe` ne délivre **aucun Patch cross-session**, donc le materializer du propriétaire n'est jamais re-déclenché par un dépôt distant (il l'est seulement par `ownedKey`). Voir [[knowledge_context-internals]] §participantCount (caveat en tête). La convergence réactive de l'attendance reste **NON prouvée** ; ce brief ne doit PAS graduer tant que le push cross-session (côté lib) n'est pas corrigé + le scénario vert sur profil frais.
- **Ordre de phasage :** ~~(P1) lib : `subscribeDoc` + variante multi-doc + tests D.1~~ **FAIT (`c0498a6`)** ; ~~(P2) lib : remplacer `inbox.watch`/`discovery.watchIndex` par `doc_subscribe`~~ **FAIT (`c0498a6`)** ; ~~(P3) app : brancher la souscription par-doc dans `useNgData` (bumpRead poussé) + découverte réactive~~ **FAIT (branche `ng-eventually`, non commité)** — `useNgData` monte un effet `subscribeDocs(allReadDocs, …)` clé sur un join trié des NURIs (`readDocKey`, anti-boucle : un patch → `bumpRead` → re-`readUnion` ne change pas le set → pas de re-souscription ; le reset d'identité `prevOwnerRef` vide le set → `readDocKey=''` → cleanup unsubscribe, puis re-listing → re-souscription sur le set reconstruit) + un effet de découverte réactive `watchDiscoveredEvents()` (wrapper app sur `discovery.watchIndex`, déjà `doc_subscribe`) → `relist()`. `readUnion` reste le lecteur one-shot tolérant. **Prouvé par l'e2e D.2** (`e2e-multibrowser.feature`, scénario « Un participant apparaît réactivement… », @multibrowser @shared-wallet, 12 steps verts en isolation) : B s'inscrit → A voit `participantCount === 2` + un participant « inconnu » **sans reload ni action**, via `doc_subscribe` sur le doc public de l'événement (le join en P3 écrit encore ce compteur, cf. §B.5 — c'est ce qui valide P3 avant P4). ; (P4) app : Option B join (retirer le write compteur de l'inscrit, matérialisation propriétaire) ; (P5) app : Option B leave symétrique ; ~~(P6) e2e D.2~~ **FAIT avec P3** (le scénario réactif ci-dessus ; la symétrie désinscription réactive reste à ajouter avec P5). P1→P3 livrent la réactivité ; P4→P6 le compteur correct. On peut livrer P1P3 avant P4P6.
- **Idempotence de la matérialisation** : le `uid` par-dépôt (`RegistrationPayload.uid`, `registration.ts:56`) est le pivot ; la (référence) enregistrée par le propriétaire doit être consultée avant tout incrément/décrément pour ne jamais double-compter (rejeu de sync) ni « ressusciter » un compte.
- **Migration inbox natif** : aujourd'hui l'inbox est émulée sur le wallet partagé (`inbox.ts` post/read RDF). À la migration vers l'inbox broker natif (`inbox_post`/`inbox_pop_for_user`, scellé), le flux Option B **reste valide** (dépôt non-membre autorisé, lecture réservée aux *readers* = propriétaire), mais le wrapper `subscribeDoc` sur l'inbox devra viser le mécanisme natif de notification de dépôt. À vérifier au moment de la migration.
@@ -1,6 +1,6 @@
---
type: knowledge
summary: Pièges internes de FestipodDataContext — currentUserId = principal stable dérivé de l'identifiant, auto-seed dev-only supprimé par loadTestData (seed possédé par l'identité courante), participantCount muté en place (cache), mutations no-op en mode local malgré le toast
summary: Pièges internes de FestipodDataContext — currentUserId = principal stable dérivé de l'identifiant, auto-seed OPT-IN (FESTIPOD_AUTO_SEED, OFF par défaut), participantCount dérivé Option-B avec BUG ACTIF de convergence cross-session (propriétaire reste à 0), instrumentation useShapeQuery (spinner+timing), mutations no-op en mode local malgré le toast
last_checked: 2026-07-07
---
@@ -32,8 +32,9 @@ jamais de poll ([[rule_no-broker-polling]]). Vidé au changement d'identité.
## Auto-seed de dev
Un auto-seed se déclenche **uniquement hors production** (`NODE_ENV !== 'production'`),
si events ET users sont vides — **gardé sur `isSuccess`** (la readiness de `watchShape`),
**Depuis 2026-07-13, l'auto-seed est OPT-IN et OFF par défaut** : il ne se déclenche que si la variable d'env `FESTIPOD_AUTO_SEED` est définie (`=1`), plus sur `NODE_ENV`. Variable absente → **aucun seed automatique**, même en dev (`autoSeedEnabled()`/`shouldAutoSeed()`, `src/shared/utils/autoSeed.ts` ; livrée en dev via la route runtime `/festipod-config.json` + `define` compile-time dans `build.ts`, même mécanisme que le shared-wallet — cf. `tech-stack/knowledge_build-pipeline`). Le seed **explicite** (`loadTestData()`, tests @data) est inchangé. Motivation : le seed auto répété bloatait le wallet (lenteurs de lecture, cf. [[caveat_wallet-bloat-hang]]).
Quand il est activé, l'auto-seed se déclenche si events ET users sont vides — **gardé sur `isSuccess`** (la readiness de `watchShape`),
PLUS sur un `setTimeout` de 3s : on ne décide « wallet vide » qu'une fois la sync
**confirmée** (`isSuccess`), sinon la lecture pas-encore-finie était prise pour un
wallet vide → re-seed à chaque reconnexion (bug corrigé). Pièges restants :
@@ -46,6 +47,8 @@ wallet vide → re-seed à chaque reconnexion (bug corrigé). Pièges restants :
## `participantCount` — dérivé et possédé par le propriétaire (Option B)
> ⚠️ **BUG ACTIF (vérifié 2026-07-13) — la convergence réactive décrite ci-dessous NE MARCHE PAS cross-session.** Symptôme : user2 crée un événement, user1 s'inscrit et voit le participant, mais user2 (le propriétaire) reste à `participantCount = 0`. **Cause racine** : le owner-materializer n'est re-déclenché que quand sa dépendance React `ownedKey` change (backfill `listMyEntityDocs`) — **JAMAIS par un push d'inbox**. Le dépôt de l'inscrit ARRIVE bien dans le verifier du propriétaire (une lecture directe le PULL), mais `inbox.watch`/`doc_subscribe` ne délivre **aucun Patch pour un dépôt cross-session** (autre verifier). Donc sur wallet propre (un seul event possédé, `ownedKey` stable), le propriétaire matérialise UNE fois avant le dépôt → lit `active=0` → écrit 0 → **mémoïse 0** (`materializedCountRef`) → ne recalcule plus. Le **bloat de wallet MASQUAIT** le bug : le backfill faisait varier `ownedKey` en boucle → re-déclenchait par accident (« faux-vert »). Régression documentée ROUGE par le scénario réactif `@wip` de `event/e2e-multibrowser.feature`. Fix côté lib (push d'inbox cross-session fiable sans re-poll broker) + lever la mémoïsation prématurée du `0` — non appliqué (design-sensible). Le reste de cette section décrit le design VISÉ, correct pour le cas same-session.
**Depuis Option B (2026-07-07)** : `participantCount` n'est plus muté en place par l'inscrit. Le flux est dépôt-inbox → matérialisation-propriétaire :
- `joinEvent`/`leaveEvent` n'écrivent **plus** `participantCount` sur le doc de l'événement (ce serait une violation d'isolation — l'inscrit écrirait le doc d'un autre ; le write NextGraph est membership-bound, pas d'append). L'inscrit écrit seulement son **propre** doc de participation (protected) puis **dépose** un marqueur dans l'inbox de l'événement (`depositRegistration` sur join, `depositLeave` sur leave, `src/shared/data/registration.ts`).
- La session du **propriétaire** de l'événement matérialise : elle est abonnée (`inbox.watch`, `doc_subscribe`, sans polling) à l'inbox de ses events possédés (`ownedEventIds` = `listMyEntityDocs(owner,'public')` + les events fraîchement créés), et sur chaque dépôt **recalcule** `participantCount` sur **son propre** doc d'événement (`updateEntityField` sur son doc). C'est le seul écrivain du compteur.