0b936d2119586bcb93167a9ee25114ec4001e707
81 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0b936d2119 |
fix: écrire est une PROPRIÉTÉ, et trois portes qui n'auraient pas dû être ouvertes
Suite de la revue adverse. Quatre trous de frontière, tous hors du champ « l'isolation est fausse jusqu'à P1b » — P1b parle de matériau de clé, ceux-ci sont des défauts de FORME et resteraient des trous avec une vraie clé. **La garde d'écriture reposait sur la mauvaise question.** Elle demandait « ce cap m'a-t-il été servi par un store public ? ». Ce prédicat était faux dans les deux sens à la fois : trop laxiste — une clé reçue dans une inbox donnait l'écriture, alors qu'en amont un Link est « external repos only » et qu'écrire est l'appartenance au repo ; trop strict — la propriétaire de son propre document public était refusée dès qu'elle l'ouvrait depuis sa référence avant que son store ne soit listé. Un prédicat poussé dans deux sens est le signe que c'était le mauvais prédicat. Écrire dépend désormais de la PROPRIÉTÉ, lue sur la branche Store (l'`AddRepo` émulé), plus la paternité de session pour les documents créés par la primitive brute qui n'a aucun store où s'inscrire. Conséquence assumée et documentée : seul le propriétaire écrit, ce qui est l'état amont d'un repo tant qu'aucun membre n'a été ajouté — mécanisme qu'on n'émule pas. **`docs.depositInto` quittait la frontière en la publiant.** Sa doc disait « `inbox.post` est le seul appelant » : vrai dans la bibliothèque, faux dès qu'on le publie. Démontré : avec la seule référence nue d'un document public, on réécrit l'adresse d'inbox posée dessus et on détourne les dépôts destinés à son propriétaire. Une porte qui saute une garde ne doit pas être ouvrable par une application — elle rejoint la machinerie. **Le filtre de lecture n'interceptait que trois membres** et transmettait tout le reste lié à la CIBLE : `.values()`, `.map()`, `.getById()` rendaient le contenu d'un autre utilisateur — précisément les membres qu'une API de set réactif met en avant. Les membres qui rendent des éléments sont désormais filtrés, les mutations passent (elles ne rendent rien), et **tout membre inconnu lève** au lieu de transmettre : une transmission est une fuite silencieuse, une levée est bruyante et greppable. **Le mémo du store public était par document.** Le premier demandeur déclenchait le téléchargement, le cap était classé chez LUI, et tout demandeur suivant recevait « oui » en ne détenant rien. En amont un broker qui sert un overlay externe répond à TOUS. Le mémo garde la valeur, l'appelant la classe pour qui est connecté. Aussi : l'exemption `declareInfrastructure` supprimée — zéro appelant, ensemble toujours vide, et une doc décrivant deux documents exemptés qui ne l'ont jamais été. Et les caps d'écriture décrits comme « partiels » sont dits **inertes**, ce qu'ils sont : `grantWrite` n'a aucun appelant de production. **Ce que l'e2e a rattrapé.** Ma première version de la garde refusait au créateur l'écriture sur un document fait par `docs.docCreate` — 7 étapes rouges contre le broker, après une suite unitaire restée verte. La primitive brute n'inscrit la paternité nulle part ; c'est ce que `mintedHere` couvre désormais. 185 tests unitaires (dont quatre régressions : la propriétaire écrit, le destinataire non, le store public sert tout demandeur, aucun membre non filtré ne transmet), e2e 40/40 et applicatif 10/10. |
||
|
|
c5878c6126 |
test: le contrôle de contrat disait oui à des noms absents
`isNamespaceMember` cherchait le nom dans **tout le texte** des modules de namespace, commentaires compris. Un nom mentionné n'importe où comptait donc comme exporté. Mesuré : `documentInboxAddress`, `escapeLiteral`, `getCaps`, `addLink`, `setCurrentUser` passaient tous — et **`linkTo`** aussi, la fonction supprimée parce qu'elle brisait la règle d'accès, dont l'absence est expliquée dans un commentaire de `surface/placement.ts`. Le commentaire qui dit pourquoi elle n'est pas là suffisait à la faire passer pour publiée. C'est très probablement ainsi que la dérive du contrat a traversé cinq sections sans rien faire rougir. Un contrôle qui dit oui à un nom absent est pire que pas de contrôle : il se lit comme une vérification. Les exports sont désormais extraits **après retrait des commentaires**, déclarations et ré-exports nommés, par un seul `moduleExports()` que les trois contrôles partagent. Vérifié : les six fantômes ci-dessus sont maintenant contrôlés, et les vrais membres (`post`, `share`, `createEntityDoc`, `docCreate`) toujours reconnus. Ajouté au passage un contrôle qui manquait dans l'autre sens : chaque groupe de namespace de l'annexe doit correspondre **exactement** aux exports réels de son module. Un membre retiré de `inbox:` ne faisait rougir personne. Trouvé par une revue adverse, reproduit puis corrigé ici. 181 tests, 0 échec. |
||
|
|
0455a408b6 |
refactor(api): le bootstrap redescend de quatre appels à un
L'objectif acté était deux appels spécifiques au polyfill, voire un. Il en publiait
quatre. Chacun des trois de trop était une raison que la BIBLIOTHÈQUE a, pas un besoin
qu'une application a :
- **`configureStoreRegistry`** existait parce qu'il y a deux internes à câbler — le SDK
injecté d'un côté, la session de l'autre. Vu de l'appelant, les deux disent « voici ce
qu'il te faut pour tourner ». Replié dans `configure`, qui prend désormais
`getSession` / `normalizeId` / `pointerGuard`.
- **`setCurrentUser`** n'a plus lieu d'être publié depuis que le portail d'accès est
passé dans le polyfill : c'est lui qui pose l'identité. Et une application qui nomme
sa propre identité est exactement le geste qui inverse le modèle — il ne doit pas
exister d'appel publié vers lequel se tourner. Le harnais e2e, lui, joue plusieurs
identités sur une même page ; il y accède par le chemin interne, ce qu'un harnais a
le droit de faire et une application non.
- **`connectedUser`** est maintenant attendu DANS `ensureIdentity`. Ce n'était pas une
commodité : la suite applicative avait montré qu'une app devait l'attendre elle-même,
sinon une note qu'on venait de lui partager se lisait comme illisible. J'avais traité
le symptôme dans l'app d'exemple ; le défaut était côté bibliothèque. En amont, ouvrir
la session EST la connexion — aucune application n'attend un second appel.
Reste donc `configure({ … })`, plus `await ensureIdentity()` dont le site d'appel
survit à la migration : une application attendra toujours une session avant de rendre.
Le test étendu hier a fait son travail : les deux contrôles de contrat sont passés au
rouge sur `configureStoreRegistry`, `connectedUser` et `StoreRegistryDeps` dès que la
surface a bougé.
180 tests unitaires, e2e 40/40 (3,4 min) et applicatif 10/10 (0,8 min).
|
||
|
|
b98fcaa77d |
docs+test: le contrat avait dérivé — et le mécanisme ne voyait pas les règles
En vérifiant l'alignement de la surface, cinq sections du contrat s'étaient désynchronisées du code sans que rien ne rougisse : - § 1 montrait `getConfig`, `getStoreRegistryDeps`, `resetConfig` et `resetStoreRegistry` comme exportés — retirés à la fusion des portes ; - § 11 documentait `escapeLiteral` / `escapeIri` / `assertNuri` comme publiés — ils ne le sont plus, et l'absence de garde de type est désormais expliquée par sa raison : les portes valident elles-mêmes (`NuriLike`), publier une garde inviterait le cast que les types servent à empêcher ; - § 12 listait sept fonctions `storeRegistry` — il y en a cinq depuis que les deux fonctions d'ADRESSE d'inbox sont parties (une app nomme un document ou une personne, jamais une adresse) ; - § 13 listait `IdentityStore`, `browserIdentityStore` et `getCurrentUser` comme publiés — retirés le 2026-08-05 ; - `ensureIdentity` était publié **sans aucune règle**, et l'annexe renvoyait à un « § 2bis » qui n'existait pas. **§ 2bis est écrit** : le portail d'accès n'a aucune contrepartie en substance — en amont un utilisateur ouvre SON portefeuille et il n'y a rien à nommer — mais son SITE D'APPEL survit, et c'est pourquoi sa signature ne prend pas d'identifiant : nommer son identité est précisément la partie qui disparaît, donc elle ne doit pas figurer dans les paramètres. **Le mécanisme est étendu.** `test/vocabulary.test.ts` épinglait l'annexe — les NOMS — et ne voyait pas les sections, là où vivent les règles. Une règle périmée est pire qu'une règle absente : elle se lit comme vérifiée. Désormais tout `export` montré dans un bloc « ### Today » doit être réellement exporté ; ce qu'on garde pour mémoire passe en commentaire, que le contrôle ignore par construction. Les cinq dérives ci-dessus auraient été rouges le jour même. Nettoyé aussi : deux commentaires de doc orphelins dans `surface/placement.ts`, restés au-dessus de l'accolade fermante après le retrait des fonctions qu'ils décrivaient. 180 tests unitaires, typecheck bibliothèque / exemple / harnais. |
||
|
|
aa6bbc436e |
test(e2e): les parcours applicatifs passent par l'app d'exemple
Une seconde suite e2e, `packages/sdk/e2e/notebook.ts` (`test:e2e:app`), qui pilote `examples/notebook` par le DOM — une page de navigateur par identité — contre le même broker réel. **Pourquoi une seconde suite plutôt qu'un ajout dans la première.** `run.ts` parle à un sac de méthodes sur `window.__sdk` : cela prouve que les fonctions TOURNENT, jamais qu'une application peut s'écrire avec. L'écart a déjà coûté un défaut livré — l'inbox d'un document était verte ici et inutilisable en pratique, parce que le harnais pouvait faire traverser une adresse d'une identité à l'autre par une variable, canal qu'aucune application n'a. Ici, rien ne traverse que ce qui traverse dans la vie : la RÉFÉRENCE d'une note, recopiée d'un écran, et un identifiant tapé dans un champ. Quatre parcours, qui se lisent comme des parcours : - Bob lit la note publique d'Alice depuis sa seule référence — la propriété pour laquelle l'émulation du store public existe, vérifiée bout en bout et sans qu'aucune clé ne circule ; - la note protégée d'Alice reste fermée jusqu'à ce qu'elle la partage — même geste côté Bob, issue opposée, décidée par où la note se trouve ; - Bob laisse un message sur la note d'Alice, et seule Alice le lit — il TROUVE l'adresse depuis la note, personne ne la lui donne ; - la liste de chacun ne contient que ses notes. **Deux étapes quittent `run.ts`** (`documentInboxDeposit`, `capsShareCap`), avec un commentaire disant où elles sont parties : ce sont des parcours, et ils valent plus joués sur deux écrans que sur deux appels d'une même page. Ce qui reste là-bas est ce qu'une application ne fait pas : primitives, caractérisation, régressions de démarrage à froid. **Trois défauts trouvés en écrivant la suite**, tous côté application et invisibles pour le harnais : `connectedUser()` devait être attendu à la connexion (sinon une note qu'on vient de vous partager se lit comme illisible — ce qui ressemble à un problème de droits alors que c'est un problème de moment) ; une réponse périmée restait affichée à côté d'une question fraîche ; et changer de portée ne rafraîchissait pas la liste. L'app affiche désormais la référence de chaque note — ce qu'aucun écran ne montre, aucun utilisateur ne peut le faire circuler. Corrigé au passage : le `.gitignore` pointait encore `packages/client/`, si bien que le commit de renommage a embarqué le profil Playwright de la suite e2e (226 fichiers). Les chemins sont réalignés et le commit précédent a été amendé — rien n'était poussé. 179 tests unitaires, e2e 40/40 (3,2 min) et applicatif 10/10 (0,7 min). |
||
|
|
0eb25286c8 |
refactor: renommer client → sdk, et fusionner les deux portes en une
Deux mouvements de surface, aucun changement de comportement. **`packages/client` → `packages/sdk`, `@ng-eventually/client` → `@ng-eventually/sdk`.** « client » ne disait rien : ce paquet EST le SDK que l'application appelle, et c'est tout ce qu'elle appelle. L'ancien nom reste comme mot-clé de recherche dans `docs/source-layout-by-fate.md` et le tableau des paquets du README. **Une seule entrée.** L'entrée `./polyfill` disparaît ; ses symboles applicatifs — `configure`, `configureStoreRegistry`, `setCurrentUser`, `connectedUser` et leurs types — vivent dans un bloc `POLYFILL-ERA` de `src/index.ts`. Ce que la seconde porte portait mérite d'être nommé avant d'être retiré : *ce qu'on importe de ce chemin est exactement ce qu'on supprimera à la migration*. Une seule porte perd ce signal — rien à la ligne d'import ne distingue `configure`, qui part, de `docs`, que le vrai SDK remplace sur place. Trois choses le portent désormais : le bloc lui-même, l'inventaire d'exports de `docs/api-contract.md` (épinglé par `test/vocabulary.test.ts`, donc il ne peut pas rancir en silence), et le contrôle de vocabulaire sur les noms publiés. **Six symboles quittent la surface au passage**, et la fusion est ce qui a rendu le choix visible plutôt qu'hérité : - `getConfig` / `getStoreRegistryDeps` — câblage interne, atteint par `shared-wallet/bootstrap` ; - `resetConfig` / `resetStoreRegistry` / `resetCaps` — remises à zéro de test, atteintes par leur chemin interne, ce qui est leur raison d'être ; - le `share` direct — `inbox.share` a toujours été la même fonction, et la publier deux fois brouillait la frontière qu'elle servait à marquer. Corrections d'affirmations fausses trouvées en chemin : le contrat annonçait `isNuri` / `hasReadCap` sur la porte SDK alors qu'ils ne sont plus exportés depuis le passage au permissif en entrée (`NuriLike` validé à la porte) ; le README du paquet documentait `capFor`, `shareCap`, `getCaps` et `publishRepoLink`, dont aucun n'existe ; et le README de l'app d'exemple affirmait que la suite e2e la pilote, ce qui reste à faire. 179 tests unitaires, typecheck bibliothèque / exemple / harnais, e2e 42/42 contre le broker en ligne — mesuré une fois après le renommage, une fois après la fusion. |
||
|
|
0832338201 |
feat: un document en store public sert son ReadCap, une référence nue suffit
Le modèle amont est explicite dans `PublicRepoLinkV0` : le lien ne porte AUCUN `read_cap`, et son commentaire dit pourquoi — *"The latest ReadCap of the branch will be downloaded from the outerOverlay, if the peer brokers listed below allow it […] the public site are served differently by brokers"* (engine/net/src/types.rs:5098). La clé n'est pas remise par un émetteur : elle est donnée par le réseau à qui la demande, parce que le broker a épinglé l'overlay externe (`expose_outer`). La bibliothèque refusait jusqu'ici la forme sans cap quel que soit le store. Sûr dans le bon sens, mais une application ne pouvait pas exprimer « fais circuler, la référence suffit » — le seul acte que le modèle rend gratuit — et son unique contournement était de distribuer la clé, ce qui détruit la confidentialité composable. `emulated-verifier/public-store.ts` émule le mécanisme SANS toucher à la garde. La possession reste l'unique critère : un document public est lisible non par exception mais parce que son cap est *obtenable*. Chaque porte de lecture demande d'abord (`readUnion`, `docs.sparqlQuery`, `ensureRepoOpen`, `documentInboxAddress`), puis le chemin ordinaire s'applique. Lire n'est pas écrire. Ce que le store sert est un droit de LECTURE : `learnFromPublicStore` le classe à part et `assertMayWrite` refuse l'écriture dessus. Sans cela une référence nue achetait une écriture, ce qu'aucun store amont n'accorde. Autres conséquences : - `recordInPublicStore` (marquer + frapper) devient `markInPublicStore` (marquer). Frapper un second cap à côté de celui qu'on vient de télécharger donnerait deux clés différentes le jour où la constante devient un secret. - `hasCap` quitte la porte polyfill : il se lisait « ai-je le droit de lire ceci ? » et un document public y répondait `false` jusqu'à ce qu'on demande son cap. Aucun appelant hors des tests. - Les tests cross-user ne font plus traverser de cap par une variable JS : Bob n'obtient que la référence nue, comme une vraie application. Écarts documentés plutôt que masqués : le pari sur un modèle DÉCLARÉ (`expose_outer` est câblé à `false` côté client et `ExtTopicSyncReq` est `unimplemented!()`), la découverte limitée à ce qu'on sait déjà nommer, `useShape` qui n'a pas d'await à dépenser, et l'absence de `locator`. 179 tests unitaires, e2e 42/42 contre le broker en ligne. |
||
|
|
94173fd3d3 |
docs: consigner l'écart sur l'inbox choisie par share
`inbox.share` dépose toujours sur l'inbox PROTÉGÉE du destinataire. En amont le
choix n'est pas fixe : un enregistrement de contact prend son inbox du PROFIL par
lequel la personne a été atteinte — `a_or_b = if details.profile.is_public()
{ "site" } else { "protected" }` (`inbox_processor.rs:787`, écrit `ng:site_inbox`
ou `ng:protected_inbox` en `:823-824`).
Mineur aujourd'hui, mais ça aplatit une distinction du modèle, et ce sera faux le
jour où une application partagera avec quelqu'un rencontré par son profil public.
Consigné plutôt que corrigé : le correctif demande une notion que rien n'a
établi ici. Ce qui EST vérifié se limite à un wallet qui contient des `sites`, un
`SiteV0` avec `id: PubKey`, `name`, `site_type` et trois stores. L'enum
`Identity`, qui nommerait le reste, est entièrement COMMENTÉ en amont
(`engine/repo/src/types.rs:586-595`) — il n'y a donc aucun modèle de profil à
lire, et rien à supposer.
Rappelé au passage, parce que la question s'est posée : le store privé n'a
aucune inbox — `new_store_default` n'en attache une que `if !private` — d'où
`InboxScope`, qui rend « l'inbox privée » inécrivable plutôt que simplement vide.
|
||
|
|
cb56f91b5e |
docs+fix: le modèle de sécurité local-first, et le retrait de linkTo qui le brisait
La règle d'accès est « qui a la référence ET la clé lit » — jamais « qui a la référence lit ». Il n'y a pas de tiers dans cette phrase : le moteur ne vérifie une permission qu'à l'ÉCRITURE, jamais à la lecture. Le contenu est chiffré, la clé EST le droit. C'est ce que veut dire local-first ici : il n'y a personne à qui demander, donc la possession est tout le mécanisme. Ce que cette règle achète, et qui n'était consigné nulle part : une référence n'accordant rien, **elle n'est pas récursive**. Un document largement diffusé peut pointer vers un document restreint — la référence dit qu'il existe, la clé dit qui le lit. D'où la confidentialité COMPOSABLE : un sommaire diffusé qui renvoie à des chapitres restreints, un événement public qui renvoie à sa liste de participants. L'auteur diffuse un document et décide encore, document référencé par document référencé, qui reçoit la clé. `linkTo` détruisait cette construction. Il rendait la CLÉ là où un appelant demande une référence, transformant la règle en « qui a la référence lit » — pour ce document et pour tout ce qu'il MENTIONNE. Ajouté et retiré le même jour. Documenté en §0, avant tout le reste, avec la raison pour laquelle un agent perd ce point avec constance : les réflexes client-serveur fournissent la moitié manquante sans qu'on s'en aperçoive — quelque part un serveur vérifierait, un lien serait un identifiant inoffensif, « Bob a-t-il le droit ? » aurait une réponse. Aucun des trois n'est vrai ici, et du code écrit là-dessus n'échoue pas : il accorde en silence. Rien ne passe au rouge. Renommé au passage : `publishRepoLink`/`isPublished` → `recordInPublicStore`/ `isInPublicStore`. Ces méthodes n'ont pas de pendant amont et « publier » ne désigne rien de précis ici. 171 tests unitaires, e2e 42/42 en 3,6 min. |
||
|
|
3c981ffadb |
docs: le nommage de NextGraph l'emporte toujours — y compris sur nos propres règles
La règle que je venais d'écrire bannissait « publish » sans réserve. Or le moteur emploie `publisher` 126 fois — `as_publisher`, `publisher_advert` — pour un rôle pub/sub sur un topic. Une règle appliquée à la lettre aurait fait rejeter du vocabulaire amont, ce qui est exactement l'inverse du but. Le principe est donc posé au-dessus, et il prime sur tout : là où la cible a un mot, c'est le mot, point. Ce qui reste banni est NOTRE « publier un document », qui n'a aucun référent en amont et recouvre trois actes distincts — placer dans un store public, rendre trouvable, remettre une clé. Ça n'autorise jamais à renommer ce que l'amont appelle `publisher`. `publisher`, `topic` et `advert` rejoignent le vocabulaire déclaré du contrôle de noms, avec la raison en commentaire. Le renommage `publishRepoLink` → `recordInPublicStore` reste justifié : cette méthode n'a pas de pendant amont, elle enregistre un document en store public (et, artefact d'émulation, lui frappe une clé). |
||
|
|
c8d02619b1 |
refactor(api): partager nomme le document, détenir répond par oui ou non
`shareCap(cap, toUser)` faisait tenir une clé à l'appelant. En amont il n'en
tient aucune : c'est le verifier qui remplit `ContactDetails.read_cap`, et une
inbox se résout depuis un profil. Cette signature a déjà changé deux fois
aujourd'hui — `(cap, toInbox)` puis `(cap, toUser)` — et les deux laissaient à
l'app quelque chose qu'elle ne tiendra pas plus tard.
- `inbox.share(doc, toUser)` : les deux choses qu'une application a, un document
et une personne. Ni la clé ni l'adresse n'apparaissent.
- `hasCap(doc)` remplace `capFor(doc)` et rend un BOOLÉEN. C'est la seule
question que le modèle admette, et l'unique appelant qui utilisait la valeur
s'en servait pour la passer à `shareCap`.
Les tests ont fait apparaître un besoin que ces retraits allaient casser :
obtenir le lien PARTAGEABLE d'un document publié, pour le faire circuler. C'est
distinct du partage dirigé et ça existe en amont — un `RepoLinkV0 { read_cap }`
est ce qu'on passe, `ContactDetails.read_cap` est la remise à quelqu'un. D'où
`linkTo(doc)`, seul endroit où une app tient légitimement une clé : on ne peut
pas faire circuler ce qu'on n'a pas le droit de toucher. La clé d'un document
protégé, elle, ne sort jamais par là — elle passe par `share`.
171 tests unitaires, e2e 42/42 en 3,5 min (synchro à froid 29s, stable contre
30s au run précédent — le wallet par batterie tient).
|
||
|
|
da6ef4b8b8 |
test(e2e): un user physique par batterie — 20 min et 286s de synchro tombent à 3,6 min et 30s
La suite se ralentissait elle-même, de façon monotone. Chaque batterie crée ~11 identités virtuelles FRAÎCHES (`@alice-…`, `@owner-…`, `@recon-…`), chacune avec ses trois documents de scope et son inbox, et toutes atterrissaient dans le MÊME user physique — un wallet créé le 10 juillet et réutilisé depuis, que rien ne nettoyait. Or une resynchronisation à froid est O(taille du user physique), ce que la doc de cette bibliothèque énonce elle-même. D'où 250s il y a une semaine, 286s avant-hier, et une batterie qui a fini par dépasser les 20 minutes. Les identités fraîches ne sont pas la faute : ce sont elles qui rendent une batterie reproductible, une inbox stable accumulant sinon les dépôts des runs précédents. La faute était de conserver le user physique qui les héberge. Mesuré : 42/42 en **3,6 min** au lieu de 20+, synchro à froid la plus lente à **30s** au lieu de 286s. Le profil reste persistant À L'INTÉRIEUR d'une batterie — CONTRACT 1 et 2 testent précisément cela (reconnexion fidèle sur le même profil, absence de fork de compte au travers). Deux garde-fous pour que la prochaine dérive se voie : - **Le budget appartient au runner**, qui échoue en nommant la cause probable. Un `timeout` posé autour de la commande tuait le navigateur, et la suite rapportait « Target page, context or browser has been closed » — un message qui se lit comme un défaut applicatif, et que j'ai diagnostiqué deux fois de travers avant de comparer les durées. - **La synchro à froid remonte dans le résumé.** C'est le nombre qui a dérivé pendant un mois sans que personne le regarde, parce qu'il n'apparaissait qu'au détour de la ligne de détail d'une étape. |
||
|
|
ebf866b1f2 |
refactor(api): précis en sortie, permissif en entrée — plus de guard publié
Un polyfill ne doit rien faire de plus que ce qui est prévu. `isNuri` / `hasReadCap` et les utilitaires SPARQL `escapeLiteral` / `escapeIri` / `assertNuri` n'ont de pendant à aucun niveau et n'en auront pas : le binding prend `nuri: String`, le moteur est fortement typé en Rust et n'a besoin d'aucun prédicat, l'ORM n'expose rien de tel. Le contrat les justifiait parce qu'ils « restent utiles à n'importe quelle app » — c'est exactement le raisonnement à refuser : utile n'est pas prévu, et chacun serait un appel à réécrire le jour du SDK. Le besoin d'un guard venait de notre propre signature : les entrées publiques exigeaient `Nuri`, donc un consommateur devait narrower ce qu'il lisait d'une URL ou du stockage. Elles prennent désormais `NuriLike` — n'importe quelle chaîne — et valident à l'intérieur (`toNuri`). Ce que la bibliothèque REND reste typé `Nuri` : l'app en profite gratuitement, et un type plus large ne cassera rien quand le SDK rendra des chaînes. Les guards et les utilitaires restent, internes, là où la validation se fait. Un défaut introduit puis corrigé en chemin, qui valait le test qu'il a produit : `readUnion` a toujours toléré les trous dans sa liste — un index de scope peut porter une entrée blanche, et un appelant qui assemble depuis des valeurs optionnelles n'a pas à compacter. Valider AVANT de filtrer a transformé cette tolérance en exception. Vide est une absence, pas une référence malformée ; les deux sont désormais distingués par un test. 170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la bibliothèque, l'exemple et le harnais. |
||
|
|
54f8389e9e |
refactor(api): l'app nomme une personne ou un document, jamais une adresse d'inbox
L'app d'exemple a servi de juge, et elle a immédiatement montré ce que l'inventaire ne montrait pas : pour partager une note elle résolvait l'inbox du destinataire, pour lire ses messages elle résolvait l'adresse de la sienne. Deux gestes qu'aucune application n'aura à faire une fois la chose native — donc deux gestes qu'elle ne doit pas apprendre. - `shareCap(cap, toUser)` remplace `shareCap(cap, toInbox)`. Partager est un acte envers quelqu'un ; où est son inbox regarde la bibliothèque. - `inbox.readForDocument(doc)` : le propriétaire lit ses messages en nommant la note, comme le déposant la nomme pour en laisser un. - `storeRegistry.userInbox` et `documentInboxAddress` sortent de la surface publiée. Ils restent joignables en interne, où le shim en a besoin. Sortent aussi de `/polyfill`, chacun parce qu'une app qui code contre apprend ce qu'il faudra désapprendre : - `getCaps` / `CapRegistry` — la salle des machines. La question du consommateur est `capFor(doc)` : est-ce que je le détiens ? Le registre n'a ni successeur ni forme inerte ; ce qui s'appuie dessus sera à réécrire, pas à laisser en place. - `getCurrentUser` — une app sait qui elle a connecté ; le redemander à la bibliothèque est une commodité du wallet partagé. - `virtualUsers` / `IdentityStore` — se souvenir d'une identité entre deux sessions est aussi le travail de l'app en amont. L'écran d'accès persiste ce dont IL a besoin ; rien d'autre n'a à être exposé. Reste sur `/polyfill` ce qu'une app appelle vraiment : `configure` et `setCurrentUser`. Le reste y est du test ou de l'injection interne. 170 tests unitaires, e2e 42/42 contre le broker, typecheck vert sur la bibliothèque, l'exemple et le harnais. |
||
|
|
d35e735c8b |
feat(example): une app d'exemple, écrite comme un consommateur
Le harnais e2e parlait à un sac de méthodes posé sur `window.__sdk`. Il prouvait que les fonctions s'exécutaient, jamais qu'on pouvait écrire une application avec — et cet écart a livré un vrai défaut : l'inbox d'un document était verte en test et inutilisable en vrai, parce que le harnais faisait traverser une adresse d'une identité à l'autre par une variable, ce qu'aucune application ne peut faire. `examples/notebook` est une application minimale en DOM natif, qui résout `@ng-eventually/client` comme un consommateur externe (workspace, dépendance déclarée, aucun import privilégié). Elle ne peut faire que ce qu'une application peut faire. Elle s'est déjà payée deux fois pendant son écriture : - `UnionSubject.subject` et `.graph` étaient typés `string` alors que ce sont toujours des références de document. Un consommateur devait donc caster ce qu'il venait de lire avant de le repasser — un cast à cet endroit précis rouvre la confusion que les types template literal existent pour fermer. - l'écran d'accès normalisait ce que l'utilisateur SAISIT mais pas ce que l'URL porte, si bien qu'un lien `?ng-id=@Erin` ouvrait un espace différent de celui de la même personne tapant `erin`. Une seule normalisation désormais, celle du registre. Le domaine est volontairement mince — des notes — mais suffit à exercer le placement par scope, la possession de caps, le partage dirigé, les inbox par document et la lecture réactive. 170 tests unitaires, typecheck vert sur la lib, l'exemple et le harnais. |
||
|
|
66a40fbb89 |
feat(access-gate): le parcours de connexion passe dans le polyfill
Chaque étape de cet écran n'existe que parce qu'un wallet héberge plusieurs
identités. Une app qui l'implémente écrit du code qu'elle devra supprimer, et
pire, du code qui enseigne à ses auteurs un modèle que NextGraph n'a pas :
« je nomme mon identité ». Le premier consommateur en avait ~300 lignes — un
gate, un écran, un module wallet, un contexte d'identité, trois features. C'est
le travail de la bibliothèque, pas le sien.
`ensureIdentity()` : un appel, avant que l'app s'affiche. Il ne prend PAS
d'identifiant — nommer une identité est l'étape qui disparaîtra, donc elle ne
doit pas figurer dans la signature. Le jour où le wallet fournit l'identité,
l'appel se résout sans rien afficher et le code de l'appelant ne bouge pas.
L'écran est en DOM natif, sous shadow root : c'est une barrière technique
montrée avant qu'une application s'affiche, comme une demande de mot de passe
sur une bêta fermée. La lier à un framework obligerait chaque consommateur à
adopter ce framework pour un écran voué à disparaître.
L'ordre de résolution de l'identité est pinné par des tests, parce que s'y
tromper échoue en SILENCE : le parcours traverse deux partitions localStorage
distinctes — la page et l'iframe du broker — et seul l'URL franchit la
frontière. Si le stockage l'emportait, l'iframe lirait une identité vide,
provisionnerait un second utilisateur virtuel, et l'utilisateur reviendrait dans
un espace vide sans la moindre erreur.
Les identifiants du wallet partagé (fichier, mot de passe) passent par
`configure()` : ce sont des données de déploiement, et cet appel est déjà celui
qui devient inerte à la migration. Au passage, l'ancien champ `sharedWallet:
{ name, secret }` — inutilisé nulle part — est remplacé.
168 tests unitaires, typecheck vert.
|
||
|
|
c42236bc00 |
test: l'inventaire du contrat est tenu par un test, plus à la main
L'appendice « inventaire des exports pour diff » d'`api-contract.md` était périmé : il listait encore les internes du shim dans le namespace `storeRegistry` alors que l'entrée avait été réduite à sept fonctions. Or c'est précisément l'instrument qu'on diffe quand la surface bouge — et un inventaire périmé est pire qu'aucun, il se lit comme vérifié. Régénéré depuis les `export`, et désormais tenu par `vocabulary.test.ts` : si la liste et le code divergent, le test échoue. Le document suit le code au lieu de dériver. Au passage, le contrôle de vocabulaire suit maintenant `export * from`, ce qui lui a fait voir trois types qu'il ignorait — d'où le suffixe structurel `…Like` (`NgLike` = « ce qui a la forme de ng ») déclaré comme de la glue de typage et non un mot de domaine. 162 tests unitaires, typecheck src/test/e2e vert. |
||
|
|
3257afe8c0 |
feat(inbox): un utilisateur a DEUX inboxes, comme en amont
Tranché par la cascade plutôt qu'en attendant une réponse : le niveau 3 (ORM) ne dit rien des inbox, le niveau 2 non plus — `@ng-org/web` n'expose aucune méthode contenant « inbox » et la session n'en publie aucune. C'est donc le modèle du moteur qui décide, et il dit DEUX : un site porte une inbox sur son repo de store public et une autre sur son protégé (`engine/verifier/src/site.rs:127-152`), les seuls `AddInboxCap` du moteur, `new_store_default` n'en posant une que `if !private`. Elles sont adressées séparément jusque dans les enregistrements de contact, qui choisissent leur prédicat selon le profil visé — `ng:site_inbox` pour un profil public, `ng:protected_inbox` sinon (`engine/verifier/src/inbox_processor.rs:787,823-824`). `userInbox(id)` en exposait une : une cardinalité que cette bibliothèque avait inventée, et que le nom `walletInbox` avait contribué à masquer. Elle prend désormais le scope, et le store PRIVÉ n'en a pas — d'où `InboxScope` plutôt que `Scope` : demander l'inbox privée n'est pas une recherche qui ne rend rien, c'est une question sans référent dans le modèle, et le type l'interdit. `myInboxes` énumère les deux, `isOwnInbox` reconnaît les deux. Le shim garde un triple par (user, scope). 160 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker. |
||
|
|
b62bfe1e63 |
refactor: ouvrir une inbox est un acte de registre, et deux invariants rendus explicites
`openDocumentInbox` passe du shim aux registres de branche. Ce qu'il fait est de
la comptabilité du verifier : vérifier la propriété, enregistrer la moitié
lecture sur la branche User, publier l'adresse. Seule la création du document
support relève du shim, et elle est appelée, pas hébergée. En amont l'acte
équivalent est générer une paire de clés et commiter `AddInboxCap`.
Deux risques de migration signalés par le contrat interne, transformés en
invariants vérifiés plutôt que supposés :
- **Le couple `(document, inbox)`** est un littéral RDF séparé par une espace là
où l'amont a une structure typée (`AddInboxCapV0 { repo_id, overlay, priv_key }`).
L'espace est sûr parce qu'un NURI n'en contient pas — alphabet base64url et
segments `:` — mais c'était une propriété implicite. `encodeInboxCap` la
vérifie désormais : un découpage erroné classerait une inbox sous un document
tronqué et perdrait les dépôts sans erreur, la classe de panne que ce chemin a
déjà payée une fois.
- **Le namespace réservé** garantit qu'aucun identifiant utilisateur ne peut s'y
loger — sauf que `normalizeId` est injecté par le consommateur et que le
défaut de la bibliothèque ne fait que trimmer. Une collision ne serait pas
cosmétique : un utilisateur se retrouverait sur un compte d'infrastructure, à
lire et écrire des documents qui ne sont pas les siens. Vérifié à la
normalisation, avec un test qui simule un `normalizeId` hostile.
160 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker.
|
||
|
|
107f9d1633 |
refactor(vocabulary): les noms publiés parlent la langue de la cible, et un test le tient
La correction de nomenclature du 2026-07-30 — en amont un *wallet* n'est qu'un trousseau, ce qui possède des stores est un **user** (un *site*) — s'était faite à la main. `walletInbox` y a échappé et a vécu des semaines, en faisant des dégâts : le nom rendait « une inbox par wallet » évident, masquant qu'un user en a **deux** en amont (repos de store public et protected, les deux seuls `AddInboxCap` du moteur). Une discipline appliquée à la main en oublie un ; un test non. D'où `test/vocabulary.test.ts` : tout nom publié est bâti sur des mots que la CIBLE emploie — vérifiés dans `nextgraph-rs` — ou porte un marqueur disant POURQUOI il n'existe qu'ici (`virtual`, `physical`, `shim`, `emulated`, `polyfill`), ce qui dit aussi quand il disparaît. Un échec n'est pas « renommer pour faire passer le test », c'est une question : la cible a-t-elle un mot pour ça ? la chose n'existe-t-elle qu'ici ? le mot est-il vraiment de la glue ? Ce que le test a trouvé, et les réponses : - `walletInbox` → `userInbox`, avec l'écart de cardinalité écrit noir sur blanc plutôt que caché par le nom. - `accounts` / `AccountRecord` / `AccountStorage` → `virtualUsers` / `VirtualUserRecord` / `VirtualUserStorage`, module `accounts.ts` → `virtual-users.ts`. « account » n'est pas de la cible : c'est notre mot pour l'utilisateur virtuel, et le marqueur le dit désormais. - `readModel` → la fonction `readUnion`, exposée directement. « model » n'était ni de la cible ni de la glue, et le namespace ne tenait qu'une fonction. - Le reste était du vocabulaire légitime à déclarer (`subject`, `base`, `schema`, `connected`, le modèle réactif de l'ORM). Corrigé au passage, sur signalement du contrat interne : l'en-tête d'`open-repo` justifiait son correctif par un mécanisme que le source contredit. Un repo absent de `self.repos` lève bien `RepoNotFound` (`engine/verifier/src/request_processor.rs:264,269`). Les 0 lignes observées viennent d'ailleurs — `Verifier::load` repeuple `self.repos` depuis le stockage sur un profil persistant (`verifier.rs:535-560`), et notre propre `readDoc` attrape toute erreur et rend `[]`. Le correctif est bon, le diagnostic écrit à côté ne l'était pas. 159 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker. |
||
|
|
e01a8dbab1 |
refactor(layout): séparer les registres de branche du shim
`store-registry.ts` portait deux destins dans un seul fichier de 1379 lignes. `emulated-verifier/branch-registers.ts` prend les compartiments durables — registre de caps de branche Store (`AddRepo`), registre de Links de branche User (`AddLink`), enregistrements d'inbox (`AddInboxCap`), adresses de branche Header — chacun nommant son mécanisme natif. Ils émulent la comptabilité du VERIFIER et survivent conceptuellement : à la migration le natif les reprend, seule notre représentation RDF disparaît. `shared-wallet/account-registry.ts` garde le shim proprement dit — indirection pointeur → doc-shim, résolution et provisionnement des comptes, cache. Aucun pendant amont, s'évapore en entier. Les imports croisés entre les deux sont délibérés et visibles : un registre a besoin du shim pour savoir À QUI il est, le shim classe la structure d'un user au moment où il le résout. Tout l'usage est en corps de fonction, donc le cycle de modules est inerte à l'évaluation. 157 tests unitaires, typecheck src/test/e2e vert, e2e 40/40 contre le broker. |
||
|
|
36c0148750 |
refactor(layout): extraire le magasin d'injection vers shared-wallet/bootstrap
L'entrée publique `/polyfill` portait le magasin d'injection (le `ng` injecté, les dépendances du registre, QUI est connecté, le singleton `CapRegistry`). Tout module interne l'importait donc pour atteindre la config — ce qui faisait de l'entrée une dépendance du code qu'elle publie, avec les cycles `polyfill` <-> `connect` et `polyfill` <-> `inbox`. Le magasin rejoint `shared-wallet/` : rien n'est injecté en amont, l'app importe le SDK et « qui suis-je » est la session — il n'y a pas de relais d'utilisateur courant parce qu'un wallet a exactement un user. Ce module est la forme de cette absence, il s'évapore en entier à la migration. L'entrée ne fait plus que ré-exporter. Plus aucun module interne n'importe `polyfill`. |
||
|
|
0b37d17c2f |
docs: reprendre les citations après le rangement, et retirer le membre login fabriqué
Les deux contrats et la doctrine portent des centaines de citations `file:line` vers `src/`. Le rangement par destin les périmait en bloc — 208 reprises (45 chemins `src/x.ts`, 163 mentions nues). Sans ça la réorganisation pourrissait l'instrument même qui tient la discipline qu'elle sert. Et un défaut relevé par le contrat interne, vérifié : `ng-proxy` fabriquait un membre `login`. `@ng-org/web` n'expose aucune méthode de ce nom — zéro occurrence dans les déclarations installées comme dans `sdk/js/lib-wasm/src/lib.rs` — mais le proxy répondait une fonction au lieu d'`undefined`, laquelle plantait à l'appel. C'était le seul endroit où ce wrapper ajoutait à la surface du SDK, contre son propre en-tête. 157 tests unitaires, typecheck src/test/e2e vert. |
||
|
|
cd096de2b0 |
refactor(layout): les portes non gardées en un seul module, mintCap chez le verifier
Trois extractions que le rangement par destin a rendues évidentes : - `subscribePhysicalDoc` et `ensurePhysicalRepoOpen` rejoignent `shared-wallet/physical.ts`, qui devient l'API non gardée de la machinerie en un seul endroit — ce que sa propre doctrine réclamait (« des fonctions séparées, jamais des exemptions »). Elles vivaient jusqu'ici à côté de leurs jumelles gardées, à un import près d'être atteintes par erreur. Les deux cœurs sont désormais exportés sous un nom `Unguarded`, pour cet unique importateur. - `mintCap` et sa valeur de remplacement quittent `model/nuri.ts` pour `emulated-verifier/caps.ts`. `model/` transcrit le vocabulaire d'adressage de la cible ; frapper une clé n'en fait pas partie — en amont le moteur frappe à la création du repo, et ensuite on cherche un cap dans ce qu'on détient ou on le reçoit. Sa présence dans le module modèle contredisait l'en-tête de ce module et logeait la seule valeur inventée de l'émulation dans le fichier qui se dit vocabulaire vérifié. Un cycle est apparu au passage (`open-repo` ↔ `physical`) : résolu en faisant appeler à `open-repo` le cœur non gardé plutôt que la porte de la machinerie. 157 tests unitaires, typecheck src/test/e2e vert. |
||
|
|
88914f50ae |
refactor(layout): ranger les modules par destin à la migration
Les 25 modules étaient à plat, nommés d'après ce qu'ils font mécaniquement (`store-registry`, `read-model`, `reach`, `caps`). Rien dans l'arborescence ne disait lesquels DEVIENDRONT le vrai SDK, lesquels tiennent lieu du travail que le verifier fera nativement, et lesquels n'existent que parce qu'un wallet est partagé — trois destins sans rapport. Quatre dossiers, les deux fichiers d'entrée restant à la racine pour que l'`exports` du paquet et le code du consommateur ne bougent pas : - `model/` — le modèle d'adressage de la cible, transcrit : vocabulaire pur, pas d'I/O. Survit comme connaissance. - `surface/` — ce que l'app touche, chaque symbole ayant un pendant cible documenté. Supprimé quand l'alias bascule ; le code de l'app est inchangé. - `emulated-verifier/` — les doublures de ce que le verifier fait nativement : possession, dépôt des caps, frontière, non-livraison, traitement des inbox, registres de branche, ouverture de repo. **C'est le dossier où diverger du modèle est possible.** Le préfixe `emulated-` porte le sens : tient lieu de, jamais est — cette bibliothèque ne réside dans aucune couche de la cible, elle les référence. - `shared-wallet/` — n'existe que parce qu'un wallet héberge toutes les identités. Aucun pendant, rien sur quoi s'aligner ; sa seule loi est de rester invisible depuis `surface/`. S'évapore, remplacé par rien. `store-registry-api.ts` devient `surface/placement.ts` : il faisait déjà à la main ce que la frontière de dossier fait structurellement — c'est la meilleure preuve interne du bien-fondé de ce rangement. Ce commit ne fait que déplacer et recâbler les imports (src, test, e2e). Les scissions des modules à cheval suivent. 157 tests unitaires, typecheck src/test/e2e vert. |
||
|
|
d07b3642aa |
refactor(api): séparer la surface de l'app et la machinerie
L'entrée SDK déversait la machinerie par deux fuites : - `export * as storeRegistry from "./store-registry"` exportait TOUT le module — `ensureAccount`, `addLink`, `readLinks`, `resolveAccount`, `reservedAccount`, `resetRegistryCache`, `isOwnInbox`, `myInboxes`, `userStoreDoc`. Remplacé par `store-registry-api.ts`, qui ne ré-expose que les sept appels destinés à l'app : createEntityDoc, listMyEntityDocs, resolveScopeGraph, resolveWriteGraph, walletInbox, openDocumentInbox, documentInboxAddress. - `accounts.*` — persistance d'identité navigateur, sans aucun pendant SDK — passe sur `/polyfill`, où sa disparition à la migration se lit sur la ligne d'import. L'en-tête d'`index.ts` affirmait n'exposer « que ce que @ng-org/web et @ng-org/orm exposent ». C'était faux et enseignait une frontière fausse : un consommateur en déduisait que tout ce qui s'importe de l'entrée survit à la migration, ce qui ne valait ni pour `accounts` ni pour l'essentiel de `storeRegistry`. Il énonce désormais ce que l'entrée promet vraiment : tout symbole y a un pendant dans le futur SDK, vérifié ou assumé, et rien n'y est de la machinerie. La frontière mord : le typecheck e2e a échoué aussitôt, le harnais atteignant `ensureAccount` et `resetRegistryCache` par l'entrée publique. Il passe désormais par le chemin interne, comme les tests unitaires — légitime, il teste la bibliothèque. Deux documents plutôt qu'un, mêmes exigences, publics différents : `docs/api-contract.md` (la surface de l'app, avec pour chaque sujet la signature que le futur SDK devrait exposer, et l'étiquette qui distingue le vérifié de l'assumé) et `docs/internal-contract.md` (le complément exact). 157 tests unitaires, e2e 40/40 contre le broker en ligne. |
||
|
|
e5779ca850 |
docs: une adresse d'inbox est TRANSMISE en amont, nous la PUBLIONS
Assertion fausse retirée de `openDocumentInbox` : « en amont, l'acte équivalent est le propriétaire qui commite AddInboxCap avec la clé du repo — personne d'autre ne le peut ». Personne d'autre ne le peut est inventé. Ce commit atterrit sur la branche User de CELUI QUI LE FAIT, donc n'importe qui peut en écrire un nommant le repo de n'importe qui. Le moteur ne pose aucune garde là-dessus. Ce qui protège en amont n'est pas une garde, c'est le mode de circulation : `inboxes: PubKey → RepoId` est une table du Verifier (`verifier.rs:105`), reconstruite vide à chaque session — l'association inbox→repo est LOCALE, pas publiée. Un déposant apprend une pubkey parce qu'on la lui a ENVOYÉE : dans un `ContactDetails` (`contact.inbox`) ou par le QR de profil. Une paire forgée n'atteint personne, faute que quiconque en ait été informé. D'où une divergence à assumer et non à maquiller : nous PUBLIONS l'adresse sur le document, seul moyen qu'un tiers la trouve dans une émulation sans canal de messages. Cela crée un vecteur que le moteur n'a pas — qui peut écrire le document peut rediriger ses dépôts — et c'est ce que la garde `ownsDocument` compense. Elle compense NOTRE conception ; elle ne reproduit aucune règle amont. Manquait aussi dans la carte des inbox dressée juste avant : elle disait qui A une inbox, et omettait comment l'adresse circule — la dimension dont tout le reste dépend. |
||
|
|
2406afec8b |
docs: l'inbox par document n'est pas une anticipation, c'est une capacité du moteur
Glissement à corriger : de « le moteur ne le FAIT pas » (vrai) j'ai conclu « c'est notre anticipation » (faux). Le moteur le PERMET, et de façon générique par conception : - `inbox: Option<PrivKey>` est un champ de TOUT `Repo` (`repo.rs:126`), pas d'une structure de store ; - `AddInboxCapV0` est clé par `repo_id` — « Repo the Inbox is opened for » ; - `update_inbox_cap_v0` l'applique via `self.repos.get_mut(repo_id)` sans AUCUNE vérification `is_store` (`verifier.rs:1920`) ; - et à tout moment, `AddInboxCap` étant un commit de branche User dont le type documente le cas de mise à jour. Ce qui est vrai est plus étroit : aucun chemin de code n'en CRÉE une pour un document. « Ne fait pas » n'est pas « ne peut pas » — c'est précisément l'interdit que ma propre règle pose, et je l'ai enfreint en le formulant. Donc l'inbox par document s'aligne sur le modèle du moteur (niveau 1) ; ce qui est de nous est la surface JS, aucune n'étant exposée aux niveaux 2 et 3. Corrigé dans store-registry.ts, nextgraph-current-state.md et les deux briefs. |
||
|
|
1de9be9167 |
docs: la cascade des trois références, et l'inbox s'ouvre à tout moment
« NextGraph » n'est pas une couche. Trois niveaux, à parcourir de haut en bas
en s'arrêtant au premier qui répond : (1) le SDK JS / ORM — s'il couvre le
besoin, ne rien implémenter ici, passthrough ; (2) le binding wasm — si le
niveau 1 manque ou ne convient pas, s'appuyer sur l'appel équivalent ; (3) le
moteur Rust — si rien au-dessus ne répond, se tenir à son MODÈLE : cardinalités,
unités d'adressage, ce qu'une structure peut ou ne peut pas exprimer.
Et dire de quel niveau vient un choix. Un passthrough de niveau 1 est un fait ;
une forme de niveau 3 est un pari contraint par le moteur. Les présenter à
l'identique est ce qui fabrique les fausses certitudes — `inbox_post_link` a été
cité dans huit fichiers comme une API NextGraph prévue alors que c'était un nom
proposé dans notre propre plan de fork.
Sur l'ouverture d'une inbox, le moteur tranche contre ce que j'avançais : elle
n'est pas réservée à la création. `AddInboxCap` est un commit de branche User
(`commit.rs:1043-1050`), donc une entrée dans un flux ouvert dans le temps, et
son type documente explicitement le cas tardif — « DEPS to the previous
AddInboxCap commit(s) if it is an update. in this case, repo_id should match ».
Le verifier l'applique à un repo déjà chargé. Que `new_store_default` pose les
deux inbox de store à la création d'un site décrit comment ces deux-là sont
faites, pas une contrainte du modèle.
Donc `openDocumentInbox` reste appelable à tout moment, et le
`createEntityDoc(…, { withInbox })` que j'avais proposé aurait rétréci le
modèle. Écrit dans le code pour qu'il ne soit pas « simplifié » plus tard.
|
||
|
|
4f5c3ed03b |
docs: inbox_post_link est notre nom, pas une API NextGraph annoncée
Le symbole n'existe nulle part dans `nextgraph-rs`, et aucune méthode de `@ng-org/web` ne contient « inbox ». Il vient de notre propre plan de fork (`docs/fork-inbox-fallback.md:32` — « expose `pub async fn inbox_post_link` »), d'où il a essaimé dans huit autres endroits, cité comme une API « proposed/ future » de NextGraph. Une proposition interne devenue un fait par répétition — le même mécanisme que « chaque document a une inbox native » et que l'inbox mutualisée. Corrigé partout sauf dans le plan de fork, où le nom est légitime puisque c'est lui qui le propose. Et l'énoncé exact est désormais posé : on ne connaît NI le nom NI la forme de la future surface JS pour les inbox — ce n'est pas « non implémenté », c'est inconnu. Ce qui est réellement vérifié côté moteur : `AppRequestCommandV0::InboxPost` existe et `AppRequest::inbox_post()` le construit, mais le request_processor n'a aucun arm pour lui — l'envoyer ne déclenche rien. Le seul dépôt qu'un client JS peut provoquer aujourd'hui passe par `import_contact_from_qrcode`, qui appelle `post_to_inbox(InboxPost::new_contact_details(...))` avec `with_readcap = false` — la variante `true` étant `unimplemented!()`. |
||
|
|
fba0128a7e |
docs(inbox): commentaires alignés sur l'inbox par document
Deux commentaires de openDocumentInbox décrivaient encore l'adresse publiée à
la création, retirée en
|
||
|
|
5a7009bd75 |
fix(inbox): une inbox appartient à un document, jamais à plusieurs
Retour sur l'adresse par défaut livrée en
|
||
|
|
8a382f29f8 |
feat(inbox): l'inbox d'un document est adressable par tout détenteur
Répond au brief 2026-08-03 remonté depuis le consommateur. `documentInbox(doc)` répondait « quelle inbox est-ce que MOI je connais pour ce document » et en créait une quand la réponse était « aucune » : un tiers n'atteignait jamais l'inbox du propriétaire, il en obtenait une à lui, que personne ne lit, et son dépôt disparaissait sans erreur. C'est l'acte central du consommateur — s'inscrire à l'événement d'un autre — qui était silencieusement perdu. Lire une inbox et savoir où y déposer sont deux actes opposés, avec des publics opposés. Ils sont désormais deux fonctions : - `openDocumentInbox(doc)` — le PROPRIÉTAIRE ouvre une inbox dédiée. Refuse sur la PROPRIÉTÉ (lue depuis les branches Store), pas sur la possession du cap : un cap se reçoit, et un destinataire ne doit pas pouvoir rediriger vers lui les dépôts destinés au propriétaire. - `documentInboxAddress(doc)` — n'importe quel détenteur trouve où déposer. Ne crée jamais rien. L'adresse est publiée dès la CRÉATION, sur la branche Header émulée du document — un sujet réservé à l'intérieur du document, donc lisible par qui détient le document. Publier seulement le jour où le propriétaire ouvre une inbox dédiée laisserait une fenêtre pendant laquelle un tiers lit le document, ne trouve aucune adresse, et ne peut pas joindre le propriétaire du tout. Sur le coût mesuré par le brief (9m37 → 21m30) : il venait de la création d'un DOCUMENT supplémentaire par document. L'adresse publiée pointe vers l'inbox propre du propriétaire, qui existe déjà et s'amortit sur tous ses documents ; la création grandit d'un triple, pas d'un document. Le dépôt porte le document concerné, donc le propriétaire matérialise toujours par document. La forme « dérivable » du brief n'était pas disponible : notre inbox est un document, et un NURI dérivé nommerait un repo que `doc_create` n'a jamais créé. Le tout reflète la séparation d'amont : un déposant scelle avec la clé PUBLIQUE de l'inbox et n'a besoin de rien d'autre, seul le propriétaire détient la moitié privée — une adresse est donc publique par nature. `src/machinery.ts` : l'espace de noms `urn:ng-eventually:` que la bibliothèque se réserve, et le prédicat que le chemin de lecture utilise. La branche Header est le premier compartiment logé dans un document que le consommateur lit ; `read-model` écarte désormais tout sujet de cet espace, par SUJET et non par prédicat — ce qui couvre toutes les branches émulées, présentes et futures. Question ouverte du brief, tranchée : « une inbox de document adressable par tout détenteur » est une invention de cette bibliothèque, pas de l'amont — aucun document n'y a d'inbox, ni le store privé. Ce qui EST vérifié, c'est la forme qui rend l'anticipation défendable : `AddInboxCapV0` est clé par `repo_id`. Tests : le test qui validait « n'importe qui dépose » passait le NURI d'inbox au déposant par une variable du test — chemin qu'aucune app n'a. Réécrit avec les deux acteurs cloisonnés : le déposant reçoit le lien du document, qui est la seule chose qui circule dans ce modèle, et doit trouver l'adresse lui-même. Le fake `ng` gagne le SELECT de la branche Header et le `DELETE WHERE` (sans quoi un remplacement devenait une accumulation, précisément le bug qu'il évite). 157 tests unitaires, e2e 40/40 contre le broker en ligne. |
||
|
|
88f396a7ac |
fix(caps): créer un document en donne le cap, + corriger 9 faits NextGraph
Le trou trouvé par l'e2e contre le broker en ligne : `docs.docCreate` ne
déposait aucun cap pour le créateur, donc un consommateur pouvait créer un
document par la primitive publique puis se voir refuser sa lecture et son
écriture. En amont c'est impossible — `doc_create` commite
`AddRepo { read_cap }` sur la branche Store du store, et le créateur le détient
dès le premier instant. Délibérément non répliqué dans `physical.ts` : les
documents du shim n'appartiennent à aucun utilisateur virtuel, et
`store-registry` classe leurs caps là où il sait à qui ils sont.
e2e : 22 passés / 8 échoués → 39 / 0. Les autres échecs venaient du harnais,
qui agissait comme une seconde identité sans l'établir, ou lisait un document
quelconque comme une inbox. Un run e2e contre un wallet persistant exige une
identité FRAÎCHE par run : `walletInbox(id)` rend l'inbox stable pour son
propriétaire — c'est son intérêt — donc un id fixe accumule les dépôts des runs
précédents (vert au 2e run, rouge au 3e, à code inchangé).
Revue adverse de la documentation, 9 défauts, tous vérifiés à la source avant
correction :
- « chaque document a une inbox native » est FAUX. Seuls les repos de store
public et protected en ont une (`site.rs:128,149`) ; `new_store_default` n'en
pose que `if !private` et `doc_create` laisse `inbox: None`. Le store privé
n'en a pas non plus. Ce que le code fait est donc une ANTICIPATION — assumée
et notée comme telle dans `documentInbox`, le brief et l'ADR discovery. Ce qui
est vérifié, c'est la FORME : `AddInboxCapV0` est clé par `repo_id`.
- `InboxMsgContent::Link` est une variante unit sans charge utile : l'inbox ne
transporte aucun ReadCap. `shareCap` était juste et le reste ; ses citations
sont complétées aux deux bouts (émetteur `unimplemented!()`, récepteur qui
ignore `details.read_cap`).
- les 3 stores appartiennent au user (`SiteV0`), pas au wallet ;
- le TODO `OpenRepo` ne concerne pas la lecture cross-wallet — il est dans
`open_branch_`, après `RepoNotFound` ; charger par cap, c'est
`load_repo_from_read_cap` ;
- la liste des méthodes JS était un sous-ensemble présenté comme la surface
(77 exportées) ;
- `outbox-log.ts` n'enregistre rien : il inspecte l'outbox du SDK ;
- l'ADR private-store-nuri-scope citait `orm_start_graph` au présent, remplacé
par `ensureRepoOpen` ;
- l'incident write-loss plaçait `disconnections_sender.send` dans `broker.rs` ;
- la section « Apps & services » n'a aucune citation et rien ne lui correspond
dans le moteur : marquée à re-confirmer, pas à citer comme vérifiée.
Aussi : `fileOwnCaps` n'existe plus (`holdOwnCap` / `readStoreCaps` /
`fileOwnStructure`) — pointeur mort corrigé dans `caps.ts`.
|
||
|
|
9d3e2d2bfe |
Fix documentation defects found by an adversarial review
Fifteen findings, all verified before acting. The ones that mattered: - Corrections added without updating what they corrected. §5's table still said a cap-less NURI is one "without :k:", two hundred lines after §4 established the discriminant is `r:`. Same shape of defect in the P1a report, which kept the sentence "it is the owner's keyring, upstream the keyring is the wallet" — the exact sentence §4quater declares wrong, and the one that produced a global in-memory keyring. - A wrong source citation: RootCapRefresh/BranchCapRefresh live in verifier/src/commits/mod.rs, not repo/src/commit.rs, and are no-op stubs. - Documentation describing deleted code: isolation.ts, discovery.readIndex, the global index, and an acceptance test that was dropped with discovery. - The P1a implementation report had aged into being wrong in four places (caps not persisted, inbox processing not started, plain string types, the :k: segment). It is dated, so it now carries a header saying what later lots overtook, rather than being rewritten. - vision.md stated "a document's data is stored encrypted" in the present tense. That is the target; here the cap value is the constant OK and nothing is encrypted. Said plainly now. - Prose left mangled by an earlier mechanical find-and-replace, in four places I had claimed were repaired. Also: reach.ts and connect.ts had no home in the permanent docs — the boundary and the connection sequence are now described in simulation.md, not only in a brief. |
||
|
|
ae9c32e271 |
Align the cap emulation on NextGraph's model, and confine it to a virtual user
Two batches, verified against nextgraph-rs throughout. P1a — the capability surface. Reading was an ACL (Map<doc, Set<principal>>), the exact inversion of key possession. It is now possession: `capFor(nuri)` is the only question, there is no principal parameter anywhere, and nothing turns a bare reference into a cap. Sharing is `shareCap(cap, toInbox)`, a Link deposit; receiving needs no operation. `Nuri` and `ReadCap` are template literal types, so passing a bare reference where a cap belongs is a compile error, with runtime guards behind it for JavaScript callers. The virtual user boundary. Every access function is now confined to the connected user, through two rules on one criterion (possession), implemented in two places so a lapse in either is caught by the other: authorization at the passage points, and "do not even attempt" at the callers. The polyfill's own machinery moved to physical.ts — unguarded, never exported — which replaced an exemption list: the machinery no longer gets waved through the guard, it calls something the guard never saw. Removed, as emulating capabilities the target does not have: - discovery.ts and its global index. There is no discovery in NextGraph; you follow links. It also pooled user data across wallets. - the cross-account fan-out (listEntityDocs, resolveReadGraphs, allAccounts, loadShim), which was cross-user enumeration by construction. - resolveInboxAnchor, a single inbox common to every user. Caps are now stored where NextGraph stores them, and read back rather than recomputed: AddRepo on the store's Store branch for documents a user creates, AddLink on its User branch for caps received. Inboxes belong to someone — the user's own, plus one per document — and connecting a user drains them all; that is the library's job, not the app's. Corrections worth recording: a ReadCap is `r:`, not `:k:` (reported by NextGraph's developer, verified in BlockRef::readcap_nuri); received caps DO have a register (AddLink), contrary to what this repo's notes claimed; and "wallet" upstream means keyring — what owns three stores is a user, so the vocabulary follows. The cap value is the constant OK: the only question the emulation answers is whether a cap is held. P1b replaces that one constant with a real key. After this the shape is right and the isolation is still fake. Nothing here may be described as anonymous or private. |
||
|
|
f2c5b30527 |
test(e2e): déterminer si doc_subscribe pousse réellement (SELF et CROSS)
Couverture manquante de la couche réactive — c'est son absence qui a laissé
passer un bug de réactivité. Le runner exerce les DEUX poussées porteuses en
production contre le VRAI broker, par la même surface publique que l'app
(`subscribeDoc` → `ng.doc_subscribe`) :
SELF — l'écriture d'une session vers un doc qu'elle-même souscrit ;
CROSS — une seconde session (même wallet partagé) écrit sur ce doc.
Chaque push est enregistré comme événement typé ({typeKey, elapsedMs}) : le
verdict est le fait que le callback re-tire, pas une relecture du document.
Chaque attente est une promesse événementielle unique + timeout (pas de boucle
de relecture) — un timeout est donc un « n'a PAS tiré » définitif.
Verdict obtenu : doc_subscribe POUSSE bien dans les deux cas — le défaut de
réactivité observé côté app n'est donc pas ce primitif.
Standalone (pas `bun test`) : `bun run test:e2e:reactivity`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
|
||
|
|
127ca3159e |
docs: known issues (perte écriture, réhydratation à froid, écho auto-écriture) + gap 4 sdk-reference
Section 'Known open issues' dans nextgraph-current-state (A ouvert, B indéterminé, C hypothèse-en-cours) + gap 4 (auto-écho non confirmé) dans sdk-reference. Statuts préservés. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
cf9500f0cf |
feat(inbox): logs du cycle de l'inbox virtuelle (dépôt/lecture/watch)
Réutilise le format identité-first [<id>][polyfill] d'access-log : deposit/read/materialize/readSynced + watch (materializing vs unchanged-skip). Gated par debugAccessLog, aucun changement de comportement. tsc 0, bun test 126. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
70de7afa3c |
feat(logs): logs données restructurés — identité en préfixe, trace résolution/barrière, inspection outbox
Chemin données bas-niveau du polyfill rendu lisible pour diagnostiquer en session live : - Format identité-en-premier : `[<identity>][polyfill] OP shortNuri (label)` ; console.error épars (store-registry, inbox) unifiés au même préfixe. - Trace (derrière le flag debug) : issue de la barrière ensureRepoOpen (synced|timed-out + durée) et résultat sémantique de chaque étage de résolution (resolvePointer/canonicalDoc/resolveAccount/resolveShimDoc/readScopeIndex). - outbox-log.ts (nouveau) : inspection read-only de l'outbox hors-ligne au démarrage de session ; console.warn si non vide (anomalie, toujours visible), sous flag si vide. Le comptage seul est fiable (payloads BARE opaques côté JS). Pas de changement fonctionnel. bun test 126 pass ; tsc 0 erreur. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
3547967d37 |
chore(client): retirer la migration legacy + alléger les logs d'accès
- Migration des comptes legacy supprimée (migrateLegacyRecords + garde migratedInto + call-sites). Un wallet pré-fix (records store-root, pas de pointeur) provisionne simplement un doc-shim frais; contenu legacy ignoré (voulu, données = dev). La résolution barrière-autoritative + anti-fork (resolvePointer/ensureRepoOpen/ canonicalDoc/ensureInFlight/pointerGuard) est inchangée. - Logs d'accès SDK préfixés [polyfill] + NURI tronqué via shortNuri() (retire did:ng:o: et :v:…, garde 8 chars) → moins verbeux. Ex: [polyfill] [user1] READ vDlwbZio… (resolvePointer) → 1 triple-rows Tests: bun test unit 126/0. Docs (nextgraph-current-state/simulation/migration-guide) mis à jour (migration legacy retirée du modèle décrit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5e91771da6 |
fix(client): résolution de compte barrière-autoritative — fin du fork à la reconnexion
Bug: à la reconnexion, resolveAccount lisait le shim depuis le store-root (did🆖${privateStoreId}), NON abonnable → pas de barrière first-State → un "0 rows" à froid est ambigu → le retry (resolveAccountReliably/provisionRetry) échoue → nouveau compte provisionné → FORK → données du compte invisibles. Cause NextGraph (vérifiée nextgraph-rs): "trouvable-sans-lookup" (store-root) et "abonnable" (did:ng:o:<RepoID aléatoire>) sont DISJOINTS — pas de doc à la fois devinable et attendable → une résolution shim purement barrière est impossible. Fix (indirection pointeur → doc-shim abonnable): - Les AccountRecord migrent dans un doc-shim doc_create'd (did:ng:o:..., a une barrière). - Un pointeur écrit-une-fois dans le store-root (<shim:root> <shim:shimDoc> <docShim>) le nomme. resolveShimDoc lit le pointeur → ensureRepoOpen(docShim) [barrière] → lecture de compte AUTORITATIVE (cold 0 = absent pour de vrai). Retry de compte SUPPRIMÉ. - Micro-garde résiduel (pointerGuard, ex-provisionRetry) sur le SEUL triple pointeur écrit-une-fois; ne peut jamais forker un compte; fork de pointeur réconcilié au doc-shim canonique (lexicographiquement-min), sans perte. - Migration: migrateLegacyRecords copie (pas déplace) les comptes de l'ancien store-root vers le doc-shim avant toute conclusion "absent"; idempotent; wallet neuf → no-op. Tests: unit 128/128, e2e réel 42/42 (CONTRACT 2 = non-fork du compte à la reconnexion), red-before/green-after prouvé. Docs: nextgraph-current-state (antagonisme + indirection), simulation, migration-guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5cdc6ce77f |
feat(client): inbox.readSynced — lecture inbox gated sur la barrière first-State
ensureRepoOpen(target) (attend le 1er State, barrière de sync déterministe) puis read(target) — même pattern que discovery.readIndex. Permet au propriétaire d'un événement de voir un dépôt DÉJÀ synchronisé au lieu de lire trop tôt un inbox vide. Consommé par Festipod (materializeAttendance). Pas de polling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3046ead08f |
fix(client): durcir le cold-start du shim — ensureRepoOpen(anchor) avant lecture/écriture
Défensif : loadShim/resolveAccount/ensureAccount ouvrent l'anchor (private-store-root) avant de lire/écrire le compte shim, comme le fait déjà readScopeIndex. Robustesse same-session si l'anchor est là-mais-pas-encore-souscrit. NB (vérifié nextgraph-rs) : sur le login broker normal, le bootstrap charge le private-store dans self.repos AVANT de rendre la session à JS → un wallet FRAIS retourne 0 rows (pas RepoNotFound) et provisionne. Il n'existe AUCUN primitif JS pour ouvrir un repo *inconnu* : ce heal n'est pas un remède à un store non bootstrappé (limite NextGraph), juste une robustesse d'ouverture same-session. Tests : cold-start-anchor.test.ts (rouge-avant/vert-après unit) ; harness e2e repro-fresh-wallet (mint un wallet neuf par run — comble le trou "aucun test de démarrage à froid sur wallet vierge"). Fakes anti-fork/watch-shape honorent désormais la barrière first-State dont dépend le heal. e2e réel 42/42. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
078d675bbf |
chore(client): clarifier le log d'accès — "N rows" → "N triple-rows"
Le journal d'accès compte des triplets RDF (?s ?p ?o), pas des objets métier. Clarification demandée côté Festipod (les logs "readDoc → N rows" étaient ambigus). Test access-log mis à jour en conséquence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9103996dbe |
fix(client): dé-dupliquer les ensureAccount concurrents au cold-start (fork résolu)
Dernière couche du bug de reconnexion : au cold-start, `watchShape` public + protected + l'effet owned-events appellent `ensureAccount(A)` quasi-simultanément AVANT la sync du shim → chacun lit 0 → chacun provisionne un nouveau jeu de docs (fork par-appelant) → la résolution déterministe canonique fait alors diverger lecteur et écrivain sur le docProtected → `readScopeIndex` vide. Fix : `ensureInFlight` (map de promesses) dé-duplique les provisions concurrentes en UNE seule ; `discovery.readIndex` ouvre son repo au cold-start (`ensureRepoOpen`). Avec la résolution canonique déjà committée, écrivain et lecteur convergent. Mesuré (levier isSuccess) : la participation protected converge `isSuccess=true, data=1` sur la page fraîche (plus « vide à 30s »). gate : tsc 0 ; bun test 123 ; test:e2e 42/42. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bd48b16e31 |
fix(client): résolution de compte DÉTERMINISTE + anti-fork restauré
Cause racine du bug de reconnexion (probe contrôlé répété) : le shim d'un compte accumule des `docPublic`/`docProtected` EN DOUBLE (forks passés), et resolveAccount/indexDocOf les choisissaient de façon NON-DÉTERMINISTE → l'écrivain et le lecteur (page fraîche) ancraient sur des docs d'index DIFFÉRENTS → lecture 0. Fix : - `canonicalDoc()`/`recordFromRows()` : parmi plusieurs valeurs d'un scope, choisir le NURI lexicographiquement le plus petit (les NURIs sont content-addressed → ordre total stable). Écrivain et lecteur résolvent TOUJOURS le même doc, même sur un shim corrompu par des doublons. - `resolveAccountReliably` RESTAURÉ (retry borné avant provision sur read shim 0 à froid) : j'avais retiré l'anti-fork à tort (`38b1521`) — le gap EST exhibé (fork non-déterministe au cold-read), et la barrière `user_connect` n'est PAS accessible côté JS → un retry borné est la compensation légitime (pas la barrière-store cassée de `45dbd9a`). Budget injecté `provisionRetry` ; défaut attempts:1 (fakes synchrones), app/e2e attempts:8. anti-fork.test.ts réécrit : docPublic dupliqué → même canonique ; retry sur lag → réutilise ; neuf → provision 1×. gate : tsc 0 ; bun test 122 ; test:e2e 42/42 (CONTRACT 2 non-fork vert). Portée : corrige la couche docPublic de la reconnexion. Une couche PROTECTED distincte (watchShape protected ne converge pas à froid) reste — diagnostic en cours. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7c233df5c0 |
feat(client): watchShape — lecture réactive à la forme TanStack useQuery
Phase A du refactor des lectures. Surface la barrière de sync interne
(open-repo `getSyncState`) dans une API useQuery-shaped, en anticipation de la
mise à jour prévue de useShape par NextGraph — distingue nativement « sync en
cours » de « synchronisé mais vide ».
`watchShape<T>(shapeType, scope): { getSnapshot(): ShapeQuery<T>, subscribe(cb),
refetch() }` avec `ShapeQuery = { data, isPending, isSuccess, isError, error }`.
OBSERVABLE (pas de dépendance React — l'app câblera useSyncExternalStore en
phase B) ; getSnapshot rend une référence stable.
- Scope LOGIQUE (public/protected/private) résolu au wallet virtuel :
listMyEntityDocs(getCurrentUser, scope) + découverte foldée pour public.
- isPending tant que la barrière n'est pas atteinte / 1er readUnion non rendu ;
isSuccess après ; timed-out → isSuccess (best-effort, pas isError).
- Réactif SANS polling : subscribeDoc sur les docs + le doc d'index de scope
(+ index découverte) → re-read/re-résolution au push ; souscriptions idempotentes.
- Générique : aucune logique domaine Festipod dans le lib ; filtre par la shape
SHEX (rdf:type). Pas de double filtre cap.
gate : tsc 0 ; bun test 120 (+4 : pending→success, synced-vide, réactif, timed-out) ;
test:e2e 42 passed (+scénario broker réel : isPending au 1er snapshot → isSuccess
avec données, scope vide → isSuccess data:[]).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
38b152136b |
refactor(client): retirer anti-fork — gap non exhibé, resolveAccount simple
Preuve e2e (wallet frais, broker RÉEL rapide : 1er State 1-2 ms) : le private store est synchronisé au login, la lecture du shim réussit à froid — le « fork sur lag » que anti-fork compensait n'est PAS exhibé. Par le principe du polyfill (compenser un gap RÉEL, jamais du poids mort), et par la règle no-polling : - la version retry = polling (bannie) ; - la version barrière `ensureRepoOpen(privateStore)` = CASSÉE (un store n'émet pas de `State`, la barrière timeout systématiquement → CONTRAT 2 e2e échouait) ; - le gap = non exhibé. → `ensureAccount` fait un `resolveAccount(id)` SIMPLE (une lecture, provision si 0). `resolveAccountReliably`, `_forceOpenedSyncState` retirés ; `provisionRetry` gardé optionnel @deprecated (ignoré) pour ne pas casser les 8 tests qui le passent. `ensureRepoOpen`/`getSyncState` inchangés (chemin de lecture open-repo). gate : tsc 0 ; bun test 116 ; test:e2e 39 passed, CONTRAT 2 VERT (« same account, no second provisioning »). Le ~10s du re-resolve public est du scaling anchorless, pas de la lenteur broker (broker mesuré à 1-2 ms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
45dbd9a33a |
refactor(client): dé-poller resolveAccountReliably — barrière au lieu de retry
Le polling est un anti-pattern NextGraph (par abonnement). La résolution de compte retentait ×8 la lecture du shim tant qu'elle rendait 0 (lag de sync) — c'est du polling. Remplacé par la BARRIÈRE d'abonnement, déjà le mécanisme de open-repo : - `resolveAccountReliably` : `await ensureRepoOpen(did🆖${privateStoreId})` (subscribe + attendre le 1er State — le shim vit dans le graphe du private store), PUIS lecture UNIQUE. Après la barrière, 0 ligne = compte réellement inexistant → provision 1×, lignes présentes = réutilisé (garantie NO-FORK préservée). Plus de boucle de re-lecture. - timed-out (barrière expirée) : throw explicite, NE provisionne PAS (un provision sur sync incomplète re-forkerait). Le « trop long » est un signal, pas un feu vert. - fake ng sans doc_subscribe : ensureRepoOpen no-op → lecture immédiate (unit intact). - `_forceOpenedSyncState` : helper test-only (underscore, non ré-exporté). anti-fork.test.ts réécrit (5 tests : no-fork, neuf→1 provision, idempotence, fake no-op, timed-out→throw) ; plus aucun test de comptage de retry. gate : tsc propre ; bun test 117. e2e À RE-VALIDER quand le broker répond (dégradé ce jour : crash Chromium post-connexion) — la barrière ensureRepoOpen est déjà validée e2e (CONTRAT 3 + reconnexion) en broker sain. provisionRetry devient un champ mort de StoreRegistryDeps (nettoyage ultérieur). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |