fix(client): durcir le cold-start du shim — ensureRepoOpen(anchor) avant lecture/écriture

Défensif : loadShim/resolveAccount/ensureAccount ouvrent l'anchor (private-store-root)
avant de lire/écrire le compte shim, comme le fait déjà readScopeIndex. Robustesse
same-session si l'anchor est là-mais-pas-encore-souscrit.

NB (vérifié nextgraph-rs) : sur le login broker normal, le bootstrap charge le
private-store dans self.repos AVANT de rendre la session à JS → un wallet FRAIS
retourne 0 rows (pas RepoNotFound) et provisionne. Il n'existe AUCUN primitif JS
pour ouvrir un repo *inconnu* : ce heal n'est pas un remède à un store non
bootstrappé (limite NextGraph), juste une robustesse d'ouverture same-session.

Tests : cold-start-anchor.test.ts (rouge-avant/vert-après unit) ; harness e2e
repro-fresh-wallet (mint un wallet neuf par run — comble le trou "aucun test de
démarrage à froid sur wallet vierge"). Fakes anti-fork/watch-shape honorent
désormais la barrière first-State dont dépend le heal. e2e réel 42/42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-13 15:06:30 +02:00
parent 078d675bbf
commit 3046ead08f
7 changed files with 473 additions and 6 deletions
+26
View File
@@ -235,6 +235,10 @@ export async function loadShim(): Promise<Map<string, AccountRecord>> {
}
}`;
const map = new Map<string, AccountRecord>();
// COLD-START heal: open the anchor (private-store-root) repo before the shim scan,
// so an anchored read on a fresh wallet whose anchor repo isn't yet in `self.repos`
// resolves instead of throwing `RepoNotFound`. Idempotent; see resolveAccount.
await ensureRepoOpen(anchor);
try {
const result = await sparqlQuery(s.sessionId, query, undefined, anchor, "loadShim");
// Group ALL bindings by account key first, then pick the CANONICAL doc per
@@ -281,6 +285,21 @@ export async function resolveAccount(id: string): Promise<AccountRecord | null>
const s = await session();
const anchor = await anchorNuri();
// COLD-START heal (polyfill-era): the shim lives in the private-store-root graph
// (the anchor). An anchored `sparql_query` on a repo absent from the verifier's
// `self.repos` throws `RepoNotFound` — a HARD error here, unlike per-entity docs
// which silently read 0 (the private/store target resolves through
// `resolve_target_for_sparql`, which requires the repo loaded). Open the anchor
// ONCE before reading, idempotently — the SAME open-before-read guard
// `readScopeIndex` applies to its index doc. NB (see docs/nextgraph-current-state):
// `ensureRepoOpen` (→ `doc_subscribe`) can only OPEN/subscribe a repo the session
// ALREADY holds; it cannot pull a genuinely-absent repo (no JS primitive does). On
// the real broker the private-store repo is bootstrapped into `self.repos` at
// connect, so this is a defensive same-session open — it makes the anchor read/write
// robust to a not-yet-subscribed anchor without ever being able to hide a truly
// unbootstrapped store. No-op with the unit fake ng and when the repo is already
// open. See open-repo.ts.
await ensureRepoOpen(anchor);
// `subj` is already IRI-safe (accountSubject → escapeIri); `anchor` is a
// trusted-shaped NURI → assertNuri. The query is bounded to this one subject.
const subj = accountSubject(id);
@@ -443,6 +462,13 @@ export async function ensureAccount(id: string): Promise<AccountRecord> {
const s = await session();
const anchor = await anchorNuri();
// COLD-START heal: open the anchor (private-store-root) repo before PROVISIONING
// into it. The provision `sparqlUpdate` targets the private-store graph, which
// resolves through the same repo-loaded requirement — an unopened anchor repo on
// a fresh wallet would throw `RepoNotFound` and the account would never persist.
// Idempotent; a no-op once the repo is open (resolveAccountReliably already
// opened it on the read above in the common path). See open-repo.ts.
await ensureRepoOpen(anchor);
const subj = accountSubject(id);
// `subj` is already IRI-safe (accountSubject → escapeIri). `anchor` is a
// trusted-shaped NURI → assertNuri. `id` is UNTRUSTED text in a LITERAL