Commit Graph

18 Commits

Author SHA1 Message Date
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
Sylvain Duchesne d39b12885a fix(client): anchored default-graph writes for inbox + scope index
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>
2026-07-06 12:45:11 +02:00
Sylvain Duchesne c85c635f63 perf(client): targeted shim account resolution (no full-scan on hot paths)
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>
2026-07-06 11:30:50 +02:00
Sylvain Duchesne ac2b026955 feat(client): read via per-doc ANCHORED queries; document virtual vs physical wallet
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>
2026-07-05 22:50:14 +02:00
Sylvain Duchesne 6a3501e700 feat(client): union-read ReadCap gate + listMyEntityDocs + doc corrections
- 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>
2026-07-05 20:49:01 +02:00
Sylvain Duchesne e24f52749f feat(client): read-model surface — open/sync + anchorless union sparql_query
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>
2026-07-05 18:20:09 +02:00
Sylvain Duchesne b1d06b68b9 fix(client): per-entity write round-trip + dedicated inbox anchor
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>
2026-07-04 17:26:31 +02:00
Sylvain Duchesne bf753770b8 feat(client): real per-document isolation + bilateral connections + deposit guards
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>
2026-07-04 10:40:44 +02:00
Sylvain Duchesne 42174608f8 fix(client): reserve the discovery @index account key (prevent user collision)
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>
2026-07-04 09:58:52 +02:00
Sylvain Duchesne 9951cd5223 feat(client): discovery via a global index (special @index account)
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>
2026-07-04 09:33:42 +02:00
Sylvain Duchesne 7db5eef33f feat(client): activate ReadCap isolation via current identity + connections
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>
2026-07-03 23:59:27 +02:00
Sylvain Duchesne e0d88b5076 feat(client): SDK-shaped scope resolvers (resolveScopeGraph/resolveInboxAnchor)
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>
2026-07-03 23:41:34 +02:00
Sylvain Duchesne d804a436d7 feat(client): inbox mechanism, write-guard, SPARQL injection hardening
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>
2026-07-03 15:51:00 +02:00
Sylvain Duchesne 654cb90d99 feat(client): generic shared-wallet shim surface (docs/storeRegistry/isolation/accounts)
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>
2026-07-03 10:12:55 +02:00
Sylvain Duchesne 88d96857fb Refactor read filter from per-item grant to per-document ReadCap
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>
2026-06-29 11:19:57 +02:00
Sylvain Duchesne 672067d513 feat(client): read filter — capability-based view over the reactive set
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>
2026-06-29 10:28:40 +02:00
Sylvain Duchesne f4ded6d8f7 feat(client): complete SDK-shaped surface — init/initNg + SDK type re-exports
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>
2026-06-25 14:53:35 +02:00
Sylvain Duchesne bb2d9c3e59 Initial scaffold: @ng-eventually/client — SDK-shaped polyfill over NextGraph
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>
2026-06-22 16:35:40 +02:00