Commit Graph

120 Commits

Author SHA1 Message Date
Sylvain Duchesne e2adfacb0b docs(concept): la validation d'existence est IMPOSSIBLE, pas optionnelle
Le brief inscriptions disait que le créateur pouvait vérifier qu'un did pointe
sur un objet réel sans détenir la clé, et rangeait ça en durcissement optionnel.
Faux : le contrôle d'accès en lecture laisse bien passer, mais l'ADRESSAGE
présuppose le cap — aucune commande d'existence au niveau SDK, et une référence
cap-less n'a ni les identifiants de blocs ni l'overlay nécessaires.

Conséquence assumée, écrite noir sur blanc : le créateur ajoute la référence SUR
PAROLE, donc le compteur est déclaratif et forgeable. Hors périmètre sécurité,
mais cette étape ne doit pas être présentée comme une validation.

Le modèle lui-même n'en dépendait pas — il était déjà noté comme non requis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 16:08:15 +02:00
Sylvain Duchesne 96e28a702f 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
2026-07-27 14:43:09 +02:00
Sylvain Duchesne a8401bd143 docs(concept): inscriptions — Participation lisible + drapeau active, purge par le créateur
Affinement PO du 2026-07-27. La Participation devient LISIBLE par tous et se
réduit à trois choses : référence à l'événement, booléen `active`, did cap-less
vers le profil protected du participant. Pas de description pour l'instant.

Ce que ça débloque : une suppression n'est pas détectable sans la clé (vérifié),
ce qui imposait un nudge forgeable pour la désinscription. Un objet lisible avec
un drapeau change la nature du problème — l'annulation n'est plus à DÉTECTER,
elle est à LIRE. Le blocage disparaît au lieu d'être contourné.

Le principe qui tient l'ensemble : la vérité est dans l'objet que le participant
contrôle, tout message n'est qu'un indice. Un faux « purge X » conduit le
créateur à lire X, la voir active, et ne rien faire. La forgerie devient
structurellement inoffensive — d'où l'absence de besoin de signer les dépôts
d'inbox, ce qui tombe bien : NextGraph ne l'offre pas (inbox non authentifiée,
vérification de signature non implémentée et exigeant de déchiffrer).

Le pointeur d'identité vise le profil protected existant, pas un second document
par participation : les connexions en détiennent déjà le cap. Ajouter une
connexion ne réécrit donc rien — on scelle une fois, durablement. Un champ
chiffré dans la Participation aurait exigé de re-sceller à N destinataires et de
réécrire à chaque nouvelle connexion (et n'est pas un primitif NextGraph : la
granularité de chiffrement est le document, en tout-ou-rien).

Arbitrages assumés : pas de filtrage à la lecture (Set.size est une borne haute,
exacte après purge — obsolescence acceptée pour garder la lecture en O(1)) ;
la purge incombe au créateur ; pas de description.

Point ouvert noté : Participation passe en scope public alors que la doctrine
produit la place en protected. Ce leaf décrit l'implémenté — à mettre à jour à
la graduation du brief, pas avant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 13:49:18 +02:00
Sylvain Duchesne ab077d8080 docs(concept): réserve durable sur le pseudonyme permanent de l'overlay
app-security/caveat_stable-overlay-pseudonym (nouveau) — toute référence
cap-less vers un document protected expose le `✌️` du store, identique partout
et pour toujours. BLAKE3 non inversible le rend OPAQUE, d'où la tentation de le
croire INOFFENSIF : ce sont deux choses différentes. C'est la constance qui
expose, pas la lisibilité. Un seul recoupement, une seule fois, et tout
l'historique bascule — y compris ce qui a été publié des années plus tôt.

Aucune porte de sortie, vérifié sur quatre axes : pas de rotation d'overlay,
store id généré une fois pour toutes, aucune migration de contenu, aucune forme
de référence n'évitant d'exposer l'overlay. Le renouvellement de capabilities
ne toucherait que l'inner ; l'outer y survit.

Placé en app-security et non dans le brief inscriptions : un brief se dissout à
sa graduation, la réserve doit lui survivre. Le brief n'en garde qu'un résumé
et pointe dessus. Déclencheurs élargis (anonymat, pseudonyme, traçage,
corrélation, overlay, cap-less) pour qu'elle remonte quand on s'apprête à
concevoir de l'« anonyme ».

Consigne pratique qui en découle : ne jamais présenter une action comme
« anonyme » si elle fait circuler une référence cap-less — c'est pseudonyme,
et le pseudonyme est permanent.

Dédup par `✌️` validée par le PO ; le brief le note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 12:31:55 +02:00
Sylvain Duchesne 42dbfd0c34 docs(concept): modèle d'inscription recalé + l'inbox NextGraph suit aussi les manques
Le brief inscriptions est réécrit sur le modèle reposé par le PO : tout est
clés et URLs, sans notion d'appartenance. Le participant crée une Participation
chiffrée, dépose son did (URI sans ReadCap) dans l'inbox de l'événement ; le
créateur traite l'inbox automatiquement, déduplique sans pouvoir lire, et range
la référence dans un Set porté par l'événement ; compteur = Set.size ; seules
les connexions détiennent la clé et reconnaissent la personne.

La dédup s'appuie sur un fait vérifié dans nextgraph-rs : l'overlay (segment
`✌️` d'un NURI) est STORE-scopé, jamais document-scopé. Deux Participations
d'une même personne portent donc le même `✌️`. Contrepartie actée dans le
brief : ce `✌️` est un pseudonyme stable et permanent — c'est le MÊME bit
d'information qui permet de dédupliquer sans lire et de tracer d'un événement
à l'autre ; on ne peut pas garder l'un sans l'autre.

Retiré du brief : le trilemme et la piste de dédup par vérification de
signature. Ils reposaient sur une notion de membership importée de l'état
courant du source Rust, où elle est un échafaudage inerte — erreur de méthode
désormais consignée en règle.

Règles :
- rule_capture-nextgraph-findings (nouvelle) — toute connaissance établie sur
  le fonctionnement réel de NextGraph se consigne AU MOMENT de la découverte
  dans la doc du polyfill ; distinguer VÉRIFIÉ d'INFÉRÉ ; ne jamais déduire la
  forme cible de l'état courant du source.
- rule_file-nextgraph-bugs → rule_nextgraph-inbox — l'inbox reçoit désormais
  DEUX familles : les dysfonctionnements ET les manques dont on a besoin. Une
  fiche de manque dit ce que le polyfill émule en attendant et ce qu'il faudra
  en RETIRER quand ça atterrit en amont : l'inbox devient un suivi de
  l'avancement de NextGraph, pas un simple bug-tracker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 12:07:06 +02:00
Sylvain Duchesne 5b536ff981 docs(concept): brief inscriptions par Set + règle de report des bugs NextGraph
- data-layer/brief_2026-07-20_attendance-set-model : réaligner les inscriptions
  sur la vision initiale — objet participation auto-possédé (la vérité) + Set
  curé de références cap-less sur l'événement + cap scellé aux seules
  connexions ⇒ compteur = `Set.size`, présence anonyme par défaut, personne ne
  désinscrit autrui. Inclut la revue adverse (trilemme anonyme/dédup/inviolable)
  et les verdicts du spike P0 vérifiés dans `nextgraph-rs` :
  fetch d'existence sans clé = OUI, détection de suppression sans clé = NON
  (⇒ la désinscription passe par un nudge), confidentialité = OUI.
  Statut : direction cible, PAS un pivot immédiat.
- data-layer/rule_file-nextgraph-bugs : tout dysfonctionnement NextGraph
  identifié donne lieu à une fiche dans `../../nextgraph/orm-tests/INBOX/`.
- to-discuss : alignement ReadCap/WriteCap, terminologie identité NextGraph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 11:30:12 +02:00
Sylvain Duchesne c5e627c5fc fix(participants): joindre participation→profil à travers les deux espaces d'id
Une Participation stocke son user comme principal stable
`urn:festipod:user:<clé>`, alors qu'un UserProfile a pour `id` son NURI
`did🆖`. La jointure brute `partUserIds.includes(u.id)` ne matchait donc
jamais en mode connecté → chaque participant s'affichait « inconnu ».

- `resolveParticipantUser` : match direct (espace seed demo) puis, à défaut,
  match sur `normalizeIdentifier(username)` après retrait du préfixe principal.
- `USER_PRINCIPAL_PREFIX` : source unique du préfixe, partagée par l'écriture
  (`currentUserId`) et la lecture, pour qu'elles ne divergent pas.
- EventDetailScreen : filtrer soi-même sur `currentUser?.id` (id de profil,
  même espace que `p.id`) et non sur `currentUserId` (principal).

Aussi : épingle `packageManager` pnpm (l'install passe par pnpm, cf.
rule_bun-first) — le runtime/test/build restent Bun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 11:30:00 +02:00
Sylvain Duchesne 7e65a83d42 fix(test): cucumber via l'entrée JS réelle (@cucumber/cucumber/bin) — pnpm casse le shim .bin
pnpm installe node_modules/.bin/cucumber-js comme shim shell (pas du JS) → 'node --import tsx/esm node_modules/.bin/cucumber-js' échoue. Pointer sur l'entrée JS réelle du paquet. Répare test:data et cucumber:run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-20 13:17:36 +02:00
Sylvain Duchesne 3a49376f17 test(reconnexion): repro à froid sans état local (@wip) + persistance/pause (@wip)
reconnexion-froide-sans-local = test décisif broker-vs-local (verdict LOCAL-ONLY), @wip. persistance-e2e + pause @wip. rename identifiant dans reconnexion/isolation/harness-ng.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-20 13:16:04 +02:00
Sylvain Duchesne e326bebd42 feat(auth): wallet partagé = seul mode + purge identifiant-wallet↔username
AccessGateScreen 3-branches (erreur config si pas de wallet partagé) ; renommage username→identifier de l'identité du wallet (registration, ngSession, hooks, steps auth) sans toucher UserProfile.username ; .env.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-20 13:16:04 +02:00
Sylvain Duchesne 46ed894621 feat(data): logs [app][data] identité-first + participantCount avant→après
Préfixe identité-first ; label participation ; valeur compteur avant/après écriture owner + lecture affichage. Additif.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-20 13:16:04 +02:00
Sylvain Duchesne a21d9b0735 docs(concept): durabilité écriture↔déconnexion, décision wallet-partagé-unique, rule_bun-first (install pnpm)
caveat_write-durability-across-disconnect + decision_2026-07-20 (wallet partagé = seul mode ; identifiant ≠ username profil) + amendement bun-first. Marqueurs _debt.md inclus (voyagent avec la branche, à réconcilier avant push).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-20 13:16:04 +02:00
Sylvain Duchesne f6fc3c262e chore: install via pnpm + polyfill depuis Gitea (git-https) + lien local réactif
Passe l'installation de bun à pnpm (runtime/build/test restent bun).
Dépendance prod @ng-eventually/client résolue depuis le Gitea public en
git+https (committée, reproductible via pnpm-lock.yaml). Script
link:polyfill (S2 copie-overlay + watcher) pour un lien local réactif
préservant l'instance @ng-org unique. bun (peer de bun-plugin-tailwind)
approuvé au build (pnpm.onlyBuiltDependencies) pour que
node_modules/.bin/bun soit un vrai binaire. Dockerfile: install pnpm
avec git + node dans l'image, runtime bun inchangé. Doctrine tech-stack
(deployment, stack-and-commands) mise à jour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-14 12:52:30 +02:00
Sylvain Duchesne 62693667a8 chore(data): provisionRetry → pointerGuard (résolution de compte barrière-autoritative)
Le polyfill ne fait plus de retry sur la résolution des comptes (désormais gated sur
la barrière first-State du doc-shim). L'app n'injecte plus qu'un micro-garde borné
(pointerGuard) sur la seule lecture du pointeur écrit-une-fois. Aucune mitigation de
fork/retry côté app — cette responsabilité vit entièrement dans le SDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:46:16 +02:00
Sylvain Duchesne 82004a30b0 fix(data): participantCount fiable à la connexion du propriétaire + source unique
Bug: user2 crée un événement, user1 s'inscrit et voit "1", mais user2 (créateur)
reste à 0. Recadrage (spec existante): l'exigence est "le propriétaire traite son
inbox à sa PROCHAINE CONNEXION", pas une notif live temps-réel.

Cause: le owner-materializer lisait l'inbox AVANT sa synchronisation → active=0 →
écrit 0 → mémoïse 0 → ne retraite plus.

Fix:
- Lecture inbox gated sur barrière: inbox.readSynced (ensureRepoOpen attend le 1er
  State, puis read — comme discovery.readIndex) au lieu de inbox.read. Un dépôt déjà
  synchronisé EST vu à la connexion. Pas de polling.
- Materializer déclenché directement à la connexion ([ready, ownedKey]).
- materializedCountRef ne verrouille plus un 0 prématuré (rôle = anti-boucle seul).
- Source UNIQUE du nombre = event.participantCount: le littéral participantCount:1
  de CreateEventScreen retiré (démarre à 0), l'affichage ne calcule plus de nombre
  local (ParticipantsListScreen). Le statut "Je participe" optimiste est intact.
- Logs [Attendance] sur tout le chemin dépôt→matérialisation→écriture.

Test: e2e-multibrowser "converge à la prochaine connexion" reframé + dé-@wip,
ROUGE avant / VERT après sur profil frais. Non-régression @multibrowser 4/4, @data 7/7.

Doctrine: knowledge_context-internals (caveat BUG ACTIF → CORRIGÉ), brief_2026-07-06
(cadrage "sans reload" = sur-cadrage; exigence = fiable à la connexion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:40:37 +02:00
Sylvain Duchesne 0958d70132 docs(tech-stack): caveat Firefox 151+ LNA bloque l'iframe app du broker en dev
Piège coûteux : iframe blanche + zéro log app + aucune erreur = pas un bug
Festipod, c'est Local Network Access de Firefox qui bloque le broker public
d'embarquer l'app locale. Fix navigateur (network.lna.enabled=false). HTTPS
n'y change rien ; le top-level charge quand même ; le smoke ne peut pas l'attraper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:06:08 +02:00
Sylvain Duchesne c0fd69344b feat(data): auto-seed opt-in (FESTIPOD_AUTO_SEED) + logs data lisibles; diag bug participantCount
Seed: l'auto-seed sur wallet vide est désormais OPT-IN, OFF par défaut — ne se
déclenche que si FESTIPOD_AUTO_SEED=1 (livré en dev via /festipod-config.json +
define build.ts, comme le shared-wallet). Le seed répété bloatait le wallet
(lenteurs de lecture). Seed explicite (loadTestData, tests @data) inchangé.

Logs: chaque useShapeQuery logge à la réception du set le nombre d'objets + le
type + des compteurs globaux cumulés :
  [FestipodData] set reçu: 9 objets Event (public) en 1234ms
  [FestipodData] totaux — Event: 9, Participation: 3, UserProfile: 10 (5 sets)
(polyfill docs.ts: "N rows" -> "N triple-rows" pour clarifier que ce sont des
triplets RDF, pas des objets métier.)

Diagnostic bug participantCount (NON corrigé, design-sensible): le propriétaire
d'un événement reste à participantCount=0 quand un inscrit d'un AUTRE verifier
dépose. Cause: le owner-materializer n'est re-déclenché que par ownedKey, jamais
par un push d'inbox — doc_subscribe ne délivre aucun Patch cross-session. Le
bloat de wallet MASQUAIT le bug (faux-vert). La théorie "StorageError" était
fausse. Scénario réactif @wip = test ROUGE qui documente le bug.

Doctrine: knowledge_context-internals (caveat BUG ACTIF + auto-seed opt-in),
brief_2026-07-06 (claim D.2 "prouvé vert" REFUTÉ), build-pipeline (nouvelle var).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:38:38 +02:00
Sylvain Duchesne 39b67feea0 feat(ui): spinner global près du titre Festipod + log du délai des requêtes
Chaque useShapeQuery s'enregistre dans un store module-level pendingQueries au
début de son cycle et se résout à son premier résultat (isPending→isSuccess|isError,
équivalent readPromise). HomeScreen affiche un Spinner à côté du titre "Festipod"
tant qu'au moins une requête est en attente ; il ne s'arrête que quand TOUTES ont
reçu leur premier résultat. Toute future useShapeQuery y contribue automatiquement.

À la 1re résolution, chaque cycle logge son délai :
  [FestipodData] <shape>/<scope> premier résultat en <N>ms (n=<len>)
→ le délai d'obtention des événements (Event/public) est visible nommément.

Store idempotent (Set d'ids, sûr sous StrictMode) ; cycleId mémoïsé sur
[shapeKey, scope] → re-begin sur switch d'identité, cleanup résout au démontage
(spinner jamais bloqué). Spinner = Loader2 lucide + @keyframes app-spin dans index.css.

Tests: pendingQueries.test.ts (6, dont "off seulement quand toutes résolues").
Doctrine: data-layer/knowledge_context-internals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:51:36 +02:00
Sylvain Duchesne c7e924abe7 fix(auth): porter l'identité par param d'URL (?id=), pas localStorage; renommer username→identifier
Cause racine du décalage d'identité : l'app tourne dans DEUX contextes avec DEUX
partitions de localStorage — top-level (127.0.0.1:3000 direct, barrière) et iframe
(embarquée sous nextgraph.net après le round-trip broker). Le navigateur partitionne
le storage par site top-level, donc l'identifiant saisi en top-level n'est jamais
celui que l'app connectée lit dans l'iframe (symptôme: deux valeurs divergentes).

Fix : le param d'URL ?id= devient la SOURCE DE VÉRITÉ. Le SDK redirige avec
encodeURIComponent(window.location.href) (URL app complète, query comprise), donc
un param d'URL TRAVERSE la frontière contrairement à localStorage. AuthGate écrit
?id=<identifiant> (replaceState) avant connect(); AccountContext résout par priorité
(1) ?id= puis (2) localStorage (préremplissage same-partition seulement).

Renommage username→identifier (champ useAccount, normalizeIdentifier, clé
festipod.account.identifier) — c'est un id technique d'espace, pas un username.
Le username de PROFIL (nom d'affichage) est laissé intact.

Test garde-fou @ui (identifiant-resolution.feature) : la priorité param>localStorage,
rouge si on l'inverse. Le flux de barrière étant désactivé en @e2e, ces @ui sont la
seule couche qui le garde.

Doctrine: knowledge_authentication (porteur URL + partition) + knowledge_context-internals (vocab).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:55:09 +02:00
Sylvain Duchesne 13da2d9e03 fix(auth): préremplir l'identifiant à la barrière — plus de re-saisie à l'arrivée
Symptôme (vraie app) : au retour dans Festipod, la barrière redemandait un
identifiant NU et VIDE alors qu'il était déjà choisi/stocké.

Cause racine (pas une perte de localStorage — l'identifiant survit au round-trip) :
au rechargement, AccountProvider restaure `username` depuis le store, mais
NextGraphContext repart en `disconnected`, donc AuthGate réaffiche la barrière ;
et AccessGateScreen initialisait son champ à useState('') → vide malgré le stocké.

Fix : AuthGate passe `initialIdentifier={username}` ; AccessGateScreen préremplit
le champ. L'identifiant est saisi UNE FOIS au premier accès, persisté, puis
prérempli au retour — jamais retapé.

Test garde-fou @ui (barriere-acces-identifiant.feature) : prérempli / vide au
premier accès / Entrer remonte la valeur. Rouge si on remet useState(''). Utile
car le flux de barrière est désactivé en @e2e (__FESTIPOD_ACCESS_GATE_DISABLED__),
donc invisible à cette couche. renderElement() ajouté au harness @ui pour rendre
un composant prop-driven hors registre/providers.

Doctrine: app-security/knowledge_authentication documente la saisie-unique + prérempli.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:13:50 +02:00
Sylvain Duchesne 7302936502 test(e2e): smoke @smoke garde la classe "page blanche une fois connecté"
Boote le VRAI App via le broker (hook Before @e2e existant), navigue vers
l'accueil connecté et asserte deux choses fortes : HomeScreen a réellement
monté (.app-navbar + bouton "Relayer", absents d'un spinner/bandeau broker)
ET aucune erreur runtime (pageerror/console.error) n'a été émise pendant le
boot connecté. Le World collecte désormais les pageErrors (réinitialisés par
scénario, logging existant préservé). Câblé dans `bun run validate` (run par
défaut) via @smoke and not @wip, avec nettoyage Chromium.

Preuve: un throw dans HomeScreen fait virer le smoke au rouge; sans lui, vert.
Comble le trou qui laissait passer la régression page-blanche (aucune suite
n'exécutait @e2e et aucune assertion ne gardait le rendu connecté).

Doctrine: bdd-testing/knowledge_e2e-layer documente le smoke @smoke + pageErrors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 19:03:21 +02:00
Sylvain Duchesne 4ffa055d62 test(@data): dé-poller les steps — attendre l'état réactif, plus de boucle broker
Applique rule_no-broker-polling aux steps @data : les boucles
`for (i<N){ authParticipationCount()/getEventParticipants(); sleep }` (re-lecture
broker) sont remplacées par le pattern correct — attendre que l'ÉTAT RÉACTIF se
settle (waitForFunction sur isParticipating/homeEventTitles, alimentés par le push
watchShape, PAS de lecture broker dans la boucle), PUIS UNE lecture autoritative
unique quand l'assertion vérifie la vérité broker.

Aucune assertion affaiblie. Un poll masquait un cold-read cassé en « lent-mais-vert »
→ ce pattern expose les vraies non-convergences. Vérifié : rien n'est exposé (ça
converge dans les bornes) et le baseline reste vert.

Steps : createur, reconnexion, isolation, inscription, inscription-inbox.
gate : bun run validate = ALL STEPS PASSED ; tsc propre.
Suivi : multibrowser-features.steps.ts:85 (readInboxDeposits dans un waitForFunction)
= même classe, hors scope @data, passe dédiée à faire.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:57:13 +02:00
Sylvain Duchesne 0fc8479e22 tooling(validate): nettoyage SingletonLock + @multibrowser réactif @wip (limite wallet-partagé)
- validate.ts : `cleanSingletons()` retire SingletonLock/Cookie/Socket avant
  polyfill:e2e (et à la rotation) → fiabilise polyfill:e2e (fini le faux rouge
  ProcessSingleton). @multibrowser lancé en `@multibrowser and not @wip`.
- e2e-multibrowser : le scénario réactif « Un participant apparaît réactivement »
  passe @wip, commentaire d'en-tête expliquant la LIMITE : en wallet-partagé A et B
  partagent UNE identité NG → B voit l'événement de A comme possédé → son
  owner-materializer écrit le doc de A → StorageError. PAS un bug produit (prod =
  wallets distincts). Vrai fix = isolation distinct-wallets (chantier T02.d/g).
  Réserve : ce scénario était vert (517045c) ; à re-vérifier lors de l'isolation
  distinct-wallets (peut masquer une interaction phase-B).

Baseline validate visé : vert sauf ce @multibrowser documenté.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:17:18 +02:00
Sylvain Duchesne 7dab6e44e2 tooling(validate): sous-ensemble @data clé rapide par défaut + flag --full + budgets
`bun run validate` finissait en timeout (@data complet >14min). Désormais : par
défaut un SOUS-ENSEMBLE CLÉ de 8 scénarios @data (les couvertures des bugs terrain :
inscription, désinscription, isolation, reconnexion, créateur, compteur, auth×2) en
une invocation → finit en ~8m30. Flag `--full` pour toute la suite @data (budget
élargi 35min). Rotation profil avant @data/@multibrowser, matrice + exit non-zéro.

Baseline actuel : 8/8 @data clé VERTS (les fixes watchShape/optimiste/reconnexion/
anti-fork tiennent) ; polyfill:unit 123 ; rouges = SingletonLock (infra) + un
@multibrowser (limite wallet-partagé A/B).

Note : gate pre-push rapide (tsc+build+polyfill unit) ajouté dans .git/hooks/pre-push
(local, non versionné — pour partage : script tracké + install, suivi).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:06:40 +02:00
Sylvain Duchesne 91ee3567aa test: reconnexion « relit ses propres données » — VERT, retrait de @wip (bug résolu)
Le bug de reconnexion (page fraîche même identité relit vide) est RÉSOLU côté lib
(résolution de compte déterministe + dé-dup des ensureAccount concurrents). Le
scénario passe 2/2 sur broker réel → retrait de @wip (redevient @data bloquant),
en-tête corrigé. `storeRegistry.ts` : config `provisionRetry` (retry anti-fork).

Non-régression vérifiée : isolation + inscription verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:30:13 +02:00
Sylvain Duchesne f366ee29a7 doctrine(data-layer): context-internals — lecture via watchShape + overlay optimiste + auto-seed sur isSuccess
Rafraîchit les sections périmées : la lecture passe par `useShapeQuery`/`watchShape`
(plus readEntities/subscribeDocs/bumpRead/relist) ; visibilité immédiate des
mutations par overlay optimiste (plus registerDoc) ; auto-seed gardé sur `isSuccess`
(plus le setTimeout 3s qui causait le re-seed à chaque reconnexion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:19:45 +02:00
Sylvain Duchesne 04a2de0b17 fix(app): visibilité immédiate des mutations — overlay optimiste sur watchShape
Répare 2 régressions de `38266d9` (les lectures 100% watchShape avaient perdu la
visibilité immédiate post-mutation, ce que faisait `registerDoc`) : après
`createEvent` l'événement n'apparaissait qu'après le push broker ; après
`leaveEvent` le partant restait listé jusqu'au push.

Fix = mise à jour OPTIMISTE (pattern mutations useQuery ; PAS de polling) dans
`useNgData` : overlay `pendingAddEvents`/`pendingAddParticipations`/
`pendingRemoveIds`. État exposé = merge(réactif, adds) moins removes, dédupé par id.
Réconciliation auto : un add dont l'id apparaît dans le réactif est retiré ; un
remove dont l'id disparaît du réactif est retiré → auto-nettoyage au push, jamais
de poll. Vidé au changement d'identité. Pas de registerDoc/readModel réintroduit.

Trouvé PAR `bun run validate` + chasse à la régression — « l'agent trouve les
problèmes sans test manuel ».

gate : tsc propre, build OK. 6 scénarios @data verts (créateur + désinscription
régressés → verts ; inscription/isolation/compteur → verts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:18:34 +02:00
Sylvain Duchesne 9e62bdea53 tooling: commande bun run validate — validation complète 2 niveaux
Fondation « l'agent valide tout d'un coup » (audit couverture, levier C1). Enchaîne
et agrège : polyfill unit + polyfill e2e réel + Festipod @data + @multibrowser
(profils frais/rotation), matrice finale + exit non-zéro si rouge, passe @wip
informative.

Baseline établi (premier run réel) : polyfill unit 120 , polyfill e2e 42/42 
(broker réel), @multibrowser 1 rouge (limite wallet-partagé A/B), @data 4 rouges
dont 2 régressions phase B confirmées. → a trouvé les problèmes tout seul.

À TUNER (suivi) : le budget @data (~14min) est trop court → la suite complète est
tuée par timeout ; augmenter le budget OU exécuter un sous-ensemble clé rapide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 01:04:57 +02:00
Sylvain Duchesne 38266d96f8 refactor(app): l'app lit via watchShape (useShapeQuery), plus de machinerie bespoke
Phase B — FestipodDataContext lit désormais via la surface SDK `watchShape`
(binding `useSyncExternalStore` dans `useShapeQuery`) + adaptateurs Fp
(`shapeAdapters.ts`), au lieu de sa machinerie maison. Applique
rule_app-uses-sdk-surface-only : l'app ne consomme que la surface SDK.

Supprimé : `readEntities.ts`, `subscribeDocs`+`bumpRead`+`readTick`+`readDocKey`,
le listing manuel (`publicDocs`/`protectedDocs`/`registerDoc` pour la lecture,
`readDiscoveredEvents`), et les commentaires raisonnant sur le hang ORM. Gardé
découplé : `listMyEntityDocs(owner,'public')` → `ownedEventIds` pour le seul
matérialiseur propriétaire.

Auto-seed : chronomètre 3 s → gate `isSuccess` (seed uniquement si synchronisé ET
vide) — fix du re-seed « First time… » au 3ᵉ connect. Mode démo inchangé.

Non-régression VÉRIFIÉE (broker réel, wallet frais) : inscription (1 passed),
isolation « identité fraîche ne voit pas » (re-run local, 5 steps passed), compteur
dérivé/Q4 (1 passed). tsc propre, build OK.

Résiduel PRÉ-EXISTANT (pas causé par ce refactor, vérifié par stash sur baseline) :
- reconnexion « relit ses propres données » → RE-@wip : défaut cold-read de l'index
  de scope PUBLIC côté lib (une page fraîche relit vide) — prochaine cible.
- un @AUTH « données pas rechargées » (timing loadFire-and-forget vs step 30 s).

Doctrine : rule_app-uses-sdk-surface-only « déviation résolue ».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:47:03 +02:00
Sylvain Duchesne 2295af610a doctrine+dev: règle « app = surface SDK seule » + access-log par défaut
- rule_app-uses-sdk-surface-only : l'app se comporte comme si NextGraph était fini
  et sans défaut ; elle lit via `useShape` (scopé wallet virtuel, fourni par le
  polyfill), jamais via des internes (readModel/subscribeDoc) ni en raisonnant sur
  un problème NextGraph. Raison d'être du polyfill = le WALLET VIRTUEL (pas le hang
  ORM, qui n'est qu'un détail interne). Cible : `useShape` polyfill à la forme
  TanStack useQuery (data + isPending/isSuccess…), en anticipation de la mise à jour
  prévue de useShape par NextGraph — distingue nativement sync-en-cours de vide.
  Déviation actuelle notée : readEntities/subscribeDocs/bumpRead côté app.
- ngSession : access-log ON par défaut (le toggle opt-in était fragile), opt-out via
  localStorage festipod.debug.accessLog=0 ; ligne de diagnostic au démarrage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 22:36:29 +02:00
Sylvain Duchesne 4c80ada3de doctrine(bdd-testing): remplacer le caveat poll par la règle « ne jamais poller »
L'ancien caveat_poll-broker-reads érigeait à tort le POLLING en pratique de test.
Remarque utilisateur : le polling est un anti-pattern dans le contexte NextGraph
(par abonnement). Remplacé par rule_no-broker-polling : attendre le push réactif /
la barrière du 1er State ; ne JAMAIS re-interroger le broker en boucle. Fallback
pragmatique admis : un intervalle court qui OBSERVE l'état réactif déjà mis à jour
(pas une re-lecture broker) — au plus près de l'utilisateur qui attend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:20:29 +02:00
Sylvain Duchesne 517045c257 test(@multibrowser): le compteur converge AUSSI côté inscrit B (Q4) + caveat polling
Q4 — le scénario réactif ne vérifiait la convergence de participantCount que du
POV du PROPRIÉTAIRE A. Ajout des assertions symétriques côté INSCRIT B : après que
B rejoint, B voit le compteur passer à 1 réactivement (sans reload) ; après
désinscription, il revient à 0 côté B. Le doc public mis à jour par A (seul
matérialiseur) se propage via le broker jusqu'au doc_subscribe de B. Ferme « A et
B ont-ils tous les deux le compteur incrémenté ? » — oui. Vert wallet frais (16 steps).

Doctrine : nouveau caveat bdd-testing/caveat_poll-broker-reads — asserter les
lectures broker en POLLING borné (lag de sync ~1s), jamais en one-shot ; vaut pour
les lectures à froid (reconnexion) et la propagation réactive (compteur
cross-navigateur). Consolide la doc-debt des features touchées cette session.

Non couvert (suivi) : observateur TIERS (découverte publique, flaky).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:09:21 +02:00
Sylvain Duchesne c07150cb27 test: reconnexion même identité — VERT (défaut résolu), retrait de @wip
Le scénario passe désormais (broker réel, 5 steps) : une page fraîche pour la
MÊME identité, sur le wallet persistant (nouveau login → session verifier
fraîche), relit son événement sur l'accueil, sa participation et un count
autoritatif de 1.

Résolution mesurée (investigation opus répétée) : le read à froid de l'index de
scope public est un LAG DE SYNC borné, pas un gap permanent — le doc se liste dès
la 1re tentative, l'accueil converge en ~1 s. Il fallait deux choses :
- les fix lib open-repo + anti-fork de compte (branche fix/session-rehydration-
  on-login, dans node_modules) ;
- et surtout que le TEST attende la convergence : les 3 assertions de la page
  fraîche POLLENT maintenant (jusqu'à ~15 s) au lieu de lire une seule fois — un
  read unique course la fenêtre de premier-open/sync et flakait. L'UI réelle est
  réactive, donc ce polling reflète le vrai comportement utilisateur.

Devient un test @data permanent (retrait de @wip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:14:22 +02:00
Sylvain Duchesne 005c052bc6 test(@wip): reconnexion même identité — documente le défaut de relecture à froid
Scénario @data @wip (exclu du run par défaut) qui documente un défaut RÉEL non
encore corrigé : une PAGE FRAÎCHE pour la MÊME identité, sur le même wallet
persistant (nouveau login broker → session verifier fraîche), relit VIDE ses
propres données (accueil vide, isParticipating=false, count=0).

Mesuré au niveau app sur broker réel. Le fix lib `open-repo` (branche
fix/session-rehydration-on-login, non mergée) fait remonter le PROTECTED
(participation) au cold-start mais PAS l'accueil PUBLIC : `readScopeIndex` de
l'index de scope public rend 0 — observé même côté écrivain même-session — alors
que le code d'index de la lib est prouvé scope-symétrique. Cause exacte encore à
mesurer sous broker (l'hypothèse « mauvais graphe » est déjà réfutée en amont).

Reste @wip tant que le fix n'est pas complet et validé.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:45:51 +02:00
Sylvain Duchesne c869c56a17 Diagnostic: activer l'access-log du SDK depuis l'app (toggle runtime)
Câble l'option `debugAccessLog` du SDK @ng-eventually/client dans le point
d'injection `ngSession.configure(...)`, pilotée par un toggle runtime sans
rebuild : `localStorage['festipod.debug.accessLog']==='1'` (ou
`window.__FESTIPOD_ACCESS_LOG__`), off par défaut.

But : VOIR la fuite d'isolation dans l'app RÉELLE. Le harness e2e ne peut pas la
reproduire (les lectures cross-invocation n'y rendent rien), donc on instrumente
l'app : chaque read/write du SDK s'imprime préfixé par l'identité active
(`[urn:festipod:user:<id>] READ <nuri> → N rows`), rendant visible le moment où
un doc est lu sous la mauvaise identité.

tsc propre, build OK. Outillage polyfill-era.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:22:40 +02:00
Sylvain Duchesne 65bd67cc20 Isolation deux-identités: test permanent + le créateur ne participe plus
Deux corrections produit/tests demandées, empiriquement validées au broker réel.

1. Créateur ≠ hôte (décision produit). Il n'y a PAS de notion d'hôte : un
   événement est public, simplement signalé par le créateur, qui n'est PAS
   obligé de participer. `createEvent` n'écrit plus de participation-hôte et
   `participantCount` démarre à 0 ; le matérialiseur du propriétaire dérive
   `participantCount = |inscriptions actives|` (plus de base « +1 hôte »).

2. Isolation deux-identités : le trou réel était l'ABSENCE d'un test de
   régression, pas un bug de code actif. Reproduction empirique (DIAG instrumenté,
   retiré) : la fuite n'apparaît QUE si le reset `useEffect([username])` est
   désactivé ET les caps vides (docs persistés d'une session antérieure sur wallet
   gonflé) — le reset en place la neutralise. La sighting live venait d'un état
   wallet pré-fix + identifiant réutilisé. Ajout du test permanent manquant :
   - isolation-deux-identites.feature (@data) : A crée+rejoint E, une identité
     fraîche B sur le même wallet ne voit E ni sur son accueil, ni via
     isParticipating(E,B), et ne lit aucune participation portant le principal de A.
   - us-13 : « Le créateur ne participe pas automatiquement » (count 0,
     isParticipating false autoritatif, puis join→1, leave→0).

Harness: 4 helpers permanents (switchIdentity, currentIdentifier, homeEventTitles,
currentParticipations) pour piloter/observer l'identité en test.
Scénarios @multibrowser/us-7 réalignés (compteur 0→1 au lieu de 1→2).
Doctrine mise à jour (context-internals, actors-and-concepts).

Gates: build OK, tsc propre, @data verts (inscription, désinscription,
idempotence, compteur dérivé, auth ×4), lib @ng-eventually/client non touchée.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:55:42 +02:00
Sylvain Duchesne 0f164300f0 refactor(data): canonical event-id matching for the owner-materializer (defensive)
Guard the Option-B owner-materializer against overlay-form drift: match inbox
deposits to owned events on the CANONICAL base repo id (canonicalEventId strips any
✌️<overlay> suffix), applied at the matching boundary in materializeAttendance /
readRegistrationNotifications and to dedup ownedEventIds (ownedKey). The count is
still WRITTEN on the real owned NURI — a stripped id is never a write/anchor target.

Honest framing: this is DEFENSIVE, not a fix for an active bug. On the current tree
create-time, listMyEntityDocs and the read @id already carry the identical NURI
(readUnion pins the subject to the input NURI, 63ecfee) — verified: the count
converges for an event owned via listMyEntityDocs. A prior investigation's 'never
matches' reading was the seeded-but-not-owned artifact (a prior-run identity owned
the seed → reached via discovery, not ownedEventIds — correct behavior).

Un-@wip the @data convergence scenario (asserts the just-joined uid enters the
owner-derived active set — deterministic despite shared-inbox accumulation); it
now passes. Fix authParticipationCount already landed separately. Doctrine:
knowledge_context-internals (canonical id-form invariant). Build + tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 15:14:25 +02:00
Sylvain Duchesne 767a18e98c test(@data): align us-7 inscription tests to Option B; fix authParticipationCount
The three us-7 count assertions encoded the OLD increment model (participantCount =
baseline ± 1 via a fictional 'au départ N' step). Under Option B the count is
owner-derived, not baseline±1 and not the joiner's to write, so those lines were
false. Drop them; keep the real @data contract (join persists + participant + in
list; leave is authoritative + gone). Move count convergence to a @data @wip
scenario with an inline rationale (single-session can't derive the absolute count —
shared-inbox accumulation + create-vs-read NURI-form; the @multibrowser reactive
scenario is the real validation).

Fix a harness bug: authParticipationCount enumerated protected docs via the
all-accounts listEntityDocs (returned 0 for a fresh per-scenario virtual account —
a false 0); use the bounded listMyEntityDocs(currentUser,'protected') (the same
read-by-need path the app's idempotence check uses), and poll to absorb index lag.

Full @data suite green: 18 scenarios / 89 steps. Désinscription contract untouched
(caveat_participation-deletion). Build + tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:23:25 +02:00
Sylvain Duchesne 22487ed575 docs(data): drop the phantom-graph justification in entity/registration writes
Comments in entityWrites.ts (writeEntity/updateEntityField) and registration.ts
asserted an explicit GRAPH <plainNuri> writes a distinct named graph the anchored
read never sees (entity 'disappears'). The lib e2e harness disproves it on the
current broker; the '0 entities' symptom was the wallet-bloat hang. Replace with
the minimal 'no-GRAPH is the canonical anchored-default-graph shape; SDK graph
details live in @ng-eventually/client'. No NextGraph internals in the app repo
(boundary); no behavior change (the safe no-GRAPH shape stays).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 09:39:09 +02:00
Sylvain Duchesne cd2a45c254 feat(data): participantCount via Option B (deposit + owner materialization)
Remove the write-isolation violation: joinEvent/leaveEvent no longer write
participantCount on the event doc (a non-owner writing the owner's public doc —
illegitimate in NextGraph). The joiner/leaver only write their own protected
participation doc and DEPOSIT a marker into the event inbox (depositRegistration /
depositLeave).

The event OWNER's session materializes: it subscribes (inbox.watch, doc_subscribe —
no polling) to the inboxes of its OWNED events (ownedEventIds), and on each deposit
recomputes participantCount on its OWN event doc. The count is DERIVED, not
incremented: materializeAttendance derives the SET of distinct active registrations
(new-participant deduped by uid, MINUS leave-participant by regUid/fallback
eventId+userId), count = 1 (host self) + |active set|. A pure function of the inbox
→ broker re-syncs converge, never double-count nor resurrect (idempotent); the write
is guarded (only on change → no loop). Authoritative deleteParticipation preserved
(caveat_participation-deletion).

Because the owner writes its own PUBLIC event doc and every session subscribes to it
(P3), the count round-trips reactively to all — no reload. Owner-offline = eventual
(V1; a future @ng-eventually/service materializes on the owner's behalf).

Real 2-browser e2e (e2e-multibrowser.feature): B registers → A materializes → count
1→2 reactively (no reload) + unknown participant; B leaves → count →1. 14/14 green.
Gates: @data auth 4/4, @data isolation 4/4, build + tsc clean. Doctrine:
knowledge_context-internals (Option B section).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 09:24:55 +02:00
Sylvain Duchesne e62a17e5a2 docs(data-layer): correct the graph-round-trip claim (it was the bloat hang)
The lib e2e harness proves that on the current broker an anchored
INSERT DATA { GRAPH <plainNuri> {…} } DOES round-trip — the earlier 'explicit GRAPH
writes a phantom named graph the read never sees' claim was false; the '0 entity'
symptom was actually the wallet-bloat hang (caveat_wallet-bloat-hang), not a graph
mismatch. Reframe the no-GRAPH default-graph rule as a simplicity/safety convention,
not a round-trip necessity. Lib/app inline comments asserting the phantom-graph
claim remain to reconcile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:56:51 +02:00
Sylvain Duchesne 4e96659bd7 feat(data): reactive cross-session reads (doc_subscribe), + real 2-browser e2e
Wire the app read path to the lib's per-doc reactive subscription so a change made
in ANOTHER session propagates without a reload or local action:
- useNgData subscribes the by-need set via subscribeDocs(allReadDocs, bumpRead) —
  one doc_subscribe per NURI, per-doc error isolation (never the ORM fan-out). Any
  patch (own write or broker-synced from a remote peer) re-runs readUnion.
- Reactive discovery: watchDiscoveredEvents(relist) subscribes the global index →
  a new public event from another session enters the read set (and gets its own sub).
- Loop-safe: the sub effect is keyed on a stable sorted-NURI key (readDocKey); a
  fire→bumpRead→read never changes the doc set, so no re-subscribe loop. Identity
  switch empties the set → clean unsubscribe → rebuild → re-subscribe (no leak).
- readUnion stays the one-shot tolerant reader; subscriptions only trigger re-reads.

Real 2-browser e2e (e2e-multibrowser.feature): B registers → A's EventDetailScreen
shows participantCount 1→2 and an 'unknown' participant WITHOUT A reloading, via A's
doc_subscribe on the public event doc (event-driven). Isolated run 12/12 green.

Count mechanism unchanged (P4/Option-B is next); the joiner still writes the public
event doc's participantCount — which is exactly what the observer sees change live.
Gates: @data auth 4/4, @data isolation 4/4, build + tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:40:43 +02:00
Sylvain Duchesne 84bc87d13c docs(data-layer): brief update — P1/P2 done, owner-offline decided, hooks
P1/P2 (lib subscribeDoc + drop polling) landed in @ng-eventually/client c0498a6.
Owner-offline count = eventual for V1, a future @ng-eventually/service takes over
when the owner is disconnected. Reactive hooks are useShape + useDiscrete (no
useQuery); the union-of-N-docs read stays subscribeDocs + re-readUnion (useShape
fan-out hangs). Next: P3 (wire per-doc subscription into the app read path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:07:14 +02:00
Sylvain Duchesne af58667b4f docs(data-layer): brief — reactive reads + option-B attendance
Implementation design brief (grounded in current code): reactive reads via a typed
per-doc doc_subscribe wrapper (no polling, no ORM fan-out -> avoids the historical
hang); participant count via option B (joiner deposits into the event inbox, the
event owner materializes into its own event doc's count; option A ruled out --
non-owner append is impossible in NextGraph). Connection-gated identity (else
'inconnu'). Test plan: polyfill low-level doc_subscribe + real 2-browser e2e
reactivity. Phased P1-P6. Open product question: owner-offline eventual count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:13:54 +02:00
Sylvain Duchesne 01d65238ce docs(data-layer): point to the SDK reference for the reactive read hook
Add a pointer in knowledge_nextgraph-stack: the SDK's recommended read is its
reactive useShape hook (subscribe/push, one-shot is the exception); full contract
in @ng-eventually/client packages/client/docs/sdk-reference.md. No NextGraph
internals copied into the app repo — just the pointer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:02:47 +02:00
Sylvain Duchesne 6ceec5e161 fix(data): reset the read set + emulated caps on identity switch (isolation)
The shared-wallet stopgap keeps ONE React tree across a faux-logout + re-login
under a different identifier (AccountContext.login only rewrites a localStorage
id; AuthGate never remounts, no page reload). FestipodDataContext's by-need read
set accumulates the current identity's scope docs and was never reset on identity
change, so the PREVIOUS identity's PROTECTED docs (its participations) survived in
the new identity's read set and leaked through the union read — the in-memory cap
gate can't filter a doc it doesn't govern this session. Symptom: user B saw A's
participation, and A's event surfaced on B's home (home = getUserEvents(currentUserId)).

Treat every identifier change as a fresh session: a ref-guarded useEffect([username])
clears publicDocs/protectedDocs, resetCaps(), resetRegistryCache(), then bumps the
read tick so the listing effect rebuilds the set bounded to the new identity.
Isolation stays per-document/emulated; the reset only drops cross-identity carryover.
Documented in knowledge_context-internals.

Validated (@data, real broker): after an A→B switch, B does not participate and
does not read A's participation; protected-isolation/read-filter/auth scenarios pass.
tsc + build green; lib untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:29:18 +02:00
Sylvain Duchesne 3dfd549af3 docs(tech-stack): correct the dev shared-wallet command (real e2e password + file)
A dummy FESTIPOD_SHARED_WALLET_PASSWORD=1 only makes the screen appear; the import
fails because the displayed password must match the imported .ngw. Document the
working invocation with the real e2e wallet (festipod-e2e-tests) + its file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:52:45 +02:00
Sylvain Duchesne 17543f04c3 fix(auth): land on home after gate entry; align the @humain e2e to the identifier flow
Removing ConnexionScreen dropped its post-login navigate('/home'). Since the
identifier is now entered at the barrier (before the broker round-trip), on return
the app can load at '/' (WelcomeScreen) with a session already open. AuthGate now
redirects welcome→/home once connected AND identified (gate-disabled paths, i.e.
@e2e/@data harness, are exempt).

Update the @humain assisted-import e2e (the real staging flow, the coverage for
this page) to the new UX: the tester types an identifier then clicks « Entrer »
(one act), and lands directly on home — the 'choisir un nom d'utilisateur'
(ConnexionScreen) steps are removed. Step bindings verified; tsc + build green.
Doctrine: knowledge_multibrowser-harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:34:01 +02:00
Sylvain Duchesne 25b1c033d9 fix(auth): show the shared-wallet flow in dev; hide re-import when already connected
The access barrier's shared-wallet steps are gated on hasSharedWallet(), which
reads a global set only by build.ts's compile-time `define`. The src-served paths
(bun run dev AND bun run start) bundle index.html via Bun's HTML import, which
applies no define and inlines neither `process.env` nor `bun --define` (verified) —
so FESTIPOD_SHARED_WALLET_PASSWORD passed to `bun run dev` never reached the
frontend, and the barrier showed the identifier-only variant.

Expose the config at runtime instead: src/index.ts serves /festipod-config.json
(+ /shared-wallet.ngw), and the entry (frontend.tsx) fetches it, sets the global,
then dynamically imports App so sharedWallet.ts reads it on eval. In a build.ts
bundle the value is inlined via define, so the fetch is skipped (NODE_ENV).
Verified in a headless browser: FESTIPOD_SHARED_WALLET_PASSWORD=1 bun run dev now
renders the download + import steps AND the identifier field, no console errors.

Also: only show the download/import steps when status !== 'connected' — after a
faux-logout the wallet is still open, so re-import must not be offered (just the
identifier). Documents the build-define-vs-runtime-config pitfall in tech-stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:23:20 +02:00
Sylvain Duchesne e951eaaf96 feat(auth)+refactor(app): identifier at the access barrier; adopt the lib fidelity refactor
Consumer-side of the @ng-eventually/client fidelity pass, plus the identifier UX:

- Identity: the user types an IDENTIFIER at the access barrier (AccessGateScreen),
  in the same act that opens the shared wallet — the separate 'pick a username'
  screen (ConnexionScreen) is removed. The identifier is a technical id (a pseudo
  in practice, not a Festipod username), normalized (trim, @-stripped, lowercased)
  and persisted before the broker redirect, then handed to the SDK as the identity.
  AccountContext keeps its API but its stored value is now this normalized id.
- Relationship/connections are app-owned: new src/shared/utils/connections.ts holds
  the bilateral registry and maps each link to the SDK's directed grantRead(doc,
  grantee); the lib no longer carries a connection concept. Rewired FestipodData
  and the @data harness to it.
- Login removed: accounts use the SDK's IdentityStore (set/clear/get); no faux
  login/logout framing in the SDK boundary.

Doctrine reconciled: app-security (knowledge_authentication flow, knowledge_trust-model
directed grants, decision_2026-07-06_identifier-at-access-barrier), data-layer
(knowledge_context-internals: stable id principal + single-seed), app-architecture
(knowledge_screens auth inventory), bdd-testing (caveat_wallet-bloat-hang).

App gates: tsc no new errors, build OK. @data path unaffected (harness bypasses the
gate and sets identity directly; login() is not on that path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:52:40 +02:00