--- type: knowledge summary: 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 last_checked: 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: 1. **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. 2. **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. 3. **The unsynced/empty collision is closed by the barrier, and where the barrier was not crossed the read says so rather than answering.** Crossing it is what makes a zero-row answer mean something (`emulated-verifier/open-repo.ts`, which keeps `synced` and `timed-out` deliberately apart). A reactive read over documents it NAMES consults that distinction and stays PENDING when the barrier was not reached - "I have no authoritative answer" rather than "there is nothing". Turning it into a rejection would be inventing a third outcome the engine does not have, and answering a success would be the collapse this whole leaf is about. **What an application can see of this, exactly.** `useShapeQuery` folds it into its own states, and that is the only published channel: a named scope whose barrier was not crossed does not reach `isSuccess` - **within one ordinary session**. Two situations break it, both known defects and both carried as acceptance tests by `T01.m` ("there must be one answer to whether a document is open"): a second `configure(...)`, which tears down the subscription layer without telling the open registry, and a broker refusing a subscription after this package has stopped waiting. In both, a document recorded as open is not, and the read answers an empty success. The REACHABILITY half is unaffected. `inbox.readSyncedForDocument` is the same guarantee on the inbox path. `getSyncState` is INTERNAL and is deliberately not published - this leaf offered it until 2026-09-24, which was a pointer at something no caller can import. It stays internal because it has no counterpart upstream at any level: publishing it would hand an application a polyfill-era symbol to unlearn at migration, on a surface that is busy removing exactly those. The consequence to state plainly rather than paper over: a caller using the anchored `docs.sparqlQuery` directly gets zero rows for an unsynced document and has **no published way to tell that from an empty one** - that is the collapse upstream has too, and it is why the reactive read is the one that carries the distinction. 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.