Ng eventually #1

Open
Sylvain wants to merge 110 commits from ng-eventually into main
13 changed files with 123 additions and 27 deletions
Showing only changes of commit aabb2b77f7 - Show all commits
@@ -23,7 +23,7 @@ Steps **partagés** (cross-domaine) dans `src/shared/steps/ui/` :
Les noms français des écrans (`"accueil"`, `"détail événement"`, `"mon profil"`…) mappent vers les IDs d'écran via `screenNameMap`.
Tags de scénario : `@ui` / `@data` / `@e2e` (couche) + **`@wip`** pour un scénario dont les steps ne sont pas encore implémentés **ou dont le comportement applicatif n'est pas encore fiable** (ex. la désinscription qui ne se reflète pas dans l'UI cf [[caveat_participation-deletion]]). **`@wip` est EXCLU du run par défaut** (`cucumber.json: "tags": "not @wip"`) : ces scénarios documentent un attendu sans casser la suite ; retirer le `@wip` quand c'est fiable. Un `Contexte` (Background) fréquent — « Étant donné que je suis connecté » — ne fait que poser un flag `isAuthenticated`, pas d'auth réelle en `@ui`.
Tags de scénario : `@ui` / `@data` / `@e2e` (couche) + **`@wip`** pour un scénario dont les steps ne sont pas encore implémentés **ou dont le comportement applicatif n'est pas encore fiable** (usage : marquer un attendu réel qui échoue à cause d'un bug produit, pas un test obsolète — ex. historique : la désinscription qui ne se reflétait pas dans l'UI, `@wip` **levé** depuis sa résolution T02.c, cf [[caveat_participation-deletion]]). **`@wip` est EXCLU du run par défaut** (`cucumber.json: "tags": "not @wip"`) : ces scénarios documentent un attendu sans casser la suite ; retirer le `@wip` quand c'est fiable. Un `Contexte` (Background) fréquent — « Étant donné que je suis connecté » — ne fait que poser un flag `isAuthenticated`, pas d'auth réelle en `@ui`.
## Config
@@ -1,6 +1,7 @@
---
type: knowledge
summary: Couche @data — Playwright pilote Chromium (profil persistant) qui s'authentifie au broker NextGraph réel chargeant harness-ng.tsx en iframe ; cycle de vie wallet automatisé (création + login bootstrap), bridge window.__testData, fallback mock
last_checked: 2026-07-03
---
# Couche `@data` (broker réel)
@@ -32,5 +33,5 @@ Cucumber → Playwright (Chromium, profil persistant)
- **Flags Chromium** (`--disable-web-security`, `--allow-insecure-localhost`, désactivation de Private Network Access) : nécessaires car le broker public charge un harness `http://127.0.0.1` en iframe.
- **Profil persistant** `.playwright-profile/` (gitignored, wallet en localStorage) — exige le vrai binaire Chrome, pas `chrome-headless-shell`.
- **Serveur HTTP** lancé en `BeforeAll` (port auto), sert le HTML + `/harness.js` (fichiers séparés — le script inline casse à cause de caractères spéciaux du bundle).
- **Subscriptions ORM** : les 3 shapes avec scope `did:ng:${session.private_store_id}` (cf. concept `data-layer`).
- **Bridge `window.__testData`** : `events`/`users`/`participations` (sets live), `currentUserId`, lookups (`getEvent`, `getEventByTitle`), mutations (`joinEvent`, `leaveEvent`, `updateEvent`), requêtes (`isParticipating`, `getEventParticipants`).
- **Subscriptions ORM** : les shapes des entités partageables avec scope `did:ng:${session.protected_store_id}` (le **protected** store depuis T02.h — `harness-ng.tsx` utilise `protectedNuri` ; le private store n'est plus le scope des entités domaine, cf. concept `data-layer` [[rule_private-store-scope]]).
- **Bridge `window.__testData`** : `events`/`users`/`participations` (sets live), `currentUserId`, lookups (`getEvent`, `getEventByTitle`), mutations (`joinEvent`, `leaveEvent`, `updateEvent``joinEvent`/`leaveEvent` réels depuis T02.b/c : persistance Participation + inbox + Notification / DELETE-WHERE), requêtes (`isParticipating`, `getEventParticipants`).
+2 -2
View File
@@ -2,13 +2,13 @@
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
triggers:
keywords: [nextgraph, useShape, ORM, SHEX, shape, store, private_store, "@graph", NURI, sparql, sparql_update, seed, wallet, RepoNotFound, FestipodData, ngGraph, bootstrap]
keywords: [nextgraph, useShape, ORM, SHEX, shape, store, private_store, "@graph", NURI, sparql, sparql_update, seed, wallet, RepoNotFound, FestipodData, ngGraph, bootstrap, multistore, document, isolation]
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-store** — tout atterrit dans le `private_store` de l'utilisateur connecté.
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`.
> 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é.
@@ -0,0 +1,58 @@
---
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,12 +1,18 @@
---
type: caveat
summary: La suppression de Participation (leaveEvent) via ngSet.delete() NE se reflète PAS dans l'UI en mode broker (vérifié e2e 2026-06-30) — le bouton reste « ✓ Je participe » ; ngSet.delete() déclenche bien la réactivité mais la suppression ne se propage pas / l'item ressuscite via la sync. Scénario e2e « Se désinscrire » marqué @wip (exclu du run par défaut)
last_checked: 2026-06-30
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.
last_checked: 2026-07-03
---
# Caveat : suppression de Participation via `ngSet.delete()`
# Caveat : suppression de Participation (RÉSOLU en T02.c)
**État actuel du code** (`src/shared/context/FestipodDataContext.tsx`, `leaveEvent` en mode NG) : la suppression d'une `Participation` se fait via **`participationsShape.ngSet.delete(ngPart)`** — pas via `ng.sparql_update()` DELETE WHERE.
**É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).
Preuve : `cycle-de-vie-evenement.feature` (@e2e, @wip levé) + `inscription-inbox.feature` (@data « désinscription persistante »). Validation multi-navigateur complète = T02.f.
## Histoire du bug (avant T02.c)
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
@@ -8,6 +8,8 @@ summary: Décision 2026-03-17 — utiliser private_store_id comme scope useShape
**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.
@@ -1,6 +1,7 @@
---
type: knowledge
summary: Types de données Fp* (Event, UserProfile, Participation persistés NextGraph ; MeetingPoint et Friendship encore local-only)
summary: Types de données Fp* Event, UserProfile, Participation, MeetingPoint et Notification sont persistés NextGraph (shapes SHEX + ORM) ; seul Friendship reste local-only (app-TS)
last_checked: 2026-07-03
---
# Entités de données
@@ -12,9 +13,12 @@ summary: Types de données Fp* (Event, UserProfile, Participation persistés Nex
| `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` | **local-only** | eventId, location, time, host |
| `FpMeetingPointData` | NextGraph (shape MeetingPoint, T02.a) | eventId, location, time, host |
| `FpNotificationData` | NextGraph (shape Notification, T02.a) | kind, target, source |
| `FpFriendshipData` | **local-only** | userId + friendId |
`MeetingPoint` et `Friendship` n'ont **pas encore de shape SHEX** ni de persistance NextGraph (cf. [[knowledge_nextgraph-stack]]). Les brancher au store est un prérequis du multi-user — voir les briefs du concept `nextgraph-platform`.
`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).
`Friendship` n'a **pas** de shape SHEX ni de persistance NextGraph — 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,25 +1,34 @@
---
type: rule
summary: Utiliser did:ng:${private_store_id} comme scope useShape ET comme @graph d'écriture ; ne jamais utiliser did:ng:i comme scope (casse toutes les écritures par RepoNotFound)
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` = private_store_id
# Règle : scope = `@graph` = `protected_store_id` pour les entités partageables
Pour lire **et** écrire via l'ORM NextGraph :
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.
- **Scope** : `useShape(shapeType, \`did:ng:${session.private_store_id}\`)`
- **`@graph`** (cible des écritures) : `did:ng:${session.private_store_id}`
Pour lire **et** écrire ces entités via l'ORM NextGraph :
C'est critique : `orm_start_graph` avec le NURI du private_store **ouvre explicitement le repo** dans la HashMap `self.repos` du verifier. Sans ça, `orm_frontend_update` échoue en `RepoNotFound`.
- **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.
**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 `private_store`).
- `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* complet et les alternatives écartées : [[decision_2026-03-17_private-store-nuri-scope]]. **Ce scope mono-store est précisément ce que le chantier multi-store viendra remplacer** — voir [[brief_2026-05-17_multi-store-refactor]].
> 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]].
@@ -15,7 +15,7 @@ summary: Ce qui est implémenté aujourd'hui (cycle événement + point de renco
- Profil utilisateur, mise à jour, partage de profil
- Liste d'amis (connexions), profil d'un autre utilisateur
> Réserve : certaines actions de données restent des no-ops en l'état (ex. `joinEvent`/`leaveEvent` côté `FestipodDataContext` — détail dans [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 3). Le router et les écrans existent, mais le branchement données suit le chantier multi-store.
> MAJ T02.b/c (2026-07-03) : l'inscription/désinscription au PdR est **réellement branchée** côté données. `joinEvent` **persiste une Participation** + **dépose dans l'inbox de l'hôte** + **crée une Notification** (shape SHEX réelle) ; `leaveEvent` **supprime autoritativement** via `SPARQL DELETE-WHERE` (le bug CRDT de désinscription est résolu). Ce ne sont plus des no-ops. La **découverte publique cross-compte** fonctionne aussi (fan-out, T02.e — un utilisateur voit un événement public d'un autre sans connexion). Détail lib : [[decision_2026-06-17_eventually-library]] §Inbox émulée.
## Évolutions identifiées (non implémentées)
@@ -6,8 +6,10 @@ last_updated: 2026-05-21
# Forker NextGraph pour exposer l'inbox au SDK JS
**Status:** Incubating — aucun travail démarré
**Last updated:** 2026-05-21
**Status:** Court-circuité (2026-07-03, T02.b/c) — approche non retenue pour l'instant
**Last updated:** 2026-07-03
> **Court-circuité par l'inbox émulée en lib (T02.b/c).** Plutôt que de forker le broker pour exposer `inbox_post`, le namespace `inbox` de `@ng-eventually/client` **émule** l'inbox : `post`/`read`/`materialize`/`watch`, curateur **émulé inline**, dépôts via **SPARQL dans un document du `private_store`** — aucun patch Rust ni auto-hébergement `ngd` requis. L'inscription PdR est déjà câblée dessus (`joinEvent`/`leaveEvent` réels, Notification persistée en shape SHEX, cf. [[decision_2026-06-17_eventually-library]] §Inbox émulée). Ce brief reste conservé comme **plan de repli** si l'inbox broker native devenait nécessaire (anonymat crypto natif via `from = None`, que l'émulation ne fournit pas), et comme mémoire des chantiers Couche 3 (dont plusieurs — shapes MeetingPoint/Notification, joinEvent réel — sont **désormais faits**, T02.a).
## Context
@@ -8,6 +8,8 @@ last_updated: 2026-06-16
Comment un utilisateur **découvre** les événements (qu'il n'a pas créés). En P2P local-first, pas de registre global natif ; la matrice ([[brief_2026-05-18_authorization-matrix]]) repoussait la question. Cette décision la tranche et **guide l'implémentation** (cible et stopgap).
> **Réalité d'implémentation (T02.e, 2026-07-03) — divergence assumée avec le stopgap décrit ici.** Ce qui **ship aujourd'hui** est le **fan-out cross-compte sur les docs publics de tous les comptes** (`FestipodDataContext` : « Public discovery (T02.e): cross-account fan-out, ALWAYS on » ; `listEntityDocs('public')` sur tous les comptes) — Alice voit l'événement public de Bob **sans connexion**. C'est **précisément la voie que cette décision qualifiait de « dérive »** à remplacer par un **index global unique** dans le wallet partagé. L'index global (cible) **n'est pas** implémenté ; le fan-out est le mécanisme de découverte réel du wallet-partagé staging. La **cible** (index global alimenté par inbox, propriétaire à trancher) reste valable ; le corps ci-dessous la décrit et n'est pas réécrit. Vérifier : `grep -n "cross-account fan-out" src/shared/context/FestipodDataContext.tsx`, `resolveReadGraphs`/`listEntityDocs` dans `storeRegistry`.
## Accès ≠ découverte
- **Accès** : ai-je le droit de lire ce document si je le tiens ? PdR/événement = **public universel** (lisible par tous, avec le NURI).
@@ -1,7 +1,7 @@
---
type: decision
summary: Tout le polyfill multi-user (wallet partagé, caps émulées, inbox émulée) est encapsulé dans une LIBRAIRIE GÉNÉRIQUE externe « ng-eventually-js » (repo hors Festipod, à côté de nextgraph-rs/orm-tests), zéro Festipod dedans. UN package pour l'instant : @ng-eventually/client (entrée principale SDK-IDENTIQUE ; bootstrap polyfill isolé sous /polyfill ; l'app n'en dépend que de lui). Le curateur d'index global (ex-@ng-eventually/service) est RETIRÉ/différé : son modèle « backend à données globales » est incorrect — NextGraph est mono-utilisateur sans données globales (cf. knowledge_apps-and-services) ; un index global passerait par une app singleton (incertain, différé, à creuser). Migration = alias de build retiré + le client redevient le vrai SDK. Festipod ne dépend que de @ng-eventually/client.
last_updated: 2026-07-02
summary: Tout le polyfill multi-user (wallet partagé, caps émulées, inbox émulée) est encapsulé dans une LIBRAIRIE GÉNÉRIQUE externe « ng-eventually-js » (repo hors Festipod, à côté de nextgraph-rs/orm-tests), zéro Festipod dedans. UN package pour l'instant : @ng-eventually/client (entrée principale SDK-IDENTIQUE ; bootstrap polyfill isolé sous /polyfill ; l'app n'en dépend que de lui). Le curateur d'index global (ex-@ng-eventually/service) est RETIRÉ/différé : son modèle « backend à données globales » est incorrect — NextGraph est mono-utilisateur sans données globales (cf. knowledge_apps-and-services) ; un index global passerait par une app singleton (incertain, différé, à creuser). Migration = alias de build retiré + le client redevient le vrai SDK. Festipod ne dépend que de @ng-eventually/client. MAJ T02.b/c (2026-07-03) : le namespace inbox (post/read/materialize/watch, curateur ÉMULÉ inline, dépôts SPARQL dans un doc du private store) est IMPLÉMENTÉ et l'inscription PdR est réellement câblée (joinEvent persiste Participation + dépôt inbox hôte + Notification ; leaveEvent DELETE-WHERE autoritatif) — court-circuite l'approche fork broker.
last_updated: 2026-07-03
---
# Décision 2026-06-17 — Librairie « ng-eventually-js » (polyfill encapsulé)
@@ -84,6 +84,16 @@ Le TODO ci-dessus est **fait** : **tout le shim est désormais DANS la lib**. La
- **Invariant atteint** : `grep -rn "from '@ng-org" src/ | grep -v "import type"` ne liste plus que **`ngSession`** (injection `configure`) + les 2 exceptions test-harness documentées (`auth-setup.tsx`, `harness.tsx`/`deepSignal`). Plus aucun `doc_create` via le proxy public.
- **Validation** : lib **36/36 `bun test` + `tsc --noEmit` rc=0** ; app `bun run build` + bundle `harness-ng` OK ; **suite BDD complète 78 passed / 0 failed / 71 skipped** (baseline 2026-06-30 respectée, dont les 3 `@data` multistore, `@humain`, ReadCap `@data`).
### Inbox émulée dans la lib — FAIT & câblée dans l'app (2026-07-03, T02.b/c)
Le « Reste à implémenter » ci-dessus (inbox `post` + matérialisation) et le stub `inbox.post` **sont faits** ; l'inscription PdR est réellement branchée. La matérialisation ne passe **pas** par un curateur/package séparé différé mais par un **curateur ÉMULÉ inline** dans la lib.
- **Namespace `inbox` de la lib** — implémente désormais `post` / `read` / `materialize` / `watch`. Les dépôts sont écrits **via SPARQL dans un document du `private_store`** (le private reste l'ancre shim/inbox ; les entités partageables domaine sont, elles, sur le `protected_store` — cf. [[rule_private-store-scope]], T02.h). Curateur **émulé** (pas d'`inbox_post` broker natif exposé).
- **App câblée (réelle inscription PdR)** : dans `FestipodDataContext`, `joinEvent` **persiste une Participation** + **dépose dans l'inbox de l'hôte** + **crée une Notification** (shape SHEX réelle, T02.a) ; `leaveEvent` **supprime autoritativement** via `SPARQL DELETE-WHERE` (le bug CRDT de désinscription est **RÉSOLU** — cf. [[caveat_participation-deletion]]).
- **Découverte publique cross-compte** fonctionne (fan-out sur les docs publics de tous les comptes ; Alice voit l'événement public de Bob sans connexion) — T02.e, réalise [[decision_2026-06-16_discovery-model]] côté découverte primaire.
L'approche **fork broker** pour exposer l'inbox ([[brief_2026-05-21_fork-nextgraph-inbox]]) est **court-circuitée** par cette émulation en lib (voir le statut superséédé de ce brief).
## Open Questions
- **Curateur d'index / index global** : package `@ng-eventually/service` **retiré pour l'instant** (2026-06-21) — modèle « backend » incorrect ([[knowledge_apps-and-services]]). À **réintroduire** (et nommer : curateur/admin) quand le **mécanisme cible d'index global** sera tranché (app singleton ? voie plus simple ?) — incertain, **à creuser plus tard**.
@@ -1,7 +1,7 @@
---
type: knowledge
summary: Référence des 5 types de stores NextGraph et leurs droits, document=repo, granularité des permissions, capability/Nuri, inbox native (anonymat via from optionnel), et ce que le SDK @ng-org/web n'expose PAS
last_checked: 2026-05-21
last_checked: 2026-07-03
---
# Stores NextGraph et droits d'accès
@@ -43,6 +43,8 @@ Tout wallet a d'office les **3 stores** private/protected/public (session : `pri
**Pas d'héritage de lecture automatique.** Détenir la ReadCap d'un **store** ne donne **pas** accès aux repos qu'il contient — **il faut la ReadCap de chaque repo**. L'héritage optionnel `inherit_perms_users_and_quorum_from_store: Option<ReadCap>` ne partage que les **users/quorum** (écriture/permissions), **pas** la possession de read-cap. (Repos d'un private_store : héritage implicite.) **Conséquence pour l'émulation** : l'unité d'accès en lecture est le **repo = le `@graph`** de chaque item — un filtre par document, pas par store ni par item (cf. [[decision_2026-06-17_eventually-library]]).
> ⚠️ **Confusion récurrente store ↔ document.** L'axe de l'isolation est le **document (repo/`@graph`)**, jamais le **store** : un store *contient* plusieurs documents et n'en partage pas la lecture. Piège concret côté Festipod : le flag `FESTIPOD_MULTISTORE` crée en réalité **plusieurs DOCUMENTS** (1 par entité) dans **un seul store partagé**, pas plusieurs stores — voir data-layer `caveat_multistore-is-multi-document`. « Plus d'isolation » = **plus de documents**, pas plus de stores.
**Capability / Nuri.** Le partage transmet un **Nuri** embarquant la capability crypto (lecture et/ou écriture). Pas d'ACL centralisée : posséder le Nuri = le droit. *« adding permissions can be done offline »* ; *« removing permissions … requires a SyncSignature »* (synchrone).
## Inbox