Two batches, verified against nextgraph-rs throughout. P1a — the capability surface. Reading was an ACL (Map<doc, Set<principal>>), the exact inversion of key possession. It is now possession: `capFor(nuri)` is the only question, there is no principal parameter anywhere, and nothing turns a bare reference into a cap. Sharing is `shareCap(cap, toInbox)`, a Link deposit; receiving needs no operation. `Nuri` and `ReadCap` are template literal types, so passing a bare reference where a cap belongs is a compile error, with runtime guards behind it for JavaScript callers. The virtual user boundary. Every access function is now confined to the connected user, through two rules on one criterion (possession), implemented in two places so a lapse in either is caught by the other: authorization at the passage points, and "do not even attempt" at the callers. The polyfill's own machinery moved to physical.ts — unguarded, never exported — which replaced an exemption list: the machinery no longer gets waved through the guard, it calls something the guard never saw. Removed, as emulating capabilities the target does not have: - discovery.ts and its global index. There is no discovery in NextGraph; you follow links. It also pooled user data across wallets. - the cross-account fan-out (listEntityDocs, resolveReadGraphs, allAccounts, loadShim), which was cross-user enumeration by construction. - resolveInboxAnchor, a single inbox common to every user. Caps are now stored where NextGraph stores them, and read back rather than recomputed: AddRepo on the store's Store branch for documents a user creates, AddLink on its User branch for caps received. Inboxes belong to someone — the user's own, plus one per document — and connecting a user drains them all; that is the library's job, not the app's. Corrections worth recording: a ReadCap is `r:`, not `:k:` (reported by NextGraph's developer, verified in BlockRef::readcap_nuri); received caps DO have a register (AddLink), contrary to what this repo's notes claimed; and "wallet" upstream means keyring — what owns three stores is a user, so the vocabulary follows. The cap value is the constant OK: the only question the emulation answers is whether a cap is held. P1b replaces that one constant with a real key. After this the shape is right and the isolation is still fake. Nothing here may be described as anonymous or private.
7.1 KiB
ADR — Discovery mechanism (inbox-fed index, fan-out)
SUPERSEDED — 2026-07-30. The premise does not hold.
There is no discovery in NextGraph. You cannot discover; you can only follow links (PO, 2026-07-30 — the principle is documented in
../readcap-and-nuri-model.md§4ter-bis). Publishing is two acts: place the data in your public store, and circulate the link — into inboxes, or into somewhere already reachable by the people concerned. It is seen only by those who received the link. This is a foundation of local-first, not a gap to be filled.A global index therefore fails on two independent counts:
- it emulates a capability the target will never have — teaching consumers a model that does not exist, which is the one failure mode this library exists to prevent;
- it is data common to several users/wallets, and nothing may be common — only indexing mechanisms that make the virtual users work (the shim qualifies; a shared index of user announcements does not).
This ADR already recorded the first half of that verdict — "a dedicated service with its own wallet sharing a freely-readable index is not a NextGraph shape", resting on a singleton-app path "not implemented, uncertain". That reservation is now the conclusion.
discovery.tsand its tests were removed on 2026-07-30, along withwatchShape's public-scope fold andINDEX_ACCOUNT. See../briefs/2026-07-30-virtual-wallet-boundary.md.What survives, and is worth keeping from the text below: the 3-stage frame (
discovery → synchronization → query) is still exactly right, with stage 1 re-read as "a link reached you" rather than "you consulted an index". You still cannot query what you have not synchronized, and you still do not synchronize what nobody gave you. The inbox is what feeds stage 1 — which makes it the bootstrap of the whole reachability graph, not a side feature.Kept in full below as a record of what was built and why, and of the reasoning that has to be re-read through the correction above.
Date: 2026-06-16 · Status: SUPERSEDED 2026-07-30 (see the block above). Originally: mechanism accepted; target owner undecided.
Ported here for the discovery mechanism it defines — the piece this lib
realizes (inbox.ts post/materialize/watch; store-registry.ts fan-out). The
product intent (what a consumer application should surface) is the consumer
application's concern, not this lib's; only the mechanism is recorded here.
Access is not discovery
- Access: may I read this document if I hold it? A public entity is world-readable with its NURI.
- Discovery: how do I learn it exists, in order to read it? This is the ADR's topic.
The mechanism
- A single global index, fed via its inbox. The creator does not edit the index directly: it deposits a reference into the index's inbox. The index is an owned document (public read), built up from its inbox (a watcher ingests deposits and adds entries).
- Primary discovery is that global index.
- Relational is a secondary axis, overlaid: a peer's participations, markers on the global list. It rests on existing per-item data (protected scope), with no new primitive.
The 3-stage frame
discovery → synchronization → query
- Discovery: the index gives the NURIs of the entity documents.
- Synchronization: subscribe to those documents so they replicate locally
(verifier:
self.repos+ oxigraph dataset). - Query: query what is now local (sort, limit, reactivity). SPARQL/ORM
run on the local set only (
resolve_target_for_sparqlsearchesself.repos) — you cannot query what is not loaded.
Corollary: a reactive query does not replace the index — it runs at stage 3 on the local union that stages 1-2 built. You don't sync what you didn't discover.
Why one reused mechanism
- No Group store. The index is not open-write: it is an owned document (public read) plus a native inbox (a primitive present on every document). Nobody writes the index but its owner (by ingesting inbox deposits). So the model stays "3 stores + Dialog + inboxes, no Group store."
- One mechanism, reused. The inbox + ingest watcher serve both
submitting an entity to the index and a registration/deposit in one consumer
app — same
inbox.postAPI, same handling. This is exactlyinbox.tsin this lib (post/read/materialize/watch). - Natural dedup / moderation point: the inbox → index ingest is where duplicates are detected / moderated before insertion.
Index owner — target model undecided
NextGraph apps and services are mono-user with no global data
(see ../nextgraph-current-state.md § Apps &
services), so a dedicated service with its own wallet sharing a freely-readable
index is not a NextGraph shape. The only path glimpsed for a global document is a
singleton app bound to the developer-user — not implemented, uncertain, to explore
later. This is why a global-index package is a deferred separate package in this lib
(see the top-level README).
Polyfill reality — the fan-out drift is now RESOLVED (special-account index)
The shared-wallet polyfill originally shipped a cross-account fan-out over
every account's public documents (store-registry.ts listEntityDocs('public')
/ resolveReadGraphs) — one account saw another's public entity without any
relationship to its creator. This ADR classified that per-account fan-out as a drift
to be replaced by the single global index.
That drift is now resolved in the polyfill. The inbox-fed global index of
this ADR is implemented on top of a reserved special account in the shim
(discovery.ts, INDEX_ACCOUNT = reservedAccount("index") — a sentinel-prefixed
key in the shim's reserved namespace that normalizeId can never produce, so it is
disjoint from any normalized user id, not the literal "@index") that owns the
index document while
the target owner stays undecided: submitToIndex(ref) deposits into the index
document's inbox; readIndex() ingests (dedups) the entries. The app-facing
discovery path is now "read the index", exactly as this ADR prescribes — not
the fan-out. The cross-account fan-out survives only as an internal lib
fallback (it still powers per-scope listing like resolveReadGraphs), never
the discovery route. The special account is the provisional owner; at migration
it disappears and ownership moves to the decided global-index owner (see
../migration-guide.md) with the consumer application's
surface (submitToIndex / readIndex) unchanged.
Alternatives rejected (mechanism)
- Open-write index (creator writes the index directly): required a collaborative document (Group store, SDK-blocked) and exposed the index to corruption. Replaced by inbox deposit + owner-side ingest.
- Purely relational discovery (
social_query): rejected as primary (a global list is wanted); kept as a secondary axis. - No index, direct reactive query: impossible — SPARQL is local-only (stage 3).