--- 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, 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, and `getSyncState` is 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.