diff --git a/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md b/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md index d8759eb..c312710 100644 --- a/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md +++ b/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md @@ -133,7 +133,7 @@ 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` until 2026-08-14 export function init(...args: any[]): any; // likewise — not the `init` passed to `configure` export function initNg(...args: any[]): any; @@ -229,7 +229,7 @@ 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. -**The wrapped `ng` is a wrapper, and a wrapper can be wrong.** It forwards all 88 members 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 ` is not a function`. 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. +**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 ` is not a function`. 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` 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. 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. diff --git a/docs/api-contract.md b/docs/api-contract.md index 6611a61..fd48add 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -121,7 +121,7 @@ export declare type Session = { }; ``` -`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. +`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 every member 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. @@ -173,11 +173,11 @@ export type { NG } from "@ng-org/web"; 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 ` 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. +**The passthrough itself was broken until `1.0.0-dev.7`, for every one of its 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 ` 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 -**PASSTHROUGH (level 2, VERIFIED).** `export declare const ng: NG` with `NG = typeof NGModule`, **88** exported members (`index.d.ts:136-231`; the namespace body is `:140-231`). The surface is identical by construction — the proxy adds no member and removes none — **and identical at the type level too since 2026-08-14**, when the published `ng` stopped being declared `Record` and took `NG`, upstream's own type. That escape hatch silently dropped all 88 signatures: an application got no completion, and a misspelt member typechecked. +**PASSTHROUGH (level 2, VERIFIED).** `export declare const ng: NG` with `NG = typeof NGModule`, **90** exported members in `@ng-org/web@0.1.2-alpha.14` - 84 functions and 6 RDF classes, counted in the `NGModule` namespace of the installed `index.d.ts`. The count is named with its release because it MOVES with one: this document recorded **88**, two short, and two of the ninety - `graph_orm_next_page` and `graph_orm_previous_page` - appear nowhere in the cited checkout's `sdk/js/lib-wasm/src/lib.rs`, the tree declaring the previous release `@ng-org/web@0.1.2-alpha.13`. The figure was never re-measured after the build moved. The surface is identical by construction — the proxy adds no member and removes none — **and identical at the type level too since 2026-08-14**, when the published `ng` stopped being declared `Record` and took `NG`, upstream's own type. That escape hatch silently dropped every signature: an application got no completion, and a misspelt member typechecked. The overrides: diff --git a/docs/internal-contract.md b/docs/internal-contract.md index 85fd399..9817fff 100644 --- a/docs/internal-contract.md +++ b/docs/internal-contract.md @@ -23,7 +23,7 @@ Builds the `ng` Proxy: forwards every property to the injected real `ng`, overri - `wrappedNg()` exists because the proxy now has **two** holders: `index.ts` publishes it, and `surface/lifecycle.ts` hands it to the ORM through `initNg`, so the two must be the same object. It keeps no state (every property is answered from `getConfig()` at access time), so it survives a re-`configure()` and needs no reset. - The `sparql_update` guard stands in for the engine's write-permission model (`verify_perm` inside `Commit::verify`, `engine/repo/src/commit.rs:892-899`) - same analysis as `docs/api-contract.md` § 3. It is deliberately left on the decorative write-cap list rather than on `assertMayWrite`: the library's own REGISTERS are written through this member (`emulated-verifier/register-write.ts`), and a store document is not owned in the authorship sense, so arming it there would refuse every register write. `graph_orm_update` has no such traffic, which is why the live predicate sits on it. - ~~**Defect - the `login` arm fabricates a member (Findings F1).**~~ **FIXED 2026-08-03**, and this bullet had outlived it: `@ng-org/web` has no `login` - none among the exports of `index.d.ts`, no `fn login` in `sdk/js/lib-wasm/src/lib.rs` - and the arm that returned a function for `prop === "login"` was the one place the proxy added a member, against its own header and the surface contract's "adds no member and removes none" (§ 3). It is gone from `ng-proxy.ts`; the note stays as the record of what the proxy must never do again. -- **Defect - the passthrough could not survive the real `ng` (FIXED `1.0.0-dev.7`).** It was `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 path-carrying callable returning a Promise. So the published `ng` handed back a promise for all 88 members and a caller got ` is not a function`. Invisible because the library's own internals bypass this proxy and every fake in the suite is a plain object. Now `Reflect.apply(real, ng, args)`; the stand-in in `test/ng-proxy.test.ts` is shaped like the real proxy, which is what makes the test one. +- **Defect - the passthrough could not survive the real `ng` (FIXED `1.0.0-dev.7`).** It was `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 path-carrying callable returning a Promise. So the published `ng` handed back a promise for every member and a caller got ` is not a function`. Invisible because the library's own internals bypass this proxy and every fake in the suite is a plain object. Now `Reflect.apply(real, ng, args)`; the stand-in in `test/ng-proxy.test.ts` is shaped like the real proxy, which is what makes the test one. - Disappears at migration (the whole module). ## 2. NURI internals — the unexported slice of `nuri.ts`, plus the minting point diff --git a/packages/polyfill/src/index.ts b/packages/polyfill/src/index.ts index 94c693b..c427f3a 100644 --- a/packages/polyfill/src/index.ts +++ b/packages/polyfill/src/index.ts @@ -144,7 +144,7 @@ export * as storeRegistry from "./surface/placement"; // @ng-org. `export type` is ERASED at build, so this adds NO runtime @ng-org import to // the lib (no risk of a duplicate SDK copy in the bundle). // @provenance ShapeType kind=passthrough level=3 ref=sdk/js/shex-orm/src/types.ts:ShapeType -// @provenance BaseType kind=passthrough level=3 ref=sdk/js/shex-orm/src/types.ts:BaseType +// @provenance BaseType kind=passthrough level=3 ref=sdk/js/shex-orm/src/types.ts:BaseType - the cited revision keeps `"@graph"` commented out behind a TODO; the published `@ng-org/shex-orm@0.1.2-alpha.9` declares it, and it is the published one that is re-exported here // @provenance Schema kind=passthrough level=3 ref=sdk/js/shex-orm/src/types.ts:Schema export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm"; // @provenance DeepSignalSet kind=passthrough level=3 ref=sdk/js/alien-deepsignals/src/types.ts:DeepSignalSet @@ -152,7 +152,7 @@ export type { DeepSignalSet } from "@ng-org/alien-deepsignals"; // The scope `useShapeQuery` takes, re-exported from the SDK rather than restated here: // restating it is how a type drifts, and the two axes it filters on (`graphs`, // `subjects`) are upstream's to change. -// @provenance Scope kind=passthrough level=3 ref=sdk/js/orm/src/types.ts:Scope +// @provenance Scope kind=passthrough level=3 ref=sdk/js/orm/src/types.ts:Scope - the cited revision declares `graphs?` OPTIONAL; the published `@ng-org/orm@0.1.2-alpha.21` makes it required, and it is the published one that is re-exported here export type { Scope } from "@ng-org/orm"; // What `getObjects` takes: a `Scope` plus `where` and `orderBy`. It is re-exported from // the SDK's `/all` subpath because that is where the SDK puts it - the type the function @@ -160,7 +160,7 @@ export type { Scope } from "@ng-org/orm"; // choice here. Restating it would be how it drifts. // @provenance RdfOrmConfig kind=passthrough level=3 ref=sdk/js/orm/src/utilTypes.ts:OrmConfig - the published `@ng-org/orm@0.1.2-alpha.21` renames it `RdfOrmConfig` and re-parameterises it on the shape's type rather than its `ShapeType`, which the cited revision has not yet done export type { RdfOrmConfig } from "@ng-org/orm/all"; -// @provenance NG kind=passthrough level=2 ref=sdk/js/web/src/index.ts:NG — 88 members, all forwarded; the proxy adds none and removes none +// @provenance NG kind=passthrough level=2 ref=sdk/js/web/src/index.ts:NG — every member forwarded; the proxy adds none and removes none. The count lives once, with the release it belongs to, in `surface/ng-proxy.ts` export type { NG } from "@ng-org/web"; // ── POLYFILL-ERA — THE DELETION LIST ──────────────────────────────────────── @@ -226,7 +226,7 @@ import { wrappedNg } from "./surface/ng-proxy"; * * Declared `NG` — upstream's own type for its `ng` (`index.d.ts:NG`). It was published as * `Record` until 2026-08-14, which announced a different surface from the one - * it forwards to: an application got no completion and no check on any of the 88 members. + * it forwards to: an application got no completion and no check on any of its members. */ // @provenance ng kind=passthrough level=2 ref=sdk/js/web/src/index.ts:ng — a Proxy over the injected real `ng`; only `sparql_update` is overridden (the emulated write guard) export const ng: NG = wrappedNg(); diff --git a/packages/polyfill/src/surface/lifecycle.ts b/packages/polyfill/src/surface/lifecycle.ts index 10ba798..98a6c12 100644 --- a/packages/polyfill/src/surface/lifecycle.ts +++ b/packages/polyfill/src/surface/lifecycle.ts @@ -109,7 +109,7 @@ export function init(...args: any[]): any { * prose, unenforceable and silent when missed - and it WAS missed, by both consumers in * this repository, which is how the hole was found. * - * Nothing is taken from the caller: `wrappedNg()` forwards every one of the 88 members to + * Nothing is taken from the caller: `wrappedNg()` forwards every one of its members to * the very object `configure()` received, and it is the same object `ng` publishes, so the * ORM now reaches the engine through exactly what the application does. The remaining * arguments and the return value pass straight through. diff --git a/packages/polyfill/src/surface/ng-proxy.ts b/packages/polyfill/src/surface/ng-proxy.ts index 37c469e..771505c 100644 --- a/packages/polyfill/src/surface/ng-proxy.ts +++ b/packages/polyfill/src/surface/ng-proxy.ts @@ -39,10 +39,18 @@ function graphsWrittenBy(patches: unknown): Nuri[] { } /** - * Typed `NG` — upstream's own type for `ng` (`NG = typeof NGModule`, 88 exported members, - * installed `index.d.ts:136-231`). The proxy adds no member and removes none, so this is - * the honest declaration; it used to be `Record`, which silently dropped all - * 88 signatures and let any misspelt member typecheck. + * Typed `NG` - upstream's own type for `ng`. `NG = typeof NGModule`, the `NGModule` + * namespace of the installed `@ng-org/web`, which at `0.1.2-alpha.14` declares 90 members: + * 84 functions and 6 RDF classes. The proxy adds no member and removes none, so this is + * the honest declaration; it used to be `Record`, which silently dropped + * every signature and let any misspelt member typecheck. + * + * The count is named with its RELEASE because it moves with one, and this one had stopped + * matching: the figure recorded here was two short, and two of the ninety - + * `graph_orm_next_page` and `graph_orm_previous_page` - appear nowhere in the cited + * checkout's `sdk/js/lib-wasm/src/lib.rs`, the tree that declares the PREVIOUS release + * `@ng-org/web@0.1.2-alpha.13`. A count with no release beside it is a claim nobody can + * re-check against anything, so it drifts in silence - which is what happened. * * The empty object is only the Proxy's TARGET: every property is answered by the `get` * trap below, never by the target, so there is nothing to put in it. Typing the target is @@ -144,7 +152,7 @@ export function makeNg(): NG { // callable for the path `.bind`, calling it posts a remote request named // after the MEMBER with the bind arguments, and what comes back is a PROMISE. // - // The published `ng` therefore answered all 88 members with a promise instead of a + // The published `ng` therefore answered every member with a promise instead of a // function, and a caller got the SDK's own ` is not a function`. What made // it invisible for months is that this library's internals never go through this // proxy - they call the injected `ng` directly - and every fake in the suite is a diff --git a/packages/polyfill/test/orm.test.ts b/packages/polyfill/test/orm.test.ts index 6256066..da043c8 100644 --- a/packages/polyfill/test/orm.test.ts +++ b/packages/polyfill/test/orm.test.ts @@ -406,7 +406,7 @@ test("initNg hands the ORM this package's wrapped ng, not the argument", async ( expect(seen[0]).not.toBe(raw); // ...and what it IS forwards to the object `configure()` received, so nothing is taken - // from the caller: the 88 members answer exactly as the real `ng`'s do. + // from the caller: its members answer exactly as the real `ng`'s do. const handed = seen[0] as Record; expect(typeof handed["graph_orm_update"]).toBe("function"); expect(handed["doc_create"]).toBeDefined();