Commit Graph

14 Commits

Author SHA1 Message Date
Sylvain Duchesne 5a7009bd75 fix(inbox): une inbox appartient à un document, jamais à plusieurs
Retour sur l'adresse par défaut livrée en 8a382f2, qui faisait pointer tout
document vers l'inbox de son propriétaire. C'était acheter le coût au prix de
la forme — le mauvais arbitrage pour cette bibliothèque.

Vérifié en amont : le verifier route un message entrant par
`inboxes: PubKey → RepoId` (`engine/verifier/src/verifier.rs:1677,1928`) et le
déchiffre avec la moitié privée de CE repo. Et `InboxMsgBody`
(`engine/net/src/types.rs:4265`) ne porte aucun document cible — il n'en a pas
besoin : l'adresse EST l'identification. Une inbox appartient donc à exactement
un repo, et faire tenir plusieurs documents derrière une inbox émule une
relation que le modèle ne peut pas exprimer.

Conséquences :

- `createEntityDoc` ne publie plus rien. Un document neuf n'a pas d'inbox et
  `documentInboxAddress` rend `undefined`.
- Une inbox s'ouvre par `openDocumentInbox(doc)`, sur décision du propriétaire.
  C'est aussi ce qui règle le coût sans toucher à la forme : seuls les
  documents destinés à RECEVOIR en paient une — l'app le sait, la bibliothèque
  non.
- `inbox.postToDocument(doc, { payload })` : l'app nomme le DOCUMENT, jamais une
  inbox. Lève quand le document n'en a pas, au lieu de rendre la main
  silencieusement — un dépôt qui disparaît sans erreur est exactement le bug que
  ce chemin traînait.
- Pas de champ « document cible » sur un dépôt. Ce serait une invention que les
  apps devraient désapprendre à la migration.

README, principe de conception : les deux moitiés sont contraignantes, et c'est
la seconde qu'on brade. La surface doit être au plus près du futur SDK, mais
l'IMPLÉMENTATION aussi doit être au plus près de ce que NextGraph prévoit, sans
exception. Ce qui est connu vaut spécification. La pression à dévier ne se
présente jamais comme une déviation : elle arrive comme un coût, une latence,
une gêne d'ergonomie — bien réels. Deux cas déjà rencontrés sont consignés, avec
le signal commun : un choix qui ferait apprendre au consommateur quelque chose
qu'il devra DÉSAPPRENDRE.

157 tests unitaires, e2e 40/40 contre le broker en ligne.
2026-08-03 16:45:28 +02:00
Sylvain Duchesne 8a382f29f8 feat(inbox): l'inbox d'un document est adressable par tout détenteur
Répond au brief 2026-08-03 remonté depuis le consommateur. `documentInbox(doc)`
répondait « quelle inbox est-ce que MOI je connais pour ce document » et en
créait une quand la réponse était « aucune » : un tiers n'atteignait jamais
l'inbox du propriétaire, il en obtenait une à lui, que personne ne lit, et son
dépôt disparaissait sans erreur. C'est l'acte central du consommateur —
s'inscrire à l'événement d'un autre — qui était silencieusement perdu.

Lire une inbox et savoir où y déposer sont deux actes opposés, avec des publics
opposés. Ils sont désormais deux fonctions :

- `openDocumentInbox(doc)` — le PROPRIÉTAIRE ouvre une inbox dédiée. Refuse sur
  la PROPRIÉTÉ (lue depuis les branches Store), pas sur la possession du cap :
  un cap se reçoit, et un destinataire ne doit pas pouvoir rediriger vers lui
  les dépôts destinés au propriétaire.
- `documentInboxAddress(doc)` — n'importe quel détenteur trouve où déposer. Ne
  crée jamais rien.

L'adresse est publiée dès la CRÉATION, sur la branche Header émulée du document
— un sujet réservé à l'intérieur du document, donc lisible par qui détient le
document. Publier seulement le jour où le propriétaire ouvre une inbox dédiée
laisserait une fenêtre pendant laquelle un tiers lit le document, ne trouve
aucune adresse, et ne peut pas joindre le propriétaire du tout.

Sur le coût mesuré par le brief (9m37 → 21m30) : il venait de la création d'un
DOCUMENT supplémentaire par document. L'adresse publiée pointe vers l'inbox
propre du propriétaire, qui existe déjà et s'amortit sur tous ses documents ;
la création grandit d'un triple, pas d'un document. Le dépôt porte le document
concerné, donc le propriétaire matérialise toujours par document. La forme
« dérivable » du brief n'était pas disponible : notre inbox est un document, et
un NURI dérivé nommerait un repo que `doc_create` n'a jamais créé.

Le tout reflète la séparation d'amont : un déposant scelle avec la clé PUBLIQUE
de l'inbox et n'a besoin de rien d'autre, seul le propriétaire détient la
moitié privée — une adresse est donc publique par nature.

`src/machinery.ts` : l'espace de noms `urn:ng-eventually:` que la bibliothèque
se réserve, et le prédicat que le chemin de lecture utilise. La branche Header
est le premier compartiment logé dans un document que le consommateur lit ;
`read-model` écarte désormais tout sujet de cet espace, par SUJET et non par
prédicat — ce qui couvre toutes les branches émulées, présentes et futures.

Question ouverte du brief, tranchée : « une inbox de document adressable par
tout détenteur » est une invention de cette bibliothèque, pas de l'amont — aucun
document n'y a d'inbox, ni le store privé. Ce qui EST vérifié, c'est la forme
qui rend l'anticipation défendable : `AddInboxCapV0` est clé par `repo_id`.

Tests : le test qui validait « n'importe qui dépose » passait le NURI d'inbox au
déposant par une variable du test — chemin qu'aucune app n'a. Réécrit avec les
deux acteurs cloisonnés : le déposant reçoit le lien du document, qui est la
seule chose qui circule dans ce modèle, et doit trouver l'adresse lui-même. Le
fake `ng` gagne le SELECT de la branche Header et le `DELETE WHERE` (sans quoi
un remplacement devenait une accumulation, précisément le bug qu'il évite).

157 tests unitaires, e2e 40/40 contre le broker en ligne.
2026-08-03 16:02:11 +02:00
Sylvain Duchesne 33751388fe Ask that a document's inbox be addressable by anyone holding the document
`documentInbox(doc)` answers "which inbox do I know for this document?" and mints
a fresh one when the answer is none. A third party therefore never reaches the
owner's inbox: they get one of their own, the owner never reads it, and the
deposit vanishes with no error.

That breaks the consumer's central act — signing up to someone else's event —
and it blocks the directory document that has to replace discovery, since
creators must deposit into a document they do not own.

The consumer tried solving it on its side: resolve the address once at creation
and publish it on the public document, so participants read it instead of
deriving it. It works, and it is still the wrong place. It puts a technical
address into a domain shape; it doubles document creation on every event, since
resolving an inbox creates one (the @data suite went from 9m37 to 21m30, with
sign-up scenarios timing out during setup on a FRESH profile — not wallet
bloat); and the library already claims this job, having stated when per-document
inboxes landed that draining them "is the library's job, not the app's".
Addressing is the other half of that same job. The app-side change is reverted.

No shape is prescribed here. Deriving the address from the document would cost
nothing and match how the rest of the model works — an overlay is derived, a
keyring is looked up, nothing is handed over out of band — with the read guard
keeping the asymmetry where it belongs. Publishing it library-side would work
too and leaves room to make it lazy.

One question decides where this note ultimately belongs: is "a document has an
inbox addressable by any holder" part of NextGraph's target model, or this
library's own invention? Upstream, it is emulation to align; otherwise it is a
design decision to take deliberately.
2026-08-03 14:47:13 +02:00
Sylvain Duchesne 88f396a7ac fix(caps): créer un document en donne le cap, + corriger 9 faits NextGraph
Le trou trouvé par l'e2e contre le broker en ligne : `docs.docCreate` ne
déposait aucun cap pour le créateur, donc un consommateur pouvait créer un
document par la primitive publique puis se voir refuser sa lecture et son
écriture. En amont c'est impossible — `doc_create` commite
`AddRepo { read_cap }` sur la branche Store du store, et le créateur le détient
dès le premier instant. Délibérément non répliqué dans `physical.ts` : les
documents du shim n'appartiennent à aucun utilisateur virtuel, et
`store-registry` classe leurs caps là où il sait à qui ils sont.

e2e : 22 passés / 8 échoués → 39 / 0. Les autres échecs venaient du harnais,
qui agissait comme une seconde identité sans l'établir, ou lisait un document
quelconque comme une inbox. Un run e2e contre un wallet persistant exige une
identité FRAÎCHE par run : `walletInbox(id)` rend l'inbox stable pour son
propriétaire — c'est son intérêt — donc un id fixe accumule les dépôts des runs
précédents (vert au 2e run, rouge au 3e, à code inchangé).

Revue adverse de la documentation, 9 défauts, tous vérifiés à la source avant
correction :

- « chaque document a une inbox native » est FAUX. Seuls les repos de store
  public et protected en ont une (`site.rs:128,149`) ; `new_store_default` n'en
  pose que `if !private` et `doc_create` laisse `inbox: None`. Le store privé
  n'en a pas non plus. Ce que le code fait est donc une ANTICIPATION — assumée
  et notée comme telle dans `documentInbox`, le brief et l'ADR discovery. Ce qui
  est vérifié, c'est la FORME : `AddInboxCapV0` est clé par `repo_id`.
- `InboxMsgContent::Link` est une variante unit sans charge utile : l'inbox ne
  transporte aucun ReadCap. `shareCap` était juste et le reste ; ses citations
  sont complétées aux deux bouts (émetteur `unimplemented!()`, récepteur qui
  ignore `details.read_cap`).
- les 3 stores appartiennent au user (`SiteV0`), pas au wallet ;
- le TODO `OpenRepo` ne concerne pas la lecture cross-wallet — il est dans
  `open_branch_`, après `RepoNotFound` ; charger par cap, c'est
  `load_repo_from_read_cap` ;
- la liste des méthodes JS était un sous-ensemble présenté comme la surface
  (77 exportées) ;
- `outbox-log.ts` n'enregistre rien : il inspecte l'outbox du SDK ;
- l'ADR private-store-nuri-scope citait `orm_start_graph` au présent, remplacé
  par `ensureRepoOpen` ;
- l'incident write-loss plaçait `disconnections_sender.send` dans `broker.rs` ;
- la section « Apps & services » n'a aucune citation et rien ne lui correspond
  dans le moteur : marquée à re-confirmer, pas à citer comme vérifiée.

Aussi : `fileOwnCaps` n'existe plus (`holdOwnCap` / `readStoreCaps` /
`fileOwnStructure`) — pointeur mort corrigé dans `caps.ts`.
2026-08-03 12:17:40 +02:00
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 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