fix: lire un document par la requête ancrée, readUnion ayant quitté le polyfill
Le contrat polyfill-surface passe de 1.0.0-dev.6 à 1.0.0-dev.15, épinglé désormais sur le tag polyfill/v1.0.0-dev.15. readUnion et UnionSubject ont été retirés en dev.8 : l'application e2e lit un document par un SELECT ancré regroupé par sujet, en ignorant les registres urn:ng-eventually: que la requête renvoie désormais. La déclaration usage_ng-helpers retire ce que l'engagement a absorbé (exemple de forme, initNg, orm_start_graph corrigé en dev.7, comportement d'un document illisible, lecture d'un document public par sa seule référence, pin par tag) et garde les demandes encore ouvertes. contract_indexing-layer 4.0.1 (patch) : la lecture d'un index ne cite plus readUnion.
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
type: contract
|
||||
summary: The API @ng-eventually/polyfill exposes to an application — signatures, guaranteed behaviour, and what it does not offer
|
||||
pulled_from: git@gitea.reconnexion.apps.gueraud.net:Reconnexion/ng-eventually.git/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md
|
||||
pulled_version: 77e78ec81de81959e90e40da8d302e274b025c51
|
||||
pulled_at: 2026-09-21
|
||||
pulled_version: ec3900077bfd090f5388e52b47a8132b2fc68d16
|
||||
pulled_at: 2026-10-01
|
||||
---
|
||||
|
||||
# contract_polyfill-surface — `@ng-eventually/polyfill`
|
||||
@@ -12,7 +12,7 @@ pulled_at: 2026-09-21
|
||||
|
||||
This package is a polyfill of NextGraph's SDK.
|
||||
|
||||
This package covers placement (creating an application's documents in one of its three stores), reading (a document's subjects, one-shot or reactive, by shape), sharing a document with a named user, and depositing into inboxes. It does not cover user management, display names, transport, or the operation of a deployment.
|
||||
This package covers placement (creating an application's documents in one of its three stores), reading (a document's subjects, one-shot or reactive, by shape), writing one shape-typed object, sharing a document with a named user, and depositing into inboxes. It does not cover user management, display names, transport, or the operation of a deployment.
|
||||
|
||||
### Deployment requirements
|
||||
|
||||
@@ -20,6 +20,8 @@ An application using this package must:
|
||||
|
||||
- serve a wallet file (`.ngw`) from its own bundle, and pass its URL and password to `configure` as `sharedWallet: { fileUrl, password }`;
|
||||
- pass `configure` the SDK's shape query (`ormSubscription: RdfOrmSubscription.getOrCreate`, from `@ng-org/orm`) and its `initNg`, and call this package's `initNg(ng, session)` once a session has arrived, if it uses `useShapeQuery`. That read goes through the SDK now, and the SDK is injected here rather than imported; without the injection the read publishes `isError` naming the field;
|
||||
- pass `configure` the SDK's `getObjects` and `insertObject` (from `@ng-org/orm`, as they stand) if it calls the calls of the same name here. Same reason and same shape as the shape query: they are injected rather than imported, and without the injection each throws naming the field to pass;
|
||||
- call this package's `initNg`, not the SDK's, whenever it uses anything of the ORM. That call decides which `ng` the ORM reaches the engine through, and this package substitutes its own - the same object `ng` publishes - so that an ORM write meets the same guard every other write meets. Calling `@ng-org/orm`'s `initNg` directly leaves ORM writes ungoverned;
|
||||
- call `init(…)` — this package's, not the one it passed to `configure` — and then await `ensureIdentity()`, in a browser context, before rendering its interface. `ensureIdentity()` resolves once a session is open, and a session arrives only through `init`: awaited before `init` has been called, it throws and names the call to make first;
|
||||
- **declare this package exactly once**, so that everything in the application resolves to one copy of it — see the single-copy rule under `## Non-guarantees`, which is the one packaging mistake this surface cannot protect you from.
|
||||
|
||||
@@ -34,10 +36,16 @@ Full typed shape: the package's `types` entry, `@ng-eventually/polyfill`. A type
|
||||
export function configure(c: EventuallyConfig): void;
|
||||
export interface EventuallyConfig {
|
||||
ng: NgLike; // the `ng` object from @ng-org/web
|
||||
useShape: UseShapeLike; // `useShape` from @ng-org/orm
|
||||
// The ORM's REACT hook. It is NOT on `@ng-org/orm`'s main entry - that resolves to
|
||||
// `core`, which exports no `useShape` - so import it from `@ng-org/orm/react` (the
|
||||
// default export) or as `reactUseShape` from `@ng-org/orm/all`. Pass it as it
|
||||
// stands; no wrapper, no cast.
|
||||
useShape: UseShapeLike;
|
||||
// `RdfOrmSubscription.getOrCreate` from @ng-org/orm - the SDK's shape query, which
|
||||
// `useShapeQuery` reads through. Pass it as it stands; no wrapper, no cast.
|
||||
ormSubscription?: OrmSubscriptionLike;
|
||||
getObjects?: GetObjectsLike; // `getObjects` from @ng-org/orm
|
||||
insertObject?: InsertObjectLike; // `insertObject` from @ng-org/orm
|
||||
sharedWallet?: SharedWalletConfig; // { fileUrl, password, importUrl? }
|
||||
debugAccessLog?: boolean;
|
||||
init?: (...args: any[]) => any;
|
||||
@@ -65,9 +73,14 @@ export const storeRegistry: { // no identity parameter — a session is
|
||||
};
|
||||
|
||||
// ── reading ──────────────────────────────────────────────────────────────
|
||||
export async function readUnion(docs: NuriLike[]): Promise<UnionSubject[]>;
|
||||
export interface UnionSubject { subject: string; graph: Nuri; props: Record<string, string[]> }
|
||||
export function useShape(shapeType: unknown, scope: unknown): unknown; // read-filtered view
|
||||
// Reading a document BY NAME is `docs.sparqlQuery` below - one anchored query, one
|
||||
// document. `readUnion` and `UnionSubject` were published here until `1.0.0-dev.8`;
|
||||
// nothing takes their place, because they were that call in a loop plus policies of
|
||||
// this package's own. See `## Change policy`.
|
||||
export function useShape<T extends BaseType>( // read-filtered view
|
||||
shapeType: ShapeType<T>,
|
||||
conf: RdfOrmConfig<T> | string | undefined,
|
||||
): ShapeResult; // { data, promise, subscription, isLoading, nextPage?, previousPage? }
|
||||
// TWO positional arguments — the same pair `useShape` takes, both of them the SDK's own
|
||||
// types. `ShapeQuery` is what you READ BACK (the snapshot), never what you pass in, and
|
||||
// its items are the SHAPE's objects: `@id`, `@graph` and the shape's readable predicates.
|
||||
@@ -83,8 +96,19 @@ export interface ShapeObservable<T extends BaseType = BaseType> {
|
||||
export interface ShapeQuery<T extends BaseType = BaseType> {
|
||||
data: T[]; isPending: boolean; isSuccess: boolean; isError: boolean; error: unknown;
|
||||
}
|
||||
// The ORM's one-shot read, at the SDK's own name and signature. `config` is a `Scope`
|
||||
// plus `where` and `orderBy`; the result is an ARRAY when it carries an `orderBy` and a
|
||||
// `Set` otherwise, and it is a CLONE - mutating or deleting an item of it reaches nothing.
|
||||
export async function getObjects<T extends BaseType>(
|
||||
shapeType: ShapeType<T>,
|
||||
config: Omit<RdfOrmConfig<T>, "pageSize" | "maxActivePages">,
|
||||
): Promise<T[] | Set<T>>;
|
||||
export type RdfOrmConfig<T> = Scope & { where?: ...; orderBy?: ... }; // the SDK's own
|
||||
export function subscribeDoc(nuri: NuriLike, onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
|
||||
export function subscribeDocs(nuris: NuriLike[], onChange: (r: DocChange, t: DocChangeType) => void): Unsubscribe;
|
||||
// Extracts the variant key from a DocChange - "State" / "Patch" / "TabInfo" / ... -
|
||||
// a convenience over the verified payload shape, not an upstream call.
|
||||
export function docChangeType(resp: DocChange): DocChangeType;
|
||||
|
||||
// ── low-level document / SPARQL primitives ───────────────────────────────
|
||||
export const docs: {
|
||||
@@ -98,6 +122,12 @@ export const docs: {
|
||||
sparqlUpdate(sessionId: string | number, query: string, anchor?: NuriLike, label?: string): Promise<unknown>;
|
||||
};
|
||||
|
||||
// -- writing one shape-typed object ---------------------------------------
|
||||
// The SDK's own name and signature. The object MUST carry `@graph` - it is the only
|
||||
// thing that says which document the write lands in - and what comes back is the object
|
||||
// with its `@id` filled in where one was left empty.
|
||||
export async function insertObject<T extends BaseType>(shapeType: ShapeType<T>, object: T): Promise<T>;
|
||||
|
||||
// ── inbox: giving to read, and depositing ────────────────────────────────
|
||||
export const inbox: {
|
||||
share(doc: NuriLike, toUser: string): Promise<void>; // give a reader the key
|
||||
@@ -116,29 +146,90 @@ export interface Deposit { from: PrincipalId | null; payload: unknown; ts: numbe
|
||||
|
||||
// ── the wrapped SDK objects ──────────────────────────────────────────────
|
||||
export const ng: NG; // call this instead of the `ng` passed to `configure`
|
||||
// `NG` is upstream's own type (`@ng-org/web`), 88 typed
|
||||
// `NG` is upstream's own type (`@ng-org/web`), fully typed
|
||||
// members; it was `Record<string, any>` until 2026-08-14
|
||||
export function init(...args: any[]): any; // likewise — not the `init` passed to `configure`
|
||||
export function initNg(...args: any[]): any;
|
||||
export function initNg(...args: any[]): any; // likewise, and position 0 is DISCARDED - see below
|
||||
```
|
||||
|
||||
**A shape, in full, because one has to be written by hand.** `ShapeType` asks for `readablePredicate`, `minCardinality` and `maxCardinality` on every predicate, a `schema` keyed by the shape's own IRI, and objects carrying `@id` and `@graph` - and assembling that from the SDK's type declarations is a read this leaf exists to spare you. The shape below is `examples/notebook/app.ts`'s, predicate for predicate - that application reads with it against a deployed broker - and `rdf-orm build` generates exactly this object from a `.shex` file, so nothing here depends on the generator. The object interface and the type argument beside it are not that application's: it reads an untyped snapshot and narrows at the call site, which works and types nothing.
|
||||
|
||||
```ts
|
||||
import { useShapeQuery } from "@ng-eventually/polyfill";
|
||||
import type { BaseType, ShapeType } from "@ng-eventually/polyfill";
|
||||
|
||||
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
||||
|
||||
// What a read HANDS BACK: `@id` and `@graph` come from `BaseType`, plus one field per
|
||||
// predicate, named by that predicate's `readablePredicate`.
|
||||
interface NoteObject extends BaseType {
|
||||
type: string;
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
const NoteShape = {
|
||||
shape: "urn:notebook:NoteShape", // the shape's own IRI...
|
||||
schema: {
|
||||
"urn:notebook:NoteShape": { // ...which is also the `schema` key...
|
||||
iri: "urn:notebook:NoteShape", // ...and the shape's own `iri`
|
||||
predicates: [
|
||||
// The matching constraint: a subject carrying `rdf:type urn:notebook:Note`.
|
||||
{ iri: RDF_TYPE, readablePredicate: "type",
|
||||
dataTypes: [{ valType: "iri", literals: ["urn:notebook:Note"] }],
|
||||
minCardinality: 1, maxCardinality: 1 },
|
||||
// And the fields that come BACK. A predicate the shape does not NAME is not
|
||||
// projected, so a shape written only to match returns objects with no fields.
|
||||
{ iri: "urn:notebook:title", readablePredicate: "title",
|
||||
dataTypes: [{ valType: "string" }], minCardinality: 1, maxCardinality: 1 },
|
||||
{ iri: "urn:notebook:body", readablePredicate: "body",
|
||||
dataTypes: [{ valType: "string" }], minCardinality: 1, maxCardinality: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
} satisfies ShapeType<NoteObject>;
|
||||
|
||||
// Pass the object type as the TYPE ARGUMENT. `ShapeType<T>` mentions `T` in none of its
|
||||
// members (it is `{ schema, shape }`), so nothing infers it from the shape: left out, `T`
|
||||
// is `BaseType` and the snapshot is untyped. MEASURED - `tsc` refuses the assignment.
|
||||
const notes = useShapeQuery<NoteObject>(NoteShape, { graphs: "did:ng:i" });
|
||||
```
|
||||
|
||||
`minCardinality: 1` is what turns a predicate into a required triple pattern, so a shape whose every predicate is optional matches every subject in reach. `maxCardinality: -1` is unbounded. `valType` is one of `"number" | "string" | "boolean" | "iri" | "shape"`, and `literals` pins the required value - which is how the `rdf:type` entry above becomes the match rather than a field you read.
|
||||
|
||||
**`initNg`'s FIRST ARGUMENT IS DISCARDED, and what goes in its place is the `ng` above.** The Deployment requirements say to call `initNg(ng, session)` while `## Surface` says to use this package's `ng` rather than the one `configure` received, so there is a choice to make - and it makes none: this call drops whatever sits in position 0 and hands NextGraph's ORM the published `ng`, the same object, then passes every remaining argument and the return value straight through. MEASURED on `1.0.0-dev.14`: a sentinel object passed in position 0 never arrives, and what does arrive is `===` the exported `ng`. That is deliberate rather than a defect - the ORM reaches the engine through whatever this call resolves, an ORM write leaves by `ng.graph_orm_update`, and that is where the write guard sits - but it is not something to have to establish by experiment. The parameter stays in the signature because upstream's `initNg(ngImpl, session)` has it, and a call site written for the target must keep compiling.
|
||||
|
||||
**`ShapeObservable` is on the deletion list, and it goes WITH the shared wallet, not before it.** It is not a framework adapter: it carries the capability filter (`getSnapshot()` returns a view filtered by the capabilities held) and the capability-arrival re-publish (a subscription that re-publishes when a capability arrives). A capability arrives mid-session only because the wallet is shared; in the target an identity holds its own wallet and its capabilities from the start, so there is nothing to filter and no arrival to re-publish on. The announced form, a hook returning `{ data, isPending, isSuccess, isError, error }`, has nowhere to hang such a subscription, because the target does not need one. The type stays exported and you may write it down; what you cannot do is plan to drop it on its own ahead of the rest of the shared-wallet code.
|
||||
|
||||
**`refetch()` has NO RECORDED COUNTERPART.** It is the only published way out of a read that is stuck pending. The announcement recorded in this repository lists five fields, `{ data, isPending, isSuccess, isError, error }`, and `refetch` is not among them. That is a statement about the RECORD, nothing more: TanStack Query's own `useQuery` does return a `refetch`, so a successor may well exist, but this repository holds no record of one and cannot name what an application leaning on `refetch()` would migrate onto. Treat it as an open migration point, not as a call that has a mapped replacement.
|
||||
|
||||
## Guarantees
|
||||
|
||||
Every entry accepts `NuriLike` and validates at the door; what it returns is a precise `Nuri`. No type guard is published.
|
||||
|
||||
A returned reference carries no key — not `createEntityDoc`, not the three store accessors, not `UnionSubject.subject` / `.graph`. A reference found inside a document yields a name, not a key.
|
||||
A returned reference carries no key — not `createEntityDoc`, not the three store accessors. A reference found inside a document yields a name, not a key.
|
||||
|
||||
You read a document whose key you hold: you created it, it was shared with you, or it sits in a public store, which serves its read key to whoever asks. No call answers "may I read this?".
|
||||
|
||||
What was shared with you becomes readable after `ensureIdentity()`.
|
||||
|
||||
`readUnion` returns one entry per distinct subject present in a document. `subject` is that subject's IRI exactly as written, and is a `string`, because a subject may be any IRI; `graph` is the document reference you passed in, and is the `Nuri` to hand back to this surface. Properties of different subjects are never merged, and the same subject IRI found in two documents stays two entries, told apart by `graph`. Several objects in one document are allowed. Recommended placement is one document per business entity: access is granted per document.
|
||||
**An anchored `docs.sparqlQuery` reads exactly the document you name, and returns its triples as written.** The anchor makes that document's graph the query's default graph, so a body with no `GRAPH <...>` wrapper reads that document and nothing else; a body that DOES carry one iterates the named graphs regardless of the anchor, so do not write one. Subjects come back as the document carries them — several subjects in one document is allowed and each stays its own, since a subject may be any IRI. Reading several documents is this call once per document; there is no call that takes a list, and composing one with an anchorless query instead would silently widen what the read reaches. Recommended placement is one document per business entity: access is granted per document.
|
||||
|
||||
`urn:ng-eventually:` is reserved, and the reservation has two faces. In a document: triples whose **subject** falls under that prefix are dropped on read and never returned by `readUnion`; every other IRI is returned. In a deposit: two payload shapes are this package's own and it acts on them — a payload that IS a reference (a bare `did:ng:…` string and nothing else), and an object whose `kind` property is a string starting with `urn:ng-eventually:inbox:`. **Every other payload is yours, and is delivered to you untouched.**
|
||||
**And a document it cannot read is refused OUT LOUD, never swallowed into an empty answer.** A document you hold no capability for - and whose public store has none to give - is refused before the read leaves, naming the document; a public store that could not be ASKED rejects with that failure instead, because both stop the read and only one of them tells you to try again. Nothing is skipped on your behalf: tolerating one unreadable document was the removed `readUnion`'s own policy, and it went with it in `1.0.0-dev.8`, so whether one ends a loop over several is now your decision at your call site. A rejection means "unknown", never "absent", here as everywhere.
|
||||
|
||||
**Which reads reach a document in a public store from nothing but its `reference`.** This is the stranger's case - the document was shared with nobody, its `reference` circulated, and the reader holds no key for it - and two calls serve it. An anchored `docs.sparqlQuery` asks the public store for the key before it reads, and already did so before `1.0.0-dev.6`: the removed `readUnion` was that same ask, once per document, so a one-shot `document read` of one named public document is exactly what it always was. `useShapeQuery` asks too, since `1.0.0-dev.9`, once per document NAMED in `scope.graphs` - so a `shape read` serves the same case now, reactively and typed, and a holder of nothing opens a public document named that way (measured in the unit suite, and live against the broker). The three that do NOT ask, and in which such a document therefore does not appear: `useShape`, `getObjects`, and `useShapeQuery` under `graphs: "did:ng:i"` or `""` - naming no document, there is nobody to ask about. Read it once by naming it and the key lands in what you hold, which all three re-read on.
|
||||
|
||||
`urn:ng-eventually:` is reserved, and the reservation has two faces. In a document: this package writes its own registers under subjects carrying that prefix, in the same graph as your data — so **an anchored `docs.sparqlQuery` returns them alongside your triples, and it is yours to ignore them**. Until `1.0.0-dev.8` the removed `readUnion` dropped them for you; nothing does now. This is not a gain of rights: the two registers an ordinary document carries are a document's inbox address and, for a public document, the read key its store already serves to whoever asks. Nothing else of this package's is written into your documents. Do not write under that prefix yourself. In a deposit: two payload shapes are this package's own and it acts on them — a payload that IS a reference (a bare `did:ng:…` string and nothing else), and an object whose `kind` property is a string starting with `urn:ng-eventually:inbox:`. **Every other payload is yours, and is delivered to you untouched.**
|
||||
|
||||
**Do not use either shape to carry your own data.** Depositing one is still accepted and it is still returned to you by `inbox.read` and its variants — nothing is withheld — but this package now also acts on it, and what you observe is one of two things: it writes an entry into the document the inbox belongs to, if that document is one it can index; otherwise it reports a refusal on its own log stream (`console.error`, ungated, as below). That refusal is reported ONCE and then the deposit is gone: this package consumes what it claims, whether or not the arm succeeded. Until `1.0.0-dev.4` the same refusal came back at every arrival on that inbox and at every connection, for as long as the deposit sat there. The same now applies when the referenced document resolves but carries **nothing for the field its index declares** — a draft, say: the reference is discarded and reported, where before it sat in the queue and would have been indexed by a later drain once the document gained that field. Deposit the reference again after the document says what it needs to say. Carry your own data in an object with a `kind` of your own — `{ kind: "join", … }` is yours and stays yours — and never as a bare reference on its own.
|
||||
|
||||
Only a document's owner writes to it. Holding its read key never grants a write.
|
||||
Only a document's owner writes to it. Holding its read key never grants a write. **Since `1.0.0-dev.7` that covers what you write through NextGraph's ORM too** - `insertObject`, and a mutation of the live set a shape query handed you (`set.add(...)`, `set.delete(...)`, assigning a property). It used to cover only `docs.sparqlUpdate`: an ORM write leaves by a different door, and nothing was standing at it. Refusal happens before the write is attempted, and one document you may not write refuses the whole batch the ORM was flushing.
|
||||
|
||||
**`getObjects(shapeType, config)` answers with what you hold, and nothing else.** Objects living in documents you have no key for are not in the result - the same rule the two shape reads follow, applied per document on what comes back. The collection keeps the shape the SDK gave it: an `orderBy` in the config yields an array, anything else a `Set`. What you get is a CLONE, detached from the database: changing an item of it changes nothing anywhere, and deleting an item of it removes nothing and reports nothing.
|
||||
|
||||
**`insertObject(shapeType, object)` needs the object's `@graph`**, and refuses a document you do not own. The `@graph` is the only thing that says which document the write lands in - upstream the insert subscribes to no document at all - and it is also what decides the refusal, so there is nothing extra to pass. An object with no `@graph` is forwarded and NextGraph refuses it in its own words.
|
||||
|
||||
**"This document has no inbox" is an answer, and only an answer, since `1.0.0-dev.9`.** `inbox.postToDocument` refuses a document whose owner never opened an inbox, and `inbox.readForDocument` / `readSyncedForDocument` answer `[]` for one - both unchanged, both still the ordinary case. What changed is that a read which could not HAPPEN no longer arrives as one of those: a broker that cannot answer for the document, or a public store that cannot be asked for its key, now rejects. Until this version a depositor was told the owner had opened nothing, and a reader was handed an empty list, for a document whose inbox is full.
|
||||
|
||||
`inbox.share(doc, toUser)` names the document and the person; the recipient calls nothing. It refuses a recipient nobody has signed in as, rather than creating them.
|
||||
|
||||
@@ -170,6 +261,16 @@ Where a call must first find out whether something already exists — a document
|
||||
|
||||
**A reactive read says "nothing" and "I could not find out" differently.** `useShapeQuery` answers in three states and only two of them are answers about your data: `isPending` while the question is still open, `isSuccess` once it has been answered, `isError` when it could not be. An empty `data` under `isSuccess` means this scope holds nothing of that shape — the distinction the surface exists for, and the one thing it will never do is publish that snapshot for a read that did not happen. And because an observable cannot take back a list a subscriber has already rendered, `data` under `isError` keeps the **last read that answered** rather than emptying — so an empty `data` is never handed to you as a failure's answer. Read the load state before `data`: **a rejection means "unknown", never "absent"** here too.
|
||||
|
||||
**A document you NAME in `graphs` is reached before the read opens, since `1.0.0-dev.9`.** Naming a document in a scope is the same act as anchoring `docs.sparqlQuery` on it, and it is now answered the same way: the key is asked of a public store if you do not already hold it, the document is refused out loud if you hold no key for it, and its repo is synced before the question is put. A named document you cannot read publishes `isError`, where until this version it reached the engine, came back, and was emptied by the per-document filter into `isSuccess` with `data: []` - the one snapshot this surface reserves for "this scope holds nothing". A named document that has never synced published that same snapshot for a document that is not empty; it now stays **PENDING** instead - the sync barrier is consulted on every publication, and a named document whose barrier was not crossed is not an answer. **Since `1.0.0-dev.10` that half carries no qualification.** It held only within one ordinary session until then, and the two situations that broke it - a second `configure(...)`, which published an empty or stale success, and a broker refusing a subscription after this package had stopped waiting for the document's first push, which left the read withheld with no way back - are closed: "is this document open in this session" now has exactly one answer, the state of the document's one real subscription, rather than three records that could disagree. Nothing failed there, so it is not `isError`: the subscribe succeeded and the read did not throw, what is missing is the authority to say the document is empty. If the document later syncs and brings data, the read answers by itself. If it never does, the read stays pending for that session - handle a `isPending` that does not end, as you already must for a shape query the engine refuses. **Naming SEVERAL documents fails as a whole** when one of them cannot be reached: a short list is the same lie as an empty one, so nothing is answered rather than half of it. What is NOT touched, and each stays a success: `graphs: "did:ng:i"` or `""` (you named no document - the per-document filter is the whole of the answer there, and an empty result means you hold nothing of that shape), `graphs: []` (no objects, by NextGraph's own definition), and a named document you can read that holds nothing of the shape.
|
||||
|
||||
**And it converges by itself.** A named scope refused this way is not a dead view. Whether you may reach the documents you named is re-read on every publication rather than decided once, so a key arriving turns the refusal into the document through the same channel that already re-published a filtered read - and a scope whose barrier failed outright is attempted again on that signal. The ordinary sequences all converge with no call from the application: a page whose components subscribe before the connection work has restored its keys, a document shared with you while you are looking at the screen, a broker that hiccupped once and healed, and a key arriving in the same instant the read was deciding. One consequence to design for: an interface that renders `isError` will briefly show it where it used to show an empty success, so branch on the load state rather than on `data` alone. Nothing here polls, so a scope refused in a session whose keyring never moves again stays `isError` until your own `refetch()` - which is what `isError` has always meant here, and what it means for every other failure this call reports.
|
||||
|
||||
**A named scope goes back to PENDING when the session changes under it.** Signing in again inside the same page gives a new session, whose repos this package has synced nothing for yet - so a named scope that was answering returns to its loading state until the documents it names are confirmed again, and `refetch()` is what confirms them. Withholding is deliberate: a barrier crossed in the session that has just gone is not one the new session has crossed, and the alternative is answering from a subscription nothing re-established. This is the one case where a read that had answered stops answering, and it does not arise on an ordinary page load - there the whole page is new.
|
||||
|
||||
**A standing failure is the SAME snapshot.** While a refusal persists unchanged, the snapshot object does not change either, so `useSyncExternalStore` and anything else comparing by identity see no update - even though the read is being attempted again underneath. A failure that says something different is published normally.
|
||||
|
||||
**What it costs.** One public-store ask and one sync barrier per document named, once per session. The opens for a scope naming several documents run together, so the wait is the slowest of them rather than their sum. The wait itself is bounded internally, so a document that never answers does not hold the open for ever - but the READ over it stays pending, because a barrier that was never crossed is not authority to call anything empty.
|
||||
|
||||
**Which failure lands in `isError` changed in `1.0.0-dev.6`, and one of them now stays PENDING.** The read goes through the SDK's own shape query, so what `isError` carries is the query failing to open: a shape the ORM cannot subscribe, or a `configure` that was never given `ormSubscription`. A start the underlying engine REFUSES does not reach it: NextGraph catches that failure and logs it, and its readiness signal is only ever resolved, never rejected, so such a read stays `isPending` indefinitely. The guarantee above is unaffected — you are still never told "there is nothing" for "nobody answered" — but an interface that handles only `isError` will show its loading state where it used to show an error. Handle a `isPending` that does not end.
|
||||
|
||||
**A read is scoped by document and by subject, never by store.** `Scope` has two axes and there is no third: `graphs` names one document's graph per entry — `did:ng:i` or `""` for the whole local dataset, `[]` or `undefined` for no objects at all — and `subjects` filters subject IRIs literally, with no prefix or hierarchy matching. There is no call that lists the documents in a store, and naming a store in `graphs` is not the question you meant: a store's graph holds its containment triples and nothing of the shape you asked for, so it answers empty rather than raising. That is the target's own shape, not a narrowing this package chose — and since `1.0.0-dev.6` nothing is translated on the way: the `Scope` object you write is the object the SDK receives.
|
||||
@@ -196,7 +297,15 @@ Where a call must first find out whether something already exists — a document
|
||||
|
||||
**Naming an identity proves nothing about who named it.** Any visitor may type any identifier at the barrier, or arrive on a URL that already carries one, and act as that identity — the wallet and its password are handed out on the barrier itself. The identifier is a choice of space, never a proof: anyone who knows one can act as it, so do not treat `ensureIdentity()`'s answer as an authenticated subject.
|
||||
|
||||
**A shape query the engine refuses never finishes loading.** Both shape reads — `useShape` and `useShapeQuery` — open a subscription through NextGraph's ORM, and a start the engine refuses is caught there, written to the console and nothing more: the set stays empty, its readiness never settles, and the only trace is that one console line. `useShapeQuery` reports it as a `isPending` that does not end (see the guarantee above); `useShape` returns a set you cannot tell from an empty one. The cause is upstream and nothing in this package can work around it. This was a total outage until the client packages caught up with the deployed broker on 2026-09-21; it is the residual failure mode, not the ordinary case.
|
||||
**The wrapped `ng` is a wrapper, and a wrapper can be wrong.** It forwards every member to the object you gave `configure`, adding two refusals of its own and nothing else. Until `1.0.0-dev.7` it did not forward them at all - every member came back as a promise, and calling one failed with NextGraph's own `<member> is not a function`. The cause, for anyone who met it: the passthrough did `Reflect.get(ng, prop).bind(ng)`, and `@ng-org/web`'s `ng` is ITSELF a proxy whose `get` trap answers every key - `bind` included - with a fresh path-carrying callable, so `bind` never reached `Function.prototype.bind` and what came back was a Promise. It is `Reflect.apply` since `1.0.0-dev.7`, pinned by a unit test that drives a proxy-shaped `ng` rather than the plain objects every other fake in the suite is. **What it cost most was a `shape read`**, and that is the part to read if you hit it: an application that passed this `ng` to `initNg` had the ORM call `ng.orm_start_graph`, which is where the Promise landed, and `@ng-org/orm` catches that to the console - so `orm_start_graph is not a function` on the console, with a `useShapeQuery` that never leaves `isPending`, is THIS defect and not the engine refusing your shape. Pin `1.0.0-dev.7` or later and it is gone. The lesson to keep is that a member of `ng` that misbehaves is worth reporting here rather than worked around: nothing in this package's own code goes through that wrapper, so this surface is the only place such a defect shows.
|
||||
|
||||
**A shape query the engine refuses never finishes loading.** Both shape reads — `useShape` and `useShapeQuery` — open a subscription through NextGraph's ORM, and a start the engine refuses is caught there, written to the console and nothing more: the set stays empty, its readiness never settles, and the only trace is that one console line. `useShapeQuery` reports it as a `isPending` that does not end (see the guarantee above); `useShape` answers with the ORM's own two states, which do not carry the distinction - its `isLoading` says whether the initial payload has arrived and has no value for "it never will". The cause is upstream and nothing in this package can work around it, and it is MEASURED in the release this package builds against rather than inferred: in `@ng-org/orm@0.1.2-alpha.21`'s shipped `dist/connector/RdfOrmSubscription.js`, `readyPromise_` is built with `resolve` alone and nothing anywhere rejects it, `resolveReady` is called on one path only (initial data arriving), and `ng.orm_start_graph` is awaited inside a `catch` whose whole body is `console.error(e)`. This was a total outage until the client packages caught up with the deployed broker on 2026-09-21; it is the residual failure mode, not the ordinary case. Since `1.0.0-dev.9` the commonest way in is closed for `useShapeQuery`: a document NAMED in `graphs` is opened and refused before the query is handed over, so an unreachable named document reads `isError` rather than pending for ever. What remains is the start the engine refuses for a document you can perfectly well reach, and everything under `graphs: "did:ng:i"`, where there was no name to check. **So the two faults you can tell apart today are the BARRIER refusing (named document, `isError`, naming the document) and the wiring being absent (`isError` naming the `configure` field); the two you cannot are the engine rejecting your shape and the ORM never having started, which arrive as the same unending `isPending`.** A way to separate those last two has been asked for and is not decided: it would have to be invented here, since there is nothing upstream to mirror and nothing upstream to read it from.
|
||||
|
||||
**No object removal call, and no document deletion at all.** There is no `deleteObject` and no `removeDocument` at any level of NextGraph, so there is none here. You remove an object by deleting it from the LIVE set a shape query handed you - the item the set is holding, reached by iterating it, by `.first()` or by `.getBy(graph, subject)`. What that removes is the predicates your SHAPE declares on that subject: triples under a predicate the shape does not name survive, the subject survives, and the document survives. Deleting an item of a `getObjects` result removes nothing, because that result is a clone. `@ng-org/orm@0.1.2-alpha.21` ships a `removeObject(graphNuri, subjectIri)`; it is not published here, because it exists in the package and not in the source this package is verified against, and a symbol whose only evidence is what an npm build happens to export is not something to build a guarantee on.
|
||||
|
||||
**A refused ORM write does not always reach you as a failed call.** `insertObject` rejects, and you can await it. A mutation of a live set does not: NextGraph batches those into one call it does not await, so a refusal surfaces as an unhandled rejection in the console rather than as an error at the line that made the change - the write did not happen, and nothing in your code was told. Write into documents you own, and treat a document you merely hold the key for as read-only.
|
||||
|
||||
**Nothing of NextGraph's ORM is re-exported as it stands.** `getObjects` and `insertObject` are the two this package offers, wrapped. `RdfOrmSubscription`, `DiscreteOrmSubscription`, `ngSession`, `getRaw`, `watch` and `effect` are NOT published and importing them from `@ng-org/orm` defeats everything above: one wallet hosts every identity here, so an unwrapped read returns every identity's objects and an unwrapped write reaches any document. `RdfOrmSubscription` in particular pools its subscriptions on the shape and the scope alone, with no identity in the key - what makes that safe is the filtering this package does on the way out.
|
||||
|
||||
**No revocation.** `inbox.share` cannot be undone.
|
||||
|
||||
@@ -212,31 +321,107 @@ Where a call must first find out whether something already exists — a document
|
||||
|
||||
**TWO COPIES OF THIS PACKAGE IN ONE APPLICATION MISBEHAVE SILENTLY, and nothing here can detect it.** What this package remembers it keeps in the package itself, not in any handle you hold: which documents are subscribed and who is listening for them, which identity the session settled on, which documents are open. None of that is shared between two copies. An application that ends up with two therefore runs two of everything — a document subscribed through one copy is invisible to the other, so changes to it simply never arrive; and the identity settled in one is not the identity the other acts as, so the same call writes as one user or the other depending on which copy it reached. Nothing raises, nothing warns, and no call can report it: from every entry point a second copy is indistinguishable from the first, and the symptoms surface far from the cause as missing updates and writes attributed to the wrong person. This is a packaging property, not a behaviour to code around — **declare this package once, as one dependency of the application**, and let your own code and every library built on it resolve to that copy. A library that builds on this package should declare it a *peer* rather than a dependency for exactly this reason, so that the application remains the one place it is named.
|
||||
|
||||
**No unfiltered read through `useShape`.** Members that yield items are filtered and mutations pass through; anything else throws. A document reached through that view alone, read nowhere else first, does not appear.
|
||||
**No unfiltered read through `useShape`, and no way round it through the object it hands back.** What the call returns is NextGraph's own result, filtered: `data` and `promise` carry only items in documents you hold the key for, `isLoading` passes through, the two page commands run and whatever they hand back is held to the same rule as any other member, and `subscription` - the ORM's own handle, which reads and writes with no capability filter at all - is refused out loud rather than handed on. **Any member this package has not DECIDED is refused, whatever its type** - that is how a member a later `@ng-org/orm` adds is settled before anyone has seen it. It is not decided by type: a COUNT is a plain number that describes the items, and nothing separates it from a page size, so nothing undecided passes. A member the SDK does not carry at all reads `undefined`, since there is nothing behind it. Inside `data`, every member that yields items is filtered, and an ordered result addresses and counts the items you may read: `size`, `length`, an index, `Object.keys`, `in` and `JSON.stringify` all agree, and none of them says how many you may not. **Reading a member's DESCRIPTOR answers exactly what reading the member answers**, which is not a detail: `Object.getOwnPropertyDescriptor` - and the spread, `Object.entries` and console inspection built on it - reads a property without going through the view's own read, so a refused member throws there too rather than handing its value over. That holds for an ACCESSOR as much as for a value: the getter you are handed reads through the view, and its setter is refused. **A mutation reaches the collection underneath, and neither hands back nor acts past what the view shows.** Writing is not broken - your own `add` reaches the document and the reactivity it drives. **All three ask the same question**: `add` THROWS for an object naming a document you hold no key for, and for one naming NO document (there is then nothing that owns it, and it would land in a collection every identity shares - set `@graph` to a document you hold) (it returns the collection whether or not it did anything, so it has no value meaning "that did not happen"), `delete` of an item the view does not show answers `false` - the Set contract's own word for "it was not there", and true of the view - and `clear` empties the view rather than the collection every identity shares. `add` hands back the VIEW rather than the collection, because `Set.prototype.add` returns the collection so calls can chain and chaining onto the unfiltered one gives it away entirely. **Writing to what you are handed is refused** - `data[0] = x`, `delete data[0]`, `data.length = 0`, `Object.defineProperty`, `Object.setPrototypeOf` and `Object.freeze` - on the RESULT OBJECT as much as on `data`, and on this package's own account rather than because the ordered collection upstream happens to refuse some of them too. **Every member `@ng-org/alien-deepsignals` declares non-mutating answers**, computed over the items you may read: the iterator helpers, the array reads (`at`, `slice`, `join`, `indexOf`, `toSorted`, `with`) and the ES2025 set combinators (`union`, `difference`, `isSubsetOf`). The one exception is `copyWithin`, which that list names and which WRITES: it is refused here, and that is STRICTER than NextGraph's own ordered collection, which runs it and destroys an element (its guard is built from the same mislabelled list). A deliberate difference, stated so you do not discover it as a surprise. A document reached through that view alone, read nowhere else first, does not appear.
|
||||
|
||||
**Neither shape read fetches a public store's key for you.** A document in a public store gives up its key to whoever asks, but asking is a round-trip and neither `useShape` nor `useShapeQuery` spends one — since `1.0.0-dev.6` the second reads through the SDK and no longer opens a document of its own. So a public document you have read nowhere else does not appear in either. Read it once by naming it (`readUnion`, `docs.sparqlQuery`) and both pick it up: the key lands in what you hold, and that is a change both of them re-read on.
|
||||
**`isLoading` does not mean what its name says, and the fault is upstream's.** `@ng-org/orm`'s declaration describes it as "`true` when no data is available yet"; the hook that ships computes it as the subscription's `isReady`, which is the inverse. Both artifacts were read and they disagree. This package forwards the value it is handed rather than correcting it, because a layer that quietly improves the target teaches you a behaviour you would have to unlearn. **Branch on `data === undefined`**, which is unambiguous, and treat `isLoading` as unusable until upstream settles it.
|
||||
|
||||
**`getObjects` does not fetch a public store's key for you either**, for the same reason as the two shape reads below: it goes through the same ORM query and spends no round-trip asking. Read the document once by naming it — `docs.sparqlQuery` anchored on it — and it appears.
|
||||
|
||||
**`useShape` does not fetch a public store's key for you, and `useShapeQuery` does so only for what you NAME.** A document in a public store gives up its key to whoever asks, but asking is a round-trip: `useShape`'s signature is NextGraph's own and has no await to spend, so a public document you have read nowhere else never appears in it. `useShapeQuery` spent none either until `1.0.0-dev.9`; it now asks once per document named in `graphs`, which is what lets a bare reference to a public document be read through that call. It still asks nothing under `graphs: "did:ng:i"` - the whole local dataset names no document, so there is nobody to ask about. For those two cases, read the document once by naming it - `docs.sparqlQuery` anchored on it, which is the whole of what `readUnion` did for this before it was removed in `1.0.0-dev.8` - and both reads pick it up: the key lands in what you hold, and that is a change both of them re-read on.
|
||||
|
||||
**A watch on one inbox that could not be opened does not come back on its own.** Opening it can fail — a broker that does not answer in that moment — and the failure is reported rather than passed over, but what follows is event-driven and this package deliberately never polls. The watch is opened again at the next moment this identity comes to hold something it did not: it creates a document, it opens an inbox on a document, a `ReadCap` reaches it through an inbox still being watched, it reads a public-store document for the first time. A session that does none of those goes on without that inbox — deposits made into it are not applied, nothing raises, and they wait unconsumed for the next connection. The exposed case is the identity that only ever READS, since it produces none of those events, where an identity that goes on creating recovers as a by-product of its own work. Awaiting `ensureIdentity()` again applies what is waiting — it drains every one of this identity's queues before it resolves, and shows no barrier a second time — but the watching itself comes back only on a fresh page.
|
||||
|
||||
### Asked for, not decided
|
||||
|
||||
Each of these is a standing request from an application, recorded here rather than answered in a message. None is refused on principle; each is an addition to a published surface, so each waits on a decision that is this package's to make. They are listed so that an application knows it is not missing a call it simply failed to find.
|
||||
|
||||
**`configure`'s `useShape` is REQUIRED even for an application that never calls it.** `@ng-org/orm` publishes `useShape` only through its react / svelte / vue adapters, so a page with no framework can produce none of them - and the field is not optional in `EventuallyConfig`, so such an application invents a value to satisfy the signature. What it does with that value is unobservable: the injected hook is consulted in exactly one place, inside this package's own `useShape`, so an application that reads through `useShapeQuery` never reaches it. This repository's own frameworkless example passes `useShape: (() => {}) as never` (`examples/notebook/app.ts`) and reads live with `useShapeQuery` beside it; a function that THROWS is equally sound and says more if anything ever does call it. Making the field optional is a change to a published signature, hence this list rather than a fix.
|
||||
|
||||
**No published NURI type guard, and no published escaping helpers.** Neither is an oversight. On the guard: every entry takes `NuriLike` and runs its own validation at the door - permissive in, precise out - so a guard would put validation in a caller's hands where the door already does it, and invite the cast the types exist to prevent. On the escaping: `escapeIri` / `escapeLiteral` were published until the surface was narrowed, and they came out under the rule that governs this whole leaf - a symbol with no counterpart at any level of the target is a symbol an application would have to unlearn, and neither `@ng-org/web` nor the ORM exposes an equivalent (the engine escapes ad hoc where it builds SPARQL). The consequence is stated rather than denied: an application composing SPARQL against `docs.sparqlUpdate` carries its own escaper, which is a second implementation of one rule, and the one running there is not the one this package tests.
|
||||
|
||||
**No published way to write triples above `docs.sparqlUpdate`.** The write that IS published above the raw primitive is `insertObject(shapeType, object)` - the SDK's own name and signature, which takes a shape-typed object, needs its `@graph`, and composes no SPARQL at all, so it removes the query building, the escaping and the NURI validation from a caller in one move. What it costs is the ORM: it is injected through `configure` (`insertObject`), so a library that wires no `@ng-org/*` of its own cannot reach it, and it writes shape-typed objects rather than arbitrary triples. An "add these triples to this document" entry, taking triples and no shape, is what has been asked for and is not decided.
|
||||
|
||||
**No call says WHICH STORE holds a document.** Not the enumeration that `storeRegistry.listMyEntityDocs` was - that was removed in `1.0.0-dev.5` because nothing upstream lists a store's documents - but the narrower question about a document whose `reference` the caller already holds. There is no accessor for it, and the consequence is worth stating plainly: an application cannot verify its own placement, so a document written to the wrong store is undetectable from here until some reader fails to open it.
|
||||
|
||||
**No per-deposit outcome, and no count that says a queue has been drained.** `## Guarantees` says a deposit this package applies is consumed whether or not the arm succeeded, that a refusal goes to this package's own log stream, and that there is no way to ask what was lost. The gap an application feels is the one before that: nothing distinguishes "this deposit was examined and discarded" from "this deposit has not been examined yet", so a check that an ineligible deposit produced NO entry cannot be waited for - only timed out, which makes it weaker than its name. Anything observable would close it, a per-deposit outcome or merely a counter that moves when a queue has been drained; neither exists.
|
||||
|
||||
## Change policy
|
||||
|
||||
**Semver, and majors are the normal case.** This surface converges on a NextGraph that does not ship yet, so most steps toward the target remove or narrow something — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time.
|
||||
|
||||
What each level means here, in this package's own terms:
|
||||
|
||||
- **major** — a published symbol is removed (`getSession`, `normalizeId`, `currentUser`, `RegistrySession`, `inbox.materialize`, `storeRegistry.listMyEntityDocs`, `resolveScopeGraph` and `resolveWriteGraph` all left this way), **or** a published symbol is renamed (`watchShape` became `useShapeQuery`), **or** an existing call narrows: it now throws where it returned (`ensureIdentity` rejects rather than resolve on an incomplete restore), it reports a state you did not have to handle before (`useShapeQuery` publishing `isError` where it used to publish a synced-empty snapshot), or what it HANDS BACK changes shape (`useShapeQuery`'s `data` carrying the shape's own objects where it carried property bags). A signature change a caller must react to counts; one that only accepts more than before does not.
|
||||
- **major** — a published symbol is removed (`getSession`, `normalizeId`, `currentUser`, `RegistrySession`, `inbox.materialize`, `storeRegistry.listMyEntityDocs`, `resolveScopeGraph`, `resolveWriteGraph`, `readUnion` and `UnionSubject` all left this way), **or** a published symbol is renamed (`watchShape` became `useShapeQuery`), **or** an existing call narrows: it now throws where it returned (`ensureIdentity` rejects rather than resolve on an incomplete restore), it reports a state you did not have to handle before (`useShapeQuery` publishing `isError` where it used to publish a synced-empty snapshot), or what it HANDS BACK changes shape (`useShapeQuery`'s `data` carrying the shape's own objects where it carried property bags). A signature change a caller must react to counts; one that only accepts more than before does not.
|
||||
- **minor** — a symbol is added and nothing existing moves (`inbox.readSyncedForDocument` arrived this way).
|
||||
- **patch** — a fix that changes neither the published surface nor anything above under `## Guarantees`.
|
||||
|
||||
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.6`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
|
||||
**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.15`), which sorts *below* that version by construction - so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you.
|
||||
|
||||
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.6` is this package, `ng-e2e-helpers/v…` is the other one. A bare `v…` tag would say nothing about which surface it froze the day the two diverge — which is the day one of them takes a major and the other does not.
|
||||
**Tags carry the package name**, because this repository publishes more than one engagement and their versions move independently: `polyfill/v1.0.0-dev.15` is this package, `ng-e2e-helpers/v...` is the other one. A bare `v...` tag would say nothing about which surface it froze the day the two diverge - which is the day one of them takes a major and the other does not.
|
||||
|
||||
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything — but the next release very likely is a major. What exists today is `1.0.0-dev.6`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it — `against: @ng-eventually/polyfill@1.0.0-dev.6`, the string you pinned, never the version it is heading for.
|
||||
**THE TAG IS THE PIN, and here is exactly what exists to pin.** The acquisition channel is agreed per application and carries no version of its own, so the mechanical anchor this engagement offers is the git tag `polyfill/v<version>` on this repository, cut over the commit at which `packages/polyfill/package.json` reads `<version>`. What an application should pin is that tag, and what its `usage_` leaf's `against:` should name is the version string the tag carries - `against: @ng-eventually/polyfill@1.0.0-dev.15`. There is deliberately no promise beyond that: **nothing is published to a registry, nothing is fetchable by version, and no range resolves** - a `*` peer range against a filesystem checkout compiles whatever is checked out at the other end, uncommitted edits included, and no pin on this side can prevent that. **And the tags lag the versions.** As of 2026-10-01 the newest tag cut on this repository is `polyfill/v1.0.0-dev.3`; every version from `1.0.0-dev.4` onward exists as a package.json value on a branch and has no tag. Until the tag for a version is cut, the only pin that names a version exactly is the COMMIT at which package.json reads it - which is what a `usage_` leaf's `against:` line names in parentheses, and it is a real pin, not a workaround. Pin the tag where there is one, the commit where there is not, and say which in the `against:` line either way.
|
||||
|
||||
`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it, so none of the changes named above is a bump from anything - but the next release very likely is a major. What exists today is `1.0.0-dev.15`, on a branch: pin that string exactly, and anchor your `usage_` leaf's `against:` on it - `against: @ng-eventually/polyfill@1.0.0-dev.15`, the string you pinned, never the version it is heading for.
|
||||
|
||||
`1.0.0-dev.15` is a **patch** (2026-10-01), and it answers a consumer's standing requests. No signature changed, nothing was added or removed from the entry, and no behaviour moved - what moved is what this leaf SAYS, which is the only channel such a request has. `## Surface` gains a complete hand-written shape, with the type argument that types the snapshot, and the statement that `initNg` discards its first argument and substitutes the published `ng` (MEASURED, not inferred). `## Guarantees` now says out loud that an anchored `docs.sparqlQuery` refuses a document it cannot read instead of swallowing it - true since `1.0.0-dev.8`, stated only in that version's release note until now - and names, in one place, which reads reach a document in a public store from nothing but its `reference` (the anchored query, always; `useShapeQuery` on a document NAMED in `graphs`, since `1.0.0-dev.9`) and which do not. `## Non-guarantees` says what the `orm_start_graph is not a function` outage actually was (this package's own `ng` wrapper, fixed in `1.0.0-dev.7`, NOT the engine refusing a shape), pins the upstream swallow to the shipped artifact it was read in, and gains an **Asked for, not decided** list so that a request an application made leaves a trace here rather than in a message. `## Change policy` says which pin exists, and that the tags lag the versions.
|
||||
|
||||
`1.0.0-dev.14` is a **patch** (2026-10-01): `ShapeObservable` moved from the entry's SDK-SHAPED section to its POLYFILL-ERA one, and `## Surface` now states that `refetch()` has no recorded counterpart. No signature changed - same name, same shape, still exported from this one entry - so nothing under `## Guarantees` moved. The type was already annotated `kind=invention level=none ref=none` (no upstream observable exists), under a heading that promises a target counterpart for every symbol it lists. It stays exported because applications genuinely write the type down. What the move records is that it is not a framework adapter: it carries the capability filter and the capability-arrival re-publish, both of which exist only because one wallet is shared, so it is deleted when the shared wallet is, as one coupled migration. `ShapeQuery` stays SDK-SHAPED: its record exists and states `{ data, isPending, isSuccess, isError, error }` field for field.
|
||||
|
||||
`1.0.0-dev.13` is a **patch**: `NgLike`, `UseShapeLike`, `OrmSubscriptionLike`, `GetObjectsLike` and `InsertObjectLike` moved from the entry's SDK-SHAPED section to its POLYFILL-ERA one, beside `EventuallyConfig`, whose fields they type. No signature changed - same five names, same five shapes, all still exported from this one entry - so nothing under `## Surface` or `## Guarantees` moved, which is the patch/major line this leaf draws itself. What changed is classification only: each was already honestly annotated `kind=invention level=none ref=none` (no target counterpart, at any level), sitting under a heading that promises one for every symbol it lists, because each exists solely to describe what `configure` receives and `configure` was already on the deletion list. An application that already understood it would stop typing these five the day it deletes its `configure()` call was not misled about behaviour; this patch only makes the surface say so, instead of implying the opposite.
|
||||
|
||||
`1.0.0-dev.12` is a **major**, and it is one removal: **`Schema` (`@ng-org/shex-orm`) and `DeepSignalSet` (`@ng-org/alien-deepsignals`) are gone from the entry, and nothing replaces them.** Both were re-exported type-only under `## Surface`'s own rule - a type is published only when a published signature uses it - and neither was: `DeepSignalSet` is named only in prose describing what `useShape`'s result carries, never as an actual type anywhere in this package, and `Schema` was named nowhere at all beyond the re-export itself. The same rule already removed `ReadCap` and `InboxScope` from this list; this is that rule catching a second, later recurrence. If your code names either type, it stays DEFINED nowhere in this package to fall back on - name the SDK package directly (`@ng-org/shex-orm`, `@ng-org/alien-deepsignals`) if you need it.
|
||||
|
||||
`1.0.0-dev.11` is a **major**, and all of it is one rule: **`useShape` hands back NextGraph's own RESULT OBJECT, filtered - and the filter no longer decides what to hand on by what a member is CALLED.** Nothing is added, renamed or removed from the entry.
|
||||
|
||||
- **Read your objects through `data`, not by iterating what the call returned.** `@ng-org/orm`'s React hook returns `{ data, promise, subscription, isLoading, nextPage?, previousPage? }` around the collection, and it has for every release this package builds against. This package went on treating the return AS the collection: `[...result]` and `result.size` threw, and `result.data`, `result.promise` and `result.subscription` each handed back **every identity's items**, with nothing raised anywhere. If you were reading `.data`, you were reading unfiltered. That is the defect this version closes, and it is why this entry is worth reading before upgrading rather than after.
|
||||
- **`subscription` is REFUSED where it used to be handed over.** It is the ORM's own `RdfOrmSubscription`, which reads and writes with no capability filter at all and which `## Non-guarantees` has always said this package does not publish. Reading it now throws, naming itself. `data`, `promise`, `isLoading` and the two page commands are what remains, and they are what a caller needs: the SDK's own example destructures `data` and `isLoading` and nothing else.
|
||||
- **A member of that object that this package has not DECIDED is refused, whatever its type.** All six of the members `@ng-org/orm` ships are decided; a seventh that a later version adds refuses until someone decides it. Two earlier rules were tried and both let a leak through - "anything that is not a function is harmless", which handed over three members of the current shape, and "a plain scalar cannot reach an item", which handed over any count not spelled `size` or `length`. There is no test on a value's type that separates a count derived from your data from a page size, so the rule is not about types at all: it is that nothing undecided is answered.
|
||||
- **The two arguments are the SDK's own types**, where the signature took `unknown` for both. `useShape(shapeType: ShapeType<T>, conf: RdfOrmConfig<T> | string | undefined)`, which is what the surface already said the pair was. A caller passing the SDK's shape and config compiles unchanged; one passing something else now does not, which is why this counts.
|
||||
- **`size` and `length` count what you may read, and so does every other way of asking.** An ordered config (`orderBy`) makes the collection an array, which says how many items it holds in four places - `length`, `Object.keys`, `in`, and the descriptor of each index - and only the first was filtered. `data[0]` now names the first item you may read rather than the first the engine returned, and the positions past them do not exist.
|
||||
- **A member's DESCRIPTOR answers what reading it answers.** `Object.getOwnPropertyDescriptor(result, "data")` returned the collection UNFILTERED, and `...(result, "subscription")` the handle that reading it refuses: that call does not go through the view's own read. Neither does a spread, `Object.entries`, `JSON.stringify` or a console inspection, so all of them were reading round the filter. They now read through it - which also means `{...result}` and `Object.keys(result)` THROW on `subscription`, where they used to hand it over. **But do not read that as "logging is filtered".** A console is the HOST's, not this package's: `console.log(result)` in Bun and in a browser devtools inspects the object BEHIND the view and prints `data` and `subscription` in full, for every identity, and no interception can prevent it - measured. That is a property of runtime introspection, not a hole a version can close. **Do not log or inspect what a shape read hands you when more than one identity shares the page**; iterate `data` and log what you mean to show.
|
||||
- **A mutation no longer hands back the collection, nor acts past the view, and all three ask the same question.** `add` returned the collection - `Set.prototype.add` returns `this` so calls chain, measured on the plain `Set` and on the real `DeepSignalSet` alike - so `view.add(x)` gave every identity's items to a caller whose every way IN was filtered, through the first member the ORM's own example reaches for. It now hands back the view, and it now REFUSES an object naming a document you hold no key for: ungated, it let you plant an object into someone else's document, never see it again, and have the two-way binding commit it there. `delete` removed an item the view declines to confirm the existence of; `clear` emptied the collection every identity shares. **Your own writes still reach the document** - that is the one thing that must not change, and `e2e/orm-live-read.ts` measures it against the deployed broker.
|
||||
- **A write to the collection itself is refused.** `data[0] = x`, `delete data[0]` and `data.length = 0` all went through against a collection that allowed them, the last defeating the emptying rule outright; `Object.setPrototypeOf(data, ...)` replaced the real collection's prototype, after which iterating it throws for every identity, and `Object.freeze(data)` made it non-extensible. The view refuses all of them itself now, rather than relying on the ordered collection upstream refusing some of them for it.
|
||||
- **The members upstream declares non-mutating answer again.** Eighteen array reads on an ordered result, plus `includes` and the seven set combinators, were throwing for the holder whose data it was - a leak traded for an outage. They are served over the items you may read.
|
||||
- **A member this package has not decided is refused, whatever its type.** The rule used to be "a plain scalar cannot reach an item, so it passes", and a COUNT is a plain scalar that describes the items: a member spelled `totalCount` published how many objects exist while you could read one. `size` and `length` were right only because they were computed by name. Nothing undecided passes now - what a later version adds refuses, loudly, naming itself.
|
||||
- **The RESULT OBJECT is as write-protected as `data`.** `delete result.data`, `result.isLoading = x`, `Object.setPrototypeOf(result, ...)` and `Object.preventExtensions(result)` all reached the SDK's own object; the last two had been closed on `data` one version earlier and not here.
|
||||
- **`add` refuses an object that names no document.** The gate asked the same question a READ asks, where an absent `@graph` means "names no document, nothing to check" - which let an object with no `@graph` into the collection every identity shares. Set `@graph` to a document you hold.
|
||||
- **An ordered result yields indices where it used to yield items.** `keys()` handed back items, `entries()` handed back `[item, item]` and `forEach` handed the item as its second argument - the Set contract, applied to an array. No leak, and wrong output on your own data.
|
||||
- **Unchanged**: the read still spends no round-trip asking a public store for a key, a document you have read nowhere else still does not appear, and nothing about `useShapeQuery`, `getObjects` or `insertObject` moves.
|
||||
|
||||
`1.0.0-dev.10` is a **major**, and all of it is one rule: **"is this document open in this session" now has exactly one answer.** This package kept three records of that fact - a set of live bootstrap subscriptions, a per-document barrier state, and the subscription layer's own listeners - and the first two outlived the third. Only one remains, the state of the document's one real subscription; the other two are gone rather than reconciled, because two records of one fact can disagree and these did. Nothing is added, renamed or removed from the entry.
|
||||
|
||||
- **The ARRIVAL half of a named scope's guarantee is delivered in full**, and the two non-guarantees `1.0.0-dev.9` carried against it are withdrawn. A second `configure(...)` no longer leaves a document recorded as synced against a subscription it tore down - that one DID publish an empty or stale success, and now stays pending until the document is heard from. A subscription the broker refuses no longer leaves the document recorded as open, in the one case where that was unbounded: when the refusal arrives after the subscription layer has been torn down from under it, nothing was ever told, and the document could not be opened again for the rest of the session. In both, the document is now re-opened and the read answers once it has actually been heard from.
|
||||
- **What a caller sees change, and the two situations differ - said per situation rather than lumped together.** After a second `configure(...)` a named scope publishes `isPending` where it published `isSuccess`, and that is why this is counted a **major** on the conservative reading: `isPending` is a state this call already reports and this surface already requires you to handle, so no new state arrives at the caller, but an interface branching on `isSuccess` is handed a different value. When the broker refuses late, the read was already withheld - it is the RECOVERY that changes: the retry now subscribes again instead of being blocked for the rest of the session, so `refetch()` is an exit where it was not. **Calling `configure` once, at start-up, is still what the deployment requirements ask for**; this makes a second call recoverable rather than supported.
|
||||
- **Unchanged, and each is still a success**: a named document you can read that holds nothing of the shape, `graphs: []`, and `graphs: "did:ng:i"` or `""`. Turning a genuine emptiness into a failure would only move the lie, and nothing here does.
|
||||
|
||||
`1.0.0-dev.9` is a **major**, and all of it is one rule: **a read that could not reach its document now FAILS, where it used to answer as though the document were empty.** Nothing is added, nothing is renamed, nothing is removed from the entry - three existing calls narrow.
|
||||
|
||||
- **`useShapeQuery` publishes `isError` for a document you NAME and cannot read.** Until this version a document named in `scope.graphs` was handed straight to the engine; whatever came back was filtered per document by what you hold, and a document you hold no key for was emptied out of the result. The snapshot was `isSuccess` with `data: []` - indistinguishable, byte for byte, from the scope genuinely holding nothing. It now asks the public store for the key, refuses out loud if there is none to be had, and crosses the sync barrier first, so the same read answers `isError`. **Handle `isError` on a named scope**; an interface that only branched on `isPending` / `isSuccess` will show its success state with nothing in it. Naming several documents fails as a whole when one of them fails.
|
||||
- **A named document that has not SYNCED keeps the read pending**, where it used to publish the same empty success. Reaching a document and hearing back from it are two questions, and only the first was being asked. An interface must therefore expect a named scope to sit in its loading state while a document it named is not answering, rather than reading an empty success as "nothing there". The same applies after an in-page re-login, which returns a named scope to pending until its documents are confirmed under the new session - see `## Guarantees`. Complete since `1.0.0-dev.10`; in `1.0.0-dev.9` it held only within one ordinary session, and the two situations that broke it were carried as non-guarantees.
|
||||
- **A named scope now costs round-trips it did not cost before**, once per named document per session: the key fetch, and the subscribe that syncs the repo. The opens run together, so several named documents cost the slowest wait rather than the sum. `graphs: "did:ng:i"` is unchanged and spends nothing.
|
||||
- **A refused named scope RECOVERS on its own.** Reachability is re-read on every publication rather than remembered, and a scope whose barrier failed is attempted again when the keyring moves - including when the move happened while the read was still deciding. Without that a refusal would be terminal, and trading a silent empty answer for a permanently stuck error would have been no improvement at all. Expect to see `isError` briefly on a page that subscribes before its keys are restored. There is no polling: in a session whose keyring never moves again, `refetch()` is the recovery, as it is for every other failure this call reports.
|
||||
- **`inbox.postToDocument`, `inbox.readForDocument` and `inbox.readSyncedForDocument` reject where they used to report "no inbox".** Resolving a document's inbox address reads that document, and when the read could not happen - the broker could not answer for it, or its public store could not be asked for the key - all three used to render the breakdown as the document's owner having opened nothing: a refusal on the deposit side, an empty list on the read side. They now reject. A document that genuinely has no inbox is unchanged, and still answers exactly as before.
|
||||
- **Nothing that was already an ANSWER moves.** An empty document is a success holding zero objects, `graphs: []` is a success holding none, a document in a public store that exposes no key is a plain "no", and a document with no inbox answers `[]`. The whole change is that a *failure* stopped borrowing those shapes.
|
||||
- **Why this direction, and not a richer set of outcomes.** The step that collapsed the two was this package's own emulation of a public store serving its key; NextGraph has no counterpart for it, because upstream nothing consults a capability on the read path at all. So there is no signature to mirror, and the alignment falls to the engine's own observable: an anchored query on a repo the verifier does not hold answers `NgError::RepoNotFound`, a rejected promise - **a read rejects, or it does not**. Inventing a third outcome for a caller to branch on would have been a vocabulary the target will never grow.
|
||||
|
||||
`1.0.0-dev.8` is a **major**, and it is one removal: **`readUnion` and `UnionSubject` are gone from this surface, and nothing replaces them.**
|
||||
|
||||
- **What they were.** `readUnion(docs)` issued one anchored `docs.sparqlQuery` per document, in parallel, and added four policies of its own on top: it swallowed a document it could not read into an empty answer, it dropped a document whose key you do not hold instead of refusing it, it grouped the rows into per-(document, subject) property bags, and it removed the reserved-prefix registers from the result. Nothing at any level of NextGraph answers that composition. Its own provenance annotation cited `sparql_query`, which is a citation a reader checks, finds correct, and moves on from - while the claim attached to it was false. **A composition cannot be aligned on a single function**, and that is the rule this removal serves.
|
||||
- **What to call instead.** `docs.sparqlQuery(sessionId, "SELECT ?s ?p ?o WHERE { ?s ?p ?o }", undefined, doc)` - the primitive `readUnion` was built on, published beside it all along. It is the same anchored query and the same public-store key fetch, so **a bare `reference` is still enough to read a document in a public store**: nothing about that property moves. Reading several documents is this call once per document.
|
||||
- **What comes back to you, and it is not a detail.** Three of the four policies were the batch's, and they are now yours: a read that FAILS rejects instead of reading as an empty document, so whether one unreadable document ends your loop is your decision at your call site; a document you hold no key for is REFUSED out loud instead of being dropped from a result; and the rows are rows - grouping them into objects is yours, or use `getObjects` / `useShapeQuery`, which return the SDK's own typed objects. The fourth is in `## Guarantees`: subjects under `urn:ng-eventually:` are no longer filtered out for you.
|
||||
- **Why the losses are alignment rather than regression.** Upstream, an anchored query on a repo the verifier does not hold REJECTS (`resolve_target_for_sparql` -> `NgError::RepoNotFound`), which reaches JavaScript as a rejected promise. A "skip this one and carry on" policy is something the target does not have and will not grow, so a caller that learned it would have had to unlearn it. The one thing that genuinely disappears is the convenience, and it is three lines at your call site.
|
||||
|
||||
`1.0.0-dev.7` is a **major**, and what makes it one is not the two calls it adds but the rule it starts enforcing. Symbol by symbol:
|
||||
|
||||
- **`getObjects` and `insertObject` are published**, at NextGraph's own names, parameters and returns, wrapping the SDK's. That part is a minor on its own: nothing existing moves. Import them from this package rather than from `@ng-org/orm` - which is the point, since the unwrapped ones bypass the capability filter and the shared-wallet identity boundary entirely. Both are injected through `configure` (`getObjects`, `insertObject`), like the shape query beside them.
|
||||
- **An ORM write into a document you do not own is now refused**, where it used to succeed. This is the major. It applies to `insertObject` and to every mutation of a live set a shape query handed you, because both leave by a door that had no guard on it. The guarantee itself did not move - *only a document's owner writes to it* has been in this contract from the start - but the implementation only enforced it on `docs.sparqlUpdate`. Code that relied on writing through the ORM into somebody else's document was relying on a hole. Note that a live-set mutation is refused WITHOUT your call failing: see the non-guarantee above.
|
||||
- **`initNg` must be this package's**, and it now substitutes the `ng` it hands NextGraph's ORM. An application that calls `@ng-org/orm`'s `initNg` directly still works and is still unguarded - which is exactly the state being left behind, so call this package's.
|
||||
- **`RdfOrmConfig` joins the published types**, because `getObjects`'s signature names it. It is re-exported from the SDK's `/all` subpath, which is where the SDK puts it.
|
||||
- **`ng` works.** Until this version the published `ng` answered every one of its members with a promise instead of the function, so `ng.<anything>(...)` failed with NextGraph's own `<member> is not a function`. If you passed it anywhere - and the surface tells you to use it instead of the one you gave `configure` - that is what you were hitting, including a `useShapeQuery` that never left `isPending` because NextGraph swallows the failure it gets from `orm_start_graph`. Nothing about the call changes; it simply answers now.
|
||||
|
||||
`1.0.0-dev.6` is a **major**, and all of it is one change: `useShapeQuery` reads through NextGraph's OWN shape query instead of a read this package built. Nothing was renamed and nothing was removed from the entry, but three things move under a caller:
|
||||
|
||||
- **`data` items are the shape's objects, not property bags.** `ShapeQuery<T>`'s `T` is now the shape's type: each item carries `@id`, `@graph` and one field per predicate the shape DECLARES, under that predicate's readable name. `note.props["urn:notebook:title"]?.[0]` becomes `note.title`, and `note.graph` becomes `note["@graph"]`. **A predicate your shape does not name is not projected**, so a shape written only to match — an `rdf:type` constraint and nothing else — now comes back with no fields at all: declare the ones you read. `readUnion` is unchanged and still returns `UnionSubject`.
|
||||
- **`data` items are the shape's objects, not property bags.** `ShapeQuery<T>`'s `T` is now the shape's type: each item carries `@id`, `@graph` and one field per predicate the shape DECLARES, under that predicate's readable name. `note.props["urn:notebook:title"]?.[0]` becomes `note.title`, and `note.graph` becomes `note["@graph"]`. **A predicate your shape does not name is not projected**, so a shape written only to match — an `rdf:type` constraint and nothing else — now comes back with no fields at all: declare the ones you read. (`readUnion` was unchanged by this version, and was removed in `1.0.0-dev.8`.)
|
||||
- **`shapeType` is typed.** It was `unknown`; it is `ShapeType<T>` (the SDK's own, re-exported here), so the snapshot is typed without a cast — and a hand-written shape object must now satisfy that type, which means `readablePredicate`, `minCardinality` and `maxCardinality` on every predicate.
|
||||
- **`configure` takes one more thing.** `ormSubscription: RdfOrmSubscription.getOrCreate`, plus `initNg` and a call to this package's `initNg(ng, session)` once the session has arrived. Optional in the type, required in practice for this read: without it `useShapeQuery` publishes `isError` naming the field.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
type: usage
|
||||
summary: The polyfill entries the indexing layer and its end-to-end application stand on - placement by store reference, one anchored write, the deposit that becomes an entry, the one-shot read, and one shape query
|
||||
against: "@ng-eventually/polyfill@1.0.0-dev.6 (pulled 77e78ec81de81959e90e40da8d302e274b025c51)"
|
||||
summary: The polyfill entries the indexing layer and its end-to-end application stand on - placement by store reference, one anchored write, the deposit that becomes an entry, the anchored read, and one shape query
|
||||
against: "@ng-eventually/polyfill@1.0.0-dev.15 (tag polyfill/v1.0.0-dev.15, commit ec3900077bfd090f5388e52b47a8132b2fc68d16)"
|
||||
---
|
||||
|
||||
# usage_ng-helpers - `@ng-helpers/indexing` on `polyfill-surface`
|
||||
@@ -24,8 +24,7 @@ Two callers here, and they consume different things. The **library** (`src/`) re
|
||||
|
||||
- `configure({ ng, useShape, ormSubscription, init, initNg, sharedWallet })`, this package's own `init(callback, true, [])` for the `session_id` its callback delivers, this package's `initNg(ng, session)` once that session has arrived, and `ensureIdentity()`. `ormSubscription` is `RdfOrmSubscription.getOrCreate` and the injected `initNg` is the SDK's, both from `@ng-org/orm@0.1.2-alpha.21`, passed as they stand; the `ng` and `init` handed to `configure` are `@ng-org/web@0.1.2-alpha.14`'s.
|
||||
- The package's own published `ng`, as the argument `initNg` is given.
|
||||
- `readUnion([doc])` and the type `UnionSubject` - how an index is read, and how a referenced document is read. See `## Needs`.
|
||||
- `docs.sparqlQuery(sessionId, query, undefined, anchor)` - an anchored SELECT, as the second of the two ways anyone reads an index.
|
||||
- `docs.sparqlQuery(sessionId, query, undefined, anchor)` - the anchored SELECT, and the ONLY way anything here reads a document by name: an index, a referenced object, and the stranger's case where the reader holds no key and the public store is asked for one. Two forms: the narrow `SELECT ?object ?value` a consumer of `@ng-helpers/indexing` is told to write, and `SELECT ?s ?p ?o`, whose rows the application groups by subject itself, skipping the polyfill's `urn:ng-eventually:` registers.
|
||||
- `useShapeQuery(shape, { graphs: "did:ng:i" })`, `ShapeObservable`'s three members (`getSnapshot`, `subscribe`, `refetch`), the three states of `ShapeQuery` and its `data`, and the types `Scope` and `ShapeType`. ONE use, and it is an observation rather than a business read: after a failure is injected into the inbox step of `create`, this is how the suite finds the document that was left behind. The shape is hand-written and names one predicate - the descriptor `create` writes - which is what makes an index document matchable at all.
|
||||
|
||||
**Not consumed**, and safely evolvable as far as this repository is concerned: `useShape` (a function that REFUSES is handed to `configure`, and nothing calls it - see `## Frictions`), `subscribeDoc` / `subscribeDocs`, `docs.docCreate`, `storeRegistry.privateStore` / `protectedStore`, `inbox.share` / `post` / `read` / `readForDocument` / `readSynced` / `readSyncedForDocument` / `processInbox` / `watch`, and the types `Deposit` and `PrincipalId`.
|
||||
@@ -54,7 +53,9 @@ Two callers here, and they consume different things. The **library** (`src/`) re
|
||||
|
||||
**Every NURI is validated before it reaches a query or a deposit**, because no type guard is published (see `## Frictions`).
|
||||
|
||||
**This layer stays out of `urn:ng-eventually:` entirely.** Its predicates are `urn:ng-helpers:` and the subjects it writes are always `did:ng:` references, so nothing it writes falls under the reserved prefix and nothing it writes is dropped on read.
|
||||
**This layer stays out of `urn:ng-eventually:` entirely.** Its predicates are `urn:ng-helpers:` and the subjects it writes are always `did:ng:` references, so nothing it writes falls under the reserved prefix - which is what lets a reader skip every subject under that prefix without losing anything of an index.
|
||||
|
||||
**A document that cannot be read is refused, not answered empty.** Every read here rests on the anchored query rejecting for a document the reader cannot reach, rather than returning no rows; the end-to-end suite lets that rejection surface as a failure and never reads it as an index holding nothing.
|
||||
|
||||
**An empty read is never concluded from.** Where the end-to-end suite reads an index it polls to a bound and reports what the index held instead, rather than reading once and calling an empty answer an answer.
|
||||
|
||||
@@ -62,30 +63,18 @@ Two callers here, and they consume different things. The **library** (`src/`) re
|
||||
|
||||
## Frictions
|
||||
|
||||
**The acquisition channel carries no version, so the Change policy has nothing to bite on.** The engagement asks for a pinned version and says the channel by which the source reaches an application is agreed with that application; nothing has been agreed here. This repository reaches the package as a filesystem link and declares the peer range `*`, so what it compiles against is whatever is checked out at the other end - uncommitted edits included. That is not theoretical: while this declaration was being rewritten the linked tree moved from `1.0.0-dev.6` to work heading for `1.0.0-dev.7`, and the end-to-end suite's result moved with it, mid-session, with no pin able to prevent it. The `against:` line above pins the last COMMIT, which is the only pin that exists on this side. A tag-addressable, fetchable source would turn the whole semver discipline from a description into a mechanism.
|
||||
**`configure` requires `useShape`, and a frameworkless application has nothing to give it.** `@ng-org/orm` publishes `useShape` only through its react / svelte / vue adapters; a page with no framework can produce none of them. The field is not optional in `EventuallyConfig`, so this application hands over a function that THROWS if anything ever calls it - a value invented to satisfy a signature. Making the field optional removes it.
|
||||
|
||||
**At the pinned version the one shape read here never starts, and the engagement says it cannot be told apart from a shape written wrong.** `useShapeQuery` is wired exactly as the Deployment requirements describe - `ormSubscription: RdfOrmSubscription.getOrCreate`, the SDK's `initNg` injected, this package's `initNg(ng, session)` called once the session has arrived - and against the broker the ORM's call into `ng` fails with `orm_start_graph is not a function` on the console. Per `## Non-guarantees` NextGraph swallows that and the read stays `isPending` for ever, so the observation it feeds reports `observed=pending` and the two end-to-end checks that rest on it fail. Reproduced on three consecutive runs, 2026-09-21. Measured through the published surface: the SDK's own `ng` answers that member, and the `ng` an application hands `initNg` makes no difference because the call substitutes one of its own. Nothing on this side can work around it - the withdrawn `listMyEntityDocs` had no other replacement - so this is carried as a failing check rather than hidden behind a looser one.
|
||||
|
||||
**`configure` requires `useShape`, and a frameworkless application has nothing to give it.** `@ng-org/orm` publishes `useShape` only through its react / svelte / vue adapters; a page with no framework can produce none of them. The field is not optional in `EventuallyConfig`, so this application hands over a function that THROWS if anything ever calls it - better than the `() => undefined` it carried before, which a caller could not tell from an empty read-filtered view, but still a value invented to satisfy a signature. Making the field optional, or saying in the engagement what a frameworkless application should pass, removes it.
|
||||
|
||||
**A shape has to be hand-written, and the engagement carries no example.** `ShapeType` demands `readablePredicate`, `minCardinality` and `maxCardinality` on every predicate, plus a `schema` keyed by the shape's own IRI and a type carrying `@id` and `@graph`. The Change policy names three of those requirements in one clause and shows none of the shape. What was written here was assembled from the SDK's own type declarations - a read this contract exists to make unnecessary. Three lines of example in `## Surface` would cover every application that has no generated shape to hand.
|
||||
|
||||
**`initNg`'s first argument has no effect.** The Deployment requirements say to call `initNg(ng, session)`, and `## Surface` says of the published `ng` that it is the one to call instead of the one `configure` was given - so an application has a choice to make. Measured through the published surface: the call substitutes an `ng` of its own and the argument is discarded, so both readings behave identically and neither can be told apart from the outside. Saying so - or dropping the parameter - saves the next application the experiment.
|
||||
|
||||
**No published NURI type guard.** `## Guarantees` says so plainly, so this layer carries its own. It is a guess at what the engagement considers a valid `Nuri`, and a wrong guess either rejects a legitimate reference or lets one through into a query.
|
||||
**No published NURI type guard.** Every entry validates at its own door, but this layer composes SPARQL before it reaches one, so it carries its own guard. It is a guess at what the engagement considers a valid `Nuri`, and a wrong guess either rejects a legitimate reference or lets one through into a query.
|
||||
|
||||
**No published escaping helpers.** The engagement lists no `escapeIri` / `escapeLiteral`, and this package composes SPARQL against `docs.sparqlUpdate` - so it carries its own escaping rather than reach into the provider. That is a security-relevant duplication: two implementations of one rule, and the one running here is not the one the provider tests.
|
||||
|
||||
**No published way to write triples above the raw SPARQL primitive.** `docs.sparqlUpdate` is the level this layer had to align on, which is where the two frictions above come from. An "add these triples to this document" entry would delete the query composition, the escaping and the NURI validation from this package in a single move.
|
||||
|
||||
**`readUnion` has no stated behaviour for a document it cannot read.** `## Guarantees` says what it returns for a document it can, and it says elsewhere that a rejection means "unknown, never absent" - but not whether an unreadable document inside the list comes back as a rejection or is swallowed into the result. The worst is assumed (swallowed, therefore indistinguishable from empty) and every read path here is written defensively around that guess. One sentence in `## Guarantees` would replace it.
|
||||
**No published way to write triples above the raw SPARQL primitive that a library without the ORM can reach.** `insertObject` is the write above `docs.sparqlUpdate`, but it is injected through `configure` and this library wires no `@ng-org/*` by design (see `## Constraints`), so `docs.sparqlUpdate` remains the level it aligns on - which is where the two frictions above come from. An "add these triples to this document" entry, taking no shape, would delete the query composition, the escaping and the NURI validation from this package in a single move.
|
||||
|
||||
## Needs
|
||||
|
||||
**A one-shot read of a named document's subjects - `readUnion` as it stands, or something that answers the same question for a document whose key the reader does not yet hold.** The ORM is wired here now, so `useShapeQuery` is reachable and the earlier objection - that this application wires none - has fallen. It still does not replace this read, and the reason is the engagement's own: **neither shape read fetches a public store's key**, so a public document read nowhere else appears in neither, and the way to make it appear is to read it once by naming it, with `readUnion` or an anchored SELECT. An index IS a public document and its central case is a STRANGER reaching it from its reference alone - precisely the case no shape read serves. Three smaller costs sit on top: a shape read is reactive and three-stated where a page-load read is one-shot; it projects only the predicates a shape declares, so an index's entries and the index's own declaration take two shapes where `readUnion([index])` takes one call; and it pushes the ORM, a hand-written shape and the `useShape` friction above onto every consumer of `@ng-helpers/indexing`, which today needs none of them. This package publishes to its own consumers that an index is an ordinary document, read with `readUnion([index])` and recognised by two IRIs; withdrawing it leaves the anchored SELECT as the only fallback, which puts every reader of an index back on raw SPARQL - where three of the frictions above come from.
|
||||
|
||||
**A way to tell which store holds a document whose reference is already held.** The withdrawn `listMyEntityDocs` had one use here, and the shape read now covers most of it: after a failure is injected into the inbox step of `create`, the suite finds the document left behind by asking for every index document this identity HOLDS and diffing. What that cannot say is WHERE it sits, so the check it feeds now establishes that a document survived, and no longer that it survived in the public store. This is not the question the Change policy declined: it enumerates nothing, it names a document the caller already holds, and a store's graph is said to hold exactly the containment triples that would answer it. As it stands an application cannot verify its own placement at all - a document written to the wrong store is undetectable until some reader fails to open it.
|
||||
|
||||
**A way to tell a shape the ENGINE refused from a shape the application wrote wrong.** `## Non-guarantees` says a refused shape query is caught upstream, written to the console and nothing more: the set stays empty and its readiness never settles. From an application both faults look identical - an `isPending` that does not end - and the console line is not something a page can act on. That is the whole diagnostic surface for the one thing an application must hand-write itself (see `## Frictions`), so the first shape anybody writes has no way of telling "the engine would not take this" from "the ORM was never started". Anything that separates them - a refusal that reaches `isError`, or a state that says "never opened" - turns a silent hang into something an interface can report.
|
||||
**A way to tell a shape the ENGINE refused from a shape the application wrote wrong.** `## Non-guarantees` says a refused shape query is caught upstream, written to the console and nothing more: the set stays empty and its readiness never settles. From an application both faults look identical - an `isPending` that does not end - and the console line is not something a page can act on. That is the whole diagnostic surface for the one thing an application must hand-write itself, so the first shape anybody writes has no way of telling "the engine would not take this" from "the ORM was never started". Anything that separates them - a refusal that reaches `isError`, or a state that says "never opened" - turns a silent hang into something an interface can report.
|
||||
|
||||
**A way to tell "examined and discarded" from "not examined yet".** A deposit this engagement applies is consumed whether or not it produced an entry, and a refusal goes to the provider's own log stream - nothing an application can observe, and by the engagement's own words there is no way to ask what was lost. The cost here is concrete: the journey that checks that an object carrying nothing for an index's field is NOT indexed cannot be waited for, because no reading distinguishes "the deposit was examined and skipped" from "the deposit has not been examined yet" - and this repository's suite says so in its own header rather than hiding it, declaring that check weaker than its name. Anything observable from the application - a per-deposit outcome, or merely a count that moves when a queue has been drained - would make it a real one. This layer holds nothing of its own on purpose: an index only ever grows, and the reference is the whole of what is deposited.
|
||||
|
||||
Reference in New Issue
Block a user