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:
Sylvain Duchesne
2026-09-21 15:09:50 +02:00
parent 75313f75a2
commit b0eec7b4dc
11 changed files with 108 additions and 38 deletions
@@ -304,12 +304,12 @@ export async function restoreOwnCaps(holderKey: string): Promise<void> {
* only its owner opens one, and only on its own document.
*
* **`undefined` is an ANSWER, and only an answer** (`1.0.0-dev.9`). It means the document
* carries no address — nobody opened an inbox on it — or that it is not this holder's to
* carries no address - nobody opened an inbox on it - or that it is not this holder's to
* see one on. It no longer means "the read did not happen": both routes by which a
* breakdown used to arrive here as `undefined` now reject. That mattered because two
* published calls turn this value into a statement an application acts on:
* `inbox.postToDocument` says "its owner never opened one", and `inbox.readForDocument`
* says zero deposits — for a document whose inbox is full.
* says zero deposits - for a document whose inbox is full.
*/
// @provenance documentInboxAddress kind=divergent level=1 ref=engine/net/src/types.rs:ContactDetails — the deposit-side half of the same divergence: upstream you are TOLD an address, you never read one off a document
export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined> {
@@ -341,7 +341,7 @@ export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>
}
} catch (error) {
// **Propagates a failed read** (`1.0.0-dev.9`). This used to answer `undefined`, on
// the ground that "refusing to read is the boundary doing its job" — but the boundary
// the ground that "refusing to read is the boundary doing its job" - but the boundary
// has already had its say at `mustNotAttempt` above, which returned before reaching
// here. What this catch could still meet is a breakdown: a repo that would not open,
// a broker that did not answer, a query that rejected. Answering `undefined` for that
@@ -188,13 +188,13 @@ async function syncSession(): Promise<void> {
* `"synced"`). Idempotent: a repo already opened (or in flight) is not re-opened.
*
* Tolerant by construction: if the injected `ng` exposes no `doc_subscribe` (e.g.
* the fake `ng` in the unit suite), this is a no-op — the read proceeds unchanged. The
* the fake `ng` in the unit suite), this is a no-op - the read proceeds unchanged. The
* open itself never throws: a `doc_subscribe` that never pushes a `State` is bounded by
* the fallback timeout and leaves the read to behave as it did before.
*
* **It DOES throw when the public store could not be asked** (`1.0.0-dev.9`). That is the
* one step here that can fail to ANSWER, and until then it answered `false` — "this
* document exposes no cap" — so the open declined silently and the anchored read that
* one step here that can fail to ANSWER, and until then it answered `false` - "this
* document exposes no cap" - so the open declined silently and the anchored read that
* followed refused the reader for holding no cap, which was true but was not the reason.
* A caller that means to carry on regardless says so with a `catch` of its own; none does
* today, because every one of them is about to read the document.
@@ -182,7 +182,7 @@ export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
* fetching, and skipping it keeps the ordinary path free of physical reads.
*
* **Two outcomes only, and a THIRD that is not an outcome.** `false` means the network
* answered and the document exposes no cap — an absence, and the normal case. `true` means
* answered and the document exposes no cap - an absence, and the normal case. `true` means
* a cap was obtained and filed. A download that REJECTED is neither: it REJECTS here too.
*
* It used to answer `false` for that as well, which is the defect this function was the
@@ -233,7 +233,7 @@ export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
// The download REJECTED: the repo would not open, the broker did not answer, the read
// threw. Whether this document may be read is UNKNOWN, and that is not what `false`
// says. Rethrown rather than returned so the caller's read fails instead of continuing
// on a document it never obtained anything for — upstream's observable is that a read
// on a document it never obtained anything for - upstream's observable is that a read
// rejects or it does not, and this is the side of it that was missing.
//
// Wrapped so the reason survives the trip: the message a caller would otherwise meet
@@ -243,7 +243,7 @@ export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
// it and dropped the memo, so the next caller asks the network again.
throw new Error(
"[ng-eventually] fetchReadCap: the public store could not be asked for this " +
"document's read cap, so whether it may be read is UNKNOWN — this is not " +
"document's read cap, so whether it may be read is UNKNOWN - this is not " +
`"the document exposes no cap". ${JSON.stringify(doc)}`,
{ cause: error },
);
@@ -292,8 +292,8 @@ async function downloadReadCap(doc: Nuri): Promise<ReadCap | null> {
return null;
}
// `fetchReadCaps(docs)` was REMOVED in `1.0.0-dev.9`. It was the batch form — a
// `Promise.all` over the singular, described as "independent and tolerant" — and it had
// `fetchReadCaps(docs)` was REMOVED in `1.0.0-dev.9`. It was the batch form - a
// `Promise.all` over the singular, described as "independent and tolerant" - and it had
// ZERO callers, the last one having gone with `readUnion` in `1.0.0-dev.8`. Keeping it
// would have left an unowned tolerance policy standing over a call that now rejects:
// "each fetch is independent" is exactly the skip-and-carry-on this line of work has
@@ -18,14 +18,22 @@
* in a PUBLIC store hands its cap to whoever asks (`public-store.ts`), but asking is a
* round-trip - so this filter drops such a document until some read path has asked.
* The paths that DO ask are the ones this library still reads with (the anchored
* document read,
* `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`). The two SHAPE reads
* ask nothing: `useShape` because its signature is the real ORM's and has no await to
* spend, and `useShapeQuery` since `1.0.0-dev.6`, because it reads through the SDK's
* own shape query and no longer opens a document of its own. An application reaching a
* public document through either of them alone, having read it nowhere first, sees
* nothing until it does - and a cap that lands later makes both re-publish, since each
* follows `CapRegistry.onChange`. A polyfill-era limit, removed with the module.
* document read, `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`) and,
* since `1.0.0-dev.9`, `useShapeQuery` for every document its scope NAMES - it crosses
* the same barrier on each before the query opens, so a named public document is held
* by the time this filter sees its objects.
*
* `useShape` still asks nothing - its signature is the real ORM's and has no await to
* spend - and neither read asks anything under `did:ng:i`, where the scope names no
* document to ask about and this filter is the whole of the answer. An application
* reaching a public document through one of those alone, having read it nowhere first,
* sees nothing until it does - and a cap that lands later makes both re-publish, since
* each follows `CapRegistry.onChange`. A polyfill-era limit, removed with the module.
*
* Note what the barrier does NOT make redundant here. Under `did:ng:i` this filter is
* the only thing standing between the caller and every identity's objects, and even on
* a named scope it keeps the answer honest per `@graph` for whatever else the engine
* returned. Refusing at the ASK and filtering the RESULT are two different questions.
*
* Note there is no `user` parameter anywhere below, and that is the point: reading
* is key possession, so the only question askable is "do I hold this document's