88f396a7ac
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`.
60 lines
3.1 KiB
Markdown
60 lines
3.1 KiB
Markdown
# ADR — Use a store NURI as the `useShape` scope AND `@graph`
|
|
|
|
**Date:** 2026-03-17 · **Status:** Accepted (partially superseded — see below).
|
|
Historical decision, ported into this lib because the *insight* still governs how
|
|
the shim opens repos. Original context: the consuming app.
|
|
|
|
> **Partially superseded (2026-07-03).** The private-store-only scope was replaced
|
|
> for shareable domain entities: they are now scoped AND written to the
|
|
> **protected** store (`did:ng:${protected_store_id}`), verified to open without
|
|
> `RepoNotFound`. **The central insight of this ADR still holds** and now applies
|
|
> to **both** stores: you must open the repo via the store's NURI
|
|
> or you get `RepoNotFound`. *(How it is opened has since changed — see the note
|
|
> under Decision.)*
|
|
|
|
## Context
|
|
|
|
Loading test data updated the in-memory ORM signals (immediate UI) but produced
|
|
`RepoNotFound` on `doc_create` and `orm_frontend_update`. Data vanished on reload
|
|
because the SPARQL writes never reached the broker: the verifier's `self.repos`
|
|
HashMap did not contain the store's repo → `resolve_target()` failed.
|
|
|
|
## Options considered
|
|
|
|
### A — `did:ng:i` scope + `doc_create` for `@graph`
|
|
`did:ng:i` is documented as a subscription scope; `doc_create` returns a real
|
|
NURI. **Against:** `did:ng:i` goes through `NuriTargetV0::UserSite`, which does
|
|
NOT open individual repos; `doc_create` calls `resolve_target(PrivateStore)`,
|
|
which requires the repo already in `self.repos` → fails; needs complex retry/timing.
|
|
|
|
### B — the store NURI as scope AND `@graph` (chosen)
|
|
Exact copy of the working `expense-tracker-rdf` example: `orm_start_graph` with
|
|
the store's NURI opens the repo in `self.repos`; subsequent `orm_frontend_update`
|
|
finds it. Simple, no retry. **Against:** slightly less flexible than `did:ng:i`
|
|
(scoped to one store); requires passing the session down to the ORM hook.
|
|
|
|
### C — `did:ng:i` scope + reuse an existing entity's `@graph`
|
|
Works for users who already have data. **Against:** fails for empty wallets (no
|
|
entity to reuse) → falls back to `doc_create` and the same `RepoNotFound`.
|
|
|
|
## Decision
|
|
|
|
**Option B**: use the store NURI as both the `useShape` scope AND the write
|
|
`@graph`, exactly like `expense-tracker-rdf`. This is why this lib's shim opens the
|
|
store repo before writing, and why **`did:ng:i` must never be used as a scope** (it
|
|
breaks writes with `RepoNotFound`). See the scope rule in
|
|
[`../simulation.md`](../simulation.md).
|
|
|
|
*The decision stands; the mechanism named in it has been replaced.* Opening was
|
|
`orm_start_graph` when this was written. It is now `ensureRepoOpen` — `doc_subscribe`
|
|
plus a wait for the first `State` (`packages/client/src/open-repo.ts:167`) — after
|
|
`orm_start_graph` was found to hang on a fan-out (`subscribe.ts:28,181`). What must be
|
|
read here is the invariant *"open the repo, by its store NURI, before writing"*, not the
|
|
call that used to implement it.
|
|
|
|
## Consequences
|
|
|
|
- **Positive:** immediate writes after connect (no retry); persistence across
|
|
reload; aligned with the official examples.
|
|
- **Risk:** if NextGraph changes the store's open behaviour, this breaks.
|