Fifteen findings, all verified before acting. The ones that mattered:
- Corrections added without updating what they corrected. §5's table still
said a cap-less NURI is one "without :k:", two hundred lines after §4
established the discriminant is `r:`. Same shape of defect in the P1a
report, which kept the sentence "it is the owner's keyring, upstream the
keyring is the wallet" — the exact sentence §4quater declares wrong, and the
one that produced a global in-memory keyring.
- A wrong source citation: RootCapRefresh/BranchCapRefresh live in
verifier/src/commits/mod.rs, not repo/src/commit.rs, and are no-op stubs.
- Documentation describing deleted code: isolation.ts, discovery.readIndex,
the global index, and an acceptance test that was dropped with discovery.
- The P1a implementation report had aged into being wrong in four places
(caps not persisted, inbox processing not started, plain string types, the
:k: segment). It is dated, so it now carries a header saying what later lots
overtook, rather than being rewritten.
- vision.md stated "a document's data is stored encrypted" in the present
tense. That is the target; here the cap value is the constant OK and nothing
is encrypted. Said plainly now.
- Prose left mangled by an earlier mechanical find-and-replace, in four places
I had claimed were repaired.
Also: reach.ts and connect.ts had no home in the permanent docs — the boundary
and the connection sequence are now described in simulation.md, not only in a
brief.
Two batches, verified against nextgraph-rs throughout.
P1a — the capability surface. Reading was an ACL (Map<doc, Set<principal>>),
the exact inversion of key possession. It is now possession: `capFor(nuri)` is
the only question, there is no principal parameter anywhere, and nothing turns
a bare reference into a cap. Sharing is `shareCap(cap, toInbox)`, a Link
deposit; receiving needs no operation. `Nuri` and `ReadCap` are template
literal types, so passing a bare reference where a cap belongs is a compile
error, with runtime guards behind it for JavaScript callers.
The virtual user boundary. Every access function is now confined to the
connected user, through two rules on one criterion (possession), implemented in
two places so a lapse in either is caught by the other: authorization at the
passage points, and "do not even attempt" at the callers. The polyfill's own
machinery moved to physical.ts — unguarded, never exported — which replaced an
exemption list: the machinery no longer gets waved through the guard, it calls
something the guard never saw.
Removed, as emulating capabilities the target does not have:
- discovery.ts and its global index. There is no discovery in NextGraph; you
follow links. It also pooled user data across wallets.
- the cross-account fan-out (listEntityDocs, resolveReadGraphs, allAccounts,
loadShim), which was cross-user enumeration by construction.
- resolveInboxAnchor, a single inbox common to every user.
Caps are now stored where NextGraph stores them, and read back rather than
recomputed: AddRepo on the store's Store branch for documents a user creates,
AddLink on its User branch for caps received. Inboxes belong to someone — the
user's own, plus one per document — and connecting a user drains them all;
that is the library's job, not the app's.
Corrections worth recording: a ReadCap is `r:`, not `:k:` (reported by
NextGraph's developer, verified in BlockRef::readcap_nuri); received caps DO
have a register (AddLink), contrary to what this repo's notes claimed; and
"wallet" upstream means keyring — what owns three stores is a user, so the
vocabulary follows.
The cap value is the constant OK: the only question the emulation answers is
whether a cap is held. P1b replaces that one constant with a real key.
After this the shape is right and the isolation is still fake. Nothing here may
be described as anonymous or private.
Dernier document du dossier docs/ encore en français. Traduction fidèle : mêmes
sections, mêmes tableaux, mêmes items, mêmes balises. Glossaire repris tel quel
du passage précédent pour que le vocabulaire soit cohérent d'un fichier à
l'autre.
Les DEUX passages délibérément rétractés sont préservés à l'identique — la
section barrée « Widened scope: the WriteCap (= membership) » avec son bloc
<details>, et le lot barré PW dans la liste des phases. Ils sont là comme
garde-fous : sans eux, quelqu'un re-proposera ces idées, qui paraissent toutes
raisonnables au premier abord. C'était le risque de la traduction — nettoyer ce
qui ressemble à du bruit — d'où la vérification chiffrée demandée.
Trois incohérences relevées à la relecture et corrigées :
- La liste des phases décrivait encore l'ANCIENNE P1a (types de marque
DocRef/DocCap, resolveCapLess, sealCapTo durable) et renvoyait à « Specified
above », qui ne pointait plus vers rien depuis l'extraction du lot dans sa
propre fiche. Créée par ma restructuration : j'avais remplacé la section sans
toucher à son résumé ailleurs.
- « Open questions » demandait encore si le fetch keyless devait être permis,
alors que le verdict corrigé plus bas tranche la question négativement.
Conservée barrée : l'hypothèse est intuitive et se reformerait sinon.
- La revue adverse annonçait 6 constats et en listait 7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Le reste du dossier docs/ était déjà en anglais ; ces trois fichiers avaient été
rédigés en français par erreur. Traduction fidèle, sans changement de fond :
mêmes sections, mêmes tableaux, mêmes blocs de code. Le retour à la ligne dur à
78 colonnes est levé (une ligne par paragraphe, convention du projet).
Marqueurs épistémiques préservés et rendus aussi visibles : VERIFIED / INFERRED /
CORRECTED / DIRECTION / GAP. Les citations verbatim de commentaires amont restent
intactes.
Deux incohérences de FOND signalées par la traduction et corrigées ici — elles
étaient invisibles tant qu'on lisait chaque section isolément :
- readcap-and-nuri-model, section « Caveats / gaps » : elle listait encore le
fetch keyless comme hypothèse INFÉRÉE à confirmer, alors que le bloc CORRIGÉ du
§4bis la déclare fausse et non constructible. Contradiction interne née de ma
correction partielle. Conservée barrée plutôt que supprimée : l'hypothèse est
intuitive et se reformera sinon.
- incident write-loss : l'intro affirmait en fait établi que « l'écriture
n'atteint jamais durablement le broker », alors que la réserve épistémique plus
bas dit explicitement que l'alternative (perte d'écriture vs réhydratation à
froid) n'est pas tranchée. L'intro ne rapporte plus que le symptôme observé.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
P1a n'était qu'une section d'un brief de ~370 lignes charriant beaucoup de
matière rétractée (lot PW barré, section membership en <details>, verdicts
corrigés). Coder depuis ce fichier aurait été pénible et risqué.
docs/briefs/2026-07-27-p1a-cap-surface.md — le lot actionnable, lisible seul :
- un seul type nouveau, ReadCap, le nom de l'amont ;
- capFor(nuri) sur le trousseau (la branche de store), avec l'avertissement que
le trousseau n'est PAS le mécanisme de partage ;
- shareCap(cap, toInbox) — un document, vers une ou plusieurs inboxes ;
- rotation de clé : re-livraison automatique, rien à implémenter côté consommateur ;
- contenu public : lisible par l'URL, non récursif ;
- la frontière index.ts / polyfill, tranchée : signatures sur des chaînes, comme
le vrai SDK ;
- le test de recette sans crypto (watch-shape moissonne aujourd'hui toute chaîne
did🆖 et la replie dans l'ensemble LU) ;
- et ce que le lot ne fait PAS, pour ne pas le croire fini.
Chaque écart écarté y est justifié plutôt que tu : types de marque, resolveCapLess,
receivedCaps, refOf, parseNuri, PrincipalId. Le premier jet introduisait 8 notions
nouvelles ; il en reste 2, et le critère est écrit noir sur blanc — toute notion
inventée est une dette de vocabulaire.
Le brief d'origine reste le chantier d'ensemble et pointe vers la fiche.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
MODÈLE CIBLE (readcap-and-nuri-model) — trois ajouts, deux corrections :
- Store public : lisible par l'URL, et NON récursif — un contenu public peut
référencer du contenu privé sans y donner accès. C'est la non-récursivité qui
porte la valeur (objet public pointant vers de l'identité privée).
- Le trousseau : la branche de store, où chaque création commite AddRepo{read_cap}
— avec l'avertissement explicite que ce n'est PAS le mécanisme de partage.
Confondre l'index privé et le geste de partage mène à « on partage le store »,
ce qui livrerait tout son contenu présent et futur.
- Rotation de clé : re-livraison par inbox, traitée automatiquement à la
connexion. Écrit comme DIRECTION, en signalant que le commentaire amont dont ça
partait décrit l'état courant.
- Levée de la confusion did/NURI en tête de la section grammaire : `did🆖` est
un préfixe de schéma présent partout, pas un marqueur de « sans cap ». C'est un
seul objet, avec ou sans la clé dedans.
- Livraison de cap par inbox signalée comme MANQUE (forme bonne, chemin absent).
ÉTAT COURANT (nextgraph-current-state) — 218 lignes ajoutées, structure intacte :
livraison de cap par inbox non implémentée ; vérification de signature d'auteur
jamais appelée au runtime (members map vide, //TODO) ; aucune sonde d'existence
au niveau SDK ; expose_outer codé en dur à false, absent du SDK ; protocole Ext
sans aucun contrôle. Plus trois constats d'exploitation : heal cold-start,
fork de compte sur provision concurrente, et l'abort du flush outbox sur
TopicNotFound. La mort du socket est seulement référencée (déjà couverte).
CORRECTION D'UN FAIT QUE J'AVAIS ÉNONCÉ FAUX : le digest d'auteur n'est PAS clé
sous le secret de lecture — il est clé par l'overlay outer, public. C'est le
CONTENU du commit qui est chiffré. La conclusion « vérifier suppose de pouvoir
lire » tient, le mécanisme diffère.
Lot PW (WriteCap = membership) RETIRÉ de la liste des phases : il restait planifié
alors que le brief déclare plus haut qu'il n'y a pas de membership. Il était en
outre justifié par un besoin de dédup par signature que le consommateur n'a pas —
sa dédup s'appuie sur l'overlay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Deux corrections de direction, données par le PO — et les deux viennent de la
même erreur de méthode : avoir lu l'ÉTAT COURANT du source comme s'il donnait
l'INTENTION. C'est précisément ce que ce brief met en garde de faire.
1. Rotation de clé. La spec disait « qui n'est pas resté abonné perd l'accès »
et demandait d'exposer une obligation d'abonnement au consommateur. Faux
comme cible : quand une clé tourne, la nouvelle est envoyée dans l'inbox des
ayants droit, et cette inbox est traitée automatiquement à la connexion
suivante d'un client. L'accès n'est pas perdu, il est différé — cohérent avec
le local-first. Donc rien à implémenter côté consommateur, et la re-livraison
emprunte le même canal que la livraison initiale : le mécanisme de partage
couvre les deux sans cas particulier.
2. Store public. Principe à exposer tel quel : un élément du store public est
public — qui a l'URL lit le contenu — mais PAS récursivement. Un contenu
public peut référencer du contenu privé sans donner accès au référencé. C'est
la non-récursivité qui porte la valeur : elle permet un objet public pointant
vers de l'identité privée, le cas exact du consommateur. NextGraph s'oriente
par ailleurs vers un non-chiffrement du contenu public (données toujours
signées) : détail d'implémentation dont la surface ne doit pas dépendre. Si le
store public ne se comporte pas comme le principe le décrit, c'est le polyfill
qui s'adapte, pas le consommateur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Deux adversaires à mandats disjoints (alignement NextGraph / économie
conceptuelle). Résultat : P1a fond de 8 notions nouvelles à 2, et un fait que
j'avais consigné comme VÉRIFIÉ était sur-lu.
CORRECTION DE FOND — le fetch keyless n'est PAS constructible. Le spike P0
concluait « Q1 OUI partiel, seul garde : l'overlay ». Il s'arrêtait au contrôle
d'accès sans regarder l'ADRESSAGE : aucune commande d'existence au niveau SDK ;
la seule sonde est interne au crate, exige des BlockId ET un repo chargé, et vise
l'overlay inner dérivé du secret de lecture. Une référence cap-less porte un
RepoId et l'overlay outer — ni BlockId, ni le bon overlay. L'adressage
présuppose le cap. Note corrigée sur place (pas empilée), avec la leçon
transposable : vérifier qu'une garde laisse passer ne prouve pas qu'une
opération est atteignable — encore faut-il pouvoir NOMMER ce qu'on demande.
P1a réécrite :
- UN seul type nouveau, `ReadCap`, le nom de l'amont. `Nuri` reste ce qu'il est
déjà (~90 usages) : la forme cap-less. Les types de marque disparaissent — le
SDK réel prend `nuri: String` et enforce au RUNTIME par la crypto ; une
garantie de compilation est un concept que NextGraph n'a pas, et un
consommateur qui typerait tout devrait dé-typer plus tard.
- `capFor(nuri) → ReadCap | undefined` : le trousseau. Comble un trou fatal du
premier jet — `doc_create` renvoie un NURI cap-less, donc l'invariant « on ne
va jamais d'une référence nue à un cap » empêchait le créateur d'obtenir le cap
de son propre document. Le trousseau existe déjà : la branche de store, où
chaque création commite AddRepo{read_cap}. En amont c'est le wallet.
- `shareCap(cap, toInbox)` : on partage UN DOCUMENT, à une ou plusieurs inboxes.
Pas le store — donner un cap de store livrerait tout son contenu présent et
futur. Les caps reçus arrivent comme dépôts d'inbox, consommés par le
inbox.watch existant (ce qui règle le point 7 de la revue adverse).
- Durabilité : ne PAS la promettre. Verbatim amont, les caps sont « not durable »
et qui ne reste pas abonné perd l'accès ; PermaCap est un TODO. La surface doit
exposer l'obligation d'abonnement, sinon le consommateur retient des caps morts.
- `PrincipalId` sort de la surface caps : n'existe pas en amont, et le brief le
supprimait de canRead en le qualifiant d'inversion ACL avant de le réintroduire
dans sealCapTo. On adresse des inboxes, comme inbox.post le fait déjà.
- Section 0 conservant les erreurs du premier jet : elles sont instructives.
- Exception publique actée : un lien de repo public n'a PAS de read_cap (il se
télécharge depuis l'outer overlay) — pour du public, « référence nue → contenu »
est bien la forme cible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Découpage de P1 en deux natures de travail : P1a = la FORME exposée aux
consommateurs, P1b = l'ENFORCEMENT. Seul P1a bloque Festipod, puisque l'app doit
être écrite comme si NextGraph était fini. Après P1a la forme est juste et
l'isolation reste fausse — le brief le dit explicitement pour qu'on n'affirme
rien d'anonyme avant P1b.
P1a spécifié :
- Types DocRef / DocCap distincts À LA COMPILATION (aujourd'hui `Nuri = string`,
aucun parseur, aucune notion de segment de clé). L'invariant central : AUCUNE
fonction ne va de DocRef vers DocCap — on n'obtient pas un cap en le demandant,
seulement en le recevant. Le compilateur refuse alors de lire depuis un
identifiant nu, et le consommateur ne PEUT PLUS écrire le modèle mental faux.
- resolveCapLess(ref) → { exists }, jamais de contenu et jamais d'état `deleted`
(vérifié : la cible ne pourra pas l'offrir).
- sealCapTo(cap, recipient) durable + receivedCaps(), qui remplacent grantRead.
Trois deltas réels vs l'ACL : durabilité, livraison-chez-le-destinataire,
re-partage par le détenteur. C'est ce qui fait disparaître declareConnections.
- Table de ce qui disparaît : le paramètre `principal` de canRead EST l'inversion
ACL ; resetCaps doit BASCULER de trousseau, pas effacer, sinon la durabilité
est un mensonge.
- Test de recette naturel : watch-shape moissonne aujourd'hui toute chaîne
`did🆖` et la replie dans l'ensemble LU — sémantique exactement inversée.
Avec les types, elle ne peut plus qu'être résolue en existence. Vérifiable sans
une ligne de crypto.
Corrigé aussi : l'inventaire des contournements était écrit beaucoup trop
doucement. Cartographie vérifiée — seuls 4 sites consultent les caps ; l'inbox
entière, store-registry (racine de confiance compte→NURI), discovery.readIndex,
subscribe et open-repo rendent de la donnée sans garde. Et le garde d'ÉCRITURE
est déjà mort-né : docs contourne ng-proxy par conception et tous les écrivains
internes passent par docs — grantWrite/canWrite ne se déclenchent jamais.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
readcap-and-nuri-model — nouvelle section sur l'OVERLAY, le concept qui
manquait à la référence. C'est l'espace réseau d'un STORE : deux formes
(outer = BLAKE3 public du store_id, calculable par tous ; inner = BLAKE3 keyed
par le ReadCapSecret, réservé aux détenteurs de la clé). Le `✌️` d'un NURI de
DOCUMENT porte l'overlay de son store — VÉRIFIÉ de bout en bout, avec une
contre-preuve mécanique : dans Store, get/put/del/has passent tous
`&self.overlay_id`, donc tous les documents d'un store partagent le namespace
de blocs et un overlay par-document est structurellement impossible.
Conséquence documentée, qui contraint tout modèle de présence anonyme : le
`✌️` est un pseudonyme stable et permanent de la personne, présent dans toute
référence cap-less vers n'importe lequel de ses documents protected. Le même
bit d'information sert à dédupliquer sans lire ET à tracer — indissociables.
vision — correction d'une forme fausse. Le document affirmait « écriture =
membership/permissions », en miroir de « lecture = possession de clé ». Faux :
il n'y a pas de notion d'appartenance dans le modèle, uniquement des clés et
des URLs. Toute forme en member/role/permission est une MAUVAISE forme.
brief caps — la section « périmètre élargi : WriteCap = membership » est
retirée, conservée barrée comme garde-fou, avec la leçon de méthode qui vaut
plus qu'elle : lire l'état courant de nextgraph-rs pour en DÉDUIRE la forme
cible est une erreur — le source contient de l'échafaudage inerte (AddMember,
PermissionV0, verify_sig jamais appelé hors tests). Le source sert à vérifier
un mécanisme, jamais à inférer une intention.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Couverture manquante de la couche réactive — c'est son absence qui a laissé
passer un bug de réactivité. Le runner exerce les DEUX poussées porteuses en
production contre le VRAI broker, par la même surface publique que l'app
(`subscribeDoc` → `ng.doc_subscribe`) :
SELF — l'écriture d'une session vers un doc qu'elle-même souscrit ;
CROSS — une seconde session (même wallet partagé) écrit sur ce doc.
Chaque push est enregistré comme événement typé ({typeKey, elapsedMs}) : le
verdict est le fait que le callback re-tire, pas une relecture du document.
Chaque attente est une promesse événementielle unique + timeout (pas de boucle
de relecture) — un timeout est donc un « n'a PAS tiré » définitif.
Verdict obtenu : doc_subscribe POUSSE bien dans les deux cas — le défaut de
réactivité observé côté app n'est donc pas ce primitif.
Standalone (pas `bun test`) : `bun run test:e2e:reactivity`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
vision.md — la charte : le polyfill n'est PAS une couche de sécurité (wallet
partagé + pas de crypto = insécurité ACCEPTÉE) ; seul objectif = exposer la
BONNE FORME des primitives futures pour que les consommateurs n'aient rien à
réécrire. Invariant tenu par une simulation crypto légère : un `did` nu (sans
ReadCap) ne permet PAS de lire ; un NURI avec ReadCap est suffisant et requis.
readcap-and-nuri-model.md — le vrai modèle, VÉRIFIÉ par lecture de
`nextgraph-rs` : ReadCap = ObjectRef {id BLAKE3, clé ChaCha20} = possession de
clé, PAS une ACL ; grant = sceller la clé à l'inbox du destinataire ;
révocation = re-key grossier et non-rétroactif ; grammaire NURI cap-less vs
cap-porteur (le segment `:k:` est le discriminant) ; table des divergences avec
l'émulation `caps.ts` (aujourd'hui une ACL — l'inversion exacte).
briefs/2026-07-20-caps-emulation-alignment.md — le chantier d'alignement :
spike P0 keyless-resolve (verdicts vérifiés : existence sans clé OUI,
détection de suppression sans clé NON, confidentialité OUI), puis P1 cap-less
vs cap-porteur, P2 possession, P3 re-key, P4 migration d'API. Inclut la revue
adverse (WriteCap = membership et non possession ; sans crypto la privacy de
lecture n'est pas applicable ; migration = re-architecture consommateur).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Section 'Known open issues' dans nextgraph-current-state (A ouvert, B indéterminé, C hypothèse-en-cours) + gap 4 (auto-écho non confirmé) dans sdk-reference. Statuts préservés.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Réutilise le format identité-first [<id>][polyfill] d'access-log : deposit/read/materialize/readSynced + watch (materializing vs unchanged-skip). Gated par debugAccessLog, aucun changement de comportement. tsc 0, bun test 126.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
Chemin données bas-niveau du polyfill rendu lisible pour diagnostiquer en session live :
- Format identité-en-premier : `[<identity>][polyfill] OP shortNuri (label)` ;
console.error épars (store-registry, inbox) unifiés au même préfixe.
- Trace (derrière le flag debug) : issue de la barrière ensureRepoOpen
(synced|timed-out + durée) et résultat sémantique de chaque étage de résolution
(resolvePointer/canonicalDoc/resolveAccount/resolveShimDoc/readScopeIndex).
- outbox-log.ts (nouveau) : inspection read-only de l'outbox hors-ligne au
démarrage de session ; console.warn si non vide (anomalie, toujours visible),
sous flag si vide. Le comptage seul est fiable (payloads BARE opaques côté JS).
Pas de changement fonctionnel. bun test 126 pass ; tsc 0 erreur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
- Migration des comptes legacy supprimée (migrateLegacyRecords + garde migratedInto
+ call-sites). Un wallet pré-fix (records store-root, pas de pointeur) provisionne
simplement un doc-shim frais; contenu legacy ignoré (voulu, données = dev). La
résolution barrière-autoritative + anti-fork (resolvePointer/ensureRepoOpen/
canonicalDoc/ensureInFlight/pointerGuard) est inchangée.
- Logs d'accès SDK préfixés [polyfill] + NURI tronqué via shortNuri() (retire
did:ng:o: et :v:…, garde 8 chars) → moins verbeux.
Ex: [polyfill] [user1] READ vDlwbZio… (resolvePointer) → 1 triple-rows
Tests: bun test unit 126/0. Docs (nextgraph-current-state/simulation/migration-guide)
mis à jour (migration legacy retirée du modèle décrit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug: à la reconnexion, resolveAccount lisait le shim depuis le store-root
(did🆖${privateStoreId}), NON abonnable → pas de barrière first-State → un "0 rows"
à froid est ambigu → le retry (resolveAccountReliably/provisionRetry) échoue → nouveau
compte provisionné → FORK → données du compte invisibles.
Cause NextGraph (vérifiée nextgraph-rs): "trouvable-sans-lookup" (store-root) et
"abonnable" (did:ng:o:<RepoID aléatoire>) sont DISJOINTS — pas de doc à la fois
devinable et attendable → une résolution shim purement barrière est impossible.
Fix (indirection pointeur → doc-shim abonnable):
- Les AccountRecord migrent dans un doc-shim doc_create'd (did:ng:o:..., a une barrière).
- Un pointeur écrit-une-fois dans le store-root (<shim:root> <shim:shimDoc> <docShim>)
le nomme. resolveShimDoc lit le pointeur → ensureRepoOpen(docShim) [barrière] → lecture
de compte AUTORITATIVE (cold 0 = absent pour de vrai). Retry de compte SUPPRIMÉ.
- Micro-garde résiduel (pointerGuard, ex-provisionRetry) sur le SEUL triple pointeur
écrit-une-fois; ne peut jamais forker un compte; fork de pointeur réconcilié au
doc-shim canonique (lexicographiquement-min), sans perte.
- Migration: migrateLegacyRecords copie (pas déplace) les comptes de l'ancien store-root
vers le doc-shim avant toute conclusion "absent"; idempotent; wallet neuf → no-op.
Tests: unit 128/128, e2e réel 42/42 (CONTRACT 2 = non-fork du compte à la reconnexion),
red-before/green-after prouvé. Docs: nextgraph-current-state (antagonisme + indirection),
simulation, migration-guide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ensureRepoOpen(target) (attend le 1er State, barrière de sync déterministe) puis
read(target) — même pattern que discovery.readIndex. Permet au propriétaire d'un
événement de voir un dépôt DÉJÀ synchronisé au lieu de lire trop tôt un inbox vide.
Consommé par Festipod (materializeAttendance). Pas de polling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Défensif : loadShim/resolveAccount/ensureAccount ouvrent l'anchor (private-store-root)
avant de lire/écrire le compte shim, comme le fait déjà readScopeIndex. Robustesse
same-session si l'anchor est là-mais-pas-encore-souscrit.
NB (vérifié nextgraph-rs) : sur le login broker normal, le bootstrap charge le
private-store dans self.repos AVANT de rendre la session à JS → un wallet FRAIS
retourne 0 rows (pas RepoNotFound) et provisionne. Il n'existe AUCUN primitif JS
pour ouvrir un repo *inconnu* : ce heal n'est pas un remède à un store non
bootstrappé (limite NextGraph), juste une robustesse d'ouverture same-session.
Tests : cold-start-anchor.test.ts (rouge-avant/vert-après unit) ; harness e2e
repro-fresh-wallet (mint un wallet neuf par run — comble le trou "aucun test de
démarrage à froid sur wallet vierge"). Fakes anti-fork/watch-shape honorent
désormais la barrière first-State dont dépend le heal. e2e réel 42/42.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le journal d'accès compte des triplets RDF (?s ?p ?o), pas des objets métier.
Clarification demandée côté Festipod (les logs "readDoc → N rows" étaient ambigus).
Test access-log mis à jour en conséquence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dernière couche du bug de reconnexion : au cold-start, `watchShape` public +
protected + l'effet owned-events appellent `ensureAccount(A)` quasi-simultanément
AVANT la sync du shim → chacun lit 0 → chacun provisionne un nouveau jeu de docs
(fork par-appelant) → la résolution déterministe canonique fait alors diverger
lecteur et écrivain sur le docProtected → `readScopeIndex` vide.
Fix : `ensureInFlight` (map de promesses) dé-duplique les provisions concurrentes
en UNE seule ; `discovery.readIndex` ouvre son repo au cold-start (`ensureRepoOpen`).
Avec la résolution canonique déjà committée, écrivain et lecteur convergent.
Mesuré (levier isSuccess) : la participation protected converge `isSuccess=true,
data=1` sur la page fraîche (plus « vide à 30s »).
gate : tsc 0 ; bun test 123 ; test:e2e 42/42.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cause racine du bug de reconnexion (probe contrôlé répété) : le shim d'un compte
accumule des `docPublic`/`docProtected` EN DOUBLE (forks passés), et
resolveAccount/indexDocOf les choisissaient de façon NON-DÉTERMINISTE → l'écrivain
et le lecteur (page fraîche) ancraient sur des docs d'index DIFFÉRENTS → lecture 0.
Fix :
- `canonicalDoc()`/`recordFromRows()` : parmi plusieurs valeurs d'un scope, choisir
le NURI lexicographiquement le plus petit (les NURIs sont content-addressed →
ordre total stable). Écrivain et lecteur résolvent TOUJOURS le même doc, même sur
un shim corrompu par des doublons.
- `resolveAccountReliably` RESTAURÉ (retry borné avant provision sur read shim 0 à
froid) : j'avais retiré l'anti-fork à tort (`38b1521`) — le gap EST exhibé (fork
non-déterministe au cold-read), et la barrière `user_connect` n'est PAS accessible
côté JS → un retry borné est la compensation légitime (pas la barrière-store
cassée de `45dbd9a`). Budget injecté `provisionRetry` ; défaut attempts:1 (fakes
synchrones), app/e2e attempts:8.
anti-fork.test.ts réécrit : docPublic dupliqué → même canonique ; retry sur lag →
réutilise ; neuf → provision 1×.
gate : tsc 0 ; bun test 122 ; test:e2e 42/42 (CONTRACT 2 non-fork vert).
Portée : corrige la couche docPublic de la reconnexion. Une couche PROTECTED
distincte (watchShape protected ne converge pas à froid) reste — diagnostic en cours.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase A du refactor des lectures. Surface la barrière de sync interne
(open-repo `getSyncState`) dans une API useQuery-shaped, en anticipation de la
mise à jour prévue de useShape par NextGraph — distingue nativement « sync en
cours » de « synchronisé mais vide ».
`watchShape<T>(shapeType, scope): { getSnapshot(): ShapeQuery<T>, subscribe(cb),
refetch() }` avec `ShapeQuery = { data, isPending, isSuccess, isError, error }`.
OBSERVABLE (pas de dépendance React — l'app câblera useSyncExternalStore en
phase B) ; getSnapshot rend une référence stable.
- Scope LOGIQUE (public/protected/private) résolu au wallet virtuel :
listMyEntityDocs(getCurrentUser, scope) + découverte foldée pour public.
- isPending tant que la barrière n'est pas atteinte / 1er readUnion non rendu ;
isSuccess après ; timed-out → isSuccess (best-effort, pas isError).
- Réactif SANS polling : subscribeDoc sur les docs + le doc d'index de scope
(+ index découverte) → re-read/re-résolution au push ; souscriptions idempotentes.
- Générique : aucune logique domaine Festipod dans le lib ; filtre par la shape
SHEX (rdf:type). Pas de double filtre cap.
gate : tsc 0 ; bun test 120 (+4 : pending→success, synced-vide, réactif, timed-out) ;
test:e2e 42 passed (+scénario broker réel : isPending au 1er snapshot → isSuccess
avec données, scope vide → isSuccess data:[]).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preuve e2e (wallet frais, broker RÉEL rapide : 1er State 1-2 ms) : le private
store est synchronisé au login, la lecture du shim réussit à froid — le « fork sur
lag » que anti-fork compensait n'est PAS exhibé. Par le principe du polyfill
(compenser un gap RÉEL, jamais du poids mort), et par la règle no-polling :
- la version retry = polling (bannie) ;
- la version barrière `ensureRepoOpen(privateStore)` = CASSÉE (un store n'émet pas
de `State`, la barrière timeout systématiquement → CONTRAT 2 e2e échouait) ;
- le gap = non exhibé.
→ `ensureAccount` fait un `resolveAccount(id)` SIMPLE (une lecture, provision si 0).
`resolveAccountReliably`, `_forceOpenedSyncState` retirés ; `provisionRetry` gardé
optionnel @deprecated (ignoré) pour ne pas casser les 8 tests qui le passent.
`ensureRepoOpen`/`getSyncState` inchangés (chemin de lecture open-repo).
gate : tsc 0 ; bun test 116 ; test:e2e 39 passed, CONTRAT 2 VERT (« same account,
no second provisioning »). Le ~10s du re-resolve public est du scaling anchorless,
pas de la lenteur broker (broker mesuré à 1-2 ms).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Le polling est un anti-pattern NextGraph (par abonnement). La résolution de compte
retentait ×8 la lecture du shim tant qu'elle rendait 0 (lag de sync) — c'est du
polling. Remplacé par la BARRIÈRE d'abonnement, déjà le mécanisme de open-repo :
- `resolveAccountReliably` : `await ensureRepoOpen(did🆖${privateStoreId})`
(subscribe + attendre le 1er State — le shim vit dans le graphe du private store),
PUIS lecture UNIQUE. Après la barrière, 0 ligne = compte réellement inexistant →
provision 1×, lignes présentes = réutilisé (garantie NO-FORK préservée). Plus de
boucle de re-lecture.
- timed-out (barrière expirée) : throw explicite, NE provisionne PAS (un provision
sur sync incomplète re-forkerait). Le « trop long » est un signal, pas un feu vert.
- fake ng sans doc_subscribe : ensureRepoOpen no-op → lecture immédiate (unit intact).
- `_forceOpenedSyncState` : helper test-only (underscore, non ré-exporté).
anti-fork.test.ts réécrit (5 tests : no-fork, neuf→1 provision, idempotence, fake
no-op, timed-out→throw) ; plus aucun test de comptage de retry.
gate : tsc propre ; bun test 117. e2e À RE-VALIDER quand le broker répond (dégradé
ce jour : crash Chromium post-connexion) — la barrière ensureRepoOpen est déjà
validée e2e (CONTRAT 3 + reconnexion) en broker sain. provisionRetry devient un
champ mort de StoreRegistryDeps (nettoyage ultérieur).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Règle posée par l'utilisateur : la seule raison d'être du polyfill est de combler
un retard d'implémentation NextGraph ou un bug. Aucune fonctionnalité additionnelle
propre (pas de feature, d'observabilité, d'API de confort qui ne soit pas « NextGraph
le fera nativement plus tard »). Corollaire : une compensation dont le gap n'est PAS
exhibé sur le broker cible est du poids mort, pas du code défensif — à retirer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avant : `ensureRepoOpen` résolvait son attente sur le 1er push d'abonnement (=
TabInfo, ~1-3ms) — donc AVANT la vraie barrière de sync. Correct par chance sur ce
broker (State suit TabInfo d'~1ms), faux si State tarde.
Maintenant :
- `subscribe.ts` : `docChangeType(resp)` extrait le variant (`State`/`Patch`/
`TabInfo`/…) sans cast `any` ; `subscribeDoc`/`subscribeDocs` le passent en 2e
arg NON-cassant (les appelants 0-arg — discovery, inbox — inchangés).
- `ensureRepoOpen` n'agit que sur `type === "State"` → attend la vraie barrière.
- État de sync par-doc explicite : `getSyncState(nuri): "syncing"|"synced"|
"timed-out"|"unknown"`. Le fallback 8s marque `timed-out`, JAMAIS `synced` — les
deux ne sont plus confondus (base du futur signal app + du « trop long = signal »).
- Fake ng (sans doc_subscribe) : résolution immédiate préservée (bun test intact).
gate : tsc propre ; bun test 117 ; test:e2e 39 passed (CONTRAT 3 vert,
events=["TabInfo","State"] ; reconnexion cold-read 176ms/10.8s — l'attente du 1er
State se déclenche vite, pas de gonflement par timeout). Pas de régression app (le
rouge du test reconnexion est pré-existant et broker-lenteur, vérifié sur lib vierge).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Épingle empiriquement (broker réel) le contrat implicite sur lequel open-repo
repose : après le 1er événement `State` d'un `doc_subscribe`, la PRÉSENCE d'une
donnée est GARANTIE (le triple écrit est déjà dans ce State, sans attente
supplémentaire) et l'ABSENCE est DÉFINITIVE (doc vide reste vide, +5s de grâce).
Si ce contrat casse (changement de version broker), ce test le détecte.
Constats mesurés : l'abonnement pousse `TabInfo` (~1-3ms) PUIS `State` (~2-3ms)
— le State est le 2e événement, pas le 1er ; latence 1er State ~2-3ms sur profil
frais. IMPORTANT (à corriger phase 2) : open-repo résout son attente sur le 1er
push (= TabInfo), PAS sur le State → il rend la main avant la vraie barrière ; le
commentaire « resolves on the FIRST push (the initial State) » est faux.
gate : test:e2e 39 passed ; bun test 117 ; tsc propre. src/ non touché.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les tests du polyfill doivent PROUVER son contrat contre le VRAI broker (pas un
fake). Ajout de deux tests e2e broker réel (wallet dédié), avec reconnexion
FIDÈLE : page fraîche sur le MÊME profil persistant + nouveau login broker
(helper `faithfulReconnect`), PAS export/réimport dans un profil vide (qui
resynchronise et masque le cold-open).
- Contrat reconnexion/cold-read : session 1 crée un doc (public + protected) ;
session fraîche fidèle ; relit → données présentes, en POLLANT une borne
généreuse (l'attente de récupération après souscription est le fonctionnement
normal).
- Contrat non-fork : re-résoudre le même identifiant après reconnexion rend les
MÊMES NURIs de docs de scope, pas un second provisioning.
Temps de sync réels observés (le SIGNAL) : protected relu en ~195ms (repo déjà
ouvert) ; PUBLIC relu en ~105s (ouverture repo + attente push + union ancrée à
l'échelle de la donnée publique) ; login de reconnexion ~2.5s. Le ~105s public
est un signal de PERFORMANCE À INVESTIGUER (probable scale/bloat de l'anchorless
union scan, cf. suivi bloat) — le contrat est REMPLI mais lent côté public.
Honnêteté : le « échoue-sans-le-fix » n'a PAS pu être montré via ce chemin de
login (le bootstrap du login broker ouvre déjà les repos → firstRawNoOpen=1).
Contrat prouvé REMPLI avec les fix ; nécessité non isolable via cette voie.
gate : test:e2e 33 passed (baseline 27 vert) ; bun test 117 ; tsc propre.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les fix récents (anti-fork, open-repo, access-log) n'avaient AUCUN test de
comportement dans le domaine de la lib. Ajout de tests DÉTERMINISTES à base de `ng`
factice modélisant la condition d'échec (pas de broker réel — le harness e2e réel
réhydrate trop vite et ne reproduit pas ces cas).
- anti-fork.test.ts : fake avec lag de sync (0 lignes les K premières lectures du
record de compte, puis les vraies). Asserte le cœur du fix : compte retrouvé au
retry → réutilisé, 0 doc_create (pas de fork) ; compte réellement neuf → 1 seul
jeu de docs provisionné, budget de retry prouvé consommé ; idempotence de session ;
cas limite « trouvé à la dernière tentative ».
- open-repo.test.ts : fake où une requête ancrée rend vide tant que doc_subscribe
n'a pas ouvert le repo. Asserte subscribe-avant-read + idempotence (Set des repos
ouverts) + no-op si le ng n'a pas doc_subscribe.
- access-log.test.ts : off par défaut (silencieux), on via config ET via env
NG_EVENTUALLY_ACCESS_LOG, préfixe = identité active (suit setCurrentUser),
row-count sur READ. Restaure console.log/env fidèlement.
bun test : 91 → 117 pass, 0 fail. tsc --noEmit propre. src/ non touché.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cause racine mesurée (broker réel, access-log) : à la première resolveAccount
d'une session, le record de compte tout juste persisté (ou d'une session
antérieure) peut lire 0 lignes à cause du LAG DE SYNC broker. ensureAccount
interprétait ce 0 comme « compte inexistant » et RE-PROVISIONNAIT un second jeu
de docs de scope (docPublic/docProtected forkés) → les lectures d'une session
tombaient sur un jeu, celles d'une autre (ou après drop de cache) sur l'autre
jeu vide → données « perdues » à la reconnexion.
Fix : `resolveAccountReliably` (store-registry.ts) — retry borné (ouverture du
repo d'ancre shim + backoff plafonné, défaut 8 tentatives / ≲8.5s) AVANT que
ensureAccount ne décide qu'un compte est neuf. Provisionne seulement si, après
le budget, la lecture rend toujours 0 (compte réellement neuf). Budget injecté
via StoreRegistryDeps.provisionRetry (polyfill.ts), ON en prod ; tests unitaires
à provisionRetry synchrone (attempts:1, fake sans lag). Idempotence de session
préservée par accountCache (hit court-circuite, déterministe).
Portée : corrige le déterminisme de provisioning. NE suffit PAS à réparer la
reconnexion (le read public à 0 same-session subsiste, cause distincte encore à
mesurer de façon déterministe — le lag broker rend les mesures non-reproductibles).
Complémentaire du commit open-repo précédent, pas redondant.
gate : tsc --noEmit propre ; bun test 91 pass ; auth @data (vide/distinctes) verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Défaut visé : sur une session verifier fraîche (reconnexion), la lecture ancrée
tape des repos pas encore ouverts dans self.repos → 0 ligne. Nouveau module
`open-repo.ts` : `ensureReposOpen`/`ensureRepoOpen` ouvrent/souscrivent un repo
via la primitive existante `subscribeDoc` (doc_subscribe) et attendent le push
d'état initial (borné, sans polling) AVANT la lecture ancrée. Câblé en amont de
`readUnion` (read-model) et `readScopeIndex` (store-registry). Idempotent par
session (Set des NURI ouverts + Map in-flight ; ré-ouverture si la session
injectée change). No-op si le `ng` injecté n'a pas doc_subscribe (fake unitaire).
État — NON MERGÉ, incomplet :
- AIDE le PROTECTED : la participation remonte au cold-start (mesuré app, timing
un peu bruité).
- N'ADRESSE PAS l'accueil PUBLIC : `readScopeIndex` de l'index public rend 0 même
côté écrivain même-session — le fix ouvre le repo mais l'index reste vide. Le
code d'index lib est prouvé scope-symétrique, donc la cause du public est
ailleurs (broker/store ou chemin app), À MESURER SOUS BROKER (actuellement
injoignable). Nécessité du fix elle-même non prouvée en e2e lib (broker down).
gate broker-indépendant : tsc --noEmit propre ; bun test 91 pass (worker).
Inclut le scaffolding e2e de repro reconnexion (broker/run/sdk-entry).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Observability probe for the shared-wallet isolation footgun: on one physical
wallet several virtual identities coexist, and a read must never surface a doc
scoped to another identity. When it does (B reading A's doc), the leak is
invisible in the data — it looks like a normal read. This makes it VISIBLE.
Every real read/write is logged, prefixed by the ACTIVE virtual identity
(getCurrentUser → the account the op is scoped under, NOT the constant shared
physical wallet id). Reads append the row count — a strong leak signal:
[urn:festipod:user:bob] READ did:ng:o:docA (readDoc) → 3 rows
Instrumented at the LOW common point in docs.ts: every read routes through
sparqlQuery, every write through sparqlUpdate, container creation through
docCreate. Callers pass a semantic label (readDoc|readUnion|listMyEntityDocs|
writeEntity|deposit|…) that is a lib-internal probe param, NOT forwarded to the
real `ng` (preserves docs.test.ts exact-forwarding assertions).
OFF by default → one boolean read on the hot path, zero output. On via
configure({ debugAccessLog: true }) or env NG_EVENTUALLY_ACCESS_LOG=1 (no code
change). Polyfill-era; removed at the real multi-store migration.
tsc --noEmit: 0 errors. bun test: 91 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The e2e harness files (broker.ts/run.ts/sdk-entry.ts) live outside the client
tsconfig include (src/test), so an editor's broad tsc flagged process/node:*/
playwright as unresolved. Add packages/client/e2e/tsconfig.json (extends base,
types: bun) — tsc -p e2e is exit 0; the client tsconfig stays unchanged/clean.
Drop an unused getCurrentUser import in sdk-entry.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Append to the probe section: the anchorless GRAPH ?g SCAN spans every named graph
(O(wallet) union — the reason reads are per-doc anchored, preserved) is distinct
from a constant anchored GRAPH <D> WRITE, which round-trips to the same repo (no
phantom graph, verified by packages/client/e2e/). Re-verify via the harness if the
broker version changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lib e2e harness now characterizes all three SPARQL graph shapes against the real
broker (24/24): (a) no-GRAPH anchored write round-trips; (b) an explicit anchored
GRAPH <plainNuri> ALSO resolves to the same repo (no phantom graph) — the earlier
'targets a phantom graph, does NOT round-trip' claim is FALSE on @ng-org/web
0.1.2-alpha.13; (c) an anchorless GRAPH ?g scan spans every named graph (O(wallet)
union, 32 graphs) — TRUE, re-verified.
Reconcile the comments accordingly: inbox.ts / store-registry.ts drop the false
phantom-graph justification (no-GRAPH stays as the canonical, always-safe shape — a
simplicity choice, not a round-trip necessity; re-verify via e2e if the broker
version changes). read-model.md keeps the anchorless-O(wallet) rationale (the real
reason reads are per-doc anchored) and appends a note distinguishing the variable
GRAPH ?g SCAN from a constant GRAPH <D> WRITE. No new absolute introduced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The polyfill had only fake-ng unit tests; real-broker behaviour (SPARQL graph
round-trip, doc_subscribe marshaling, inbox/index) was only ever exercised by
borrowing Festipod's harness. Add packages/client/e2e/ — a standalone Playwright
harness in the SDK's own domain: a minimal SDK page (configure() + the real
@ng-org/web ng, a window.__sdk bridge, zero Festipod domain) loaded in the broker
iframe on the real broker, authenticating with a DEDICATED wallet (name
ng-eventually-e2e, profile e2e/.playwright-profile-lib — gitignored, separate from
Festipod's). Runner: bun run e2e/run.ts (script test:e2e); not mixed into bun test.
Covers, against the real broker (23 checks, all green): docCreate; sparqlUpdate/
Query anchored default-graph round-trip; readUnion N-doc + per-doc tolerance + cap
gate; doc_subscribe initial+write+unsub and subscribeDocs per-doc isolation; inbox
post/read/watch + spoof guard; discovery submit/read/watchIndex + reserved-account
isolation; store-registry idempotency + bounded listMyEntityDocs + scope resolvers;
caps read-filter (in-memory, honestly scoped); IdentityStore.
Finding (reported, not a failure): on @ng-org/web 0.1.2-alpha.13 an anchored
INSERT DATA { GRAPH <plainNuri> {…} } DOES round-trip (no phantom graph) — several
lib comments assert the opposite; stale, to reconcile. Behaviour is unaffected (the
lib writes the always-safe no-GRAPH default-graph shape). bun test still 91 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose subscribeDoc(nuri, onChange) / subscribeDocs(nuris, onChange) wrapping the
real ng.doc_subscribe — per-document, event-driven (initial State + a Patch per
commit, local or broker-synced from a remote peer), returning a sync unsubscribe.
subscribeDocs isolates per doc (a failing/unsynced doc never aborts the others),
so it sidesteps the ORM fan-out hang (never orm_start_graph(graphs:[…])).
Replace the setInterval polling in inbox.watch() and discovery.watchIndex() with
doc_subscribe — same public contract, now push not poll. NextGraph is subscription-
first; no polling remains.
Verified against the real broker (@data harness spike): the doc_subscribe callback
marshals across the iframe RPC (@ng-org/web strips the callback and drives it via a
MessagePort — no DataCloneError) and fires on the initial push and on a real write.
Lib bun test 91 pass, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the SDK's public read surface grounded in real NextGraph: the reactive
useShape hook (subscribe/push — a doc change, local or broker-synced from a remote
peer, propagates to every subscriber; no polling) is THE recommended read path;
one-shot sparqlQuery/readUnion is the exception. Includes the write surface,
identity/scope (per-document isolation, public = owner-writes-only), and a separate
'current emulation status' section flagging where the polyfill does not yet honor
the reactive contract (entity reads one-shot + polling inbox/index watchers) as
gaps to close. Shared reference: honored by the lib, used by Festipod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align the polyfill's surface and docs with the verified NextGraph reality and
remove application-level concepts:
- Identity is an ID, not a username: AccountRecord.id, shim predicate shim:id,
normalizeId; accounts core becomes IdentityStore (set/clear/get) — the faux
login/logout framing is gone (identity is set at wallet-import time).
- Relationship/connection is an application concept, not a platform primitive
(NextGraph has no bilateral-connection primitive: grantee is unpersisted
scaffolding, cap-send is unimplemented). Remove connections.ts; caps exposes
only a directed grantRead(doc, granteeId) + a read-only protectedDocsOf(owner).
Delete the now-dead isolation.ts social-visibility axis.
- Inbox docs: NextGraph has no separate curator — the recipient's own verifier
unseals and applies each queued sealed message inline (process_inbox);
inbox_post_link is a proposed/future API. Stop attributing the emulated
curator to the platform.
- Read isolation reframed around the outcome: no cap -> empty union read;
targeted read of an unheld repo -> RepoNotFound; cap introspection
(canRead/governsRead) is emulation-only with no NextGraph API behind it.
- read-model.md corrected: the listing path is per-doc ANCHORED default-graph
queries, never the anchorless GRAPH ?g union (that is O(wallet)); the probe
section no longer claims the opposite.
- README recap table restructured (target | current NextGraph status | current
emulation); INDEX_ACCOUNT documented as reservedAccount("index") in the
sentinel namespace; de-domained generic-layer comments; softened tone.
Consumer application (Festipod) rewired separately to own the relationship
concept and feed the lib an id. Lib gates: bun test 83 pass / 0 fail, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inbox deposits and the per-(account,scope) index append were written into an
explicit GRAPH <plainNuri> named graph, which the real broker stores separately
from the repo's default graph (repo_graph_name with overlay suffix) — so an
anchored default-graph read (read-model.readDoc) never saw them and deposits
did not round-trip. Drop the GRAPH wrapper: the anchor scopes the write to the
repo's default graph, matching the read. Mocks updated accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
loadShim() read EVERY account record in the shim (SELECT over the whole anchor
graph). On a shim that accumulates accounts (the shared wallet grows), that is
O(accounts) and hangs the hot path (~90s at ensureAccount → loadShim). Same
principle as per-doc reads: never scan a shared structure.
Add resolveAccount(username): a BOUNDED SELECT anchored on the single subject
accountSubject(username) → O(1), independent of account count. Cache in a
per-account Map (cleared by resetRegistryCache). Hot paths now use it:
ensureAccount (existence check), indexInboxNuri/@index (discovery), resolveInbox
Anchor, resolveWriteGraph, createEntityDoc, listMyEntityDocs. loadShim kept only
for genuine all-accounts needs (allAccounts, the listEntityDocs fan-out fallback).
The 90s ensureAccount/loadShim hang is gone. 93 tests pass; tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README: new section with a recap table (11 rows) — for each emulated behavior:
what the consumer sees (SDK-shaped API), how it's emulated on one shared wallet,
the real NextGraph target, and the lib-only migration. Makes "emulated ≠ real,
migration is a lib-only swap" explicit.
simulation.md: opening banner that EVERYTHING in the file is emulation pending
real NextGraph; corrected the stale read-path paragraph (per-doc anchored, never
an anchorless union-scan). read-model.md: reactivity bullet aligned to per-doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a diagram + prose to simulation.md: a virtual wallet = one shim account keyed
by a virtual-wallet id; it has the 3 native stores (public/protected/private) but
EMULATED — each "store" is an index document (AccountRecord.docPublic/Protected/
Private) listing that scope's per-entity doc NURIs. Everything physical lives in
the ONE shared wallet's private store; the 3-store structure is the per-account
logical layer. At migration the index docs become real native stores.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The anchorless union query (`GRAPH ?g`) scanned EVERY named graph in the local
store (the whole shared physical wallet) → O(wallet size), slow/timeouts on a
bloated wallet. Rewrite `readUnion` to run ONE ANCHORED `sparql_query` per by-need
doc (in parallel, per-doc tolerant): an anchored query is restricted to that
repo's graph, so it is O(1) per doc, INDEPENDENT of physical-wallet size. Keep the
ReadCap defense-in-depth gate.
docs/simulation.md: new "Physical wallet vs virtual wallet" section — the physical
shared wallet is a substrate that accumulates and must NEVER be enumerated/scanned;
each user's VIRTUAL wallet (the account's scope index in the shim) is the bounded
thing you enumerate ("list my documents"), then read those docs per-doc anchored.
read-model.md / nextgraph-current-state.md updated to the per-doc anchored rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- read-model.ts `readUnion` now applies the emulated ReadCap gate (drops a
subject when its doc is governsRead && !canRead for the current identity), so
per-scope isolation holds by construction AND by filter.
- store-registry: `listMyEntityDocs(username, scope)` (current account only) vs
the all-accounts `listEntityDocs` fallback (documented as the enumeration to
avoid on the read path).
- docs: nextgraph-current-state / read-model — corrected to the SOURCE-VERIFIED
reality that the JS SDK exposes NO open/sync-by-cap primitive
(load_repo_from_read_cap is pub(crate)); in the mono-wallet all repos are
already local (same session), so the anchorless union spans them with no open
step. simulation.md: listEntityDocs+useShape({graphs}) is a fallback, not the
read path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proven against the real broker (probe): opening docs then a single anchorless
`sparql_query` with a `GRAPH ?g { ... }` body reads the LOCAL UNION of all synced
named graphs — the fast, hang-free replacement for the reactive-ORM per-document
fan-out (which aborted on any unsynced/fresh repo → 75s never-fires). New
`read-model.ts` (readUnion) exposes this; there is no reactive union query, so
listing is one-shot and consumers re-query on change. docs/read-model.md refined
with the probe finding (an explicit `GRAPH ?g` body iterates all named graphs
regardless of the anchor; the anchor only bounds the default graph). 93 tests
pass; tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Source-verified against nextgraph-rs:
- nextgraph-current-state.md: NextGraph keeps ONE local oxigraph store per
session; each synced repo is a named graph. sparql_query with NO anchor
(UserSite/None) queries the UNION of all synced graphs (set_default_graph_as
_union); with an anchor it is restricted to one repo. Union is read-only
(updates need a doc anchor). No reactive SPARQL (one-shot). Root cause of the
ORM fan-out hang: orm_start_graph opens every graph in scope; a fresh/unsynced
per-entity doc → RepoNotFound aborts the subscription → the 75s never-fires.
- read-model.md (new): the read model — events via the global index (the one
enumeration hack); everything else by following a shared graph, opened/synced,
then listed via a single anchorless union sparql_query (never the ORM per-doc
fan-out); reactivity via re-query on a doc_subscribe/ORM change signal. Plus
the minimal broker probe to confirm the union behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>