refactor: renommer client → sdk, et fusionner les deux portes en une
Deux mouvements de surface, aucun changement de comportement. **`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.** « client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans `docs/source-layout-by-fate.md` et le tableau des paquets du README. **Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types — vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`. Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de `docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par `test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de vocabulaire sur les noms publiés. **Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le choix visible plutôt qu'hérité : - `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par `shared-wallet/bootstrap` ; - `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes par leur chemin interne, ce qui est leur raison d'être ; - le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux fois brouillait la frontière qu'elle servait à marquer. Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` / `hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait `capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire. 179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le broker en ligne — mesuré une fois après le renommage, une fois après la fusion.
This commit is contained in:
+2
-2
@@ -5,6 +5,6 @@ dist/
|
||||
bun.lockb
|
||||
|
||||
# SDK e2e real-broker harness — dedicated wallet profile + bundle (never committed)
|
||||
packages/client/e2e/.playwright-profile-lib/
|
||||
packages/client/e2e/.dist/
|
||||
packages/sdk/e2e/.playwright-profile-lib/
|
||||
packages/sdk/e2e/.dist/
|
||||
*.ngw
|
||||
|
||||
@@ -67,9 +67,9 @@ is needed), and how this lib emulates it today.
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@ng-eventually/client` | The SDK-identical wrapper the app imports instead of `@ng-org/web` / `@ng-org/orm`. It adds the polyfills the broker/verifier will do natively (shared-wallet identity, capability enforcement, anticipated cap/inbox methods). As NextGraph matures, the app points back at the real SDK (build alias removed) and this package falls away. |
|
||||
| `@ng-eventually/sdk` *(was `@ng-eventually/client` until 2026-08-07)* | The SDK-identical wrapper the app imports instead of `@ng-org/web` / `@ng-org/orm`. It adds the polyfills the broker/verifier will do natively (shared-wallet identity, capability enforcement, anticipated cap/inbox methods). As NextGraph matures, the app points back at the real SDK (build alias removed) and this package falls away. |
|
||||
|
||||
A global-index package is deferred. Data common to all of an application's users comes from a **singleton app**: a document or store shared by all users and hardcoded in the app, write-owned by the developer and delegable — but never to all users, so user contributions reach it **through an inbox** (nothing in NextGraph is freely writable by everyone). That is the direction the NextGraph developer has named; it is **not implemented**, and several points are still open (what exactly is hardcoded, how delegation travels, who materializes the inbox). So there is no second package for now — it will be introduced once the mechanism exists, and it will be separate from the client. See [`docs/nextgraph-current-state.md`](docs/nextgraph-current-state.md) § Apps & services.
|
||||
A global-index package is deferred. Data common to all of an application's users comes from a **singleton app**: a document or store shared by all users and hardcoded in the app, write-owned by the developer and delegable — but never to all users, so user contributions reach it **through an inbox** (nothing in NextGraph is freely writable by everyone). That is the direction the NextGraph developer has named; it is **not implemented**, and several points are still open (what exactly is hardcoded, how delegation travels, who materializes the inbox). So there is no second package for now — it will be introduced once the mechanism exists, and it will be separate from the SDK wrapper. See [`docs/nextgraph-current-state.md`](docs/nextgraph-current-state.md) § Apps & services.
|
||||
|
||||
## Design principle
|
||||
|
||||
|
||||
+54
-46
@@ -1,10 +1,10 @@
|
||||
# API contract — what `@ng-eventually/client` exposes today, and what the future SDK should expose per subject
|
||||
# API contract — what `@ng-eventually/sdk` exposes today, and what the future SDK should expose per subject
|
||||
|
||||
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
|
||||
|
||||
**Scope: the APP-FACING contract only.** Everything reachable from the two published entry points, and nothing else. The library's internal modules — the shim machinery, the read paths, the boundary guards — are held to the same standard (as close as possible to what NextGraph does or plans) but have their own document, `docs/internal-contract.md`: a consumer never reads that one, a maintainer does. This split was made on 2026-08-03, together with the export change described in § 15.
|
||||
|
||||
**Scope.** The real exported surface of `@ng-eventually/client` (verified against the `export` statements in `packages/client/src/index.ts` and `packages/client/src/polyfill.ts` — `package.json` maps exactly two entry points, `.` and `./polyfill`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
**Scope.** The real exported surface of `@ng-eventually/sdk` (verified against the `export` statements in `packages/sdk/src/index.ts` and `packages/sdk/src/polyfill.ts` — `package.json` maps exactly two entry points, `.` and `./polyfill`), and, for each subject, the target signature the future NextGraph JS SDK is expected to expose. Written 2026-08-03, verified against the `nextgraph-rs` clone (HEAD `213338f6`, 2026-05-16) and the installed `@ng-org/web@0.1.2-alpha.13` type declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
|
||||
**How to read the epistemic labels.** Every target-side claim carries one of:
|
||||
|
||||
@@ -19,10 +19,10 @@ Per the design principle (`README.md` § *Design principle*): an absent implemen
|
||||
|
||||
## 1. Bootstrap and configuration
|
||||
|
||||
### Today — `@ng-eventually/client/polyfill` (everything here is removed at migration)
|
||||
### Today — `@ng-eventually/sdk` (the POLYFILL-ERA block of `src/index.ts`; everything here is removed at migration)
|
||||
|
||||
```ts
|
||||
// polyfill.ts:44
|
||||
// shared-wallet/bootstrap.ts
|
||||
export interface EventuallyConfig {
|
||||
ng: NgLike;
|
||||
useShape: UseShapeLike;
|
||||
@@ -32,23 +32,23 @@ export interface EventuallyConfig {
|
||||
init?: (...args: any[]) => any;
|
||||
initNg?: (...args: any[]) => any;
|
||||
}
|
||||
// polyfill.ts:99
|
||||
// shared-wallet/bootstrap.ts
|
||||
export function configure(c: EventuallyConfig): void;
|
||||
// polyfill.ts:113 — tests only
|
||||
// shared-wallet/bootstrap.ts — tests only
|
||||
export function resetConfig(): void;
|
||||
|
||||
// polyfill.ts:24
|
||||
// shared-wallet/bootstrap.ts
|
||||
export interface StoreRegistryDeps {
|
||||
getSession: () => Promise<RegistrySession>;
|
||||
normalizeId?: (id: string) => string;
|
||||
pointerGuard?: { attempts?: number; baseMs?: number; maxStepMs?: number };
|
||||
}
|
||||
// polyfill.ts:124
|
||||
// shared-wallet/bootstrap.ts
|
||||
export function configureStoreRegistry(deps: StoreRegistryDeps): void;
|
||||
// polyfill.ts:161 — tests only
|
||||
// shared-wallet/bootstrap.ts — tests only
|
||||
export function resetStoreRegistry(): void;
|
||||
|
||||
// polyfill.ts:106 / :153 — both tagged @internal, exported so the SDK-shaped wrappers can reach the injected SDK
|
||||
// shared-wallet/bootstrap.ts — both tagged @internal, exported so the SDK-shaped wrappers can reach the injected SDK
|
||||
export function getConfig(): EventuallyConfig;
|
||||
export function getStoreRegistryDeps(): ResolvedRegistryDeps;
|
||||
```
|
||||
@@ -61,7 +61,7 @@ export function getStoreRegistryDeps(): ResolvedRegistryDeps;
|
||||
|
||||
## 2. Lifecycle
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// lifecycle.ts:11 — forwards to the real @ng-org/web init injected at configure()
|
||||
@@ -98,7 +98,7 @@ Divergence: none in behaviour (pure forwarding), but the wrapper erases the para
|
||||
|
||||
## 3. The `ng` object
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// index.ts:55
|
||||
@@ -122,7 +122,7 @@ The two overrides:
|
||||
|
||||
## 4. Reactive typed reads — `useShape`
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// use-shape.ts:12
|
||||
@@ -160,7 +160,7 @@ Divergence to note: the wrapper types everything `unknown`, losing the generic `
|
||||
|
||||
## 5. Reactive typed reads with load state — `watchShape`
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// watch-shape.ts:73
|
||||
@@ -201,7 +201,7 @@ So the constraint on the bet: the target can already answer "synced?" (`readyPro
|
||||
|
||||
## 6. One-shot listing — the read-model
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// read-model.ts:59
|
||||
@@ -237,7 +237,7 @@ The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query
|
||||
|
||||
## 7. Raw document / SPARQL primitives — `docs.*`
|
||||
|
||||
### Today — `@ng-eventually/client` (namespace `docs`)
|
||||
### Today — `@ng-eventually/sdk` (namespace `docs`)
|
||||
|
||||
```ts
|
||||
// docs.ts:46
|
||||
@@ -298,7 +298,7 @@ So the target's direction for scope placement is **already visible in the source
|
||||
|
||||
## 8. Per-document subscription — `subscribeDoc`
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// subscribe.ts:47,60,79
|
||||
@@ -336,7 +336,7 @@ declare function doc_subscribe(repo_o: string, session_id: any, callback: Functi
|
||||
|
||||
## 9. Inbox — deposits, and cap delivery
|
||||
|
||||
### Today — `@ng-eventually/client` (namespace `inbox`; `share` also re-exported from `/polyfill`)
|
||||
### Today — `@ng-eventually/sdk` (namespace `inbox`)
|
||||
|
||||
```ts
|
||||
// inbox.ts:48,58
|
||||
@@ -397,21 +397,21 @@ Consequences per function:
|
||||
### Today
|
||||
|
||||
```ts
|
||||
// @ng-eventually/client — nuri.ts:50,60 (type guards; the only doors from string to typed)
|
||||
export function isNuri(s: string): s is Nuri;
|
||||
export function hasReadCap(s: string): s is ReadCap;
|
||||
|
||||
// @ng-eventually/client — types.ts:11,34
|
||||
// @ng-eventually/sdk — model/types.ts. The types are the whole published cap surface.
|
||||
export type Nuri = `did:ng:${string}`;
|
||||
export type ReadCap = `did:ng:${string}:r:${string}`;
|
||||
export type NuriLike = Nuri | string;
|
||||
|
||||
// @ng-eventually/client/polyfill
|
||||
// tests / fresh wallet only — the registry itself is NOT published, and neither is any
|
||||
// "do I hold this?" predicate (`hasCap`, removed 2026-08-06: it read like "may I read
|
||||
// this?", and a document in a public store answers `false` until something asks for it).
|
||||
export function resetCaps(): void;
|
||||
// NOT published, each deliberately:
|
||||
// isNuri / hasReadCap — the type guards (`model/nuri.ts`). Unpublished since the
|
||||
// permissive-in change: every entry takes `NuriLike` and validates at the door, so
|
||||
// a consumer holding a plain string narrows nothing. Publishing a guard would
|
||||
// invite the cast it exists to prevent.
|
||||
// hasCap(doc) — removed 2026-08-06. It read like "may I read this?", and a
|
||||
// document in a public store answers `false` until something asks for its cap.
|
||||
// getCaps / CapRegistry / resetCaps — the emulation's engine room and its test reset.
|
||||
|
||||
// @ng-eventually/client/polyfill — caps.ts:59 (class CapRegistry)
|
||||
// INTERNAL — `emulated-verifier/caps.ts` (class CapRegistry). Never published; listed for the maintainer.
|
||||
constructor(holder?: () => PrincipalId | null);
|
||||
mint(nuri: Nuri): ReadCap;
|
||||
learn(cap: ReadCap): void;
|
||||
@@ -446,7 +446,7 @@ The `CapRegistry` class itself is machinery (the in-memory record of what the co
|
||||
|
||||
## 11. NURI and SPARQL string utilities
|
||||
|
||||
### Today — `@ng-eventually/client`
|
||||
### Today — `@ng-eventually/sdk`
|
||||
|
||||
```ts
|
||||
// sparql.ts:32,66,95
|
||||
@@ -455,7 +455,7 @@ export function escapeIri(value: string): string;
|
||||
export function assertNuri<T extends string>(nuri: T): T;
|
||||
```
|
||||
|
||||
(`isNuri` / `hasReadCap` are in § 10; `targetOf`, `parseNuri`, `mintCap` exist in `nuri.ts` but are **not** exported from either entry point — deliberately: nothing on the surface turns a bare reference into a cap.)
|
||||
(`isNuri`, `hasReadCap`, `targetOf`, `parseNuri`, `toNuri` and `mintCap` all exist in `model/nuri.ts` and **none** is exported — deliberately: nothing on the surface turns a bare reference into a cap, and validation happens at the door rather than in the caller's hands.)
|
||||
|
||||
### Target
|
||||
|
||||
@@ -465,9 +465,9 @@ export function assertNuri<T extends string>(nuri: T): T;
|
||||
|
||||
## 12. Scope resolution, per-entity documents, and the store registry
|
||||
|
||||
### Today — `@ng-eventually/client` (namespace `storeRegistry`) — plus `Scope` from `types.ts`
|
||||
### Today — `@ng-eventually/sdk` (namespace `storeRegistry`) — plus `Scope` from `types.ts`
|
||||
|
||||
> **Narrowed 2026-08-03.** The entry used to re-export the WHOLE `store-registry` module. It now re-exports an app-facing slice (`src/surface/placement.ts`): `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `userInbox`, `openDocumentInbox`, `documentInboxAddress`. The rest — `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `resolveAccount`, `ensureAccount`, `reservedAccount`, `resetRegistryCache`, and the `VirtualUserRecord` / `RegistrySession` types — is **no longer importable from `@ng-eventually/client`** and is covered by `docs/internal-contract.md`. The signatures below are kept for the record, marked accordingly.
|
||||
> **Narrowed 2026-08-03.** The entry used to re-export the WHOLE `store-registry` module. It now re-exports an app-facing slice (`src/surface/placement.ts`): `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `userInbox`, `openDocumentInbox`, `documentInboxAddress`. The rest — `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `resolveAccount`, `ensureAccount`, `reservedAccount`, `resetRegistryCache`, and the `VirtualUserRecord` / `RegistrySession` types — is **no longer importable from `@ng-eventually/sdk`** and is covered by `docs/internal-contract.md`. The signatures below are kept for the record, marked accordingly.
|
||||
|
||||
```ts
|
||||
// types.ts:38 — NB: NOT the ORM's Scope (a graphs/subjects filter); this is the store scope
|
||||
@@ -530,7 +530,7 @@ export function resetRegistryCache(): void;
|
||||
### Today
|
||||
|
||||
```ts
|
||||
// @ng-eventually/client — virtualUsers.ts (namespace accounts)
|
||||
// @ng-eventually/sdk — virtualUsers.ts (namespace accounts)
|
||||
export const ACCOUNT_STORAGE_KEY = "ng-eventually.account.id"; // :18
|
||||
export interface VirtualUserStorage { // :26
|
||||
getItem(key: string): string | null;
|
||||
@@ -545,9 +545,9 @@ export class IdentityStore { // :37
|
||||
}
|
||||
export function browserIdentityStore(key?: string): IdentityStore; // :89
|
||||
|
||||
// @ng-eventually/client/polyfill
|
||||
export function setCurrentUser(id: PrincipalId | null): void; // polyfill.ts:171
|
||||
export function getCurrentUser(): PrincipalId | null; // polyfill.ts:187
|
||||
// @ng-eventually/sdk — the POLYFILL-ERA block
|
||||
export function setCurrentUser(id: PrincipalId | null): void; // shared-wallet/bootstrap.ts
|
||||
export function getCurrentUser(): PrincipalId | null; // shared-wallet/bootstrap.ts
|
||||
export async function connectedUser(): Promise<void>; // connect.ts:52
|
||||
```
|
||||
|
||||
@@ -571,7 +571,7 @@ declare function user_disconnect(user_id: string): Promise<void>;
|
||||
|
||||
## 14. Type re-exports
|
||||
|
||||
`@ng-eventually/client` re-exports, type-only (erased at build, `index.ts:48-50`):
|
||||
`@ng-eventually/sdk` re-exports, type-only (erased at build, `index.ts:48-50`):
|
||||
|
||||
```ts
|
||||
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
|
||||
@@ -600,7 +600,8 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
### Places the current surface teaches something to unlearn
|
||||
|
||||
- ~~**The SDK entry is not as pure as its header claims.**~~ **FIXED 2026-08-03.** The header claimed the entry "exposes ONLY what `@ng-org/web` / `@ng-org/orm` expose" while also shipping `virtualUsers` and the whole `store-registry` module. Both are gone from it, and the header now states what the entry actually promises: *every symbol here has a target-SDK counterpart, verified or assumed, listed in this document*. It still exports `docs`, `readUnion`, `watchShape`, `subscribeDoc(s)`, the SPARQL helpers and the NURI guards — justified inventions, documented per subject above — so the promise is no longer "@ng-org surface only", which was never true, but "nothing here is machinery".
|
||||
- **`share` is importable from both entries** (`inbox.share` on the SDK entry via `export * as inbox`, and a named re-export on `/polyfill`). The polyfill re-export exists "so the cap vocabulary stays on the polyfill side" — but the namespace export undoes that. Harmless functionally; blurs the same boundary.
|
||||
- ~~**`share` is importable from both entries.**~~ **FIXED 2026-08-07** with the entry merge: there is one entry and one `share`, under `inbox`.
|
||||
- **One entry means the import line no longer says what disappears.** Until 2026-08-07 a second import path (`/polyfill`) WAS the deletion list. It is now the `POLYFILL-ERA` block in `src/index.ts`, this appendix's note above, and the per-subject rulings in this document. That is a documentation-carried signal where it used to be a mechanical one — the appendix is pinned by a test, the grouping is not.
|
||||
- **`inbox.read`/`materialize` as a mailbox** — enumerating raw deposits is emulation detail (§ 9); the durable contract is deposit-and-it-gets-applied. An app building UI on the deposit list should expect that surface to change shape entirely.
|
||||
- **`watchShape`'s "planned `useShape` upgrade"** — stated in the module header with no provenance in this repo or the clone (§ 5). The load-state *distinction* is safe; the claim that NextGraph plans this exact hook shape is an assumption and must not be cited as an announced API.
|
||||
- **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `watchShape`'s design already assumes).
|
||||
@@ -610,19 +611,26 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
|
||||
## Appendix — full export inventory (for diffing)
|
||||
|
||||
*Generated from the `export` statements, and pinned by `packages/client/test/vocabulary.test.ts` — if this list and the code disagree, that test fails. It went stale once, still listing `storeRegistry`'s shim internals after the entry had been narrowed, which is what a hand-maintained inventory does.*
|
||||
*Generated from the `export` statements, and pinned by `packages/sdk/test/vocabulary.test.ts` — if this list and the code disagree, that test fails. It went stale once, still listing `storeRegistry`'s shim internals after the entry had been narrowed, which is what a hand-maintained inventory does.*
|
||||
|
||||
### `@ng-eventually/client` — `src/index.ts`
|
||||
### `@ng-eventually/sdk` — `src/index.ts` (the only entry since 2026-08-07)
|
||||
|
||||
```text
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, watchShape
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, InboxScope, NG, NgLike, Nuri, NuriLike, PrincipalId, ReadCap, RegistrySession, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, StoreRegistryDeps, UnionSubject, Unsubscribe, UseShapeLike, configure, configureStoreRegistry, connectedUser, docChangeType, ensureIdentity, init, initNg, ng, readUnion, setCurrentUser, subscribeDoc, subscribeDocs, useShape, watchShape
|
||||
docs: depositInto, docCreate, sparqlQuery, sparqlUpdate
|
||||
inbox: Deposit, PostOptions, materialize, post, postToDocument, processInbox, read, readForDocument, readSynced, share, watch
|
||||
storeRegistry: createEntityDoc, listMyEntityDocs, openDocumentInbox, resolveScopeGraph, resolveWriteGraph
|
||||
```
|
||||
|
||||
### `@ng-eventually/client/polyfill` — `src/polyfill.ts`
|
||||
**Of these, four are POLYFILL-ERA and have no target counterpart** — `configure`,
|
||||
`configureStoreRegistry`, `setCurrentUser`, `connectedUser` (plus the types
|
||||
`EventuallyConfig`, `StoreRegistryDeps`, `RegistrySession`). They are the deletion list,
|
||||
and `src/index.ts` groups them under a heading that says so. `ensureIdentity` is a fifth
|
||||
in substance — the shared-wallet gate — but the *call site* survives (§ 2bis).
|
||||
|
||||
```text
|
||||
direct: EventuallyConfig, RegistrySession, StoreRegistryDeps, VirtualUserRecord, configure, configureStoreRegistry, connectedUser, getConfig, getStoreRegistryDeps, resetCaps, resetConfig, resetStoreRegistry, setCurrentUser, share
|
||||
```
|
||||
Six symbols the previous `/polyfill` entry published are gone from the surface entirely:
|
||||
`getConfig` and `getStoreRegistryDeps` (internal wiring, reached through
|
||||
`shared-wallet/bootstrap`), `resetConfig` / `resetStoreRegistry` / `resetCaps` (test
|
||||
resets, reached by their internal path), and the direct `share` re-export — `inbox.share`
|
||||
was always the same function, and publishing it twice blurred the boundary it was meant
|
||||
to mark.
|
||||
|
||||
@@ -142,4 +142,4 @@ An adversary refuted the brief (7 findings — the 7th marked *(Plausible)*). **
|
||||
|
||||
**Consequence**: add **P0 (keyless-fetch spike)** up front and a **distinct WriteCap track**; requalify P2 (the real content = durability + cap-less + re-sharing, not "inverting the ACL"); record that **without crypto, read privacy is not applicable** (choose: real crypto vs masked projection).
|
||||
|
||||
Links: `readcap-and-nuri-model.md`, `packages/client/src/emulated-verifier/caps.ts`. On the consumer side, the Festipod brief "realign the sign-ups" depends on this effort.
|
||||
Links: `readcap-and-nuri-model.md`, `packages/sdk/src/emulated-verifier/caps.ts`. On the consumer side, the Festipod brief "realign the sign-ups" depends on this effort.
|
||||
|
||||
@@ -22,7 +22,7 @@ The spec below is unchanged — read it first. Everything from here to *Why this
|
||||
|
||||
| Spec | Where |
|
||||
|---|---|
|
||||
| `Nuri` / `ReadCap` (plain strings, `:r:` discriminant) | `packages/client/src/model/types.ts`, `src/model/nuri.ts` (internal parse/mint/derive) |
|
||||
| `Nuri` / `ReadCap` (plain strings, `:r:` discriminant) | `packages/sdk/src/model/types.ts`, `src/model/nuri.ts` (internal parse/mint/derive) |
|
||||
| Keyring, one per identity — `capFor` | `src/emulated-verifier/caps.ts` (`CapRegistry`), surfaced as `capFor` in `src/polyfill.ts` |
|
||||
| Caps of my OWN documents (the emulated `AddRepo { read_cap }`) | `src/shared-wallet/account-registry.ts` `fileOwnCaps`, called from `createEntityDoc` and `listMyEntityDocs` |
|
||||
| `shareCap(cap, toInbox)` + reception with no dedicated operation | `src/surface/inbox.ts` (`shareCap`, and the inline absorption in `read`) |
|
||||
@@ -45,7 +45,7 @@ One property this makes explicit and that is worth confirming: **the bare NURI o
|
||||
|
||||
## The exact surface a consumer codes against
|
||||
|
||||
From `@ng-eventually/client/polyfill`:
|
||||
From `@ng-eventually/sdk/polyfill`:
|
||||
|
||||
```ts
|
||||
capFor(nuri: Nuri): ReadCap | undefined // the keyring lookup
|
||||
@@ -192,13 +192,13 @@ Not started. It changes the consumer contract in the right direction (one less o
|
||||
- **Unit suite green — 146 tests**, typecheck clean on `src`, `test` and the e2e harness.
|
||||
- The typing was verified from a **consumer's** point of view, not just the library's: a synthetic app compiled against the entry points shows the two real mistakes (`shareCap(bareNuri, …)` and passing a raw `string` from storage) as compile errors, while every correct path — `capFor(doc)` → `shareCap(cap, inbox)`, and narrowing with the exported guards — needs no cast.
|
||||
- The acceptance test was **mutation-checked**: reverting both gardes (the discovery fold and the `readUnion` possession gate) makes `watch-shape.test.ts` (e) fail with the bare-referenced document reappearing. The test has teeth.
|
||||
- **The e2e ran against the live broker (`nextgraph.eu`) on 2026-08-03 — 39 passed, 0 failed.** The first run was 22/8, and the eight refusals were not test noise: they exposed a **real hole in the surface**. `docs.docCreate` filed no cap for the creator, so a consumer could create a document through the public primitive and then be refused reading or writing it. Upstream that cannot happen — `doc_create` commits `AddRepo { read_cap }` to the store's Store branch, so the creator holds it from the first instant. Fixed at `packages/client/src/surface/docs.ts:73`, and deliberately NOT replicated in `shared-wallet/physical.ts`: the shim's own documents belong to no user, and `store-registry` files their caps where it knows whose they are. The remaining failures were the harness acting as a second identity without establishing it (`createEntityDoc(id, …)` with someone else connected) or reading an arbitrary document as an inbox; both are now `setCurrentUser` + `userInbox`, which is what a consumer must do too.
|
||||
- **The e2e ran against the live broker (`nextgraph.eu`) on 2026-08-03 — 39 passed, 0 failed.** The first run was 22/8, and the eight refusals were not test noise: they exposed a **real hole in the surface**. `docs.docCreate` filed no cap for the creator, so a consumer could create a document through the public primitive and then be refused reading or writing it. Upstream that cannot happen — `doc_create` commits `AddRepo { read_cap }` to the store's Store branch, so the creator holds it from the first instant. Fixed at `packages/sdk/src/surface/docs.ts:73`, and deliberately NOT replicated in `shared-wallet/physical.ts`: the shim's own documents belong to no user, and `store-registry` files their caps where it knows whose they are. The remaining failures were the harness acting as a second identity without establishing it (`createEntityDoc(id, …)` with someone else connected) or reading an arbitrary document as an inbox; both are now `setCurrentUser` + `userInbox`, which is what a consumer must do too.
|
||||
- **An e2e run against a persistent wallet must use a FRESH identity per run.** The second run was green and the third was not, on unchanged code: moving the inbox tests onto `userInbox(id)` made the inbox *stable for its owner* — which is the point of an inbox — so a fixed id accumulates every past run's deposits and `deposits.length === 2` drifts to 4. Green-then-red on identical code is the tell. The disposable thing is the **user**, not the inbox: `run.ts` now stamps `@inbox-user-`/`@watcher-`/`@friend-` with `Date.now()`, as it already did for `@alice-`. Any future step that resolves a durable per-user document (inbox, stores, Links) inherits this constraint.
|
||||
- **The cap registry is process-wide and `bun test` shares modules across files**, so suites that read without declaring caps now reset explicitly (`read-model.test.ts`, `watch-shape.test.ts`). Worth knowing before adding a suite.
|
||||
|
||||
## Documentation state
|
||||
|
||||
The permanent documentation was updated in the same pass (root `README.md`, `packages/client/README.md`, `docs/simulation.md`, `docs/migration-guide.md` §1 + the assumed `declareConnections` break, `docs/read-model.md`, `docs/readcap-and-nuri-model.md` §5, `docs/nextgraph-current-state.md`, `packages/client/docs/sdk-reference.md`). **If the review changes the surface, those are the files to re-align** — they describe the code as it stands now, not a validated state.
|
||||
The permanent documentation was updated in the same pass (root `README.md`, `packages/sdk/README.md`, `docs/simulation.md`, `docs/migration-guide.md` §1 + the assumed `declareConnections` break, `docs/read-model.md`, `docs/readcap-and-nuri-model.md` §5, `docs/nextgraph-current-state.md`, `packages/sdk/docs/sdk-reference.md`). **If the review changes the surface, those are the files to re-align** — they describe the code as it stands now, not a validated state.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
> And four more, all confirmed:
|
||||
>
|
||||
> 4. **D4 would delete a working recovery path.** Inbox deposits are never removed (`packages/client/src/surface/inbox.ts`), so a second device/tab recovers its caps by re-reading. localStorage-without-re-reading loses them permanently, and contradicts P1a's delivered doctrine that per-process rebuild "is correct".
|
||||
> 4. **D4 would delete a working recovery path.** Inbox deposits are never removed (`packages/sdk/src/surface/inbox.ts`), so a second device/tab recovers its caps by re-reading. localStorage-without-re-reading loses them permanently, and contradicts P1a's delivered doctrine that per-process rebuild "is correct".
|
||||
> 5. **D3 is false outside entity documents.** `capFor(scopeIndexDoc)` and `capFor(userInbox)` are undefined before *and after* `listMyEntityDocs` — their caps can only ever be derived. Yet the boundary brief requires them reachable. Upstream that root comes from the wallet plus `AddSignerCap` on the User branch — a level the fact table omitted entirely.
|
||||
> 6. **`doc_create` writes four times, not two** (+ the class quad on the Header branch, + `AddSignerCap` on the User branch).
|
||||
> 7. **Ordering defect: D2 before the boundary guard opens cap harvesting.** Once caps are triples in `scopeIndexDoc(bob,…)`, and both `scopeIndexDoc` and `docs.sparqlQuery` are exported, `setCurrentUser("mallory")` reads Bob's caps. Today `mintCap` is unexported, so a NURI yields nothing. **The guard must land before the caps become triples.**
|
||||
|
||||
@@ -47,7 +47,7 @@ breaks writes with `RepoNotFound`). See the scope rule in
|
||||
|
||||
*The decision stands; the mechanism named in it has been replaced.* Opening was
|
||||
`orm_start_graph` when this was written. It is now `ensureRepoOpen` — `doc_subscribe`
|
||||
plus a wait for the first `State` (`packages/client/src/emulated-verifier/open-repo.ts:167`) — after
|
||||
plus a wait for the first `State` (`packages/sdk/src/emulated-verifier/open-repo.ts:167`) — after
|
||||
`orm_start_graph` was found to hang on a fan-out (`subscribe.ts:28,181`). What must be
|
||||
read here is the invariant *"open the repo, by its store NURI, before writing"*, not the
|
||||
call that used to implement it.
|
||||
|
||||
@@ -70,11 +70,11 @@ The surface consequence: the *act* — obtain a link, circulate it — is the sa
|
||||
|
||||
## 5. Recommendation for the polyfill
|
||||
|
||||
The surface already exists: `linkTo(doc: NuriLike): ReadCap` (`packages/client/src/surface/placement.ts:65-77`) for the traveling value, `inbox.share(doc, toUser)` (`packages/client/src/surface/inbox.ts:285`) for directed delivery. **Keep `linkTo` — the act is the right one** — with four adjustments:
|
||||
The surface already exists: `linkTo(doc: NuriLike): ReadCap` (`packages/sdk/src/surface/placement.ts:65-77`) for the traveling value, `inbox.share(doc, toUser)` (`packages/sdk/src/surface/inbox.ts:285`) for directed delivery. **Keep `linkTo` — the act is the right one** — with four adjustments:
|
||||
|
||||
1. **Label it LEVEL-1 SHAPE in `docs/api-contract.md`.** What supports it: the `NgLink` family and its stated sharing flow, the `PermaShare` permission, the exercised object-URL and profile-QR precedents, and the PO doctrine that circulation is the only distribution. The model's own stated flows are unusable without *some* produce-a-link affordance, which is as much confidence as an unbuilt feature allows. What cannot be promised: the SDK's name for it, sync vs async (upstream link-building needs overlay + peers from the session, so async is plausible — same adapter-sized delta class as `subscribeDoc`'s sync unsubscribe), and whether the value is a NURI string or a structured link. Therefore: **the returned value is opaque**; a consumer that stores it, transmits it, and hands it back unmodified learns nothing to unlearn; a consumer that parses it does.
|
||||
2. **Fix the comment-vs-code mismatch in `linkTo`.** The docstring claims *"A protected document's key never comes out this way — it goes through `share`"*; the code returns any held cap, with no scope check. The **code** is the model-true side: `RepoLinkV0`-with-key IS the protected-document link, and sharing it out-of-band is the documented normal case (`:5059`). Align the comment: a protected link carries the key and is legitimate to circulate — with the §4 durability caveat, not a prohibition.
|
||||
3. **The recipient verb is missing.** Nothing exported ingests an out-of-band link: `learn` is reached only by inbox processing and the connection drain (`packages/client/src/surface/inbox.ts:410`, `packages/client/src/emulated-verifier/connect.ts:68`), and `getCaps()` is documented machinery (api-contract §15). The model names the recipient act precisely — open the link: load the repo from its read cap, file `AddLink` durably on the User branch, subscribe (`:5059`; `engine/repo/src/types.rs:1934-1950`; `verifier.rs:2237`). Suggested surface, same epistemic label as `linkTo`: `openLink(link: string): Promise<Nuri>` — files the cap in the emulated registers and returns the cap-less target for use in reads. Without it, path 2 has a producer and no consumer, and the multi-actor test where Bob *obtains* the document through calls (never through a shared variable) cannot be written — the exact failure mode `rules/engineering/multi-actor-tests-obtain-not-receive.md` records.
|
||||
3. **The recipient verb is missing.** Nothing exported ingests an out-of-band link: `learn` is reached only by inbox processing and the connection drain (`packages/sdk/src/surface/inbox.ts:410`, `packages/sdk/src/emulated-verifier/connect.ts:68`), and `getCaps()` is documented machinery (api-contract §15). The model names the recipient act precisely — open the link: load the repo from its read cap, file `AddLink` durably on the User branch, subscribe (`:5059`; `engine/repo/src/types.rs:1934-1950`; `verifier.rs:2237`). Suggested surface, same epistemic label as `linkTo`: `openLink(link: string): Promise<Nuri>` — files the cap in the emulated registers and returns the cap-less target for use in reads. Without it, path 2 has a producer and no consumer, and the multi-actor test where Bob *obtains* the document through calls (never through a shared variable) cannot be written — the exact failure mode `rules/engineering/multi-actor-tests-obtain-not-receive.md` records.
|
||||
4. **Do not add**: link options (expiry, audience, revoke-this-link), per-reader introspection for public documents, or any API that parses or inspects a link's insides — nothing upstream supports any of them, and each teaches a lever the model does not have.
|
||||
|
||||
## 6. The question for the NextGraph developer
|
||||
|
||||
@@ -37,7 +37,7 @@ Interpretation (**plausible mechanism, not settled**): the write was pushed into
|
||||
|
||||
## What the SDK exposes but does not consume
|
||||
|
||||
`disconnections_subscribe` **does fire** on this failure — but neither the polyfill (`@ng-eventually/client`) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.
|
||||
`disconnections_subscribe` **does fire** on this failure — but neither the polyfill (`@ng-eventually/sdk`) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.
|
||||
|
||||
## Scope & not reproduced
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Internal contract — what `@ng-eventually/client` keeps off its surface, and what NextGraph does or would do about each subject
|
||||
# Internal contract — what `@ng-eventually/sdk` keeps off its surface, and what NextGraph does or would do about each subject
|
||||
|
||||
> **Updated 2026-08-03, after the source layout was reorganised by migration fate** (`docs/source-layout-by-fate.md`). Paths, and three names, changed under this document: `readModel` became the directly-exported `readUnion`; `accounts` / `AccountRecord` / `AccountStorage` became `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage` (module `shared-wallet/virtual-users.ts`); `store-registry-api.ts` became `surface/placement.ts`. Two modules were created and are covered here: `emulated-verifier/branch-registers.ts` (the four durable registers, split out of the shim) and `shared-wallet/bootstrap.ts` (the injection store, split out of the `/polyfill` entry). The subject-by-subject rulings below are unaffected — what moved is where the code lives, not what it promises.
|
||||
|
||||
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/client/src/` that is NOT reachable from the two published entry points (`package.json` maps exactly `.` → `src/index.ts` and `./polyfill` → `src/polyfill.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
**Scope.** The complement of [`docs/api-contract.md`](./api-contract.md): every module export under `packages/sdk/src/` that is NOT reachable from the two published entry points (`package.json` maps exactly `.` → `src/index.ts` and `./polyfill` → `src/polyfill.ts`). A consumer never reads this document; a maintainer does. The internal code is held to the same standard as the surface — as close as possible to what NextGraph does or plans — so every subject below carries the same target-side analysis. Written 2026-08-04, verified against the `nextgraph-rs` clone (HEAD `213338f6`) and the installed `@ng-org/web@0.1.2-alpha.13` declarations (`node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts`, hereafter `index.d.ts`).
|
||||
|
||||
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, plus `isNuri`/`hasReadCap` from `nuri.ts` and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (7 functions: `createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `userInbox`, `openDocumentInbox`, `documentInboxAddress`). `polyfill.ts` re-exports `share` from `inbox.ts`, `connectedUser` from `emulated-verifier/connect.ts`, `* as accounts` from `shared-wallet/virtualUsers.ts`, and the types `VirtualUserStorage`, `VirtualUserRecord`, `RegistrySession`. Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
|
||||
**How the boundary was computed — mechanically, from the `export` statements.** `index.ts` re-exports wholesale (`export *` / `export * as ns`) from `types.ts`, `inbox.ts`, `docs.ts`, `surface/read-model.ts`, and by name everything `surface/use-shape.ts`, `surface/watch-shape.ts`, `lifecycle.ts`, `sparql.ts` export, and `subscribeDoc`/`subscribeDocs`/`docChangeType` (+ types) from `subscribe.ts`; its `storeRegistry` namespace is the **`surface/placement.ts` slice only** (`createEntityDoc`, `listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `openDocumentInbox`). `model/nuri.ts` is now internal in full — the guards `isNuri`/`hasReadCap` stopped being published when the entries became permissive-in (`NuriLike` validated at the door). *(The second entry, `polyfill.ts`, was merged into `index.ts` on 2026-08-07; its polyfill-era symbols — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` and their types — now sit in a marked block of the single entry, and the machinery accessors and test resets it used to publish are no longer published at all.)* Everything else that carries `export` in a `src/` module is internal and inventoried here. Eight modules are internal in their entirety: `shared-wallet/access-log.ts`, `emulated-verifier/machinery.ts`, `surface/ng-proxy.ts`, `emulated-verifier/open-repo.ts`, `shared-wallet/outbox-log.ts`, `shared-wallet/physical.ts`, `emulated-verifier/reach.ts`, `emulated-verifier/read-filter.ts`. Four are internal in part: `nuri.ts`, `emulated-verifier/connect.ts`, `subscribe.ts`, `shared-wallet/account-registry.ts`.
|
||||
|
||||
**Labels** are those of `docs/api-contract.md`: **PASSTHROUGH (level 3/2, VERIFIED)**, **LEVEL-1 SHAPE (model VERIFIED, JS surface ASSUMED)**, **ASSUMPTION**, **NO COUNTERPART**. Level numbers per `README.md` § *The three references*: 3 = JS ORM, 2 = wasm binding (`@ng-org/web`), 1 = Rust engine. One label recurs here that the surface contract rarely needs: **NO COUNTERPART, shared-wallet machinery** — the code below the emulation's floor, which the target has no image of because the target has no shared wallet. Per the design principle, an absent implementation is never treated as evidence about the future.
|
||||
|
||||
@@ -146,7 +146,7 @@ Fire-and-forget wrapper over the published `connectedUser()` (restore Links, the
|
||||
|
||||
## 9. The shim registry — the unexported slice of `shared-wallet/account-registry.ts`
|
||||
|
||||
The sharpest boundary case: `surface/placement.ts` publishes the 7 app-facing calls; the 9 exports below stay internal (importable by the lib's modules, unit tests and the e2e harness, not by an application through the package entries). The types `VirtualUserRecord` (`store-registry.ts:90`) and `RegistrySession` (`:234`) are published via `/polyfill` and covered by the surface contract.
|
||||
The sharpest boundary case: `surface/placement.ts` publishes the 7 app-facing calls; the 9 exports below stay internal (importable by the lib's modules, unit tests and the e2e harness, not by an application through the package entries). The type `RegistrySession` is published by the entry (a consumer types its injected `getSession` with it) and covered by the surface contract; `VirtualUserRecord` is internal.
|
||||
|
||||
### 9a. Account shim — provision, resolve, reserved names, cache
|
||||
|
||||
@@ -164,7 +164,7 @@ export async function ensureAccount(id: string): Promise<VirtualUserRecord>;
|
||||
`resolveAccount` — barrier-authoritative O(1) lookup of one account's record in the doc-shim; `ensureAccount` — resolve-or-provision (creates the three scope docs on first sight, concurrency-deduped); `reservedAccount` — NUL-prefixed sentinel namespace for lib-internal accounts; `resetRegistryCache` — test/wallet-switch reset.
|
||||
|
||||
- **NO COUNTERPART, shared-wallet machinery — the whole group.** The target has no directory of identities to resolve or provision: a user's site (three stores + their inboxes) is created once at wallet creation (`engine/verifier/src/site.rs` — the site-creation flow committing the stores and the two store-inbox `AddInboxCap`s at `:128,149`), and "which user" is the session. `ensureAccount`'s provision-on-first-sight has no target analogue and is exactly what `connectedUser` refuses to trigger (`connect.ts:60-65`). All of it disappears with the shim (`docs/migration-guide.md` § 3).
|
||||
- `reservedAccount`'s collision-safety rests on an **ASSUMPTION about a consumer-injected function**: the comment (`store-registry.ts:200-206`) asserts the injected `normalizeId` can never produce a U+0000-prefixed key, but `normalizeId` is injected by the consumer and the lib's own default is a bare `trim()` (`polyfill.ts:145`), which does not strip U+0000. Bound: a consumer id would have to begin with a literal NUL to collide — implausible from any UI, but the property is the consumer's to keep, not the lib's (see Findings F5).
|
||||
- `reservedAccount`'s collision-safety rests on an **ASSUMPTION about a consumer-injected function**: the comment (`store-registry.ts:200-206`) asserts the injected `normalizeId` can never produce a U+0000-prefixed key, but `normalizeId` is injected by the consumer and the lib's own default is a bare `trim()` (`shared-wallet/bootstrap.ts`), which does not strip U+0000. Bound: a consumer id would have to begin with a literal NUL to collide — implausible from any UI, but the property is the consumer's to keep, not the lib's (see Findings F5).
|
||||
|
||||
### 9b. Scope-index resolution
|
||||
|
||||
@@ -249,9 +249,9 @@ export function inspectOutbox(): void;
|
||||
|
||||
**F3 — incomplete citation in `subscribe.ts`.** `subscribe.ts:31` cites the ORM fan-out abort as "`initialize.rs:125-128`" with no path. The file is `engine/verifier/src/orm/graph/initialize.rs`; lines 125-128 are the graph loop calling `self.open_for_target(&nuri.target, true).await?` — verified, the `?` propagates `RepoNotFound` and aborts the whole subscription. Substance correct; the bare filename is unfindable without this note.
|
||||
|
||||
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts:34` routes through `surface/placement.ts`; `polyfill.ts:238` carries `virtualUsers`) those are internal or `/polyfill`. That file is being edited concurrently; noted here, deliberately not fixed by this document.
|
||||
**F4 — `docs/api-contract.md` lags the `surface/placement.ts` split.** Its § 12 and appendix still list `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`, `reservedAccount`, `resetRegistryCache` as the SDK entry's `storeRegistry` namespace, and § 13/§ 15 place `virtualUsers.*` on the SDK entry — since the split (`index.ts` routes through `surface/placement.ts`) those are internal. That file is being edited concurrently; noted here, deliberately not fixed by this document.
|
||||
|
||||
**F5 — `reservedAccount`'s collision guarantee is asserted about code the lib does not own.** `store-registry.ts:200-206` states the injected `normalizeId` "strips a leading `@`, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is `id.trim()` (`polyfill.ts:145`), which passes U+0000 through. The reserved namespace is disjoint only if every consumer's normalizer keeps it so. Either document the requirement on `StoreRegistryDeps.normalizeId`, or reject NUL-prefixed raw ids at `accountKey`.
|
||||
**F5 — `reservedAccount`'s collision guarantee is asserted about code the lib does not own.** `store-registry.ts:200-206` states the injected `normalizeId` "strips a leading `@`, trims, and lowercases, so a NUL prefix is unreachable" — that describes ONE consumer's normalizer, not a contract; the lib's own default is `id.trim()` (`shared-wallet/bootstrap.ts`), which passes U+0000 through. The reserved namespace is disjoint only if every consumer's normalizer keeps it so. Either document the requirement on `StoreRegistryDeps.normalizeId`, or reject NUL-prefixed raw ids at `accountKey`.
|
||||
|
||||
**Migration-risk flags (shapes that will not travel):**
|
||||
|
||||
@@ -268,4 +268,4 @@ Fully internal modules: `shared-wallet/access-log.ts` (`AccessOp`, `setAccessLog
|
||||
|
||||
Internal slices of partially-published modules: `nuri.ts` (`targetOf`, `parseNuri`, `mintCap`); `emulated-verifier/connect.ts` (`startConnect`); `subscribe.ts` (`subscribePhysicalDoc`); `shared-wallet/account-registry.ts` (`reservedAccount`, `resetRegistryCache`, `resolveAccount`, `ensureAccount`, `userStoreDoc`, `isOwnInbox`, `myInboxes`, `addLink`, `readLinks`).
|
||||
|
||||
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/virtualUsers.ts`, `emulated-verifier/caps.ts`, `sparql.ts`, `lifecycle.ts`, `surface/use-shape.ts`, `surface/watch-shape.ts`, `surface/placement.ts`, and the two entry points.
|
||||
Modules with no internal exports (everything they export is published): `types.ts`, `docs.ts`, `inbox.ts`, `surface/read-model.ts`, `shared-wallet/virtualUsers.ts`, `emulated-verifier/caps.ts`, `sparql.ts`, `lifecycle.ts`, `surface/use-shape.ts`, `surface/watch-shape.ts`, `surface/placement.ts`, and the entry point.
|
||||
|
||||
@@ -96,7 +96,7 @@ The consumer application imports `@ng-org/web` / `@ng-org/orm` resolved to this
|
||||
via a build alias during the polyfill period. Removing the alias makes those imports
|
||||
resolve to the real SDK — the `ng`/`useShape`/`inbox` surface is SDK-identical, so
|
||||
no consumer code changes. The one non-SDK call — `configure(...)` /
|
||||
`@ng-eventually/client/polyfill` — is deleted. The lib itself disappears.
|
||||
the POLYFILL-ERA block of `@ng-eventually/sdk` — is deleted. The lib itself disappears.
|
||||
|
||||
## The one break already taken: `declareConnections`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Current-state NextGraph — what the SDK/broker do and do NOT expose
|
||||
|
||||
**Owner:** this library. `@ng-eventually/client` exists because the *current*
|
||||
**Owner:** this library. `@ng-eventually/sdk` exists because the *current*
|
||||
NextGraph JS SDK is immature. This file is the authoritative reference on what
|
||||
today's SDK/broker actually give us — the ground truth every polyfill in this
|
||||
lib compensates for. Read [`simulation.md`](./simulation.md) for how we emulate
|
||||
@@ -323,7 +323,7 @@ has both**:
|
||||
- **Subscribable with a sync BARRIER.** `doc_subscribe(nuri)` delivers `TabInfo` then
|
||||
an initial **`State`** (`verifier.rs:470`/`:476`); that first `State` is the sync
|
||||
barrier — **after it, presence is guaranteed and absence is definitive** (pinned
|
||||
empirically by CONTRACT 3 in `packages/client/e2e/`). But this barrier exists only
|
||||
empirically by CONTRACT 3 in `packages/sdk/e2e/`). But this barrier exists only
|
||||
for a repo `doc_subscribe` can open, i.e. a `did:ng:o:<RepoID>` repo. A **store-root
|
||||
has no first-`State` barrier**: an anchored read on it can return 0 rows during
|
||||
sync-lag with no signal distinguishing "still syncing" from "genuinely empty".
|
||||
@@ -747,7 +747,7 @@ chain never runs and consumers keep a stale value until the next connection
|
||||
delivers a fresh initial `State`. REMOTE commits DO push correctly (verified:
|
||||
cross-browser reactive update works). Verdict pending a live instrumented run.
|
||||
Full write-up (suspect link, instrumentation, planned polyfill-side fix):
|
||||
[`../packages/client/docs/sdk-reference.md`](../packages/client/docs/sdk-reference.md)
|
||||
[`../packages/sdk/docs/sdk-reference.md`](../packages/sdk/docs/sdk-reference.md)
|
||||
§ *Current emulation status*.
|
||||
|
||||
### Cold-start anchored read returns 0 rows instead of an error — symptom VERIFIED, mechanism INFERRED, healed polyfill-side
|
||||
@@ -756,7 +756,7 @@ On a FRESH session over the SAME persistent wallet (reconnect, new page, re-logi
|
||||
anchored `sparql_query` against a document written in an earlier session comes back with
|
||||
**0 rows and no error** — persisted documents read as empty. Observed on every anchored
|
||||
reader of the polyfill and healed identically in each (`ensureRepoOpen` before the read,
|
||||
`packages/client/src/emulated-verifier/open-repo.ts`): the user's own documents,
|
||||
`packages/sdk/src/emulated-verifier/open-repo.ts`): the user's own documents,
|
||||
the user's store (`shared-wallet/account-registry.ts` `readUserStore`), the by-need doc batch
|
||||
(`surface/read-model.ts` `readUnion`), and the store-root pointer read (`shared-wallet/account-registry.ts`
|
||||
`resolvePointer`). The heal is `doc_subscribe(nuri)` → await the first `State` (the sync
|
||||
@@ -792,7 +792,7 @@ is a single account subject carrying MULTIPLE values for one scope predicate (ob
|
||||
five `shim:docPublic`), after which a writer and a later reader can resolve DIFFERENT
|
||||
scope docs and the reader's anchored read returns 0.
|
||||
|
||||
Two polyfill-side guards, both in `packages/client/src/shared-wallet/account-registry.ts`: `ensureInFlight`
|
||||
Two polyfill-side guards, both in `packages/sdk/src/shared-wallet/account-registry.ts`: `ensureInFlight`
|
||||
(a bounded promise map keyed by account, so concurrent `ensureAccount` calls share ONE
|
||||
resolve-or-provision) prevents new forks; `canonicalDoc` (pick the lexicographically
|
||||
smallest NURI among all distinct values for a scope predicate — NURIs are
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ shared wallet. This is a design decision, grounded entirely in the query
|
||||
capability documented in
|
||||
[`nextgraph-current-state.md`](./nextgraph-current-state.md) § *The query
|
||||
capability*. The consumer application never sees any of this: it asks
|
||||
`@ng-eventually/client` for its lists by need and trusts the answer — the whole
|
||||
`@ng-eventually/sdk` for its lists by need and trusts the answer — the whole
|
||||
read mechanism lives here, in the polyfill.
|
||||
|
||||
> The rule in one line: read each by-need doc with its own anchored
|
||||
@@ -172,7 +172,7 @@ and never iterates the other named graphs. (A repo absent from `self.repos` thro
|
||||
`RepoNotFound` and is skipped per-doc, see the VERIFIED note above — the read cannot
|
||||
sync an unknown repo.)
|
||||
|
||||
> **Re-confirmed by the standing e2e harness (`packages/client/e2e/`, broker
|
||||
> **Re-confirmed by the standing e2e harness (`packages/sdk/e2e/`, broker
|
||||
> `@ng-org/web 0.1.2-alpha.13`).** The `docRoundTrip` check measures all three shapes
|
||||
> anchored to a doc D: (a) a no-GRAPH default-graph write round-trips; (b) an explicit
|
||||
> `INSERT DATA { GRAPH <D> {…} }` — a **constant** plain NURI — **also** round-trips
|
||||
|
||||
@@ -374,7 +374,7 @@ One thing a consumer must not conclude from the emulation: that a public documen
|
||||
|
||||
## 5. What the polyfill emulates (caps.ts) — and where it still diverges
|
||||
|
||||
**Realigned 2026-07-28 (batch P1a).** `packages/client/src/emulated-verifier/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `shared-wallet/account-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.
|
||||
**Realigned 2026-07-28 (batch P1a).** `packages/sdk/src/emulated-verifier/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `shared-wallet/account-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.
|
||||
|
||||
| | Real NextGraph | caps.ts emulation (post-P1a) |
|
||||
|---|---|---|
|
||||
|
||||
+4
-4
@@ -11,7 +11,7 @@
|
||||
> file for *how* each emulation works; read those two for *what is fake* and *what
|
||||
> replaces it*.
|
||||
|
||||
The consumer application writes against `@ng-eventually/client` as if NextGraph
|
||||
The consumer application writes against `@ng-eventually/sdk` as if NextGraph
|
||||
already shipped per-entity documents in public/protected/private stores, capabilities
|
||||
and inboxes. It hasn't (see [`nextgraph-current-state.md`](./nextgraph-current-state.md)).
|
||||
This file is the lib's own engineering doctrine on how it fabricates that mature
|
||||
@@ -143,7 +143,7 @@ public/protected/private stores — on top of one shared wallet.
|
||||
- **Generic by construction.** The registry knows only the three native scopes,
|
||||
zero application entity kind. The consumer application maps its entities to a scope
|
||||
and injects the session + identity-id normalization via `configureStoreRegistry({
|
||||
getSession, normalizeId })` (`polyfill.ts`).
|
||||
getSession, normalizeId })` (`shared-wallet/bootstrap.ts`, published by the entry).
|
||||
|
||||
The `store≠document` two axes materialize here directly: the registry moves along
|
||||
axis B (more documents = more isolation), never axis A (it always writes into the
|
||||
@@ -361,7 +361,7 @@ another name.
|
||||
|
||||
### Sharing, publication, and the recipient
|
||||
|
||||
- **`setCurrentUser(id)` (`polyfill.ts`)** — the SDK's "current identity" call.
|
||||
- **`setCurrentUser(id)` (`shared-wallet/bootstrap.ts`)** — the SDK's "current identity" call.
|
||||
It selects *whose* caps are consulted, lazily, so the delivered subset always
|
||||
reflects the identity in effect at read time.
|
||||
- **`inbox.share(doc, toUser)`** — the one sharing act the lib exposes. Recipients
|
||||
@@ -639,5 +639,5 @@ document trust root):
|
||||
(`did:ng:...`): validates and throws on IRI-breaking chars rather than emitting
|
||||
a malformed/injected query.
|
||||
|
||||
These are re-exported from `@ng-eventually/client` so the consumer application
|
||||
These are re-exported from `@ng-eventually/sdk` so the consumer application
|
||||
reuses the same escaping when it builds SPARQL.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Source layout by migration fate — analysis and recommendation
|
||||
|
||||
**Status: analysis only.** Nothing has been moved; no source file was modified. Written 2026-08-04 from the source of `packages/client/src/` (25 modules), the two contracts (`docs/api-contract.md`, `docs/internal-contract.md`) used as the export-level inventory, and the read-only `nextgraph-rs` clone (HEAD `213338f6`); the upstream facts this analysis leans on were re-verified at the source and are cited with layer numbers per `README.md` § *The three references* (1 = engine, 2 = wasm binding, 3 = JS ORM).
|
||||
**Status: ADOPTED.** The layout this document recommends is the one in the tree — `model/`, `surface/`, `emulated-verifier/`, `shared-wallet/`. Read the rest as the reasoning that produced it, not as a proposal. *(Two things have moved under it since: the package became `packages/sdk` / `@ng-eventually/sdk` on 2026-08-07 — it was `packages/client` / `@ng-eventually/client`, kept here as a search keyword — and `emulated-verifier/public-store.ts` was added on 2026-08-06. Paths below point at where the files are now.)*
|
||||
|
||||
Written 2026-08-04, when nothing had yet been moved, from the source of `src/` (25 modules), the two contracts (`docs/api-contract.md`, `docs/internal-contract.md`) used as the export-level inventory, and the read-only `nextgraph-rs` clone (HEAD `213338f6`); the upstream facts this analysis leans on were re-verified at the source and are cited with layer numbers per `README.md` § *The three references* (1 = engine, 2 = wasm binding, 3 = JS ORM).
|
||||
|
||||
**The question.** Today all 25 modules sit flat in `src/`, named mechanically. Three different fates coexist undistinguished: modules whose *shape* the consumer keeps (the surface the real SDK replaces), modules standing in for what the engine/verifier will do natively, and modules that exist only because the emulation runs on one shared wallet. The bet under evaluation: if the folder structure mirrors the target's own structure, divergence gets harder to commit and easier to spot.
|
||||
|
||||
@@ -44,11 +46,11 @@ And a fourth group the three-fate framing misses: the **target's model vocabular
|
||||
|
||||
## 2. The recommended layout
|
||||
|
||||
Both entry files stay at `src/` root, so `package.json`'s `exports` map (exactly `.` and `./polyfill`) is untouched and the consumer application sees no change.
|
||||
Both entry files stay at `src/` root, so `package.json`'s `exports` map is untouched and the consumer application sees no change. *(The two doors were merged into one on 2026-08-07 — the `exports` map is now just `.`, and the polyfill-era symbols sit in a marked block of `index.ts`.)*
|
||||
|
||||
| Folder | What the name asserts | Alignment reference | Fate at migration |
|
||||
|---|---|---|---|
|
||||
| `src/` root (`index.ts`, `polyfill.ts`) | The two published doors, nothing else. `index.ts` may re-export only from `surface/` and `model/`; `polyfill.ts` may re-export by name from anywhere — it is the polyfill-era door and its imports *are* the list of what dies. | — | `index.ts` is replaced by the real SDK via the build alias; `polyfill.ts` is deleted. |
|
||||
| `src/` root (`index.ts`, `polyfill.ts`) | The two published doors, nothing else. `index.ts` may re-export only from `surface/` and `model/`; `polyfill.ts` may re-export by name from anywhere — it is the polyfill-era door and its imports *are* the list of what dies. *(Merged 2026-08-07: one door, `index.ts`, with the polyfill-era symbols in a marked block. The rule survives as a block boundary instead of a file boundary.)* | — | The SDK-shaped half is replaced by the real SDK via the build alias; the polyfill-era block is deleted. |
|
||||
| `model/` | The target's addressing model, transcribed: pure vocabulary (types, NURI grammar, guards). No I/O, no state, no minting. Importable by every layer. | Level 1, verified (`NuriV0`, `readcap_nuri` — `engine/repo/src/types.rs:518-521`) | Survives as knowledge; the guards stay useful against the real SDK (which takes plain strings). |
|
||||
| `surface/` | App-facing, and every symbol has a target counterpart — verified or a documented bet — in `docs/api-contract.md`. A consumer coding against this folder learns nothing to unlearn. | Levels 3/2 where they answer, level-1 shape where they do not (per subject, in the contract) | Deleted when the alias flips; the consumer's code is unchanged. |
|
||||
| `emulated-verifier/` | Stand-ins for what the engine/verifier/broker do natively: possession, filing, boundary, non-delivery, inbox processing, branch registers, repo opening. Aligned on the level-1 model; each module names its native counterpart mechanism. **This is the folder where divergence from the model is possible, and its main risk.** | Level 1 (the model is the specification) | Deleted — the native side takes over. |
|
||||
@@ -128,7 +130,7 @@ Full mixed list: `docs.ts`, `surface/read-model.ts`, `inbox.ts`, `subscribe.ts`,
|
||||
|
||||
## 5. Cost and risk
|
||||
|
||||
**What does not change: the published surface.** Both entries keep their `src/` paths; `package.json`'s `exports` map is untouched; the `inbox.*` and `storeRegistry.*` namespaces are re-assembled at the entries with identical contents. A consumer application importing the two entries sees nothing — with one deliberate exception below.
|
||||
**What does not change: the published surface.** Both entries keep their `src/` paths *(they were merged into one on 2026-08-07)*; `package.json`'s `exports` map is untouched; the `inbox.*` and `storeRegistry.*` namespaces are re-assembled at the entries with identical contents. A consumer application importing the two entries sees nothing — with one deliberate exception below.
|
||||
|
||||
**Import churn — the inventory:**
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Vision & principles of the `@ng-eventually/client` polyfill
|
||||
# Vision & principles of the `@ng-eventually/sdk` polyfill
|
||||
|
||||
## Purpose
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Notebook — the library's example application
|
||||
|
||||
A minimal application written against `@ng-eventually/client`, in plain DOM.
|
||||
A minimal application written against `@ng-eventually/sdk`, in plain DOM.
|
||||
|
||||
It exists for two reasons, and the second is the one that matters.
|
||||
|
||||
**It shows how to use the library.** Every call in `app.ts` is what a real consumer writes. There is no test scaffolding, no privileged import, no reaching into the library's internals — it resolves `@ng-eventually/client` as an external consumer does. If something reads awkwardly here, it reads awkwardly for everyone.
|
||||
**It shows how to use the library.** Every call in `app.ts` is what a real consumer writes. There is no test scaffolding, no privileged import, no reaching into the library's internals — it resolves `@ng-eventually/sdk` as an external consumer does. If something reads awkwardly here, it reads awkwardly for everyone.
|
||||
|
||||
**It is what the e2e suite drives.** The suite used to talk to a bag of methods on `window.__sdk`, which proved the functions ran but never that an application could be written with them. That gap shipped a real defect: a document's inbox was green in tests and unusable in practice, because the harness handed an address across an identity boundary through a variable — something no application can do. This app can only do what an application can do.
|
||||
**It is what the e2e suite is meant to drive** — and does not yet, which this line says out loud rather than implying otherwise. The suite still talks to a bag of methods on `window.__sdk` (`packages/sdk/e2e/sdk-entry.ts`), which proves the functions run but never that an application can be written with them. That gap shipped a real defect once: a document's inbox was green in tests and unusable in practice, because the harness handed an address across an identity boundary through a variable — something no application can do. This app can only do what an application can do, which is why the applicative scenarios belong here.
|
||||
|
||||
It has already paid for itself twice: writing it surfaced that `UnionSubject` returned `string` where the values are always document references (so a consumer had to cast whatever it had just read before passing it back), and that the access gate normalized what a user typed but not what the URL carried.
|
||||
|
||||
@@ -16,4 +16,4 @@ Signing in, writing notes by scope, listing one's own, reading a note received a
|
||||
|
||||
## Running it
|
||||
|
||||
The e2e suite builds and serves it (`packages/client/e2e/`). To open it by hand you need a wallet: serve the folder with a bundled `app.js` and a `/shared-wallet.ngw`, and set `__NOTEBOOK_WALLET_PASSWORD__`.
|
||||
The e2e suite builds and serves it (`packages/sdk/e2e/`). To open it by hand you need a wallet: serve the folder with a bundled `app.js` and a `/shared-wallet.ngw`, and set `__NOTEBOOK_WALLET_PASSWORD__`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Notebook — a minimal application written against `@ng-eventually/client`.
|
||||
* Notebook — a minimal application written against `@ng-eventually/sdk`.
|
||||
*
|
||||
* It exists for two reasons, and the second is the one that matters:
|
||||
*
|
||||
@@ -28,6 +28,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
// SDK-shaped — these survive migration, the real SDK replaces them in place.
|
||||
docs,
|
||||
ensureIdentity,
|
||||
inbox,
|
||||
@@ -36,8 +37,11 @@ import {
|
||||
subscribeDoc,
|
||||
type Nuri,
|
||||
type Scope,
|
||||
} from "@ng-eventually/client";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "@ng-eventually/client/polyfill";
|
||||
// Polyfill-era — these three go away, and they are the whole of what goes away.
|
||||
configure,
|
||||
configureStoreRegistry,
|
||||
setCurrentUser,
|
||||
} from "@ng-eventually/sdk";
|
||||
import { ng as realNg, init as realInit } from "@ng-org/web";
|
||||
|
||||
// --- the domain, such as it is ---------------------------------------------
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"name": "@ng-eventually/example-notebook",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "A minimal application written against @ng-eventually/client — the library's usage example, and what the e2e suite drives.",
|
||||
"description": "A minimal application written against @ng-eventually/sdk \u2014 the library's usage example, and what the e2e suite drives.",
|
||||
"dependencies": {
|
||||
"@ng-eventually/client": "workspace:*",
|
||||
"@ng-eventually/sdk": "workspace:*",
|
||||
"@ng-org/web": "0.1.2-alpha.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,8 @@
|
||||
"DOM"
|
||||
],
|
||||
"paths": {
|
||||
"@ng-eventually/client": [
|
||||
"../../packages/client/src/index.ts"
|
||||
],
|
||||
"@ng-eventually/client/polyfill": [
|
||||
"../../packages/client/src/polyfill.ts"
|
||||
"@ng-eventually/sdk": [
|
||||
"../../packages/sdk/src/index.ts"
|
||||
],
|
||||
"@ng-org/web": [
|
||||
"../../node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts"
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"typecheck": "bunx tsc --noEmit -p packages/client/tsconfig.json"
|
||||
"typecheck": "bunx tsc --noEmit -p packages/sdk/tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
# @ng-eventually/client
|
||||
|
||||
Two entry points — the data-plane is SDK-identical, the polyfill bootstrap is
|
||||
separate:
|
||||
|
||||
| Import | Surface |
|
||||
|---|---|
|
||||
| `@ng-eventually/client` | The same signature as the SDK — `ng`, `useShape`, `inbox` (+ types). A drop-in for `@ng-org/web` / `@ng-org/orm`; as NextGraph matures it resolves to the real SDK (build alias removed) with no code change. |
|
||||
| `@ng-eventually/client/polyfill` | The only non-SDK surface — `configure`, `setCurrentUser`, and the capability surface (`capFor`, `shareCap`, `getCaps`). It falls away as NextGraph matures. |
|
||||
|
||||
> **Reading is key possession, and the isolation here is still fake.** The cap
|
||||
> surface has the shape of the real model — you hold a document's `ReadCap` or you
|
||||
> do not read it, and there is no authorization list anywhere — but nothing is
|
||||
> encrypted yet and several read paths bypass the guard entirely. Nothing this
|
||||
> library does may be described as "anonymous" or "private" until per-document
|
||||
> encryption lands (P1b).
|
||||
|
||||
```ts
|
||||
// bootstrap (the only non-SDK call) — inject the real SDK
|
||||
import { configure } from "@ng-eventually/client/polyfill";
|
||||
configure({ ng: realNg, useShape: realUseShape, sharedWallet, currentUser });
|
||||
|
||||
// from here on, a pure SDK surface:
|
||||
import { ng, useShape, inbox } from "@ng-eventually/client";
|
||||
await ng.doc_create(/* … */);
|
||||
const set = useShape(MyShape, scope); // filtered to what the identity may read
|
||||
await inbox.post(targetInbox, ref); // deposit (anticipated SDK API)
|
||||
```
|
||||
|
||||
## Principle — the polyfill compensates, it never extends
|
||||
|
||||
**The polyfill's ONLY reason to exist is to bridge a NextGraph implementation gap
|
||||
or a bug.** Every non-SDK surface must map to a capability NextGraph will provide
|
||||
natively, and must fall away at that point. The polyfill MUST NOT add functionality
|
||||
of its own — no bespoke features, no observability/tooling, no convenience API that
|
||||
isn't strictly "NextGraph will do this natively later." The test for any proposed
|
||||
addition: *does it compensate a real, exhibited NextGraph gap or bug?* If not, it
|
||||
does not belong here — build it in the consumer application, not in the polyfill.
|
||||
Corollary: a compensation whose gap is not actually exhibited on the target broker
|
||||
is dead weight, not defensive code — it should be removed, not kept "just in case."
|
||||
|
||||
What the polyfill adds on top of the real SDK (each emulated for now, native as
|
||||
NextGraph matures):
|
||||
- Shared-wallet identity (one wallet for everyone; the current identity id is
|
||||
relayed to the SDK).
|
||||
- Capability emulation — per-identity **cap possession** (`capFor`) and a read filter
|
||||
over it: you read the documents whose cap you hold. Creating a document files its
|
||||
cap; receiving one is an inbox deposit. There is no authorization list.
|
||||
- Anticipated methods (inbox `post`, `shareCap`) with their future-SDK shapes,
|
||||
emulated for now.
|
||||
|
||||
Generic: no application domain. The consumer application injects its shapes and
|
||||
performs the acts of sharing. The relationship concept ("who is connected to whom")
|
||||
is the consumer application's own — the client exposes only "share this one
|
||||
document's cap to that inbox".
|
||||
|
||||
### The cap surface in three calls
|
||||
|
||||
```ts
|
||||
import { capFor, shareCap, getCaps } from "@ng-eventually/client/polyfill";
|
||||
import { storeRegistry } from "@ng-eventually/client";
|
||||
|
||||
// Creating a document records its cap and you hold it — nothing to declare.
|
||||
const doc = await storeRegistry.createEntityDoc(myId, "protected");
|
||||
capFor(doc); // → `${doc}:r:…` — you hold it
|
||||
|
||||
// Share it with one recipient, addressed by their inbox. They need no "receive"
|
||||
// operation: their existing inbox.watch absorbs it.
|
||||
await shareCap(capFor(doc)!, theirInbox);
|
||||
|
||||
// Publishing is TWO acts: place the data in your public store, and circulate its
|
||||
// LINK. There is no discovery — you cannot be found, you can only be reached — so
|
||||
// the link has to travel: into an inbox, or into a document the reader already
|
||||
// holds. The bare NURI would name the document without opening it.
|
||||
const link = getCaps().publishRepoLink(publicDoc);
|
||||
await shareCap(link, theirInbox);
|
||||
```
|
||||
|
||||
The one invariant to keep in mind: **you never derive a cap from a bare reference.**
|
||||
You look it up in what you hold, or you were given it. A `did:ng:o:…` without `:r:`
|
||||
names a document and grants nothing.
|
||||
|
||||
### The types carry that invariant
|
||||
|
||||
`Nuri` and `ReadCap` are **template literal types**, not `string` aliases:
|
||||
|
||||
```ts
|
||||
type Nuri = `did:ng:${string}`
|
||||
type ReadCap = `did:ng:${string}:r:${string}`
|
||||
```
|
||||
|
||||
They are still strings — assignable to `string`, JSON-serializable, no wrapper — but
|
||||
the distinction is checked. A `ReadCap` goes wherever a `Nuri` is expected (a cap
|
||||
*is* a NURI with the key inside); the reverse does not compile:
|
||||
|
||||
```ts
|
||||
await shareCap(doc, theirInbox); // ✗ Argument of type '`did:ng:${string}`' is not
|
||||
// assignable to '`did:ng:${string}:r:${string}`'
|
||||
```
|
||||
|
||||
A string that comes from outside your code — storage, a URL, JSON, a form — is a
|
||||
plain `string`. **Narrow it, do not cast it**: a cast re-opens exactly the confusion
|
||||
the types close.
|
||||
|
||||
```ts
|
||||
import { isNuri, hasReadCap } from "@ng-eventually/client";
|
||||
|
||||
const saved = localStorage.getItem("cap");
|
||||
if (saved && hasReadCap(saved)) await shareCap(saved, theirInbox); // ✓ narrowed
|
||||
```
|
||||
|
||||
The runtime guards remain regardless — a JavaScript caller never meets the compiler,
|
||||
and a cast bypasses it — so passing a bare reference where a cap belongs throws with
|
||||
a message that says so.
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* @ng-eventually/client — the surface a consumer application codes against.
|
||||
*
|
||||
* Everything here has a target-SDK counterpart, verified or assumed, listed in
|
||||
* `docs/api-contract.md`. Import `ng` / `useShape` from here rather than from the
|
||||
* SDK during the polyfill period; at migration the build alias is removed and
|
||||
* these resolve to the real SDK.
|
||||
*
|
||||
* **This entry carries no machinery.** The earlier header claimed it exposed "ONLY
|
||||
* what `@ng-org/web` / `@ng-org/orm` expose", which was false as written: it also
|
||||
* shipped the whole `store-registry` module (account resolution, cap registers,
|
||||
* cache resets) and `accounts` (browser identity persistence, polyfill-era with no
|
||||
* SDK counterpart). Both leaked machinery onto the entry whose promise is that it
|
||||
* survives migration. `storeRegistry` is now the app-facing slice only
|
||||
* (`store-registry-api.ts`); `accounts` moved to `/polyfill`.
|
||||
*
|
||||
* The polyfill bootstrap — `configure`, the capability helpers, the current user,
|
||||
* identity persistence — lives at `@ng-eventually/client/polyfill`: everything an
|
||||
* application needs TODAY that will not exist tomorrow, kept apart so what goes
|
||||
* away is visible at the import line.
|
||||
*/
|
||||
|
||||
export * from "./model/types";
|
||||
export { useShape } from "./surface/use-shape";
|
||||
export { watchShape } from "./surface/watch-shape";
|
||||
export type { ShapeQuery, ShapeObservable } from "./surface/watch-shape";
|
||||
export { init, initNg } from "./surface/lifecycle";
|
||||
// The access gate: one call, before the app renders. It shows a technical barrier only
|
||||
// while the shared wallet needs one — the day the wallet supplies the identity it
|
||||
// resolves silently, and this line stays as it is (`shared-wallet/access-gate.ts`).
|
||||
export { ensureIdentity } from "./shared-wallet/access-gate";
|
||||
export type { SharedWalletConfig } from "./shared-wallet/access-gate";
|
||||
export * as inbox from "./surface/inbox";
|
||||
export * as docs from "./surface/docs";
|
||||
export { subscribeDoc, subscribeDocs, docChangeType } from "./surface/subscribe";
|
||||
export type { DocChange, DocChangeType, Unsubscribe } from "./surface/subscribe";
|
||||
// `readUnion` is exposed as a function, not under a `readModel` namespace: "model" is
|
||||
// neither the target's vocabulary nor neutral glue, and the namespace bought nothing —
|
||||
// it held one published function. Renamed 2026-08-03 by the vocabulary check.
|
||||
export { readUnion } from "./surface/read-model";
|
||||
export type { UnionSubject } from "./surface/read-model";
|
||||
export * as storeRegistry from "./surface/placement";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// SDK type re-exports — so the app imports these from @ng-eventually/client too,
|
||||
// not from @ng-org. `export type` is ERASED at build, so this adds NO runtime
|
||||
// @ng-org import to the lib (no risk of a duplicate SDK copy in the bundle).
|
||||
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
|
||||
export type { DeepSignalSet } from "@ng-org/alien-deepsignals";
|
||||
export type { NG } from "@ng-org/web";
|
||||
|
||||
import { makeNg } from "./surface/ng-proxy";
|
||||
|
||||
/** SDK-identical `ng` (wrapped). Drop-in replacement for `@ng-org/web`'s `ng`. */
|
||||
export const ng: Record<string, any> = makeNg();
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* `@ng-eventually/client/polyfill` — the polyfill-era door.
|
||||
*
|
||||
* Everything importable here disappears at migration, and that is the point of the
|
||||
* separate entry: what an application imports from this path is exactly what it will
|
||||
* have to delete. The SDK-identical entry (`@ng-eventually/client`) carries the other
|
||||
* promise — a target counterpart for every symbol.
|
||||
*
|
||||
* This file only RE-EXPORTS. The implementation lives with the fate it belongs to:
|
||||
* the injection store and the current-user relay in `shared-wallet/bootstrap.ts`
|
||||
* (no counterpart, evaporates), the cap registry in `emulated-verifier/caps.ts`
|
||||
* (stands in for the verifier's own bookkeeping). Keeping the store here made the
|
||||
* published entry a dependency of the code it publishes.
|
||||
*/
|
||||
|
||||
export type { StoreRegistryDeps, EventuallyConfig } from "./shared-wallet/bootstrap";
|
||||
export {
|
||||
configure,
|
||||
getConfig,
|
||||
resetConfig,
|
||||
configureStoreRegistry,
|
||||
getStoreRegistryDeps,
|
||||
resetStoreRegistry,
|
||||
setCurrentUser,
|
||||
resetCaps,
|
||||
} from "./shared-wallet/bootstrap";
|
||||
|
||||
// Cap surface — polyfill-era (caps are emulated now; native at migration).
|
||||
// Re-exported here so the whole polyfill API lives under /polyfill. `share`
|
||||
// lives in `inbox.ts` because sharing IS an inbox deposit (upstream: a sealed
|
||||
// message carrying the cap), but it is surfaced here so the cap vocabulary stays
|
||||
// on the polyfill side of the boundary rather than in the SDK-identical entry.
|
||||
export { share } from "./surface/inbox";
|
||||
export { connectedUser } from "./emulated-verifier/connect";
|
||||
|
||||
// --- what is deliberately NOT published --------------------------------------
|
||||
//
|
||||
// Removed 2026-08-05, each because an application coding against it learns something it
|
||||
// must unlearn — the one failure this library exists to prevent:
|
||||
//
|
||||
// - `getCaps` / `CapRegistry` — the emulation's engine room. The consumer question is
|
||||
// `capFor(doc)`: do I hold this? The registry object has neither a successor nor an
|
||||
// inert form, so anything built on it must be rewritten rather than left alone.
|
||||
// - `getCurrentUser` — an application knows who it signed in; asking the library back
|
||||
// is a convenience of the shared wallet, not a brick of the model.
|
||||
// - `virtualUsers` / `IdentityStore` — remembering an identity between sessions is the
|
||||
// application's job upstream too. The gate persists what IT needs
|
||||
// (`shared-wallet/access-gate.ts`); nothing else has to be exposed.
|
||||
//
|
||||
// And one more, removed 2026-08-06 with the public-store emulation:
|
||||
//
|
||||
// - `hasCap(doc)` — "do I hold this document's cap?". It read like "may I read this?",
|
||||
// and once a public store serves its caps to whoever asks
|
||||
// (`emulated-verifier/public-store.ts`) the two answers part company: a readable
|
||||
// document answers `false` right up until something asks for it. Nor is the question
|
||||
// one the target answers — upstream you open a document and find out. It had no
|
||||
// caller outside the tests, which now use the internal registry directly.
|
||||
//
|
||||
// What remains here is the whole polyfill-era surface: `configure`, `setCurrentUser`,
|
||||
// `share`, `connectedUser` and the test resets. Two of them are what an application calls.
|
||||
|
||||
// --- identity persistence (polyfill-era, no SDK counterpart) ----------------
|
||||
//
|
||||
// Moved here from the SDK-identical entry on 2026-08-03. `accounts` persists WHICH
|
||||
// virtual user is connected, in browser storage — a notion that exists only because
|
||||
// one shared wallet hosts several identities. The real SDK has no counterpart: there
|
||||
// each user opens their own wallet, and "who am I" is the session. Shipping it from
|
||||
// the SDK entry advertised as durable something that disappears at migration.
|
||||
// Config-shaped types the bootstrap needs; both describe the shim, not the SDK.
|
||||
export type { VirtualUserRecord, RegistrySession } from "./shared-wallet/account-registry";
|
||||
@@ -0,0 +1,123 @@
|
||||
# @ng-eventually/sdk
|
||||
|
||||
One entry point. Most of what it publishes has the same signature as the future SDK —
|
||||
`ng`, `useShape`, `watchShape`, `docs`, `inbox`, `storeRegistry`, `readUnion` (+ types) —
|
||||
and is a drop-in for `@ng-org/web` / `@ng-org/orm`: as NextGraph matures it resolves to
|
||||
the real SDK (build alias removed) with no code change.
|
||||
|
||||
**Four calls do not, and they are the whole of what you will delete:** `configure`,
|
||||
`configureStoreRegistry`, `setCurrentUser`, `connectedUser`. They exist because one
|
||||
shared wallet hosts every user; upstream, an application imports the SDK and each user
|
||||
opens their own wallet. `src/index.ts` groups them under a heading that says so.
|
||||
|
||||
*(There were two entry points until 2026-08-07, `.` and `./polyfill`, and the second one
|
||||
WAS that list. One door is easier to import from and says less — hence the grouping, and
|
||||
hence `docs/api-contract.md`, whose export inventory a test keeps honest.)*
|
||||
|
||||
Per-symbol, with the target signature and an epistemic label on every claim:
|
||||
[`docs/api-contract.md`](../../docs/api-contract.md).
|
||||
|
||||
> **Reading is key possession, and the isolation here is still fake.** The cap surface
|
||||
> has the shape of the real model — you hold a document's `ReadCap` or you do not read
|
||||
> it, and there is no authorization list anywhere — but nothing is encrypted yet and
|
||||
> the stand-in key is a constant. Nothing this library does may be described as
|
||||
> "anonymous" or "private" until per-document encryption lands (P1b).
|
||||
|
||||
```ts
|
||||
import {
|
||||
// SDK-shaped — the real SDK replaces these in place.
|
||||
ensureIdentity, storeRegistry, inbox, readUnion, docs,
|
||||
// Polyfill-era — these go away, and they are the whole of what goes away.
|
||||
configure, configureStoreRegistry, setCurrentUser,
|
||||
} from "@ng-eventually/sdk";
|
||||
|
||||
configure({ ng: realNg, useShape: realUseShape, sharedWallet });
|
||||
configureStoreRegistry({ getSession });
|
||||
await ensureIdentity(); // who am I (shared wallet)
|
||||
const doc = await storeRegistry.createEntityDoc(me, "protected");
|
||||
await docs.sparqlUpdate(sid, `INSERT DATA { … }`, doc);
|
||||
const subjects = await readUnion(await storeRegistry.listMyEntityDocs(me, "protected"));
|
||||
```
|
||||
|
||||
## Principle — the polyfill compensates, it never extends
|
||||
|
||||
**Its only reason to exist is to bridge a NextGraph implementation gap.** Every
|
||||
non-SDK surface must map to something NextGraph will provide natively, and must fall
|
||||
away at that point — no bespoke features, no observability, no convenience API that
|
||||
isn't strictly *"NextGraph will do this later"*. The test for any proposed addition:
|
||||
*does it compensate a real, exhibited gap?* If not, it belongs in the consumer
|
||||
application. And a compensation whose gap is not actually exhibited on the target
|
||||
broker is dead weight, not defensive code.
|
||||
|
||||
Both halves are binding — **the surface AND the implementation** stay as close as
|
||||
possible to what NextGraph plans. The question to ask at every choice: *would this
|
||||
make a caller learn something it has to UNLEARN at migration?* If yes, it is a
|
||||
deviation, whatever it buys.
|
||||
|
||||
What the polyfill adds, each emulated now and native later:
|
||||
|
||||
- **Shared-wallet identity** — one wallet hosts every user, so the library fabricates
|
||||
*virtual users* and confines every access to the connected one
|
||||
(`emulated-verifier/reach.ts`). Upstream, each user opens their own wallet.
|
||||
- **Capability emulation** — per-identity cap possession plus a read filter over it:
|
||||
you read the documents whose cap you hold. There is no authorization list, because
|
||||
the real model has none.
|
||||
- **Inbox** — `post`, `postToDocument`, `share`, and the recipient's processing.
|
||||
The model is verified (an inbox is a keypair on one repo); no JS surface exists yet.
|
||||
|
||||
Generic by construction: no application domain here. See
|
||||
[`examples/notebook`](../../examples/notebook) for an application written against it,
|
||||
which the e2e suite drives.
|
||||
|
||||
## How a document is reached — the three acts, and no others
|
||||
|
||||
```ts
|
||||
import { storeRegistry, inbox, readUnion } from "@ng-eventually/sdk";
|
||||
|
||||
// 1. CREATE — you hold its cap, with nothing to declare.
|
||||
const doc = await storeRegistry.createEntityDoc(me, "protected");
|
||||
|
||||
// 2. GIVE TO READ — name the document and the person. The key is looked up and
|
||||
// sealed into a deposit; the recipient applies it by connecting, with nothing
|
||||
// to call. Irreversible: there is no revoking a key already handed out.
|
||||
await inbox.share(doc, "bob");
|
||||
|
||||
// 3. CIRCULATE THE REFERENCE — no call at all. Every reference this surface returns
|
||||
// is BARE: it names the document and grants nothing. If the document sits in a
|
||||
// PUBLIC store, the store serves its read cap to whoever asks, so the bare
|
||||
// reference is enough to read it — and if it does not, the reference still names
|
||||
// it and opens nothing.
|
||||
const publicDoc = await storeRegistry.createEntityDoc(me, "public");
|
||||
// …put `publicDoc` in a QR code, a message, another document. Nothing else to do.
|
||||
await readUnion([publicDoc]); // a stranger holding only this reads it
|
||||
```
|
||||
|
||||
**The invariant behind all three: you never derive a cap from a bare reference.** You
|
||||
look it up in what you hold, you were given it, or a public store served it. A
|
||||
`did:ng:o:…` without `:r:` names a document and opens nothing — which is what makes
|
||||
confidentiality composable: a widely circulated document may point at a restricted
|
||||
one, and following the reference gets you a name, not a key. See
|
||||
[`docs/readcap-and-nuri-model.md`](../../docs/readcap-and-nuri-model.md) § 0.
|
||||
|
||||
## The types carry that invariant
|
||||
|
||||
`Nuri` and `ReadCap` are **template literal types**, not `string` aliases:
|
||||
|
||||
```ts
|
||||
type Nuri = `did:ng:${string}`
|
||||
type ReadCap = `did:ng:${string}:r:${string}`
|
||||
```
|
||||
|
||||
They are still strings — assignable to `string`, JSON-serializable, no wrapper — but
|
||||
the distinction is checked. A `ReadCap` goes wherever a `Nuri` is expected (a cap *is*
|
||||
a NURI with the key inside); the reverse does not compile.
|
||||
|
||||
**Permissive in, precise out.** Public entries take `NuriLike` (`Nuri | string`) and
|
||||
validate at the door, so a value coming from storage, a URL or a form needs no
|
||||
narrowing and no cast on your side; what they *return* is a precise `Nuri`. The
|
||||
runtime checks stay regardless — a JavaScript caller never meets the compiler.
|
||||
|
||||
```ts
|
||||
const saved = localStorage.getItem("doc"); // string | null
|
||||
if (saved) await readUnion([saved]); // ✓ validated at the door
|
||||
```
|
||||
@@ -1,10 +1,10 @@
|
||||
# SDK reference — reading data with `@ng-eventually/client`
|
||||
# SDK reference — reading data with `@ng-eventually/sdk`
|
||||
|
||||
**Audience:** anyone using `@ng-eventually/client` (the app that consumes it, and
|
||||
**Audience:** anyone using `@ng-eventually/sdk` (the app that consumes it, and
|
||||
the lib itself when honoring the contract). This is the reference on the SDK's
|
||||
**read/reactivity surface** — how you read data and how a read stays live.
|
||||
|
||||
`@ng-eventually/client` is written and consumed as if NextGraph were a **finished,
|
||||
`@ng-eventually/sdk` is written and consumed as if NextGraph were a **finished,
|
||||
mature SDK**: documents per entity placed by scope, capabilities, inboxes, and a
|
||||
**reactive ORM**. This file documents that finished-SDK contract. Where today's
|
||||
emulation does not yet deliver it, that is called out in one clearly-separated
|
||||
@@ -27,7 +27,7 @@ cited by `file:symbol` throughout so a future agent can re-verify cheaply.
|
||||
> reads are the exception, not the rule.**
|
||||
|
||||
```ts
|
||||
import { useShape } from "@ng-eventually/client";
|
||||
import { useShape } from "@ng-eventually/sdk";
|
||||
import { EventShapeType } from "…/shapes/orm/…";
|
||||
|
||||
function EventList() {
|
||||
@@ -108,9 +108,9 @@ and every subsequent patch to a `DeepSignalSet`
|
||||
`useDeepSignal` (`@ng-org/alien-deepsignals/react`). Vue and Svelte adapters exist
|
||||
alongside the React one (`sdk/js/orm/src/frontendAdapters/{vue,svelte}/`).
|
||||
|
||||
`@ng-eventually/client` re-exports `useShape` from
|
||||
`@ng-eventually/sdk` re-exports `useShape` from
|
||||
[`../src/surface/use-shape.ts`](../src/surface/use-shape.ts); import it from the SDK
|
||||
(`@ng-eventually/client`), never from `@ng-org/orm` directly.
|
||||
(`@ng-eventually/sdk`), never from `@ng-org/orm` directly.
|
||||
|
||||
### What you get, in order
|
||||
|
||||
@@ -161,7 +161,7 @@ computes a result and returns once (`sparql_query`,
|
||||
`sdk/js/lib-wasm/src/lib.rs:352`/`553`; no "subscribe to a query" exists —
|
||||
`sparql_query` is not reactive).
|
||||
|
||||
In `@ng-eventually/client` the one-shot read is exposed as:
|
||||
In `@ng-eventually/sdk` the one-shot read is exposed as:
|
||||
|
||||
- **`docs.sparqlQuery(sid, query, base?, anchor?)`** — a raw anchored SPARQL query
|
||||
([`../src/surface/docs.ts`](../src/surface/docs.ts)). `anchor` = the document NURI to read; the
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Real-broker plumbing for the SDK e2e harness — a DEDICATED test wallet for
|
||||
* `@ng-eventually/client`, fully separate from any consumer app's profile.
|
||||
* `@ng-eventually/sdk`, fully separate from any consumer app's profile.
|
||||
*
|
||||
* Adapted from the Festipod app's `src/shared/support/hooks.ts` (the reference
|
||||
* real-broker Playwright flow): headless wallet CREATION on nextgraph.eu, broker
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
*
|
||||
* Standalone (NOT `bun test`). Run:
|
||||
* bun run e2e/reactivity-doc-subscribe.ts
|
||||
* (or `bun run test:e2e:reactivity` from packages/client)
|
||||
* (or `bun run test:e2e:reactivity` from packages/sdk)
|
||||
*
|
||||
* It reuses the exact real-broker plumbing of run.ts / broker.ts: the dedicated lib
|
||||
* wallet, the broker iframe, `window.__sdk`. The CROSS case opens a SECOND page on
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Real-broker e2e runner for `@ng-eventually/client` — the polyfill's OWN suite,
|
||||
* Real-broker e2e runner for `@ng-eventually/sdk` — the polyfill's OWN suite,
|
||||
* in the SDK domain (no application concepts), with a DEDICATED wallet.
|
||||
*
|
||||
* Standalone (NOT `bun test`), so it never mixes into the fake-ng unit suite.
|
||||
* Run: `bun run e2e/run.ts` (or `bun run test:e2e` from packages/client).
|
||||
* Run: `bun run e2e/run.ts` (or `bun run test:e2e` from packages/sdk).
|
||||
*
|
||||
* It: builds the SDK page bundle, creates/reuses the dedicated lib wallet, opens
|
||||
* the broker iframe on the real broker with that wallet, waits for `window.__sdk`
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* SDK e2e harness entry — the MINIMAL page loaded inside the broker iframe.
|
||||
*
|
||||
* It imports the REAL `@ng-org/web` `ng` + this package (`@ng-eventually/client`),
|
||||
* It imports the REAL `@ng-org/web` `ng` + this package (`@ng-eventually/sdk`),
|
||||
* configures the polyfill session injection exactly the way a consumer does
|
||||
* (`configure` + `configureStoreRegistry`), waits for the real broker to hand back
|
||||
* a session, then exposes `window.__sdk`: a flat bag of async methods the
|
||||
@@ -19,10 +19,7 @@ import {
|
||||
configure,
|
||||
configureStoreRegistry,
|
||||
setCurrentUser,
|
||||
resetCaps,
|
||||
connectedUser,
|
||||
} from "@ng-eventually/client/polyfill";
|
||||
import {
|
||||
docs,
|
||||
subscribeDoc,
|
||||
subscribeDocs,
|
||||
@@ -31,19 +28,19 @@ import {
|
||||
storeRegistry,
|
||||
useShape as libUseShape,
|
||||
watchShape,
|
||||
} from "@ng-eventually/client";
|
||||
} from "@ng-eventually/sdk";
|
||||
// The harness tests the LIBRARY, so it legitimately reaches machinery a consumer
|
||||
// application must not — but through the internal path, never the published entry.
|
||||
// `storeRegistry` above is the app-facing slice; these are the shim internals.
|
||||
import * as registryInternals from "../src/shared-wallet/account-registry";
|
||||
import { getCaps, getCurrentUser } from "../src/shared-wallet/bootstrap";
|
||||
import { getCaps, getCurrentUser, resetCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { documentInboxAddress } from "../src/emulated-verifier/branch-registers";
|
||||
import * as virtualUsers from "../src/shared-wallet/virtual-users";
|
||||
import { ensureIdentity } from "@ng-eventually/client";
|
||||
import { ensureIdentity } from "@ng-eventually/sdk";
|
||||
// The harness narrows for its OWN assertions; a consumer never has to (the entries take
|
||||
// plain strings and validate inside). Internal path, like the rest of its machinery.
|
||||
import { isNuri } from "../src/model/nuri";
|
||||
import type { Nuri, ShapeObservable, ShapeQuery } from "@ng-eventually/client";
|
||||
import type { Nuri, ShapeObservable, ShapeQuery } from "@ng-eventually/sdk";
|
||||
|
||||
const { IdentityStore } = virtualUsers;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "@ng-eventually/client",
|
||||
"name": "@ng-eventually/sdk",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"description": "SDK-identical client wrapper over @ng-org/web + @ng-org/orm with emulated capabilities and inbox. Drop-in; remove at migration.",
|
||||
"description": "SDK-identical wrapper over @ng-org/web + @ng-org/orm with emulated capabilities and inbox. Drop-in; remove at migration.",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./polyfill": "./src/polyfill.ts"
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ng-org/web": "*",
|
||||
@@ -16,10 +15,18 @@
|
||||
"@ng-org/alien-deepsignals": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ng-org/web": { "optional": true },
|
||||
"@ng-org/orm": { "optional": true },
|
||||
"@ng-org/shex-orm": { "optional": true },
|
||||
"@ng-org/alien-deepsignals": { "optional": true }
|
||||
"@ng-org/web": {
|
||||
"optional": true
|
||||
},
|
||||
"@ng-org/orm": {
|
||||
"optional": true
|
||||
},
|
||||
"@ng-org/shex-orm": {
|
||||
"optional": true
|
||||
},
|
||||
"@ng-org/alien-deepsignals": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ng-org/web": "0.1.2-alpha.13",
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* `@ng-eventually/sdk` — the one door. Everything an application imports, it imports
|
||||
* from here.
|
||||
*
|
||||
* ── What the single entry costs, and how that cost is paid ────────────────
|
||||
* There were two entries until 2026-08-07 (`.` and `./polyfill`), and the second one
|
||||
* carried a signal worth naming before removing it: *what you import from that path is
|
||||
* exactly what you will delete at migration*. One door loses that — nothing at an
|
||||
* import line now distinguishes `configure`, which goes away, from `docs`, which the
|
||||
* real SDK replaces in place. Three things carry it instead:
|
||||
*
|
||||
* 1. **The `POLYFILL-ERA` block below**, which is the deletion list. It is short by
|
||||
* construction, and it is meant to keep shrinking.
|
||||
* 2. **`docs/api-contract.md`**, which rules on every symbol with an epistemic label
|
||||
* (PASSTHROUGH / LEVEL-1 SHAPE / ASSUMPTION / NO COUNTERPART) and whose export
|
||||
* inventory is pinned by `test/vocabulary.test.ts` — so it cannot go stale
|
||||
* quietly, which a hand-kept list would.
|
||||
* 3. **The names themselves.** Every published name is built from the target's own
|
||||
* vocabulary or carries a marker saying why it exists only here — pinned by the
|
||||
* same test. A name that has to disappear says so.
|
||||
*
|
||||
* ── What is deliberately NOT published ────────────────────────────────────
|
||||
* The entry publishes what an application CALLS, and nothing else. Not the machinery
|
||||
* accessors (`getConfig`, `getStoreRegistryDeps` — internal wiring the surface reaches
|
||||
* through `shared-wallet/bootstrap`), and not the test resets (`resetConfig`,
|
||||
* `resetStoreRegistry`, `resetCaps` — the suite reaches them by their internal path,
|
||||
* which is what they are for). Merging the entries made publishing those a visible
|
||||
* choice rather than an inherited one; the choice is no.
|
||||
*
|
||||
* Earlier removals, each because an application coding against it learns something it
|
||||
* must unlearn — the one failure this library exists to prevent:
|
||||
*
|
||||
* - `getCaps` / `CapRegistry` (2026-08-05) — the emulation's engine room. It has
|
||||
* neither a successor nor an inert form, so anything built on it must be rewritten.
|
||||
* - `getCurrentUser` (2026-08-05) — an application knows who it signed in; asking the
|
||||
* library back is a convenience of the shared wallet, not a brick of the model.
|
||||
* - `virtualUsers` / `IdentityStore` (2026-08-05) — remembering an identity between
|
||||
* sessions is the application's job upstream too. The gate persists what IT needs.
|
||||
* - `hasCap(doc)` (2026-08-06) — it read like "may I read this?", and once a public
|
||||
* store serves its caps to whoever asks (`emulated-verifier/public-store.ts`) the
|
||||
* two answers part company: a readable document answers `false` right up until
|
||||
* something asks. Upstream you open a document and find out.
|
||||
*/
|
||||
|
||||
// ── SDK-SHAPED — a target counterpart for every symbol ──────────────────────
|
||||
// At migration the build alias is removed and these resolve to the real SDK. The
|
||||
// per-symbol ruling, with its epistemic label, is in `docs/api-contract.md`.
|
||||
|
||||
export * from "./model/types";
|
||||
export { useShape } from "./surface/use-shape";
|
||||
export { watchShape } from "./surface/watch-shape";
|
||||
export type { ShapeQuery, ShapeObservable } from "./surface/watch-shape";
|
||||
export { init, initNg } from "./surface/lifecycle";
|
||||
export * as inbox from "./surface/inbox";
|
||||
export * as docs from "./surface/docs";
|
||||
export { subscribeDoc, subscribeDocs, docChangeType } from "./surface/subscribe";
|
||||
export type { DocChange, DocChangeType, Unsubscribe } from "./surface/subscribe";
|
||||
// `readUnion` is exposed as a function, not under a `readModel` namespace: "model" is
|
||||
// neither the target's vocabulary nor neutral glue, and the namespace bought nothing —
|
||||
// it held one published function. Renamed 2026-08-03 by the vocabulary check.
|
||||
export { readUnion } from "./surface/read-model";
|
||||
export type { UnionSubject } from "./surface/read-model";
|
||||
export * as storeRegistry from "./surface/placement";
|
||||
|
||||
// SDK type re-exports — so the app imports these from @ng-eventually/sdk too, not from
|
||||
// @ng-org. `export type` is ERASED at build, so this adds NO runtime @ng-org import to
|
||||
// the lib (no risk of a duplicate SDK copy in the bundle).
|
||||
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
|
||||
export type { DeepSignalSet } from "@ng-org/alien-deepsignals";
|
||||
export type { NG } from "@ng-org/web";
|
||||
|
||||
// ── POLYFILL-ERA — THE DELETION LIST ────────────────────────────────────────
|
||||
// Everything below exists because one shared wallet hosts every user, and nothing
|
||||
// below has a target counterpart. At migration each call goes, and the imports with
|
||||
// them. Keep this block short: an addition here is a promise to delete it later.
|
||||
|
||||
/**
|
||||
* Inject the real SDK, and tell the library about the shared wallet. Upstream nothing
|
||||
* is injected — an application imports the SDK and opens its own wallet — so this call
|
||||
* is the shape of that absence. `docs/api-contract.md` § 1.
|
||||
*/
|
||||
export { configure, configureStoreRegistry, setCurrentUser } from "./shared-wallet/bootstrap";
|
||||
export type { EventuallyConfig, StoreRegistryDeps } from "./shared-wallet/bootstrap";
|
||||
export type { RegistrySession } from "./shared-wallet/account-registry";
|
||||
/**
|
||||
* Await the connection work `setCurrentUser` fires: restore what was shared with this
|
||||
* user, and drain its inboxes. An application need not call it — the work runs anyway —
|
||||
* but it may want to know it has finished. Upstream this is the session opening.
|
||||
*/
|
||||
export { connectedUser } from "./emulated-verifier/connect";
|
||||
|
||||
// ── the access gate — polyfill-era in substance, one line in the app ────────
|
||||
// One call before the app renders. It shows a technical barrier only while the shared
|
||||
// wallet needs one; the day the wallet supplies the identity it resolves silently, and
|
||||
// this line stays as it is (`shared-wallet/access-gate.ts`).
|
||||
export { ensureIdentity } from "./shared-wallet/access-gate";
|
||||
export type { SharedWalletConfig } from "./shared-wallet/access-gate";
|
||||
|
||||
import { makeNg } from "./surface/ng-proxy";
|
||||
|
||||
/** SDK-identical `ng` (wrapped). Drop-in replacement for `@ng-org/web`'s `ng`. */
|
||||
export const ng: Record<string, any> = makeNg();
|
||||
@@ -25,11 +25,11 @@ export type Nuri = `did:ng:${string}`;
|
||||
* That confusion, left to runtime, silently turns "naming is not reading" into
|
||||
* "naming is reading" — the exact inversion this model exists to remove.
|
||||
*
|
||||
* It constrains the consumer's code the same way, which is the point: an app that
|
||||
* reads a cap back from storage, a URL or JSON gets a `string` and must pass it
|
||||
* through {@link isNuri} / {@link hasReadCap} (exported from the SDK entry) to use
|
||||
* it — a validation it should be doing anyway. The runtime guards stay regardless:
|
||||
* a JavaScript consumer bypasses the compiler entirely.
|
||||
* A consumer holding a plain `string` (from storage, a URL, JSON, a form) does NOT
|
||||
* have to narrow it: every public entry takes {@link NuriLike} and validates at the
|
||||
* door (`toNuri`), which is why no type guard is exported. Permissive in, precise
|
||||
* out. The runtime checks stay regardless — a JavaScript consumer never meets the
|
||||
* compiler, and a cast bypasses it.
|
||||
*/
|
||||
export type ReadCap = `did:ng:${string}:r:${string}`;
|
||||
|
||||
+1
-1
@@ -920,7 +920,7 @@ export async function createEntityDoc(id: string, scope: Scope): Promise<Nuri> {
|
||||
// `indexDoc` anchor scopes it) — the CANONICAL, always-safe shape the
|
||||
// anchored default-graph read queries (readUserStore below, same as
|
||||
// read-model.ts). Not a round-trip necessity on the current broker: the e2e
|
||||
// harness (`packages/client/e2e/`) verified an anchored `GRAPH <plainNuri>`
|
||||
// harness (`packages/sdk/e2e/`) verified an anchored `GRAPH <plainNuri>`
|
||||
// write ALSO round-trips here (same repo graph, no phantom graph); no-GRAPH
|
||||
// is kept as a simplicity/safety convention. entityNuri is a NURI stored as
|
||||
// a literal → escapeLiteral.
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
* (BARE-encoded Rust structs, base64url'd); decoding them to report concrete
|
||||
* write TARGETS (topics/docs) would mean duplicating the WASM verifier's wire
|
||||
* format in this polyfill, which is explicitly out of scope (SDK internals live
|
||||
* in the `@ng-eventually/client`-independent core repo, per this repo's
|
||||
* in the `@ng-eventually/sdk`-independent core repo, per this repo's
|
||||
* doctrine) — so only the pending COUNT is reported, never fabricated targets.
|
||||
* `sessionStorage` access itself can throw (sandboxed iframe, disabled storage —
|
||||
* see the exact error string handled in the core repo's `main.ts`
|
||||
@@ -168,7 +168,7 @@ export async function post(targetInboxLike: NuriLike, opts: PostOptions): Promis
|
||||
// default graph (same shape as read-model.ts readDoc/readUnion). This is the
|
||||
// CANONICAL, always-safe shape and the one the anchored default-graph read
|
||||
// queries. (Not a round-trip necessity on the current broker: the e2e harness
|
||||
// `packages/client/e2e/` verified that an anchored `GRAPH <plainNuri>` write
|
||||
// `packages/sdk/e2e/` verified that an anchored `GRAPH <plainNuri>` write
|
||||
// ALSO round-trips here — it resolves to the same repo graph, no phantom graph.
|
||||
// The no-GRAPH form is kept as a simplicity/safety convention; re-verify with
|
||||
// that harness if the broker version changes.)
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Lifecycle re-exports — SDK-shaped forwarders so the app imports `init` /
|
||||
* `initNg` from `@ng-eventually/client` rather than from `@ng-org/*`. They
|
||||
* `initNg` from `@ng-eventually/sdk` rather than from `@ng-org/*`. They
|
||||
* delegate to the REAL functions injected at `configure()`. Passthrough today;
|
||||
* a hook point later (e.g. opening the shared wallet on `init`).
|
||||
*/
|
||||
@@ -7,7 +7,8 @@
|
||||
*/
|
||||
import { getCurrentUser } from "../src/shared-wallet/bootstrap";
|
||||
import { test, expect, afterEach } from "bun:test";
|
||||
import {configure,configureStoreRegistry,resetConfig,resetStoreRegistry,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { ensureIdentity } from "../src/shared-wallet/access-gate";
|
||||
|
||||
const KEY = "ng-eventually:identity";
|
||||
@@ -21,7 +21,8 @@
|
||||
import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test";
|
||||
import { setAccessLog, enabled, shortNuri } from "../src/shared-wallet/access-log";
|
||||
import { docCreate, sparqlUpdate, sparqlQuery } from "../src/surface/docs";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,setCurrentUser,resetCaps,connectedUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, connectedUser, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
resetRegistryCache,
|
||||
} from "../src/shared-wallet/account-registry";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry } from "../src/index";
|
||||
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { resetOpenedRepos } from "../src/emulated-verifier/open-repo";
|
||||
|
||||
afterAll(() => {
|
||||
+2
-1
@@ -23,7 +23,8 @@
|
||||
import { describe, it, expect, mock, afterAll, beforeEach } from "bun:test";
|
||||
import { ensureAccount, resolveWriteGraph, resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
import { resetOpenedRepos } from "../src/emulated-verifier/open-repo";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,resetCaps,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { resetInfrastructure } from "../src/emulated-verifier/reach";
|
||||
|
||||
const SESSION = { sessionId: "sid-cold", privateStoreId: "PRIV-COLD" };
|
||||
+3
-1
@@ -26,7 +26,9 @@ import {
|
||||
} from "../src/shared-wallet/account-registry";
|
||||
import { documentInboxAddress, openDocumentInbox } from "../src/emulated-verifier/branch-registers";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,resetCaps,setCurrentUser,share,connectedUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, connectedUser, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { share } from "../src/surface/inbox";
|
||||
import { post, postToDocument, read as readInbox } from "../src/surface/inbox";
|
||||
import { readUnion } from "../src/surface/read-model";
|
||||
import { sparqlUpdate } from "../src/surface/docs";
|
||||
@@ -25,7 +25,8 @@ test("throws a clear error when configure() was not called", async () => {
|
||||
});
|
||||
|
||||
// From here on, a fake real `ng` is injected via configure().
|
||||
import {configure,resetCaps,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, setCurrentUser } from "../src/index";
|
||||
import { resetCaps } from "../src/shared-wallet/bootstrap";
|
||||
|
||||
function fakeNg() {
|
||||
return {
|
||||
@@ -2,7 +2,8 @@ import { test, expect, mock, beforeEach, afterAll } from "bun:test";
|
||||
import { post, read, materialize, watch } from "../src/surface/inbox";
|
||||
import { userInbox, resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
import type { Deposit } from "../src/surface/inbox";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
|
||||
// This suite injects a fake `ng` via configure() and reuses the storeRegistry's
|
||||
+3
-1
@@ -21,7 +21,9 @@ import { test, expect, mock, afterAll } from "bun:test";
|
||||
import { createEntityDoc, resetRegistryCache, userInbox, listMyEntityDocs } from "../src/shared-wallet/account-registry";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
import type { Nuri, ReadCap } from "../src/model/types";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,resetCaps,setCurrentUser,share} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { share } from "../src/surface/inbox";
|
||||
import { read as readInbox } from "../src/surface/inbox";
|
||||
import { filterReadable } from "../src/emulated-verifier/read-filter";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { test, expect, mock, afterEach } from "bun:test";
|
||||
import { makeNg } from "../src/surface/ng-proxy";
|
||||
import {configure,resetConfig,resetCaps,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig } from "../src/shared-wallet/bootstrap";
|
||||
|
||||
// This suite injects a fake `ng` via configure() and declares WRITE caps —
|
||||
// which stay an authorization list on purpose: only READING is key possession
|
||||
@@ -20,7 +20,8 @@
|
||||
import { describe, it, expect, mock, beforeEach, afterAll } from "bun:test";
|
||||
import { ensureRepoOpen, ensureReposOpen, resetOpenedRepos } from "../src/emulated-verifier/open-repo";
|
||||
import { readUnion } from "../src/surface/read-model";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,resetCaps,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { resetInfrastructure } from "../src/emulated-verifier/reach";
|
||||
import { resetRegistryCache } from "../src/shared-wallet/account-registry";
|
||||
|
||||
@@ -9,14 +9,8 @@ import { test, expect, mock, afterEach } from "bun:test";
|
||||
import { exposeReadCap, fetchReadCap, resetPublicStoreFetches } from "../src/emulated-verifier/public-store";
|
||||
import { mintCap } from "../src/emulated-verifier/caps";
|
||||
import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import {
|
||||
configure,
|
||||
configureStoreRegistry,
|
||||
resetConfig,
|
||||
resetStoreRegistry,
|
||||
resetCaps,
|
||||
setCurrentUser,
|
||||
} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import type { Nuri } from "../src/model/types";
|
||||
|
||||
const SHIM = "urn:ng-eventually:shim";
|
||||
@@ -14,7 +14,8 @@ import { test, expect, mock, afterAll } from "bun:test";
|
||||
import { sparqlQuery, sparqlUpdate, depositInto } from "../src/surface/docs";
|
||||
import { createEntityDoc, resetRegistryCache, userInbox } from "../src/shared-wallet/account-registry";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,resetCaps,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { mayReach, mustNotAttempt } from "../src/emulated-verifier/reach";
|
||||
import { hasReadCap } from "../src/model/nuri";
|
||||
|
||||
@@ -192,17 +193,18 @@ test("a BARE reference is reachable when the cap is possessed elsewhere", async
|
||||
// machinery reaches every virtual user's documents.
|
||||
test("the machinery is NOT part of the package's public surface", async () => {
|
||||
const entry: Record<string, unknown> = await import("../src/index");
|
||||
const polyfill: Record<string, unknown> = await import("../src/polyfill");
|
||||
|
||||
for (const surface of [entry, polyfill]) {
|
||||
for (const name of Object.keys(surface)) {
|
||||
for (const name of Object.keys(entry)) {
|
||||
expect(name).not.toMatch(/^physical/);
|
||||
}
|
||||
}
|
||||
// Named explicitly, so adding one and forgetting the rule fails here.
|
||||
for (const forbidden of ["physicalQuery", "physicalUpdate", "physicalCreate", "subscribePhysicalDoc"]) {
|
||||
expect(entry[forbidden]).toBeUndefined();
|
||||
expect(polyfill[forbidden]).toBeUndefined();
|
||||
}
|
||||
// …and the machinery accessors the merged entry deliberately stopped publishing
|
||||
// (2026-08-07): internal wiring and test resets are reached by their internal path.
|
||||
for (const unpublished of ["getConfig", "getStoreRegistryDeps", "resetConfig", "resetStoreRegistry", "resetCaps", "getCaps", "getCurrentUser"]) {
|
||||
expect(entry[unpublished]).toBeUndefined();
|
||||
}
|
||||
// The cross-account fan-out is gone from the registry entirely.
|
||||
const registry = entry.storeRegistry as Record<string, unknown>;
|
||||
@@ -2,7 +2,8 @@ import { getCaps } from "../src/shared-wallet/bootstrap";
|
||||
import { test, expect, mock, afterAll } from "bun:test";
|
||||
import { readUnion } from "../src/surface/read-model";
|
||||
import type { Nuri } from "../src/model/types";
|
||||
import {configure,configureStoreRegistry,resetCaps,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetCaps } from "../src/shared-wallet/bootstrap";
|
||||
|
||||
// The cap registry is process-wide, so each inject() starts from an empty one:
|
||||
// once ANY cap exists the possession gate is in force for every reader, and a
|
||||
+2
-1
@@ -10,7 +10,8 @@ import {
|
||||
resetRegistryCache,
|
||||
} from "../src/shared-wallet/account-registry";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry } from "../src/index";
|
||||
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
|
||||
// This suite injects a fake `ng` via configure(); bun runs test files in a
|
||||
// shared process with a single module singleton, and may run this file BEFORE
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test, expect, mock, afterAll } from "bun:test";
|
||||
import { subscribeDoc, subscribeDocs } from "../src/surface/subscribe";
|
||||
import {configure,configureStoreRegistry,resetConfig,resetStoreRegistry} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry } from "../src/index";
|
||||
import { resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import type { RegistrySession } from "../src/shared-wallet/account-registry";
|
||||
|
||||
// subscribeDoc/subscribeDocs wrap the REAL injected `ng.doc_subscribe`. This
|
||||
@@ -91,10 +91,10 @@ function words(name: string): string[] {
|
||||
|
||||
const SRC = path.join(import.meta.dir, "..", "src");
|
||||
|
||||
/** Every identifier the two entry points publish, read from the `export` statements. */
|
||||
/** Every identifier the entry point publishes, read from its `export` statements. */
|
||||
function publishedNames(): string[] {
|
||||
const out = new Set<string>();
|
||||
for (const entry of ["index.ts", "polyfill.ts"]) {
|
||||
for (const entry of ["index.ts"]) {
|
||||
const text = fs.readFileSync(path.join(SRC, entry), "utf8");
|
||||
// `export * as ns from "…"`
|
||||
for (const m of text.matchAll(/export \* as (\w+) from/g)) out.add(m[1]!);
|
||||
@@ -156,7 +156,7 @@ test("a reserved-namespace key cannot be produced by a consumer's normalizeId",
|
||||
// injected by the consumer — so a careless one must be refused, not trusted. A
|
||||
// collision would key a user onto an infrastructure account: reads and writes on
|
||||
// documents that are not theirs.
|
||||
const { configureStoreRegistry, resetStoreRegistry } = await import("../src/polyfill");
|
||||
const { configureStoreRegistry, resetStoreRegistry } = await import("../src/shared-wallet/bootstrap");
|
||||
const { ensureAccount, resetRegistryCache } = await import(
|
||||
"../src/shared-wallet/account-registry"
|
||||
);
|
||||
@@ -195,7 +195,7 @@ function contractInventory(): Record<string, string[]> {
|
||||
return out;
|
||||
}
|
||||
|
||||
test("the api-contract appendix lists exactly what the entries export", () => {
|
||||
test("the api-contract appendix lists exactly what the entry exports", () => {
|
||||
// The appendix is the instrument a reader diffs against when the surface moves. It
|
||||
// went stale once — still naming `storeRegistry`'s shim internals after the entry had
|
||||
// been narrowed to seven functions — and a stale inventory is worse than none: it
|
||||
@@ -217,7 +217,7 @@ test("the api-contract appendix lists exactly what the entries export", () => {
|
||||
/** Names that live inside a re-exported namespace rather than on the entry itself. */
|
||||
function isNamespaceMember(name: string): boolean {
|
||||
const src = path.join(import.meta.dir, "..", "src");
|
||||
for (const entry of ["index.ts", "polyfill.ts"]) {
|
||||
for (const entry of ["index.ts"]) {
|
||||
const text = fs.readFileSync(path.join(src, entry), "utf8");
|
||||
for (const m of text.matchAll(/export \* as \w+ from "\.\/([^"]+)"/g)) {
|
||||
const file = path.join(src, m[1]! + ".ts");
|
||||
@@ -25,7 +25,8 @@
|
||||
|
||||
import { describe, it, expect, mock, beforeEach, afterEach, afterAll } from "bun:test";
|
||||
import { watchShape } from "../src/surface/watch-shape";
|
||||
import {configure,configureStoreRegistry,resetStoreRegistry,resetConfig,resetCaps,setCurrentUser} from "../src/polyfill";
|
||||
import { configure, configureStoreRegistry, setCurrentUser } from "../src/index";
|
||||
import { resetCaps, resetConfig, resetStoreRegistry } from "../src/shared-wallet/bootstrap";
|
||||
import { resetRegistryCache, createEntityDoc } from "../src/shared-wallet/account-registry";
|
||||
import { resetOpenedRepos, setOpenTimeoutForTests, getSyncState } from "../src/emulated-verifier/open-repo";
|
||||
|
||||
Reference in New Issue
Block a user