Commit Graph

10 Commits

Author SHA1 Message Date
Sylvain Duchesne 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.
2026-08-14 10:00:40 +02:00
Sylvain Duchesne 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.
2026-08-13 11:58:29 +02:00
Sylvain Duchesne 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.
2026-08-13 09:49:24 +02:00
Sylvain Duchesne 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.
2026-08-12 18:32:23 +02:00
Sylvain Duchesne 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 }).
2026-08-12 17:39:12 +02:00
Sylvain Duchesne 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.
2026-08-11 19:24:00 +02:00
Sylvain Duchesne 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.
2026-08-11 19:14:49 +02:00
Sylvain Duchesne 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.
2026-08-11 19:03:58 +02:00
Sylvain Duchesne 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é.
2026-08-11 12:52:49 +02:00
Sylvain Duchesne 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. »
2026-08-10 17:14:25 +02:00