Assertion fausse retirée de `openDocumentInbox` : « en amont, l'acte équivalent
est le propriétaire qui commite AddInboxCap avec la clé du repo — personne
d'autre ne le peut ». Personne d'autre ne le peut est inventé. Ce commit atterrit
sur la branche User de CELUI QUI LE FAIT, donc n'importe qui peut en écrire un
nommant le repo de n'importe qui. Le moteur ne pose aucune garde là-dessus.
Ce qui protège en amont n'est pas une garde, c'est le mode de circulation :
`inboxes: PubKey → RepoId` est une table du Verifier (`verifier.rs:105`),
reconstruite vide à chaque session — l'association inbox→repo est LOCALE, pas
publiée. Un déposant apprend une pubkey parce qu'on la lui a ENVOYÉE : dans un
`ContactDetails` (`contact.inbox`) ou par le QR de profil. Une paire forgée
n'atteint personne, faute que quiconque en ait été informé.
D'où une divergence à assumer et non à maquiller : nous PUBLIONS l'adresse sur
le document, seul moyen qu'un tiers la trouve dans une émulation sans canal de
messages. Cela crée un vecteur que le moteur n'a pas — qui peut écrire le
document peut rediriger ses dépôts — et c'est ce que la garde `ownsDocument`
compense. Elle compense NOTRE conception ; elle ne reproduit aucune règle amont.
Manquait aussi dans la carte des inbox dressée juste avant : elle disait qui A
une inbox, et omettait comment l'adresse circule — la dimension dont tout le
reste dépend.
Glissement à corriger : de « le moteur ne le FAIT pas » (vrai) j'ai conclu
« c'est notre anticipation » (faux). Le moteur le PERMET, et de façon générique
par conception :
- `inbox: Option<PrivKey>` est un champ de TOUT `Repo` (`repo.rs:126`), pas
d'une structure de store ;
- `AddInboxCapV0` est clé par `repo_id` — « Repo the Inbox is opened for » ;
- `update_inbox_cap_v0` l'applique via `self.repos.get_mut(repo_id)` sans
AUCUNE vérification `is_store` (`verifier.rs:1920`) ;
- et à tout moment, `AddInboxCap` étant un commit de branche User dont le type
documente le cas de mise à jour.
Ce qui est vrai est plus étroit : aucun chemin de code n'en CRÉE une pour un
document. « Ne fait pas » n'est pas « ne peut pas » — c'est précisément
l'interdit que ma propre règle pose, et je l'ai enfreint en le formulant.
Donc l'inbox par document s'aligne sur le modèle du moteur (niveau 1) ; ce qui
est de nous est la surface JS, aucune n'étant exposée aux niveaux 2 et 3.
Corrigé dans store-registry.ts, nextgraph-current-state.md et les deux briefs.
Vérifié parce qu'elles ressemblent à des appels « ouvrir un repo » tout faits, ce
qui aurait rendu notre `ensureRepoOpen` redondant. Elles ne le sont pas : ni
l'une ni l'autre ne fait d'I/O — chacune construit un `AppRequest` et le renvoie
sérialisé (`lib.rs:1890`, `:1900`), sans session_id ni callback. `doc_subscribe`
construit la MÊME requête (`AppRequest::doc_fetch_repo_subscribe` →
`Fetch(Subscribe)`), y ajoute la session et la passe à `app_request_stream_`.
Ce sont des constructeurs de requête pour qui veut dépêcher lui-même.
Donc rien à récupérer : les employer reviendrait à réimplémenter ce que
`doc_subscribe` fait déjà. Et `doc_fetch_private_subscribe` ne diffère que par
sa cible (le store-root privé), ce qui ne change rien à la barrière — un
store-root n'en a toujours pas.
Le symbole n'existe nulle part dans `nextgraph-rs`, et aucune méthode de
`@ng-org/web` ne contient « inbox ». Il vient de notre propre plan de fork
(`docs/fork-inbox-fallback.md:32` — « expose `pub async fn inbox_post_link` »),
d'où il a essaimé dans huit autres endroits, cité comme une API « proposed/
future » de NextGraph. Une proposition interne devenue un fait par répétition —
le même mécanisme que « chaque document a une inbox native » et que l'inbox
mutualisée.
Corrigé partout sauf dans le plan de fork, où le nom est légitime puisque c'est
lui qui le propose. Et l'énoncé exact est désormais posé : on ne connaît NI le
nom NI la forme de la future surface JS pour les inbox — ce n'est pas
« non implémenté », c'est inconnu.
Ce qui est réellement vérifié côté moteur : `AppRequestCommandV0::InboxPost`
existe et `AppRequest::inbox_post()` le construit, mais le request_processor n'a
aucun arm pour lui — l'envoyer ne déclenche rien. Le seul dépôt qu'un client JS
peut provoquer aujourd'hui passe par `import_contact_from_qrcode`, qui appelle
`post_to_inbox(InboxPost::new_contact_details(...))` avec `with_readcap = false`
— la variante `true` étant `unimplemented!()`.
Réécriture de « Apps & services » : la version précédente déduisait la forme
cible de l'absence d'implémentation dans le moteur — exactement ce que le
principe de conception du README interdit — et concluait l'inverse de ce que le
développeur NextGraph énonce.
Deux couches désormais séparées et étiquetées comme telles :
- ce que le moteur CONTIENT (vérifié) : `AppManifestV0` avec `singleton: bool`,
`access_requests`, `installs`, `dependencies` ; `init(callback, singleton,
access_requests)` côté JS. Et personne ne le consomme — le module
`permissions` n'est importé par aucune crate, `AppManifest` n'est construit
nulle part. Du vocabulaire, pas du comportement.
- ce que le modèle SERA (énoncé par le développeur, non implémenté) : une app
singleton peut aussi gérer les documents par utilisateur ; les données
communes prennent la forme d'un document ou d'un store partagé par tous les
utilisateurs et codé en dur dans l'app ; le développeur détient les droits
d'écriture et peut les déléguer, jamais à tous — les contributions arrivent
par une inbox.
Le commentaire du champ dit `/// cannot create Documents?`, avec le point
d'interrogation. Une seconde glose publiée le contredit et rejoint ce que dit
le développeur : `sdk/js/web/README.md:90,108` annote l'argument « will your app
create many docs in the system, or should it be launched as a unique instance »
— `singleton` porte sur la multiplicité d'instance, pas sur une interdiction de
créer des documents. C'est cette lecture qu'il faut retenir.
Conséquence sur l'ADR discovery : sa moitié « la voie app singleton est
incertaine » est caduque et doit être re-posée, sans que cela rétablisse la
découverte, qui tient sur son propre appui.
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`.
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.
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
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
- 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>
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>
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>
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>
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>