chore: le resultat de T01.j enregistre le tour 6 et la garantie publiee en avance
Les trois constats comme un seul defaut, l'invariant du registre et ce qui dependait du sens plus lache, verifie plutot que suppose. Et la lecon consignee sans l-expliquer : la garantie publiee au tour precedent n'etait pas tenue, c'est la deuxieme fois que cette tache livre du texte de contrat avant le comportement, donc desormais la garantie et le test qui l'epingle arrivent dans le meme commit. Ma reponse a l'arret dur, donnee sans attendre la question : ce tour etait contenu - un invariant, un module, quatre lecteurs enumeres - et l'appelant en ressort plus petit, pas plus grand. Si une passe retrouve cette famille, je prendrais la scission moi-meme.
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user