Deux defauts, aucun des deux sur la reprise - elle a passe cette revue intacte et n'a pas ete rouverte. 1. La barriere verifiait qu'un document nomme peut etre JOINT, jamais qu'il a REPONDU. Elle attendait `ensureRepoOpen` puis demandait `assertMayReach`, qui est la question de la capacite, et ne consultait pas l'etat de synchro que l'ouverture venait de produire. Un document dont l'abonnement a reussi mais dont le premier `State` n'est jamais venu publiait donc `isSuccess` avec `data: []` : le defaut d'origine, dans le cas precis pour lequel la barriere a ete ajoutee. La verification rejoint le reste de la decision, dans `publish`, donc rejouee a chaque publication au lieu d'etre memorisee. Les trois valeurs, enumerees avant d'ecrire la condition : `"synced"` fait autorite - zero ligne VEUT DIRE zero objet, la ligne tenue depuis cinq revues -, `"timed-out"` et `"syncing"` non, et `"unknown"` PASSE deliberement, parce qu'il veut dire « pas de semantique de `State` sur ce `ng` » et que le traiter autrement est exactement la regression du cas inerte livree aux tours 1 et 3. `isPending` plutot que `isError`, des deux que le contrat autorise : rien n'a echoue, il manque l'autorite de dire que le document est vide. Le test (t) EPINGLAIT le defaut - il attendait `isSuccess` apres trois barrieres expirees. Corrige. Ce n'est pas un test affaibli : l'ancienne assertion etait fausse. Et un trou dans ma propre suite, trouve par mutation : inverser la condition pour refuser `"unknown"` passait tous les tests, faute d'un `ng` SANS `doc_subscribe` nulle part. (cc) le couvre. 2. Regression de mon correctif du tour 4. Terminer une ouverture abandonnee faisait tourner sa continuation juste apres que la reinit avait vide les tables, donc elle ecrivait une entree `opened` perimee dans le registre neuf - et toute ouverture ulterieure de ce nuri court-circuitait dessus. Le depot n'etait jamais abonne dans la nouvelle session. Une epoque de registre corrige cela : une reinit l'incremente, une continuation dont l'epoque ne correspond plus n'ecrit rien. Une reinit INVALIDE ce qu'elle abandonne, en plus de le terminer. `getSyncState` reste interne, et la doctrine cesse de l'offrir : elle pointait sur un symbole qu'aucun appelant ne peut importer.
6.6 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.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.