7bcb52a5fd
Deux feuilles affirmaient que la richesse de la config ORM est perdue au
niveau 1 - "no caller can reach any of it. The richness is real and
unreachable". La preuve invoquee etait le `json!({})` de
`Verifier::start_orm` a 213338f6, et elle ne porte pas cet enonce.
Ce qui est verifie a 213338f6 : `OrmConfig` declare where_, order_by,
select, page_size et max_active_pages ; `from_json` les analyse tous les
cinq et refuse un pageSize sans orderBy ; schema_shape_to_sparql et le
pagineur les consomment. La machinerie est implementee. Il manque un
seul saut, a l'ENTREE : start_orm ne prend aucun argument de config, et
son TODO voisin nomme le maillon absent, "use json in libwasm context".
Or ce maillon existe dans le build : @ng-org/web@0.1.2-alpha.14 publie
orm_start_graph avec SIX parametres, config en position 4, quand l'arbre
cite en declare CINQ, et @ng-org/orm@0.1.2-alpha.21 l'appelle avec six
en passant tout son objet d'options. Lus dans les artefacts publies.
Donc la config quitte le niveau 3 et est acceptee au niveau 2 aux
versions installees. Ce que le moteur derriere ce binding en fait n'est
etabli par rien d'atteignable, et c'est dit ainsi.
65 lines
10 KiB
Markdown
65 lines
10 KiB
Markdown
---
|
|
type: knowledge
|
|
summary: What the target offers for writing and removing — deletion is a SET MUTATION wired end to end, it removes the shape's predicates rather than the subject, and the ORM's writes never pass through `sparql_update`
|
|
last_checked: 2026-09-24
|
|
---
|
|
|
|
# What the ORM writes and deletes upstream
|
|
|
|
Read at the source in `nextgraph-rs` (`213338f697df15afc086c042f4bab1bffd01aa04`), which declares `@ng-org/orm@0.1.2-alpha.18`, `@ng-org/web@0.1.2-alpha.13`, `@ng-org/shex-orm@0.1.2-alpha.8` and `@ng-org/alien-deepsignals@0.1.2-alpha.11`. `packages/polyfill` installs `@ng-org/orm@0.1.2-alpha.21`, `@ng-org/web@0.1.2-alpha.14`, `@ng-org/shex-orm@0.1.2-alpha.9` and `@ng-org/alien-deepsignals@0.1.2-alpha.13` - the published packages are AHEAD of the source this repo cites, so where the two disagree both are named below. That gap is checked rather than remembered: the `ANCHOR` in `packages/polyfill/test/provenance.test.ts` holds it once and goes red from either end. Every claim points at a symbol rather than a line, so `git grep` settles any doubt after the code moves. Companion to [[knowledge_what-a-read-scope-is-upstream]], which answers the reading half.
|
|
|
|
## Removing an object is a set mutation, and LEVEL 3 answers
|
|
|
|
`sdk/js/orm/README.md` teaches it as one line — `dogs.delete(aDog)` — on the `DeepSignalSet` a subscription hands back. There is no `deleteObject` symbol at any version: `sdk/js/orm/src/core.ts` at the cited commit exports exactly `initNg`, `ngSession`, `OrmSubscription`, `DiscreteOrmSubscription`, `insertObject`, `getObjects`, plus `getRaw`/`watch`/`effect` re-exported from `@ng-org/alien-deepsignals`. The installed `@ng-org/orm@0.1.2-alpha.21` renames `OrmSubscription` to `RdfOrmSubscription` and adds one export the source does not carry, `removeObject(graphNuri, subjectIri): Promise<void>` - see the note at the end of this section. That absence is **not** the finding, and per [[no-divergence-from-nextgraph]] it would prove nothing on its own — the finding is that the set mutation is fully implemented, so the cascade stops at level 3 and there is nothing to invent.
|
|
|
|
**`removeObject` exists in the package and not in the source.** Its own documentation declares a wider effect than the set mutation: `DELETE WHERE { GRAPH <graphNuri> { <subjectIri> ?p ?o } }`, every predicate of the subject rather than the shape's own, while claiming in the same comment to have "the same effect" as a `delete()` on a subscription's object. The two cannot both hold, and nothing in the cited source settles it. It stays unpublished here for the reason `contract_polyfill-surface` records: a symbol whose only evidence is an npm build is not something to build a guarantee on.
|
|
|
|
## The path, hop by hop
|
|
|
|
1. **The set proxy.** `deepSignal`'s set handler (`sdk/js/alien-deepsignals/src/deepSignal.ts`, the `delete` branch) emits, for an OBJECT member, `{ path: [...containerPath, syntheticId], op: "remove" }` — a `DeepPatch` with no `type`. For a LITERAL inside a multi-valued property it emits `{ path: containerPath, op: "remove", type: "set", value }` instead.
|
|
2. **The synthetic id is `"<graph>|<subject>"`**, minted by `OrmSubscription.signalObjectPropGenerator` (`RdfOrmSubscription` in alpha.21), which also generates a subject IRI (`<first 53 chars of the graph NURI>:q:<33 random bytes base64url>`) when `@id` is `""`.
|
|
3. **`deepPatchesToWasm`** (`sdk/js/orm/src/connector/utils.ts`) joins the path into the string `/<graph>|<subject>`.
|
|
4. **`OrmSubscription.onSignalObjectUpdate`** (`RdfOrmSubscription` in alpha.21) batches in a microtask (or into `pendingPatches` inside a transaction) and calls `ng.graph_orm_update(subscriptionId, patches, session_id)`.
|
|
5. **The binding.** `graph_orm_update` (`sdk/js/lib-wasm/src/lib.rs`) deserialises `OrmPatches` — `OrmPatch { op, valType, path, value }` with `OrmPatchOp::{add, remove}` and `OrmPatchType::set` (`engine/net/src/orm.rs`) — into `AppRequest::new_orm_update`.
|
|
6. **The engine.** `Verifier::orm_frontend_update` → `create_sparql_update_query_for_patches` (`engine/verifier/src/orm/graph/handle_frontend_update.rs`): a `remove` whose path resolves to a ROOT object (one segment, no predicate) calls `SparqlBuilder::remove_object` and marks the tracked object `TrackedOrmObjectValidity::Invalid`. The queries are then run through `process_sparql_update`, anchored on the graph taken from the first patch's path.
|
|
|
|
**The trap that makes a delete a no-op.** The patch is emitted only when `target.delete(rawValue)` returned true **and** a synthetic id is known for that object. So the object passed must be the one the set is holding — reached by iterating it, by `.first()` or by `.getBy(graph, subject)`. `getObjects` returns a `deepClone`, so deleting an item of ITS result removes nothing and reports nothing.
|
|
|
|
## `remove_object` deletes the SHAPE's predicates, not the subject and not the document
|
|
|
|
`SparqlBuilder::remove_object` loops the shape's own `predicates` and emits one `DELETE { GRAPH <g> { <s> <p> ?o } } WHERE { … }` per predicate. Triples on the same subject under a predicate the shape does not declare **survive**, and so does the document: nothing on this path deletes a document, and the ORM offers no call that does.
|
|
|
|
Removing a child from a parent's multi-valued object property is a different arm — `remove_link`, a `DELETE DATA` of the one triple that ties parent to child. The README states the consequence plainly: only that tie goes, the child's own quads stay.
|
|
|
|
## `getObjects` and `insertObject` — the exact surface
|
|
|
|
```ts
|
|
getObjects<T extends BaseType>(shapeType: ShapeType<T>, scope: Scope | string): Promise<DeepSignalSet<T>>
|
|
insertObject<T extends BaseType>(shapeType: ShapeType<T>, object: T): Promise<void>
|
|
```
|
|
|
|
Both return types are **inferred**, not annotated, and both functions are thin wrappers over `OrmSubscription`.
|
|
|
|
- **`getObjects`** (`sdk/js/orm/src/connector/getObjects.ts`) — `getOrCreate(shapeType, normalizeScope(scope))`, awaits `readyPromise`, schedules `close()` one second later, returns `deepClone(signalObject)`. The result is **detached**: a plain `Set` of plain objects, no two-way binding, no reactivity. `docs/api-contract.md` § 6 records this accurately — checked against the source, not re-derived.
|
|
- **`insertObject`** (`sdk/js/orm/src/connector/insertObject.ts`) — opens a subscription scoped to `{ graphs: [] }` (deliberately subscribing to no document), awaits ready, `signalObject.add(object)`, `close()`. The object **must** carry `@graph`: a root object without one makes `signalObjectPropGenerator` throw *"When adding new root orm objects, you must specify the @graph"*. `@id: ""` means "generate one".
|
|
|
|
## The three remaining symbols
|
|
|
|
**`OrmSubscription`**, `RdfOrmSubscription` in alpha.21 — a class, and the machinery every other read and write goes through. Private constructor; `static getOrCreate(shapeType, scope)` pooled on `"<shape>|<graphs>|<subjects>"` with refcounting and a 500 ms close delay. Instance surface: `signalObject: DeepSignalSet<T>`, `readyPromise`, `close()`, `beginTransaction()`, `commitTransaction()`, `inTransaction`, `shapeType`, `scope`. In alpha.21 the second argument is no longer a bare `Scope` but an `RdfOrmConfig` (`Scope & { where?, orderBy?, pageSize?, maxActivePages? }`), `readyPromise` resolves to the subscription data rather than to nothing, and the instance also carries `mode`, `isReady`, `addChangeListener`, `removeChangeListener`, `nextPage` and `previousPage`. How far that config travels is answered in [[knowledge_what-a-read-scope-is-upstream]]: the shipped binding takes it at index 4 and the shipped ORM sends it there, while what the engine behind that binding does with it is settled by nothing reachable. The `json!({})` in `213338f6`'s `Verifier::start_orm` does NOT settle it, and reading it as *"level 1 discards the richness"* - the claim this leaf used to carry - is inference from an absent implementation. It hands a caller nothing the target will not hand them — a shape, a scope and a set — and `useShape`, `getObjects` and `insertObject` are all built on it.
|
|
|
|
**`DiscreteOrmSubscription`** — the same class shape for JSON-CRDT documents. `static getOrCreate(documentId: string)`; `signalObject` is `DeepSignal<DiscreteArray | DiscreteObject> | undefined` (a getter, undefined before the initial payload); it reaches the engine through `orm_start_discrete` and `discrete_orm_update`. Its unit of subscription is **one document id**, never a `Scope`.
|
|
|
|
**`ngSession`** — not a function and not a class: a module-level `Promise<{ ng: NG; session: Session }>` (`sdk/js/orm/src/connector/initNg.ts`), resolved exactly once by `initNg`. Every subscription awaits it before touching the engine. Its value is the SDK handle plus the session object.
|
|
|
|
## A write never passes through `sparql_update`
|
|
|
|
`graph_orm_update` is its own SDK member, distinct from `sparql_update`. So a guard implemented by overriding `ng.sparql_update` — which is what `packages/polyfill/src/surface/ng-proxy.ts` does — **does not see an ORM write at all**, neither an add nor a delete. The read-filtered view (`packages/polyfill/src/emulated-verifier/read-filter.ts`) is symmetrical by design: it forwards `add`, `delete` and `clear` to the underlying set untouched, because those yield no items to leak.
|
|
|
|
That is a fact about where a guard can sit, not a proposal for one.
|
|
|
|
## Live behaviour against the deployed broker
|
|
|
|
Verified by measurement, not by reading: a live ORM read STARTS against the deployment, and the delete reaches the document. `packages/polyfill/e2e/orm-live-read.ts` is the measurement - `5 passed, 0 failed` on 2026-09-21, the read ready in 13ms, the seeded subject's triples gone after `.delete()`.
|
|
|
|
The read was blocked until 2026-09-21, and the recorded reason was wrong twice over. It was not that the broker needed to catch up: the DEPLOYMENT was ahead and the pinned client packages were behind, `orm_start_graph` having gained a `config` parameter at index 4, before the callback, which the five-argument client of the day filled with its callback. Updating the packages answered it. The bug leaf that carried the wrong mechanism was deleted with the question it described.
|