doctrine: Festipod treats @ng-eventually/client as a finished NextGraph SDK

Enforce the project boundary: Festipod is written as if NextGraph were a mature,
finished SDK; @ng-eventually/client IS that SDK. NO current-NextGraph-state,
simulation, polyfill, shim, mono-store, store-id or broker-internal knowledge
remains in this repo — it now lives in the @ng-eventually/client repo.

- Dissolved the `nextgraph-platform` concept entirely (12 leaves — all
  current-state/simulation, now in the lib's docs/). Rescued the genuine domain
  parts into functional-domain/knowledge_data-scopes-and-discovery.md (which
  entity → which scope; product-level discovery/notification intent), framed as
  SDK usage with no mechanism.
- data-layer re-anchored to "how Festipod persists via the SDK": stripped
  mono-store/private_store_id/RepoNotFound/DataCloneError/FESTIPOD_MULTISTORE.
  Deleted the current-SDK compensation leaves (private-store-scope, multistore,
  the 2026-03-17 ADRs, conditional-ng-init). Kept/reworded the domain + app
  leaves; caveat_participation-deletion reduced to the domain contract.
- app-security reworded (isolation delegated to the SDK; app trusts it).
- AGENTS.md: dropped the nextgraph-platform row, reworded data-layer/
  functional-domain/app-security, added the "Frontière SDK NextGraph" note.
- Fixed dangling [[links]]; concept lint clean (43 leaves).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-03 23:23:23 +02:00
parent aabb2b77f7
commit db9eb1cf47
37 changed files with 162 additions and 1309 deletions
+14 -21
View File
@@ -1,35 +1,28 @@
---
type: _overview
summary: Couche données NextGraph telle qu'utilisée AUJOURD'HUI (mono-store) — stack ORM/SHEX, modes connected/demo, entités, seed, et 3 règles d'écriture critiques
summary: Comment Festipod persiste ses données via le SDK @ng-eventually/client — entités stockées comme documents par scope, stack ORM/SHEX, modes connected/demo, seed
triggers:
keywords: [nextgraph, useShape, ORM, SHEX, shape, store, private_store, "@graph", NURI, sparql, sparql_update, seed, wallet, RepoNotFound, FestipodData, ngGraph, bootstrap, multistore, document, isolation]
keywords: [nextgraph, "@ng-eventually", useShape, ORM, SHEX, shape, scope, "@graph", NURI, sparql, seed, wallet, FestipodData, ngSession, ngGraph, bootstrap, document, entité]
paths: ["src/shared/shapes/**", "src/shared/hooks/useShape*", "src/shared/context/NextGraphContext.tsx", "src/shared/context/FestipodDataContext.tsx", "src/shared/utils/ng*", "src/shared/data/seedData.ts"]
---
# Data layer
Comment Festipod **persiste ses données aujourd'hui** via NextGraph (P2P, local-first, chiffré). État actuel : **mono-document** — par défaut tout atterrit dans **un seul document**, le repo racine du `private_store` partagé (`@graph = did:ng:${private_store_id}`). ⚠️ « mono-store » est un raccourci trompeur : l'axe qui compte est le **document (repo/`@graph`)**, pas le store — voir `caveat_multistore-is-multi-document`.
Comment Festipod **persiste ses données** via NextGraph (P2P, local-first, chiffré de bout en bout). Le SDK de données est **`@ng-eventually/client`** : on le traite comme un SDK NextGraph fini — chaque entité est un **document** placé dans le store de son **scope** (public / protected / private), lu et écrit via l'ORM réactif. Le mapping *quelle entité → quel scope* est un fait **produit** (concept `functional-domain`, [[knowledge_data-scopes-and-discovery]]) ; ce concept décrit la **mécanique de persistance**.
> Distinction importante : ce concept décrit le **code actuel**. Le modèle *cible* (multi-store, multi-user, autorisations) est de la doctrine **prospective** qui vit dans le concept `nextgraph-platform` (briefs). NextGraph comme **système externe** (stores, permissions, inbox, SDK) y est aussi documenté.
**À lire avant de toucher aux écritures :** les 3 règles ci-dessous — chacune corrige un bug réel (`RepoNotFound`, suppression non persistée, redirect intempestif).
## Règles d'écriture (chacune adossée à une décision)
- [[rule_private-store-scope]] ← [[decision_2026-03-17_private-store-nuri-scope]]
- [[rule_conditional-ng-init]] ← [[decision_2026-03-13_conditional-ng-init-broker-detection]]
## Pièges (lire avant de toucher au contexte / aux suppressions / aux champs d'event)
- [[knowledge_context-internals]] — currentUser `@mariedupont`, auto-seed dev, `participantCount` cache, IRI vide, no-op local
- [[caveat_participation-deletion]] — `leaveEvent` via `ngSet.delete()` (décision SPARQL annulée), persistance possiblement partielle
- [[caveat_event-fields-not-persisted]] — `startTime`/`themes`… perdus en connecté (SHEX incomplet)
> **Frontière SDK.** Le SDK de données de Festipod est `@ng-eventually/client` — initialisé/injecté **une seule fois** via `ngSession.configure(...)`. On l'écrit comme un SDK NextGraph **fini** : ne jamais documenter ici l'état courant de NextGraph (contraintes, contournements, internes broker) — cela vit dans le repo `@ng-eventually/client`. Voir [[knowledge_nextgraph-stack]].
## Modèle & données
- [[knowledge_nextgraph-stack]] — paquets `@ng-org/*`, SHEX, ORM, `build:orm`
- [[knowledge_data-modes]] — connected vs disconnected/demo, providers selon le statut NG
- [[knowledge_entities]] — types `Fp*` et shapes
- [[knowledge_nextgraph-stack]] — SDK `@ng-eventually/client`, shapes SHEX, ORM réactif, `build:orm`, injection via `ngSession`
- [[knowledge_data-modes]] — connected (SDK) vs disconnected/demo (état local seedé), choix du provider
- [[knowledge_entities]] — types `Fp*` et leurs shapes SHEX
- [[knowledge_seed-data]] — données de seed, `CURRENT_USER_ID`
- [[knowledge_context-internals]] — pièges de `FestipodDataContext` (currentUser, auto-seed dev, `participantCount` cache, no-op local)
> Sécurité/confidentialité (mono-store, confiance broker) : concept `app-security`.
## Pièges (lire avant de toucher aux suppressions / aux champs d'event)
- [[caveat_participation-deletion]] — la désinscription doit être **autoritative** et ne pas réapparaître
- [[caveat_event-fields-not-persisted]] — `startTime`/`themes`… non couverts par la shape Event → perdus en connecté
> Confidentialité (isolation par scope, confiance dans le SDK) : concept `app-security`. Périmètres produit par entité + découverte : concept `functional-domain`.
@@ -10,8 +10,8 @@ Le type app `FpEventData` (`src/shared/data/types.ts`) et le seed (`seedData.ts`
## Conséquence
En **mode connected** (NextGraph), le mapping (`mapEvent` dans `FestipodDataContext.tsx`) ne lit/écrit que les champs de la shape. Les champs hors-shape sont **silencieusement perdus** : remplis par des defaults ou vides. Or des écrans **les affichent** (ex. `startTime`/`endTime` dans `EventDetailScreen`) — donc en mode démo (seed local) ils apparaissent, mais en connecté ils disparaissent. Décalage observable seulement à l'usage.
En **mode connected** (SDK), le mapping (`mapEvent` dans `FestipodDataContext.tsx`) ne lit/écrit que les champs de la shape. Les champs hors-shape sont **silencieusement perdus** : remplis par des defaults ou vides. Or des écrans **les affichent** (ex. `startTime`/`endTime` dans `EventDetailScreen`) — donc en mode démo (seed local) ils apparaissent, mais en connecté ils disparaissent. Décalage observable seulement à l'usage.
## Pour corriger (si on veut les persister)
Ajouter les champs à `festipodShapes.shex` puis `bun run build:orm`, et étendre `mapEvent`. C'est aussi un prérequis de la modélisation complète du point de rencontre (cf. concept `nextgraph-platform`, [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 3). Tant que ce n'est pas fait, **ne pas se fier aux champs date/heure/thèmes en mode connecté**.
Ajouter les champs à `festipodShapes.shex` puis `bun run build:orm`, et étendre `mapEvent`. Tant que ce n'est pas fait, **ne pas se fier aux champs date/heure/thèmes en mode connecté**.
@@ -1,58 +0,0 @@
---
type: caveat
summary: DEUX AXES à ne pas confondre — (A) quel STORE natif (private/protected/public) ; (B) combien de DOCUMENTS dans un store. Depuis T02.h : le chemin par défaut écrit les entités PARTAGEABLES dans le vrai store PROTECTED (axe A étape 1 faite) ; le private n'ancre plus que le shim/inbox + settings. Le flag FESTIPOD_MULTISTORE ne bascule QUE l'axe B (multi-document), et ces documents multi-scope vivent dans le store du wallet partagé — public/protected/private y sont des ÉTIQUETTES LOGIQUES du shim, pas des stores. L'isolation (ReadCap) est PAR-DOCUMENT. Cible (brief_2026-05-17) : vraiment utiliser les 3 stores natifs par périmètre.
last_checked: 2026-07-03
---
# Caveat : store ≠ document — et « MULTISTORE » n'est PAS multi-store
Confusion récurrente. Deux axes **orthogonaux** que la terminologie a fusionnés :
- **Axe A — quel STORE natif ?** Un wallet a d'office 3 stores : `private_store_id`,
`protected_store_id`, `public_store_id` (cf. [[knowledge_stores-permissions]]). C'est
l'origine historique de « mono-store / multi-store » (utiliser 1 store vs les 3).
- **Axe B — combien de DOCUMENTS dans un store ?** Un store contient des documents ;
**le document (= repo = `@graph`) est la frontière de partage et de droits** ; on y stocke
des objets (dans le graphe). La ReadCap — donc l'**isolation** — est **PAR-DOCUMENT**.
## État réel du code (vérifié 2026-07-03)
1. **Depuis T02.h, le chemin par défaut écrit les entités partageables dans le vrai store
`protected`** (`@graph = did:ng:${protected_store_id}`, cf. [[rule_private-store-scope]]) —
**axe A étape 1 faite** : le protected s'ouvre pour ORM+SPARQL sans `RepoNotFound`
(vérifié). Les trois `*_store_id` sont résolus en session (`NextGraphContext`) ; le
**private** n'est plus la cible des entités domaine — il n'ancre que le shim/inbox
(cf. `nextgraph-platform`) et les settings privés. `public_store_id` reste non écrit en
tant que store natif (le scope « public » des entités reste une étiquette logique, cf.
point 2). Chemin par défaut mono-document → ReadCap tout-ou-rien sur ce document.
2. **`FESTIPOD_MULTISTORE` ne bascule QUE l'axe B**, et son nom est trompeur. ON :
- événements → **un `doc_create` par entité** (`createEntityDoc`), NURI indexé dans le
document-index « public » du compte ;
- participations/profils → **groupés** dans le document-index « protected » du compte ;
- lecture → **fan-out** sur les documents de tous les comptes par scope.
MAIS dans la lib `store-registry`, chaque `doc_create` passe `store=undefined`
**tous ces documents vivent physiquement dans le store `private`** du wallet partagé.
Le triplet `public|protected|private` y est une **ÉTIQUETTE LOGIQUE** trackée en RDF par
le shim, **pas** un store NextGraph. Donc « MULTISTORE » = en réalité **multi-DOCUMENT à
étiquettes de scope logiques**, jamais multi-store.
## Conséquences
- « Plus d'isolation » = **plus de documents** (axe B), pas plus de stores.
- Rendre l'isolation ReadCap **active** exige : chemin multi-document **+** câbler
`setCurrentUser` au login (aujourd'hui appelé seulement dans le harness → filtre dormant).
- **L'axe A (3 stores natifs) est désormais AMORCÉ mais pas complet.** Cible retenue
(2026-07-03, cf. [[brief_2026-05-17_multi-store-refactor]]) : utiliser les **3 stores par
périmètre** (public→événements/PdR, protected→profil réseau/participations,
private→settings). **Étape immédiate faite (T02.h)** : les entités partageables sont écrites
dans le **vrai store `protected`** (`did:ng:${protected_store_id}`) — représentatif du futur
wallet per-user — après vérification qu'il s'ouvre sans `RepoNotFound` (le private avait été
choisi précisément parce qu'il s'ouvrait, cf. [[decision_2026-03-17_private-store-nuri-scope]],
insight toujours valide pour les deux stores). Restent non exercés : `public_store_id` comme
store natif, et l'usage des 3 stores par périmètre distinct.
**Vérifier** : `ensureGraphNuri`/`resolveWriteGraph` (choix du `@graph` = protected),
`grep FESTIPOD_MULTISTORE` (le flag axe B), `createEntityDoc`, lib `store-registry.ts`
(`docCreate(..., undefined)` = store du wallet partagé), `protected_store_id` (écrit),
`public_store_id` (résolu mais non écrit comme store natif).
@@ -1,40 +1,15 @@
---
type: caveat
summary: RÉSOLU (T02.c, 2026-07-03) — leaveEvent supprime désormais la Participation via SPARQL DELETE-WHERE (docs.sparqlUpdate, le ng injecté), puis reflète en réactif. L'item ne ressuscite plus via la sync broker ; le scénario e2e « Se désinscrire » et un @data « désinscription persistante » passent, @wip levé. Historique du bug ngSet.delete() conservé ci-dessous.
summary: La désinscription à un point de rencontre doit être AUTORITATIVE — une fois la Participation supprimée, elle ne doit plus réapparaître ; vérifier après un vrai rafraîchissement que l'inscription a bien disparu côté données
last_checked: 2026-07-03
---
# Caveat : suppression de Participation (RÉSOLU en T02.c)
# Caveat : la désinscription doit être autoritative
**État actuel du code** (`src/shared/context/FestipodDataContext.tsx`, `leaveEvent` en mode NG, depuis T02.c 2026-07-03) : la suppression d'une `Participation` se fait via **SPARQL DELETE-WHERE** (`docs.sparqlUpdate` = le `ng` injecté réel, helper `deleteParticipation` dans `src/shared/data/registration.ts`) qui supprime le sujet Participation côté données ; on reflète ensuite le résultat dans l'état réactif (`participationsShape.ngSet.delete`) pour le rendu immédiat. Le DELETE-WHERE est **autoritatif** : l'item ne ressuscite plus après re-sync. **Ne pas** revenir à `ngSet.delete()` seul comme mécanisme de persistance (l'ancien bug ci-dessous).
Contrat métier : quand un utilisateur **se désinscrit** d'un point de rencontre (`leaveEvent` dans `src/shared/context/FestipodDataContext.tsx`), la `Participation` doit être **supprimée durablement**. Elle ne doit **pas ressusciter** après une resynchronisation.
Preuve : `cycle-de-vie-evenement.feature` (@e2e, @wip levé) + `inscription-inbox.feature` (@data « désinscription persistante »). Validation multi-navigateur complète = T02.f.
## Le piège
## Histoire du bug (avant T02.c)
Refléter la suppression uniquement dans l'état réactif de l'UI ne suffit pas : l'inscription peut réapparaître si la suppression n'est pas **persistée** côté données. La désinscription doit donc être **autoritative** au niveau du document, pas seulement au niveau de l'affichage.
Auparavant la suppression se faisait via **`participationsShape.ngSet.delete(ngPart)`** seul, ce qui NE se reflétait PAS durablement — l'item ressuscitait via la sync broker.
## Constat e2e (2026-06-30) — la désinscription ne se reflète PAS dans l'UI
Vérifié en `@e2e` contre le vrai broker (scénario auto-suffisant : s'inscrire puis se désinscrire dans la même session) :
- **L'inscription se reflète** (clic « J'y serai » → bouton « ✓ Je participe »).
- **La désinscription NON** : après le clic « Je participe », le bouton **reste** « ✓ Je participe » même après >10 s d'attente — `isParticipating` reste vrai.
Ce **n'est pas** un défaut de réactivité du set : `DeepSignalSet.delete()` appelle bien `touchIterable(meta, target)` quand l'item existait (`@ng-org/alien-deepsignals/dist/deepSignal.js`, bras `delete`), donc le composant **re-render**. Le problème est en aval : la suppression **ne se propage pas durablement** / **l'item ressuscite via la sync broker** (le bug CRDT historique ci-dessous). En `@data` la mutation peut sembler passer, mais le parcours `@e2e` réel montre que l'utilisateur reste inscrit.
→ Le scénario `@e2e` « Se désinscrire d'un événement » (`src/modules/event/features/cycle-de-vie-evenement.feature`) est **`@wip`**, et le profil cucumber par défaut **exclut `@wip`** (`cucumber.json: "tags": "not @wip"`) — la suite reste verte sans masquer un faux succès. Le retirer du `@wip` quand la désinscription sera fiable.
## Histoire (important)
Une décision antérieure ([[decision_2026-03-17_sparql-delete-for-orm-objects]], **annulée le 2026-06-15**) imposait SPARQL DELETE car `ngSet.delete()` ne persistait pas (l'objet réapparaissait au refresh). Ce **bug du `@ng-org/orm` a depuis été en grande partie corrigé** : `ngSet.delete()` est redevenu le chemin utilisé.
## Le piège (pourquoi un caveat et pas une règle)
La correction **semble partielle** : selon les cas, la suppression via `ngSet.delete()` peut ne **pas se propager complètement** au broker. Donc :
- **Ne pas tenir pour acquis** que `leaveEvent` persiste à coup sûr — **vérifier après un vrai refresh** que la participation a bien disparu côté wallet.
- Si une suppression se révèle non persistée, le repli connu reste `ng.sparql_update()` avec `DELETE WHERE { GRAPH <…> { <…> ?p ?o } }` (le mécanisme décrit dans la décision annulée). **Ne pas combiner** les deux (conflit CRDT — c'était l'autre enseignement de la décision).
- Re-tester ce point à chaque montée de version de `@ng-org/orm`.
> À valider : ouvrir `FestipodDataContext.tsx` → `leaveEvent` (mode NG, `console.log('Deleting participation via ngSet.delete()')`). Si le code est repassé à `sparql_update`, mettre ce caveat à jour ou le promouvoir en règle.
**À vérifier après toute évolution de `leaveEvent`** : s'inscrire puis se désinscrire, faire un **vrai rafraîchissement**, et confirmer que la participation a bien disparu (le bouton ne doit pas rester « ✓ Je participe »). Couvert par le scénario `@e2e` « Se désinscrire d'un événement » (`src/modules/event/features/cycle-de-vie-evenement.feature`) et un `@data` « désinscription persistante » (`inscription-inbox.feature`).
@@ -1,35 +0,0 @@
---
type: decision
summary: Décision 2026-03-13 — auto-init NextGraph seulement quand dans l'iframe broker (window.self !== window.top), sinon initNgWeb() redirige la page et casse le dev/démo standalone
---
# Conditional NextGraph Init Based on Broker Iframe Detection
**Date:** 2026-03-13 14:00
**Status:** Accepted
## Context
`initNgWeb()` de `@ng-org/web` teste `window.self === window.top`. En standalone (hors iframe), il redirige toute la page vers `nextgraph.net/redir/` pour déclencher l'auth broker. Résultat : l'app redirigeait à chaque chargement — même en dev ou quand l'utilisateur n'avait pas cliqué « Se connecter ».
## Options Considered
### Option A: toujours auto-init NG au mount
- Plus simple (pas de branchement).
- **Contre** : redirect immédiat vers le broker en standalone ; casse le workflow de dev ; l'utilisateur voit la page de login broker au lieu de l'app.
### Option B: auto-init conditionnel selon détection iframe
- En iframe, le broker a déjà authentifié → auto-init sûr ; en standalone, l'utilisateur doit cliquer « Se connecter » ; préserve l'expérience démo/dev ; calque la propre logique de détection de `@ng-org/web`.
- **Contre** : repose sur l'heuristique `window.self !== window.top` (théoriquement faillible si embarqué dans une iframe non-broker).
## Decision
**Option B.** `NextGraphContext` calcule `isInsideBroker = typeof window !== 'undefined' && window.self !== window.top` au niveau module. `useEffect` n'auto-appelle `initNg()` que si `isInsideBroker`. Le callback `connect()` reste disponible pour la connexion explicite. De plus, `FestipodDataContext` rend des données vides (pas le seed) pendant `connecting` pour éviter de flasher le contenu démo.
## Consequences
**Positif :** l'app charge sans rediriger (standalone dev/démo) ; en iframe broker, connexion fluide et automatique ; pas de flash de seed pendant la connexion.
**Négatif :** aucun significatif.
**Risque :** si `@ng-org/web` change sa logique de détection, notre garde peut diverger — les garder alignés.
> Règle dérivée : [[rule_conditional-ng-init]].
@@ -1,41 +0,0 @@
---
type: decision
summary: Décision 2026-03-17 — utiliser private_store_id comme scope useShape ET @graph (calqué sur expense-tracker-rdf) pour que orm_start_graph ouvre le repo et que les écritures ne lèvent plus RepoNotFound
---
# Use private_store_id as useShape scope and @graph
**Date:** 2026-03-17 16:00
**Status:** Accepted
> **Superseded (partiel, 2026-07-03, T02.h).** Le scope private-store-only est **remplacé pour les entités domaine partageables** (events/profils/participations) : elles sont désormais scopées ET écrites sur le **protected store** (`did:ng:${protected_store_id}`), vérifié ouvrable sans `RepoNotFound` — cf. [[rule_private-store-scope]] et [[caveat_multistore-is-multi-document]]. **L'insight central de cet ADR reste vrai** : il faut ouvrir le repo via le NURI du store (`orm_start_graph`) sinon `RepoNotFound` — ceci s'applique désormais aux **DEUX** stores. Le corps ci-dessous est conservé tel quel (mémoire d'arbitrage).
## Context
Cliquer « Charger données de test » chargeait les données en mémoire (signaux ORM) mais produisait des `RepoNotFound` sur `doc_create` et `orm_frontend_update`. Les données disparaissaient au reload car les écritures SPARQL n'atteignaient jamais le broker. La HashMap `self.repos` du verifier ne contenait pas le repo du private store → `resolve_target()` échouait.
## Options Considered
### Option A: `did:ng:i` scope + `doc_create` pour @graph
- `did:ng:i` bien documenté comme scope d'abonnement, `doc_create` renvoie un vrai NURI.
- **Contre** : `did:ng:i` passe par `NuriTargetV0::UserSite` qui n'ouvre pas les repos individuels ; `doc_create` appelle `resolve_target(PrivateStore)` qui exige le repo dans `self.repos` → échoue ; exige une logique de retry/timing complexe.
### Option B: `private_store_id` comme scope ET @graph
- Calque exact de l'exemple `expense-tracker-rdf` qui fonctionne ; `orm_start_graph` avec le NURI du private store ouvre le repo dans `self.repos` ; les écritures `orm_frontend_update` trouvent ensuite le repo. Simple, sans retry.
- **Contre** : un peu moins flexible que `did:ng:i` (scopé à un store) ; exige de passer la session à `useShapeWithDefaults`.
### Option C: `did:ng:i` scope + réutiliser le @graph d'une entité existante
- Marche pour les users qui ont déjà des données.
- **Contre** : échoue pour les wallets vides (aucune entité à réutiliser) ; retombe sur `doc_create` et le même `RepoNotFound`.
## Decision
**Option B** : `did:ng:${session.private_store_id}` comme scope `useShape` ET `@graph` d'écriture, exactement comme `expense-tracker-rdf`. `useShapeWithDefaults` accepte un `storeNuri` ; `FestipodDataContext.useNgData()` récupère la session via `useNextGraph()` et passe le NURI du private store. `ensureGraphNuri()` simplifié : entités existantes d'abord (optimisation), sinon fallback `private_store`.
## Consequences
**Positif :** écritures immédiates après connexion (sans retry) ; persistance au reload ; aligné sur les exemples officiels ; les 7 scénarios e2e passent (dont la persistance).
**Négatif :** signature de `useShapeWithDefaults` modifiée (param `storeNuri`).
**Risque :** si NextGraph change le comportement du private store, ça casse.
> Règle dérivée : [[rule_private-store-scope]]. Décision *remise en cause* par le futur multi-store : [[brief_2026-05-17_multi-store-refactor]].
@@ -1,51 +0,0 @@
---
type: decision
summary: Décision 2026-03-17 — supprimer les objets ORM via ng.sparql_update (DELETE WHERE) seul, car ngSet.delete() ne persiste pas et les combiner crée un conflit CRDT
---
# Use SPARQL DELETE instead of ORM ngSet.delete() for object removal
**Date:** 2026-03-17 18:00
**Status:** ~~Accepted~~**Superseded (2026-06-15)**
> **Annulée le 2026-06-15.** Le bug de non-persistance de `ngSet.delete()` qui motivait cette décision a depuis été en grande partie corrigé côté `@ng-org/orm` : le code (`leaveEvent`) est repassé à `ngSet.delete()`. La persistance reste toutefois possiblement partielle — l'état courant et le repli SPARQL sont décrits dans [[caveat_participation-deletion]]. Décision conservée comme mémoire d'arbitrage (le conflit CRDT « ne pas combiner les deux » reste vrai).
## Context
Quitter un event exige de supprimer l'objet `Participation` du store NextGraph. `DeepSignalSet.delete()` met à jour l'état réactif local (UI immédiate) mais **ne persiste pas** au broker — après refresh, la participation réapparaît.
## Options Considered
### Option A: ORM `ngSet.delete(item)`
- API officielle (README ORM), update réactif local instantané.
- **Contre** : ne persiste pas en pratique (`delete()` renvoie `true`, set local à jour, mais objet de retour après refresh) ; `graph_orm_update` semble mal gérer les patches "remove" pour objets de set top-level (bug moteur probable) ; échoue silencieusement.
### Option B: `ng.sparql_update()` avec SPARQL DELETE
- `DELETE WHERE { GRAPH <graph> { <subject> ?p ?o } }` retire tous les triples RDF.
- **Pour** : persiste (survit au refresh) ; le broker confirme via `GraphOrmUpdate` remove qui retire réactivement l'item du set ORM ; contrôle direct.
- **Contre** : pas instantané (round-trip SPARQL + callback broker, ~50ms) ; ne doit pas être combiné avec `ngSet.delete()`.
### Option C: les deux ensemble
- **Ne marche pas** : le patch ORM `.delete()` et le DELETE SPARQL entrent en conflit CRDT → ni UI ni persistance.
## Decision
**Option B : SPARQL DELETE seul.** Le broker renvoie un `GraphOrmUpdate` `op: "remove"` qui retire réactivement l'item du set ORM (UI à jour, juste pas synchrone). **Ne pas** appeler `ngSet.delete()` à côté.
```typescript
// FestipodDataContext.tsx leaveEvent():
const session = await sessionPromise;
await ng.sparql_update(
session.session_id,
`DELETE WHERE { GRAPH <${partGraph}> { <${partId}> ?p ?o } }`,
partGraph,
);
```
## Consequences
**Positif :** suppression persistée ; source de vérité unique (broker → ORM → UI).
**Négatif :** léger délai UI (~50ms) ; diverge des exemples README ORM.
**Risque :** si `ng.sparql_update` change, ça casse ; toute future suppression doit suivre le même pattern ; revisiter si `ngSet.delete()` est corrigé en montée de version.
> État courant (la règle a été retirée) : [[caveat_participation-deletion]].
@@ -1,29 +1,28 @@
---
type: knowledge
summary: Deux modes (connected = NextGraph ORM, disconnected/demo = état local seedé) ; FestipodDataContext choisit le provider selon le statut NextGraphContext, tous les écrans passent par useFestipodData()
summary: Deux modes (connected = SDK @ng-eventually/client, disconnected/demo = état local seedé) ; FestipodDataContext choisit le provider selon le statut de connexion, tous les écrans passent par useFestipodData()
---
# Modes de données & contextes
L'app a **deux modes**, tous deux consommés via le hook `useFestipodData()` :
1. **Connected** — shapes ORM NextGraph (P2P, chiffré, local-first)
1. **Connected** — shapes ORM du SDK `@ng-eventually/client` (P2P, chiffré, local-first)
2. **Disconnected / Demo** — état React local seedé depuis `seedData.ts` (voir [[knowledge_seed-data]])
## NextGraphContext (`src/shared/context/NextGraphContext.tsx`)
- Cycle de connexion : `disconnected``connecting``connected` | `error`.
- Fournit la session avec les IDs de stores (private, protected, public).
- **Auto-init conditionnel** : voir [[rule_conditional-ng-init]] (n'auto-initialise que dans l'iframe broker).
- Fournit la session (l'utilisateur courant et son accès aux stores par scope).
## FestipodDataContext (`src/shared/context/FestipodDataContext.tsx`)
- Enveloppe les shapes via `useShapeWithDefaults()`.
- Expose `useFestipodData()` (consommé par tous les écrans) + CRUD (`createEvent`, `updateEvent`, etc.).
- **Provider selon le statut NG** :
- Expose `useFestipodData()` (consommé par tous les écrans) + CRUD (`createEvent`, `updateEvent`, `joinEvent`, `leaveEvent`, etc.).
- **Provider selon le statut de connexion** :
- `disconnected``LocalDataProvider` avec seed (démo)
- `connecting``LocalDataProvider` **vide** (évite de flasher le seed avant le chargement du wallet)
- `connected``NgDataProvider` (données réelles du wallet)
- `error``LocalDataProvider` avec seed (fallback gracieux)
> Réserve : certaines mutations (`joinEvent`/`leaveEvent`) sont encore des **no-ops** (`console.log`) en attendant le chantier données — cf. [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 3.
> Les mutations sont **réellement persistées** en mode connected (`joinEvent` écrit une Participation et notifie l'hôte du PdR, `leaveEvent` supprime de façon autoritative — cf. [[caveat_participation-deletion]]). En mode local/demo elles sont des no-ops (cf. [[knowledge_context-internals]]).
@@ -10,15 +10,15 @@ last_checked: 2026-07-03
| Type | Persistance | Champs clés |
|---|---|---|
| `FpEventData` | NextGraph (shape Event) | id, title, date, location, distance, themes |
| `FpUserData` | NextGraph (shape UserProfile) | id, name, username, bio, city, counts |
| `FpParticipationData` | NextGraph (shape Participation) | eventId + userId + confirmed |
| `FpMeetingPointData` | NextGraph (shape MeetingPoint, T02.a) | eventId, location, time, host |
| `FpNotificationData` | NextGraph (shape Notification, T02.a) | kind, target, source |
| `FpEventData` | SDK (shape Event) | id, title, date, location, distance, themes |
| `FpUserData` | SDK (shape UserProfile) | id, name, username, bio, city, counts |
| `FpParticipationData` | SDK (shape Participation) | eventId + userId + confirmed |
| `FpMeetingPointData` | SDK (shape MeetingPoint) | eventId, location, time, host |
| `FpNotificationData` | SDK (shape Notification) | kind, target, source |
| `FpFriendshipData` | **local-only** | userId + friendId |
`MeetingPoint` et `Notification` ont désormais de vraies **shapes SHEX** (`src/shared/shapes/shex/festipodShapes.shex`) avec bindings ORM générés (`festipodShapes.shapeTypes.ts` : `FpMeetingPointShapeType`, `FpNotificationShapeType`) et **sont persistés** (T02.a). `Notification` est notamment créée lors de l'inscription à un PdR (`joinEvent`, cf. `nextgraph-platform` inbox).
`MeetingPoint` et `Notification` ont de vraies **shapes SHEX** (`src/shared/shapes/shex/festipodShapes.shex`) avec bindings ORM générés (`festipodShapes.shapeTypes.ts` : `FpMeetingPointShapeType`, `FpNotificationShapeType`) et **sont persistés**. `Notification` est notamment créée lors de l'inscription à un point de rencontre (`joinEvent`).
`Friendship` n'a **pas** de shape SHEX ni de persistance NextGraph — il reste app-TS-only (cf. [[knowledge_nextgraph-stack]]).
`Friendship` n'a **pas** de shape SHEX ni de persistance — il reste app-TS-only (cf. [[knowledge_nextgraph-stack]]).
> Piège : même pour `FpEvent` (persisté), plusieurs champs du type app ne sont **pas** dans la shape et sont perdus en connecté — voir [[caveat_event-fields-not-persisted]].
@@ -1,28 +1,32 @@
---
type: knowledge
summary: Paquets @ng-org/* (web, orm, shex-orm, alien-deepsignals), shapes SHEX festipodShapes, bindings ORM générés, régénérés via build:orm
summary: Le SDK de données est @ng-eventually/client (traité comme un SDK NextGraph fini) — injecté une seule fois via ngSession.configure ; ORM réactif useShape sur shapes SHEX festipodShapes, bindings régénérés via build:orm ; ne jamais documenter l'état courant de NextGraph ici
---
# Stack NextGraph (côté app)
# Stack de données (SDK `@ng-eventually/client`)
Festipod persiste via **`@ng-eventually/client`** — le SDK NextGraph que l'app consomme. On le traite comme un **SDK fini et mature** : documents par entité placés par scope, capabilities, inboxes, ORM réactif.
```
@ng-org/web # Runtime navigateur (proxy postMessage vers l'iframe)
@ng-org/orm # ORM réactif basé sur les shapes RDF (useShape…)
@ng-org/shex-orm # Génération SHEX → TypeScript
@ng-org/alien-deepsignals # Pont de signaux réactifs
@ng-eventually/client # LE SDK de données de l'app (ORM réactif useShape, docs, scopes, inbox)
```
Installés depuis npm (`@ng-org/*`, versions alpha). Pour développer contre un build local non publié de `nextgraph-rs`, `scripts/build-ng-packages.sh` pack le monorepo en tarballs et repointe `package.json` (cf. `nextgraph-platform` — le pattern d'origine du projet, réactivable pour un fork).
## Frontière SDK (règle d'or)
> **Indirection via `ng-eventually` (depuis 2026-06-22).** Le data-plane ne consomme plus le SDK directement : `useShape` est importé de **`@ng-eventually/client`** (wrapper SDK-identique), et `ngSession` injecte le vrai SDK dans la lib via `configure()` (`@ng-eventually/client/polyfill`). Aujourd'hui la lib **forwarde tout** (passthrough) — comportement identique, validé `@data`. Détails et raison d'être : [[decision_2026-06-17_eventually-library]]. Les imports **de types** (`ShapeType`, `DeepSignalSet`…) restent sur `@ng-org/*`.
- L'app **ne dépend que de `@ng-eventually/client`** pour la donnée.
- Le SDK est **initialisé/injecté une seule fois** via `ngSession.configure(...)` (`src/shared/utils/ngSession.ts`) — point d'injection unique. Le reste de l'app (data-plane, lifecycle, login, types) passe par la lib.
- **Ne jamais documenter dans ce repo l'état courant de NextGraph** (contraintes du SDK sous-jacent, contournements, internes broker/verifier, mécanique d'émulation) : cela vit dans le repo `@ng-eventually/client`. Ici on décrit seulement **comment Festipod utilise ce SDK**.
## Shapes SHEX
## ORM & shapes SHEX
L'ORM réactif (`useShape`) s'appuie sur des **shapes SHEX** : `src/shared/shapes/shex/festipodShapes.shex` définit :
`src/shared/shapes/shex/festipodShapes.shex` définit :
- **Event** — titre, description, dates, lieu, thèmes, participants
- **UserProfile** — nom, username, bio, ville, visibilité
- **Participation** — lie event + user, statut de confirmation
- **MeetingPoint** — point de rencontre (lieu, horaire, hôte)
- **Notification** — notification (créée notamment à l'inscription à un PdR)
Bindings ORM dans `src/shared/shapes/orm/` (`*.schema.ts`, `*.shapeTypes.ts`, `*.typings.ts`). **Régénérer** avec `bun run build:orm` après toute modif `.shex`.
Bindings ORM générés dans `src/shared/shapes/orm/` (`*.schema.ts`, `*.shapeTypes.ts`, `*.typings.ts`). **Régénérer** avec `bun run build:orm` après toute modif `.shex`.
> Manque côté shapes : **pas de `MeetingPoint`** ni d'entité notification — le point de rencontre est aujourd'hui local-only côté types (voir [[knowledge_entities]]). Leur modélisation est un chantier de [[brief_2026-05-21_fork-nextgraph-inbox]].
> `Friendship` n'a **pas** de shape SHEX ni de persistance — il reste app-TS-only (cf. [[knowledge_entities]]).
@@ -14,4 +14,4 @@ summary: seedData.ts fournit des fixtures déterministes (10 users, events, part
Ces fixtures servent (a) le **mode démo** (`LocalDataProvider`, cf. [[knowledge_data-modes]]) et (b) les tests **`@ui`** qui rendent les écrans avec ces données prévisibles (`Marie Dupont`/`@mariedupont` = currentUser, `Jean Durand`/`@jeandurand` existe, etc. — voir concept `bdd-testing`).
> `bootstrapWallet()` (`src/shared/utils/ngBootstrap.ts`) seede ces données dans le wallet NG en mode connected — déclenché uniquement par action explicite de l'utilisateur (« Charger données de test »). Sa refonte par documents/périmètres est un point des briefs `nextgraph-platform`.
> `bootstrapWallet()` (`src/shared/utils/ngBootstrap.ts`) seede ces données dans le wallet en mode connected — déclenché uniquement par action explicite de l'utilisateur (« Charger données de test »).
@@ -1,14 +0,0 @@
---
type: rule
summary: N'auto-initialiser NextGraph que dans l'iframe broker (window.self !== window.top) ; en standalone, initNgWeb() redirige toute la page — attendre un connect() explicite
---
# Règle : auto-init NextGraph seulement dans l'iframe broker
`initNgWeb()` de `@ng-org/web` teste `window.self === window.top`. **Hors iframe** (app standalone), il **redirige toute la page** vers `nextgraph.net/redir/` pour déclencher l'auth broker.
Donc `NextGraphContext` calcule `isInsideBroker = window.self !== window.top` et **n'auto-appelle `initNg()` que si `isInsideBroker`**. En standalone, la connexion attend un `connect()` explicite (clic « Se connecter ») — sinon l'app redirige à chaque chargement et casse le dev/démo.
De plus, `FestipodDataContext` rend des données **vides** (pas le seed) pendant la phase `connecting`, pour éviter de flasher du contenu démo avant le chargement du wallet (voir [[knowledge_data-modes]]).
> Garder ce garde **aligné** sur la détection interne de `@ng-org/web` : si leur heuristique change, le nôtre doit suivre. Pourquoi + alternatives : [[decision_2026-03-13_conditional-ng-init-broker-detection]].
@@ -1,34 +0,0 @@
---
type: rule
summary: Les entités domaine PARTAGEABLES (events/profils/participations) se lisent ET s'écrivent via did:ng:${protected_store_id} (scope useShape ET @graph) depuis T02.h ; le private store reste l'ancre shim/inbox + settings privés ; ne JAMAIS utiliser did:ng:i comme scope (RepoNotFound) — les DEUX stores doivent être ouverts via orm_start_graph
---
# Règle : scope = `@graph` = `protected_store_id` pour les entités partageables
Depuis **T02.h** (axe A, cf. [[caveat_multistore-is-multi-document]]), le chemin par défaut (mono-document) lit **et** écrit les **entités domaine partageables** (events, profils, participations) dans le **store protected natif** — plus dans le private.
Pour lire **et** écrire ces entités via l'ORM NextGraph :
- **Scope** : `useShape(shapeType, \`did:ng:${session.protected_store_id}\`)`
- **`@graph`** (cible des écritures) : `did:ng:${session.protected_store_id}`
C'est critique : `orm_start_graph` avec le NURI d'un store **ouvre explicitement le repo** dans la HashMap `self.repos` du verifier. Sans ça, `orm_frontend_update` échoue en `RepoNotFound`. Vérifié empiriquement que le **protected** s'ouvre pour ORM+SPARQL de la même façon que le private (round-trip probe, pas de `RepoNotFound`). Les **DEUX** stores utilisés doivent donc être ouverts via `orm_start_graph`.
## Rôle résiduel du private store
Le **private store** reste l'ancre pour :
- le shim shared-wallet et les dépôts d'inbox (cf. `nextgraph-platform`) ;
- les **settings privés** (cible future).
## Interdit
**Ne pas utiliser `did:ng:i` comme scope.** Il s'abonne au site entier de l'utilisateur via un chemin de code spécial (`NuriTargetV0::UserSite`) qui **n'ouvre pas les repos individuels** → casse toutes les écritures par `RepoNotFound`.
## Fichiers porteurs
- `src/shared/hooks/useShapeWithDefaults.ts` — accepte un `storeNuri`, le passe à `useShape`.
- `src/shared/utils/ngGraph.ts``ensureGraphNuri()` retourne le `@graph` (entités existantes d'abord, sinon fallback `protected_store`).
- `src/shared/context/FestipodDataContext.tsx` — récupère la session et passe le NURI du protected store (`protectedNuri`).
- `src/shared/utils/ngBootstrap.ts` — seede en utilisant `ensureGraphNuri()`.
> Le *pourquoi* du choix historique (private, avant T02.h) et les alternatives écartées : [[decision_2026-03-17_private-store-nuri-scope]] (dont l'insight « ouvrir le repo via le NURI du store sinon RepoNotFound » reste vrai pour les DEUX stores). Les deux axes store/document et la cible : [[caveat_multistore-is-multi-document]] et [[brief_2026-05-17_multi-store-refactor]].