Trois reponses coexistaient et pouvaient se contredire : un ensemble de souscriptions bootstrap et une carte d'etat de barriere dans `open-repo`, et les auditeurs du fan-out dans `surface/subscribe`. Les deux premieres survivaient a la disparition de la troisieme, parce que `resetDocSubscriptions()` - ce que fait le `configure()` publie - largue ses auditeurs sans rien dire a personne. Il n'en reste qu'une, l'etat de l'unique souscription reelle du document, exposee par `docChannel(nuri)`. `open-repo` n'enregistre plus que l'auditeur qu'il a pose - « est-ce que j'ecoute encore », rien de plus - et derive `getSyncState` a chaque lecture, si bien qu'aucune reponse ne peut survivre au canal qu'elle decrit. Disparaissent : `held`, `syncState`, `inFlight`, `settleInFlight` et le compteur de generation, remplace par l'identite de l'enregistrement. Les deux echecs prouves deviennent des tests qui echouent sans le changement : un second `configure()` apres une synchro reussie (`"synced"` au lieu de `"timed-out"`, zero re-souscription, et `isSuccess` cote application), et un etablissement encore en cours quand la couche de souscription est demontee - cas que le canal `onSetupFailed` ne peut pas rattraper, puisqu'il ne rapporte que la tentative encore attendue. La ligne qui ne casse pas : un document synchronise qui ne contient rien repond toujours un succes a zero objet. `1.0.0-dev.10`, les deux non-garanties nommees sont retirees et la garantie livree porte le test qui l'epingle.
7.2 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, 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 keepssyncedandtimed-outdeliberately 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.
useShapeQueryfolds it into its own states, and that is the only published channel: a named scope whose barrier was not crossed does not reachisSuccess. That held only within one ordinary session until1.0.0-dev.10: a secondconfigure(...)tore down the subscription layer without telling the open registry, and a broker refusing a subscription after this package had stopped waiting left the document recorded as open - in both, a document recorded as open was not, and the read answered an empty success. Closed by giving the question ONE answer: openness is the state of the document's one real subscription, read rather than remembered, so no record of it can outlive the channel it describes. The REACHABILITY half was never affected.inbox.readSyncedForDocumentis the same guarantee on the inbox path.getSyncStateis 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 anchoreddocs.sparqlQuerydirectly 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.