diff --git a/.tasks/doing/T01-align-the-published-surface.md b/.tasks/doing/T01-align-the-published-surface.md index 5574ed0..d06c901 100644 --- a/.tasks/doing/T01-align-the-published-surface.md +++ b/.tasks/doing/T01-align-the-published-surface.md @@ -41,7 +41,8 @@ The polyfill's published surface has drifted from the SDK's: invented names, inv - T01.f Remove readUnion, and the documents that cite it (deps: T01.e) - T01.g Adversarial review of the whole published surface (deps: all) -- T01.i Record the failure-reads-as-an-absence chain, and clear two leftovers (deps: T01.h) +- T01.i Clear two mechanical leftovers from the removal (deps: T01.h) +- T01.j A read that cannot reach its document must fail, not answer empty (deps: T01.i) ## Learnings digest diff --git a/.tasks/todo/T01.g-adversarial-review-of-the-published-surface.md b/.tasks/todo/T01.g-adversarial-review-of-the-published-surface.md index a12a894..4141985 100644 --- a/.tasks/todo/T01.g-adversarial-review-of-the-published-surface.md +++ b/.tasks/todo/T01.g-adversarial-review-of-the-published-surface.md @@ -2,7 +2,7 @@ id: T01.g title: Adversarial review of the whole published surface parent: T01 -depends_on: [T01.a, T01.b, T01.c, T01.d, T01.e, T01.f, T01.h, T01.i] +depends_on: [T01.a, T01.b, T01.c, T01.d, T01.e, T01.f, T01.h, T01.i, T01.j] created: 2026-09-16 claimed_by: null claimed_at: null diff --git a/.tasks/todo/T01.i-a-failure-that-reads-as-an-absence.md b/.tasks/todo/T01.i-a-failure-that-reads-as-an-absence.md deleted file mode 100644 index 28821f8..0000000 --- a/.tasks/todo/T01.i-a-failure-that-reads-as-an-absence.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -id: T01.i -title: Record the failure-reads-as-an-absence chain, and clear two leftovers -parent: T01 -depends_on: [T01.h] -created: 2026-09-21 -claimed_by: null -claimed_at: null -blocked: false ---- - -## Description - -Three leftovers from T01.f. The first is the substantial one; the other two are mechanical. - -### 1. Open a `bug_` leaf for a read that answers empty where it should fail - -This repo has closed the "failure disguised as an absence" family repeatedly and it has two open `bug_` leaves on it already (`bug_a-lost-inbox-watch-is-never-retried`, `bug_an-empty-inbox-list-can-mean-a-failure`). This is another member, and it spans three layers. Write ONE leaf in `.project/concepts/app-contract/`, naming the chain rather than any single symptom. - -**The upstream fact that makes it a divergence and not a heritage - VERIFIED, do not re-derive it, and do NOT go read `nextgraph-rs` for it.** An anchored SPARQL query on a repo the verifier does not know REJECTS. The deciding line is `engine/verifier/src/request_processor.rs:269`: `let repo = self.repos.get(repo_id).ok_or(NgError::RepoNotFound)?;` inside `resolve_target_for_sparql`. It crosses into JS as a REJECTED PROMISE carrying the string `"RepoNotFound"` (`sdk/js/lib-wasm/src/lib.rs:403` for nodejs, `:606` for browser). Upstream's observable partition is "in `self.repos`" vs "not in `self.repos`": "does not exist" and "exists but my session does not know it" are INDISTINGUISHABLE, while "known but unsynced" collides with "genuinely empty". No read-capability check exists anywhere on that path - `resolve_target_for_sparql` reads only `nuri.target`. - -**The three layers of our chain**, each VERIFIED by reading the code in this working copy: - -- `fetchReadCap` (`packages/polyfill/src/emulated-verifier/public-store.ts:190`) collapses three unrelated situations into one `false`: the emulation is not in force (`:196`), the network could not be asked (`:223`), and the document exposes no cap (`:225`). Its own comment at `:220` states the collapse out loud. The function BELOW it, `downloadReadCap`, deliberately distinguishes resolving from rejecting and documents why - so the distinction exists and is destroyed one line later. Worse, NO caller reads the boolean at all: `ensureRepoOpen:208`, `docs.sparqlQuery:155`, `branch-registers.ts:316` and `fetchReadCaps:277` all call it as a bare statement. The information is not merely flattened, it is unrecoverable. -- The read then proceeds on a document that was never opened. -- `useShapeQuery` answers a SUCCESS holding zero objects. - -**What the target says the answer should be.** `useShapeQuery`'s return type is modelled on TanStack Query's `useQuery`, which distinguishes purely by whether the query function REJECTS: a rejection gives `isError` with the error, a resolution gives `isSuccess` with whatever data, empty included. That maps exactly onto upstream's `RepoNotFound`. So "the document is not reachable" must surface as `isError`, and `isSuccess` with zero objects must mean "it is there and holds nothing". - -**Also record, because it is the same family and currently unowned:** `useShapeQuery` never opens the documents named in `scope.graphs`, so an application that names an unopened document gets an empty success with no error. The `ensureReposOpen` barrier was removed from that path by T01.d. A fix for it was written during T01.f and REVERTED on instruction, because it was justified by a misdiagnosis (see the parent's digest); the defect itself is real and stands. - -**Severity is yours to argue in the leaf**, from the two existing bug leaves' bar. State what an application observes, not what the code does. - -**Explicitly OUT OF SCOPE - do not implement a fix.** Changing `fetchReadCap`'s return type to name the three cases, or making a read fail where it currently answers empty, is a DESIGN CHANGE the owner has not arbitrated. This task RECORDS the defect. It does not repair it. - -### 2. Remove `ensureReposOpen`, now dead - -T01.f removed its last caller. Verify that claim with a grep before deleting - if a caller survives, say so and leave it. `ensureRepoOpen` (singular) is NOT dead and must stay. - -### 3. The example's typecheck - -`examples/notebook/tsconfig.json` fails typecheck. T01.f reports this as pre-existing and verified as such. The owner has asked for the example to be repaired. Make it typecheck, without weakening the config - if the only way through is to loosen a compiler option, stop and report instead. - -**Hard gate:** `bun test` -> 0 fail; `bunx tsc --noEmit -p packages/polyfill/tsconfig.json` -> exit 0; `bunx tsc --noEmit -p packages/polyfill/e2e/tsconfig.json` -> exit 0; the example's own typecheck -> exit 0; `python3 ~/.claude/skills/concept/lint.py` -> 0 ERROR. Anti-cheat: no `any`, no `@ts-ignore`, no `as unknown as`, no deleted or skipped test, no weakened assertion, no loosened compiler option. - -## Progress - -## Result diff --git a/.tasks/todo/T01.i-two-leftovers-from-the-removal.md b/.tasks/todo/T01.i-two-leftovers-from-the-removal.md new file mode 100644 index 0000000..b0ca971 --- /dev/null +++ b/.tasks/todo/T01.i-two-leftovers-from-the-removal.md @@ -0,0 +1,30 @@ +--- +id: T01.i +title: Clear two mechanical leftovers from the removal +parent: T01 +depends_on: [T01.h] +created: 2026-09-21 +claimed_by: null +claimed_at: null +blocked: false +--- + +## Description + +Two small, independent leftovers reported by T01.f. Neither carries a design question; if one turns out to, stop and report rather than deciding it here. + +### 1. Remove `ensureReposOpen`, now dead + +T01.f removed its last caller. VERIFY that with a grep across `packages/`, `examples/` and `e2e/` before deleting - if a caller survives, leave it and say so. `ensureRepoOpen` (SINGULAR) is NOT dead, it is on the live path, and must stay. + +### 2. Make the example typecheck + +`examples/notebook/tsconfig.json` fails typecheck. T01.f reports this as pre-existing and verified as such. The owner has asked for the example to be repaired. + +Make it typecheck WITHOUT weakening the configuration. If the only way through is to loosen a compiler option, disable a check, or annotate a real type error away, STOP and report - that is a finding, not a repair. The example exists to be the honest demonstration of what an application writes, so an example that only compiles because the rules were relaxed for it is worse than one that does not compile. + +**Hard gate:** `bun test` -> 0 fail; `bunx tsc --noEmit -p packages/polyfill/tsconfig.json` -> exit 0; `bunx tsc --noEmit -p packages/polyfill/e2e/tsconfig.json` -> exit 0; the example's own typecheck -> exit 0; `python3 ~/.claude/skills/concept/lint.py` -> 0 ERROR. Anti-cheat: no `any`, no `@ts-ignore`, no `as unknown as`, no deleted or skipped test, no weakened assertion, no loosened compiler option. + +## Progress + +## Result diff --git a/.tasks/todo/T01.j-a-failure-must-not-read-as-an-absence.md b/.tasks/todo/T01.j-a-failure-must-not-read-as-an-absence.md new file mode 100644 index 0000000..4bba57f --- /dev/null +++ b/.tasks/todo/T01.j-a-failure-must-not-read-as-an-absence.md @@ -0,0 +1,56 @@ +--- +id: T01.j +title: A read that cannot reach its document must fail, not answer empty +parent: T01 +depends_on: [T01.i] +created: 2026-09-21 +claimed_by: null +claimed_at: null +blocked: false +--- + +## Description + +A read whose document is unreachable currently answers an EMPTY SUCCESS. It must answer a failure. This is the "failure disguised as an absence" family, which this repo has closed roughly eight times and still carries two open `bug_` leaves for. Study it properly before changing anything: the facts below are established, the DESIGN is not. + +### The established facts - do NOT re-derive them, and do NOT read `nextgraph-rs` for them + +**Upstream (VERIFIED).** An anchored SPARQL query on a repo the verifier does not know REJECTS. The deciding line is `engine/verifier/src/request_processor.rs:269`: `let repo = self.repos.get(repo_id).ok_or(NgError::RepoNotFound)?;` inside `resolve_target_for_sparql`. It crosses into JS as a REJECTED PROMISE carrying the string `"RepoNotFound"` (`sdk/js/lib-wasm/src/lib.rs:403` nodejs, `:606` browser). The observable partition upstream is "in `self.repos`" vs "not in `self.repos`": "does not exist" and "exists but this session does not know it" are INDISTINGUISHABLE, while "known but unsynced" collides with "genuinely empty". There is NO read-capability check anywhere on that path - `resolve_target_for_sparql` reads only `nuri.target`. + +**Our chain, three layers (VERIFIED by reading this working copy).** + +- `fetchReadCap` (`packages/polyfill/src/emulated-verifier/public-store.ts:190`) returns a `boolean` collapsing three unrelated situations: the emulation is not in force (`:196`), the network could not be asked (`:223`), the document exposes no cap (`:225`). Its own comment at `:220` states the collapse out loud. `downloadReadCap`, directly below, DELIBERATELY distinguishes resolving from rejecting and documents why - so the distinction is produced and then destroyed one line later. And NO caller reads the boolean: `ensureRepoOpen:208`, `docs.sparqlQuery:155`, `branch-registers.ts:316`, `fetchReadCaps:277` all call it as a bare statement. +- The read then proceeds on a document that was never opened. +- `useShapeQuery` answers `isSuccess` holding zero objects. + +**What the target says the answer should be.** `useShapeQuery`'s return type is modelled on TanStack Query's `useQuery`, which distinguishes purely by whether the query function REJECTS: a rejection gives `isError` with the error; a resolution gives `isSuccess` with whatever data, empty included. That maps exactly onto upstream's `RepoNotFound`. So an unreachable document must surface as `isError`, and `isSuccess` with zero objects must mean "it is there and it holds nothing". + +### The design is yours to establish, and the enumeration comes FIRST + +Do NOT start from a preferred shape. `.project/rules/nextgraph/no-divergence-from-nextgraph.md` and `polyfill-must-match-the-target` both apply, and so does the discipline of listing the cases before concluding - this repo has a recorded incident of deciding from one convenient case and having to be dragged back row by row. + +**List the callers and their states before choosing anything.** For each of the four call sites above: what is it doing, what does it already hold, and what should it do when the cap cannot be obtained? Include the INERT case - before the first cap exists the emulation deliberately does not enforce, and reading works anyway; whatever you build must stay inert there or it will break every early path. + +**The constraint that shapes the answer.** `fetchReadCap` has NO upstream counterpart - upstream never consults a capability on the read path. So there is no SDK signature to mirror, and the cascade goes to the ENGINE'S MODEL: upstream's observable is that a read REJECTS or it does not. That is an argument about where the distinction should become visible, not a licence to invent a rich vocabulary of outcomes that the target does not have. Weigh it; do not treat this paragraph as the decision. + +**Re-examine the reverted barrier on its own merits.** `useShapeQuery` opens nothing named in `scope.graphs` - the barrier was removed by T01.d. A fix was written during T01.f and REVERTED, because it had been justified as unblocking `readUnion`'s removal, which was FALSE (an index is read by anchor, not by shape). The revert killed a bad JUSTIFICATION, not a bad change. The defect is real and unowned: an application that names an unopened document gets an empty success. Decide it fresh, on the enumeration. + +**Check the two open bug leaves** (`bug_a-lost-inbox-watch-is-never-retried`, `bug_an-empty-inbox-list-can-mean-a-failure`). They are the same family. State whether this work closes either. Do NOT claim it does without a test that proves it - a bug leaf deleted on a plausible argument is worse than one left open. + +### Blast radius - this is a behaviour change applications see + +Reads that silently answered empty will start failing. That is the POINT, and it is also how this breaks callers that were quietly relying on the silence. So: the published contract at `.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md` must state the new behaviour, with a version bump per its own `## Change policy` (a call that narrows is a MAJOR). Follow the existing pre-release scheme, do not invent one. Expect internal callers and tests to surface real breakage - each one is a case to understand, never a reason to soften the change. + +**Record the durable fact** in `.project/concepts/app-contract/` as a `knowledge_` leaf: what the target answers for an unreachable, an unsynced and an empty document, and which of those a caller can tell apart. That fact is worth keeping whatever the fix turns out to be. Do NOT open a `bug_` leaf for something this task repairs. + +### Verification + +**The e2e must run.** Unit tests cannot judge this: the whole subject is what happens when the network does not answer. Run the polyfill e2e suite and report its counts verbatim. If the broker is unreachable, report the suite as UNRUN - never substitute a typecheck for the measurement. + +**Hard gate:** `bun test` -> 0 fail; `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. Anti-cheat: no `any`, no `@ts-ignore`, no `as unknown as`, no deleted or skipped test, no weakened assertion. Specifically forbidden here: catching the new failure somewhere upstream to keep an old test green - if a test breaks, it is telling you something. + +**An adversarial review is part of this task, not optional.** When the work is done, it goes to a fresh reviewer mandated to REFUTE it, on at least these angles: a path where the failure is still swallowed; a path where the change now fails something that legitimately has nothing (an empty document must still be a SUCCESS); and the inert case. A finding counts only with a concrete failure scenario. Re-run the adversary on whatever you fix in response. + +## Progress + +## Result