chore: T01.j porte son enumeration et son resultat

L'enumeration a ete ecrite avant le code, comme demande : les quatre sites
d'appel, ce que chacun fait aujourd'hui quand la capacite ne peut pas etre
obtenue, le cas inerte, et la liste de ce qui doit rester un succes. Le resultat
consigne le choix, les quatre options ecartees avec leur raison, ce qui a casse
et ce que ces tests affirmaient vraiment, et les deux bug_ qui ne se ferment pas.
This commit is contained in:
Sylvain Duchesne
2026-09-21 15:24:40 +02:00
parent 534d8f91b7
commit aa38b37f33
@@ -96,3 +96,69 @@ Rejected, with the deciding reason for each:
- **Adding no open barrier to `useShapeQuery`, only the refusal.** Rejected: an unsynced named document then still reads `isSuccess` with `data: []`, which is the same lie for a document that is not empty. Every other read path in this library crosses that barrier; this one stopped because T01.d removed the scope TRANSLATION that produced the document set, and the caller now names documents directly, so the barrier has a subject again.
## 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).
### What each call site does now when the cap cannot be obtained
- **`ensureRepoOpen`** (`open-repo.ts:209`) - propagates. The open has not happened, and the read that follows must not proceed as though it had. It used to decline silently and let the anchored read blame the reader for holding no cap.
- **`docs.sparqlQuery`** (`docs.ts:155`) - rejects with the true reason instead of `assertMayReach`'s "you do not hold this document's cap", which is a different event calling for a different action.
- **`documentInboxAddress`** (`branch-registers.ts:317`) - propagates, and so does its own address read, whose `catch` was swallowing breakdowns into `undefined`. `undefined` now means only "no inbox".
- **`fetchReadCaps`** (`public-store.ts:277`) - DELETED. Zero callers, grep-verified, not in `src/index.ts`; an unowned per-document tolerance over a call that now rejects.
- **`useShapeQuery`** - crosses the barrier on every document `scope.graphs` NAMES before opening the query: public-store fetch, refusal out loud, sync barrier. Any of them failing publishes `isError`.
### The design, and what was rejected
`fetchReadCap` stops converting a rejected download into `false`. `downloadReadCap` already distinguishes the two and documents why; one `catch` destroyed it a line later. `boolean` now honestly means "a cap was obtained". Rejected: a three-valued result (all three live callers want the same thing - fail - and the engine has no such vocabulary); fixing only `fetchReadCap` (`useShapeQuery` never calls it, so the headline defect would have survived); making the capability filter refuse instead of drop (under `did:ng:i` dropping is exactly right, and by then the engine has already answered - the question belongs at the ASK); partial answers for a multi-document scope (a short list is the same lie as an empty one); the refusal without the open (an unsynced named document would still read `isSuccess` with `data: []`, for a document that is not empty).
### How "genuinely empty" stayed a success
Every row of the enumeration's "must not change" list is covered by a test that passes both with and without the change: `use-shape-query.test.ts` (m) a named, reachable document holding nothing of the shape is `isSuccess` with `data: []`; (h) `graphs: []`; (p) `graphs: ""`; (j) the whole dataset filtered down to the holder's own; (o) the INERT case; `document-inbox-surfaces-failure.test.ts` third test, a document nobody opened an inbox on answering `[]` on both reads and refusing the deposit with "has no inbox"; `public-store.test.ts`, a document exposing no cap still answering `false` and still asked only once.
The four tests that DO discriminate were verified non-vacuous by temporarily removing the change: (l) and (n) on the barrier, and both inbox failure tests. (q) caught a real leak when the generation check was removed.
### What broke, and what it was really asserting
Four tests in `public-store.test.ts`, all of them `expect(await fetchReadCap(unreachable)).toBe(false)`. What they were asserting was the MEMO's arithmetic - "a failure is not remembered, an absence is" - which is why every `asksAbout` count in that file is unchanged. The value they asserted alongside it was the collapse itself. They now assert a rejection, which is strictly stronger, plus one new test that the rejection names the document and keeps the broker's own words on `cause`.
Nothing else broke. The reason is worth recording: the three live `fetchReadCap` callers that read a STORE or an INBOX already hold that cap (`fileOwnStructure` / `holdOwnCap` file it at connection), so `fetchReadCap` answers from the registry and never reaches the network. The blast radius is much narrower than four call sites suggest - it is a third party naming a document it does not hold.
### Gate
- `bun test` -> `431 pass / 0 fail`, `Ran 431 tests across 40 files` (baseline `421 pass / 0 fail`, measured before any change; +10 new tests, no test deleted or skipped)
- `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: 43 passed, 0 failed, 43 total - batch 3.3 min, slowest cold sync 27s`. Same count as T01.h's baseline. NOTE: the broker login is flaky - two of five runs died with `[e2e broker login] gave up after 45.0s - the wallet-list screen never changed`, at a different step each time, before any library code runs. A clean 43/43 was obtained twice, once at `75313f7` and once at HEAD.
### The contract
`1.0.0-dev.9`, MAJOR, per the leaf's own `## Change policy` ("an existing call narrows: ... it reports a state you did not have to handle before"). `packages/polyfill/package.json` bumped. Two NON-guarantees corrected because the change makes them false: "Neither shape read fetches a public store's key for you" (now `useShape` only, plus `did:ng:i`), and "a shape query the engine refuses never finishes loading" (the commonest way in is closed for a NAMED scope). Two out-of-leaf documents carried the same now-false claim and were corrected in one sentence each: `docs/api-contract.md:262` and `docs/readcap-and-nuri-model.md:367`. `emulated-verifier/read-filter.ts`'s header said both shape reads ask nothing - corrected, with the note that refusing at the ASK and filtering the RESULT remain two different questions.
**A rights movement, stated as the grid requires.** `useShapeQuery` now fetches a public store's cap for a document named in `graphs`, which files that cap in the connected holder's ring. That is a GAIN on that call. It grants nothing new overall: it is exactly the reachability `docs.sparqlQuery` anchored on the same document already gave, and `exposeReadCap` is called only for `scope === "public"` (VERIFIED in T01.f). No document becomes readable that was not already readable by naming it. Said in `## Non-guarantees`.
`knowledge_what-a-failed-read-answers-upstream` records the durable fact: two observable outcomes only, the two collisions ("does not exist" vs "not known to me"; "unsynced" vs "genuinely empty"), no capability check on the read path, and the three rules that follow - including that the unsynced/empty collision is closed by the sync barrier and never by an error.
`docs/internal-contract.md` section 6 only: line numbers, the `ensurePhysicalRepoOpen` export this module has never had (it is `shared-wallet/physical.ts:134`), the deleted `ensureReposOpen`, the missing `openRepoUnguarded`, and `ensureRepoOpen`'s second parameter. A dated note says the rest of that document's drift was left alone deliberately.
### The two bug leaves: NEITHER is closed
- `bug_a-lost-inbox-watch-is-never-retried` - **not closed.** It asks for a recovery signal that does not depend on the identity performing a capability-bearing act. Nothing here adds one. This work arguably makes it more visible (a failed open reports a truer reason), which is not the same as retrying.
- `bug_an-empty-inbox-list-can-mean-a-failure` - **not closed.** Its mechanism is `resolveAccount` yielding nothing RATHER THAN raising, inside `enumerateMyInboxes`, which then reports a list that is complete and empty. That path was not touched: `readInboxCapPairs` already rethrows what it catches, and `record?.docPrivate` returning `undefined` still answers `[]` with no shortfall. No test proves otherwise, so the leaf stays.
### Surprises
- **VERIFIED: `fetchReadCaps` was dead.** Zero callers anywhere in `src`, `test` or `e2e`, and absent from `src/index.ts`. Its last caller went with `readUnion` in `1.0.0-dev.8`.
- **VERIFIED: `useShapeQuery`'s empty success does not come from `fetchReadCap` at all.** That call site does not exist on this path - T01.d removed it with the scope translation. It comes from the capability filter emptying a named document's objects, and from a named-but-unsynced document answering nothing. Tests (l) and (n) fail without the barrier; the task's three-layer chain describes the ANCHORED path, which is a different (and already loud) one.
- **VERIFIED: the blast radius is narrow.** Six of the eight `ensureRepoOpen` callers hold the cap they are about to use, so they never ask the network. That is why only four tests broke.
- **VERIFIED: `g === ""` in `namedDocuments` is unreachable** - `isNuri("")` is false. Kept as documentation of the SDK's own alias, said so in the comment and the commit. Test (p) is therefore a behaviour guard, not a discriminating test, and is labelled as one.
- **INFERRED: the barrier's cost.** A named scope now spends one public-store fetch and one sync barrier per document per session, serially. Memoised, so a re-subscribe is a map hit; the worst case is a cold scope naming many documents, each bounded by the 8s fallback. Stated in the contract; no measurement was taken.
### Follow-ups, reported and not fixed
- `packages/polyfill/src/shared-wallet/account-registry.ts:78` imports `documentInboxAddress` and never uses it - a dead import. `tsconfig.base.json` sets no `noUnusedLocals`, so nothing flags it.
- 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 5 runs in this session.
- `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.