docs: dire partout que la lecture par lot n'existe plus

Le contrat publie et l'api-contract etaient traites avec le retrait lui-meme ;
restaient les documents qui decrivaient readUnion comme la primitive de
lecture du paquet, ou qui nommaient le module disparu.

Ce que les lecteurs apprennent maintenant : lire un document, c'est
docs.sparqlQuery ancre dessus, et lire plusieurs documents, c'est cet appel
une fois par document -- avec la decision sur une lecture qui echoue ecrite
au point d'appel. Deux consequences sont dites plutot que decouvertes : une
lecture refusee leve au lieu de rendre une liste vide, et les sujets sous le
prefixe reserve urn:ng-eventually: reviennent dans le resultat, a l'appelant
de les ignorer -- sans aucun gain de droits, les deux registres qu'un
document ordinaire porte etant l'adresse de sa boite et, pour un document
public, la cle que son magasin sert deja a qui la demande.

Les briefs dates ne sont pas touches : ils decrivent un etat de juillet, et
le reecrire falsifierait ce qui avait ete decide alors.
This commit is contained in:
Sylvain Duchesne
2026-09-21 14:11:35 +02:00
parent 0c21adebf2
commit 0a2b8b1fbf
29 changed files with 90 additions and 77 deletions
@@ -232,7 +232,8 @@ export async function readStoreCaps(storeDoc: Nuri): Promise<ReadCap[]> {
* store it was created in ({@link holdOwnCap}) — and until 2026-08-16 exactly one path
* read it back: `listMyEntityDocs`. So a creator that reloaded the page and went straight
* to the document it had just made, without listing that scope first, held nothing for it:
* the read was refused, the batch read (`readUnion`) dropped it silently, and the write
* the read was refused, the batch read of the day (`readUnion`, removed in `1.0.0-dev.8`)
* dropped it silently, and the write
* was refused. Its own document, denied to its own author, on the first refresh.
*
* Restoring what a user HOLDS is already what connecting means here — that is what the
@@ -288,7 +288,7 @@ async function applyLink(payload: unknown, ctx: Applying): Promise<boolean> {
// reports the whole inbox, and everything not yet popped is preserved for the next
// attempt. That is the rule stated beside {@link report}, and BOTH arms follow it — but
// the reference arm only since its `unreadable` outcome was split out of `refused` on the
// same day. It had been the opposite: `read-model.readDoc` swallows a failed read and
// same day. It had been the opposite: the batch read of the day swallowed a failed read and
// answers `[]`, so an outage arrived as an ordinary refusal and the drain walked the
// whole queue destroying it. Correcting one arm and leaving the other is precisely how
// that survived a round of review.
@@ -32,7 +32,7 @@
* nothing had to tell an absence from a failure. **That is no longer true, and it is the
* sharpest consequence of consuming.** An absence is deterministic and costs its own deposit;
* a silent broker is transient and must stop the drain, or one outage walks the queue and
* destroys every reference in it. `read-model.readDoc` answers `[]` for both, so this module
* destroys every reference in it. An anchored read answers `[]` for both, so this module
* passes it an `onUnreadable` reporter and asks which one happened.
*
* What it no longer means, since deposits became consumed on 2026-08-26, is "and a later
@@ -2,7 +2,7 @@
* open-repo — cold-start repo opening for the ANCHORED read path (polyfill-era).
*
* ── The cold-start defect this heals ──────────────────────────────────────
* The anchored read path (`surface/read-model.ts` `readDoc`,
* The anchored read path (`surface/read-document.ts` `readDocumentSubjects`,
* `shared-wallet/account-registry.ts` `readUserStore`) assumes the target repo is
* already usable by the verifier — true within the session that CREATED the doc
* (every `doc_create` opens it), but FALSE on a FRESH session over the same
@@ -19,9 +19,10 @@
* the verifier: on a persistent profile `Verifier::load` repopulates `self.repos` from
* user storage at construction (`engine/verifier/src/verifier.rs:535-560`), so the repo
* is PRESENT but unsynced and the anchored query legitimately matches nothing; and this
* library's own `readDoc` catches every error and returns `[]`
* (`surface/read-model.ts:122`), so anything that did throw would reach the caller as
* emptiness anyway. The fix below is right; the diagnosis written beside it was not.
* library's own per-document read caught every error and returned `[]`
* (`surface/read-model.ts`'s `readDoc`, until `1.0.0-dev.8` removed it along with the
* batch it served), so anything that did throw reached the caller as emptiness anyway.
* It no longer does: `surface/read-document.ts` lets the rejection through. The fix below is right; the diagnosis written beside it was not.
*
* The circularity that made this self-inflicted: `doc_subscribe` WOULD open the
* repo, but the reactive layer only subscribes AFTER the listing produced NURIs —
@@ -165,7 +165,7 @@ export async function assertMayWrite(nuri: Nuri, op: string): Promise<void> {
}
/**
* **Rule 2 — do not even attempt**, at the CALLERS (`read-model`, `open-repo`,
* **Rule 2 — do not even attempt**, at the CALLERS (`open-repo`,
* `subscribe`'s callers…).
*
* A reader that does not hold a document's cap must not issue the operation at all.
@@ -17,7 +17,8 @@
* It is SYNCHRONOUS and decides from what the holder holds at that instant. A document
* 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 (`readUnion`,
* 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
@@ -166,7 +166,7 @@ export const STORE_BRANCH_SUBJECT = `${SHIM}:storeBranch`;
/**
* Fixed subject of the **Header branch** emulation, inside an ENTITY document — the
* first compartment we put in a document the consumer also reads, hence the filter in
* `read-model.ts` (every `${SHIM}:` subject is machinery and never surfaces as data).
* `read-document.ts` (every `${SHIM}:` subject is machinery, and the internal read drops it).
*
* It carries what must be readable by *whoever can read the document*, as opposed to
* what belongs to its owner alone. Today that is one thing: the ADDRESS of the
@@ -1165,7 +1165,7 @@ export async function createEntityDoc(id: string, scope: StoreType): Promise<Nur
s.sessionId,
// NO explicit `GRAPH <…>` wrapper: write the anchored DEFAULT graph (the
// `indexDoc` anchor scopes it) — the CANONICAL, always-safe shape the anchored
// default-graph read queries (readUserStore below, same as read-model.ts).
// default-graph read queries (readUserStore below, same as read-document.ts).
// entityNuri is a NURI stored as a literal → escapeLiteral.
`INSERT DATA { <${MAIN_BRANCH_SUBJECT}> <${P.contains}> "${escapeLiteral(entityNuri)}" }`,
indexDoc,
@@ -315,7 +315,7 @@ export function configureStoreRegistry(deps: StoreRegistryDeps): void {
// Fire the outbox inspection (Volet 3 of the low-level data-path trace) once,
// on the FIRST successful `getSession()` resolution — the most reliable
// "a session is established" signal available: every low-level reader/writer
// (store-registry, open-repo, read-model, subscribe, inbox) reaches its
// (store-registry, open-repo, read-document, subscribe, inbox) reaches its
// session through this SAME injected `getSession`, so wrapping it HERE catches
// the first success from whichever caller happens to run first, instead of
// tying the probe to one particular call site. Only on SUCCESS (an error
@@ -424,7 +424,7 @@ export function getCurrentUser(): PrincipalId | null {
}
/** The emulated cap registry — what the current identity holds, plus the emulated
* public store. The read filter and the read-model consult it. */
* public store. The read filter and the document read consult it. */
export function getCaps(): CapRegistry {
return caps;
}
+1 -1
View File
@@ -24,7 +24,7 @@ import type { Nuri, NuriLike } from "../model/types";
// through `sparqlQuery`, every write through `sparqlUpdate` (+ container creation
// through `docCreate`) — each ultimately calling the real injected `ng` here. The
// access log is therefore instrumented HERE so no access path escapes it. Callers
// pass a semantic `label` (readDoc|readUnion|listMyEntityDocs|writeEntity|deposit
// pass a semantic `label` (readDocument|listMyEntityDocs|writeEntity|deposit
// |…); it is a lib-internal probe param, NOT forwarded to the real `ng` (the docs
// primitives forward the exact SDK signature — see test/docs.test.ts). When the
// log is OFF (default) the extra param is inert and costs one boolean read.
+1 -1
View File
@@ -212,7 +212,7 @@ export async function post(targetInboxLike: NuriLike, opts: PostOptions): Promis
// NO explicit `GRAPH <…>` wrapper — write the anchored DEFAULT graph:
// `sparqlUpdate(sid, update, targetInbox)` scopes the write to that repo's
// default graph (same shape as read-model.ts readDoc/readUnion). This is the
// default graph (same shape as `read-document.ts`'s anchored read). This is the
// CANONICAL, always-safe shape and the one the anchored default-graph read
// queries. (Not a round-trip necessity on the current broker: the e2e harness
// `packages/polyfill/e2e/` verified that an anchored `GRAPH <plainNuri>` write
+1 -1
View File
@@ -137,7 +137,7 @@ export async function createEntityDoc(store?: NuriLike): Promise<Nuri> {
//
// - `listMyEntityDocs` has no counterpart at any level: there is no `doc_list`, no
// `list_docs`, nothing in the binding. Listing what one created is an ordinary query
// over the store's `ldp:contains` — the same shape `readUnion` is, sugar over
// over the store's `ldp:contains` — the same shape the anchored document read is, sugar over
// something already expressible. It stays reachable at `../shared-wallet/account-registry`
// for this library's own read path, which still calls it until that path queries the
// store itself.
+1 -1
View File
@@ -86,7 +86,7 @@ import type { Nuri, NuriLike } from "../model/types";
* A push from the platform to a document subscriber. Loosely typed: the raw
* serialized `AppResponse` (`{ V0: { State | Patch | TabInfo | ... } }`). The
* consumer typically ignores the payload and uses the push purely as a
* change SIGNAL (re-query on change — the read-model pattern), so this stays
* change SIGNAL (re-query on change — the read model in `docs/read-model.md`), so this stays
* permissive rather than modelling every AppResponse variant.
*/
// @provenance DocChange kind=aligned level=1 ref=engine/net/src/app_protocol.rs:AppResponseV0 — the raw serialized response, left `unknown` rather than modelling every variant
@@ -22,9 +22,9 @@
* The SDK's own shape query - `RdfOrmSubscription.getOrCreate(shapeType, scope)` - and
* nothing of this library's own. Four steps went with the change, and they were one
* mechanism: a logical scope resolved to a document set (`listMyEntityDocs`), those
* documents opened through `ensureReposOpen` for a sync barrier, `readUnion` over them,
* and a filter of the union by the shape's `@type` IRI. All four existed because the
* parameter had to be TRANSLATED; once the parameter is upstream's own `Scope`, the
* documents opened through `ensureReposOpen` for a sync barrier, one anchored read over
* each of them, and a filter of the result by the shape's `@type` IRI. All four existed
* because the parameter had to be TRANSLATED; once the parameter is upstream's own `Scope`, the
* question goes straight to the thing that answers it. `graphs`, `subjects` and the
* whole-dataset marker `did:ng:i` are now read by the engine, literally, and this module
* does not interpret them at all - so the three readings are upstream's, not ours.