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>
This commit is contained in:
Sylvain Duchesne
2026-07-13 17:46:16 +02:00
parent 5cdc6ce77f
commit 5e91771da6
17 changed files with 744 additions and 351 deletions
+16 -6
View File
@@ -98,12 +98,22 @@ public/protected/private stores — on top of one shared wallet.
`docs.docCreate` primitive. The `scope` (`public|protected|private`) is a
logical attribute tracked here, not a physical store.
- **The `sharedWalletShim`** is the mapping `account → its 3 scope-document
NURIs`, persisted as RDF in the shared wallet's private store (the anchor,
always known from the session: `RegistrySession.privateStoreId`). That makes
identity resolution cross-device: another device opening the same wallet reads
the same shim and finds the same accounts. It is the account→document trust root,
which is why every untrusted value that reaches its SPARQL is escaped (see
SPARQL hardening below).
NURIs`. It is persisted as RDF, but **not directly in the store-root graph** — it
lives in a subscribable **doc-shim** reached through a write-once **pointer** in the
store-root, an indirection forced by a NextGraph fact: "findable-without-lookup"
(store-root) and "subscribable / cold-read-authoritative" (`did:ng:o:` repo with a
first-`State` barrier) are DISJOINT. The pointer (findable) names the doc-shim
(authoritative); resolution reads the pointer from the store-root, opens the doc-shim
through its barrier, and reads the account authoritatively — so a fresh reconnecting
session never mistakes sync-lag for "account absent" (which would provision a FORK).
Full rationale — including why the old account-level retry (`provisionRetry`) is
removed and how legacy store-root records are migrated forward — is in
[`nextgraph-current-state.md`](./nextgraph-current-state.md) §§ *Findable vs
subscribable* / *The pointer → doc-shim indirection*. This map is the
account→document trust root, which is why every untrusted value that reaches its
SPARQL is escaped (see SPARQL hardening below). It makes identity resolution
cross-device: another device opening the same wallet reads the same pointer → the
same doc-shim → the same accounts.
- **Per-entity documents + per-scope index.** `createEntityDoc(id, scope)`
makes a dedicated document for one entity (mirrors the target, where each entity
is its own document/repo with a future inbox) and appends its NURI to the