Files
ng-eventually/.tasks/done/T01.e-publish-the-orm-family.md

9.0 KiB

id, title, parent, depends_on, created, claimed_by, claimed_at, blocked
id title parent depends_on created claimed_by claimed_at blocked
T01.e Publish the ORM family with the rights filter T01
T01.b
T01.d
2026-09-16 worker-T01.e 2026-09-21 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.

Resolved since planning (2026-09-16). A write is not rights-filtered like a read: upstream bounds a write by write capability, not by hiding results. So insertObject gets a GUARD BEFORE the write, following the package's existing pattern (assertOwnInbox, mustNotAttempt, assertMayReach) — never a filter over what it returns.

Resolved since planning (2026-09-21), and it overturns T01.b's proposal. NO SDK element is published as-is, ever. Re-exporting one hands the caller a path straight to the SDK that SHORT-CIRCUITS this package's emulation - the capability filter, and above all the shared-wallet identity boundary. T01.b proposed publishing OrmSubscription unchanged; that proposal is void. Every symbol this package offers from the ORM family is WRAPPED, at the same name and signature, with the emulation applied. If a symbol cannot be wrapped meaningfully, it is not published at all - that is a valid outcome, not a failure.

Note also: OrmSubscription was renamed RdfOrmSubscription in @ng-org/orm@0.1.2-alpha.21, and getOrCreate returns a narrowed RdfOrmSubscriptionFor<...> rather than the class. T01.b's proposal named the old symbol.

Progress

  • Read the two governing rules, T01.b's and T01.d's ## Result, the parent's ## Learnings digest, and knowledge_what-the-orm-writes-and-deletes-upstream.md. Baseline taken first: bun test 408/0, both tsc 0, lint 0 ERROR.
  • Read the ORM family AT THE SOURCE and in the installed @ng-org/orm@0.1.2-alpha.21: getObjects, insertObject, removeObject (NEW, absent from the pinned nextgraph-rs), RdfOrmSubscription, DiscreteOrmSubscription, ngSession, plus getRaw/watch/effect. Decided each against the "nothing published as-is" rule; reasons recorded in src/surface/orm.ts and docs/api-contract.md section 6bis.
  • Published getObjects and insertObject wrapped, at upstream's own names, parameters and returns, injected through configure like the shape query beside them. getObjects's config parameter is RdfOrmConfig (alpha.21's widening of Scope | string), which forced publishing that type from the SDK's /all subpath.
  • Closed the graph_orm_update rights hole in two pieces: the guard (an override in ng-proxy.ts, ownership via assertMayWrite, reading the document out of each patch's path), and the door it sits on (initNg now hands the ORM this package's wrapped ng, because both consumers in the repo hand it the raw SDK object).
  • That substitution turned the e2e RED on the two useShapeQuery journeys; the reference run was green, so it was mine. Measured the cause: makeNg()'s passthrough did Reflect.get(ng, prop).bind(ng), and @ng-org/web's ng is itself a Proxy answering every key - bind included - with a path-carrying callable returning a Promise. The published ng therefore answered all 88 members with a promise. Forwarded with Reflect.apply instead; pinned by a test whose fake is shaped like the real proxy, and which was verified to FAIL on the old line.
  • Contract leaf edited (Scope, Deployment requirements, Surface, Guarantees, Non-guarantees, Change policy), bumped to 1.0.0-dev.7 as a MAJOR under the leaf's own policy, and in package.json. docs/api-contract.md (new section 6bis, sections 2/3/14 and the appendix), docs/internal-contract.md, README.md, docs/polyfill-reference.md updated.

Result

Done. getObjects and insertObject are published WRAPPED, at upstream's names, parameters and returns, injected through configure. The read is capability-filtered per @graph (collection shape preserved); the write takes assertMayReach + assertMayWrite BEFORE the call. graph_orm_update now carries the same ownership guard in ng-proxy.ts, and initNg substitutes this package's wrapped ng so the ORM knocks on that door - the rights hole is closed. NOT published, each with a checkable reason: RdfOrmSubscription (pooled with no identity in the key), DiscreteOrmSubscription, ngSession, getRaw, watch/effect, and removeObject (exists in alpha.21, absent from the pinned nextgraph-rs, so no citation resolves). 1.0.0-dev.7, MAJOR: an ORM write into a document you do not own is now refused.

Found and fixed, MEASURED. makeNg()'s passthrough did .bind(ng); the SDK's ng is itself a Proxy answering every key including bind, so the published ng returned a PROMISE for all 88 members - orm_start_graph is not a function, swallowed by the ORM, read pending for ever. Now Reflect.apply. The e2e proves it only since initNg routes the ORM through the wrapper.

Gate. bun test 424 pass / 0 fail; both tsc exit 0; lint 0 ERROR; == Polyfill e2e summary: 42 passed, 0 failed, 42 total - batch 3.3 min, slowest cold sync 27s ==.

Follow-ups. examples/notebook fails its OWN tsc on HEAD already (its shape literal needs satisfies). discrete_orm_update is unguarded. removeObject awaits a refreshed nextgraph-rs.