diff --git a/.tasks/doing/T01.j-a-failure-must-not-read-as-an-absence.md b/.tasks/doing/T01.j-a-failure-must-not-read-as-an-absence.md index 84fa13b..50006f3 100644 --- a/.tasks/doing/T01.j-a-failure-must-not-read-as-an-absence.md +++ b/.tasks/doing/T01.j-a-failure-must-not-read-as-an-absence.md @@ -211,14 +211,35 @@ Stated rather than hidden: a document whose `State` never arrives leaves that re **`getSyncState`: decided, not left dangling.** It stays INTERNAL, and `knowledge_what-a-failed-read-answers-upstream` no longer offers it - that pointer was at a symbol no caller can import. The reason for not exporting: it has no counterpart upstream at any level, so publishing it would hand an application one more polyfill-era symbol to unlearn, on a surface that is busy removing exactly those; and the distinction it carries is now folded into `useShapeQuery`'s own states, which is where a caller needs it. The leaf now also states the consequence plainly instead of papering over it: a caller using the anchored `docs.sparqlQuery` directly gets zero rows for an unsynced document and has no published way to tell that from an empty one - the same collapse upstream has. +### ROUND 6 - the registry was what lied, and it now has one meaning + +Three findings, one defect under three faces: **state recorded as true that is not, with no way to correct itself.** Two of the three were in `open-repo.ts`. Five rounds had gone into the caller; the bookkeeper was the problem, and the repair is there. + +**The invariant, established and written into the module header.** `held` is the ONE record of openness - *this session holds a live bootstrap subscription to this document*. Beside it stood `opened`, meaning "we once called `doc_subscribe` and stopped waiting", and that looser meaning is what lied three ways. `opened` is DELETED; two records of one fact can disagree, and this pair did. What else relied on the looser meaning, checked rather than assumed: `opened` had exactly one reader (`openRepoUnguarded`'s own short-circuit), `held` one (the reset's teardown), and `getSyncState` two outside the module (`index-deposit.noRowsMeansAbsent` and `useShapeQuery`) - both of which the change moves toward the safe reading. + +The three consequences, now true by construction: +- **a subscribe the broker REFUSED records nothing.** `subscribe.ts` reports a failed establish through `onSetupFailed` precisely so a caller can retry; `open-repo` never passed it, waited out the 8s fallback, and then recorded the repo as open - so `refetch()` issued zero new subscribes and the read had no exit at all. The channel is wired, the failure marks `"timed-out"`, and nothing is recorded. (Finding 3.) +- **a reset DOWNGRADES the sync state of every document it takes a subscription from**, instead of clearing it. Clearing left them reading `"unknown"`, which is deliberately authoritative because on an `ng` with no `doc_subscribe` there is no barrier to have missed - so a read correctly withheld came back claiming success against a query the new session never re-subscribed. (Finding 1.) +- **`"unknown"` now means one thing only**: no barrier was ever expected here. Never "we forgot". + +**Finding 2, the one that was in the caller.** When a named document has not arrived, `publish` now PUBLISHES "the question is still open" rather than returning and leaving whatever snapshot stood. Returning left a refusal that had already been LIFTED on display, so an application read "you do not hold this document's cap" about a document it now holds - a false reason, and `isError` where round 5 decided the answer is `isPending`. + +**FALSIFIED BY THIS PASS, and corrected:** the guarantee published last round at `contract_polyfill-surface.md:196` ("a named document that has never synced now stays PENDING") was not delivered - Finding 1 bypassed it. It is now delivered and pinned by (ee). I had filed that bypass as pre-existing, which was wrong: the BYPASS was new, because before round 5 nothing consulted that state. This is the second time this task shipped contract text ahead of the behaviour, and the lesson is recorded rather than explained away: the guarantee and the test that pins it land in the same commit from here. + +**One caller-visible consequence, published rather than left implicit:** an in-page re-login returns a named scope to PENDING until its documents are confirmed under the new session, `refetch()` being what confirms them. It is the only case where a read that had answered stops answering, and it does not arise on an ordinary page load. + +**Two fixtures carried the old meaning and are corrected with the reason beside them.** This file's fake broker restarted its id counter inside every `makeFake`, so two tests minted the same nuri and aliased each other in the process-global registry - invisible while a reset cleared sync state, immediate once it downgraded it; the counter is now monotonic, which is `wallet-fake`'s own recorded lesson at this file's scale. And the cold-read suite asserted `"unknown"` for a note this page never opened; `"timed-out"` is now what "this page has not confirmed it" says, and the test notes that this particular value is a harness artefact (`reloadPage` resets inside one process, where a browser reload would start with an empty module). + +**On the hard stop, and I am answering it rather than waiting to be asked.** I judged this round CONTAINED and did not split: the fix is one invariant in one module with four readers, all enumerated above, and it made the caller smaller rather than larger - no sixth condition was added, one was removed. If a further pass finds this family again I would take the split myself, because the evidence would then be that the registry needs its own reviews rather than that this one needs another patch. + ### Gate -- `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, 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. +- `bun test` -> `449 pass / 0 fail`, `Ran 449 tests across 40 files` (baseline `421 pass / 0 fail`, measured before any change; +28 new tests, none deleted or skipped). **Run six times: green every time, 3.21-3.27s.** 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, fifteen ways.** Round 6: removing the reset downgrade kills (ee), the abandoned-open test and the cold-read assertion; ignoring `onSetupFailed` kills the refused-subscribe test; restoring a second record of openness kills the refused-subscribe test; making `publish` return instead of publishing pending kills (ff). 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/e2e/tsconfig.json` -> exit 0 - `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 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. +- polyfill e2e, run at HEAD: `Polyfill e2e summary: 46 passed, 0 failed, 46 total - batch 3.5 min, slowest cold sync 30s`. **The reviewer ran it independently this round and confirmed both the count and what it does and does not prove.** Earlier in this task every count reported for it was 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.