Ng eventually #1
Reference in New Issue
Block a user
Delete Branch "ng-eventually"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Migrate .project/{knowledge,decisions,briefs} and the always-loaded AGENTS.md/CLAUDE.md into the in-repo `concept` system (hook-delivered, typed leaves). Then audit the actual code to verify the migrated doctrine and capture knowledge that lived only in the source. Concepts (53 leaves): - functional-domain — produit : point de rencontre greffé, acteurs, déduplication - app-architecture — modules, invariant d'imports, routing, écrans, styling-system, screen-pattern, cookbook d'ajout d'écran - tech-stack — Bun-first, APIs, build pipeline, deployment (Dockerfile), commandes - data-layer — NextGraph mono-store, shapes, modes, règles + caveats (suppression, champs non persistés, internals du contexte) - bdd-testing — Cucumber multi-couches, contrat de couches, harness, cookbook - app-security — posture actuelle (mono-store, confiance broker), auth wallet, brief matrice d'autorisations cible - nextgraph-platform — NextGraph système externe + briefs (multi-store, shim, fork) Audit corrections: - décision SPARQL-delete annulée (superseded) → caveat (le code utilise ngSet.delete, persistance possiblement partielle) - divergences relevées : routing path-based (pas hash), thème moderne sous components/sketchy, ConnectScreen hors registre, build:orm au chemin périmé, champs d'event perdus en connecté Strip migrated sources; AGENTS.md/CLAUDE.md réduits au cœur (but, invariants, carte des concepts) + pointeurs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Consume the shim mechanics now living in the lib (docs/storeRegistry/ isolation/accounts) and remove the remaining direct @ng-org runtime imports. - storeRegistry.ts keeps ONLY the Festipod EntityKind/entityScope mapping, injects it via configureStoreRegistry({ getSession, normalizeUser }), and re-exports the lib's storeRegistry.* (callers unchanged). Drops `import { ng } from '@ng-org/web'`. - harness-ng.tsx createSmokeDoc now uses docs.docCreate (real injected ng, no DataCloneError) instead of ng.doc_create. Drops the @ng-org import. - AccountContext.tsx thin React wrapper over accounts.AccountStore + normalizeUsername; historical key `festipod.account.username` pinned → zero behavior change. Context/Provider stay in the app. - isolation.ts Festipod wrapper over the lib's pure isolation.applyIsolation. Invariant reached: `grep "from '@ng-org'" src/ | grep -v 'import type'` lists only ngSession (the configure injection point) + the two documented test-harness exceptions (auth-setup.tsx, harness.tsx mock). No doc_create goes through the lib's public proxy. App build + harness-ng bundle OK. 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>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 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>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>bun run validate— validation complète 2 niveaux 9e62bdea53Applique 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>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>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_014GbGgNEHRejVKoREvFuDFgThe scenario reads "an account not connected to another does not read its protected ENTITY, then reads it after connecting", but the probe was reading `did🆖${protected_store_id}` — the STORE document — and writing its test entities straight into it. Under an ACL that shortcut was harmless. Under key possession it is wrong, and for a reason the model states outright: sharing a store capability would hand over everything the store contains, present and future. The unit of sharing is the document. `declareConnections` therefore shares the keys of entity documents, the store is not one of them, and the reader legitimately saw nothing. The code was right; the probe was standing in the store for an entity. Writing several entities into a store-level document also broke this repo's own one-document-per-entity rule. The probe now creates a real protected entity document, writes the entity there, and mounts its subscription on THAT document. A second defect surfaced while fixing the first: `connect()` asserted both directions from the READER's session, but a capability can only be shared by whoever holds it, and `capFor` answers for the connected identity alone — so the owner-side call returned early having shared nothing. Each direction is now asserted from its own session, and the reader drains its inbox afterwards. The reader is a genuine second identity (per-run identifiers give it its own account, stores, inbox and keyring), not the same one in disguise — a test that passes because the state is unreal proves nothing. Checked by breaking it on purpose: without `connect` it fails with `expected +0 to equal 1`. Also recorded, and worth knowing before writing another probe: `resetCaps()` clears the "a capability was issued" flag, which disarms the read filter entirely — it has to run BEFORE the first mint, or reads go straight through and the reader sees everything. tsc 0, @ui 7/7, target scenario green, read-filter not regressed.The app derived its identity from a profile lookup and, when nothing matched, picked somebody else. That is backwards: signing in returns who I am, and the profile is looked up by it. - Identity and profile are now two things. The identity is what `ensureIdentity()` returns: opaque, never rendered, never written, never passed to a data-layer call. The profile is Festipod's own object -- pseudo, name, initials -- in a document we create and write. - "My profile" is the profile document I own, resolved through `listMyEntityDocs('protected')`. No username matching, no positional pick. A failed listing leaves the answer UNKNOWN rather than collapsing to "none". - Having no profile now resolves to having no profile. Two impersonation fallbacks are gone, including one in `updateProfile` that would have written your pseudo into a stranger's document. - A profile is created at sign-in when none exists. The shape makes name, initials and username mandatory, so it is written with placeholders that read as instructions -- never a plausible human name, never anything derived from the opaque identity. Nothing succeeds in silence any more `joinEvent` used to return without writing and without throwing when it could not attribute the participation, while the screen announced success. It rejects now, and the confirmation follows the write. Withdrawal likewise -- the doctrine requires it to be authoritative. The host notification stops being written into the joiner's own store, where its recipient could never read it, and the optimistic notice shown to the wrong person goes with it. The creator signs up through the common path: no owner branch anywhere, no special case, the same deposit and the same derived count.Three changes the product model asked for. The fixture seed no longer writes anything into a connected wallet, by any route. `bootstrapWallet` is the single enforcement point -- both call sites funnel through it -- so the switch cannot be walked around by a screen or a bridge. The fixtures, the seeding code, the demo path and the rendering tests are untouched; a unit test now fails if a document is created after all. An event has no host. The domain says so -- the meeting point has a host, the event is only the anchor -- while the shape carried `hostName`/`hostInitials` and every created event was written with the fabricated `'Moi'` / `'MD'`. Gone from the shape, the ORM bindings, the type, the adapters, the writes and the screens. `fp:MeetingPoint.host` stays: that one is real. Regenerating the ORM revealed the committed bindings had drifted from what the generator emits -- stylistic, verified predicate by predicate, plus the loss of the `Fp` prefix. The prefix cannot be restored at the generator: the name comes from the shape IRI, and those IRIs are the persisted RDF classes. Aliased at the three import sites instead, so nothing downstream moved and the DOM `Event` and `Notification` types are never shadowed. Write rights are ownership, read from the list The contract leaves no other reading -- only an owner writes, and no call adds a writer -- so `listMyEntityDocs('public')` is what says which events are mine. The hard-coded `isOwner = true` is replaced by a three-state answer, and the UNKNOWN state renders neither a pencil nor a greyed one: a disabled look-alike invites a dead click. Two adversarial passes refuted the first attempt and both defects are fixed. A latched boolean denied an owner their own event forever once a listing had missed it; the ruling is now rebuilt rather than accumulated, so a later listing overturns an earlier one. Residual, deliberate and commented: "not mine" is inferred from absence, and the reactive read and the listing are separate mechanisms, so a freshly arrived event is ruled out for the window between them. Closing it needs a timer, which the doctrine forbids.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.