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.
5.4 KiB
type, summary, last_checked
| type | summary | last_checked |
|---|---|---|
| knowledge | What the target answers for an unreachable, an unsynced and an empty document - a read rejects or it does not, so "not known to me" and "does not exist" are one answer while "unsynced" and "genuinely empty" are another | 2026-09-21 |
What a failed read answers upstream, and what a caller can tell apart
The question a caller actually asks: when a read comes back with nothing, what can I conclude? This leaf records the target's answer, because every version of the failure-as-absence defect in this package has come from getting it wrong in one direction or the other - either swallowing a breakdown into an empty result, or turning a real emptiness into an error.
The engine's observable: a read REJECTS, or it does not
An anchored SPARQL query resolves its target through resolve_target_for_sparql, whose first act is self.repos.get(repo_id).ok_or(NgError::RepoNotFound)? (engine/verifier/src/request_processor.rs:269, at 213338f6). A repo the verifier does not hold is an error, not an empty result. It crosses into JavaScript as a rejected promise carrying the string RepoNotFound (sdk/js/lib-wasm/src/lib.rs:403 for nodejs, :606 for the browser).
So the target offers exactly two observable outcomes for a read, and no third: it rejects, or it resolves with rows - possibly zero of them. There is no "partially answered", no "could not find out but here is what I have", and no status object. Any richer vocabulary in this package would be something a caller learns here and unlearns at migration.
The three states a caller wants to distinguish, and the two it gets
| the document is... | what the engine does | what a caller can conclude |
|---|---|---|
not in self.repos - never existed, or exists but this session does not know it |
REJECTS with RepoNotFound |
"I could not read it." Not why. |
in self.repos but unsynced - a persistent profile reloads every known repo at Verifier::load (engine/verifier/src/verifier.rs:535-560), so a fresh session over the same wallet has the repo without its remote commits |
resolves, zero rows | nothing distinguishes this from the row below |
in self.repos, synced, and genuinely empty |
resolves, zero rows | nothing distinguishes this from the row above |
Two collapses, and they run in opposite directions. Above the line, "does not exist" and "exists but I do not know it" are indistinguishable - the partition the engine exposes is membership of self.repos, and nothing finer. Below it, "known but unsynced" collides with "genuinely empty" - both are a legitimate zero-row answer, and the error channel is not involved at all.
That second collapse is the one a caller gets bitten by, because zero rows looks like an answer. The only thing that separates the two is a sync barrier: doc_subscribe pushes TabInfo and then the first State, and past that first State presence is guaranteed and absence definitive (pinned empirically by CONTRACT 3 in packages/polyfill/e2e/). Crossing it is what turns "zero rows" into a statement.
There is no capability check on the read path at all
resolve_target_for_sparql reads nuri.target and nothing else. Upstream a document you cannot read is one whose ReadCap does not open it - the refusal is cryptographic and happens further down, never as a "may I?" question answered on the way in. So a read never fails for want of a right granted to you; it fails because the repo is not there, or it succeeds.
This is why this package's fetchReadCap (the emulated "downloaded from the outer overlay", PublicRepoLinkV0) has no upstream counterpart to mirror: there is no signature at any level to copy, and the alignment falls to the engine's model instead. That is the reasoning behind 1.0.0-dev.9: the emulation's own step was collapsing "the store could not be asked" into "the document exposes no key", so the honest projection of a failed emulation step is the one outcome the engine has for a read that cannot happen - a rejection.
What this package does with it, and the line it must not cross
Three rules follow, and all three are load-bearing:
- A breakdown rejects. Anything that stops a read from happening - a repo that will not open, a broker that does not answer, an emulation step that could not be performed - reaches the caller as a failure.
A rejection means "unknown", never "absent"is the contract's own wording. - An empty document is a SUCCESS holding zero objects. The repair for (1) must never be bought by making a real emptiness into an error; that only moves the lie. An empty document, an empty scope, a document nobody opened an inbox on, a document in a public store that exposes no key - each is an answer, and each stays one.
- The unsynced/empty collision is closed by the barrier, not by an error. Where this package can cross the sync barrier before answering, it does (
emulated-verifier/open-repo.ts), which is what makes a zero-row answer mean something. Where it cannot, the answer is still a success, andgetSyncStateis what says whether it was authoritative. Turning an unsynced read into a rejection would be inventing a third outcome the engine does not have.
Related: knowledge_what-a-read-scope-is-upstream (what a read request can SAY, including the [] that means the empty set and the [] that means no filtering), and polyfill-surface/contract_polyfill-surface ## Guarantees for the caller-facing form of all of this.