Type-safe contracts for AMQP/RabbitMQ messaging with automatic runtime validation. TypeScript ESM monorepo using pnpm catalogs and Turbo.
| Rule | Read this when… |
|---|---|
| Project Overview | Orienting yourself in the repo, looking up which package owns X |
| Commands | Running anything (pnpm, turbo filters, integration tests) |
| Contract Patterns | Defining or modifying a contract (publishers/consumers/RPCs) |
| Handlers | Writing worker handlers, dealing with AsyncResult / unthrown |
| Runtime | Touching connections, telemetry, compression |
| Code Style | Reviewing or writing TS — composition, anti-patterns |
| Testing | Adding unit or integration tests, using the testing fixtures |
| Build & Release | Adding a package, modifying tsdown config, shipping a release |
| Dependencies | Adding a dep, picking a schema lib, catalog questions |
| Recipes | "How do I add a new consumer / RPC / publishable package?" |
This is the canonical list — sub-files reference these rather than restating them.
- No
any— useunknownand narrow (enforced by oxlint). - Type aliases over interfaces —
type Foo = {}, notinterface Foo {}. .jsextensions required in all imports (ESM).- Standard Schema v1 for validation (Zod, Valibot, ArkType — anything that implements the spec).
- Error handling uses
unthrown(not neverthrow). It adds a thirdDefectchannel for unexpected throws alongsideOk/Err. - Handlers return
AsyncResult<void, HandlerError>(regular consumer) orAsyncResult<TResponse, HandlerError>(RPC). Noasync/awaitin handler signatures. await asyncResultresolves to aResult<T, E>— it does not throw onErr.result.match({ ok, err, defect })is boxed and has three branches. Positionalmatch(okFn, errFn)is the old neverthrow shape and is not supported.- Build async results with
Ok(value).toAsync()/Err(error).toAsync()— there is nookAsync/errAsync. - Wrap promises with the free function
fromPromise(promise, qualify)(not a staticResultAsync.fromPromise);qualify(cause, defect)maps the rejection reason toE(or, for an unexpected failure, a defect via thedefectcallback it receives —defect(cause)) and is required. There is no standaloneDefectimport in unthrown 3.x. .isOk()/.isErr()/.isDefect()are type guards that narrowthis(since unthrown 0.2.0), soif (result.isErr()) result.errorworks. Prefer the method form — the standaloneisOk(result)/isErr(result)/isDefect(result)functions narrow identically but are not used in this codebase.
- Quorum queues by default. Classic queues only for features quorum doesn't support (
exclusive,autoDelete,maxPriority). - Composition pattern — define resources first, then reference; never inline inside
defineContract.
- Catalog dependencies via
pnpm-workspace.yaml— never hardcode versions inpackage.json. - Conventional commits required (feat, fix, docs, chore, test, refactor, ci, build, perf, revert, style). Enforced by commitlint on
commit-msg. - Git hooks: lefthook runs
oxfmtandoxlintonpre-commit; commitlint oncommit-msg.pnpm typecheckis not in the hook, so run it before pushing if you changed types.
Before doing any of the following, confirm with the user:
- Pushing to
main, force-pushing, or rewriting history (main is protected, but local mistakes happen — see Build & Release for the release flow). - Running
git reset --hard,git clean -fdx, or any destructivegitoperation when the working tree contains uncommitted state. - Closing or merging a PR.
- Anything that publishes to npm or pushes to GitHub Releases (the changeset/release pipeline owns publishing — never run
pnpm releaseornpm publishmanually). - Skipping commit hooks (
--no-verify) or signing flags. If a hook fails, fix the underlying issue. - Editing
.github/workflows/*.yml— release uses Trusted Publishing OIDC; small changes can break npm auth. Read Build & Release first.
These do not need confirmation:
- Local commits, branches, file edits, running
pnpm build/test/lint. - Pushing a feature branch to origin.
- Creating a PR (always go via PR — never push directly to
main).
These have been re-introduced more than once across recent migrations / reviews — flag them in self-review:
- Treating
await TypedAmqp(Client|Worker).create(...)as a client/worker. It returnsAsyncResult<Client, TechnicalError>;awaitgives you aResult. Unwrap with.unwrap()(or pattern-match) before calling instance methods. - Wrapping
client.publish(...)infromPromise(...).publishalready returns anAsyncResult— wrap it again and you getAsyncResult<AsyncResult<...>>. Chain.map/.mapErr/.flatMapdirectly. - Calling
fromPromise(p)without thequalifymapper. The mapper is a required second argument with signature(cause, defect) => E | defect(cause)— return a modeled error, or call thedefectcallback for an unexpected failure. The fix to the opaque "expected 2 arguments, got 1" error is always: pass the mapper. - Using positional
result.match(okFn, errFn). That's the old neverthrow shape. unthrown'smatchis boxed with three branches:result.match({ ok, err, defect }). - Reaching for
okAsync/errAsyncorResultAsync/_unsafeUnwrap. Those are neverthrow. UseOk(v).toAsync()/Err(e).toAsync(), theAsyncResulttype, and.unwrap(). - Using lowercase
ok/err/defectconstructors. unthrown 1.0 renamed them toOk/Err/Defect(the lowercase forms are removed). The.match({ ok, err, defect })handler keys stay lowercase, though — those are case branches, not constructors. - Adding a publishable package without
repository,homepage,bugs,author,license— npm will reject with a 422 on provenance validation under Trusted Publishing. - Hardcoding a dep version in a
package.json. Use"catalog:"and add the actual version once inpnpm-workspace.yaml. - Forgetting to add a changeset when changing public API. The release will silently skip your change.
- Before claiming a refactor is done, run
pnpm typecheck(it isn't in the pre-commit hook). If you changed a public type in package A, also rebuild it (pnpm --filter @amqp-contract/<a> build) before typechecking package B that depends on it — workspace packages are typed against theirdist/output. - Public API changes need a changeset (
pnpm changeset). The six publishable packages are in afixedgroup — they all bump together; you only add one entry. - Don't claim integration tests "pass" if they didn't run. They require Docker; if you can't run them locally, say so explicitly rather than implying coverage.
- When deferring to a doc, link to a specific
path/to/file.tsline rather than restating it. Stale duplication is the dominant failure mode of these rule files.