chore: le resultat de T01.j enregistre le tour 5 et ce que la relecture a rattrape

Les deux constats de la revue, l'enumeration des trois valeurs d'etat de synchro
ecrite avant la condition, le trou de ma propre suite trouve par mutation (refuser
« unknown » passait tous les tests), et la phrase de contrat que je venais
d'ecrire et qui etait fausse jusqu'a ce que `onState` enregistre un `State`
tardif. Chiffres corriges, et la derive des tours precedents avec eux.
This commit is contained in:
Sylvain Duchesne
2026-09-24 11:09:34 +02:00
parent 2e5bc100b9
commit 2571f8e738
@@ -199,7 +199,11 @@ The recovery design survived this pass and was not reopened. Both findings repro
The fix sits where the rest of the decision already sits: `publish` calls `namedDocumentsArrived()` on every publication, so the question is re-asked at the level rather than remembered from the barrier. The enumeration of the three values, written before the condition, is above under "Round 5". The short form: `"synced"` is authority (zero rows MEANS zero objects - the line that has held through five passes), `"timed-out"` and `"syncing"` are not, and **`"unknown"` PASSES deliberately** because it means "no `State` semantics on this `ng`" and treating it as not-synced is exactly the inert regression rounds 1 and 3 shipped. The fix sits where the rest of the decision already sits: `publish` calls `namedDocumentsArrived()` on every publication, so the question is re-asked at the level rather than remembered from the barrier. The enumeration of the three values, written before the condition, is above under "Round 5". The short form: `"synced"` is authority (zero rows MEANS zero objects - the line that has held through five passes), `"timed-out"` and `"syncing"` are not, and **`"unknown"` PASSES deliberately** because it means "no `State` semantics on this `ng`" and treating it as not-synced is exactly the inert regression rounds 1 and 3 shipped.
**`isPending`, not `isError`**, of the two the contract allows. Nothing failed: the subscribe succeeded, the query opened, no read threw - what is missing is the authority to call the document empty, and `isPending` is this surface's word for a question still open while `isError` has meant "something threw" for five passes. It also needs no machinery: `publish` re-reads the level, so a `State` that lands later and brings data republishes through the ORM's own change signal. Stated rather than hidden: a document whose `State` never arrives leaves that read pending for the session, which is the state the contract already tells applications to handle. **`isPending`, not `isError`**, of the two the contract allows. Nothing failed: the subscribe succeeded, the query opened, no read threw - what is missing is the authority to call the document empty, and `isPending` is this surface's word for a question still open while `isError` has meant "something threw" for five passes. It also needs no machinery of its own: `publish` re-reads the level, so a `State` that lands later and brings data republishes through the ORM's own change signal.
**That last sentence was not true when I first wrote it, and I found it by re-reading my own claim rather than by review.** A late `State` was DISCARDED - `onState` returned early once the bounded fallback had settled the open - so `"timed-out"` stood for ever on a repo that had since synced, and the reader consulting it could never learn better. The fallback means "this open stopped waiting", never "the barrier will not be crossed": the subscription is never dropped, so the `State` still lands. `onState` now records `"synced"` before the settled check. `"timed-out"` -> `"synced"` is the only transition this adds and it goes from less authority to more; the two readers of that value (`index-deposit.noRowsMeansAbsent` and the new arrival check) both want it that way. Pinned by (dd), which fails if the recording moves back below the check.
Stated rather than hidden: a document whose `State` never arrives leaves that read pending for the session, and so does one whose late `State` brings no data - nothing republishes then, because there is no change to signal. The contract already tells applications to handle a `isPending` that does not end.
**A GAP IN MY OWN SUITE, found by mutation rather than by review.** Inverting the condition to treat `"unknown"` as not-arrived - the precise regression the blast-radius note warned about - passed every test. Nothing in the file exercised an `ng` WITHOUT `doc_subscribe`, which is the only way `"unknown"` is reached, and it is what `wallet-fake` does by default and what any consumer lacking the member does. Closed by (cc), which now fails on that inversion. **A GAP IN MY OWN SUITE, found by mutation rather than by review.** Inverting the condition to treat `"unknown"` as not-arrived - the precise regression the blast-radius note warned about - passed every test. Nothing in the file exercised an `ng` WITHOUT `doc_subscribe`, which is the only way `"unknown"` is reached, and it is what `wallet-fake` does by default and what any consumer lacking the member does. Closed by (cc), which now fails on that inversion.
@@ -209,12 +213,12 @@ The fix sits where the rest of the decision already sits: `publish` calls `named
### Gate ### Gate
- `bun test` -> `445 pass / 0 fail`, `Ran 445 tests across 40 files` (baseline `421 pass / 0 fail`, measured before any change; +24 new tests, none deleted or skipped). **Run six times: green every time, 2.78-2.98s.** Two tests were REWRITTEN rather than weakened, each because the design under them changed and the old assertion had become a statement about something that is no longer true: (s) asserted the targeting predicate round 3 proved circular, and now asserts that a re-ask is made for the class it can repair and not for the one it cannot, each half guarding the other against vacuity; (w) asserted that no query is opened under another identity, a rule that left two classes terminal, and now asserts the stronger invariant that replaced it - no snapshot ever claims success for a document the CURRENT holder cannot reach. - `bun test` -> `446 pass / 0 fail`, `Ran 446 tests across 40 files` (baseline `421 pass / 0 fail`, measured before any change; +25 new tests, none deleted or skipped). **Run six times: green every time, 2.95-4.20s.** Two tests were REWRITTEN rather than weakened, each because the design under them changed and the old assertion had become a statement about something that is no longer true: (s) asserted the targeting predicate round 3 proved circular, and now asserts that a re-ask is made for the class it can repair and not for the one it cannot, each half guarding the other against vacuity; (w) asserted that no query is opened under another identity, a rule that left two classes terminal, and now asserts the stronger invariant that replaced it - no snapshot ever claims success for a document the CURRENT holder cannot reach.
- **Mutation-tested, ten ways.** Round 5: removing the arrival check kills (bb) and (t); inverting it to refuse `"unknown"` kills (cc); removing the registry-epoch invalidation kills the new `open-repo` case. Earlier rounds: Removing the raced-edge re-attempt kills (y); removing the publish-time refusal kills (w); removing the snapshot-stability guard kills (aa) and (k); removing the re-attempt entirely kills (r)(s)(v)(w)(z); `Promise.all` in place of `allSettled` kills (x). Round 2's circular gate kills (s)(t)(v) and round 3's holder rule kills (w)(z), so the suite now discriminates against both previously-shipped designs. Removing the abandoned-open settle kills the new `open-repo` test. - **Mutation-tested, eleven ways.** Round 5: removing the arrival check kills (bb) and (t); inverting it to refuse `"unknown"` kills (cc); moving the late-`State` recording back below the settled check kills (dd); removing the registry-epoch invalidation kills the new `open-repo` case. Earlier rounds: Removing the raced-edge re-attempt kills (y); removing the publish-time refusal kills (w); removing the snapshot-stability guard kills (aa) and (k); removing the re-attempt entirely kills (r)(s)(v)(w)(z); `Promise.all` in place of `allSettled` kills (x). Round 2's circular gate kills (s)(t)(v) and round 3's holder rule kills (w)(z), so the suite now discriminates against both previously-shipped designs. Removing the abandoned-open settle kills the new `open-repo` test.
- `bunx tsc --noEmit -p packages/polyfill/tsconfig.json` -> exit 0 - `bunx tsc --noEmit -p packages/polyfill/tsconfig.json` -> exit 0
- `bunx tsc --noEmit -p packages/polyfill/e2e/tsconfig.json` -> exit 0 - `bunx tsc --noEmit -p packages/polyfill/e2e/tsconfig.json` -> exit 0
- `python3 ~/.claude/skills/concept/lint.py` -> 0 ERROR (warnings pre-existing; the new leaf carries none) - `python3 ~/.claude/skills/concept/lint.py` -> 0 ERROR (warnings pre-existing; the new leaf carries none)
- polyfill e2e, run at HEAD: `Polyfill e2e summary: 46 passed, 0 failed, 46 total - batch 3.5 min, slowest cold sync 30s`. **No reviewer has ever run this suite; every count reported for it in this task is mine alone.** 43 was the count before this task; three named-scope checks were added and a fourth removed as non-discriminating. NOTE: the broker login is flaky and independent of this work - three of thirteen runs in this session died with `[e2e broker login] gave up after 45.0s - the wallet-list screen never changed`, at a different step each time, on the login UI before any library code runs. Every such run was re-run to a clean result. - polyfill e2e, run at HEAD: `Polyfill e2e summary: 46 passed, 0 failed, 46 total - batch 3.5 min, slowest cold sync 31s`. **No reviewer has ever run this suite; every count reported for it in this task is mine alone.** 43 was the count before this task; three named-scope checks were added and a fourth removed as non-discriminating. NOTE: the broker login is flaky and independent of this work - three of thirteen runs in this session died with `[e2e broker login] gave up after 45.0s - the wallet-list screen never changed`, at a different step each time, on the login UI before any library code runs. Every such run was re-run to a clean result.
**What the e2e PROVES**, and it is the whole of it: against the real broker, a named scope reads for the holder who owns the document; a holder of nothing is refused out loud with `dataLen: 0` rather than told the document is empty; and a public document opens for that same holder from nothing but its reference, through the barrier's own fetch. **What it does NOT prove**: anything about the re-attempt. Every recovery path - the public class repaired by re-asking, the cost of a doomed re-attempt, and the holder a re-attempt is made as - is measured only in the unit suite, against the `CapRegistry`, which is where that mechanism lives and which the e2e's own header says the broker does not implement. Driving the positive recovery live needs a second actor delivering through `inbox.share`, and belongs in the applicative suite. **What the e2e PROVES**, and it is the whole of it: against the real broker, a named scope reads for the holder who owns the document; a holder of nothing is refused out loud with `dataLen: 0` rather than told the document is empty; and a public document opens for that same holder from nothing but its reference, through the barrier's own fetch. **What it does NOT prove**: anything about the re-attempt. Every recovery path - the public class repaired by re-asking, the cost of a doomed re-attempt, and the holder a re-attempt is made as - is measured only in the unit suite, against the `CapRegistry`, which is where that mechanism lives and which the e2e's own header says the broker does not implement. Driving the positive recovery live needs a second actor delivering through `inbox.share`, and belongs in the applicative suite.
@@ -248,6 +252,7 @@ The fix sits where the rest of the decision already sits: `publish` calls `named
- Every e2e run logs, at connection: `connect failed: docs.sparqlQuery: refused - the connected user does not hold this document's cap ... at readStoreCaps <- restoreOwnCaps <- restoreAndDrain`. That is `assertMayReach`'s own pre-existing message (this work would have produced a different one), so `restoreOwnCaps` is being refused one of the connected user's OWN store documents at connect, and the failure is logged rather than raised. - Every e2e run logs, at connection: `connect failed: docs.sparqlQuery: refused - the connected user does not hold this document's cap ... at readStoreCaps <- restoreOwnCaps <- restoreAndDrain`. That is `assertMayReach`'s own pre-existing message (this work would have produced a different one), so `restoreOwnCaps` is being refused one of the connected user's OWN store documents at connect, and the failure is logged rather than raised.
- The e2e broker login fails intermittently at 45s on the wallet-list screen, before any library code runs - 2 of 8 runs in this session. - The e2e broker login fails intermittently at 45s on the wallet-list screen, before any library code runs - 2 of 8 runs in this session.
- A two-actor journey for "a document is shared with someone while they are watching, and their named `useShapeQuery` converges" belongs in the applicative suite (`e2e/notebook.ts`), beside the journey that already checks "after Alice shares it, the same reference opens it". It is the live form of unit test (r). - A two-actor journey for "a document is shared with someone while they are watching, and their named `useShapeQuery` converges" belongs in the applicative suite (`e2e/notebook.ts`), beside the journey that already checks "after Alice shares it, the same reference opens it". It is the live form of unit test (r).
- Pre-existing and out of scope, surfaced while tracing the arrival check: an in-page session change (`resetOpenedRepos`) clears every document's sync state, so a named scope that was authoritative reads `"unknown"` afterwards and keeps publishing successes against a query the new session never re-subscribed. The observable does not react to a session change at all - which predates this work and is unchanged by it, since before the arrival check nothing consulted that state.
- This observable re-PUBLISHES under whoever is connected at that instant rather than under the identity that subscribed. That part is genuinely pre-existing and out of scope - `publish` has always read the keyring of the current holder. The RE-ATTEMPT no longer does: it is bound to the holder that was refused (`refusedFor`), because issuing a subscribe and a fetch is a new access rather than a re-filtering, and the two must not be lumped together - round 2's Result did lump them, and that was wrong. - This observable re-PUBLISHES under whoever is connected at that instant rather than under the identity that subscribed. That part is genuinely pre-existing and out of scope - `publish` has always read the keyring of the current holder. The RE-ATTEMPT no longer does: it is bound to the holder that was refused (`refusedFor`), because issuing a subscribe and a fetch is a new access rather than a re-filtering, and the two must not be lumped together - round 2's Result did lump them, and that was wrong.
- `docs/internal-contract.md:7` still names `surface/watch-shape.ts`, `surface/read-model.ts` and a `storeRegistry` namespace listing three removed symbols. Out of this task's boundary. - `docs/internal-contract.md:7` still names `surface/watch-shape.ts`, `surface/read-model.ts` and a `storeRegistry` namespace listing three removed symbols. Out of this task's boundary.
- `knowledge_what-a-failed-read-answers-upstream` cites `213338f6`, the checkout T01.h found to be three ORM releases behind. The facts it cites are engine-side rather than ORM-side, but T01.k owns reconciling what a citation's version means. - `knowledge_what-a-failed-read-answers-upstream` cites `213338f6`, the checkout T01.h found to be three ORM releases behind. The facts it cites are engine-side rather than ORM-side, but T01.k owns reconciling what a citation's version means.