Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module.exports = {
forbidden: [
{
name: "domain-is-io-free",
// TODO(#291): this rule still names the deleted `service` package, in the
// comment below and in the last clause of `to.path`. Tracked separately.
comment:
"@otta-sh/domain imports nothing with IO — no pg/kysely/better-sqlite3/hono/http, " +
"and no dependency on adapter/service/plugin packages (DEVELOPMENT.md §3).",
Expand All @@ -23,7 +25,7 @@ module.exports = {
comment:
"@otta-sh/plugin's src (loaded inside the workerd sandbox) has NO DB/" +
"driver, filesystem, process, socket or network-client surface, and no " +
"dependency on a SQL store, the service or a payment adapter. Its egress " +
"dependency on a SQL store or a payment adapter. Its egress " +
"is the injected ctx.http; its commerce truth is the injected ctx.storage " +
"(DEVELOPMENT.md §5, ADR-0018, sandbox-clean guard). The forbidden list is " +
"a superset of domain-is-io-free's, plus HTTP/WS client libs (undici, " +
Expand Down Expand Up @@ -58,12 +60,12 @@ module.exports = {
"importing an ADAPTER can, which is why every adapter except one stays " +
"banned. Second, that one exception: `store-[^/]+` in the packages clause " +
"became `(?!store-emdash/)store-[^/]+`, so packages/store-emdash is " +
"admitted while store-postgres — and any store-* added later — is banned " +
"admitted while any other store-* — including any added later — is banned " +
"by default rather than by anyone remembering to add it — and the same list " +
"is mirrored into the two SPECIFIER clauses, not only the packages " +
"clause, because pnpm's strict isolation leaves an UNDECLARED import as " +
"a bare specifier that never resolves to a packages/ path: naming only " +
"admin-react there meant an undeclared @otta-sh/store-postgres, service " +
"admin-react there meant an undeclared @otta-sh/store-* " +
"or payments-* import tripped nothing at all, which is the same class of " +
"silent miss as the `^node:`-only builtin clause. store-emdash is " +
"admissible because it carries no IO of its own: it is written against a " +
Expand All @@ -87,11 +89,25 @@ module.exports = {
"still be caught — by the driver and node-builtin clauses of this same " +
"rule, which the carve-out does not touch. " +
"packages/plugin/test/depcruise-boundary.test.ts pins both halves: these " +
"two admitted, a third payments-* package still forbidden.",
"two admitted, a third payments-* package still forbidden.\n\n" +
"FOURTH CHANGE (work order 02, INC-D3c): `service` is no longer named in " +
"any of the three clauses, because @otta-sh/service no longer EXISTS — " +
"INC-D3b deleted packages/service (and packages/store-postgres with it) " +
"once the service was folded into the plugin. A ban on a package that " +
"cannot be imported is a clause no fixture can exercise, so it rots " +
"silently: nothing would notice if it stopped matching, which is the same " +
"failure mode as the `^node:`-only builtin clause above. store-postgres " +
"was never named literally — it was caught by the " +
"`(?!store-emdash(/|$))store-[^/]+` lookahead, which is untouched and " +
"still bans every store-* but the one, so a store-postgres reintroduced " +
"tomorrow is forbidden on the day it is created. A reintroduced `service` " +
"package would NOT be, and that is deliberate: after the fold-in " +
"(ADR-0018) a second deployable is a decision that needs its own ADR, not " +
"something a lint rule should pre-judge on a name.",
severity: "error",
from: { path: "^packages/plugin/src" },
to: {
path: "(node_modules/(pg|pg-pool|kysely|better-sqlite3|workerd|hono|node-fetch|undici|axios|ws)(/|$)|node_modules/@otta-sh/((?!store-emdash(/|$))store-[^/]+|service|(?!payments-(stripe|x402)(/|$))payments-[^/]+|admin-react)(/|$)|^(pg|pg-pool|kysely|better-sqlite3|workerd|hono|node-fetch|undici|axios|ws)(/|$)|^@otta-sh/((?!store-emdash(/|$))store-[^/]+|service|(?!payments-(stripe|x402)(/|$))payments-[^/]+|admin-react)(/|$)|^(node:)?(fs|child_process|net|http|https|os|dgram|dns|tls|worker_threads|cluster|vm)(/|$)|^packages/((?!store-emdash(/|$))store-[^/]+|service|(?!payments-(stripe|x402)(/|$))payments-[^/]+|admin-react)/)",
path: "(node_modules/(pg|pg-pool|kysely|better-sqlite3|workerd|hono|node-fetch|undici|axios|ws)(/|$)|node_modules/@otta-sh/((?!store-emdash(/|$))store-[^/]+|(?!payments-(stripe|x402)(/|$))payments-[^/]+|admin-react)(/|$)|^(pg|pg-pool|kysely|better-sqlite3|workerd|hono|node-fetch|undici|axios|ws)(/|$)|^@otta-sh/((?!store-emdash(/|$))store-[^/]+|(?!payments-(stripe|x402)(/|$))payments-[^/]+|admin-react)(/|$)|^(node:)?(fs|child_process|net|http|https|os|dgram|dns|tls|worker_threads|cluster|vm)(/|$)|^packages/((?!store-emdash(/|$))store-[^/]+|(?!payments-(stripe|x402)(/|$))payments-[^/]+|admin-react)/)",
},
},
{
Expand Down Expand Up @@ -196,6 +212,8 @@ module.exports = {
"the plugin rule's comment sets out. Every case this rule and " +
"the plugin rule turn on are executed in " +
"packages/plugin/test/depcruise-boundary.test.ts.",
// TODO(#291): this rule still names the deleted `service` package, in the
// comment above and in three clauses of `to.path`. Tracked separately.
severity: "error",
from: { path: "^packages/store-emdash/src" },
to: {
Expand Down Expand Up @@ -276,7 +294,7 @@ module.exports = {
"static import would be a second one — compiled in, invisible to the " +
"empty capability set and to the empty allowedHosts that are this " +
"descriptor's only declared controls. It is also, for the server " +
"packages (domain/service/store/payments), Node and database code " +
"packages (domain/store/payments), Node and database code " +
"reached from a module that ships to a BROWSER. So: no workspace " +
"package, in either direction. The consequence is deliberate and has " +
"one known bill to pay — INC-20 owes the React tier a formatMoney, and " +
Expand Down
10 changes: 5 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ Operational guide for Claude working in this repo. The **why** lives in
conventions, and the guardrails that must not be crossed.

> **Status: shipped, pre-1.0.** Phases 0–7 are merged and the full toolchain below is wired —
> `@otta-sh/domain`, `@otta-sh/service`, the storefront/admin adapters, and the EmDash plugin
> all exist under `packages/`. Treat the commands below as live, not aspirational; if one
> genuinely doesn't exist, say so rather than inventing output.
> `@otta-sh/domain`, the EmDash plugin (which now carries the commerce service in-process),
> `@otta-sh/store-emdash`, the payment adapters and the React admin all exist under
> `packages/`. Treat the commands below as live, not aspirational; if one genuinely doesn't
> exist, say so rather than inventing output.

---

Expand Down Expand Up @@ -72,8 +73,7 @@ changed. Migrations are forward-only.
| Area changed | Tag |
|---|---|
| `@otta-sh/domain` (ports, use-cases, invariants) | `[Domain]` |
| `@otta-sh/service` (REST API, HTTP serialization) | `[Service]` |
| Store/client/payment **adapters** (postgres, sqlite, d1, stripe, x402) | `[Adapters]` |
| Store/client/payment **adapters** (store-emdash, stripe, x402) | `[Adapters]` |
| The EmDash **plugin** (storefront, Block Kit panel, sync hooks) | `[Plugin]` |
| `sites/*` (the reference storefront site/theme) | `[Site]` |
| Shared test/contract packages | `[Test]` |
Expand Down
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ Pick the tag for the area your change touches:
| Area changed | Tag |
|---|---|
| `@otta-sh/domain` (ports, use-cases, invariants) | `[Domain]` |
| `@otta-sh/service` (REST API, HTTP serialization) | `[Service]` |
| Store/client/payment **adapters** (postgres, sqlite, d1, stripe, x402) | `[Adapters]` |
| Store/client/payment **adapters** (store-emdash, stripe, x402) | `[Adapters]` |
| The EmDash **plugin** (storefront, Block Kit panel, sync hooks) | `[Plugin]` |
| Shared test/contract packages | `[Test]` |
| CI / tooling / build | `[CI]` |
Expand Down
12 changes: 8 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,14 @@ domain is a build-breaking bug, not a code-review nit.

- Enforce the boundary with a dependency check (dependency-cruiser or an import-restriction
lint rule) wired into `lint`, so the layering can't rot silently.
- **HTTP mirrors the port 1:1.** The REST API in `@otta-sh/service` is a serialization of the
domain use-cases — no endpoint has semantics the port lacks, no status-code-as-logic. The
same client-side contract suite runs against `HttpCommerceClient` (over a live test
server) so the wire format can't drift from the port.
- **There is no wire to keep in step.** Commerce runs in-process: the plugin builds
`InProcessCommerceClient` through its single composition root, `makeCommerceClient`, which
binds the `@otta-sh/domain` use-cases to the `@otta-sh/store-emdash` stores over
`ctx.storage` (ADR-0018). No REST API, no `@otta-sh/service`, no serialization layer that
could drift from the port. The behavioral contract suite that used to run twice — once
over HTTP against a live test server, once in-process — still runs every one of those
cases, now against that single tier, over a real document store, with `ctx.http` bound to
a rejecting stub so an accidental egress fails the suite.
- **Add an adapter only when a second real implementation exists.** No speculative
`EmdashStore` / `InProcessCommerceClient` before the EmDash primitive ships.

Expand Down
28 changes: 16 additions & 12 deletions packages/plugin/test/depcruise-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ const STUB_PACKAGES = [
"domain",
"admin-react",
"store-emdash",
"store-postgres",
// A hypothetical sibling SQL store, resolvable on purpose: it is how the
// RESOLVED half of the store ban (`^packages/…store-[^/]+/`) gets exercised,
// the bare-specifier half being covered by the unresolved `store-d1` cases.
// Deliberately not a real package name — the rule bans every store-* but
// store-emdash by lookahead, so the fixture must not depend on any particular
// adapter continuing to exist (`store-postgres`, which this stub replaced,
// was deleted in INC-D3b).
"store-sqlite",
"payments-stripe",
"payments-x402",
"plugin",
Expand Down Expand Up @@ -188,20 +195,17 @@ describe("plugin-is-sandbox-clean: what the plugin perimeter forbids", () => {
).toEqual(["plugin-is-sandbox-clean"]);
});

test("an unresolved service import is forbidden, for the same reason", () => {
test("a RESOLVED SQL store adapter is still forbidden — the narrowing admitted one store, not every store", () => {
// The counterpart to the case above: this name IS linked into the fixture's
// node_modules, so dependency-cruiser reports it as a `packages/…` path and
// the third clause is what has to catch it. INC-D3c dropped the deleted
// `service` case that used to sit here; the store ban is a lookahead over
// the whole family, so it is exercised by a stand-in rather than by whichever
// SQL adapter happens to exist this month.
expect(
rulesViolatedBy(
"plugin",
'import { stub } from "@otta-sh/service";\nexport const x = stub;\n',
),
).toEqual(["plugin-is-sandbox-clean"]);
});

test("a SQL store adapter is still forbidden — narrowing admitted one store, not every store", () => {
expect(
rulesViolatedBy(
"plugin",
'import { stub } from "@otta-sh/store-postgres";\nexport const x = stub;\n',
'import { stub } from "@otta-sh/store-sqlite";\nexport const x = stub;\n',
),
).toEqual(["plugin-is-sandbox-clean"]);
});
Expand Down
Loading