docs(concepts): migrate project docs into 7 concepts + code-grounded audit
Migrate .project/{knowledge,decisions,briefs} and the always-loaded
AGENTS.md/CLAUDE.md into the in-repo `concept` system (hook-delivered,
typed leaves). Then audit the actual code to verify the migrated doctrine
and capture knowledge that lived only in the source.
Concepts (53 leaves):
- functional-domain — produit : point de rencontre greffé, acteurs, déduplication
- app-architecture — modules, invariant d'imports, routing, écrans, styling-system,
screen-pattern, cookbook d'ajout d'écran
- tech-stack — Bun-first, APIs, build pipeline, deployment (Dockerfile), commandes
- data-layer — NextGraph mono-store, shapes, modes, règles + caveats (suppression,
champs non persistés, internals du contexte)
- bdd-testing — Cucumber multi-couches, contrat de couches, harness, cookbook
- app-security — posture actuelle (mono-store, confiance broker), auth wallet,
brief matrice d'autorisations cible
- nextgraph-platform — NextGraph système externe + briefs (multi-store, shim, fork)
Audit corrections:
- décision SPARQL-delete annulée (superseded) → caveat (le code utilise ngSet.delete,
persistance possiblement partielle)
- divergences relevées : routing path-based (pas hash), thème moderne sous components/sketchy,
ConnectScreen hors registre, build:orm au chemin périmé, champs d'event perdus en connecté
Strip migrated sources; AGENTS.md/CLAUDE.md réduits au cœur (but, invariants,
carte des concepts) + pointeurs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,274 +0,0 @@
|
||||
# Matrice d'autorisations et inventaire des requêtes
|
||||
|
||||
**Status:** Incubating — analyse en cours
|
||||
**Last updated:** 2026-05-18
|
||||
|
||||
## Context
|
||||
|
||||
Préalable au refactor multi-store ([brief](./multi-store-refactor.md)) et à toute évolution multi-user. La structure de stores NextGraph cible doit être *dérivée* de :
|
||||
|
||||
1. Une matrice d'autorisations (qui peut faire quoi sur quel type de donnée).
|
||||
2. Un inventaire des requêtes nécessaires (lectures, abonnements, écritures par écran).
|
||||
3. Les partitions naturelles qui en découlent (regroupements de données qui partagent autorisations *et* schéma d'accès).
|
||||
|
||||
Ce brief porte cette analyse. Il alimentera la décision finale sur la structure de stores.
|
||||
|
||||
## Cadre
|
||||
|
||||
### Acteurs (tous authentifiés)
|
||||
|
||||
- `Alice` — l'utilisateur dont on adopte le point de vue ; propriétaire de la donnée en focus (varie par type : auteur d'un message, titulaire d'un profil, inscrit à un PdR, hôte d'un PdR…)
|
||||
- `Bob` — un autre utilisateur, second protagoniste utilisé pour les relations bilatérales (connexion à Alice, etc.)
|
||||
- `D` — Déclarant d'un événement (celui qui a inséré la référence dans Festipod ; pas l'organisateur réel)
|
||||
- `H` — Hôte d'un point de rencontre (celui qui l'a créé)
|
||||
- `I` — Inscrit à un point de rencontre
|
||||
- `C` — Connexion (« ami ») d'un autre acteur lié à la donnée
|
||||
- `U` — Utilisateur authentifié quelconque, sans relation à la donnée
|
||||
|
||||
### Verbes
|
||||
|
||||
- `créer`
|
||||
- `lire` (one-shot)
|
||||
- `s'abonner` (lecture longue / réactive)
|
||||
- `modifier`
|
||||
- `supprimer`
|
||||
|
||||
### Conventions
|
||||
|
||||
`✓` autorisé · `✗` interdit · `cond` autorisé sous condition (notée) · `—` sans objet
|
||||
|
||||
## Décisions cadre (acquises)
|
||||
|
||||
- **Tous les utilisateurs sont authentifiés.** Pas d'accès anonyme.
|
||||
- **Points de rencontre publics universels.** Tout utilisateur peut lire et s'abonner.
|
||||
- **Création de point de rencontre ouverte à tous.** Pas de prérequis (adhésion, invitation).
|
||||
- **Hôte = détenteur technique des droits d'écriture** sur un point de rencontre. À ce stade : 1 hôte par PdR, celui qui l'a créé. Le fait d'être hôte est public (l'offre n'a de sens que si on sait qui la fait).
|
||||
- **Informations personnelles = réservées au réseau.** Toute donnée qualifiée de « personnelle » n'est visible qu'à l'utilisateur titulaire et à ses connexions. Inclut explicitement :
|
||||
- les participations à un événement ou un point de rencontre,
|
||||
- l'intégralité du profil d'un utilisateur,
|
||||
- la liste de connexions d'un utilisateur,
|
||||
- et par extension, tout état déclaratif dont la divulgation à des tiers serait une fuite de vie privée.
|
||||
Le statut « public » (PdR, événement) et le statut « personnel » (profil, participations, liste de connexions) coexistent au sein du même utilisateur.
|
||||
- **Connexion bilatérale.** Une connexion (« lien d'amitié ») n'existe qu'après acceptation par les deux côtés. Modélisée en deux objets : `DemandeDeConnexion` (unilatérale, transitoire) et `Connexion` (bilatérale, persistante).
|
||||
- **Notification d'inscription via l'inbox NextGraph du PdR.** L'acte « s'inscrire à un PdR » est composite : (a) écriture d'un objet `Inscription` dans le `protected_store` de l'inscrit, et (b) dépôt d'un lien (DID cap) pointant vers cet objet dans l'**inbox** du document PdR. L'inbox est un primitive natif de chaque document NextGraph (cf. doc protocole : *« each document has an inbox, which is used in this case to drop the link »*). L'identification du sender côté hôte se fait par résolution du DID contre le graphe de connexions de l'hôte :
|
||||
- si l'inscrit est connexion de l'hôte → l'hôte a la capability pour résoudre le lien, voit l'inscription complète (identité + éventuel message) ;
|
||||
- sinon → le lien reste opaque, l'hôte voit *« quelqu'un (DID …) s'est inscrit »* sans pouvoir aller plus loin.
|
||||
L'anonymat partiel est ainsi natif aux capabilities, pas une logique applicative.
|
||||
- **Adhésion à une communauté : hors périmètre actuel.** Le rôle « Membre de communauté » n'est pas analysé ici.
|
||||
- **Suivi de communauté ou d'utilisateur : hors périmètre actuel.** À reprendre quand la fonctionnalité de discovery par abonnement sera traitée.
|
||||
|
||||
## Matrice par type de donnée
|
||||
|
||||
### Point de rencontre
|
||||
|
||||
| Verbe | Alice (= Hôte) | I (autre inscrit) | D (déclarant de l'événement parent) | U (utilisateur lambda) |
|
||||
|---|---|---|---|---|
|
||||
| créer | ✓ (l'acte de créer rend l'utilisateur hôte) | — | ✗ | ✓ (l'acte le rend hôte) |
|
||||
| lire | ✓ | ✓ | ✓ | ✓ |
|
||||
| s'abonner | ✓ | ✓ | ✓ | ✓ |
|
||||
| modifier | ✓ | ✗ | ✗ | ✗ |
|
||||
| supprimer | ✓ | ✗ | ✗ | ✗ |
|
||||
|
||||
**Notes :**
|
||||
- Pas de différenciation `C` (connexion de l'hôte) — les connexions sont un filtre d'affichage côté UI, pas un droit d'accès, puisque tout est public.
|
||||
- Le `D` n'a pas de droit particulier sur les PdR greffés sur son événement déclaré — il a juste déclaré la référence.
|
||||
|
||||
### Inscription à un point de rencontre
|
||||
|
||||
L'objet `Inscription` lie un utilisateur et un point de rencontre. Représente l'engagement à participer. **Donnée personnelle** — visible uniquement par l'inscrit et ses connexions.
|
||||
|
||||
**L'acte de créer une inscription est composite** (cf. décision cadre sur l'inbox) :
|
||||
- (a) écriture de l'objet `Inscription` dans le `protected_store` de l'inscrit,
|
||||
- (b) dépôt d'un lien (DID cap) pointant vers cet objet dans l'**inbox du document PdR**.
|
||||
|
||||
| Verbe | Alice (l'inscrite) | C (connexion d'Alice) | H (hôte du PdR) | I (autre inscrit) | U (utilisateur lambda) |
|
||||
|---|---|---|---|---|---|
|
||||
| créer (= acte composite (a)+(b)) | ✓ | — | ✗ | ✗ | ✓ (l'acte fait d'Alice l'inscrite) |
|
||||
| lire le contenu de l'inscription | ✓ | ✓ | cond : ✓ si H ∈ connexions(Alice) ; sinon voit le lien dans l'inbox sans pouvoir le résoudre | cond : ✓ si I ∈ connexions(Alice) | ✗ |
|
||||
| s'abonner | ✓ | ✓ | cond (idem) | cond (idem) | ✗ |
|
||||
| lire l'inbox du PdR (entrées brutes, sans résolution) | — | — | ✓ | ✗ | ✗ |
|
||||
| modifier | ? **à trancher** (selon champs) | ✗ | ✗ | ✗ | ✗ |
|
||||
| supprimer | ✓ (se désinscrire ; doit aussi retirer le lien de l'inbox du PdR si possible) | ✗ | cond : ✓ uniquement modération de l'inbox (refuser / retirer le lien) ; ne supprime pas l'objet `Inscription` de Bob | ✗ | ✗ |
|
||||
|
||||
**Visibilité hôte : résolue.** Combinée à l'inbox NextGraph, la mécanique donne *« inscription identifiée si l'hôte est connecté à l'inscrit, anonyme sinon »* — natif via les capabilities, pas de logique applicative à ajouter. Plus de question ouverte sur ce point.
|
||||
|
||||
**Questions ouvertes restantes :**
|
||||
- **Champs modifiables d'une inscription.** Booléen seul, ou champs additionnels (commentaire, statut « peut-être », nombre d'accompagnants) ?
|
||||
- **Suppression côté inbox.** Quand Alice se désinscrit, peut-elle retirer le lien qu'elle avait déposé dans l'inbox d'un document qu'elle ne contrôle pas ? À vérifier dans le mécanisme protocolaire NextGraph — soit le déposant garde un droit de retrait sur ses propres dépôts, soit l'hôte doit faire le ménage. À creuser avec la doc protocole quand le sujet sera repris.
|
||||
|
||||
### Événement
|
||||
|
||||
| Verbe | Alice (= D, déclarant) | H (hôte d'un PdR greffé) | U (utilisateur lambda) |
|
||||
|---|---|---|---|
|
||||
| créer | ✓ (l'acte rend déclarant) | — | ✓ (l'acte le rend déclarant) |
|
||||
| lire | ✓ | ✓ | ✓ |
|
||||
| s'abonner | ✓ | ✓ | ✓ |
|
||||
| modifier | ? **à trancher** | ? **à trancher** | ? **à trancher** |
|
||||
| supprimer | ? **à trancher** | ✗ | ✗ |
|
||||
|
||||
**Questions ouvertes :**
|
||||
- **Qui peut modifier un événement déclaré ?** Le déclarant seul (modèle propriétaire) ? Tout utilisateur (modèle wiki, pour compléter/corriger) ? Personne après création (modèle immuable, pour éviter les modifications mal intentionnées) ? Cette question est centrale pour le défi de déduplication évoqué dans le README — un modèle wiki facilite la convergence, un modèle propriétaire complique.
|
||||
- **Qui peut supprimer ?** Si le déclarant supprime, que deviennent les PdR greffés (orphelins ? supprimés en cascade ? l'événement reste mais marqué supprimé ?) ?
|
||||
|
||||
### Profil utilisateur
|
||||
|
||||
**Rien dans le profil n'est public.** Le profil se divise en deux périmètres seulement :
|
||||
|
||||
- **Profil réseau** — visible par Alice et ses connexions (tout ce qui décrit l'utilisateur : nom d'affichage, avatar, bio, ville, intérêts…).
|
||||
- **Profil privé** — visible par Alice seule (paramètres, email, préférences notifications, langue, etc.).
|
||||
|
||||
| Verbe | Alice | C (connexion) | U (utilisateur lambda) |
|
||||
|---|---|---|---|
|
||||
| créer | ✓ (à l'inscription) | — | — |
|
||||
| lire — *profil réseau* | ✓ | ✓ | ✗ |
|
||||
| lire — *profil privé* | ✓ | ✗ | ✗ |
|
||||
| s'abonner | ✓ | ✓ (réseau) | ✗ |
|
||||
| modifier | ✓ | ✗ | ✗ |
|
||||
| supprimer (compte) | ✓ | ✗ | ✗ |
|
||||
|
||||
**Questions ouvertes — tension à résoudre :**
|
||||
|
||||
Cette décision crée une **tension forte** avec la visibilité publique des points de rencontre. Un PdR est lisible par tous, mais son hôte ne devrait *pas* être identifiable par un utilisateur lambda. Comment un visiteur perçoit l'hôte d'un PdR ?
|
||||
|
||||
Trois positions possibles :
|
||||
|
||||
- (i) **Pseudonyme par DID seul.** Un lambda voit « hôte : `did:ng:…123` » sans nom ni avatar. Le nom et l'avatar se résolvent uniquement si le visiteur est une connexion de l'hôte.
|
||||
- (ii) **Identité dénormalisée dans l'offre.** L'hôte choisit, au moment de créer le PdR, quels éléments d'identité il *accepte* d'exposer dans cette offre publique (par ex. juste un prénom et une photo). Ces données vivent dans l'objet PdR, pas dans le profil. Le profil reste fermé, mais l'utilisateur consent à publier une « carte de visite » par PdR. Distinction conceptuelle nette : *publier sous un visage choisi* ≠ *exposer son profil*.
|
||||
- (iii) **Anonymat de l'hôte.** Le PdR est offert sans identité visible publiquement ; un lambda voit « un PdR à tel endroit, telle heure » sans savoir qui héberge. Identité révélée seulement aux connexions.
|
||||
|
||||
À trancher — c'est la pièce manquante pour que la matrice soit cohérente.
|
||||
|
||||
**Autres questions ouvertes :**
|
||||
- **Composition exacte de chaque périmètre.** Champ par champ (bio → réseau ? ville → réseau ? URL personnelle → privé ?). Sous-tableau à faire quand la liste sera arrêtée.
|
||||
- **Le username.** S'il sert d'identifiant stable de connexion ou de découverte, il est *de facto* visible aux personnes qui le connaissent déjà. Public, réseau, ou supprimé du modèle ?
|
||||
|
||||
### Connexion (lien d'amitié)
|
||||
|
||||
**La connexion est bilatérale** : les deux utilisateurs doivent accepter pour qu'elle existe. Deux objets distincts en découlent :
|
||||
|
||||
- `DemandeDeConnexion` — unilatérale, créée par l'initiateur, en attente d'acceptation par le destinataire.
|
||||
- `Connexion` — bilatérale, persistante, créée à l'acceptation. C'est cet objet qui ouvre l'accès aux données personnelles des deux côtés.
|
||||
|
||||
La liste de connexions d'Alice est une **donnée personnelle** (même principe que les participations) : visible à Alice et aux connexions d'Alice, pas au monde.
|
||||
|
||||
| Verbe | Alice (initiatrice) | Bob (l'autre côté de la connexion) | C (autre connexion d'Alice) | U (utilisateur lambda) |
|
||||
|---|---|---|---|---|
|
||||
| créer la demande de connexion | ✓ | — | — | — |
|
||||
| accepter la demande | — | ✓ | — | ✗ |
|
||||
| lire la liste de connexions d'Alice | ✓ | ✓ | ✓ | ✗ |
|
||||
| s'abonner à la liste de connexions d'Alice | ✓ | ✓ | ✓ | ✗ |
|
||||
| modifier | — | — | — | — |
|
||||
| supprimer (rompre la connexion Alice↔Bob) | ✓ | ✓ | ✗ | ✗ |
|
||||
|
||||
**Questions ouvertes :**
|
||||
- **Granularité de visibilité côté Bob.** Bob voit-il *toute* la liste de connexions d'Alice (au même titre que les autres connexions), ou seulement le lien Alice↔Bob ? Conséquence du principe « personnel = réseau » : Bob, étant connexion d'Alice, accède au même périmètre que les autres connexions — donc toute la liste.
|
||||
- **Découvrabilité réciproque des connexions « amis d'amis ».** Si Alice est connectée à Bob et Bob à Carole, Alice peut-elle voir que Bob est connecté à Carole ? Conséquence du principe : non, sauf si Carole est aussi connectée directement à Alice. À confirmer pour les besoins de découverte (« amis d'amis »).
|
||||
|
||||
## Hors périmètre actuel
|
||||
|
||||
À reprendre quand ces concepts deviendront actifs :
|
||||
|
||||
- **Communauté d'intérêt** (membres, modération, création)
|
||||
- **Adhésion à une communauté**
|
||||
- **Liste curated** (création, partage, abonnement)
|
||||
- **Suivi d'utilisateur ou de communauté** pour discovery distribuée
|
||||
|
||||
## Inventaire des requêtes par écran
|
||||
|
||||
*À remplir une fois la matrice des autorisations stabilisée.*
|
||||
|
||||
Schéma prévu :
|
||||
|
||||
| Écran | Lectures one-shot | Abonnements | Écritures | Acteur déclencheur |
|
||||
|---|---|---|---|---|
|
||||
|
||||
Écrans à analyser (depuis [AGENTS.md](../../AGENTS.md#routing)) :
|
||||
|
||||
- `WelcomeScreen` `/`
|
||||
- `LoginScreen` `/login`
|
||||
- `HomeScreen` `/home`
|
||||
- `EventsScreen` `/events`
|
||||
- `CreateEventScreen` `/events/new`
|
||||
- `EventDetailScreen` `/events/:id`
|
||||
- `UpdateEventScreen` `/events/:id/edit`
|
||||
- `InviteScreen` `/events/:id/invite` (à voir si encore pertinent)
|
||||
- `ParticipantsListScreen` `/events/:id/participants`
|
||||
- `MeetingPointsScreen` `/events/:id/meeting-points`
|
||||
- `ProfileScreen` `/profile`
|
||||
- `UpdateProfileScreen` `/profile/edit`
|
||||
- `FriendsListScreen` `/profile/friends`
|
||||
- `ShareProfileScreen` `/profile/share`
|
||||
- `UserProfileScreen` `/users/:id`
|
||||
- `SettingsScreen` `/settings`
|
||||
|
||||
## Partitions naturelles dérivées
|
||||
|
||||
Heuristique : on regroupe dans un même store les données qui (a) partagent leur cellule d'autorisation pour les verbes d'écriture, *et* (b) sont accédées ensemble dans la majorité des requêtes.
|
||||
|
||||
À partir des seuls points validés (les questions ouvertes seront tranchées plus tard), trois périmètres distincts émergent. **Ces trois périmètres correspondent presque parfaitement aux trois stores NextGraph par défaut d'un utilisateur.**
|
||||
|
||||
### Trois périmètres par utilisateur
|
||||
|
||||
| Périmètre | Écriture | Lecture | Données qui y vivent (validées) |
|
||||
|---|---|---|---|
|
||||
| **Public** | Alice seule (titulaire) | Tous les utilisateurs authentifiés | PdR dont Alice est hôte ; événements qu'Alice a déclarés *(sous réserve du modèle d'écriture événement, à trancher)* |
|
||||
| **Réseau / personnel** | Alice seule | Alice + connexions d'Alice | Profil réseau d'Alice ; participations d'Alice à des PdR ; index de la liste des connexions d'Alice |
|
||||
| **Privé** | Alice seule | Alice seule | Profil privé d'Alice (paramètres, email, préférences) |
|
||||
|
||||
### Mapping aux stores NextGraph natifs
|
||||
|
||||
- **Périmètre public ↔ `public_store` d'Alice.** Définition NextGraph : *« everyone can read; only you write »*. Match exact.
|
||||
- **Périmètre réseau ↔ `protected_store` d'Alice.** Définition NextGraph : *« share data with other users, but they will need a special link and permission »* et *« functions as a protected social profile »*. C'est précisément le périmètre « réseau » du modèle Festipod.
|
||||
- **Périmètre privé ↔ `private_store` d'Alice.** Définition NextGraph : *« only you have access to »*. Match exact.
|
||||
|
||||
### Cas particulier : la Connexion bilatérale
|
||||
|
||||
Une `Connexion` Alice↔Bob est une donnée à *deux* écrivains (Alice et Bob peuvent tous deux la rompre, mutuellement la voir, etc.). Elle ne tient dans aucun store individuel d'un seul utilisateur. NextGraph dispose d'un primitive natif pour ce cas : le **Dialog store** *(« A two-person-only store for direct messages and shared content between individual users »)*.
|
||||
|
||||
Modèle dérivé :
|
||||
|
||||
- **Une `Connexion` Alice↔Bob = un Dialog store** entre Alice et Bob, contenant l'objet `Connexion` et — naturellement — la matière à conversation/messagerie directe future.
|
||||
- **L'index « toutes les connexions d'Alice »** vit dans le `protected_store` d'Alice et liste les NURIs des Dialog stores auxquels elle participe.
|
||||
- La **`DemandeDeConnexion`** (transitoire, asymétrique avant acceptation) peut vivre :
|
||||
- soit dans le Dialog store provisoire créé dès l'envoi de la demande (qui devient une Connexion à l'acceptation),
|
||||
- soit dans un objet à part dans le `public_store` du destinataire (« boîte de réception » publique des demandes). À trancher selon la mécanique d'invitation que NextGraph permettra côté SDK.
|
||||
|
||||
### Inbox du document PdR
|
||||
|
||||
Le document PdR (qui vit dans le `public_store` de l'hôte) dispose nativement d'une **inbox** (primitive NextGraph, présente sur tout document). Elle est utilisée pour :
|
||||
|
||||
- recevoir les **dépôts d'inscription** (liens DID cap pointant vers l'objet `Inscription` chez chaque inscrit) ;
|
||||
- potentiellement, plus tard, recevoir des commentaires ou d'autres signaux non-éditeurs sur le PdR.
|
||||
|
||||
L'inbox **n'est pas un store séparé**, c'est un attribut du document PdR. Pas d'impact sur la dérivation des partitions.
|
||||
|
||||
### Ce qui ne demande aucun Group store
|
||||
|
||||
Sur le périmètre actuellement validé, **aucune donnée ne demande de Group store**. Toutes les autorisations validées (PdR + inbox, profil, participations, connexions) tiennent dans la combinaison :
|
||||
|
||||
- 3 stores natifs par utilisateur : `public_store` + `protected_store` + `private_store`,
|
||||
- Dialog stores pour les connexions bilatérales,
|
||||
- inboxes natives sur les documents PdR.
|
||||
|
||||
Les Group stores ne deviennent nécessaires que si :
|
||||
|
||||
- le modèle d'écriture événement choisi est « wiki » (plusieurs écrivains sur la même référence événement) ;
|
||||
- ou les communautés / suivi / collaboration multi-hôte sortent du hors-périmètre actuel.
|
||||
|
||||
### Implications pour le brief `multi-store-refactor`
|
||||
|
||||
Le [brief multi-store-refactor](./multi-store-refactor.md) propose une structure à 4 niveaux de Group stores (index communautaire / communauté / event / meeting point). **Cette analyse, sur la base des seules décisions validées, dérive une structure différente** : 3 stores natifs par utilisateur + Dialog stores pour les connexions, sans aucun Group store nécessaire.
|
||||
|
||||
L'écart vient du fait que les concepts qui justifient les Group stores (communautés, collaboration multi-utilisateurs sur un même objet) ont été mis hors périmètre. Quand ils reviendront, des Group stores apparaîtront dans la cible — mais probablement pas selon la hiérarchie initiale, qui sera elle aussi à ré-évaluer à partir d'une matrice étendue.
|
||||
|
||||
### Données restant suspendues aux questions ouvertes
|
||||
|
||||
- **Événement (où vit-il, qui le détient)** dépend du modèle d'écriture (propriétaire / wiki / immuable). Si propriétaire ou immuable : `public_store` du déclarant. Si wiki : nécessite un Group store ou une indirection par une référence externe canonique.
|
||||
- **Identité visible de l'hôte d'un PdR aux yeux d'un lambda** influence la structure du PdR lui-même (option ii « carte de visite dénormalisée » ajoute des champs dans l'objet PdR ; options i et iii ne changent rien). Pas d'impact sur la partition.
|
||||
- **Champs modifiables d'une inscription** : impact mineur sur la structure ; juste sur le schéma de l'objet `Inscription`.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Brief : refactor multi-store](./multi-store-refactor.md) — consommateur principal de cette analyse
|
||||
- [README §Modèle fonctionnel](../../README.md) — source des acteurs et concepts
|
||||
- [Knowledge : data layer](../knowledge/data-layer.md) — état actuel mono-store
|
||||
@@ -1,136 +0,0 @@
|
||||
# Forker NextGraph pour exposer l'inbox au SDK JS
|
||||
|
||||
**Status:** Incubating — aucun travail démarré
|
||||
**Last updated:** 2026-05-21
|
||||
|
||||
## Context
|
||||
|
||||
Festipod doit notifier l'hôte d'un point de rencontre quand quelqu'un s'inscrit, avec **identification si connexion / anonyme sinon** (voir la décision cadre inbox dans [authorization-matrix](./authorization-matrix.md)). L'**inbox** NextGraph est le mécanisme natif idéal — le champ `from` optionnel donne l'anonymat gratuitement — **mais elle n'est pas exposée au SDK JS** (voir [nextgraph-stores-permissions §Inbox](../knowledge/nextgraph-stores-permissions.md)).
|
||||
|
||||
Ce brief évalue l'option de **forker / patcher `nextgraph-rs`** pour l'exposer. Travail non démarré.
|
||||
|
||||
### Posture stratégique (cadrée par l'utilisateur)
|
||||
|
||||
Le fork est **explicitement temporaire et non destiné à être intégré upstream**. Hypothèse de travail : les développeurs de NextGraph finiront par exposer leur **propre** solution d'inbox au SDK JS, **possiblement différente** de notre patch. Quand elle arrivera, on **abandonnera notre fork et on adaptera Festipod à leur solution**.
|
||||
|
||||
Conséquences tant que leur solution n'est pas là :
|
||||
|
||||
- **Maintenir le fork à jour** (rebase régulier sur `upstream/main`, qui bouge vite en `0.1.2-alpha`).
|
||||
- **Déployer le broker (et le ng-app) depuis le fork**, pas depuis les binaires officiels — c'est notre build patché qui doit tourner.
|
||||
- **Surveiller l'upstream** pour détecter l'arrivée de leur API inbox et basculer dès que possible (réduit la dette de maintenance).
|
||||
|
||||
On ne cherche donc **pas** à faire accepter une PR (ce n'est pas le but) ; on assume un fork jetable en attendant.
|
||||
|
||||
## What We Know
|
||||
|
||||
Le travail s'étend sur **trois couches**, pas une :
|
||||
|
||||
1. **Fork SDK** — patch Rust (moteur) + paquets JS clients patchés.
|
||||
2. **Auto-hébergement** — `ngd` + ng-app déployés depuis le fork (Coolify).
|
||||
3. **Intégration dans Festipod** — l'app doit *utiliser* ces libs : appeler l'écriture inbox au bon endroit, modéliser et lire les notifications, câbler le tout.
|
||||
|
||||
Les trois sections ci-dessous les détaillent.
|
||||
|
||||
### Couche 1 — Le patch Rust : 4 fichiers, tous côté moteur (broker vanilla)
|
||||
|
||||
1. **`engine/net/src/types.rs`** — `InboxMsgContent::Link` est aujourd'hui une variante **unit** (stub). Lui donner un payload, ou ajouter une variante (ex. `Notification`) portant le NURI du PdR + un lien vers l'`Inscription`. Ajouter un builder `InboxPost::new_link(...)` calqué sur `new_contact_details` (≈ ligne 3772). `from = None` → anonymat.
|
||||
2. **`engine/verifier/src/request_processor.rs`** — ajouter le bras de commande manquant. Le dispatch n'a **pas** de bras `InboxPost` ; commandes traitées : `OrmStart(Discrete)`, `Fetch`, `FileGet`, `OrmUpdate`, `OrmDiscreteUpdate`, `SocialQueryStart`, `QrCodeProfile(Import)`, `Header`, `Create`, `FilePut`. Idéalement une commande haut-niveau (`NotifyInbox`) qui construit le post côté Rust (garde le scellement crypto en Rust). Calquer sur le bras `SocialQueryStart`.
|
||||
3. **`sdk/js/lib-wasm/src/lib.rs`** — exposer `pub async fn inbox_post_link(session_id, to_inbox_nuri, to_profile_nuri, link, anonymous)`, calqué sur `social_query_start` (prend des NURI string, construit l'`AppRequest`, appelle `local_broker::app_request`).
|
||||
4. **`engine/verifier/src/inbox_processor.rs`** (`process_inbox`) — ajouter le bras de réception qui **matérialise** le message reçu en document dans le store de l'hôte (calquer sur le handler `ContactDetails` qui crée un doc `social:contact`). L'app lit ensuite via ORM/SPARQL — pas de nouvelle API de lecture d'inbox.
|
||||
|
||||
**Résolution d'identité** (connu / anonyme) : tombe gratuitement via SPARQL côté app (JOIN du NURI d'inbox émetteur contre les docs `social:contact`, qui stockent les NURI d'inbox). Probablement zéro Rust supplémentaire.
|
||||
|
||||
**Découverte de l'inbox de l'hôte** : l'inscrit a besoin du NURI d'inbox du `public_store` de l'hôte ; à embarquer dans le doc PdR ou le profil public (le flux QR-code de partage de profil porte déjà cette info).
|
||||
|
||||
### Couche 2 — Déploiement (depuis le fork)
|
||||
|
||||
Détail du modèle dans [nextgraph-integration-model](../knowledge/nextgraph-integration-model.md). Le verifier patché tourne **dans l'iframe ng-app** → il faut **construire et auto-héberger, depuis le fork, le `ngd` + le ng-app** (`app/nextgraph`), puis rebuilder le `@ng-org/web` de Festipod avec `NG_REDIR_SERVER` / `NG_DEV*` pointant sur ce ng-app auto-hébergé. **Aucune réécriture de l'intégration Festipod** (elle reste iframe).
|
||||
|
||||
Précision : le *routage* inbox du broker est déjà natif (un `ngd` officiel routerait l'inbox). Mais comme on auto-héberge de toute façon le ng-app patché (qui embarque le verifier patché), **on déploie toute la stack depuis le fork** — un seul arbre source à maintenir, build cohérent, pas de mélange binaires-officiels / fork.
|
||||
|
||||
- **Local** : `ngd` + ng-app buildés depuis le fork (DEV.md « first run ») ; Festipod buildé avec `NG_DEV` / `NG_DEV_LOCAL_BROKER`.
|
||||
- **Serveur de test** : `ngd` + ng-app du fork déployés sur notre domaine ; Festipod buildé avec `NG_REDIR_SERVER=notre-domaine`.
|
||||
|
||||
### Hébergement sur Coolify
|
||||
|
||||
Auto-héberger = **3 pièces web** derrière notre domaine (détails pérennes dans [nextgraph-integration-model](../knowledge/nextgraph-integration-model.md)) :
|
||||
|
||||
1. **`ngd`** — démon WebSocket **stateful**. Sur Coolify : conteneur avec **volume persistant** pour `--base-path` (RocksDB + clés + PeerId — à ne jamais wiper entre redéploiements), lancé en mode `--domain` derrière le **Traefik de Coolify** (TLS terminé, X-Forwarded-For). Build : pas de Dockerfile officiel utilisable (les 3 fournis sont cassés) → **écrire notre propre Dockerfile multi-stage Rust** (RocksDB exige llvm/clang). Premier démarrage **interactif** (lien d'invitation pour le wallet admin) → à scripter via `ngcli` ou à faire une fois à la main puis persister dans le volume.
|
||||
2. **ng-app** (le frontend iframe, embarquant le wasm patché) — **build statique** (`pnpm webfilebuild`, nécessite pnpm + wasm-pack). Servi comme site statique (buildpack static Coolify ou conteneur nginx).
|
||||
3. **Routage** : un même domaine doit servir le **statique du ng-app** ET proxifier le **WebSocket vers ngd** (le broker ne sert pas de statique). À configurer dans Coolify (routes/domaines).
|
||||
|
||||
Plus **Festipod** lui-même (app Bun → le skill `coolify-hosting` s'applique pour CELLE-CI, mais pas pour le `ngd` Rust).
|
||||
|
||||
**Drivers de complexité** : build Rust+RocksDB sans Dockerfile prêt, conteneur stateful à volume critique, premier-run interactif, et le double-service (statique + WS) sur un domaine. → ops **modéré-à-conséquent**, surtout au premier montage.
|
||||
|
||||
### Couche 1 (libs JS) — Gestion des libs npm clientes
|
||||
|
||||
**On maintient des versions patchées des paquets clients, pas seulement le wasm.** Le fait que les 3 maillons JS soient génériques (proxy `@ng-org/web` → `call_sdk` d'api-web → `Reflect.apply` du worker, cf. [knowledge](../knowledge/nextgraph-integration-model.md)) permet *techniquement* d'atteindre une nouvelle méthode wasm d'écriture sans toucher au JS — mais c'est un **hack** (non typé, non documenté, fragile) qu'on ne retient que comme test rapide, pas comme plan.
|
||||
|
||||
Ce qu'il faut réellement modifier :
|
||||
|
||||
- **`@ng-org/web`** — modifié de toute façon (URL broker, voir ci-dessus) → y ajouter `inbox_post_link` dans la **surface d'API typée + les `.d.ts`**, plutôt qu'un appel string casté.
|
||||
- **Méthodes streamées (cas obligatoire)** — si on lit un jour l'inbox en *flux* (au lieu du doc matérialisé lu via ORM/SPARQL), il faut une entrée dans la table de streaming **des deux côtés** : `E` dans `@ng-org/web` et `streamed_api` dans api-web. Pour la seule **écriture** (requête/réponse), pas nécessaire.
|
||||
- **`@ng-org/orm`** — à modifier **si** on intègre l'écriture inbox au flux ORM (helper, ou couplage écriture `Inscription` + post inbox). Si on appelle `ng.inbox_post_link` directement à côté de l'ORM, pas nécessaire.
|
||||
- **`@ng-org/alien-deepsignals`, `@ng-org/shex-orm`** — a priori inchangés (sans rapport avec l'inbox).
|
||||
|
||||
Donc on porte un **fork JS** (au moins `@ng-org/web`, possiblement `@ng-org/orm`) en parallèle du fork Rust.
|
||||
|
||||
#### Comment Festipod obtient ces libs custom — l'outillage existe déjà
|
||||
|
||||
Le script **`scripts/build-ng-packages.sh`** (alias `bun run build:ng`) fait exactement ça depuis le fork local :
|
||||
|
||||
1. Build des 4 paquets (`alien-deepsignals`, `shex-orm`, `web`, `orm`) depuis `$NEXTGRAPH_RS/sdk/js/*` (défaut `NEXTGRAPH_RS=../../nextgraph/nextgraph-rs`).
|
||||
2. `pnpm pack` → `.tgz` dans `.ng-tarballs/`.
|
||||
3. `bun add .ng-tarballs/ng-org-*.tgz` → **réécrit `package.json`** pour pointer chaque dep vers le tarball local au lieu du registre.
|
||||
|
||||
C'est le **pattern d'origine du projet** : le commit `fd6d408` (« install from npm instead of local tarballs ») l'a abandonné quand les alphas ont été publiées sur npm (suppression de `.ng-tarballs/`). Pour repasser au custom : **réactiver `bun run build:ng`** (le script est toujours présent).
|
||||
|
||||
Nuances :
|
||||
- **`@ng-org/web` est un proxy TS pur (sans wasm)** — le script crée un *stub* `lib-wasm`. Le tarball porte donc l'**API inbox typée + l'URL broker bakée au build**, mais **pas** le wasm (qui vit dans le ng-app auto-hébergé, couche 2).
|
||||
- **Fork temporaire** : le script fait `git pull --ff-only` sur `nextgraph-rs` → le pointer sur notre **branche patchée** (ou retirer le pull) pour builder le fork, pas l'upstream.
|
||||
- **Option complémentaire (rec.)** : patcher `@ng-org/web` pour lire l'URL broker au **runtime** (env/global), pour éviter de rebuilder le tarball à chaque changement de domaine (local/test/prod).
|
||||
|
||||
Flux complet à chaque rebase : patcher le fork → `bun run build:ng` (rebuild tarballs + repointe `package.json`) → `bun install`. Les libs non touchées peuvent rester sur les versions npm publiées.
|
||||
|
||||
### Couche 3 — Intégration dans Festipod
|
||||
|
||||
Exposer la méthode ne suffit pas : le code de l'app doit l'**utiliser**. Plusieurs chantiers, dont certains préexistent à l'inbox (l'app n'est pas encore prête côté données) :
|
||||
|
||||
- **Modéliser le point de rencontre.** Les SHEX (`src/shared/shapes/shex/festipodShapes.shex`) ne définissent que `Event`, `UserProfile`, `Participation` — **pas de `MeetingPoint`** (aujourd'hui local-only), ni d'entité « notification d'inscription ». Ajouter les shapes + `bun run build:orm`.
|
||||
- **Implémenter l'inscription (aujourd'hui un no-op).** Dans `src/shared/context/FestipodDataContext.tsx`, `joinEvent`/`leaveEvent` sont des `console.log('… (local, no-op)')`. Le vrai flux d'inscription à un PdR doit : (a) écrire l'`Inscription` dans le `protected_store` de l'inscrit (ORM, via le multi-store — voir [multi-store-refactor](./multi-store-refactor.md)), **et** (b) appeler `ng.inbox_post_link(...)` pour notifier l'inbox du PdR de l'hôte.
|
||||
- **Porter le NURI d'inbox de l'hôte sur le doc PdR** (ou via lookup profil) pour que l'inscrit puisse cibler l'inbox.
|
||||
- **Lire et résoudre les notifications côté hôte.** `getEventParticipants` / l'écran liste des inscrits doit lire les docs « notification » matérialisés (ORM/SPARQL) et faire le JOIN identité contre les contacts (`social:contact`). UI à prévoir : « N inscrits dont X identifiés ».
|
||||
- **Câblage session** : l'appel direct `ng.inbox_post_link` passe par le `ng`/session de `src/shared/utils/ngSession.ts`.
|
||||
|
||||
**Dépendances** : cette couche présuppose (1) le fork SDK livré et (2) le [refactor multi-store](./multi-store-refactor.md) (les inscriptions vivent dans le `protected_store`, pas le store unique actuel).
|
||||
|
||||
**Surface jetable** : quand NextGraph livrera sa propre API inbox (possiblement différente), il faudra migrer **aussi** ces points d'appel Festipod (l'appel `inbox_post_link`, la shape notification, la logique de lecture/résolution) — pas seulement les libs.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Commande haut-niveau (`NotifyInbox`) vs `InboxPost` brut dans `request_processor` ? (haut-niveau préféré : garde la crypto en Rust)
|
||||
- Où sourcer le NURI d'inbox de l'hôte (champ du doc PdR vs lookup profil) ?
|
||||
- Forme de la matérialisation côté réception (quels triples pour une notification d'inscription) ?
|
||||
- Suppression côté inbox : un déposant peut-il retirer son propre dépôt d'un doc qu'il ne contrôle pas ? (déjà noté en question résiduelle dans [authorization-matrix](./authorization-matrix.md))
|
||||
- Cadence de rebase du fork sur `upstream/main` : à chaque alpha, ou par jalons ? (arbitrer coût de maintenance vs dérive)
|
||||
- Critère de bascule : à quel signal upstream considère-t-on leur solution inbox « adoptable » et démarre-t-on la migration ?
|
||||
- `@ng-org/web` : patch runtime (build unique, multi-env) vs tarball local par domaine ? (le patch runtime est recommandé mais ajoute une ligne au fork à maintenir)
|
||||
- `ngd` sur Coolify : comment automatiser le premier-run (création du wallet admin via `ngcli`) pour un déploiement reproductible vs one-shot manuel persisté dans le volume ?
|
||||
- Faut-il un seul service Coolify (reverse-proxy maison servant statique + WS) ou deux services (static ng-app + ngd) avec routage de domaine Coolify ?
|
||||
|
||||
## Possible Approaches
|
||||
|
||||
Posture retenue (voir Context) : **fork temporaire auto-hébergé**, abandonné dès que NextGraph expose sa propre solution.
|
||||
|
||||
- **A. Fork temporaire + auto-hébergement (retenu comme stopgap)** — patch des 4 fichiers, build et déploiement de `ngd` + ng-app depuis le fork. Vrai inbox, anonymat natif, livrable sans attendre l'upstream. Coût : maintenir le fork rebasé + héberger la stack. Jetable : on migrera vers la solution officielle quand elle sortira.
|
||||
- **B. Contribution upstream — écartée comme objectif.** On ne vise pas à faire accepter une PR ; on attend plutôt la solution propre des développeurs NextGraph (qui sera possiblement différente) et on s'y adaptera. (Rien n'interdit de signaler le besoin à l'auteur, mais ce n'est pas le plan.)
|
||||
- **C. Pas de patch, détourner `social_query_start` (déjà exposé)** — repli si l'auto-hébergement n'est pas souhaité à court terme. Livrable tout de suite mais limité aux **contacts** : pas de notification anonyme vers un hôte non-connecté.
|
||||
|
||||
## Starting Points
|
||||
|
||||
- [nextgraph-integration-model](../knowledge/nextgraph-integration-model.md) — modèle d'intégration/déploiement
|
||||
- [nextgraph-stores-permissions](../knowledge/nextgraph-stores-permissions.md) — inbox au protocole, exposition SDK, chemin du repo local
|
||||
- [authorization-matrix](./authorization-matrix.md) — la décision cadre inbox que ce patch sert
|
||||
- Repo local `nextgraph-rs` : `sdk/js/lib-wasm/src/lib.rs`, `engine/verifier/src/{request_processor,inbox_processor}.rs`, `engine/net/src/types.rs`
|
||||
- Remotes du repo local : `origin` = `git.nextgraph.org/slaivyn/nextgraph-rs` (fork perso, déjà en place pour pousser un patch), `upstream` = `git.nextgraph.org/NextGraph/nextgraph-rs` (officiel, pour PR / rebase).
|
||||
@@ -1,133 +0,0 @@
|
||||
# Refactor multi-store NextGraph
|
||||
|
||||
**Status:** Incubating — aucun travail démarré
|
||||
**Last updated:** 2026-05-17
|
||||
|
||||
## Context
|
||||
|
||||
L'app Festipod est aujourd'hui *mono-store* : tout ce que l'app écrit (events, profils, participations, friendships) atterrit dans le `private_store` de l'utilisateur connecté. C'est un héritage du sample expense-tracker-rdf, formalisé dans [la décision du 2026-03-17](../decisions/2026-03-17-1600-private-store-nuri-scope.md).
|
||||
|
||||
Ce choix bloque toute évolution vers du multi-utilisateurs : par construction le `private_store` est non partageable (cf. [data-layer](../knowledge/data-layer.md) et la doc NextGraph officielle — *« It is not possible to share the documents of your private store with anybody else »*). Tant que tout est dans le private_store, Bob ne pourra jamais voir l'event d'Alice.
|
||||
|
||||
Le modèle natif NextGraph est *multi-store par utilisateur* (private, protected, public, group, dialog) — chaque type d'information a sa place. Festipod doit s'aligner sur ce modèle avant de pouvoir devenir collaboratif.
|
||||
|
||||
**Déclencheur :** discussion du 2026-05-17 sur la suite multi-user. Décision prise : *poser le cap, exécuter plus tard*.
|
||||
|
||||
## What We Know
|
||||
|
||||
### État actuel du code
|
||||
|
||||
Deux fichiers concentrent le hardcoding du store unique :
|
||||
|
||||
- `src/shared/utils/ngGraph.ts:30` — `ensureGraphNuri()` retourne `did:ng:${session.private_store_id}` pour TOUTES les entités, peu importe leur nature.
|
||||
- `src/shared/hooks/useShapeWithDefaults.ts` — accepte un `storeNuri` mais l'appelant unique (`FestipodDataContext`) lui passe systématiquement le NURI du private_store.
|
||||
|
||||
Entités impactées (toutes mélangées dans le même store aujourd'hui) :
|
||||
- `FpEvent` — devrait vivre dans un store partagé (logique multi-user)
|
||||
- `FpUserProfile` — devrait être en partie privée, en partie publique
|
||||
- `FpParticipation` — liée à un event, devrait vivre avec lui
|
||||
- `FpMeetingPoint` — actuellement local-only côté types ([`src/shared/data/types.ts:106`](../../src/shared/data/types.ts)), pas encore branché à NextGraph
|
||||
- `FpFriendship` — actuellement local-only, naturellement privée
|
||||
|
||||
### Modèle cible proposé
|
||||
|
||||
> **Note (2026-05-19)** : la [matrice d'autorisations](./authorization-matrix.md) a depuis dérivé, à partir des seuls points validés, une structure différente — 3 stores natifs par utilisateur (`public_store` + `protected_store` + `private_store`) + Dialog stores pour les connexions bilatérales, sans Group store dans le périmètre actuel. La structure à 4 niveaux ci-dessous reste pertinente pour le périmètre élargi (communautés, collaboration multi-hôte), qui est aujourd'hui hors périmètre. À reconcilier au moment de l'exécution.
|
||||
|
||||
Structure hiérarchique en **4 niveaux de Group stores** (pas de private/public pour le métier collaboratif — tout en Group) :
|
||||
|
||||
```
|
||||
┌─ Group store « index communautaire » ────────────────────┐
|
||||
│ Référence tous les events visibles dans la communauté │
|
||||
│ Lecture par tous les membres, sert d'annuaire/discovery │
|
||||
│ │
|
||||
│ ┌─ Group store « communauté » ──────────────────────┐ │
|
||||
│ │ Propriétaire de l'event │ │
|
||||
│ │ Permissions = qui peut modifier l'event │ │
|
||||
│ │ (organisateurs / membres de la communauté) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─ Group store « event » ─────────────────────┐ │ │
|
||||
│ │ │ Tout ce qui se rattache à l'event : │ │ │
|
||||
│ │ │ participations, infos pratiques, discu… │ │ │
|
||||
│ │ │ Membres = participants à l'event │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ ┌─ Group store « meeting point » ───────┐ │ │ │
|
||||
│ │ │ │ Un RDV de l'event = son propre group │ │ │ │
|
||||
│ │ │ │ Permet participations + discu │ │ │ │
|
||||
│ │ │ │ scopées au point de rencontre │ │ │ │
|
||||
│ │ │ └───────────────────────────────────────┘ │ │ │
|
||||
│ │ └─────────────────────────────────────────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Mapping entités → store cible :
|
||||
|
||||
| Entité | Store cible | Justification |
|
||||
|---|---|---|
|
||||
| Event (métadonnées : titre, dates, description) | Group store « communauté » | C'est la communauté qui possède l'event, donc qui contrôle qui peut le modifier |
|
||||
| Référence d'event (pointeur depuis l'index) | Group store « index communautaire » | Discovery : « voici les events visibles » |
|
||||
| Participation | Group store « event » | Une participation n'a de sens que dans le contexte de son event |
|
||||
| MeetingPoint (métadonnées) | Group store « event » | Le RDV appartient à l'event |
|
||||
| Participation à un MeetingPoint | Group store « meeting point » | RSVP/présence scopés au RDV |
|
||||
| UserProfile (partie publique) | public_store de l'utilisateur | Modèle natif NextGraph |
|
||||
| Friendship | private_store de l'utilisateur | Donnée purement personnelle |
|
||||
|
||||
### Contrainte SDK bloquante
|
||||
|
||||
Plusieurs primitives présentes au niveau protocole NextGraph **ne sont pas exposées dans le SDK `@ng-org/web` actuel** (vérifié en `0.1.2-alpha.13` = `upstream/main` au 2026-05-21, version installée dans Festipod). Méthodes disponibles : `doc_create`, `doc_subscribe`, `sparql_query/update`, `orm_start_*`, `file_get`, `app_request_stream`. Absents du SDK alors qu'existant côté protocole :
|
||||
|
||||
- création de Group stores et gestion des invitations/permissions (`share_doc`, `invite_user`, `create_group_store`, `accept_invite`) ;
|
||||
- **dépôt et lecture de l'inbox d'un document** (cf. [matrice d'autorisations](./authorization-matrix.md) — l'inbox est le mécanisme natif retenu pour la notification d'inscription au PdR). À noter que `app_request_stream` est la méthode générique la plus susceptible de porter ce mécanisme une fois exposé, à confirmer en lisant le code Rust du broker.
|
||||
|
||||
La doc NextGraph annonce qu'*« An API will be provided for permission manipulation »* — pas de date.
|
||||
|
||||
**Implication :** le refactor *structurel* (passer d'un store unique à un système de stores par entité) peut commencer sans attendre cette API, en utilisant des placeholders (par ex. continuer à pointer vers `private_store_id` pour les Group stores qui ne peuvent pas encore exister). Mais l'**aboutissement complet** (vrai multi-user, partage entre wallets distincts) dépend de l'arrivée de l'API SDK ou d'un contournement (fork du wallet, accès Rust direct, etc.).
|
||||
|
||||
### Implications côté code
|
||||
|
||||
Le refactor touche au moins :
|
||||
|
||||
1. **Disparition de `ensureGraphNuri()`** comme helper unique. Remplacé par des helpers par entité (`getEventStore(communityId)`, `getParticipationStore(eventId)`, `getProfileStore(scope: 'public' | 'private')`, …) ou par une couche `storeRegistry` qui résout le NURI selon `(entité, contexte)`.
|
||||
2. **`useShapeWithDefaults` reste un wrapper utile** mais l'appelant choisit explicitement le store. Aujourd'hui un seul appelant ([`FestipodDataContext`](../../src/shared/context/FestipodDataContext.tsx)), demain N appelants ou un appelant qui résout dynamiquement.
|
||||
3. **Chaque entité de domaine déclare son store cible** — soit via un mapping centralisé, soit via une convention (shape → store).
|
||||
4. **`bootstrapWallet()`** ([`src/shared/utils/ngBootstrap.ts`](../../src/shared/utils/ngBootstrap.ts)) doit être revu : on ne seed plus dans un unique store, on doit seed dans plusieurs (ou décider que seed ne crée que des données de l'utilisateur courant — ce qui colle mieux à la réalité multi-user).
|
||||
5. **`FestipodDataContext`** : structurer les hooks par entité, chacun avec son store résolu.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Quand crée-t-on un Group store de communauté ?** L'API n'existe pas en SDK aujourd'hui. Faut-il que ce soit un acte explicite de l'utilisateur (« créer une communauté ») ou bien tout user a une communauté par défaut à la création de son wallet ?
|
||||
2. **Comment Bob connaît-il l'index communautaire d'Alice ?** Discovery toujours ouverte — possiblement via le public_store d'Alice qui annonce le NURI de l'index communautaire.
|
||||
3. **Faut-il vraiment 4 niveaux d'imbrication ?** Le « meeting point comme group store » mérite d'être validé — quel besoin réel justifie une couche de permission supplémentaire vs un simple sous-graphe du group store de l'event ?
|
||||
4. **Que devient le seed de démo** quand l'app est multi-store et que les Group stores ne peuvent pas encore exister ? Mode dégradé en private_store le temps que le SDK rattrape, ou retirer le seed en mode connecté ?
|
||||
5. **Migration des wallets existants** : les wallets de test ont déjà des données dans le private_store. Comment on les fait évoluer (script de migration, wipe and reseed, ignore) ?
|
||||
6. **Bootstrap d'un user vierge** : à la première connexion, faut-il auto-créer un Group store communautaire « par défaut » pour lui ou attendre une action utilisateur ?
|
||||
|
||||
## Possible Approaches
|
||||
|
||||
Esquisses sans engagement (les arbitrages se feront dans une décision dédiée au moment de l'exécution) :
|
||||
|
||||
- **Refactor structurel d'abord, partage ensuite.** Réorganiser l'app en multi-store dès maintenant en utilisant `private_store_id` comme placeholder pour les Group stores manquants. Quand l'API arrive, on remplace les placeholders par de vrais NURIs de Group stores.
|
||||
- **Registry centralisé** vs **résolution par convention**. Soit un `storeRegistry.ts` qui mappe explicitement `(entité, contexte) → NURI`, soit chaque shape porte sa propre logique de scope.
|
||||
- **Big-bang** vs **par entité**. Tout migrer en un coup vs migrer entité par entité (commencer par Event qui est le plus stratégique).
|
||||
- **Maintenir un mode mono-store** parallèle pour le dev/demo tant que les Group stores ne sont pas fonctionnels.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Ce brief — et le refactor qui en découlera — **ne traite pas** :
|
||||
- L'invitation effective d'utilisateurs à un Group store (capability sharing, Nuri d'invitation)
|
||||
- La gestion des permissions par rôle (organisateur / membre / lecteur)
|
||||
- La résolution du problème de discovery cross-wallet
|
||||
- Le contournement éventuel de l'UI wallet (jugée dysfonctionnelle dans cette conversation)
|
||||
- Le mode P2P direct sans broker
|
||||
|
||||
Ces sujets relèvent d'un **second chantier multi-user** dont le refactor multi-store est seulement le *prérequis structurel*.
|
||||
|
||||
## Starting Points
|
||||
|
||||
- [decision: private_store NURI scope](../decisions/2026-03-17-1600-private-store-nuri-scope.md) — la décision actuelle qu'on viendra modifier
|
||||
- [knowledge: data-layer](../knowledge/data-layer.md) — état actuel du pattern d'écriture
|
||||
- [`src/shared/utils/ngGraph.ts`](../../src/shared/utils/ngGraph.ts) — point de hardcoding principal
|
||||
- [`src/shared/hooks/useShapeWithDefaults.ts`](../../src/shared/hooks/useShapeWithDefaults.ts) — l'autre point de hardcoding
|
||||
- [`src/shared/context/FestipodDataContext.tsx`](../../src/shared/context/FestipodDataContext.tsx) — l'unique appelant aujourd'hui
|
||||
- [`src/shared/utils/ngBootstrap.ts`](../../src/shared/utils/ngBootstrap.ts) — le seed à revoir
|
||||
- NextGraph docs : [Documents et Stores](https://docs.nextgraph.org/en/documents/), [Getting started](https://docs.nextgraph.org/en/getting-started/)
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
type: _overview
|
||||
summary: Architecture feature-based de l'app — modules par domaine, invariant d'imports, app shell à providers, routing path-based, écrans et registre
|
||||
triggers:
|
||||
keywords: [module, modules, screen, écran, routing, route, navigate, useNavigate, useParams, registry, registre, app shell, shared, import]
|
||||
paths: ["src/app/**", "src/screens/**", "src/modules/*/screens/**", "src/shared/components/**", "src/shared/context/**"]
|
||||
---
|
||||
|
||||
# App architecture
|
||||
|
||||
Comment le code de l'app est **structuré** et **assemblé**. Architecture *feature-based* : le code est organisé par **domaine métier** (module), pas par couche technique.
|
||||
|
||||
**À lire en premier :** [[rule_module-imports]] — l'invariant central qui garde les modules découplés.
|
||||
|
||||
## Liens
|
||||
|
||||
- [[knowledge_module-structure]] — arborescence modules + couche `shared/`
|
||||
- [[knowledge_app-shell]] — `src/app/`, pile de providers, points d'entrée
|
||||
- [[knowledge_routing]] — routing path-based (History API), table de routes, hooks
|
||||
- [[knowledge_screens]] — inventaire des écrans, registre, lib de composants
|
||||
- [[knowledge_screen-pattern]] — anatomie canonique d'un écran (sans props, layout flex, showToast)
|
||||
- [[knowledge_styling-system]] — `src/index.css`, classes `app-*`, vars, pièges (Tailwind non-utilisé, `user-content` inerte)
|
||||
- [[cookbook_add-screen]] — procédure pour câbler un nouvel écran (registre + router + shell)
|
||||
- `tech-stack` — build, bundler Bun, commandes
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
type: cookbook
|
||||
summary: Procédure pour ajouter un écran — créer le composant dans le module, l'enregistrer dans src/screens/index.ts, ajouter la route dans router.tsx, le monter dans App.tsx, et un alias screenNameMap si testé en BDD
|
||||
---
|
||||
|
||||
# Cookbook : ajouter un écran
|
||||
|
||||
Un écran doit être câblé à **plusieurs endroits** — en oublier un produit des bugs silencieux (cf. le cas `ConnectScreen`, [[knowledge_screens]]).
|
||||
|
||||
1. **Créer le composant** : `src/modules/{module}/screens/MyScreen.tsx`, en suivant [[knowledge_screen-pattern]] (fonction sans props, `useFestipodData`/`useNavigate`/`useParams`, layout flex, style via [[knowledge_styling-system]]). Respecter [[rule_module-imports]] (importer seulement depuis `shared/`).
|
||||
|
||||
2. **Enregistrer dans le registre** : `src/screens/index.ts` — ajouter l'import + l'entrée (`id`, `name` FR, `path`, `component`). **Étape la plus oubliée** : un écran absent du registre est invisible à Storybook et aux consommateurs du registre, même s'il fonctionne en route.
|
||||
|
||||
3. **Ajouter la route** : `src/app/router.tsx` — étendre le type `Route`, ajouter le cas dans `parsePath()` (et la conversion inverse si présente).
|
||||
|
||||
4. **Monter dans le shell** : `src/app/App.tsx` — ajouter le cas dans le switch qui mappe `route.page` → composant.
|
||||
|
||||
5. **(Si testé en BDD)** : ajouter un alias dans `screenNameMap` (`src/shared/steps/ui/navigation.steps.ts`) si le nom français du `.feature` ne se résout pas trivialement vers l'`id`. Voir concept `bdd-testing`.
|
||||
|
||||
> Vérifier la cohérence : l'`id` doit être identique entre le registre, le router et `screenNameMap`. Un écart silencieux = écran injoignable ou non rendu.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: src/app/ est le shell réel de l'app — App.tsx empile les providers (Theme > NextGraph > FestipodData > Router) et bascule l'écran selon la route
|
||||
---
|
||||
|
||||
# App shell
|
||||
|
||||
`src/app/` est le **shell de l'app réelle** (mobile web app), pas un outil de prototypage.
|
||||
|
||||
> Note de migration : d'anciennes notes décrivaient `src/app/` comme un « prototyping tool » en routing par hash (`#/`, `#/demo/...`). C'est **périmé** depuis la restructuration en vraie app. La vérité courante : routing path-based via History API (voir [[knowledge_routing]]).
|
||||
|
||||
## Pile de providers
|
||||
|
||||
`App.tsx` empile les providers puis bascule l'écran selon la route courante :
|
||||
|
||||
```
|
||||
ThemeProvider
|
||||
└ NextGraphProvider (cycle de connexion NextGraph — concept data-layer)
|
||||
└ FestipodDataProvider (données, mode connected/demo — concept data-layer)
|
||||
└ RouterProvider (route courante + navigate)
|
||||
```
|
||||
|
||||
Le composant racine lit `useRouter()` pour résoudre `route.page` → écran à rendre.
|
||||
|
||||
## Points d'entrée
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `src/index.ts` | `Bun.serve()` — serveur HTTP, sert `index.html` + rapport cucumber |
|
||||
| `src/index.html` | Entrée HTML, charge `src/app/frontend.tsx` |
|
||||
| `src/app/frontend.tsx` | Racine React, rend `<App />` |
|
||||
|
||||
Le build et le bundler (Bun + Tailwind, alias `@/* → ./src/*`) sont documentés dans le concept `tech-stack`.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Arborescence feature-based — modules métier (event, user, home, auth, workshop, meeting, notification) et couche shared/ importable par tous
|
||||
---
|
||||
|
||||
# Structure des modules
|
||||
|
||||
Le code est organisé par **domaine métier**, pas par couche technique.
|
||||
|
||||
```
|
||||
src/modules/
|
||||
event/ # Événements : CRUD, discovery, participants, points de rencontre
|
||||
user/ # Profils, connexions (« amis »), partage
|
||||
home/ # Dashboard, settings
|
||||
auth/ # Login, welcome/onboarding
|
||||
workshop/ # Specs atelier (features seulement, pas d'écrans)
|
||||
meeting/ # Specs point de rencontre (features seulement)
|
||||
notification/ # Specs notification (features seulement)
|
||||
```
|
||||
|
||||
Chaque module peut contenir :
|
||||
- `screens/` — composants d'écran React
|
||||
- `features/` — fichiers Gherkin `.feature` (specs BDD, voir concept `bdd-testing`)
|
||||
- `steps/{ui,data,e2e}/` — step definitions Cucumber par couche
|
||||
|
||||
## Couche `shared/`
|
||||
|
||||
`src/shared/` contient tout le réutilisable inter-modules :
|
||||
|
||||
| Répertoire | Contenu |
|
||||
|---|---|
|
||||
| `components/` | Lib de composants UI (voir [[knowledge_screens]]) |
|
||||
| `context/` | `ThemeContext`, `NextGraphContext`, `FestipodDataContext` (voir concept `data-layer`) |
|
||||
| `data/` | User stories, `features.ts` (auto-généré), `seedData.ts`, `types.ts` |
|
||||
| `hooks/` | `useShapeWithDefaults` (NextGraph) |
|
||||
| `shapes/` | SHEX + bindings ORM (voir concept `data-layer`) |
|
||||
| `utils/` | `ngSession.ts`, `ngBootstrap.ts`, `ngGraph.ts` |
|
||||
| `steps/`, `support/` | Step definitions et hooks Cucumber partagés (concept `bdd-testing`) |
|
||||
| `lib/` | Helpers (`cn`, etc.) |
|
||||
|
||||
La règle de dépendance entre modules et `shared/` est dans [[rule_module-imports]].
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Routing path-based via History API (router maison dans src/app/router.tsx) — table de routes, hooks useNavigate/useParams, pas de prop drilling
|
||||
---
|
||||
|
||||
# Routing
|
||||
|
||||
Routing **path-based** via l'History API — router maison dans `src/app/router.tsx` (`window.history.pushState` + `popstate`, `parsePath(pathname)`). Pas de routing par hash.
|
||||
|
||||
## Table de routes
|
||||
|
||||
| Path | Écran |
|
||||
|---|---|
|
||||
| `/` | WelcomeScreen |
|
||||
| `/login` | LoginScreen |
|
||||
| `/home` | HomeScreen |
|
||||
| `/events` | EventsScreen |
|
||||
| `/events/new` | CreateEventScreen |
|
||||
| `/events/:id` | EventDetailScreen |
|
||||
| `/events/:id/edit` | UpdateEventScreen |
|
||||
| `/events/:id/invite` | InviteScreen |
|
||||
| `/events/:id/participants` | ParticipantsListScreen |
|
||||
| `/events/:id/meeting-points` | MeetingPointsScreen |
|
||||
| `/profile` | ProfileScreen |
|
||||
| `/profile/edit` | UpdateProfileScreen |
|
||||
| `/profile/friends` | FriendsListScreen |
|
||||
| `/profile/share` | ShareProfileScreen |
|
||||
| `/profile/connect` | (connexion) |
|
||||
| `/users/:id` | UserProfileScreen |
|
||||
| `/settings` | SettingsScreen |
|
||||
|
||||
> Cette table reflète `parsePath()` dans `router.tsx` — y revenir si elle évolue, c'est la source de vérité.
|
||||
|
||||
## Hooks
|
||||
|
||||
Les écrans utilisent `useNavigate()` et `useParams()` du router — **pas de prop drilling**. Le shell intercepte la navigation pour basculer l'écran affiché (voir [[knowledge_app-shell]]).
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Anatomie canonique d'un écran — fonction nommée sans props, lit tout via useFestipodData/useNavigate/useParams, layout flex colonne (Header / contenu scrollable / BottomNav pour les écrans hub), feedback via showToast, libellés français en dur
|
||||
---
|
||||
|
||||
# Pattern canonique d'un écran
|
||||
|
||||
Tous les écrans suivent la même forme. La connaître évite de réinventer ou de diverger.
|
||||
|
||||
## Forme
|
||||
|
||||
```tsx
|
||||
export function MyScreen() { // fonction nommée, JAMAIS de props
|
||||
const navigate = useNavigate();
|
||||
const { eventId, userId } = useParams();
|
||||
const { getEvent, currentUser, … } = useFestipodData();
|
||||
const [local, setLocal] = useState(…); // état local d'écran (étapes, sélections)
|
||||
|
||||
const handleAction = () => {
|
||||
// …muter via useFestipodData
|
||||
showToast('Message', 'success'); // feedback
|
||||
navigate('/path');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
|
||||
<Header title="…" /* left/right optionnels */ />
|
||||
<div style={{ flex:1, overflow:'auto' }}>{/* contenu scrollable */}</div>
|
||||
<BottomNav active="…" /> {/* seulement sur les écrans hub */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Invariants
|
||||
|
||||
- **Zéro prop** : l'écran ne reçoit rien ; tout vient du contexte/hooks (`useFestipodData`, `useNavigate`, `useParams`). Exceptions légitimes : `LoginScreen`/`WelcomeScreen` n'utilisent pas `useFestipodData` (auth/intro).
|
||||
- **Layout** : flex colonne pleine hauteur ; `Header` en haut, contenu en `flex:1; overflow:auto`, `BottomNav` en bas **uniquement pour les écrans hub** (Home, Events, Profile, Friends). Les écrans de flux (création, édition, détail) n'ont pas de `BottomNav`.
|
||||
- **Feedback** : `showToast(message, 'success'|'info'|'error')` (mécanisme `ToastContainer` exporté par `sketchy/`).
|
||||
- **Libellés** : **français, en dur** — aucun i18n, aucune clé de traduction dans le projet.
|
||||
- Style : voir [[knowledge_styling-system]]. Navigation/registre : [[knowledge_routing]], [[knowledge_screens]].
|
||||
|
||||
Pour **créer** un écran (les 3+ endroits à câbler), voir [[cookbook_add-screen]].
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Inventaire des écrans par module, registre central src/screens/index.ts, et lib de composants sous shared/components/sketchy/ — dont le NOM est conservé mais qui rend un thème moderne (pas hand-drawn)
|
||||
---
|
||||
|
||||
# Écrans et composants
|
||||
|
||||
## Lib de composants : `sketchy/` = thème moderne
|
||||
|
||||
⚠️ **Piège de nommage.** La lib de composants vit sous `src/shared/components/sketchy/` (chemin conservé, importé par ~17 écrans), **mais elle ne rend plus un style « hand-drawn »** : elle a été portée vers un thème **moderne** (DM Sans / orange, classes `app-*`). Le *chemin d'import* est bon, la *description visuelle « sketchy »* est périmée. Ne pas réintroduire d'esthétique dessinée en se fiant au nom du dossier.
|
||||
|
||||
Composants typiques : `Header`, `BottomNav`, `Button`, `Card`, `Input`, `Badge`, `Avatar`/`AvatarStack`, `Text`/`Title`, `Toggle`, `ListItem`, `Divider`, `Placeholder`, `BrokerBanner`, `NgStatus`.
|
||||
|
||||
## Registre d'écrans
|
||||
|
||||
`src/screens/index.ts` importe tous les écrans de tous les modules et expose :
|
||||
|
||||
```typescript
|
||||
export const screenGroups // groupés par domaine (home, events, user, general)
|
||||
export const screens // liste à plat
|
||||
export function getScreen(id): Screen | undefined
|
||||
```
|
||||
|
||||
Utilisé notamment par Storybook (voir concept `tech-stack`) pour parcourir les écrans.
|
||||
|
||||
## Inventaire
|
||||
|
||||
Écrans par module (IDs = clés du registre) :
|
||||
|
||||
- **home/** : `welcome`, `home`, `settings`
|
||||
- **event/** : `events`, `event-detail`, `create-event`, `update-event`, `invite`, `participants-list`, `meeting-points`
|
||||
- **user/** : `profile`, `update-profile`, `user-profile`, `friends-list`, `share-profile`
|
||||
- **auth/** : `login`
|
||||
|
||||
> Le mapping path → écran est dans [[knowledge_routing]]. La plupart des écrans consomment `useFestipodData()` (concept `data-layer`) ; exceptions : `LoginScreen`/`WelcomeScreen`.
|
||||
|
||||
## Piège : registre incomplet
|
||||
|
||||
Le registre doit lister **tous** les écrans. Cas observé : `ConnectScreen` (`src/modules/user/screens/`, routé `/profile/connect`, monté dans `App.tsx`) est **absent de `src/screens/index.ts`** → invisible à Storybook et aux consommateurs du registre, bien qu'il fonctionne en route. Toujours vérifier que l'écran est enregistré (cf. [[cookbook_add-screen]]).
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: src/index.css est la source de vérité du style — variables --app-* (couleurs, rayons, police DM Sans) et classes app-* rendues par les composants ; les écrans combinent ces classes avec des styles inline ; Tailwind est dans le build mais les écrans n'utilisent pas d'utilitaires Tailwind ; la classe user-content est inerte
|
||||
last_checked: 2026-06-15
|
||||
---
|
||||
|
||||
# Système de style
|
||||
|
||||
**Source de vérité : `src/index.css`** (thème « Modern clean — DM Sans »). C'est là que vivent les variables CSS et les classes `app-*`. Pas de fichiers CSS par module.
|
||||
|
||||
## Variables (`:root`)
|
||||
|
||||
- Couleurs : `--app-black #1a1a1a`, `--app-gray #888`, `--app-bg/--app-white #fff`, accent orange `--app-accent #E8590C` (+ `-light #FFF7ED`, `-border`, `-dark #C05621`), vert `--app-green #22543D` (+ `-light`, `-border`, `-text`).
|
||||
- Rayons : `--app-radius 16px`, `--app-radius-sm 12px`, `--app-radius-xs 8px`.
|
||||
- Police : `--font-app: 'DM Sans', …`.
|
||||
|
||||
## Classes `app-*`
|
||||
|
||||
Définies dans `index.css`, rendues par les composants de `shared/components/sketchy/` : `app-btn` (+ `-primary`/`-green`), `app-input`, `app-card`, `app-title`/`app-subtitle`/`app-text`, `app-badge`, `app-toggle`, `app-checkbox`, `app-header`, `app-navbar`, `app-list-item`, `app-avatar`, `app-placeholder`, `app-divider`, `app-tab`.
|
||||
|
||||
## Conventions d'écriture d'un écran
|
||||
|
||||
- Utiliser les **composants `sketchy/`** (qui portent les classes `app-*`) pour boutons/inputs/cartes/typo.
|
||||
- Pour le **layout** (flex, gaps, paddings, couleurs ponctuelles), les écrans utilisent des **styles inline** (`style={{…}}`) — c'est le pattern normal, pas une déviation.
|
||||
- Icônes : **emojis**/symboles Unicode (📅 📍 📝 🎪…), pas d'imports d'icônes en général.
|
||||
- Largeur : `.app-container` borne à **`max-width: 768px`, `height: 100dvh`** (mobile-first/tablette portrait). Aucune media query — pas de responsive desktop.
|
||||
|
||||
## Pièges
|
||||
|
||||
- **Tailwind est dans le build** (plugin `bun-plugin-tailwind`, dépendance `tailwindcss`), mais **les écrans n'utilisent pas de classes utilitaires Tailwind** — le style réel passe par `app-*` + inline. Ne pas « tailwindiser » un écran en pensant suivre la convention.
|
||||
- **`user-content` est une classe INERTE** : utilisée sur de nombreux titres/noms dans les écrans, **sans aucune définition CSS**. C'est un marqueur legacy sans effet — ne pas s'appuyer dessus pour styler, ne pas croire qu'elle fait quelque chose.
|
||||
- Pas de **dark mode** : le toggle « darkMode » de `SettingsScreen` n'est branché à rien.
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
type: rule
|
||||
summary: Un module n'importe QUE depuis shared/ (et le registre d'écrans) — jamais depuis un autre module ; c'est l'invariant qui garde l'architecture feature-based
|
||||
---
|
||||
|
||||
# Règle : un module n'importe jamais d'un autre module
|
||||
|
||||
**Les modules importent uniquement depuis `shared/` — jamais entre eux.**
|
||||
|
||||
```
|
||||
src/modules/event/screens/EventDetailScreen.tsx
|
||||
✅ import depuis 'shared/components/...'
|
||||
✅ import depuis 'shared/context/FestipodDataContext'
|
||||
✅ import depuis 'src/screens' (types du registre)
|
||||
❌ import depuis 'modules/user/screens/...'
|
||||
```
|
||||
|
||||
## Pourquoi
|
||||
|
||||
C'est ce qui rend l'architecture *feature-based* réelle et pas cosmétique : chaque domaine reste un bloc autonome, déplaçable/supprimable sans casser les autres. Tout besoin partagé **remonte dans `shared/`** ; toute dépendance inter-domaines passe par un contrat de `shared/` (souvent `FestipodDataContext` ou le registre d'écrans), jamais par un import direct.
|
||||
|
||||
## Vérifier
|
||||
|
||||
`grep -rE "from '\.\./\.\./(event|user|home|auth|workshop|meeting|notification)/" src/modules/` ne doit rien remonter d'un module vers un *autre* module. Un import qui croise deux noms de modules différents est une violation.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
type: _overview
|
||||
summary: Sécurité & confidentialité de Festipod — posture ACTUELLE (mono-store, confiance broker, aucun contrôle d'accès côté app) et modèle d'autorisations CIBLE (incubation) ; authentification par wallet NextGraph
|
||||
triggers:
|
||||
keywords: [sécurité, security, confidentialité, privacy, accès, "access control", contrôle d'accès, trust, confiance, authz, autorisation, permission, wallet, auth, authentification, anonyme, identité, login]
|
||||
paths: ["src/modules/auth/**", "src/shared/context/NextGraphContext.tsx"]
|
||||
---
|
||||
|
||||
# App security
|
||||
|
||||
Le modèle de **sécurité, confidentialité et autorisations** de Festipod. Le pilier se lit en deux temps :
|
||||
|
||||
- **Actuel** — ce que le code applique aujourd'hui : voir [[knowledge_trust-model]]. Résumé brutal : **aucun contrôle d'accès côté app**, l'app affiche le `private_store` de l'utilisateur connecté et fait confiance au broker. Mono-user de fait.
|
||||
- **Cible** — le modèle d'autorisations dérivé (qui peut faire quoi, données personnelles = réseau, anonymat via inbox) : [[brief_2026-05-18_authorization-matrix]]. **Incubation, non implémenté.** Il graduera en `rule_`/`behavior_` quand le multi-user atterrira (chantiers data dans le concept `nextgraph-platform`).
|
||||
|
||||
L'écart entre les deux est volontaire : tant que l'app est mono-store (cf. concept `data-layer`), il n'y a rien à autoriser côté app.
|
||||
|
||||
## Liens
|
||||
|
||||
- [[knowledge_trust-model]] — posture de sécurité actuelle (mono-store, confiance broker, pas d'enforcement app)
|
||||
- [[knowledge_authentication]] — auth par wallet NextGraph, tous authentifiés, pas d'accès anonyme
|
||||
- [[brief_2026-05-18_authorization-matrix]] — modèle d'autorisations cible (incubation)
|
||||
- `nextgraph-platform` — les primitives (stores, capabilities, inbox) et les chantiers data qui porteront la cible
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
type: brief
|
||||
summary: Matrice d'autorisations par type de donnée (PdR, inscription, événement, profil, connexion) ; dérive que 3 stores natifs par utilisateur + Dialog stores suffisent, aucun Group store sur le périmètre validé ; questions ouvertes sur modèle d'écriture événement et identité de l'hôte
|
||||
last_updated: 2026-05-18
|
||||
---
|
||||
|
||||
# Matrice d'autorisations et inventaire des requêtes
|
||||
|
||||
**Status:** Incubating — analyse en cours
|
||||
**Last updated:** 2026-05-18
|
||||
|
||||
## Context
|
||||
|
||||
Préalable au refactor multi-store ([[brief_2026-05-17_multi-store-refactor]]) et à toute évolution multi-user. La structure de stores NextGraph cible doit être *dérivée* de : (1) une matrice d'autorisations ; (2) un inventaire des requêtes par écran ; (3) les partitions naturelles qui en découlent (données partageant autorisations *et* schéma d'accès).
|
||||
|
||||
C'est aussi le **modèle de confidentialité/sécurité** de Festipod (pilier sécurité), non encore implémenté.
|
||||
|
||||
## Cadre
|
||||
|
||||
### Acteurs (tous authentifiés)
|
||||
|
||||
`Alice` (point de vue, propriétaire de la donnée en focus) · `Bob` (second protagoniste, relations bilatérales) · `D` (déclarant d'événement) · `H` (hôte d'un PdR) · `I` (inscrit) · `C` (connexion) · `U` (utilisateur lambda sans relation).
|
||||
|
||||
### Verbes
|
||||
|
||||
`créer` · `lire` (one-shot) · `s'abonner` (lecture réactive) · `modifier` · `supprimer`. Conventions : `✓` autorisé · `✗` interdit · `cond` sous condition · `—` sans objet.
|
||||
|
||||
## Décisions cadre (acquises)
|
||||
|
||||
- **Tous authentifiés.** Pas d'accès anonyme.
|
||||
- **Points de rencontre publics universels.** Tout utilisateur peut lire et s'abonner.
|
||||
- **Création de PdR ouverte à tous.** Pas de prérequis.
|
||||
- **Hôte = détenteur des droits d'écriture** sur un PdR (1 hôte, le créateur ; le fait d'être hôte est public).
|
||||
- **Informations personnelles = réservées au réseau.** Visibles seulement au titulaire et à ses connexions : participations, intégralité du profil, liste de connexions, et tout état déclaratif dont la divulgation serait une fuite. Statut « public » (PdR, événement) et « personnel » (profil, participations, connexions) coexistent dans le même utilisateur.
|
||||
- **Connexion bilatérale.** Existe après acceptation des deux côtés. Deux objets : `DemandeDeConnexion` (unilatérale, transitoire) et `Connexion` (bilatérale, persistante).
|
||||
- **Notification d'inscription via l'inbox NextGraph du PdR.** L'acte « s'inscrire » est composite : (a) écriture d'un objet `Inscription` dans le `protected_store` de l'inscrit, (b) dépôt d'un lien (DID cap) dans l'**inbox** du document PdR. Identification du sender par résolution du DID contre le graphe de connexions de l'hôte : connexion → inscription complète visible ; sinon → lien opaque (« quelqu'un (DID…) s'est inscrit »). Anonymat partiel **natif aux capabilities** (cf. [[knowledge_stores-permissions]] §Inbox).
|
||||
- **Adhésion à une communauté / suivi : hors périmètre actuel.**
|
||||
|
||||
## Matrice par type de donnée
|
||||
|
||||
### Point de rencontre
|
||||
|
||||
| Verbe | Alice (= Hôte) | I (autre inscrit) | D (déclarant parent) | U (lambda) |
|
||||
|---|---|---|---|---|
|
||||
| créer | ✓ (rend hôte) | — | ✗ | ✓ (rend hôte) |
|
||||
| lire | ✓ | ✓ | ✓ | ✓ |
|
||||
| s'abonner | ✓ | ✓ | ✓ | ✓ |
|
||||
| modifier | ✓ | ✗ | ✗ | ✗ |
|
||||
| supprimer | ✓ | ✗ | ✗ | ✗ |
|
||||
|
||||
Notes : pas de différenciation `C` (les connexions sont un filtre d'affichage UI, pas un droit, tout étant public). Le `D` n'a aucun droit particulier sur les PdR greffés sur son événement.
|
||||
|
||||
### Inscription à un point de rencontre
|
||||
|
||||
`Inscription` lie un utilisateur et un PdR. **Donnée personnelle** (inscrit + ses connexions). Acte composite (a)+(b) ci-dessus.
|
||||
|
||||
| Verbe | Alice (inscrite) | C (connexion) | H (hôte) | I (autre inscrit) | U |
|
||||
|---|---|---|---|---|---|
|
||||
| créer (acte composite) | ✓ | — | ✗ | ✗ | ✓ (rend inscrite) |
|
||||
| lire le contenu | ✓ | ✓ | cond : ✓ si H ∈ connexions(Alice) ; sinon lien opaque | cond : ✓ si I ∈ connexions(Alice) | ✗ |
|
||||
| s'abonner | ✓ | ✓ | cond (idem) | cond (idem) | ✗ |
|
||||
| lire l'inbox du PdR (entrées brutes) | — | — | ✓ | ✗ | ✗ |
|
||||
| modifier | ? **à trancher** (selon champs) | ✗ | ✗ | ✗ | ✗ |
|
||||
| supprimer | ✓ (se désinscrire ; retirer le lien de l'inbox si possible) | ✗ | cond : modération inbox seule (ne supprime pas l'objet) | ✗ | ✗ |
|
||||
|
||||
**Visibilité hôte : résolue** (identifiée si connecté, anonyme sinon — natif). **Questions ouvertes :** champs modifiables d'une inscription (booléen seul ou +commentaire/statut/accompagnants ?) ; **suppression côté inbox** — un déposant peut-il retirer son lien d'un doc qu'il ne contrôle pas ? (à vérifier au protocole).
|
||||
|
||||
### Événement
|
||||
|
||||
| Verbe | Alice (= D) | H (hôte d'un PdR greffé) | U |
|
||||
|---|---|---|---|
|
||||
| créer | ✓ (rend déclarant) | — | ✓ (rend déclarant) |
|
||||
| lire / s'abonner | ✓ | ✓ | ✓ |
|
||||
| modifier | ? **à trancher** | ? **à trancher** | ? **à trancher** |
|
||||
| supprimer | ? **à trancher** | ✗ | ✗ |
|
||||
|
||||
**Questions ouvertes :** qui peut **modifier** un événement déclaré — déclarant seul (propriétaire) ? tout utilisateur (wiki) ? personne (immuable) ? Central pour la déduplication (cf. concept `functional-domain`, [[brief_2026-06-15_event-deduplication]] côté functional-domain). Qui peut **supprimer**, et que deviennent les PdR greffés (orphelins/cascade/marqué supprimé) ?
|
||||
|
||||
### Profil utilisateur
|
||||
|
||||
**Rien dans le profil n'est public.** Deux périmètres : **profil réseau** (Alice + connexions : nom, avatar, bio, ville, intérêts) ; **profil privé** (Alice seule : settings, email, préférences).
|
||||
|
||||
| Verbe | Alice | C | U |
|
||||
|---|---|---|---|
|
||||
| créer | ✓ (à l'inscription) | — | — |
|
||||
| lire — réseau | ✓ | ✓ | ✗ |
|
||||
| lire — privé | ✓ | ✗ | ✗ |
|
||||
| s'abonner | ✓ | ✓ (réseau) | ✗ |
|
||||
| modifier | ✓ | ✗ | ✗ |
|
||||
| supprimer (compte) | ✓ | ✗ | ✗ |
|
||||
|
||||
**Tension à résoudre :** un PdR est lisible par tous, mais son hôte ne devrait pas être identifiable par un lambda. Trois positions : (i) **pseudonyme par DID seul** (nom/avatar résolus seulement aux connexions) ; (ii) **identité dénormalisée dans l'offre** (l'hôte choisit une « carte de visite » par PdR, vivant dans l'objet PdR, profil fermé) ; (iii) **anonymat de l'hôte** (identité révélée seulement aux connexions). À trancher. Autres : composition champ-par-champ de chaque périmètre ; statut du `username` (public/réseau/supprimé ?).
|
||||
|
||||
### Connexion (lien d'amitié)
|
||||
|
||||
Bilatérale. `DemandeDeConnexion` (unilatérale, en attente) → `Connexion` (bilatérale, à l'acceptation ; ouvre l'accès aux données personnelles). La liste de connexions d'Alice est **personnelle** (Alice + ses connexions).
|
||||
|
||||
| Verbe | Alice (initiatrice) | Bob (autre côté) | C | U |
|
||||
|---|---|---|---|---|
|
||||
| créer la demande | ✓ | — | — | — |
|
||||
| accepter | — | ✓ | — | ✗ |
|
||||
| lire la liste d'Alice | ✓ | ✓ | ✓ | ✗ |
|
||||
| s'abonner | ✓ | ✓ | ✓ | ✗ |
|
||||
| supprimer (rompre A↔B) | ✓ | ✓ | ✗ | ✗ |
|
||||
|
||||
**Questions ouvertes :** granularité côté Bob (voit-il toute la liste d'Alice ou juste A↔B ? — conséquence du principe : toute la liste) ; découvrabilité « amis d'amis » (Alice voit-elle Bob↔Carole ? — non, sauf si Carole ∈ connexions(Alice)).
|
||||
|
||||
## Partitions naturelles dérivées
|
||||
|
||||
Heuristique : même store si (a) même cellule d'autorisation en écriture *et* (b) accédées ensemble. À partir des seuls points validés, **trois périmètres** émergent — qui correspondent **presque parfaitement aux 3 stores natifs**.
|
||||
|
||||
| Périmètre | Écriture | Lecture | Données validées |
|
||||
|---|---|---|---|
|
||||
| **Public** ↔ `public_store` | Alice seule | Tous | PdR hébergés par Alice ; événements déclarés *(sous réserve du modèle d'écriture)* |
|
||||
| **Réseau** ↔ `protected_store` | Alice seule | Alice + connexions | Profil réseau ; participations ; index des connexions |
|
||||
| **Privé** ↔ `private_store` | Alice seule | Alice seule | Profil privé (settings, email, préférences) |
|
||||
|
||||
### Cas particulier : la Connexion bilatérale
|
||||
|
||||
Donnée à *deux* écrivains → ne tient dans aucun store individuel. Primitive native : le **Dialog store**. Modèle : **une `Connexion` A↔B = un Dialog store** (contient l'objet + matière à messagerie future) ; l'**index « toutes les connexions d'Alice »** vit dans le `protected_store` d'Alice (liste les NURIs des Dialog stores). La `DemandeDeConnexion` : soit dans un Dialog store provisoire, soit dans le `public_store` du destinataire (à trancher selon le SDK).
|
||||
|
||||
### Inbox du document PdR
|
||||
|
||||
Le doc PdR (dans le `public_store` de l'hôte) a une **inbox** native : reçoit les dépôts d'inscription (liens DID cap), plus tard commentaires/signaux. **Pas un store séparé**, attribut du document. Pas d'impact sur les partitions.
|
||||
|
||||
### Ce qui ne demande aucun Group store
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
## Inventaire des requêtes par écran
|
||||
|
||||
*À remplir une fois la matrice stabilisée.* Schéma prévu : `| Écran | Lectures one-shot | Abonnements | Écritures | Acteur déclencheur |`. Écrans à analyser : voir la table de routes (concept `app-architecture`).
|
||||
|
||||
## See Also
|
||||
|
||||
- [[brief_2026-05-17_multi-store-refactor]] — consommateur principal
|
||||
- [[brief_2026-06-15_shared-wallet-shim]] — stopgap reprenant ces périmètres
|
||||
- `README.md §Modèle fonctionnel` / concept `functional-domain` — source des acteurs
|
||||
- Concept `data-layer` — état actuel mono-store
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Authentification = possession d'un wallet NextGraph ; tous les utilisateurs sont authentifiés (pas d'accès anonyme) ; l'auth passe par le redirect/iframe broker, et l'app n'auto-connecte que dans l'iframe
|
||||
---
|
||||
|
||||
# Authentification
|
||||
|
||||
**L'identité d'un utilisateur = son wallet NextGraph.** Il n'y a **pas d'accès anonyme** à l'app : tout utilisateur est authentifié (cf. concept `functional-domain`). Il n'y a pas de système de comptes/mots de passe applicatif — l'auth est déléguée à NextGraph.
|
||||
|
||||
## Flux
|
||||
|
||||
- `LoginScreen` (`src/modules/auth/screens/`) déclenche la connexion via `useNextGraph()` (ne consomme pas `useFestipodData`).
|
||||
- Le flux standard `@ng-org/web` est un **redirect vers le broker** (`nextgraph.net/redir/`) qui recharge l'app dans une **iframe** après authentification (détail dans concept `nextgraph-platform`, [[knowledge_integration-model]] côté nextgraph-platform).
|
||||
- **L'app n'auto-connecte que dans l'iframe broker** (`window.self !== window.top`) — sinon `initNgWeb()` redirigerait toute la page. Cette règle vit côté data-layer ([[rule_conditional-ng-init]]) car elle concerne le cycle `NextGraphContext`, mais elle a une conséquence sécurité directe : **hors iframe, aucune session n'est ouverte sans action explicite** de l'utilisateur.
|
||||
|
||||
## Le wallet de test
|
||||
|
||||
Les tests `@data`/`@e2e` créent/ouvrent un wallet réel (`festipod-tests`/`festipod-tests`, profil persistant) — voir concept `bdd-testing`. Ce sont des **credentials de test en clair**, sans enjeu de sécurité, dédiés au staging (cohérent avec la posture « utilisateurs amicaux » du stopgap, concept `nextgraph-platform`).
|
||||
|
||||
> Le modèle d'autorisations qui s'appuiera sur cette identité (connexions bilatérales, données personnelles = réseau, anonymat hôte) est en incubation : [[brief_2026-05-18_authorization-matrix]].
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Posture de sécurité actuelle — aucun contrôle d'accès côté app, l'app lit/affiche le private_store de l'utilisateur connecté et fait confiance au broker NextGraph pour ne retourner que des données autorisées ; mono-user de fait
|
||||
last_checked: 2026-06-15
|
||||
---
|
||||
|
||||
# Modèle de confiance actuel
|
||||
|
||||
**Posture observée dans `src/shared/context/FestipodDataContext.tsx` (`useNgData`) :** l'app lit tout ce que les subscriptions ORM retournent depuis le `private_store` de l'utilisateur connecté et l'affiche **sans aucun filtre d'autorisation côté app**.
|
||||
|
||||
Conséquences (à connaître avant de raisonner sécurité) :
|
||||
|
||||
1. **Aucun contrôle d'accès applicatif.** Pas de vérification « l'utilisateur a-t-il le droit de voir cette donnée ». L'app suppose que **le broker/NextGraph ne retourne que ce que l'utilisateur peut voir**. Toute la confidentialité repose sur cette confiance dans la couche NextGraph, pas sur du code Festipod.
|
||||
2. **Mono-store, donc mono-user de fait.** Tout (events, profils, participations) vit dans le `private_store` de l'utilisateur connecté (cf. concept `data-layer`, [[decision_2026-03-17_private-store-nuri-scope]] côté data-layer). Un autre utilisateur ne voit rien — par construction, le `private_store` n'est pas partageable. Il n'y a donc rien à « autoriser » : chacun ne voit que ses propres données.
|
||||
3. **Pas de séparation de périmètres.** Le découpage public / réseau / privé du modèle cible ([[brief_2026-05-18_authorization-matrix]]) **n'existe pas encore** dans le code : aucun `protected_store`/`public_store` n'est utilisé pour le métier.
|
||||
|
||||
## Le piège pour la suite
|
||||
|
||||
Le jour où le multi-user arrive (lecture cross-wallet, voir les briefs de `nextgraph-platform`), cette **absence d'enforcement applicatif devient un risque** : si la séparation reste portée seulement par la crypto/capabilities NextGraph et que l'app continue d'afficher « tout ce qu'elle reçoit », une fuite de capability = une fuite de données. Le stopgap `shared-wallet-shim` (concept `nextgraph-platform`) prévoit d'ailleurs un **filtre d'isolation applicatif** explicite parce que, dans ce mode, un seul wallet rend tout physiquement lisible.
|
||||
|
||||
> À vérifier si on doute : `useNgData` dans `FestipodDataContext.tsx` ne contient aucune branche de filtrage par identité ; les seuls IDs manipulés sont ceux du wallet courant.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
type: _overview
|
||||
summary: BDD Cucumber/Gherkin en français sur 3 couches (@ui, @data, @e2e) — setup, contrat de couches (quoi tester où), harness broker réel, et le piège des vestiges source-grep
|
||||
triggers:
|
||||
keywords: [cucumber, gherkin, bdd, feature, scenario, scénario, step, steps, "@ui", "@data", "@e2e", playwright, broker, harness, wallet, world, hooks, renderHelper]
|
||||
paths: ["src/modules/*/features/**", "src/modules/*/steps/**", "src/shared/steps/**", "src/shared/support/**", "src/shared/test-harness/**", "cucumber.json"]
|
||||
---
|
||||
|
||||
# BDD testing
|
||||
|
||||
Tests BDD **Cucumber/Gherkin en français** (`Etant donné`, `Quand`, `Alors`) sur **3 couches** de coût croissant.
|
||||
|
||||
**À lire avant d'écrire un test :** [[rule_test-layer-contracts]] — chaque couche répond à une question distincte ; mélanger produit des tests fragiles. C'est la règle qui décide *où* va une assertion.
|
||||
|
||||
## Les 3 couches
|
||||
|
||||
```
|
||||
/\ @e2e app réelle dans l'iframe broker — parcours critiques
|
||||
/ \
|
||||
/----\ @data mutations & persistance via broker NextGraph réel
|
||||
/------\
|
||||
/ @ui \ rendu d'écran in-process (happy-dom + seed) — le gros du volume
|
||||
/__________\
|
||||
```
|
||||
|
||||
## Liens
|
||||
|
||||
- [[rule_test-layer-contracts]] — quoi tester à chaque couche (le contrat)
|
||||
- [[knowledge_cucumber-setup]] — config, layout, scripts, fichiers auto-générés
|
||||
- [[knowledge_ui-layer]] — couche `@ui` : render helper, fixtures, bons/anti patterns
|
||||
- [[knowledge_data-layer-broker]] — couche `@data` : harness broker, cycle de vie wallet, bridge
|
||||
- [[knowledge_e2e-layer]] — couche `@e2e` : app réelle dans l'iframe
|
||||
- [[decision_2026-03-12_headless-wallet-creation]] — pourquoi le wallet de test est créé en UI headless
|
||||
- [[caveat_source-grep-vestiges]] — vestiges de l'ère « analyse de source » dans `world.ts`
|
||||
- [[cookbook_add-scenario]] — ajouter un scénario/step (couches, piège de sérialisation `evaluate`, `@wip`)
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
type: caveat
|
||||
summary: world.ts garde des vestiges de l'ère « analyse de source » (screenFileMap, screenFieldDetectors, screenExpectedContent, screenRequiredFields ; hasText/hasField/hasElement à fallback source) — à supprimer une fois la migration @ui vers le DOM rendu terminée
|
||||
last_checked: 2026-06-15
|
||||
---
|
||||
|
||||
# Caveat : vestiges d'analyse de source dans `world.ts`
|
||||
|
||||
La suite `@ui` **précède** le contrat de couches ([[rule_test-layer-contracts]]). Des restes de l'ère « grep sur le code source » subsistent et **ne doivent pas être étendus** :
|
||||
|
||||
- `world.ts:screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` — mappings de l'approche analyse-de-source.
|
||||
- `hasText` / `hasField` / `hasElement` — **préfèrent désormais le DOM rendu** mais **retombent sur la source** pour que les steps non migrés continuent de marcher pendant la transition.
|
||||
|
||||
## Plan de migration (en cours)
|
||||
|
||||
1. Réécrire les assertions grep-source → requêtes DOM via le render helper.
|
||||
2. Supprimer les tests sur détails d'implémentation (`/showDuplicateWarning/`, `/importableEvents/`, regex sur JSX).
|
||||
3. Déplacer les assertions comportementales vers `@e2e` quand pas déjà couvertes.
|
||||
4. Retirer les checks de contenu `@e2e` redondants avec `@ui`.
|
||||
|
||||
Une fois la migration terminée, les 4 maps vestiges peuvent disparaître au profit d'assertions sur le DOM rendu + seed. **Tant qu'elles existent, ne pas s'appuyer dessus pour de nouveaux tests.**
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: cookbook
|
||||
summary: Procédure pour ajouter un scénario/step BDD — .feature français taggé, steps par couche, piège de sérialisation de appFrame.evaluate (passer les args, pas de closure), ajouter les helpers aux DEUX harness, tag @wip pour le non-implémenté
|
||||
---
|
||||
|
||||
# Cookbook : ajouter un scénario / un step
|
||||
|
||||
1. **Écrire le `.feature`** : `src/modules/{module}/features/us-N-slug.feature`, `# language: fr`, tag de tête `@CATEGORIE @priority-N`, et un tag de couche par scénario (`@ui` / `@data` / `@e2e`). Mots-clés FR : `Fonctionnalité`, `Contexte` (Background), `Scénario`, `Étant donné`/`Quand`/`Alors`. Tagger `@wip` un scénario dont les steps ne sont pas encore écrits.
|
||||
|
||||
2. **Choisir la couche** (cf. [[rule_test-layer-contracts]]) : assertion de rendu → `@ui` ; mutation/persistance → `@data` ; parcours complet → `@e2e`.
|
||||
|
||||
3. **Écrire les steps** dans `src/modules/{module}/steps/{ui,data,e2e}/*.steps.ts` (ou `src/shared/steps/ui/` si cross-domaine). Signature : `async function (this: FestipodWorld, …)`. Importer `FestipodWorld` depuis `../../../../shared/support/world` (ajuster le chemin relatif).
|
||||
|
||||
4. **Accès aux données selon la couche** :
|
||||
- `@ui` : `this.renderedDoc` / `this.getDomText()` / `this.hasText(...)` après `navigateTo(...)` (voir [[knowledge_ui-layer]]).
|
||||
- `@data`/`@e2e` : `await this.appFrame!.evaluate(fn, ...args)` sur le bridge `window.__testData` (voir [[knowledge_data-layer-broker]]).
|
||||
|
||||
5. **⚠️ Piège de sérialisation `appFrame.evaluate`** : la fonction passée s'exécute **dans l'iframe**, les variables du step **ne sont pas capturées** (closures perdues). **Passer toute valeur en argument** :
|
||||
```ts
|
||||
// ❌ const title = eventTitle; await appFrame.evaluate(() => td.getEventByTitle(title)) // title undefined
|
||||
// ✅ await appFrame.evaluate((t) => td.getEventByTitle(t), eventTitle)
|
||||
```
|
||||
Toujours `await` (oublier → assertion avant résolution).
|
||||
|
||||
6. **Si tu ajoutes une opération de données** : exposer le helper sur `window.__testData` dans **les deux** harness (`src/shared/test-harness/harness.tsx` ET `harness-ng.tsx`) — sinon le fallback mock diverge du broker réel.
|
||||
|
||||
7. **Câbler un écran testé** : si le nom français de l'écran ne se résout pas vers son `id`, ajouter un alias dans `screenNameMap` (`src/shared/steps/ui/navigation.steps.ts`).
|
||||
|
||||
8. **Lancer** : `bun run test:cucumber` (tout) ou `bun run test:data` (@data). Rapport : `reports/cucumber-report.html`. Le `@data`/`@e2e` exige le wallet de test (`bun run test:auth-setup` au premier coup si besoin, sinon création auto — cf. [[decision_2026-03-12_headless-wallet-creation]]).
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
type: decision
|
||||
summary: Décision 2026-03-12 — créer le wallet de test en automatisant l'UI broker headless (Playwright) plutôt que par API NG, car ça teste le vrai flux d'auth et évite de reverse-engineer l'API d'inscription
|
||||
---
|
||||
|
||||
# Automated Headless Wallet Creation for CI
|
||||
|
||||
**Date:** 2026-03-12 15:00
|
||||
**Status:** Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Les tests `@data` exigent un wallet NextGraph dans un profil Chromium persistant. Avant, le premier run exigeait une interaction manuelle (navigateur visible, création de wallet à la main) → bloquait le CI.
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option A: création programmatique du wallet via SDK NG
|
||||
Appeler `ng.wallet_create()` depuis Node/Bun, sans UI.
|
||||
- **Pour** : plus rapide, pas de navigateur.
|
||||
- **Contre** : `@ng-org/web` est browser-only (WASM + postMessage) ; il faudrait reverse-engineer l'API d'inscription d'`account.nextgraph.eu` ; ne teste pas le vrai flux d'auth.
|
||||
|
||||
### Option B: automatiser le flux UI headless
|
||||
Piloter via Playwright la même UI de création de wallet, en headless.
|
||||
- **Pour** : teste le vrai flux auth/login de bout en bout ; pas de reverse-engineering ; même profil persistant réutilisé ; CI-ready sans étape manuelle.
|
||||
- **Contre** : dépend de `nextgraph.eu`/`account.nextgraph.eu` joignables ; fragile aux changements d'UI NextGraph ; +~27s au premier run.
|
||||
|
||||
## Decision
|
||||
|
||||
**Option B** — automatiser l'UI broker. Le flux de création (navigate → Create Wallet → ToS → username/password → submit) est lui-même un test légitime de la feature d'auth. La dépendance aux services externes est acceptable puisque les tests dépendent déjà du broker joignable.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positif :** tests pleinement CI-ready (zéro interaction) ; flux auth testé en passant ; `bun run test:data` part d'un état propre.
|
||||
**Négatif :** exige un accès internet (nextgraph.eu, account.nextgraph.eu) ; fragile aux changements d'UI NextGraph (textes de boutons, IDs de formulaire).
|
||||
**Risque :** rate-limiting d'`account.nextgraph.eu` si le CI recrée souvent des wallets.
|
||||
|
||||
> Mécanique de cycle de vie détaillée : [[knowledge_data-layer-broker]].
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Config Cucumber (cucumber.json, langue fr, loader tsx), layout des features/steps colocalisés par module, steps partagés dans shared/steps/, et les scripts qui génèrent features.ts/testResults.ts/stepDefinitions.ts
|
||||
---
|
||||
|
||||
# Setup Cucumber
|
||||
|
||||
26 fichiers `.feature` (US-1 à US-26), tous en **français**, taggés `@CATEGORIE @priority-N` (catégories EVENT, WORKSHOP, USER, MEETING, NOTIF).
|
||||
|
||||
## Layout
|
||||
|
||||
Features et steps **colocalisés avec leur module** :
|
||||
|
||||
```
|
||||
src/modules/event/features/us-13-creer-evenement.feature
|
||||
src/modules/event/steps/{ui,data,e2e}/
|
||||
```
|
||||
|
||||
Steps **partagés** (cross-domaine) dans `src/shared/steps/ui/` :
|
||||
- `navigation.steps.ts` — navigation, auth, clics/sélections, assertions section/bouton/champ
|
||||
- `form.steps.ts` — validation de champs, champs requis, import/duplicate
|
||||
- `screen.steps.ts` — contenu d'écran (participants, events, profils, QR)
|
||||
|
||||
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. 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
|
||||
|
||||
`cucumber.json` : `import` de `src/shared/support/**`, `src/shared/steps/**`, `src/modules/*/steps/**` ; `paths` = `src/modules/*/features/**`; `language: fr`. **Runner = Node + tsx** (`node --import tsx/esm node_modules/.bin/cucumber-js`), pas Bun — les plugins (Playwright, happy-dom) ne chargent pas en import Bun natif. Ne pas « bunifier » `cucumber:run`/`test:data`.
|
||||
|
||||
## Le harness de test est buildé à la demande
|
||||
|
||||
Les harness `@data`/`@e2e` (`src/shared/test-harness/harness.tsx`, `harness-ng.tsx`) **ne sont pas** buildés par `build.ts`. Le `BeforeAll` de `hooks.ts` les compile **à la demande** (`bun build` → `dist/test-harness*.js`). Le wallet de test peut être créé d'avance via `bun run test:auth-setup` (`scripts/setup-test-auth.ts`), sinon il est créé automatiquement au premier run (cf. [[decision_2026-03-12_headless-wallet-creation]]).
|
||||
|
||||
## Fichiers auto-générés
|
||||
|
||||
Des scripts `scripts/` parsent features/steps en data TS consommée par l'outil de parcours :
|
||||
|
||||
| Script | Entrée | Sortie |
|
||||
|---|---|---|
|
||||
| `parse-features.ts` | `*/features/*.feature` | `src/shared/data/features.ts` |
|
||||
| `parse-test-results.ts` | `reports/cucumber-report.json` | `src/shared/data/testResults.ts` |
|
||||
| `extract-step-definitions.ts` | `shared/steps/ui/*.ts` | `src/shared/data/stepDefinitions.ts` |
|
||||
|
||||
Lancer : `bun run test:cucumber` (tout), `bun run test:data` (@data). Après ajout de steps : `bun run steps:extract`.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
# Couche `@data` (broker réel)
|
||||
|
||||
`@data` teste le **vrai pipeline NextGraph** via un broker, pas des données mockées.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Cucumber → Playwright (Chromium, profil persistant)
|
||||
→ broker wallet login (automatisé)
|
||||
→ broker charge le harness en iframe (http://127.0.0.1:{port})
|
||||
→ harness-ng.tsx (init → useShape → ORM → broker)
|
||||
→ bridge window.__testData
|
||||
```
|
||||
|
||||
**Dual mode** : broker réel (`harness-ng.tsx`, défaut) ou fallback mock (`harness.tsx`, DeepSignalSets standalone si le build NG échoue).
|
||||
|
||||
## Cycle de vie du wallet (automatisé, CI-ready)
|
||||
|
||||
- **Premier run** : pas de marker `.wallet-ready` → Chromium headless crée le wallet (`nextgraph.eu` → Create Wallet → ToS sur `account.nextgraph.eu` → username/password → submit), **puis se logge** — ce login déclenche le bootstrap du verifier depuis le broker distant (peuple `self.repos`, sauvé en localStorage). **Sans ce login initial, toutes les écritures échoueraient en `RepoNotFound`.** Marker écrit.
|
||||
- **Runs suivants** : marker trouvé → login automatisé (click Login → wallet → password → submit) → harness en iframe → `window.__testData.ready`.
|
||||
- Credentials wallet : `festipod-tests` / `festipod-tests`.
|
||||
|
||||
> Le choix « automatiser l'UI headless plutôt que créer le wallet par API » est tranché dans [[decision_2026-03-12_headless-wallet-creation]].
|
||||
|
||||
## Détails techniques
|
||||
|
||||
- **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`).
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Couche @e2e — Playwright boote l'app RÉELLE (pas un harness) dans l'iframe broker, interagit via appFrame.evaluate()/locator(), réutilise setupBrokerPage() de @data ; teste navigation/redirects/clics, pas de fallback mock
|
||||
---
|
||||
|
||||
# Couche `@e2e` (app réelle)
|
||||
|
||||
`@e2e` teste l'**UI de l'app réelle** tournant dans l'iframe broker — contrairement à `@data` qui charge un harness de test.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Cucumber → Playwright (Chromium, profil persistant)
|
||||
→ https://nextgraph.net/redir/#/?o=http://127.0.0.1:{appPort}
|
||||
→ login broker (automatisé, même mécanique que @data)
|
||||
→ broker charge la VRAIE APP en iframe
|
||||
→ app rend avec NextGraphProvider auto-connectant
|
||||
→ steps via appFrame.evaluate() + locators Playwright
|
||||
```
|
||||
|
||||
**Serveur app** : lancé en `BeforeAll` (`spawn('bun', ['src/index.ts'], { env: { PORT } })`, poll jusqu'à réponse HTTP, tué en `AfterAll`). Réutilise le helper `setupBrokerPage()` de `@data` (redirect, login, découverte de l'iframe).
|
||||
|
||||
## Step definitions
|
||||
|
||||
Dans les modules (ex. `src/modules/auth/steps/e2e/connexion.steps.ts`) :
|
||||
- `this.appFrame!.evaluate()` — JS dans l'iframe app (navigation hash/path, checks de contenu)
|
||||
- `this.appFrame!.locator()` — éléments DOM
|
||||
- `this.appFrame!.waitForFunction()` — poll d'état attendu
|
||||
- `SCREEN_MARKERS` — map ID d'écran → texte unique de vérification
|
||||
|
||||
Navigation : `window.history.pushState` + dispatch `popstate` (routing path-based, cf. `app-architecture`).
|
||||
|
||||
## Différences avec `@data`
|
||||
|
||||
| Aspect | `@data` | `@e2e` |
|
||||
|---|---|---|
|
||||
| Chargé en iframe | harness (`harness-ng.tsx`) | app réelle (`src/index.ts`) |
|
||||
| Signal ready | `window.__testData.ready` | `root.innerHTML.length > 100` |
|
||||
| Interaction | bridge `evaluate()` | `evaluate()` + locators |
|
||||
| Fallback mock | oui | **non** (broker réel requis) |
|
||||
| Teste | opérations données | comportement UI (nav, redirects, clics) |
|
||||
|
||||
> **Ne pas re-vérifier en `@e2e` ce que `@ui` couvre déjà** — `@e2e` doit casser quand la *collaboration* entre couches casse, pas quand une icône change (cf. [[rule_test-layer-contracts]]).
|
||||
|
||||
## Fichiers clés
|
||||
|
||||
`src/shared/support/hooks.ts` (lifecycle Playwright), `world.ts` (champs `page`/`appFrame`), `scripts/debug-browser.ts` (debug headed), `.playwright-profile{,-debug}/` (gitignored).
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Couche @ui — renderHelper.tsx rend tout écran dans LocalDataProvider + happy-dom, world.renderCurrentScreen() l'invoque à chaque navigateTo, assertions sur le DOM rendu avec les fixtures de seed déterministes
|
||||
---
|
||||
|
||||
# Couche `@ui`
|
||||
|
||||
`@ui` rend un écran avec `LocalDataProvider` (seed) + `RouterProvider` via happy-dom, puis assert sur le **DOM rendu**.
|
||||
|
||||
- Helper : `src/shared/test-harness/renderHelper.tsx` (installe les globals happy-dom, enveloppe l'écran). Invoqué depuis `world.ts:renderCurrentScreen()` à chaque `navigateTo(...)`.
|
||||
- Fixtures déterministes (`src/shared/data/seedData.ts`, voir concept `data-layer`) : `Marie Dupont`/`@mariedupont` = currentUser, `Jean Durand`/`@jeandurand` existe, 5 events, etc.
|
||||
|
||||
## Bons patterns d'assertion
|
||||
|
||||
```ts
|
||||
// Texte visible
|
||||
expect(this.getDomText()).to.include('Marie Dupont');
|
||||
// Présence d'élément par classe/rôle
|
||||
expect(this.renderedDoc!.querySelector('.app-avatar')).to.not.be.null;
|
||||
// Rendu conditionnel (rempli vs vide)
|
||||
expect(this.renderedDoc!.querySelectorAll('.app-card').length).to.be.greaterThan(0);
|
||||
// Champ requis rendu avec label + astérisque
|
||||
const labels = Array.from(this.renderedDoc!.querySelectorAll('p')).map(p => p.textContent ?? '');
|
||||
expect(labels.some(t => t.includes("Nom de l'événement *"))).to.be.true;
|
||||
```
|
||||
|
||||
## Champs & helpers de `FestipodWorld` (`src/shared/support/world.ts`)
|
||||
|
||||
- `renderedDoc: Document | null` — le DOM happy-dom rendu (peuplé par `renderCurrentScreen()`, appelé à chaque `navigateTo(...)`).
|
||||
- `currentScreenId: string | null` — l'écran courant.
|
||||
- Helpers d'assertion : `getDomText()` (texte du DOM), `hasText(t)`, `hasField(name)`, `hasElement(selector)` — ils **préfèrent le DOM rendu** mais **retombent sur la source** des écrans pour les steps non migrés (vestige, voir [[caveat_source-grep-vestiges]]).
|
||||
|
||||
> Les classes `app-*` confirment le thème moderne (cf. `app-architecture`). Les anti-patterns (regex sur source, détails d'implémentation) sont proscrits par [[rule_test-layer-contracts]]. Pour écrire un nouveau scénario, voir [[cookbook_add-scenario]].
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: rule
|
||||
summary: Chaque couche BDD répond à une question distincte — @ui = rendu (DOM + seed), @data = mutations/persistance broker, @e2e = collaboration des couches sur un parcours ; descendre chaque assertion à la couche la plus basse qui peut y répondre
|
||||
---
|
||||
|
||||
# Règle : contrat des couches de test
|
||||
|
||||
Chaque couche répond à **une question distincte**. Mélanger les préoccupations produit des tests fragiles qui cassent au refactor sans attraper de vraie régression. **Descendre toute assertion à la couche la plus basse qui peut y répondre.**
|
||||
|
||||
- **`@ui` — couche affichage.** Rend un écran avec `LocalDataProvider` (seed) + happy-dom et assert sur le DOM. Vérifie que *données connues → l'écran montre le texte et les éléments attendus*. **Ne teste pas** la navigation, les mutations, ni la persistance.
|
||||
|
||||
- **`@data` — couche données.** Pilote des mutations ORM via le **broker NextGraph réel** (harness headless, pas d'UI app). Vérifie que *les opérations sur shapes sont persistées et observables dans le wallet*. Pas de DOM ici — utiliser le bridge `window.__testData`.
|
||||
|
||||
- **`@e2e` — couche intégration.** Boote l'app réelle dans l'iframe broker (Playwright/Chromium). Vérifie que *les couches collaborent pour livrer un parcours* (créer → lister → modifier → recharger → toujours là). **Rare** : 1 scénario par chemin critique ; **ne jamais dupliquer** un check de contenu `@ui`.
|
||||
|
||||
## Pourquoi le coût impose la pyramide
|
||||
|
||||
`@ui` tourne in-process (instantané) ; `@data` boote un broker (~50s) ; `@e2e` boote broker + app + navigateur (~2min). Une affirmation de rendu appartient à `@ui`, pas à `@e2e`.
|
||||
|
||||
## Anti-patterns `@ui` à proscrire
|
||||
|
||||
```ts
|
||||
// ❌ regex sur la source : couple le test à la structure du code
|
||||
expect(/<Title[^>]*>Marie Dupont<\/Title>/.test(source)).to.be.true;
|
||||
// ❌ détails d'implémentation
|
||||
expect(/showDuplicateWarning/.test(source)).to.be.true;
|
||||
```
|
||||
|
||||
Préférer des assertions sur le **DOM rendu** + données de seed (voir [[knowledge_ui-layer]]). Les helpers/maps d'analyse de source sont des vestiges en voie de suppression : [[caveat_source-grep-vestiges]].
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
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]
|
||||
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é.
|
||||
|
||||
> 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)
|
||||
|
||||
## 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_seed-data]] — données de seed, `CURRENT_USER_ID`
|
||||
|
||||
> Sécurité/confidentialité (mono-store, confiance broker) : concept `app-security`.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
type: caveat
|
||||
summary: Le type FpEventData et le seed portent startDate/endDate/startTime/endTime/themes, mais le SHEX Event ne les définit pas — ces champs sont silencieusement perdus en mode connected (NextGraph)
|
||||
last_checked: 2026-06-15
|
||||
---
|
||||
|
||||
# Caveat : champs d'événement non persistés en mode connected
|
||||
|
||||
Le type app `FpEventData` (`src/shared/data/types.ts`) et le seed (`seedData.ts`) portent des champs **`startDate`, `endDate`, `startTime`, `endTime`, `themes`** — mais la **shape SHEX `Event`** (`src/shared/shapes/shex/festipodShapes.shex`) ne les définit **pas**. La shape ne couvre que : `title, description, date, location, distance, participantCount, coverImage, hostName, hostInitials` (à vérifier dans le `.shex`).
|
||||
|
||||
## 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.
|
||||
|
||||
## 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é**.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
type: caveat
|
||||
summary: La suppression de Participation (leaveEvent) se fait via ngSet.delete() — le bug de non-persistance qui avait motivé SPARQL DELETE est en grande partie corrigé, mais la persistance peut rester partielle ; vérifier après refresh
|
||||
last_checked: 2026-06-15
|
||||
---
|
||||
|
||||
# Caveat : suppression de Participation via `ngSet.delete()`
|
||||
|
||||
**É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.
|
||||
|
||||
## 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.
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
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]].
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
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
|
||||
|
||||
## 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]].
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
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]].
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Pièges internes de FestipodDataContext — currentUser NG résolu par username '@mariedupont' (fallback users[0]), auto-seed dev-only après 3s sans retry, participantCount muté en place (cache), currentUserId vide → IRI invalide, mutations no-op en mode local malgré le toast
|
||||
last_checked: 2026-06-15
|
||||
---
|
||||
|
||||
# Internals & pièges de `FestipodDataContext`
|
||||
|
||||
Comportements non évidents de `src/shared/context/FestipodDataContext.tsx` à connaître avant de toucher au contexte de données.
|
||||
|
||||
## Résolution du `currentUser` (mode NG)
|
||||
|
||||
En mode connected, le currentUser n'est **pas** `CURRENT_USER_ID` ('user-1', qui ne vaut qu'en mode local). Il est résolu par **`users.find(u => u.username === '@mariedupont') || users[0]`** (vers ligne 286). Pièges :
|
||||
- **Fallback silencieux** sur `users[0]` si `@mariedupont` absent → currentUser arbitraire.
|
||||
- Si le wallet est **vide** (`users.length === 0`), `currentUserId` devient `''` → toute `Participation` créée a un `user: ''` (**IRI invalide**), sans alerte. Bug silencieux possible à la première connexion sur un wallet vierge.
|
||||
- L'IRI du currentUser diffère entre mode local (ID de seed statique) et mode NG (IRI NextGraph dynamique) — ne pas comparer les deux.
|
||||
|
||||
## Auto-seed de dev
|
||||
|
||||
Un auto-seed se déclenche (vers lignes 263-283) **uniquement hors production** (`process.env.NODE_ENV !== 'production'`), après un **`setTimeout` de ~3s**, si les sets events ET users sont vides. Pièges :
|
||||
- **Pas de retry** : `hasTriedAutoSeed` (useRef) est posé une fois ; si le seed échoue, jamais réessayé (écran vide, juste un `console.error`).
|
||||
- Le délai de 3s est **heuristique** : si l'hydratation ORM est lente, le seed peut partir alors que des données arrivent.
|
||||
|
||||
## `participantCount` muté en place
|
||||
|
||||
`joinEvent`/`leaveEvent`/`updateEvent` **mutent directement** `ngEvent.participantCount` (`+1`/`-1`) — c'est un **cache** du nombre de `Participation`, pas une valeur recalculée. Il peut **désynchroniser** des objets `Participation` réels (ex. après un crash, un rejeu, ou la suppression partielle décrite dans [[caveat_participation-deletion]]). Ne pas s'y fier comme source de vérité du nombre de participants.
|
||||
|
||||
## Mutations no-op en mode local
|
||||
|
||||
En mode local/demo (`useLocalData`), `createEvent`/`joinEvent`/`leaveEvent`/`updateEvent` sont des **no-ops** (`console.log`, l'état ne change pas) — mais les écrans affichent quand même un **toast de succès** (« Tu participes »). UX potentiellement trompeuse : l'utilisateur croit s'être inscrit alors que rien n'a changé. Voir [[knowledge_data-modes]] pour le choix du provider selon le statut.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
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()
|
||||
---
|
||||
|
||||
# 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)
|
||||
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).
|
||||
|
||||
## 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** :
|
||||
- `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.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Types de données Fp* (Event, UserProfile, Participation persistés NextGraph ; MeetingPoint et Friendship encore local-only)
|
||||
---
|
||||
|
||||
# Entités de données
|
||||
|
||||
`src/shared/data/types.ts` :
|
||||
|
||||
| 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` | **local-only** | eventId, location, time, host |
|
||||
| `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`.
|
||||
|
||||
> 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]].
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
# Stack NextGraph (côté app)
|
||||
|
||||
```
|
||||
@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
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Shapes SHEX
|
||||
|
||||
`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
|
||||
|
||||
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`.
|
||||
|
||||
> 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]].
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: seedData.ts fournit des fixtures déterministes (10 users, events, participations) avec CURRENT_USER_ID = 'user-1' (Marie Dupont) ; utilisé en mode démo et par les tests @ui
|
||||
---
|
||||
|
||||
# Seed data
|
||||
|
||||
`src/shared/data/seedData.ts` fournit des fixtures **déterministes** :
|
||||
|
||||
- 10 users — **Marie Dupont = utilisateur courant**, `user-1`
|
||||
- Plusieurs events (dates, lieux, thèmes)
|
||||
- Participations, meeting points, friendships
|
||||
- `CURRENT_USER_ID = 'user-1'`
|
||||
|
||||
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`.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
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]].
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
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)
|
||||
---
|
||||
|
||||
# Règle : scope = `@graph` = private_store_id
|
||||
|
||||
Pour lire **et** écrire via l'ORM NextGraph :
|
||||
|
||||
- **Scope** : `useShape(shapeType, \`did:ng:${session.private_store_id}\`)`
|
||||
- **`@graph`** (cible des écritures) : `did:ng:${session.private_store_id}`
|
||||
|
||||
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`.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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/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]].
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
type: _overview
|
||||
summary: Modèle produit Festipod — le point de rencontre greffé sur un événement public comme unité de valeur, ses acteurs et ses concepts métier
|
||||
triggers:
|
||||
keywords: [point de rencontre, rencontre, greffe, greffer, événement, déclarant, hôte, inscrit, inscription, communauté, connexion, festival, déduplication]
|
||||
paths: ["src/modules/*/features/**"]
|
||||
---
|
||||
|
||||
# Functional domain
|
||||
|
||||
Le **domaine fonctionnel** de Festipod : ce que le produit promet et le vocabulaire métier qui le décrit. Source d'origine : `README.md §Modèle fonctionnel`.
|
||||
|
||||
**À lire en premier :** [[knowledge_business-model]] — sans lui, on confond l'événement (l'ancrage) et le point de rencontre (la valeur), et on modélise à l'envers.
|
||||
|
||||
## Idée pivot
|
||||
|
||||
Festipod laisse les utilisateurs créer des **points de rencontre** qui se *greffent* sur des **événements publics** existants. L'événement (festival, conférence…) n'est qu'un *prétexte* et un point d'ancrage spatio-temporel ; la valeur produite, c'est le point de rencontre. **On s'inscrit à un point de rencontre, jamais à un événement.**
|
||||
|
||||
## Périmètre & sécurité
|
||||
|
||||
Le modèle d'**autorisations / confidentialité** (qui voit quoi : « données personnelles = réseau seulement », anonymat via inbox, capabilities) n'est pas encore implémenté — il vit aujourd'hui comme incubation dans [[brief_2026-05-18_authorization-matrix]] (concept `nextgraph-platform`). Il graduera en règles/`behavior_` quand le multi-user atterrira. C'est la raison pour laquelle il n'y a pas encore de concept `app-security` distinct.
|
||||
|
||||
## Liens
|
||||
|
||||
- [[knowledge_actors-and-concepts]] — référence des acteurs et concepts métier
|
||||
- [[knowledge_roadmap]] — fonctionnalités actuelles vs évolutions à venir
|
||||
- [[brief_2026-06-15_event-deduplication]] — défi ouvert de déduplication des événements en P2P
|
||||
- `nextgraph-platform` — où vit la dérivation de la structure de données cible (authz matrix, multi-store)
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
type: brief
|
||||
summary: Défi ouvert — en infra P2P, deux utilisateurs peuvent déclarer le même événement public et fragmenter les points de rencontre greffés ; pistes non tranchées
|
||||
---
|
||||
|
||||
# Déduplication des événements en infra décentralisée
|
||||
|
||||
**Status:** Défi ouvert — non tranché
|
||||
**Capturé:** 2026-06-15 (issu de `README.md §Défis ouverts`)
|
||||
|
||||
## Problème
|
||||
|
||||
NextGraph étant P2P, rien n'empêche deux utilisateurs de **déclarer indépendamment le même événement public** (par ex. « Eurockéennes 2027 ») et de produire deux entrées distinctes. La dispersion qui en résulte **fragmente les points de rencontre greffés** et réduit leur visibilité — ce qui va à l'encontre de la fonction première de l'app (cf. [[knowledge_business-model]]).
|
||||
|
||||
## Pistes envisagées (non tranchées)
|
||||
|
||||
- **Recherche avant création** — proposer à l'utilisateur, lors de la déclaration, les événements déjà déclarés dans son réseau / ses communautés qui correspondent à sa saisie.
|
||||
- **Identifiant externe canonique** — utiliser une URL officielle de l'événement, Wikidata, ou `schema.org/Event` pour reconnaître les doublons et les présenter comme un seul événement à l'affichage.
|
||||
- **Curation** — laisser des curators (humains ou communautaires) fusionner / vetter les entrées canoniques.
|
||||
|
||||
## Lien avec le modèle d'écriture
|
||||
|
||||
Ce défi est couplé à une question ouverte de [[brief_2026-05-18_authorization-matrix]] : **qui peut modifier un événement déclaré** (propriétaire / wiki / immuable). Un modèle *wiki* faciliterait la convergence ; un modèle *propriétaire* la complique. À arbitrer ensemble.
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Référence des acteurs (utilisateur, connexion, déclarant, hôte, inscrit, membre) et des concepts métier (point de rencontre, événement, communauté, liste curated, connexion)
|
||||
---
|
||||
|
||||
# Acteurs et concepts métier
|
||||
|
||||
Référence du vocabulaire. Tous les acteurs sont des spécialisations d'un **utilisateur** authentifié dans un contexte donné — pas des rôles de compte distincts.
|
||||
|
||||
## Acteurs
|
||||
|
||||
| Acteur | Définition |
|
||||
|---|---|
|
||||
| **Utilisateur** | Toute personne ayant un compte (un wallet NextGraph). Racine de tous les autres. |
|
||||
| **Connexion (« ami »)** | Un autre utilisateur avec qui je suis connecté. Sert à scoper les listes (« mes amis qui participent à… ») et la confiance. Bilatérale (acceptation des deux côtés). |
|
||||
| **Déclarant d'un événement** | L'utilisateur qui a inséré l'événement dans Festipod. *N'est pas forcément l'organisateur réel* : juste celui qui le référence. |
|
||||
| **Hôte d'un point de rencontre** | L'utilisateur qui a créé un point de rencontre rattaché à un événement. |
|
||||
| **Inscrit à un point de rencontre** | Un utilisateur inscrit à un point de rencontre ; de fait il devient participant à l'événement parent. |
|
||||
| **Membre d'une communauté d'intérêt** | Un utilisateur abonné à une communauté pour découvrir les événements qu'elle référence. |
|
||||
|
||||
## Concepts métier
|
||||
|
||||
| Concept | Définition |
|
||||
|---|---|
|
||||
| **Point de rencontre** | *L'unité de valeur de l'app.* Un moment de rencontre proposé par un hôte à un endroit et un horaire donnés, greffé sur un événement public. C'est ce à quoi on s'inscrit. |
|
||||
| **Événement** | L'ancrage. Un événement public réel référencé dans Festipod pour servir de support à des points de rencontre. Simple prétexte (titre, dates, lieu, thèmes). |
|
||||
| **Communauté d'intérêt** | Un groupement thématique d'utilisateurs. Sert surtout à découvrir des événements (via abonnement) et à délimiter les périmètres de référencement. |
|
||||
| **Liste curated** | Une liste d'événements éditorialisée (par un utilisateur ou une communauté), distincte de « les événements que j'ai déclarés ». Permet d'organiser/recommander. |
|
||||
| **Connexion** | Lien de confiance bilatéral entre deux utilisateurs (équivalent « ami »). |
|
||||
|
||||
> Communauté, liste curated et abonnement sont en grande partie **prospectifs** (cf. [[knowledge_roadmap]]). La matrice d'autorisations détaillée par type de donnée vit dans [[brief_2026-05-18_authorization-matrix]].
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Le point de rencontre est l'unité de valeur, greffée sur un événement-prétexte ; on s'inscrit au point de rencontre, pas à l'événement
|
||||
---
|
||||
|
||||
# Modèle métier : le point de rencontre greffé
|
||||
|
||||
> Festipod permet aux utilisateurs de créer des **points de rencontre** qui viennent se « greffer » sur des **événements publics existants**. L'objectif : favoriser les rencontres autour de ces événements.
|
||||
|
||||
## L'inversion à comprendre
|
||||
|
||||
L'**événement public** (festival, conférence, salon, exposition…) n'est **qu'un prétexte** et un *point d'ancrage temporel et géographique*. La valeur produite par l'app, c'est le **point de rencontre** que les utilisateurs viennent y greffer pour se retrouver.
|
||||
|
||||
Conséquences directes sur la modélisation :
|
||||
|
||||
- **On s'inscrit à un point de rencontre, pas à un événement.** Sans points de rencontre, un événement Festipod n'a aucun intérêt.
|
||||
- Le **déclarant** d'un événement n'est *pas* (forcément) son organisateur réel — c'est juste quelqu'un qui a inséré la référence dans Festipod pour que d'autres puissent y attacher des points de rencontre.
|
||||
- L'**hôte** d'un point de rencontre est celui qui l'a créé ; l'acte de créer rend hôte. De même l'acte de déclarer un événement rend déclarant.
|
||||
|
||||
## Authentification
|
||||
|
||||
**Tous les utilisateurs sont authentifiés** (chacun possède un wallet NextGraph) — il n'y a pas d'accès anonyme à l'app. Les différents « acteurs » (déclarant, hôte, inscrit, connexion…) sont des *spécialisations d'un utilisateur dans un contexte donné*, pas des comptes distincts. Voir [[knowledge_actors-and-concepts]].
|
||||
|
||||
## Stack porteuse
|
||||
|
||||
App web mobile-first, Bun + React + **NextGraph** (P2P, local-first, chiffré de bout en bout). Le choix P2P a une conséquence métier forte : voir le défi de [[brief_2026-06-15_event-deduplication]].
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Ce qui est implémenté aujourd'hui (cycle événement + point de rencontre, profils, connexions) vs les évolutions identifiées mais non faites (communautés, abonnements, listes curated, multi-user)
|
||||
---
|
||||
|
||||
# Fonctionnalités actuelles vs évolutions à venir
|
||||
|
||||
## Implémenté (écrans visibles via le router)
|
||||
|
||||
- Authentification via wallet NextGraph
|
||||
- Cycle de vie d'événement (déclaration, consultation, mise à jour)
|
||||
- Cycle de vie de point de rencontre (rattaché à un événement)
|
||||
- Inscription / désinscription à un point de rencontre
|
||||
- Liste des participants à un événement
|
||||
- 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.
|
||||
|
||||
## Évolutions identifiées (non implémentées)
|
||||
|
||||
- **Abonnement à une communauté d'intérêt** pour découvrir ses événements (discovery distribué).
|
||||
- **Abonnement à un utilisateur** pour suivre ses déclarations sans être ami.
|
||||
- **Listes curated** — créer/partager des sélections éditorialisées.
|
||||
- **Multi-utilisateurs collaboratif** : aujourd'hui chaque utilisateur a ses données isolées dans son wallet. Le passage collaboratif (un point de rencontre vu par plusieurs) suppose un refactor de la couche données — voir [[brief_2026-05-17_multi-store-refactor]].
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: _overview
|
||||
summary: NextGraph comme système EXTERNE (stores, permissions, inbox, modèle d'intégration iframe, limites SDK) + les 4 briefs prospectifs qui dérivent la structure de données cible et le chemin multi-user de Festipod
|
||||
triggers:
|
||||
keywords: [nextgraph-rs, store, group store, dialog store, protected_store, public_store, inbox, capability, nuri, fork, ngd, broker, verifier, multi-store, multi-user, sharedWalletShim, storeRegistry, permission, social_query, OpenRepo]
|
||||
paths: ["src/shared/utils/ngGraph.ts", "src/shared/hooks/useShapeWithDefaults.ts", "scripts/build-ng-packages.sh"]
|
||||
---
|
||||
|
||||
# NextGraph platform
|
||||
|
||||
Deux choses ici, distinctes du concept `data-layer` (qui décrit l'**usage actuel** de NextGraph par l'app) :
|
||||
|
||||
1. **Référence du système externe NextGraph** — ses primitives de stockage et de permission, son inbox, son modèle d'intégration/déploiement, et ce que son SDK JS expose (ou pas).
|
||||
2. **Briefs prospectifs** — la dérivation de la structure de données *cible* de Festipod et les chemins pour y arriver (stopgap wallet partagé, refactor multi-store, fork moteur pour l'inbox).
|
||||
|
||||
> Le code de l'app touché par ces chantiers : `src/shared/utils/ngGraph.ts`, `useShapeWithDefaults.ts`, `FestipodDataContext.tsx`, `ngBootstrap.ts` — les seams du futur multi-store. Le modèle de **confidentialité/autorisations** (qui peut faire quoi) vit dans le concept `app-security` ([[brief_2026-05-18_authorization-matrix]]) ; ces chantiers data en sont l'infrastructure.
|
||||
|
||||
## Source locale
|
||||
|
||||
Le repo `nextgraph-rs` est cloné en `/home/sylvain/projects/nextgraph/nextgraph-rs` (soit `../../nextgraph/nextgraph-rs` depuis la racine projet). À consulter pour vérifier ce qui est réellement exposé au protocole/SDK plutôt que la doc.
|
||||
|
||||
## Référence (système externe)
|
||||
|
||||
- [[knowledge_stores-permissions]] — 5 types de stores, document/repo, capabilities/Nuri, inbox, exposition SDK JS
|
||||
- [[knowledge_integration-model]] — paquets JS, modèle iframe, où tourne le verifier, broker `ngd`, déploiement, reciblage build-time
|
||||
|
||||
## Briefs (chantiers prospectifs)
|
||||
|
||||
- [[brief_2026-05-17_multi-store-refactor]] — passer du mono-store actuel à une structure par entité
|
||||
- [[brief_2026-06-15_shared-wallet-shim]] — stopgap staging : wallet partagé unique + `storeRegistry`
|
||||
- [[brief_2026-05-21_fork-nextgraph-inbox]] — forker `nextgraph-rs` pour exposer l'inbox au SDK JS
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
type: brief
|
||||
summary: Passer du mono-store actuel (tout dans private_store) à une structure de stores par entité ; hardcoding dans ngGraph.ts + useShapeWithDefaults ; contrainte SDK bloquante (Group stores/inbox non exposés) ; refactor structurel possible avec placeholders en attendant l'API
|
||||
last_updated: 2026-05-17
|
||||
---
|
||||
|
||||
# Refactor multi-store NextGraph
|
||||
|
||||
**Status:** Incubating — aucun travail démarré
|
||||
**Last updated:** 2026-05-17
|
||||
|
||||
## Context
|
||||
|
||||
L'app est aujourd'hui *mono-store* : tout (events, profils, participations, friendships) atterrit dans le `private_store` de l'utilisateur connecté. Héritage de l'exemple expense-tracker-rdf, formalisé dans la décision du 2026-03-17 (concept `data-layer`, [[decision_2026-03-17_private-store-nuri-scope]]).
|
||||
|
||||
Ce choix bloque le multi-utilisateurs : le `private_store` est non partageable (*« not possible to share the documents of your private store »*, cf. [[knowledge_stores-permissions]]). Tant que tout y est, Bob ne verra jamais l'event d'Alice. Le modèle natif NextGraph est *multi-store par utilisateur* — Festipod doit s'y aligner avant de devenir collaboratif.
|
||||
|
||||
**Déclencheur :** discussion du 2026-05-17 — *poser le cap, exécuter plus tard*.
|
||||
|
||||
## What We Know
|
||||
|
||||
### État actuel du code
|
||||
|
||||
Deux fichiers concentrent le hardcoding du store unique :
|
||||
- `src/shared/utils/ngGraph.ts` — `ensureGraphNuri()` retourne `did:ng:${session.private_store_id}` pour TOUTES les entités.
|
||||
- `src/shared/hooks/useShapeWithDefaults.ts` — accepte un `storeNuri` mais l'appelant unique (`FestipodDataContext`) lui passe toujours le NURI du private_store.
|
||||
|
||||
Entités impactées (toutes mélangées) : `FpEvent` (→ store partagé), `FpUserProfile` (→ partie privée/publique), `FpParticipation` (→ avec son event), `FpMeetingPoint` (local-only aujourd'hui), `FpFriendship` (local-only, privée). Cf. concept `data-layer` §entités.
|
||||
|
||||
### Modèle cible proposé
|
||||
|
||||
> **Note (2026-05-19)** : [[brief_2026-05-18_authorization-matrix]] a depuis dérivé, à partir des seuls points validés, une structure différente — 3 stores natifs par utilisateur + Dialog stores, **sans Group store** dans le périmètre actuel. La structure à 4 niveaux ci-dessous reste pertinente pour le périmètre élargi (communautés, collaboration multi-hôte), aujourd'hui hors périmètre. À reconcilier à l'exécution.
|
||||
|
||||
Structure hiérarchique en **4 niveaux de Group stores** : index communautaire ⊃ communauté ⊃ event ⊃ meeting point.
|
||||
|
||||
| Entité | Store cible | Justification |
|
||||
|---|---|---|
|
||||
| Event (métadonnées) | Group « communauté » | La communauté possède l'event → contrôle qui le modifie |
|
||||
| Référence d'event (pointeur) | Group « index communautaire » | Discovery |
|
||||
| Participation | Group « event » | N'a de sens que dans son event |
|
||||
| MeetingPoint (métadonnées) | Group « event » | Le RDV appartient à l'event |
|
||||
| Participation à un MeetingPoint | Group « meeting point » | RSVP scopé au RDV |
|
||||
| UserProfile (partie publique) | public_store de l'utilisateur | Modèle natif |
|
||||
| Friendship | private_store de l'utilisateur | Purement personnelle |
|
||||
|
||||
### Contrainte SDK bloquante
|
||||
|
||||
Primitives présentes au protocole mais **non exposées dans `@ng-org/web`** (vérifié `0.1.2-alpha.13`) : création de Group stores + invitations/permissions ; **dépôt/lecture d'inbox** (mécanisme retenu pour la notif d'inscription, cf. [[brief_2026-05-18_authorization-matrix]]). `app_request_stream` est la méthode générique la plus susceptible de porter ce mécanisme une fois exposée (à confirmer côté Rust). Cf. [[knowledge_stores-permissions]] §Limites SDK.
|
||||
|
||||
**Implication :** le refactor *structurel* peut commencer sans attendre l'API, avec des placeholders (continuer à pointer `private_store_id` pour les Group stores impossibles). L'**aboutissement complet** (vrai multi-user) dépend de l'arrivée de l'API ou d'un contournement (voir [[brief_2026-05-21_fork-nextgraph-inbox]], [[brief_2026-06-15_shared-wallet-shim]]).
|
||||
|
||||
### Implications côté code
|
||||
|
||||
1. **Disparition de `ensureGraphNuri()`** comme helper unique → helpers par entité ou couche `storeRegistry` résolvant le NURI selon `(entité, contexte)`.
|
||||
2. **`useShapeWithDefaults` reste un wrapper** mais l'appelant choisit explicitement le store (N appelants demain).
|
||||
3. **Chaque entité déclare son store cible** (mapping centralisé ou convention shape→store).
|
||||
4. **`bootstrapWallet()`** (`src/shared/utils/ngBootstrap.ts`) revu : seed réparti, ou seed = données de l'utilisateur courant seulement.
|
||||
5. **`FestipodDataContext`** : hooks par entité, chacun avec son store résolu.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Quand crée-t-on un Group store de communauté (API absente) ? Acte explicite vs communauté par défaut ?
|
||||
2. Comment Bob connaît-il l'index communautaire d'Alice ? (possiblement via le public_store d'Alice)
|
||||
3. Faut-il vraiment 4 niveaux ? Le « meeting point = group store » mérite validation.
|
||||
4. Que devient le seed de démo quand les Group stores n'existent pas encore ?
|
||||
5. Migration des wallets de test existants (script / wipe-reseed / ignore) ?
|
||||
6. Bootstrap d'un user vierge : auto-créer un Group store « par défaut » ou attendre ?
|
||||
|
||||
## Possible Approaches
|
||||
|
||||
- **Refactor structurel d'abord, partage ensuite** (placeholders `private_store_id`).
|
||||
- **Registry centralisé** vs **résolution par convention**.
|
||||
- **Big-bang** vs **par entité** (commencer par Event).
|
||||
- **Maintenir un mode mono-store** parallèle pour dev/demo.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Invitation effective (capability sharing), permissions par rôle, discovery cross-wallet, contournement de l'UI wallet, mode P2P direct sans broker. → second chantier multi-user dont ce refactor est le prérequis structurel.
|
||||
|
||||
## Starting Points
|
||||
|
||||
- Concept `data-layer` → [[decision_2026-03-17_private-store-nuri-scope]] (la décision qu'on viendra modifier), état du pattern d'écriture
|
||||
- `src/shared/utils/ngGraph.ts`, `src/shared/hooks/useShapeWithDefaults.ts`, `src/shared/context/FestipodDataContext.tsx`, `src/shared/utils/ngBootstrap.ts`
|
||||
- NextGraph docs : [Documents et Stores](https://docs.nextgraph.org/en/documents/), [Getting started](https://docs.nextgraph.org/en/getting-started/)
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
type: brief
|
||||
summary: Forker temporairement nextgraph-rs pour exposer l'inbox au SDK JS (notif d'inscription, anonymat via from optionnel) — 3 couches : patch Rust (4 fichiers), auto-hébergement ngd+ng-app sur Coolify, intégration Festipod ; fork jetable abandonné quand l'upstream livrera sa solution
|
||||
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
|
||||
|
||||
## Context
|
||||
|
||||
Festipod doit notifier l'hôte d'un PdR quand quelqu'un s'inscrit, avec **identification si connexion / anonyme sinon** (cf. décision cadre inbox dans [[brief_2026-05-18_authorization-matrix]]). L'**inbox** NextGraph est idéale (le `from` optionnel donne l'anonymat) **mais n'est pas exposée au SDK JS** (cf. [[knowledge_stores-permissions]] §Inbox). Ce brief évalue **forker/patcher `nextgraph-rs`** pour l'exposer.
|
||||
|
||||
### Posture stratégique (cadrée par l'utilisateur)
|
||||
|
||||
Le fork est **explicitement temporaire, non destiné à l'upstream**. Hypothèse : NextGraph finira par exposer sa **propre** solution d'inbox au SDK JS, **possiblement différente**. Quand elle arrivera, on **abandonne le fork et on adapte Festipod**. Tant que leur solution n'est pas là : maintenir le fork à jour (rebase sur `upstream/main`, qui bouge vite en `0.1.2-alpha`) ; **déployer broker + ng-app depuis le fork** ; surveiller l'upstream pour basculer dès que possible. On ne vise **pas** une PR.
|
||||
|
||||
## What We Know
|
||||
|
||||
Trois couches.
|
||||
|
||||
### Couche 1 — Le patch Rust : 4 fichiers (broker vanilla)
|
||||
|
||||
1. **`engine/net/src/types.rs`** — `InboxMsgContent::Link` est une variante **unit** (stub) ; lui donner un payload (ou variante `Notification`) portant le NURI du PdR + lien vers l'`Inscription`. Ajouter un builder `InboxPost::new_link(...)` calqué sur `new_contact_details`. `from = None` → anonymat.
|
||||
2. **`engine/verifier/src/request_processor.rs`** — ajouter le bras de commande manquant (pas de bras `InboxPost`). Idéalement une commande haut-niveau (`NotifyInbox`) construisant le post côté Rust (garde le scellement crypto en Rust). Calquer sur `SocialQueryStart`.
|
||||
3. **`sdk/js/lib-wasm/src/lib.rs`** — exposer `pub async fn inbox_post_link(session_id, to_inbox_nuri, to_profile_nuri, link, anonymous)`, calqué sur `social_query_start`.
|
||||
4. **`engine/verifier/src/inbox_processor.rs`** (`process_inbox`) — bras de réception qui **matérialise** le message en document dans le store de l'hôte (calquer sur le handler `ContactDetails`). L'app lit ensuite via ORM/SPARQL — pas de nouvelle API de lecture d'inbox.
|
||||
|
||||
**Résolution d'identité** (connu/anonyme) : gratuite via SPARQL côté app (JOIN du NURI d'inbox émetteur contre les docs `social:contact`). **Découverte de l'inbox de l'hôte** : embarquer le NURI d'inbox du `public_store` de l'hôte dans le doc PdR ou le profil public (le flux QR-code de partage de profil le porte déjà).
|
||||
|
||||
### Couche 2 — Déploiement (depuis le fork)
|
||||
|
||||
Détail dans [[knowledge_integration-model]]. Le verifier patché tourne **dans l'iframe ng-app** → **construire et auto-héberger le `ngd` + le ng-app** depuis le fork, puis rebuilder le `@ng-org/web` de Festipod avec `NG_REDIR_SERVER`/`NG_DEV*` pointant sur ce ng-app. **Aucune réécriture de l'intégration Festipod** (reste iframe). Le routage inbox du broker est déjà natif, mais comme on auto-héberge le ng-app patché, **on déploie toute la stack depuis le fork** (un seul arbre source).
|
||||
|
||||
- **Local** : `ngd` + ng-app du fork ; Festipod buildé avec `NG_DEV`/`NG_DEV_LOCAL_BROKER`.
|
||||
- **Serveur de test** : `ngd` + ng-app du fork sur notre domaine ; Festipod buildé avec `NG_REDIR_SERVER=notre-domaine`.
|
||||
|
||||
#### Hébergement sur Coolify — 3 pièces web
|
||||
|
||||
1. **`ngd`** — démon WebSocket **stateful** : conteneur avec **volume persistant** pour `--base-path` (RocksDB + clés + PeerId, jamais wipé), mode `--domain` derrière le Traefik de Coolify. Build : Dockerfiles officiels cassés → **écrire notre Dockerfile multi-stage Rust** (RocksDB exige llvm/clang). Premier démarrage **interactif** (lien d'invitation wallet admin) → scripter via `ngcli` ou faire une fois à la main puis persister dans le volume.
|
||||
2. **ng-app** (frontend iframe, wasm patché) — **build statique** (`pnpm webfilebuild`). Servi en statique (buildpack ou nginx).
|
||||
3. **Routage** : un même domaine sert le statique du ng-app ET proxifie le WebSocket vers ngd.
|
||||
|
||||
Plus **Festipod** lui-même (app Bun → skill `coolify-hosting` pour CELLE-CI, pas pour le `ngd` Rust). Drivers de complexité : build Rust+RocksDB sans Dockerfile prêt, conteneur stateful à volume critique, premier-run interactif, double-service (statique + WS).
|
||||
|
||||
### Couche 1 (libs JS) — paquets npm clients patchés
|
||||
|
||||
**On maintient des versions patchées des paquets clients, pas seulement le wasm.** Le forwarding générique permet *techniquement* d'atteindre une méthode wasm sans toucher le JS, mais c'est un **hack** (non typé, fragile) — test rapide seulement. À modifier réellement :
|
||||
|
||||
- **`@ng-org/web`** — modifié de toute façon (URL broker) → y ajouter `inbox_post_link` dans la **surface d'API typée + `.d.ts`**.
|
||||
- **Méthodes streamées** (si lecture inbox en *flux* un jour) — entrée des deux côtés (`E` + `streamed_api`). Pour la seule **écriture** (requête/réponse), inutile.
|
||||
- **`@ng-org/orm`** — à modifier **si** on intègre l'écriture inbox au flux ORM. Sinon (appel `ng.inbox_post_link` à côté), inutile.
|
||||
- **`@ng-org/alien-deepsignals`, `@ng-org/shex-orm`** — a priori inchangés.
|
||||
|
||||
#### Outillage existant : `scripts/build-ng-packages.sh`
|
||||
|
||||
`bun run build:ng` build les 4 paquets depuis `$NEXTGRAPH_RS/sdk/js/*` (défaut `../../nextgraph/nextgraph-rs`) → `pnpm pack` → `.tgz` dans `.ng-tarballs/` → `bun add` réécrit `package.json` vers les tarballs locaux. **Pattern d'origine du projet** : le commit `fd6d408` l'a abandonné quand les alphas ont été publiées sur npm. Pour repasser au custom : **réactiver `bun run build:ng`**. Nuances : `@ng-org/web` est TS pur (le script crée un *stub* `lib-wasm` ; le tarball porte l'API inbox typée + l'URL broker bakée, **pas** le wasm) ; pointer le script sur la **branche patchée** (retirer le `git pull --ff-only`) ; option recommandée : patcher `@ng-org/web` pour lire l'URL broker au **runtime** (évite de rebuilder par domaine).
|
||||
|
||||
### Couche 3 — Intégration dans Festipod
|
||||
|
||||
Exposer la méthode ne suffit pas. Chantiers (certains préexistent à l'inbox) :
|
||||
|
||||
- **Modéliser le PdR.** Les SHEX (`src/shared/shapes/shex/festipodShapes.shex`) ne définissent qu'`Event`/`UserProfile`/`Participation` — **pas de `MeetingPoint`** (local-only), ni d'entité notification. Ajouter les shapes + `bun run build:orm`.
|
||||
- **Implémenter l'inscription (aujourd'hui no-op).** Dans `FestipodDataContext.tsx`, `joinEvent`/`leaveEvent` sont des `console.log`. Le vrai flux : (a) écrire l'`Inscription` dans le `protected_store` de l'inscrit (via multi-store, [[brief_2026-05-17_multi-store-refactor]]), (b) appeler `ng.inbox_post_link(...)` pour notifier l'inbox du PdR.
|
||||
- **Porter le NURI d'inbox de l'hôte** sur le doc PdR (ou lookup profil).
|
||||
- **Lire et résoudre les notifications côté hôte** : lire les docs notification matérialisés (ORM/SPARQL), JOIN identité contre `social:contact`. UI : « N inscrits dont X identifiés ».
|
||||
- **Câblage session** via `src/shared/utils/ngSession.ts`.
|
||||
|
||||
**Dépendances** : présuppose (1) le fork SDK livré, (2) le refactor multi-store. **Surface jetable** : à l'arrivée de l'API officielle, migrer aussi ces points d'appel Festipod.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- `NotifyInbox` haut-niveau vs `InboxPost` brut ? (haut-niveau préféré, garde la crypto en Rust)
|
||||
- Où sourcer le NURI d'inbox de l'hôte (doc PdR vs lookup profil) ?
|
||||
- Forme de la matérialisation côté réception (quels triples) ?
|
||||
- Suppression côté inbox : un déposant peut-il retirer son dépôt ? (résiduelle, cf. [[brief_2026-05-18_authorization-matrix]])
|
||||
- Cadence de rebase du fork ? Critère de bascule vers la solution upstream ?
|
||||
- `@ng-org/web` : patch runtime vs tarball par domaine ?
|
||||
- `ngd` Coolify : automatiser le premier-run vs one-shot manuel persisté ? Un service (reverse-proxy maison) ou deux ?
|
||||
|
||||
## Possible Approaches
|
||||
|
||||
- **A. Fork temporaire + auto-hébergement (retenu comme stopgap)** — patch des 4 fichiers, déploiement depuis le fork. Vrai inbox, anonymat natif. Coût : maintenir le fork + héberger. Jetable.
|
||||
- **B. Contribution upstream — écartée** comme objectif.
|
||||
- **C. Pas de patch, détourner `social_query_start`** — repli, livrable tout de suite mais limité aux **contacts** (pas d'anonyme vers un hôte non-connecté).
|
||||
|
||||
> Voir aussi [[brief_2026-06-15_shared-wallet-shim]] : le vrai multi-user (lecture cross-wallet) suppose en plus un patch `OpenRepo` + capabilities, au-delà de l'inbox.
|
||||
|
||||
## Starting Points
|
||||
|
||||
- [[knowledge_integration-model]], [[knowledge_stores-permissions]]
|
||||
- [[brief_2026-05-18_authorization-matrix]] — la décision cadre inbox que ce patch sert
|
||||
- Repo local `nextgraph-rs` : `sdk/js/lib-wasm/src/lib.rs`, `engine/verifier/src/{request_processor,inbox_processor}.rs`, `engine/net/src/types.rs`
|
||||
- Remotes : `origin` = `git.nextgraph.org/slaivyn/nextgraph-rs` (fork perso), `upstream` = `git.nextgraph.org/NextGraph/nextgraph-rs`
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
# Stopgap multi-user : wallet partagé unique (`sharedWalletShim`)
|
||||
|
||||
**Status:** Cadré — décisions prises, implémentation non démarrée
|
||||
**Last updated:** 2026-06-15
|
||||
|
||||
## Context
|
||||
|
||||
NextGraph ne permet **aucun partage de données entre wallets** aujourd'hui. Vérifié dans `nextgraph-rs` (2026-06-15) :
|
||||
|
||||
- une session de verifier ne contient que ses **3 stores** dans `self.repos` ;
|
||||
- un NURI étranger lève `RepoNotFound` (`engine/verifier/src/request_processor.rs`, `resolve_target`) ;
|
||||
- `OpenRepo` est un **TODO non implémenté** côté broker (`engine/verifier/src/verifier.rs:1423`) ;
|
||||
- le champ `access`/`ReadCap` du NURI **n'est jamais inspecté** → les capabilities sont ignorées.
|
||||
|
||||
Donc lire le store d'un autre utilisateur — **même son `public_store`** — est impossible via le SDK. Cela élimine toute la famille « chacun garde son wallet, les autres lisent son public » (piste C ci-dessous).
|
||||
|
||||
**Objectif :** mettre Festipod en **staging** avec des **utilisateurs amicaux**, **sans enjeu de sécurité**, tout en branchant l'app sur le vrai NextGraph et en **préfigurant l'infra cible** (structure dérivée dans [[brief_2026-05-18_authorization-matrix]]).
|
||||
|
||||
**Décision retenue :** Piste A (wallet partagé unique) + couche `storeRegistry`, broker **`nextgraph.net`**.
|
||||
|
||||
## What We Know
|
||||
|
||||
### Les trois familles de contournement (et pourquoi A)
|
||||
|
||||
| Famille | Idée | Verdict |
|
||||
|---|---|---|
|
||||
| **A — wallet partagé** | un seul wallet pour tous, multi-user simulé côté app | **retenue** : livrable vite, zéro travail moteur, local-first préservé |
|
||||
| B — NG comme backend | un backend Bun détient un wallet, clients en HTTP | écartée : abandonne le local-first, plus lourd |
|
||||
| C — lecture cross-wallet | chacun son wallet, on lit le public des autres | **infaisable** (cf. Context) sans fork moteur |
|
||||
| D — fork moteur | patcher `OpenRepo` + capabilities | hors stopgap : chemin cible réel, lourd (cf. [[brief_2026-05-21_fork-nextgraph-inbox]]) |
|
||||
|
||||
### Architecture en trois couches
|
||||
|
||||
```
|
||||
┌─ 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 │
|
||||
├─ 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.
|
||||
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.**
|
||||
|
||||
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 :
|
||||
|
||||
| Entité | Périmètre | Doc aujourd'hui | Store 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 |
|
||||
| Profil privé de U (settings, email) | private | `U/private` | `private_store` de U |
|
||||
| Connexion A↔B | dialog | `dialog/A∙B` | Dialog store A↔B |
|
||||
|
||||
### Filtre d'isolation (retenu)
|
||||
|
||||
Un seul wallet ⇒ tout lisible par tous. Pour que le staging se **comporte** comme la cible, la couche données filtre les lectures par `currentAccountId` + connexions : `private` → propriétaire seul ; `protected` → propriétaire + connexions ; `public` → tous. Isolation **pas appliquée par la crypto** mais **honorée** par l'app (démo réaliste, bugs de conception attrapés tôt). **Supprimé à la migration** (la crypto prend le relais).
|
||||
|
||||
### Ce qui survit vs ce qui est jetable
|
||||
|
||||
- **Survit** : mapping entité→périmètre, abstraction `storeRegistry`, séparation par documents, docs dialog, forme UX signup/login.
|
||||
- **Jetable** : le wallet partagé unique, le `sharedWalletShim`, le filtre d'isolation. (Pas de mots de passe applicatifs — **username seul**.)
|
||||
|
||||
### Code impacté
|
||||
|
||||
- `src/shared/utils/ngGraph.ts` — `ensureGraphNuri()` remplacé par `storeRegistry`.
|
||||
- `src/shared/hooks/useShapeWithDefaults.ts` — `storeNuri` résolu par (entité, compte).
|
||||
- `src/shared/context/FestipodDataContext.tsx` — câbler `joinEvent`/`leaveEvent` (no-op aujourd'hui) ; appliquer le filtre d'isolation.
|
||||
- `CURRENT_USER_ID` constant → `currentAccountId` sélectionnable (persisté `localStorage`).
|
||||
- `src/shared/utils/ngBootstrap.ts` — seed réparti **par documents**.
|
||||
|
||||
## 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`) ?
|
||||
|
||||
## Possible Approaches
|
||||
|
||||
Posture retenue : **A + `storeRegistry` maintenant**, structuré pour la migration. Introduire dès à présent l'indirection `storeRegistry` (esquissée dans [[brief_2026-05-17_multi-store-refactor]]) — chaque entité *déclare* le store où elle *devrait* vivre, le résolveur renvoyant aujourd'hui vers le document du périmètre dans le wallet partagé. Le jour du vrai multi-user (fork moteur D ou solution upstream), on **bascule le résolveur** sans réécrire les écrans.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Le **vrai multi-user** (lecture cross-wallet) : suspendu à un **fork moteur** (`OpenRepo` + capabilities) — voir [[brief_2026-05-21_fork-nextgraph-inbox]].
|
||||
- L'**auto-hébergement** du broker/ng-app (le staging tourne sur `nextgraph.net`).
|
||||
- Toute **sécurité réelle** (credential partagé, mots de passe, chiffrement par utilisateur).
|
||||
|
||||
## Starting Points
|
||||
|
||||
- [[brief_2026-05-18_authorization-matrix]] — les périmètres repris exactement
|
||||
- [[brief_2026-05-17_multi-store-refactor]] — l'indirection `storeRegistry` y est esquissée
|
||||
- [[brief_2026-05-21_fork-nextgraph-inbox]] — le chemin cible réel (hors stopgap)
|
||||
- Concept `data-layer` — état actuel mono-store ; [[knowledge_stores-permissions]] — limites SDK, inbox
|
||||
- `src/shared/utils/ngGraph.ts`, `src/shared/hooks/useShapeWithDefaults.ts`, `src/shared/context/FestipodDataContext.tsx`, `src/shared/utils/ngBootstrap.ts`
|
||||
- Source `nextgraph-rs` : `sdk/rust/src/tests/sparql_regressions.rs:136-200` (preuve multi-document), `engine/verifier/src/verifier.rs` (TODO `OpenRepo`)
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
type: decision
|
||||
summary: Flux login/logout du stopgap wallet partagé — le vrai login NextGraph (redirect broker) apparaît en premier, perçu comme une barrière technique d'accès à l'environnement ; l'écran applicatif « Connexion » (username seul → localStorage) EST le login perçu ; « Déconnexion » efface juste le username sans toucher NG ; vrai logout planqué
|
||||
last_updated: 2026-06-15
|
||||
---
|
||||
|
||||
# Décision 2026-06-15 — Flux de login/logout du stopgap wallet partagé
|
||||
|
||||
Arbitrage du flux d'authentification perçu pour le stopgap [[brief_2026-06-15_shared-wallet-shim]]. Frozen.
|
||||
|
||||
## Contrainte de départ
|
||||
|
||||
Le login NextGraph **n'est pas programmable** : c'est une **redirection web** vers la page du broker (`nextgraph.net`). Impossible d'ouvrir le wallet partagé en silence — il faut au minimum un passage par le redirect broker, au moins une fois par device. La question n'est donc pas *« comment éviter le redirect »* mais *« comment l'ordonner et le présenter »* pour que l'UX reste cohérente.
|
||||
|
||||
## Décision : option 2 — gate technique d'abord, « Connexion » applicative ensuite
|
||||
|
||||
Deux couches d'auth distinctes, présentées dans cet ordre :
|
||||
|
||||
1. **Couche réelle (technique, non perçue comme login)** — le redirect broker apparaît **immédiatement, avant tout rendu de l'app**. Comme il précède l'app, l'utilisateur le lit comme une **barrière technique d'accès à l'environnement de test** (type mur de beta), **pas** comme un login applicatif. Mêmes credentials partagés pour tous (donnés dans l'invitation, façon « code d'accès »). Une fois par device, puis persistant. **Jamais étiqueté « login ».** Un splash Festipod minimal précède le redirect pour donner du contexte.
|
||||
2. **Couche applicative (perçue comme LE login)** — écran **« Connexion »** = saisie du **username** (→ `localStorage`, `currentAccountId`). C'est le login *dans la perception* de l'utilisateur. **Sans mot de passe** (décision username-seul) → connexion **déclarative** : n'importe qui prend n'importe quel username (cohérent zéro-sécurité / amis). **« Déconnexion »** = efface **seulement** le username et revient à l'écran « Connexion » ; **n'appelle aucune fonction NG**.
|
||||
|
||||
Le **vrai logout** (`ng.session_stop` / `user_disconnect` / `wallet_close`) reste **planqué** (réglages/debug), car il force un nouveau redirect.
|
||||
|
||||
Le label **« Connexion »/« Déconnexion »** (et non « Changer de profil ») est un choix explicite : on assume de faire passer le username pour le login applicatif, puisque la barrière technique n'est pas perçue comme tel.
|
||||
|
||||
## Pourquoi (vs option 1 écartée)
|
||||
|
||||
**Option 1 écartée** — faux login d'abord (username), puis page d'avertissement « saisissez tel username/password », puis bouton *Continuer* déclenchant le redirect. Rejetée : workflow étrange, **double-login dissonant** (« je me suis déjà connecté, pourquoi je recommence ailleurs ? »), page d'avertissement qui **ressemble à une arnaque**, et le redirect **ressurgit en plein usage** à chaque expiration de session.
|
||||
|
||||
**Option 2 retenue** parce que :
|
||||
- **Cohérence du modèle mental** : la barrière technique n'étant pas perçue comme un login, la paire **Connexion/Déconnexion** applicative est complète et auto-cohérente — plus aucun mismatch sur le logout (se déconnecter ramène à l'écran de connexion, les deux dans la même couche).
|
||||
- **Dégradation gracieuse** : un re-gate après redémarrage navigateur (perte de `sessionStorage`) se lit comme « reconnexion à l'environnement », pas comme un bug.
|
||||
- **Implémentation plus simple** : `NextGraphContext` fait déjà le flux `connect`/redirect ; l'écran « Connexion » est un écran in-app normal ; pas de page d'avertissement bespoke.
|
||||
- **Similarité avec l'infra cible** (objectif directeur du stopgap) : la forme **« redirect broker → app »** est exactement le flux du vrai multi-wallet. À la migration, on **supprime l'écran « Connexion » username** et la **barrière technique devient le vrai login per-user** — la forme du flux ne change pas.
|
||||
|
||||
## Faits techniques vérifiés (`nextgraph-rs`, 2026-06-15)
|
||||
|
||||
- **Persistance de session : OUI.** Wallet mémorisé côté iframe broker (`localStorage` long-terme + `sessionStorage` pour la session active) ; au rechargement, `init()` retrouve la session **sans re-déclencher le redirect** tant que la session broker existe (`sdk/js/web/src/index.ts`, `sdk/js/api-web/main.ts`). Un **redémarrage complet du navigateur** (perte de `sessionStorage`) peut re-déclencher le gate.
|
||||
- **Logout réel exposé : OUI.** `ng.session_stop()`, `ng.user_disconnect()`, `ng.wallet_close()` (`sdk/js/lib-wasm/src/lib.rs`) ; arrêtent la session / effacent le wallet ; **forcent un nouveau redirect** ensuite → d'où le choix de **ne pas** les appeler dans la « Déconnexion » applicative et de planquer le vrai logout.
|
||||
|
||||
## Conséquences côté code (Festipod)
|
||||
|
||||
- `NextGraphContext` — déclencher le `connect`/redirect **au boot**, avant le rendu de l'app (+ splash pré-redirect).
|
||||
- Un écran applicatif **« Connexion »** (username → `localStorage` / `currentAccountId`), username résolu contre les comptes du `sharedWalletShim`.
|
||||
- Une **« Déconnexion »** qui efface seulement le username (aucun appel NG).
|
||||
- Vrai logout exposé seulement en réglages/debug.
|
||||
|
||||
## See Also
|
||||
|
||||
- [[brief_2026-06-15_shared-wallet-shim]] — le stopgap que cette décision complète
|
||||
- Concept `data-layer` — `NextGraphContext`, auto-init conditionnel, flux redirect broker
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Modèle d'intégration NextGraph — @ng-org/web est un proxy iframe (verifier tourne dans l'iframe ng-app, pas dans le broker), reciblable au build via NG_REDIR_SERVER/NG_DEV*, broker ngd stateful WebSocket ; modifier le verifier = rebuilder le ng-app, pas le broker
|
||||
last_checked: 2026-05-21
|
||||
---
|
||||
|
||||
# Modèle d'intégration et de déploiement NextGraph
|
||||
|
||||
Comment une app web tierce s'intègre à NextGraph, et **où tourne le moteur (verifier)**. Vérifié dans `nextgraph-rs` le 2026-05-21.
|
||||
|
||||
NextGraph s'utilise via un **proxy iframe** (`@ng-org/web`) : l'app tierce ne contient pas le moteur, elle délègue à un ng-app hébergé (défaut `nextgraph.net`) qui exécute le moteur dans une iframe.
|
||||
|
||||
## Les paquets JS
|
||||
|
||||
- **`@ng-org/web`** — paquet **publié**. Proxy postMessage léger (aucun wasm embarqué). **Le** chemin d'intégration tierce ; `@ng-org/orm` et tous les exemples en dépendent. **Festipod l'utilise.**
|
||||
- **`@ng-org/api-web`** — **privé** (non publié). Moteur navigateur complet (charge `@ng-org/lib-wasm` dans un Web Worker). Consommé uniquement par `app/nextgraph` (frontend ng-app) — **pas** une cible d'intégration tierce.
|
||||
- **`@ng-org/lib-wasm`** — moteur compilé wasm (contient le verifier). Source `sdk/js/lib-wasm/`.
|
||||
- **`nextgraph`** (npm) — API NodeJS (build `pkg-node`).
|
||||
- **`@ng-org/orm`** — ORM réactif (`useShape`…), bâti sur `@ng-org/web`.
|
||||
|
||||
## Où tourne le verifier
|
||||
|
||||
Dans le modèle web standard (iframe), le verifier tourne **dans l'iframe** : `app/nextgraph` charge `api-web` → `lib-wasm` dans un Web Worker, côté navigateur. Le broker (`ngd`) ne fait que **transport et stockage**.
|
||||
|
||||
**Conséquence** : modifier la logique du verifier (`request_processor`, `inbox_processor`) = reconstruire le **ng-app**, pas le broker.
|
||||
|
||||
## Le modèle iframe & reciblage build-time
|
||||
|
||||
`@ng-org/web` redirige vers le ng-app hébergé, qui recharge l'app tierce en iframe après auth, puis relaie par `postMessage`. **Reciblable au build** (`sdk/js/web/src/index.ts`, `import.meta.env`) :
|
||||
|
||||
| Variable | Cible |
|
||||
|---|---|
|
||||
| `NG_REDIR_SERVER` | défaut `nextgraph.net` |
|
||||
| `NG_DEV3` | `127.0.0.1:3033` |
|
||||
| `NG_DEV` | `localhost:14402`/`14404` |
|
||||
| `NG_DEV_LOCAL_BROKER` | `localhost:1421` |
|
||||
|
||||
**Pas d'override runtime** — `init()` ne prend pas d'URL broker. Pour pointer vers un ng-app auto-hébergé : **rebuilder `@ng-org/web`** (TS pur, sans wasm → build trivial).
|
||||
|
||||
## Plomberie proxy ↔ iframe ↔ worker (générique)
|
||||
|
||||
Le chemin d'appel d'une méthode est **entièrement générique** (aucune allowlist) : `@ng-org/web` est un `Proxy` JS qui relaie *n'importe quel* nom de méthode par `postMessage` ; `app/nextgraph` dispatch via `Reflect.apply(ng[method], …)`. **Conséquence** : une nouvelle fonction wasm en requête/réponse simple est *atteignable* sans toucher le JS — mais c'est un **hack** non typé (test rapide, pas un plan ; cf. [[brief_2026-05-21_fork-nextgraph-inbox]]). Cas **streamé** : exige une entrée des deux côtés (`E` dans `@ng-org/web` + `streamed_api` dans api-web ; méthodes streamées actuelles : `doc_subscribe`, `orm_start_graph`, `orm_start_discrete`, `file_get`, `app_request_stream`).
|
||||
|
||||
## Le broker (ngd)
|
||||
|
||||
- Supporte déjà nativement l'inbox (`inbox_post`, `inbox_register`, `inbox_pop_for_user` dans `engine/net/src/server_broker.rs`) — un `ngd` standard routerait l'inbox, **aucun patch broker nécessaire**.
|
||||
- Démon **WebSocket** (`async-tungstenite`), **stateful** : RocksDB sous `--base-path`, PeerId persisté (volume critique).
|
||||
- CLI : `--local PORT`, `--domain DOMAIN:PORT,LOCAL_PORT` (mode derrière reverse-proxy TLS-terminé type Traefik/Coolify).
|
||||
- **Ne sert pas de statique** : le ng-app frontend est un déploiement statique séparé (`pnpm webfilebuild`). Premier démarrage **interactif** (lien d'invitation wallet admin). Dockerfiles officiels **cassés**.
|
||||
|
||||
> Détail du déploiement depuis un fork : [[brief_2026-05-21_fork-nextgraph-inbox]] §Couche 2.
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
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
|
||||
---
|
||||
|
||||
# Stores NextGraph et droits d'accès
|
||||
|
||||
Référence des primitives de stockage et permission de NextGraph (**système externe**, pas le code de Festipod). Socle des briefs [[brief_2026-05-17_multi-store-refactor]] et [[brief_2026-05-18_authorization-matrix]].
|
||||
|
||||
Source : doc NextGraph officielle ([Documents & Stores](https://docs.nextgraph.org/en/documents/), [Getting started](https://docs.nextgraph.org/en/getting-started/)) vérifiée le 2026-05-21.
|
||||
|
||||
## Points d'entrée du code source local
|
||||
|
||||
Repo cloné en `../../nextgraph/nextgraph-rs` (cf. `_overview`) :
|
||||
- `sdk/js/lib-wasm/src/lib.rs` — API wasm effectivement exposée au JS.
|
||||
- `engine/net/src/app_protocol.rs` — enum `AppRequestCommandV0`, formats `NuriV0`.
|
||||
- `engine/verifier/src/request_processor.rs` — dispatch effectif des `app_request` (la vérité sur ce qui est *traité*).
|
||||
- `engine/net/src/types.rs` — types inbox (`InboxPost`, `InboxMsg`, `InboxMsgContent`).
|
||||
- `engine/verifier/src/inbox_processor.rs` — traitement des messages d'inbox.
|
||||
|
||||
## Les 5 types de stores
|
||||
|
||||
| Store | Lecture | Écriture | Création |
|
||||
|---|---|---|---|
|
||||
| **Private** | Titulaire seul | Titulaire seul | Par défaut |
|
||||
| **Protected** | Titulaire + détenteurs d'un lien + permission | Titulaire + collaborateurs permissionnés | Par défaut |
|
||||
| **Public** | Tout le monde, sans capability | Titulaire seul | Par défaut |
|
||||
| **Group** | Membres du groupe | Membres du groupe (collaboratif) | À la demande |
|
||||
| **Dialog** | Les deux utilisateurs uniquement | Les deux utilisateurs uniquement | À la demande |
|
||||
|
||||
Citations doc (verbatim) : Private — *« only you have access to … not possible to share »* ; Protected — *« share … but they will need a special link and permission »*, *« protected social profile »* ; Public — *« equivalent to your website … without the need for special permissions »* ; Group — *« each Group is a separate Store … documents inherit the permissions of the store »* ; Dialog — *« hold all the data you exchange with another user (and only with that other user) … You cannot add more users »*.
|
||||
|
||||
Tout wallet a d'office les **3 stores** private/protected/public (session : `private_store_id`, `protected_store_id`, `public_store_id`). Group et Dialog se créent à la demande.
|
||||
|
||||
## Concepts transverses
|
||||
|
||||
**Document vs Repo.** *« A Repo is the equivalent of an E2EE group for one and only one Document. »* **1 document = 1 repo** (commits + permissions). Identifiant : `did:ng:o:<RepoID>`. Un **store** est lui-même un document spécial qui regroupe et permissionne d'autres documents.
|
||||
|
||||
**Granularité.** Écriture gérée au niveau **Document (repo)**, pas branche/bloc. Lecture plus fine possible (par bloc/branche). Héritage : un Group store peut faire hériter ses permissions à ses documents.
|
||||
|
||||
**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
|
||||
|
||||
**Chaque document a une inbox native.** Un non-éditeur peut y **déposer un lien (DID cap)** sans être invité éditeur ; le propriétaire **modère**. NURI : `did:ng:d:<inbox_id>`. Contenu : enum `InboxMsgContent` (`ContactDetails`, `DialogRequest`, **`Link`**, `Patch`, `ServiceRequest`, `ExtRequest`, `RemoteQuery`, `SocialQuery`…). Message **scellé** (`crypto_box::seal`) vers la pubkey de l'inbox → seul le titulaire déchiffre. Champ `from` **optionnel** → expéditeur **anonyme** possible. C'est le « identifié si connu, anonyme sinon » voulu par Festipod, **natif au protocole** (mécanisme retenu pour la notification d'inscription, cf. [[brief_2026-05-18_authorization-matrix]]).
|
||||
|
||||
### L'inbox n'est PAS utilisable directement depuis le SDK JS
|
||||
|
||||
- `app_request(request)` est exposé, et `AppRequestCommandV0::InboxPost` + `AppRequest::inbox_post()` existent. **MAIS** le `request_processor` du verifier **n'a aucun bras `InboxPost`** (commandes traitées : `OrmStart(Discrete)`, `Fetch`, `FileGet`, `OrmUpdate`, `OrmDiscreteUpdate`, `SocialQueryStart`, `QrCodeProfile(Import)`, `Header`, `Create`, `FilePut`). Envoyer un `InboxPost` ne déclenche rien.
|
||||
- Construire un `InboxPost` exige le scellement crypto côté Rust ; **aucun helper wasm** ne l'expose.
|
||||
- Le dépôt en inbox n'est déclenché qu'**en interne** par `QrCodeProfileImport` (`post_to_inbox(new_contact_details)`) et `social_query_start` (propagation via inbox des **contacts**).
|
||||
|
||||
**Conséquence** : pas de moyen propre de « drop a Link » arbitraire dans l'inbox d'un PdR depuis le SDK JS aujourd'hui. → chantier [[brief_2026-05-21_fork-nextgraph-inbox]]. Piste connexe : `social_query_start` EST exposé (requête fédérée via inbox jusqu'à `degree` sauts) mais limité aux **contacts** (ne couvre pas la notif anonyme vers un hôte non-connecté).
|
||||
|
||||
## Limites du SDK JS
|
||||
|
||||
`@ng-org/web` (vérifié `0.1.2-alpha.13` = `upstream/main` au 2026-05-21, version installée) **n'expose pas** : création de Group/Dialog store ; partage de capability (Nuri avec droits) ; manipulation de permissions ; dépôt/lecture d'inbox.
|
||||
|
||||
Méthodes JS disponibles : `doc_create`, `doc_subscribe`, `sparql_query`, `sparql_update`, `orm_start_graph`, `orm_start_discrete`, `graph_orm_update`, `discrete_orm_update`, `file_get`, `app_request_stream`. La doc annonce *« An API will be provided for permission manipulation »* (sans date).
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
type: _overview
|
||||
summary: Stack et outillage — Bun-first (runtime, bundler, APIs natives), build pipeline, et commandes du projet
|
||||
triggers:
|
||||
keywords: [bun, bunx, build, bundler, vite, webpack, jest, npm, storybook, "bun.serve", hmr, tailwind, package.json]
|
||||
paths: ["build.ts", "package.json", "bunfig.toml", "tsconfig.json", "src/index.ts", "src/index.html", ".storybook/**", "scripts/**"]
|
||||
---
|
||||
|
||||
# Tech stack
|
||||
|
||||
Stack et outillage du projet. Principe directeur : **Bun-first** — Bun remplace Node/npm/vite/webpack/jest et fournit les APIs serveur natives.
|
||||
|
||||
**À lire en premier :** [[rule_bun-first]] — la convention qui décide quel outil utiliser.
|
||||
|
||||
## Liens
|
||||
|
||||
- [[rule_bun-first]] — utiliser Bun, pas Node/npm/vite/jest/express/ws/pg…
|
||||
- [[knowledge_bun-apis]] — APIs natives Bun (serve, sqlite, redis, sql, file, shell)
|
||||
- [[knowledge_build-pipeline]] — build.ts, bundler, serveur, harness buildé à part, Storybook
|
||||
- [[knowledge_stack-and-commands]] — composants de la stack + scripts réels (+ quirks)
|
||||
- [[knowledge_deployment]] — Dockerfile, prod depuis src/, pas de CI, `portless` en dev
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Dev en bun --hot, build prod via build.ts (bundler Bun + plugin Tailwind) vers dist/, alias @/* → ./src/*
|
||||
---
|
||||
|
||||
# Build pipeline
|
||||
|
||||
- **Dev** : `bun --hot src/index.ts` (via `bun run dev`) — HMR, port 3000.
|
||||
- **Prod** : `bun run build` → `build.ts` (bundler Bun + plugin Tailwind) → `dist/`.
|
||||
- **Alias de chemin** : `@/* → ./src/*` (déclaré dans `tsconfig.json`).
|
||||
|
||||
Le serveur sert `src/index.html`, qui charge `src/app/frontend.tsx` (voir `app-architecture` §app-shell). Le bundler transpile le TSX et bundle le CSS sans outil externe — pas de Vite/webpack/esbuild (cf. [[rule_bun-first]]).
|
||||
|
||||
## Détails de `build.ts` et du serveur
|
||||
|
||||
- `build.ts` scanne `src/**/*.html` comme entrypoints (aujourd'hui un seul : `src/index.html`), `target: 'browser'`, minify + sourcemap linked, plugin `bun-plugin-tailwind`. Ajouter un 2e `.html` créerait un 2e bundle.
|
||||
- `src/index.ts` (`Bun.serve`) sert : `/reports/cucumber` (rapport HTML), des stubs `/api/hello*`, et un **catch-all `/*` → `src/index.html`** (routing SPA, doit rester en dernier). HMR si `NODE_ENV !== 'production'`, port via `PORT`.
|
||||
|
||||
## Le harness de test est buildé à part
|
||||
|
||||
⚠️ `build.ts` ne build **pas** les harness de test. Les hooks Cucumber (`src/shared/support/hooks.ts`) lancent un `bun build` **à la demande** pour `src/shared/test-harness/harness.tsx` (et `harness-ng.tsx`) → `dist/test-harness*.js`. C'est un entrypoint séparé du build app — voir concept `bdd-testing`.
|
||||
|
||||
## Storybook
|
||||
|
||||
`storybook dev -p 6006` — **webpack5 + SWC** (pas Vite). Les décorateurs (`.storybook/`) injectent la pile complète de providers (Theme > NextGraph > FestipodData > Router) et importent `src/index.css` ; viewport mobile par défaut. Couplage dur au contexte projet (pas réutilisable hors Festipod).
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: APIs natives Bun utilisées par le projet — Bun.serve (HTTP/WS/routes), HTML imports bundlés, bun:sqlite, Bun.redis, Bun.sql, Bun.file, Bun.$
|
||||
---
|
||||
|
||||
# APIs natives Bun
|
||||
|
||||
Référence des APIs Bun à privilégier (cf. [[rule_bun-first]]). Doc complète : `node_modules/bun-types/docs/**.mdx`.
|
||||
|
||||
## Serveur — `Bun.serve()`
|
||||
|
||||
Supporte WebSockets, HTTPS et routes. Pas besoin d'`express`/`ws`.
|
||||
|
||||
```ts
|
||||
import index from "./index.html"
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": { GET: (req) => new Response(JSON.stringify({ id: req.params.id })) },
|
||||
},
|
||||
websocket: { open: (ws) => ws.send("hello"), message: (ws, m) => ws.send(m), close: (ws) => {} },
|
||||
development: { hmr: true, console: true },
|
||||
})
|
||||
```
|
||||
|
||||
C'est le mécanisme de `src/index.ts` (voir concept `app-architecture` §app-shell).
|
||||
|
||||
## HTML imports (frontend)
|
||||
|
||||
`Bun.serve()` sert des HTML imports ; le bundler Bun transpile/bundle automatiquement `.tsx`/`.jsx`/`.js` et le CSS (Tailwind inclus). Un `<script type="module" src="./frontend.tsx">` dans le HTML suffit — pas de Vite.
|
||||
|
||||
## Stockage & shell
|
||||
|
||||
- **`bun:sqlite`** pour SQLite (pas `better-sqlite3`)
|
||||
- **`Bun.redis`** pour Redis (pas `ioredis`)
|
||||
- **`Bun.sql`** pour Postgres (pas `pg`/`postgres.js`)
|
||||
- **`WebSocket`** intégré (pas `ws`)
|
||||
- **`Bun.file`** plutôt que `node:fs` readFile/writeFile
|
||||
- **`Bun.$\`ls\`** plutôt qu'`execa`
|
||||
|
||||
Bun charge `.env` automatiquement → ne pas utiliser `dotenv`.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Déploiement — Dockerfile multi-stage Bun Alpine qui lance `bun run start` depuis src/ (pas dist/), EXPOSE 3000, env PORT/NODE_ENV ; aucun CI/CD committé ; dev passe par le wrapper portless
|
||||
last_checked: 2026-06-15
|
||||
---
|
||||
|
||||
# Déploiement & infra
|
||||
|
||||
## Dockerfile
|
||||
|
||||
Un `Dockerfile` existe (multi-stage Bun Alpine) :
|
||||
- `FROM oven/bun:1-alpine`, stages `install` (`bun install --frozen-lockfile` depuis `package.json` + `bun.lock`) puis `release` (copie `node_modules` + source).
|
||||
- `ENV NODE_ENV=production`, `USER bun`, `EXPOSE 3000/tcp`, `ENTRYPOINT ["bun","run","start"]`.
|
||||
|
||||
**Quirk** : `start` = `NODE_ENV=production bun src/index.ts` → le conteneur **exécute la source TypeScript directement** (Bun transpile à la volée), il **n'utilise pas `dist/`**. Le `bun run build` (→ `dist/`) n'est donc **pas** sur le chemin de prod par défaut. Si on veut servir le build, il faut changer l'entrypoint.
|
||||
|
||||
## CI/CD
|
||||
|
||||
**Aucun** pipeline committé (`.github/workflows/` absent, pas de config Coolify dans le repo). Angle mort assumé. Pour héberger l'app Bun, le skill `coolify-hosting` s'applique (mentionné aussi dans concept `nextgraph-platform` pour distinguer Festipod du `ngd` Rust).
|
||||
|
||||
## Variables d'environnement
|
||||
|
||||
- `PORT` (défaut 3000), `NODE_ENV` (active/désactive HMR et l'auto-seed dev — cf. concept `data-layer`).
|
||||
- Aucun `.env*` committé (`.env` est gitignored). Pas de gestion de secrets dans le repo.
|
||||
|
||||
## Dev
|
||||
|
||||
`bun run dev` = **`portless festipod bun --hot src/index.ts`** — passe par le wrapper **`portless`** (outil externe de gestion de port), pas un `bun --hot` nu. HMR actif hors production.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Composants de la stack (Bun, React, NextGraph, Storybook, Cucumber, Tailwind-dans-le-build) et liste réelle des scripts package.json, dont les quirks (cucumber via node+tsx, build:orm au chemin périmé, build:ng pour le fork local)
|
||||
---
|
||||
|
||||
# Stack & commandes
|
||||
|
||||
## Composants
|
||||
|
||||
| Couche | Techno |
|
||||
|---|---|
|
||||
| Runtime / bundler / test | **Bun** (cf. [[rule_bun-first]]) |
|
||||
| UI | **React** (mobile-first, largeur max 768px — style dans concept `app-architecture`) |
|
||||
| Données | **NextGraph** P2P local-first (concept `data-layer`) |
|
||||
| Build CSS | **Tailwind** (`tailwindcss` + `bun-plugin-tailwind`) — présent dans le build, mais les écrans stylent via `app-*`/inline, pas d'utilitaires Tailwind (cf. concept `app-architecture`) |
|
||||
| Exploration UI | **Storybook** (webpack5 + SWC, port 6006) |
|
||||
| Tests | **Cucumber/Gherkin** FR multi-couches + Playwright + happy-dom + chai (concept `bdd-testing`) |
|
||||
|
||||
## Scripts `package.json` (réels)
|
||||
|
||||
| Script | Commande / rôle |
|
||||
|---|---|
|
||||
| `dev` | `portless festipod bun --hot src/index.ts` — dev HMR via wrapper `portless` (cf. [[knowledge_deployment]]) |
|
||||
| `start` | `NODE_ENV=production bun src/index.ts` — prod, depuis `src/` (pas `dist/`) |
|
||||
| `build` | `bun run build.ts` — bundler Bun + Tailwind → `dist/` ([[knowledge_build-pipeline]]) |
|
||||
| `test:cucumber` | enchaîne `cucumber:run` → `cucumber:report` → `features:parse` → `steps:extract` |
|
||||
| `cucumber:run` | `node --import tsx/esm …/cucumber-js` — **via Node+tsx, pas Bun** (compat plugins Playwright/happy-dom) |
|
||||
| `test:data` | idem `--tags @data` |
|
||||
| `test:auth-setup` | `bun scripts/setup-test-auth.ts` — bootstrap wallet de test persistant |
|
||||
| `cucumber:report` | `bun scripts/parse-test-results.ts` — `cucumber-report.json` → HTML |
|
||||
| `features:parse` | `bun scripts/parse-features.ts` → `features.ts` |
|
||||
| `steps:extract` | `bun scripts/extract-step-definitions.ts` |
|
||||
| `build:orm` | `rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm` |
|
||||
| `build:ng` | `bash scripts/build-ng-packages.sh` — rebuild des `@ng-org/*` depuis le fork local (concept `nextgraph-platform`) |
|
||||
| `storybook` / `build-storybook` | Storybook dev (6006) / build statique |
|
||||
|
||||
## Pièges
|
||||
|
||||
- **`cucumber:run`/`test:data` tournent sous Node+tsx**, pas Bun — les plugins de test ne chargent pas en import Bun natif. Ne pas « bunifier » ces scripts.
|
||||
- **`build:orm` cible `./src/shapes/shex` et `./src/shapes/orm`**, alors que les shapes réelles vivent sous **`src/shared/shapes/`** — le chemin du script est vraisemblablement **périmé** (à corriger ou exécuter avec les bons chemins ; vérifier avant de régénérer l'ORM).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
type: rule
|
||||
summary: Par défaut utiliser Bun et ses APIs natives, jamais les équivalents Node — bun au lieu de node/ts-node, bun install/test/build, bunx, et pas d'express/ws/pg/dotenv
|
||||
---
|
||||
|
||||
# Règle : Bun-first
|
||||
|
||||
Par défaut, utiliser **Bun** et ses APIs natives plutôt que les équivalents Node.js.
|
||||
|
||||
| Au lieu de… | Utiliser |
|
||||
|---|---|
|
||||
| `node <file>`, `ts-node` | `bun <file>` |
|
||||
| `jest`, `vitest` | `bun test` |
|
||||
| `npm/yarn/pnpm install` | `bun install` |
|
||||
| `npm run <script>` | `bun run <script>` |
|
||||
| `npx <pkg>` | `bunx <pkg>` |
|
||||
| `webpack`, `esbuild`, `vite` | `bun build` / bundler Bun (HTML imports) |
|
||||
| `express` | `Bun.serve()` |
|
||||
| `better-sqlite3` | `bun:sqlite` |
|
||||
| `ioredis` | `Bun.redis` |
|
||||
| `pg`, `postgres.js` | `Bun.sql` |
|
||||
| `ws` | `WebSocket` (intégré) |
|
||||
| `node:fs` readFile/writeFile | `Bun.file` |
|
||||
| `execa` | `Bun.$\`...\`` |
|
||||
| `dotenv` | (inutile — Bun charge `.env` automatiquement) |
|
||||
|
||||
Détail des APIs : [[knowledge_bun-apis]].
|
||||
|
||||
## Pourquoi
|
||||
|
||||
Le projet est tout-Bun (runtime, bundler, test, serveur). Réintroduire un outil Node redondant ajoute une dépendance, divergerait des conventions du repo, et casse l'intégration native (HMR, transpilation TS automatique, chargement `.env`). C'est un choix de cohérence, pas une préférence cosmétique.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Automated Headless Wallet Creation for CI
|
||||
|
||||
**Date:** 2026-03-12 15:00
|
||||
**Status:** Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Data-layer BDD tests (`@data` scenarios) require a NextGraph wallet in a persistent Chromium profile. Previously, the first run required manual interaction: a visible browser opened and the user had to create a wallet and close the browser. This blocked CI execution.
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option A: Programmatic wallet creation via NG SDK
|
||||
Call `ng.wallet_create()` directly from Node/Bun, bypassing the UI entirely.
|
||||
|
||||
**Arguments for:**
|
||||
- Fastest execution
|
||||
- No browser needed for wallet creation
|
||||
|
||||
**Arguments against:**
|
||||
- `@ng-org/web` is browser-only (WASM + postMessage)
|
||||
- Would need to reverse-engineer the registration API at `account.nextgraph.eu`
|
||||
- Doesn't test the real auth flow
|
||||
|
||||
### Option B: Automate the browser UI flow headlessly
|
||||
Use Playwright to drive the same wallet creation UI a real user would use, but in headless mode.
|
||||
|
||||
**Arguments for:**
|
||||
- Tests the real auth/login feature end-to-end
|
||||
- No API reverse-engineering needed
|
||||
- Same persistent profile used for subsequent test runs
|
||||
- CI-ready with no manual steps
|
||||
|
||||
**Arguments against:**
|
||||
- Depends on `nextgraph.eu` and `account.nextgraph.eu` being reachable
|
||||
- UI changes in NextGraph could break the automation
|
||||
- Adds ~27s to first run
|
||||
|
||||
## Decision
|
||||
|
||||
Option B — automate the browser UI. The wallet creation flow (navigate to `nextgraph.eu` → "Create Wallet" → accept ToS at `account.nextgraph.eu` → fill username/password → submit) is itself a legitimate test of the app's auth feature. The dependency on external services is acceptable since the tests already depend on the broker being reachable.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Tests are fully CI-ready (no human interaction)
|
||||
- Auth/login flow is tested as a side effect
|
||||
- Single command `bun run test:data` works from a clean state
|
||||
|
||||
**Negative:**
|
||||
- Requires internet access (nextgraph.eu, account.nextgraph.eu)
|
||||
- Fragile to NextGraph UI changes (button text, form IDs)
|
||||
|
||||
**Risks:**
|
||||
- `account.nextgraph.eu` rate limiting could block CI runs that frequently recreate wallets
|
||||
@@ -1,48 +0,0 @@
|
||||
# Conditional NextGraph Init Based on Broker Iframe Detection
|
||||
|
||||
**Date:** 2026-03-13 14:00
|
||||
**Status:** Accepted
|
||||
|
||||
## Context
|
||||
|
||||
`@ng-org/web`'s `initNgWeb()` checks `window.self === window.top`. When the app runs standalone (not in an iframe), it redirects the entire page to `nextgraph.net/redir/` to trigger broker authentication. This caused the app to redirect on every load — even during development or when the user hadn't clicked "Se connecter".
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option A: Always auto-init NG on mount
|
||||
**Arguments for:**
|
||||
- Simpler code — no branching logic
|
||||
|
||||
**Arguments against:**
|
||||
- Causes immediate redirect to broker when loaded standalone
|
||||
- Breaks development workflow
|
||||
- User sees broker login page instead of the app
|
||||
|
||||
### Option B: Conditional auto-init based on iframe detection
|
||||
**Arguments for:**
|
||||
- When in iframe, the broker has already authenticated — safe to auto-init
|
||||
- When standalone, user must explicitly click "Se connecter" to trigger the redirect
|
||||
- Preserves standalone demo/development experience
|
||||
- Matches `@ng-org/web`'s own detection logic
|
||||
|
||||
**Arguments against:**
|
||||
- Relies on `window.self !== window.top` heuristic (could theoretically be wrong if embedded in non-broker iframe)
|
||||
|
||||
## Decision
|
||||
|
||||
Option B. `NextGraphContext` checks `const isInsideBroker = typeof window !== 'undefined' && window.self !== window.top` at module level. `useEffect` only auto-calls `initNg()` when `isInsideBroker` is true. The `connect()` callback remains available for explicit user-initiated connection.
|
||||
|
||||
Additionally, `FestipodDataContext` now renders empty data (not seed data) during the `connecting` phase to avoid flashing demo content before the wallet loads.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- App loads without redirecting — works standalone for development and demo
|
||||
- In broker iframe, connection is seamless and automatic
|
||||
- No seed data flash during wallet connection
|
||||
|
||||
**Negative:**
|
||||
- None significant
|
||||
|
||||
**Risks:**
|
||||
- If `@ng-org/web` changes its detection logic, our guard may diverge — keep them aligned
|
||||
@@ -1,67 +0,0 @@
|
||||
# Use private_store_id as useShape scope and @graph
|
||||
|
||||
**Date:** 2026-03-17 16:00
|
||||
**Status:** Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Clicking "Charger données de test" loaded data in-memory (via ORM signals) but produced `RepoNotFound` errors from `doc_create` and `orm_frontend_update`. Data disappeared after page reload because SPARQL writes never reached the broker. The NextGraph verifier's `self.repos` HashMap didn't contain the private store repo, so `resolve_target()` failed.
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option A: `did:ng:i` scope + `doc_create` for @graph
|
||||
Use "entire user site" scope for reads, create a new document for writes.
|
||||
|
||||
**Arguments for:**
|
||||
- `did:ng:i` is well-documented as a valid subscription scope
|
||||
- `doc_create` returns a real document NURI
|
||||
|
||||
**Arguments against:**
|
||||
- `did:ng:i` uses a special code path (`NuriTargetV0::UserSite`) that doesn't open individual repos
|
||||
- `doc_create` calls `resolve_target(NuriTargetV0::PrivateStore)` which needs the repo in `self.repos` — fails if repo wasn't opened
|
||||
- Requires complex retry logic / timing workarounds
|
||||
|
||||
### Option B: `private_store_id` as both scope AND @graph
|
||||
Mirror the expense-tracker-rdf example: `useShape(type, `did:ng:${session.private_store_id}`)` and `@graph: `did:ng:${session.private_store_id}``.
|
||||
|
||||
**Arguments for:**
|
||||
- Proven pattern: expense-tracker-rdf uses exactly this and works
|
||||
- `orm_start_graph` with private store NURI opens the repo in the verifier's `self.repos` HashMap
|
||||
- Subsequent writes via `orm_frontend_update` find the repo because it's now in the cache
|
||||
- Simple, no retry logic needed
|
||||
|
||||
**Arguments against:**
|
||||
- Slightly less flexible than `did:ng:i` (scoped to one store)
|
||||
- Requires passing session to `useShapeWithDefaults`
|
||||
|
||||
### Option C: `did:ng:i` scope + reuse existing entity @graph
|
||||
Subscribe with `did:ng:i`, then reuse `@graph` from any existing entity for writes.
|
||||
|
||||
**Arguments for:**
|
||||
- Works for returning users who already have data
|
||||
|
||||
**Arguments against:**
|
||||
- Fails for empty wallets (no existing entities to reuse)
|
||||
- Still needs `doc_create` fallback which hits the same `RepoNotFound` issue
|
||||
|
||||
## Decision
|
||||
|
||||
**Option B**: Use `did:ng:${session.private_store_id}` as both `useShape` scope and `@graph` for writes. This matches the official expense-tracker-rdf example exactly.
|
||||
|
||||
The `useShapeWithDefaults` hook accepts a `storeNuri` parameter. `FestipodDataContext.useNgData()` gets the session from `useNextGraph()` and passes `did:ng:${session.private_store_id}`.
|
||||
|
||||
`ensureGraphNuri()` simplified: checks existing entities first (optimization), then falls back to `did:ng:${session.private_store_id}`.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Writes work immediately after connection (no retries needed)
|
||||
- Data persists across page reloads
|
||||
- Pattern matches official NextGraph examples
|
||||
- All 7 e2e scenarios pass including data persistence
|
||||
|
||||
**Negative:**
|
||||
- `useShapeWithDefaults` signature changed (added `storeNuri` parameter)
|
||||
|
||||
**Risks:**
|
||||
- If NextGraph changes the private store behavior, this would break
|
||||
@@ -1,81 +0,0 @@
|
||||
# Use SPARQL DELETE instead of ORM ngSet.delete() for object removal
|
||||
|
||||
**Date:** 2026-03-17 18:00
|
||||
**Status:** Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Leaving an event requires deleting the user's `Participation` object from the NextGraph store. The ORM's `DeepSignalSet.delete()` method updates the local reactive state (UI reflects the change immediately) but the deletion does not persist to the broker — after page refresh, the participation reappears.
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option A: ORM `ngSet.delete(item)`
|
||||
|
||||
The ORM README shows `dogs.delete(aDog)` as the intended API. Internally, `.delete()` generates a `{ op: "remove", path: "/<syntheticId>" }` patch, delivered via microtask to `OrmSubscription.onSignalObjectUpdate`, which calls `ng.graph_orm_update()`.
|
||||
|
||||
**Arguments for:**
|
||||
- Official ORM API, shown in README examples
|
||||
- Immediate local reactive update (instant UI feedback)
|
||||
|
||||
**Arguments against:**
|
||||
- Does not persist in practice: `delete()` returns `true`, local set updates, but after refresh the object is back
|
||||
- The `graph_orm_update` WASM call may not correctly handle "remove" patches for top-level set objects (possible engine bug)
|
||||
- No error is thrown — fails silently
|
||||
|
||||
### Option B: `ng.sparql_update()` with SPARQL DELETE
|
||||
|
||||
Bypass the ORM patch mechanism entirely. Use `DELETE WHERE { GRAPH <graph> { <subject> ?p ?o } }` to remove all RDF triples for the object.
|
||||
|
||||
**Arguments for:**
|
||||
- Works: deletion persists across page refresh
|
||||
- The broker confirms via `TORMO became invalid` + `GraphOrmUpdate` remove, which reactively removes the item from the ORM set
|
||||
- Direct control over RDF triple removal
|
||||
|
||||
**Arguments against:**
|
||||
- Not instant: UI update waits for the SPARQL round-trip + broker `GraphOrmUpdate` callback (near-instant in practice, ~50ms)
|
||||
- Must not combine with `ngSet.delete()` — running both causes CRDT conflicts where the item reappears
|
||||
|
||||
### Option C: `ngSet.delete()` + `ng.sparql_update()` together
|
||||
|
||||
Use `.delete()` for instant UI and SPARQL for persistence.
|
||||
|
||||
**Arguments for:**
|
||||
- Instant UI feedback + guaranteed persistence
|
||||
|
||||
**Arguments against:**
|
||||
- **Does not work**: the ORM `.delete()` patch and the SPARQL DELETE backend update conflict in the CRDT, resulting in neither UI change nor persistence
|
||||
|
||||
## Decision
|
||||
|
||||
**Option B: SPARQL DELETE only.** The broker sends back a `GraphOrmUpdate` with `op: "remove"` that reactively removes the item from the ORM set, so the UI still updates — just not synchronously.
|
||||
|
||||
Do NOT call `ngSet.delete()` alongside `sparql_update()` — they conflict.
|
||||
|
||||
## Implementation
|
||||
|
||||
```typescript
|
||||
// In FestipodDataContext.tsx leaveEvent():
|
||||
const session = await sessionPromise;
|
||||
await ng.sparql_update(
|
||||
session.session_id,
|
||||
`DELETE WHERE { GRAPH <${partGraph}> { <${partId}> ?p ?o } }`,
|
||||
partGraph,
|
||||
);
|
||||
```
|
||||
|
||||
Imports: `ng` from `@ng-org/web`, `sessionPromise` from `../utils/ngSession`.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- Deletion actually persists
|
||||
- Single source of truth (broker → ORM → UI)
|
||||
|
||||
**Negative:**
|
||||
- Slight UI delay (~50ms) vs instant for property mutations
|
||||
- Pattern diverges from ORM README examples
|
||||
|
||||
**Risks:**
|
||||
- If `ng.sparql_update` API changes, this breaks
|
||||
- Other delete operations (if added) must follow the same pattern
|
||||
- The ORM `ngSet.delete()` bug may be fixed in a future version — revisit when upgrading `@ng-org/orm`
|
||||
@@ -1,84 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
Feature-based architecture where code is organized by business domain (module), not by technical layer.
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
src/modules/
|
||||
event/ # 7 screens, 5 features — events CRUD, discovery, participants, meeting points
|
||||
user/ # 5 screens, 11 features — profiles, friends, sharing
|
||||
home/ # 2 screens — dashboard, settings
|
||||
auth/ # 2 screens — login, welcome/onboarding
|
||||
workshop/ # 0 screens, 6 features — workshop/atelier specs (future)
|
||||
meeting/ # 0 screens, 1 feature — meeting point specs
|
||||
notification/ # 0 screens, 3 features — notification specs
|
||||
```
|
||||
|
||||
Each module can contain:
|
||||
- `screens/` — React screen components
|
||||
- `features/` — Gherkin `.feature` files (BDD specs)
|
||||
- `steps/{ui,data,e2e}/` — Cucumber step definitions by layer
|
||||
|
||||
## Import Rules
|
||||
|
||||
**Modules only import from `shared/` — never from each other.**
|
||||
|
||||
```
|
||||
src/modules/event/screens/EventDetailScreen.tsx
|
||||
✅ import from '../../../shared/components/sketchy'
|
||||
✅ import from '../../../shared/context/FestipodDataContext'
|
||||
✅ import from '../../../screens' (registry types)
|
||||
❌ import from '../../user/screens/...'
|
||||
```
|
||||
|
||||
## Shared Layer
|
||||
|
||||
`src/shared/` contains everything reusable across modules:
|
||||
|
||||
| Directory | Contents |
|
||||
|-----------|----------|
|
||||
| `components/sketchy/` | Hand-drawn UI library (Button, Card, Avatar, Header, NavBar, etc.) |
|
||||
| `components/ui/` | Shadcn/Radix components (used only in prototyping tool) |
|
||||
| `context/` | ThemeContext, NextGraphContext, FestipodDataContext |
|
||||
| `data/` | User stories (`index.ts`), auto-generated `features.ts`, `testResults.ts`, `seedData.ts`, `types.ts` |
|
||||
| `hooks/` | `useShapeWithDefaults` (NextGraph) |
|
||||
| `shapes/` | SHEX definitions + ORM TypeScript bindings |
|
||||
| `utils/` | `ngSession.ts`, `ngBootstrap.ts` |
|
||||
| `steps/ui/` | Shared BDD step definitions (navigation, screen, form) |
|
||||
| `support/` | Cucumber `world.ts`, `hooks.ts` |
|
||||
| `types/` | `gherkin.ts` (ParsedFeature, ParsedScenario types) |
|
||||
| `lib/` | `utils.ts` (cn helper for Tailwind) |
|
||||
|
||||
## App Shell
|
||||
|
||||
`src/app/` is the prototyping tool — not part of the Festipod app itself:
|
||||
|
||||
- `App.tsx` — Root: ThemeProvider > NextGraphProvider > FestipodDataProvider > RouterProvider
|
||||
- `router.tsx` — Hash-based routing: `#/` (gallery), `#/demo/{screenId}`, `#/specs/{featureId}`
|
||||
- `frontend.tsx` — React entry point (referenced from `src/index.html`)
|
||||
- `components/Gallery.tsx` — Screen preview grid
|
||||
- `components/DemoMode.tsx` — Interactive mockup viewer with sidebar navigation
|
||||
- `components/specs/` — BDD specs browser (SpecsPage, FeatureView, GherkinHighlighter)
|
||||
|
||||
## Screen Registry
|
||||
|
||||
`src/screens/index.ts` is the central registry that imports all screens from all modules and exports:
|
||||
- `screenGroups` — Grouped by domain (Accueil, Evenements, Utilisateur, General)
|
||||
- `screens` — Flat list
|
||||
- `getScreen(id)` — Lookup by ID
|
||||
- `ScreenProps` interface — `{ navigate: (screenId: string) => void }`
|
||||
|
||||
## Entry Points
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/index.ts` | Bun.serve() — HTTP server, serves index.html + cucumber report |
|
||||
| `src/index.html` | HTML entry, loads `src/app/frontend.tsx` |
|
||||
| `src/app/frontend.tsx` | React root, renders `<App />` |
|
||||
|
||||
## Build
|
||||
|
||||
- Dev: `bun --hot src/index.ts` (via `bun run dev`)
|
||||
- Prod: `bun run build.ts` — Bun bundler + Tailwind plugin → `dist/`
|
||||
- Path alias: `@/*` → `./src/*` (tsconfig)
|
||||
@@ -1,113 +0,0 @@
|
||||
# BDD Testing
|
||||
|
||||
Cucumber/Gherkin BDD specs in French with multi-layer step definitions.
|
||||
|
||||
## Overview
|
||||
|
||||
- 26 feature files (US-1 to US-26), all in French
|
||||
- Categories: EVENT, WORKSHOP, USER, MEETING, NOTIF
|
||||
- Priorities: 0 (Impossible), 1 (Haute), 2 (Moyenne), 3 (Basse)
|
||||
- Current results: 51 passed, 7 failed, 75 skipped (133 scenarios total)
|
||||
|
||||
## Multi-Layer BDD
|
||||
|
||||
Each module has step directories for three test layers:
|
||||
|
||||
```
|
||||
src/modules/event/steps/
|
||||
ui/ # UI/screen assertions (source analysis)
|
||||
data/ # Data layer assertions (Playwright + broker)
|
||||
e2e/ # E2E assertions (Playwright + broker + real app UI)
|
||||
```
|
||||
|
||||
Shared steps (cross-domain) live in `src/shared/steps/ui/`.
|
||||
|
||||
## Feature Files
|
||||
|
||||
Collocated with their module:
|
||||
|
||||
```
|
||||
src/modules/event/features/us-13-creer-evenement.feature
|
||||
src/modules/user/features/us-23-connexion-utilisateurs.feature
|
||||
src/modules/workshop/features/us-1-visualiser-atelier-termine.feature
|
||||
...
|
||||
```
|
||||
|
||||
Tagged with `@CATEGORY @priority-N` for filtering.
|
||||
|
||||
## Step Definitions
|
||||
|
||||
### Shared Steps (`src/shared/steps/ui/`)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `navigation.steps.ts` | Screen navigation, authentication, click/select actions, section/button/field assertions |
|
||||
| `form.steps.ts` | Form field validation, required fields, import/duplicate detection |
|
||||
| `screen.steps.ts` | Screen content assertions (participants, events, profiles, QR codes) |
|
||||
|
||||
### How UI Steps Work
|
||||
|
||||
`@ui` steps render the screen with `LocalDataProvider` (seed data) and `RouterProvider` via happy-dom, then assert on the rendered DOM. The render helper lives in `src/shared/test-harness/renderHelper.tsx` and is invoked from `world.ts:renderCurrentScreen()` on every `navigateTo(...)`.
|
||||
|
||||
See [test-layer-contracts](./test-layer-contracts.md) for what `@ui` is allowed to test and the patterns to follow (and avoid).
|
||||
|
||||
Legacy: `screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` in `world.ts` are vestiges of an earlier source-code-grep approach. `hasText`/`hasField`/`hasElement` now prefer the rendered DOM and fall back to source so unmigrated steps keep working during the transition.
|
||||
|
||||
### Screen Name Resolution
|
||||
|
||||
French names in `.feature` files map to screen IDs via `screenNameMap`:
|
||||
- `"accueil"` → `home`
|
||||
- `"détail événement"` → `event-detail`
|
||||
- `"mon profil"` → `profile`
|
||||
- `"relayer un événement"` → `create-event`
|
||||
|
||||
## Cucumber Configuration
|
||||
|
||||
`cucumber.json`:
|
||||
```json
|
||||
{
|
||||
"default": {
|
||||
"import": [
|
||||
"src/shared/support/**/*.ts",
|
||||
"src/shared/steps/**/*.ts",
|
||||
"src/modules/*/steps/**/*.ts"
|
||||
],
|
||||
"paths": ["src/modules/*/features/**/*.feature"],
|
||||
"language": "fr"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Requires `tsx` loader: `node --import tsx/esm node_modules/.bin/cucumber-js`
|
||||
|
||||
## Auto-Generated Files
|
||||
|
||||
Scripts in `scripts/` parse features and steps into TypeScript data files consumed by the prototyping tool:
|
||||
|
||||
| Script | Input | Output |
|
||||
|--------|-------|--------|
|
||||
| `parse-features.ts` | `src/modules/*/features/*.feature` | `src/shared/data/features.ts` |
|
||||
| `parse-test-results.ts` | `reports/cucumber-report.json` | `src/shared/data/testResults.ts` |
|
||||
| `extract-step-definitions.ts` | `src/shared/steps/ui/*.ts` | `src/shared/data/stepDefinitions.ts` |
|
||||
|
||||
Run all: `bun run test:cucumber`
|
||||
|
||||
## Data-Layer Testing
|
||||
|
||||
`@data` scenarios test through the real NextGraph broker. See [data-layer-testing](./data-layer-testing.md) for full architecture.
|
||||
|
||||
## E2E Testing
|
||||
|
||||
`@e2e` scenarios test the real app running in the broker iframe. See [data-layer-testing](./data-layer-testing.md#e2e-layer) for architecture. Key differences from `@data`:
|
||||
|
||||
- Uses the **real app** (not a test harness) served on a local HTTP port
|
||||
- Interacts via Playwright locators and `evaluate()` on the app iframe
|
||||
- Tests actual UI behavior: navigation, redirects, button clicks, screen content
|
||||
- Requires real broker mode (fails with `Error` if broker unavailable)
|
||||
|
||||
## Adding New Steps
|
||||
|
||||
1. **Module-specific**: Create in `src/modules/{module}/steps/ui/`
|
||||
2. **Cross-domain**: Add to `src/shared/steps/ui/`
|
||||
3. Import `FestipodWorld` type from `../../support/world` (shared) or adjust relative path
|
||||
4. Run `bun run steps:extract` to regenerate tooltip data
|
||||
@@ -1,177 +0,0 @@
|
||||
# Data-Layer Testing
|
||||
|
||||
BDD scenarios tagged `@data` test the real NextGraph data pipeline through a broker, not mocked data.
|
||||
|
||||
## Overview
|
||||
|
||||
`@data` scenarios run Cucumber steps against a real NextGraph broker. Playwright drives a Chromium instance that authenticates with the broker, which loads our test harness in an iframe. The harness uses real `useShape`/ORM subscriptions and exposes a `window.__testData` bridge for step definitions.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Cucumber steps → Playwright (Chromium, persistent profile)
|
||||
↓
|
||||
https://nextgraph.eu/auth/#/?o=http://127.0.0.1:{port}
|
||||
↓
|
||||
Broker wallet login (automated)
|
||||
↓
|
||||
Broker loads app in iframe → http://127.0.0.1:{port}
|
||||
↓
|
||||
harness-ng.tsx (init → useShape → ORM → broker)
|
||||
↓
|
||||
window.__testData bridge
|
||||
```
|
||||
|
||||
## Dual Mode
|
||||
|
||||
- **Real broker** (default): `harness-ng.tsx` with NextGraph ORM through broker iframe
|
||||
- **Mock fallback**: `harness.tsx` with standalone DeepSignalSets (if NG harness build fails)
|
||||
|
||||
## Wallet Lifecycle
|
||||
|
||||
Fully automated — no manual interaction required. CI-ready.
|
||||
|
||||
### First Run (wallet creation + bootstrap)
|
||||
1. `BeforeAll` detects no `.wallet-ready` marker in `.playwright-profile/`
|
||||
2. Launches headless Chromium with persistent profile
|
||||
3. Navigates to `https://nextgraph.eu/` → clicks "Create Wallet"
|
||||
4. Redirected to `account.nextgraph.eu` → clicks "I accept" (ToS)
|
||||
5. Redirected back → fills username/password form → submits
|
||||
6. Wallet created in localStorage
|
||||
7. **Logs in to the wallet** — this triggers the verifier bootstrap from the remote broker, populating localStorage with repo data
|
||||
8. Waits 10s for bootstrap to complete, then closes context
|
||||
9. Marker written
|
||||
|
||||
Step 7 is critical: the NextGraph verifier starts with an empty `repos` HashMap. On first login, `verifier.sync()` bootstraps from the remote broker, downloading repo data (including store repos). This data is saved to localStorage via `session_save`. Without this initial login, subsequent sessions would have empty repos and all writes would fail with `RepoNotFound`.
|
||||
|
||||
### Subsequent Runs (automated login)
|
||||
1. Marker found → skip wallet creation
|
||||
2. Headless Chromium with persistent profile
|
||||
3. Automated login: click "Login" → click wallet link → fill password → submit
|
||||
4. Broker authenticates, loads app harness in iframe
|
||||
5. Harness initializes NG, creates ORM subscriptions, seeds data if needed
|
||||
6. `window.__testData.ready` → steps execute via `appFrame.evaluate()`
|
||||
|
||||
### Wallet Credentials
|
||||
- Name: `festipod-tests`
|
||||
- Password: `festipod-tests`
|
||||
|
||||
## Key Technical Details
|
||||
|
||||
### Chromium Flags
|
||||
```
|
||||
--disable-features=PrivateNetworkAccessRespectPreflightResults,BlockInsecurePrivateNetworkRequests,...
|
||||
--allow-insecure-localhost
|
||||
--disable-web-security
|
||||
```
|
||||
Required because broker at `nextgraph.eu` (public) loads harness from `http://127.0.0.1:{port}` (local) in an iframe — Chromium's Private Network Access blocks this by default.
|
||||
|
||||
### Persistent Profile (`.playwright-profile/`)
|
||||
- Stores NG wallet in localStorage (`ng_wallets` on `nextgraph.eu`, `ng_bootstrap` on `nextgraph.net`)
|
||||
- Gitignored
|
||||
- Must use full Chrome binary, not `chrome-headless-shell`
|
||||
|
||||
### HTTP Server
|
||||
- Started in `BeforeAll` on auto-assigned port (`127.0.0.1:0`)
|
||||
- Serves harness HTML at `/` and JS bundle at `/harness.js` (separate files — inline script breaks due to special characters in bundle)
|
||||
- Shut down in `AfterAll`
|
||||
|
||||
### ORM Subscriptions
|
||||
Harness creates subscriptions for all three shapes with scope `did:ng:${session.private_store_id}` (opens the store repo for reads AND writes):
|
||||
- `FpEventShapeType` → events
|
||||
- `FpUserProfileShapeType` → users
|
||||
- `FpParticipationShapeType` → participations
|
||||
|
||||
### Test Bridge (`window.__testData`)
|
||||
Exposed by the harness, consumed by steps via `appFrame.evaluate()`:
|
||||
- `events`, `users`, `participations` — live DeepSignalSets
|
||||
- `currentUserId` — IRI of the test user
|
||||
- `getEvent(id)`, `getEventByTitle(title)` — lookups
|
||||
- `joinEvent(eventId, userId)`, `leaveEvent(eventId, userId)` — mutations
|
||||
- `isParticipating(eventId, userId)`, `getEventParticipants(eventId)` — queries
|
||||
- `updateEvent(eventId, updates)` — field updates
|
||||
|
||||
## E2E Layer (`@e2e`)
|
||||
|
||||
`@e2e` scenarios test the real app UI running inside the broker iframe. Unlike `@data` which loads a test harness, `@e2e` loads the actual app.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
Cucumber steps → Playwright (Chromium, persistent profile)
|
||||
↓
|
||||
https://nextgraph.net/redir/#/?o=http://127.0.0.1:{appPort}
|
||||
↓
|
||||
Broker wallet login (automated, same as @data)
|
||||
↓
|
||||
Broker loads REAL APP in iframe → http://127.0.0.1:{appPort}
|
||||
↓
|
||||
App renders with NextGraphProvider auto-connecting
|
||||
↓
|
||||
Steps interact via appFrame.evaluate() and Playwright locators
|
||||
```
|
||||
|
||||
### App Server
|
||||
|
||||
Started in `BeforeAll` alongside the harness server:
|
||||
1. Find a free port
|
||||
2. `spawn('bun', ['src/index.ts'], { env: { PORT: appPort } })`
|
||||
3. Poll until the server responds to HTTP GET
|
||||
4. Killed in `AfterAll`
|
||||
|
||||
### Shared Infrastructure
|
||||
|
||||
`@e2e` reuses the same `setupBrokerPage()` helper as `@data` — handles broker redirect URL construction, wallet login automation, and iframe discovery.
|
||||
|
||||
### Step Definitions
|
||||
|
||||
E2E steps live in module directories (e.g., `src/modules/auth/steps/e2e/connexion.steps.ts`). They use:
|
||||
- `this.appFrame!.evaluate()` — run JS in the app iframe (hash navigation, content checks)
|
||||
- `this.appFrame!.locator()` — find and interact with DOM elements
|
||||
- `this.appFrame!.waitForFunction()` — poll for expected state (screen content, URL changes)
|
||||
- `SCREEN_MARKERS` — map screen IDs to unique text content for verification
|
||||
|
||||
### Before Hook (`@e2e`)
|
||||
|
||||
```
|
||||
1. Open new Playwright page
|
||||
2. setupBrokerPage(page, realAppUrl) → automated login → find app iframe
|
||||
3. Wait for React render (root.innerHTML.length > 100)
|
||||
4. Wait 3s for NG connection + provider stabilization
|
||||
```
|
||||
|
||||
### Differences from `@data`
|
||||
|
||||
| Aspect | `@data` | `@e2e` |
|
||||
|--------|---------|--------|
|
||||
| What loads in iframe | Test harness (`harness-ng.tsx`) | Real app (`src/index.ts`) |
|
||||
| Ready signal | `window.__testData.ready === true` | `root.innerHTML.length > 100` |
|
||||
| Interaction | `evaluate()` on test bridge | `evaluate()` + Playwright locators |
|
||||
| Mock fallback | Yes (standalone DeepSignalSets) | No — requires real broker |
|
||||
| Tests | Data operations (CRUD, queries) | UI behavior (navigation, redirects, clicks) |
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/shared/test-harness/harness-ng.tsx` | Real broker harness (useShape through broker iframe) |
|
||||
| `src/shared/test-harness/harness.tsx` | Mock harness (DeepSignalSets, no broker) |
|
||||
| `src/shared/support/hooks.ts` | Playwright lifecycle (wallet creation, login automation, iframe detection, app server) |
|
||||
| `src/shared/support/world.ts` | World with `page`/`appFrame` fields |
|
||||
| `src/modules/event/steps/data/inscription.steps.ts` | Inscription data steps |
|
||||
| `src/modules/auth/steps/e2e/connexion.steps.ts` | Auth/connection e2e steps |
|
||||
| `.playwright-profile/` | Persistent Chromium profile (gitignored) |
|
||||
| `scripts/debug-browser.ts` | Manual browser debug tool — launches headed Chromium to inspect broker interactions |
|
||||
| `.playwright-profile-debug/` | Chromium profile created by debug-browser.ts (gitignored) |
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
bun run test:data # Run @data scenarios (real broker if wallet exists, mock fallback)
|
||||
bun run test:cucumber # Run all scenarios (UI + data + e2e)
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [BDD Testing](./bdd-testing.md) — general Cucumber setup, UI-layer steps
|
||||
- [Data Layer](./data-layer.md) — NextGraph stack, shapes, context providers
|
||||
@@ -1,115 +0,0 @@
|
||||
# Data Layer
|
||||
|
||||
NextGraph-backed local-first data with fallback to local state for demo/disconnected mode.
|
||||
|
||||
## Overview
|
||||
|
||||
The app has two data modes:
|
||||
1. **Connected** — NextGraph ORM shapes (P2P, encrypted, local-first)
|
||||
2. **Disconnected/Demo** — Local React state seeded from `seedData.ts`
|
||||
|
||||
All screens use `useFestipodData()` hook regardless of mode.
|
||||
|
||||
## NextGraph Stack
|
||||
|
||||
```
|
||||
@ng-org/web # Browser WASM runtime
|
||||
@ng-org/orm # RDF shape-based ORM
|
||||
@ng-org/shex-orm # SHEX → TypeScript code generation
|
||||
@ng-org/alien-deepsignals # Reactive signals bridge
|
||||
```
|
||||
|
||||
Packages installed from npm (`@ng-org/*` alpha versions). For local development against an unreleased `nextgraph-rs` build, `scripts/build-ng-packages.sh` packs the monorepo into `.ng-tarballs/` and updates `package.json` to point at those paths.
|
||||
|
||||
## SHEX Shapes
|
||||
|
||||
`src/shared/shapes/shex/festipodShapes.shex` defines:
|
||||
- **Event** — title, description, dates, location, themes, participants
|
||||
- **UserProfile** — name, username, bio, city, visibility
|
||||
- **Participation** — links event + user, confirmation status
|
||||
|
||||
ORM bindings in `src/shared/shapes/orm/`:
|
||||
- `festipodShapes.schema.ts` — Schema registration
|
||||
- `festipodShapes.shapeTypes.ts` — Shape type constants
|
||||
- `festipodShapes.typings.ts` — TypeScript interfaces
|
||||
|
||||
Regenerate with `bun run build:orm`.
|
||||
|
||||
## NextGraph Read/Write Pattern
|
||||
|
||||
The app follows the same pattern as the official expense-tracker-rdf example:
|
||||
|
||||
- **Scope**: `useShape(shapeType, `did:ng:${session.private_store_id}`)` — opens the private store repo in the verifier
|
||||
- **@graph**: `did:ng:${session.private_store_id}` — writes target the same NURI
|
||||
|
||||
This is critical: `orm_start_graph` with the private store NURI explicitly opens the repo in the verifier's `self.repos` HashMap. Without this, `orm_frontend_update` fails with `RepoNotFound`.
|
||||
|
||||
**Do NOT use `did:ng:i` as scope** — it subscribes to the entire user site via a special code path that doesn't open individual repos, breaking all writes.
|
||||
|
||||
### Deleting Objects
|
||||
|
||||
`ngSet.delete(item)` updates the local reactive set but does **not** persist to the broker. Use `ng.sparql_update()` with SPARQL DELETE instead:
|
||||
|
||||
```typescript
|
||||
import { ng } from '@ng-org/web';
|
||||
import { sessionPromise } from '../utils/ngSession';
|
||||
|
||||
const session = await sessionPromise;
|
||||
await ng.sparql_update(
|
||||
session.session_id,
|
||||
`DELETE WHERE { GRAPH <${item["@graph"]}> { <${item["@id"]}> ?p ?o } }`,
|
||||
item["@graph"],
|
||||
);
|
||||
```
|
||||
|
||||
The broker sends back a `GraphOrmUpdate` with `op: "remove"` that reactively removes the item from the ORM set. **Do NOT combine with `ngSet.delete()`** — the two operations conflict in the CRDT.
|
||||
|
||||
See [decision record](../decisions/2026-03-17-1800-sparql-delete-for-orm-objects.md) for details.
|
||||
|
||||
### Key files
|
||||
|
||||
- `src/shared/hooks/useShapeWithDefaults.ts` — Accepts `storeNuri` param, passes to `useShape`
|
||||
- `src/shared/utils/ngGraph.ts` — `ensureGraphNuri()` returns `@graph` for entity creation
|
||||
- `src/shared/utils/ngBootstrap.ts` — Seeds test data using `ensureGraphNuri()` for `@graph`
|
||||
|
||||
See [decision record](../decisions/2026-03-17-1600-private-store-nuri-scope.md) for why.
|
||||
|
||||
## Context Providers
|
||||
|
||||
### NextGraphContext (`src/shared/context/NextGraphContext.tsx`)
|
||||
- Connection lifecycle: `disconnected` → `connecting` → `connected` | `error`
|
||||
- Provides session with store IDs (private, protected, public)
|
||||
- **Conditional auto-init**: Only auto-calls `initNg()` when running inside the broker iframe (`window.self !== window.top`). Outside the iframe, `initNgWeb()` would redirect the page to the broker — so connection waits for explicit `connect()` call.
|
||||
- `connect()`: Called by user clicking "Se connecter". When outside broker, triggers the redirect flow.
|
||||
|
||||
#### `@ng-org/web` redirect behavior
|
||||
`initNgWeb()` checks `window.self === window.top`. If the app is NOT in an iframe, it redirects to `nextgraph.net/redir/` with the current URL encoded as a return parameter. The broker then loads the app back in an iframe after auth. This means the app must NOT auto-init NG when loaded standalone.
|
||||
|
||||
### FestipodDataContext (`src/shared/context/FestipodDataContext.tsx`)
|
||||
- Wraps NextGraph shapes with `useShapeWithDefaults()` hook
|
||||
- CRUD: `createEvent()`, `updateEvent()`, `joinEvent()`, `leaveEvent()`, etc.
|
||||
- Exposes `useFestipodData()` hook consumed by all screens
|
||||
- `selectedEventId` state for cross-screen event navigation
|
||||
- `loadTestData()`: Calls `bootstrapWallet()` to seed test data into NG wallet — only triggered by explicit user action
|
||||
- **Provider states based on NG status**:
|
||||
- `disconnected` → `LocalDataProvider` with seed data (demo mode)
|
||||
- `connecting` → `LocalDataProvider` with **empty data** (avoids flashing seed data before wallet loads)
|
||||
- `connected` → `NgDataProvider` with real wallet data
|
||||
- `error` → `LocalDataProvider` with seed data (graceful fallback)
|
||||
|
||||
## Data Types
|
||||
|
||||
`src/shared/data/types.ts`:
|
||||
- `FpEventData` — id, title, date, location, distance, themes, etc.
|
||||
- `FpUserData` — id, name, username, bio, city, counts
|
||||
- `FpParticipationData` — eventId + userId + confirmed
|
||||
- `FpMeetingPointData` — eventId, location, time, host (local-only)
|
||||
- `FpFriendshipData` — userId + friendId (local-only)
|
||||
|
||||
## Seed Data
|
||||
|
||||
`src/shared/data/seedData.ts`:
|
||||
- 10 users (Marie Dupont = current user, `user-1`)
|
||||
- Multiple events with dates, locations, themes
|
||||
- Participations, meeting points, friendships
|
||||
- `CURRENT_USER_ID = 'user-1'`
|
||||
@@ -1,75 +0,0 @@
|
||||
# Modèle d'intégration et de déploiement NextGraph
|
||||
|
||||
Comment une app web tierce s'intègre à NextGraph, et où tourne le moteur (verifier).
|
||||
|
||||
## Overview
|
||||
|
||||
NextGraph s'utilise depuis une app web via un **proxy iframe** (`@ng-org/web`) : l'app tierce ne contient pas le moteur, elle délègue à un ng-app hébergé (par défaut `nextgraph.net`) qui exécute le moteur dans une iframe. Comprendre ce découpage est nécessaire pour savoir ce qu'on peut modifier sans auto-héberger. Vérifié dans `nextgraph-rs` le 2026-05-21 (voir [chemin du repo local](./nextgraph-stores-permissions.md#code-source-local)).
|
||||
|
||||
## Les paquets JS
|
||||
|
||||
- **`@ng-org/web`** — paquet **publié**. Proxy postMessage léger (aucun wasm embarqué). C'est **le** chemin d'intégration d'une app web tierce. `@ng-org/orm` et tous les exemples officiels (expense-tracker…) en dépendent. **Festipod l'utilise.**
|
||||
- **`@ng-org/api-web`** — paquet **privé** (`"private": true`, non publié). Moteur navigateur complet : charge `@ng-org/lib-wasm` dans un Web Worker (`?worker&inline`), utilise `sessionStorage`/`Worker`. Consommé uniquement par `app/nextgraph` (le frontend ng-app) et `engine/broker/auth`. C'est le moteur **interne** de l'app NextGraph, **pas** une cible d'intégration tierce.
|
||||
- **`@ng-org/lib-wasm`** — le moteur compilé en wasm (contient le verifier via la dépendance `nextgraph` / `local_broker`). Source : `sdk/js/lib-wasm/`.
|
||||
- **`nextgraph`** (npm) — l'API **NodeJS** (build `pkg-node` de lib-wasm).
|
||||
- **`@ng-org/orm`** — l'ORM réactif (`useShape`…), bâti sur `@ng-org/web`.
|
||||
|
||||
## Où tourne le verifier
|
||||
|
||||
Dans le modèle web standard (iframe), le verifier tourne **dans l'iframe** : `app/nextgraph` charge `api-web` → `lib-wasm` dans un Web Worker, côté navigateur. Le broker (`ngd`) ne fait que **le transport et le stockage**.
|
||||
|
||||
**Conséquence** : modifier la logique du verifier (ex. `request_processor`, `inbox_processor`) = reconstruire le **ng-app**, pas le broker.
|
||||
|
||||
## Le modèle iframe (intégration tierce)
|
||||
|
||||
- `@ng-org/web` redirige vers le ng-app hébergé, qui recharge l'app tierce dans une iframe après authentification, puis relaie les appels par `postMessage`.
|
||||
- **Reciblable au build** via variables d'env (fichier `sdk/js/web/src/index.ts`) :
|
||||
|
||||
| Variable | Cible |
|
||||
|---|---|
|
||||
| `NG_REDIR_SERVER` | défaut `nextgraph.net` |
|
||||
| `NG_DEV3` | `127.0.0.1:3033` |
|
||||
| `NG_DEV` | `localhost:14402` (redir) / `14404` (origin) |
|
||||
| `NG_DEV_LOCAL_BROKER` | `localhost:1421` |
|
||||
|
||||
Une app tierce peut donc pointer `@ng-org/web` vers un ng-app **auto-hébergé** sans changer son code, juste en rebuildant avec ces variables.
|
||||
|
||||
## Build pipeline lib-wasm
|
||||
|
||||
Scripts cargo dans `sdk/js/lib-wasm/Cargo.toml` (`[package.metadata.scripts]`) :
|
||||
|
||||
- `web` / `webdev` — `wasm-pack build --target web`
|
||||
- `node` / `nodedev` — `wasm-pack build -t nodejs`
|
||||
- `app` / `appdev` — `wasm-pack build --target bundler`
|
||||
|
||||
Post-traités par `prepare-web.js` / `prepare-node.js`.
|
||||
|
||||
## Plomberie proxy ↔ iframe ↔ worker (générique)
|
||||
|
||||
Le chemin d'appel d'une méthode du moteur est **entièrement générique** — aucune allowlist :
|
||||
|
||||
- `@ng-org/web` (proxy) : un `Proxy` JS qui relaie *n'importe quel* nom de méthode à l'iframe par `postMessage` (`apply` → `postMessage({method, args})`). Seules les méthodes *streamées* ont une entrée dans une table interne (positions d'arguments) ; les autres passent en simple requête/réponse.
|
||||
- `app/nextgraph` → `api-web/wasm-worker.js` : dispatch générique `Reflect.apply(ng[method], null, args)` (la table `mapping` est commentée/inutilisée).
|
||||
|
||||
**Conséquence** : une nouvelle fonction wasm en **requête/réponse simple** est *atteignable* de bout en bout via ce forwarding générique sans modifier le JS. Mais c'est un mécanisme de relais, **pas un substitut à une API typée** : l'appeler ainsi est un appel string non typé/non documenté (hack de test). Pour une intégration propre, on ajoute la méthode à la surface d'API du paquet (`@ng-org/web`) et à ses `.d.ts`, et éventuellement à `@ng-org/orm` (qui, lui, n'est **pas** un forwarder générique).
|
||||
|
||||
Cas **streamé** : une méthode en flux exige une entrée dans la table de streaming **des deux côtés** — `E` dans `@ng-org/web` (`ngweb.js`) **et** `streamed_api` dans `api-web/main.ts`. (Méthodes streamées actuelles : `doc_subscribe`, `orm_start_graph`, `orm_start_discrete`, `file_get`, `app_request_stream`.)
|
||||
|
||||
## Ciblage du broker : build-time uniquement
|
||||
|
||||
La cible (broker/ng-app) est figée **au build** de `@ng-org/web` via `import.meta.env` (`sdk/js/web/src/index.ts`) — **pas d'override runtime**, et `init()` ne prend pas d'URL de broker. Pour pointer une app vers un ng-app auto-hébergé, il faut donc **rebuilder `@ng-org/web`** avec `NG_REDIR_SERVER`/`NG_DEV*` (paquet en TypeScript pur, sans wasm → build trivial).
|
||||
|
||||
## Le broker (ngd)
|
||||
|
||||
- Supporte déjà nativement l'inbox (`inbox_post`, `inbox_register`, `inbox_pop_for_user` dans `engine/net/src/server_broker.rs`). Un `ngd` standard routerait l'inbox — aucun patch broker nécessaire.
|
||||
- C'est un démon **WebSocket** (`async-tungstenite`), **stateful** : stockage RocksDB sous `--base-path`, identité de pair (PeerId) persistée. Le volume est critique (clés + données chiffrées des users).
|
||||
- CLI (`bin/ngd/src/cli.rs`) : `--local PORT`, et surtout `--domain DOMAIN:PORT,LOCAL_PORT` = mode « derrière reverse-proxy TLS-terminé qui envoie X-Forwarded-For » (adapté à Traefik/Coolify).
|
||||
- **Ne sert pas de fichiers statiques** : pas de `ServeDir`/HTTP statique dans le crate. Le **ng-app frontend est un déploiement statique séparé** (`pnpm webfilebuild`). En prod, un reverse-proxy sert le statique du ng-app et proxy le WebSocket vers ngd sur un même domaine.
|
||||
- Premier démarrage **interactif** : ngd émet un lien d'invitation pour créer le wallet admin (cf. DEV.md « first run »). Wrinkle pour un déploiement conteneurisé headless.
|
||||
- Les Dockerfiles officiels (`bin/ngd/docker/Dockerfile.{alpine,fedora,ubuntu}`) sont **incomplets/cassés** (chemins obsolètes, échec de link llvm/clang documenté en commentaire) — pas de build conteneur turnkey.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Stores NextGraph et droits d'accès](./nextgraph-stores-permissions.md) — stores, permissions, inbox au protocole, chemin du repo local
|
||||
- [Data Layer](./data-layer.md) — usage actuel côté Festipod (auto-init iframe conditionnel)
|
||||
- [Brief : forker NextGraph pour l'inbox](../briefs/fork-nextgraph-inbox.md) — consommateur de cette fiche
|
||||
@@ -1,108 +0,0 @@
|
||||
# Stores NextGraph et droits d'accès
|
||||
|
||||
Fiche de référence des 5 types de stores NextGraph et de leurs droits de lecture/écriture.
|
||||
|
||||
## Overview
|
||||
|
||||
Décrit les primitives de stockage et de permission de NextGraph (système externe, pas le code de Festipod). Sert de socle aux briefs [multi-store-refactor](../briefs/multi-store-refactor.md) et [authorization-matrix](../briefs/authorization-matrix.md), qui dérivent la structure de données cible de Festipod à partir de ces primitives.
|
||||
|
||||
Source : doc NextGraph officielle — [Documents & Stores](https://docs.nextgraph.org/en/documents/) et [Getting started](https://docs.nextgraph.org/en/getting-started/), vérifiée le 2026-05-21.
|
||||
|
||||
## Code source local
|
||||
|
||||
Le repo `nextgraph-rs` est cloné localement à **`../../nextgraph/nextgraph-rs`** (relatif à la racine du projet, soit `/home/sylvain/projects/nextgraph/nextgraph-rs`). À consulter pour vérifier ce qui est réellement exposé au protocole/SDK plutôt que de se fier à la doc. Points d'entrée utiles :
|
||||
|
||||
- `sdk/js/lib-wasm/src/lib.rs` — l'API wasm effectivement exposée au JS (`@ng-org/web` n'est qu'un proxy postMessage vers ces fonctions).
|
||||
- `engine/net/src/app_protocol.rs` — l'enum `AppRequestCommandV0` (commandes de l'app protocol) et `NuriV0` (formats de NURI).
|
||||
- `engine/verifier/src/request_processor.rs` — le dispatch effectif des commandes `app_request` (la vérité sur ce qui est *traité*, pas seulement déclaré).
|
||||
- `engine/net/src/types.rs` — types inbox (`InboxPost`, `InboxMsg`, `InboxMsgContent`).
|
||||
- `engine/verifier/src/inbox_processor.rs` — traitement des messages d'inbox.
|
||||
|
||||
## Les 5 types de stores
|
||||
|
||||
| Store | Lecture | Écriture | Création |
|
||||
|---|---|---|---|
|
||||
| **Private** | Titulaire seul | Titulaire seul | Par défaut |
|
||||
| **Protected** | Titulaire + utilisateurs disposant d'un lien + permission (capability) | Titulaire + collaborateurs permissionnés | Par défaut |
|
||||
| **Public** | Tout le monde, sans capability | Titulaire seul | Par défaut |
|
||||
| **Group** | Membres du groupe | Membres du groupe (collaboratif) | À la demande |
|
||||
| **Dialog** | Les deux utilisateurs uniquement | Les deux utilisateurs uniquement | À la demande |
|
||||
|
||||
### Citations doc (verbatim)
|
||||
|
||||
- **Private** — *« this is a place where you put only private and personal information that only you have access to »*, *« It is not possible to share the documents of your private store with anybody else »*.
|
||||
- **Protected** — *« a space where you can share data, documents, and media with other users, but they will need a special link and permission in order to access them »* ; fait office de *« protected social profile »*.
|
||||
- **Public** — *« equivalent to your website, blog, or public profile on social networks … that you want everybody to have access to, without the need for special permissions »*.
|
||||
- **Group** — *« each Group is a separate Store … you can configure the store so that all the documents included in this store, inherit the permissions of the store »*.
|
||||
- **Dialog** — *« hold all the data you exchange with another user (and only with that other user) … You cannot add more users to this store »*.
|
||||
|
||||
### Stores par défaut vs à la demande
|
||||
|
||||
Tout wallet utilisateur dispose d'office des **3 stores** private / protected / public. Ils sont exposés dans la session du SDK sous `private_store_id`, `protected_store_id`, `public_store_id`. Les **Group** et **Dialog** stores se créent à la demande.
|
||||
|
||||
## Concepts transverses
|
||||
|
||||
### Document vs Repo
|
||||
|
||||
- *« A Repo is basically the equivalent of an E2EE group for one and only one Document. »*
|
||||
- **1 document = 1 repo.** Le repo détient les commits (changements) **et** les permissions du document.
|
||||
- Identifiant du repo : `did:ng:o:<RepoID>` (RepoID de 44 caractères).
|
||||
- Un **store** est lui-même un document spécial qui regroupe et permissionne d'autres documents.
|
||||
|
||||
### Granularité des permissions
|
||||
|
||||
- **Écriture** : gérée au niveau du **Document (repo)**, pas de la branche ni du bloc — *« Write permissions are managed at the level of the Document, not at the level of the branch or block »*.
|
||||
- **Lecture** : peut être plus fine, **par bloc ou par branche** — *« Read permissions can be by block or branch »*.
|
||||
- **Héritage** : un store (notamment Group) peut être configuré pour que tous les documents qu'il contient héritent des permissions du store.
|
||||
|
||||
### Capability / Nuri
|
||||
|
||||
- Le partage se fait en transmettant un **Nuri** qui embarque la capability cryptographique (lecture et/ou écriture). Pas d'ACL centralisée : la possession du Nuri = le droit.
|
||||
- *« adding permissions can be done offline »* — l'ajout de permission est asynchrone.
|
||||
- *« removing permissions is a synchronous operation that requires a SyncSignature »* — le retrait est synchrone et nécessite une SyncSignature.
|
||||
|
||||
### Inbox
|
||||
|
||||
- **Chaque document a une inbox native.** Un non-éditeur (sans capability d'écriture) peut y **déposer un lien (DID cap)** sans être invité comme éditeur.
|
||||
- Le propriétaire **modère** : accepter / rejeter / retirer.
|
||||
- Citation : *« each document has an inbox, which is used in this case to drop the link »*.
|
||||
- C'est le mécanisme retenu par Festipod pour la notification d'inscription à un point de rencontre (voir [authorization-matrix](../briefs/authorization-matrix.md)).
|
||||
|
||||
#### Modèle inbox au protocole (vérifié dans `nextgraph-rs`, 2026-05-21)
|
||||
|
||||
- NURI d'inbox : `did:ng:d:<inbox_id>`.
|
||||
- Contenu : enum `InboxMsgContent` avec les variantes `ContactDetails`, `DialogRequest`, **`Link`**, `Patch`, `ServiceRequest`, `ExtRequest`, `RemoteQuery`, `SocialQuery` (`Comment`, `Transaction`, `BackLink` encore en TODO).
|
||||
- Le message est **scellé** (`crypto_box::seal`) vers la pubkey de l'inbox destinataire → seul le titulaire de l'inbox déchiffre.
|
||||
- Le champ `from` est **optionnel** → l'expéditeur peut être **anonyme** (pas de signature, pas de `from_inbox`). C'est exactement le « identifié si connu, anonyme sinon » voulu par Festipod, **natif au protocole**.
|
||||
|
||||
#### Exposition côté SDK JS : l'inbox n'est PAS utilisable directement
|
||||
|
||||
Investigation dans `lib-wasm` + `request_processor.rs` :
|
||||
|
||||
- `app_request(request)` est exposé au JS, et l'enum `AppRequestCommandV0::InboxPost` + le constructeur `AppRequest::inbox_post()` existent.
|
||||
- **MAIS** le `request_processor` du verifier (qui traite les `app_request`) **n'a aucun bras `InboxPost`**. Commandes réellement traitées : `OrmStart`, `OrmStartDiscrete`, `Fetch`, `FileGet`, `OrmUpdate`, `OrmDiscreteUpdate`, `SocialQueryStart`, `QrCodeProfile`, `QrCodeProfileImport`, `Header`, `Create`, `FilePut`. Envoyer un `InboxPost` via `app_request` ne déclenche donc rien.
|
||||
- En plus, construire un `InboxPost` exige le scellement crypto côté Rust ; **aucun helper wasm** n'expose cette construction.
|
||||
- Le dépôt en inbox n'est déclenché qu'**en interne** par deux features, elles exposées au JS :
|
||||
- `QrCodeProfileImport` → `post_to_inbox(InboxPost::new_contact_details(...))` (échange de contact) ;
|
||||
- `social_query_start(...)` → propagation de requête sociale via les inbox des **contacts**.
|
||||
|
||||
**Conséquence** : pas de moyen propre, aujourd'hui, de faire un « drop a Link » arbitraire dans l'inbox d'un PdR depuis le SDK JS. Il faudrait, dans `nextgraph-rs`, soit exposer un helper `inbox_post_link(...)` dans `lib-wasm` **et** ajouter le bras `InboxPost` au `request_processor`, soit détourner `social_query`.
|
||||
|
||||
**Piste connexe — `social_query_start`** : EST exposé au JS. C'est une requête fédérée sur le graphe social (propagée via inbox jusqu'à `degree` sauts), pertinente pour « qui dans mon réseau participe à X » et pour la découverte. Limite : ne touche que les **contacts**, donc ne couvre pas la notification anonyme vers un hôte non-connecté.
|
||||
|
||||
## Limites du SDK JS
|
||||
|
||||
Le SDK `@ng-org/web` (vérifié en `0.1.2-alpha.13`, soit `upstream/main` au 2026-05-21 — la version installée dans Festipod) **n'expose pas** les primitives suivantes, pourtant présentes au niveau protocole :
|
||||
|
||||
- création de Group / Dialog store ;
|
||||
- partage de capability (transmission de Nuri avec droits) ;
|
||||
- manipulation de permissions (ajout / retrait) ;
|
||||
- dépôt et lecture d'inbox.
|
||||
|
||||
Méthodes JS effectivement disponibles : `doc_create`, `doc_subscribe`, `sparql_query`, `sparql_update`, `orm_start_graph`, `orm_start_discrete`, `graph_orm_update`, `discrete_orm_update`, `file_get`, `app_request_stream`. La doc annonce qu'*« An API will be provided for permission manipulation »* (sans date). Détail dans [multi-store-refactor §Contrainte SDK](../briefs/multi-store-refactor.md).
|
||||
|
||||
## See Also
|
||||
|
||||
- [Brief : refactor multi-store](../briefs/multi-store-refactor.md) — consommateur de cette fiche
|
||||
- [Brief : matrice d'autorisations](../briefs/authorization-matrix.md) — dérive la structure de stores Festipod
|
||||
- [Knowledge : data layer](./data-layer.md) — état actuel mono-store de l'app
|
||||
@@ -1,94 +0,0 @@
|
||||
# Screens
|
||||
|
||||
16 mobile mockup screens using the sketchy hand-drawn component library.
|
||||
|
||||
## Screen Inventory
|
||||
|
||||
### Home Module (`src/modules/home/screens/`)
|
||||
|
||||
| ID | Name | File | Description |
|
||||
|----|------|------|-------------|
|
||||
| `welcome` | Bienvenue | WelcomeScreen.tsx | Onboarding/welcome page |
|
||||
| `home` | Accueil | HomeScreen.tsx | Dashboard with upcoming events, quick actions |
|
||||
| `settings` | Parametres | SettingsScreen.tsx | Notifications, privacy, location settings |
|
||||
|
||||
### Event Module (`src/modules/event/screens/`)
|
||||
|
||||
| ID | Name | File | Description |
|
||||
|----|------|------|-------------|
|
||||
| `events` | Decouvrir | EventsScreen.tsx | Event discovery/search |
|
||||
| `event-detail` | Detail evenement | EventDetailScreen.tsx | Event info, participants, join/leave |
|
||||
| `create-event` | Relayer evenement | CreateEventScreen.tsx | Create/relay event, import from Mobilizon/Transiscope |
|
||||
| `update-event` | Modifier evenement | UpdateEventScreen.tsx | Edit existing event |
|
||||
| `invite` | Inviter des amis | InviteScreen.tsx | Invite contacts to event |
|
||||
| `participants-list` | Liste des participants | ParticipantsListScreen.tsx | Event participant list |
|
||||
| `meeting-points` | Points de rencontre | MeetingPointsScreen.tsx | Carpooling/meeting coordination |
|
||||
|
||||
### User Module (`src/modules/user/screens/`)
|
||||
|
||||
| ID | Name | File | Description |
|
||||
|----|------|------|-------------|
|
||||
| `profile` | Mon profil | ProfileScreen.tsx | Current user profile |
|
||||
| `update-profile` | Modifier mon profil | UpdateProfileScreen.tsx | Edit profile form |
|
||||
| `user-profile` | Profil d'un utilisateur | UserProfileScreen.tsx | View another user's profile |
|
||||
| `friends-list` | Mon reseau | FriendsListScreen.tsx | Network/friends list |
|
||||
| `share-profile` | Partager mon profil | ShareProfileScreen.tsx | QR code + link sharing |
|
||||
|
||||
### Auth Module (`src/modules/auth/screens/`)
|
||||
|
||||
| ID | Name | File | Description |
|
||||
|----|------|------|-------------|
|
||||
| `login` | Connexion | LoginScreen.tsx | Login (NextGraph + email fallback) |
|
||||
|
||||
## Screen Registry
|
||||
|
||||
`src/screens/index.ts` imports all screens and exports:
|
||||
|
||||
```typescript
|
||||
export interface ScreenProps {
|
||||
navigate: (screenId: string) => void;
|
||||
}
|
||||
|
||||
export const screenGroups: ScreenGroup[] // Grouped: home, events, user, general
|
||||
export const screens: Screen[] // Flat list
|
||||
export function getScreen(id: string): Screen | undefined
|
||||
```
|
||||
|
||||
## Sketchy Component Library
|
||||
|
||||
`src/shared/components/sketchy/` — hand-drawn UI with custom font:
|
||||
|
||||
| Component | Usage |
|
||||
|-----------|-------|
|
||||
| `Header` | Screen header with back button |
|
||||
| `NavBar` | Bottom tab navigation |
|
||||
| `Button` | Action buttons |
|
||||
| `Card` | Content cards |
|
||||
| `Input` | Text inputs |
|
||||
| `Title`, `Subtitle`, `Text` | Typography |
|
||||
| `Avatar` | User avatars with initials |
|
||||
| `Badge` | Status/category badges |
|
||||
| `Toggle`, `Checkbox` | Form controls |
|
||||
| `ListItem` | List row items |
|
||||
| `Divider` | Section separators |
|
||||
| `Placeholder` | Image/content placeholders |
|
||||
| `PhoneFrame` | Phone device frame wrapper |
|
||||
| `BrokerBanner` | NextGraph connection status banner |
|
||||
| `NgStatus` | Connection indicator dot |
|
||||
|
||||
## Screen Patterns
|
||||
|
||||
All screens follow the same pattern:
|
||||
|
||||
```typescript
|
||||
import { Header, Button, ... } from '../../../shared/components/sketchy';
|
||||
import { useFestipodData } from '../../../shared/context/FestipodDataContext';
|
||||
import type { ScreenProps } from '../../../screens';
|
||||
|
||||
export function MyScreen({ navigate }: ScreenProps) {
|
||||
const { events, currentUser, ... } = useFestipodData();
|
||||
// render with sketchy components
|
||||
}
|
||||
```
|
||||
|
||||
Navigation between screens uses `navigate(screenId)` — the prototyping tool intercepts this to switch the displayed screen.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Test Layer Contracts
|
||||
|
||||
Each BDD test layer (`@ui`, `@data`, `@e2e`) answers a distinct question. Mixing concerns produces brittle tests that fail on refactors without catching real regressions.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
/\ @e2e ~10 scénarios, parcours utilisateur critiques
|
||||
/ \
|
||||
/----\
|
||||
/ @data\ ~10 scénarios, mutations & persistance NG
|
||||
/--------\
|
||||
/ @ui \ ~60 scénarios, 1-5 par état d'écran × 15 écrans
|
||||
/____________\
|
||||
```
|
||||
|
||||
The pyramid reflects cost: `@ui` runs in-process (instant), `@data` boots a broker (~50s for the suite), `@e2e` boots broker + real app + navigates a real browser (~2min). Move every assertion to the lowest layer that can answer the question — UI rendering claims belong in `@ui`, not `@e2e`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **`@ui` — display layer.** Renders a screen with `LocalDataProvider` (seed data) + happy-dom and asserts on the resulting DOM. Verifies that *given known data, the screen shows the expected text and elements*. Does **not** test navigation outcomes, mutations, or data persistence.
|
||||
|
||||
- **`@data` — data layer.** Drives ORM mutations through the real NextGraph broker via a headless test harness. No app UI involved. Verifies that *operations on shapes are correctly persisted and observable in the wallet*. See [data-layer-testing](./data-layer-testing.md).
|
||||
|
||||
- **`@e2e` — integration layer.** Boots the real app inside the broker iframe with a Playwright-controlled Chromium. Verifies that *layers collaborate to deliver a user journey* (e.g. create → list → modify → reload → still there). Sparse: 1 scenario per critical path; never duplicate `@ui` content checks here.
|
||||
|
||||
## Implementation
|
||||
|
||||
### `@ui` — rendering helper
|
||||
|
||||
`src/shared/test-harness/renderHelper.tsx` installs happy-dom globals and renders any screen wrapped in `LocalDataProvider` + `RouterProvider`. Called from `world.ts:renderCurrentScreen()` on every `navigateTo(...)`. Seed data (`src/shared/data/seedData.ts`) provides predictable fixtures — `Marie Dupont`/`@mariedupont` is `currentUser`, `Jean Durand`/`@jeandurand` exists in `users`, 5 seed events, etc.
|
||||
|
||||
**Good `@ui` assertion patterns:**
|
||||
|
||||
```ts
|
||||
// Text visible to the user
|
||||
expect(this.getDomText()).to.include('Marie Dupont');
|
||||
|
||||
// Element presence by class/role
|
||||
expect(this.renderedDoc!.querySelector('.app-avatar')).to.not.be.null;
|
||||
|
||||
// Conditional rendering (filled state vs empty state)
|
||||
const cards = this.renderedDoc!.querySelectorAll('.app-card');
|
||||
expect(cards.length).to.be.greaterThan(0);
|
||||
|
||||
// Required form fields rendered with their label + asterisk
|
||||
const labels = Array.from(this.renderedDoc!.querySelectorAll('p'))
|
||||
.map(p => p.textContent ?? '');
|
||||
expect(labels.some(t => t.includes("Nom de l'événement *"))).to.be.true;
|
||||
```
|
||||
|
||||
**Anti-patterns to remove:**
|
||||
|
||||
```ts
|
||||
// ❌ Regex on source: couples test to code structure, fails on refactor
|
||||
expect(/<Title[^>]*>Marie Dupont<\/Title>/.test(source)).to.be.true;
|
||||
|
||||
// ❌ Testing implementation details
|
||||
expect(/showDuplicateWarning/.test(source)).to.be.true;
|
||||
expect(/importableEvents/.test(source)).to.be.true;
|
||||
|
||||
// ❌ Testing JSX structure rather than rendered output
|
||||
expect(/<Avatar[^>]*initials="MD"[^>]*size="lg"/.test(source)).to.be.true;
|
||||
```
|
||||
|
||||
### `@data` — broker-only
|
||||
|
||||
Already isolated correctly. See [data-layer-testing](./data-layer-testing.md). Don't touch the DOM here; use the test harness bridge (`window.__testData`).
|
||||
|
||||
### `@e2e` — full stack
|
||||
|
||||
Path-based routing: navigate via `window.history.pushState` + `popstate` dispatch (`src/modules/auth/steps/e2e/connexion.steps.ts`). Assert on actual DOM text after `appFrame.waitForFunction`. **Do not** re-verify here what `@ui` already covers — `@e2e` should fail when *collaboration* between layers breaks, not when an icon changes.
|
||||
|
||||
## Migration Consequences
|
||||
|
||||
The current `@ui` suite predates this contract. The migration plan:
|
||||
|
||||
1. **Rewrite source-grep assertions** → DOM queries via the helper. The `world.ts:hasText/hasField/hasElement` methods already prefer the rendered DOM and fall back to source — so unmigrated steps still work during the transition.
|
||||
2. **Delete tests on implementation details** (`/showDuplicateWarning/`, `/importableEvents/`, regex on JSX). They protect nothing the user sees.
|
||||
3. **Move behavioral assertions to `@e2e`** when not already covered ("clicking Suivant advances the wizard" — exercise it via Playwright if it's not redundant with existing journeys).
|
||||
4. **Drop redundant `@e2e` content checks** that duplicate `@ui` (e.g. "screen contains 'Découvrir'" — let `@ui` own that).
|
||||
|
||||
`world.ts:screenFileMap`, `screenFieldDetectors`, `screenExpectedContent`, `screenRequiredFields` are vestiges of the source-analysis era. Once the migration is complete, they can be removed in favor of seed-data assertions on the rendered DOM.
|
||||
|
||||
## See Also
|
||||
|
||||
- [BDD Testing setup](./bdd-testing.md) — Cucumber config, file layout, scripts
|
||||
- [Data Layer](./data-layer.md) — NextGraph shapes, seed data, contexts
|
||||
- [Data-Layer Testing](./data-layer-testing.md) — broker harness, wallet setup, Playwright
|
||||
- [Architecture](./architecture.md) — module structure
|
||||
@@ -1,78 +1,31 @@
|
||||
# Festipod
|
||||
|
||||
Mobile-first web app for discovering and sharing festival/event recommendations through trusted networks.
|
||||
Web app mobile-first où les utilisateurs créent des **points de rencontre** qui se *greffent* sur des **événements publics** existants, pour favoriser les rencontres. L'événement n'est qu'un prétexte/ancrage ; la valeur, c'est le point de rencontre — **on s'inscrit à un point de rencontre, pas à un événement**. Stack : Bun + React + NextGraph (P2P, local-first, chiffré).
|
||||
|
||||
## Architecture
|
||||
## Invariants à toujours garder
|
||||
|
||||
Feature-based: code organized by business domain, not technical layer. See [architecture](.project/knowledge/architecture.md).
|
||||
- **Architecture feature-based** : le code est organisé par domaine métier, pas par couche technique.
|
||||
```
|
||||
src/modules/{event,user,home,auth,workshop,meeting,notification}/
|
||||
src/shared/ # Composants, context, data — importable par tous les modules
|
||||
src/app/ # App shell (router, providers, entrée)
|
||||
src/screens/index.ts # Registre d'écrans (utilisé par Storybook)
|
||||
```
|
||||
- **Un module n'importe QUE depuis `shared/` — jamais d'un autre module.** C'est l'invariant qui rend l'archi réelle.
|
||||
- **Bun-first** : `bun` / `bun install` / `bun test` / `bun build`, jamais node/npm/vite/jest. `bun run dev` (port 3000).
|
||||
|
||||
```
|
||||
src/modules/{event,user,home,auth,workshop,meeting,notification}/
|
||||
src/shared/ # Components, context, data — importable by all modules
|
||||
src/app/ # App shell (router, providers, entry point)
|
||||
src/screens/index.ts # Screen registry (used by Storybook)
|
||||
```
|
||||
## Doctrine du projet — concepts (livrée automatiquement)
|
||||
|
||||
## Routing
|
||||
La connaissance détaillée vit dans `.project/concepts/` (système *concept*) : fiches courtes, typées, **livrées par un hook quand tu touches leur territoire** — tu n'as pas à les charger d'avance. Les 6 concepts :
|
||||
|
||||
Path-based routing with History API (custom router in `src/app/router.tsx`).
|
||||
| Concept | Couvre |
|
||||
|---|---|
|
||||
| `functional-domain` | Modèle produit : point de rencontre, acteurs, concepts métier, défi déduplication |
|
||||
| `app-architecture` | Modules, invariant d'imports, app shell, routing path-based, écrans |
|
||||
| `tech-stack` | Bun-first, APIs Bun, build pipeline, commandes |
|
||||
| `data-layer` | NextGraph actuel (mono-store), shapes, modes connected/demo, règles + pièges (suppression, champs perdus, internals) |
|
||||
| `bdd-testing` | Cucumber multi-couches FR, contrat `@ui`/`@data`/`@e2e`, harness broker, cookbook |
|
||||
| `app-security` | Posture de sécurité actuelle (mono-store, confiance broker), auth wallet, modèle d'autorisations cible |
|
||||
| `nextgraph-platform` | NextGraph système externe (stores, inbox, SDK) + briefs prospectifs (multi-store, fork, shim) |
|
||||
|
||||
| Path | Screen |
|
||||
|------|--------|
|
||||
| `/` | WelcomeScreen |
|
||||
| `/login` | LoginScreen |
|
||||
| `/home` | HomeScreen |
|
||||
| `/events` | EventsScreen |
|
||||
| `/events/new` | CreateEventScreen |
|
||||
| `/events/:id` | EventDetailScreen |
|
||||
| `/events/:id/edit` | UpdateEventScreen |
|
||||
| `/events/:id/invite` | InviteScreen |
|
||||
| `/events/:id/participants` | ParticipantsListScreen |
|
||||
| `/events/:id/meeting-points` | MeetingPointsScreen |
|
||||
| `/profile` | ProfileScreen |
|
||||
| `/profile/edit` | UpdateProfileScreen |
|
||||
| `/profile/friends` | FriendsListScreen |
|
||||
| `/profile/share` | ShareProfileScreen |
|
||||
| `/users/:id` | UserProfileScreen |
|
||||
| `/settings` | SettingsScreen |
|
||||
|
||||
Screens use `useNavigate()` and `useParams()` hooks from the router — no prop drilling.
|
||||
|
||||
## Data Layer
|
||||
|
||||
NextGraph (P2P/local-first) with SHEX shapes and ORM. See [data-layer](.project/knowledge/data-layer.md).
|
||||
|
||||
## BDD Testing
|
||||
|
||||
Multi-layer Cucumber/Gherkin in French. See [bdd-testing](.project/knowledge/bdd-testing.md) for the setup and [test-layer-contracts](.project/knowledge/test-layer-contracts.md) for what each layer is allowed to test.
|
||||
|
||||
`@ui` scenarios render screens in-process (happy-dom + seed data) and assert on the DOM. `@data` scenarios test data operations through the real NextGraph broker. `@e2e` scenarios test the real app UI in the broker iframe. See [data-layer-testing](.project/knowledge/data-layer-testing.md).
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
bun run dev # Dev server with HMR (port 3000)
|
||||
bun run build # Production build to dist/
|
||||
bun run storybook # Browse screens and components
|
||||
bun run test:cucumber # Run all BDD tests
|
||||
bun run features:parse # Regenerate features.ts from .feature files
|
||||
bun run steps:extract # Extract step definitions for tooltips
|
||||
bun run build:orm # Regenerate ORM from SHEX shapes
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Architecture](.project/knowledge/architecture.md) — module structure, import rules, app shell
|
||||
- [Data Layer](.project/knowledge/data-layer.md) — NextGraph, shapes, context, seed data
|
||||
- [BDD Testing](.project/knowledge/bdd-testing.md) — Cucumber setup, step layers, feature files
|
||||
- [Test Layer Contracts](.project/knowledge/test-layer-contracts.md) — what each of `@ui`/`@data`/`@e2e` is allowed to test
|
||||
- [Screens](.project/knowledge/screens.md) — screen inventory, registry, sketchy components
|
||||
- [Data-Layer Testing](.project/knowledge/data-layer-testing.md) — real broker testing, wallet setup, Playwright harness, e2e layer
|
||||
- [Stores NextGraph et droits d'accès](.project/knowledge/nextgraph-stores-permissions.md) — fiche de référence des 5 types de stores et de leurs permissions
|
||||
- [Modèle d'intégration NextGraph](.project/knowledge/nextgraph-integration-model.md) — paquets JS, modèle iframe, où tourne le verifier, reciblage du broker
|
||||
|
||||
## Briefs (work not yet started)
|
||||
|
||||
- [Multi-store refactor](.project/briefs/multi-store-refactor.md) — passer du mono-store actuel à une structure de Group stores par communauté/event/RDV (prérequis multi-user)
|
||||
- [Matrice d'autorisations et requêtes](.project/briefs/authorization-matrix.md) — analyse qui doit guider la structure de stores cible
|
||||
- [Forker NextGraph pour l'inbox](.project/briefs/fork-nextgraph-inbox.md) — patcher nextgraph-rs pour exposer l'inbox au SDK JS (notification d'inscription)
|
||||
Pour **documenter** un fait projet : `/concept document <sujet>` (ne pas écrire en libre dans `.project/`).
|
||||
|
||||
@@ -2,195 +2,8 @@
|
||||
|
||||
# Festipod Project
|
||||
|
||||
Mobile-first web app for discovering and sharing festival/event recommendations through trusted networks. Uses a sketchy hand-drawn UI style.
|
||||
Le cœur toujours-chargé (but produit, invariants, conventions Bun-first, carte des concepts) vit dans `@AGENTS.md` ci-dessus. Toute la doctrine détaillée est dans `.project/concepts/` et **livrée automatiquement par le hook concept** quand tu touches le territoire concerné — ne la recopie pas ici.
|
||||
|
||||
## Architecture
|
||||
|
||||
Feature-based architecture: code is organized by business domain (module), not by technical layer. A module can only import from `shared/` — never from another module.
|
||||
|
||||
Multi-layer BDD: each module has `steps/ui/`, `steps/data/`, `steps/e2e/` directories. Shared step definitions live in `src/shared/steps/`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
modules/ # Business domain modules
|
||||
event/ # Events (create, discover, detail, update, invite, participants, meeting points)
|
||||
screens/ # EventsScreen, EventDetailScreen, CreateEventScreen, etc.
|
||||
features/ # Gherkin .feature files for this domain
|
||||
steps/ # BDD step definitions
|
||||
ui/ # UI-layer steps
|
||||
data/ # Data-layer steps
|
||||
e2e/ # E2E steps
|
||||
user/ # User profiles, friends, sharing
|
||||
screens/ # ProfileScreen, FriendsListScreen, ShareProfileScreen, etc.
|
||||
features/
|
||||
steps/
|
||||
home/ # Home dashboard, settings
|
||||
screens/ # HomeScreen, SettingsScreen
|
||||
auth/ # Authentication, onboarding
|
||||
screens/ # LoginScreen, WelcomeScreen
|
||||
workshop/ # Workshop/atelier specs (no screens yet)
|
||||
features/
|
||||
steps/
|
||||
meeting/ # Meeting point specs
|
||||
features/
|
||||
steps/
|
||||
notification/ # Notification specs
|
||||
features/
|
||||
steps/
|
||||
shared/ # Shared code (importable by all modules)
|
||||
components/
|
||||
sketchy/ # Hand-drawn UI components (Button, Card, Avatar, etc.)
|
||||
ui/ # Shadcn/Radix components
|
||||
context/ # ThemeContext, NextGraphContext, FestipodDataContext
|
||||
data/ # User stories, features.ts (auto-generated), testResults.ts
|
||||
hooks/ # Custom hooks (useShapeWithDefaults)
|
||||
shapes/ # SHEX shapes + ORM bindings (NextGraph)
|
||||
utils/ # ngSession, ngBootstrap
|
||||
steps/ # Shared BDD step definitions (cross-domain)
|
||||
ui/ # navigation.steps.ts, form.steps.ts, screen.steps.ts
|
||||
data/
|
||||
support/ # Cucumber hooks.ts, world.ts
|
||||
types/ # TypeScript type definitions
|
||||
lib/ # Utility functions (cn, etc.)
|
||||
app/ # App shell
|
||||
App.tsx # Root component with providers + route switch
|
||||
router.tsx # Path-based routing (History API)
|
||||
frontend.tsx # React entry point
|
||||
screens/
|
||||
index.ts # Screen registry (used by Storybook)
|
||||
scripts/ # Build scripts for parsing features
|
||||
docs/ # Documentation
|
||||
.storybook/ # Storybook configuration
|
||||
```
|
||||
|
||||
## Key Commands
|
||||
|
||||
```bash
|
||||
bun run dev # Start dev server with HMR
|
||||
bun run storybook # Browse screens and components in Storybook
|
||||
bun run test:cucumber # Run Cucumber tests
|
||||
bun run features:parse # Regenerate features.ts from .feature files
|
||||
bun run steps:extract # Extract step definitions for tooltips
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
Path-based routing via `src/app/router.tsx`. Screens use `useNavigate()` and `useParams()` hooks. See AGENTS.md for the full route table.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Gherkin specs are in French (Etant donne, Quand, Alors)
|
||||
- UI labels are in French
|
||||
- User stories are prefixed US-1 to US-26
|
||||
- Screens use the sketchy component library, not Tailwind
|
||||
- Max app width: 768px (tablet portrait)
|
||||
|
||||
---
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
||||
- Specs Gherkin et libellés UI en **français** (`Etant donné`, `Quand`, `Alors`).
|
||||
- Conventions techniques (Bun, APIs, build) : concept `tech-stack`. Architecture et écrans : concept `app-architecture`.
|
||||
- Documenter un fait projet : `/concept document <sujet>`.
|
||||
|
||||
@@ -40,7 +40,7 @@ Implémentées dans le code (écrans visibles via le router) :
|
||||
- Liste d'amis (connexions)
|
||||
- Profil d'un autre utilisateur
|
||||
|
||||
Voir le tableau des routes dans [AGENTS.md](./AGENTS.md#routing) et l'inventaire des écrans dans [.project/knowledge/screens.md](./.project/knowledge/screens.md).
|
||||
Voir l'inventaire des routes et des écrans dans le concept [app-architecture](./.project/concepts/app-architecture/).
|
||||
|
||||
### Défis ouverts
|
||||
|
||||
@@ -56,7 +56,7 @@ Identifiées comme nécessaires (notamment pour la scalabilité et la découvert
|
||||
- **Abonnement à une communauté d'intérêt** pour découvrir ses événements (mécanisme de discovery distribué).
|
||||
- **Abonnement à un utilisateur** pour suivre les événements qu'il déclare (sans nécessairement être ami).
|
||||
- **Listes curated** — créer et partager des sélections d'événements éditorialisées.
|
||||
- **Multi-utilisateurs collaboratif** : aujourd'hui chaque utilisateur a ses données isolées dans son wallet. Le passage en mode collaboratif (un point de rencontre vu par plusieurs personnes) suppose un refactor de la couche données vers les Group stores NextGraph. Voir [brief multi-store-refactor](./.project/briefs/multi-store-refactor.md).
|
||||
- **Multi-utilisateurs collaboratif** : aujourd'hui chaque utilisateur a ses données isolées dans son wallet. Le passage en mode collaboratif (un point de rencontre vu par plusieurs personnes) suppose un refactor de la couche données. Voir le concept [nextgraph-platform](./.project/concepts/nextgraph-platform/) (briefs multi-store, matrice d'autorisations, wallet partagé, fork inbox).
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -78,7 +78,5 @@ bun run build:orm # Régénérer l'ORM depuis les SHEX shapes
|
||||
|
||||
## Documentation
|
||||
|
||||
- [AGENTS.md](./AGENTS.md) — architecture, routes, points d'entrée pour contribuer
|
||||
- [.project/knowledge/](./.project/knowledge/) — comment les choses fonctionnent (data layer, BDD, écrans…)
|
||||
- [.project/decisions/](./.project/decisions/) — choix techniques figés
|
||||
- [.project/briefs/](./.project/briefs/) — chantiers à venir, recherche préparatoire
|
||||
- [AGENTS.md](./AGENTS.md) — cœur : but produit, invariants, carte des concepts
|
||||
- [.project/concepts/](./.project/concepts/) — toute la doctrine projet (savoir, règles, décisions, briefs), typée et livrée par hook au moment pertinent. 6 concepts : `functional-domain`, `app-architecture`, `tech-stack`, `data-layer`, `bdd-testing`, `nextgraph-platform`.
|
||||
|
||||
Reference in New Issue
Block a user