feat(config): @btravstack/config — one-value Config(id)(shape, options?), used by the examples - #35
feat(config): @btravstack/config — one-value Config(id)(shape, options?), used by the examples#35btravers wants to merge 11 commits into
Conversation
single-repo release instead of a cross-repo one: config's only consumers are start's kernel and starters, so the pending 0.1.0 changeset moves with it. adopt start's coverage-gated test script, typecheck/test:types shape, and package metadata; add @standard-schema/spec to the catalog.
start holds packages to 100% lines and functions; config arrived from its own repo two branches short. Both are worth testing rather than exempting: describeIssues is what an operator actually reads on a failed boot, and the provider's defect path is what happens when the kernel's validation guarantee is skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The readme snippet used ValueOf without importing it, so it would not copy-paste. collect walked with shift(), which re-indexes the array on every step; a cursor keeps the same breadth-first declaration order that describeIssues promises, without the churn. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tions?) value A starter no longer declares a port and a separate env adapter for it: Config now returns one value that is both the di port token and the module that serves it, mirroring @btravstack/entity's Entity(tag)(fields, options?). The port stays adaptable — a test can provide it as a literal without ever importing it as a module — because it is still just a port, carrying di's module statics rather than being welded to a second value. Requires ConcretePortClass from an unpublished btravstack/di branch; blocked on that publishing.
`export const amqpConfig = Config("Amqp")({ … })` has an inferred type, and a
package compiled with `declaration` must be able to write that type down. The
inline intersection expanded to di's `[ID]`/`[SERVICE]` brands and this
package's own `CONFIG_ADAPTER`, none of them exported, so every such consumer
failed with TS4023. `DeclaredConfig<Id, S>` gives the emitter a name to stop
at, the way di's `ConcretePortClass` does one layer down, and grants no new
power — the brand keys stay unnameable.
Found by using the package in `examples/`: nothing inside it exports a declared
config, so nothing inside it could have caught the gap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each deployment loses its `env.ts` — a `z.object` of SCREAMING_SNAKE keys, a `readEnv` over `fromSchema`, and a fold that needed a `P._` catch-all behind a lint-disable — and gains a `config.ts` of `Config(id)(shape, options?)` values. Every environment variable keeps its name bar one: `order-api` reads `HTTP_PORT` where it read `PORT`, because a config's variables are `PREFIX_KEY` and no prefix and key join to a bare `PORT`. Configuration now travels through the graph rather than through `main`: a runtime names its configs in `needs` and reads them off `host.ctx`, so `orderAmqpRuntime()`, `orderApiRuntime()` and `temporalWorkerRuntime` no longer take a broker URL, a listening port or a namespace as arguments, and `start`'s gate proves the graph carries them at compile time. `main.ts` is one `Config.parse(Config.collect(Root), process.env)` fold, which reports every wrong variable in the whole graph at once and enumerates `ConfigInvalid` by tag instead of reaching for `P._`. Two values are still read from `process.env` by hand, each with a comment at the line: `PROBE_PORT`, because `start` binds the probe server before the graph exists, and `TEMPORAL_ADDRESS`, because the connection must be open before the runtime is handed it. Both are declared and validated with the rest. Kernel integration is phase 2's. The three packages also turn `declaration` off: they emit no declarations, and a composition root exporting a config cannot emit one until `@btravstack/di` exports `PortInstance`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its whole job was `wholeNumber`, `port` and `describeEnvIssues`, shared by the three deployments so the fragment and its seven cases were pinned once. `@btravstack/config` owns all three now — the first two as `@btravstack/config/zod`, the third as `describeIssues` — and pins the same cases in its own suite, so the package and its spec go rather than being ported. Nothing imports `@btravstack/start-example-order-config` any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each deployment's README gains a Configuration section naming the variables,
what declares them and where they are read; `examples/README.md` gains one
covering all three, drops the `order-config` row and says where its idiom went.
`packages/config/README.md` gains a "Seeing it used" section pointing at the
three consumers it now has, and records the two things using it surfaced: the
declaration-emit gap `DeclaredConfig` half-closes, and `probes: { port }`
having no clean answer until the kernel owns a source.
CLAUDE.md's configuration rule is rewritten around the package: the hand-rolled
`env.ts` shape it described no longer exists anywhere in the repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rtInstance The three deployables had turned off `declaration`/`declarationMap` because a composition root exporting a config emitted `Module<InstanceType<typeof cfg>>`, which reduced to di's unexported `PortInstance` and raised TS4023. di exports it now (btravstack/di#12), so the override is unnecessary — and it was the wrong shape of fix regardless: these packages are `noEmit`, so it silenced the check without saving any output. di's own emit-guards.ts exists because its examples once did exactly this, leaving the repo green while no consumer could build.
|
Two decisions for you, and one correction to what I said earlier. 1.
|
The note still described `Config(port, "PREFIX")({ ... })` with a separately
declared port — the shape replaced by the one-value `Config(id)(shape, options?)`.
@btravstack/config has never been published, so this is its first release note
and there is no earlier API to reconcile with.
…removal The lockfile still listed examples/order-config as an importer and lacked the @btravstack/config dependency the three deployables now declare, so a --frozen-lockfile install would have failed. Regenerated from the committed workspace, with no local override in it.
| // and phase 1 of `@btravstack/config` has no way to read one config's value | ||
| // outside a graph. Phase 2's kernel integration is what deletes this line: | ||
| // `start` will own the source and resolve its own configuration. | ||
| const probePort = Number(process.env["PROBE_PORT"] ?? PROBE_PORT_DEFAULT); |
Summary
Adds
@btravstack/configto the monorepo and uses it, closing #20's copy-pasted env-to-exit boot tail. Supersedes #34, which held the same move behind an earlier API.One value, not two.
Config(id)(shape, options?)returns a single value that is both a di port token and the module serving it from the environment, soimports: [amqpConfig]andctx.get(amqpConfig)name the same thing. The signature mirrors@btravstack/entity'sEntity(tag)(fields, options?): curried on the identity, then the field map, then an optional{ prefix }. Omitted, the prefix is the screaming-snake of the identity ("AmqpConfig"→AMQP_CONFIG_URL); given, it is used verbatim. Either way the resolved prefix is validated as upper-snake at declaration.Being one value costs nothing in adaptability, because it is a token first: its module statics are consulted only when it appears in a module's
imports:. A test can hand it a literalProvider(amqpConfig)({ value })without importing it and with no environment involved — there is a test asserting exactly that.The examples use it
examples/order-configis deleted. It was a hand-rolled prototype of this package — the samewholeNumber,port, anddescribeEnvIssues— now@btravstack/config/zodanddescribeIssues. All three deployables tradedenv.ts/env.spec.tsfor aconfig.ts.Config no longer travels through
main.RuntimeHost<Needs>already hands a runtime aContext, so runtimes declare their configs inneedsand read them directly:orderAmqpRuntime()andorderApiRuntime()take no arguments at all,temporalWorkerRuntimeno longer takes a namespace, andstart'sUNSATISFIED RUNTIME NEEDSgate proves the graph supplies them.main.tsis only the one-report boot:Two things fall out.
ConfigInvalidis aTaggedError, so the matcher enumerates it and everyoxlint-disable unthrown/no-catch-all-patternis deleted — theP._only existed becauseSchemaIssuesis a single type with no discriminant. And the report aggregates every config in the graph, so three typo'd variables across two packages come back from one failed boot.Decisions worth your attention
PORT→HTTP_PORTin order-api. Every other variable name is unchanged. A config variable isPREFIX_KEYwith a mandatory underscore, so no prefix-and-key pair joins to a barePORT. It only bites single-word names, butPORTis the one most PaaS hosts inject. Alternatives (empty prefix, per-key name override) are discussed in the comments.probes.portstays a direct read.startneeds it before the graph exists, and phase 1 has no API for reading one config outside a graph. It is still declared as a config so it is validated and reported with everything else; the constraint is commented at the line and belongs to phase 2.Blocked on btravstack/di#12
Config's return type needsConcretePortClassandPortInstance, neither published yet. They fix opposite shapes — a factory returning a data-built port needs the class name, a module exporting one needs the instance name — so neither closes the hole alone.Built and typechecked here against a local, uncommitted
pnpm-workspace.yamloverride. That override is not in this diff and must not land. Merge order: di#12 merges and publishes → bump this repo's catalog off the pinned0.1.0→ merge this.Test plan
build/typecheck/lint— 20/20 tasks greenstart-core's "binds 9000 when no probe port is given", which fails on this host because something else holds 9000 (untouched code)@btravstack/configat 100% line and function coverage, the repo's gate for packagesdeclarationoff to dodgeTS4023; reverted ind25ce6cand fixed properly in di--frozen-lockfilematches the deleted package (82377eb)🤖 Generated with Claude Code