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:
@@ -0,0 +1,43 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user