Files
ng-eventually/.project/concepts/app-contract/knowledge_what-a-failed-read-answers-upstream.md
T
Sylvain Duchesne b0eec7b4dc 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.
2026-09-21 15:09:50 +02:00

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:

  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.