1957a8fe0c6844062d760f1bc862166d64cc93ab
31 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1957a8fe0c |
docs: des contrats à deux voix, et une politique de version qui dit la vérité
Le régime des contrats est devenu bidirectionnel : chaque partie rédige sa position dans son dépôt et tire celle de l'autre. Le fournisseur écrit son engagement, chaque consommateur écrit sa déclaration — ce qu'il appelle vraiment, sous quelles contraintes, et avec quelles frictions. Ce dernier point change nos usages : la section Frictions est le chemin par lequel un consommateur expose un problème, le canal hors-bande ne portant que le signal. Les huit points que Festipod nous a remontés auraient dû arriver par là, et notre réponse appartient à l'engagement plutôt qu'à un échange qui s'évapore. Trois interfaces existaient, une seule était documentée. Elles ont maintenant leur dossier et leurs deux voix : la surface du polyfill, ng-e2e-helpers, et la couche d'indexation. On a écrit les engagements dont nous sommes l'auteur et les déclarations des consommateurs qui sont à nous — délibérément AUCUN usage_festipod : ce n'est pas notre dépôt, et décider à leur place ce qu'ils consomment viderait le mécanisme de son sens. Et la Change policy s'inverse. Elle disait « ce paquet n'offre pas de stabilité sémantique ». Refuser un schéma ne ralentit pas le mouvement — ça retire au consommateur le seul outil pour le gérer, alors qu'on faisait déjà le travail d'une publication versionnée sans lui en donner l'étiquette. Semver, donc, et les majeurs sont le cas NORMAL : une surface qui converge vers une cible qu'elle n'atteint pas encore casse souvent, et c'est le signal vrai. Le texte dit les déclencheurs plutôt que le nom du schéma — retirer un symbole publié ou resserrer un appel est un majeur, ajouter est un mineur. Version pleine sur main, pré-version sur une branche : Festipod peut épingler aujourd'hui sans qu'on lui promette du stable, et sans qu'on ait à lui retirer son adresse à la fusion. Au passage, la signature publiée de watchShape était fausse — elle annonçait un argument unique là où l'appel en prend deux. Rapporté par Festipod, confirmé indépendamment. |
||
|
|
76ae9ffbb7 |
fix: trois chemins vers une inbox en double, et la lecture qui manquait
Une application a rapporté quatre appels simultanés sur un même document enregistrant trois inboxes. Le contrat garantissait l'inverse. En cherchant, on en a trouvé DEUX autres, indépendantes, qui produisent le même dégât durable : le propriétaire surveille une inbox pendant que les dépôts arrivent dans une autre. La concurrence. openDocumentInbox ne partageait rien avec userInbox — module différent, registre propre, aucune coalescence. Reproduit pire que rapporté : quatre appels donnaient QUATRE inboxes. Une carte en vol par (détenteur, document), et le corps déplacé pour que l'invariant soit porté par la composition plutôt que par la position d'une vérification. La limite est nommée plutôt que cachée : deux onglets ne partagent aucune carte, chacun lit, chacun ne trouve rien, chacun frappe. Ce n'est pas réparable ici — une branche est en ajout seul, et ça ne se réconcilie pas après coup, le propriétaire lisant sa branche User quand un déposant lit l'adresse publiée du document. Le contrat porte donc une garantie positive ET une non-garantie. La page froide. readInboxCapPairs était le seul lecteur de store sans barrière, correct uniquement parce qu'une autre fonction s'exécutait avant lui à la connexion. Une dépendance d'ordre, pas une garantie portée par la lecture : sur une page froide il lisait le store privé non synchronisé, répondait « aucune inbox » et en frappait une seconde. Un seul appel, aucune concurrence. La barrière est désormais dans la lecture, et elle ne coûte rien aux chemins connectés, la connexion ayant déjà ouvert les trois stores. Et la lecture qui manquait. readSynced donnait la garantie, readForDocument l'adressage, pas leur intersection — si bien que matérialiser des dépôts obligeait une application à résoudre une adresse d'inbox elle-même, ce que le contrat lui interdit explicitement. inbox.readSyncedForDocument la lui épargne. Elle traverse deux dépôts, l'adresse vivant sur l'en-tête du document et les dépôts sur l'inbox — franchir la barrière sur la seule inbox ne réparait rien. Au passage, le compteur d'identifiants de la doublure était par page : une page rechargée refrappait le même identifiant PAR-DESSUS une inbox existante, aliasant deux dépôts en silence. Il est monotone. |
||
|
|
90712e0ad0 |
fix: une liste qu'on ne peut pas ouvrir n'est pas une liste
listMyEntityDocs rendait la liste des documents même quand la lecture des clés échouait. Vu de l'appelant, une liste dont les documents ne s'ouvrent pas est INDISCERNABLE d'une liste dont ils s'ouvrent : rien ne signale la différence jusqu'à une lecture ultérieure qui revient vide, la cause étant alors loin derrière. C'était un choix délibéré — ne pas transformer un appel publié en levée, la liste étant déjà en main à ce moment-là. C'est précisément ce qui en faisait une demi-vérité plutôt qu'un raccourci. Et c'était le dernier membre connu de la famille qui a produit une panne chez une application cette semaine. Les deux lectures sur lesquelles l'appel repose remontent désormais : la branche Main dit quels documents sont là, la branche Store dit ce qui ouvre chacun. Un tableau vide signifie donc que ce compte n'a rien créé dans cette portée, et jamais que le store n'a pas été lu. readUserStore avalait le même échec pour son propre compte ; son autre appelant, ownsDocument, garde le comportement actuel par un catch explicite et documenté — toutes ses réponses étant des refus, il échoue en fermeture, ce qui est la règle qu'e32b6d0 avait posée. C'est un changement de comportement d'un appel publié, donc le contrat le dit, et docs/api-contract.md aussi. Au passage, deux citations pourries corrigées en citant un SYMBOLE plutôt qu'une ligne — types.rs:4251 désignait DialogRequest et non Link, index.d.ts:138 désignait const ng et non le type NG. Les douze autres références numériques du voisinage ont été vérifiées : aucune n'avait bougé. |
||
|
|
43aadbeb45 |
docs: chaque symbole dit d'où il vient
98 annotations posées à côté des déclarations, et un test qui les exige sur la surface publiée. Elles portent trois choses : le niveau qui répond, la référence amont, et la catégorie parmi les cinq. La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que j'avais classé « notre invention » en raisonnant depuis l'absence, alors que c'est le meilleur alignement disponible. Les références citent un SYMBOLE, jamais une ligne : trois citations du document avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source — un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos noms, plus un argument jamais transmis. La sémantique survit à la migration, les sites d'appel non, et la nuance disparaissait sous une étiquette trop flatteuse. Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à une invention qui prétendrait citer une référence — vérifié en cassant les trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé dans index.ts se faisait analyser comme une annotation. La classification couvre l'interne qui prétend ressembler à la cible — tout emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine portait sur une fonction non exportée ; n'être pas publié n'a protégé personne. Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la seconde nommée dans la note plutôt que lissée. |
||
|
|
6138d831da |
docs: une définition est un fait, son absence de câblage est un silence
J'ai classé readLinks comme notre invention parce qu'AddLinkV0 n'est construit nulle part en amont. C'était raisonner depuis l'absence — ce que cette règle interdit, et que je citais dans la même session. La bonne lecture était que la cible a DÉFINI la forme et ne l'a pas câblée : nous l'implémentons, ce qui est le meilleur alignement disponible. Les deux arrivent ensemble et l'absence est la plus bruyante, ce qui lui fait gagner des arguments qu'elle devrait perdre. D'où la règle : lire la déclaration, et laisser le silence autour ne rien dire. Elle gagne sa place dans la taxonomie, parce que la confondre avec « invention » fait croire qu'on est plus loin de la cible qu'on ne l'est — et un lecteur ultérieur, voyant « notre idée », se sentira libre de la changer. Cinq catégories désormais, et une provenance à côté de chaque symbole plutôt que dans un document qui dérive : le niveau qui répond, la référence amont, la catégorie. Un test l'exige sur la surface publiée. Il ne peut pas vérifier que la citation est vraie, et c'est acceptable : une citation se réfute en quelques secondes, une prose non — docs/api-contract.md affirmait un passthrough « 1:1 » faux sur deux points et un décompte de membres erroné, sans que rien n'oblige personne à aller voir. La classification vaut aussi pour le code non publié qui PRÉTEND ressembler à la cible : la faute qui motive tout ceci a été commise sur une fonction interne, et n'être pas exportée n'a protégé personne. |
||
|
|
a33fb8a214 |
docs: se connecter restaure ce qu'on possède, et un dépôt fautif ne verrouille personne
Deux affirmations à corriger, toutes deux écrites par moi et démenties par le bug d'aujourd'hui. La feuille disait que se connecter « restaure ce qui vous a été partagé ». C'est la moitié : le portefeuille garde vos propres clés ailleurs, et ne rejouer que le registre des partages refusait à une application son PROPRE document après un rechargement. La règle qui reste : rejouer TOUS les registres durables, pas celui qui se trouve sur le chemin qu'on teste. Et la règle « tout échec remonte » était trop grossière. Elle était juste pour la restauration ; appliquée à chaque étape, elle verrouillait. Un dépôt inapplicable n'est pas consommé par son échec, donc il refusait la session à chaque tentative — une personne qui ne peut plus entrer, là où il n'y avait qu'un partage manquant. La distinction consignée : ATTEINDRE les files et restaurer sont de l'infrastructure, et refuser la session y est juste ; APPLIQUER un élément est de la donnée, et ça se signale sans priver personne de sa session. Plus la forme générale, parce qu'elle se reproduira : devant un « tout échec remonte », demander lesquels se rattrapent en réessayant plus tard et lesquels sont définitifs pour l'acteur. Les définitifs ne doivent jamais garder une porte qu'il n'a pas d'autre moyen de franchir. |
||
|
|
07dfe68473 |
feat: un dépôt est traité vingt secondes après, sans attendre son destinataire
Un dépôt attendait la prochaine connexion de son destinataire — potentiellement des heures. Un vrai NextGraph aura un service qui traite les inbox en continu ; il n'existe pas. On l'émule : après une écriture dans une inbox, une échéance unique de vingt secondes draine l'inbox DE LA CIBLE. C'est une usurpation d'identité, possible seulement parce qu'un portefeuille partagé détient toutes les identités virtuelles. Elle est acceptable parce que l'application n'apprend rien de faux : elle observe que les dépôts finissent par converger, ce qui restera vrai avec un vrai service. Ce qui ne doit pas fuir, c'est le mécanisme. Trois gardes, tenues par du code et non par des consignes. Rien n'atteint la surface publiée : les exports sont épinglés par un test, et publier ceci reviendrait à publier un appel qui traite l'inbox d'autrui — après quoi il ne resterait rien du modèle de confidentialité. Le drainage agit avec un détenteur EXPLICITE, jamais l'identité ambiante. Le propriétaire vient d'un enregistrement de routage (shim:inboxOwner), et cet identifiant est passé à chaque étape. C'était le vrai danger : readLinks et myInboxes demandent getCurrentUser() au moment où elles s'exécutent, donc un drainage lancé pendant la session d'Alice aurait classé les capacités de Bob chez elle. Le test l'épingle — après le drainage, Alice n'a aucune capacité sur le document concerné, et les deux Links sont bien chez Bob, durablement. Et les échecs remontent au journal d'accès au lieu de disparaître. Une boucle différée qui avale ses erreurs, c'est la famille retirée en |
||
|
|
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é. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |