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`.
114 lines
7.5 KiB
Markdown
114 lines
7.5 KiB
Markdown
# 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`](../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**:
|
|
>
|
|
> 1. 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;
|
|
> 2. 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.ts` and its tests were removed on 2026-07-30**, along with `watchShape`'s public-scope fold and `INDEX_ACCOUNT`. See [`../briefs/2026-07-30-virtual-wallet-boundary.md`](../briefs/2026-07-30-virtual-wallet-boundary.md).
|
|
>
|
|
> One factual error below is worth naming so it is not carried into a future design: *"a native inbox (a primitive present on every document)"* is **false**. No document has an inbox upstream — only the public and protected STORE repos do (`engine/verifier/src/site.rs:128,149`; `doc_create` leaves `inbox: None`, `engine/repo/src/repo.rs:574`). See [`../nextgraph-current-state.md`](../nextgraph-current-state.md) § Inbox.
|
|
>
|
|
> 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
|
|
|
|
1. 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).
|
|
2. Primary discovery is that global index.
|
|
3. 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`
|
|
|
|
1. **Discovery**: the index gives the NURIs of the entity documents.
|
|
2. **Synchronization**: subscribe to those documents so they replicate locally
|
|
(verifier: `self.repos` + oxigraph dataset).
|
|
3. **Query**: query what is now local (sort, limit, reactivity). SPARQL/ORM
|
|
run on the local set only (`resolve_target_for_sparql` searches `self.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.post` API, same handling. This is exactly `inbox.ts` in 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`](../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`](../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).
|