chore: l'arbre de travail pour l'alignement de la surface

This commit is contained in:
Sylvain Duchesne
2026-09-16 12:20:57 +02:00
parent 820552d263
commit eb8a7163a2
9 changed files with 298 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Task Index
_Generated 2026-09-16 from filesystem._
## T01 align-the-published-surface — todo
- T01.a a provenance annotation for a target announced but not yet written — todo
- T01.b what the ORM offers for writing and deleting, and what the three remaining symbols are — todo
- T01.c watchShape becomes useShapeQuery and takes the SDK's Scope — todo (deps: T01.a)
- T01.d useShapeQuery reads through the SDK's shape query — todo (deps: T01.c) [blocker recorded: the open MAJOR bug names watchShape/readUnion as the unaffected paths, and the contract publishes that as the workaround]
- T01.e publish the ORM family with the rights filter — todo (deps: T01.b, T01.d)
- T01.f remove readUnion, and the documents that cite it — todo (deps: T01.e)
- T01.g adversarial review of the whole published surface — todo (deps: T01.a–T01.f)
@@ -0,0 +1,44 @@
---
id: T01
title: Align the published surface on the NextGraph SDK
parent: null
depends_on: []
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
The polyfill's published surface has drifted from the SDK's: invented names, invented parameters, and symbols with no upstream counterpart. This task brings it back and installs what stops it recurring.
**The principle to apply** — `.project/rules/nextgraph/surface-mirrors-the-sdk.md`, and its semantic sibling `no-divergence-from-nextgraph.md`. Publish the SDK's **names**, **parameters** and **return values**; a difference must be duly justified beside the code, and the best justification is that it is NextGraph's own direction as announced by its developers.
**The facts this rests on** — `.project/concepts/app-contract/knowledge_what-a-read-scope-is-upstream.md`. In particular: a `graphs` entry is one document's graph; a store's graph holds only its containment triples, so "search my private store" returns empty silently; `did:ng:i` means the whole local dataset with no permission narrowing; scoping a read by store is not expressible at all.
**Four pieces of work:**
1. `watchShape` becomes `useShapeQuery`, takes the SDK's `Scope` (`{ graphs?, subjects? }`) instead of the three literals `"public" | "protected" | "private"`, and reads through the SDK's shape query instead of enumerating documents. The `ShapeQuery` return shape (TanStack-like) and the capability filter both stay — that difference is the justified kind.
2. Wrap the ORM family the polyfill does not expose: `getObjects` and `insertObject` at identical signatures with the rights filter; arbitrate `OrmSubscription`, `DiscreteOrmSubscription`, `ngSession`; establish upstream's shape for deletion before specifying it. Today an application must import them straight from `@ng-org/orm`, and then receives every identity's data from the shared wallet with nothing signalling it — a rights hole, not a missing convenience.
3. Remove `readUnion` entirely once nothing internal calls it. Three single-document reads become an anchored SPARQL query. `ng-helpers`' contract cites it and must be revised.
4. The annotation mechanism: a mandatory reference on every function, which must **resolve**; plus a form for "target announced by the developers, not yet written", which the current format cannot express — so an author holding the strongest justification is pushed to miscite or to understate.
**Boundary that must not be lost:** the store axis survives for **creation** (`docCreate` takes a destination upstream) and dies for **reading**.
**Success criteria:** every published symbol either mirrors an SDK symbol in name, parameters and return, or carries a justified annotation; no symbol without an upstream counterpart survives unjustified; gates green (`bun test`, both typechecks, `lint.py` 0 ERROR, e2e); and an adversarial review over the whole published surface finds nothing unaddressed.
## Progress
## Result
## Subtasks
- T01.a A provenance annotation for a target announced but not yet written
- T01.b What the ORM offers for writing and deleting, and what the three remaining symbols are (reconnaissance)
- T01.c watchShape becomes useShapeQuery and takes the SDK's Scope (deps: T01.a)
- T01.d useShapeQuery reads through the SDK's shape query (deps: T01.c)
- T01.e Publish the ORM family with the rights filter (deps: T01.b, T01.d)
- T01.f Remove readUnion, and the documents that cite it (deps: T01.e)
- T01.g Adversarial review of the whole published surface (deps: all)
## Learnings digest
@@ -0,0 +1,33 @@
---
id: T01.a
title: A provenance annotation for a target announced but not yet written
parent: T01
depends_on: []
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
Extend the `@provenance` mechanism so that a symbol aligned on a target **its developers have announced but not yet written** can be annotated honestly, and make the reference requirement enforced.
**Why this runs first.** Every later child in this batch annotates at least one symbol that has no upstream `path:symbol` to cite — `useShapeQuery` has **zero** occurrences in `nextgraph-rs`. The weakness is recorded, deliberately unfixed, in `.project/rules/nextgraph/surface-mirrors-the-sdk.md` § "The gap that same example exposes": an author holding the strongest possible justification has only two bad moves left — cite an approximate symbol, which is precisely the `readUnion` failure, or declare `invention`, which understates the alignment and tells a later reader the shape is ours to change. Fix the mechanism before anyone has to use it.
**Files in scope:** `packages/polyfill/test/provenance.test.ts` (the format and its checks), the `@provenance` header block in `packages/polyfill/src/index.ts`, the `kind` table in `.project/rules/nextgraph/no-divergence-from-nextgraph.md` (the taxonomy lives there and must gain the same entry), and any existing annotation whose kind changes under the new taxonomy.
**This child does NOT change the published surface.** No symbol is added, removed or re-signed, so there is **no contract edit and no version bump** here. Do not touch `.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md` — the children that change the surface each own their own edit to it.
**Open questions — record the decision beside the code, do not guess a design:**
1. *"A mandatory reference on every function, which must resolve"* collides with a reason already written down. `test/provenance.test.ts` states, at length, that it deliberately does **not** check the `ref` path exists on disk: `nextgraph-rs` is a sibling checkout that may be absent on another machine, and a test that goes red for that reason teaches nothing — it just trains people to skip it. Decide what "must resolve" means here (a well-formed citation into a named checkout; resolve when the checkout is present and skip otherwise; resolve against a pinned commit) and record the reasoning next to the test. If it cannot be settled from the doctrine leaves, **stop and ask** rather than inventing a checker that will be skipped.
2. What the new kind is **called**, and what it takes as evidence. There is no `file:symbol`, so the citation has to be the statement itself — whose statement, and where it was made. Neither existing kind fits: `declared-not-wired` needs a declaration in the tree, `reserved-name` needs a name in the tree. Name it so it can never be read as `invention`, because the whole point is that it is the *strongest* justification, not the weakest.
**Pass condition.** The hard gate below, plus: every published symbol still carries a well-formed annotation; the new kind is exercised by a test case (its first real annotation belongs to T01.c, which reads this child's `## Result` before annotating); and the taxonomy in the rule file and the one in the test agree.
**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 deleting or skipping a test, no widening an assertion.
## Progress
## Result
@@ -0,0 +1,36 @@
---
id: T01.b
title: What the ORM offers for writing and deleting, and what the three remaining symbols are
parent: T01
depends_on: []
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
**Reconnaissance, not implementation.** Piece 2 of the parent task rests on a fact nobody has established: upstream's shape for **deletion**. Specifying a wrapper before knowing it is how this project has invented before. This child establishes the facts and proposes the arbitrations; T01.e implements against its `## Result`.
**Read the target at the source.** `/home/sylvain/projects/nextgraph/nextgraph-rs` is a sibling checkout, at `213338f6` when this was planned — the commit the existing knowledge leaves were read at. Record in the leaf you write the commit you actually read.
**The three questions:**
1. **Deletion.** What does the target offer to delete an object? Starting facts, verified at planning: `sdk/js/orm/src/connector/` holds `getObjects.ts` and `insertObject.ts` and **no deletion file**; `sdk/js/orm/src/core.ts` exports exactly `initNg`, `ngSession`, `OrmSubscription`, `DiscreteOrmSubscription`, `insertObject`, `getObjects`, plus `getRaw` / `watch` / `effect` re-exported from `@ng-org/alien-deepsignals` and the types. Run the cascade of `.project/rules/nextgraph/no-divergence-from-nextgraph.md` — level 3 first, then level 2 (`sdk/js/lib-wasm`, `sdk/js/web`), then the engine's model — and report **which level answers**, with symbols. A shape the target *declares* and constructs nowhere is `declared-not-wired` and is the strongest thing to align on; an **absent implementation is not evidence** the target will not have one, and must not be reported as one.
2. **`getObjects` and `insertObject`.** Exact signature, parameters and return of each, precisely enough that a wrapper can be published at an identical signature without translating anything. `docs/api-contract.md` § 6 already records `getObjects` as level 3 VERIFIED (`sdk/js/orm/src/connector/getObjects.ts:23`) — check it rather than re-deriving it, and correct it if it has drifted.
3. **`OrmSubscription`, `DiscreteOrmSubscription`, `ngSession`.** What each one is, what it hands the caller, and whether an application coding against this package needs it. **Propose** an arbitration for each — publish as-is / publish under what name / do not publish — with the evidence. Do not implement. Weigh it against the rule's own tell: *a symbol that makes the caller handle what the target will never hand it* (a document's key, a store id, its own identity) is a divergence even when upstream exports it.
**Established facts — cite them, do not re-derive them from `nextgraph-rs`:** `.project/concepts/app-contract/knowledge_what-a-read-scope-is-upstream.md` (the `Scope` type and its two axes; a `graphs` entry is one document's graph; `did:ng:i` means the whole local dataset with no permission narrowing; the graph filter is literal; `OrmSubscription.readyPromise` distinguishes not-ready from ready-and-empty at level 3 while the React `useShape` adapter drops it; `OrmConfig`'s richness is real and unreachable), `knowledge_what-an-iri-says-upstream.md`, `knowledge_what-an-inbox-is-upstream.md`.
**The problem being served is a rights hole, not a missing convenience.** Today an application must import these straight from `@ng-org/orm`, and then receives **every identity's data** out of the shared wallet with nothing signalling it. The filter that closes it is `makeReadFilteredView` (`packages/polyfill/src/emulated-verifier/read-filter.ts`, annotated `kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap`), applied per document on what comes back — where the target bounds it. For each candidate wrapper, report **how** that filter would have to apply, or why it cannot. A write path is the hard case: the filter is a *read* emulation, and `packages/polyfill/src/surface/ng-proxy.ts` already overrides `sparql_update` with an emulated write guard — read it before concluding.
**Deliverable.** A `knowledge_` leaf under `.project/concepts/app-contract/`, written by invoking `/concept document` rather than by writing the file directly. Every claim points at a **symbol**, never a line number. The arbitration proposals and every unresolved question go in this task's `## Result`, where T01.e will read them.
**Out of scope:** no edit under `packages/`, no contract edit, no version bump. If a fact you establish changes what the published contract says, name it in `## Result` and leave the edit to the child that owns it.
**Pass condition for this child** (it changes no code, so the implementation gate does not apply): `python3 ~/.claude/skills/concept/lint.py` → 0 ERROR; the leaf names the commit it was read at; every target-side claim carries a symbol reference; each of the three questions has either an answer or an explicitly stated open question.
## Progress
## Result
@@ -0,0 +1,36 @@
---
id: T01.c
title: watchShape becomes useShapeQuery and takes the SDK's Scope
parent: T01
depends_on: [T01.a]
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
`watchShape` becomes **`useShapeQuery`** and takes the SDK's `Scope` — `{ graphs?: string[] | string; subjects?: string[] }` (`sdk/js/orm/src/types.ts`) — in place of the three invented literals `"public" | "protected" | "private"` (`packages/polyfill/src/model/types.ts`).
**The name and the parameter only.** The read path underneath stays as it is in this child; T01.d replaces it. The translation from the SDK `Scope` down to what the current pipeline needs lives here and is **explicitly temporary** — say so beside the code, because *a parameter that must be translated is the tell that a mechanism below it is about to become ours* (`.project/rules/nextgraph/surface-mirrors-the-sdk.md`).
**What stays, and why that is not a fault.** The `ShapeQuery` return shape (TanStack-like: `{ data, isPending, isSuccess, isError, error }`) and the capability filter both survive. That is the **justified** kind of difference: NextGraph's direction, as reported by its developers, is a `useShapeQuery` returning exactly that shape, and deviating *toward* the announced target is alignment. `useShape` (the existing passthrough to the SDK's current hook, `packages/polyfill/src/surface/use-shape.ts`) is a different symbol and keeps its own life — do not collapse the two.
**Facts — cite them, do not re-derive them.** `.project/concepts/app-contract/knowledge_what-a-read-scope-is-upstream.md`: a `graphs` entry is **one document's graph**, never a set of them; `did:ng:i` or `""` discards every other entry and means the whole local dataset **with no permission narrowing**; a store's graph holds only its containment triples, so *"search my private store"* returns empty **silently**; `[]` or `undefined` returns no objects at all; the graph filter is literal, with no prefix or hierarchy matching; **scoping a read by store is not expressible at all**.
**The boundary that must not be lost.** The store axis survives for **creation** — `docs.docCreate` takes a destination upstream, and `storeRegistry.createEntityDoc(scope)` keeps its three literals — and dies for **reading**.
**Open question — settle it in the open, or escalate.** After this change two different types would want the name `Scope`. The published `Scope` today is the three literals and is used by `storeRegistry.createEntityDoc / listMyEntityDocs / resolveScopeGraph / resolveWriteGraph`. The SDK's `Scope` is the object. The rule says publish the SDK's names, so the SDK's type should take the name and the **placement axis needs a name of its own** — but what that name is, and whether `listMyEntityDocs` (a *read* keyed by the store axis) survives at all, is settled by no leaf. Propose it in `## Progress`; if `listMyEntityDocs`'s fate is genuinely in question, **stop and ask** rather than removing a published symbol on your own judgment.
**This child owns the contract edit and the version bump for its own change.** Edit `.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md`: `## Surface` (the signature, the `Scope` type block), `## Non-guarantees` (every mention of `watchShape`), `## Change policy` (the new pre-release string and what it narrows). By the contract's own `## Change policy` this is a **major** — a published symbol is removed, and an existing call changes a parameter a caller must react to. No other child edits the contract for this change; do not touch the sections another child owns.
**Also in scope:** `packages/polyfill/src/surface/watch-shape.ts`, `packages/polyfill/src/index.ts` (export and the comments around it), `packages/polyfill/src/model/types.ts`, `test/vocabulary.test.ts` and `test/app-surface.test.ts` (they pin the export inventory and the vocabulary), `test/watch-shape.test.ts`, `test/watch-shape-surfaces-failure.test.ts`, `docs/api-contract.md`, `packages/polyfill/README.md`, `packages/polyfill/docs/polyfill-reference.md`, `examples/notebook/app.ts`, and the `packages/polyfill/e2e/` callers.
**The annotation.** `useShapeQuery` has zero occurrences in `nextgraph-rs`; its justification is NextGraph's own direction as stated by its developers. Read T01.a's `## Result` and use the form it installed. Do **not** cite an approximate symbol — that is the `readUnion` failure this whole task exists to undo.
**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 deleting or skipping a test, no widening an assertion.
## Progress
## Result
@@ -0,0 +1,34 @@
---
id: T01.d
title: useShapeQuery reads through the SDK's shape query
parent: T01
depends_on: [T01.c]
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
`useShapeQuery` stops resolving a logical scope, enumerating documents, awaiting a sync barrier and calling `readUnion`, and reads through the **SDK's shape query** instead. The `ShapeQuery` snapshot and the capability filter survive; the home-grown pipeline goes.
**What "the pipeline" is, concretely.** `packages/polyfill/src/surface/watch-shape.ts` owns four steps today: resolve the logical scope to a document set (`listMyEntityDocs` in `shared-wallet/account-registry.ts`), open them through `ensureReposOpen` / `getSyncState` (`emulated-verifier/open-repo.ts`) which is the sync barrier `isPending` waits on, call `readUnion(docs)`, then filter the union by the shape's `@type` IRI. All four are the mechanism a translated parameter forced into existence, and all four are what this child removes.
**A blocker to resolve BEFORE implementing, not after.** `.project/concepts/app-contract/bug_useshape-live-read-needs-a-newer-broker.md` is an **open MAJOR bug**: the SDK's ORM subscription **never starts** against the broker deployed today — `orm_start_graph` is missing from two hand-maintained, independently-copied dispatch tables in any build made before commit `d5ecd0fd` — and the failure is an eternal pending set with one `console.error` as its only trace, indistinguishable from "this scope holds nothing". That leaf states explicitly that `watchShape` is **not affected**, because it is built on `doc_subscribe` (continuously present in both tables under a stable name), and the contract's `## Non-guarantees` tells applications to use `watchShape` for a reactive read for exactly that reason.
So this change takes the one reactive read that works and puts it on the path that does not, and the workaround the contract publishes stops existing. Establish what happens to that guarantee — a broker redeployment, a documented narrowing of the contract, a fallback — **before** writing the change. If it cannot be settled from the leaves, **stop and ask**. Do not implement and discover the consequence afterwards.
**`isError` has no counterpart at any level**, and that is recorded rather than assumed: `OrmSubscription`'s readiness promise is built with `resolve` alone, nothing rejects it, and `resolveReady()` runs on one path only — the arrival of initial data — so upstream's *"I could not find out"* **is** its *"still pending"* (annotation on `ShapeQuery` in `watch-shape.ts`; `docs/api-contract.md` § 6). Whatever replaces the pipeline must still be able to publish `isError`, or dropping it is a contract narrowing of its own that has to be declared.
**Facts — cite them, do not re-derive them.** `.project/concepts/app-contract/knowledge_what-a-read-scope-is-upstream.md`: the two axes and nothing else; the graph filter is literal; `did:ng:i` drops the graph filter entirely and runs over the whole local dataset with **nothing** added to narrow it by what the reader may open; the only bound is what the verifier has materialised locally; `subjects` filters the **root** shape only; `OrmSubscription.readyPromise` distinguishes not-ready from ready-and-empty at level 3 while the React `useShape` adapter drops that distinction. Also relevant: `makeReadFilteredView` (`packages/polyfill/src/emulated-verifier/read-filter.ts`) is the emulation of what the broker will bound **at delivery**, applied per document on what comes back — that is where the rights filter belongs, and `packages/polyfill/src/surface/use-shape.ts` already shows the shape of it.
**Leave `readUnion` alone here.** After this child `watch-shape.ts` no longer calls it, but `packages/polyfill/src/emulated-verifier/index-deposit.ts:253` and `:304` still do. Removing the symbol is T01.f's work.
**This child owns the contract edit and the version bump for its own change.** Edit `.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md` — `## Guarantees` and `## Non-guarantees` first, because that is where a narrowing shows up, then `## Surface`, then `## Change policy`. Apply the contract's own rules to decide the level rather than assuming one. No other child edits the contract for this change.
**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 deleting or skipping a test, no widening an assertion.
## Progress
## Result
@@ -0,0 +1,36 @@
---
id: T01.e
title: Publish the ORM family with the rights filter
parent: T01
depends_on: [T01.b, T01.d]
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
Wrap and publish the ORM family this package does not expose: `getObjects` and `insertObject` at **identical signatures**, with the rights filter; deletion in whatever shape T01.b established; and whatever T01.b's arbitration settled for `OrmSubscription`, `DiscreteOrmSubscription` and `ngSession`.
**Why the dependencies.** On **T01.b**: the deletion shape and the three arbitrations are established there — read its `## Result` and the `knowledge_` leaf it names, rather than re-reading `nextgraph-rs`. On **T01.d**: both children change the published surface and edit the same contract, and they must not run concurrently.
**The problem is a rights hole, not a missing convenience.** Today an application has to import these straight from `@ng-org/orm`, and then receives **every identity's data** out of the shared wallet with **nothing signalling it**. The filter that closes it is `makeReadFilteredView` (`packages/polyfill/src/emulated-verifier/read-filter.ts`, annotated `kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap`): it keeps only items whose document — their `@graph` — the current holder has a cap for, applied per document on what comes back. That is the emulation of what the broker will bound at delivery, done where the target does it. `packages/polyfill/src/surface/use-shape.ts` is the existing example of a passthrough that filters results.
**Identical signature means passed through, not translated.** If a parameter cannot be passed through, stop and say what would have to be built beneath it — that is the real decision, and it is being made by the parameter (`.project/rules/nextgraph/surface-mirrors-the-sdk.md`). Do not invent an ergonomic wrapper that an application would have to unlearn.
**Open questions to carry or close — do not invent past them:**
- If T01.b left the **deletion** shape unestablished, this child does **not** specify one. Report it and publish the rest.
- **A write path cannot be rights-filtered the way a read is.** The filter is a read emulation. `packages/polyfill/src/surface/ng-proxy.ts` already overrides `sparql_update` with an emulated write guard — read it before deciding what `insertObject` does with an unauthorized write. Do not invent a write guard the target does not have; if the answer is "the existing guard covers it", say which line does.
- If T01.b's arbitration for `OrmSubscription` / `DiscreteOrmSubscription` / `ngSession` is a proposal the user has not confirmed, and acting on it would **remove or rename** something, stop and ask. Adding is reversible; removing a published symbol is a major that a caller pays for.
**This child owns the contract edit and the version bump for its own change.** Edit `.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md` — `## Surface`, `## Guarantees` (what the filter promises about what comes back), `## Non-guarantees`, `## Change policy`. By the contract's own rules, symbols added with nothing existing moved is a **minor**; if the arbitration removes or narrows anything it is a **major**. Apply the rule, do not assume a level. No other child edits the contract for this change.
**Also in scope:** `packages/polyfill/src/index.ts` (the export and its `@provenance` lines — each new symbol needs one, in the form T01.a installed), `test/vocabulary.test.ts` and `test/app-surface.test.ts` (they pin the export inventory), `test/provenance.test.ts` coverage, `docs/api-contract.md`, `packages/polyfill/README.md`, `packages/polyfill/docs/polyfill-reference.md`. New behaviour needs new tests, including the rights case: a second identity's data must not come back.
**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 deleting or skipping a test, no widening an assertion.
## Progress
## Result
+35
View File
@@ -0,0 +1,35 @@
---
id: T01.f
title: Remove readUnion, and the documents that cite it
parent: T01
depends_on: [T01.e]
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
Remove `readUnion` — and `UnionSubject` with it, if no published signature still uses it — from the published surface entirely, once nothing internal calls it. The remaining internal single-document reads become **anchored SPARQL queries**.
**Why it goes.** It has no counterpart upstream: it is N calls plus a batch policy. Its head annotation reads `kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:sparql_query` — a citation that is exact, attached to a claim that is false, which is the worst failure mode of the annotation mechanism because a reader checks it, finds it correct, and moves on (`.project/rules/nextgraph/surface-mirrors-the-sdk.md`). A **composition cannot be aligned on a single function**.
**Call sites at planning time — verify, they may have moved.** In `src`: `packages/polyfill/src/emulated-verifier/index-deposit.ts:253` and `:304`, both single-document reads; `packages/polyfill/src/surface/watch-shape.ts:351`, which T01.d should have removed — confirm it is gone rather than assuming. Outside `src`: `examples/notebook/app.ts:108` and `:130`; `packages/polyfill/e2e/polyfill-entry.ts:402, 425, 427, 675`. Tests that exercise it: `test/read-model.test.ts`, `test/open-repo.test.ts`, `test/cross-user-access.test.ts`, `test/reload-own-document.test.ts`, `test/watch-shape.test.ts`, `test/inbox-dispatch.test.ts`, `test/listing-surfaces-failure.test.ts`, `test/deferred-inbox-processing.test.ts`. The anti-cheat applies in full: a test that proved behaviour **through** `readUnion` is re-pointed at whatever replaces it, never deleted and never weakened. `test/open-repo.test.ts` in particular uses it as the only integration path onto `ensureRepoOpen` — that coverage has to survive by another route.
**Anchored, not anchorless.** The mechanics are level-1 VERIFIED and recorded in `docs/api-contract.md` § 6: an anchor restricts the query to that repo's graph as default graph (`resolve_target_for_sparql`, `engine/verifier/src/request_processor.rs`), while an **anchorless** query unions every named graph in the session store. Replacing several anchored reads with one anchorless query would be a silent widening of what a read can reach — do not do it.
**Two documents cite it, and both must be revised.**
1. `.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md` — `## Surface` (the signature and the `UnionSubject` interface) and `## Non-guarantees`, where the entry for the `useShape` broker bug currently tells applications *"Use `watchShape` for a reactive read and `readUnion` for a one-shot one — neither goes through that path"*. With `watchShape` renamed (T01.c), possibly re-pathed (T01.d), and `readUnion` gone, that published workaround **for an open MAJOR bug** no longer exists. Say what replaces it, or say plainly that nothing does. Do not leave the sentence standing.
2. The parent task states that `ng-helpers`' contract cites `readUnion` and must be revised. At planning time a grep of `.project/concepts/e2e-harness/ng-e2e-helpers/` found **no** such citation — neither `contract_ng-e2e-helpers.md` nor `usage_polyfill-e2e.md` names it — while the e2e **code** under `packages/polyfill/e2e/` uses it heavily. Find where the citation actually lives before revising anything; if it turns out to be only the code, record that in `## Result` so the parent's claim is corrected rather than silently dropped.
**This child owns the contract edit and the version bump for its own change** — a published symbol removed is a **major** by the contract's own `## Change policy`. No other child edits the contract for this change.
**Also in scope:** `packages/polyfill/src/surface/read-model.ts` (the module goes if nothing else lives in it), `packages/polyfill/src/index.ts`, `test/vocabulary.test.ts` and `test/app-surface.test.ts`, `docs/api-contract.md`, `packages/polyfill/README.md`, `packages/polyfill/docs/polyfill-reference.md`, and the `docs/` briefs only where they state something that is now false.
**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 deleting or skipping a test, no widening an assertion.
## Progress
## Result
@@ -0,0 +1,32 @@
---
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]
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
An adversarial pass over the **whole** published surface — every symbol exported from `packages/polyfill/src/index.ts`, not only the ones this batch touched — trying to **refute** the parent task's success criterion: *every published symbol either mirrors an SDK symbol in name, parameters and return, or carries a justified annotation*.
**Method, in this order.** For each published symbol ask the two questions of `.project/rules/nextgraph/no-divergence-from-nextgraph.md` in their stated order: *does the target do this?* **first**, verified in `nextgraph-rs` or stated by its author; *would an application have to unlearn it?* second, and only to rank severity — never to authorise a divergence. Then, for the name and the shape (`.project/rules/nextgraph/surface-mirrors-the-sdk.md`): is this the target's own name, does the verb belong to something else upstream, is the parameter passed through or translated, is the return the SDK's.
**Check every `@provenance` line against the CLAIM, not just against the tree.** Same granularity on both sides: a function may cite a function; a composition, a pipeline or a policy cites nothing at level 2 and must say so. *"The reference resolves"* is not *"the annotation is true"* — that is precisely how `readUnion` survived. Use the mechanism as T01.a left it, including whatever the new kind for an announced-but-unwritten target requires as evidence.
**Known, and not findings.** The three open `bug_` leaves in `.project/concepts/app-contract/`: `bug_useshape-live-read-needs-a-newer-broker` (MAJOR), `bug_a-lost-inbox-watch-is-never-retried` (MAJOR), `bug_an-empty-inbox-list-can-mean-a-failure`. Do not re-report them as new. **Do** report it if a change in this batch altered their status, or invalidated a workaround they or the contract publish — the `useShape` bug leaf and the contract's `## Non-guarantees` both name `watchShape` and `readUnion` as the unaffected paths, and both were changed here.
**Check the contract as delivered**, since three children edited it in sequence: `## Surface` matches the actual exports symbol for symbol; `## Change policy` records a version whose bump level follows the contract's own rules for what actually changed; no section was half-edited or edited twice for the same change; nothing published is left described by a sentence that is no longer true.
**Findings are leads, not mandates, and this child fixes nothing.** A finding without a reproducible failure scenario or a checkable citation does not count. Report symptom, precise location (`file:line`, the failing assertion, the offending extract) and, where it is already known, the cause — but do not instrument, bisect or repair. Each fix is a new task.
**Deliverable.** In `## Result`: findings by severity (blocking / major / minor), atomic, numbered `1.1`, `1.2`, … so they can be answered precisely. Neutral tone, no first or second person. One line of verdict on the test suites, not a list of what passes.
**Pass condition for this child** (it writes no code): run `bun test`, `bunx tsc --noEmit -p packages/polyfill/tsconfig.json`, `bunx tsc --noEmit -p packages/polyfill/e2e/tsconfig.json` and `python3 ~/.claude/skills/concept/lint.py` to **establish** the delivered state and report it verbatim; the review covers every symbol exported from `packages/polyfill/src/index.ts`, with the ones found clean stated as a count rather than an inventory.
## Progress
## Result