Deferred, deliberately
Explanation. This register records proposals the repository has weighed, including decisions that later shipped. Read it before proposing a feature, package, lint rule or gate that may already have an answer.
A feature request that matches an entry below is not a new idea. A struck-through entry has SHIPPED — proposing it again re-proposes work that exists. An open entry names the trigger that would reopen it; absent that trigger, the answer is no.
Reaping the shared containers.
withReuse()deliberately keeps them out of Ryuk's hands, so they outlive the run — which is the whole point locally and pure waste on an ephemeral CI runner that discards the machine anyway. There is no harm today; if it ever matters, the fix is aCI-conditionalwithReuse()ininternal/test-infra/src/containers.ts, not a teardown that would pull a container out from under a concurrent workspace.The
@btravstack/oxlintrule banningcurrentUnit()outside infrastructure adapters (Thesis #2) — it needs a way to identify an adapter.Traces and metrics inClosed by shipping the shape as written (issue #64):@btravstack/observability.Tracer/Meterports and the OTelNodeSDKas a resourceful provider whosereleaseflushes, behind the@btravstack/observability/otelsubpath on thepinooptional-peer protocol;UnitSpanModuleas a span per unit through a starter's own boundunitoption; W3Ctraceparenthonoured inbound by@btravstack/http-serverand@btravstack/amqp-worker(trace-id field only — the parent span id is dropped, never half-carried), with@btravstack/temporal-workerdeliberately keeping the workflow id as its correlation. A starter's OWN instrumentation is graph-owned: it contributes toInstrumentations(declared incore) andotel()registers every contribution, so composing@btravstack/prismadeclares engine tracing and composingotel()turns it on — nothing registers when no SDK is composed. The auto-instrumentation constraint held for the PRELOAD alone: the preload cannot be DI-provided, so the package ships the graph-owned half and the--importline stays the deployment's. Surfaces inpackages/observability/AGENTS.md.AClosed by the doc-samples gate (issue #94):docs-examples.test-d.tsfor@btravstack/temporal-worker,@btravstack/amqp-workerand@btravstack/observability.docs/scripts/extract-doc-samples.tsnow compiles everytsfence on the site and in every README underpnpm typecheck— see Documentation site. The trigger had fired again (the amqp and temporal READMEs still showed the two-argumentexecutefrom before the branded tenant, a wrong consumer key, and aDuplicateOrder-only triage missing theInvalidOrderId/InvalidQuantityarms), and the sweep that built the gate fixed a dozen more: pages predating declaredneeds, the slices split,defineHttp, and di's keyed deps. Regression-proved: reverting the temporal README'sexecuteto the two-argument form failstypecheckwithTS2554naming the README.The HTTP half is no longer deferred, because that trigger fired twice in two days (issues #74 and #75, six pages describing
examples/order-apias it was before it had authentication).examples/order-api/src/docs-examples.test-d.tsis the gate, and it lives in the example rather than inpackages/http-server: the samples call the realPlaceOrder/FindOrder/FindCustomeragainst the realcontractthrough the application's ownsrc/auth.ts, and a stub would have accepted every broken call — passing an order id where a tenant goes was exactly the drift. It covers both controllers, the composed router, theHttpModuleroot whose authenticators ride the router, the lifted single-slice root and the bareapi.OrpcRouter(contract)({ inject?, unit?, sync })form the three router-shaped pages share —docs/index.md,docs/reference/http-server.mdanddocs/how-to/serve-orpc-over-http.md, none of which puts a controller in between. Every deps record it compiles is keyed, di's one shape sincefeat(di)!: a provider declares its dependencies by name; a positional array is refused asnot assignable to parameter of type 'Readonly<Record<string, AnyPort>>', which is what several pages outside this gate carried until the doc-samples gate swept them. It does not cover the pages' own contract declarations:zodand@btravstack/contractareexamples/order-api-contract's dependencies, notexamples/order-api's, so a fragment is compiled where it lives — though a marker removed from it still fails this file, since the controllers are typed by it. No config change was needed; the workspace already runs the test-d pass undertypecheck.The local loop is the production shape, not an exception to it (issue #67). Three deployments meant three terminals, and the tempting fix — a kernel API booting all three in one process, which
startwould happily support — was measured and declined: it cannot watch (reloading an ESM graph in place is a bespoke loader), it shares one event loop and the process-global uncaught handlers, so one crash takes all three down and a blocking worker starves the API, and it exercises the drain through one shared signal instead of three real ones. A dev loop that misrepresents failure isolation teaches the wrong lesson about the very thesis it sits under. Sopnpm devisturbo run dev --filter=./examples/*: one process per deployment,tsx watchon each, output prefixed by workspace — the mechanics are inexamples/AGENTS.md."HTML" here means fragments, and only fragments (#179's open question). Four things were being called HTML support: a template engine's rendered pages, an endpoint answering
text/htmlfor a partial, static assets with an SPA fallback, and JSX/SSR with a component model.htmx()is the second — the one closest to a procedure and hardest to tell apart from one, which is why it sharpened the second-answerer question rather than dodging it. The first and fourth are the application's (#166), with i18n beside them: each is a library choice with no wiring problem underneath, and choosing one would put an untyped template body or a second escaping model in the framework. Mail stays string-in for the same reason —docs/how-to/send-a-formatted-email.mdis the pattern, and the position is the rootAGENTS.md's, thesis #1. The third is declined (#161): the ingress or a CDN serves assets, and with no SPA left the asset set is htmx plus a stylesheet. The narrower option — a minimal asset route sopnpm devneeds no ingress — was the development loop's argument, not production's, and two files a layout can link from a CDN or vendor inline do not carry it. The reasoning, and the CSP that stays the deployment's, is inpackages/http-server/AGENTS.md's cross-cutting section.Each transport package is named for the HALF it implements.
http-server,temporal-workerandamqp-worker— nothttp,temporal,amqp, which claimed a whole transport and delivered the serving side of it. The calling side uses upstream libraries.@btravstack/amqp-clientand@btravstack/temporal-clientbind resourceful callers to DI scopes without worker peers; the oRPC caller uses@orpc/clientdirectly. Three things decided the spelling:- The neighbours qualify both sides —
@orpc/server/@orpc/client,@temporal-contract/worker//client— so an unqualified name reads as the umbrella containing both, which is exactly what it is not. - "worker" rather than a uniform
-server, because it is Temporal's and AMQP's own word, and becausetemporal-serveralready means the Temporal Service — the clusterinternal/test-infraruns astemporalio/auto-setup. A name that suggests you are booting the cluster is worse than a suffix that varies. - A client is a PACKAGE, never a subpath. Peers are per-package, so
@btravstack/http-server/clientwould drag@orpc/serverinto a consumer that only ever calls — the same reasonexamples/*-contractare packages of their own: a client must be able to take a contract without the server.
The rename cost nothing because only
@btravstack/dihad ever been published (0.1.0); after the first release it would have cost a deprecation cycle, which is why it happened when it did.- The neighbours qualify both sides —
The LEAF is one shape, and oRPC's is the one (issue #207, closed by btravstack/temporal-contract#415 and btravstack/amqp-contract#671). A developer writes the same function on all three transports: one record carrying everything the invocation has — the input included — and that input repeated as a second positional parameter.
Schematically, HTTP and Temporal use
place: ({ errors, context, input }) => …; AMQP also carriesrawinprocess: ({ errors, context, raw, input }) => ….oRPC is the reference because it is the most widely used of the three, not because the shape is inherently better: a developer arriving here is more likely to have seen it than either of the others, so it is what costs the least to match. It is oRPC's shape down to the DUPLICATION — its
ProcedureHandlerOptionscarriesinputand its handler still takes it positionally — so({ errors }, input)remains the same call, and a caller picks. The record is what the docs teach, because it is the spelling that needs no_placeholder when a leaf wants only its input.inputis the field name on all three, notargsormessage. A local synonym per transport would put the relearning back on the one field every leaf touches.The convergence happened UPSTREAM, not in an adapter here. A starter could have reshaped the leaf at the call site it already owns, and did not: the leaf's type is INFERRED from each contract library's own types, so an adapter would re-derive rather than infer it, and it would leave the starter's documentation and the library's documentation describing the same function with two different signatures. Both libraries are this org's and were in beta, so it cost a beta bump rather than a deprecation cycle.
The AMQP half was the one that was not merely cosmetic: it had no helpers record at all, so a handler wanting "infrastructure comes back" imported and constructed
RetryableErrorby hand. Its record now carriesretryableandnonRetryablebesideerrors, so that triage reads like HTTP's — andraw, the amqplib delivery, which used to be a third parameter no other transport had.The naming asymmetry is a separate, smaller decision and is still NOT made.
AmqpHandler/AmqpHandlersdiffer by one letter, andTemporalWorkflowActivities/TemporalActivitiesgive the piece the longer name where HTTP gives the composer a different word entirely (OrpcController/OrpcRouter). The recommendation on the table is HTTP's rule — piece and composer get different words, never singular and plural — but it renames public API on two packages with no obviously-right replacement, so it is recorded here rather than guessed at.A GraphQL mutation's result union is not derived from
E(issue #474).Eis a type, and a union's members are runtime object types, so deriving them needs runtime error classes or a registry — and the registry is the list that drifts. The exhaustivemapErrCasesa resolver already writes cannot: wideningEfails every fold at compile time. A union, where a schema wants one, is built from that same fold. The trigger to reopen: a schema whose unions are written twice — once as types, once as the fold.@btravstack/pothosdoes build a union from a registry, for a field calling an oRPC procedure only, and the drift is what it closes: the members are the codes the procedure's contract declares at runtime, and a registry missing one fails that field at compile time. Outside an oRPC procedure the decline above stands.A contract-to-GraphQL argument adapter is deferred (issue #474). oRPC 2 keeps a procedure's runtime schemas as an untyped
inputSchemasarray, but its type parameters keep the declared schema, so a gateway can validate a GraphQL argument through the contract's own schemas, typed — whichexamples/order-graphql-api'sinputOfdoes in a dozen lines. A framework helper would put an@orpc/contractpeer on@btravstack/graphql-server, which accepts anyGraphQLSchemaand owns no protocol. Graph exposure and joins stay explicit either way. The trigger: a second gateway writing the same function.The trigger fired, and it shipped as a package of its own:
@btravstack/pothosvalidates a field's input through the procedure's own contract, for a gateway that builds its schema with Pothos, so@btravstack/graphql-serverkeeps no oRPC peer. Its spec ispackages/pothos/AGENTS.md.Filtering with operators is declined (issue #261). A normed
{ field, op, value }owes an operator set per type, nesting and null handling, and then a translator into Prisma or SQL — a query builder those libraries already are, and thesis #8's territory. It has no trigger. A listing declaring its own filter fields throughpageRequestOf({ minQuantity })is the narrow version, and it ships.Sorting is deferred with its cursor rule already chosen.Closed by shipping (issue #261):sortableBy(item, keys)is a curated vocabulary checked against the item's own schema,pageRequestOf(filters, { sortableBy, defaultSort })takes it beside the filters withdefaultSortrequired, and a sortedkeyset(request)answersSortedKeyset<F> | CursorRefused— a union the adapter must branch on, discriminated byresumable. The cursor rule this entry used to name as decided-but-unbuilt is now how it ships: a cursor is valid only for the sort it was issued under (field:direction, carried verbatim rather than hashed — the vocabulary is already public in the emitted document), and a mismatch is refused withreason: "sort-mismatch", told apart from an unreadable cursor's"malformed". The full position ispackages/contract/AGENTS.md's.Server push is answering's, over SSE; WebSocket is declined for now (issue #162). SSE already ships and drains (#137), so the role map stays three roles. An upgrade path would be a second protocol on a connection that is no longer a request, and a push runtime would be the fourth runtime the map refuses by default. Fan-out across replicas is an application pattern — a broker subscription per replica feeding that process's streams — not a shared connection registry, which would be N registries. The trigger that would reopen it is a client-to-server stream a request cannot carry. The position is the root
AGENTS.md's, thesis #1.Rate limiting is declined; it belongs to the ingress (issue #502). A per-process counter is the wrong unit: an
apideployment is N pods, so a per-process budget is N independent budgets and none of them is the limit anybody meant. The ingress or gateway counts a request once, refuses it before it costs a process anything, and answers429withRetry-After. An application that wants an in-process limit anyway passes an oRPC plugin throughorpc()'splugins, or a Yoga plugin throughgraphql()'s. The position ispackages/http-server/AGENTS.md's. The trigger would be a deployment with no ingress in front of it.Configuration profiles, file layering, a secrets provider and live refresh are declined (issue #167): the process reads its environment once at boot, and the four around it are the platform's — an overlay per environment,
node --env-file, a secret store synced into a Kubernetes Secret, and a rolling restart that drains. The reasons arepackages/config/AGENTS.md's; the Kubernetes shape isdocs/how-to/configure-a-kubernetes-deployment.md. A trigger would be a value that must change faster than a rollout can replace a pod.Introspection: the route list ships as a recipe; a graph dump and a REPL are declined (issue #168). The route list is a few lines over
openApiDocument— the composed contract is what a modulith serves, and the document is already that list in a standard format — with the htmx and login routes read off the composition root, since they live outside the contract (docs/how-to/list-what-a-process-serves.md). A graph dump would print a graph the type checker already accepted: a missing provider or a forgotten slice import is a compile error at the root naming the port, and what does reach runtime is aWiringDefectnaming it. A REPL against a booted application is a test booting the real root. The trigger would be a wiring failure that reaches runtime without naming its port.No resident CLI and no per-slice generator (issue #59). A slice is byte-shaped the same on all three transports — one transport file minting a piece from
(contract, key)and onemodule.ts— so the sibling directory is the template (docs/how-to/add-a-slice.md). The two arguments for a generator are gone: the wiring mistake it was to prevent (a slice on disk the root forgot to import) is already a compile error naming the port, and the edit no template collapses — the contract key lives in another package — is the source of truth staying where a client can take it. Aturbo genconfig would cost a commitment to turborepo as the blessed project shape, which was not made. A one-shot new-app initialiser (npm create btravstack) remains separate. The copyable standalone template (#437) removes manifest, peer, contract and composition setup; a trial still needs clone/copy, project naming, install, check/build and a port. An initializer would currently remove only the copy commands and name edit, so no package is added.A router carries the DECLARED authenticators, not the reachable ones (issue #288). Narrowing a root's
Needsto the schemes its composed pieces actually name was declined for stability: a contract gaining a marker never silently changes a root's requirements, and the compile error stays at the root composing whatdefineHttpdeclared. The cost — a router-only root providessessionCodec()for a scheme it never reaches — is accepted and stated inpackages/http-server/AUTH.md. The trigger would be a deployment that cannot satisfy a declared scheme's needs at all.Outbound token exchange shipped (issue #504).
@btravstack/oauth-clientkeeps the service's client credentials grant and adds RFC 8693 exchange forTokenRequest.onBehalfOf: a subject credential with its token type and, when required, the acting service's credential. Keys include both credentials, so subjects never share cached tokens. The real-issuer trigger is met by the gate's Keycloak, with native delegation enabled; its preview status is stated on the reference page. The issuer authorizes delegation and producesact, rather than the client inventing an identity assertion.A background runtime is deferred (issue #508). A process whose only work is
outbox()'s relay would want a runtime with probes and a drain and no answerer. Composing the relay into the API or the consumer process already gives it both — replicas take turns per tenant, the kernel serves the probes for any runtime, and the relay stops in the scope's teardown — and a relay-only process is a few lines ofdocs/how-to/write-a-runtime.md: a runtime holding one referenced timer, cleared instop(docs/how-to/publish-a-message.md, "Where the relay runs"). A shipped runtime for one starter's loop would be a fourth runtime by default. The trigger is a second starter growing a loop of its own.The outbox's tenants from a port is deferred (issue #495). The relay serves the tenants
OUTBOX_TENANTSnames, so onboarding one is a configuration change and a rollout, and an unlisted tenant is silently undelivered and invisible to the lag check. A source re-read each sweep makes the tenant count unbounded, and the concurrency bound (#493) is what keeps that from being the pool's count; the trigger is revisiting once the bound has run in a deployment, against onboarding that cannot wait for a rollout.packages/outbox/AGENTS.mdcarries the cost.Parking a poison outbox message is declined (issue #494). The publisher holds the terminal lever — record the message as a dead letter and answer
Ok, and the relay marks it — at the stated cost that later facts about that subject go out without it. An attempt counter and a parked column would skip on a count rather than on unpublishability, letting a tombstone overtake its create silently after a broker outage, and would migrate every adopter's table, which is the application's model. The trigger would be a terminal refusal the publisher cannot recognise as one.Binding HTTP request scopes as values is declined; the record stays stated twice and is gated both ways (issue #384). The proposal was a value-bearing second stage,
auth.withRequestScopes({ anonymous, user }), carrying the modules on the router soHttpModulenames nounit:. A prototype overexamples/order-apiheld every guarantee the types can state, emitted declarations under both compilers and had today's TS7022 profile — and lost on three measured costs: the value import makesauth.ts↔ the scope modules a runtime cycle that compiles clean and dies at load (Cannot access 'auth' before initialization), so a third file is mandatory with no diagnostic; two bindings with equal types and different modules can be refused only through a duplicate provider on an internal port; andhttp()/httpServer()keepunit:anyway. What the duplicate record had actually cost — a root binding fewer kinds thanunits<…>()declared, compiling and answering500— is closed by the gate instead: a declared kind the root serves must be bound. The measurements arepackages/http-server/AGENTS.md's; the teaching half shipped asdocs/how-to/use-request-scoped-services.md. The trigger would be a root that must bind its kinds where the gate cannot see it — a hand-rolledhttp()root becoming the common shape.A per-request deadline (
HTTP_REQUEST_DEADLINE_MS) is declined (issue #501). Its disconnect half shipped: the kernel aborts a unit's signal once the unit settles, and@btravstack/http-serversettles a unit on the response's'close', so a client that walks away aborts the handler it left behind. With that in place the deadline is the ingress's upstream (response) timeout — not its read timeout, which bounds only the client sending the request: an ingress that gives up waiting for the response closes the connection, and that cancels the work through the same path a disconnect does — so a second clock in the process would only have to be kept below the first. The trigger would be a deployment with no ingress in front of it, or one whose ingress has no response timeout that closes the connection.