Ng eventually #1

Open
Sylvain wants to merge 110 commits from ng-eventually into main
7 changed files with 28 additions and 63 deletions
Showing only changes of commit ff26f26e60 - Show all commits
@@ -10,7 +10,7 @@ A **probe** is a one-off Playwright script, outside Cucumber — no World, no ho
## When to reach for one
Before believing a flow works. The create-and-participate flow had been declared *correct by construction* on typecheck, build and reading; the first probe ever run against it found **three defects** none of those could see — two still open ([[bug_signup-breaks-the-next-connection]], [[bug_participant-count-stays-at-zero]] in `data-layer`) and one shipped as a fix.
Before believing a flow works. The create-and-participate flow had been declared *correct by construction* on typecheck, build and reading; the first probe ever run against it found **three defects** none of those could see — one still open ([[bug_participant-count-stays-at-zero]] in `data-layer`) and two shipped as fixes.
Reach for it when the suite cannot answer the question: the `@data` run dies silently from around its sixth scenario ([[caveat_wallet-bloat-hang]]), its scenarios have no fixtures ([[caveat_data-suite-has-no-fixtures]]), and entry paths are covered by nothing ([[caveat_first-time-entry-untested]], `app-architecture` → [[caveat_boot-unverified-outside-broker]]).
+2 -3
View File
@@ -27,10 +27,9 @@ How Festipod **persists its data** through NextGraph (P2P, local-first, end-to-e
- [[rule_document-per-entity]] — every entity gets **its own document** (per scope), never one at store level; access is granted per document, so this is what makes isolation possible
- [[rule_app-uses-sdk-surface-only]] — the pulled contract is the only reference; a gap in it is raised with the provider, never worked around here
## Open defects — the sign-up flow does not deliver
## Open defects — the sign-up flow does not deliver an honest count
- [[bug_signup-breaks-the-next-connection]] — after a sign-up, the next connection fails; a **provider-side gap**, raised with the provider, nothing to work around here
- [[bug_participant-count-stays-at-zero]] — the count never moves in the session that signs up; whether it converges later is **unknown**, and unmeasurable while the bug above holds
- [[bug_participant-count-stays-at-zero]] — the count stays at 0 through the session that signs up and through the first reconnect, only catching up on the second
## Pitfalls (read before touching deletions / event fields)
@@ -3,29 +3,36 @@ type: bug
severity: major
opened: 2026-08-16
last_checked: 2026-08-16
summary: After a sign-up the count stays at 0 for the rest of the session while the button reads « ✓ Je participe » — observed with the event's owner (the counter's only writer) present and connected. Whether it converges at the next connection is UNKNOWN, not known-good.
summary: After a sign-up the count stays at 0 for the rest of the session and through the first reconnect — it only reaches the true value on the SECOND reconnect, one connection later than expected. Withdrawal converges in a single connection, for a reason not yet established.
---
# The participant count does not converge in the same session
# The participant count takes one connection more than expected to converge
## What happens, VERIFIED
In the create-and-participate flow — declare an event, sign up to it — the event's `participantCount` **stays at 0 for the rest of the session** while the button reads « ✓ Je participe ». **VERIFIED 2 runs out of 2**, the count still 0 **120 s** and **75 s** after the sign-up.
In the create-and-participate flow — declare an event, sign up to it — the event's `participantCount` **stays at 0 for the rest of the session** while the button reads « ✓ Je participe ». VERIFIED 2 runs out of 2, the count still 0 **120 s** and **75 s** after the sign-up.
The count starting at 0 on creation is correct and is not the defect ([[knowledge_context-internals]] §participantCount: no host baseline). The defect is that it never moves afterwards.
The count starting at 0 on creation is correct and is not the defect ([[knowledge_context-internals]] §participantCount: no host baseline). The defect is that it does not move within the session — and, now measured, not on the reconnect that immediately follows it either.
**The "owner offline" explanation does not apply.** In this flow the signer **is** the event's owner, so the counter's only writer is present, connected, and watching the inbox it deposited into. Eventual delivery to an absent owner explains nothing here.
## What is NOT established
## The convergence, now measured, VERIFIED
**Whether the count converges at the next connection is UNKNOWN** — it could not be measured, because [[bug_signup-breaks-the-next-connection]] makes the next connection fail. Do not write it down as converging, and do not treat "it will settle on reload" as a known behaviour: nobody has seen a reload.
The count does converge, but **one connection later than expected**: the first reconnect after the sign-up still reads 0; the count only reaches the true value on the **second** reconnect.
**Which side is at fault is also open** — never written, or written and not re-read. The pair of measurement points laid down for exactly this question (the owner's materializer logging `participantCount` before → after its write, and the display read logging the value as exposed to the render — [[knowledge_context-internals]] §logging) is where a diagnosis starts; the probe read the value as displayed and did not settle the pair.
INFERRED: the first reconnect is the one that drains the deposit and writes the counter, and the write's own value shows up only on the load that follows it, not on the load that triggered it.
**Withdrawal, by contrast, converges in a single connection — VERIFIED.** Why it converges one connection sooner than sign-up does is NOT established: `leaveEvent` deposits its own marker (`depositLeave`) into the same inbox, processed by the same owner materializer as a join ([[knowledge_context-internals]] §participantCount), so the asymmetry is not explained by withdrawal skipping the deposit — that explanation does not survive a look at how `leaveEvent` is wired. Record the one-connection gap as open rather than reach for a mechanism.
## What is still open
Which side is at fault for the extra connection is still open — never written, or written and not re-read. The pair of measurement points laid down for exactly this question (the owner's materializer logging `participantCount` before → after its write, and the display read logging the value as exposed to the render — [[knowledge_context-internals]] §logging) is where a diagnosis starts.
## Reproduce
1. Connect, declare an event (the count shows 0 — correct).
2. Sign up to it; the button reaches « ✓ Je participe ».
3. Stay on the page and watch the count for a couple of minutes. It stays at 0.
3. Stay on the page and watch the count for a couple of minutes — it stays at 0.
4. Reconnect once — still 0. Reconnect a second time — now correct.
Method: `bdd-testing` → [[cookbook_live-probe]]. Watching *after* the confirmation, over a real interval, is what makes this visible at all — every individual step reports success.
Method: `bdd-testing` → [[cookbook_live-probe]]. Watching *after* the confirmation, over a real interval, and across two reconnects, is what makes this visible at all — every individual step reports success.
@@ -1,43 +0,0 @@
---
type: bug
severity: major
opened: 2026-08-16
last_checked: 2026-08-16
summary: After a sign-up, the NEXT connection fails — ensureIdentity() rejects inside the data layer's own inbox processing and AuthGate renders « Connexion impossible ». A provider-side gap, raised with the provider; the app names no document to any call and has nothing to fix here.
---
# Signing up locks the account out of its next connection
## What happens, VERIFIED
Drive the create-and-participate flow (declare an event, sign up to it), then reconnect. `ensureIdentity()` **rejects**, and the app renders its named error panel, « Connexion impossible ». The rejection comes from **inside the data layer's own inbox processing** — a call the app never made.
The message, verbatim:
```
docs.sparqlQuery: refused — the connected user does not hold this document's cap.
Naming a document does not grant access to it
```
**VERIFIED 3 runs out of 3**, including one on a **brand-new origin with a brand-new identity** — so this is not accumulated state from an old wallet ([[caveat_wallet-bloat-hang]] in `bdd-testing` is a different phenomenon and does not explain it).
**VERIFIED**: the symptom and the sequence — a sign-up, then a failing reconnection.
**INFERRED**: that the deposit is what causes it. The sign-up is the only thing between a connection that works and the next one that does not, but nothing observed names the failing document.
## Why there is nothing to work around here
The app deposits through the **published** `inbox.postToDocument(doc, …)` ([[contract_polyfill-surface]]) and **names no document to any call** that could refuse one — it holds no inbox address at all ([[caveat_event-fields-not-persisted]] on why the vestigial `inbox` field must stay unused). The refusal is raised by a query the data layer issues for itself while draining what it was given.
So this is a **provider-side gap**, raised with the provider — not an app-side problem with a clever fix ([[rule_app-uses-sdk-surface-only]] §2: a workaround is a doctrine violation even when it works). There is no app-side recovery either: a rejected `ensureIdentity()` is the contract's own instruction *not to render past it*, because a session that failed looks exactly like an account that owns nothing.
## Reproduce
1. Connect, declare an event, sign up to it (the button reaches « ✓ Je participe »).
2. Reconnect — a new page load through the broker, same identity.
3. The barrier resolves, then the app shows « Connexion impossible » with the message above in the console.
A **fresh origin and a fresh identity** is what separates this defect from accumulated wallet state; run it that way before reporting anything new about it. Method: `bdd-testing` → [[cookbook_live-probe]].
## Blast radius
Every account is one sign-up away from being locked out, and the lock-out is permanent for that identity as far as anything observed goes. It also **blocks measurement of other defects**: whether the participant count converges across connections cannot be established while this holds — [[bug_participant-count-stays-at-zero]].
@@ -27,4 +27,4 @@ The app has **two modes**, both consumed through the `useFestipodData()` hook:
> Mutations are **genuinely persisted** in connected mode: `joinEvent` writes a Participation into its own document and **deposits** into the event's inbox (the deposit is the delivery — no notification is written for the host), `leaveEvent` deletes authoritatively (see [[caveat_participation-deletion]]). Both **reject** rather than returning quietly when they cannot write, and the screen's confirmation follows the write. In local/demo mode they are **no-ops that still show a success toast** — see [[knowledge_context-internals]].
>
> **Per-call honesty is not flow-level honesty.** Every one of those calls tells the truth about itself, and the sign-up flow driven end to end still fails — [[bug_participant-count-stays-at-zero]] and [[bug_signup-breaks-the-next-connection]]. Do not read the paragraph above as "signing up works".
> **Per-call honesty is not flow-level honesty.** Every one of those calls tells the truth about itself, and the sign-up flow driven end to end still does not show the user what happened: the count stays at 0 through the session and the first reconnect, only catching up on the second [[bug_participant-count-stays-at-zero]]. Do not read the paragraph above as "signing up works".
@@ -15,7 +15,7 @@ summary: What is implemented today (event + meeting point lifecycle, profiles, c
- User profile, profile update, profile sharing
- Friends list (connections), another user's profile
> **Signing up is wired step by step, and the flow as a whole does not deliver.** Each step is honest: `joinEvent` persists a Participation and deposits into the event's inbox, where its owner reads it; `leaveEvent` deletes the Participation authoritatively (concept `data-layer`, [[caveat_participation-deletion]]); neither succeeds in silence, and the confirmation the user sees follows the write. Driven end to end in a real browser, the sign-up nonetheless **announces a success it does not obtain**: the user is told they participate, the count never moves ([[bug_participant-count-stays-at-zero]]), and their next connection fails ([[bug_signup-breaks-the-next-connection]]). Honest steps do not add up to an honest flow, and nothing short of exercising the whole thing shows it (concept `bdd-testing`, [[cookbook_live-probe]]). Treat the bullet above as *screens reachable*, not as a working journey. Public discovery — a user seeing another user's public event — works too.
> **Signing up is wired step by step, and the flow as a whole does not yet deliver an honest count.** Each step is honest: `joinEvent` persists a Participation and deposits into the event's inbox, where its owner reads it; `leaveEvent` deletes the Participation authoritatively (concept `data-layer`, [[caveat_participation-deletion]]); neither succeeds in silence, and the confirmation the user sees follows the write. Driven end to end in a real browser, the sign-up nonetheless **announces a success ahead of what it shows**: the user is told they participate, and the count they see stays at 0 through the session and the first reconnect, only catching up on the second ([[bug_participant-count-stays-at-zero]]). Honest steps do not add up to an honest flow, and nothing short of exercising the whole thing shows it (concept `bdd-testing`, [[cookbook_live-probe]]). Treat the bullet above as *screens reachable*, not as a working journey. Public discovery — a user seeing another user's public event — works too.
> **Updating an event is reserved to its declarer**, and the interface says so rather than discovering it late: the edit route is decided by ownership, and the confirmation follows the write instead of preceding it (concept `app-architecture`, [[knowledge_screen-pattern]]). Owner-only is not a policy choice here — it is the only reading the data model allows ([[knowledge_data-scopes-and-discovery]]).
@@ -1,14 +1,16 @@
---
type: caveat
summary: A running `bun run dev` never picks up a refreshed data-layer package — the overlay lives in node_modules, which watchers exclude, so the server keeps serving whatever it loaded at startup. Restart after every `link:polyfill`, and suspect a stale server before suspecting the code.
summary: A running `bun run dev` never picks up a refreshed data-layer package — VERIFIED, not even across a real rebuild (new bundle hash, still the stale dependency). Only a restart serves the fresh copy; suspect a stale server before suspecting the code.
last_checked: 2026-08-16
---
# Pitfall: refreshing the data-layer package does not reach a running dev server
# Pitfall: refreshing the data-layer package never reaches a running dev server, not even across a rebuild
`pnpm run link:polyfill` overlays the local package into `node_modules/@ng-eventually/polyfill/` as real files, and keeps them current. **That is all it does.** A `bun run dev` already running goes on serving the package it loaded at startup, however many times the overlay is rewritten underneath it`node_modules` is excluded by file watchers as a matter of convention, so the change happens in the one place nothing is looking.
`pnpm run link:polyfill` overlays the local package into `node_modules/@ng-eventually/polyfill/` as real files, and keeps them current. **That is all it does.** A `bun run dev` already running goes on serving the package it loaded at startup, however many times the overlay is rewritten underneath it, and however many rebuilds happen in between.
**So: restart `bun run dev` after every refresh of the package.** There is no signal that you needed to; a stale server looks exactly like a current one.
**VERIFIED, controlled sandbox test with this project's own bun.** A dependency resolving to copy A, overlaid with copy B: the running server still serves A at +3 s and +13 s after the overlay. An edit to **application source** then triggers a genuine rebuild — a new bundle hash confirms it — and the rebuilt bundle **still serves A**. Only a restart serves B. So the mechanism is not "the watcher never fires because `node_modules` is excluded" — a rebuild the watcher DOES trigger still carries the stale dependency forward; the server's resolution of that import is pinned at process start, and a rebuild does not re-resolve it.
**So: restart `bun run dev` after every refresh of the package — a rebuild is not a substitute, even a real one.** There is no signal that you needed to; a stale server looks exactly like a current one.
## Why this is worth a leaf
@@ -26,6 +28,6 @@ When a fix does not appear to take effect, or when a hand-run and an automated r
ps -o lstart= -p $(pgrep -f 'bun --hot src/index.ts' | head -1)
```
The same reasoning applies to anything else served out of `node_modules` — the trap is the location, not this package.
Do not reach for "touch a source file to force a rebuild" as a lighter alternative to restarting — it does trigger a real rebuild, and the rebuild still serves the stale dependency. The same reasoning applies to anything else served out of `node_modules` — the trap is the location, not this package.
Related: [[cookbook_live-probe]] (bdd-testing) — a probe answers only for the code the server actually holds, so a stale server invalidates the probe's conclusion, not the product's behaviour.