10 KiB
id, title, parent, depends_on, created, claimed_by, claimed_at, blocked
| id | title | parent | depends_on | created | claimed_by | claimed_at | blocked | |
|---|---|---|---|---|---|---|---|---|
| T01.c | watchShape becomes useShapeQuery and takes the SDK's Scope | T01 |
|
2026-09-16 | null | null | 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.
Resolved since planning (2026-09-16). Our Scope type is NOT renamed — it DISAPPEARS. A renamed invented name is still an invented name. This extends to creation: the earlier note that "the store axis survives for creation" was preserving an invention because it looked necessary. Upstream docCreate(sessionId, crdt, cls, dest, store?) does take a store, so the NOTION survives; what is invented is our three-literal encoding. The creation path aligns on docCreate's own parameters. listMyEntityDocs is DELETED: no upstream equivalent exists (no doc_list, no list_docs, nothing in the binding), and listing one's documents is an ordinary query over the store graph's ldp:contains — sugar over an expressible query, the same shape as readUnion.
Progress
2026-09-16 — the open question, settled in the open. The planning note left two things unsettled: what the placement axis should be called once the SDK's Scope takes the name, and whether listMyEntityDocs survives. The Resolved block answered the second (deleted) and refused a rename of the first (Scope disappears, a renamed invented name is still invented). That forces a third answer the note did not ask for, because resolveScopeGraph and resolveWriteGraph took the same three literals and could not keep them.
What the three literals actually are. They are not this package's invention as VALUES: local_broker::doc_create(session_id, crdt, class_name, destination, store_type, store_repo) takes a store_type of exactly "public" | "protected" | "private" and turns it into a StoreRepo with from_type_and_repo. What was invented is publishing them under the name Scope, for a READ. So the type survives as StoreType, DEFINED in model/types.ts and published nowhere - the standing ReadCap and InboxScope already have.
How an application names a store now. By its REFERENCE, which is what doc_create takes and what upstream's Session hands out (private_store_id, protected_store_id, public_store_id). Three accessors, one per store, and createEntityDoc(store?) takes one of them - private when omitted, as upstream. A reference that is not one of the connected identity's three is refused rather than silently placed.
Verified obstacle, which is why the reference is the per-store INDEX document and not the session store NURI: in this emulation public and protected are co-located on one physical store id (store-registry.test.ts pins resolveScopeGraph("public") === resolveScopeGraph("protected")), so a session store NURI cannot tell the two apart.
resolveScopeGraph unpublished, not re-parameterised. Its stated use was a READ scope, and knowledge_what-a-read-scope-is-upstream establishes that a store's graph holds its containment triples and nothing else - so upstream that query matches nothing and says so with an empty set. resolveWriteGraph returned the very reference the three accessors return. Both stay internal; the e2e isolation probe reaches resolveScopeGraph by its internal path, which is what a harness may do.
The translation, and why it is only half a translation. graphs naming documents needs none - it IS the doc set. Only did:ng:i / "" does, and it is served by enumerating the identity's documents across the three stores. subjects is applied literally to the union's subject IRIs, as FILTER(?s IN (…)) is upstream. [] / undefined returns no objects at all, as Scope's own documentation states.
The example application is where the removal bites. myNotes(scope) was listMyEntityDocs + readUnion, and there is no per-store read to replace it with. It now holds ONE useShapeQuery over { graphs: "did:ng:i" } and writes an rdf:type triple so the shape can match. The shelf selector governs where the next note is WRITTEN and nothing else. Every list assertion in the applicative e2e is an includes positive or a cross-identity negative, so all of them survive a global list.
Result
Done. watchShape is useShapeQuery and takes the SDK's Scope ({ graphs, subjects }, re-exported from @ng-org/orm); ShapeQuery and the capability filter are unchanged. The annotation is announced-not-written, copied verbatim from the rule. Our three-literal Scope is gone from the published surface: it is StoreType, defined in model/types.ts, published nowhere. A store is now named by its REFERENCE - storeRegistry.privateStore() / protectedStore() / publicStore() - and createEntityDoc(store?) takes one, private when omitted, as doc_create does. listMyEntityDocs, resolveScopeGraph and resolveWriteGraph are unpublished (still internal). Contract leaf rewritten with a symbol-by-symbol migration note; 1.0.0-dev.5.
Files. packages/polyfill/src/{index,model/types,surface/placement,surface/use-shape-query,shared-wallet/account-registry,emulated-verifier/caps,emulated-verifier/branch-registers}.ts, 13 test files, e2e/{polyfill-entry,run,notebook}.ts, examples/notebook/app.ts, docs/api-contract.md, both READMEs, contract_polyfill-surface.md, package.json.
Gate. bun test 401 pass / 0 fail; both tsc exit 0; concept lint 0 ERROR; grep on index.ts no match.
Surprises. VERIFIED: the three literals are upstream's own store_type, so only the NAME was invented. VERIFIED: public and protected share one store id here, so the store reference must be the index document. VERIFIED: listMyEntityDocs is still the read path's - T01.d removes the last caller.
Follow-ups. The whole-dataset read now opens three store documents instead of one. surface-mirrors-the-sdk.md still calls the rename "not yet applied". docs/internal-contract.md, docs/simulation.md and the root README.md still name the removed symbols.