feat!: retirer readUnion de la surface publiee, et lire un document par requete ancree

readUnion etait N requetes ancrees plus une politique de lot : une lecture
qui echoue avalee en reponse vide, un document dont on ne detient pas la cle
ecarte au lieu d'etre refuse, un regroupement en sacs de proprietes, et le
filtrage du prefixe reserve. Rien en amont ne repond a cette composition, et
son annotation citait sparql_query -- une citation exacte attachee a une
affirmation fausse. Une composition ne s'aligne pas sur une fonction.

Ce qui reste est ce qu'il y avait dessous : docs.sparqlQuery ancre sur un
document, qui va deja chercher la cle servie par le magasin public -- donc
une reference nue suffit toujours pour lire un document public, sans rien
changer a useShapeQuery. Lire plusieurs documents, c'est cet appel une fois
par document, et la tolerance a une lecture qui echoue revient a l'appelant,
la ou l'amont la laisse : une requete ancree sur un depot que le verifier ne
detient pas rejette.

Cote src, l'unique lecture interne restante devient read-document.ts : un
document, une requete ancree, precedee de l'ouverture du depot -- barriere
que index-deposit consulte pour distinguer une absence d'un depot non
synchronise. Le harnais e2e et l'exemple composent la leur, avec leur catch
visible au point d'appel.

Version majeure : 1.0.0-dev.8.
This commit is contained in:
Sylvain Duchesne
2026-09-21 14:08:05 +02:00
parent 05a0f7b148
commit 0c21adebf2
18 changed files with 659 additions and 516 deletions
+25 -13
View File
@@ -259,13 +259,13 @@ async function main(): Promise<void> {
);
});
// ── read-model ──────────────────────────────────────────────────────────
console.log("\n── read-model ──");
await step("readUnion returns one entry per SUBJECT, with the subject it was written under", async () => {
// ── the anchored document read ──────────────────────────────────────────
console.log("\n── the anchored document read ──");
await step("an anchored read returns one entry per SUBJECT, with the subject it was written under", async () => {
// 3 documents, and the last carries TWO subjects → 4 entries, not 3. Counting alone
// could not distinguish grouping-by-document from grouping-by-subject, which is why
// this step stayed green while `readUnion` conflated them (fixed 2026-08-10).
const r = await sdk<any>(frame, "readUnionOverDocs", 3, false);
// this step stayed green while the read conflated them (fixed 2026-08-10).
const r = await sdk<any>(frame, "readDocsOneByOne", 3, false);
const iris: string[] = r.subjectIris ?? [];
check(
"one entry per subject, not per document",
@@ -283,14 +283,26 @@ async function main(): Promise<void> {
JSON.stringify(r.graphs),
);
});
await step("readUnion per-doc tolerance (bad NURI skipped)", async () => {
const r = await sdk<any>(frame, "readUnionOverDocs", 2, true);
// 2 documents, the last carrying two subjects → 3 entries.
check("bad NURI does not abort the batch", r.subjectCount === 3, `entries=${r.subjectCount}/3 (+1 bad NURI)`);
await step("a document that cannot be read REJECTS, and the caller decides to carry on", async () => {
const r = await sdk<any>(frame, "readDocsOneByOne", 2, true);
// 2 documents, the last carrying two subjects → 3 entries. The bad NURI did not
// reduce that, and it is named in `skipped` — the tolerance is the harness's own
// `catch`, not a policy hidden in a primitive (`readUnion` carried one until
// `1.0.0-dev.8`; upstream an unknown repo rejects, so there was nothing to align it on).
check("the readable documents still answer", r.subjectCount === 3, `entries=${r.subjectCount}/3 (+1 bad NURI)`);
check(
"the unreadable one is REPORTED as skipped, not silently absent",
(r.skipped ?? []).length === 1,
`skipped=${JSON.stringify(r.skipped)}`,
);
});
await step("readUnion cap gate", async () => {
const r = await sdk<any>(frame, "readUnionCapGate");
check("cap gate drops doc for stranger, keeps for owner", r.strangerCount === 0 && r.ownerCount === 1, `stranger=${r.strangerCount} owner=${r.ownerCount}`);
await step("anchored read cap gate", async () => {
const r = await sdk<any>(frame, "anchoredReadCapGate");
check(
"cap gate REFUSES the stranger, and the owner reads",
r.strangerRefused === true && r.ownerCount === 1,
`strangerRefused=${r.strangerRefused} strangerCount=${r.strangerCount} owner=${r.ownerCount}`,
);
});
// ── reactivity (doc_subscribe) ──────────────────────────────────────────
@@ -544,7 +556,7 @@ async function main(): Promise<void> {
// session must relist + re-read its OWN persisted markers, POLLING for the sync —
// the wait IS the normal path. NB the observed cost is dominated by broker READ
// latency, not pure sync-lag: ONE reconnectRead cycle (open-repo heal awaits the
// initial-state push per repo + anti-fork retry budget + anchored readUnion, all
// initial-state push per repo + anti-fork retry budget + the anchored reads, all
// round-tripping the real broker) measures ~90-105s. So the poll deadline is a
// generous MULTI-cycle bound (120s past post-connect) rather than a tight 30s — a
// single slow cycle must not be mistaken for a sync failure. We report the observed