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.
This commit is contained in:
Sylvain Duchesne
2026-08-10 12:09:20 +02:00
parent 0d9e2bbe97
commit f378c71739
13 changed files with 183 additions and 67 deletions
+15 -11
View File
@@ -161,7 +161,7 @@ Consistent with the rest of the model: no role and no list, only "do you hold th
- There is **no existence command at the SDK level**.
- The only probe (`BlocksExist`) is **internal to the crate**, requires `BlockId`s **and** an already **loaded** repo, and addresses the **inner** overlay — which is derived from the **read secret**.
- A cap-less reference carries a RepoId and the **outer** overlay: no `BlockId` to probe. And the outer is never registered anyway (`expose_outer` hard-coded to `false`, with no SDK parameter).
- The only primitive accessible to a non-member (`ExtObjectGet`) requires the ObjectIds **and their keys**.
- The primitive a non-member can reach (`ExtObjectGet`) requires the **ObjectIds**, which one only holds once one can already read. *(Corrected 2026-08-10 — this line used to say "the ObjectIds **and their keys**", and to call `ExtObjectGet` the **only** such primitive. Both are wrong at the source: `ExtObjectGetV0 { overlay, ids: Vec<ObjectId>, include_files }` has **no key field** at all (`engine/net/src/types.rs:4492-4501`), and `ExtRequestContentV0` has **three** variants — `WalletGetExport`, `ExtObjectGet`, `ExtTopicSyncReq` (`:4520-4526`), the last of which falls into `unimplemented!()` (`:4533`). The conclusion is unchanged and rests on addressing, not on keys: blocks come back **encrypted**, and naming them needs ObjectIds a non-holder does not have — the formulation `docs/nextgraph-current-state.md` § "The `Ext` protocol serves blocks with no control" already carried.)*
> **Addressing itself presupposes the cap.** Proving a document's existence without holding its key is not constructible today, and nothing indicates that it is planned.
@@ -258,16 +258,18 @@ A **wallet is only a keyring**. What we have been calling a "virtual user" is, u
### Giving access is a **Link** — one word, three places, all already named
**VERIFIED 2026-07-30.** The delivery message, the register and the record all exist upstream under the same word, which is what a shape being real looks like:
**VERIFIED 2026-07-30, state column corrected 2026-08-10.** The delivery message, the register and the record all exist upstream under the same word, which is what a shape being real looks like. What none of them is, is *implemented* — the table said so of three rows, and re-reading the source refuted it:
| Step | Upstream | State |
|---|---|---|
| The message deposited in the recipient's inbox | `InboxMsgContent::Link` (`engine/net/src/types.rs:4249-4261`) | **declared, payload-less** — a variant with no fields, i.e. specified and not implemented |
| Where the recipient files it on processing | `AddLink { read_cap }` on the **User branch** of the private store (`engine/repo/src/types.rs:1934-1950`) | implemented (verifier arm `commits/mod.rs:681`) |
| Withdrawing it | `RemoveLink`, ORset (`engine/repo/src/types.rs:1952`) | implemented |
| What circulates | `RepoLinkV0 { read_cap, … }` (`engine/net/src/types.rs:5062`) | implemented |
| Where the recipient files it on processing | `AddLink { read_cap }` on the **User branch** of the private store (`engine/repo/src/types.rs:1934-1950`) | **declared, stubbed** — the verifier arm is a no-op `Ok(())` (`engine/verifier/src/commits/mod.rs:681-693`), and nothing in the workspace constructs one |
| Withdrawing it | `RemoveLink`, ORset (`engine/repo/src/types.rs:1952`) | **declared, stubbed** — the same no-op `Ok(())` arm (`commits/mod.rs:695-707`) |
| What circulates | `RepoLinkV0 { read_cap, … }` (`engine/net/src/types.rs:5062-5078`) | **declared only** — zero constructors and zero consumers in the workspace (only the `RepoLink` wrapper and its two accessors, `:5082-5097`) |
So: **deposit a Link into the recipient's inbox; on connection the recipient processes the inbox and files it with `AddLink` on their User branch.** That is the whole gesture, and every piece of it has a name.
**What makes "stub" the right word rather than a quibble**: the neighbouring arm in the same file does real work. `CommitVerifier for AddRepo` calls `load_repo_from_read_cap` then `add_doc` (`commits/mod.rs:644-664`); `AddLink` and `RemoveLink`, twenty lines below, return `Ok(())` with `#[allow(unused_variables)]` on every parameter. Same trait, same file, opposite states — so "there is a verifier arm" cannot be read as "the register works".
So: **deposit a Link into the recipient's inbox; on connection the recipient processes the inbox and files it with `AddLink` on their User branch.** That is the whole gesture, and every piece of it has a **name** — which is not the same as having a behaviour. Per this document's own rule, none of that says what the target will do; it says the gesture is fully specified and none of it runs.
Two consequences worth stating, because both are easy to get wrong:
@@ -286,23 +288,25 @@ There IS a register, and it is a fourth commit type next to `AddRepo`:
pub struct AddLinkV0 { pub read_cap: ReadCap, /**/ }
```
`engine/repo/src/types.rs:1934-1950`, with `RemoveLink` as its ORset counterpart (`:1952`) and a verifier arm at `engine/verifier/src/commits/mod.rs:681`. So:
`engine/repo/src/types.rs:1934-1950`, with `RemoveLink` as its ORset counterpart (`:1952`) and a verifier arm — a no-op `Ok(())` one — at `engine/verifier/src/commits/mod.rs:681-693`. So:
- it lives on the **User branch** — created only on the **private store** (`engine/repo/src/store.rs:448-452`; the public/protected stores get an `Overlay` branch instead), which also carries `AddInboxCap { repo_id, overlay, priv_key }`*"so that a user can share with all its device"* (`engine/repo/src/types.rs:1969-1981`). So the User branch answers two questions with one mechanism: **which caps I received**, and **which inboxes I may read**;
- it is explicitly for **external repos** — someone else's documents, exactly the received-cap case;
- and its stated purpose is to **share the link with all of the user's devices**. It is wallet-resident and cross-device, not a local cache.
**Level 3 (local user storage) is therefore a cache, not the register.** The register is level 2': `AddLink` on the User branch of the private store.
**Level 3 (local user storage) is therefore a cache, not the register.** The register — the durable, cross-device record — is level 2': `AddLink` on the User branch of the private store.
What remains true, and is a separate matter — the *delivery* path is unimplemented:
**But "the register exists, only the delivery is missing" is FALSE, and this section said it until 2026-08-10.** Both ends are declared and stubbed, as the table above now records: nothing constructs an `AddLink` commit anywhere in the workspace, and the arm that would apply one returns `Ok(())`. So the corrected statement is: `AddLink` on the User branch is **where a received cap belongs in the model** — a placement the source states unambiguously and this library aligns on — and no part of the gesture runs today, neither the road nor the destination. Per the design principle that gap says nothing about the target; it forbids only calling the register "implemented".
- `InboxMsgContent::ContactDetails` processing (`engine/verifier/src/inbox_processor.rs:778-847`) creates a contact document holding the profile, inbox, name and email — and **never reads `details.read_cap`**. Confirmed on sight: the receiver discards it. So the cap never reaches the User branch today — the register exists, the road to it does not.
The delivery half, in detail:
- `InboxMsgContent::ContactDetails` processing (`engine/verifier/src/inbox_processor.rs:778-847`) creates a contact document holding the profile, inbox, name and email — and **never reads `details.read_cap`**. Confirmed on sight: the receiver discards it. So no cap reaches the User branch today.
- `RepoLinkV0` states the intended flow (`engine/net/src/types.rs:5055-5061`): *"the link is shared and then the recipient opens it and subscribes soon afterward"*. **The key IS kept**: opening the repo persists its `read_cap` in local user storage, so the next session decrypts fine. What is not durable is the key's **validity** — a `RootCapRefresh` (§3) mints a new one, and receiving it depends on **the rotating party choosing to send it to you** (§3's DIRECTION block), not on any subscription state.
> **Do not write "only a subscriber receives the new key".** That reads the `RepoLinkV0` comment as intent, which §3 already forbids. **Subscribing is a purely LOCAL act** — automatic pull of changes — and the other party records nothing about it; there is no subscriber list to send to. Who gets a rotated key is the rotating party's decision, delivered to an inbox.
- `PermaCap` — still a **TODO** (`engine/repo/src/types.rs:578`) — covers exactly the gap that leaves: a link *"stored on disk and kept there unopened for a long period"*, i.e. never loaded, therefore never subscribed, therefore missing every refresh.
> **So there are TWO registers, by origin**: `AddRepo` on the **Store** branch for the documents a user creates in that store, and `AddLink` on the **User** branch of the private store for caps received for someone else's documents. Local user storage caches both. Opening a repo persists its cap locally, but that is the cache filling — not the durable record.
> **So there are TWO registers, by origin**: `AddRepo` on the **Store** branch for the documents a user creates in that store, and `AddLink` on the **User** branch of the private store for caps received for someone else's documents. (`AddRepo` runs; `AddLink` is declared and stubbed — see the table above. The *split by origin* is the model's, whatever each half's state.) Local user storage caches both. Opening a repo persists its cap locally, but that is the cache filling — not the durable record.
*Consequence for this library*: **both durable registers are now emulated** (2026-07-30) — `AddRepo` as a `shim:readCap` record on a distinct subject of the store document (`storeBranch`), `AddLink` as `shim:link` on another (`userBranch`) — and the in-memory `CapRegistry` is what it always was, level 3: the cache. Caps are READ back from those records, never recomputed. What stays an invention is representing branches as RDF subjects at all: upstream both branches carry `BranchCrdt::None` and hold service commits, not triples. What is faithful is that the key sits beside the document, and that the listing (`contains`, the Main branch) is separate from the keys.