refactor(comments): retirer les raisonnements sur l'état de NextGraph du code app

Application de la règle qu'on vient de durcir : l'app IGNORE entièrement l'état
d'implémentation de NextGraph. Le CODE était légitime — `inbox.readSynced` est
une surface SDK exportée ; ce sont les JUSTIFICATIONS qui fautaient, en
expliquant les choix par des internes du cœur.

Réécrit en termes de CONTRAT :
- registration.ts / FestipodDataContext : « barrier-gated read, le repo d'inbox
  n'est pas encore ouvert dans le verifier, un read ancré renverrait 0 » devient
  « `read` rend ce qui est connu localement maintenant, `readSynced` rend une
  fois les dépôts synchronisés visibles ; ce site a besoin du second parce qu'il
  lit depuis une session froide ».
- ngBootstrap : « le verifier sérialise les créations » devient « `docCreate` est
  un aller-retour qui ne recouvre pas le suivant, donc le coût du seed croît
  LINÉAIREMENT avec le nombre de documents ». Le ~2s mesuré est conservé, mais
  explicitement comme une observation, pas comme un contrat.
- entityWrites : description de lecture périmée (ORM fan-out, ngSet couplé au
  scope) remplacée par la vue réactive.

La distinction read/readSynced vit désormais là où elle est légitime :
knowledge_sdk-surface, avec le critère de choix (`read` dans une session qui
observe déjà l'inbox, `readSynced` dès que la justesse dépend d'une session
froide voyant le dépôt d'une autre identité). knowledge_context-internals cesse
d'expliquer le fix par `ensureRepoOpen`/premier `State` et pointe le contrat.

Laissé tel quel : `src/shared/support/hooks.ts` et les steps e2e — le harness de
test connaît légitimement la plomberie ; la règle vise l'app. Et le « no
cross-account fan-out » de FestipodDataContext, qui décrit le périmètre de l'app
et non NextGraph.

tsc : 0 erreur.

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-28 17:17:47 +02:00
parent 7459d49e83
commit 05ee576d7d
6 changed files with 26 additions and 29 deletions
+8 -9
View File
@@ -599,10 +599,9 @@ function useNgData(): FestipodDataContextValue {
` — participantCount before write (as currently read) = ${knownCount ?? '(unknown)'}`,
);
// (1) COUNT — derive the distinct active-registration set for this event
// and write it on MY OWN event doc (only when it changed). The read inside
// `materializeAttendance` is BARRIER-GATED (`inbox.readSynced`): at the
// owner's connection it waits for the inbox sync barrier before reading, so
// a registrant's already-synced deposit IS seen (no premature 0).
// and write it on MY OWN event doc (only when it changed). `materializeAttendance`
// reads through the synced-view contract (`inbox.readSynced`), so a
// registrant's deposit is visible even on a cold session.
const active = await materializeAttendance(targetInbox, evId);
const nextCount = active.length; // no host baseline (creator not auto-in)
const prevCount = materializedCountRef.current.get(evId);
@@ -663,11 +662,11 @@ function useNgData(): FestipodDataContextValue {
}
};
// (A) RELIABLE-AT-CONNECTION: run one barrier-gated materialization directly on
// this trigger ([ready, ownedKey]). This is the spec's core — the owner, at its
// NEXT CONNECTION, deterministically processes its owned events' inbox (the read
// waits for the inbox sync barrier, so a registrant's synced deposit is seen).
// It does NOT depend on a cross-session inbox push arriving.
// (A) RELIABLE-AT-CONNECTION: run one materialization directly on this trigger
// ([ready, ownedKey]). This is the spec's core — the owner, at its NEXT
// CONNECTION, deterministically processes its owned events' inbox, reading
// through the synced-view contract. It does NOT depend on a cross-session
// inbox push arriving.
void materialize('connection');
// (B) SAME-SESSION LIVE: `inbox.watch` fires on the initial state push and on
+3 -4
View File
@@ -16,10 +16,9 @@
* `docs.sparqlUpdate` primitive (the real injected `ng`) — the same direct-write
* path `insertNotification` already uses. The document was just created and is
* openable, so the write lands immediately. The READ stays reactive: the document
* NURI is registered into the scope's `useShape({ graphs })`, and the ORM reads
* the entity back. Write (direct, per-document) and read (reactive fan-out) are
* decoupled — the model (one document per entity, per-document isolation) is
* unchanged; only the write mechanism moves off the scope-coupled ngSet.
* NURI joins the scope's reactive view, which reads the entity back. Write
* (direct, per-document) and read (reactive) are decoupled — the model (one
* document per entity, per-document isolation) is unchanged.
*
* TYPED TERMS. The ORM reads back via the SHEX shapes (festipodShapes.shex), so
* each field must be written with the RIGHT RDF term: xsd:integer/float/boolean
+7 -8
View File
@@ -250,14 +250,13 @@ export async function materializeAttendance(
targetInbox: string,
eventId: string,
): Promise<ActiveRegistration[]> {
// BARRIER-GATED read (`inbox.readSynced`, not `inbox.read`): the owner
// materializes at its NEXT CONNECTION, and on a fresh session the event inbox
// repo is not yet open in the verifier — a plain anchored read would silently
// return 0 deposits even for a registrant's deposit already synced to the broker
// (the premature-0 that made the owner stick at count 0). `readSynced` awaits the
// inbox's first `State` (deterministic sync barrier) before reading, so the
// synced deposits ARE visible. No polling (rule_no-broker-polling): one barrier
// wait, then one read.
// `inbox.readSynced`, not `inbox.read` the two differ by CONTRACT, and this
// call site needs the stronger one. `read` returns what is known locally right
// now; `readSynced` returns once the deposits synced to this inbox are visible.
// The owner materializes at its NEXT CONNECTION, i.e. from a cold session where
// "known locally right now" is not yet the truth — so the synced view is the
// only correct one here. Single wait, single read: no polling
// (see `rule_no-broker-polling`).
const deposits = await inbox.readSynced(targetInbox);
// Match deposits to this event on the CANONICAL id-form (base repo id, stripping
// any `:v:<overlay>` suffix). On the current tree the forms already agree, but
+5 -5
View File
@@ -38,8 +38,7 @@ export interface BootstrapResult {
* model's doc set for the union READ.
*
* `walletHasData` tells the seed whether the wallet already carries entities (a
* returning user → skip). The caller computes it from the union read (no ORM set
* needed — the read side is now the one-shot union query, not a reactive fan-out).
* returning user → skip). The caller computes it from what it has already read.
*/
export async function bootstrapWallet(
walletHasData: boolean,
@@ -70,9 +69,10 @@ export async function bootstrapWallet(
// OWNER is shared). Falls back to the fixture username when no login is present.
const seedOwner = owner ?? (seedUsers[0] ? normalizeIdentifier(seedUsers[0].username) : 'seed');
// SEED FOOTPRINT (perf). Each entity is its OWN document, and each `docCreate`
// is a SERIAL ~2s broker round-trip (the verifier serializes creations — they
// do NOT parallelize), so the seed cost is ~2s × (#docs). Seeding the full
// SEED FOOTPRINT (perf). Each entity is its OWN document, and `docCreate` is a
// round-trip that does not overlap with the next one, so the seed cost grows
// LINEARLY with the number of documents (measured around 2s each on our test
// setup — an observation, not a contract). Seeding the full
// fixture (14 users + 5 events + 5 participations = 24 docs) blows past the test
// step budget. So the CONNECTED seed writes only what the app/@data needs to be
// exercised: ALL events (looked up by title), a FEW user profiles ("wallet has