935cce4d7b2286184055966997b352f6f38cc05e
27 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
935cce4d7b | fix: un second doc_subscribe tuait le premier, et les inbox n'étaient lues qu'à la connexion | ||
|
|
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. |
||
|
|
a8d53010c2 |
fix: une lecture réactive qui n'a pas pu répondre ne dit plus « rien »
watchShape attrapait l'échec de résolution des documents, journalisait, et
passait une liste vide. Or une barrière sur zéro document est franchie
trivialement — la surface publiait donc { data: [], isPending: false,
isSuccess: true }, octet pour octet l'instantané « synchronisé et vide ». La
seule distinction pour laquelle ce module existe était celle qu'il détruisait.
Un échec de résolution devient isError, jamais isSuccess. Et comme un
observable ne peut pas dé-émettre, l'état « je ne sais plus » conserve la
DERNIÈRE lecture qui a répondu, avec isSuccess à faux : une liste vide n'est
jamais la réponse d'un échec.
Le canal choisi est l'état de chargement, parce que c'est celui qu'une
application lit déjà pour distinguer « en attente » de « vide ». La troisième
valeur ne lui coûte aucun vocabulaire neuf.
Vérification faite en amont plutôt qu'en supposant : readyPromise n'aurait pas
aidé — construit avec resolve seul, rien ne le rejette, et l'échec
d'orm_start_graph n'est qu'un console.error. Le « je n'ai pas pu savoir » de la
cible EST son « toujours en attente ». La classification invention tient, et les
annotations le disent désormais.
C'est le dernier membre connu de cette famille dans le polyfill : après
connectedUser, resolveAccount, userInbox, readInboxCapPairs et
listMyEntityDocs, la couche réactive était le dernier endroit où un échec se
présentait comme une absence.
|
||
|
|
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. |
||
|
|
98ee511d3a |
test(e2e): le parcours qui manquait — Alice revient, et tout est encore à elle
Deux défauts ont été livrés et rapportés par une application, sans que la suite applicative puisse les voir. Le trou était précis : aucun parcours ne faisait revenir un PROPRIÉTAIRE après qu'il a ouvert son document aux messages. Le parcours 3 fait ouvrir Alice et revenir Bob ; Alice, elle, ne se reconnecte jamais. Il s'arrêtait une reconnexion trop tôt. Alice écrit deux notes publiques, en ouvre une aux messages, Bob y dépose en la nommant, puis Alice revient. Elle doit être reconnue, lire la note qu'elle a faite en ne tenant que sa référence, POUVOIR ENCORE Y ÉCRIRE, et trouver le message laissé en son absence. L'écriture compte autant que la lecture : un document public survit à une relecture après rechargement, sa clé étant retrouvée dans le store, et n'échoue qu'à l'écriture. Un parcours qui se contenterait de relire aurait manqué la moitié. Et il a été vérifié contre le code d'AVANT le correctif, dans un worktree jetable : les quatre vérifications échouent, sur « docs.sparqlQuery: refused — the connected user does not hold this document's cap ». Ce refus apparaît une fois dans chaque journal d'avant et zéro fois dans les trois d'après. Un parcours qui passe des deux côtés ne prouve rien — c'est exactement comme ça que ce trou avait survécu. La reconnexion est vraie : nouvelle page, réalisme JS neuf, donc tous les caches de module disparaissent pendant que le portefeuille reste intact. Rien n'est pré-injecté — la référence qu'Alice colle, elle l'a lue sur son propre écran. Total de vérifications : 34 → 39. |
||
|
|
f6d1734679 |
fix: à la reconnexion, on retrouve ce qu'on possède — pas seulement ce qu'on a reçu
Une application signalait deux symptômes. Ils sont indépendants, et ils ont une racine commune : se connecter ne rejouait qu'UN des registres durables du portefeuille. connectedUser lisait les AddLink de la branche User — ce qu'on vous a partagé — et rien d'autre. Les capacités des documents que vous avez FAITS vivent sur la branche Store, et un seul chemin les relisait : listMyEntityDocs. Une application qui recharge et va droit à sa note ne tenait donc rien pour elle. Constaté : capFor(note) vaut undefined juste après une connexion résolue, et devient défini dès que listMyEntityDocs passe. Le public survivait en lecture parce que fetchReadCap va rechercher la clé dans le store ; il échouait quand même à l'écriture, qui ne fait pas cette démarche. Deuxième défaut : myInboxes énumérait les inbox sans en remettre la clé au détenteur. Se connecter demandait donc un document qu'on n'avait pas de quoi lire — sur une inbox, que l'application n'a jamais nommée puisque rien ne le lui permet. Troisième défaut, et c'est lui qui rendait tout ça fatal : un drainage refusé faisait échouer toute la connexion. Une inbox n'étant pas consommée par un échec, elle refusait à chaque tentative suivante. D'où le « trois fois sur trois », et d'où un verrouillage plutôt qu'un partage manquant. C'est ma spécification qui l'a créé. En rendant les échecs visibles j'avais écrasé une distinction : ne pas ATTEINDRE la file est une panne, et refuser la session est juste ; ne pas pouvoir APPLIQUER un élément est une donnée, et ça ne doit priver personne de sa session. Le drainage par inbox est désormais isolé — signalé haut et fort, les autres files drainées, la session accordée — tandis qu'énumérer les files et restaurer rejettent toujours. Le parcours e2e ratait les deux : personne ne se reconnecte après avoir ouvert son document aux messages. Il s'arrêtait une reconnexion trop tôt. |
||
|
|
b50591f5bd |
feat(e2e-helpers): fabriquer un portefeuille, pour un test comme pour un déploiement
Trois appelants veulent la même chose — un portefeuille neuf, ses octets et son mot de passe : la suite du polyfill à son démarrage, la suite d'une application au sien, et un humain une fois pour provisionner un déploiement. Rien ne produisait ce fichier ; il fallait aller le chercher à la main sur nextgraph.eu. Mêmes entrées, mêmes sorties, mais des appelants de nature différente : une suite ne va pas lancer un sous-processus et analyser sa sortie, et un humain ne va pas écrire un fichier jetable pour appeler une fonction. Donc une fonction, et un script mince par-dessus. mintWalletBytes(password, name) rend les OCTETS, pas un chemin. C'est une correction de ce qui existait : exportWalletFile imposait le disque à tout le monde, et la suite applicative écrivait un fichier temporaire pour le relire aussitôt en mémoire, puis devait le nettoyer. Elle ne le fait plus — serveApp prend les octets. Qui veut un fichier l'écrit ; personne n'y est forcé. Le script exige --password et refuse d'en inventer un, et refuse d'écraser un .ngw existant sans --force, vérifié AVANT de fabriquer quoi que ce soit. Exécuté pour de vrai : 800 octets, et le mot de passe imprimé est celui passé en entrée. C'est le seul des trois cas qu'aucune suite n'exerce, donc le seul qui pouvait être livré cassé sans que rien ne le dise. Ce qui ne change pas : les suites fabriquent un portefeuille par exécution et n'en héritent jamais ; le provisionnement veut l'inverse, durable et conservé. Ils partagent la fabrication et l'export, ils divergent sur la durée de vie. Les identifiants en dur deviennent un paramètre — les suites passent toujours les leurs, le script prend ceux qu'on lui donne, et il n'existe aucun mot de passe par défaut. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. » |