feat: absorb @btravstack/di into the framework - #37
Merged
Conversation
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>
There was a problem hiding this comment.
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/diconsumer example workspaces (hexagonal-order-api,request-scope,plugin-registry) and updates the examples index/docs accordingly. - Updates workspace dependency wiring (
catalog:→workspace:*) and introduces atypescript-consumercatalog 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.
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The container was its own repository. It is merged here with its full
history (
git merge --allow-unrelated-histories), so blame onpackages/di/srcstill reaches the commit that wrote each line — the "Commits"tab below carries di's own eight PRs.
What lands
packages/dicore→di, never backexamples/hexagonal-order-api,examples/request-scope,examples/plugin-registry@btravstack/di-example-*→@btravstack/example-*; they compose aModuleand never callstart, which is what keeps them tests of the wiring rather than of the lifecyclepackages/di/CLAUDE.mdWiring
workspace:*indevDependenciesand keep^0.1.0in
peerDependencies, so a published consumer still installs one copy of itsown — the dual-copy hazard is why di is a peer at all. The
@btravstack/dicatalog entry is gone.
typescript-consumer(npm:typescript@5.9.3) joins the catalog.examples/hexagonal-order-apicompiles declaration emit twice — once with thecatalog's TypeScript, once with the version a consumer is realistically on —
and it is the only thing that needs the alias.
**/type-assert.ts, theemit-guards.tsentry).
knip.jsoncis dropped in favour of this repo'sknip.json.side wins: di's source passes all eight
@unthrown/oxlintrules 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 deployworkflow goes with it. New documentation gets written for the framework later.
CONTRIBUTING.md— di-shaped (clone URL, di's gate), andCLAUDE.mdisthe authoritative spec here.
release.yml,dependabot.yml,.github/actions/setup— repo policy thisrepository has deliberately not adopted. Say the word and I'll port the release
workflow properly rather than smuggling it in with a merge.
SECURITY.mdis kept, unchanged for now; it namesbtravstack/dias thereporting repo, which is worth a follow-up once the framework's own name is
settled.
Gate
format✓lint✓typecheck✓ (24 tasks)knip✓build✓ (5 packages)test23/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)returnsEADDRINUSEonthis 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