Skip to content

feat: absorb @btravstack/di into the framework - #37

Merged
btravers merged 30 commits into
mainfrom
feat/absorb-di
Aug 14, 2026
Merged

feat: absorb @btravstack/di into the framework#37
btravers merged 30 commits into
mainfrom
feat/absorb-di

Conversation

@btravers

Copy link
Copy Markdown
Contributor

The container was its own repository. It is merged here with its full
history
(git merge --allow-unrelated-histories), so blame on
packages/di/src still reaches the commit that wrote each line — the "Commits"
tab below carries di's own eight PRs.

What lands

packages/di the fifth published package, and the only one that depends on nothing else in the workspace: the arrow runs coredi, never back
examples/hexagonal-order-api, examples/request-scope, examples/plugin-registry the container's own three, renamed @btravstack/di-example-*@btravstack/example-*; they compose a Module and never call start, which is what keeps them tests of the wiring rather than of the lifecycle
packages/di/CLAUDE.md di's root spec, rewritten as a package-scoped one in the house style

Wiring

  • Consumers link di with workspace:* in devDependencies and keep ^0.1.0
    in peerDependencies, so a published consumer still installs one copy of its
    own — the dual-copy hazard is why di is a peer at all. The @btravstack/di
    catalog entry is gone.
  • typescript-consumer (npm:typescript@5.9.3) joins the catalog.
    examples/hexagonal-order-api compiles declaration emit twice — once with the
    catalog's TypeScript, once with the version a consumer is realistically on —
    and it is the only thing that needs the alias.
  • knip learns di's two exemptions (**/type-assert.ts, the emit-guards.ts
    entry). knip.jsonc is dropped in favour of this repo's knip.json.
  • Every root config conflict resolved in this repo's favour, so the stricter
    side wins: di's source passes all eight @unthrown/oxlint rules unchanged,
    including the three opt-ins it did not have switched on.

Dropped rather than merged

  • docs/ — the VitePress site, di-specific, as requested. Its deploy
    workflow goes with it. New documentation gets written for the framework later.
  • CONTRIBUTING.md — di-shaped (clone URL, di's gate), and CLAUDE.md is
    the authoritative spec here.
  • release.yml, dependabot.yml, .github/actions/setup — repo policy this
    repository has deliberately not adopted. Say the word and I'll port the release
    workflow properly rather than smuggling it in with a merge.
  • SECURITY.md is kept, unchanged for now; it names btravstack/di as the
    reporting repo, which is worth a follow-up once the framework's own name is
    settled.

Gate

formatlinttypecheck ✓ (24 tasks) knipbuild ✓ (5 packages)
test 23/24 — 253 specs, di's 40 among them.

The one failure is invariants.spec.ts > binds 9000 when no probe port is given,
the pre-existing local port clash (a bare listen(9000) returns EADDRINUSE on
this machine); it is unrelated to the merge and does not reproduce in CI. The
AMQP suites also timed out once on a 60 s container wait and passed on a rerun —
Docker contention on this laptop, not a regression.

🤖 Generated with Claude Code

btravers and others added 28 commits August 9, 2026 18:54
Set up the repo shell following @btravstack/entity's conventions: pnpm
workspace with the shared @btravstack/* catalog packages (tsconfig,
oxlint, lefthook, commitlint), turbo pipeline, changesets, knip, and CI.
No package yet — packages/* and examples/* are declared as workspace
globs for the di package and its future examples.
Ports declare what an application needs, providers bind a port to a
concrete construction (value, factory, class, or an acquire/release
resource), and modules group providers behind explicit imports/exports.
Every wiring mistake the type system can catch — a missing dependency,
an internal port leaking out of a module, a re-export of something
never imported — is a compile error; a cycle or a duplicate provider is
caught before any factory runs, as a defect via unthrown.

Ported from the reviewed @saas/platform-di implementation (93 tests),
adapted to this repo's layout: runtime tests renamed *.test.ts ->
*.spec.ts per @btravstack/entity's convention, relative imports moved
to .js extensions to match the shared tsconfig's NodeNext resolution,
and interface declarations converted to type aliases per the shared
oxlint config's consistent-type-definitions rule.
Root README pitches the package and links to the package docs, in
entity's style (badges, one-paragraph pitch, worked example). The
package README preserves the platform-di walkthrough — ports, providers,
modules, the construction family — with package names updated for the
published @btravstack/di surface.
Ports named by the application, a private connection pool behind a
public repository, and one application module composed against a
production adapter and an in-memory one — the core story for
@btravstack/di, built as a real workspace package rather than just a
README walkthrough. Includes the compile-time guarantees (port
privacy, the Module.build/Module.scoped gate) pinned in a dedicated
index.test-d.ts, mirroring packages/di's own example.test-d.ts split.
Lifetime management: a connection pool acquired once under
Module.scoped, and a per-request transaction layered over the
already-built parent with Module.forkScope. The spec proves — not
just type-checks — that each request's resource releases before the
next begins, that the parent outlives every fork, and that a fork
resolves its dependency from the parent context rather than a copy
of its own.
Multi-binding: a Port.many health-check registry fed independently by
two modules via Provider.member, collected and run together at the
composition root. The spec proves contributions accumulate across
module boundaries, a failing check comes back as reported data rather
than aborting the run, and the registry is purely additive when a
third module joins.
Indexes the three examples workspace packages, in the style of
btravstack/entity's examples/README.md. Also picks up pnpm-lock.yaml's
update from installing the three new example packages' devDependencies.
…larations

A consumer that exports a port it declares could not emit declarations:

  export class OrderRepository extends Port("OrderRepository")<Shape> {}

emits as `declare const OrderRepository_base: <the heritage expression's
type>`, and the emitter can only write that type using names the consumer can
reach. `PortClass`/`ManyPortClass` were not exported from the package index, so
it had none: it expanded the heritage expression down to `PortInstance`'s
`[ID]`/`[SERVICE]`/`[MANY]` keys — module-private `unique symbol`s — and
reported TS4020, "has or is using private name 'ID'". That is the pattern
`packages/di/README.md` teaches on its first page, so it affected essentially
every real consumer.

Exporting the two class *types* is the fix that costs least. The emitter now
stops at `PortClass<"OrderRepository">` (2,683 bytes of consumer declarations
across the reproduction, against 3,545 when only the instance types are
nameable and the construct signature has to be written out), and `port.ts` is
untouched.

The brand symbols stay unexported deliberately. Exporting them also fixes emit,
but a consumer who can name `ID`/`SERVICE` can hand-write
`{ [ID]: "Logger", [SERVICE]: Shape }` and pass it off as a `Logger` — measured,
it type-checks. Naming the class types grants no such thing: the brand keys stay
unreachable, so port identity stays nominal and a port instance stays
unforgeable. As an ambient `export declare const` they have no runtime binding
and rolldown rejects the index re-export outright (MISSING_EXPORT); making them
real `Symbol()` values would add runtime surface to phantom tokens that are
never constructed.

The three example packages carried `declaration: false` in their own tsconfigs
to dodge this. That is legitimate only because they are private, and it is what
kept the repo green while no consumer could build, so it goes with the fix.
`packages/di`'s own checks never emitted a consumer's declarations, so TS4020
could not be seen from inside the repo — the three example packages had turned
`declaration` off, and the library's own `tsc --noEmit` only ever compiles code
that can name `port.ts`'s brand symbols directly.

`examples/hexagonal-order-api/src/emit-guards.ts` is that missing consumer: a
file imported by nothing, which exists to be *compiled*. It names a plain port,
a `Port.many` set port, a port reaching through another port's `ServiceOf`, the
providers and module built on them, and the two factories whose return type is
a port class rather than an instance — the shapes that fail through different
brands, so a fix naming only one of the class types leaves the other broken.

`tsconfig.emit.json` turns `noEmit` back off (TS4020 is raised by the
declaration *emitter*, so a `--noEmit` pass cannot be the whole gate) and the
package's `typecheck` script runs it under both 7.0.2 and a stable-line 5.9.3,
then feeds the emitted `.d.ts` back through the compiler — a dangling reference
in the output is not an emit-time diagnostic and would otherwise ship.
`emit-guards.d.ts` is named explicitly in that last step because nothing imports
it, so it would go unchecked on `index.d.ts` alone.

The `@ts-expect-error` directives in the fixture are the other half: they assert
that `ID`/`SERVICE`/`MANY` are still unreachable and that two structurally
identical ports with different ids still do not unify. An unused directive there
is a failure, not noise — it is the signal that someone bought declaration emit
by widening the export surface far enough to forge a port.

Verified with teeth: reverting the index re-export puts eleven TS4020/TS4023
errors back through this fixture. `knip.jsonc` names it an entry, without which
knip reports it as an unused file.
fix(di): let consumers emit declarations for their own ports
The beet is not totally formed — dome, eyes and smile only, coalescing at the
needle end, one drop already out. Mid-formation is temporal-contract's own
grammar, so the two logos rhyme: the container constructs the dependency, then
injects it. Replaces the socket mark, which the history keeps.
Dependabot groups verbatim (weekly npm + actions); release rides
btravstack/config's reusable workflow after a green CI on main, exactly as the
sibling repos do.
docs: guidance for future Claude Code instances, as CLAUDE.md
docs: the documentation site — vitepress + typedoc, on the entity template
chore: make 0.1.0 the first published version
…go pair

Three things the landing surfaced when di joined it as the fifth panel
(btravstack/btravstack.github.io#43). Closes #6. Closes #7.

**The accent.** `--accent` was `#2A62B8`, the logo's deep blue. The landing
paints each package name in the raw accent against a `#100F12` card, and that
hex measures 3.22 there — the other four packages sit at 5.00–7.25. It is now
`#3E7FD4`, the logo's *lighter* blue: 4.72 on the dark card, 7.10 as darkened
text on white. The original reasoning is untouched and still holds — blue is
plumbing, and the hex is one the mark itself already paints (barrel rim,
plunger, needle hub), so chrome and artwork stay one color.

**The logo pair.** di shipped only `logo.svg`, and it was the one package
without a `logo-{light,dark}` pair. Three of its values read against the canvas
rather than against the artwork, so on a light background the near-white barrel
washed out and the mark read as a floating beet with a blue cap. `logo-dark` is
the existing file unchanged; `logo-light` inverts exactly those three — barrel
`#EAF2FB`→`#2A2730`, plunger `#2A62B8`→`#3E7FD4`, needle `#9FB2C8`→`#5A6675`.
The ticks stay at 50% opacity: 1.89 against the dark barrel versus 1.81 against
the light one, so they read equally faint either way. The hero and the nav logo
now take the pair; `logo.svg` stays the favicon and the JSON-LD mark.

**The theme.** Catalog moves 1.7.0 → 2.0.0, which is where `--pkg-di` lives.
The major is the removal of `--pkg-demesne` / `--pkg-start`; neither is
referenced here, so nothing breaks. pnpm recorded the new version as a pinned
`minimumReleaseAgeExclude` entry — replaced with the unversioned name and the
rationale comment the other repos use, so the next release needs no entry.

Docs build clean: `--accent: #3E7FD4` in the emitted CSS, no `2a62b8` left, all
five `--pkg-*` tokens present and both retired ones gone, and both logo variants
verified in both themes.
`pnpm format --check` failed CI on this file. oxfmt normalises CSS hex values
to lowercase, which is why every sibling site's `--accent` is lowercase and why
this file's own `#2a62b8` was before it changed. The comment's hex follows, so
the file does not mix cases with its own declaration.

Note this is the opposite of the markdown convention: prose in changesets and
`design.md` writes hexes uppercase. Each file type is internally consistent.

Root cause of the miss: the shared pre-commit hook in @btravstack/lefthook
globs `*.{ts,tsx,js,jsx,json,yaml,yml,md}`, which has no `css`, so this file was
never formatted locally — the hook reported three files (config.ts, index.md,
pnpm-workspace.yaml) and skipped this one. CI runs `oxfmt .` over everything.
feat(docs): theme 2.0.0, the lighter blue accent, and a light/dark logo pair
The container was its own repository. It is merged here with its full
history (`--allow-unrelated-histories`), so blame on `packages/di/src`
still reaches the commit that wrote each line.

- `packages/di` is the fifth published package, and the only one that
  depends on nothing else in the workspace: the arrow runs `core` → `di`
  and never back.
- Consumers link it with `workspace:*` in `devDependencies` and keep
  `^0.1.0` in `peerDependencies`, so a published consumer still installs
  one copy of its own. The `@btravstack/di` catalog entry is gone.
- Its three examples come with it, renamed into this repo's family:
  `@btravstack/di-example-*` → `@btravstack/example-*`. They compose a
  `Module` and never call `start`, which is what keeps them tests of the
  wiring rather than of the lifecycle.
- `typescript-consumer` joins the catalog: `examples/hexagonal-order-api`
  compiles declaration emit twice, and that is the only thing needing it.
- knip learns di's two exemptions (`**/type-assert.ts`, the
  `emit-guards.ts` entry); `knip.jsonc` is dropped for our `knip.json`.

Dropped rather than merged: `docs/` (the VitePress site, di-specific —
new documentation will be written for the framework), its deploy
workflow, `CONTRIBUTING.md` (di-shaped, and CLAUDE.md is the
authoritative spec here), and di's `release.yml` / `dependabot.yml`,
which are repo policy this repository has deliberately not adopted.

di's root `CLAUDE.md` becomes `packages/di/CLAUDE.md`, and the root spec,
both READMEs and `examples/README.md` are updated in the same commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 14, 2026 21:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR merges @btravstack/di (the DI container) into the start monorepo (with full history), wires it into the workspace, and brings in the container’s three consumer examples alongside updates to repo docs and tooling to reflect the new package layout.

Changes:

  • Adds a new published workspace package packages/di (container) with runtime + type-level tests and package-scoped spec/docs.
  • Adds three @btravstack/di consumer example workspaces (hexagonal-order-api, request-scope, plugin-registry) and updates the examples index/docs accordingly.
  • Updates workspace dependency wiring (catalog:workspace:*) and introduces a typescript-consumer catalog alias for declaration-emit compatibility checks.

Reviewed changes

Copilot reviewed 74 out of 75 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
SECURITY.md Adds security policy document
README.md Updates root documentation links to local packages/di
pnpm-workspace.yaml Removes @btravstack/di catalog entry; adds typescript-consumer alias
pnpm-lock.yaml Locks workspace:* wiring for @btravstack/di and new typescript-consumer
packages/core/package.json Switches @btravstack/di devDependency to workspace:*
packages/http/package.json Switches @btravstack/di devDependency to workspace:*
packages/temporal/package.json Switches @btravstack/di devDependency to workspace:*
packages/amqp/package.json Switches @btravstack/di devDependency to workspace:*
knip.json Adjusts ignore patterns; adds hexagonal emit-guards entry/project
packages/di/package.json Adds @btravstack/di package manifest + scripts
packages/di/README.md Adds package README with worked example
packages/di/CHANGELOG.md Adds initial changelog for @btravstack/di
packages/di/LICENSE Adds package license file
packages/di/CLAUDE.md Adds package-scoped spec for packages/di
packages/di/tsconfig.json Adds DI package TS config
packages/di/tsconfig.test-d.json Adds DI package type-test TS config
packages/di/vitest.config.ts Adds DI package vitest config
packages/di/src/index.ts Defines DI package public surface exports
packages/di/src/port.ts Implements ports, set ports, and Scope phantom port
packages/di/src/provider.ts Implements provider construction family + hooks/member support
packages/di/src/module.ts Implements module algebra + build/scoped/fork APIs
packages/di/src/build.ts Implements graph planning/build/scoped execution
packages/di/src/context.ts Implements built context and internal mutation helpers
packages/di/src/lifecycle.ts Implements level construction + onStart sequencing
packages/di/src/scope.ts Implements scope finaliser registry + close semantics
packages/di/src/type-assert.ts Adds type-level Equal helper for .test-d.ts files
packages/di/src/build.spec.ts Adds runtime specs for build/plan behavior
packages/di/src/build.test-d.ts Adds type-level tests for Module.build typing gates
packages/di/src/context.spec.ts Adds runtime specs for context mutation behavior
packages/di/src/context.test-d.ts Adds type-level tests for Context variance/get typing
packages/di/src/example.spec.ts Adds runtime hexagonal example spec
packages/di/src/example.test-d.ts Adds type-level half of hexagonal example (privacy)
packages/di/src/fork.spec.ts Adds runtime specs for Module.forkScope teardown behavior
packages/di/src/fork.test-d.ts Adds type-level tests for forkScope typing gates
packages/di/src/lifecycle.spec.ts Adds runtime specs for hooks ordering and failure behavior
packages/di/src/many.spec.ts Adds runtime specs for set-port accumulation behavior
packages/di/src/many.test-d.ts Adds type-level tests for set-port/member typing
packages/di/src/module.test-d.ts Adds type-level tests for module algebra/variance guarantees
packages/di/src/port.spec.ts Adds runtime specs for port ids + duplicate warning
packages/di/src/port.test-d.ts Adds type-level tests for port nominal identity + ServiceOf
packages/di/src/provider.spec.ts Adds runtime specs for provider arms behavior
packages/di/src/provider.test-d.ts Adds type-level tests for provider variance/exclusivity gates
packages/di/src/scoped.spec.ts Adds runtime specs for scope unwind behavior + Scope value export guard
packages/di/src/scoped.test-d.ts Adds type-level tests for scoped resource gating
examples/README.md Updates examples index to include DI example family
examples/hexagonal-order-api/package.json Adds DI consumer example workspace manifest
examples/hexagonal-order-api/README.md Adds DI consumer example README
examples/hexagonal-order-api/tsconfig.json Adds example TS config
examples/hexagonal-order-api/tsconfig.test-d.json Adds example type-test TS config
examples/hexagonal-order-api/tsconfig.emit.json Adds declaration-emit gate TS config
examples/hexagonal-order-api/vitest.config.ts Adds example vitest config
examples/hexagonal-order-api/src/index.ts Adds hexagonal example implementation
examples/hexagonal-order-api/src/index.spec.ts Adds runtime tests for both graphs
examples/hexagonal-order-api/src/index.test-d.ts Adds compile-time privacy/arity gate assertions
examples/hexagonal-order-api/src/emit-guards.ts Adds declaration-emit regression fixture (TS4020 guard)
examples/plugin-registry/package.json Adds DI plugin-registry example workspace manifest
examples/plugin-registry/README.md Adds DI plugin-registry example README
examples/plugin-registry/tsconfig.json Adds example TS config
examples/plugin-registry/vitest.config.ts Adds example vitest config
examples/plugin-registry/src/index.ts Adds set-port plugin registry example
examples/plugin-registry/src/index.spec.ts Adds runtime tests for contribution accumulation
examples/request-scope/package.json Adds DI request-scope example workspace manifest
examples/request-scope/README.md Adds DI request-scope example README
examples/request-scope/tsconfig.json Adds example TS config
examples/request-scope/vitest.config.ts Adds example vitest config
examples/request-scope/src/index.ts Adds forked request-scope example implementation
examples/request-scope/src/index.spec.ts Adds runtime tests for per-request teardown ordering
examples/order-api/package.json Switches @btravstack/di dependency to workspace:*
examples/order-application/package.json Switches @btravstack/di dependency to workspace:*
examples/order-infrastructure/package.json Switches @btravstack/di dependency to workspace:*
examples/order-temporal-worker/package.json Switches @btravstack/di dependency to workspace:*
examples/order-amqp-worker/package.json Switches @btravstack/di dependency to workspace:*
CLAUDE.md Updates root spec/docs to include packages/di and example count
.changeset/README.md Adds Changesets readme boilerplate
.changeset/no-release-first-publish.md Adds “no release” changeset note
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread SECURITY.md Outdated
Comment thread packages/di/src/provider.spec.ts Outdated
Comment thread packages/di/src/module.test-d.ts Outdated
Comment thread packages/di/src/module.ts Outdated
Benoit Travers and others added 2 commits August 14, 2026 23:47
`hexagonal-order-api` stays: it is the only workspace that compiles twice
(declaration emit under the catalog's TypeScript, re-checked under
`typescript-consumer`), and `emit-guards.ts` is the fixture that keeps
TS4020 out of the published `.d.ts`. Nothing else in the repository
guards that.

`request-scope` and `plugin-registry` go. Between them they were five
specs that re-asserted what the container's own unit tests already pin —
`fork.spec.ts` and `scoped.spec.ts` for the forked scope and LIFO
unwind, `many.spec.ts` for set-port accumulation — with `order-api`
forking a real per-request scope over a real HTTP server besides. By
this repository's own rule, `examples/` is part of the gate and not a
folder of illustrations; an example that proves nothing new fails that
charter.

They stay in history, here and in the di repository, if the framework's
documentation wants them back as prose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- **`SECURITY.md` pointed private vulnerability reports at
  `btravstack/di`'s advisory page**, which is the wrong repository now
  that di is released from here. It names this repository's packages and
  its advisory page instead.
- **`provider.spec.ts`'s class-arm test cast its class to `never`**,
  which bypassed the very typing the arm exists to enforce — the
  constructor parameters and the constructed service shape. The cast is
  unnecessary: `{ class: Impl }` typechecks and the test still passes, so
  the arm is now covered rather than merely exercised.
- **Two comments still described all three phantom channels as
  contravariant.** They have carried mixed variance since the fix that
  made obligation channels covariant (`_port`/`_exports` contravariant,
  `_error`/`_needs` covariant), so `module.ts`'s bound rationale and
  `module.test-d.ts`'s `ChannelsOf` note were both stating a rule the
  code no longer follows. `module.ts` also claimed the wildcarded bound
  `Provider<any, any, any>` is rejected outright; measured, it compiles
  today. The comment now says why the structural bound stays anyway —
  `any` is a lint error here, and a bound that compares channels it does
  not read is the next variance bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@btravers
btravers merged commit 12b70b3 into main Aug 14, 2026
13 checks passed
@btravers
btravers deleted the feat/absorb-di branch August 14, 2026 21:55
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.

2 participants