Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a264613
chore: scaffold pnpm workspace and shared tooling
btravers Aug 9, 2026
ee964e8
feat: add @btravstack/di dependency-injection package
btravers Aug 9, 2026
e7c6027
docs: add root and package READMEs
btravers Aug 9, 2026
14979d4
feat(examples): add hexagonal-order-api example
btravers Aug 9, 2026
45b8cd1
feat(examples): add request-scope example
btravers Aug 9, 2026
8397240
feat(examples): add plugin-registry example
btravers Aug 9, 2026
3bec749
docs(examples): add examples/ index
btravers Aug 9, 2026
6384640
fix(di): export PortClass and ManyPortClass so consumers can emit dec…
btravers Aug 9, 2026
9b33045
test(examples): guard declaration emit with a compiled fixture
btravers Aug 9, 2026
fdc2e3d
chore: drop a duplicated comment block in knip.jsonc
btravers Aug 9, 2026
8769ac8
Merge pull request #1 from btravstack/fix/port-declaration-emit
btravers Aug 9, 2026
a85b1fb
docs: beetroot-plug logo — blue is di's accent in the family
btravers Aug 9, 2026
8036373
docs: demesne is retired — di supersedes it
btravers Aug 9, 2026
379bd65
docs: the syringe logo, vectorised from the author's sketch
btravers Aug 9, 2026
2b97f99
ci: dependabot and the release workflow, ported from entity
btravers Aug 9, 2026
f8eab91
docs: guidance for future Claude Code instances, as CLAUDE.md
btravers Aug 9, 2026
8dfe332
docs: distinguish the engines floor from the pinned dev version
btravers Aug 9, 2026
5156eda
Merge pull request #2 from btravstack/docs/claude-md
btravers Aug 9, 2026
f17fd46
docs: the documentation site — vitepress + typedoc, on the entity tem…
btravers Aug 9, 2026
5952184
docs: the og social card, rendered on the family template
btravers Aug 9, 2026
b59df6f
ci: deploy docs from the commit CI validated, not the branch tip
btravers Aug 9, 2026
b1cf578
Merge pull request #3 from btravstack/docs/vitepress-site
btravers Aug 9, 2026
2a72f43
chore: make 0.1.0 the first published version
btravers Aug 9, 2026
9384fef
Merge pull request #5 from btravstack/chore/first-publish-0.1.0
btravers Aug 9, 2026
ceba11a
feat(docs): theme 2.0.0, the lighter blue accent, and a light/dark lo…
btravers Aug 10, 2026
9ea815b
style(docs): lowercase the accent hex, as oxfmt requires
btravers Aug 10, 2026
e4ef06e
Merge pull request #8 from btravstack/feat/theme-2-accent-logos
btravers Aug 10, 2026
8aa7bbf
Add '.incoming-di/' from commit 'e4ef06eb67a1296b390676c68923a90ccd7c…
Aug 13, 2026
0c94db8
chore: relocate di into the start monorepo layout
Aug 13, 2026
1bcc104
chore: merge di's toolchain into the root and rewire it as a workspace
Aug 13, 2026
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
5 changes: 4 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": [],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
6 changes: 6 additions & 0 deletions .changeset/no-release-first-publish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
---

Release plumbing only, deliberately no bump: the 0.1.0 CHANGELOG is written by
hand because the changeset it replaces described a fix between two states that
were never published.
31 changes: 31 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: "Setup Node.js and pnpm"
description: "Setup Node.js and pnpm, install dependencies"

# CI and Release call btravstack/config's reusable workflows, which do their own
# setup. This composite is kept for parity with the sibling btravstack repos,
# for any workflow in this repo that runs its own steps and needs the same
# toolchain.

runs:
using: "composite"
steps:
- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: pnpm

- name: Setup Turbo Cache
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-

- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile
71 changes: 71 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Deploy Documentation

# Publish the VitePress site once CI is green on main. Chaining off CI rather
# than pushing directly means the site is never built from a commit that does
# not compile, and the `github-pages` environment's branch policy sees `main`
# (a `workflow_run` event runs against the default branch).
on:
workflow_run:
workflows: ["CI"]
types:
- completed
branches:
- main
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
# A Pages deploy replaces the whole site, so only one may be in flight.
group: pages
cancel-in-progress: false

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
# A `workflow_run` fires on ANY CI conclusion (failure, cancelled); deploy
# only after a successful one. `workflow_dispatch` is unconditional.
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# A `workflow_run` checkout defaults to the default branch's CURRENT
# tip, which a push landing after CI went green can have moved — the
# site would then build from a commit no CI run has validated,
# defeating the point of chaining off CI at all. `head_sha` is the
# exact commit the green run measured. Empty (hence `|| github.sha`)
# only for `workflow_dispatch`, where the dispatched ref is the
# intent.
ref: ${{ github.event.workflow_run.head_sha || github.sha }}

- name: Setup
uses: ./.github/actions/setup

# The docs build runs TypeDoc (straight from packages/di/src into
# docs/api/di) and then VitePress — see docs/package.json.
- name: Build documentation
run: pnpm --filter ./docs exec turbo build

- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
path: docs/.vitepress/dist

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Release

on:
workflow_run:
workflows: ["CI"]
types:
- completed
branches:
- main

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
release:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: write
pull-requests: write
id-token: write
uses: btravstack/config/.github/workflows/release-reusable.yml@workflows-v1
secrets:
RELEASE_PAT: ${{ secrets.RELEASE_PAT }}
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ coverage/
# outlived live in git history. Ignored so they cannot grow back.
docs/superpowers/

# VitePress (`docs/.vitepress/dist/` is already covered by `**/dist/` above)
docs/.vitepress/cache/
# The di package's API reference, generated by TypeDoc at build time
# (the hand-written docs/api/index.md overview is kept)
docs/api/di/

# Generated Prisma client, minted by the persistence example's `generate` script
# (run by its `test` / `typecheck` scripts). Ignored at the root so a checkout
# predating the package cannot leave it visible and sweepable into a commit.
Expand Down
40 changes: 24 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,33 @@ reasoning behind them. Keep it in sync with the code as the package evolves

## What this is

`@btravstack/start` — the application kernel. It boots a
[`@btravstack/di`](https://github.com/btravstack/di) module into a running
process with one runtime, drains in-flight work on SIGTERM, and closes the
application scope on every path. It owns three things — the lifecycle state
machine, the unit-of-work registry, and the `Runtime` contract — and knows
nothing about HTTP, AMQP or Temporal.
`@btravstack/start` — the application kernel. It boots a `@btravstack/di`
module into a running process with one runtime, drains in-flight work on
SIGTERM, and closes the application scope on every path. It owns three
things — the lifecycle state machine, the unit-of-work registry, and the
`Runtime` contract — and knows nothing about HTTP, AMQP or Temporal.

`di` proves the wiring before the process exists. `start` owns **when** an
already-proven graph is constructed and torn down, and nothing more. Nothing
throws to callers: every fallible operation returns an
[`unthrown`](https://github.com/btravstack/unthrown) `Result`.

pnpm workspace + turbo monorepo. `packages/` holds four published packages,
`start` (the kernel), `start-http` (the HTTP runtime), `start-temporal` (the
Temporal worker runtime) and `start-amqp` (the AMQP consumer runtime);
`examples/` holds eleven private ones — a clean-architecture application
pnpm workspace + turbo monorepo. `packages/` holds five published packages:
`di` (the module-based DI container the kernel boots — merged in from the
former `btravstack/di` repo, history included; still published as
`@btravstack/di` and still a **peer** of the other four), `start` (the
kernel), `start-http` (the HTTP runtime), `start-temporal` (the Temporal
worker runtime) and `start-amqp` (the AMQP consumer runtime).
`examples/` holds fourteen private ones — a clean-architecture application
(`order-domain` → `order-application` → `order-infrastructure`) booted under
four different runtimes (`order-api`, `order-worker`, `order-temporal`,
`order-amqp`), with each transport's contract in a package of its own
(`order-api-contract`, `order-temporal-contract`, `order-amqp-contract`)
because a client must be able to take a contract without the server. They are
consumers, not fixtures: they are part of the gate, and `examples/README.md`
is their index.
because a client must be able to take a contract without the server, plus
di's three consumer examples (`hexagonal-order-api`, `request-scope`,
`plugin-registry`). They are consumers, not fixtures: they are part of the
gate, and `examples/README.md` is their index. `docs/` is the VitePress +
TypeDoc site for `packages/di` (deployed by `deploy-docs.yml`).

## Commands

Expand Down Expand Up @@ -468,8 +472,11 @@ namespace }` back off `Serving.info`. The Worker's lifecycle, the unit per
dependencies of `start` — the dual-copy hazard is real for both (di's port
identity and unthrown's `isResult` each compare across copies). `start-http`
peers on both of those plus `@btravstack/start` itself, for the same reason.
`node:` builtins only otherwise. Do not add a dependency.
- `declarationMap: false` on all four published packages — the published
`node:` builtins only otherwise. Do not add a dependency. di living in this
repo changes none of that: the kernel packages reference it as
`workspace:^` in devDependencies, and the published peer range stays
`^0.1.0` — a consumer still installs `@btravstack/di` themselves.
- `declarationMap: false` on all five published packages — the published
tarball has no `src/`, so maps would be dead ends.
- **Relative imports carry `.js`.** `moduleResolution: NodeNext` plus
`verbatimModuleSyntax`, both inherited from `@btravstack/tsconfig/base.json` —
Expand Down Expand Up @@ -528,7 +535,8 @@ namespace }` back off `Serving.info`. The Worker's lifecycle, the unit per
them, `packages/start/CLAUDE.md` too — and for a runtime package, its own:
`packages/start-http/CLAUDE.md`, `packages/start-temporal/CLAUDE.md` or
`packages/start-amqp/CLAUDE.md`, whichever is where that package's public
surface lives. There are **five** `CLAUDE.md` files; naming the wrong one is
surface lives. `packages/di/CLAUDE.md` plays the same role for the DI
container. There are **six** `CLAUDE.md` files; naming the wrong one is
how the last drift happened.

## Test conventions
Expand Down
140 changes: 140 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Contributing

Thanks for your interest in improving `@btravstack/start` and
`@btravstack/di`. These are small, focused libraries — the guiding principle
is **one concept = one name**, and each surface is meant to stay small enough
that the library can be "done". Contributions that sharpen the existing design
are more welcome than ones that grow it.

## Prerequisites

- **Node** `>=22.19`
- **pnpm** `11.7.0` (pinned via `packageManager`; run `corepack enable` to get it)

## Getting started

```sh
git clone https://github.com/btravstack/start.git
cd start
pnpm install
```

## The gate

Every change must keep all of these green (CI runs the same set):

```sh
pnpm format --check # oxfmt
pnpm lint # oxlint
pnpm typecheck # tsc (incl. type-level tests)
pnpm test # vitest
pnpm knip # dead code / unused deps
pnpm build # tsdown dual CJS/ESM + d.ts
```

Run `pnpm format` (no `--check`) to auto-fix formatting.

### Type-level tests

Behaviour that only shows up at the type level — the variance of `Provider`'s
and `Module`'s phantom channels, the construction family's mutual exclusivity,
`Scope` being excluded from `Needs` only by the right entry point — is pinned
in `packages/di/src/*.test-d.ts` and checked by
`tsc --noEmit -p tsconfig.test-d.json` (run as part of `pnpm typecheck`). If
you change a type-level guarantee, update or add the matching
`@ts-expect-error` assertion.

`*.test-d.ts` files are excluded from the main `tsc` pass by
`tsconfig.json`, so that pass can keep `noUnusedLocals` strict while the
assertions declare bindings they never read. `src/type-assert.ts` exports the
shared `Equal<A, B>` helper those assertions pin values against — it is a
test-only helper, not part of the published surface, and is excluded from
knip's scope the same way (`knip.jsonc`) since nothing in the runtime source
imports it.

### Publishing settings

`declarationMap` is off in `packages/di/tsconfig.json`: `files: ["dist"]`
excludes `src/`, so published declaration maps would be dead-ends (broken
go-to-definition). Consumers get the TSDoc'd `.d.ts` instead.

Declaration settings reach further than they look — `tsdown` reads that
tsconfig for its `--dts` emit, so what is set there shapes the _published_
types, while the plain `tsc` pass is `noEmit` from the shared base.

## Design rules (binding)

The package README documents the public behaviour and the rationale behind
it; several of the comments throughout `src/` record decisions measured
against a specific compiler version or a real failure mode (a TypeScript
diagnostic code, a variance bug, an unsoundness a review caught), not
assumed. Treat those as regression guards, not decoration — verify before
"simplifying" them away.

- **oxlint rules are binding**, including the `unthrown/*` rules enforcing this
repo's errors-as-values convention (no throwing outside a documented defect
path). Genuine exceptions carry a targeted `oxlint-disable` with a reason.
- **One name per concept.** Resist convenience aliases.

## Node versions

Three numbers, and they mean different things:

| Where | Value | Meaning |
| ----------------------------- | ---------------------- | --------------------------------------------------------- |
| `.node-version` | the pinned dev version | what contributors and the primary CI job run |
| root `package.json` `engines` | `>=22.19` | the oldest Node this repo is _developed_ on |
| `packages/di` `engines` | `>=20` | the oldest Node the _published package_ claims to support |

CI runs the test job on `["", "22.19", "24", "26"]` — the pinned version, the
repo's own development floor, and the two current release lines.

**The published package's floor is not covered, and this matrix cannot cover
it.** These jobs run the development toolchain, and pnpm 11 requires
`node:sqlite`, so a Node 20 row dies at `setup-node` before installing
anything: `ERR_UNKNOWN_BUILTIN_MODULE: No such built-in module: node:sqlite`.
That would test the toolchain, not the package — and it contradicts the root
`engines` above, which already says development needs `>=22.19`.

`engines` on `packages/di` is a claim about **consumers**, who install the
published tarball with their own package manager and import it. Proving it
needs a consumer-side job: pack, `npm install` the tarball on the floor
version, import it. Until that exists the floor is declared, not proven — so
treat `>=20` as an intention rather than a guarantee.

`24` overlaps `""` for as long as `.node-version` stays on 24.x. It is listed
explicitly anyway, so that bumping `.node-version` to 26 does not silently
drop 24 from the matrix.

## Commit convention

Commits follow [Conventional Commits](https://www.conventionalcommits.org/) and
are checked by **commitlint** via a **lefthook** `commit-msg` hook. Examples:

```
feat: add Provider.member for set-port contributions
fix: reject a provider registered for Scope as a wiring defect
docs: clarify the resourceful provider's Scope requirement
chore(deps): bump unthrown
```

## Changesets

User-facing changes need a changeset so the release notes and version bumps are
generated correctly:

```sh
pnpm changeset
```

Describe the change in one line and pick a semver bump. Purely internal changes
(tests, CI, refactors with no API/behaviour impact) don't need one.

## Pull requests

- Keep PRs focused — one concern each.
- Make sure the full gate passes locally before pushing.
- Reference the issue you're addressing, if any.

By contributing, you agree that your contributions are licensed under the
project's [MIT License](./LICENSE).
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,12 +577,15 @@ lines done well.
## Documentation

See [`packages/start`](./packages/start) for the package README,
[`examples/`](./examples) for an eleven-package clean-architecture application
booted under four different runtimes, and [`CLAUDE.md`](./CLAUDE.md) for the
authoritative spec: the theses, the public surface and the conventions. The
load-bearing invariants with the test that guards each, and the internal design
notes, live in
[`packages/start/CLAUDE.md`](./packages/start/CLAUDE.md).
[`packages/di`](./packages/di) for the DI container the kernel boots (merged
into this repo from the former `btravstack/di` repository, history included —
still published separately as `@btravstack/di`, with its VitePress + TypeDoc
site in [`docs/`](./docs)), [`examples/`](./examples) for an eleven-package
clean-architecture application booted under four different runtimes plus di's
three consumer examples, and [`CLAUDE.md`](./CLAUDE.md) for the authoritative
spec: the theses, the public surface and the conventions. The load-bearing
invariants with the test that guards each, and the internal design notes, live
in [`packages/start/CLAUDE.md`](./packages/start/CLAUDE.md).

## License

Expand Down
Loading