docs(concepts): NextGraph multi-user design — ng-eventually polyfill, discovery, apps/services

Captures the design worked out this session:
- decision: ng-eventually generic polyfill library (external repo) encapsulates all
  multi-user compensation; @ng-eventually/client is SDK-identical, app depends only on it.
- decision: discovery via a single global index fed through its inbox (owned doc,
  materialized) — no Group store; index owner = open question (singleton app, deferred).
- knowledge: NextGraph apps/services are mono-user with no global data (corrects the
  earlier 'index service with its own wallet' model).
- reconciled shared-wallet-shim brief (per-entity docs, login flow, polyfill terminology),
  authorization-matrix (no Group store), data-layer stack (ng-eventually indirection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-06-22 16:35:58 +02:00
parent 222658a75d
commit 3ca2d10c49
6 changed files with 224 additions and 36 deletions
@@ -127,6 +127,8 @@ Le doc PdR (dans le `public_store` de l'hôte) a une **inbox** native : reçoit
Sur le périmètre validé, **aucune donnée ne demande de Group store**. Tout tient dans : 3 stores natifs par utilisateur + Dialog stores + inboxes natives. Les Group stores ne deviennent nécessaires que si le modèle d'écriture événement est « wiki », ou si communautés/suivi/collaboration multi-hôte reviennent dans le périmètre.
> **Note (2026-06-17) — la découverte n'impose PAS de Group store.** On a un instant cru qu'un **index global des événements** exigerait un document à écriture ouverte (= Group store). La [[decision_2026-06-16_discovery-model]] a finalement retenu un index **possédé** (lecture publique) **alimenté via son inbox** (le créateur y *dépose* une référence ; le propriétaire matérialise). Comme l'**inbox est une primitive native de tout document**, l'index tient dans un `public_store` ordinaire → **« aucun Group store » reste vrai**. Les Group stores ne redeviennent nécessaires que pour communautés / collaboration multi-écrivains réels.
### Implication pour [[brief_2026-05-17_multi-store-refactor]]
Ce brief y propose une structure à 4 niveaux de Group stores. **Cette analyse dérive une structure différente** (3 stores natifs + Dialog, sans Group) parce que les concepts qui justifient les Group stores ont été mis hors périmètre. À reconcilier à l'exécution.
@@ -14,6 +14,8 @@ summary: Paquets @ng-org/* (web, orm, shex-orm, alien-deepsignals), shapes SHEX
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).
> **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/*`.
## Shapes SHEX
`src/shared/shapes/shex/festipodShapes.shex` définit :
@@ -1,13 +1,13 @@
---
type: brief
summary: Stopgap staging multi-user — un wallet partagé unique + couche storeRegistry (Piste A), comptes/login Festipod simulés (username seul), 1 document par (utilisateur × périmètre) via doc_create, filtre d'isolation applicatif ; structure préfigurant l'infra cible, sharedWalletShim jetable à la migration
last_updated: 2026-06-15
summary: Stopgap staging multi-user — un wallet partagé unique (Piste A) + couche storeRegistry, comptes/login Festipod simulés (username seul, cf. decision login-flow), 1 document PAR ENTITÉ (événement/PdR) + index global de découverte, participations groupées en protected, filtre d'isolation applicatif ; structure préfigurant l'infra cible (stores per-user, Group store pour l'index), sharedWalletShim jetable. Prototype implémenté puis réverti — à (ré)implémenter.
last_updated: 2026-06-16
---
# Stopgap multi-user : wallet partagé unique (`sharedWalletShim`)
**Status:** En cours — couche compte/login + isolation livrées et vérifiées ; couche multi-document livrée derrière flag, à valider sur broker
**Last updated:** 2026-06-15
**Status:** Conçu — décisions prises ([[decision_2026-06-15_shared-wallet-login-flow]], [[decision_2026-06-16_discovery-model]], granularité par entité). Faits clés vérifiés sur broker via un **prototype**. ⚠️ **Le prototype a été réverti — aucun code dans le tree** ; à (ré)implémenter.
**Last updated:** 2026-06-16
## Context
@@ -24,6 +24,8 @@ Donc lire le store d'un autre utilisateur — **même son `public_store`** — e
**Décision retenue :** Piste A (wallet partagé unique) + couche `storeRegistry`, broker **`nextgraph.net`**.
> **Évolution majeure (2026-06-17)** : tout ce polyfill est désormais **encapsulé dans une librairie générique externe** (`ng-eventually-js`, hors repo), pas dans l'app — voir [[decision_2026-06-17_eventually-library]]. L'app Festipod ne dépendra que de `@ng-eventually/client` (wrapper SDK-identique). Ce brief décrit donc la **conception du polyfill** ; son lieu d'implémentation est la lib, et les mécanismes ci-dessous (storeRegistry, caps émulées, inbox, index) y sont réalisés. Le `storeRegistry` et le filtre d'isolation décrits plus bas sont la **version « dans l'app »** désormais remplacée par la lib.
## What We Know
### Les trois familles de contournement (et pourquoi A)
@@ -41,34 +43,37 @@ Donc lire le store d'un autre utilisateur — **même son `public_store`** — e
┌─ Couche COMPTE (simulée — UX cible, jetable à la migration) ────────┐
│ signup / login Festipod · currentAccountId en localStorage │
├─ Couche STORES VIRTUELS (fidèle — survit à la migration) ───────────┤
│ storeRegistry : (appUser, scope) → NURI de document
1 document par (utilisateur × périmètre), créé via doc_create
│ storeRegistry : 1 document PAR ENTITÉ (événement/PdR) via
│ doc_create ; index global de découverte ; protected groupé/compte
├─ Couche NEXTGRAPH (réelle mais invisible) ──────────────────────────┤
│ UN wallet partagé, mêmes credentials pour tous │
└─────────────────────────────────────────────────────────────────────┘
```
1. **NextGraph (réelle mais invisible)** — un wallet partagé, mêmes credentials. Le login NextGraph **n'est pas programmable** (redirect web vers `nextgraph.net/redir`, cf. `NextGraphContext`, concept `data-layer`) ; il est donc présenté comme une **barrière technique d'accès** avant l'app, pas comme un login (flux arrêté dans [[decision_2026-06-15_shared-wallet-login-flow]]). Session **persistante** côté iframe broker → ouverture **une fois par device** dans une même session navigateur.
2. **Stores virtuels (fidèle, survit à la migration)****1 document par (utilisateur × périmètre)** via `doc_create`. Vérifié : `doc_create` retourne un NURI `did:ng:o:…`, le repo est **inséré immédiatement** dans `self.repos` (`verifier.rs:2900`), et `orm_start_graph`/`sparql_update` l'acceptent **sans pin explicite** (`sdk/rust/src/tests/sparql_regressions.rs:136-200`). À la migration : **swap du résolveur** `storeRegistry` vers les vrais stores, sans réécrire les écrans.
2. **Stores virtuels (fidèle, survit à la migration)****1 document par entité** (événement/PdR) via `doc_create`, référencé par un **index global de découverte** (cf. [[decision_2026-06-16_discovery-model]]) ; les données *protected* (profil, participations) restent **groupées** par compte. Vérifié : `doc_create` retourne un NURI `did:ng:o:…`, le repo est **inséré immédiatement** dans `self.repos` (`verifier.rs:2900`), et `orm_start_graph`/`sparql_update` l'acceptent **sans pin explicite** (`sdk/rust/src/tests/sparql_regressions.rs:136-200`). À la migration : **swap du résolveur** `storeRegistry` vers les vrais stores, sans réécrire les écrans.
3. **Compte/login simulés (UX, jetable)** — signup/login Festipod, **username seul** (pas de mot de passe), `currentAccountId` en `localStorage`.
### `sharedWalletShim`
Nom **volontairement explicite** du mapping temporaire (hack) : comptes simulés → NURIs des stores virtuels. Ancré dans le **`private_store` du wallet partagé** (`session.private_store_id`, toujours présent → ancre de bootstrap). **Seul artefact sans équivalent cible** (l'infra cible n'a **pas** d'index central : la découverte y passe par les connexions et les `public_store`). Rend possibles le **login cross-device** et le **picker d'utilisateurs**. **À supprimer à la migration.**
Nom **volontairement explicite** du mapping temporaire (hack) : comptes simulés → NURIs des stores virtuels. Ancré dans le **`private_store` du wallet partagé** (`session.private_store_id`, toujours présent → ancre de bootstrap). **Sans équivalent cible** : la cible n'a pas d'**annuaire de comptes** (l'identité = le wallet). Rend possibles le **login cross-device** et le **picker d'utilisateurs**. **À supprimer à la migration.**
> **Découverte des événements** (à ne pas confondre avec le shim) : modèle tranché dans [[decision_2026-06-16_discovery-model]] — un **index global unique** (pas une découverte par-compte), **alimenté via son inbox** : le créateur **dépose** une référence dans l'inbox de l'index ; l'index (document **possédé**, lecture publique) est matérialisé depuis son inbox. Architecture en 3 étapes (découverte → synchronisation → requête locale). **Pas de Group store** (index = doc possédé + inbox native) ; inbox + watcher réutilisés (même mécanisme que l'inscription au PdR).
Contenu par compte : `username → profileId → { docPublic, docProtected, docPrivate }`. Chaîne de bootstrap d'un device : session → `private_store_id` → lire le `sharedWalletShim` → comptes + NURIs par périmètre.
### Placement des entités
Identique à la dérivation de [[brief_2026-05-18_authorization-matrix]], au mapping `document ↔ store` près :
Dérivée de [[brief_2026-05-18_authorization-matrix]] (périmètres) + [[decision_2026-06-16_discovery-model]] (granularité par entité + index global) :
| Entité | Périmètre | Doc aujourd'hui | Store cible |
| Entité | Périmètre | Doc stopgap (wallet partagé) | Cible |
|---|---|---|---|
| Événement déclaré par U | public | `U/public` | `public_store` de U |
| PdR hébergé par U | public | `U/public` | `public_store` de U |
| Profil réseau de U | protected | `U/protected` | `protected_store` de U |
| Participation de U | protected | `U/protected` | `protected_store` de U |
| Index des connexions de U | protected | `U/protected` | `protected_store` de U |
| Événement (créé par U) | public | **1 doc / événement**, référencé dans l'index global | doc dans le `public_store` de U |
| PdR (hôte U) | public | **1 doc / PdR**, référencé dans l'index global | doc dans le `public_store` de U (+ inbox native) |
| **Index global des événements** | — (possédé + inbox) | **1 doc** alimenté via son **inbox** (dépôt + matérialisation) | doc **possédé** (lecture publique) **+ inbox** dans le `public_store` du propriétaire |
| Profil réseau de U | protected | groupé dans `U/protected` | `protected_store` de U |
| Participation de U | protected | groupé dans `U/protected` | `protected_store` de U |
| Index des connexions de U | protected | groupé dans `U/protected` | `protected_store` de U |
| Profil privé de U (settings, email) | private | `U/private` | `private_store` de U |
| Connexion A↔B | dialog | `dialog/A∙B` | Dialog store A↔B |
@@ -89,35 +94,45 @@ Un seul wallet ⇒ tout lisible par tous. Pour que le staging se **comporte** co
- `CURRENT_USER_ID` constant → `currentAccountId` sélectionnable (persisté `localStorage`).
- `src/shared/utils/ngBootstrap.ts` — seed réparti **par documents**.
## État d'implémentation (2026-06-15)
## Plan d'implémentation & faits vérifiés
Deux drapeaux de build, tous deux **OFF par défaut** (le mono-store validé reste le défaut ; dev/`@ui`/`@e2e` inchangés) :
> ⚠️ **Un prototype a été développé puis réverti** : le code décrit ici **n'est pas dans le tree** (working tree propre). Cette section sert de **plan de (ré)implémentation** et consigne les **faits vérifiés sur broker** durant le prototype — ils restent vrais indépendamment du code.
- **`FESTIPOD_STAGING=1`** — active le flux login option 2 (gate technique → écran « Connexion »). Découplé de `NODE_ENV` exprès, pour qu'un `@e2e` en build production ne soit pas bloqué par le gate.
- **`FESTIPOD_MULTISTORE=1`**active la couche multi-document (storeRegistry).
**Mécanisme prévu — deux drapeaux de build, OFF par défaut** (mono-store reste le défaut ; dev/`@ui`/`@e2e` inchangés) :
- `FESTIPOD_STAGING=1`flux login option 2 (gate technique → écran « Connexion »). **À découpler de `NODE_ENV`** (sinon un `@e2e` en build prod est bloqué par le gate).
- `FESTIPOD_MULTISTORE=1` — couche multi-document (storeRegistry).
| Pièce | Fichier | État |
|---|---|---|
| Couche compte (faux login, localStorage) | `src/shared/context/AccountContext.tsx` | ✅ livré, vérifié (build + `@ui`) |
| Gate technique + écran « Connexion » + orchestrateur | `src/modules/auth/screens/{AccessGateScreen,ConnexionScreen}.tsx`, `src/app/AuthGate.tsx` | ✅ livré |
| Vrai logout planqué | `ngSession.ts:logoutNg`, `SettingsScreen.tsx` | ✅ livré |
| Filtre d'isolation (mode connecté) | `src/shared/utils/isolation.ts` + `FestipodDataContext` (useNgData) | ✅ livré, pur, vérifié |
| storeRegistry + sharedWalletShim (doc_create, SPARQL shim, entité→périmètre) | `src/shared/utils/storeRegistry.ts` | ⚠️ livré, **compile**, runtime NG **à valider sur broker** |
| Câblage multi-document (reads `{graphs}` + writes par périmètre) | `FestipodDataContext` (useNgData) derrière `MULTISTORE` | ⚠️ livré, à valider sur broker |
**Pièces à (ré)créer** :
**Pourquoi le flag** : le runtime NextGraph (doc_create, shim SPARQL, abonnement multi-graphes) ne peut pas être validé sans broker live. Conformément au « mode mono-store parallèle » endossé par [[brief_2026-05-17_multi-store-refactor]], il ship OFF — rien de fonctionnel n'est cassé.
| Pièce | Fichier prévu |
|---|---|
| Couche compte (faux login, localStorage) | `src/shared/context/AccountContext.tsx` |
| Gate technique + écran « Connexion » + orchestrateur | `src/modules/auth/screens/{AccessGateScreen,ConnexionScreen}.tsx`, `src/app/AuthGate.tsx` |
| Vrai logout planqué | `ngSession.ts:logoutNg`, `SettingsScreen` |
| Filtre d'isolation (mode connecté) | `src/shared/utils/isolation.ts` |
| storeRegistry (1 doc/entité + index global) + sharedWalletShim | `src/shared/utils/storeRegistry.ts` |
| Câblage multi-document (lecture via index global + fan-out ; écriture per-entité) | `FestipodDataContext` derrière `MULTISTORE` |
| Scénarios `@data` de validation | `src/modules/workshop/…` |
**Étapes de validation broker** (pour passer `MULTISTORE` ON) :
1. Sur `nextgraph.net`, vérifier que `doc_create(session, "Graph", "data:graph", "store", undefined)` retourne un NURI utilisable comme `@graph` ORM (le test rust `sparql_regressions.rs:136-200` le suggère, à confirmer côté SDK JS).
2. Vérifier l'écriture/lecture du shim via `sparql_update`/`sparql_query` sur le NURI du private store (et le format de retour de `sparql_query``readBindings()` est tolérant mais à confirmer).
3. Vérifier l'abonnement `useShape(shape, { graphs: [...] })` sur plusieurs documents et sa réactivité quand la liste grandit.
4. Décider du seeding multi-document (le dev auto-seed est neutralisé en `MULTISTORE`).
> **Piège vérifié sur le prototype** : un `process.env.FESTIPOD_*` lu au **top-level** plante dans le navigateur (« process is not defined ») car le bundler n'inline que `NODE_ENV` ; lire en `typeof process !== 'undefined' && process.env.X`.
**Faits vérifiés sur broker** (prototype, `@data` contre `nextgraph.net`, 2026-06-16) — restent vrais :
1.`doc_create("Graph","data:graph","store",undefined)` → NURI utilisable comme `@graph` ORM (write+read via `useShape({graphs:[nuri]})`). On n'est **pas** limité au `private_store` comme scope (cf. [[rule_private-store-scope]]).
2.**Shim r/w** : 3 docs créés + `sparql_update`, rechargés via `sparql_query`.
3.**Fan-out par entité** : 2 comptes × 1 doc-événement → `useShape({graphs:[docA,docB]})` lit les deux ; un index liste les deux.
**Restes à traiter** (à la réimplémentation) :
- **Découverte réactive via l'index global** (un doc partagé réactif) — cf. [[decision_2026-06-16_discovery-model]] ; remplace le fan-out par-compte du prototype.
- **Réactivité de la création in-app** (best-effort : le nouveau doc rejoint le fan-out ; `@id` éventuellement en attente → envisager de frapper l'`@id` soi-même).
- **Synchronisation** (étape 2 du modèle 3-étapes) et **seeding** multi-document.
## Open Questions
- ~~**Login NextGraph invisible**~~ → **tranché** : login non programmable, présenté comme barrière technique d'accès ; session persistante. Voir [[decision_2026-06-15_shared-wallet-login-flow]].
- **Création des documents au signup** : `doc_create` ×3 synchrone, ou paresseux au premier write par périmètre ?
- **Picker d'utilisateurs** : UX pour l'écran « Connexion » (saisie libre vs liste des comptes du `sharedWalletShim`) ?
- ~~**Login NextGraph invisible**~~ → **tranché** ([[decision_2026-06-15_shared-wallet-login-flow]]).
- ~~**Modèle de découverte**~~ → **tranché** : index global à écriture ouverte ([[decision_2026-06-16_discovery-model]]).
- ~~**Granularité documentaire**~~ → **tranché** : 1 doc par entité (événement/PdR) ; protected groupé.
- **Synchronisation** : prochain sujet — cible (réplication des docs souscrits via le broker) vs simulation dans le wallet partagé.
- **Picker d'utilisateurs** : UX de l'écran « Connexion » (saisie libre vs liste des comptes du `sharedWalletShim`).
## Possible Approaches
@@ -0,0 +1,58 @@
---
type: decision
summary: Modèle de découverte des événements — index GLOBAL unique, alimenté via SON INBOX (le créateur y dépose une référence ; l'index est un document possédé, lisible par tous, matérialisé depuis son inbox). Découverte primaire ; relationnel secondaire (participations des connexions). Architecture en 3 étapes : découverte (index) → synchronisation (réplication des docs souscrits) → requête (SPARQL/ORM, LOCAL uniquement). Pas de Group store (index = doc possédé + inbox native) → cohérent avec la matrice. Inbox + watcher de matérialisation réutilisés (même mécanisme que l'inscription au PdR) ; point de dédup/modération naturel.
last_updated: 2026-06-16
---
# Décision 2026-06-16 — Modèle de découverte des événements
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).
## 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).
- **Découverte** : comment j'apprends qu'il existe, pour le lire ? ← l'objet de cette décision.
## Décision
1. **Index global unique des événements**, **alimenté via son inbox**. Le créateur **ne modifie pas l'index directement** : il **dépose une référence de son événement dans l'inbox de l'index**. L'index est un **document possédé** (lecture publique), **matérialisé depuis son inbox** (un watcher ingère les dépôts → ajoute les entrées). Découpage en **index communautaires** = plus tard.
2. **Découverte primaire = cet index global.**
3. **Relationnel = axe secondaire**, en surimpression : (a) page d'un ami → ses participations (événements passés / à venir) ; (b) sur la liste globale, marquer si une de mes connexions participe. Repose sur les **participations** (périmètre *protected*, visibles des connexions) — **aucune brique nouvelle**.
## Architecture en 3 étapes (cadre directeur)
`découverte → synchronisation → requête`
1. **Découverte** : l'**index** donne les NURIs des documents-événements.
2. **Synchronisation** : s'abonner à ces documents → ils se **répliquent en local** (verifier : `self.repos` + dataset oxigraph).
3. **Requête** : interroger ce qui est **désormais local** (tri par date, limite, réactivité). **SPARQL/ORM ne portent que sur le local** (`resolve_target_for_sparql` cherche dans `self.repos` ; on ne requête pas ce qui n'est pas chargé).
**Corollaire** : une requête réactive **ne remplace pas l'index** — elle s'exécute à l'étape 3, sur l'union locale que 1-2 ont constituée. On ne synchronise pas ce qu'on n'a pas découvert.
État de la couche requête : l'**ORM (`useShape`) est réactif mais scopé par graphes, sans `ORDER BY`/`LIMIT`** (tri/limite en JS). Une **souscription SPARQL réactive** (`SELECT … ORDER BY … LIMIT n` auto-réévaluée) serait l'idéal de l'étape 3 — **à vérifier dans le SDK** (non confirmée). Si absente : ORM + tri JS.
## Granularité documentaire (rappel, cf. discussion)
Chaque **événement / PdR = son propre document** (adressable, futur inbox du PdR). L'**index global liste des références** (NURIs) vers ces documents — pas une copie dénormalisée (la dénormalisation « résumé dans l'index » est une optimisation d'échelle ultérieure).
## Conséquences
- **Pas de Group store** (correction du 2026-06-17). L'index n'est **pas** à écriture ouverte : c'est un **document possédé** (lecture publique) **+ inbox native** (primitive présente sur tout document). Personne n'écrit l'index sauf son propriétaire (via la matérialisation des dépôts d'inbox). Donc on **reste dans le modèle « 3 stores + Dialog + inboxes, sans Group store »** de [[brief_2026-05-18_authorization-matrix]] — la matrice **reste cohérente**, contrairement à ce qu'on avait d'abord cru.
- **Un seul mécanisme réutilisé** : l'**inbox + le watcher de matérialisation** servent **à la fois** la soumission d'un événement à l'index **et** l'inscription à un PdR. Même API (`inbox.post`), même traitement.
- **Point de dédup / modération naturel** : la matérialisation (inbox → index) est l'endroit où détecter les doublons / modérer **avant** insertion. Donne une prise concrète à [[brief_2026-06-15_event-deduplication]] ; logique de dédup non spécifiée ici.
- **Propriétaire de l'index — modèle cible à revoir (corrigé 2026-06-19).** Le « service dédié avec son propre wallet qui partage l'index en lecture libre » était **incorrect** : dans NextGraph, **apps et services sont mono-utilisateur** et il n'y a **pas de données globales** ([[knowledge_apps-and-services]]). Le seul chemin entrevu pour un **document global** est une **app singleton** liée à l'utilisateur-**développeur**, qui administre ce document global — mais c'est **non implémenté et incertain**, et **d'autres voies plus simples** sont possibles. **À creuser plus tard.** La mécanique de soumission tient quand même : un document d'index **alimenté via son inbox** (dépôt par le créateur + matérialisation par l'administrateur). En **stopgap** : l'index est un document du **wallet partagé** (les clients ne peuvent pas lire un autre wallet) ; un **curateur émulé** matérialise les dépôts ; les lecteurs s'abonnent. Cela **remplace** le fan-out-sur-tous-les-comptes (une dérive).
## Alternatives écartées
- **Index à écriture ouverte** (le créateur écrit l'index directement) : écartée — imposait un document collaboratif (Group store), bloqué SDK, et exposait l'index à la corruption. Remplacée par **dépôt dans l'inbox de l'index** + matérialisation par le propriétaire.
- **Découverte purement relationnelle** (connexions + `social_query`) : écartée comme modèle **primaire** (on veut une liste globale) ; **gardée comme axe secondaire**.
- **Pas d'index, requête réactive directe** : impossible — SPARQL local seulement (cf. étape 3).
- **Index par-utilisateur + fan-out sur tous les comptes** (état antérieur du stopgap) : remplacé par l'index global unique.
## See Also
- [[brief_2026-06-15_shared-wallet-shim]] — le stopgap (index global + inbox ; remplace le fan-out par-compte)
- [[brief_2026-05-18_authorization-matrix]] — **reste cohérente** : pas de Group store (index = doc possédé + inbox)
- [[brief_2026-05-21_fork-nextgraph-inbox]] — l'inbox (mécanisme réutilisé pour l'index)
- [[brief_2026-06-15_event-deduplication]] — doublons : la matérialisation inbox→index est le point de dédup
- [[knowledge_stores-permissions]] — inbox native sur tout document ; SPARQL/local
@@ -0,0 +1,67 @@
---
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-06-17
---
# Décision 2026-06-17 — Librairie « ng-eventually-js » (polyfill encapsulé)
Tout le polyfill qui compense l'immaturité de NextGraph (pas de lecture cross-wallet, pas de capabilities ni d'inbox exposées au SDK, pas de Group store) est **sorti de l'app Festipod** et encapsulé dans une **librairie générique externe**. But : l'app ne voit **aucune** de cette complexité, et **migrer = remplacer la dépendance par le vrai SDK**.
## Principe directeur
1. **Forme client = identique au SDK.** Ce que le code applicatif appelle a **exactement** les signatures de `@ng-org/web` / `@ng-org/orm`. Mécanisme : un **Proxy** qui forwarde tout vers le vrai SDK et **n'override que le nécessaire** ; l'ORM (`useShape`/set réactif) est enveloppé. Migration = **alias de build** retiré (l'app importe `@ng-org/*`, résolus vers le wrapper pendant le polyfill) → le code applicatif ne mentionne jamais le wrapper.
2. **Compensation « à côté », jamais dans le métier.** Le code applicatif est écrit *comme si* l'infra cible existait ; la compensation vit dans la librairie.
3. **Générique, zéro Festipod.** La lib ne connaît que des mécanismes et les scopes NextGraph natifs. Le domaine (shapes, actes d'attribution de droits, collections concrètes) est **fourni par le consommateur**.
## Décision
### Repo & packaging
- **Repo** : `/home/sylvain/projects/nextgraph/ng-eventually-js`**hors du repo Festipod** (sibling de `nextgraph-rs`, `orm-tests`, `expense-tracker`), pour éviter toute confusion.
- **Un seul package pour l'instant** (préfixe commun `@ng-eventually` réservé) :
- **`@ng-eventually/client`** — le wrapper **SDK-identique** + les polyfills qui, en cible, sont assurés **par le broker/verifier** (donc *retirés* à la migration) : login du wallet partagé, **enforcement des capabilities** (filtre de lecture + garde d'écriture), méthodes **anticipées** (caps, inbox `post`). **L'app Festipod ne dépend QUE de ce package.** Entrée principale = surface **SDK-identique** ; le bootstrap polyfill (le seul non-SDK) est isolé sous `@ng-eventually/client/polyfill`.
- **Curateur d'index — retiré / différé (2026-06-21).** Le package `@ng-eventually/service` a été **supprimé du scaffold** : son modèle (« backend à données globales ») était **incorrect** — NextGraph est **mono-utilisateur sans données globales** ([[knowledge_apps-and-services]]) — et le mécanisme cible d'index global (**app singleton** ? voie plus simple ?) est **incertain et différé**. Le curateur (qui ne doit **jamais** être chargé côté client) sera réintroduit comme **package séparé** quand le mécanisme sera tranché.
### Comment les mécanismes tranchés s'y logent
- **Identité / login** : le client fixe l'utilisateur courant (username en polyfill ; wallet en cible — [[decision_2026-06-15_shared-wallet-login-flow]]).
- **Droits d'accès** : **capabilities émulées** comme données (grants attachés aux documents), enforcées **génériquement** par le client. L'app **attache les grants** via des opérations de cap anticipées (créer public, accorder à une connexion…) — **comme en cible**. Aucune politique n'est injectée ; seuls les shapes et les *actes* d'attribution viennent du consommateur.
- **Inbox** : `inbox.post(...)` (signature anticipée) côté client ; **matérialisation** par un **curateur** (package séparé, **différé**). Mécanisme réutilisé pour l'inscription PdR **et** la soumission à l'index.
- **Découverte** : index **alimenté via son inbox** ([[decision_2026-06-16_discovery-model]]). Le client **dépose** (inbox) + **lit** (abonnement) ; un **curateur** matérialise. Le **propriétaire cible** de l'index reste à décider (app singleton ?, incertain — [[knowledge_apps-and-services]]).
- **Synchronisation** : `s'abonner à un document` (natif). En polyfill, wallet partagé ⇒ sync multi-device native entre sessions.
### Tests
- Les tests du **polyfill contre le vrai broker** vivent **dans la lib** (sa propre suite). Festipod teste ses features contre l'**API propre de la lib, mockée** (rapide, sans broker).
## Conséquences
- **Festipod ne dépend que de `@ng-eventually/client`** ; la complexité du polyfill est invisible côté app ; rien de Festipod dans la lib.
- **Migration** : retirer l'alias de build + l'appel de bootstrap → le client redevient le vrai SDK ; **traduire les grants émulés en vraies caps** (étape de données). Le **mécanisme cible de l'index global** reste à décider (app singleton ?, [[knowledge_apps-and-services]]) — ce n'est **pas** un backend. Le code applicatif ne bouge pas.
- Le [[brief_2026-06-15_shared-wallet-shim]] décrit désormais **comment Festipod consomme `ng-eventually`** (les mécanismes y sont *réalisés par la lib*), plus une implémentation interne à l'app.
## Statut d'intégration (2026-06-22)
Premier branchement réalisé, **en passthrough** (la lib forwarde tout vers le vrai SDK ; mécanismes du polyfill encore stubés) :
- `@ng-eventually/client` ajouté en **dépendance locale** de Festipod (`file:../../nextgraph/ng-eventually-js/packages/client`).
- **`useShape`** importé depuis `@ng-eventually/client` (`useShapeWithDefaults`, `harness-ng`) ; **vrai SDK injecté** via `configure({ ng, useShape })` dans `ngSession` (`@ng-eventually/client/polyfill`).
- Types `NgLike`/`UseShapeLike` de la lib **assouplis** pour accepter le vrai SDK.
- **Validé** : build Festipod · `@ui` 4/4 · **`@data` 8/8 contre le broker** · lib (typecheck + 4 tests). Comportement identique (passthrough) → la plomberie de remplacement est prouvée.
Reste à implémenter dans la lib (les stubs `TODO`) : filtre de lecture sur l'ORM réactif, garde d'écriture, `inbox.post`, login wallet partagé.
## 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**.
- **Signatures anticipées** (caps, inbox) : à ajuster si l'API officielle NextGraph diffère (point unique dans la lib).
- **Scope npm** `@ng-eventually` vs préfixe non-scopé `ng-eventually-*` (à confirmer) ; publication éventuelle plus tard.
- **Exécution du curateur** (quand réintroduit) : processus dédié (Node, API `nextgraph`) vs watcher idempotent — à trancher à l'implémentation.
- **Enveloppe de l'ORM réactif** (filtrer un `DeepSignalSet` vivant, garder les écritures) = le morceau technique le plus délicat.
## See Also
- [[brief_2026-06-15_shared-wallet-shim]] — le polyfill Festipod, réalisé par cette lib
- [[decision_2026-06-16_discovery-model]] — index alimenté via son inbox (propriétaire cible à revoir)
- [[knowledge_apps-and-services]] — apps/services mono-utilisateur, pas de données globales (corrige le modèle « service »)
- [[decision_2026-06-15_shared-wallet-login-flow]] — utilisateur courant / login
- [[knowledge_integration-model]] — `@ng-org/web` est déjà un Proxy (d'où le wrapper)
- [[knowledge_stores-permissions]] — caps / inbox non exposées au SDK (d'où l'émulation)
@@ -0,0 +1,44 @@
---
type: knowledge
summary: Apps ET services NextGraph sont mono-utilisateur — ils ne voient que ce que l'utilisateur leur met à disposition, PAS de données globales. Toute app/service a un document de settings local. Une app non-singleton est instanciée plusieurs fois (ex. 1 instance par fichier ouvert). Une app SINGLETON est mono-utilisateur mais liée à un utilisateur précis (le développeur) et peut détenir un document global administré par lui → seul chemin entrevu pour un index global, mais NON implémenté et incertain.
---
# Apps et services NextGraph : mono-utilisateur, pas de données globales
Modèle d'exécution des applications et services dans NextGraph (système externe).
Important parce qu'il **invalide** l'idée d'un « service avec son propre wallet
qui partagerait des données globales ».
## Règles
- **Apps ET services sont mono-utilisateur.** Ils ne voient que **ce que
l'utilisateur leur met à disposition**. Il n'y a **pas de données globales**
nativement, ni de service central qui détiendrait des données partagées.
- **Document de settings local.** Toute app — même singleton — et tout service
dispose d'un **document de settings**, qui permet à l'utilisateur de la
paramétrer.
- **Apps multi-instances.** Une app **non-singleton** peut être **instanciée
plusieurs fois** par l'utilisateur. Exemple : un traitement de texte est
instancié autant de fois qu'il y a de fichiers ouverts avec lui.
- **Apps singleton.** Aussi **mono-utilisateur**, mais **liées à un utilisateur
particulier (le développeur)**. Une app singleton **peut détenir un document
global**, **administré par cet utilisateur**.
## Conséquence : le « document global » (ex. index)
- Le seul chemin entrevu pour un **document global** (un index global de
découverte, par exemple) est l'**app singleton** : le document global est
administré par l'utilisateur-développeur lié à cette app.
- **Mais : non implémenté aujourd'hui, et le choix n'est pas garanti.** D'autres
voies plus simples sont possibles. **À creuser plus tard.**
- **Ce qui était incorrect** : un « service dédié avec son propre wallet qui
partage l'index en lecture libre » — ça n'existe pas dans le modèle NextGraph
(un service est mono-utilisateur, sans données globales). Voir la correction
dans [[decision_2026-06-16_discovery-model]].
## See Also
- [[decision_2026-06-16_discovery-model]] — l'index global : propriétaire cible à revoir (app singleton, incertain)
- [[decision_2026-06-17_eventually-library]] — le package `@ng-eventually/service` (fondé sur ce modèle incorrect) a été **retiré/différé**
- [[knowledge_stores-permissions]] — stores, caps, inbox
- [[knowledge_integration-model]] — modèle d'intégration iframe / verifier