From ff26f26e607f7ca667d1b5964f48256921bc9243 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Sun, 16 Aug 2026 22:40:08 +0200 Subject: [PATCH] Close the fixed blocker, and record what withdrawal converging faster does NOT mean Six variations against the real application, one thing varied at a time: the sign-up-breaks-the-next-connection defect is gone. New identity, immediate reload or delayed; an identity already used for several cycles; and the very identity poisoned by the pre-fix code, which now reconnects cleanly three times out of three. So nothing needed healing -- the fix does not merely stop writing the bad state, it makes what was already written harmless. The leaf is deleted rather than graduated: its reproduction method, the stale-server trap it led to, and "a provider gap is not worked around here" all live elsewhere already. The count defect stays open, with its shape now known: it does converge across connections, but takes one more than expected -- the first reconnect still shows zero, the second shows the value. And a correction worth keeping. The explanation offered for why withdrawal converges in a single connection -- that it writes straight to the owner's document instead of depositing -- is contradicted by the code: it deposits its own marker into the same inbox, drained by the same owner routine, "symmetric" by the code's own comment. The asymmetry is real and recorded as verified; its cause is recorded as unknown rather than filled with a plausible story. Same mechanism, same inbox, two connections against one, is a lead worth having honestly. The dev-server caveat is upgraded from inferred to verified, and it is worse than it read: an edit to application source triggers a genuine rebuild, new bundle hash and all, and that rebuild still carries the stale dependency. Only a restart works, and "touch a file to force a rebuild" is now explicitly ruled out as a substitute. --- .../bdd-testing/cookbook_live-probe.md | 2 +- .project/concepts/data-layer/_overview.md | 5 +-- .../bug_participant-count-stays-at-zero.md | 25 +++++++---- .../bug_signup-breaks-the-next-connection.md | 43 ------------------- .../data-layer/knowledge_data-modes.md | 2 +- .../functional-domain/knowledge_roadmap.md | 2 +- ...at_polyfill-overlay-needs-a-dev-restart.md | 12 +++--- 7 files changed, 28 insertions(+), 63 deletions(-) delete mode 100644 .project/concepts/data-layer/bug_signup-breaks-the-next-connection.md diff --git a/.project/concepts/bdd-testing/cookbook_live-probe.md b/.project/concepts/bdd-testing/cookbook_live-probe.md index e2e2073..d1b5655 100644 --- a/.project/concepts/bdd-testing/cookbook_live-probe.md +++ b/.project/concepts/bdd-testing/cookbook_live-probe.md @@ -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]]). diff --git a/.project/concepts/data-layer/_overview.md b/.project/concepts/data-layer/_overview.md index 57778ed..493a815 100644 --- a/.project/concepts/data-layer/_overview.md +++ b/.project/concepts/data-layer/_overview.md @@ -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) diff --git a/.project/concepts/data-layer/bug_participant-count-stays-at-zero.md b/.project/concepts/data-layer/bug_participant-count-stays-at-zero.md index 4a82631..5779494 100644 --- a/.project/concepts/data-layer/bug_participant-count-stays-at-zero.md +++ b/.project/concepts/data-layer/bug_participant-count-stays-at-zero.md @@ -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. diff --git a/.project/concepts/data-layer/bug_signup-breaks-the-next-connection.md b/.project/concepts/data-layer/bug_signup-breaks-the-next-connection.md deleted file mode 100644 index dde3293..0000000 --- a/.project/concepts/data-layer/bug_signup-breaks-the-next-connection.md +++ /dev/null @@ -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]]. diff --git a/.project/concepts/data-layer/knowledge_data-modes.md b/.project/concepts/data-layer/knowledge_data-modes.md index 2f9414b..9d41796 100644 --- a/.project/concepts/data-layer/knowledge_data-modes.md +++ b/.project/concepts/data-layer/knowledge_data-modes.md @@ -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". diff --git a/.project/concepts/functional-domain/knowledge_roadmap.md b/.project/concepts/functional-domain/knowledge_roadmap.md index 6d54f60..a7b9b23 100644 --- a/.project/concepts/functional-domain/knowledge_roadmap.md +++ b/.project/concepts/functional-domain/knowledge_roadmap.md @@ -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]]). diff --git a/.project/concepts/tech-stack/caveat_polyfill-overlay-needs-a-dev-restart.md b/.project/concepts/tech-stack/caveat_polyfill-overlay-needs-a-dev-restart.md index efef6ff..36fba0b 100644 --- a/.project/concepts/tech-stack/caveat_polyfill-overlay-needs-a-dev-restart.md +++ b/.project/concepts/tech-stack/caveat_polyfill-overlay-needs-a-dev-restart.md @@ -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.