feat!: retirer readUnion de la surface publiee, et lire un document par requete ancree
readUnion etait N requetes ancrees plus une politique de lot : une lecture qui echoue avalee en reponse vide, un document dont on ne detient pas la cle ecarte au lieu d'etre refuse, un regroupement en sacs de proprietes, et le filtrage du prefixe reserve. Rien en amont ne repond a cette composition, et son annotation citait sparql_query -- une citation exacte attachee a une affirmation fausse. Une composition ne s'aligne pas sur une fonction. Ce qui reste est ce qu'il y avait dessous : docs.sparqlQuery ancre sur un document, qui va deja chercher la cle servie par le magasin public -- donc une reference nue suffit toujours pour lire un document public, sans rien changer a useShapeQuery. Lire plusieurs documents, c'est cet appel une fois par document, et la tolerance a une lecture qui echoue revient a l'appelant, la ou l'amont la laisse : une requete ancree sur un depot que le verifier ne detient pas rejette. Cote src, l'unique lecture interne restante devient read-document.ts : un document, une requete ancree, precedee de l'ouverture du depot -- barriere que index-deposit consulte pour distinguer une absence d'un depot non synchronise. Le harnais e2e et l'exemple composent la leur, avec leur catch visible au point d'appel. Version majeure : 1.0.0-dev.8.
This commit is contained in:
+26
-20
@@ -1,6 +1,6 @@
|
||||
# API contract — what `@ng-eventually/polyfill` 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.
|
||||
> **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` (itself removed in `1.0.0-dev.8` — see § 6); `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 published entry point, 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.
|
||||
|
||||
@@ -257,9 +257,9 @@ export function useShapeQuery<T extends BaseType = BaseType>(
|
||||
): ShapeObservable<T>;
|
||||
```
|
||||
|
||||
**What it reads THROUGH, since `1.0.0-dev.6`: the SDK's own shape query.** `RdfOrmSubscription.getOrCreate(shapeType, scope)`, injected at `configure` as `ormSubscription` (§ 1). Four steps went with the change and they were one mechanism: resolving the logical scope to a document set (`listMyEntityDocs`), opening those documents through `ensureReposOpen` for a sync barrier, `readUnion` over them, and filtering the union by the shape's `@type` IRI. All four existed because the parameter had to be TRANSLATED; once it is upstream's own `Scope`, the question goes to the thing that answers it, and `graphs` / `subjects` / `did:ng:i` are read by the engine literally. It does NOT delegate to the SDK's `useShape` — that is React's hook and its `DeepSignalSet` cannot carry the request state, which is the whole reason this symbol exists. What is taken from the SDK is the QUERY; the observable, the snapshot and the state tracking stay here, and so does the capability filter (`emulated-verifier/read-filter.ts`, applied per `@graph` on what comes back, exactly where § 4 applies it).
|
||||
**What it reads THROUGH, since `1.0.0-dev.6`: the SDK's own shape query.** `RdfOrmSubscription.getOrCreate(shapeType, scope)`, injected at `configure` as `ormSubscription` (§ 1). Four steps went with the change and they were one mechanism: resolving the logical scope to a document set (`listMyEntityDocs`), opening those documents through `ensureReposOpen` for a sync barrier, an anchored read over each, and filtering the result by the shape's `@type` IRI. All four existed because the parameter had to be TRANSLATED; once it is upstream's own `Scope`, the question goes to the thing that answers it, and `graphs` / `subjects` / `did:ng:i` are read by the engine literally. It does NOT delegate to the SDK's `useShape` — that is React's hook and its `DeepSignalSet` cannot carry the request state, which is the whole reason this symbol exists. What is taken from the SDK is the QUERY; the observable, the snapshot and the state tracking stay here, and so does the capability filter (`emulated-verifier/read-filter.ts`, applied per `@graph` on what comes back, exactly where § 4 applies it).
|
||||
|
||||
**Consequences a caller sees.** `data` items are now the SHAPE's own objects — `@id`, `@graph` and the shape's readable predicates — where they were `UnionSubject` property bags; `shapeType` is typed `ShapeType<T>` instead of `unknown`, so `T` follows the shape and the snapshot is typed without a cast; and the read no longer opens a document of its own, so a public-store document whose cap has not been fetched by some other read does not appear until it has been (the same limit § 4 carries, and the same `CapRegistry.onChange` recovery).
|
||||
**Consequences a caller sees.** `data` items are now the SHAPE's own objects — `@id`, `@graph` and the shape's readable predicates — where they were the property bags the then-published `readUnion` returned; `shapeType` is typed `ShapeType<T>` instead of `unknown`, so `T` follows the shape and the snapshot is typed without a cast; and the read no longer opens a document of its own, so a public-store document whose cap has not been fetched by some other read does not appear until it has been (the same limit § 4 carries, and the same `CapRegistry.onChange` recovery).
|
||||
|
||||
**The name and the scope, since `1.0.0-dev.5`.** It was `watchShape(shapeType, "public" | "protected" | "private")`. `watchShape` has ZERO occurrences in `nextgraph-rs` and spends `watch`, a verb the ORM already spends on signal observation; the target's own name is `useShapeQuery`, announced by NextGraph's developers and on record in `.project/rules/nextgraph/surface-mirrors-the-sdk.md`, which is what the symbol's `announced-not-written` annotation cites. The three literals were this package's own and named the scope of a READ, which the SDK answers on two axes instead: `graphs` (one document's graph per entry, `did:ng:i` or `""` for the whole dataset, `[]` or `undefined` for no objects at all) and `subjects`. Nothing is translated any more: the `Scope` object a caller writes is the object the SDK receives.
|
||||
|
||||
@@ -284,31 +284,37 @@ So the constraint on the bet: the target can already answer "synced?" (`readyPro
|
||||
|
||||
---
|
||||
|
||||
## 6. One-shot listing — the read-model
|
||||
## 6. One-shot reading by name — the anchored query, and nothing above it
|
||||
|
||||
### Today — `@ng-eventually/polyfill`
|
||||
|
||||
```ts
|
||||
// read-model.ts:60
|
||||
export interface UnionSubject {
|
||||
subject: string; // the subject IRI as the document carries it — any IRI, not a Nuri
|
||||
graph: Nuri; // the document reference the caller passed, unchanged
|
||||
props: Record<string, string[]>;
|
||||
}
|
||||
// read-model.ts:166
|
||||
export async function readUnion(docs: Nuri[]): Promise<UnionSubject[]>;
|
||||
// docs.ts — the SDK's own primitive, and the whole of what this package offers here
|
||||
export async function sparqlQuery(
|
||||
sessionId: string | number,
|
||||
query: string,
|
||||
base?: string,
|
||||
anchorLike?: NuriLike,
|
||||
): Promise<unknown>;
|
||||
```
|
||||
|
||||
Behaviour: one anchored `sparql_query` per doc (default-graph body, no `GRAPH` wrapper), parallel, per-doc failure tolerance, cap filter applied inside, machinery subjects dropped.
|
||||
**`readUnion` and `UnionSubject` were published here until `1.0.0-dev.8`, and are gone.** `readUnion(docs)` issued one anchored `sparql_query` per document, in parallel, tolerated a per-document failure by swallowing it, applied the capability filter inside and dropped machinery subjects. Every one of those beyond the first is a POLICY, and none of them has a counterpart at any level: it was N calls plus a batch policy, and its annotation cited `sparql_query` — a citation that is exact attached to a claim that is false. **A composition cannot be aligned on a single function.**
|
||||
|
||||
**Grouping is per (document, subject), and subjects come back as written.** A document holding several subjects yields several entries — one each, properties never merged across subjects; the same subject IRI seen in two documents stays two entries, told apart by `graph`. This matches level 3, where an object carries `@id` and `@graph` as two distinct read-only properties and the ORM fabricates an `@id` when the writer leaves it empty (`sdk/js/orm/src/connector/GraphOrmSubscription.ts`, `":q:"`) — several objects per graph is the provided case, and `@id` is what distinguishes them inside a `@graph`. Only `graph` is a `Nuri`; `subject` is typed `string` because an RDF subject may be any IRI. One document per business entity remains the recommended placement (a key is per repo, so isolating an entity needs a repo of its own), but that is a recommendation about writing — the read reports what is there rather than making the other arrangement invisible.
|
||||
What replaces it is what was underneath: `docs.sparqlQuery(sessionId, query, undefined, doc)`, once per document you want. Reading several is that call in a loop, and the two policies come back to the caller, where upstream leaves them:
|
||||
|
||||
- **a failure is a failure.** The anchored query REJECTS for a document the verifier does not hold (`resolve_target_for_sparql` → `NgError::RepoNotFound`, `engine/verifier/src/request_processor.rs`), and this package refuses it earlier still for a document you hold no capability for. Whether one unreadable document ends your batch is now your decision, written at your call site rather than assumed by a primitive.
|
||||
- **shaping is yours.** The grouping into per-(document, subject) property bags was `UnionSubject`, a polyfill type with nothing upstream behind it. A SELECT's rows come back as rows; a caller that wants objects should use `getObjects` (§ 6bis) or `useShapeQuery` (§ 5), which are the SDK's own typed reads.
|
||||
- **the reserved prefix is yours to ignore.** `readUnion` dropped subjects under `urn:ng-eventually:` for you. A raw anchored read does not — see `## Guarantees` in the published contract, which now says so.
|
||||
|
||||
**Several subjects in one document is the PROVIDED case, whichever read is used.** Level 3 carries `@id` and `@graph` as two distinct read-only properties and fabricates an `@id` when the writer leaves it empty (`sdk/js/orm/src/connector/GraphOrmSubscription.ts`, `":q:"`), so a read must report subjects as written and never fold a document into one bag. One document per business entity remains the recommended placement (a key is per repo, so isolating an entity needs a repo of its own), but that is a recommendation about writing.
|
||||
|
||||
### Target
|
||||
|
||||
Two verified counterparts, one per level; neither returns `UnionSubject` — that grouping is lib-invented:
|
||||
Two verified counterparts, one per level:
|
||||
|
||||
```ts
|
||||
// level 2, VERIFIED — the primitive readUnion composes: index.d.ts:295, source sdk/js/lib-wasm/src/lib.rs:352 (nodejs) / :555 (web)
|
||||
// level 2, VERIFIED — the primitive, published here as `docs.sparqlQuery`: index.d.ts:295,
|
||||
// source sdk/js/lib-wasm/src/lib.rs:352 (nodejs) / :555 (web)
|
||||
declare function sparql_query(session_id: any, sparql: string, base: any, nuri: any): Promise<any>;
|
||||
|
||||
// level 3, VERIFIED — the one-shot typed read: sdk/js/orm/src/connector/getObjects.ts:23
|
||||
@@ -318,7 +324,7 @@ export async function getObjects<T extends BaseType>(
|
||||
); // returns a deep-cloned Set of matching objects
|
||||
```
|
||||
|
||||
The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query to that repo's graph as default graph (`resolve_target_for_sparql`, `engine/verifier/src/request_processor.rs:256-285`), an anchorless query unions every named graph in the session store (same function, `UserSite → None` → `set_default_graph_as_union`). At migration `readUnion` survives as composition (the anchored per-doc read is native); a consumer that wants typed results should be on `useShape`/`getObjects`, not on `UnionSubject` — the property-bag shape is a polyfill artifact, kept generic precisely so the consumer maps it into its own types and can drop it later.
|
||||
The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query to that repo's graph as default graph (`resolve_target_for_sparql`, `engine/verifier/src/request_processor.rs:256-285`), an anchorless query unions every named graph in the session store (same function, `UserSite → None` → `set_default_graph_as_union`). Reading several documents must therefore stay N ANCHORED queries: replacing them with one anchorless query is a silent widening of what a read can reach, and on a shared wallet it is O(wallet size).
|
||||
|
||||
---
|
||||
|
||||
@@ -794,12 +800,12 @@ 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`, `useShapeQuery`, `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".
|
||||
- ~~**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`, `useShapeQuery`, `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.**~~ **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` 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"**~~ **ANSWERED in `1.0.0-dev.5`.** The claim was in the module header with no provenance anywhere. It has one now, and it is the strongest kind available to something the target has not written: NextGraph's developers stated the direction, the statement is written down in `.project/rules/nextgraph/surface-mirrors-the-sdk.md` with who said it and when, and the symbol carries `kind=announced-not-written` citing that record. The name went with it — `useShapeQuery`, not `watchShape` (§ 5).
|
||||
- **`UnionSubject` property bags** — polyfill read-model shape, not a target type; map them into app types at the boundary (which `useShapeQuery`'s design already assumes).
|
||||
- ~~**`UnionSubject` property bags**~~ — REMOVED in `1.0.0-dev.8`, with `readUnion`. Nothing of the read model is published any more: an application reads one document with `docs.sparqlQuery` and shapes the rows itself, or reads by shape and gets the SDK's own objects.
|
||||
- **The sync-returning `subscribeDoc` unsubscribe** vs the target's promise-resolved one (§ 8) — a deliberate, documented ergonomic delta; an adapter is one line at migration, but it is a delta.
|
||||
|
||||
---
|
||||
@@ -811,7 +817,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
|
||||
### `@ng-eventually/polyfill` — `src/index.ts` (the only entry since 2026-08-07)
|
||||
|
||||
```text
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, GetObjectsLike, InsertObjectLike, NG, NgLike, Nuri, NuriLike, OrmSubscriptionLike, PrincipalId, RdfOrmConfig, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, getObjects, init, initNg, insertObject, ng, readUnion, subscribeDoc, subscribeDocs, useShape, useShapeQuery
|
||||
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, GetObjectsLike, InsertObjectLike, NG, NgLike, Nuri, NuriLike, OrmSubscriptionLike, PrincipalId, RdfOrmConfig, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, getObjects, init, initNg, insertObject, ng, subscribeDoc, subscribeDocs, useShape, useShapeQuery
|
||||
docs: docCreate, sparqlQuery, sparqlUpdate
|
||||
inbox: Deposit, PostOptions, post, postToDocument, processInbox, read, readForDocument, readSynced, readSyncedForDocument, share, watch
|
||||
storeRegistry: createEntityDoc, openDocumentInbox, privateStore, protectedStore, publicStore
|
||||
|
||||
Reference in New Issue
Block a user