chore: les paquets ng-org rattrapent le deploiement, et la lecture vive demarre

This commit is contained in:
Sylvain Duchesne
2026-09-21 11:02:08 +02:00
parent 4a4e6af337
commit 82769c4939
6 changed files with 34 additions and 81 deletions
@@ -1,46 +0,0 @@
---
type: bug
summary: A live read through the published `useShape` never starts against the deployed broker — its `orm_start_graph` expects a `config` argument the published `@ng-org/orm` does not send, so the subscription stays empty and pending for ever with one console error as the only trace
severity: major
opened: 2026-08-17
last_checked: 2026-09-16
---
# `useShape` opens no live read against the deployed broker
The published `useShape` is a passthrough to `@ng-org/orm`'s hook (`packages/polyfill/src/surface/use-shape.ts`) — this package adds the read filter and forwards. Against the broker deployed today, the subscription that hook opens **never starts**: its set stays empty, its readiness never settles, no engine update ever arrives, and the only trace is one `console.error`. An application sees "this scope holds nothing", indefinitely, which is the exact confusion the rest of the read surface is built to prevent.
The cause is upstream, in the disagreement between the deployed broker and the published client packages. **Nothing in this package can work around it.**
## The mechanism — an ARGUMENT, not a name
MEASURED 2026-09-16 against the real broker by `packages/polyfill/e2e/orm-live-read.ts`, which drives the published `useShape` over the real `@ng-org/orm`. The engine answers:
```
Deserialization error of config Error: invalid type: JsValue(Function), expected any valid JSON value
```
A live ORM read is one call and it is streamed: `OrmSubscription`'s constructor issues `ng.orm_start_graph(graphs, subjects, shapeType, session_id, onBackendMessage)` — **five** arguments, callback last — and the engine pushes into that callback rather than answering once. What comes back instead names a parameter, `config`, that the client never sent.
The same measurement establishes where that parameter sits, by calling the SDK member directly with `{}` in position 4: **six arguments are ACCEPTED**, and the engine then streams back. So the deployed signature is `(graph_scope, subject_scope, shapeType, session_id, config, callback)`, and the published client's five-argument call lands its **callback** exactly where `config` is expected — which is precisely what the error describes.
**The direction is the opposite of what this leaf said until 2026-09-16.** The earlier reading — a broker built before `orm_start` was renamed `orm_start_graph` (upstream `d5ecd0fd`, 2026-01-16) — is **refuted**: the name resolves, and the call fails *inside* the function, on an argument. That arity exists in **no** commit of `nextgraph-rs` at `213338f6`: the one window that carried a `config` at all (`c38aa371` 2026-04-12 → `7c8abd0a` 2026-04-21, *"rm unused config param"*) put it **last**, after the callback, where a five-argument call would leave it merely absent. The deployment is therefore built from a source **ahead of** that checkout, and it is the published `@ng-org/orm@0.1.2-alpha.19` / `@ng-org/web@0.1.2-alpha.13` this package pins that are behind it.
**Why it fails in silence:** `orm_start_graph` is awaited inside a `try` whose `catch` is `console.error(e)`, and `resolveReady()` is called on one path only — the arrival of initial data (`GraphOrmSubscription.ts:handleInitialResponse`). A rejected start leaves the readiness promise pending for ever and the set permanently empty. This is the same upstream property already recorded in `docs/api-contract.md` (a failed read *is* an eternal pending upstream), reached here through a different door.
## What is affected, and what is not
- **Affected:** the published `useShape`, and only it. By the same argument, `insertObject` and `getObjects` — both built on `OrmSubscription` — cannot start either, so **no ORM write or deletion can be exercised against this deployment**.
- **Not affected:** `watchShape`, `subscribeDoc`, `subscribeDocs`, `readUnion`. These open one `doc_subscribe` per document, and `doc_subscribe: 2` has been in the method tables continuously under a stable name and arity. `watchShape` is built on that path, not on the ORM subscription — it is a working reactive read today.
That is why the library's own suites and the reference application do not show it: nothing in this package calls `useShape` internally.
## What to do
- **Do not build on live `useShape`.** Use `watchShape` for a reactive read, `readUnion` for a one-shot one. This is stated in the contract's `## Non-guarantees` so a consumer meets it without reading this leaf.
- **The fix is not ours, and it is an ALIGNMENT, not an upgrade in one direction.** Either the deployment rolls to a build whose `orm_start_graph` matches the published packages, or `@ng-org/orm` / `@ng-org/web` are republished against the deployed signature. Report it; do not patch around it, and do not pass a `config` argument from here — a client that guesses an unpublished arity breaks the day the two ends agree.
- **To check whether it is closed:** `bun run e2e/orm-live-read.ts` from `packages/polyfill` (it needs `@ng-org/orm`, an optional peer `bun install` does not fetch). It exits non-zero while this is open and names what the engine expected. Green means the two ends agree and this leaf can go.
## The shape worth remembering
A method **signature** — name and arity together — duplicated across three independently-deployed artifacts, with a **silent** failure when they disagree, is a defect no single repository's tests can catch. Pinning the client packages does not help: the broker-side half (`@ng-org/api-web`, `private: true`, shipped only inside the broker build) is pinnable by nobody, so the two ends drift in either direction and the symptom is identical both ways. Anything this package routes through a streamed method inherits the same exposure — which is the reason `subscribeDocs` composing per-document `doc_subscribe` is worth more than it looks.
@@ -46,3 +46,5 @@ The polyfill's published surface has drifted from the SDK's: invented names, inv
- T01.a — the taxonomy gains `announced-not-written`: its `ref` points at the `.md` in THIS repo where the statement is on record, never a source file, plus `announced-by=<who>@<date>`. Upstream citations now resolve-check only when the sibling checkout is present; announced records always. Two latent defects fell out: the test's closed set was missing `reserved-name`, and the rule said "five kinds" over six rows. All 92 existing refs already resolved. - T01.a — the taxonomy gains `announced-not-written`: its `ref` points at the `.md` in THIS repo where the statement is on record, never a source file, plus `announced-by=<who>@<date>`. Upstream citations now resolve-check only when the sibling checkout is present; announced records always. Two latent defects fell out: the test's closed set was missing `reserved-name`, and the rule said "five kinds" over six rows. All 92 existing refs already resolved.
- T01.b — deletion is answered at level 3 and fully wired: `DeepSignalSet.delete` -> a remove patch -> `graph_orm_update` -> `SparqlBuilder::remove_object`, which removes the SHAPE's predicates, not the subject; no document deletion exists at any level. `getObjects` returns a DETACHED clone; `insertObject` throws without `@graph`. Proposals: publish `OrmSubscription` as-is (the read filter applies to `signalObject`), publish NEITHER `DiscreteOrmSubscription` (JSON-CRDT, no `@graph`, nothing for a per-document filter to read) NOR `ngSession` (hands out the SDK handle and the session). MEASURED: `useShape` is still broken live, and the bug leaf's mechanism was FALSE - the deployment is AHEAD of our pinned `@ng-org/*`, its `orm_start_graph` takes `config` BEFORE the callback, so the five-argument client lands its callback in the wrong slot. T01.d's blocker STANDS. Also VERIFIED: ORM writes bypass `sparql_update` entirely, so the emulated write guard never sees an ORM add or delete. - T01.b — deletion is answered at level 3 and fully wired: `DeepSignalSet.delete` -> a remove patch -> `graph_orm_update` -> `SparqlBuilder::remove_object`, which removes the SHAPE's predicates, not the subject; no document deletion exists at any level. `getObjects` returns a DETACHED clone; `insertObject` throws without `@graph`. Proposals: publish `OrmSubscription` as-is (the read filter applies to `signalObject`), publish NEITHER `DiscreteOrmSubscription` (JSON-CRDT, no `@graph`, nothing for a per-document filter to read) NOR `ngSession` (hands out the SDK handle and the session). MEASURED: `useShape` is still broken live, and the bug leaf's mechanism was FALSE - the deployment is AHEAD of our pinned `@ng-org/*`, its `orm_start_graph` takes `config` BEFORE the callback, so the five-argument client lands its callback in the wrong slot. T01.d's blocker STANDS. Also VERIFIED: ORM writes bypass `sparql_update` entirely, so the emulated write guard never sees an ORM add or delete.
- T01.c - `useShapeQuery` takes the SDK's `Scope`, re-exported as a passthrough. Our three literals became `StoreType`, internal only: VERIFIED they are upstream's own `store_type`, so only the NAME was invented. A store is now named by its REFERENCE (`privateStore()` / `protectedStore()` / `publicStore()`), and `createEntityDoc(store?)` takes one, private when omitted, as `doc_create` does. `listMyEntityDocs`, `resolveScopeGraph` and `resolveWriteGraph` unpublished, still internal - T01.d removes the read path's last caller. VERIFIED: public and protected share one store id here, so the reference must be the index document. `1.0.0-dev.5`. - T01.c - `useShapeQuery` takes the SDK's `Scope`, re-exported as a passthrough. Our three literals became `StoreType`, internal only: VERIFIED they are upstream's own `store_type`, so only the NAME was invented. A store is now named by its REFERENCE (`privateStore()` / `protectedStore()` / `publicStore()`), and `createEntityDoc(store?)` takes one, private when omitted, as `doc_create` does. `listMyEntityDocs`, `resolveScopeGraph` and `resolveWriteGraph` unpublished, still internal - T01.d removes the read path's last caller. VERIFIED: public and protected share one store id here, so the reference must be the index document. `1.0.0-dev.5`.
- Package refresh (2026-09-21) - `@ng-org/*` moved to web alpha.14 / orm alpha.21 / shex-orm alpha.9 / alien-deepsignals alpha.13. The live ORM read now STARTS against the deployed broker: `6 passed, 0 failed, 6 declared`, ready in 16ms, delete reached the document. `bug_useshape-live-read-needs-a-newer-broker` DELETED - the deployment was never behind, our packages were. T01.d's blocker LIFTS. Also surfaced: `OrmSubscription` is now `RdfOrmSubscription` (T01.e's proposal names the old one); `Scope.graphs` is REQUIRED and its `[]` means the EMPTY set while `subjects: []` means NO filtering - the two axes read `[]` in opposite directions, which T01.d must not get wrong; six e2e files imported `ng-e2e-helpers` without the polyfill declaring it, fixed as `workspace:*`.
+22 -17
View File
@@ -250,19 +250,26 @@ async function seededTripleCount(doc: Nuri, subject: string): Promise<number> {
/** /**
* CHARACTERISATION of the deployed broker's `orm_start_graph` ARITY. * CHARACTERISATION of the deployed broker's `orm_start_graph` ARITY.
* *
* The failure the runner reports names a parameter — `config` — that the ORM does not * The failure the runner reports named a parameter - `config` - that the client of
* send and that `nextgraph-rs` at the read commit does not declare. Two upstream * the day did not send. Two upstream arrangements could put one there, and they differ
* arrangements could put one there, and they differ by POSITION: * by POSITION:
* - config LAST, after the callback (upstream 2026-04-12 to 2026-04-21) — an * - config LAST, after the callback (upstream 2026-04-12 to 2026-04-21) — an
* unsupplied `config`, never a Function; * unsupplied `config`, never a Function;
* - config at index 4, BEFORE the callback — the 5-argument call from `@ng-org/orm` * - config at index 4, BEFORE the callback: the 5-argument call of the client of
* then lands its callback exactly there, which is what the error describes. * the day then lands its callback exactly there, which is what the error described.
* *
* So this calls the SDK member directly with an empty config in position 4. It does not * THE UPDATE ANSWERED THAT QUESTION. `@ng-org/web@0.1.2-alpha.14` declares six
* go through the ORM, which is fixed at five arguments — this is the one question the * arguments with `config` at index 4 and routes the stream callback from index 5
* ORM cannot ask. The app-side proxy will mistake the `{}` for the stream callback, so * (`dist/ngweb.js`: `orm_start_graph: 5`), and `@ng-org/orm@0.1.2-alpha.21` sends six
* nothing usable comes back; what is being read is only whether the engine still * to match (`dist/connector/RdfOrmSubscription.js:159`). So the second arrangement was
* refuses on `config`. * the right reading, and the ordinary `liveStart` path now exercises it.
*
* This is therefore kept as a FALLBACK, not as the live question: the runner only asks
* it when `liveStart` did not become ready, to say whether the engine still refuses on
* `config` when the client no longer can be blamed. It calls the SDK member directly
* rather than through the ORM. The app-side proxy takes index 5 as the stream callback,
* so the `{}` at index 4 is read as a config and nothing usable comes back; what is
* being read is only whether the start is refused.
*/ */
async liveStartWithConfig() { async liveStartWithConfig() {
const s = await sessionReady; const s = await sessionReady;
@@ -273,14 +280,12 @@ async function seededTripleCount(doc: Nuri, subject: string): Promise<number> {
"store", "store",
undefined, undefined,
)) as Nuri; )) as Nuri;
// Six arguments against a declaration that has five: the declaration describes the // Called against the PUBLISHED declaration, which now has these six arguments, so the
// arity of the PUBLISHED client, and the arity of the DEPLOYED engine is the question. // widening this used to need is gone: the arity is checked here rather than asserted.
// Widened through `unknown`, never `any` — the call is deliberate, not unchecked. // What remains unknowable at compile time is the DEPLOYED engine's arity, which is
const startGraph = realNg.orm_start_graph as unknown as ( // the question this asks.
...a: unknown[]
) => Promise<unknown>;
try { try {
await startGraph([doc], [], LIVE_SHAPE, s.session_id, {}, () => {}); await realNg.orm_start_graph([doc], [], LIVE_SHAPE, s.session_id, {}, () => {});
return { doc, error: null }; return { doc, error: null };
} catch (e) { } catch (e) {
return { doc, error: e instanceof Error ? e.message : String(e) }; return { doc, error: e instanceof Error ? e.message : String(e) };
+7 -5
View File
@@ -261,9 +261,11 @@ async function main(): Promise<void> {
} }
check(CHECKS[4]!, remaining === 0, `triples left for the seeded subject: ${remaining}`); check(CHECKS[4]!, remaining === 0, `triples left for the seeded subject: ${remaining}`);
// Only worth asking when the five-argument call did NOT start: it is the diagnosis of // Only worth asking when the published call did NOT start: it is the diagnosis of
// that failure, and a deployment that answers the published arity has nothing to // that failure, and a deployment that answers the published arity has nothing to
// characterise. // characterise. Since `@ng-org/web@0.1.2-alpha.14` the published call is itself the
// six-argument one, so this asks whether the engine refuses a start the client got
// right - no longer whether the client was sending the wrong number.
if (readyMs === null) { if (readyMs === null) {
const withConfig = await sdk<{ doc: string; error: string | null }>( const withConfig = await sdk<{ doc: string; error: string | null }>(
frame, frame,
@@ -273,12 +275,12 @@ async function main(): Promise<void> {
CHECKS[5]!, CHECKS[5]!,
withConfig.error === null withConfig.error === null
? "six arguments with `{}` in position 4 are ACCEPTED — the deployed " + ? "six arguments with `{}` in position 4 are ACCEPTED — the deployed " +
"`orm_start_graph` takes a `config` BEFORE the callback, so the published " + "`orm_start_graph` takes a `config` BEFORE the callback, which is what the " +
"client's five-argument call puts its callback there" "published client now sends, so the refusal is not an arity mismatch"
: `six arguments were refused too: ${withConfig.error}`, : `six arguments were refused too: ${withConfig.error}`,
); );
} else { } else {
record(CHECKS[5]!, "not asked — the published five-argument call started"); record(CHECKS[5]!, "not asked — the published six-argument call started");
} }
} finally { } finally {
if (page) await closeQuietly("the ORM live-read page", () => page!.close()); if (page) await closeQuietly("the ORM live-read page", () => page!.close());
+1 -4
View File
@@ -2,10 +2,7 @@
"extends": "../../../tsconfig.base.json", "extends": "../../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"types": ["bun"], "types": ["bun"],
"noEmit": true, "noEmit": true
"paths": {
"@ng-org/web": ["../node_modules/@ng-org/web"]
}
}, },
"include": ["."] "include": ["."]
} }
+2 -9
View File
@@ -21,8 +21,8 @@ importers:
packages/ng-e2e-helpers: packages/ng-e2e-helpers:
devDependencies: devDependencies:
'@ng-org/web': '@ng-org/web':
specifier: 0.1.2-alpha.13 specifier: 0.1.2-alpha.14
version: 0.1.2-alpha.13 version: 0.1.2-alpha.14
playwright: playwright:
specifier: ^1.61.1 specifier: ^1.61.1
version: 1.61.1 version: 1.61.1
@@ -87,9 +87,6 @@ packages:
peerDependencies: peerDependencies:
typescript: ^4.9.0 || ^5.0.0 || ^6.0.0 typescript: ^4.9.0 || ^5.0.0 || ^6.0.0
'@ng-org/web@0.1.2-alpha.13':
resolution: {integrity: sha512-/xO0c+3NTphnws5Do2LDqgZWmAf+aNnYdChJKdU0dnp1U1iVSgi/y3yb8AYryf0v9sooj0aYJxt08B6DpirFMQ==}
'@ng-org/web@0.1.2-alpha.14': '@ng-org/web@0.1.2-alpha.14':
resolution: {integrity: sha512-FPQ7kyngMhAXK/WJtZ6pkoEJwGcau6GaBH2nhjjYorMq7OG//I+zwghat90FsIeMrfYsps0+5Y0UrRqfxNdUXA==} resolution: {integrity: sha512-FPQ7kyngMhAXK/WJtZ6pkoEJwGcau6GaBH2nhjjYorMq7OG//I+zwghat90FsIeMrfYsps0+5Y0UrRqfxNdUXA==}
@@ -514,10 +511,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
'@ng-org/web@0.1.2-alpha.13':
dependencies:
async-proxy: 0.4.1
'@ng-org/web@0.1.2-alpha.14': '@ng-org/web@0.1.2-alpha.14':
dependencies: dependencies:
async-proxy: 0.4.1 async-proxy: 0.4.1