docs+refactor(client): fidelity pass — id identity, drop connections, no faux-login, accurate NextGraph framing
Align the polyfill's surface and docs with the verified NextGraph reality and
remove application-level concepts:
- Identity is an ID, not a username: AccountRecord.id, shim predicate shim:id,
normalizeId; accounts core becomes IdentityStore (set/clear/get) — the faux
login/logout framing is gone (identity is set at wallet-import time).
- Relationship/connection is an application concept, not a platform primitive
(NextGraph has no bilateral-connection primitive: grantee is unpersisted
scaffolding, cap-send is unimplemented). Remove connections.ts; caps exposes
only a directed grantRead(doc, granteeId) + a read-only protectedDocsOf(owner).
Delete the now-dead isolation.ts social-visibility axis.
- Inbox docs: NextGraph has no separate curator — the recipient's own verifier
unseals and applies each queued sealed message inline (process_inbox);
inbox_post_link is a proposed/future API. Stop attributing the emulated
curator to the platform.
- Read isolation reframed around the outcome: no cap -> empty union read;
targeted read of an unheld repo -> RepoNotFound; cap introspection
(canRead/governsRead) is emulation-only with no NextGraph API behind it.
- read-model.md corrected: the listing path is per-doc ANCHORED default-graph
queries, never the anchorless GRAPH ?g union (that is O(wallet)); the probe
section no longer claims the opposite.
- README recap table restructured (target | current NextGraph status | current
emulation); INDEX_ACCOUNT documented as reservedAccount("index") in the
sentinel namespace; de-domained generic-layer comments; softened tone.
Consumer application (Festipod) rewired separately to own the relationship
concept and feed the lib an id. Lib gates: bun test 83 pass / 0 fail, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,91 +1,94 @@
|
||||
# ADR — Discovery mechanism (inbox-fed index, curator, fan-out)
|
||||
# ADR — Discovery mechanism (inbox-fed index, fan-out)
|
||||
|
||||
**Date:** 2026-06-16 · **Status:** mechanism accepted; target owner undecided.
|
||||
Ported here for the **discovery MECHANISM** it defines — the piece this lib
|
||||
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.
|
||||
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 ≠ discovery
|
||||
## 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 ADR.
|
||||
- **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), **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.
|
||||
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 → they **replicate locally**
|
||||
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.
|
||||
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
|
||||
- **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 + 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
|
||||
- **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
|
||||
|
||||
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**
|
||||
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
|
||||
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 a
|
||||
connection**. This ADR classified that per-account fan-out as a **drift** to be
|
||||
replaced by the single global 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 = "@index"`) that owns the index document while
|
||||
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()` 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
|
||||
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 surface
|
||||
(`submitToIndex` / `readIndex`) unchanged.
|
||||
[`../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 materialization.
|
||||
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).
|
||||
|
||||
Reference in New Issue
Block a user