Skip to content

A contract key containing a literal dot compiles green and then 404s #121

Description

@btravers

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:

const contract = { "a.b": oc };
const piece = api.HttpController(contract, "a.b")({ sync: () => () => OkAsync("ok") });
const router = api.HttpRouter(contract)([piece]);   // compiles clean

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.
  • Worth closing before @btravstack/http is published, per the final review; not worth blocking feat(http)!: a controller is minted by contract key and composed as an array #116.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions