Files
Sylvain Duchesne 63ecfeeff8 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>
2026-07-06 14:02:16 +02:00

5.0 KiB

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 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 § 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).