Trois constats, un seul defaut sous trois visages : un etat enregistre comme vrai
qui ne l'est pas, sans moyen de se corriger. Deux des trois vivaient dans
`open-repo.ts`. Cinq tours sont alles dans l'appelant ; c'est le teneur de
registre qui mentait.
CE QUE LE REGISTRE VEUT DIRE, ecrit dans l'en-tete. `held` est le SEUL registre
d'ouverture : cette session detient un abonnement vivant a ce document. Il y avait
a cote un `opened` voulant dire « on a appele `doc_subscribe` une fois et on a
cesse d'attendre », et ce sens plus lache mentait de trois facons : il survivait a
un abonnement REFUSE par le courtier (donc la reprise que `subscribe.ts` offre
etait bloquee a jamais), il survivait a une reinitialisation (donc une nouvelle
session court-circuitait sur l'enregistrement de la precedente), et il laissait un
document annoncer une barriere jamais franchie. `opened` est supprime.
Les trois consequences, desormais vraies par construction :
- un abonnement qui a ECHOUE n'enregistre rien - `onSetupFailed`, le canal que
`subscribe.ts` offre exactement pour cela, est enfin branche - donc l'appelant
suivant s'abonne a nouveau ;
- une reinitialisation efface l'enregistrement ET DECLASSE l'etat de synchro :
une barriere franchie dans la session qui vient de disparaitre n'en est pas une
que celle-ci a franchie ;
- `"unknown"` ne veut plus dire qu'une chose : aucune barriere n'etait attendue ici
(jamais demande, ou un `ng` sans `doc_subscribe`). Jamais « on a oublie ».
Cote appelant, un seul changement, et c'est le troisieme constat : quand un
document nomme n'a pas repondu, `publish` PUBLIE « la question reste ouverte » au
lieu de sortir en laissant en place l'instantane precedent. Sortir laissait un
refus DEJA LEVE a l'affichage : l'application lisait « vous ne detenez pas la
capacite de ce document » a propos d'un document qu'elle detient desormais.
Deux tests portaient l'ancien sens et sont corriges avec leur raison : celui du
`ng` sans `doc_subscribe` (le compteur d'ids du faux courtier redemarrait a chaque
test, donc deux tests aliasaient leurs depots - la lecon de `wallet-fake` a
l'echelle du fichier), et celui de la lecture froide, ou « timed-out » est
desormais ce que dit « cette page ne l'a pas confirme ».
Deux defauts, aucun des deux sur la reprise - elle a passe cette revue intacte et
n'a pas ete rouverte.
1. La barriere verifiait qu'un document nomme peut etre JOINT, jamais qu'il a
REPONDU. Elle attendait `ensureRepoOpen` puis demandait `assertMayReach`, qui
est la question de la capacite, et ne consultait pas l'etat de synchro que
l'ouverture venait de produire. Un document dont l'abonnement a reussi mais dont
le premier `State` n'est jamais venu publiait donc `isSuccess` avec `data: []` :
le defaut d'origine, dans le cas precis pour lequel la barriere a ete ajoutee.
La verification rejoint le reste de la decision, dans `publish`, donc rejouee a
chaque publication au lieu d'etre memorisee. Les trois valeurs, enumerees avant
d'ecrire la condition : `"synced"` fait autorite - zero ligne VEUT DIRE zero
objet, la ligne tenue depuis cinq revues -, `"timed-out"` et `"syncing"` non, et
`"unknown"` PASSE deliberement, parce qu'il veut dire « pas de semantique de
`State` sur ce `ng` » et que le traiter autrement est exactement la regression
du cas inerte livree aux tours 1 et 3.
`isPending` plutot que `isError`, des deux que le contrat autorise : rien n'a
echoue, il manque l'autorite de dire que le document est vide.
Le test (t) EPINGLAIT le defaut - il attendait `isSuccess` apres trois
barrieres expirees. Corrige. Ce n'est pas un test affaibli : l'ancienne
assertion etait fausse.
Et un trou dans ma propre suite, trouve par mutation : inverser la condition
pour refuser `"unknown"` passait tous les tests, faute d'un `ng` SANS
`doc_subscribe` nulle part. (cc) le couvre.
2. Regression de mon correctif du tour 4. Terminer une ouverture abandonnee
faisait tourner sa continuation juste apres que la reinit avait vide les
tables, donc elle ecrivait une entree `opened` perimee dans le registre neuf -
et toute ouverture ulterieure de ce nuri court-circuitait dessus. Le depot
n'etait jamais abonne dans la nouvelle session. Une epoque de registre corrige
cela : une reinit l'incremente, une continuation dont l'epoque ne correspond
plus n'ecrit rien. Une reinit INVALIDE ce qu'elle abandonne, en plus de le
terminer.
`getSyncState` reste interne, et la doctrine cesse de l'offrir : elle pointait sur
un symbole qu'aucun appelant ne peut importer.
Quatrieme tour. Ce n'est pas un drapeau de plus : c'est la forme qui change.
Les tours 2 et 3 construisaient tous deux la reprise ainsi - noter qu'on a
refuse, puis reagir a un evenement ulterieur - et c'est la note qui etait
fautive les deux fois : une fois conditionnee a quelque chose qui ne pouvait pas
devenir vrai, une fois ecrite apres la fenetre ou arrive l'evenement decisif. Un
front manque n'est pas rattrapable par construction. Donc :
- LA DECISION EST UN NIVEAU. Les documents nommes sont reinterroges a chaque
publication, jamais reportes depuis la barriere. Une capacite qui arrive change
`isError` en document par le canal qui existait deja, et rien n'etant memorise,
il n'y a aucun evenement a manquer. C'est aussi ce qui rend sans objet la
question du porteur : une requete ouverte pendant qu'un autre etait connecte ne
peut pas revenir a cet abonne en succes auquel il n'a pas droit.
- UNE TENTATIVE QUI A COURU CONTRE UN CHANGEMENT S'EN APERCOIT au lieu
d'attendre. `open()` echantillonne `CapRegistry.version()` avant la barriere et
compare apres son echec ; un ecart signifie qu'elle a decide sur un terrain
perime, et elle retente d'elle-meme. Cela termine : une tentative ne se repete
que parce que le trousseau a bouge, les mouvements sont finis, et ceux qu'une
tentative provoque elle-meme sont les capacites qu'elle depose - au plus une par
document nomme.
La comparaison de porteur disparait : `holderKey()` vaut "" tant que personne
n'est connecte, donc un refus pris avant que l'identite se fixe - la ou les
composants se montent - ne correspondait plus jamais ensuite.
Un instantane d'echec inchange n'est plus republie : la surface documente sa
stabilite, et huit arrivees sans rapport produisaient huit objets porteurs de huit
`Error` distinctes pour un etat identique.
Enfin, un defaut a moi, trouve en mesurant : le changement rendait la suite
intermittente. `resetOpenedRepos()` abandonnait les ouvertures en vol sans les
terminer, donc un appelant attendait les 8s du repli sur un abonnement demonte,
puis repartait avec son etat de synchro efface - et « unknown » est la lecture
LAXE, celle qui accepte qu'une lecture a zero ligne soit une absence averee. Une
absence-qui-cache-un-echec latente, de la famille meme de cette tache. Une
reinitialisation termine desormais ce qu'elle abandonne, en « timed-out ».
Deux fixtures qui se couraient apres sont corrigees, raison ecrite a cote.
Troisieme tour de revue adversariale, trois constats, tous reproduits avant
correction.
1. La garde etait CIRCULAIRE, et c'etait structurel. `namedDocumentsReachable`
demandait si la capacite est DETENUE. Pour un document en store public, le seul
canal qui puisse jamais la deposer est `fetchReadCap` - l'appel meme que la
garde retenait. Elle attendait une reponse que seule l'action qu'elle bloquait
pouvait produire, et toute cette classe restait irrecuperable.
La bonne question n'est pas une meilleure garde, c'est de RETENTER. Les deux
facons d'etre refuse se reparent par des choses opposees, et le memo les
distingue deja : un echec n'est deliberement pas memoise, donc une nouvelle
tentative redemande vraiment au reseau - c'est la reparation de cette classe,
pas un cout a eviter ; une absence est memoise, donc une nouvelle tentative ne
demande a personne et coute un instantane, jusqu'au jour ou la capacite arrive
d'ailleurs, seule chose qui repare celle-la. La garde est supprimee : le module
est plus petit qu'au tour precedent.
2. La reouverture faisait un ACCES sous la mauvaise identite. Republier REFILTRE
une requete existante ; rouvrir EMET un abonnement et une requete au store
public pour un document que cet abonne a nomme. C'est un acte neuf, pas un
ancien qui ressort, et l'etat final etait `isSuccess` avec `data: []` pour un
document illisible - le mensonge que cette tache supprime, par la porte du
correctif. `refusedFor` retient QUI a ete refuse ; on ne retente que tant que
c'est encore lui le connecte.
Etabli plutot qu'affirme : une application ne peut pas changer d'identite en
cours de session. `ensureIdentity` est le seul appel publie qui en etablit une,
et `resolveIdentity` commence par `getCurrentUser()`. Corrige quand meme.
3. `Promise.all` court-circuite a la premiere rejection ARRIVEE, et un fetch qui
rejette n'attend pas la barriere de synchronisation : il double un refus portant
sur un document nomme avant lui. Le comportement est corrige plutot que le
commentaire - `allSettled` puis parcours dans l'ordre du scope.
Et le quatrieme controle e2e est retire, pas repare : il passait avec toute la
machinerie de reprise supprimee. Un controle qui ne peut echouer pour aucune
regression de ce qu'il garde se lit comme de la couverture et vaut moins que rien.
Revue adversariale : la barriere livree au tour precedent rendait le refus
TERMINAL. Le signal du trousseau s'ecrit `if (query !== null) publish(...)`, parce
qu'avant la barriere il n'y avait qu'une seule raison de n'avoir pas de requete -
rien n'avait encore ete tente - et qu'une cle qui arrive ne pouvait changer que ce
qu'une requete OUVERTE avait le droit de montrer. La barriere en a ajoute une
seconde : elle abandonne AVANT que la requete existe, donc le seul canal qui fait
converger une cle tardive passait a cote de l'etat qui en avait le plus besoin.
Une page qui s'abonne pendant que son trousseau se restaure, ou un document
partage sous les yeux de son destinataire, publiait `isError` et y restait. Ce
n'est pas une surface plus stricte, c'est une pire : echanger un vide mensonger
contre une erreur bloquee ne repare rien, et cela casse une garantie que ce paquet
publie.
Le signal rouvre donc un scope refuse, et seulement quand le refus est leve
(`namedDocumentsReachable`). Le garde-fou porte autant que la reouverture :
rouvrir a chaque changement de trousseau rejouerait la barriere pour chaque cle
qu'une connexion depose, redemanderait au reseau tout document dont le
telechargement avait ECHOUE - un echec n'est deliberement pas memoise - et
republierait la meme erreur a chaque abonne. La variante indiscriminee casse deux
tests, dont celui de demontage qui preexistait.
La barriere passe en parallele. Elle etait serie, donc N documents qui ne poussent
jamais de `State` coutaient N replis bornes, 8s chacun en production. Les refus
restent lus dans l'ordre du scope apres coup.
Et ce qui a laisse passer les deux : l'e2e ne parcourait pas ce chemin. Les deux
journeys `useShapeQuery` lisent `{graphs: "did:ng:i"}`, donc 43/43 n'a jamais rien
prouve sur la barriere. Quatre controles ajoutes sur un scope NOMME, contre le
vrai broker : la proprietaire lit son document, un porteur de rien recoit
`isError` et jamais un succes vide, un document PUBLIC s'ouvre pour lui depuis la
seule reference via la requete de la barriere, et une cle qui tombe pour un AUTRE
document laisse le scope refuse en place.
Majeur : trois appels existants se resserrent. `useShapeQuery` publie `isError`
pour un document NOMME qu'il ne peut pas lire, la ou il rendait `isSuccess` avec
`data: []` - la seule image que cette surface reserve a « ce scope ne contient
rien ». `inbox.postToDocument`, `readForDocument` et `readSyncedForDocument`
rejettent la ou elles annoncaient « pas d'inbox ». Rien de ce qui etait deja une
reponse ne bouge : un document vide, `graphs: []`, un document sans inbox, un
document public qui n'expose aucune cle.
Deux non-garanties corrigees parce que le changement les rend fausses : « aucune
des deux lectures par forme ne va chercher la cle d'un store public » ne vaut
plus que pour `useShape` et pour `did:ng:i`, et le pending eternel d'un demarrage
refuse se referme sur le cas nomme.
Nouveau `knowledge_what-a-failed-read-answers-upstream` : ce que la cible repond
pour un document injoignable, non synchronise ou vide, et les deux collisions que
l'appelant ne peut pas demeler - « n'existe pas » contre « pas connu de moi »
d'un cote, « non synchronise » contre « vraiment vide » de l'autre. C'est le fait
durable qui justifie la direction prise, et il vaut quel que soit le remede.
`docs/internal-contract.md`, section open-repo seulement : numeros de ligne,
`ensurePhysicalRepoOpen` qui n'a jamais ete un export de ce module,
`ensureReposOpen` supprime, `openRepoUnguarded` absent, et le second parametre de
`ensureRepoOpen`. Le reste du document porte une derive du meme age, laissee.
pnpm-workspace.yaml listed only packages/*, so pnpm never installed
examples/notebook's dependencies even though root package.json's
workspaces field named examples/* too; the example ran on a stray
node_modules a manual bun install had left behind (symlinks into
node_modules/.bun, including a dangling @ng-eventually/sdk link to a
package that no longer exists). Added examples/* to pnpm-workspace.yaml
and reinstalled; examples/notebook now resolves @ng-org/orm and
@ng-org/web to the exact same pnpm store instances packages/polyfill
uses (verified via the symlinks and node_modules/.pnpm listing) - no
package ended up duplicated.
That alone did not fix the example's TS2345: NoteShape's object
literal had its dataTypes[].valType widened from the "iri"/"string"
literals to plain string (no satisfies clause), which the existing
test suite already guards against elsewhere (satisfies
ShapeType<BaseType>). Applied the same pattern here, importing the
already re-exported ShapeType/BaseType from @ng-eventually/polyfill -
no new dependency needed, no compiler option touched.
Les quatre paquets @ng-org sont deja a leur derniere version publiee
(web alpha.14, orm alpha.21, shex-orm alpha.9, alien-deepsignals
alpha.13) : le refresh ne deplace rien. Ce qui restait faux, c'est la
doctrine, laissee en arriere par le refresh precedent.
Le point de fond : le checkout epingle de nextgraph-rs (213338f6)
declare orm alpha.18, donc la source citee est TROIS versions derriere
les paquets contre lesquels ce depot compile. La ou les deux divergent,
les leaves nomment desormais les deux.
Faits corriges, tous verifies dans node_modules :
- les versions installees annoncees en tete de leaf (alpha.19/13/11)
- OrmSubscription est renomme RdfOrmSubscription dans alpha.21, fichier
compris (connector/RdfOrmSubscription.ts)
- alpha.21 exporte removeObject(graphNuri, subjectIri), absent de la
source ; sa propre documentation se contredit sur sa portee
- getOrCreate prend un RdfOrmConfig, plus un Scope nu, et l'instance
gagne mode/isReady/addChangeListener/nextPage/previousPage
- Scope.graphs est REQUIS dans alpha.21, et les deux axes lisent [] en
sens INVERSE : graphs [] = ensemble vide, subjects [] = aucun filtre.
La table le dit explicitement, c'est le piege deja paye une fois
- la lecture ORM live DEMARRE contre le deploiement (5 passed, 0 failed,
pret en 13ms) ; le lien mort vers bug_useshape-live-read-needs-a-newer
-broker disparait, ainsi que le "exit non-zero attendu" de la suite
Ce qui n'est PAS affirme : que le where/orderBy/pageSize d'alpha.21
atteigne le moteur. A 213338f6 start_orm construit encore son config a
partir de json!({}) derriere un TODO, donc la richesse est exprimable au
niveau 3 et jetee au niveau 1. Non mesure, donc non ecrit.
Le contrat publie et l'api-contract etaient traites avec le retrait lui-meme ;
restaient les documents qui decrivaient readUnion comme la primitive de
lecture du paquet, ou qui nommaient le module disparu.
Ce que les lecteurs apprennent maintenant : lire un document, c'est
docs.sparqlQuery ancre dessus, et lire plusieurs documents, c'est cet appel
une fois par document -- avec la decision sur une lecture qui echoue ecrite
au point d'appel. Deux consequences sont dites plutot que decouvertes : une
lecture refusee leve au lieu de rendre une liste vide, et les sujets sous le
prefixe reserve urn:ng-eventually: reviennent dans le resultat, a l'appelant
de les ignorer -- sans aucun gain de droits, les deux registres qu'un
document ordinaire porte etant l'adresse de sa boite et, pour un document
public, la cle que son magasin sert deja a qui la demande.
Les briefs dates ne sont pas touches : ils decrivent un etat de juillet, et
le reecrire falsifierait ce qui avait ete decide alors.
readUnion etait N requetes ancrees plus une politique de lot : une lecture
qui echoue avalee en reponse vide, un document dont on ne detient pas la cle
ecarte au lieu d'etre refuse, un regroupement en sacs de proprietes, et le
filtrage du prefixe reserve. Rien en amont ne repond a cette composition, et
son annotation citait sparql_query -- une citation exacte attachee a une
affirmation fausse. Une composition ne s'aligne pas sur une fonction.
Ce qui reste est ce qu'il y avait dessous : docs.sparqlQuery ancre sur un
document, qui va deja chercher la cle servie par le magasin public -- donc
une reference nue suffit toujours pour lire un document public, sans rien
changer a useShapeQuery. Lire plusieurs documents, c'est cet appel une fois
par document, et la tolerance a une lecture qui echoue revient a l'appelant,
la ou l'amont la laisse : une requete ancree sur un depot que le verifier ne
detient pas rejette.
Cote src, l'unique lecture interne restante devient read-document.ts : un
document, une requete ancree, precedee de l'ouverture du depot -- barriere
que index-deposit consulte pour distinguer une absence d'un depot non
synchronise. Le harnais e2e et l'exemple composent la leur, avec leur catch
visible au point d'appel.
Version majeure : 1.0.0-dev.8.
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.
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.
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é.
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.
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.
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.
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.
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.
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 8c8ade7 et e32b6d0.
Coalescence : une seule échéance en attente par cible, et deux drainages d'une
même inbox ne se chevauchent jamais — processInbox écrit ce qu'il applique.
Limite assumée : si la page disparaît avant l'échéance, le dépôt attend la
prochaine connexion. C'est le comportement honnête d'une émulation qui tient la
place d'un service absent.
La feuille disait le contraire, et c'est moi qui l'ai écrite hier : « deux
exécutions ne doivent jamais se chevaucher », « c'est désormais tenu par un
verrou », « une suite d'un dépôt consommateur entre en collision ». Les trois
sont fausses depuis l'extraction.
Chaque exécution a son propre répertoire de profil et tue le navigateur qui le
détient en finissant. Les exécutions concurrentes — y compris celle d'une
application consommatrice, depuis son propre dépôt — sont indépendantes par
construction.
Et la leçon de forme est consignée, parce qu'elle se généralise : le verrou ne
traitait qu'un symptôme, supprimer le partage a supprimé le besoin. Quand deux
exécutions se disputent une chose, la rendre propre à chacune plutôt que de
sérialiser l'accès.
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.
La leçon du tour, et elle valait des jours : SIGN_IN_MS valait 180 s sur des
étapes totalisant 270 s. La borne du dessus se déclenchait donc toujours la
première, et chaque échec rapportait son nom à elle — jamais celui de l'étape en
cause. On a cherché une cause que le harnais était structurellement incapable de
nommer.
D'où les deux règles consignées : calculer une borne englobante à partir de ses
parties au lieu de choisir un nombre, et dimensionner chaque borne terminale sur
une durée MESURÉE inscrite à côté d'elle. Un chiffre nu ne dit pas s'il est
généreux ou serré, et pourrit sans que personne le voie.
Plus une troisième : déclarer les parcours et leurs vérifications avant que quoi
que ce soit puisse échouer, pour qu'une exécution rende toujours le même nombre
de lignes. Quand le total bouge avec la panne, deux exécutions ne sont plus
comparables — et un total qui rétrécit se lit comme un problème plus petit alors
qu'il est plus gros.
Enfin, un fait observé : notre verrou ne garde que ce dépôt. Une suite
appartenant à une application consommatrice, lancée depuis son propre checkout
contre le même broker, entre en collision exactement comme deux des nôtres — et
c'est invisible des deux côtés.
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.
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é.
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é.
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.