[Adapters] Scaffold @otta-sh/store-emdash with the StorageAccess port and a real-repository dialect harness - #247
Merged
vedanshujain merged 1 commit intoSep 13, 2026
Conversation
… and a real-repository dialect harness
The package the commerce adapters will live in, and the two things that have to
exist before any of them can be written: the port they bind to, and a harness
that runs that port against a real host repository rather than a stand-in.
`src/storage-access.ts` is the seam. It declares exactly the nine methods an
adapter needs — the five plain reads and writes, and the conditional-write group
`updateIf` / `getVersioned` / `compareAndSet` / `compareAndDelete` — written in
terms of the host's own storage types, imported as types only. The filter
algebra and the result unions are named once rather than copied; three of them
(`WhereClause`, `QueryOptions`, the query page) the host does not export at all,
so they are derived from the collection interface it does export, which is the
one spelling that cannot drift. The two error shapes it exports no class for are
restated as structural interfaces with predicates, because an error crossing the
sandbox bridge arrives as a plain object carrying the fields, never as an
instance of anything. Production injects `ctx.storage` and tests inject a real
repository; because that is the only surface an adapter sees, changing which
build of the host supplies it is a dependency change, not an adapter rewrite.
`collectionOf` is the one audited narrowing from the untyped map to a typed
collection, so an adapter states its document type once instead of casting per
call site, and a collection the descriptor never declared fails by name.
Three depcruise rules replace the blanket EmDash ban this package had to be
exempted from, and the exemption is a handoff rather than a hole — which is worth
spelling out, because the first version of it claimed "nothing is lost" while
leaving `test/**` free to import react. What is actually true:
`store-emdash-runs-no-host-code` bans `emdash` and @emdash-cms/* in `src` as
runtime imports and permits type-only ones, and it is its own rule precisely so
that allowance cannot leak — written as one merged clause it also permitted
`import type { Pool } from "pg"`, which is how a module starts being written
against a host it must never touch. `store-emdash-is-sandbox-clean` carries the
perimeter with no type-only escape: no DB driver, no filesystem or socket
builtin, no HTTP client, no sibling server package, matched by negative lookahead
so a future store package is banned the day it is created.
`store-emdash-no-console-react` binds the WHOLE package, tests included, because
no legitimate Node test here imports react. Four plants prove each edge: runtime
host import fails, `react` in a test fails, a type-only host import passes, a
type-only `pg` import fails.
The harness builds its collections out of real `PluginStorageRepository`
instances over in-memory SQLite and, when a Postgres connection string is
present, over a schema of its own. One database per test FILE, rows cleared
between cases: the migration set is 77 migrations and a database per test cost
~2.5s a case on Postgres for no isolation the reset does not give. Emptying the
table is also the only reset that KEEPS what the schema is for — the storage
table's revisions are assigned by a trigger a migration creates, so a hand-built
or recreated table would leave every compare-and-set looking at an unchanging
revision and quietly agreeing with itself. The Postgres migration call carries
the sibling package's three-attempt retry for the reason recorded there, and
teardown ends the admin pool and drops the schema through `finally`, so a
rejecting `destroy()` cannot leak a connection pool or litter a shared database.
Declared indexes reach each collection through the repository's constructor
argument, indexes and unique indexes composed exactly as the host composes them.
The suite pins the round trip, the indexed query with ordering, the page ceiling
and one page past it, `count`, `delete`, the guarded decrement that applies once
and then stops at its guard, the guarded update that never inserts,
create-if-absent, the stale-revision refusals for both set and delete, and the
refusal to query a field the collection never declared — asserted on the field,
not on the host's wording. A collection declared with a unique index proves the
composed allow-list is real, and the README records what it does not buy: the
host's index-materializing function is unexported, so no physical index exists in
either tier, uniqueness is enforced nowhere here, and once-only has to come from
a conditional write. Postgres adds the case SQLite structurally cannot express,
because it serializes writes in one process: ten concurrent compare-and-sets on
one revision, exactly one applying, every loser either refused or retryably
aborted — nothing else counts as losing — and the surviving document the
winner's.
Two build-level facts, stated where they can be checked. The host is external to
the bundle: left bundlable, the declaration rollup walks its whole type graph and
fails. And the `emdash` specifier is the plain registry version even though no
published release carries the primitives this port is written against — so
adopting a release is a one-line change — which makes the workspace override
load-bearing in the meantime, and the package description, README and changeset
all say so rather than letting a pin imply a compatibility that does not hold.
The package also declares no `@types/node`: pinning one of the host's peers
differently from every other consumer makes pnpm materialize a second copy of the
same host tarball, which the host-pin suite counts and fails on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NjdC8awspUte5wML6eY2X
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
New adapter package
@otta-sh/store-emdash. It gives the commerce adapters two things that have to exist before any of them can be written: a local structuralStorageAccessport — the nine methods an adapter needs, the five plain reads/writes plus the conditional-write groupupdateIf/getVersioned/compareAndSet/compareAndDelete— that production satisfies withctx.storageand tests satisfy with a realPluginStorageRepository; and a dialect harness that runs that port against real repository instances on both better-sqlite3 and Postgres, with schema for both tiers coming from the host's ownrunMigrationsrather than a hand-built table. The package also ships smallIdGenandClockimplementations the adapters will depend on. This is the second increment of work order 02 (plans/work-order-02-fold-service-into-plugin.md).Design points
import type; nothing insrc/imports host code at runtime.srcrule banning any runtime import of the host or a sibling IO package, and asrcrule banning DB drivers, filesystem/socket builtins and HTTP clients outright, with a type-only allowance carved out for the host import the port needs.collectionOf<T>is the single audited narrowing from the untyped storage map to a typed collection; a collection the descriptor never declared fails by name.PluginContext["storage"]is assignable to the port, so the port can't silently drift from what the host actually provides.indexesanduniqueIndexesinto the repository constructor exactly as the host does. Physical unique indexes are therefore not materialized in either test tier — recorded as a known gap in the package README — so once-only enforcement has to come from a conditional write rather than a database constraint.emdashpeer stays pinned to the exact registry version (0.37.0) per the work order's de-vendoring rule, even though the conditional-write primitives the port is written against exist only in the vendored build right now; the manifest, README and changeset all say so rather than letting the pin imply a compatibility that doesn't hold yet.Verification
pnpm lint(incl. dependency-cruiser)pnpm typecheckpnpm formatcompareAndSeton one revision with exactly one winner and every loser a retryable errorpnpm -r buildsrcfails;reactin a test fails; type-onlypgimport insrcfails; type-only host import insrcpassesReview
Two independent reviews: round 1 requested changes on the quarantine rule, race assertions and harness hardening; round 2 both approve; an independent verification run passed.
🤖 Generated with Claude Code
https://claude.ai/code/session_011NjdC8awspUte5wML6eY2X