refactor(layout): ranger les modules par destin à la migration

Les 25 modules étaient à plat, nommés d'après ce qu'ils font mécaniquement
(`store-registry`, `read-model`, `reach`, `caps`). Rien dans l'arborescence ne
disait lesquels DEVIENDRONT le vrai SDK, lesquels tiennent lieu du travail que
le verifier fera nativement, et lesquels n'existent que parce qu'un wallet est
partagé — trois destins sans rapport.

Quatre dossiers, les deux fichiers d'entrée restant à la racine pour que
l'`exports` du paquet et le code du consommateur ne bougent pas :

- `model/` — le modèle d'adressage de la cible, transcrit : vocabulaire pur,
  pas d'I/O. Survit comme connaissance.
- `surface/` — ce que l'app touche, chaque symbole ayant un pendant cible
  documenté. Supprimé quand l'alias bascule ; le code de l'app est inchangé.
- `emulated-verifier/` — les doublures de ce que le verifier fait nativement :
  possession, dépôt des caps, frontière, non-livraison, traitement des inbox,
  registres de branche, ouverture de repo. **C'est le dossier où diverger du
  modèle est possible.** Le préfixe `emulated-` porte le sens : tient lieu de,
  jamais est — cette bibliothèque ne réside dans aucune couche de la cible,
  elle les référence.
- `shared-wallet/` — n'existe que parce qu'un wallet héberge toutes les
  identités. Aucun pendant, rien sur quoi s'aligner ; sa seule loi est de
  rester invisible depuis `surface/`. S'évapore, remplacé par rien.

`store-registry-api.ts` devient `surface/placement.ts` : il faisait déjà à la
main ce que la frontière de dossier fait structurellement — c'est la meilleure
preuve interne du bien-fondé de ce rangement.

Ce commit ne fait que déplacer et recâbler les imports (src, test, e2e). Les
scissions des modules à cheval suivent.

157 tests unitaires, typecheck src/test/e2e vert.
This commit is contained in:
Sylvain Duchesne
2026-08-04 12:46:44 +02:00
parent d07b3642aa
commit 88914f50ae
46 changed files with 302 additions and 137 deletions
@@ -0,0 +1,103 @@
/**
* physical — the polyfill's OWN machinery, operating on the PHYSICAL user.
*
* ── Two levels, two APIs, and only one of them is the app's ───────────────
* NextGraph sees exactly one user: the physical one, whose wallet everybody opens.
* On top of it the library fabricates **virtual users** — what the consumer calls
* an identity. Those are two different levels, and conflating them is how a
* boundary gets a hole in it:
*
* | | Level | Who calls it | Guarded |
* |---|---|---|---|
* | `docs.*`, `subscribeDoc` | the **virtual user** | the consumer app, and the library on the user's behalf | YES — confined to the connected user (`reach.ts`) |
* | this module | the **physical user** | the library's own machinery, and nothing else | no — it *is* the machinery the boundary is built on |
*
* **Nothing here is exported from the package.** `index.ts` must never re-export
* this module: an app holding these functions could read any document of any
* virtual user, which is precisely the boundary they exist below.
*
* ── Why a separate module rather than exemptions ──────────────────────────
* The store-root pointer and the doc-shim — the index of virtual users — cannot be
* subject to the boundary: resolving *which* documents a virtual user owns is what
* makes virtual users exist at all. An earlier version handled that with a list of
* exempt NURIs consulted by the guard. Separating the FUNCTIONS is stronger: the
* machinery does not call the guarded primitive and get waved through, it calls a
* different primitive that was never guarded. There is no exemption list to widen,
* to get wrong, or to infer.
*
* The rule for deciding which side a call belongs to:
*
* > Does this operate on the index of virtual users (the shim), or on the content
* > of one virtual user? The first is machinery; everything else is the user's,
* > and is confined.
*
* A virtual user's own stores, its inbox and its documents are the user's — they go
* through `docs.*` and are guarded, even though the library is what calls them.
*
* At migration this module disappears with the shim: there is no physical/virtual
* split once each user opens their own wallet.
*/
import { getConfig } from "../polyfill";
import { logAccess } from "./access-log";
import { isNuri } from "../model/nuri";
import type { Nuri } from "../model/types";
/**
* Create a document as the PHYSICAL user — the shim's own documents (the doc-shim,
* a virtual user's store documents at provisioning time, an inbox document).
*
* Creation is the one operation with no boundary to check: the document does not
* exist yet, so nobody can hold its cap. What matters is who is credited with it
* afterwards, which the caller decides by filing the cap among the caps that holder holds.
*/
export async function physicalCreate(
sessionId: string,
crdt = "Graph",
cls = "data:graph",
dest = "store",
store?: unknown,
): Promise<Nuri> {
const { ng } = getConfig();
const nuri = await ng.doc_create(sessionId, crdt, cls, dest, store);
if (typeof nuri !== "string" || !isNuri(nuri)) {
throw new Error(
`[ng-eventually] physicalCreate: the broker returned something that is not a NextGraph reference: ${JSON.stringify(nuri)}`,
);
}
logAccess("WRITE", nuri, "physicalCreate");
return nuri;
}
/**
* Read as the PHYSICAL user — for the shim only (the store-root pointer, the
* doc-shim's account records).
*
* Unguarded by design: this is how the library learns which documents a virtual
* user owns, so it cannot itself depend on knowing that. Do not reach for it to
* read a virtual user's content — that is `docs.sparqlQuery`, which is confined.
*/
export async function physicalQuery(
sessionId: string,
query: string,
base: string | undefined,
anchor: Nuri,
label = "physicalQuery",
): Promise<unknown> {
const { ng } = getConfig();
const result = await ng.sparql_query(sessionId, query, base, anchor);
logAccess("READ", anchor, label, " (physical)");
return result;
}
/** Write as the PHYSICAL user — the shim's own records. See {@link physicalQuery}. */
export async function physicalUpdate(
sessionId: string,
query: string,
anchor: Nuri,
label = "physicalUpdate",
): Promise<void> {
const { ng } = getConfig();
logAccess("WRITE", anchor, label, " (physical)");
return ng.sparql_update(sessionId, query, anchor);
}