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
+7 -6
View File
@@ -86,12 +86,13 @@ configureStoreRegistry({
},
// Identity normalization used as the shim key (lowercase, strip leading `@`).
normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(),
// REAL broker: enable the anti-fork bounded retry. On a faithful reconnect the
// shim (private-store graph) may not be synced when the first read fires → 0
// rows; without the retry the registry would provision a NEW account (a fork),
// stranding session 1's data. Bounded backoff waits the sync-lag window out
// before concluding "genuinely new" (CONTRACT 2 non-fork).
provisionRetry: { attempts: 8, baseMs: 150, maxStepMs: 2000 },
// REAL broker: enable the POINTER micro-guard. The account records live in a
// subscribable doc-shim reached via a write-once pointer triple in the store-root;
// the account read is barrier-authoritative (no account retry). The only residual
// store-root sync-lag is the pointer read — this bounded guard re-reads JUST that
// one write-once triple on a cold reconnect (CONTRACT 2 non-fork). It can never
// provision or fork an account.
pointerGuard: { attempts: 8, baseMs: 150, maxStepMs: 2000 },
});
// ── Bridge marshaling note ─────────────────────────────────────────────────