feat(client): read via per-doc ANCHORED queries; document virtual vs physical wallet

The anchorless union query (`GRAPH ?g`) scanned EVERY named graph in the local
store (the whole shared physical wallet) → O(wallet size), slow/timeouts on a
bloated wallet. Rewrite `readUnion` to run ONE ANCHORED `sparql_query` per by-need
doc (in parallel, per-doc tolerant): an anchored query is restricted to that
repo's graph, so it is O(1) per doc, INDEPENDENT of physical-wallet size. Keep the
ReadCap defense-in-depth gate.

docs/simulation.md: new "Physical wallet vs virtual wallet" section — the physical
shared wallet is a substrate that accumulates and must NEVER be enumerated/scanned;
each user's VIRTUAL wallet (the account's scope index in the shim) is the bounded
thing you enumerate ("list my documents"), then read those docs per-doc anchored.
read-model.md / nextgraph-current-state.md updated to the per-doc anchored rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-05 22:50:14 +02:00
parent 6a3501e700
commit ac2b026955
5 changed files with 210 additions and 140 deletions
+29
View File
@@ -21,6 +21,35 @@ application fiction the lib maintains. On top of that one wallet the lib rebuild
by emulation, the per-user stores + capabilities + inbox the consumer codes
against.
## Physical wallet vs virtual wallet — never enumerate the physical one
Because the emulation runs on ONE shared wallet, distinguish two levels:
- **Physical wallet** — the real NextGraph wallet everyone opens. Its local store
holds **every account's documents PLUS the lib's own internals** (the shim index,
the inbox docs, the discovery index) as named graphs. It **accumulates without
bound** across sessions/runs. **Listing / scanning "all documents" of the physical
wallet is meaningless AND O(size)** — it mixes every user's data with lib internals,
and it is exactly what a `sparql_query` with **no anchor** (`GRAPH ?g { … }`) does
(it spans every synced graph). **Never do it.** The physical wallet is a substrate,
not something to enumerate.
- **Virtual wallet** — the lib's emulation of **one user's** wallet: the set of
documents the shim attributes to that account (its per-scope index in
`store-registry.ts`). This is what "the user owns". Over a *virtual* wallet,
"**list my documents**" is meaningful and **bounded** (only that account's docs).
**Consequence for reads (see `read-model.md`):** to list a user's entities you
enumerate the **virtual** wallet — the account's scope index (bounded, O(my docs)),
NOT the physical union — then read those specific documents with a **per-doc anchored**
`sparql_query`. A non-empty / bloated physical wallet then costs nothing, because the
physical union is never scanned. Discovery (all public events) is the one bounded
enumeration hack and goes through the discovery **index**, not a physical scan.
At migration each virtual wallet becomes a real per-user wallet; the physical/virtual
distinction — and the "never enumerate the physical wallet" rule — dissolves into
native per-wallet reads.
## Two axes, never conflate them (store ≠ document)
The single most load-bearing distinction. Two **orthogonal** axes the