9951cd5223
Add a generic discovery-index surface: submitToIndex(ref) deposits a reference into the index document's inbox; readIndex() returns the materialized entries. A reserved special account (@index) owns the index document; deposits flow through the emulated inbox and are materialized by the emulated curator (the dedup/ moderation point). This replaces cross-account fan-out as the discovery path and is more faithful to the target (a single owned index fed via its inbox). Generic (the consumer supplies the reference to index). 79 tests pass; tsc rc=0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
4.8 KiB
Markdown
92 lines
4.8 KiB
Markdown
# ADR — Discovery mechanism (inbox-fed index, curator, fan-out)
|
|
|
|
**Date:** 2026-06-16 · **Status:** 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 *should* surface) is the consumer's concern, not
|
|
this lib's; only the mechanism is recorded here.
|
|
|
|
## Access ≠ 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 ADR.
|
|
|
|
## 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), **materialized from its inbox** (a
|
|
watcher ingests deposits → adds entries).
|
|
2. **Primary discovery = that global index.**
|
|
3. **Relational = secondary axis**, overlaid: a connection's participations,
|
|
markers on the global list. Rests on existing per-item data (protected scope) —
|
|
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 → 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) **+ native inbox** (a primitive present on every document). Nobody
|
|
writes the index but its owner (by materializing inbox deposits). So the model
|
|
stays "3 stores + Dialog + inboxes, no Group store."
|
|
- **One mechanism, reused.** The **inbox + materialization watcher** serve BOTH
|
|
submitting an entity to the index AND registering to a meeting-point — same
|
|
`inbox.post` API, same handling. This is exactly `inbox.ts` in this lib (`post`
|
|
/ `read` / `materialize` / `watch`).
|
|
- **Natural dedup / moderation point:** materialization (inbox → index) is where
|
|
duplicates are detected / moderated before insertion.
|
|
|
|
## Index owner — target model undecided
|
|
|
|
The "dedicated service with its own wallet sharing a freely-readable index" was
|
|
**incorrect**: NextGraph apps and services are **mono-user with no global data**
|
|
(see [`../nextgraph-current-state.md`](../nextgraph-current-state.md) § Apps &
|
|
services). 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 curator 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 a
|
|
connection**. 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 = "@index"`) that owns the index document while
|
|
the target owner stays undecided: `submitToIndex(ref)` deposits into the index
|
|
document's inbox; `readIndex()` materializes (dedup) 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 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 materialization.
|
|
- **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).
|