docs(concept): inscriptions — Participation lisible + drapeau active, purge par le créateur

Affinement PO du 2026-07-27. La Participation devient LISIBLE par tous et se
réduit à trois choses : référence à l'événement, booléen `active`, did cap-less
vers le profil protected du participant. Pas de description pour l'instant.

Ce que ça débloque : une suppression n'est pas détectable sans la clé (vérifié),
ce qui imposait un nudge forgeable pour la désinscription. Un objet lisible avec
un drapeau change la nature du problème — l'annulation n'est plus à DÉTECTER,
elle est à LIRE. Le blocage disparaît au lieu d'être contourné.

Le principe qui tient l'ensemble : la vérité est dans l'objet que le participant
contrôle, tout message n'est qu'un indice. Un faux « purge X » conduit le
créateur à lire X, la voir active, et ne rien faire. La forgerie devient
structurellement inoffensive — d'où l'absence de besoin de signer les dépôts
d'inbox, ce qui tombe bien : NextGraph ne l'offre pas (inbox non authentifiée,
vérification de signature non implémentée et exigeant de déchiffrer).

Le pointeur d'identité vise le profil protected existant, pas un second document
par participation : les connexions en détiennent déjà le cap. Ajouter une
connexion ne réécrit donc rien — on scelle une fois, durablement. Un champ
chiffré dans la Participation aurait exigé de re-sceller à N destinataires et de
réécrire à chaque nouvelle connexion (et n'est pas un primitif NextGraph : la
granularité de chiffrement est le document, en tout-ou-rien).

Arbitrages assumés : pas de filtrage à la lecture (Set.size est une borne haute,
exacte après purge — obsolescence acceptée pour garder la lecture en O(1)) ;
la purge incombe au créateur ; pas de description.

Point ouvert noté : Participation passe en scope public alors que la doctrine
produit la place en protected. Ce leaf décrit l'implémenté — à mettre à jour à
la graduation du brief, pas avant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
Sylvain Duchesne
2026-07-27 13:49:18 +02:00
parent ab077d8080
commit a8401bd143
@@ -1,80 +1,104 @@
---
type: brief
summary: Modèle cible des inscriptions — le participant crée une Participation chiffrée et dépose son did (URI sans ReadCap) dans l'inbox de l'événement ; le créateur traite l'inbox automatiquement, déduplique sur l'overlay (store-scopé) sans pouvoir lire, et range la référence dans un Set porté par l'événement ; compteur = Set.size ; seules les connexions détiennent la clé et reconnaissent la personne. Supersede l'Option-B (compteur muté + userId en clair dans l'inbox).
summary: Modèle cible des inscriptions — Participation LISIBLE par tous (réf. événement + booléen `active` + did cap-less vers le profil du participant), déposée dans l'inbox de l'événement ; le créateur traite l'inbox, déduplique sur l'overlay sans savoir qui, range la référence dans un Set de l'événement et PURGE les annulées ; compteur = Set.size sans filtrage (borne haute assumée) ; seules les connexions détiennent le cap du profil et reconnaissent la personne. Supersede l'Option-B (compteur muté + userId en clair).
---
# Brief (2026-07-20, révisé 2026-07-27) — inscriptions par Set
## Le modèle
Posé par le PO le 2026-07-27. Tout est **clés et URLs** — pas de rôle, pas d'appartenance, pas de liste d'autorisation.
Posé et affiné par le PO les 2026-07-27. Tout est **clés et URLs** — pas de rôle, pas d'appartenance, pas de liste d'autorisation.
1. Le participant crée un objet **Participation****chiffré**, dans son store *protected* — décrivant son inscription.
2. Il dépose dans l'**inbox de l'événement** le **did** de cet objet : l'URI **sans ReadCap**.
3. Le **créateur** de l'événement traite son inbox **automatiquement**, dès qu'il est en ligne.
4. Il **ne peut pas lire** la Participation, donc il **ignore l'identité** du participant — mais il a de quoi **dédupliquer** (voir ci-dessous).
1. Le participant crée un objet **Participation**, **lisible par tous**, contenant : la **référence à l'événement**, un **booléen `active`**, et un **did cap-less vers son profil** *protected*. **Rien d'autre** — pas de description pour l'instant.
2. Il dépose le **did de la Participation** dans l'**inbox de l'événement**.
3. Le **créateur** traite son inbox **automatiquement**, dès qu'il est en ligne.
4. Il **déduplique** (voir plus bas) — **sans savoir qui est le participant** : il détient le did du profil, pas son cap.
5. Il range une **référence** à la Participation dans un **Set** porté par le document de l'événement.
6. N'importe qui lit **`Set.size`** → le nombre de participants.
7. Une personne **connectée** au participant détient la clé, **déchiffre** la Participation et **reconnaît** la personne. *(Traité dans un 2e temps.)*
7. Une personne **connectée** au participant détient le cap de son profil, le lit, et **reconnaît** la personne.
Trois propriétés en découlent, et ce sont elles qu'on cherchait : **présence anonyme par défaut** (le créateur lui-même ne voit pas qui) ; **personne ne désinscrit autrui** (l'objet est auto-possédé) ; **aucun `userId` en clair** ne circule.
**Désinscription** : le participant passe `active` à faux **sur son propre objet**. Le créateur le constate en lisant, et **purge** — il retire la référence du Set.
Trois propriétés en découlent : **présence anonyme par défaut** (le créateur lui-même ne voit pas qui) ; **personne ne modifie l'inscription d'autrui** (seul le participant détient la clé d'écriture de son objet) ; **aucun `userId` en clair** ne circule.
### Le principe qui tient tout : la vérité est dans l'objet, les messages sont des indices
C'est l'objet **contrôlé par le participant** qui fait foi. Tout message — dépôt d'inbox, notification de purge — n'est qu'un **indice** qui déclenche une vérification, jamais une autorité.
Conséquence : la **forgerie devient structurellement inoffensive**. Un faux « purge X » conduit le créateur à lire X, constater qu'elle est encore active, et ne rien faire. C'est pourquoi les dépôts d'inbox **n'ont pas besoin d'être signés** — ce qui tombe bien, puisque NextGraph ne l'offre pas (voir tableau).
### Pourquoi un booléen plutôt qu'une suppression
Une **suppression** n'est **pas détectable** sans la clé de lecture (VÉRIFIÉ : append-only, tombstone chiffré). Un objet **lisible** avec un **drapeau** transforme le problème : l'annulation n'est plus à *détecter*, elle est à *lire*. Le blocage disparaît au lieu d'être contourné par un message forgeable.
### Pourquoi le pointeur d'identité vise le profil existant
Pas besoin d'un second document par participation : le **profil protected** du participant joue ce rôle, et ses connexions en détiennent **déjà** le cap — c'est la définition d'« être connecté ». Un tiers voit un did opaque.
L'avantage sur un champ chiffré dans la Participation : **ajouter une connexion ne réécrit rien**. On lui scelle le cap du profil, une fois, durablement. Un champ chiffré exigerait de re-sceller à N destinataires et de réécrire la Participation à chaque nouvelle connexion. *(Accessoirement, un champ chiffré n'est pas un primitif NextGraph : la granularité de chiffrement est le document, en tout-ou-rien.)*
## Sur quoi ça repose — faits établis dans NextGraph
Vérifiés par lecture de `nextgraph-rs`. Le détail et les pointeurs vivent côté polyfill (`docs/readcap-and-nuri-model.md`) — cf. [[rule_capture-nextgraph-findings]].
Vérifiés par lecture de `nextgraph-rs`. Détail et pointeurs côté polyfill (`docs/readcap-and-nuri-model.md`) — cf. [[rule_capture-nextgraph-findings]].
| Fait | Statut | Ce qu'il permet ici |
| Fait | Statut | Rôle ici |
|---|---|---|
| Un NURI cap-less (`did:ng:o:{repo}:v:{overlay}`, sans `:k:`) **nomme sans donner à lire** | VÉRIFIÉ | Le did déposé dans l'inbox référence sans divulguer |
| L'**overlay** (`:v:`) est **store-scopé**, jamais document-scopé | VÉRIFIÉ | **La clé de dédup** — voir section suivante |
| Sans la clé, les blocs restent **du ciphertext** | VÉRIFIÉ | Le créateur ne peut vraiment pas lire |
| L'existence d'un objet est **vérifiable sans clé** (protocole `Ext`, sans identité ni contrôle) | VÉRIFIÉ | *Optionnel* : valider qu'un did pointe sur un objet réel avant de l'ajouter |
| Une **suppression** n'est **PAS** détectable sans la clé (append-only, tombstone chiffré) | VÉRIFIÉ | La désinscription ne peut pas être *observée* — voir points ouverts |
| L'**overlay** (`:v:`) est **store-scopé**, jamais document-scopé | VÉRIFIÉ | **La clé de dédup** |
| Un NURI cap-less **nomme sans donner à lire** | VÉRIFIÉ | Le did du profil pointe sans divulguer |
| Un cap se **scelle durablement** à un destinataire (pas d'ACL re-déclarée) | VÉRIFIÉ | Le cap du profil, scellé une fois aux connexions |
| Sans la clé, les blocs restent du **ciphertext** | VÉRIFIÉ | Le créateur ne peut vraiment pas lire le profil |
| Une **suppression** n'est **PAS** détectable sans la clé | VÉRIFIÉ | **Pourquoi c'est un drapeau, pas une suppression** |
| Un dépôt d'inbox n'est **PAS authentifié** (sealed box anonyme) | VÉRIFIÉ | **Pourquoi les messages doivent rester des indices** |
| La vérification de signature d'auteur **n'est pas implémentée** au runtime, et exigerait de déchiffrer | VÉRIFIÉ | Écarte l'alternative « dépôt d'inbox signé » |
## La dédup : sur quoi exactement
**Validé par le PO (2026-07-27).** Le mécanisme derrière « il a normalement assez d'informations pour dédupliquer » :
**Validé par le PO (2026-07-27).**
Le segment `:v:` d'un NURI ne vient **pas du document** mais de **son store** (`ProtectedStore(id) → outer(id)`). Or une personne a **un seul** store protected. Donc **toutes ses Participations portent le même `:v:`**, quel que soit le nombre d'objets qu'elle crée. Le créateur déduplique là-dessus : deux références de même `:v:` dans le Set d'un même événement = la même personne. **Sans jamais savoir qui.**
Le segment `:v:` d'un NURI ne vient **pas du document** mais de **son store**. Or une personne a un seul store par scope. Donc **toutes ses Participations portent le même `:v:`**, quel que soit le nombre d'objets qu'elle crée. Le créateur déduplique là-dessus : deux références de même `:v:` dans le Set d'un même événement = la même personne. **Sans jamais savoir qui.**
C'est le critère **robuste** — plus que le did du profil, qu'un participant pourrait multiplier en créant plusieurs documents de profil dans son store.
Conséquence de conception : le Set est **indexé par `:v:`** — au plus une référence par `:v:`. `Set.size` = nombre de `:v:` distincts = nombre de personnes distinctes.
### La contrepartie — réserve durable, à ne pas perdre
> **Elle vit dans `app-security/caveat_stable-overlay-pseudonym`**, pas ici. Ce brief a vocation à être dissous à sa graduation ; la réserve, elle, doit survivre. Résumé ci-dessous, référence là-bas.
> **Elle vit dans `app-security/`[[caveat_stable-overlay-pseudonym]]**, pas ici. Ce brief a vocation à être dissous à sa graduation ; la réserve doit lui survivre.
Ce `:v:` est un **pseudonyme stable et permanent de la personne**, présent dans **toute** référence cap-less vers **n'importe lequel** de ses documents protected. Il ne dit pas *qui* (`BLAKE3(store_id)` n'est pas inversible), mais c'est un **handle constant**, le même partout et pour toujours.
En bref : ce `:v:` est un **pseudonyme stable et permanent** de la personne, présent dans toute référence cap-less vers ses documents. Il ne dit pas *qui*, mais un **seul** recoupement dé-anonymise **rétroactivement** tout son historique — et **aucune porte de sortie n'existe** (aucune rotation possible, VÉRIFIÉ). C'est **le même bit d'information** qui permet de dédupliquer sans lire et de tracer d'un événement à l'autre : les deux ne se séparent pas. Rendre la Participation publique **augmente la surface de collecte** de ce pseudonyme.
- Un créateur organisant plusieurs événements voit « le même `:v:` revient » → **corrélation de présence** d'un événement à l'autre.
- Si quelqu'un apprend un jour, par un seul recoupement, que `:v:X` = Marie, **toutes** les participations de Marie deviennent liables **rétroactivement**.
## Arbitrages assumés (PO, 2026-07-27)
**Le point structurel** : c'est **le même bit d'information** qui permet de dédupliquer sans lire et qui permet de tracer. On ne peut pas garder l'un en supprimant l'autre — sauf à changer le **découpage en stores**, ce qui déplace le curseur sans faire disparaître l'arbitrage. Ce n'est pas un défaut de l'émulation : la propriété survit au vrai NextGraph.
- **Pas de filtrage à la lecture.** Le compteur est `Set.size`, **sans** vérifier les `active`. On accepte le **risque d'obsolescence** : une participation annulée compte encore tant que le créateur n'a pas purgé. `Set.size` est donc une **borne haute**, exacte après purge. *Motif : garder la lecture simple et en O(1).*
- **La purge incombe au créateur.** Pas de service curateur, pas de rattrapage par les lecteurs.
- **Pas de description** dans la Participation pour l'instant. *(À rouvrir quand le besoin viendra : ce qu'on y mettrait deviendrait public.)*
- **Créateur hors-ligne** : le Set ne bouge pas tant qu'il n'a pas traité son inbox. Accepté.
## Ce qui change vs l'implémentation actuelle (Option-B)
L'existant ([[brief_2026-07-06_reactive-reads-and-attendance]]) dérive un `participantCount` **muté en place** depuis des marqueurs d'inbox portant le **`userId` en clair**.
- **Retirer le `userId`** des dépôts d'inbox → ne reste que le **did cap-less**.
- **Retirer le `userId`** des dépôts d'inbox → ne reste que le **did de la Participation**.
- **Compter des références distinctes** (par `:v:`), plus des `userId`.
- **`event.participantCount` muté disparaît** au profit de `Set.size`.
- La **résolution d'identité** passe par la **lecture de l'objet** (donc par la clé), plus par le marqueur.
- La **résolution d'identité** passe par la **lecture du profil** (donc par son cap), plus par le marqueur.
- **La désinscription cesse d'être une suppression** → un `active` à faux + purge par le créateur. Cf. [[caveat_participation-deletion]], dont l'exigence (« autoritative, ne doit pas réapparaître ») reste valable mais change de mécanisme.
Ce qui reste valable tel quel : la **lecture réactive** et le **ré-armement à la reconnexion** ; le **fix d'espaces d'id** déjà livré.
Reste valable tel quel : la **lecture réactive**, le **ré-armement à la reconnexion**, le **fix d'espaces d'id** déjà livré.
## Points ouverts
- **Désinscription.** Supprimer son objet ne suffit pas : sans clé, le créateur ne peut pas *observer* la suppression (VÉRIFIÉ). Il faut donc un **acte explicite** — un second dépôt d'inbox « retire la référence X ». Forgeable (n'importe qui peut le déposer), ce qui est **hors périmètre sécurité** mais mérite d'être acté. Cf. [[caveat_participation-deletion]] : la désinscription doit rester **autoritative**.
- **Créateur hors-ligne.** Le Set ne bouge pas tant qu'il n'a pas traité son inbox. Accepté en V1 ; un service curateur est la sortie éventuelle.
- **Validation d'existence.** Le créateur *peut* vérifier qu'un did pointe sur un objet réel avant de l'ajouter (protocole `Ext`). **Pas requis** par le modèle — à ranger comme durcissement optionnel, pas comme prérequis.
- **Reconnaissance par les connexions** (étape 7) — explicitement remise à un 2e temps : comment la clé est scellée aux connexions, et ce qu'il advient d'une connexion rompue.
- **Scope de Participation** — elle devient **publique** alors que la doctrine produit actuelle la place en *protected* ([[knowledge_data-scopes-and-discovery]], concept `functional-domain`). Ce leafcrit **ce qui est implémenté** : ne pas le modifier tant que ce brief n'a pas gradué, mais **le mettre à jour à ce moment-là**.
- **Reconnaissance par les connexions** (étape 7) — comment le cap du profil est scellé, et ce qu'il advient d'une connexion rompue (la révocation est un re-key grossier et non rétroactif). Explicitement remis à un 2e temps.
- **Validation d'existence** — le créateur *peut* vérifier qu'un did pointe sur un objet réel sans clé (protocole `Ext`). **Pas requis** ; durcissement optionnel, et à ne pas rendre load-bearing : la garde correspondante n'est pas branchée côté NextGraph et pourrait l'être un jour.
## Dépendances
- **Bloquant** : l'**émulation caps du polyfill**. Aujourd'hui `caps.ts` modélise une **ACL** (set de principals par document) là où le réel est **possession de clé**, et le contenu reste lisible en clair (`sparqlQuery`, `inbox.read` contournent le filtre). Tant que ce n'est pas corrigé, coder l'anonymat côté Festipod produirait du code qui **prétend** isoler sans isoler. Brief polyfill `2026-07-20-caps-emulation-alignment`, lot P1.
- **Bloquant** : l'**émulation caps du polyfill**. `caps.ts` modélise aujourd'hui une **ACL** (set de principals par document) là où le réel est **possession de clé**, et le contenu reste lisible en clair (`sparqlQuery`, `inbox.read` contournent le filtre). Tant que ce n'est pas corrigé, coder l'anonymat côté Festipod produirait du code qui **prétend** isoler sans isoler. Brief polyfill `2026-07-20-caps-emulation-alignment`, lot P1.
- **Parké** : la **terminologie identité** (wallet / user / profil) — cf. `.project/to-discuss.md`.
## Statut : direction cible validée, mise en œuvre gatée
## Statut : modèle tranché, mise en œuvre gatée
Le modèle est **tranché** (PO, 2026-07-27) et ses fondations sont **vérifiées**. Ce qui reste gaté, c'est la **mise en œuvre** : elle attend le lot P1 du polyfill. **Ne pas retirer l'Option-B** d'ici là.
Le modèle est **arrêté** (PO, 2026-07-27) et ses fondations sont **vérifiées**. Ce qui reste gaté, c'est la **mise en œuvre** : elle attend le lot P1 du polyfill. **Ne pas retirer l'Option-B** d'ici là.
Liens : [[brief_2026-07-06_reactive-reads-and-attendance]] (superseded), [[caveat_participation-deletion]], [[rule_capture-nextgraph-findings]], [[rule_document-per-entity]], app-security ([[brief_2026-05-18_authorization-matrix]], [[knowledge_trust-model]]), polyfill `readcap-and-nuri-model.md` + `docs/vision.md`.
Liens : [[brief_2026-07-06_reactive-reads-and-attendance]] (superseded), [[caveat_participation-deletion]], [[rule_capture-nextgraph-findings]], [[rule_document-per-entity]], app-security ([[caveat_stable-overlay-pseudonym]], [[brief_2026-05-18_authorization-matrix]], [[knowledge_trust-model]]), polyfill `readcap-and-nuri-model.md` + `docs/vision.md`.