Commit Graph

29 Commits

Author SHA1 Message Date
Sylvain Duchesne 9d3e2d2bfe Fix documentation defects found by an adversarial review
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.
2026-08-03 11:34:24 +02:00
Sylvain Duchesne ae9c32e271 Align the cap emulation on NextGraph's model, and confine it to a virtual user
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.
2026-08-03 11:22:01 +02:00
Sylvain Duchesne 6f0d0586e2 docs(brief): passer le brief caps en anglais et solder trois incohérences
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
2026-07-28 15:59:37 +02:00
Sylvain Duchesne 0d52c82ba9 docs: passer vision, readcap-and-nuri-model et l'incident en anglais
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
2026-07-28 15:51:22 +02:00
Sylvain Duchesne 518292498a docs(brief): extraire P1a dans son propre brief, en anglais
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
2026-07-28 15:44:24 +02:00
Sylvain Duchesne b2cb774124 docs: état courant NextGraph enrichi + modèle cible aligné + retrait du lot PW
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
2026-07-27 17:42:14 +02:00
Sylvain Duchesne 8764daff4f docs: corriger la rotation de clé et poser le principe du store public
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
2026-07-27 17:05:36 +02:00
Sylvain Duchesne 60a9fd3ede docs: réécrire P1a après double revue adverse, corriger le verdict Q1 sur-lu
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
2026-07-27 16:08:02 +02:00
Sylvain Duchesne d7e0ee6a4b docs(brief): spécifier P1a (la surface) et corriger l'inventaire des contournements
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
2026-07-27 14:48:48 +02:00
Sylvain Duchesne ead5aececf docs: l'overlay est store-scopé ; retrait de la fausse piste "membership"
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
2026-07-27 12:07:24 +02:00
Sylvain Duchesne 1791c31f42 docs: vision du polyfill (shape-fidelity), modèle ReadCap/NURI réel, brief caps
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
2026-07-27 11:30:33 +02:00
Sylvain Duchesne 127ca3159e docs: known issues (perte écriture, réhydratation à froid, écho auto-écriture) + gap 4 sdk-reference
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
2026-07-20 13:16:51 +02:00
Sylvain Duchesne 138d37c02f docs(incident): perte d'écriture sur mort de socket (SerializationError)
Post-mortem 2026-07-14 (ouvert) : symptôme + preuves Firefox verbatim, chaîne causale tracée (socket→Disconnected→reconnexion en TODO), réserve (i) perte-écriture vs (ii) réhydratation à froid, repro @data décisive (test de reconnexion existant faux-vert = lecture IndexedDB locale).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-14 18:58:38 +02:00
Sylvain Duchesne 3547967d37 chore(client): retirer la migration legacy + alléger les logs d'accès
- 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>
2026-07-13 18:08:30 +02:00
Sylvain Duchesne 5e91771da6 fix(client): résolution de compte barrière-autoritative — fin du fork à la reconnexion
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>
2026-07-13 17:46:16 +02:00
Sylvain Duchesne f7dd9223e0 docs(read-model): distinguish the GRAPH ?g scan from a GRAPH <D> write
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>
2026-07-07 09:39:42 +02:00
Sylvain Duchesne 63ecfeeff8 docs+refactor(client): fidelity pass — id identity, drop connections, no faux-login, accurate NextGraph framing
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>
2026-07-06 14:02:16 +02:00
Sylvain Duchesne 5717e08f6d docs: "what is emulated (and how it goes away)" recap table + read-path reconcile
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>
2026-07-06 11:18:10 +02:00
Sylvain Duchesne b15229fbd5 docs: explicit virtual-wallet structure — the 3 emulated stores
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>
2026-07-06 10:15:06 +02:00
Sylvain Duchesne ac2b026955 feat(client): read via per-doc ANCHORED queries; document virtual vs physical wallet
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>
2026-07-05 22:50:14 +02:00
Sylvain Duchesne 6a3501e700 feat(client): union-read ReadCap gate + listMyEntityDocs + doc corrections
- 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>
2026-07-05 20:49:01 +02:00
Sylvain Duchesne e24f52749f feat(client): read-model surface — open/sync + anchorless union sparql_query
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>
2026-07-05 18:20:09 +02:00
Sylvain Duchesne 5acc07a7e3 docs: query capability (local-union sparql_query) + the read model
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>
2026-07-05 17:32:22 +02:00
Sylvain Duchesne b1d06b68b9 fix(client): per-entity write round-trip + dedicated inbox anchor
Real-broker validation of the per-document model surfaced round-trip breaks the
fake-ng unit tests missed:
- The inbox anchor was the shim graph itself, making loadShim ~60s; give the
  inbox its own document so anchor resolution is fast and isolated.
- store-registry adjustments so per-entity documents created via the SDK are
  indexed and readable back through the scope fan-out.
docs/simulation.md updated. 89 tests pass; tsc rc=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 17:26:31 +02:00
Sylvain Duchesne bf753770b8 feat(client): real per-document isolation + bilateral connections + deposit guards
The ReadCap filter now enforces on per-entity documents (consumers create one doc
per entity, so each has a declared policy — private→owner, protected→owner+
connections, public→all). Isolation is genuinely active, not dormant.

- connections.ts (new): a BILATERAL connection registry — a link grants protected
  read only when BOTH sides have asserted it (each assertion bound to its author).
  A unilateral/self-declared connection grants nothing (closes the confused-deputy
  hole). declareConnections is authenticated to the current identity.
- inbox.post: `from` is bound to the current identity — a spoofed `from` throws.
- discovery.submitToIndex: PUBLIC-ONLY — a governed non-public doc is refused
  (no protected/private leak into the world-readable index).
- docs/simulation.md: documents this as application-level emulated isolation on a
  shared wallet (not crypto); at NextGraph maturity → real caps, consumer unchanged.

89 tests pass (+10 covering: active protected isolation via bilateral connect,
unilateral grants nothing, from-spoof rejected, non-public submit refused). tsc rc=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:40:44 +02:00
Sylvain Duchesne 9951cd5223 feat(client): discovery via a global index (special @index account)
Add a generic discovery-index surface: submitToIndex(ref) deposits a reference
into the index document's inbox; readIndex() returns the materialized entries. A
reserved special account (@index) owns the index document; deposits flow through
the emulated inbox and are materialized by the emulated curator (the dedup/
moderation point). This replaces cross-account fan-out as the discovery path and
is more faithful to the target (a single owned index fed via its inbox). Generic
(the consumer supplies the reference to index). 79 tests pass; tsc rc=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 09:33:42 +02:00
Sylvain Duchesne 7db5eef33f feat(client): activate ReadCap isolation via current identity + connections
Isolation was dormant (no current identity ever set). Now: setCurrentUser
records who is reading; declareConnections(neighborsOf) grants each protected
document's read cap to owner + connections. Reads discriminate through the
ReadCap filter: private→owner, protected→owner+connections, public→all. Generic
(the consumer injects identity + connections). Write-guard coverage limits
documented honestly in docs/simulation.md (real write paths bypass the JS proxy;
full enforcement awaits native caps). isolation-active.test.ts proves the
protected+connections path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:59:27 +02:00
Sylvain Duchesne e0d88b5076 feat(client): SDK-shaped scope resolvers (resolveScopeGraph/resolveInboxAnchor)
Expose a clean scope-based surface so consumers work by scope (public/protected/
private) and never see a physical store id — the library resolves placement and
performs the shared-wallet simulation internally. RegistrySession gains optional
protected/public store ids, supplied at the single injection point
(configureStoreRegistry). Zero domain knowledge. docs/simulation.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:41:34 +02:00
Sylvain Duchesne bea9f51d91 docs: own the current-NextGraph-state knowledge + boundary (lib side)
This library presents a mature-NextGraph SDK face to consumers while
compensating for the current SDK's gaps via a shared-wallet simulation. It
therefore OWNS all current-state + simulation knowledge — moved here out of the
Festipod app repo, which must treat this library as a finished SDK.

New docs/:
- nextgraph-current-state.md — what the current SDK/broker do and don't expose
  (5 store types, document=repo, per-document ReadCap, inbox not exposed, iframe
  RPC proxy, mono-user/no-global-data, wallet import constraint). Keeps the
  nextgraph-rs source pointers.
- simulation.md — how the lib emulates the mature behaviour on one shared wallet
  (shim, store!=document two axes, docCreate→private store, RepoNotFound scope
  rule, @ng-org double-proxy DataCloneError, emulated ReadCap/inbox/curator).
- decisions/ — the current-SDK ADRs (private-store-nuri-scope, sparql-delete,
  shared-wallet-login, discovery mechanism).
- fork-inbox-fallback.md — the Rust-patch/self-host route not taken.
- migration-guide.md — the checklist for when real NextGraph matures.

README: boundary framing from the lib's side + docs/ index; replaced the stale
"scaffold/stubbed" status with the actually-implemented mechanisms per source.

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