docs(concept): solder la doc-debt des 6 concepts
Dette accumulée depuis le 13/07 (27 marqueurs). Au-delà du vidage, trois
corrections de doctrine réellement fausse — c'est ce que le reconcile devait
attraper :
- app-security : `sharedWallet.ts` capture le mot de passe à l'ÉVALUATION du
module. Tant qu'un repli existait, un global posé trop tard ne faisait que
dégrader ; depuis que le wallet partagé est l'unique mode, il rend la barrière
INUTILISABLE (écran d'erreur, aucun champ). Conséquence non anticipée de la
décision shared-wallet-only → nouveau caveat.
- bdd-testing : la doctrine rendait des tests faux-verts. `ctx.newPage()` sur le
profil persistant relit l'IndexedDB local et ne prouve JAMAIS la durabilité
broker ; seul un contexte partagé neuf tranche. Un agent suivant la doctrine
écrivait un test qui passe sans rien vérifier → nouveau caveat.
- app-architecture : `knowledge_routing` décrivait encore une route `/login`
disparue, et `knowledge_screen-pattern` citait `LoginScreen` qui n'existe
plus. Nouveau caveat sur les deux espaces d'id vus depuis un écran.
Aussi : data-layer/knowledge_context-internals décrit la jointure
participation→profil et corrige un mécanisme de changement d'identité périmé ;
tech-stack raccroche la table des scripts au vrai point d'entrée cucumber ;
functional-domain note qu'« implémenté » ≠ « durable ».
Trois marqueurs soldés comme sans objet : ils visaient
`reconnexion-socket-mort.{feature,steps.ts}`, absents de l'arbre ET de tout
l'historique — expérience abandonnée avant tout commit. Ce qu'elle devait
établir est capturé ailleurs (caveat de durabilité, post-mortem polyfill, fiche
INBOX socket-death).
Liens morts vers une décision disparue avec le concept `nextgraph-platform`
réparés. Reste au lint : le brief 07-06 (superseded) porte des file:line et des
références aux internes NextGraph — laissé intact, il décrit l'Option-B encore
implémentée et se dissoudra à la graduation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
# Doc-debt — app-architecture
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/shared/context/FestipodDataContext.tsx @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/auth/screens/AccessGateScreen.tsx @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/context/AccountContext.tsx @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/screens/EventDetailScreen.tsx @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -19,6 +19,7 @@ Comment le code de l'app est **structuré** et **assemblé**. Architecture *feat
|
||||
- [[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)
|
||||
- [[caveat_identity-ids-in-screens]] — `currentUserId` (principal) vs `currentUser.id` (NURI de profil) : deux espaces d'id non interchangeables
|
||||
- [[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,28 @@
|
||||
---
|
||||
type: caveat
|
||||
summary: Un écran manipule DEUX ids de l'utilisateur courant qui ne sont pas interchangeables — currentUserId (principal urn:festipod:user:…) pour les queries de participation/amitié, currentUser.id (NURI du profil) pour comparer aux profils rendus ; se tromper ne lève aucune erreur, ça rend une liste vide ou se compte soi-même comme un participant inconnu
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# Piège : deux ids de l'utilisateur courant dans un écran
|
||||
|
||||
`useFestipodData()` expose **deux** identifiants de l'utilisateur courant. Ils vivent dans des **espaces différents** et ne sont **jamais égaux en mode connecté** :
|
||||
|
||||
| Valeur | Espace | À quoi elle sert |
|
||||
|---|---|---|
|
||||
| `currentUserId` | **principal** stable dérivé de l'identifiant de connexion (`urn:festipod:user:<clé>`) | c'est ce que les **participations** et **amitiés** stockent |
|
||||
| `currentUser.id` | **NURI du document de profil** (`did:ng:…`) | c'est ce que portent les **profils** rendus |
|
||||
|
||||
En mode seed/demo les deux coïncident (`user-1`) — **le piège ne se manifeste qu'en connecté**, et jamais sous forme d'erreur : juste un résultat faux.
|
||||
|
||||
## La règle
|
||||
|
||||
- Les queries qui **filtrent des participations/amitiés** — `getUserEvents(userId)`, `isParticipating(eventId, userId?)`, `getFriends(userId?)` — attendent le **principal**. Leur valeur par défaut (`currentUserId`) est correcte ; **ne leur passe pas** un `user.id` de profil, sinon la liste revient **vide**.
|
||||
- `getEventParticipants(eventId)` rend des **profils**. Toute comparaison sur son résultat (typiquement « me retirer de la liste ») se fait donc sur **`currentUser?.id`**, jamais sur `currentUserId`.
|
||||
|
||||
## Ce que coûte l'erreur (observé)
|
||||
|
||||
- Comparer `participant.id !== currentUserId` pour se filtrer soi-même **ne retire rien** : on apparaît dans sa propre liste, et comme la ligne n'est plus reconnue elle s'affiche en « participant inconnu ».
|
||||
- Symétriquement, un écran qui affiche les événements d'**un autre utilisateur** à partir de son **id de profil** (`getUserEvents(viewedUser.id)`) rend une liste vide en connecté — même cause.
|
||||
|
||||
La jointure participation→profil elle-même n'est **pas** l'affaire de l'écran : elle est faite dans le provider (`resolveParticipantUser`), à travers l'identifiant normalisé. Mécanique complète et invariant d'écriture/lecture : concept `data-layer`, [[knowledge_context-internals]].
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
summary: src/app/ est le shell réel de l'app — App.tsx empile les providers (Theme > NextGraph > Account > FestipodData > Router), AuthGate garde tout écran routé derrière la barrière d'accès, et le shell bascule l'écran selon la route
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# App shell
|
||||
@@ -15,12 +16,28 @@ summary: src/app/ est le shell réel de l'app — App.tsx empile les providers (
|
||||
|
||||
```
|
||||
ThemeProvider
|
||||
└ NextGraphProvider (cycle de connexion NextGraph — concept data-layer)
|
||||
└ FestipodDataProvider (données, mode connected/demo — concept data-layer)
|
||||
└ RouterProvider (route courante + navigate)
|
||||
└ NextGraphProvider (cycle de connexion NextGraph — concept data-layer)
|
||||
└ AccountProvider (identité courante = l'identifiant — concept app-security)
|
||||
└ FestipodDataProvider (données, mode connected/demo — concept data-layer)
|
||||
└ RouterProvider (route courante + navigate)
|
||||
└ div.app-container
|
||||
├ AuthGate (barrière d'accès)
|
||||
│ └ AppContent (switch route.page → écran)
|
||||
└ ToastContainer
|
||||
```
|
||||
|
||||
Le composant racine lit `useRouter()` pour résoudre `route.page` → écran à rendre.
|
||||
`AppContent` lit `useRouter()` pour résoudre `route.page` → écran à rendre.
|
||||
|
||||
### Invariants d'ordre (ce qui casse si tu déplaces une couche)
|
||||
|
||||
- **`AccountProvider` est AU-DESSUS de `FestipodDataProvider`.** Le provider de données appelle `useAccount()` pour dériver son principal (`currentUserId`) *et* pour réinitialiser sa session au changement d'identité. Inverser l'ordre casse toute la résolution d'identité, silencieusement.
|
||||
- **`AuthGate` est À L'INTÉRIEUR du router** : il lit `useRouter()`/`useNavigate()` pour quitter la route d'accueil déconnectée une fois connecté **et** identifié. Le sortir du `RouterProvider` le casse.
|
||||
- **`AuthGate` enveloppe TOUT écran routé.** Tant que le wallet n'est pas ouvert **ou** que l'identifiant n'est pas résolu, `AccessGateScreen` est rendu **à la place** d'`AppContent`. Conséquence : **aucun écran ne peut supposer qu'il est atteignable sans identité** — sauf barrière désactivée (voir ci-dessous).
|
||||
- **`ToastContainer` est HORS d'`AuthGate`** (mais dans `.app-container`) : il est monté quel que soit l'état de la barrière.
|
||||
|
||||
### Désactivation de la barrière (deux consommateurs)
|
||||
|
||||
`AuthGate` est **ON par défaut** ; il ne s'efface que si `globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ === true`, posé soit par `build.ts` (depuis `ACCESS_GATE_DISABLED=1`, build sans barrière), soit par le harness de test via `addInitScript` pour les `@e2e` (qui exercent les écrans, pas le flux d'auth). **Impact** : le flux de barrière n'est donc **pas** couvert par les `@e2e` — ses gardes sont des tests `@ui` (concept `bdd-testing`).
|
||||
|
||||
## Points d'entrée
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Chaque module peut contenir :
|
||||
| Répertoire | Contenu |
|
||||
|---|---|
|
||||
| `components/` | Lib de composants UI (voir [[knowledge_screens]]) |
|
||||
| `context/` | `ThemeContext`, `NextGraphContext`, `FestipodDataContext` (voir concept `data-layer`) |
|
||||
| `context/` | `ThemeContext`, `NextGraphContext`, `AccountContext` (identité courante — concept `app-security`), `FestipodDataContext` (concept `data-layer`) ; leur **ordre d'empilement** est contraint, voir [[knowledge_app-shell]] |
|
||||
| `data/` | User stories, `features.ts` (auto-généré), `seedData.ts`, `types.ts` |
|
||||
| `hooks/` | `useShapeWithDefaults` (NextGraph) |
|
||||
| `shapes/` | SHEX + bindings ORM (voir concept `data-layer`) |
|
||||
|
||||
@@ -12,7 +12,6 @@ Routing **path-based** via l'History API — router maison dans `src/app/router.
|
||||
| Path | Écran |
|
||||
|---|---|
|
||||
| `/` | WelcomeScreen |
|
||||
| `/login` | LoginScreen |
|
||||
| `/home` | HomeScreen |
|
||||
| `/events` | EventsScreen |
|
||||
| `/events/new` | CreateEventScreen |
|
||||
@@ -29,7 +28,7 @@ Routing **path-based** via l'History API — router maison dans `src/app/router.
|
||||
| `/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é.
|
||||
> Cette table reflète `parsePath()` dans `router.tsx` — y revenir si elle évolue, c'est la source de vérité. Il n'y a **pas** de route d'authentification : la barrière d'accès n'est pas routée, elle est rendue *au-dessus* du switch de routes par `AuthGate` (voir [[knowledge_app-shell]]).
|
||||
|
||||
## Hooks
|
||||
|
||||
|
||||
@@ -34,7 +34,10 @@ export function MyScreen() { // fonction nommée, JAMAIS de props
|
||||
|
||||
## 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).
|
||||
- **Zéro prop** : l'écran ne reçoit rien ; tout vient du contexte/hooks (`useFestipodData`, `useNavigate`, `useParams`). Deux exceptions, de nature différente :
|
||||
- `WelcomeScreen` n'utilise pas `useFestipodData` (intro) — mais reste sans props. (`LoginScreen`/`ConnexionScreen` n'existent plus.)
|
||||
- **`AccessGateScreen` est la seule vraie exception au zéro-prop** : ce n'est **pas un écran routé**, il est rendu par `src/app/AuthGate.tsx` qui lui passe `status`/`error`/`initialIdentifier`/`onEnter`. Il est donc **hors registre et hors table de routes**, et n'a accès ni au router ni aux données. Voir [[knowledge_screens]] et [[knowledge_app-shell]].
|
||||
- **Identité : deux espaces d'id.** `currentUserId` (principal) et `currentUser.id` (NURI de profil) ne sont **pas** interchangeables selon la query — voir [[caveat_identity-ids-in-screens]] avant de comparer un id dans un écran.
|
||||
- **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.
|
||||
|
||||
@@ -30,7 +30,9 @@ Utilisé notamment par Storybook (voir concept `tech-stack`) pour parcourir les
|
||||
- **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/** : `AccessGateScreen` — la **barrière d'accès** (login NextGraph + saisie de l'identifiant), rendue par `src/app/AuthGate.tsx`, **hors registre/routing** (ce n'est pas un écran routé). Les anciens `LoginScreen` puis `ConnexionScreen` ont été retirés (cf. concept `app-security`, [[knowledge_authentication]]).
|
||||
- **auth/** : `WelcomeScreen` (intro, routé `/`) et `AccessGateScreen` — la **barrière d'accès** (login NextGraph + saisie de l'identifiant), rendue par `src/app/AuthGate.tsx`, **hors registre/routing** (ce n'est pas un écran routé) et **pilotée par props** (`status`/`error`/`initialIdentifier`/`onEnter`), seule exception au zéro-prop ([[knowledge_screen-pattern]]). Les anciens `LoginScreen` puis `ConnexionScreen` ont été retirés (cf. concept `app-security`, [[knowledge_authentication]]).
|
||||
|
||||
Structurellement, cet écran ne rend **pas** un layout d'écran standard mais un **choix entre trois branches d'accès** mutuellement exclusives, pilotées par `status` + la présence d'un wallet partagé. **Impact** : un nouveau cas d'accès s'ajoute comme une branche ici, **pas** comme une route. Le contenu et l'ordre des branches sont doctrine `app-security` ([[knowledge_authentication]]) — ne pas les redéfinir depuis ici.
|
||||
|
||||
> Le mapping path → écran est dans [[knowledge_routing]]. La plupart des écrans consomment `useFestipodData()` (concept `data-layer`) ; exceptions : `WelcomeScreen` et la barrière `AccessGateScreen`.
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Doc-debt — app-security
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/modules/auth/screens/AccessGateScreen.tsx @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/auth/steps/ui/barriere-acces.steps.ts @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/auth/steps/data/connexion.steps.ts @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -16,6 +16,7 @@ Le modèle de **sécurité, confidentialité et autorisations** de Festipod.
|
||||
## Pièges (lire AVANT de concevoir quoi que ce soit d'« anonyme »)
|
||||
|
||||
- [[caveat_stable-overlay-pseudonym]] — une référence cap-less expose un **pseudonyme permanent** de la personne ; un seul recoupement dé-anonymise **rétroactivement** tout son historique, et aucune rotation n'est connue
|
||||
- [[caveat_shared-wallet-global-before-gate-import]] — le wallet partagé étant l'unique mode, un global de mot de passe posé **après** l'import de la barrière la rend inutilisable (écran d'erreur de config, aucun champ)
|
||||
|
||||
## Liens
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
type: caveat
|
||||
summary: Le mot de passe du wallet partagé est capturé à l'ÉVALUATION de src/modules/auth/sharedWallet.ts ; depuis que le wallet partagé est l'unique mode, une valeur absente à cet instant ne donne plus un formulaire dégradé mais un écran d'erreur de config SANS champ identifiant — tout point d'entrée qui rend AccessGateScreen doit poser le global AVANT le premier import du module
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# Piège : poser le global du wallet partagé AVANT d'importer la barrière
|
||||
|
||||
**L'invariant.** `src/modules/auth/sharedWallet.ts` lit `globalThis.__FESTIPOD_SHARED_WALLET_PASSWORD__` **une seule fois, à l'évaluation du module** (la constante `SHARED_WALLET_PASSWORD`, exposée par `hasSharedWallet()`). Une valeur posée *après* ce premier import n'est jamais relue.
|
||||
|
||||
**Pourquoi c'est devenu bloquant.** Tant que « pas de wallet partagé » était un mode replié, un global manquant dégradait vers un formulaire encore utilisable — l'ordre d'évaluation était cosmétique. Depuis [[decision_2026-07-20_shared-wallet-only-mode]], `hasSharedWallet() === false` est une **erreur de configuration** : `AccessGateScreen` rend un bloc d'erreur **sans champ identifiant**. La barrière devient une impasse, pas un login dégradé.
|
||||
|
||||
## Impact — si je touche X, Y casse
|
||||
|
||||
- **Import statique = piège.** Un `import` statique de `AccessGateScreen` (ou de n'importe quel module qui remonte à `sharedWallet.ts`) depuis un point d'entrée qui pose lui-même le global est **hoisté au-dessus de l'affectation** → mot de passe vide → écran d'erreur, sans erreur JS pour le signaler. Le remède est l'**import dynamique** (`await import(...)`) exécuté après avoir posé le global.
|
||||
- **Points d'entrée concernés aujourd'hui** : le frontend servi depuis `src/` (`src/app/frontend.tsx` récupère `/festipod-config.json`, pose le global, puis importe l'app dynamiquement — mécanique détaillée dans tech-stack → [[knowledge_build-pipeline]]) et le harness `@ui` qui rend la barrière (`src/modules/auth/steps/ui/barriere-acces.steps.ts`, même séquence pose-puis-lazy-import). Un bundle produit par `build.ts` n'est **pas** concerné : la valeur y est inline par `define`.
|
||||
- **Exploitation** : un serveur sans `FESTIPOD_SHARED_WALLET_PASSWORD` ne sert **aucune** barrière fonctionnelle — par conception (échec franc). À traiter comme une panne de configuration, pas comme un bug d'écran.
|
||||
|
||||
**Vérifié (2026-07-27)** : capture à l'évaluation dans `sharedWallet.ts`, et garde `!hasSharedWallet()` en première branche de `AccessGateScreen`.
|
||||
|
||||
> Réserve : l'en-tête de `sharedWallet.ts` décrit encore l'ancien repli (« the gate falls back to the plain flow ») — commentaire périmé, c'est le rendu de `AccessGateScreen` qui fait foi.
|
||||
@@ -7,7 +7,7 @@ summary: L'identifiant de l'espace virtuel se saisit à la barrière d'accès (A
|
||||
|
||||
## Contexte
|
||||
|
||||
Le flux stopgap de [[decision_2026-06-15_shared-wallet-login-flow]] enchaînait **deux
|
||||
Le flux stopgap antérieur (décision du 2026-06-15, fiche disparue avec le concept `nextgraph-platform` — voir `git log`) enchaînait **deux
|
||||
écrans** : (1) `AccessGateScreen`, la barrière d'accès (vrai login NextGraph, ouverture du
|
||||
wallet partagé) ; (2) `ConnexionScreen`, un « login perçu » où l'utilisateur choisissait un
|
||||
**nom d'utilisateur**. Cette identité applicative était en réalité la clé du **wallet virtuel**
|
||||
@@ -40,6 +40,6 @@ n'est pas posé, puis l'app directement — sans écran intermédiaire.
|
||||
|
||||
## Portée
|
||||
|
||||
Supersede la partie « écran 2 / login perçu » de [[decision_2026-06-15_shared-wallet-login-flow]]
|
||||
Supersede la partie « écran 2 / login perçu » du flux stopgap du 2026-06-15
|
||||
(l'ouverture du wallet partagé via broker reste inchangée). État courant du flux :
|
||||
[[knowledge_authentication]].
|
||||
|
||||
@@ -9,7 +9,9 @@ summary: L'identité d'un utilisateur = son wallet NextGraph ; tous les utilisat
|
||||
|
||||
## Flux
|
||||
|
||||
- La **barrière d'accès** (`AccessGateScreen`, rendue par `src/app/AuthGate.tsx`) est le vrai login NextGraph : elle ouvre le wallet partagé via la redirection broker. **Dans le même acte**, l'utilisateur saisit un **identifiant** qui nomme son espace virtuel (`onEnter`). Il n'y a **plus d'écran « login perçu » séparé** (l'ancien `ConnexionScreen` « choisissez un nom d'utilisateur » a été retiré — cf. [[decision_2026-07-06_identifier-at-access-barrier]] ; supersede le flux à deux écrans de [[decision_2026-06-15_shared-wallet-login-flow]]).
|
||||
- La **barrière d'accès** (`AccessGateScreen`, rendue par `src/app/AuthGate.tsx`) est le vrai login NextGraph : elle ouvre le wallet partagé via la redirection broker. **Dans le même acte**, l'utilisateur saisit un **identifiant** qui nomme son espace virtuel (`onEnter`). Il n'y a **plus d'écran « login perçu » séparé** (l'ancien `ConnexionScreen` « choisissez un nom d'utilisateur » a été retiré — cf. [[decision_2026-07-06_identifier-at-access-barrier]] ; supersede le flux à deux écrans du stopgap du 2026-06-15).
|
||||
- **Le wallet partagé est le SEUL mode supporté** : `AccessGateScreen` a **trois branches** — (1) *erreur de configuration* si aucun wallet partagé n'est configuré (plus de formulaire nu en impasse), (2) flux d'**import assisté** tant que la session n'est pas connectée, (3) **champ identifiant seul** une fois connecté. Voir [[decision_2026-07-20_shared-wallet-only-mode]], et le piège d'ordre d'évaluation [[caveat_shared-wallet-global-before-gate-import]] (le global du mot de passe doit être posé avant le premier import de l'écran, sinon on tombe sur la branche 1).
|
||||
- **Vocabulaire du code** : l'identité du wallet s'appelle `identifier` partout (`registration.ts`, `ngSession`, hooks et steps de test) — **jamais** `username`, qui désigne exclusivement le handle de profil `UserProfile.username`. Ne pas ré-étiqueter l'un en l'autre : ce sont deux espaces d'identité distincts.
|
||||
- Cet **identifiant est un id technique** (un pseudo en pratique, **pas** un username Festipod) : il est **normalisé** (trim, `@` retiré, **minuscules**) puis persisté (`AccountContext` → `IdentityStore`), donc un rechargement — ou un autre appareil rouvrant le même wallet partagé — retombe sur le même espace. C'est cet id qui est donné au SDK (`setCurrentUser`) et sur lequel les caps et le compte shim sont clés.
|
||||
- **Porté cross-frontière par un PARAM D'URL `?id=`** (source de vérité), PAS par localStorage. L'app tourne dans deux contextes — **top-level** (`127.0.0.1:3000` direct, `window.self === window.top`, où s'affiche la barrière) et **iframe** (embarquée sous `nextgraph.net` après le round-trip broker, `window.self !== window.top`). Le navigateur **partitionne le storage par site top-level** : le localStorage du top-level et celui de l'iframe sont **deux partitions distinctes** → localStorage NE PEUT PAS porter l'identité d'un contexte à l'autre (symptôme observé : deux valeurs divergentes selon le contexte). Le SDK redirige via `location.href = broker + encodeURIComponent(window.location.href)` (embarque l'URL app complète, query comprise, dans le `o=` rechargé en iframe), donc un **param d'URL traverse**. `AuthGate` écrit `?id=<identifiant>` (`history.replaceState`) **avant** `connect()` ; `AccountContext` résout l'identifiant par priorité **(1) `?id=` de l'URL** puis **(2) localStorage** (préremplissage/convenance same-partition uniquement). Clé localStorage : `festipod.account.identifier`.
|
||||
- **Saisi UNE SEULE FOIS au premier accès + prérempli au retour.** Au rechargement top-level, la session NG n'est pas restaurée d'office (`NextGraphContext` repart en `disconnected`) : `AuthGate` réaffiche la barrière tant que `status !== 'connected'`, mais le champ d'`AccessGateScreen` est **prérempli** (prop `initialIdentifier`) — jamais un champ nu et vide. Régressions gardées par `src/modules/auth/features/{barriere-acces-identifiant,identifiant-resolution}.feature` (@ui) — d'autant plus utiles que le flux de barrière est **désactivé** dans les tests @e2e (`__FESTIPOD_ACCESS_GATE_DISABLED__`), donc invisible à cette couche.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Doc-debt — bdd-testing
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/modules/event/features/reconnexion-persistance-e2e.feature @2026-07-13 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/steps/e2e/reconnexion-persistance.steps.ts @2026-07-13 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/test-harness/harness-ng.tsx @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/test-harness/harness.tsx @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/features/reconnexion-socket-mort.feature @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/steps/data/reconnexion-socket-mort.steps.ts @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/features/reconnexion-meme-identite.feature @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/steps/data/reconnexion.steps.ts @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/features/reconnexion-froide-sans-local.feature @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/steps/data/reconnexion-froide-sans-local.steps.ts @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/auth/steps/ui/barriere-acces.steps.ts @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/support/hooks.ts @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/steps/data/isolation.steps.ts @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/auth/steps/data/connexion.steps.ts @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -31,6 +31,7 @@ Tests BDD **Cucumber/Gherkin en français** (`Etant donné`, `Quand`, `Alors`) s
|
||||
- [[knowledge_data-layer-broker]] — couche `@data` : harness broker, cycle de vie wallet, bridge
|
||||
- [[knowledge_e2e-layer]] — couche `@e2e` : app réelle dans l'iframe
|
||||
- [[knowledge_multibrowser-harness]] — plusieurs navigateurs isolés × modèle de wallet (private/shared), injection storageState
|
||||
- [[caveat_reconnexion-froide-local-vs-broker]] — « page fraîche » ≠ démarrage à froid : quel montage prouve la durabilité broker, et lequel relit le local
|
||||
- [[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,85 @@
|
||||
---
|
||||
type: caveat
|
||||
summary: Une « page fraîche » ouverte via ctx.newPage() sur le contexte Chromium PERSISTANT ne prouve JAMAIS la durabilité broker — elle relit l'IndexedDB local du même profil. Seul un contexte non-persistant issu de freshBrowser, amorcé uniquement par le storageState capturé au BeforeAll, tranche broker-vs-local.
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# Piège : « page fraîche » ≠ démarrage à froid (local vs broker)
|
||||
|
||||
Les scénarios de **reconnexion** posent tous la même question — *l'utilisateur retrouve-t-il ses
|
||||
propres données après avoir fermé et rouvert ?* — mais **selon le contexte navigateur choisi, ils
|
||||
ne répondent pas à la même question**. C'est le piège : le montage le plus naturel (`ctx.newPage()`)
|
||||
donne un vert qui ne prouve rien sur le broker.
|
||||
|
||||
## Les deux montages, et ce que chacun prouve
|
||||
|
||||
| Montage | Où | Ce qu'il prouve | Ce qu'il ne prouve PAS |
|
||||
|---|---|---|---|
|
||||
| `this.page!.context().newPage()` — page fraîche sur le contexte **persistant** (`.playwright-profile`) | `reconnexion.steps.ts` (@data), `reconnexion-persistance.steps.ts` (@e2e) | nouveau login broker → **session verifier fraîche** (mémoire vide), remontage complet des providers | rien sur la **durabilité broker** : le profil détient **encore les repos locaux** en IndexedDB, un lecteur « frais » peut donc rouvrir **depuis le local** |
|
||||
| `spawnContext('shared')` — contexte **non-persistant** issu de `freshBrowser` | `reconnexion-froide-sans-local.steps.ts` (@data) | la donnée **a atteint le broker** (ou non) | rien sur le parcours UI réel (c'est le harness, pas l'app) |
|
||||
|
||||
**Invariant.** Toute assertion de la forme « l'écriture est durable côté broker » **exige** le second
|
||||
montage. Écrire cette assertion sur une page fraîche du contexte persistant produit un test
|
||||
faussement vert (ou un rouge qu'on impute au broker alors qu'il est local/timing).
|
||||
|
||||
## Ce qui rend le verdict « no-local » valide (à ne pas casser)
|
||||
|
||||
Trois conditions, toutes vérifiées dans `reconnexion-froide-sans-local.steps.ts` :
|
||||
|
||||
1. **Process séparé** — `freshBrowser` est un `chromium.launch` non-persistant, distinct du profil
|
||||
porteur du wallet (cf. [[knowledge_multibrowser-harness]] pour l'isolation prouvée jusqu'à
|
||||
l'origine broker).
|
||||
2. **Partition hermétique** — chaque `newContext()` Playwright a son propre stockage ; aucun
|
||||
IndexedDB partagé avec la page d'écriture.
|
||||
3. **Le seul état pré-injecté est `pool.sharedWalletState`**, capturé **une fois au `BeforeAll`**,
|
||||
donc **avant** que le scénario n'écrive quoi que ce soit → le snapshot **ne peut pas** contenir la
|
||||
donnée sous test.
|
||||
|
||||
> **Impact si on touche à la capture du storageState** (`hooks.ts` `BeforeAll` → `pool.sharedWalletState`) :
|
||||
> la déplacer plus tard, la ré-capturer par scénario, ou y ajouter un warm-up qui écrit des données
|
||||
> **invalide silencieusement** le verdict de tous les scénarios « à froid sans local » — ils
|
||||
> passeraient au vert en relisant le snapshot. Le step **échoue franchement** si
|
||||
> `sharedWalletState` est absent (c'est voulu : pas de verdict plutôt qu'un faux verdict).
|
||||
|
||||
## Reconnexion ≠ isolation : c'est l'identifiant qui décide
|
||||
|
||||
`isolation.steps.ts` et `reconnexion.steps.ts` montent **la même mécanique** (page fraîche + un
|
||||
identifiant injecté dans `localStorage['festipod.account.identifier']` via `addInitScript`, avant
|
||||
tout script, sur toutes les origines). Une seule chose les sépare :
|
||||
|
||||
- **reconnexion** : on réinjecte `this.freshIdentifier` — **la MÊME identité** que la page d'écriture.
|
||||
- **isolation** : on frappe un **nouvel** identifiant → identité B distincte.
|
||||
|
||||
Changer cet identifiant transforme donc silencieusement un test de reconnexion en test d'isolation
|
||||
(et réciproquement). `this.freshIdentifier` est posé par le `Before` de `hooks.ts` pour **tout**
|
||||
scénario `@data`/`@e2e` mono-navigateur.
|
||||
|
||||
## Lecture : réactive, même quand on « attend longtemps »
|
||||
|
||||
Les `Then` de reconnexion lisent l'état **réactif** (`homeEventTitles` sur le bridge, via
|
||||
`waitForFunction`) — jamais une boucle de re-lecture broker ([[rule_no-broker-polling]]). Le step de
|
||||
diagnostic long (« … en laissant jusqu'à 60 secondes à la barrière avec rechargements ») boucle bien,
|
||||
mais sur **l'état réactif déjà poussé** + des **rechargements complets** de la page (chaque reload =
|
||||
nouveau montage = nouvelle tentative de barrière de sync) : c'est le fallback pragmatique explicitement
|
||||
autorisé par la règle, pas du polling broker. Le distinguo à garder : *observer l'état réactif* ↔
|
||||
*ré-émettre une lecture broker*.
|
||||
|
||||
## État courant des scénarios
|
||||
|
||||
`reconnexion-froide-sans-local.feature`, le scénario `@reconnexion-pause` de
|
||||
`reconnexion-meme-identite.feature` et `reconnexion-persistance-e2e.feature` sont **`@wip`** : ce sont
|
||||
des **instruments de diagnostic** (ils impriment un verdict sur stdout / en pièce jointe Cucumber),
|
||||
pas des gardes de régression. `@wip` est exclu du run par défaut (`cucumber.json`) — les lancer
|
||||
explicitement avec `--tags`. Le scénario **non-`@wip`** de `reconnexion-meme-identite.feature`, lui,
|
||||
est une vraie garde et doit rester vert.
|
||||
|
||||
> Le *pourquoi* côté NextGraph (ce qu'une écriture doit franchir pour être durable, comportement du
|
||||
> socket, réouverture des repos) appartient au SDK `@ng-eventually/client` — pas à ce repo. Ici on ne
|
||||
> décrit que **le montage de test qui rend un verdict lisible**.
|
||||
|
||||
## Liens
|
||||
|
||||
- [[knowledge_data-layer-broker]] — la couche `@data` mono-navigateur (profil persistant).
|
||||
- [[knowledge_multibrowser-harness]] — `freshBrowser`, `spawnContext`, `pool.sharedWalletState`.
|
||||
- [[knowledge_e2e-layer]] — le pendant `@e2e` (app réelle) du montage « fermer et rouvrir ».
|
||||
- [[rule_no-broker-polling]] — la ligne rouge que les steps d'attente ne doivent pas franchir.
|
||||
@@ -8,7 +8,7 @@ last_checked: 2026-07-06
|
||||
|
||||
Le profil Chromium persistant `.playwright-profile` (racine du working tree) porte le **wallet
|
||||
partagé** ouvert par toute la suite `@data`/`@e2e`. Ce wallet **accumule des données à chaque
|
||||
run** : comptes shim (un par scénario, via l'identifiant frais `freshScenarioUsername`), docs
|
||||
run** : comptes shim (un par scénario, via l'identifiant frais `freshScenarioIdentifier`), docs
|
||||
d'entités seedés, dépôts d'inbox historiques… Le private store est le **point d'ancrage du shim**
|
||||
(résolution de compte) et est interrogé par **toute** lecture/écriture (`resolveAccount`,
|
||||
`listMyEntityDocs`, …).
|
||||
@@ -26,7 +26,7 @@ recréer un frais :
|
||||
mv .playwright-profile /tmp/festipod-bloated-$(date +%s)
|
||||
```
|
||||
|
||||
L'identifiant frais par scénario (`freshScenarioUsername`) borne le *registre* des comptes mais
|
||||
L'identifiant frais par scénario (`freshScenarioIdentifier`) borne le *registre* des comptes mais
|
||||
**pas** la croissance physique du private store partagé — d'où la récurrence. Une hygiène durable
|
||||
(purge périodique / wallet jetable par run) reste à mettre en place ; en attendant, si les
|
||||
`resolveAccount failed`/timeouts réapparaissent, repartir d'un profil frais.
|
||||
|
||||
@@ -27,7 +27,7 @@ Tags de scénario : `@ui` / `@data` / `@e2e` (couche) + **`@wip`** pour un scén
|
||||
|
||||
## Config
|
||||
|
||||
`cucumber.json` : `import` de `src/shared/support/**`, `src/shared/steps/**`, `src/modules/*/steps/**` ; `paths` = `src/modules/*/features/**`; `tags: "not @wip"` (exclut les scénarios WIP) ; `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`.
|
||||
`cucumber.json` : `import` de `src/shared/support/**`, `src/shared/steps/**`, `src/modules/*/steps/**` ; `paths` = `src/modules/*/features/**`; `tags: "not @wip"` (exclut les scénarios WIP) ; `language: fr`. **Runner = Node + tsx**, pas Bun — les plugins (Playwright, happy-dom) ne chargent pas en import Bun natif. Ne pas « bunifier » `cucumber:run`/`test:data`. ⚠️ Le runner doit pointer sur l'**entrée JS réelle du paquet** (`node_modules/@cucumber/cucumber/bin/cucumber.js`), **jamais** sur `node_modules/.bin/cucumber-js` : selon l'installeur, `.bin/` contient un **shim shell** (pas du JS) que `node --import tsx/esm` ne peut pas exécuter.
|
||||
|
||||
## Le harness de test est buildé à la demande
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Couche @data — Playwright pilote Chromium (profil persistant) qui s'authentifie au broker NextGraph réel chargeant harness-ng.tsx en iframe ; cycle de vie wallet automatisé (création + login bootstrap), bridge window.__testData, fallback mock
|
||||
last_checked: 2026-07-05
|
||||
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 ; isolation par identifiant virtuel frais (this.freshIdentifier), plus de purge par scénario
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# Couche `@data` (broker réel)
|
||||
@@ -50,17 +50,22 @@ Cucumber → Playwright (Chromium, profil persistant)
|
||||
`ensureCurrentUser()` avant `joinEvent` (sinon participation écrite sans user → jetée en
|
||||
lecture, ne fait jamais l'aller-retour) et attendent (`waitForFunction`) que la participation
|
||||
soit relue.
|
||||
- **Caveat wallet persistant + isolation par scénario (T03.j)** : le wallet partagé **accumule**
|
||||
le registre de comptes émulé et les docs per-entité à chaque scénario/run. Le fan-out de lecture
|
||||
(`listEntityDocs` = `allAccounts()` → 1 SELECT/compte) parcourt tous les docs de tous les comptes
|
||||
→ ralentit et fait *timeouter* les steps quand le wallet est pollué. Ce registre vit **côté
|
||||
broker** : supprimer `.playwright-profile/` ne le nettoie PAS (re-sync depuis le broker) et force
|
||||
une re-auth lente — mauvais levier. À la place, le `Before` @data appelle
|
||||
`window.__testData.resetDataState()` : **UN** SPARQL DELETE sur le graphe ancre (private-store)
|
||||
qui efface tous les records `urn:ng-eventually:shim:Account` → `allAccounts()` s'effondre à vide →
|
||||
le fan-out se **borne** à ce que le scénario courant reprovisionne (comptes recréés paresseusement
|
||||
par `ensureAccount`). O(1) sur UN graphe — **pas** un delete en fan-out (qui saturait le navigateur,
|
||||
cf. T03.i `authClearParticipation` retiré). Borné à ≤10s (`Promise.race`) pour ne pas disputer le
|
||||
budget 60s du `Before` (login broker déjà lent). Infra de test uniquement — ne touche ni la lib ni
|
||||
le modèle produit ni le chemin de lecture applicatif. Le seed connecté reste **allégé** (peu de
|
||||
docs) car chaque `docCreate` est un aller-retour broker sériel ~2s.
|
||||
- **Isolation par scénario = identifiant virtuel frais, PAS de purge.** Le `Before` @data mint un
|
||||
identifiant unique par scénario (`freshScenarioIdentifier` dans `hooks.ts`), l'expose en
|
||||
`this.freshIdentifier` sur le World, et l'injecte par `addInitScript` dans
|
||||
`localStorage['festipod.account.identifier']` **sur toutes les origines** (y compris l'iframe
|
||||
harness sur 127.0.0.1) — avant tout script. Le shim sert alors un **compte virtuel frais et vide**,
|
||||
dont le registre part vide *par construction* : **rien à purger**. L'ancien reset par scénario
|
||||
(`window.__testData.resetDataState()`, un SPARQL DELETE des records
|
||||
`urn:ng-eventually:shim:Account` sur le graphe ancre) **n'est plus appelé** — il coûtait jusqu'à
|
||||
10 s prélevés sur le budget 60 s du `Before`, déjà mangé par le login broker. Le helper existe
|
||||
encore sur le bridge (`harness-ng.tsx`) mais n'est plus dans le chemin par défaut : ne pas le
|
||||
remettre dans le `Before` sans mesurer.
|
||||
- **Ce que l'identifiant frais NE borne PAS** : la croissance *physique* du wallet partagé — voir
|
||||
[[caveat_wallet-bloat-hang]] (profil à mettre de côté quand les lectures ancrées se mettent à
|
||||
*hang*).
|
||||
- `this.freshIdentifier` est aussi ce qui distingue un test de **reconnexion** (même identifiant
|
||||
réinjecté) d'un test d'**isolation** (nouvel identifiant) — cf.
|
||||
[[caveat_reconnexion-froide-local-vs-broker]].
|
||||
- Le seed connecté reste **allégé** (peu de docs) car chaque `docCreate` est un aller-retour broker
|
||||
sériel ~2s.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
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 ; identité par scénario (this.freshIdentifier) + barrière d'accès désactivée par init script ; idiome « fermer et rouvrir » pour les scénarios de reconnexion
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# Couche `@e2e` (app réelle)
|
||||
@@ -42,6 +43,45 @@ Navigation : `window.history.pushState` + dispatch `popstate` (routing path-base
|
||||
|
||||
> **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]]).
|
||||
|
||||
## Identité du scénario + barrière d'accès
|
||||
|
||||
Deux réglages posés par le `Before` de `hooks.ts` conditionnent **tout** scénario `@e2e` :
|
||||
|
||||
- **`this.freshIdentifier`** — un identifiant virtuel **unique par scénario**, injecté par
|
||||
`addInitScript` dans `localStorage['festipod.account.identifier']` sur **toutes** les origines
|
||||
avant tout script. L'app réelle boote donc directement sur cette identité, et chaque scénario part
|
||||
d'un espace vide. C'est la **même** mécanique qu'en `@data` (même champ du World).
|
||||
- **Barrière d'accès désactivée** — `browserContext.addInitScript` pose
|
||||
`globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ = true` sur le contexte **persistant** : `@e2e` voit
|
||||
l'app, pas l'`AccessGateScreen`. Les contextes **frais** (`@humain`, cf.
|
||||
[[knowledge_multibrowser-harness]]) n'héritent pas de ce réglage → barrière ON chez eux.
|
||||
|
||||
> **Impact :** toute page ouverte à la main dans un step (`ctx.newPage()`) doit **re-poser les deux
|
||||
> init scripts elle-même** — `addInitScript` du contexte ne s'applique qu'aux pages du contexte, et
|
||||
> l'identifiant doit être écrit **avant** le premier script de l'app.
|
||||
|
||||
## Idiome « fermer et rouvrir » (scénarios de reconnexion)
|
||||
|
||||
`reconnexion-persistance-e2e.feature` / `src/modules/event/steps/e2e/reconnexion-persistance.steps.ts`
|
||||
reproduisent, dans la VRAIE app, le parcours « je crée, je ferme, je reviens » :
|
||||
|
||||
1. **Création par le vrai formulaire** — le step pilote l'assistant de création réel au DOM
|
||||
(assistant en 3 étapes, sélection par *placeholder* : nom de l'événement, lieu ; bouton de
|
||||
soumission par son libellé). ⚠️ **Ces steps sont couplés aux libellés FR de l'écran de création** :
|
||||
renommer un placeholder ou le bouton de soumission casse le scénario, pas l'app.
|
||||
2. **Réouverture** — seconde page sur le **même** contexte persistant, avec la **même**
|
||||
`this.freshIdentifier` + la barrière désactivée, puis `pool.setupBrokerPage(page, pool.appUrl!)`
|
||||
→ nouveau login broker, session verifier fraîche.
|
||||
3. **Preuve** — le step capture la console des **deux** pages et publie un résumé via `this.attach`
|
||||
(pièce jointe Cucumber) + stdout ; un dump brut des lignes de connexion/sync est **opt-in** par
|
||||
la variable d'environnement `RECO_RAW_DUMP=1` (bruyant, coupé par défaut).
|
||||
|
||||
> **Limite à connaître** : ce montage prouve la reconnexion *du parcours*, **pas** la durabilité
|
||||
> broker de l'écriture — la seconde page partage l'IndexedDB du profil persistant. Voir
|
||||
> [[caveat_reconnexion-froide-local-vs-broker]] pour le montage qui, lui, tranche broker-vs-local.
|
||||
|
||||
Le scénario est **`@wip`** (instrument de diagnostic, exclu du run par défaut).
|
||||
|
||||
## Smoke `@smoke` — garde la classe « page blanche une fois connecté »
|
||||
|
||||
`@e2e @smoke` (`src/modules/home/features/accueil-connecte-rend.feature`) garde une
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
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
|
||||
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 ; piège des écrans qui lisent un global injecté au build (barrière d'accès → import lazy obligatoire)
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# Couche `@ui`
|
||||
@@ -30,4 +31,27 @@ expect(labels.some(t => t.includes("Nom de l'événement *"))).to.be.true;
|
||||
- `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]]).
|
||||
|
||||
## ⚠️ Écrans qui lisent un global injecté au **build** (barrière d'accès)
|
||||
|
||||
`src/modules/auth/sharedWallet.ts` **capture au moment de l'évaluation du module** un global posé par
|
||||
`build.ts` (`__FESTIPOD_SHARED_WALLET_PASSWORD__`). Le harness `@ui` tourne sous Node **sans passer
|
||||
par le build** → ce global est absent, `hasSharedWallet()` retourne faux, et comme le **wallet
|
||||
partagé est le seul mode supporté** (concept `app-security`), `AccessGateScreen` rend sa branche
|
||||
**erreur de configuration** : **aucun champ identifiant** dans le DOM → tous les steps de la barrière
|
||||
échouent avec un message trompeur (« champ introuvable »).
|
||||
|
||||
**Le montage obligatoire** (appliqué dans `src/modules/auth/steps/ui/barriere-acces.steps.ts`) :
|
||||
|
||||
1. poser le global **en tête du module de steps**, avant tout import de l'écran ;
|
||||
2. **importer l'écran paresseusement** (`await import(...)` mémoïsé) — un `import` statique serait
|
||||
**hissé au-dessus** de l'affectation et `sharedWallet.ts` capturerait une valeur vide.
|
||||
|
||||
> **Impacts si tu touches à ça :**
|
||||
> - Ajouter un `import` statique de `AccessGateScreen` (ou de tout module qui atteint
|
||||
> `sharedWallet.ts`) dans **n'importe quel** fichier de steps `@ui` ré-introduit le bug — Cucumber
|
||||
> charge tous les modules de steps, l'écran serait évalué avant que le global soit posé.
|
||||
> - Le déterminisme actuel repose sur le fait que **ce fichier est le seul** module `@ui` à atteindre
|
||||
> `sharedWallet.ts`. Un second point d'entrée rendrait l'ordre d'évaluation non garanti → il
|
||||
> faudrait alors déplacer l'injection du global dans le support partagé, pas la dupliquer.
|
||||
|
||||
> 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]].
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Doc-debt — data-layer
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/shared/context/FestipodDataContext.tsx @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/shared/utils/ngSession.ts @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -18,7 +18,7 @@ Comment Festipod **persiste ses données** via NextGraph (P2P, local-first, chif
|
||||
- [[knowledge_data-modes]] — connected (SDK) vs disconnected/demo (état local seedé), choix du provider
|
||||
- [[knowledge_entities]] — types `Fp*` et leurs shapes SHEX
|
||||
- [[knowledge_seed-data]] — données de seed, `CURRENT_USER_ID`
|
||||
- [[knowledge_context-internals]] — pièges de `FestipodDataContext` (currentUser, auto-seed dev, `participantCount` cache, no-op local)
|
||||
- [[knowledge_context-internals]] — pièges de `FestipodDataContext` (currentUser, **deux espaces d'id** principal↔NURI de profil, auto-seed dev, `participantCount` cache, reset au changement d'identité, no-op local)
|
||||
|
||||
## Règles d'écriture
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: knowledge
|
||||
summary: Pièges internes de FestipodDataContext — currentUserId = principal stable dérivé de l'identifiant, auto-seed OPT-IN (FESTIPOD_AUTO_SEED, OFF par défaut), participantCount dérivé Option-B (fiable à la connexion du propriétaire via lecture inbox gated sur barrière ; source unique = event.participantCount), instrumentation useShapeQuery (spinner+timing), mutations no-op en mode local malgré le toast
|
||||
last_checked: 2026-07-07
|
||||
summary: Pièges internes de FestipodDataContext — currentUserId = principal stable dérivé de l'identifiant, DEUX espaces d'id joints par l'identifiant normalisé (resolveParticipantUser / USER_PRINCIPAL_PREFIX), auto-seed OPT-IN (FESTIPOD_AUTO_SEED, OFF par défaut), participantCount dérivé Option-B (fiable à la connexion du propriétaire via lecture inbox gated sur barrière ; source unique = event.participantCount), reset de session au changement d'identité (overlay + caps), instrumentation useShapeQuery (spinner+timing) + logs identité-first, mutations no-op en mode local malgré le toast
|
||||
last_checked: 2026-07-27
|
||||
---
|
||||
|
||||
# Internals & pièges de `FestipodDataContext`
|
||||
@@ -14,6 +14,27 @@ En mode connected, le **principal** du currentUser (`currentUserId`) n'est **pas
|
||||
- L'objet `currentUser` (le profil affiché) est, lui, résolu par `users.find(u => normalizeIdentifier(u.username) === identifiant)` avec **fallback** `@mariedupont` puis `users[0]` — un fallback silencieux si l'identifiant ne correspond à aucun profil (l'identifiant est un id d'espace, pas forcément le `username` d'un profil seedé).
|
||||
- Sans identifiant connecté (dev/demo), `currentUserId` retombe sur l'IRI du profil lu (ou `''` si le wallet est vide → `Participation` avec `user: ''` invalide) : ne créer une participation qu'une fois le principal résolu.
|
||||
|
||||
## DEUX espaces d'id se rencontrent — joindre une participation à son profil
|
||||
|
||||
**Invariant.** Une `Participation` stocke son user comme **principal** (`urn:festipod:user:<identifiant-normalisé>`, = `currentUserId`), alors qu'un `UserProfile` a pour `id` le **NURI de son document** (`did:ng:…`). En mode connecté, **ces deux valeurs ne sont jamais égales**. Une jointure brute `participation.userId === profile.id` ne matche donc **jamais** — symptôme livré puis corrigé (2026-07-27) : chaque participant s'affichait « participant inconnu ». Toute jointure participation→profil passe par **`resolveParticipantUser`** (`FestipodDataContext`), jamais par une comparaison directe.
|
||||
|
||||
Le **pont** entre les deux espaces est l'**identifiant normalisé** : `principal − préfixe` == `normalizeIdentifier(profile.username)` (la même égalité que la résolution de `currentUser`). D'où l'ordre d'essai de `resolveParticipantUser` : (1) **match direct** `u.id === userId` — l'espace du seed demo, où les deux côtés valent le même id nu (`user-1`) et où le username seedé `@mariedupont` ne normaliserait *pas* vers cet id, donc le direct doit passer en premier ; (2) à défaut, **match sur l'identifiant normalisé** après retrait du préfixe.
|
||||
|
||||
**`USER_PRINCIPAL_PREFIX` est la source unique du préfixe**, partagée par l'**écriture** (dérivation de `currentUserId`) et la **lecture** (`resolveParticipantUser`). Si tu changes la forme du principal, change-la **là** : sinon écriture et lecture divergent en silence et la jointure retombe sur « inconnu » sans lever d'erreur.
|
||||
|
||||
Un **troisième** espace d'id existe et ne participe **pas** à cette jointure : l'`uid` de dépôt d'inbox (`mint…`) — il identifie un **dépôt** pour le compteur, jamais un utilisateur.
|
||||
|
||||
> **Horizon.** Ce paragraphe décrit l'**implémenté** (Option-B). Le modèle cible retire le `userId` en clair et fait passer la résolution d'identité par la **lecture du profil** — cf. [[brief_2026-07-20_attendance-set-model]], dont la mise en œuvre est gatée. Le fix des espaces d'id y est explicitement noté comme **restant valable** : ne pas le défaire en anticipant la cible.
|
||||
|
||||
### Quel espace attend chaque query (contrat de `buildQueries`)
|
||||
|
||||
| Query | Ce qu'elle attend / rend |
|
||||
|---|---|
|
||||
| `getUserEvents(userId)`, `isParticipating(eventId, userId?)`, `getFriends(userId?)` | **attendent le principal** (elles filtrent sur `participation.userId` / `friendship.userId`) — leur défaut est `currentUserId`, correct |
|
||||
| `getEventParticipants(eventId)` | **rend des profils** (`FpUserData` → `id` = NURI), la jointure étant faite en interne |
|
||||
|
||||
**Impact côté écran** : se filtrer soi-même hors d'une liste de participants se compare à **`currentUser?.id`** (NURI de profil, même espace que les éléments rendus), **pas** à `currentUserId` (principal) — sinon on ne se retire pas et on se voit soi-même apparaître comme un participant de plus. Inversement, passer un **id de profil** à `getUserEvents`/`isParticipating` rend une liste **vide** en mode connecté. Voir `app-architecture`, [[caveat_identity-ids-in-screens]].
|
||||
|
||||
## Lecture = `watchShape` (surface SDK), plus de machinerie bespoke
|
||||
|
||||
**Depuis 2026-07-10** : `useNgData` lit via `useShapeQuery(shape, scope)` (binding
|
||||
@@ -66,16 +87,28 @@ Le `@id` d'un événement **est** son NURI de document (`did:ng:o:<repo>[:v:<ove
|
||||
|
||||
## Changement d'identité = session fraîche (isolation)
|
||||
|
||||
Le jeu de lecture par besoin (`publicDocs`/`protectedDocs`) **accumule** les docs de scope de l'identité courante (pour ne pas perdre un doc juste créé avant la re-liste). Or le stopgap wallet-partagé garde **un seul arbre React** au travers d'un faux-logout + re-login sous un **autre identifiant** (pas de rechargement — `AccountContext.login` ne fait que réécrire l'identifiant en localStorage, `AuthGate` ne remonte rien). Sans réinitialisation, **les docs PROTECTED de l'identité précédente (ses participations) survivent dans le jeu de lecture de la nouvelle identité et fuient** via la lecture union : le cap gate ne peut pas les filtrer quand le registre de caps (en mémoire) ne gouverne pas ce doc *cette* session (doc persisté d'un run antérieur, ou chargement frais où les caps sont vides). Symptôme observé : un utilisateur B voyait la participation de A (et l'événement de A apparaissait sur l'**accueil** de B, car l'accueil = `getUserEvents(currentUserId)`, cf. concept `app-architecture`).
|
||||
> **Historique du symptôme** (le paragraphe qui suit décrit le montage d'alors — le jeu de lecture bespoke `publicDocs`/`protectedDocs`/`readTick` **n'existe plus** depuis le passage à `watchShape`). Il est conservé parce qu'il explique *pourquoi* la règle du reset existe ; le **mécanisme courant** est décrit plus bas.
|
||||
|
||||
**Règle** : traiter **tout changement d'identifiant** comme une session fraîche — un `useEffect([identifier])` (ref-gardé pour ne pas tirer au premier mount) vide `publicDocs`/`protectedDocs`, appelle `resetCaps()` + `resetRegistryCache()`, puis bump le read tick ; l'effet de listing reconstruit le jeu **borné à la nouvelle identité**. L'isolation reste par-document/émulée (concept `app-security`, [[knowledge_trust-model]]) ; ce reset ne fait que supprimer le report d'état inter-identités.
|
||||
Le jeu de lecture par besoin (`publicDocs`/`protectedDocs`) **accumulait** les docs de scope de l'identité courante (pour ne pas perdre un doc juste créé avant la re-liste). Or le stopgap wallet-partagé garde **un seul arbre React** au travers d'un faux-logout + re-login sous un **autre identifiant** (pas de rechargement — `AccountContext.login` ne fait que réécrire l'identifiant en localStorage, `AuthGate` ne remonte rien). Sans réinitialisation, **les docs PROTECTED de l'identité précédente (ses participations) survivent dans le jeu de lecture de la nouvelle identité et fuient** via la lecture union : le cap gate ne peut pas les filtrer quand le registre de caps (en mémoire) ne gouverne pas ce doc *cette* session (doc persisté d'un run antérieur, ou chargement frais où les caps sont vides). Symptôme observé : un utilisateur B voyait la participation de A (et l'événement de A apparaissait sur l'**accueil** de B, car l'accueil = `getUserEvents(currentUserId)`, cf. concept `app-architecture`).
|
||||
|
||||
**Mécanisme confirmé empiriquement (2026-07-07)** : le leak se reproduit UNIQUEMENT quand DEUX conditions coïncident — (a) le jeu de lecture porte encore le doc PROTECTED de A au travers du switch (pas de reset), ET (b) le registre de caps en mémoire ne gouverne pas ce doc (`resetCaps()` déjà tiré / caps vides pour un doc persisté d'une session antérieure au reload). Alors la participation de A traverse la lecture union de B (le filtre par-document n'a aucun cap à vérifier). Avec le reset ci-dessus tiré, `setProtectedDocs([])` retire le doc de A du jeu de lecture de B AVANT que la lecture cap-less ne l'expose → plus de fuite quel que soit l'état des caps. **Régression gardée** par le scénario `@data` « Une identité fraîche ne voit pas la participation d'une autre » (event/isolation-deux-identites.feature) : A crée E + s'y inscrit, B (page fraîche sur le même wallet, identifiant distinct) n'a NI E sur son accueil (`getUserEvents(B)`), NI `isParticipating(E,B)`, ET ne lit AUCUNE participation portant le principal de A. Le symptôme historique « B voit “Je participe” » survenait surtout quand B **réutilisait un identifiant déjà employé par A** (même principal normalisé) sur un wallet **bloaté** (docs persistés d'un run antérieur, caps vides).
|
||||
**Règle** : traiter **tout changement d'identifiant** comme une **session fraîche**. Un `useEffect([identifier])`, **ref-gardé** (il ne tire pas au premier mount, seulement sur un vrai changement de valeur), remet à zéro **tout l'état de session porté par l'app**. L'isolation reste par-document/émulée (concept `app-security`, [[knowledge_trust-model]]) ; ce reset ne fait que supprimer le report d'état inter-identités.
|
||||
|
||||
**Mécanisme courant** (depuis la lecture par `watchShape`) : la **lecture** n'a plus rien à réinitialiser — `watchShape` re-résout son scope sur le nouveau `getCurrentUser()` au prochain push. Ce que l'effet vide est l'état **app-side** : `ownedEventIds` (le jeu du matérialiseur du propriétaire), la map `joinUids` (uid de dépôt de la session courante), l'**overlay optimiste** (`pendingAddEvents`/`pendingAddParticipations`/`pendingRemoveIds` — sinon les mutations de l'ancienne identité saignent dans les lectures de la nouvelle), puis `resetCaps()` + `resetRegistryCache()`.
|
||||
|
||||
> **Impact — l'invariant à ne pas casser** : **tout nouvel état de session** ajouté au provider (cache, `useRef`, overlay, jeu de docs) doit être ajouté à cet effet. Un état oublié **fuit d'une identité à l'autre** sans erreur — c'est exactement la classe de bug que la garde de régression ci-dessous couvre.
|
||||
|
||||
**Mécanisme confirmé empiriquement (2026-07-07)** : le leak se reproduit UNIQUEMENT quand DEUX conditions coïncident — (a) le jeu de lecture porte encore le doc PROTECTED de A au travers du switch (pas de reset), ET (b) le registre de caps en mémoire ne gouverne pas ce doc (`resetCaps()` déjà tiré / caps vides pour un doc persisté d'une session antérieure au reload). Alors la participation de A traverse la lecture union de B (le filtre par-document n'a aucun cap à vérifier). Avec le reset tiré, le doc de A quittait le jeu de lecture de B AVANT que la lecture cap-less ne l'expose → plus de fuite quel que soit l'état des caps (à l'époque via `setProtectedDocs([])` ; aujourd'hui c'est `watchShape` qui re-résout le scope, et le reset ne porte plus que l'état app-side listé plus haut). **Régression gardée** par le scénario `@data` « Une identité fraîche ne voit pas la participation d'une autre » (event/isolation-deux-identites.feature) : A crée E + s'y inscrit, B (page fraîche sur le même wallet, identifiant distinct) n'a NI E sur son accueil (`getUserEvents(B)`), NI `isParticipating(E,B)`, ET ne lit AUCUNE participation portant le principal de A. Le symptôme historique « B voit “Je participe” » survenait surtout quand B **réutilisait un identifiant déjà employé par A** (même principal normalisé) sur un wallet **bloaté** (docs persistés d'un run antérieur, caps vides).
|
||||
|
||||
## Instrumentation `useShapeQuery` — spinner global + timing
|
||||
|
||||
`useShapeQuery` (binding `useSyncExternalStore` sur `watchShape`) instrumente **chaque cycle de requête** : au début d'un cycle il s'enregistre dans un store module-level `src/shared/data/pendingQueries.ts` (`beginQuery`/`resolveQuery`, Set d'ids — idempotent, sûr sous StrictMode), et à la 1re transition `isPending → isSuccess|isError` (le « premier résultat », équivalent readPromise) il se résout ET logge le délai : `[FestipodData] <shape>/<scope> premier résultat en <N>ms (n=<len>)` (le délai des événements Event/public est donc visible nommément). Le `cycleId` est mémoïsé sur `[shapeKey, scope]` → un switch d'identité/scope recrée l'observable ET un nouveau cycle (re-`beginQuery`), et le cleanup résout au démontage (jamais bloqué). Le hook `usePendingQueries()` expose le nombre de requêtes en attente ; `HomeScreen` affiche un `Spinner` (sketchy, `.app-spinner` + `@keyframes app-spin` dans `index.css`) à côté du titre « Festipod » tant que le compte > 0 → il ne s'arrête que quand **toutes** les requêtes en cours ont reçu leur premier résultat. Toute future `useShapeQuery` y contribue automatiquement. La mesure vit côté app (délai perçu React), **pas** dans le polyfill.
|
||||
|
||||
## Convention de log — préfixe identité-first, et compteur avant→après
|
||||
|
||||
Tout log DATA du provider passe par **`logPrefix`** : `[<currentUserId>][app][data]` quand le principal est résolu, `[app][data]` sinon (état transitoire de connexion). Raison : avec le wallet partagé, **deux identités partagent la même console** (deux onglets / un multi-navigateur) — une ligne non préfixée ne dit pas *de qui* elle parle et devient inexploitable pour diagnostiquer une fuite ou un compteur bloqué. **Ajouter un log DATA = réutiliser `logPrefix`**, pas un `console.log` nu.
|
||||
|
||||
Deux points de mesure sont posés **par paire** et servent ensemble : le matérialiseur du propriétaire logge `participantCount` **avant → après** son écriture, et la lecture d'affichage logge la valeur **telle qu'exposée au rendu**. Les comparer tranche un compteur bloqué entre un problème **DONNÉE** (jamais incrémenté) et un problème **AFFICHAGE** (incrémenté mais pas relu avant la session suivante). Ne pas retirer l'un des deux sans l'autre — isolément ils ne diagnostiquent rien.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# Doc-debt — functional-domain
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED src/modules/event/features/reconnexion-persistance-e2e.feature @2026-07-13 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/features/reconnexion-socket-mort.feature @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/features/reconnexion-meme-identite.feature @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
- TOUCHED src/modules/event/features/reconnexion-froide-sans-local.feature @2026-07-14 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -17,6 +17,8 @@ summary: Ce qui est implémenté aujourd'hui (cycle événement + point de renco
|
||||
|
||||
> L'inscription/désinscription au point de rencontre est **réellement branchée** côté données : `joinEvent` persiste une Participation, notifie l'hôte du PdR et crée une Notification ; `leaveEvent` supprime la Participation de façon autoritative (cf. concept `data-layer`, [[caveat_participation-deletion]] côté data-layer). La découverte publique — un utilisateur voit un événement public d'un autre — fonctionne aussi.
|
||||
|
||||
> **Réserve produit — la persistance n'est pas garantie de bout en bout.** Un événement créé peut **disparaître** après une période d'inactivité puis une reconnexion sous la même identité (même wallet). C'est un **défaut ouvert**, pas une propriété du modèle produit : impact et pointeur côté `data-layer` → [[caveat_write-durability-across-disconnect]]. Conséquence pour le domaine : « mes événements / mes inscriptions » se comportent comme *implémentés* mais **pas encore comme durables** — ne pas bâtir de promesse produit (rappels, historique, engagement) dessus tant que ce caveat est ouvert. Les scénarios `src/modules/event/features/reconnexion-*.feature` du module `event` sont la garde de non-régression de cette promesse (statut d'exécution : concept `bdd-testing`).
|
||||
|
||||
## Évolutions identifiées (non implémentées)
|
||||
|
||||
- **Abonnement à une communauté d'intérêt** pour découvrir ses événements (discovery distribué).
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# Doc-debt — tech-stack
|
||||
|
||||
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
|
||||
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
|
||||
|
||||
## Raw markers (consolidate into blocks, then delete)
|
||||
- TOUCHED package.json @2026-07-20 (session 0b064e8b-1717-421f-a20e-a4318ad217b1)
|
||||
@@ -25,7 +25,7 @@ summary: Composants de la stack (Bun runtime/build/test, install via pnpm, React
|
||||
| `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) |
|
||||
| `cucumber:run` | `node --import tsx/esm node_modules/@cucumber/cucumber/bin/cucumber.js` — **via Node+tsx, pas Bun** (compat plugins Playwright/happy-dom), et via l'**entrée JS réelle du paquet**, pas le shim `.bin/` (voir Pièges) |
|
||||
| `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 |
|
||||
@@ -39,4 +39,5 @@ summary: Composants de la stack (Bun runtime/build/test, install via pnpm, React
|
||||
## 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.
|
||||
- **Ne jamais faire pointer un script sur `node_modules/.bin/*`.** L'install passe par pnpm ([[rule_bun-first]] §exception), qui y place des **shims shell** et non des entrées JS : `node --import tsx/esm node_modules/.bin/cucumber-js` échoue. Invoquer l'**entrée JS réelle du paquet** (`node_modules/@cucumber/cucumber/bin/cucumber.js`). Vaut pour tout script npm qui lancerait un binaire de dépendance sous `node`.
|
||||
- **`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).
|
||||
|
||||
Reference in New Issue
Block a user