9c487b59f323eea4e6e52248a8b888f882943b86
90 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9c487b59f3 | refactor: le résumé du harnais e2e dit polyfill, pas SDK | ||
|
|
737729c9ce |
refactor: le paquet s'appelle polyfill, « SDK » désigne celui de NextGraph
Le nom @ng-eventually/sdk entrait en collision avec le SDK de NextGraph, dont ce paquet est justement un polyfill. Impossible d'écrire « le SDK » sans lever l'ambiguïté à chaque phrase — et le contrat publié, lu par une application, était le pire endroit pour laisser traîner ça. packages/sdk → packages/polyfill, @ng-eventually/sdk → @ng-eventually/polyfill, contract_sdk-surface → contract_polyfill-surface, e2e/sdk-entry.ts → e2e/polyfill-entry.ts, docs/sdk-reference.md → docs/polyfill-reference.md. Les occurrences de « SDK » qui désignent celui de NextGraph restent intactes, y compris les chemins dans nextgraph-rs (sdk/js/orm, sdk/js/web). Le tri s'est fait occurrence par occurrence, pas par substitution. Le contrat énonce désormais son identité en une phrase : « This package is a polyfill of NextGraph's SDK. » |
||
|
|
7076c0cca8 |
fix: readUnion regroupe par sujet réel — la fusion était une divergence
`readUnion` indexait par DOCUMENT une table nommée `bySubject`, créait ses entrées avec `subject: doc`, et jetait le sujet réellement lu après s'en être servi pour écarter la machinerie. Tout triplet non-machinerie d'un document tombait donc dans un sac unique étiqueté par la référence du document : deux entités écrites sous deux sujets revenaient **conflées**, une entité écrite sous un autre sujet revenait **ré-étiquetée**. Sans erreur, sans trace. **C'était une divergence, et c'est à ce titre qu'elle tombe.** NextGraph dit l'inverse aux deux niveaux : une requête ancrée résout le graphe du repo comme graphe par défaut et rend les sujets tels qu'ils sont ; et l'ORM porte sur chaque objet **deux** propriétés distinctes, `@id` et `@graph`, dont il FABRIQUE la première quand on la laisse vide (`graphIri + ":q:" + aléa`). Plusieurs objets par graphe est le cas prévu, et `@id` existe pour les distinguer à l'intérieur d'un `@graph`. La règle du projet reste **« un document séparé par entité métier »**, mais c'est une recommandation de placement dictée par le modèle de sécurité — une clé est par repo, donc l'isolation par entité exige un repo par entité. Ce n'est pas une contrainte que la lecture a le droit d'imposer en rendant l'autre disposition invisible. Le contrat porte désormais la recommandation, le code porte la capacité ; il faisait exactement l'inverse. **La justification de l'épinglage était une erreur de catégorie**, et elle a été retirée plutôt que contournée : `repo_graph_name` formate un nom de GRAPHE, il est estampillé sur les quads et aucun sujet n'est réécrit. Deux confirmations indépendantes, dont la suite e2e qui écrit un sujet puis le relit par correspondance exacte contre le vrai broker. `UnionSubject.subject` passe de `Nuri` à `string` — un sujet RDF réel est un IRI quelconque. `graph` reste `Nuri` et devient le champ à repasser au SDK ; l'app d'exemple l'utilise à ses deux sites, où le sens était « le document ». **Et la suite e2e ne comptait que les entrées.** C'est pour cela qu'elle est restée verte pendant tout le défaut : compter ne distingue pas un regroupement par document d'un regroupement par sujet. Elle écrit maintenant deux sujets dans le dernier document et vérifie les trois choses qui comptent — quatre entrées pour trois documents, chaque entrée portant le sujet sous lequel elle a été écrite, et son `graph` étant la référence du document. 202 tests unitaires (5 ajoutés, dont 3 échouent si l'on restaure l'ancien repliage), e2e 42/42 et applicatif 12/12. |
||
|
|
f378c71739 |
docs: sept affirmations sur NextGraph requalifiées à la source
Lot F de la revue adverse. Chaque affirmation relue dans `nextgraph-rs` par symbole avant
d'être réécrite ; aucune ne s'est révélée exacte.
- `AddLink` / `RemoveLink` / `RepoLinkV0` étaient présentés comme **implémentés**. Leurs
arms de vérificateur sont des `Ok(())`, là où celui d'`AddRepo` fait un vrai travail, et
rien ne les construit. Le tableau dit désormais « déclaré, stubbé », et la conclusion qui
en déduisait « le registre existe, seule la livraison manque » est corrigée : **les deux
bouts** sont déclarés-et-stubbés.
- La table `inboxes` était dite « reconstruite vide à chaque session » — elle est
repeuplée au chargement, et la clé privée d'inbox est persistée par repo. L'argument de
sécurité qui s'appuyait dessus repose maintenant sur le bon motif : la table est **par
vérificateur**, pas éphémère.
- La citation de « `doc_create` laisse `inbox: None` » pointait un constructeur réservé aux
tests ; re-ciblée sur le chemin de production.
- `ExtObjectGet` était dit « le seul » primitif accessible à un non-membre et exigeant les
clés : il y en a trois, et sa structure n'a aucun champ de clé.
- L'en-tête de `public-store.ts` était marqué **VERIFIED** alors qu'il repose sur un
commentaire de doc, et la condition qu'il citait (« si les brokers pairs l'autorisent »)
disparaissait de la conclusion. Requalifié en **pari**, condition rétablie.
- Deux sur-restrictions corrigées (ce qu'écrit le traitement d'un `ContactDetails`, et le
prétendu « miroir 1:1 » de `NuriV0`, qui a dix champs).
**Sur la grammaire du ReadCap, une correction de MA correction.** J'avais écrit que la
forme `{target}:r:{cap}` était notre invention. Faux : le segment `r:` et son encodage sont
ceux d'amont, et l'auteur de NextGraph l'a énoncé. Ce qui est établi est plus étroit —
aucun parseur amont n'accepte aujourd'hui un NURI de repo qui le porte, et le segment est
produit comme valeur de champ. J'avais conclu d'une implémentation absente à ce que la
cible ferait, ce que la doctrine du projet interdit nommément. Seule « P1b remplace la
valeur, pas la forme » est corrigée, requalifiée en **pari**.
**Et la surface ne publie plus de type que personne n'utilise.** `export * from
"./model/types"` publiait huit types en bloc ; c'est une liste nommée de six. `ReadCap`
sort — aucune signature publiée ne le prend ni ne le rend, seules deux fonctions privées
de `inbox.ts` s'en servent — et `InboxScope` aussi. Un type n'est publié que si une
signature publiée l'utilise.
197 tests, 0 échec ; les trois typechecks propres ; `lint` sans erreur.
|
||
|
|
0d9e2bbe97 |
fix: mon correctif sur createEntityDoc était faux dans les deux sens
Second tour adverse sur le lot C. Six trouvailles, dont trois sur ce que je venais de livrer. **Le correctif de `createEntityDoc` reproduisait le défaut qu'il annonçait avoir fermé.** Il levait sur la PREMIÈRE écriture de registre en échec. Or : - lever sur le listing sautait l'écriture de la clé — détruisant le chemin de récupération que le commentaire d'à côté décrit explicitement (« la clé doit rester récupérable même si le listing a échoué »), et laissant le document orphelin ; - lever sur la clé laissait le document LISTÉ sans clé — précisément l'état « se lit vide pour toujours » que je prétendais empêcher, en pire, puisque l'appelant n'a même plus sa référence. Les deux écritures sont désormais tentées, ce qui atterrit reste, et l'échec est rapporté après en nommant la moitié manquante. **Le refus de `share` reposait sur une valeur qui confond absence et ignorance.** `resolveAccount` avale toute erreur de lecture et rend `null`, si bien qu'un incident réseau faisait répondre « personne ne s'est connecté sous ce nom » à propos de quelqu'un qui existe. `lookupAccount` propage désormais l'erreur ; `resolveAccount` reste la forme tolérante que tous les autres appelants veulent. **J'avais livré ce comportement sans un seul test.** `test/app-surface.test.ts` en ajoute huit, tous sur ce qu'un APPELANT voit : `ensureIdentity` rend l'identité, un appel de placement avant connexion nomme l'erreur, le placement agit comme l'utilisateur connecté, `share` refuse un nom inventé mais laisse remonter une panne, et une création à moitié écrite échoue en disant quelle moitié — dont le cas « le listing a échoué, la clé est quand même là ». En écrivant ces tests j'ai refait dans leur faux la faute que cette revue a corrigée ailleurs : ignorer le sujet dans la requête de compte, ce qui rendait le dossier d'un autre utilisateur. Deux des huit échouaient pour cette raison, sans rapport avec le code. **Et la documentation contredisait le code livré dans le même commit** : le README enseignait encore `createEntityDoc(me, "protected")` — en JS la portée devient `"alice"` — et le contrat déclarait `Nuri` là où le code et la feuille disent `NuriLike`. 197 tests unitaires, e2e 40/40 et applicatif 12/12. |
||
|
|
cdc09a1a1d |
refactor(api): l'application ne nomme plus son identité — elle l'apprend
Lot C de la revue adverse. Cinq corrections, dont une qui change la forme de la surface. **L'application ne pouvait pas obtenir son identité par l'API.** `ensureIdentity()` rendait `void`, `getCurrentUser` n'est plus publié — et pourtant `createEntityDoc(id, …)` et `listMyEntityDocs(id, …)` l'exigeaient. L'app d'exemple s'en sortait en lisant `localStorage["ng-eventually:identity"]` et le paramètre `?ng-id`, deux constantes PRIVÉES du portail d'accès. Une frontière qu'aucun consommateur ne devrait voir, et encore moins dont il devrait dépendre. Vérifié au niveau 2 avant de trancher : `session_start(wallet_name, user_id)` prend l'identité — donc en amont l'application la DÉTIENT, elle la tient du portefeuille qu'elle a ouvert. Ici c'est le portail qui la choisit, donc c'est au portail de la rendre. Deux changements, tous deux vers la cible : - `ensureIdentity()` rend l'identité qu'il a établie ; - `createEntityDoc(scope)`, `listMyEntityDocs(scope)`, `resolveWriteGraph(scope)` perdent leur paramètre d'identité. En amont `doc_create(session_id, …)` ne porte aucun utilisateur : une session EST celle d'un utilisateur. Passer la sienne à chaque appel de placement était un geste sans successeur. L'application garde l'identité pour l'afficher, et ne la passe plus à rien. **`inbox.share` provisionnait un destinataire inexistant.** Une faute de frappe créait les trois stores et l'inbox de ce nom, et la clé atterrissait où personne ne regarde — sans la moindre erreur. En amont on ne peut pas viser un nom qu'on invente : un dépôt est scellé vers une clé d'inbox qui vous est parvenue par un contact entrant. Refuser est fidèle ; provisionner était l'invention. **`createEntityDoc` avalait l'échec de ses deux écritures** et rendait quand même une référence — le document n'était dans aucun store, donc la session suivante ne le listait pas et sa lecture rendait vide, en silence. Il lève maintenant, comme `doc_create` en amont propage les siennes. **Deux entrées prenaient `Nuri` au lieu de `NuriLike`** (`inbox.watch`, `openDocumentInbox`), ce qui contredisait la raison même pour laquelle aucune garde de type n'est publiée. Et **deux messages d'erreur nommaient des symboles retirés** (`storeRegistry.documentInboxAddress`, `setCurrentUser`) : une erreur qui envoie vers une fonction inexistante est pire qu'une erreur muette. Contrat d'API et feuille `contract_sdk-surface` mis à jour ; `readForDocument` et le refus de `share` obtiennent enfin leur règle en §9. 189 tests unitaires, e2e 40/40 et applicatif 12/12. |
||
|
|
b7dc8ca2c3 |
fix: la suite n'était pas hermétique, et deux tests ne pouvaient pas échouer
Second tour adverse sur le lot D. Trois trouvailles, et une erreur de diagnostic de ma part qui vaut d'être consignée. **La suite verte dépendait de l'ordre des fichiers.** `bun test isolation-active public-store` donnait 5 échecs quand chaque fichier seul était vert — donc un checkout de CI avec un autre ordre d'inodes livrait rouge. Deux causes distinctes : - le travail de connexion, lancé sans être attendu par `setCurrentUser`, débordait d'un fichier sur le suivant et armait l'émulation. `connectedUser` abandonne désormais dès que l'identité pour laquelle il a démarré n'est plus connectée — ce qui est de toute façon la bonne sémantique : en amont une session appartient à un utilisateur, et changer d'utilisateur est une autre session ; - et surtout **mon propre test de store public exposait le cap d'un document que personne ne détient** — un état que la bibliothèque ne produit jamais. Il ne passait que tant que l'émulation était désarmée. Alice crée sa note avant de l'exposer, maintenant. Balayage des 21 paires de fichiers : plus aucune ne pollue. **Le contrôle symétrique ajouté hier ne pouvait pas échouer.** « La liste d'Alice ne contient pas la note de Bob » lisait un rendu ANTÉRIEUR à l'écriture de Bob : l'attente de `showScope` était satisfaite au premier sondage par le marqueur déjà à l'écran, sans synchroniser quoi que ce soit. Alice écrit désormais une note APRÈS celle de Bob — `writeNote` attend son apparition, donc ce qui suit est un rendu qui post-date. Et le `.catch` qui avalait le délai d'attente est retiré : une liste qui ne se stabilise jamais est un échec à voir, pas une dégradation à absorber. **Le test anti-fork prouvait « pas le premier », pas « le canonique ».** Son minimum lexicographique était aussi le DERNIER élément, si bien qu'un choix positionnel — la faute exacte que ce test existe pour attraper — restait vert. Le minimum est déplacé au milieu ; vérifié par mutation, « prendre le dernier » le fait rougir. **Mon erreur de diagnostic.** J'ai cru trouver, sous la trouvaille d'ordre, une fuite entre utilisateurs — les caps d'Alice classés chez Bob — et je l'ai « reproduite ». Le repro était faux : son faux `ng` ignorait le sujet dans la requête d'inbox, donc l'inbox de Bob résolvait vers celle d'Alice. Une fois le faux corrigé, la fuite ne se reproduit plus, ni avec ni sans correctif. Le danger reste réel en lecture du code — trois chemins classent des caps plusieurs `await` après la garde qui les autorisait — donc `caps.holderKey`/`learnFor` le ferment par construction, mais les commentaires disent maintenant ce que c'est : un risque fermé, pas un défaut observé. 189 tests unitaires, e2e 40/40 et applicatif 12/12. |
||
|
|
44a9b6ee04 |
test: quatre tests prouvaient autre chose que ce qu'ils annonçaient
Lot D de la revue adverse. Aucun changement de comportement de la bibliothèque : ce sont
les tests qui mentaient, et deux faux `ng` qui fabriquaient un état que le vrai broker ne
produit pas.
**« Un tiers résout l'inbox d'un autre utilisateur » prouvait le CACHE.** `userInbox`
indexe par (compte, portée) sans regarder qui demande, donc Bob tombait sur l'entrée que
la session d'Alice venait de chauffer. Rien de la persistance n'était exercé — le faux ne
servait même pas la requête `docInbox` — si bien que dans une seconde SESSION, ou une
seconde page de navigateur comme en pilote la suite applicative, Bob aurait obtenu une
inbox DIFFÉRENTE et son dépôt serait parti où personne ne lit. C'est la panne que cette
bibliothèque a déjà payée une fois.
Deux causes, toutes deux dans les faux : la requête `docInbox` n'était servie nulle part,
et le faux de `cross-user-access` prenait le **nom de graphe** pour le sujet dans un
`INSERT DATA { GRAPH <g> { … } }` — donc le pointeur du shim n'était jamais retrouvé et
chaque résolution à froid créait un nouveau shim. Les deux corrigées, et les tests qui
franchissent une frontière d'identité purgent maintenant le cache à la frontière.
**Mon propre index d'inbox avait le même défaut**, découvert en faisant ça : `isKnownInbox`
ne répondait que par sa mémoire, parce qu'aucun faux ne servait la requête. La moitié
durable n'était pas exercée — exactement la faute que ce lot corrigeait ailleurs.
**« Connecting drains BOTH levels » n'observait pas le second niveau.** Rejoué contre un
`connectedUser` qui ne draine que les inbox de l'utilisateur, il restait vert. La raison
n'est pas un test faible : le second niveau n'a **aucun producteur**. Le seul appel qui
dépose un cap est `inbox.share(doc, toUser)`, qui résout l'inbox d'un UTILISATEUR, jamais
celle d'un document. Drainer une inbox de document n'applique donc rien. Le test dit
désormais ce qu'il prouve, et l'anticipation est nommée comme telle : en amont
`AddInboxCap` est générique sur les repos et `InboxMsgContent::Link` existe, donc viser
cela est légitime — annoncer que c'est exercé ne l'était pas.
**« La liste de Bob ne contient pas la note d'Alice » n'avait pas de contrôle positif.**
Bob n'écrivait jamais de note publique : sa liste était vide quoi qu'il arrive. Il en
écrit une maintenant, et la vérification symétrique est ajoutée. Au passage, `showScope`
lisait le DOM avant le rendu — le gestionnaire `change` de l'application lance
`refresh()` sans l'attendre.
189 tests unitaires, e2e 40/40 et applicatif 12/12.
|
||
|
|
b5f05472d9 |
fix: second tour adverse — mes correctifs avaient quatre trous, dont un qui les annulait
J'ai relancé un adversaire sur les correctifs du commit précédent, comme la règle l'exige. Il en a trouvé quatre. Le premier annulait la garde que je venais d'écrire. **Le registre de propriété était écrivable depuis la surface publiée.** `assertMayWrite` lit la propriété dans l'index de store de l'appelant — et `caps.open` marquait les documents de STRUCTURE (les trois stores, les inbox) comme « créés par moi ». Donc un porteur pouvait, par le `docs.sparqlUpdate` publié, insérer `contains "<n'importe quel document>"` dans son propre index et s'en déclarer propriétaire. Démontré : Bob écrit dans le document protégé d'Alice, et détourne l'inbox d'un de ses documents — exactement le vecteur que le commit précédent prétendait fermer. `open` classe désormais sans marquer : un document de structure n'est possédé par personne au sens de la paternité, donc les deux moitiés de la garde répondent non, ce qui est correct. **`inbox.post` acceptait n'importe quel NURI.** Déplacer `depositInto` hors de la surface ne suffisait pas : `post` atteint la même porte, qui saute les deux gardes par conception. Bob, ne détenant rien, écrivait quatre triplets dans le document d'Alice. En amont la confusion est impossible — `InboxPost` scelle vers une CLÉ d'inbox et le broker route par `inboxes: PubKey → RepoId` ; adresser un document n'est pas refusé, c'est inexprimable. Le shim tient maintenant un index des inbox, l'équivalent émulé de ce que le broker sait par construction, et `post` refuse ce qui n'en est pas une. **Le filtre de lecture fuyait encore par les clés dunder.** `DeepSignalSet` expose la collection brute sur `__raw__` / `__meta__` : `[...view]` rendait zéro élément pendant que `view.__raw__` rendait le Set complet, tous utilisateurs confondus. Mon en-tête affirmait qu'« une propriété simple ne porte aucun élément » — faux pour ce type. **Et il cassait des membres légitimes** : ma liste blanche couvrait la moitié des helpers d'itération, si bien que `toArray`, `reduce`, `first`, `take`, `drop`, `flatMap` levaient sur les données du porteur lui-même. Tous filtrés désormais ; le refus ne vaut que pour l'inconnu. **Deux tests réparés à la source plutôt qu'en affaiblissant les gardes.** Le faux `doc_create` de `inbox.test.ts` rendait une CONSTANTE — tous les documents créés étaient le même NURI, donc la garde de propriété n'avait rien à distinguer et deux tests lisaient l'inbox d'Alice sous l'identité de Bob sans que rien ne proteste. Et le harnais e2e utilisait un document ordinaire comme inbox. Enfin, mon propre cache d'inbox a reproduit la faute que la revue avait relevée ailleurs : un mémo qui survit à sa session. Rattaché à `resetRegistryCache`. 189 tests unitaires (six régressions de plus), e2e 40/40 et applicatif 10/10 — après un échec réseau non reproductible, relancé sans modification. |
||
|
|
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> |