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:
Sylvain Duchesne
2026-07-06 14:02:16 +02:00
parent d39b12885a
commit 63ecfeeff8
31 changed files with 1059 additions and 1396 deletions
+54 -55
View File
@@ -5,38 +5,36 @@
* an opaque reference and interprets the entries it reads back.
*
* ── The mechanism (see docs/decisions/discovery-model.md) ─────────────────
* Access discovery. A public entity is world-readable *with its NURI*; the
* discovery index is how a client learns that NURI EXISTS without holding a
* connection to its creator. There is ONE global index — an OWNED document
* (public read), fed via ITS OWN inbox, and MATERIALIZED by a curator. Nobody
* writes the index directly: a creator DEPOSITS a reference into the index's
* inbox; the (emulated) curator ingests deposits into entries. Materialization
* is the natural dedup / moderation point.
* Access and discovery are separate concerns. A public entity is world-readable
* with its NURI; the discovery index is how a client learns that NURI exists
* without holding a grant to read its creator's other documents. There is one
* global index — an owned document (public read), fed via its own inbox. A
* creator deposits a reference into the index's inbox; reading the index folds
* those deposits into entries, deduplicating identical references along the way.
*
* ── The special account (polyfill owner) ──────────────────────────────────
* "Who owns the global index" is undecided in the target (NextGraph is
* mono-user with no global data — see docs/nextgraph-current-state.md § Apps &
* services; a singleton app is the only glimpsed path). So the polyfill parks
* ownership on a RESERVED SPECIAL ACCOUNT in the shim ({@link INDEX_ACCOUNT}).
* Its `public` scope document is the index document; deposits land in that
* document's inbox (a stable NURI: every client opening the same shared wallet
* resolves the same account → same document). This replaces the cross-account
* fan-out (`store-registry.ts` `listEntityDocs`) as the app-facing discovery
* path — the fan-out survives only as an internal fallback (see {@link readIndex}).
* Ownership of a truly global index is undecided in the real platform, where an
* identity's apps and services see only what that identity shares. The polyfill
* therefore parks ownership on a reserved special account in the shim
* ({@link INDEX_ACCOUNT}). Its `public` scope document is the index document;
* deposits land in that document's inbox (a stable NURI: every client opening the
* same shared wallet resolves the same account, so the same document). This is
* the app-facing discovery path, in place of a cross-account fan-out
* (`store-registry.ts` `listEntityDocs`), which survives only as an internal
* fallback (see {@link readIndex}).
*
* ── TARGET vs POLYFILL ────────────────────────────────────────────────────
* Target: `submitToIndex` seals a reference into the index's native inbox
* (`inbox_post_link`) and a SEPARATE curator package materializes deposits into
* the owned index document; `readIndex` is a query on the materialized index.
* Here, everything is emulated in-lib on the shared wallet (deposit via
* `inbox.post`, materialize via `inbox.read`). At migration the special account
* disappears; ownership moves to the decided global-index owner and this module
* points `readIndex` at the real materialized document. The consumer surface
* (`submitToIndex` / `readIndex`) is designed to survive that swap unchanged.
* ── Real target vs this emulation ─────────────────────────────────────────
* The intended real shape is: `submitToIndex` seals a reference into the index
* document's own inbox (a future `inbox_post_link`), and reading the index is a
* query on the materialized index document. Here, everything runs in-lib on the
* shared wallet (deposit via `inbox.post`, fold via `inbox.read`). Against real
* NextGraph the special account gives way to the decided global-index owner and
* `readIndex` points at that document; the consumer surface (`submitToIndex` /
* `readIndex`) is designed to survive that change unchanged.
*
* All NextGraph I/O routes through `inbox.ts` (which routes through the T01.a
* `docs` primitives, the REAL injected `ng`), so this module imports NO
* `@ng-org` package.
* All NextGraph I/O routes through `inbox.ts` (which routes through the `docs`
* primitives, the real injected `ng`), so this module imports no `@ng-org`
* package.
*/
import * as inbox from "./inbox";
@@ -45,12 +43,12 @@ import { getCaps } from "./polyfill";
import type { Nuri, PrincipalId } from "./types";
/**
* The reserved SPECIAL ACCOUNT that owns the global discovery index in the
* polyfill. It hosts the index document but is never a real user. It lives in
* the registry's RESERVED namespace ({@link reservedAccount}), whose key
* `normalizeUser` can never produce so a user named "index"/"@index" can NOT
* hijack it (they would normalize to "index", a disjoint key). Disappears at
* migration (see file header).
* The reserved special account that owns the global discovery index in the
* polyfill. It hosts the index document but is never a real identity. It lives in
* the registry's reserved namespace ({@link reservedAccount}), whose key
* `normalizeId` can never produce, so an id of "index"/"@index" cannot hijack it
* (it normalizes to "index", a disjoint key). Removed against real NextGraph
* (see file header).
*/
export const INDEX_ACCOUNT = reservedAccount("index");
@@ -68,16 +66,16 @@ export interface IndexEntry {
export interface SubmitOptions {
/**
* Who is submitting. Omit for the current identity, or pass `null` for an
* ANONYMOUS submission. `from` is BOUND to the current identity by the inbox
* (naming another principal is rejected as a spoof — see {@link inbox.post}).
* anonymous submission. `from` is bound to the current identity by the inbox
* (naming another identity is rejected as a spoof — see {@link inbox.post}).
*/
from?: PrincipalId | null;
/**
* The NURI of the document being made discoverable. When given, the index
* enforces PUBLIC-ONLY: a document under a non-public (protected/private) read
* policy is REFUSED — the public index must never leak a governed document's
* NURI. Omit it only for a ref with no addressable document (rare); a governed
* doc always passes it so the guard can fire.
* admits only a public document: one under a non-public (protected/private)
* read policy is refused, so the world-readable index never exposes a governed
* document's NURI. Omit it only for a ref with no addressable document (rare);
* a governed document passes it so the guard can fire.
*/
doc?: Nuri;
/** Optional deposit timestamp (ms epoch). Omitted → `Date.now()`. Passing it
@@ -105,14 +103,15 @@ async function indexInboxNuri(): Promise<Nuri> {
/**
* Submit a reference to the global discovery index — the SDK act "make this
* discoverable". Deposits `ref` into the index document's inbox via
* {@link inbox.post}; the curator ({@link readIndex}) materializes it into an
* entry. GENERIC: `ref` is opaque here (the consumer serializes whatever a
* client needs to later locate the entity — e.g. an entity document NURI plus
* discovery metadata). `from` follows the inbox convention (anonymous if `null`).
* {@link inbox.post}; reading the index ({@link readIndex}) folds it into an
* entry. `ref` is opaque here (the consumer serializes whatever a client needs to
* later locate the entity — e.g. an entity document NURI plus discovery metadata).
* `from` follows the inbox convention (anonymous when `null`).
*
* PUBLIC-ONLY: when `opts.doc` names the document being surfaced, a document under
* a non-public read policy (protected/private) is REFUSED — the global index is
* world-readable, so admitting a governed doc's NURI would leak it past its scope.
* When `opts.doc` names the document being surfaced, a document under a
* non-public read policy (protected/private) is refused: the global index is
* world-readable, so admitting a governed document's NURI would expose it past
* its scope.
*/
export async function submitToIndex(ref: unknown, opts?: SubmitOptions): Promise<void> {
const doc = opts?.doc;
@@ -135,11 +134,11 @@ export async function submitToIndex(ref: unknown, opts?: SubmitOptions): Promise
}
/**
* Read (materialize) the global discovery index — the EMULATED CURATOR. Reads
* every submission from the index inbox, DEDUPLICATES by serialized `ref` (the
* materialization dedup / moderation point of the discovery model: a duplicate
* submission surfaces once), and returns the entries sorted by `ts` ascending.
* At migration this becomes a query on the real materialized index document.
* Read the global discovery index. Reads every submission from the index inbox,
* deduplicates by serialized `ref` (a duplicate submission surfaces once — the
* discovery model's moderation point), and returns the entries sorted by `ts`
* ascending. Against real NextGraph this becomes a query on the materialized
* index document.
*/
export async function readIndex(): Promise<IndexEntry[]> {
const target = await indexInboxNuri();
@@ -157,9 +156,9 @@ export async function readIndex(): Promise<IndexEntry[]> {
}
/**
* Watch the discovery index — the emulated curator's watcher. Polls
* {@link readIndex} and fires `onEntries` whenever the index changes. Returns an
* unsubscribe. Fires once immediately. (Deduplication is applied on each read.)
* Watch the discovery index. Polls {@link readIndex} and fires `onEntries`
* whenever the index changes. Returns an unsubscribe. Fires once immediately.
* (Deduplication is applied on each read.)
*/
export function watchIndex(
onEntries: (entries: IndexEntry[]) => void,