You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found in PR #116's final review, on the dotted-path work. Recorded debt: the ponytail: comment on nest in packages/http/src/orpc.ts already names this ceiling and its upgrade paths; this issue gives it an owner.
The failure
HttpController(contract, key) now takes a dotted path into the contract tree, and the composing arm rebuilds the nested implementation record by splitting each piece's path on .. A contract whose own key contains a literal dot therefore breaks:
ControllerKeyOf<{ "a.b": oc }> is "a.b", so the mint is accepted; coverage is satisfied; then nest splits on the dot and produces { a: { b: fn } }, which routerOf's stray-key drop discards because the contract has no a. The route 404s with a fully green compile and no diagnostic anywhere — the failure class this stack exists to eliminate, so it should not stay documented-only forever.
It is unreachable today: no contract in the repo keys a dot, and the escape hatch is real — the (deps, arm) form splits nothing and serves such a contract correctly.
Why the encoding is still right
The alternatives were worse. A tuple key (["v1", "orders"]) does not survive as a port-id suffix without its own encoding, and the port id is what carries the key so the composing form can recover it without spelling it twice — the mechanism AmqpHandler and authenticatorPort share. A separator that cannot appear in an identifier would be unreadable at the call site, which is the surface a developer actually types.
The fix, when it is worth taking
Refuse the ambiguity at the type level rather than guarding at runtime:
exclude keys containing . from ControllerKeyOf, so a literal-dot key is never mintable; and
give LeafPathsOf a matching arm, so a contract carrying such a key is refused at the composing call — with its own marker sentence — rather than silently left uncoverable (which would report the leaf as "uncovered", the wrong diagnostic for "unsliceable").
Roughly two lines each plus type tests, and it deletes the failure class instead of documenting it. Alternative if that proves awkward: a runtime guard in nest that defects on a segment mismatch — worse, because it moves a compile-time-knowable fact to runtime.
Acceptance
A contract key containing . is refused at HttpController(contract, key), with a diagnostic naming the key and saying why.
A contract carrying such a key is refused at HttpRouter(contract)([...]) against a marker that says unsliceable, not uncovered, and points at the (deps, arm) form.
Pinned in packages/http/src/controller.test-d.ts; the ponytail: comment on nest and the ceiling paragraphs in packages/http/CLAUDE.md / docs/reference/http.md are replaced by a statement of the refusal.
Found in PR #116's final review, on the dotted-path work. Recorded debt: the
ponytail:comment onnestinpackages/http/src/orpc.tsalready names this ceiling and its upgrade paths; this issue gives it an owner.The failure
HttpController(contract, key)now takes a dotted path into the contract tree, and the composing arm rebuilds the nested implementation record by splitting each piece's path on.. A contract whose own key contains a literal dot therefore breaks:ControllerKeyOf<{ "a.b": oc }>is"a.b", so the mint is accepted; coverage is satisfied; thennestsplits on the dot and produces{ a: { b: fn } }, whichrouterOf's stray-key drop discards because the contract has noa. The route 404s with a fully green compile and no diagnostic anywhere — the failure class this stack exists to eliminate, so it should not stay documented-only forever.It is unreachable today: no contract in the repo keys a dot, and the escape hatch is real — the
(deps, arm)form splits nothing and serves such a contract correctly.Why the encoding is still right
The alternatives were worse. A tuple key (
["v1", "orders"]) does not survive as a port-id suffix without its own encoding, and the port id is what carries the key so the composing form can recover it without spelling it twice — the mechanismAmqpHandlerandauthenticatorPortshare. A separator that cannot appear in an identifier would be unreadable at the call site, which is the surface a developer actually types.The fix, when it is worth taking
Refuse the ambiguity at the type level rather than guarding at runtime:
.fromControllerKeyOf, so a literal-dot key is never mintable; andLeafPathsOfa matching arm, so a contract carrying such a key is refused at the composing call — with its own marker sentence — rather than silently left uncoverable (which would report the leaf as "uncovered", the wrong diagnostic for "unsliceable").Roughly two lines each plus type tests, and it deletes the failure class instead of documenting it. Alternative if that proves awkward: a runtime guard in
nestthat defects on a segment mismatch — worse, because it moves a compile-time-knowable fact to runtime.Acceptance
.is refused atHttpController(contract, key), with a diagnostic naming the key and saying why.HttpRouter(contract)([...])against a marker that says unsliceable, not uncovered, and points at the(deps, arm)form.packages/http/src/controller.test-d.ts; theponytail:comment onnestand the ceiling paragraphs inpackages/http/CLAUDE.md/docs/reference/http.mdare replaced by a statement of the refusal.@btravstack/httpis published, per the final review; not worth blocking feat(http)!: a controller is minted by contract key and composed as an array #116.