refactor(api): précis en sortie, permissif en entrée — plus de guard publié

Un polyfill ne doit rien faire de plus que ce qui est prévu. `isNuri` /
`hasReadCap` et les utilitaires SPARQL `escapeLiteral` / `escapeIri` /
`assertNuri` n'ont de pendant à aucun niveau et n'en auront pas : le binding
prend `nuri: String`, le moteur est fortement typé en Rust et n'a besoin
d'aucun prédicat, l'ORM n'expose rien de tel. Le contrat les justifiait parce
qu'ils « restent utiles à n'importe quelle app » — c'est exactement le
raisonnement à refuser : utile n'est pas prévu, et chacun serait un appel à
réécrire le jour du SDK.

Le besoin d'un guard venait de notre propre signature : les entrées publiques
exigeaient `Nuri`, donc un consommateur devait narrower ce qu'il lisait d'une
URL ou du stockage. Elles prennent désormais `NuriLike` — n'importe quelle
chaîne — et valident à l'intérieur (`toNuri`). Ce que la bibliothèque REND
reste typé `Nuri` : l'app en profite gratuitement, et un type plus large ne
cassera rien quand le SDK rendra des chaînes.

Les guards et les utilitaires restent, internes, là où la validation se fait.

Un défaut introduit puis corrigé en chemin, qui valait le test qu'il a produit :
`readUnion` a toujours toléré les trous dans sa liste — un index de scope peut
porter une entrée blanche, et un appelant qui assemble depuis des valeurs
optionnelles n'a pas à compacter. Valider AVANT de filtrer a transformé cette
tolérance en exception. Vide est une absence, pas une référence malformée ; les
deux sont désormais distingués par un test.

170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la
bibliothèque, l'exemple et le harnais.
This commit is contained in:
Sylvain Duchesne
2026-08-06 10:51:36 +02:00
parent 54f8389e9e
commit ebf866b1f2
11 changed files with 96 additions and 35 deletions
+9 -3
View File
@@ -46,8 +46,9 @@ import { getCaps, getStoreRegistryDeps } from "../shared-wallet/bootstrap";
import { mustNotAttempt } from "../emulated-verifier/reach";
import { ensureReposOpen } from "../emulated-verifier/open-repo";
import { assertNuri } from "./sparql";
import { toNuri } from "../model/nuri";
import { isMachinerySubject } from "../emulated-verifier/machinery";
import type { Nuri } from "../model/types";
import type { Nuri, NuriLike } from "../model/types";
// Keep the primitives referenced so tree-shaking never drops the import used by
// the (side-effecting) open step below; `docCreate`/`sparqlUpdate` are not used
@@ -146,9 +147,14 @@ async function readDoc(
* read with an anchored default-graph query, O(1) per doc, independent of wallet
* size — a non-empty wallet no longer matters. Reads run in parallel via `Promise.all`.
*/
export async function readUnion(docs: Nuri[]): Promise<UnionSubject[]> {
export async function readUnion(docsLike: NuriLike[]): Promise<UnionSubject[]> {
const sid = await sessionId();
const unique = [...new Set(docs.filter(Boolean))];
// Drop the empties BEFORE validating, not after: this call has always tolerated a
// list with holes in it — a scope index can carry a blank entry, and a caller
// building a list from optional values should not have to compact it. Validating
// first turned that tolerance into a throw, which took down a whole reconnect run.
// Empty is absence, and absence is not a malformed reference.
const unique = [...new Set(docsLike.filter(Boolean))].map((d) => toNuri(d, "readUnion"));
if (unique.length === 0) return [];
// RULE 2 — do not even attempt. Drop the documents whose cap this user does not