docs(brief): extraire P1a dans son propre brief, en anglais
P1a n'était qu'une section d'un brief de ~370 lignes charriant beaucoup de matière rétractée (lot PW barré, section membership en <details>, verdicts corrigés). Coder depuis ce fichier aurait été pénible et risqué. docs/briefs/2026-07-27-p1a-cap-surface.md — le lot actionnable, lisible seul : - un seul type nouveau, ReadCap, le nom de l'amont ; - capFor(nuri) sur le trousseau (la branche de store), avec l'avertissement que le trousseau n'est PAS le mécanisme de partage ; - shareCap(cap, toInbox) — un document, vers une ou plusieurs inboxes ; - rotation de clé : re-livraison automatique, rien à implémenter côté consommateur ; - contenu public : lisible par l'URL, non récursif ; - la frontière index.ts / polyfill, tranchée : signatures sur des chaînes, comme le vrai SDK ; - le test de recette sans crypto (watch-shape moissonne aujourd'hui toute chaîne did🆖 et la replie dans l'ensemble LU) ; - et ce que le lot ne fait PAS, pour ne pas le croire fini. Chaque écart écarté y est justifié plutôt que tu : types de marque, resolveCapLess, receivedCaps, refOf, parseNuri, PrincipalId. Le premier jet introduisait 8 notions nouvelles ; il en reste 2, et le critère est écrit noir sur blanc — toute notion inventée est une dette de vocabulaire. Le brief d'origine reste le chantier d'ensemble et pointe vers la fiche. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -198,159 +198,20 @@ par une vérification de signature — voir le brief Festipod « inscriptions »
|
||||
- **Le retrait doit être un message, pas une observation.** Côté consommateur : un *nudge* explicite. Le polyfill n'a **rien** à émuler pour ça — juste à ne pas prétendre le contraire.
|
||||
- **Tranché par la correction de Q1** : le compteur anonyme ne peut **pas** s'appuyer sur une validation d'existence — elle n'est pas constructible. Il repose donc sur du **déclaratif**, ce qui est acceptable (hors périmètre sécurité) tant que la **forme exposée ne ment pas** : ne pas exposer de primitif d'existence que la cible n'offrira pas.
|
||||
|
||||
## P1a — la surface (spécification, révisée 2026-07-27 après double revue adverse)
|
||||
## P1a — la surface
|
||||
|
||||
**Le découpage.** P1a = la **forme** exposée aux consommateurs ; P1b = l'**enforcement**
|
||||
(chiffrement, fermeture de l'inventaire ci-dessus). Seul **P1a bloque Festipod**.
|
||||
**Après P1a la forme est juste et l'isolation reste fausse** : ne rien affirmer
|
||||
d'anonyme avant P1b.
|
||||
**Extrait dans son propre brief : [`2026-07-27-p1a-cap-surface.md`](2026-07-27-p1a-cap-surface.md).**
|
||||
|
||||
**Critère directeur, énoncé par le PO** : être **au plus près des concepts de
|
||||
NextGraph et de son SDK**, pour réduire la complexité de développement et le
|
||||
nombre de notions à découvrir pour qui connaît déjà le SDK. Toute notion inventée
|
||||
ici est une **dette de vocabulaire** — le lecteur doit maintenir une table de
|
||||
traduction dans sa tête.
|
||||
Ce lot est **spécifié et prêt à implémenter** ; il a sa propre fiche pour qu'on
|
||||
puisse coder depuis elle sans traverser la matière rétractée de ce document-ci.
|
||||
|
||||
### 0. Ce que la première version de cette spec avait faux
|
||||
En deux lignes : un seul type nouveau (`ReadCap`), un trousseau (`capFor`), un
|
||||
partage par document vers une inbox (`shareCap`) — et rien d'autre. Les types de
|
||||
marque, `resolveCapLess`, `receivedCaps`, `refOf`, `parseNuri` et `PrincipalId`
|
||||
ont été **écartés** après double revue adverse ; les raisons sont dans la fiche.
|
||||
|
||||
Conservé délibérément : ces erreurs sont instructives et faciles à refaire.
|
||||
|
||||
| Erreur | Le réel |
|
||||
|---|---|
|
||||
| Types `DocRef` / `DocCap` | **Deux noms pour un seul objet.** En amont c'est **un** type, `NuriV0 { target, access }` : la clé est un **champ**, pas un autre type. Un NURI cap-less = `access` vide. `did:ng:` est le préfixe de **schéma** (inbox `d:`, branche `b:`, overlay `v:`…), pas un marqueur de « sans cap » — le discriminant est le segment **`:k:`**. |
|
||||
| `DocCap` comme nom | S'appelle **`ReadCap`** en amont, et ce brief l'écrivait déjà ainsi. Un `WriteCap` étant prévu, un nom générique ne dirait même pas *quel* cap on manipule. |
|
||||
| Types de marque TypeScript | Le SDK réel prend `nuri: String`. L'enforcement y est **runtime, par la crypto**, pas à la compilation. Une garantie de compilation est un concept que NextGraph **n'a pas** — et un consommateur qui typerait tout devrait *dé-typer* le jour où le vrai SDK arrive. L'inverse du but. |
|
||||
| `recipient: PrincipalId` | `Principal` n'existe nulle part en amont. On adresse une **inbox** — et `inbox.post(targetInbox: Nuri)` le fait **déjà** ainsi dans ce paquet. |
|
||||
| `sealCapTo` promis **durable** | Verbatim amont : « *Those capabilities are **not durable**… if they don't subscribe, they will lose access after the refresh. For durable capabilities, see PermaCap* » — et `PermaCap` est un TODO. |
|
||||
| `resolveCapLess(ref)` | **Inconstruisible** : aucune commande d'existence au niveau SDK ; la seule sonde est interne au crate, exige des `BlockId` **et** un repo chargé, et adresse l'overlay **inner** (dérivé du secret de lecture). L'**adressage présuppose le cap**. |
|
||||
| `parseNuri`, `refOf` | Doublons — `assertNuri` existe déjà à l'entrée SDK ; dégrader un cap se lit dans le champ `target`. |
|
||||
|
||||
### 1. Les types — un seul, la clé est un champ
|
||||
|
||||
`Nuri` **reste ce qu'il est déjà** dans ce paquet (une chaîne, ~90 usages) : la forme
|
||||
**cap-less**, fidèle à la surface du SDK réel. On ajoute **un seul** nom, celui de
|
||||
l'amont :
|
||||
|
||||
```ts
|
||||
type Nuri = string // did:ng:o:{doc}:v:{overlay} — nomme, ne lit pas
|
||||
type ReadCap = string // …:k:{clé} — nomme ET lit
|
||||
```
|
||||
|
||||
La forme analysée `{ target, readCap? }` — miroir 1:1 de `NuriV0 { target, access }` —
|
||||
sert **à l'intérieur** du polyfill ; elle ne remonte pas dans les signatures de
|
||||
l'entrée SDK. **Un seul type nouveau, zéro churn, zéro cast aux frontières
|
||||
ORM/SPARQL.**
|
||||
|
||||
### 2. Le trousseau — d'où viennent les caps
|
||||
|
||||
Le premier jet posait « aucune fonction ne va d'une référence nue vers un cap », ce
|
||||
qui rendait la spec **inapplicable** : `doc_create` renvoie un NURI **cap-less**, donc
|
||||
le créateur ne pouvait jamais obtenir le cap de son propre document.
|
||||
|
||||
Le mécanisme réel était sous nos yeux : à chaque création, un `AddRepo { read_cap }`
|
||||
est commité **sur la branche du store**. Cette branche **est le trousseau du
|
||||
propriétaire** — c'est ainsi qu'il retrouve les caps de ses documents. *(Ce n'est
|
||||
PAS le mécanisme de partage : voir §3.)*
|
||||
|
||||
```ts
|
||||
capFor(nuri: Nuri): ReadCap | undefined // cherche dans le trousseau
|
||||
```
|
||||
|
||||
L'invariant se reformule, et devient à la fois vrai et plus simple à dire :
|
||||
|
||||
> **On ne dérive pas un cap depuis une référence nue. On le cherche dans son
|
||||
> trousseau — ou on l'a reçu.**
|
||||
|
||||
Le trousseau, en amont, c'est le **wallet** : un concept que tout lecteur du SDK
|
||||
connaît déjà. `capFor` absorbe `canRead(doc)` (`capFor(n) !== undefined`) et supprime
|
||||
son verbe d'ACL.
|
||||
|
||||
### 3. Le partage — un DOCUMENT, à un ou plusieurs destinataires
|
||||
|
||||
**L'unité de partage est le document**, pas le store. Donner le cap d'un store
|
||||
livrerait tout son contenu, présent **et futur** — ce n'est pas le geste voulu, et
|
||||
c'est cohérent avec la doctrine consommateur (« le document est l'unité de partage
|
||||
et de droits »).
|
||||
|
||||
```ts
|
||||
shareCap(cap: ReadCap, toInbox: Nuri): Promise<void>
|
||||
```
|
||||
|
||||
On adresse une **inbox**, comme `inbox.post` le fait déjà. Les caps **reçus**
|
||||
n'ont pas besoin d'une opération dédiée : ils arrivent comme **dépôts d'inbox**
|
||||
d'un genre `cap`, consommés par l'`inbox.watch` **existant**. *(Ce qui résout au
|
||||
passage le point 7 de la revue adverse : la livraison d'un cap déclenche alors
|
||||
naturellement une re-lecture, au lieu de laisser des vues périmées.)*
|
||||
|
||||
**Statut amont — c'est un MANQUE, pas un désaccord** : le champ existe
|
||||
(`ContactDetails.read_cap`, « *if user wants to share the content of profile* »)
|
||||
mais le chemin est `unimplemented!()` et le récepteur **jette** le cap reçu. Le
|
||||
polyfill l'émule donc en attendant → fiche dans le bug-inbox, avec ce qu'il faudra
|
||||
en retirer le jour où l'amont l'implémente.
|
||||
|
||||
### 4. La rotation de clé — re-livraison automatique, pas perte d'accès
|
||||
|
||||
**CORRIGÉ le 2026-07-27 (direction confirmée par le PO).** Une version antérieure
|
||||
de cette section disait « qui n'est pas resté abonné perd l'accès » et demandait
|
||||
d'exposer une obligation d'abonnement. **C'est faux comme cible.** Le commentaire
|
||||
amont dont ça venait (« *if they don't subscribe, they will lose access after the
|
||||
refresh* ») décrit **l'état courant**, pas l'intention — l'erreur de méthode que ce
|
||||
brief met justement en garde de commettre.
|
||||
|
||||
La **direction** : quand une clé tourne, la nouvelle est **envoyée dans l'inbox**
|
||||
des utilisateurs qui conservent le droit d'accès, et cette inbox est **traitée
|
||||
automatiquement** dès qu'un client de l'utilisateur se connecte.
|
||||
|
||||
Conséquences pour la surface :
|
||||
|
||||
- **Aucune obligation d'abonnement à exposer.** Le consommateur n'a rien à
|
||||
implémenter pour « garder » un accès.
|
||||
- L'accès n'est pas perdu, il est **différé** jusqu'à la prochaine connexion —
|
||||
cohérent avec le reste du modèle local-first.
|
||||
- Une re-livraison emprunte **le même canal** que la livraison initiale : l'inbox.
|
||||
Donc le mécanisme de §3 couvre les deux, sans cas particulier.
|
||||
- La **révocation** reste ce qu'elle est : on cesse de re-livrer à qui ne doit plus
|
||||
lire, et c'est non rétroactif.
|
||||
|
||||
### 5. Ce qui disparaît ou change de nom
|
||||
|
||||
| Aujourd'hui | Devient |
|
||||
|---|---|
|
||||
| `grantRead(doc, grantee)` | `shareCap(cap, toInbox)` |
|
||||
| `canRead(doc, principal)` | absorbé par `capFor(nuri)` — le paramètre `principal` **était** l'inversion ACL |
|
||||
| `protectedDocsOf(owner)` | **supprimé** — la boucle de re-dérivation disparaît |
|
||||
| `makePublic(doc)` | `publishRepoLink` — le lien partageable a un nom en amont (`RepoLinkV0`) |
|
||||
| `grantWrite` / `canWrite` | traités en P1b — aujourd'hui **décoratifs** (garde jamais déclenché) |
|
||||
| `resetCaps()` au changement d'identité | **basculer** de trousseau, **pas effacer** |
|
||||
| `PrincipalId` dans la surface caps | **sort** — on adresse des inboxes |
|
||||
|
||||
### 6. Le public — lisible par l'URL, et NON récursif
|
||||
|
||||
Principe cible (confirmé par le PO, 2026-07-27), à exposer tel quel :
|
||||
|
||||
> **Un élément du store public est public : qui a l'URL lit le contenu.**
|
||||
> Mais **pas récursivement** — un contenu public peut *référencer* du contenu
|
||||
> privé, et la référence ne donne pas le contenu référencé.
|
||||
|
||||
C'est un **second mécanisme** à côté de la possession de clé du §2, pas une
|
||||
entorse à l'invariant. Et c'est la non-récursivité qui porte la valeur : elle
|
||||
permet un objet public qui **pointe** vers de l'identité privée — le cas exact du
|
||||
consommateur.
|
||||
|
||||
*Détail d'implémentation à ignorer côté forme* : NextGraph s'oriente vers un
|
||||
**non-chiffrement** du contenu du store public (les données restant signées). La
|
||||
surface ne doit pas en dépendre. **Si le store public ne fonctionne pas** comme ce
|
||||
principe le décrit, c'est **le polyfill** qui s'adapte — pas le consommateur.
|
||||
|
||||
### 7. Recette de P1a — vérifiable sans une ligne de crypto
|
||||
|
||||
`watch-shape` moissonne aujourd'hui **toute** chaîne `did:ng:` trouvée dans une
|
||||
référence de découverte et replie ces documents dans l'ensemble **lu** — une
|
||||
référence nue y donne donc **lecture complète**, la sémantique exactement inversée.
|
||||
|
||||
Après P1a : une référence nue moissonnée **ne rend rien** faute de cap au trousseau
|
||||
— ce que fait exactement le vrai NextGraph. Le test tient sans chiffrement, ce qui
|
||||
rend le découpage P1a/P1b honnête plutôt que cosmétique.
|
||||
Ce brief-ci reste le **chantier d'ensemble** : verdicts P0, périmètre de P1b,
|
||||
lots P2–P4, et les revues adverses.
|
||||
|
||||
## Esquisse de phases
|
||||
|
||||
|
||||
Reference in New Issue
Block a user