Skip to content

[Adapters] Scaffold @otta-sh/store-emdash with the StorageAccess port and a real-repository dialect harness - #247

Merged
vedanshujain merged 1 commit into
feat/in-process-commercefrom
feat/store-emdash-scaffold
Sep 13, 2026
Merged

vedanshujain merged 1 commit into
feat/in-process-commercefrom
feat/store-emdash-scaffold

Conversation

@vedanshujain

Copy link
Copy Markdown
Contributor

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 structural StorageAccess port — the nine methods an adapter needs, the five plain reads/writes plus the conditional-write group updateIf/getVersioned/compareAndSet/compareAndDelete — that production satisfies with ctx.storage and tests satisfy with a real PluginStorageRepository; 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 own runMigrations rather than a hand-built table. The package also ships small IdGen and Clock implementations 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

  • The port names host types only via import type; nothing in src/ imports host code at runtime.
  • Three new dependency-cruiser rules enforce that boundary: a package-wide quarantine on React and console component libraries (tests included), a src rule banning any runtime import of the host or a sibling IO package, and a src rule 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.
  • A compile-time assertion checks that the host's PluginContext["storage"] is assignable to the port, so the port can't silently drift from what the host actually provides.
  • The harness uses a per-file Postgres schema, truncating tables between tests rather than recreating them — the storage table's revision column is assigned by a trigger the migration creates, and recreating the table would leave every compare-and-set looking at a revision that never changes.
  • The function that materializes declared indexes into physical indexes is not exported by the host, so the harness composes indexes and uniqueIndexes into 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.
  • The emdash peer 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

Check Result
pnpm lint (incl. dependency-cruiser) clean
pnpm typecheck clean
pnpm format clean
Tests, sqlite tier 17 passed / 16 skipped
Tests, Postgres tier 32 passed / 1 skipped, incl. a 10-way concurrent compareAndSet on one revision with exactly one winner and every loser a retryable error
Host-pin suite 4 passed
pnpm -r build ok; emitted JS carries no runtime import of the host
Dependency-cruiser plants (4 planted imports) runtime host import in src fails; react in a test fails; type-only pg import in src fails; type-only host import in src passes

Review

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

… 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
@vedanshujain
vedanshujain merged commit 185d618 into feat/in-process-commerce Sep 13, 2026
1 check passed
@vedanshujain
vedanshujain deleted the feat/store-emdash-scaffold branch September 13, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant