1271d48e9f8aeb50778cb5525e58b9dd502d0629
140 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1271d48e9f |
refactor(e2e): la mécanique de test devient un paquet à part, ng-e2e-helpers
Créer un portefeuille, en obtenir le .ngw, traverser le broker : ce n'est pas du ressort du polyfill. C'est un besoin commun au polyfill et à toute application NextGraph — et surtout, ça SURVIT à la migration, alors que le polyfill est fait pour disparaître. L'y laisser, c'était le faire mourir avec lui ou rendre le polyfill indéracinable. Le paquet n'importe rien du polyfill — vérifié mécaniquement — et déclare Playwright et @ng-org/web en pairs, le consommateur devant maîtriser les versions. Sa surface : attentes bornées, mesure, navigateur, profils, portefeuille, traversée du broker, rapport d'exécution, et reconnaissance des modes de panne connus. La preuve qu'il est utilisable de l'extérieur : le polyfill le CONSOMME, sans garder de copie. Restent chez lui les parcours, la barrière et les identités virtuelles, qui lui sont propres. Le verrou entre exécutions disparaît, remplacé par un profil par exécution. Il ne traitait qu'un symptôme — un répertoire partagé que la création de portefeuille effaçait. Avec un profil par exécution il n'y a plus rien à sérialiser, les exécutions concurrentes deviennent indépendantes, et la collision entre deux dépôts s'évanouit au lieu d'être exportée. Six exécutions : aucun répertoire ni Chromium orphelin. Et la connaissance descriptive est séparée du pilotage : URL, sélecteurs et inventaire ordonné des écrans sont des données, passées DANS la page pour la reconnaissance — donc un échec nomme le même écran que celui sur lequel on dispatchait. Un échec de navigateur est désormais nommé comme tel — « the actors browser STOPPED ANSWERING » — au lieu de sortir sous le nom de l'opération innocente qui se trouvait en vol. |
||
|
|
cf3c7c7d8b |
docs: une borne englobante plus courte que ses étapes rend tout échec muet
La leçon du tour, et elle valait des jours : SIGN_IN_MS valait 180 s sur des étapes totalisant 270 s. La borne du dessus se déclenchait donc toujours la première, et chaque échec rapportait son nom à elle — jamais celui de l'étape en cause. On a cherché une cause que le harnais était structurellement incapable de nommer. D'où les deux règles consignées : calculer une borne englobante à partir de ses parties au lieu de choisir un nombre, et dimensionner chaque borne terminale sur une durée MESURÉE inscrite à côté d'elle. Un chiffre nu ne dit pas s'il est généreux ou serré, et pourrit sans que personne le voie. Plus une troisième : déclarer les parcours et leurs vérifications avant que quoi que ce soit puisse échouer, pour qu'une exécution rende toujours le même nombre de lignes. Quand le total bouge avec la panne, deux exécutions ne sont plus comparables — et un total qui rétrécit se lit comme un problème plus petit alors qu'il est plus gros. Enfin, un fait observé : notre verrou ne garde que ce dépôt. Une suite appartenant à une application consommatrice, lancée depuis son propre checkout contre le même broker, entre en collision exactement comme deux des nôtres — et c'est invisible des deux côtés. |
||
|
|
ed0f872f5d |
test(e2e): un échec ne peut plus emporter les suivants, et les bornes sont mesurées
Trois choses, dont une qui explique pourquoi aucun diagnostic n'aboutissait. SIGN_IN_MS valait 180 s alors que la somme de ses propres étapes en faisait 270. La borne englobante se déclenchait donc TOUJOURS avant celle de l'étape en cause, et ne pouvait dire qu'une chose : « bob-… to sign in ». Le message était structurellement condamné à ne rien apprendre — on a cherché des jours une cause que le harnais s'interdisait de nommer. Les bornes englobantes sont maintenant des sommes calculées de leurs étapes. Les parcours sont isolés. La suite déclare ses 7 parcours et leurs 27 vérifications AVANT tout lancement de navigateur, et rend donc toujours 34 lignes — y compris quand le montage meurt, où les parcours non exécutés sont rapportés comme tels. Auparavant le total valait 24, 26 ou 27 selon ce qui mourait : deux exécutions ne mesuraient même pas la même chose. Un échec est contenu, pas absorbé — il reste compté. Et chaque borne est dimensionnée sur une durée MESURÉE, inscrite à côté d'elle dans le code. Le premier rendu d'un acteur prend 4,9 à 7,4 s et vaut 45 s ; la traversée du broker 1,3 à 2,8 s et vaut 75 s. Un nombre nu n'apprend rien et pourrit en silence. Au passage : un walletPage.close() n'avait aucune borne du tout. |
||
|
|
1ecf511e9d |
docs(contract): un rejet veut dire « inconnu », jamais « absent »
Le contrat n'énonçait qu'un cas particulier — createEntityDoc lève si le document ne peut pas être enregistré — alors que c'est devenu la règle de toute une famille d'appels. Ceux qui doivent d'abord SAVOIR si une chose existe avant d'agir lèvent désormais quand ils n'ont pas pu le savoir, au lieu de continuer comme si la réponse était « rien ». C'est ce qui a cessé de fabriquer une seconde inbox, un compte dupliqué ou une racine de registre en double. La conséquence est actionnable et doit être dite : un rejet ne signifie pas que la chose est absente, il signifie qu'on l'ignore. Le relancer ou le remonter, mais surtout ne pas le lire comme un résultat vide — ce qui reviendrait à réintroduire côté appelant exactement le défaut qu'on a retiré. |
||
|
|
f30685bdb9 |
docs: la doctrine enseignait le réflexe qui a coûté la journée
La feuille disait : « si la suite rapporte un délai nommé plutôt qu'une assertion échouée, soupçonner le transport avant le code ». C'est faux, et je l'ai écrite hier. Un délai dit seulement qu'une chose n'est pas arrivée à temps. Il ne dit jamais pourquoi — et se tourner vers l'environnement est la réponse confortable, puisqu'elle exonère le code. Le cas est maintenant raconté dans la feuille : tous les sign-in expiraient, on a accusé le broker et le réseau de l'hôte pendant des heures, et la cause était une correspondance de sous-chaîne sur une URL, lisible depuis le début. Le propriétaire du dépôt a tranché contre cette attribution — « je n'ai jamais eu de problème avec le broker, les tests si » — et il avait raison. La discipline devient : lire son propre harnais d'abord, et ne parler de transport qu'une fois le mécanisme nommé. |
||
|
|
7062364569 |
fix(e2e): le harnais reconnaissait la page du broker comme l'application
Le prédicat qui cherchait l'iframe applicative était une correspondance de
sous-chaîne : f.url().includes("127.0.0.1"). Or la page d'authentification du
broker porte l'adresse de l'application DANS SA PROPRE requête —
nextgraph.eu/auth/#/?o=http%3A%2F%2F127.0.0.1%3A39975. Elle correspondait donc
dès le premier instant.
Conséquence en chaîne : la boucle d'attente sortait immédiatement, le clic sur
le portefeuille et la saisie du mot de passe étaient sautés comme « déjà
connecté », et la frame principale du broker était rendue à l'appelant comme si
c'était l'application. Celui-ci attendait alors une minute un élément qui
n'existe pas sur cette page.
Ce qui sauvait une exécution était le clic sur « Login », qui change l'URL et
lui retire le paramètre — gardé par une sonde de 2 s sur un bouton mesuré à
1,0–1,6 s d'affichage. Ce tirage au sort était toute l'intermittence, et il
expliquait l'asymétrie : le premier acteur fait la cérémonie du mot de passe,
les suivants non, le portefeuille étant déjà ouvert et diffusé entre les onglets
du broker par BroadcastChannel — vérifié, 3,4 s contre 1,4 s.
Le harnais attend désormais des ÉTATS, plus des durées : il énumère les écrans
possibles, attend celui qui se présente, et aiguille — sous une seule échéance.
Plus aucun waitForTimeout dans ce chemin. Le chemin sans mot de passe est une
branche attendue de plein droit.
Et un échec nomme maintenant le dernier écran reconnu, l'origine attendue, la
trace horodatée des écrans traversés, les URL de toutes les frames et le texte
visible. Les échecs d'aujourd'hui ne disaient qu'une chose : qu'une chose
n'était pas apparue. C'est ce silence qui a coûté la journée en conjectures.
J'avais attribué tout ça au broker. C'était faux, et c'était lisible dans le
code.
|
||
|
|
12eba6eea6 |
fix: quatre écarts entre la surface publiée et ce que NextGraph déclare
Un audit de la surface contre la source amont en a trouvé cinq ; voici les
quatre mécaniques. La cinquième — l'adresse d'inbox, qui traverse sept symboles
— relève du dessin et reste ouverte.
L'identifiant de session bloquait. Amont le déclare string | number
(sdk/js/web/src/index.ts:16) et le binding désérialise un u64 ; nous exigions
une chaîne. Une application ne pouvait donc pas passer la valeur que le SDK
venait de lui remettre. Élargi à ce qu'amont déclare, sur toute la chaîne, et
jamais converti : une chaîne échoue pour de vrai (Deserialization error of
session_id JsValue("1"), observé).
sparqlUpdate annonçait Promise<void> alors qu'il relayait DÉJÀ les commits.
C'était donc un mensonge de typage, pas un comportement — et la doublure de test
qui rendait undefined, un état que le vrai broker ne produit jamais, est ce qui
l'a laissé sans contradicteur.
ng était publié en Record<string, any>, ce qui perdait les 88 membres typés
d'amont — 88, pas 77 : le chiffre de notre propre documentation était faux.
Et materialize, second nom publié de read, sans appelant ni contrepartie amont,
est retiré.
docs/api-contract.md qualifiait docs.* de passthrough « 1:1 ». C'était faux sur
les deux premiers points. Corrigé, pas complété : un document qui se déclare
vérifié et qui ment est pire qu'un document absent, parce qu'on cesse d'aller
voir.
Une déviation assumée : amont type le retour en any, interdit ici ; on rend
unknown, comme sparqlQuery le fait déjà pour le même any amont.
|
||
|
|
e32b6d04fc |
fix: un échec de lecture ne fabrique plus d'état durable et faux
userInbox avalait deux échecs, et chacun laissait des dégâts sur le disque. À la lecture : la requête « quelle inbox ce compte possède-t-il » levait, on journalisait, et on créait une inbox de plus. Deux associations pour un même couple utilisateur/portée — vérifié en conditions, sdoc5 et sdoc6 coexistants. À l'écriture : l'INSERT levait, on retournait quand même la référence en la mettant en cache. Le propriétaire tenait sdoc5 pendant qu'un déposant résolvait sdoc6. Il lit une boîte où nul n'écrit, ils écrivent dans une boîte que nul ne lit. Et un troisième que je n'avais pas vu : recordInbox avalait son propre INSERT puis marquait son index en mémoire — l'inbox était associée mais refusée à la session suivante, donc tous les dépôts rebondissaient, définitivement. Le balayage demandé a trouvé la famille entière : onze sites de cette forme, un catch qui journalise puis une exécution qui continue comme si la chose cherchée était absente. Les huit autres corrigés vont d'une seconde racine de registre créée sur budget épuisé, à un document public qui ne sert plus jamais sa clé. La règle appliquée partout : seule une absence VÉRIFIÉE autorise à créer, et une référence n'est remise à personne avant que son association ait atterri. Les sites laissés échouent en fermeture — un refus, une liste vide — sans rien écrire. Ils sont listés, pas oubliés. |
||
|
|
0f1e493e93 |
docs(contract): ensureIdentity lève quand la connexion n'a pas abouti
Le contrat disait qu'il « termine le travail de connexion qu'il entame », sans dire ce qui arrive quand il n'y parvient pas. Depuis que les échecs remontent, il lève — et c'est actionnable : une application qui rend son interface malgré un rejet affiche un compte vide, pas un écran vide, ce qu'aucun utilisateur ne sait interpréter. Dans l'autre sens c'est une garantie renforcée : résolu signifie que tout ce qui vous a été partagé est lisible. |
||
|
|
8c8ade7a9e |
fix: une connexion qui échoue ne se résout plus comme une réussite
connectedUser() restaure les capacités reçues et draine les inbox. Aucun de ses chemins ne pouvait échouer : un broker injoignable rendait exactement la même promesse qu'un succès complet. L'application affichait alors des listes vides, et rien nulle part ne disait que la restauration n'avait pas eu lieu. L'énumération m'avait échappé sur deux points, l'agent les a établis. resolveAccount attrapait tout et rendait null : une lecture qui ÉCHOUAIT ressortait donc comme « ce compte n'existe pas ». L'échec était déguisé en absence — c'est la racine du partage cassé trouvé ce matin, dont on n'avait traité que le déclencheur. lookupAccount le remplace : le silence n'est plus possible que sur une absence VÉRIFIÉE. Et readLinks comme readInboxCapPairs avalaient leur propre erreur en rendant un tableau vide, un étage sous le catch de connect. Une panne n'y parvenait même pas. Elles relèvent désormais. La règle est simple : tout échec remonte, seul « il n'y avait rien à faire » se résout en silence. Ce qui reste silencieux — aucun détenteur, compte réellement absent, identité changée en route — l'est parce que c'est la vérité. Le piège consigné hier est fermé par là même : une exécution qui ne peut pas répondre rejette, et ceux qui la rejoignent en héritent. Sa feuille est supprimée, la question qu'elle laissait ouverte étant tranchée. Trois fixtures de test utilisaient un ng vide — une forme qu'aucune plateforme ne présente, dont le TypeError était mangé par le catch. Remplacées par un broker au portefeuille vide. Aucune assertion modifiée. |
||
|
|
55714d0a23 |
fix: trois coûts qui revenaient à l'appelant reviennent au paquet
Le contrat faisait porter à l'application trois choses qui sont des artefacts de notre implémentation, pas de la cible. Le rechargement de page. Au retour depuis le cache du navigateur, la barrière se rechargeait pour rejouer init() — et détruisait au passage l'état de l'application, qui ne pouvait ni s'y opposer ni nettoyer avant. Le paquet détenait pourtant ce qu'il fallait : la fonction init injectée et le callback de l'appelant. Il enregistre désormais sa délégation, ranime sa barrière au retour — champ conservé, bouton réactivé — et redélègue à la confirmation. Rien hors de la barrière n'est touché. Vérifié dans le bundle amont : en page de tête, init navigue à chaque appel, sa garde « une seule fois » ne portant que sur la branche iframe. L'ordre d'appel silencieux. ensureIdentity() attendu avant init() ne se résolvait jamais, sans erreur. Le paquet possédant la session, il distingue maintenant les deux cas sans délai ni heuristique : session pas encore arrivée → il attend ; init jamais appelé → elle n'arrivera pas, il lève en nommant l'appel à faire d'abord. Et la clause qui annonçait la barrière était rangée dans les exigences de déploiement, alors qu'une application n'y peut rien. Elle passe dans les garanties, avec ce qui la remplace : la page n'est jamais rechargée. Il reste deux lignes d'exigences : servir le fichier de portefeuille, et appeler init avant d'attendre l'identité — ce qui échoue désormais bruyamment. |
||
|
|
f77317c4d1 |
docs(contract): trois comportements que l'appelant doit connaître
Les signatures étaient à jour, les affirmations non. Trois choses visibles par une application n'y figuraient pas. L'ordre d'appel : ensureIdentity() se résout quand une session est ouverte, donc l'attendre avant d'avoir appelé init() ne se résout JAMAIS. Silencieux, sans erreur — le pire mode de défaillance possible pour qui intègre. C'est aussi la raison de dire lequel des deux init() appeler, comme on le dit déjà pour ng. La barrière monte à chaque chargement en page de tête, pas seulement au premier. Et la page peut se recharger toute seule quand on y revient : une application qui garde de l'état en mémoire le perd, et ne pouvait pas le deviner. Aucune de ces phrases ne décrit comment c'est fait — seulement ce qu'un appelant doit prévoir. |
||
|
|
dbd99738f0 |
docs: deux affirmations que les mesures ont démenties
La doctrine disait que la suite applicative ne peut pas être mesurée sur une machine dont le réseau bouge. Elle vient de passer 27/27 quatre fois d'affilée sous exactement ce bruit. Ce qui tranche n'est donc pas l'état de la machine mais la FORME de l'échec — un délai nommé sur une opération broker désigne le transport, une assertion qui rend une valeur inattendue désigne le code — et la répétition. Et la feuille sur le règlement de l'identité décrivait encore la session comme un thunk fourni par l'application. Elle appartient désormais au paquet, dont le wrapper init() capture l'événement : plus aucune application ne peut la câbler de travers, ce qui était pourtant la cause exacte de la régression racontée juste au-dessus. Dette de doc soldée. |
||
|
|
cc8a95d303 |
feat: le polyfill possède la session et la normalisation des identités
Pour démarrer, une application devait écrire une promesse autour du callback
d'init(), attraper l'événement loggedin, puis fournir un thunk getSession qui
dépiaute session_id et les trois identifiants de store dans notre forme. Plus un
normalizeId. C'est précisément la plomberie que ce paquet existe pour absorber :
chaque application la réécrirait à l'identique, et c'est elle qui a produit deux
défauts aujourd'hui — un blocage et un partage cassé en silence.
En amont, une session est RENDUE ; une application n'en assemble jamais une à
partir de champs bruts. Et les identités virtuelles sont une invention du
polyfill, donc leur normalisation lui appartient.
Le wrapper init() enveloppe désormais le callback de l'appelant : il capture
l'événement, en dérive la session, puis appelle le callback avec le même
événement. Le paquet n'appelle jamais init de sa propre initiative — il
l'enveloppe. Sans callback, il capture quand même.
getSession et normalizeId quittent la surface publiée. Le chemin d'injection
reste pour les harnais, mais inatteignable depuis l'entrée : vérifié par un
import à l'exécution et par un configure() refusé à la compilation.
Défaut trouvé et corrigé en route : le broker envoie session_id en NOMBRE, et le
convertir en chaîne faisait refuser tous les appels par le binding wasm. La
valeur ne fait que transiter, elle est relayée telle quelle. Reste que toute la
chaîne la type string — inexactitude antérieure à ce commit, à traiter à part.
Une application écrit maintenant : configure({ ng, useShape, init, sharedWallet }).
|
||
|
|
7a4d9b492f |
refactor: une application ne fournit pas sa propre identité
configure() acceptait un champ currentUser publié. Il faisait manier à l'appelant ce que la cible ne lui remettra jamais : en amont, session_start reçoit un identifiant qui VIENT du portefeuille qu'une personne a ouvert. Et il rouvrait la porte qu'on venait de fermer : renseigné, il fait sauter la barrière en page de tête — exactement le comportement retiré au commit précédent. Un commentaire disait « rien de ce qu'expédie une application ne fait ça » ; c'est une note, pas un mécanisme, et une exception nommée cesse d'en être une dès qu'elle est publiée. Personne ne s'en servait — aucun test, aucun harnais, aucun exemple. Un symbole gardé parce qu'il était déjà là n'est pas une décision. Le chemin interne subsiste pour les harnais qui pilotent la surface sans navigateur, mais il n'est plus atteignable depuis la surface publiée. |
||
|
|
218c9ab6b3 |
docs: la doctrine dit quand la barrière s'affiche, pas seulement ce qu'elle fait
La feuille décrivait l'ordre de résolution de l'identifiant mais taisait la règle qui décide de l'affichage — page de tête toujours, iframe jamais — et c'est exactement ce qui vient d'être corrigé dans le code. Elle porte désormais le raisonnement : le discriminant est le cadre parce que l'identifiant est un état observable alors que la présence d'un portefeuille ne l'est pas, et qu'un écran conditionnel doit donc deviner l'état qui compte. Plus les deux faits observés sur les sites réels qui ferment les alternatives — l'import ouvre un sélecteur sur place, et notre onglet ne reçoit aucun signal quand il réussit. |
||
|
|
f5a3adc385 |
fix: la barrière s'affiche à chaque chargement en page de tête, comme chez Festipod
Notre version décidait d'afficher la barrière sur la présence de l'identifiant. Festipod décidait sur la session — jamais établie en page de tête, donc l'écran s'affichait toujours, l'identifiant servant seulement à pré-remplir le champ. La différence n'est pas ergonomique. L'identifiant est un état qu'on observe ; le portefeuille, lui, vit dans le stockage d'une autre origine et nous est illisible. Un écran conditionnel doit donc DEVINER cet état invisible — et quand il devine « déjà installé » alors que le portefeuille a disparu du navigateur, il cache les seuls contrôles qui répareraient la situation et précipite la personne dans une impasse. Impasse observée sur le site réel : sans portefeuille, la page du broker affiche un texte statique, zéro bouton, un seul lien vers nextgraph.eu qui NE TRANSPORTE AUCUN retour vers l'application. Le retour arrière du navigateur est la seule issue — et il ne sert à rien si la barrière ne reprend pas la personne à l'arrivée. Le discriminant devient le cadre, pas l'identifiant : page de tête → toujours, iframe → on s'efface. C'est le signal que @ng-org/web utilise lui-même et que Festipod utilisait un étage plus bas. On ne détecte rien et on ne demande rien. Les trois étapes s'affichent toujours ; qui possède déjà son portefeuille ignore les deux premières. Aucune case « je l'ai déjà » : savoir si l'on a importé un portefeuille dans ce navigateur est une question trop technique pour être posée. Garde-fou repris de Festipod, qu'on n'avait pas : après un aller-retour vers l'onglet NextGraph et un retour arrière, la barrière restait figée sur un bouton mort. Elle recharge désormais sur pageshow persisted — seul moyen de rejouer le init() qui porte la redirection. Vérifié sur navigateur : cliquer « Import a Wallet File » ouvre un sélecteur sur place, sans navigation ni changement d'onglet, et notre onglet ne reçoit AUCUN signal quand l'import réussit. Détecter le retour est donc impossible, pas seulement fragile. |
||
|
|
c5b4703687 |
test(e2e): le parcours d'un primo-arrivant, et un harnais qui échoue au lieu de se pendre
Aucun parcours n'avait jamais marché le chemin d'un nouvel arrivant : tous pré-injectaient l'identifiant dans l'URL, ce qui fait résoudre l'identité sans jamais afficher la barrière. La suite était verte par-dessus un lien de téléchargement pointant sur un fichier que personne ne servait — et le serveur de test répondait la page HTML de l'application pour tout chemin inconnu, donc un fichier manquant ne POUVAIT pas échouer. Le nouveau parcours part d'un profil vide : barrière, téléchargement réel, import dans l'application portefeuille, saisie de l'identifiant, remise au broker, retour dans l'iframe. Il vérifie neuf points, dont celui qui compte — l'identifiant a survécu et l'identité rapportée est celle qui a été saisie. Le harnais, lui, se pendait au lieu d'échouer. Cause observée : le tuyau devtools de Chromium lâche et Playwright n'émet ni close ni disconnected, si bien que la suite bloquait dans son propre nettoyage sans imprimer ni résumé ni l'échec déjà en route. Toutes les attentes sont désormais bornées et nomment ce qu'elles attendaient ; vérifié en cassant délibérément une attente, et observé en conditions réelles — trois minutes et « gave up waiting for: alice to sign in » là où j'ai tué trois exécutions d'une heure ce matin. Deux exécutions simultanées ne se détruisent plus : verrou atomique sur le profil, et récupération d'un navigateur laissé par une exécution tuée. Le marqueur devient .user-consumed — il n'a jamais attesté d'une disponibilité, seulement qu'un lot avait déjà pris l'utilisateur de ce profil. Au passage, la destruction du profil dépendait du marqueur, écrit en FIN de lot : une exécution tuée avant laissait un profil que la suivante réutilisait, et héritait de sa casse. Elle dépend maintenant du profil. La suite applicative reste non mesurée sur cette machine : un conteneur en boucle de redémarrage recycle son interface réseau, et sept exécutions sur dix échouent sur le transport. Trois sont passées 21/21. |
||
|
|
7c2e8d8f1f |
docs: deux concepts pour ce que la journée a appris
sign-in — comment un utilisateur passe de rien à une identité qui agit. La barrière, l'identifiant qui franchit une frontière de partition par l'URL, et surtout : la redirection vers le broker appartient à @ng-org/web, vérifié dans son bundle. Le polyfill ne la réimplémente pas ; ce qui lui revient est la seule chose qu'init() ne peut pas faire, écrire l'identifiant dans l'URL avant qu'il ne la lise. La feuille centrale dit pourquoi régler l'identité et se connecter sont deux actes séparés : l'un ne demande aucune session, l'autre en exige une, et les confondre bloque dans un sens et casse le partage en silence dans l'autre. Le piège encore vivant — une connexion abandonnée qui reste joignable — est consigné comme tel, non corrigé. e2e-harness — ce que chaque suite juge, et deux choses qu'un agent doit savoir avant de diagnostiquer : le raccourci qui pré-injectait l'identifiant dans l'URL a tenu deux défauts invisibles pendant des mois (un lien de téléchargement vers un 404 que rien ne servait, et une barrière inatteignable pour tout nouvel arrivant) ; et un tuyau devtools qui lâche tue une exécution sans que Playwright n'émette d'événement, ce qui ressemble à un défaut produit et n'en est pas. Le vocabulaire gagne settle, barrier, journey et batch. J'avais aussi introduit « hand-over » pour la redirection : retiré, c'est le mot de NextGraph qui l'emporte. |
||
|
|
16e24f67f9 |
refactor: les commentaires disent cap-surface et cap-enforcement
Suite du balayage commencé dans la doc : 20 occurrences de P1a/P1b dans les commentaires, les titres de tests et le README. Les phrases ont été récrites, pas substituées : « the breach P1a opened » devient « the breach that cap-surface opened », et « labelled P1b's » ne survivait pas à un nom plus long. Un lecteur qui n'a jamais entendu ni l'un ni l'autre doit comprendre la phrase. L'avertissement de déploiement du README garde sa force et gagne un nom : « "anonymous" or "private" until cap-enforcement lands per-document encryption. » Reste une occurrence dans e2e/polyfill-entry.ts, qui part avec le lot e2e. |
||
|
|
2726f4a26f |
docs: nommer par la fonction, et n'annoncer qu'un point d'entrée
Deux corrections indépendantes dans la doc vivante, les briefs et décisions
datés restant tels qu'écrits.
P1a et P1b ne disaient rien à personne. Six mois plus tard il aurait fallu lire
le code pour savoir de quoi on parle, et le coût de la recherche se repaie à
chaque lecture. Ils deviennent cap-surface — la forme des capacités, livrée le
2026-07-28 — et cap-enforcement — ce qui reste : le chiffrement par document et
les gardes d'écriture aujourd'hui décoratives. 28 occurrences.
Et api-contract.md se contredisait à quatre lignes d'intervalle : il annonçait
deux points d'entrée en tête, et en bas qu'il n'y en a qu'un depuis la fusion du
2026-08-07. Vérifié dans package.json avant d'écrire — exports mappe exactement
{".": "./src/index.ts"} et src/polyfill.ts n'existe pas.
Ce qui identifie un symbole polyfill-era ne change pas : le bloc marqué dans
src/index.ts et le test de vocabulaire, plus aucun chemin d'import.
|
||
|
|
3be8da2178 |
fix: régler l'identité n'a plus le droit de réclamer une session
Le partage était cassé : Bob n'ouvrait pas le document qu'Alice venait de lui partager, sans erreur, juste « (illisible) ». Régression introduite en scindant ensureIdentity(). Chaîne observée aux sondes, pas déduite : settleIdentity() appelait setCurrentUser, qui déclenche startConnect(), qui va chercher la session via le thunk getSession de l'application. Or l'exemple appelle init() DEPUIS L'EXÉCUTEUR qui construit sessionReady — le thunk ne peut donc pas répondre, par construction. Il lève, resolveAccount rend null, l'exécution est abandonnée sans restauration ni drainage, mais s'est déjà enregistrée « en vol ». Le ensureIdentity() suivant rejoint cette exécution morte et se résout sans avoir rien fait. Avant la scission, rien n'appelait setCurrentUser pendant l'évaluation du module : la session existait, l'exécution était saine, et la rejoindre était sans danger. C'était bien une affaire de moment. bootstrap.ts scinde le setter : adoptCurrentUser enregistre qui agit, setCurrentUser reste « enregistrer + connecter » pour tous les autres appelants. La moitié sans session ne réclame donc plus de session, et se connecter redevient l'affaire du seul ensureIdentity(), attendu, là où une session existe. Ce que ça bloque, tracé avant de livrer : une application qui appellerait init() sans jamais appeler ensureIdentity() n'aurait plus de restauration en arrière- plan. Aucun appelant de ce genre n'existe, et avant la scission init() était un passthrough nu qui ne déclenchait rien — c'est une répartition rétablie, pas un comportement retiré. Reste connu, non corrigé : connectedUser mémorise toujours une exécution abandonnée. Le piège est documenté sur setCurrentUser. |
||
|
|
3547de202c |
fix: régler l'identité ne demande pas de session, se connecter oui
init() de @ng-org/web redirige vers le broker en première instruction, dès qu'on est en tête. L'application appelait donc init() au chargement du module, la page partait, et ensureIdentity() ne s'exécutait jamais : la barrière n'apparaissait pas, ?ng-id= restait absent de l'URL remise au broker, et un primo-arrivant se retrouvait devant la page de connexion sans portefeuille et sans moyen d'en obtenir un — sans la moindre erreur. Appeler ensureIdentity() avant init() ne marchait pas non plus : il attend la session, que seul le callback d'init() résout. Cycle vérifié empiriquement. La cause n'était ni l'ordre ni la redirection, mais une confusion dans ensureIdentity() entre deux actes de nature différente — régler qui est l'utilisateur (barrière, URL, stockage : aucune session) et se connecter (session requise). settleIdentity() porte le premier ; le wrapper init() du polyfill l'attend avant de déléguer. L'invariant d'ordre est ainsi porté par la composition, pas par une consigne d'ordre d'appel que personne ne lit. Piège trouvé et épinglé en écrivant les tests : init() et ensureIdentity() dans le même tick montaient deux barrières, l'utilisateur répondait à l'une et l'autre ne se résolvait jamais. Le règlement en vol est désormais partagé. |
||
|
|
fc3c129bd3 |
fix: l'identifiant est dans l'URL avant qu'init() ne l'emporte
@ng-org/web fait déjà la redirection vers le broker — même hôte, même forme, même test de cadre (dist/ngweb.js). Le polyfill n'a donc rien à réimplémenter là : ce qui lui revient, c'est la seule chose qu'init() ne peut pas faire, à savoir mettre ?ng-id= dans window.location.href avant qu'il ne le lise. rememberIdentity() s'exécute désormais sur les trois chemins — identité posée par l'appelant, venue du stockage, ou saisie à la barrière. Le cas du stockage était le silencieux : le paramètre restait absent, l'iframe lisait une identité vide et provisionnait un second espace virtuel, sans erreur. L'écriture dans le stockage et celle dans la barre d'adresse sont deux try indépendants : un stockage qui refuse d'écrire ne doit pas emporter avec lui le paramètre, qui est le seul à franchir la frontière de partition. Le contrat retire l'obligation « être ouverte via la redirection du broker » : elle n'a jamais été celle de l'application. Ni broker, ni iframe, ni redirection n'y sont plus nommés. |
||
|
|
9c487b59f3 | refactor: le résumé du harnais e2e dit polyfill, pas SDK | ||
|
|
737729c9ce |
refactor: le paquet s'appelle polyfill, « SDK » désigne celui de NextGraph
Le nom @ng-eventually/sdk entrait en collision avec le SDK de NextGraph, dont ce paquet est justement un polyfill. Impossible d'écrire « le SDK » sans lever l'ambiguïté à chaque phrase — et le contrat publié, lu par une application, était le pire endroit pour laisser traîner ça. packages/sdk → packages/polyfill, @ng-eventually/sdk → @ng-eventually/polyfill, contract_sdk-surface → contract_polyfill-surface, e2e/sdk-entry.ts → e2e/polyfill-entry.ts, docs/sdk-reference.md → docs/polyfill-reference.md. Les occurrences de « SDK » qui désignent celui de NextGraph restent intactes, y compris les chemins dans nextgraph-rs (sdk/js/orm, sdk/js/web). Le tri s'est fait occurrence par occurrence, pas par substitution. Le contrat énonce désormais son identité en une phrase : « This package is a polyfill of NextGraph's SDK. » |
||
|
|
49b046268e |
docs(contract): le contrat décrit l'API, et rien d'autre
Récriture complète de `contract_sdk-surface.md` sur le cadrage du propriétaire du projet : **le contrat dit ce que le SDK met à disposition, point.** Ce qui en sort, et pourquoi ça n'y avait pas sa place : - **tout état d'implémentation** — chiffrement, confidentialité, valeur de remplacement, ce qui est émulé, ce qui n'est pas encore fait. Le lecteur est un agent qui développe une application appelante : s'il lit qu'une chose est provisoire, il conçoit des compensations — sa propre couche de chiffrement, un choix de ne pas stocker telle donnée, un avertissement d'interface — toutes fausses et toutes à retirer. Il doit pouvoir considérer que ce SDK **est** celui de NextGraph ; - **la fabrique** — « polyfill », « portefeuille partagé », « multi-utilisateurs », la migration, ce que l'application supprimera un jour, les écarts par rapport à la cible ; - **l'argumentaire** — ce que le modèle « permet », ce que telle règle « achète », la confidentialité composable. Un appelant a besoin de savoir qu'une référence rendue ne porte pas de clé, pas de savoir ce que ça lui apporte. Ce qui entre : les **trois obligations de déploiement**, vérifiées dans le code — servir un `.ngw` depuis son bundle et le passer à `configure`, être ouverte via la redirection du broker, appeler `ensureIdentity()` dans un contexte navigateur avant de rendre — et trois clauses contraignantes qui manquaient : l'identifiant rendu est **opaque**, le préfixe `urn:ng-eventually:` est **réservé sur les sujets**, et le placement recommandé est un document par entité métier, plusieurs objets dans un document restant permis. `## Guarantees` devient une suite d'énoncés plats. `## Non-guarantees` ne liste que des **absences de capacité** — pas de nom d'affichage, pas de révocation, rien par lecteur sur un document en store public, pas d'écriture déléguée — jamais un manque par rapport à autre chose. L'application d'exemple n'affiche plus l'identifiant comme un nom : elle le montre pour ce qu'il est, un identifiant technique. C'était exactement ce que la clause « opaque » interdit, dans le fichier censé montrer le bon geste. 202 tests, typechecks propres, `lint` sans erreur. 148 → 135 lignes. |
||
|
|
33b96fdc8d |
docs(concept): la règle interdit la divergence, elle ne la met plus en balance
La feuille testait ce que l'APPELANT apprendrait. Le propriétaire du projet a énoncé la règle plus large : rester au plus près de NextGraph, et n'admettre aucune implémentation qui en diverge — que l'appelant s'en aperçoive ou non. L'ordre des deux questions est ce qui a manqué. Sur la fusion de `readUnion`, posée en premier, « l'appelant devra-t-il désapprendre ? » ne tranchait pas : « une entité par document » est une bonne pratique par ailleurs, alors que désapprendrait-il au juste ? Le raisonnement a piétiné des heures là-dessus. Posée en premier, « la cible fait-elle ça ? » a demandé un regard : le niveau 1 rend les sujets réels, et l'ORM du niveau 3 porte `@id` ET `@graph` sur chaque objet en fabriquant le premier quand on l'omet. Divergence, fin. « Devra-t-il désapprendre ? » reste, mais mesure la gravité d'une divergence inévitable — jamais son autorisation. Et un tell est ajouté, celui qui a produit ce défaut : une recommandation que le code impose au lieu de la guider, en rendant l'autre disposition invisible. |
||
|
|
7076c0cca8 |
fix: readUnion regroupe par sujet réel — la fusion était une divergence
`readUnion` indexait par DOCUMENT une table nommée `bySubject`, créait ses entrées avec `subject: doc`, et jetait le sujet réellement lu après s'en être servi pour écarter la machinerie. Tout triplet non-machinerie d'un document tombait donc dans un sac unique étiqueté par la référence du document : deux entités écrites sous deux sujets revenaient **conflées**, une entité écrite sous un autre sujet revenait **ré-étiquetée**. Sans erreur, sans trace. **C'était une divergence, et c'est à ce titre qu'elle tombe.** NextGraph dit l'inverse aux deux niveaux : une requête ancrée résout le graphe du repo comme graphe par défaut et rend les sujets tels qu'ils sont ; et l'ORM porte sur chaque objet **deux** propriétés distinctes, `@id` et `@graph`, dont il FABRIQUE la première quand on la laisse vide (`graphIri + ":q:" + aléa`). Plusieurs objets par graphe est le cas prévu, et `@id` existe pour les distinguer à l'intérieur d'un `@graph`. La règle du projet reste **« un document séparé par entité métier »**, mais c'est une recommandation de placement dictée par le modèle de sécurité — une clé est par repo, donc l'isolation par entité exige un repo par entité. Ce n'est pas une contrainte que la lecture a le droit d'imposer en rendant l'autre disposition invisible. Le contrat porte désormais la recommandation, le code porte la capacité ; il faisait exactement l'inverse. **La justification de l'épinglage était une erreur de catégorie**, et elle a été retirée plutôt que contournée : `repo_graph_name` formate un nom de GRAPHE, il est estampillé sur les quads et aucun sujet n'est réécrit. Deux confirmations indépendantes, dont la suite e2e qui écrit un sujet puis le relit par correspondance exacte contre le vrai broker. `UnionSubject.subject` passe de `Nuri` à `string` — un sujet RDF réel est un IRI quelconque. `graph` reste `Nuri` et devient le champ à repasser au SDK ; l'app d'exemple l'utilise à ses deux sites, où le sens était « le document ». **Et la suite e2e ne comptait que les entrées.** C'est pour cela qu'elle est restée verte pendant tout le défaut : compter ne distingue pas un regroupement par document d'un regroupement par sujet. Elle écrit maintenant deux sujets dans le dernier document et vérifie les trois choses qui comptent — quatre entrées pour trois documents, chaque entrée portant le sujet sous lequel elle a été écrite, et son `graph` étant la référence du document. 202 tests unitaires (5 ajoutés, dont 3 échouent si l'on restaure l'ancien repliage), e2e 42/42 et applicatif 12/12. |
||
|
|
f378c71739 |
docs: sept affirmations sur NextGraph requalifiées à la source
Lot F de la revue adverse. Chaque affirmation relue dans `nextgraph-rs` par symbole avant
d'être réécrite ; aucune ne s'est révélée exacte.
- `AddLink` / `RemoveLink` / `RepoLinkV0` étaient présentés comme **implémentés**. Leurs
arms de vérificateur sont des `Ok(())`, là où celui d'`AddRepo` fait un vrai travail, et
rien ne les construit. Le tableau dit désormais « déclaré, stubbé », et la conclusion qui
en déduisait « le registre existe, seule la livraison manque » est corrigée : **les deux
bouts** sont déclarés-et-stubbés.
- La table `inboxes` était dite « reconstruite vide à chaque session » — elle est
repeuplée au chargement, et la clé privée d'inbox est persistée par repo. L'argument de
sécurité qui s'appuyait dessus repose maintenant sur le bon motif : la table est **par
vérificateur**, pas éphémère.
- La citation de « `doc_create` laisse `inbox: None` » pointait un constructeur réservé aux
tests ; re-ciblée sur le chemin de production.
- `ExtObjectGet` était dit « le seul » primitif accessible à un non-membre et exigeant les
clés : il y en a trois, et sa structure n'a aucun champ de clé.
- L'en-tête de `public-store.ts` était marqué **VERIFIED** alors qu'il repose sur un
commentaire de doc, et la condition qu'il citait (« si les brokers pairs l'autorisent »)
disparaissait de la conclusion. Requalifié en **pari**, condition rétablie.
- Deux sur-restrictions corrigées (ce qu'écrit le traitement d'un `ContactDetails`, et le
prétendu « miroir 1:1 » de `NuriV0`, qui a dix champs).
**Sur la grammaire du ReadCap, une correction de MA correction.** J'avais écrit que la
forme `{target}:r:{cap}` était notre invention. Faux : le segment `r:` et son encodage sont
ceux d'amont, et l'auteur de NextGraph l'a énoncé. Ce qui est établi est plus étroit —
aucun parseur amont n'accepte aujourd'hui un NURI de repo qui le porte, et le segment est
produit comme valeur de champ. J'avais conclu d'une implémentation absente à ce que la
cible ferait, ce que la doctrine du projet interdit nommément. Seule « P1b remplace la
valeur, pas la forme » est corrigée, requalifiée en **pari**.
**Et la surface ne publie plus de type que personne n'utilise.** `export * from
"./model/types"` publiait huit types en bloc ; c'est une liste nommée de six. `ReadCap`
sort — aucune signature publiée ne le prend ni ne le rend, seules deux fonctions privées
de `inbox.ts` s'en servent — et `InboxScope` aussi. Un type n'est publié que si une
signature publiée l'utilise.
197 tests, 0 échec ; les trois typechecks propres ; `lint` sans erreur.
|
||
|
|
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. |
||
|
|
30f6263db5 |
docs(concept): le contrat entre le polyfill et l'application qui l'utilise
Amorce le système `concept` dans ce dépôt et ouvre `app-contract` — la frontière entre cette bibliothèque et les applications qui la consomment. C'est un contrat **inter-dépôts** et ce dépôt en est le FOURNISSEUR : les applications vivent ailleurs et tireront `sdk-surface` d'ici. D'où le type `contract_`, ses cinq sections obligatoires, et l'inscription dans `.project/contracts.yaml` — c'est l'inscription qui publie. Trois feuilles : - **`contract_sdk-surface`** — l'engagement, écrit du point de vue de l'appelant. Ce qu'il peut tenir pour acquis : permissif en entrée et précis en sortie ; toute référence rendue est NUE, aucun appel ne rend jamais de clé ; lire est la possession, écrire est la propriété ; donner à lire est un seul acte et le destinataire n'appelle rien ; un dépôt s'adresse à une inbox, jamais à un document ; `ensureIdentity()` est toute la connexion ; et `configure` est le seul appel qu'il supprimera. Ce qu'il ne doit PAS tenir pour acquis, dit aussi crûment : aucune confidentialité, rien de « par lecteur » sur un document public, aucune révocation, aucune écriture déléguée, et les références ne voyagent que dans un déploiement. - **`rule_would-the-caller-unlearn-it`** — le test qui décide de tout : est-ce que ceci ferait apprendre à l'appelant quelque chose qu'il devra DÉSAPPRENDRE ? Avec les deux tells que la revue de ces jours-ci a rendus concrets : une exception nommée cesse d'en être une dès qu'on la publie, et un symbole gardé parce qu'il était là n'est pas une décision. - **`knowledge_what-an-app-deletes-at-migration`** — les deux destins d'un symbole publié, le cas intermédiaire d'`ensureIdentity` (substance jetée, site d'appel conservé), et le fait que la liste de suppression n'est plus portée par un chemin d'import depuis la fusion des entrées : une garantie mécanique remplacée par une garantie documentaire, dont seule la moitié est tenue par un test. Le vocabulaire du concept fixe trois termes que ce projet a déjà payé cher : `reference` (jamais « lien »), `ReadCap`, `polyfill-era`. `lint` est conformant. Reste à décider : ce dépôt n'a pas de `CLAUDE.md` racine, donc l'`AGENTS.md` généré n'est chargé nulle part. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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).
|
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
0832338201 |
feat: un document en store public sert son ReadCap, une référence nue suffit
Le modèle amont est explicite dans `PublicRepoLinkV0` : le lien ne porte AUCUN `read_cap`, et son commentaire dit pourquoi — *"The latest ReadCap of the branch will be downloaded from the outerOverlay, if the peer brokers listed below allow it […] the public site are served differently by brokers"* (engine/net/src/types.rs:5098). La clé n'est pas remise par un émetteur : elle est donnée par le réseau à qui la demande, parce que le broker a épinglé l'overlay externe (`expose_outer`). La bibliothèque refusait jusqu'ici la forme sans cap quel que soit le store. Sûr dans le bon sens, mais une application ne pouvait pas exprimer « fais circuler, la référence suffit » — le seul acte que le modèle rend gratuit — et son unique contournement était de distribuer la clé, ce qui détruit la confidentialité composable. `emulated-verifier/public-store.ts` émule le mécanisme SANS toucher à la garde. La possession reste l'unique critère : un document public est lisible non par exception mais parce que son cap est *obtenable*. Chaque porte de lecture demande d'abord (`readUnion`, `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`), puis le chemin ordinaire s'applique. Lire n'est pas écrire. Ce que le store sert est un droit de LECTURE : `learnFromPublicStore` le classe à part et `assertMayWrite` refuse l'écriture dessus. Sans cela une référence nue achetait une écriture, ce qu'aucun store amont n'accorde. Autres conséquences : - `recordInPublicStore` (marquer + frapper) devient `markInPublicStore` (marquer). Frapper un second cap à côté de celui qu'on vient de télécharger donnerait deux clés différentes le jour où la constante devient un secret. - `hasCap` quitte la porte polyfill : il se lisait « ai-je le droit de lire ceci ? » et un document public y répondait `false` jusqu'à ce qu'on demande son cap. Aucun appelant hors des tests. - Les tests cross-user ne font plus traverser de cap par une variable JS : Bob n'obtient que la référence nue, comme une vraie application. Écarts documentés plutôt que masqués : le pari sur un modèle DÉCLARÉ (`expose_outer` est câblé à `false` côté client et `ExtTopicSyncReq` est `unimplemented!()`), la découverte limitée à ce qu'on sait déjà nommer, `useShape` qui n'a pas d'await à dépenser, et l'absence de `locator`. 179 tests unitaires, e2e 42/42 contre le broker en ligne. |
||
|
|
32ef756b0b |
docs: deux actes, pas quatre besoins
La section listait quatre « besoins » — nommer, donner à lire, faire trouver, joindre. C'était confondre ce qu'un acte FAIT avec ce dont son résultat dépend. Un émetteur ne fait que deux choses, et tout cas d'usage est l'une des deux : - transmettre la RÉFÉRENCE (`access` vide) : le destinataire peut nommer le document — et le lire si celui-ci se trouve dans un store public. Même valeur transmise, résultat différent, décidé par l'endroit où est le document et non par ce qu'on a envoyé. - transmettre la référence ET la clé : le destinataire lit. Irréversible. « Faire trouver » n'était donc pas un troisième acte, mais le premier appliqué à un document en store public. Et « joindre » n'est pas un acte du tout : le locator dit si la référence est assez COMPLÈTE pour être résolue par un inconnu — cela relève du destinataire. D'où la conséquence pour tout ce que cette bibliothèque expose : un appel remet la clé ou ne la remet pas, et ce doit être l'intention explicite de l'appelant — jamais l'effet de bord d'une demande de référence. Où en est la bibliothèque, acte par acte : transmettre la référence est couvert sans appel dédié (toutes les références rendues sont nues) ; transmettre référence et clé l'est par `inbox.share`. Et une propriété du modèle n'est PAS émulée : un document en store public est lisible depuis une référence nue, là où `mayReach` exige toujours un cap détenu. L'écart va dans le sens sûr — nous sous-accordons — mais une app ne peut pas exprimer « je diffuse largement, la référence suffit », et son seul contournement serait de remettre la clé, ce qui casse la confidentialité composable. Section redondante supprimée, et le mot banni retiré des exemples. |
||
|
|
94173fd3d3 |
docs: consigner l'écart sur l'inbox choisie par share
`inbox.share` dépose toujours sur l'inbox PROTÉGÉE du destinataire. En amont le
choix n'est pas fixe : un enregistrement de contact prend son inbox du PROFIL par
lequel la personne a été atteinte — `a_or_b = if details.profile.is_public()
{ "site" } else { "protected" }` (`inbox_processor.rs:787`, écrit `ng:site_inbox`
ou `ng:protected_inbox` en `:823-824`).
Mineur aujourd'hui, mais ça aplatit une distinction du modèle, et ce sera faux le
jour où une application partagera avec quelqu'un rencontré par son profil public.
Consigné plutôt que corrigé : le correctif demande une notion que rien n'a
établi ici. Ce qui EST vérifié se limite à un wallet qui contient des `sites`, un
`SiteV0` avec `id: PubKey`, `name`, `site_type` et trois stores. L'enum
`Identity`, qui nommerait le reste, est entièrement COMMENTÉ en amont
(`engine/repo/src/types.rs:586-595`) — il n'y a donc aucun modèle de profil à
lire, et rien à supposer.
Rappelé au passage, parce que la question s'est posée : le store privé n'a
aucune inbox — `new_store_default` n'en attache une que `if !private` — d'où
`InboxScope`, qui rend « l'inbox privée » inécrivable plutôt que simplement vide.
|
||
|
|
cb56f91b5e |
docs+fix: le modèle de sécurité local-first, et le retrait de linkTo qui le brisait
La règle d'accès est « qui a la référence ET la clé lit » — jamais « qui a la référence lit ». Il n'y a pas de tiers dans cette phrase : le moteur ne vérifie une permission qu'à l'ÉCRITURE, jamais à la lecture. Le contenu est chiffré, la clé EST le droit. C'est ce que veut dire local-first ici : il n'y a personne à qui demander, donc la possession est tout le mécanisme. Ce que cette règle achète, et qui n'était consigné nulle part : une référence n'accordant rien, **elle n'est pas récursive**. Un document largement diffusé peut pointer vers un document restreint — la référence dit qu'il existe, la clé dit qui le lit. D'où la confidentialité COMPOSABLE : un sommaire diffusé qui renvoie à des chapitres restreints, un événement public qui renvoie à sa liste de participants. L'auteur diffuse un document et décide encore, document référencé par document référencé, qui reçoit la clé. `linkTo` détruisait cette construction. Il rendait la CLÉ là où un appelant demande une référence, transformant la règle en « qui a la référence lit » — pour ce document et pour tout ce qu'il MENTIONNE. Ajouté et retiré le même jour. Documenté en §0, avant tout le reste, avec la raison pour laquelle un agent perd ce point avec constance : les réflexes client-serveur fournissent la moitié manquante sans qu'on s'en aperçoive — quelque part un serveur vérifierait, un lien serait un identifiant inoffensif, « Bob a-t-il le droit ? » aurait une réponse. Aucun des trois n'est vrai ici, et du code écrit là-dessus n'échoue pas : il accorde en silence. Rien ne passe au rouge. Renommé au passage : `publishRepoLink`/`isPublished` → `recordInPublicStore`/ `isInPublicStore`. Ces méthodes n'ont pas de pendant amont et « publier » ne désigne rien de précis ici. 171 tests unitaires, e2e 42/42 en 3,6 min. |
||
|
|
3c981ffadb |
docs: le nommage de NextGraph l'emporte toujours — y compris sur nos propres règles
La règle que je venais d'écrire bannissait « publish » sans réserve. Or le moteur emploie `publisher` 126 fois — `as_publisher`, `publisher_advert` — pour un rôle pub/sub sur un topic. Une règle appliquée à la lettre aurait fait rejeter du vocabulaire amont, ce qui est exactement l'inverse du but. Le principe est donc posé au-dessus, et il prime sur tout : là où la cible a un mot, c'est le mot, point. Ce qui reste banni est NOTRE « publier un document », qui n'a aucun référent en amont et recouvre trois actes distincts — placer dans un store public, rendre trouvable, remettre une clé. Ça n'autorise jamais à renommer ce que l'amont appelle `publisher`. `publisher`, `topic` et `advert` rejoignent le vocabulaire déclaré du contrôle de noms, avec la raison en commentaire. Le renommage `publishRepoLink` → `recordInPublicStore` reste justifié : cette méthode n'a pas de pendant amont, elle enregistre un document en store public (et, artefact d'émulation, lui frappe une clé). |
||
|
|
7672915bb9 |
docs: ce qu'un NURI transporte, et les quatre besoins d'un lien
Une discussion de conception a calé plusieurs fois sur une séparation supposée entre « un NURI » et « un lien ». Elle n'existe pas, et l'avoir crue est ce qui a fait livrer un `linkTo` faux sur trois points à la fois. `NuriV0` porte exactement ce qu'un lien porte — `target`, `overlay`, `access: Vec<NgAccessV0>`, `locator` — et `NgLinkV0` en est la forme structurée. La vraie question n'est donc pas « NURI ou lien » mais ce qu'on met dans `access` et `locator`. Nuance qui compte pour tout ce qui s'imprime : le TYPE a tous les emplacements, la GRAMMAIRE de chaîne ne les expose pas tous. `access` est une LISTE de formes d'accès (`ReadCap | Token | ExtRequest | Key | Inbox | Topic`), donc une liste VIDE est un état légitime et courant : la référence NOMME sa cible et n'accorde rien. D'où les quatre besoins, chacun avec ce que la référence doit porter : nommer (access vide), donner à lire (+ ReadCap, irréversible), faire trouver un document public (overlay/locator, aucune clé), joindre depuis ailleurs (locator, sans quoi un inconnu n'ouvre rien). Trois confusions nommées parce que chacune a été faite ici : « sans clé » ne veut pas dire « public » ; nommer n'est pas lire et c'est l'acte PAR DÉFAUT (ce que notre test central prouve déjà) ; rien n'est vérifié à l'accès, donc une référence ne peut pas être neutre en droits — ce qu'on y met EST l'autorisation, et un lien diffusé ne se révoque pas. Et la divergence la plus lourde, qui n'était consignée nulle part : publier signifie ICI distribuer une clé, alors qu'en amont un document public est lisible parce que son STORE l'est et que les brokers le servent. L'émulation est trop stricte, pas inversée — elle sous-accorde — mais un consommateur ne doit pas en conclure que publier est un acte de distribution de clé, ni attendre quoi que ce soit de per-lecteur sur un document public. |
||
|
|
c8d02619b1 |
refactor(api): partager nomme le document, détenir répond par oui ou non
`shareCap(cap, toUser)` faisait tenir une clé à l'appelant. En amont il n'en
tient aucune : c'est le verifier qui remplit `ContactDetails.read_cap`, et une
inbox se résout depuis un profil. Cette signature a déjà changé deux fois
aujourd'hui — `(cap, toInbox)` puis `(cap, toUser)` — et les deux laissaient à
l'app quelque chose qu'elle ne tiendra pas plus tard.
- `inbox.share(doc, toUser)` : les deux choses qu'une application a, un document
et une personne. Ni la clé ni l'adresse n'apparaissent.
- `hasCap(doc)` remplace `capFor(doc)` et rend un BOOLÉEN. C'est la seule
question que le modèle admette, et l'unique appelant qui utilisait la valeur
s'en servait pour la passer à `shareCap`.
Les tests ont fait apparaître un besoin que ces retraits allaient casser :
obtenir le lien PARTAGEABLE d'un document publié, pour le faire circuler. C'est
distinct du partage dirigé et ça existe en amont — un `RepoLinkV0 { read_cap }`
est ce qu'on passe, `ContactDetails.read_cap` est la remise à quelqu'un. D'où
`linkTo(doc)`, seul endroit où une app tient légitimement une clé : on ne peut
pas faire circuler ce qu'on n'a pas le droit de toucher. La clé d'un document
protégé, elle, ne sort jamais par là — elle passe par `share`.
171 tests unitaires, e2e 42/42 en 3,5 min (synchro à froid 29s, stable contre
30s au run précédent — le wallet par batterie tient).
|
||
|
|
da6ef4b8b8 |
test(e2e): un user physique par batterie — 20 min et 286s de synchro tombent à 3,6 min et 30s
La suite se ralentissait elle-même, de façon monotone. Chaque batterie crée ~11 identités virtuelles FRAÎCHES (`@alice-…`, `@owner-…`, `@recon-…`), chacune avec ses trois documents de scope et son inbox, et toutes atterrissaient dans le MÊME user physique — un wallet créé le 10 juillet et réutilisé depuis, que rien ne nettoyait. Or une resynchronisation à froid est O(taille du user physique), ce que la doc de cette bibliothèque énonce elle-même. D'où 250s il y a une semaine, 286s avant-hier, et une batterie qui a fini par dépasser les 20 minutes. Les identités fraîches ne sont pas la faute : ce sont elles qui rendent une batterie reproductible, une inbox stable accumulant sinon les dépôts des runs précédents. La faute était de conserver le user physique qui les héberge. Mesuré : 42/42 en **3,6 min** au lieu de 20+, synchro à froid la plus lente à **30s** au lieu de 286s. Le profil reste persistant À L'INTÉRIEUR d'une batterie — CONTRACT 1 et 2 testent précisément cela (reconnexion fidèle sur le même profil, absence de fork de compte au travers). Deux garde-fous pour que la prochaine dérive se voie : - **Le budget appartient au runner**, qui échoue en nommant la cause probable. Un `timeout` posé autour de la commande tuait le navigateur, et la suite rapportait « Target page, context or browser has been closed » — un message qui se lit comme un défaut applicatif, et que j'ai diagnostiqué deux fois de travers avant de comparer les durées. - **La synchro à froid remonte dans le résumé.** C'est le nombre qui a dérivé pendant un mois sans que personne le regarde, parce qu'il n'apparaissait qu'au détour de la ligne de détail d'une étape. |