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>
5.1 KiB
Migration guide — when real NextGraph matures
The whole point of this library: the consumer application already writes SDK-shaped code, so when NextGraph ships cross-wallet reads, capabilities and inboxes, only this lib changes. The consumer application's code does not change. This is the checklist.
Guiding invariant
Every emulated piece has a 1:1 image in the real infra. Migration = swap the
emulation for the real primitive, remove the scaffold. If a piece of the emulation
has no clear target image, that is a drift signal (see
simulation.md).
Checklist
1. Emulated ReadCaps → real capabilities
Translate the per-document CapRegistry (caps.ts) into real NextGraph caps: the
broker/verifier enforces them, and useShape already returns only authorized
documents. The directed grantRead(doc, granteeId) maps to a native per-document
ReadCap issued to that identity. The read filter (read-filter.ts) and the write
guard (ng-proxy.ts sparql_update override) are then dead code — remove them. The
access unit is already the document (@graph), matching the native per-repo cap
model, so this is a data step, not a reshape.
2. Place documents in real native stores
Today docCreate(..., undefined) writes every document into the shared wallet's
private store, and the public|protected|private scope is a logical label
in the shim (see the two-axes section in simulation.md).
doc_createcannot target a non-private native store today — verified:StoreRepois not JS-constructible from the SDK, so there is no way to pass a public/protected store as the create destination (docCreate's trailingstorearg is leftundefined→ private store). The private store works only because it opens withoutRepoNotFound.- When the SDK lets you construct/target a native store, the migration adds a
getNativeStore(scope)-style resolver returning the real store to pass as thedocCreatedestination, so the logical scope label becomes a real store placement. (No such helper exists yet — it is blocked on the SDK gap above.) - At that point
store-registry.tsmaps(account, scope)to the user's real store NURI instead of a document in the shared wallet; the per-scope index document (the store-container emulation) is replaced by the store itself. The surface facing the consumer application (createEntityDoc,listEntityDocs, resolvers) is designed to survive that swap unchanged.
3. Drop the resolver / shim
The sharedWalletShim (account → 3 scope-document NURIs, held in a subscribable
doc-shim reached via a write-once pointer in the store-root — see
nextgraph-current-state.md § The pointer → doc-shim
indirection) has no target equivalent — the target has no central directory. Remove
it entirely: store-registry.ts, configureStoreRegistry, the pointer + doc-shim +
migration SPARQL, and the pointerGuard dep. Cross-wallet reads replace the fan-out;
per-user wallets replace the shared one.
4. Real inbox → drop the in-lib read emulation
Replace the emulated inbox.ts deposit (docs.sparqlUpdate into a shared-wallet
document) with the native inbox_post_link (proposed/future). On the read side the
recipient's own verifier unseals each queued sealed message and applies it inline
when it processes its inbox — there is no separate curator to build; the in-lib read
emulation simply goes away (see the deferred global-index note in the top-level
README and decisions/discovery-model.md). The
single global index replaces the cross-account fan-out.
5. Retire the identity store → real per-user login
Remove accounts.ts (the IdentityStore that persists the identity id in
localStorage) and the app-level "Connexion" screen. The technical broker gate
becomes the real per-user login
(see decisions/shared-wallet-login-flow.md).
The flow shape ("broker redirect → app") does not change.
6. Drop the isolation scaffold
isolation.ts (application-visibility scaffold) disappears against a
different piece of infra than the caps: real per-account wallets, and the
relationship concept the consumer application owns. Distinct axis from ReadCaps —
remove independently.
7. Remove the build alias — the client becomes the real SDK
The consumer application imports @ng-org/web / @ng-org/orm resolved to this lib
via a build alias during the polyfill period. Removing the alias makes those imports
resolve to the real SDK — the ng/useShape/inbox surface is SDK-identical, so
no consumer code changes. The one non-SDK call — configure(...) /
@ng-eventually/client/polyfill — is deleted. The lib itself disappears.
What does not change
The consumer application's code. Shapes, screens, the acts of granting access, entity→scope mapping, the relationship graph — all injected, all untouched. Migration is entirely inside this library plus removing the alias + the bootstrap call. That asymmetry — a mature SDK face outward, all compensation inward — is the library's reason to exist.