fix: quatre écarts entre la surface publiée et ce que NextGraph déclare

Un audit de la surface contre la source amont en a trouvé cinq ; voici les
quatre mécaniques. La cinquième — l'adresse d'inbox, qui traverse sept symboles
— relève du dessin et reste ouverte.

L'identifiant de session bloquait. Amont le déclare string | number
(sdk/js/web/src/index.ts:16) et le binding désérialise un u64 ; nous exigions
une chaîne. Une application ne pouvait donc pas passer la valeur que le SDK
venait de lui remettre. Élargi à ce qu'amont déclare, sur toute la chaîne, et
jamais converti : une chaîne échoue pour de vrai (Deserialization error of
session_id JsValue("1"), observé).

sparqlUpdate annonçait Promise<void> alors qu'il relayait DÉJÀ les commits.
C'était donc un mensonge de typage, pas un comportement — et la doublure de test
qui rendait undefined, un état que le vrai broker ne produit jamais, est ce qui
l'a laissé sans contradicteur.

ng était publié en Record<string, any>, ce qui perdait les 88 membres typés
d'amont — 88, pas 77 : le chiffre de notre propre documentation était faux.

Et materialize, second nom publié de read, sans appelant ni contrepartie amont,
est retiré.

docs/api-contract.md qualifiait docs.* de passthrough « 1:1 ». C'était faux sur
les deux premiers points. Corrigé, pas complété : un document qui se déclare
vérifié et qui ment est pire qu'un document absent, parce qu'on cesse d'aller
voir.

Une déviation assumée : amont type le retour en any, interdit ici ; on rend
unknown, comme sparqlQuery le fait déjà pour le même any amont.
This commit is contained in:
Sylvain Duchesne
2026-08-14 10:00:40 +02:00
parent e32b6d04fc
commit 12eba6eea6
17 changed files with 227 additions and 99 deletions
@@ -250,7 +250,8 @@ export function accountKey(id: string): string {
/** Minimal session shape the registry needs — provided by the consumer. */
export interface RegistrySession {
sessionId: string;
/** Relayed untouched to `ng` — upstream declares it `string | number` (`index.d.ts:266`). */
sessionId: string | number;
/** The shared wallet's private store id — the pointer anchor. */
privateStoreId: string;
/** The shared wallet's protected store id (native store). Optional: only the
@@ -55,7 +55,7 @@ import type { Nuri } from "../model/types";
* afterwards, which the caller decides by filing the cap among the caps that holder holds.
*/
export async function physicalCreate(
sessionId: string,
sessionId: string | number,
crdt = "Graph",
cls = "data:graph",
dest = "store",
@@ -81,7 +81,7 @@ export async function physicalCreate(
* read a virtual user's content — that is `docs.sparqlQuery`, which is confined.
*/
export async function physicalQuery(
sessionId: string,
sessionId: string | number,
query: string,
base: string | undefined,
anchor: Nuri,
@@ -95,7 +95,7 @@ export async function physicalQuery(
/** Write as the PHYSICAL user — the shim's own records. See {@link physicalQuery}. */
export async function physicalUpdate(
sessionId: string,
sessionId: string | number,
query: string,
anchor: Nuri,
label = "physicalUpdate",
@@ -95,15 +95,15 @@ export function sessionIsComing(): boolean {
*
* ── The session id is RELAYED, never rebuilt — and that is load-bearing ────
* Upstream declares it `string | number` (`Session`, `index.d.ts:266`) and the broker
* returns a NUMBER; the whole chain below here types it `string` and hands it to `ng.*`,
* returns a NUMBER; the chain below here now types it the same way and hands it to `ng.*`,
* whose binding takes it as-is. So this reads the field and passes it on untouched. It is
* not a detail: normalizing it to a string was written here first, and the applicative e2e
* refused every call in the batch with `Deserialization error of session_id JsValue("1")`
* — the wasm side deserializes the id by its own type, and a stringified number is not it.
*
* The `string` in the declared shape is therefore inherited, not asserted: the inaccuracy
* is the chain's and predates this module (every consumer's thunk declared it the same way
* and relayed the same value). Widening it belongs to the chain, not to the capture.
* The chain used to narrow it to `string`, which made the value the SDK hands an application
* unpassable through this library. Widened to upstream's own declared type on 2026-08-14, at
* the capture and along every hop that relays it; the id is still only ever RELAYED.
*/
export function captureSession(event: unknown): boolean {
if (typeof event !== "object" || event === null) return false;
@@ -116,7 +116,7 @@ export function captureSession(event: unknown): boolean {
protected_store_id: protectedStoreId,
public_store_id: publicStoreId,
} = session as {
session_id?: string;
session_id?: string | number;
private_store_id?: string;
protected_store_id?: string;
public_store_id?: string;