feat!: la famille ORM est publiee enveloppee, et le ng publie cesse de rendre des promesses

This commit is contained in:
Sylvain Duchesne
2026-09-21 12:38:17 +02:00
parent 2aac703784
commit d0ea043a5b
21 changed files with 1102 additions and 35 deletions
+70 -9
View File
@@ -94,13 +94,14 @@ So an application's whole bootstrap is `configure({ ng, useShape, ormSubscriptio
// lifecycle.ts:11 — settles the identity, wraps the callback, then forwards to the real
// @ng-org/web init injected at configure()
export function init(...args: any[]): any;
// lifecycle.ts:18 — forwards to the real @ng-org/orm initNg injected at configure()
// lifecycle.ts:18 - forwards to the real @ng-org/orm initNg injected at configure(),
// with the `ng` in position 0 replaced by this package's wrapped one
export function initNg(...args: any[]): any;
```
### Target
**PASSTHROUGH, VERIFIED at both levels — with one argument touched, deliberately.** `init`'s callback in position 0 is wrapped since 2026-08-12: the wrapper reads the event, keeps the session it carries (§ 1), and calls the caller's callback with that same event, unchanged. Everything else — the remaining arguments, the return value, what the callback observes — passes straight through, so an application's call site is what it would write against the real SDK. This is the only place the capture can sit: it is the only one that knows both what the caller asked and what the SDK will answer, and the alternative was every consumer re-implementing it (which is what it replaces). The real signatures forwarded to are:
**PASSTHROUGH, VERIFIED at both levels - with one argument touched in each, deliberately.** `init`'s callback in position 0 is wrapped since 2026-08-12: the wrapper reads the event, keeps the session it carries (§ 1), and calls the caller's callback with that same event, unchanged. Everything else - the remaining arguments, the return value, what the callback observes - passes straight through, so an application's call site is what it would write against the real SDK. This is the only place the capture can sit: it is the only one that knows both what the caller asked and what the SDK will answer, and the alternative was every consumer re-implementing it (which is what it replaces). The real signatures forwarded to are:
```ts
// level 2 — @ng-org/web: index.d.ts:108, source sdk/js/web/src/index.ts:51
@@ -120,7 +121,9 @@ export declare type Session = {
};
```
Divergence: none in behaviour (pure forwarding), but the wrapper erases the parameter types. A consumer typing calls against the wrapper learns nothing it must unlearn — it just gets no compile-time help the real SDK would give.
`initNg`'s argument in position 0 is substituted since `1.0.0-dev.7`, and it is the same kind of move for the same reason. That argument is what the ORM reaches the engine through afterwards - every subscription, every `insertObject`, every batched patch awaits the promise this call resolves - and an ORM write leaves by `ng.graph_orm_update`, which is where the emulated write guard now sits (§ 3). Handing the caller's raw SDK object over leaves that guard on a door nothing knocks on, and "pass the package's `ng` here" would be an ordering rule in prose: unenforceable, silent when missed, and in fact missed by both consumers in this repository. What is substituted is the package's own `ng` - the same object the entry publishes, forwarding all 88 members to the very object `configure()` received - so the ORM now reaches the engine through exactly what the application does.
Divergence: none in behaviour (pure forwarding), but the wrappers erase the parameter types. A consumer typing calls against them learns nothing it must unlearn - it just gets no compile-time help the real SDK would give.
---
@@ -166,7 +169,11 @@ export const ng: NG;
export type { NG } from "@ng-org/web";
```
`ng` is a `Proxy` (`ng-proxy.ts:26`) forwarding every property to the injected real `ng`, overriding exactly one thing: `sparql_update` (the emulated write-cap guard, rejecting a write when a write policy governs the anchored document and the current user lacks the cap). `session_start` is intercepted but is a pure passthrough today, with a TODO for shared-wallet credentials. (`login` was named here until 2026-08-14 but has not existed since 2026-08-03: `@ng-org/web` exposes no such method, and the proxy was FABRICATING it.)
`ng` is a `Proxy` (`ng-proxy.ts`) forwarding every property to the injected real `ng`, overriding two things: `sparql_update` (the emulated write-cap guard, rejecting a write when a write policy governs the anchored document and the current user lacks the cap) and, since `1.0.0-dev.7`, `graph_orm_update` (the ORM's own write door, guarded by OWNERSHIP). `session_start` is intercepted but is a pure passthrough today, with a TODO for shared-wallet credentials. (`login` was named here until 2026-08-14 but has not existed since 2026-08-03: `@ng-org/web` exposes no such method, and the proxy was FABRICATING it.)
There is exactly one instance of that proxy for the whole package (`wrappedNg()`), because two things now hold it: the entry publishes it, and `initNg` hands it to the ORM (§ 2).
**The passthrough itself was broken until `1.0.0-dev.7`, for every one of the 88 members, and nothing noticed.** It read `Reflect.get(ng, prop).bind(ng)`. `@ng-org/web`'s `ng` is ITSELF a Proxy - an "async proxy" whose `get` trap answers EVERY key with a fresh path-carrying callable and whose reflection traps throw (`dist/ngweb.js`, the `l(e = [])` factory) - so `.bind` never reached `Function.prototype.bind`: it was just another key, calling the result posted a remote request named after the MEMBER carrying the bind arguments, and what came back was a **Promise**. A caller therefore got the SDK's own `<member> is not a function`. Two things hid it: this library's internals never go through this proxy (they call the injected `ng` directly), and every fake in the suite is a plain object, on which `bind` behaves. It surfaced the moment `initNg` handed the ORM this object - `orm_start_graph` was not a function, the ORM catches that and only `console.error`s it, so a shape read stayed `isPending` for ever, which is the failure mode § 5 describes. MEASURED against `@ng-org/web@0.1.2-alpha.14`. The member is now FORWARDED (`Reflect.apply(real, ng, args)`), which gives a plain object the receiver it needs and lets the async proxy use the path it already carries; `test/ng-proxy.test.ts` pins it against a stand-in shaped like the real one.
### Target
@@ -175,7 +182,8 @@ export type { NG } from "@ng-org/web";
The overrides:
- `session_start(wallet_name: string, user_id: any): Promise<any>` (`index.d.ts:276`) — target signature unchanged; only the emulated credential injection disappears.
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`index.d.ts:297`) — target signature unchanged. The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller — see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until cap-enforcement.
- `graph_orm_update(subscription_id: any, diff: any, session_id: any): Promise<void>` (`index.d.ts:104`) - target signature unchanged. **This is the door an ORM write actually uses**, and until `1.0.0-dev.7` nothing here guarded it: a mutation of a live signal object (`set.add(...)`, `set.delete(...)`, assigning a property) never passes through `sparql_update` at all, it is batched into patches and sent through this member. So the `sparql_update` guard saw no ORM add and no ORM delete - reachable from the published surface, since `useShape` forwards `add`/`delete`/`clear` to the underlying set and `useShapeQuery`'s `data` items ARE that set's objects. The guard reads the document out of each patch's `path`, whose first segment is the synthetic id `<graph>|<subject>` the ORM itself minted, and asks `assertMayWrite` - OWNERSHIP, the same predicate `docs.sparqlUpdate` asks, not the decorative write-cap list the `sparql_update` branch consults. A patch naming no NextGraph reference passes: the emulation cannot say whose document that is, and upstream does not have to guess because `verify_permission` fires on the commit itself.
- `sparql_update(session_id: any, sparql: string, nuri: any): Promise<any>` (`index.d.ts:297`) - target signature unchanged. It is deliberately left on the decorative write-cap list rather than moved to `assertMayWrite`: this library's own REGISTERS are written through this member, and a store document is not owned in the authorship sense, so arming it here would refuse every register write (`emulated-verifier/register-write.ts`). The native enforcement the guard stands in for is the engine's permission model (`verify_perm`, `engine/repo/src/commit.rs:897`), which today is **called only from tests** (its enclosing `Commit::verify` has no runtime caller - see `docs/nextgraph-current-state.md` § *Author-signature verification*). That absence says nothing about the target: write permissions are the engine's declared model, so the guard's *behaviour* (a refused write) is target-shaped even though its *mechanism* (a JS-side check) is emulation. Known limit, documented in `README.md`: the guard fires only on this proxy, and the lib's own writers call the injected `ng` directly, so it is best-effort until cap-enforcement.
---
@@ -314,6 +322,50 @@ The anchored-read mechanics are level-1 VERIFIED: an anchor restricts the query
---
## 6bis. The ORM's one-shot read and subscription-free write - `getObjects` / `insertObject`
### Today — `@ng-eventually/polyfill`
```ts
// orm.ts - the SDK's own read, with the capability filter on what comes back
export async function getObjects<T extends BaseType>(
shapeType: ShapeType<T>,
config: Omit<RdfOrmConfig<T>, "pageSize" | "maxActivePages">,
): Promise<T[] | Set<T>>;
// orm.ts - the SDK's own write, with the ownership guard before it
export async function insertObject<T extends BaseType>(shapeType: ShapeType<T>, object: T): Promise<T>;
```
**PASSTHROUGH (level 3, VERIFIED)** - both at upstream's own name, parameters and return, forwarding to the real functions injected at `configure()` (`getObjects`, `insertObject`; § 1). Published since `1.0.0-dev.7`; before that an application had to import them straight from `@ng-org/orm`, which is the problem rather than an inconvenience - an unwrapped ORM read returns EVERY identity's data out of the shared wallet with nothing signalling it, and an unwrapped ORM write reaches any document at all.
**What each wrapper adds, and where the target does the same thing.** A READ is filtered on what comes back: `emulated-verifier/read-filter.ts` keeps only items whose document - their `@graph` - the current holder holds a cap for, applied per document, exactly where § 4 and § 5 apply it. The filter keeps the collection's shape, so a `Set` in is a `Set` out and an `orderBy`ed array stays an array; before the first cap is issued the emulation is not in force and everything passes through. A WRITE is NOT filtered, and the asymmetry is upstream's: a write is bounded by write capability (`verify_permission`, reachable only from `Commit::verify`, so it fires on commits and never on reads) and nothing is hidden from the writer. So `insertObject` takes the same pair of guards `docs.sparqlUpdate` takes - `assertMayReach` then `assertMayWrite` - BEFORE the call, on the `@graph` the object carries. An object naming no document is forwarded untouched and the ORM's own refusal is heard (it throws without a `@graph`, because upstream the insert subscribes to no document and has no other way to know where the write lands).
**What comes back from `getObjects` is DETACHED** - `structuredClone` of the raw signal object - so mutating an item of it reaches nothing, and deleting an item of it removes nothing and reports nothing. Removal is a mutation of the LIVE set a subscription holds (§ 4), which leaves through `graph_orm_update` and is guarded there (§ 3).
**What is deliberately NOT published from this family**, each for a reason a reader can check:
- **`RdfOrmSubscription`** - an application never holds a subscription: it hands the shape query to `configure` and reads `useShapeQuery`'s snapshot back (§ 5, § 14). And `getOrCreate` POOLS on a key built from the shape and the scope alone - no identity in it - so a second virtual user asking the same question is handed the FIRST one's live instance. What makes that safe here is the filter this package applies on the way out; publishing the handle would hand out the instance instead.
- **`DiscreteOrmSubscription`** - a JSON-CRDT document. Its items carry no `@graph`, so the read filter has nothing to read, and its writes leave through `discrete_orm_update`, a third channel nothing here guards. Publishing it would open a surface and a hole in the same gesture.
- **`ngSession`** - a module-level PROMISE resolving to the SDK handle plus the SESSION. One physical session hosts every virtual identity here, so handing it out hands every identity the same one (§ 1, § 13). Wrapping a resolved value would also force a RUNTIME `@ng-org/orm` import into this package, which is what the injection discipline exists to avoid.
- **`getRaw`** - it returns the object BEHIND a signal, the same escape hatch the read filter refuses on a dunder key (`__raw__`): the unfiltered collection, every identity's items in it. There is no filtered form of it.
- **`watch` / `effect`** - `@ng-org/alien-deepsignals`' reactivity primitives, re-exported by the ORM's entry. They carry no document and grant nothing, so there is nothing to wrap.
- **`removeObject`** - **NOT YET RULED ON, and it is the one gap in this section.** It exists in the published `@ng-org/orm@0.1.2-alpha.21` (`removeObject(graphNuri, subjectIri)`, a `DELETE WHERE` on that subject, stated to have the same effect as `delete()` on a subscription object) and it does NOT exist at the `nextgraph-rs` revision this repository reads (`213338f`): no file, no symbol. So nothing in the source can be cited for it, and the project's own rule refuses a claim inferred from what an npm package happens to expose. It is left unpublished until the source carries it; an application removes an object by mutating the live set, which is the path § 4 publishes and § 3 guards.
### Target
```ts
// level 3, VERIFIED - sdk/js/orm/src/connector/getObjects.ts:getObjects
export async function getObjects<T extends BaseType>(shapeType: ShapeType<T>, scope: Scope | string);
// level 3, VERIFIED - sdk/js/orm/src/connector/insertObject.ts:insertObject
export async function insertObject<T extends BaseType>(shapeType: ShapeType<T>, object: T);
```
**One difference between the cited revision and the published package, and the published one is what is mirrored here.** `getObjects`'s second parameter was `Scope | string` at `213338f`; `@ng-org/orm@0.1.2-alpha.21` widened it to the config `RdfOrmConfig` declares, minus the two pagination fields a one-shot read has no use for - a `Scope` plus `where` and `orderBy`. The wrapper takes the wider one, because that is what a consumer's TypeScript actually checks against and because narrowing it back to `Scope` would reject a `where` the SDK accepts. Both return types are inferred upstream rather than annotated; the published declaration says `Promise<T[] | Set<T>>` for the first and `Promise<T>` for the second.
At migration both wrappers disappear and the imports resolve to these two functions: same names, same arguments, same returns. What goes with them is the filtering and the guard, which the broker and the verifier then do.
---
## 7. Raw document / SPARQL primitives — `docs.*`
### Today — `@ng-eventually/polyfill` (namespace `docs`)
@@ -691,10 +743,16 @@ declare function user_disconnect(user_id: string): Promise<void>;
```ts
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
export type { DeepSignalSet } from "@ng-org/alien-deepsignals";
export type { Scope } from "@ng-org/orm";
export type { RdfOrmConfig } from "@ng-org/orm/all";
export type { NG } from "@ng-org/web";
```
**PASSTHROUGH (levels 2/3, VERIFIED)** — `ShapeType`/`BaseType` at `@ng-org/shex-orm` `dist/types.d.ts:5,12` (installed 0.1.2-alpha.8); `NG` at `index.d.ts:136`. At migration these imports point at the same packages directly; nothing changes for the consumer.
**PASSTHROUGH (levels 2/3, VERIFIED)** - `ShapeType`/`BaseType` at `@ng-org/shex-orm` `dist/types.d.ts:5,12`; `NG` at `index.d.ts:136`. At migration these imports point at the same packages directly; nothing changes for the consumer.
A type is erased at build, so re-exporting one adds no runtime `@ng-org` import and opens no path around the emulation - which is why the rule that nothing of the SDK is published as it stands is about VALUES, and these are the exception it does not reach.
`RdfOrmConfig` comes from the SDK's `/all` subpath rather than its main entry, and that is upstream's packaging rather than a choice here: the config type `getObjects` declares is not exported beside the function that takes it (§ 6bis). Restating it locally is how a type drifts, so it is re-exported from where the SDK puts it.
### The library's own model types — published by NAME since 2026-08-10
@@ -703,10 +761,13 @@ The entry used to say `export * from "./model/types"`, a blanket re-export publi
> **A type is published only if a PUBLISHED SIGNATURE uses it.**
```ts
export type { Nuri, NuriLike, PrincipalId, NgLike, UseShapeLike } from "./model/types";
export type {
Nuri, NuriLike, PrincipalId, NgLike, UseShapeLike, OrmSubscriptionLike,
GetObjectsLike, InsertObjectLike,
} from "./model/types";
```
Each one's warrant: `Nuri` is what every reference-returning call returns and `NuriLike` what every entry accepts (§ 10, § 11); `Scope` left this list in `1.0.0-dev.5` — the read takes the SDK's own `Scope`, re-exported from `@ng-org/orm`, and a store is named by its reference (§ 5, § 12); `PrincipalId` is `ensureIdentity`'s return and a field of `Deposit`, `PostOptions` and `EventuallyConfig` (§ 2bis, § 9, § 1); `NgLike`, `UseShapeLike` and `OrmSubscriptionLike` type the three injected members of `EventuallyConfig` (§ 1). `OrmSubscriptionHandle` — what the last of them RETURNS — is deliberately not published: an application hands the shape query in and reads the snapshot back, and publishing the handle would offer a path around the capability filter and the shared-wallet identity boundary.
Each one's warrant: `Nuri` is what every reference-returning call returns and `NuriLike` what every entry accepts (§ 10, § 11); `Scope` left this list in `1.0.0-dev.5` - the read takes the SDK's own `Scope`, re-exported from `@ng-org/orm`, and a store is named by its reference (§ 5, § 12); `PrincipalId` is `ensureIdentity`'s return and a field of `Deposit`, `PostOptions` and `EventuallyConfig` (§ 2bis, § 9, § 1); `NgLike`, `UseShapeLike`, `OrmSubscriptionLike`, `GetObjectsLike` and `InsertObjectLike` type the five injected members of `EventuallyConfig` (§ 1), the last two since `1.0.0-dev.7` (§ 6bis). `OrmSubscriptionHandle` - what the last of them RETURNS - is deliberately not published: an application hands the shape query in and reads the snapshot back, and publishing the handle would offer a path around the capability filter and the shared-wallet identity boundary.
Two types the blanket export published are now internal, each because **nothing published names it**:
@@ -750,7 +811,7 @@ Exported, but not SDK surface. Coding against these builds knowledge that migrat
### `@ng-eventually/polyfill` — `src/index.ts` (the only entry since 2026-08-07)
```text
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, NG, NgLike, Nuri, NuriLike, OrmSubscriptionLike, PrincipalId, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, init, initNg, ng, readUnion, subscribeDoc, subscribeDocs, useShape, useShapeQuery
direct: BaseType, DeepSignalSet, DocChange, DocChangeType, EventuallyConfig, GetObjectsLike, InsertObjectLike, NG, NgLike, Nuri, NuriLike, OrmSubscriptionLike, PrincipalId, RdfOrmConfig, Schema, Scope, ShapeObservable, ShapeQuery, ShapeType, SharedWalletConfig, UnionSubject, Unsubscribe, UseShapeLike, configure, docChangeType, ensureIdentity, getObjects, init, initNg, insertObject, ng, readUnion, subscribeDoc, subscribeDocs, useShape, useShapeQuery
docs: docCreate, sparqlQuery, sparqlUpdate
inbox: Deposit, PostOptions, post, postToDocument, processInbox, read, readForDocument, readSynced, readSyncedForDocument, share, watch
storeRegistry: createEntityDoc, openDocumentInbox, privateStore, protectedStore, publicStore