docs: le contrat descend a ce que le code tient vraiment, et T01.j est scindee

La sixieme revue a refute la reparation du tour 6 et a trouve la famille sur
laquelle nous nous etions engages a scinder. `held` n'est pas le registre unique :
il y en a trois, et celui que personne n'avait compte est `fanOuts` dans
`subscribe.ts`. `configure()`, qui est PUBLIE, vide les auditeurs sans le dire a
`open-repo` ; et `held.set()` s'execute avant meme que `establish` atteigne
`doc_subscribe`. `held` veut donc toujours dire « on a demande » - le sens plus
lache pour lequel j'avais supprime `opened`. J'ai remplace l'un des deux registres
qui se contredisaient ; il y en avait trois.

Aucun code touche ici. Le defaut de registre part dans T01.m, avec ses deux echecs
prouves comme tests d'acceptation.

Le contrat dit desormais ce que le code tient : la moitie « le document a-t-il
repondu » vaut DANS UNE SESSION ordinaire, et pas apres un second `configure(...)`
ni quand le courtier refuse un abonnement tardivement. Les deux situations sont
nommees en non-garanties avec leur cause et le renvoi vers T01.m. La moitie
« peut-on joindre le document » n'a pas cette reserve : elle est verifiee deux fois
contre un vrai courtier, et la retirer ramenerait un succes vide silencieux
partout, ce qui est pire qu'une garantie partielle honnetement enoncee.

Pas de changement de version : aucun comportement publie ne bouge, seule la
justesse de sa description.

Et une faute de compte-rendu consignee, parce que c'est un motif : trois fois j'ai
affirme qu'un changement avait reduit ou simplifie quelque chose, trois fois la
mesure disait le contraire. Regle desormais mecanique - toute affirmation
structurelle porte son `wc -l` ou son `--numstat` dans la meme phrase, sinon elle
ne s'ecrit pas.
This commit is contained in:
Sylvain Duchesne
2026-09-24 11:55:12 +02:00
parent 4c8efbc10a
commit 52ac4d974e
3 changed files with 44 additions and 8 deletions
@@ -40,6 +40,6 @@ Three rules follow, and all three are load-bearing:
2. **An empty document is a SUCCESS holding zero objects.** The repair for (1) must never be bought by making a real emptiness into an error; that only moves the lie. An empty document, an empty scope, a document nobody opened an inbox on, a document in a public store that exposes no key - each is an answer, and each stays one. 2. **An empty document is a SUCCESS holding zero objects.** The repair for (1) must never be bought by making a real emptiness into an error; that only moves the lie. An empty document, an empty scope, a document nobody opened an inbox on, a document in a public store that exposes no key - each is an answer, and each stays one.
3. **The unsynced/empty collision is closed by the barrier, and where the barrier was not crossed the read says so rather than answering.** Crossing it is what makes a zero-row answer mean something (`emulated-verifier/open-repo.ts`, which keeps `synced` and `timed-out` deliberately apart). A reactive read over documents it NAMES consults that distinction and stays PENDING when the barrier was not reached - "I have no authoritative answer" rather than "there is nothing". Turning it into a rejection would be inventing a third outcome the engine does not have, and answering a success would be the collapse this whole leaf is about. 3. **The unsynced/empty collision is closed by the barrier, and where the barrier was not crossed the read says so rather than answering.** Crossing it is what makes a zero-row answer mean something (`emulated-verifier/open-repo.ts`, which keeps `synced` and `timed-out` deliberately apart). A reactive read over documents it NAMES consults that distinction and stays PENDING when the barrier was not reached - "I have no authoritative answer" rather than "there is nothing". Turning it into a rejection would be inventing a third outcome the engine does not have, and answering a success would be the collapse this whole leaf is about.
**What an application can see of this, exactly.** `useShapeQuery` folds it into its own states, and that is the only published channel: a named scope whose barrier was not crossed does not reach `isSuccess`. `inbox.readSyncedForDocument` is the same guarantee on the inbox path. `getSyncState` is INTERNAL and is deliberately not published - this leaf offered it until 2026-09-24, which was a pointer at something no caller can import. It stays internal because it has no counterpart upstream at any level: publishing it would hand an application a polyfill-era symbol to unlearn at migration, on a surface that is busy removing exactly those. The consequence to state plainly rather than paper over: 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** - that is the collapse upstream has too, and it is why the reactive read is the one that carries the distinction. **What an application can see of this, exactly.** `useShapeQuery` folds it into its own states, and that is the only published channel: a named scope whose barrier was not crossed does not reach `isSuccess` - **within one ordinary session**. Two situations break it, both known defects and both carried as acceptance tests by `T01.m` ("there must be one answer to whether a document is open"): a second `configure(...)`, which tears down the subscription layer without telling the open registry, and a broker refusing a subscription after this package has stopped waiting. In both, a document recorded as open is not, and the read answers an empty success. The REACHABILITY half is unaffected. `inbox.readSyncedForDocument` is the same guarantee on the inbox path. `getSyncState` is INTERNAL and is deliberately not published - this leaf offered it until 2026-09-24, which was a pointer at something no caller can import. It stays internal because it has no counterpart upstream at any level: publishing it would hand an application a polyfill-era symbol to unlearn at migration, on a surface that is busy removing exactly those. The consequence to state plainly rather than paper over: 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** - that is the collapse upstream has too, and it is why the reactive read is the one that carries the distinction.
Related: `knowledge_what-a-read-scope-is-upstream` (what a read request can SAY, including the `[]` that means the empty set and the `[]` that means no filtering), and `polyfill-surface/contract_polyfill-surface` `## Guarantees` for the caller-facing form of all of this. Related: `knowledge_what-a-read-scope-is-upstream` (what a read request can SAY, including the `[]` that means the empty set and the `[]` that means no filtering), and `polyfill-surface/contract_polyfill-surface` `## Guarantees` for the caller-facing form of all of this.
@@ -193,7 +193,7 @@ Where a call must first find out whether something already exists — a document
**A reactive read says "nothing" and "I could not find out" differently.** `useShapeQuery` answers in three states and only two of them are answers about your data: `isPending` while the question is still open, `isSuccess` once it has been answered, `isError` when it could not be. An empty `data` under `isSuccess` means this scope holds nothing of that shape — the distinction the surface exists for, and the one thing it will never do is publish that snapshot for a read that did not happen. And because an observable cannot take back a list a subscriber has already rendered, `data` under `isError` keeps the **last read that answered** rather than emptying — so an empty `data` is never handed to you as a failure's answer. Read the load state before `data`: **a rejection means "unknown", never "absent"** here too. **A reactive read says "nothing" and "I could not find out" differently.** `useShapeQuery` answers in three states and only two of them are answers about your data: `isPending` while the question is still open, `isSuccess` once it has been answered, `isError` when it could not be. An empty `data` under `isSuccess` means this scope holds nothing of that shape — the distinction the surface exists for, and the one thing it will never do is publish that snapshot for a read that did not happen. And because an observable cannot take back a list a subscriber has already rendered, `data` under `isError` keeps the **last read that answered** rather than emptying — so an empty `data` is never handed to you as a failure's answer. Read the load state before `data`: **a rejection means "unknown", never "absent"** here too.
**A document you NAME in `graphs` is reached before the read opens, since `1.0.0-dev.9`.** Naming a document in a scope is the same act as anchoring `docs.sparqlQuery` on it, and it is now answered the same way: the key is asked of a public store if you do not already hold it, the document is refused out loud if you hold no key for it, and its repo is synced before the question is put. A named document you cannot read publishes `isError`, where until this version it reached the engine, came back, and was emptied by the per-document filter into `isSuccess` with `data: []` - the one snapshot this surface reserves for "this scope holds nothing". A named document that has never synced published that same snapshot for a document that is not empty; it now stays **PENDING** instead - the sync barrier is consulted on every publication, and a named document whose barrier was not crossed is not an answer. Nothing failed there, so it is not `isError`: the subscribe succeeded and the read did not throw, what is missing is the authority to say the document is empty. If the document later syncs and brings data, the read answers by itself. If it never does, the read stays pending for that session - handle a `isPending` that does not end, as you already must for a shape query the engine refuses. **Naming SEVERAL documents fails as a whole** when one of them cannot be reached: a short list is the same lie as an empty one, so nothing is answered rather than half of it. What is NOT touched, and each stays a success: `graphs: "did:ng:i"` or `""` (you named no document - the per-document filter is the whole of the answer there, and an empty result means you hold nothing of that shape), `graphs: []` (no objects, by NextGraph's own definition), and a named document you can read that holds nothing of the shape. **A document you NAME in `graphs` is reached before the read opens, since `1.0.0-dev.9`.** Naming a document in a scope is the same act as anchoring `docs.sparqlQuery` on it, and it is now answered the same way: the key is asked of a public store if you do not already hold it, the document is refused out loud if you hold no key for it, and its repo is synced before the question is put. A named document you cannot read publishes `isError`, where until this version it reached the engine, came back, and was emptied by the per-document filter into `isSuccess` with `data: []` - the one snapshot this surface reserves for "this scope holds nothing". A named document that has never synced published that same snapshot for a document that is not empty; it now stays **PENDING** instead **within one session** - the sync barrier is consulted on every publication, and a named document whose barrier was not crossed is not an answer. **That half is narrower than the sentence above, and the two holes in it are named under `## Non-guarantees` - read them before relying on it.** Nothing failed there, so it is not `isError`: the subscribe succeeded and the read did not throw, what is missing is the authority to say the document is empty. If the document later syncs and brings data, the read answers by itself. If it never does, the read stays pending for that session - handle a `isPending` that does not end, as you already must for a shape query the engine refuses. **Naming SEVERAL documents fails as a whole** when one of them cannot be reached: a short list is the same lie as an empty one, so nothing is answered rather than half of it. What is NOT touched, and each stays a success: `graphs: "did:ng:i"` or `""` (you named no document - the per-document filter is the whole of the answer there, and an empty result means you hold nothing of that shape), `graphs: []` (no objects, by NextGraph's own definition), and a named document you can read that holds nothing of the shape.
**And it converges by itself.** A named scope refused this way is not a dead view. Whether you may reach the documents you named is re-read on every publication rather than decided once, so a key arriving turns the refusal into the document through the same channel that already re-published a filtered read - and a scope whose barrier failed outright is attempted again on that signal. The ordinary sequences all converge with no call from the application: a page whose components subscribe before the connection work has restored its keys, a document shared with you while you are looking at the screen, a broker that hiccupped once and healed, and a key arriving in the same instant the read was deciding. One consequence to design for: an interface that renders `isError` will briefly show it where it used to show an empty success, so branch on the load state rather than on `data` alone. Nothing here polls, so a scope refused in a session whose keyring never moves again stays `isError` until your own `refetch()` - which is what `isError` has always meant here, and what it means for every other failure this call reports. **And it converges by itself.** A named scope refused this way is not a dead view. Whether you may reach the documents you named is re-read on every publication rather than decided once, so a key arriving turns the refusal into the document through the same channel that already re-published a filtered read - and a scope whose barrier failed outright is attempted again on that signal. The ordinary sequences all converge with no call from the application: a page whose components subscribe before the connection work has restored its keys, a document shared with you while you are looking at the screen, a broker that hiccupped once and healed, and a key arriving in the same instant the read was deciding. One consequence to design for: an interface that renders `isError` will briefly show it where it used to show an empty success, so branch on the load state rather than on `data` alone. Nothing here polls, so a scope refused in a session whose keyring never moves again stays `isError` until your own `refetch()` - which is what `isError` has always meant here, and what it means for every other failure this call reports.
@@ -253,6 +253,13 @@ Where a call must first find out whether something already exists — a document
**TWO COPIES OF THIS PACKAGE IN ONE APPLICATION MISBEHAVE SILENTLY, and nothing here can detect it.** What this package remembers it keeps in the package itself, not in any handle you hold: which documents are subscribed and who is listening for them, which identity the session settled on, which documents are open. None of that is shared between two copies. An application that ends up with two therefore runs two of everything — a document subscribed through one copy is invisible to the other, so changes to it simply never arrive; and the identity settled in one is not the identity the other acts as, so the same call writes as one user or the other depending on which copy it reached. Nothing raises, nothing warns, and no call can report it: from every entry point a second copy is indistinguishable from the first, and the symptoms surface far from the cause as missing updates and writes attributed to the wrong person. This is a packaging property, not a behaviour to code around — **declare this package once, as one dependency of the application**, and let your own code and every library built on it resolve to that copy. A library that builds on this package should declare it a *peer* rather than a dependency for exactly this reason, so that the application remains the one place it is named. **TWO COPIES OF THIS PACKAGE IN ONE APPLICATION MISBEHAVE SILENTLY, and nothing here can detect it.** What this package remembers it keeps in the package itself, not in any handle you hold: which documents are subscribed and who is listening for them, which identity the session settled on, which documents are open. None of that is shared between two copies. An application that ends up with two therefore runs two of everything — a document subscribed through one copy is invisible to the other, so changes to it simply never arrive; and the identity settled in one is not the identity the other acts as, so the same call writes as one user or the other depending on which copy it reached. Nothing raises, nothing warns, and no call can report it: from every entry point a second copy is indistinguishable from the first, and the symptoms surface far from the cause as missing updates and writes attributed to the wrong person. This is a packaging property, not a behaviour to code around — **declare this package once, as one dependency of the application**, and let your own code and every library built on it resolve to that copy. A library that builds on this package should declare it a *peer* rather than a dependency for exactly this reason, so that the application remains the one place it is named.
**The "has it arrived" half of a named scope is NOT reliable across two situations, and both are known defects rather than design.** The guarantee under `## Guarantees` - that a named document which has not synced keeps the read pending instead of answering an empty success - holds inside one ordinary session. It does **not** hold in these two, where a named scope can still publish `isSuccess` with `data: []` for a document this session never heard from:
- **After a second `configure(...)`.** That call tears down the subscription layer's listeners and does not tell the part of this package that records which documents are open. A document synced before it is still recorded as synced, so nothing re-subscribes it and the read answers from a subscription that no longer exists. `refetch()` does not recover it. Calling `configure` once, at start-up, avoids this entirely - which is what the deployment requirements already ask for.
- **When the broker refuses a subscription late** - after this package has stopped waiting for the document's first push. The document stays recorded as open and the retry does not subscribe again.
Both come from the same cause: **this package keeps more than one record of "is this document open in this session", and they can disagree.** It is tracked in this repository as `T01.m` ("there must be one answer to whether a document is open"), which carries both failures as its acceptance tests. Until it lands, treat the pending state as a real improvement on the empty success it replaced and not as a promise that an empty success can no longer appear. **The REACHABILITY half of the same barrier is unaffected and is verified against a live broker**: a document you hold no key for is refused out loud rather than emptied out of the result, and a public document opens from a bare reference.
**No unfiltered read through `useShape`.** Members that yield items are filtered and mutations pass through; anything else throws. A document reached through that view alone, read nowhere else first, does not appear. **No unfiltered read through `useShape`.** Members that yield items are filtered and mutations pass through; anything else throws. A document reached through that view alone, read nowhere else first, does not appear.
**`getObjects` does not fetch a public store's key for you either**, for the same reason as the two shape reads below: it goes through the same ORM query and spends no round-trip asking. Read the document once by naming it — `docs.sparqlQuery` anchored on it — and it appears. **`getObjects` does not fetch a public store's key for you either**, for the same reason as the two shape reads below: it goes through the same ORM query and spends no round-trip asking. Read the document once by naming it — `docs.sparqlQuery` anchored on it — and it appears.
@@ -280,7 +287,7 @@ What each level means here, in this package's own terms:
`1.0.0-dev.9` is a **major**, and all of it is one rule: **a read that could not reach its document now FAILS, where it used to answer as though the document were empty.** Nothing is added, nothing is renamed, nothing is removed from the entry - three existing calls narrow. `1.0.0-dev.9` is a **major**, and all of it is one rule: **a read that could not reach its document now FAILS, where it used to answer as though the document were empty.** Nothing is added, nothing is renamed, nothing is removed from the entry - three existing calls narrow.
- **`useShapeQuery` publishes `isError` for a document you NAME and cannot read.** Until this version a document named in `scope.graphs` was handed straight to the engine; whatever came back was filtered per document by what you hold, and a document you hold no key for was emptied out of the result. The snapshot was `isSuccess` with `data: []` - indistinguishable, byte for byte, from the scope genuinely holding nothing. It now asks the public store for the key, refuses out loud if there is none to be had, and crosses the sync barrier first, so the same read answers `isError`. **Handle `isError` on a named scope**; an interface that only branched on `isPending` / `isSuccess` will show its success state with nothing in it. Naming several documents fails as a whole when one of them fails. - **`useShapeQuery` publishes `isError` for a document you NAME and cannot read.** Until this version a document named in `scope.graphs` was handed straight to the engine; whatever came back was filtered per document by what you hold, and a document you hold no key for was emptied out of the result. The snapshot was `isSuccess` with `data: []` - indistinguishable, byte for byte, from the scope genuinely holding nothing. It now asks the public store for the key, refuses out loud if there is none to be had, and crosses the sync barrier first, so the same read answers `isError`. **Handle `isError` on a named scope**; an interface that only branched on `isPending` / `isSuccess` will show its success state with nothing in it. Naming several documents fails as a whole when one of them fails.
- **A named document that has not SYNCED keeps the read pending**, where it used to publish the same empty success. Reaching a document and hearing back from it are two questions, and only the first was being asked. An interface must therefore expect a named scope to sit in its loading state while a document it named is not answering, rather than reading an empty success as "nothing there". The same applies after an in-page re-login, which returns a named scope to pending until its documents are confirmed under the new session - see `## Guarantees`. - **A named document that has not SYNCED keeps the read pending**, where it used to publish the same empty success. Reaching a document and hearing back from it are two questions, and only the first was being asked. An interface must therefore expect a named scope to sit in its loading state while a document it named is not answering, rather than reading an empty success as "nothing there". The same applies after an in-page re-login, which returns a named scope to pending until its documents are confirmed under the new session - see `## Guarantees`. **This half is partial: two situations where it does not hold are named under `## Non-guarantees`, and they are defects rather than design.** The reachability half beside it has no such qualification.
- **A named scope now costs round-trips it did not cost before**, once per named document per session: the key fetch, and the subscribe that syncs the repo. The opens run together, so several named documents cost the slowest wait rather than the sum. `graphs: "did:ng:i"` is unchanged and spends nothing. - **A named scope now costs round-trips it did not cost before**, once per named document per session: the key fetch, and the subscribe that syncs the repo. The opens run together, so several named documents cost the slowest wait rather than the sum. `graphs: "did:ng:i"` is unchanged and spends nothing.
- **A refused named scope RECOVERS on its own.** Reachability is re-read on every publication rather than remembered, and a scope whose barrier failed is attempted again when the keyring moves - including when the move happened while the read was still deciding. Without that a refusal would be terminal, and trading a silent empty answer for a permanently stuck error would have been no improvement at all. Expect to see `isError` briefly on a page that subscribes before its keys are restored. There is no polling: in a session whose keyring never moves again, `refetch()` is the recovery, as it is for every other failure this call reports. - **A refused named scope RECOVERS on its own.** Reachability is re-read on every publication rather than remembered, and a scope whose barrier failed is attempted again when the keyring moves - including when the move happened while the read was still deciding. Without that a refusal would be terminal, and trading a silent empty answer for a permanently stuck error would have been no improvement at all. Expect to see `isError` briefly on a page that subscribes before its keys are restored. There is no polling: in a session whose keyring never moves again, `refetch()` is the recovery, as it is for every other failure this call reports.
- **`inbox.postToDocument`, `inbox.readForDocument` and `inbox.readSyncedForDocument` reject where they used to report "no inbox".** Resolving a document's inbox address reads that document, and when the read could not happen - the broker could not answer for it, or its public store could not be asked for the key - all three used to render the breakdown as the document's owner having opened nothing: a refusal on the deposit side, an empty list on the read side. They now reject. A document that genuinely has no inbox is unchanged, and still answers exactly as before. - **`inbox.postToDocument`, `inbox.readForDocument` and `inbox.readSyncedForDocument` reject where they used to report "no inbox".** Resolving a document's inbox address reads that document, and when the read could not happen - the broker could not answer for it, or its public store could not be asked for the key - all three used to render the breakdown as the document's owner having opened nothing: a refusal on the deposit side, an empty list on the read side. They now reject. A document that genuinely has no inbox is unchanged, and still answers exactly as before.
@@ -112,7 +112,7 @@ Rejected, with the deciding reason for each:
## Result ## Result
Status: done. `1.0.0-dev.9`, MAJOR. Three commits: `75313f7` (the behaviour change), `b0eec7b` (the contract, the knowledge leaf and the narrow `internal-contract` repair), `534d8f9` (two guard tests). Status: done, and SPLIT. `1.0.0-dev.9`, MAJOR. What this task delivers is a real, verified, PARTIAL improvement: a failure no longer reads as an absence at four call sites and on the reachability half of the reactive read, and the one half that is still incomplete - "has this document arrived" - is documented as incomplete rather than promised. The registry defect underneath it is `.tasks/todo/T01.m-one-answer-to-whether-a-document-is-open.md`, which carries both proved failures as acceptance tests. Seven rounds, six independent adversarial reviews; round 7 is the split and changed no code.
### What each call site does now when the cap cannot be obtained ### What each call site does now when the cap cannot be obtained
@@ -156,7 +156,7 @@ Fix, round 2: the keyring signal RE-OPENS a scope the barrier refused, gated on
**Finding 1, blocking and STRUCTURAL - the gate was circular.** `namedDocumentsReachable()` tested `mayReach`, i.e. the cap is HELD. For a named document in a PUBLIC store the only channel that can ever file that cap is `fetchReadCap` - the very call that had failed and set the flag. The gate waited on an answer that only the action it gated could produce, so that whole class was unrecoverable however many caps arrived. Reproduced as `use-shape-query.test.ts` (v): the broker hiccups on the `exposedReadCap` read, the fetch rejects, the broker heals, an ordinary cap arrival follows, and the view never re-asks. **Finding 1, blocking and STRUCTURAL - the gate was circular.** `namedDocumentsReachable()` tested `mayReach`, i.e. the cap is HELD. For a named document in a PUBLIC store the only channel that can ever file that cap is `fetchReadCap` - the very call that had failed and set the flag. The gate waited on an answer that only the action it gated could produce, so that whole class was unrecoverable however many caps arrived. Reproduced as `use-shape-query.test.ts` (v): the broker hiccups on the `exposedReadCap` read, the fetch rejects, the broker heals, an ordinary cap arrival follows, and the view never re-asks.
**The readiness question, answered rather than patched.** There are exactly two ways this barrier can refuse, and the memo already tells them apart. The store could not be ASKED: a failure is deliberately not memoised (an earlier fix in this repo), so a re-attempt genuinely re-asks the network - which IS the repair for this class, not a cost to avoid. Or the store ANSWERED and there is no cap to be had: an absence is memoised, so a re-attempt asks nobody, re-runs the barrier against what is already known, and republishes the same refusal for the price of one snapshot - and it stops the day the cap arrives from elsewhere, which is the only thing that repairs THAT class. So the right signal is not a cleverer predicate, it is **attempting again**: cheap for the class it cannot help, correct for the class it can, self-limiting because the moment an attempt succeeds there is a query again. `namedDocumentsReachable()` is DELETED. **CORRECTED: the claim that this made the module smaller than round 2 left it was FALSE** - measured, `use-shape-query.ts` went 534 -> 563 lines and 203 -> 204 non-comment across that round, and is 664 / 232 now. It has grown at every round. One predicate left; the reasoning that had to be written down beside the code grew more than it. Pinned by (v) and, for the cost, by (s), which now asserts both halves (the re-attempt ran, and it spent no round-trip) so it cannot pass vacuously. **The readiness question, answered rather than patched.** There are exactly two ways this barrier can refuse, and the memo already tells them apart. The store could not be ASKED: a failure is deliberately not memoised (an earlier fix in this repo), so a re-attempt genuinely re-asks the network - which IS the repair for this class, not a cost to avoid. Or the store ANSWERED and there is no cap to be had: an absence is memoised, so a re-attempt asks nobody, re-runs the barrier against what is already known, and republishes the same refusal for the price of one snapshot - and it stops the day the cap arrives from elsewhere, which is the only thing that repairs THAT class. So the right signal is not a cleverer predicate, it is **attempting again**: cheap for the class it cannot help, correct for the class it can, self-limiting because the moment an attempt succeeds there is a query again. `namedDocumentsReachable()` is DELETED. **CORRECTED: the claim that this made the module smaller than round 2 left it was FALSE** - measured, `use-shape-query.ts` went 534 -> 563 lines and 203 -> 204 non-comment across that round, and is **727 lines** now (`wc -l`). It has grown at every round. One predicate left; the reasoning that had to be written down beside the code grew more than it. Pinned by (v) and, for the cost, by (s), which now asserts both halves (the re-attempt ran, and it spent no round-trip) so it cannot pass vacuously.
**Finding 2 - the re-open performed an access under the wrong identity, and my "pre-existing" defence was wrong in kind.** The reviewer is right: re-publishing RE-FILTERS an existing query, while a re-open NEWLY ISSUES a subscribe and a public-store fetch for a document that subscriber named. That is a new action taken on behalf of whoever happens to be connected, not an old one resurfacing, and the end state was `{isSuccess: true, data: []}` for a document the subscriber cannot read - this task's own defect, through the repair's door. Fixed by binding the refusal to a holder: `refusedFor` records WHOSE barrier failed, and a re-attempt is made only while that holder is still the connected one. Pinned by (w), which fails without the binding. **Finding 2 - the re-open performed an access under the wrong identity, and my "pre-existing" defence was wrong in kind.** The reviewer is right: re-publishing RE-FILTERS an existing query, while a re-open NEWLY ISSUES a subscribe and a public-store fetch for a document that subscriber named. That is a new action taken on behalf of whoever happens to be connected, not an old one resurfacing, and the end state was `{isSuccess: true, data: []}` for a document the subscriber cannot read - this task's own defect, through the repair's door. Fixed by binding the refusal to a holder: `refusedFor` records WHOSE barrier failed, and a re-attempt is made only while that holder is still the connected one. Pinned by (w), which fails without the binding.
@@ -230,16 +230,38 @@ The three consequences, now true by construction:
**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). **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. **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. 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.
> **CORRECTED after the sixth pass, and this sentence used to carry two false claims.** It said the fix "made the caller smaller rather than larger - no sixth condition was added, one was removed". Measured: `packages/polyfill/src/surface/use-shape-query.ts` went **664 -> 727 lines** across that commit, and `git show 6b2b8ab --numstat` reports **34 insertions / 8 deletions** on it - `setNotYetAnswered` ADDS a three-term condition rather than removing one. The caller grew. See the note at the end of this Result on why this kept happening.
### ROUND 7 - the task is SPLIT, and that is the outcome
The sixth pass refuted the round-6 repair and found the family we had both pre-committed to splitting on. It is split, and the follow-up is `.tasks/todo/T01.m-one-answer-to-whether-a-document-is-open.md`, written by the coordinator and carrying both proved failures as its acceptance tests. **No registry code was touched in this round**; what follows is the honest accounting.
**What the sixth pass proved, and it falsifies round 6's central claim.** I wrote that `held` is "the ONE record of openness". It is not - there are three, and the one nobody counted is `fanOuts` in `subscribe.ts`. `configure()`, which is PUBLISHED, calls `resetDocSubscriptions()` and clears every fan-out listener including the bootstrap one, without telling `open-repo`; so `held` keeps a dead entry, `syncState` keeps `"synced"`, and the short-circuit skips the re-open for ever - measured as `doc_subscribe calls = 0` and `{isPending: false, isSuccess: true, data: []}`, unrecoverable even through `refetch()`. And `held.set()` runs synchronously before `establish` has reached `doc_subscribe` at all, so a broker rejecting after the bounded fallback still leaves the repo recorded as held. **`held` still means "we asked"** - the exact looser meaning I deleted `opened` for. I replaced one of two disagreeing records; there were three.
**What this task delivers, and every part of it survived six independent reviews:**
- the four call sites - `docs.sparqlQuery`, `ensureRepoOpen`'s propagation, `documentInboxAddress`, and `fetchReadCap` no longer collapsing a rejection into `false`;
- the `fetchReadCaps` deletion;
- `knowledge_what-a-failed-read-answers-upstream`;
- the empty-is-success guards, never once faulted;
- the e2e named-scope coverage, run live by two independent reviewers;
- **the barrier's REACHABILITY half**, kept deliberately: a holder of nothing gets `isError` with `dataLen: 0`, and a public document opens from a bare reference, both verified against a live broker twice. Removing it would restore a silent empty success universally, which is worse than a partial guarantee honestly stated;
- **the barrier's ARRIVAL half, honestly qualified rather than withdrawn.** It holds inside one ordinary session and is pinned by (bb), (t), (ee), (ff) and (cc). It does not hold after a second `configure(...)`, nor when a broker refuses a subscription late.
**The contract is brought down to that.** `## Guarantees` now says the arrival half holds "within one session" and sends the reader to `## Non-guarantees`, where both situations are named with their cause - more than one record of "is this document open" - and pointed at `T01.m`. The same qualification is in the knowledge leaf. **No version bump**: no published behaviour changed in this round, only the accuracy of its description.
**Why no `bug_` leaf for this.** The task's own instruction was not to open one for what it repairs; this is what it does NOT repair, and it is now a task with acceptance tests, which is the stronger record. A leaf would duplicate `T01.m` and drift from it.
### Gate ### Gate
- `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. - `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 on the delivered state: green every time, 3.18-3.34s.** 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. - **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/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`. **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. - polyfill e2e, run at HEAD: `Polyfill e2e summary: 46 passed, 0 failed, 46 total - batch 3.5 min, slowest cold sync 30s`, re-run on the delivered state. **Two reviewers have now run it independently 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. **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.
@@ -277,4 +299,11 @@ The three consequences, now true by construction:
- 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.
- The in-page session change and the identity-following publication, both listed above, are inside `T01.m`'s territory now rather than loose follow-ups.
### The reporting fault this task had, recorded because it is a pattern and not an oversight
Three times I asserted that a change had simplified or shrunk something, and all three were contradicted by measurement: "the module is smaller than round 2 left it" (534 -> 563), "the caller came out smaller" (664 -> 727), "no sixth condition was added, one was removed" (`--numstat`: 34 insertions, 8 deletions, and `setNotYetAnswered` adds a three-term condition). Two of them were relayed onward before anyone measured them. The common shape: I wrote the claim from the INTENT of the change - a predicate deleted, a record unified - while the file was growing around it, and the sentence read as a fact.
The rule I am holding from here, and it is mechanical rather than a resolution to be careful: **a structural claim - smaller, simpler, a condition removed, a mechanism deleted - carries the `wc -l` or `git show --numstat` that supports it, in the same sentence. If the number cannot be pasted, the claim is not written.** Every structural claim in this Result now carries its measurement.