Épingle empiriquement (broker réel) le contrat implicite sur lequel open-repo
repose : après le 1er événement `State` d'un `doc_subscribe`, la PRÉSENCE d'une
donnée est GARANTIE (le triple écrit est déjà dans ce State, sans attente
supplémentaire) et l'ABSENCE est DÉFINITIVE (doc vide reste vide, +5s de grâce).
Si ce contrat casse (changement de version broker), ce test le détecte.
Constats mesurés : l'abonnement pousse `TabInfo` (~1-3ms) PUIS `State` (~2-3ms)
— le State est le 2e événement, pas le 1er ; latence 1er State ~2-3ms sur profil
frais. IMPORTANT (à corriger phase 2) : open-repo résout son attente sur le 1er
push (= TabInfo), PAS sur le State → il rend la main avant la vraie barrière ; le
commentaire « resolves on the FIRST push (the initial State) » est faux.
gate : test:e2e 39 passed ; bun test 117 ; tsc propre. src/ non touché.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les tests du polyfill doivent PROUVER son contrat contre le VRAI broker (pas un
fake). Ajout de deux tests e2e broker réel (wallet dédié), avec reconnexion
FIDÈLE : page fraîche sur le MÊME profil persistant + nouveau login broker
(helper `faithfulReconnect`), PAS export/réimport dans un profil vide (qui
resynchronise et masque le cold-open).
- Contrat reconnexion/cold-read : session 1 crée un doc (public + protected) ;
session fraîche fidèle ; relit → données présentes, en POLLANT une borne
généreuse (l'attente de récupération après souscription est le fonctionnement
normal).
- Contrat non-fork : re-résoudre le même identifiant après reconnexion rend les
MÊMES NURIs de docs de scope, pas un second provisioning.
Temps de sync réels observés (le SIGNAL) : protected relu en ~195ms (repo déjà
ouvert) ; PUBLIC relu en ~105s (ouverture repo + attente push + union ancrée à
l'échelle de la donnée publique) ; login de reconnexion ~2.5s. Le ~105s public
est un signal de PERFORMANCE À INVESTIGUER (probable scale/bloat de l'anchorless
union scan, cf. suivi bloat) — le contrat est REMPLI mais lent côté public.
Honnêteté : le « échoue-sans-le-fix » n'a PAS pu être montré via ce chemin de
login (le bootstrap du login broker ouvre déjà les repos → firstRawNoOpen=1).
Contrat prouvé REMPLI avec les fix ; nécessité non isolable via cette voie.
gate : test:e2e 33 passed (baseline 27 vert) ; bun test 117 ; tsc propre.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Les fix récents (anti-fork, open-repo, access-log) n'avaient AUCUN test de
comportement dans le domaine de la lib. Ajout de tests DÉTERMINISTES à base de `ng`
factice modélisant la condition d'échec (pas de broker réel — le harness e2e réel
réhydrate trop vite et ne reproduit pas ces cas).
- anti-fork.test.ts : fake avec lag de sync (0 lignes les K premières lectures du
record de compte, puis les vraies). Asserte le cœur du fix : compte retrouvé au
retry → réutilisé, 0 doc_create (pas de fork) ; compte réellement neuf → 1 seul
jeu de docs provisionné, budget de retry prouvé consommé ; idempotence de session ;
cas limite « trouvé à la dernière tentative ».
- open-repo.test.ts : fake où une requête ancrée rend vide tant que doc_subscribe
n'a pas ouvert le repo. Asserte subscribe-avant-read + idempotence (Set des repos
ouverts) + no-op si le ng n'a pas doc_subscribe.
- access-log.test.ts : off par défaut (silencieux), on via config ET via env
NG_EVENTUALLY_ACCESS_LOG, préfixe = identité active (suit setCurrentUser),
row-count sur READ. Restaure console.log/env fidèlement.
bun test : 91 → 117 pass, 0 fail. tsc --noEmit propre. src/ non touché.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cause racine mesurée (broker réel, access-log) : à la première resolveAccount
d'une session, le record de compte tout juste persisté (ou d'une session
antérieure) peut lire 0 lignes à cause du LAG DE SYNC broker. ensureAccount
interprétait ce 0 comme « compte inexistant » et RE-PROVISIONNAIT un second jeu
de docs de scope (docPublic/docProtected forkés) → les lectures d'une session
tombaient sur un jeu, celles d'une autre (ou après drop de cache) sur l'autre
jeu vide → données « perdues » à la reconnexion.
Fix : `resolveAccountReliably` (store-registry.ts) — retry borné (ouverture du
repo d'ancre shim + backoff plafonné, défaut 8 tentatives / ≲8.5s) AVANT que
ensureAccount ne décide qu'un compte est neuf. Provisionne seulement si, après
le budget, la lecture rend toujours 0 (compte réellement neuf). Budget injecté
via StoreRegistryDeps.provisionRetry (polyfill.ts), ON en prod ; tests unitaires
à provisionRetry synchrone (attempts:1, fake sans lag). Idempotence de session
préservée par accountCache (hit court-circuite, déterministe).
Portée : corrige le déterminisme de provisioning. NE suffit PAS à réparer la
reconnexion (le read public à 0 same-session subsiste, cause distincte encore à
mesurer de façon déterministe — le lag broker rend les mesures non-reproductibles).
Complémentaire du commit open-repo précédent, pas redondant.
gate : tsc --noEmit propre ; bun test 91 pass ; auth @data (vide/distinctes) verts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Défaut visé : sur une session verifier fraîche (reconnexion), la lecture ancrée
tape des repos pas encore ouverts dans self.repos → 0 ligne. Nouveau module
`open-repo.ts` : `ensureReposOpen`/`ensureRepoOpen` ouvrent/souscrivent un repo
via la primitive existante `subscribeDoc` (doc_subscribe) et attendent le push
d'état initial (borné, sans polling) AVANT la lecture ancrée. Câblé en amont de
`readUnion` (read-model) et `readScopeIndex` (store-registry). Idempotent par
session (Set des NURI ouverts + Map in-flight ; ré-ouverture si la session
injectée change). No-op si le `ng` injecté n'a pas doc_subscribe (fake unitaire).
État — NON MERGÉ, incomplet :
- AIDE le PROTECTED : la participation remonte au cold-start (mesuré app, timing
un peu bruité).
- N'ADRESSE PAS l'accueil PUBLIC : `readScopeIndex` de l'index public rend 0 même
côté écrivain même-session — le fix ouvre le repo mais l'index reste vide. Le
code d'index lib est prouvé scope-symétrique, donc la cause du public est
ailleurs (broker/store ou chemin app), À MESURER SOUS BROKER (actuellement
injoignable). Nécessité du fix elle-même non prouvée en e2e lib (broker down).
gate broker-indépendant : tsc --noEmit propre ; bun test 91 pass (worker).
Inclut le scaffolding e2e de repro reconnexion (broker/run/sdk-entry).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Observability probe for the shared-wallet isolation footgun: on one physical
wallet several virtual identities coexist, and a read must never surface a doc
scoped to another identity. When it does (B reading A's doc), the leak is
invisible in the data — it looks like a normal read. This makes it VISIBLE.
Every real read/write is logged, prefixed by the ACTIVE virtual identity
(getCurrentUser → the account the op is scoped under, NOT the constant shared
physical wallet id). Reads append the row count — a strong leak signal:
[urn:festipod:user:bob] READ did:ng:o:docA (readDoc) → 3 rows
Instrumented at the LOW common point in docs.ts: every read routes through
sparqlQuery, every write through sparqlUpdate, container creation through
docCreate. Callers pass a semantic label (readDoc|readUnion|listMyEntityDocs|
writeEntity|deposit|…) that is a lib-internal probe param, NOT forwarded to the
real `ng` (preserves docs.test.ts exact-forwarding assertions).
OFF by default → one boolean read on the hot path, zero output. On via
configure({ debugAccessLog: true }) or env NG_EVENTUALLY_ACCESS_LOG=1 (no code
change). Polyfill-era; removed at the real multi-store migration.
tsc --noEmit: 0 errors. bun test: 91 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The e2e harness files (broker.ts/run.ts/sdk-entry.ts) live outside the client
tsconfig include (src/test), so an editor's broad tsc flagged process/node:*/
playwright as unresolved. Add packages/client/e2e/tsconfig.json (extends base,
types: bun) — tsc -p e2e is exit 0; the client tsconfig stays unchanged/clean.
Drop an unused getCurrentUser import in sdk-entry.ts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lib e2e harness now characterizes all three SPARQL graph shapes against the real
broker (24/24): (a) no-GRAPH anchored write round-trips; (b) an explicit anchored
GRAPH <plainNuri> ALSO resolves to the same repo (no phantom graph) — the earlier
'targets a phantom graph, does NOT round-trip' claim is FALSE on @ng-org/web
0.1.2-alpha.13; (c) an anchorless GRAPH ?g scan spans every named graph (O(wallet)
union, 32 graphs) — TRUE, re-verified.
Reconcile the comments accordingly: inbox.ts / store-registry.ts drop the false
phantom-graph justification (no-GRAPH stays as the canonical, always-safe shape — a
simplicity choice, not a round-trip necessity; re-verify via e2e if the broker
version changes). read-model.md keeps the anchorless-O(wallet) rationale (the real
reason reads are per-doc anchored) and appends a note distinguishing the variable
GRAPH ?g SCAN from a constant GRAPH <D> WRITE. No new absolute introduced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The polyfill had only fake-ng unit tests; real-broker behaviour (SPARQL graph
round-trip, doc_subscribe marshaling, inbox/index) was only ever exercised by
borrowing Festipod's harness. Add packages/client/e2e/ — a standalone Playwright
harness in the SDK's own domain: a minimal SDK page (configure() + the real
@ng-org/web ng, a window.__sdk bridge, zero Festipod domain) loaded in the broker
iframe on the real broker, authenticating with a DEDICATED wallet (name
ng-eventually-e2e, profile e2e/.playwright-profile-lib — gitignored, separate from
Festipod's). Runner: bun run e2e/run.ts (script test:e2e); not mixed into bun test.
Covers, against the real broker (23 checks, all green): docCreate; sparqlUpdate/
Query anchored default-graph round-trip; readUnion N-doc + per-doc tolerance + cap
gate; doc_subscribe initial+write+unsub and subscribeDocs per-doc isolation; inbox
post/read/watch + spoof guard; discovery submit/read/watchIndex + reserved-account
isolation; store-registry idempotency + bounded listMyEntityDocs + scope resolvers;
caps read-filter (in-memory, honestly scoped); IdentityStore.
Finding (reported, not a failure): on @ng-org/web 0.1.2-alpha.13 an anchored
INSERT DATA { GRAPH <plainNuri> {…} } DOES round-trip (no phantom graph) — several
lib comments assert the opposite; stale, to reconcile. Behaviour is unaffected (the
lib writes the always-safe no-GRAPH default-graph shape). bun test still 91 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose subscribeDoc(nuri, onChange) / subscribeDocs(nuris, onChange) wrapping the
real ng.doc_subscribe — per-document, event-driven (initial State + a Patch per
commit, local or broker-synced from a remote peer), returning a sync unsubscribe.
subscribeDocs isolates per doc (a failing/unsynced doc never aborts the others),
so it sidesteps the ORM fan-out hang (never orm_start_graph(graphs:[…])).
Replace the setInterval polling in inbox.watch() and discovery.watchIndex() with
doc_subscribe — same public contract, now push not poll. NextGraph is subscription-
first; no polling remains.
Verified against the real broker (@data harness spike): the doc_subscribe callback
marshals across the iframe RPC (@ng-org/web strips the callback and drives it via a
MessagePort — no DataCloneError) and fires on the initial push and on a real write.
Lib bun test 91 pass, tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the SDK's public read surface grounded in real NextGraph: the reactive
useShape hook (subscribe/push — a doc change, local or broker-synced from a remote
peer, propagates to every subscriber; no polling) is THE recommended read path;
one-shot sparqlQuery/readUnion is the exception. Includes the write surface,
identity/scope (per-document isolation, public = owner-writes-only), and a separate
'current emulation status' section flagging where the polyfill does not yet honor
the reactive contract (entity reads one-shot + polling inbox/index watchers) as
gaps to close. Shared reference: honored by the lib, used by Festipod.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Inbox deposits and the per-(account,scope) index append were written into an
explicit GRAPH <plainNuri> named graph, which the real broker stores separately
from the repo's default graph (repo_graph_name with overlay suffix) — so an
anchored default-graph read (read-model.readDoc) never saw them and deposits
did not round-trip. Drop the GRAPH wrapper: the anchor scopes the write to the
repo's default graph, matching the read. Mocks updated accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
loadShim() read EVERY account record in the shim (SELECT over the whole anchor
graph). On a shim that accumulates accounts (the shared wallet grows), that is
O(accounts) and hangs the hot path (~90s at ensureAccount → loadShim). Same
principle as per-doc reads: never scan a shared structure.
Add resolveAccount(username): a BOUNDED SELECT anchored on the single subject
accountSubject(username) → O(1), independent of account count. Cache in a
per-account Map (cleared by resetRegistryCache). Hot paths now use it:
ensureAccount (existence check), indexInboxNuri/@index (discovery), resolveInbox
Anchor, resolveWriteGraph, createEntityDoc, listMyEntityDocs. loadShim kept only
for genuine all-accounts needs (allAccounts, the listEntityDocs fan-out fallback).
The 90s ensureAccount/loadShim hang is gone. 93 tests pass; tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The anchorless union query (`GRAPH ?g`) scanned EVERY named graph in the local
store (the whole shared physical wallet) → O(wallet size), slow/timeouts on a
bloated wallet. Rewrite `readUnion` to run ONE ANCHORED `sparql_query` per by-need
doc (in parallel, per-doc tolerant): an anchored query is restricted to that
repo's graph, so it is O(1) per doc, INDEPENDENT of physical-wallet size. Keep the
ReadCap defense-in-depth gate.
docs/simulation.md: new "Physical wallet vs virtual wallet" section — the physical
shared wallet is a substrate that accumulates and must NEVER be enumerated/scanned;
each user's VIRTUAL wallet (the account's scope index in the shim) is the bounded
thing you enumerate ("list my documents"), then read those docs per-doc anchored.
read-model.md / nextgraph-current-state.md updated to the per-doc anchored rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- read-model.ts `readUnion` now applies the emulated ReadCap gate (drops a
subject when its doc is governsRead && !canRead for the current identity), so
per-scope isolation holds by construction AND by filter.
- store-registry: `listMyEntityDocs(username, scope)` (current account only) vs
the all-accounts `listEntityDocs` fallback (documented as the enumeration to
avoid on the read path).
- docs: nextgraph-current-state / read-model — corrected to the SOURCE-VERIFIED
reality that the JS SDK exposes NO open/sync-by-cap primitive
(load_repo_from_read_cap is pub(crate)); in the mono-wallet all repos are
already local (same session), so the anchorless union spans them with no open
step. simulation.md: listEntityDocs+useShape({graphs}) is a fallback, not the
read path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proven against the real broker (probe): opening docs then a single anchorless
`sparql_query` with a `GRAPH ?g { ... }` body reads the LOCAL UNION of all synced
named graphs — the fast, hang-free replacement for the reactive-ORM per-document
fan-out (which aborted on any unsynced/fresh repo → 75s never-fires). New
`read-model.ts` (readUnion) exposes this; there is no reactive union query, so
listing is one-shot and consumers re-query on change. docs/read-model.md refined
with the probe finding (an explicit `GRAPH ?g` body iterates all named graphs
regardless of the anchor; the anchor only bounds the default graph). 93 tests
pass; tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real-broker validation of the per-document model surfaced round-trip breaks the
fake-ng unit tests missed:
- The inbox anchor was the shim graph itself, making loadShim ~60s; give the
inbox its own document so anchor resolution is fast and isolated.
- store-registry adjustments so per-entity documents created via the SDK are
indexed and readable back through the scope fan-out.
docs/simulation.md updated. 89 tests pass; tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ReadCap filter now enforces on per-entity documents (consumers create one doc
per entity, so each has a declared policy — private→owner, protected→owner+
connections, public→all). Isolation is genuinely active, not dormant.
- connections.ts (new): a BILATERAL connection registry — a link grants protected
read only when BOTH sides have asserted it (each assertion bound to its author).
A unilateral/self-declared connection grants nothing (closes the confused-deputy
hole). declareConnections is authenticated to the current identity.
- inbox.post: `from` is bound to the current identity — a spoofed `from` throws.
- discovery.submitToIndex: PUBLIC-ONLY — a governed non-public doc is refused
(no protected/private leak into the world-readable index).
- docs/simulation.md: documents this as application-level emulated isolation on a
shared wallet (not crypto); at NextGraph maturity → real caps, consumer unchanged.
89 tests pass (+10 covering: active protected isolation via bilateral connect,
unilateral grants nothing, from-spoof rejected, non-public submit refused). tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The special discovery-index account was keyed as "@index", which the consumer's
normalizeUsername collapses to "index" — colliding with a real user named
"index" (who could then hijack/tamper the global index document). Introduce a
reserved-account namespace (a sentinel key unreachable by any typed username) so
the index account can never collide with user input. Test proves a user named
"index"/"@index" resolves to a DIFFERENT document than the reserved index
account. 80 tests pass; tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Isolation was dormant (no current identity ever set). Now: setCurrentUser
records who is reading; declareConnections(neighborsOf) grants each protected
document's read cap to owner + connections. Reads discriminate through the
ReadCap filter: private→owner, protected→owner+connections, public→all. Generic
(the consumer injects identity + connections). Write-guard coverage limits
documented honestly in docs/simulation.md (real write paths bypass the JS proxy;
full enforcement awaits native caps). isolation-active.test.ts proves the
protected+connections path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose a clean scope-based surface so consumers work by scope (public/protected/
private) and never see a physical store id — the library resolves placement and
performs the shared-wallet simulation internally. RegistrySession gains optional
protected/public store ids, supplied at the single injection point
(configureStoreRegistry). Zero domain knowledge. docs/simulation.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Polyfill capabilities landed for Festipod's T02 features (all generic,
zero-domain — the consumer injects the domain).
- inbox: implement the previously-stubbed namespace. post(target,{from?,
payload,ts?}) deposits {from,payload,ts} as RDF via docs.sparqlUpdate (the
real injected ng, never makeNg); read/materialize + watch emulate the curator
in-lib (deposits read via docs.sparqlQuery). `from` optional = anonymity.
- write-guard: caps.hasWritePolicy() + ng-proxy.sparql_update rejects when the
target doc is under a write policy and the current user lacks its write cap;
passthrough otherwise (no regression). Read-cap registry unchanged.
- sparql.ts (new): escapeLiteral / escapeIri / assertNuri, exported from index.
store-registry now escapes every literal and validates/encodes every IRI-
position value — closes a SPARQL-injection hole where an untrusted username
could inject triples into the shim (the account→doc-NURI trust root).
Tests: inbox, sparql (incl. injection), ng-proxy write-guard, isolation-active.
68 tests pass; tsc --noEmit rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port the shared-wallet shim mechanics from the Festipod app into the
generic @ng-eventually/client library — zero app-specific knowledge, the
consumer injects the domain (entity->scope mapping, connections, storage).
New namespaces exposed from src/index.ts:
- docs docCreate/sparqlUpdate/sparqlQuery via the REAL injected `ng`
(getConfig().ng), never the public makeNg proxy — the JS-over-
iframe double proxy breaks doc_create postMessage marshaling
(DataCloneError). Validated hard constraint.
- storeRegistry generic (account,scope)->NURI resolver, createEntityDoc/
listEntityDocs + per-scope index, sharedWalletShim in the
private_store, cache. Consumer wiring injected via
configureStoreRegistry({ getSession, normalizeUser }).
- isolation pure applyIsolation (public=all / protected=owner+connections
/ private=owner); accessors + connection graph injected.
- accounts AccountStore (localStorage-backed faux login, storage injected)
+ normalizeUsername. React wrapper intentionally NOT ported.
polyfill.ts gains configureStoreRegistry/getStoreRegistryDeps + resetConfig.
36/36 bun test, tsc --noEmit rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model the read filter on NextGraph's real ReadCap mechanism instead of an
invented per-item grant. Verified in nextgraph-rs: there is no Document type
(document = repo); a store is a container repo referencing other repos by RDF
overlay; holding a store's cap does NOT grant the repos it contains (each repo
needs its own cap; no read-cap inheritance). So the access unit is the
DOCUMENT = an item's `@graph`, never the item.
- caps.ts: CapRegistry (read/write caps per document NURI + public docs;
open/grantRead/grantWrite/makePublic/canRead/canWrite/governsRead/
hasReadPolicy). Replaces access.ts (Grant).
- read-filter.ts: filter keeps an item iff its `@graph` document is readable
(held cap or public); items with no `@graph` or in an ungoverned document are
kept. No injected grantOf — the filter reads `@graph` and consults the
registry (automatic, domain-agnostic).
- polyfill.ts: getCaps()/resetCaps() replace setGrantOf/getGrantOf; useShape
filters only when caps.hasReadPolicy() (else passthrough, no regression).
- tests: caps.test.ts (6) + read-filter.test.ts (4), incl. no-inheritance
between documents. 10 pass; tsc rc=0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
makeReadFilteredView wraps a DeepSignalSet in a Proxy whose iteration/size/forEach
yield only items the current user may read (canRead on the item's emulated grant), while
add/delete and the underlying reactivity pass through. filterReadable is the pure core.
useShape applies it only when a grantOf resolver is configured (else passthrough, so
ungranted apps are unaffected). grantOf/setGrantOf added to the polyfill surface. Mirrors
the broker delivering only authorized docs; removed at migration. 4 unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lifecycle.ts forwards init (@ng-org/web) and initNg (@ng-org/orm) to the injected
SDK; index re-exports the SDK types (ShapeType, BaseType, Schema, DeepSignalSet, NG) so
consumers import everything from @ng-eventually/client. Type re-exports are erased at
build → no runtime @ng-org import added (no duplicate SDK copy). @ng-org added as
devDependencies (typecheck only) + peerDependencies. EventuallyConfig accepts init/initNg.
Typecheck + 4 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generic polyfill layer that makes a single NextGraph broker behave like the
not-yet-shipped multi-user NextGraph (emulated capabilities + inbox). Zero app domain.
@ng-eventually/client exposes an SDK-identical surface (ng, useShape, inbox); the
polyfill bootstrap (configure + capability helpers) is isolated under /polyfill, so
the main entry stays a drop-in for @ng-org/web|orm. The real SDK is injected at
configure() (no hard import → build-alias safe + testable).
Scaffold: NextGraph wiring stubbed with TODO; capability helpers implemented and
unit-tested (4 tests, typecheck clean). The global-index curator is deferred — in
NextGraph apps/services are mono-user with no global data.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>