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
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
- 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
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
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
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
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Multiple compounding defects kept the connected @data read at 0 entities:
- writeEntity/updateEntityField and registration helpers wrote into an explicit
GRAPH <plainNuri> named graph, invisible to the anchored default-graph read
(read-model.readDoc) after the read switched to per-doc anchored. Drop the
wrapper so writes land in the repo's default graph (matches the read).
- Seed entities are now owned by the CURRENT account, so protected seed docs
(user profiles) pass the per-document ReadCap gate and round-trip.
- Suppress the double seed (explicit loadTestData + 3s dev auto-seed) and add a
re-list signal so freshly-seeded protected docs enter the read set.
- @data step awaits the seed result and waits for events AND users > 0.
Documents the anchored-default-graph write pitfall in rule_document-per-entity.
Validated: connexion-nextgraph.feature @data = 4 scenarios / 13 steps green.
NB: the shared test wallet's private store bloats across runs and makes anchored
queries hang (>15s); a fresh .playwright-profile restores ~1.5s — durable wallet
hygiene is a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- @data Before hook sets a UNIQUE virtual-wallet id (username) per scenario so each
scenario starts on a fresh, empty virtual wallet — isolation without touching the
physical wallet; "le portefeuille est vide" is now a fast check, not a full scan.
resetDataState / clearWallet fan-out dropped.
- bootstrapWallet now submits each seeded PUBLIC event to the discovery index
(mirrors the product createEvent), so a fresh virtual wallet can see seeded events
through discovery rather than as its own docs.
Note: @data still red — seeded/published events do not surface in the discovery
read (submit→readIndex round-trip against the real broker), and some publish steps
time out. The 75s ORM hang is gone; this is a distinct discovery-index integration
issue, still under diagnosis.
Read each by-need entity document with its own anchored query (bounded to the
current account's virtual wallet), never an anchorless scan of the physical shared
wallet. The 75s ORM hang stays gone; a non-empty PHYSICAL wallet now costs nothing
(never scanned). Removed the throwaway anchorless-union probe.
Known remaining (test-infra, not the product): the @data suite still times out
because THIS test account's VIRTUAL wallet is bloated (hundreds of docs
accumulated across this session's many runs) → per-doc reads are O(my docs), and
`clearWallet` still enumerates all accounts. Needs per-scenario test isolation
(fresh/small virtual wallet) + a virtual-wallet-scoped clear to validate green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the reactive-ORM per-entity fan-out read (which HUNG 75s: orm_start_graph
opened every scope graph and RepoNotFound on any fresh/unsynced doc aborted the
subscription) with the read model:
- readEntities.ts → lib readUnion: resolve the by-need doc set (my own scope docs
via listMyEntityDocs + public events via the discovery index — NOT all-accounts
fan-out), then ONE anchorless union sparql_query (GRAPH ?g, VALUES-pinned). Map
to app types. Re-query on a change signal (no reactive union query).
- countUserParticipations no longer fans out over all accounts (own docs only).
- await loadTestData in the seed step; deleted orphaned useShapeWithDefaults;
removed the old multistore-stopgap fan-out scenarios; added the read-model-probe.
- Doctrine: rule_document-per-entity read half + _overview rewritten to the union
model (write half unchanged).
Result: the 75s ORM hang is ELIMINATED (0 hangs; build/tsc/lib-93-tests green;
boundary clean). @data is NOT yet fully green: remaining failures are 90s step
timeouts in the test-harness broker data ops (clearWallet / runUnionProbe / seed)
this run — a harness/broker-op issue, not the read path. To finish separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@data oscillated 15-20/21 because the persistent test wallet accumulated data
across scenarios, growing the read fan-out. Add a cheap per-scenario reset
(resetDataState): a single SPARQL DELETE on the shim anchor graph clears the
account records, so allAccounts() collapses and the fan-out is bounded to what
the current scenario re-provisions (accounts recreated lazily). O(1) on one
graph — not a fan-out delete (which saturated the browser before). Called in the
@data Before hook, time-boxed so it can't starve the broker login budget.
Test-infra only — product model, boundary and app read path untouched.
Note: not yet re-measured to stable-green — the broker was degraded during the
bounded validation window (DNS/timeout flakiness). To re-measure when the broker
is stable. knowledge_data-layer-broker updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-document isolation refactor (one doc per entity) broke every @data
round-trip against the real broker (0 events readable) — fake-ng unit tests
missed it. Root causes + fixes:
- ngSet.add cannot write to an empty subscription scope ("Set is readonly
because scope is empty") → write each entity DIRECTLY into its own document via
SPARQL (new data/entityWrites.ts: writeEntity/updateEntityField), typing each
field with the correct RDF term per the SHEX shape (else the ORM drops the
entity on read). Reactive set stays read-only; the doc NURI is registered into
useShape({graphs}) for reactive reads.
- Current principal made STABLE and username-derived (urn:festipod:user:<name>),
available immediately at login and invariant — so a Participation's mandatory
fp:user is never empty and identity/cap-owner/connections all key on the same
value.
- Discovery deposits AS the current identity (harness sets current user first).
- Idempotence/deregistration checks made authoritative against the broker;
participantCount persisted via SPARQL. rule_document-per-entity enriched with
these write/read + stable-principal lessons.
Round-trip restored (seed readable, inscription+notif, persistent deregistration,
public discovery all pass in isolation). NOT yet stably green as a full suite:
@data oscillates 15–20/21 — residual failures are environmental (participation-
read fan-out lag on an accumulating persistent test wallet), same class as the
Chromium saturation; not a logic bug. Durable fix (follow-up): non-fan-out
materialized read + per-scenario test-wallet isolation. app build+tsc + lib 89
tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Festipod persists each entity as its own document (via the SDK), placed in its
scope. The document is the SDK's unit of sharing/permission, so per-document
isolation (private→owner, protected→owner+connections, public→all) is only
possible when each entity has its own document. Writing several entities into a
store-level document defeats per-scope isolation. Framed as SDK usage; the SDK
owns enforcement (app-security/knowledge_trust-model).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enforce the boundary in code-comments and doctrine (adversarial-review cleanup):
- App comments in the data plane no longer narrate the SDK's internals: "emulated
curator"→"the inbox read", "fan-out"→"discovered", removed store-placement
reasoning and "polyfill/shim/mono-store" wording (FestipodDataContext,
registration, storeRegistry, ngSession, AccountContext, isolation, sharedWallet,
AccessGateScreen). Executable logic unchanged.
- Removed dangling references to the dissolved `nextgraph-platform` concept and
`brief_2026-06-15_shared-wallet-shim` from app code.
- knowledge_nextgraph-stack: dropped "mécanique d'émulation" from the boundary note.
- Settled and deleted all concept _debt.md (confirmatory; target leaves clean).
(Test-infra under workshop/ + generated features.ts still carry some simulation
vocabulary — parked as a separate below-SDK decision.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On creating a public event, Festipod submits it to the discovery index (an SDK
call); the discovery screen reads the index instead of enumerating accounts. The
app knows nothing of the index's owner, inbox, or materialization — it treats the
lib as a finished SDK whose discovery is a global index. No store ids.
Unit-validated in the lib (79 tests). @data broker validation deferred: the
NextGraph broker (nextgraph.net/eu) was unreachable at run time — to be re-run
in T03.d once the broker recovers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Festipod performs the domain acts that make isolation real: AccountContext
declares the current identity at login/change; FestipodDataContext declares its
connections (friendships) to the data SDK. Reads then discriminate by scope
through the SDK (private→owner, protected→owner+connections, public→all) — no
app-side filtering, no store ids, no awareness that isolation is emulated. New
@data scenario proves an unconnected account can't read another's protected
entity but can after connecting; public stays visible. @data 21/21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Festipod now treats @ng-eventually/client as a finished NextGraph SDK: the app
decides only each entity's logical scope (events/PdR public, profiles/
participations protected, settings private) and calls the lib by scope. The old
mono-store default and the FESTIPOD_MULTISTORE path collapse into ONE scope path.
Removed every physical-store leak from the app data-plane (ngGraph, registration,
FestipodDataContext, NextGraphContext, useShapeWithDefaults): no more
did🆖${store_id} construction. The session is handed to the lib only at the
sanctioned injection point (ngSession/configureStoreRegistry). Product behavior
unchanged. @data 20/20; build + tsc clean.
(_debt.md included; the T03.e doctrine pass settles accumulated doc-debt.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enforce the project boundary: Festipod is written as if NextGraph were a mature,
finished SDK; @ng-eventually/client IS that SDK. NO current-NextGraph-state,
simulation, polyfill, shim, mono-store, store-id or broker-internal knowledge
remains in this repo — it now lives in the @ng-eventually/client repo.
- Dissolved the `nextgraph-platform` concept entirely (12 leaves — all
current-state/simulation, now in the lib's docs/). Rescued the genuine domain
parts into functional-domain/knowledge_data-scopes-and-discovery.md (which
entity → which scope; product-level discovery/notification intent), framed as
SDK usage with no mechanism.
- data-layer re-anchored to "how Festipod persists via the SDK": stripped
mono-store/private_store_id/RepoNotFound/DataCloneError/FESTIPOD_MULTISTORE.
Deleted the current-SDK compensation leaves (private-store-scope, multistore,
the 2026-03-17 ADRs, conditional-ng-init). Kept/reworded the domain + app
leaves; caveat_participation-deletion reduced to the domain contract.
- app-security reworded (isolation delegated to the SDK; app trusts it).
- AGENTS.md: dropped the nextgraph-platform row, reworded data-layer/
functional-domain/app-security, added the "Frontière SDK NextGraph" note.
- Fixed dangling [[links]]; concept lint clean (43 leaves).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- data-layer/caveat_multistore-is-multi-document (new): the recurring store vs
document confusion. Two axes — (A) which native store, (B) documents within a
store. FESTIPOD_MULTISTORE toggles axis B (multi-document), not multi-store.
Isolation (ReadCap) is per-document. As of T02.h the default path writes
shareable entities to the real protected store (axis A, step 1).
- rule_private-store-scope: rewritten — shareable entities now scope/@graph the
protected store; private anchors the shim/inbox + settings; "never did:ng:i"
kept. decision_2026-03-17 marked partially superseded.
- knowledge_stores-permissions: ⚠️ store↔document callout.
- knowledge_entities: MeetingPoint/Notification now persisted (not local-only).
- nextgraph-platform: decision_2026-06-17 records the emulated inbox; fork-inbox
brief marked short-circuited; discovery-model divergence (shipped fan-out vs
global-index target) flagged for confirmation.
- functional-domain/knowledge_roadmap, bdd-testing leaves updated. All doc-debt
settled; lint clean (60 leaves).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the completed T01 migration + validation in the two relevant leaves.
- decision_2026-06-17_eventually-library.md: new dated section "Shim migré
dans la lib — 2026-07-02" — the integration boundary moved from "doc_create
stays on the real ng / shim still in-app" to "everything in the lib; the app
touches @ng-org at runtime only via ngSession". TODO "primitive doc_create/
SPARQL via injected ng" checked done. Namespaces docs/storeRegistry/
isolation/accounts; isolation<->ReadCap = coexist (distinct axes).
- brief_2026-06-15_shared-wallet-shim.md: Status/summary/Direction "shim
in-app" -> "shim in the lib".
Validation captured: lib 36/36 + tsc rc=0; app build + harness bundle OK;
full BDD suite 78 passed / 0 failed / 71 skipped (baseline held).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full-suite validation of the merge surfaced 4 failures, all multistore: routing
doc_create through the lib's `ng` proxy (685f6d3) breaks @ng-org/web's iframe
postMessage marshaling — DataCloneError "function could not be cloned" (a JS
Proxy over the iframe-RPC proxy = double proxy).
Fix: storeRegistry.ts and harness-ng.tsx (createSmokeDoc) call doc_create /
SPARQL on the real @ng-org/web `ng` directly again. useShape / init / login /
ReadCap still route through the lib. After the fix the 3 multistore scenarios
pass; full suite = 77 passed, 0 merge regressions.
Integration boundary documented in decision_2026-06-17: the in-app shim's
low-level NextGraph calls stay on the real SDK until storeRegistry moves INTO
the lib (where it would use the injected real ng, no double proxy). Lib TODO:
expose a doc_create/SPARQL primitive that uses the injected ng.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-merge audit of main's shared-wallet shim: storeRegistry.ts was the only
runtime path still importing `ng` from @ng-org/web directly, bypassing the lib.
Route it through @ng-eventually/client (the ng proxy forwards doc_create /
sparql_update / sparql_query). Now the only @ng-org runtime imports in the app
are the single injection point (ngSession) + documented exceptions (auth-setup,
mock harness) + generated ORM type-only bindings — the decision_2026-06-17
invariant holds again.
Still in-app, to move into the lib later: storeRegistry, AccountContext, the
isolation filter (distinct from the lib's ReadCap filter).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings 266e335 (staging shared wallet: file-assisted import + multi-browser
e2e) into the ng-eventually branch. Conflicts resolved so both lines of work
coexist and route through the lib where they overlap:
- harness-ng.tsx: combine ReadCap FilterProbe (ours) with main's SmokeProbe/
FanoutProbe; useShape + ng imported from @ng-eventually/client.
- ngSession.ts (auto): our single-injection-point configure() + main's hidden
logoutNg, which uses the lib's ng.
- useShapeWithDefaults.ts (auto): lib useShape + main's { graphs } multistore
scope.
- cucumber.json: single "tags": "not @wip" (both branches added it).
- brief_2026-06-15_shared-wallet-shim: keep main's implemented status; record
that the read filter now lives in the lib (decision_2026-06-17) while the
rest of the shim (storeRegistry/accounts/isolation) is still in-app, slated
to move into the lib.
Build OK; harness-ng bundles. TODO (next): verify all of main's NextGraph
surface routes through @ng-eventually/client (storeRegistry uses ng directly).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Se désinscrire" e2e wasn't obsolete: verified against the broker that
join reflects in the UI but leave does NOT — the button stays "✓ Je participe"
(>10s). DeepSignalSet.delete() does fire reactivity (touchIterable), so the
real cause is downstream: the deletion doesn't propagate / the item resurrects
via broker sync (the documented CRDT limitation).
- cycle-de-vie-evenement.feature: rewrite the désinscription scenario to be
self-contained (join → leave → "J'y serai" in one session, no cross-scenario
/ persistence dependency), and tag it @wip with an accurate comment.
- cucumber.json: add tags "not @wip" so known-incomplete scenarios document an
expectation without failing the suite (default run: 146 scenarios).
- docs: caveat_participation-deletion records the e2e finding (leave doesn't
reflect in the UI; delete fires reactivity but the item resurrects via sync);
knowledge_cucumber-setup documents @wip = excluded from the default run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>