Commit Graph

8 Commits

Author SHA1 Message Date
Sylvain Duchesne 44a9b6ee04 test: quatre tests prouvaient autre chose que ce qu'ils annonçaient
Lot D de la revue adverse. Aucun changement de comportement de la bibliothèque : ce sont
les tests qui mentaient, et deux faux `ng` qui fabriquaient un état que le vrai broker ne
produit pas.

**« Un tiers résout l'inbox d'un autre utilisateur » prouvait le CACHE.** `userInbox`
indexe par (compte, portée) sans regarder qui demande, donc Bob tombait sur l'entrée que
la session d'Alice venait de chauffer. Rien de la persistance n'était exercé — le faux ne
servait même pas la requête `docInbox` — si bien que dans une seconde SESSION, ou une
seconde page de navigateur comme en pilote la suite applicative, Bob aurait obtenu une
inbox DIFFÉRENTE et son dépôt serait parti où personne ne lit. C'est la panne que cette
bibliothèque a déjà payée une fois.

Deux causes, toutes deux dans les faux : la requête `docInbox` n'était servie nulle part,
et le faux de `cross-user-access` prenait le **nom de graphe** pour le sujet dans un
`INSERT DATA { GRAPH <g> { … } }` — donc le pointeur du shim n'était jamais retrouvé et
chaque résolution à froid créait un nouveau shim. Les deux corrigées, et les tests qui
franchissent une frontière d'identité purgent maintenant le cache à la frontière.

**Mon propre index d'inbox avait le même défaut**, découvert en faisant ça : `isKnownInbox`
ne répondait que par sa mémoire, parce qu'aucun faux ne servait la requête. La moitié
durable n'était pas exercée — exactement la faute que ce lot corrigeait ailleurs.

**« Connecting drains BOTH levels » n'observait pas le second niveau.** Rejoué contre un
`connectedUser` qui ne draine que les inbox de l'utilisateur, il restait vert. La raison
n'est pas un test faible : le second niveau n'a **aucun producteur**. Le seul appel qui
dépose un cap est `inbox.share(doc, toUser)`, qui résout l'inbox d'un UTILISATEUR, jamais
celle d'un document. Drainer une inbox de document n'applique donc rien. Le test dit
désormais ce qu'il prouve, et l'anticipation est nommée comme telle : en amont
`AddInboxCap` est générique sur les repos et `InboxMsgContent::Link` existe, donc viser
cela est légitime — annoncer que c'est exercé ne l'était pas.

**« La liste de Bob ne contient pas la note d'Alice » n'avait pas de contrôle positif.**
Bob n'écrivait jamais de note publique : sa liste était vide quoi qu'il arrive. Il en
écrit une maintenant, et la vérification symétrique est ajoutée. Au passage, `showScope`
lisait le DOM avant le rendu — le gestionnaire `change` de l'application lance
`refresh()` sans l'attendre.

189 tests unitaires, e2e 40/40 et applicatif 12/12.
2026-08-10 09:10:51 +02:00
Sylvain Duchesne b5f05472d9 fix: second tour adverse — mes correctifs avaient quatre trous, dont un qui les annulait
J'ai relancé un adversaire sur les correctifs du commit précédent, comme la règle
l'exige. Il en a trouvé quatre. Le premier annulait la garde que je venais d'écrire.

**Le registre de propriété était écrivable depuis la surface publiée.** `assertMayWrite`
lit la propriété dans l'index de store de l'appelant — et `caps.open` marquait les
documents de STRUCTURE (les trois stores, les inbox) comme « créés par moi ». Donc un
porteur pouvait, par le `docs.sparqlUpdate` publié, insérer `contains "<n'importe quel
document>"` dans son propre index et s'en déclarer propriétaire. Démontré : Bob écrit dans
le document protégé d'Alice, et détourne l'inbox d'un de ses documents — exactement le
vecteur que le commit précédent prétendait fermer. `open` classe désormais sans marquer :
un document de structure n'est possédé par personne au sens de la paternité, donc les deux
moitiés de la garde répondent non, ce qui est correct.

**`inbox.post` acceptait n'importe quel NURI.** Déplacer `depositInto` hors de la surface
ne suffisait pas : `post` atteint la même porte, qui saute les deux gardes par
conception. Bob, ne détenant rien, écrivait quatre triplets dans le document d'Alice. En
amont la confusion est impossible — `InboxPost` scelle vers une CLÉ d'inbox et le broker
route par `inboxes: PubKey → RepoId` ; adresser un document n'est pas refusé, c'est
inexprimable. Le shim tient maintenant un index des inbox, l'équivalent émulé de ce que
le broker sait par construction, et `post` refuse ce qui n'en est pas une.

**Le filtre de lecture fuyait encore par les clés dunder.** `DeepSignalSet` expose la
collection brute sur `__raw__` / `__meta__` : `[...view]` rendait zéro élément pendant que
`view.__raw__` rendait le Set complet, tous utilisateurs confondus. Mon en-tête affirmait
qu'« une propriété simple ne porte aucun élément » — faux pour ce type.

**Et il cassait des membres légitimes** : ma liste blanche couvrait la moitié des
helpers d'itération, si bien que `toArray`, `reduce`, `first`, `take`, `drop`, `flatMap`
levaient sur les données du porteur lui-même. Tous filtrés désormais ; le refus ne vaut
que pour l'inconnu.

**Deux tests réparés à la source plutôt qu'en affaiblissant les gardes.** Le faux
`doc_create` de `inbox.test.ts` rendait une CONSTANTE — tous les documents créés étaient
le même NURI, donc la garde de propriété n'avait rien à distinguer et deux tests lisaient
l'inbox d'Alice sous l'identité de Bob sans que rien ne proteste. Et le harnais e2e
utilisait un document ordinaire comme inbox.

Enfin, mon propre cache d'inbox a reproduit la faute que la revue avait relevée ailleurs :
un mémo qui survit à sa session. Rattaché à `resetRegistryCache`.

189 tests unitaires (six régressions de plus), e2e 40/40 et applicatif 10/10 — après un
échec réseau non reproductible, relancé sans modification.
2026-08-07 14:26:32 +02:00
Sylvain Duchesne 0b936d2119 fix: écrire est une PROPRIÉTÉ, et trois portes qui n'auraient pas dû être ouvertes
Suite de la revue adverse. Quatre trous de frontière, tous hors du champ « l'isolation
est fausse jusqu'à P1b » — P1b parle de matériau de clé, ceux-ci sont des défauts de
FORME et resteraient des trous avec une vraie clé.

**La garde d'écriture reposait sur la mauvaise question.** Elle demandait « ce cap m'a-t-il
été servi par un store public ? ». Ce prédicat était faux dans les deux sens à la fois :
trop laxiste — une clé reçue dans une inbox donnait l'écriture, alors qu'en amont un Link
est « external repos only » et qu'écrire est l'appartenance au repo ; trop strict — la
propriétaire de son propre document public était refusée dès qu'elle l'ouvrait depuis sa
référence avant que son store ne soit listé. Un prédicat poussé dans deux sens est le
signe que c'était le mauvais prédicat.

Écrire dépend désormais de la PROPRIÉTÉ, lue sur la branche Store (l'`AddRepo` émulé),
plus la paternité de session pour les documents créés par la primitive brute qui n'a
aucun store où s'inscrire. Conséquence assumée et documentée : seul le propriétaire écrit,
ce qui est l'état amont d'un repo tant qu'aucun membre n'a été ajouté — mécanisme qu'on
n'émule pas.

**`docs.depositInto` quittait la frontière en la publiant.** Sa doc disait « `inbox.post`
est le seul appelant » : vrai dans la bibliothèque, faux dès qu'on le publie. Démontré :
avec la seule référence nue d'un document public, on réécrit l'adresse d'inbox posée
dessus et on détourne les dépôts destinés à son propriétaire. Une porte qui saute une
garde ne doit pas être ouvrable par une application — elle rejoint la machinerie.

**Le filtre de lecture n'interceptait que trois membres** et transmettait tout le reste
lié à la CIBLE : `.values()`, `.map()`, `.getById()` rendaient le contenu d'un autre
utilisateur — précisément les membres qu'une API de set réactif met en avant. Les membres
qui rendent des éléments sont désormais filtrés, les mutations passent (elles ne rendent
rien), et **tout membre inconnu lève** au lieu de transmettre : une transmission est une
fuite silencieuse, une levée est bruyante et greppable.

**Le mémo du store public était par document.** Le premier demandeur déclenchait le
téléchargement, le cap était classé chez LUI, et tout demandeur suivant recevait « oui »
en ne détenant rien. En amont un broker qui sert un overlay externe répond à TOUS. Le
mémo garde la valeur, l'appelant la classe pour qui est connecté.

Aussi : l'exemption `declareInfrastructure` supprimée — zéro appelant, ensemble toujours
vide, et une doc décrivant deux documents exemptés qui ne l'ont jamais été. Et les caps
d'écriture décrits comme « partiels » sont dits **inertes**, ce qu'ils sont : `grantWrite`
n'a aucun appelant de production.

**Ce que l'e2e a rattrapé.** Ma première version de la garde refusait au créateur
l'écriture sur un document fait par `docs.docCreate` — 7 étapes rouges contre le broker,
après une suite unitaire restée verte. La primitive brute n'inscrit la paternité nulle
part ; c'est ce que `mintedHere` couvre désormais.

185 tests unitaires (dont quatre régressions : la propriétaire écrit, le destinataire non,
le store public sert tout demandeur, aucun membre non filtré ne transmet), e2e 40/40 et
applicatif 10/10.
2026-08-07 13:59:13 +02:00
Sylvain Duchesne c5878c6126 test: le contrôle de contrat disait oui à des noms absents
`isNamespaceMember` cherchait le nom dans **tout le texte** des modules de namespace,
commentaires compris. Un nom mentionné n'importe où comptait donc comme exporté.

Mesuré : `documentInboxAddress`, `escapeLiteral`, `getCaps`, `addLink`, `setCurrentUser`
passaient tous — et **`linkTo`** aussi, la fonction supprimée parce qu'elle brisait la
règle d'accès, dont l'absence est expliquée dans un commentaire de `surface/placement.ts`.
Le commentaire qui dit pourquoi elle n'est pas là suffisait à la faire passer pour
publiée.

C'est très probablement ainsi que la dérive du contrat a traversé cinq sections sans
rien faire rougir. Un contrôle qui dit oui à un nom absent est pire que pas de contrôle :
il se lit comme une vérification.

Les exports sont désormais extraits **après retrait des commentaires**, déclarations et
ré-exports nommés, par un seul `moduleExports()` que les trois contrôles partagent.
Vérifié : les six fantômes ci-dessus sont maintenant contrôlés, et les vrais membres
(`post`, `share`, `createEntityDoc`, `docCreate`) toujours reconnus.

Ajouté au passage un contrôle qui manquait dans l'autre sens : chaque groupe de namespace
de l'annexe doit correspondre **exactement** aux exports réels de son module. Un membre
retiré de `inbox:` ne faisait rougir personne.

Trouvé par une revue adverse, reproduit puis corrigé ici. 181 tests, 0 échec.
2026-08-07 13:32:49 +02:00
Sylvain Duchesne 0455a408b6 refactor(api): le bootstrap redescend de quatre appels à un
L'objectif acté était deux appels spécifiques au polyfill, voire un. Il en publiait
quatre. Chacun des trois de trop était une raison que la BIBLIOTHÈQUE a, pas un besoin
qu'une application a :

- **`configureStoreRegistry`** existait parce qu'il y a deux internes à câbler — le SDK
  injecté d'un côté, la session de l'autre. Vu de l'appelant, les deux disent « voici ce
  qu'il te faut pour tourner ». Replié dans `configure`, qui prend désormais
  `getSession` / `normalizeId` / `pointerGuard`.
- **`setCurrentUser`** n'a plus lieu d'être publié depuis que le portail d'accès est
  passé dans le polyfill : c'est lui qui pose l'identité. Et une application qui nomme
  sa propre identité est exactement le geste qui inverse le modèle — il ne doit pas
  exister d'appel publié vers lequel se tourner. Le harnais e2e, lui, joue plusieurs
  identités sur une même page ; il y accède par le chemin interne, ce qu'un harnais a
  le droit de faire et une application non.
- **`connectedUser`** est maintenant attendu DANS `ensureIdentity`. Ce n'était pas une
  commodité : la suite applicative avait montré qu'une app devait l'attendre elle-même,
  sinon une note qu'on venait de lui partager se lisait comme illisible. J'avais traité
  le symptôme dans l'app d'exemple ; le défaut était côté bibliothèque. En amont, ouvrir
  la session EST la connexion — aucune application n'attend un second appel.

Reste donc `configure({ … })`, plus `await ensureIdentity()` dont le site d'appel
survit à la migration : une application attendra toujours une session avant de rendre.

Le test étendu hier a fait son travail : les deux contrôles de contrat sont passés au
rouge sur `configureStoreRegistry`, `connectedUser` et `StoreRegistryDeps` dès que la
surface a bougé.

180 tests unitaires, e2e 40/40 (3,4 min) et applicatif 10/10 (0,8 min).
2026-08-07 12:06:15 +02:00
Sylvain Duchesne b98fcaa77d docs+test: le contrat avait dérivé — et le mécanisme ne voyait pas les règles
En vérifiant l'alignement de la surface, cinq sections du contrat s'étaient
désynchronisées du code sans que rien ne rougisse :

- § 1 montrait `getConfig`, `getStoreRegistryDeps`, `resetConfig` et
  `resetStoreRegistry` comme exportés — retirés à la fusion des portes ;
- § 11 documentait `escapeLiteral` / `escapeIri` / `assertNuri` comme publiés — ils ne
  le sont plus, et l'absence de garde de type est désormais expliquée par sa raison :
  les portes valident elles-mêmes (`NuriLike`), publier une garde inviterait le cast
  que les types servent à empêcher ;
- § 12 listait sept fonctions `storeRegistry` — il y en a cinq depuis que les deux
  fonctions d'ADRESSE d'inbox sont parties (une app nomme un document ou une personne,
  jamais une adresse) ;
- § 13 listait `IdentityStore`, `browserIdentityStore` et `getCurrentUser` comme
  publiés — retirés le 2026-08-05 ;
- `ensureIdentity` était publié **sans aucune règle**, et l'annexe renvoyait à un
  « § 2bis » qui n'existait pas.

**§ 2bis est écrit** : le portail d'accès n'a aucune contrepartie en substance — en
amont un utilisateur ouvre SON portefeuille et il n'y a rien à nommer — mais son SITE
D'APPEL survit, et c'est pourquoi sa signature ne prend pas d'identifiant : nommer son
identité est précisément la partie qui disparaît, donc elle ne doit pas figurer dans les
paramètres.

**Le mécanisme est étendu.** `test/vocabulary.test.ts` épinglait l'annexe — les NOMS —
et ne voyait pas les sections, là où vivent les règles. Une règle périmée est pire
qu'une règle absente : elle se lit comme vérifiée. Désormais tout `export` montré dans
un bloc « ### Today » doit être réellement exporté ; ce qu'on garde pour mémoire passe
en commentaire, que le contrôle ignore par construction. Les cinq dérives ci-dessus
auraient été rouges le jour même.

Nettoyé aussi : deux commentaires de doc orphelins dans `surface/placement.ts`,
restés au-dessus de l'accolade fermante après le retrait des fonctions qu'ils
décrivaient.

180 tests unitaires, typecheck bibliothèque / exemple / harnais.
2026-08-07 11:51:24 +02:00
Sylvain Duchesne aa6bbc436e test(e2e): les parcours applicatifs passent par l'app d'exemple
Une seconde suite e2e, `packages/sdk/e2e/notebook.ts` (`test:e2e:app`), qui pilote
`examples/notebook` par le DOM — une page de navigateur par identité — contre le même
broker réel.

**Pourquoi une seconde suite plutôt qu'un ajout dans la première.** `run.ts` parle à un
sac de méthodes sur `window.__sdk` : cela prouve que les fonctions TOURNENT, jamais
qu'une application peut s'écrire avec. L'écart a déjà coûté un défaut livré — l'inbox
d'un document était verte ici et inutilisable en pratique, parce que le harnais pouvait
faire traverser une adresse d'une identité à l'autre par une variable, canal qu'aucune
application n'a. Ici, rien ne traverse que ce qui traverse dans la vie : la RÉFÉRENCE
d'une note, recopiée d'un écran, et un identifiant tapé dans un champ.

Quatre parcours, qui se lisent comme des parcours :

- Bob lit la note publique d'Alice depuis sa seule référence — la propriété pour
  laquelle l'émulation du store public existe, vérifiée bout en bout et sans qu'aucune
  clé ne circule ;
- la note protégée d'Alice reste fermée jusqu'à ce qu'elle la partage — même geste côté
  Bob, issue opposée, décidée par où la note se trouve ;
- Bob laisse un message sur la note d'Alice, et seule Alice le lit — il TROUVE l'adresse
  depuis la note, personne ne la lui donne ;
- la liste de chacun ne contient que ses notes.

**Deux étapes quittent `run.ts`** (`documentInboxDeposit`, `capsShareCap`), avec un
commentaire disant où elles sont parties : ce sont des parcours, et ils valent plus joués
sur deux écrans que sur deux appels d'une même page. Ce qui reste là-bas est ce qu'une
application ne fait pas : primitives, caractérisation, régressions de démarrage à froid.

**Trois défauts trouvés en écrivant la suite**, tous côté application et invisibles pour
le harnais : `connectedUser()` devait être attendu à la connexion (sinon une note qu'on
vient de vous partager se lit comme illisible — ce qui ressemble à un problème de droits
alors que c'est un problème de moment) ; une réponse périmée restait affichée à côté
d'une question fraîche ; et changer de portée ne rafraîchissait pas la liste. L'app
affiche désormais la référence de chaque note — ce qu'aucun écran ne montre, aucun
utilisateur ne peut le faire circuler.

Corrigé au passage : le `.gitignore` pointait encore `packages/client/`, si bien que le
commit de renommage a embarqué le profil Playwright de la suite e2e (226 fichiers). Les
chemins sont réalignés et le commit précédent a été amendé — rien n'était poussé.

179 tests unitaires, e2e 40/40 (3,2 min) et applicatif 10/10 (0,7 min).
2026-08-07 11:20:58 +02:00
Sylvain Duchesne 0eb25286c8 refactor: renommer client → sdk, et fusionner les deux portes en une
Deux mouvements de surface, aucun changement de comportement.

**`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.**
« client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est
tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans
`docs/source-layout-by-fate.md` et le tableau des paquets du README.

**Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs —
`configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types
— vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`.

Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on
importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule
porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de
`docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc
lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par
`test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de
vocabulaire sur les noms publiés.

**Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le
choix visible plutôt qu'hérité :

- `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par
  `shared-wallet/bootstrap` ;
- `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes
  par leur chemin interne, ce qui est leur raison d'être ;
- le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux
  fois brouillait la frontière qu'elle servait à marquer.

Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` /
`hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au
permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait
`capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README
de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire.

179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le
broker en ligne — mesuré une fois après le renommage, une fois après la fusion.
2026-08-07 11:16:57 +02:00