docs: le contrat passe en 1.0.0-dev.9, et ce qu'une lecture echouee repond est consigne

Majeur : trois appels existants se resserrent. `useShapeQuery` publie `isError`
pour un document NOMME qu'il ne peut pas lire, la ou il rendait `isSuccess` avec
`data: []` - la seule image que cette surface reserve a « ce scope ne contient
rien ». `inbox.postToDocument`, `readForDocument` et `readSyncedForDocument`
rejettent la ou elles annoncaient « pas d'inbox ». Rien de ce qui etait deja une
reponse ne bouge : un document vide, `graphs: []`, un document sans inbox, un
document public qui n'expose aucune cle.

Deux non-garanties corrigees parce que le changement les rend fausses : « aucune
des deux lectures par forme ne va chercher la cle d'un store public » ne vaut
plus que pour `useShape` et pour `did:ng:i`, et le pending eternel d'un demarrage
refuse se referme sur le cas nomme.

Nouveau `knowledge_what-a-failed-read-answers-upstream` : ce que la cible repond
pour un document injoignable, non synchronise ou vide, et les deux collisions que
l'appelant ne peut pas demeler - « n'existe pas » contre « pas connu de moi »
d'un cote, « non synchronise » contre « vraiment vide » de l'autre. C'est le fait
durable qui justifie la direction prise, et il vaut quel que soit le remede.

`docs/internal-contract.md`, section open-repo seulement : numeros de ligne,
`ensurePhysicalRepoOpen` qui n'a jamais ete un export de ce module,
`ensureReposOpen` supprime, `openRepoUnguarded` absent, et le second parametre de
`ensureRepoOpen`. Le reste du document porte une derive du meme age, laissee.
This commit is contained in:
Sylvain Duchesne
2026-09-21 15:09:50 +02:00
parent 75313f75a2
commit b0eec7b4dc
11 changed files with 108 additions and 38 deletions
+1 -1
View File
@@ -259,7 +259,7 @@ export function useShapeQuery<T extends BaseType = BaseType>(
**What it reads THROUGH, since `1.0.0-dev.6`: the SDK's own shape query.** `RdfOrmSubscription.getOrCreate(shapeType, scope)`, injected at `configure` as `ormSubscription` (§ 1). Four steps went with the change and they were one mechanism: resolving the logical scope to a document set (`listMyEntityDocs`), opening those documents through `ensureReposOpen` for a sync barrier, an anchored read over each, and filtering the result by the shape's `@type` IRI. All four existed because the parameter had to be TRANSLATED; once it is upstream's own `Scope`, the question goes to the thing that answers it, and `graphs` / `subjects` / `did:ng:i` are read by the engine literally. It does NOT delegate to the SDK's `useShape` — that is React's hook and its `DeepSignalSet` cannot carry the request state, which is the whole reason this symbol exists. What is taken from the SDK is the QUERY; the observable, the snapshot and the state tracking stay here, and so does the capability filter (`emulated-verifier/read-filter.ts`, applied per `@graph` on what comes back, exactly where § 4 applies it).
**Consequences a caller sees.** `data` items are now the SHAPE's own objects — `@id`, `@graph` and the shape's readable predicates — where they were the property bags the then-published `readUnion` returned; `shapeType` is typed `ShapeType<T>` instead of `unknown`, so `T` follows the shape and the snapshot is typed without a cast; and the read no longer opens a document of its own, so a public-store document whose cap has not been fetched by some other read does not appear until it has been (the same limit § 4 carries, and the same `CapRegistry.onChange` recovery).
**Consequences a caller sees.** `data` items are now the SHAPE's own objects — `@id`, `@graph` and the shape's readable predicates — where they were the property bags the then-published `readUnion` returned; `shapeType` is typed `ShapeType<T>` instead of `unknown`, so `T` follows the shape and the snapshot is typed without a cast; and the read no longer opens a document of its own, so a public-store document whose cap has not been fetched by some other read does not appear until it has been (the same limit § 4 carries, and the same `CapRegistry.onChange` recovery). *(Corrected 2026-09-21, `1.0.0-dev.9`: that last clause now holds only for `graphs: "did:ng:i"`. A document NAMED in `scope.graphs` is put through the same public-store fetch and sync barrier `docs.sparqlQuery` applies to its anchor, before the query opens - and is refused out loud rather than silently emptied when its cap cannot be obtained.)*
**The name and the scope, since `1.0.0-dev.5`.** It was `watchShape(shapeType, "public" | "protected" | "private")`. `watchShape` has ZERO occurrences in `nextgraph-rs` and spends `watch`, a verb the ORM already spends on signal observation; the target's own name is `useShapeQuery`, announced by NextGraph's developers and on record in `.project/rules/nextgraph/surface-mirrors-the-sdk.md`, which is what the symbol's `announced-not-written` annotation cites. The three literals were this package's own and named the scope of a READ, which the SDK answers on two axes instead: `graphs` (one document's graph per entry, `did:ng:i` or `""` for the whole dataset, `[]` or `undefined` for no objects at all) and `subjects`. Nothing is translated any more: the `Scope` object a caller writes is the object the SDK receives.
+17 -11
View File
@@ -95,27 +95,33 @@ export function subscribePhysicalDoc(nuri: Nuri, onChange: (r: DocChange, type:
## 6. Bootstrap repo opening — `emulated-verifier/open-repo.ts`
```ts
// open-repo.ts:75
// open-repo.ts:90
export type SyncState = "syncing" | "synced" | "timed-out";
// open-repo.ts:104 — TEST-ONLY
// open-repo.ts:120 - TEST-ONLY
export function setOpenTimeoutForTests(ms: number): void;
// open-repo.ts:110
// open-repo.ts:127
export function resetOpenedRepos(): void;
// open-repo.ts:135
// open-repo.ts:162
export function getSyncState(nuri: Nuri): SyncState | "unknown";
// open-repo.ts:167
export async function ensureRepoOpen(nuri: Nuri): Promise<void>;
// open-repo.ts:184
export async function ensurePhysicalRepoOpen(nuri: Nuri): Promise<void>;
// open-repo.ts:259
export async function ensureReposOpen(nuris: Nuri[]): Promise<void>;
// open-repo.ts:210
export async function ensureRepoOpen(nuri: Nuri, holderKey?: string): Promise<void>;
// open-repo.ts:234 - the unguarded core, for ONE importer (`shared-wallet/physical.ts`)
export async function openRepoUnguarded(nuri: Nuri): Promise<void>;
```
> Corrected 2026-09-21 (`1.0.0-dev.9`), for this module only. The listing above used to name
> an `ensurePhysicalRepoOpen` export that this module has never had - it lives in
> `shared-wallet/physical.ts:134`, the machinery's door - and an `ensureReposOpen(nuris)`
> batch that was deleted as dead code. It also missed `openRepoUnguarded` and
> `ensureRepoOpen`'s second parameter. The rest of this document carries drift of the same
> age and was deliberately left alone.
Heals the cold-start defect of the anchored read path: on a fresh session a not-yet-open repo reads empty, so before an anchored read the repo is opened by subscribing (`subscribePhysicalDoc`) and awaiting the first `State` push — the sync barrier — with a bounded timeout. The subscription is held for the session; per-nuri `SyncState` keeps `synced` and `timed-out` apart.
- The opening mechanism is **level 2, VERIFIED as a composition**: `doc_subscribe` exists (`sdk/js/lib-wasm/src/lib.rs:1908`), and the push variants `TabInfo`/`State`/`Patch` are the engine's `AppResponseV0` (`engine/net/src/app_protocol.rs:1354-1358`). The ORDER (TabInfo first, then the initial State) and "first State = presence guaranteed, absence definitive" are **empirical, pinned by the in-repo e2e CONTRACT-3 probe — an ASSUMPTION about ordering as far as upstream is concerned**: no upstream statement fixes the push order, so a future reordering upstream would silently break the barrier. Bound: the e2e probe fails loudly if the order changes.
- "Hold a live subscription to keep the repo open" — **ASSUMPTION** (nothing upstream documents subscription lifetime as what retains a repo in `self.repos`); observed to work, bounded by the same probe.
- **Defect — the header's mechanism claim is contradicted at the source (see Findings F2).** `open-repo.ts:10-12` says an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Verified upstream: absence from `self.repos` yields `Err(NgError::RepoNotFound)` (`request_processor.rs:264,269`), the ReadQuery arm converts it into `AppResponse::error` (`:1293-1296`), and the web binding REJECTS the JS promise with it (`sdk/js/lib-wasm/src/lib.rs:606`). The observed 0-rows-no-error behaviour has two candidate explanations that the source does support: a persistent verifier reloads every known repo into `self.repos` at `Verifier::load` (`engine/verifier/src/verifier.rs:535-560`) so the repo is present-but-unsynced (a genuine 0-row read), and/or the lib's own per-doc tolerance (`readUserStore`, and the `surface/read-model.ts` per-doc catch until `1.0.0-dev.8` removed it) converts a rejection into an empty result. The healed symptom is real and the fix correct; the stated mechanism is not established, and a maintainer reasoning from it would mispredict behaviour whenever the verifier is not persistent.
- **`ensureRepoOpen` THROWS when the public store could not be asked** (`1.0.0-dev.9`). It calls `emulated-verifier/public-store.ts`'s `fetchReadCap` first - a document in a public store hands its key to whoever asks, so the question has to be put before the boundary decides - and that call used to answer `false` for a download that had REJECTED, which is the same value a document exposing no key answers with. The open then declined silently and the anchored read that followed refused the reader for holding no key: a true statement, made out of an outage, and the wrong one to act on. The rejection now reaches the caller. The open itself still never throws (a `doc_subscribe` that never pushes is bounded by the fallback timeout), and `holderKey` skips the fetch entirely, so the processor's door is unaffected.
- **Defect, SINCE REPAIRED in the code — the header's mechanism claim was contradicted at the source (see Findings F2).** The header was corrected on 2026-08-03 and now states the mechanism below itself; what follows is kept as the record of what was wrong and how it was established. `open-repo.ts:10-12` says an anchored `sparql_query` on a repo absent from `self.repos` "silently returns 0 rows (never a `RepoNotFound`)". Verified upstream: absence from `self.repos` yields `Err(NgError::RepoNotFound)` (`request_processor.rs:264,269`), the ReadQuery arm converts it into `AppResponse::error` (`:1293-1296`), and the web binding REJECTS the JS promise with it (`sdk/js/lib-wasm/src/lib.rs:606`). The observed 0-rows-no-error behaviour has two candidate explanations that the source does support: a persistent verifier reloads every known repo into `self.repos` at `Verifier::load` (`engine/verifier/src/verifier.rs:535-560`) so the repo is present-but-unsynced (a genuine 0-row read), and/or the lib's own per-doc tolerance (`readUserStore`, and the `surface/read-model.ts` per-doc catch until `1.0.0-dev.8` removed it) converts a rejection into an empty result. The healed symptom is real and the fix correct; the stated mechanism is not established, and a maintainer reasoning from it would mispredict behaviour whenever the verifier is not persistent.
- `SyncState` and `getSyncState` are lib-invented vocabulary — **NO COUNTERPART** (upstream has no consumer-facing "sync state of a repo" API at any level; `OrmSubscription.readyPromise`, `sdk/js/orm/src/connector/GraphOrmSubscription.ts:260`, is the closest level-3 signal, per-subscription not per-repo).
- At migration the whole module becomes "open the store by cap at bootstrap" (native) and is removed with the shim.
+1 -1
View File
@@ -364,7 +364,7 @@ The consequence for anything this library exposes: **a call either hands over th
**And the property that makes the first act worth anything — a public store SERVES its documents' caps.** `PublicRepoLinkV0` (`engine/net/src/types.rs:5098-5124`) carries `repo`, `public_store` and `peers` and no `read_cap`, and its own comment says why: *"The latest ReadCap of the branch (or main branch) will be **downloaded from the outerOverlay**, if the peer brokers listed below allow it. […] This link is durable, because the public site are **served differently by brokers**."* The key is not something a sender hands over; it is something the network gives to whoever asks, because the broker pinned the outer overlay (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`).
That is emulated, since 2026-08-06, in `emulated-verifier/public-store.ts` — and emulated **without touching the guard**. Possession remains the one criterion: a public document is readable not because `mayReach` makes an exception, but because its cap is *obtainable* — the library asks, files what it gets, and from there the ordinary path applies. Every read door asks first (`docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`).
That is emulated, since 2026-08-06, in `emulated-verifier/public-store.ts` — and emulated **without touching the guard**. Possession remains the one criterion: a public document is readable not because `mayReach` makes an exception, but because its cap is *obtainable* — the library asks, files what it gets, and from there the ordinary path applies. Every read door asks first (`docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`, and since `1.0.0-dev.9` `useShapeQuery` for every document its scope NAMES). A door that could not ASK says so: the fetch rejects rather than answering "this document exposes no cap", because a reader told the second would stop looking.
Where the emulation shows its seams, stated rather than hidden: