Files
ng-eventually/.tasks/done/T01.f-remove-read-union.md
T

18 KiB

id, title, parent, depends_on, created, claimed_by, claimed_at, blocked
id title parent depends_on created claimed_by claimed_at blocked
T01.f Remove readUnion, and the documents that cite it T01
T01.e
2026-09-16 T01.f-worker 2026-09-21 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

Status: done. 1.0.0-dev.8, major. Two commits: 0c21ade (the removal) and 0a2b8b1 (the documents).

The opening barrier in useShapeQuery was NOT added

The orchestrator first framed the blocker as "no shape read serves a public index", then withdrew it: an index is read BY ANCHOR, not by shape, and docs.sparqlQuery already fetches the public store's key at its door (packages/polyfill/src/surface/docs.ts:155). The barrier had been written and verified against a new test; both were reverted, uncommitted, on instruction. use-shape-query.ts carries one comment edit in this task and nothing else.

What unblocks the removal, and how it was verified

VERIFIED: packages/polyfill/test/cross-user-access.test.ts — "a bare reference is enough for a PUBLIC document, and not for a protected one". It now asserts, under Bob, that he holds no capability for either document, reads the public one through the anchored docs.sparqlQuery, and holds its key afterwards — obtained from the document, not handed across by the test. That is the property readUnion was protecting, and it belongs to the primitive underneath it.

What was removed, and what each call site became

readUnion and UnionSubject are gone from the entry. packages/polyfill/src/surface/read-model.ts is deleted; packages/polyfill/src/surface/read-document.ts replaces it with readDocumentSubjects(doc) — ONE anchored query, INTERNAL, kind=invention (the anchored query itself is docs.sparqlQuery, annotated there; what is ours is the cold-start open and the row grouping).

  • index-deposit.ts (two sites) — the onUnreadable callback became a catch; both still return unreadable, and noRowsMeansAbsent still consults getSyncState, which is why the ensureRepoOpen in front of the read cannot move to the caller.
  • examples/notebook/app.ts, packages/polyfill/e2e/polyfill-entry.ts — each composes its own read over the published docs.sparqlQuery. The two multi-document e2e sites carry a VISIBLE catch and report what they skipped.
  • Tests re-pointed, none deleted or weakened. open-repo.test.ts keeps its integration path onto ensureRepoOpen by the same route. read-model.test.ts → read-document.test.ts: three cases that tested the batch POLICY were re-pointed at what replaced it (a failed read throws; an empty document answers []; two reads never merge), and the file header says which and why.

The per-document tolerance

RELOCATED to the caller, not lost — and shedding it is an alignment gain, since upstream an anchored query on a repo the verifier does not hold rejects (resolve_target_for_sparql → NgError::RepoNotFound). No tolerant batch helper was introduced anywhere.

Two behaviour changes, both recorded in the contract

  1. A document the holder has no capability for is REFUSED out loud instead of dropped from a batch.
  2. Subjects under the reserved urn:ng-eventually: prefix are no longer filtered for an application. No rights move: the only registers an ordinary document carries are its inbox address and, for a public document, the key its store already serves to whoever asks; the cap-bearing inboxCap register lives in its owner's private store.

Does our emulation reject, or answer empty?

VERIFIED: it REJECTS, and it does not swallow. docs.sparqlQuery has no catch — it forwards ng.sparql_query's rejection unchanged — and readDocumentSubjects has none either. The swallow that existed was readDoc inside the deleted module. One nuance, pre-existing: for a document whose public-store cap fetch failed transiently, fetchReadCap returns false and the rejection that reaches the caller is this package's "you hold no cap", not the broker's outage. That conflation is documented in public-store.ts and is not introduced here.

Where ng-helpers cites readUnion

In its OWN published usage_ng-helpers.md against polyfill-surface, under ## Consumed surface and ## Needs — not in any leaf of this repo, and not in contract_ng-e2e-helpers.md / usage_polyfill-e2e.md. The planner's grep was right about where it is NOT; the parent's claim that "ng-helpers' contract cites readUnion" was wrong about which document. Read through the declaration only, never their code.

Follow-ups (not fixed here)

  • useShapeQuery never opens the documents named in scope.graphs, so an application naming an unopened or public-store document gets an empty result with no error. Real, out of scope, probably deserves a bug_ leaf in app-contract.
  • ensureReposOpen now has no production caller (only open-repo.test.ts). Internal dead code.
  • examples/notebook/tsconfig.json does not typecheck — PRE-EXISTING, verified against the branch tip before any edit: the hand-written NoteShape does not satisfy ShapeType<BaseType>.
  • The ng-helpers declaration still reports 1 passed / 29 failed-era breakage and three Needs this removal does not answer; their against: pin is 1.0.0-dev.6 and the surface is now 1.0.0-dev.8.
  • .project/concepts/app-contract/_debt.md carries markers for orm.ts and use-shape-query.ts raised before this task; left alone.
  • The dated docs/briefs/ were deliberately not rewritten: they describe a July state that the documents themselves date.
  • The e2e suite was NOT run (it needs a live broker); it is not part of this task's gate. ` 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.

Resolved since planning (2026-09-16). Do NOT read ng-helpers — not its code, not its tests. The isolation discipline says a counterpart is reached through its engagement and its usage declaration only. The planner's finding (that no contract leaf cites readUnion) was correct but obtained by a forbidden method. If this task needs to know who depends on readUnion, the answer belongs in a usage_ declaration; ng-helpers has never authored one, and producing that is the fix, not a workaround by reading.

Resolved since planning (2026-09-21) - readUnion HAS a declared consumer. ng-helpers published its usage declaration (polyfill-surface--ng-helpers) and it says YES: e2e/indexing-app.ts calls readUnion([doc]) twice, and src/index.ts REPUBLISHES it to ng-helpers' own consumers as the documented way to read an index. No ORM is wired there (useShape: () => undefined), so useShapeQuery is unreachable for them; their only fallback is an anchored docs.sparqlQuery. They declared this as a Need at full strength.

So the removal is not free. Before removing, decide with the owner what a consumer with no ORM reads with. Their declaration is the channel: read it at ng-helpers' repo through its published usage_ leaf, never by reading their code.

Also relevant, and worse than it looks. That declaration reports ng-helpers still calling createEntityDoc("public") and listMyEntityDocs("public"), both invalid since 1.0.0-dev.5, while its own test suite stays green because it runs against a fake that still models the superseded surface. A consumer can therefore be broken against us with nothing red on either side. Do not treat their green suite as evidence.

Update (2026-09-21) - wiring the ORM does NOT unblock the removal, and the reason is ours. ng-helpers wired the ORM as the contract prescribes and re-measured. Its verdict, recorded in its published usage declaration: neither shape read fetches a PUBLIC STORE's key, so a stranger reading a public index from its reference alone - the central case for an index - is served by no shape query at all. readUnion is the only thing that serves it today.

So the removal is blocked by a capability gap on OUR side, not by a consumer's reluctance. Before removing, either that gap closes or the removal does not happen. Read their declaration for the three secondary costs they list; do not read their code.

Their e2e also went from a measured 1 passed / 29 failed (they were already broken against us, with both suites green) to 28 passed / 2 failed, the two remaining being a provider defect: useShapeQuery never starts because orm_start_graph is not a function, swallowed, isPending for ever - measured on three consecutive runs, with the SDK's ng answering that member and initNg discarding its first argument.

Progress

Result

Status: done. 1.0.0-dev.8, major. Two commits: 0c21ade (the removal) and 0a2b8b1 (the documents).

The opening barrier in useShapeQuery was NOT added

The orchestrator first framed the blocker as "no shape read serves a public index", then withdrew it: an index is read BY ANCHOR, not by shape, and docs.sparqlQuery already fetches the public store's key at its door (packages/polyfill/src/surface/docs.ts:155). The barrier had been written and verified against a new test; both were reverted, uncommitted, on instruction. use-shape-query.ts carries one comment edit in this task and nothing else.

What unblocks the removal, and how it was verified

VERIFIED: packages/polyfill/test/cross-user-access.test.ts — "a bare reference is enough for a PUBLIC document, and not for a protected one". It now asserts, under Bob, that he holds no capability for either document, reads the public one through the anchored docs.sparqlQuery, and holds its key afterwards — obtained from the document, not handed across by the test. That is the property readUnion was protecting, and it belongs to the primitive underneath it.

What was removed, and what each call site became

readUnion and UnionSubject are gone from the entry. packages/polyfill/src/surface/read-model.ts is deleted; packages/polyfill/src/surface/read-document.ts replaces it with readDocumentSubjects(doc) — ONE anchored query, INTERNAL, kind=invention (the anchored query itself is docs.sparqlQuery, annotated there; what is ours is the cold-start open and the row grouping).

  • index-deposit.ts (two sites) — the onUnreadable callback became a catch; both still return unreadable, and noRowsMeansAbsent still consults getSyncState, which is why the ensureRepoOpen in front of the read cannot move to the caller.
  • examples/notebook/app.ts, packages/polyfill/e2e/polyfill-entry.ts — each composes its own read over the published docs.sparqlQuery. The two multi-document e2e sites carry a VISIBLE catch and report what they skipped.
  • Tests re-pointed, none deleted or weakened. open-repo.test.ts keeps its integration path onto ensureRepoOpen by the same route. read-model.test.ts → read-document.test.ts: three cases that tested the batch POLICY were re-pointed at what replaced it (a failed read throws; an empty document answers []; two reads never merge), and the file header says which and why.

The per-document tolerance

RELOCATED to the caller, not lost — and shedding it is an alignment gain, since upstream an anchored query on a repo the verifier does not hold rejects (resolve_target_for_sparql → NgError::RepoNotFound). No tolerant batch helper was introduced anywhere.

Two behaviour changes, both recorded in the contract

  1. A document the holder has no capability for is REFUSED out loud instead of dropped from a batch.
  2. Subjects under the reserved urn:ng-eventually: prefix are no longer filtered for an application. No rights move: the only registers an ordinary document carries are its inbox address and, for a public document, the key its store already serves to whoever asks; the cap-bearing inboxCap register lives in its owner's private store.

Does our emulation reject, or answer empty?

VERIFIED: it REJECTS, and it does not swallow. docs.sparqlQuery has no catch — it forwards ng.sparql_query's rejection unchanged — and readDocumentSubjects has none either. The swallow that existed was readDoc inside the deleted module. One nuance, pre-existing: for a document whose public-store cap fetch failed transiently, fetchReadCap returns false and the rejection that reaches the caller is this package's "you hold no cap", not the broker's outage. That conflation is documented in public-store.ts and is not introduced here.

Where ng-helpers cites readUnion

In its OWN published usage_ng-helpers.md against polyfill-surface, under ## Consumed surface and ## Needs — not in any leaf of this repo, and not in contract_ng-e2e-helpers.md / usage_polyfill-e2e.md. The planner's grep was right about where it is NOT; the parent's claim that "ng-helpers' contract cites readUnion" was wrong about which document. Read through the declaration only, never their code.

Follow-ups (not fixed here)

  • useShapeQuery never opens the documents named in scope.graphs, so an application naming an unopened or public-store document gets an empty result with no error. Real, out of scope, probably deserves a bug_ leaf in app-contract.
  • ensureReposOpen now has no production caller (only open-repo.test.ts). Internal dead code.
  • examples/notebook/tsconfig.json does not typecheck — PRE-EXISTING, verified against the branch tip before any edit: the hand-written NoteShape does not satisfy ShapeType<BaseType>.
  • The ng-helpers declaration still reports 1 passed / 29 failed-era breakage and three Needs this removal does not answer; their against: pin is 1.0.0-dev.6 and the surface is now 1.0.0-dev.8.
  • .project/concepts/app-contract/_debt.md carries markers for orm.ts and use-shape-query.ts raised before this task; left alone.
  • The dated docs/briefs/ were deliberately not rewritten: they describe a July state that the documents themselves date.
  • The e2e suite was NOT run (it needs a live broker); it is not part of this task's gate.