0d9e2bbe979674be5a2a1b35491d14bbb96ca830
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0d9e2bbe97 |
fix: mon correctif sur createEntityDoc était faux dans les deux sens
Second tour adverse sur le lot C. Six trouvailles, dont trois sur ce que je venais de livrer. **Le correctif de `createEntityDoc` reproduisait le défaut qu'il annonçait avoir fermé.** Il levait sur la PREMIÈRE écriture de registre en échec. Or : - lever sur le listing sautait l'écriture de la clé — détruisant le chemin de récupération que le commentaire d'à côté décrit explicitement (« la clé doit rester récupérable même si le listing a échoué »), et laissant le document orphelin ; - lever sur la clé laissait le document LISTÉ sans clé — précisément l'état « se lit vide pour toujours » que je prétendais empêcher, en pire, puisque l'appelant n'a même plus sa référence. Les deux écritures sont désormais tentées, ce qui atterrit reste, et l'échec est rapporté après en nommant la moitié manquante. **Le refus de `share` reposait sur une valeur qui confond absence et ignorance.** `resolveAccount` avale toute erreur de lecture et rend `null`, si bien qu'un incident réseau faisait répondre « personne ne s'est connecté sous ce nom » à propos de quelqu'un qui existe. `lookupAccount` propage désormais l'erreur ; `resolveAccount` reste la forme tolérante que tous les autres appelants veulent. **J'avais livré ce comportement sans un seul test.** `test/app-surface.test.ts` en ajoute huit, tous sur ce qu'un APPELANT voit : `ensureIdentity` rend l'identité, un appel de placement avant connexion nomme l'erreur, le placement agit comme l'utilisateur connecté, `share` refuse un nom inventé mais laisse remonter une panne, et une création à moitié écrite échoue en disant quelle moitié — dont le cas « le listing a échoué, la clé est quand même là ». En écrivant ces tests j'ai refait dans leur faux la faute que cette revue a corrigée ailleurs : ignorer le sujet dans la requête de compte, ce qui rendait le dossier d'un autre utilisateur. Deux des huit échouaient pour cette raison, sans rapport avec le code. **Et la documentation contredisait le code livré dans le même commit** : le README enseignait encore `createEntityDoc(me, "protected")` — en JS la portée devient `"alice"` — et le contrat déclarait `Nuri` là où le code et la feuille disent `NuriLike`. 197 tests unitaires, e2e 40/40 et applicatif 12/12. |
||
|
|
cdc09a1a1d |
refactor(api): l'application ne nomme plus son identité — elle l'apprend
Lot C de la revue adverse. Cinq corrections, dont une qui change la forme de la surface. **L'application ne pouvait pas obtenir son identité par l'API.** `ensureIdentity()` rendait `void`, `getCurrentUser` n'est plus publié — et pourtant `createEntityDoc(id, …)` et `listMyEntityDocs(id, …)` l'exigeaient. L'app d'exemple s'en sortait en lisant `localStorage["ng-eventually:identity"]` et le paramètre `?ng-id`, deux constantes PRIVÉES du portail d'accès. Une frontière qu'aucun consommateur ne devrait voir, et encore moins dont il devrait dépendre. Vérifié au niveau 2 avant de trancher : `session_start(wallet_name, user_id)` prend l'identité — donc en amont l'application la DÉTIENT, elle la tient du portefeuille qu'elle a ouvert. Ici c'est le portail qui la choisit, donc c'est au portail de la rendre. Deux changements, tous deux vers la cible : - `ensureIdentity()` rend l'identité qu'il a établie ; - `createEntityDoc(scope)`, `listMyEntityDocs(scope)`, `resolveWriteGraph(scope)` perdent leur paramètre d'identité. En amont `doc_create(session_id, …)` ne porte aucun utilisateur : une session EST celle d'un utilisateur. Passer la sienne à chaque appel de placement était un geste sans successeur. L'application garde l'identité pour l'afficher, et ne la passe plus à rien. **`inbox.share` provisionnait un destinataire inexistant.** Une faute de frappe créait les trois stores et l'inbox de ce nom, et la clé atterrissait où personne ne regarde — sans la moindre erreur. En amont on ne peut pas viser un nom qu'on invente : un dépôt est scellé vers une clé d'inbox qui vous est parvenue par un contact entrant. Refuser est fidèle ; provisionner était l'invention. **`createEntityDoc` avalait l'échec de ses deux écritures** et rendait quand même une référence — le document n'était dans aucun store, donc la session suivante ne le listait pas et sa lecture rendait vide, en silence. Il lève maintenant, comme `doc_create` en amont propage les siennes. **Deux entrées prenaient `Nuri` au lieu de `NuriLike`** (`inbox.watch`, `openDocumentInbox`), ce qui contredisait la raison même pour laquelle aucune garde de type n'est publiée. Et **deux messages d'erreur nommaient des symboles retirés** (`storeRegistry.documentInboxAddress`, `setCurrentUser`) : une erreur qui envoie vers une fonction inexistante est pire qu'une erreur muette. Contrat d'API et feuille `contract_sdk-surface` mis à jour ; `readForDocument` et le refus de `share` obtiennent enfin leur règle en §9. 189 tests unitaires, e2e 40/40 et applicatif 12/12. |
||
|
|
b7dc8ca2c3 |
fix: la suite n'était pas hermétique, et deux tests ne pouvaient pas échouer
Second tour adverse sur le lot D. Trois trouvailles, et une erreur de diagnostic de ma part qui vaut d'être consignée. **La suite verte dépendait de l'ordre des fichiers.** `bun test isolation-active public-store` donnait 5 échecs quand chaque fichier seul était vert — donc un checkout de CI avec un autre ordre d'inodes livrait rouge. Deux causes distinctes : - le travail de connexion, lancé sans être attendu par `setCurrentUser`, débordait d'un fichier sur le suivant et armait l'émulation. `connectedUser` abandonne désormais dès que l'identité pour laquelle il a démarré n'est plus connectée — ce qui est de toute façon la bonne sémantique : en amont une session appartient à un utilisateur, et changer d'utilisateur est une autre session ; - et surtout **mon propre test de store public exposait le cap d'un document que personne ne détient** — un état que la bibliothèque ne produit jamais. Il ne passait que tant que l'émulation était désarmée. Alice crée sa note avant de l'exposer, maintenant. Balayage des 21 paires de fichiers : plus aucune ne pollue. **Le contrôle symétrique ajouté hier ne pouvait pas échouer.** « La liste d'Alice ne contient pas la note de Bob » lisait un rendu ANTÉRIEUR à l'écriture de Bob : l'attente de `showScope` était satisfaite au premier sondage par le marqueur déjà à l'écran, sans synchroniser quoi que ce soit. Alice écrit désormais une note APRÈS celle de Bob — `writeNote` attend son apparition, donc ce qui suit est un rendu qui post-date. Et le `.catch` qui avalait le délai d'attente est retiré : une liste qui ne se stabilise jamais est un échec à voir, pas une dégradation à absorber. **Le test anti-fork prouvait « pas le premier », pas « le canonique ».** Son minimum lexicographique était aussi le DERNIER élément, si bien qu'un choix positionnel — la faute exacte que ce test existe pour attraper — restait vert. Le minimum est déplacé au milieu ; vérifié par mutation, « prendre le dernier » le fait rougir. **Mon erreur de diagnostic.** J'ai cru trouver, sous la trouvaille d'ordre, une fuite entre utilisateurs — les caps d'Alice classés chez Bob — et je l'ai « reproduite ». Le repro était faux : son faux `ng` ignorait le sujet dans la requête d'inbox, donc l'inbox de Bob résolvait vers celle d'Alice. Une fois le faux corrigé, la fuite ne se reproduit plus, ni avec ni sans correctif. Le danger reste réel en lecture du code — trois chemins classent des caps plusieurs `await` après la garde qui les autorisait — donc `caps.holderKey`/`learnFor` le ferment par construction, mais les commentaires disent maintenant ce que c'est : un risque fermé, pas un défaut observé. 189 tests unitaires, e2e 40/40 et applicatif 12/12. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |