Recherche menee sur les 94 citations vers engine/ et sdk/, en comparant
l'arbre cite aux artefacts publies. Onze citations de niveau 2
(doc_subscribe, sparql_update, sparql_query, doc_create) sont
identiques de part et d'autre, ainsi que ShapeType, Schema et
DeepSignalSet. Trois ne l'etaient pas.
- Scope : la revision citee declare `graphs?` OPTIONNEL,
@ng-org/orm@0.1.2-alpha.21 le rend requis - et c'est le paquet
installe qui est reexporte ici.
- BaseType : la revision citee garde "@graph" en commentaire derriere un
TODO, @ng-org/shex-orm@0.1.2-alpha.9 le declare.
- NG : le compte de membres disait 88 en neuf endroits, dont un marque
VERIFIED et un autre annoncant une mesure contre alpha.14. Le paquet
installe en declare 90 - 84 fonctions et 6 classes RDF - et deux des
quatre-vingt-dix, graph_orm_next_page et graph_orm_previous_page, sont
absents de l'arbre cite. Le compte vit desormais a un seul endroit,
avec la version a laquelle il appartient ; ailleurs il disparait, parce
que la phrase disait "tous les membres" et que le chiffre n'y ajoutait
qu'une occasion de perimer.
Les deux premieres reprennent la forme que RdfOrmConfig et
OrmSubscriptionHandle portaient deja : une clause finale nommant la
version publiee la ou elle diverge de la revision citee.
Une citation nomme un chemin et un symbole, jamais une version. Or le
checkout contre lequel toutes les citations `engine/` et `sdk/` se
resolvent declare des versions inferieures a celles que ce depot
installe, donc une citation prouve "c'etait vrai a ces versions-la" la
ou un lecteur lit "c'est vrai de ce qu'on construit".
La version est une propriete du CHECKOUT, pas d'une citation : aucune
des annotations n'est touchee. Elle est enregistree une seule fois, dans
l'`ANCHOR` de provenance.test.ts, a cote du resolveur qui s'en sert.
Ce que le mecanisme verifie maintenant, aux DEUX bouts de l'ecart, parce
qu'un enonce d'ecart peut perimer par l'un ou l'autre et a deja perime
par le bout eloigne : les versions que le checkout declare (a vide quand
le checkout est absent, comme la verification de resolution), celles que
les manifestes de l'espace de travail epinglent, et celle qui est
reellement installee sous packages/polyfill.
Pas un nouveau `kind` : les kinds classent la preuve d'UN symbole,
l'anciennete de toute la preuve appartient a l'arbre.
Dernier chemin pouvant remettre une barriere deja oubliee. Le rejeu
lisait `lastState` a la souscription et le livrait une micro-tache plus
tard ; l'etablissement peut echouer dans cet intervalle, et
`reportSetupFailure` desavoue son `State` avec lui.
La fenetre est d'exactement UN saut de micro-tache, mesuree et non
raisonnee : a zero saut l'effacement n'a pas encore eu lieu, a deux ou
plus `lastState` est deja nul donc aucun rejeu n'est mis en file et
l'arrivant ouvre une tentative neuve. `isCurrentAttempt` ne couvre pas
ce cas : cet echec n'incremente pas `attempt`, donc l'entree et la
tentative sont toutes deux encore les courantes.
DIAGNOSTIC, et cadre comme tel : `getSyncState` lit l'enregistrement et
non la poussee, donc il repond toujours `"timed-out"` et aucune lecture
ne gagne d'autorite. Ce que la livraison perimee cassait est l'attente
bootstrap de `open-repo`, qui franchissait sa barriere dessus et
journalisait `BARRIER <nuri> synced` pour une barriere qui n'existait
plus - or cette ligne de trace est celle qui distingue une absence reelle
d'une lecture non synchronisee.
J'avais ecrit ce remede puis l'avais RETIRE, faute de test qui
discrimine. L'ordonnancement ci-dessus est ce qui manquait ; le test
echoue sans le correctif et passe avec.
Trouve en enumerant moi-meme les ecritures de `establishing` et de
`lastState` plutot qu'en croyant l'argument du commit precedent, qui
affirmait que le garde retire etait inatteignable. Il ne l'etait pas.
`isCurrentAttempt` dit que cette entree et cet appel sont encore les
bons ; il ne dit rien sur le fait que l'appel ait REUSSI. Le courtier
expedie avant de repondre, donc un `State` peut arriver apres que
l'appel a deja ete rapporte en echec - et ce module tient lui-meme pour
atteignable qu'une poussee survive a son demontage, c'est la raison
d'etre du garde sur ce callback. Mesure : la poussee tardive recreait la
barriere morte que `reportSetupFailure` venait d'oublier, sans canal
derriere, et `getSyncState` y repondait `"synced"` - donc autorite.
`lastState !== null` implique desormais qu'une tentative tient, a
l'ecriture comme a l'effacement, donc les deux ne peuvent plus diverger
et aucun lecteur n'a a le reimposer. La poussee reste diffusee aux
auditeurs : seule la BARRIERE est une affirmation sur le document.
Seconde revue adverse, sur le correctif precedent - et elle a raison :
celui-ci MASQUAIT la barriere perimee au lieu de l'oublier.
`reportSetupFailure` effacait `establishing` et laissait `lastState`. Le
premier correctif avait ajoute « et un canal tient » du cote du LECTEUR,
ce qui ne cachait la barriere morte que tant que rien ne tenait. La
souscription suivante sur le meme fan-out remettait `establishing` a
vrai, le masque tombait - et le fan-out rejouait ce `State` au nouvel
arrivant. Mesure : l'ouverture bootstrap franchissait sa barriere en 2 ms
sur un `State` d'un appel qui avait ECHOUE, journalisait
`BARRIER ... synced`, et `getSyncState` repondait `"synced"` - donc
autorite, donc `noRowsMeansAbsent` prenait une lecture a zero ligne pour
une absence definitive. Le depot detruit que le commit precedent
pretendait empecher.
Corrige a la source : une tentative qui echoue oublie sa barriere, comme
le font deja `releaseFanOut` et `resubscribeDocs`. Le garde du lecteur
est RETIRE avec, et pas garde en ceinture-bretelles : il etait devenu
inatteignable, et c'est lui qui avait cache la faute. Apres : la barriere
est oubliee, l'ouverture attend la sienne (202 ms), `"timed-out"`.
Et le test que j'avais ecrit affirmait le defaut - `barrierCrossed`
vrai - au lieu de l'etat correct. Corrige, plus un second test qui
conduit le rejeu de bout en bout.
Chacun reproduit avant d'etre corrige, chacun epingle par un test qui
echoue si la correction est retiree.
1. Une barriere pouvait survivre a la tentative qui l'avait produite.
`getSyncState` repondait `"synced"` sur le seul `barrierCrossed`, alors
que son propre commentaire disait « sur le canal qui tient MAINTENANT » :
`reportSetupFailure` efface `establishing` et laisse `lastState`, donc un
`doc_subscribe` qui poussait un `State` puis echouait laissait une
autorite - et `noRowsMeansAbsent` prend une lecture a zero ligne pour une
absence definitive. Le registre precedent repondait `"timed-out"` la ;
strict est la lecture de cette famille, son cout est la latence, jamais
la perte.
2. `release` ne repondait pas « est-ce que j'ecoute encore ».
`resetDocSubscriptions()` largue les auditeurs et supprime l'entree sans
rien dire, donc le handle continuait de paraitre vivant ; si autre chose
souscrivait d'abord au meme document, le court-circuit sautait l'ouverture
due et le document restait tenu par un auditeur libre de partir. Le
fan-out numerote desormais ses incarnations, et l'ouverture compare.
3. Une tentative sans reponse TIENT quand meme. Elle ne comptait pas, si
bien que chaque lecture d'un document dont le `doc_subscribe` ne repond
jamais repayait le repli borne - 8 s en production, sur le chemin de la
lecture ancree. Un second `doc_subscribe` est de toute facon impossible
tant qu'un est en vol, donc la tentative fraiche n'achetait rien.
Les trois questions sont maintenant une fonction nommee, `stillStanding`,
et chacune est posee a qui detient la reponse.
L'enonce disait « seulement l'auditeur pose » ; l'enregistrement porte
aussi l'attente bornee. Les deux sont du ressort de la demande, pas du
fait - c'est ce que la phrase doit dire.
Le JSDoc de `resetOpenedRepos` parlait encore de demonter "les
souscriptions bootstrap detenues", et celui de `getSyncState` decrivait
ses valeurs comme stockees. Les deux disent maintenant ce que le code
fait : liberer les auditeurs et desavouer les documents, et deriver.
Le commentaire de `namedDocumentsArrived` decrivait `open-repo` comme
gardant trois valeurs, et disait que l'attente "resout sur le State ou
sur le repli, et chacun pose sa propre valeur" - plus vrai depuis que
rien ne les stocke.
Deux blocs disaient « la souscription est maintenue vivante pour la
session », l'un accroche a onSetupFailed et l'autre a l'appel de
souscription - exactement le defaut de commentaire orphelin que cette
tache devait corriger ailleurs.
Trois reponses coexistaient et pouvaient se contredire : un ensemble de
souscriptions bootstrap et une carte d'etat de barriere dans `open-repo`, et les
auditeurs du fan-out dans `surface/subscribe`. Les deux premieres survivaient a
la disparition de la troisieme, parce que `resetDocSubscriptions()` - ce que fait
le `configure()` publie - largue ses auditeurs sans rien dire a personne.
Il n'en reste qu'une, l'etat de l'unique souscription reelle du document, exposee
par `docChannel(nuri)`. `open-repo` n'enregistre plus que l'auditeur qu'il a pose
- « est-ce que j'ecoute encore », rien de plus - et derive `getSyncState` a chaque
lecture, si bien qu'aucune reponse ne peut survivre au canal qu'elle decrit.
Disparaissent : `held`, `syncState`, `inFlight`, `settleInFlight` et le compteur
de generation, remplace par l'identite de l'enregistrement.
Les deux echecs prouves deviennent des tests qui echouent sans le changement :
un second `configure()` apres une synchro reussie (`"synced"` au lieu de
`"timed-out"`, zero re-souscription, et `isSuccess` cote application), et un
etablissement encore en cours quand la couche de souscription est demontee - cas
que le canal `onSetupFailed` ne peut pas rattraper, puisqu'il ne rapporte que la
tentative encore attendue.
La ligne qui ne casse pas : un document synchronise qui ne contient rien repond
toujours un succes a zero objet. `1.0.0-dev.10`, les deux non-garanties
nommees sont retirees et la garantie livree porte le test qui l'epingle.
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 ».
En relisant ce que je venais d'ecrire dans le contrat - « si le document se
synchronise plus tard et apporte des donnees, la lecture repond d'elle-meme » -
j'ai constate que c'etait faux. `onState` sortait aussitot des lors que le repli
borne avait deja termine l'ouverture, donc un `State` tardif etait jete :
`"timed-out"` restait indefiniment sur un depot desormais synchronise, et le
lecteur qui consulte cet etat ne pouvait jamais l'apprendre.
Le repli veut dire « cette ouverture a cesse d'attendre », jamais « la barriere ne
sera pas franchie » : l'abonnement n'est pas ferme, donc le `State` arrive encore.
`onState` enregistre desormais `"synced"` avant le controle. `"timed-out"` ->
`"synced"` est la seule transition ajoutee, et elle va de moins d'autorite vers
plus ; les deux lecteurs de cette valeur la veulent ainsi.
(dd) l'epingle : elle tombe si l'enregistrement repasse sous le controle.
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.
(p) `graphs: ""` est l'autre orthographe du dataset entier, jamais un nom de
document : le filtre reste seul a decider. C'est une garde, pas un test
discriminant - `isNuri` ecarte deja la chaine vide -, et elle epingle le
comportement plutot que le chemin.
(q) se desabonner pendant que la barriere est en train d'etre franchie ne doit
laisser aucune requete ouverte. La barriere rend l'ouverture asynchrone, donc il
existe desormais une fenetre ou l'observable a des abonnes, pas de requete, et une
ouverture en chemin ; verifie en retirant le controle de generation, ou la
requete est creee apres le dernier auditeur et plus rien ne la ferme.
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.
`fetchReadCap` rendait `false` pour trois situations sans rapport : l'emulation
inerte, le document qui n'expose aucune capacite, et le reseau qu'on n'a pas pu
interroger. Les trois appelants vivants lisaient ce `false` comme « pas de
capacite » et transformaient une panne en affirmation sur le document :
`ensureRepoOpen` renoncait en silence et laissait la lecture suivante accuser le
lecteur de ne rien detenir, et `documentInboxAddress` repondait « ce document n'a
pas d'inbox », ce qu'une application lit comme zero depot.
Le telechargement rejette desormais jusqu'a l'appelant, avec le document nomme et
la cause d'origine conservee. `false` ne veut plus dire qu'une chose : le document
n'expose rien, et c'est une reponse. Le memo garde la meme asymetrie qu'avant.
`documentInboxAddress` propage aussi l'echec de sa propre lecture d'adresse : le
`catch` qui rendait `undefined` se justifiait par un refus que `mustNotAttempt` a
deja ecarte une ligne plus haut, donc il n'avalait que des pannes.
`useShapeQuery` franchit la barriere sur chaque document NOMME par `scope.graphs`
avant d'ouvrir la requete : capacite demandee au store public, refus a voix haute
si elle n'est pas detenue, ouverture et synchronisation. Un document nomme
qu'on ne peut pas lire donnait `isSuccess` avec `data: []` apres que le filtre de
capacites l'eut vide - la seule reponse que cette surface existe pour reserver a
« ce scope ne contient rien ». Restent intacts et succes : `did:ng:i`, `graphs:
[]`, un document joignable et vide, et tout le cas inerte.
`fetchReadCaps` supprime : aucun appelant, et une politique de tolerance par lot
sans proprietaire au-dessus d'un appel qui rejette desormais.
Its only callers were its own definition and its dedicated test block;
no production code used it since the read-union removal (T01.f). The
one test that exercised readDocumentSubjects' real integration path
(not ensureReposOpen itself) is kept, moved under the ensureRepoOpen
describe block it actually belongs to.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 }).
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.