protocol/json_rpc: own and export the response-envelope constructors - #1720
Conversation
Export errorResponseOf, add successResponseOf, and drop the four downstream copies in protocol/mcp and its stdio transport. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | 30c7259 | Commit Preview URL Branch Preview URL |
Aug 26 2026, 06:29 PM |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4d75c8837d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
_errResponse / _okResponse no longer exist; 18 references across the 66D design, two other todo files and mcp/types.ts named them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4da1f904ce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Name the two branches of the Response union so a caller holding one can say so. The constructors keep answering the union, which is what every dispatcher consumer needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
o2alexanderfedin
left a comment
There was a problem hiding this comment.
The move is exact and neither caller changed. All five base construction sites build the same two shapes in the same key order, expression-for-expression including how jsonrpc is bound. I measured it rather than reading it: 56 cases through mcpStep and dispatch at base and head — success, empty result, all five standard codes plus -32002, ids string/number/null, notification, errors with and without data — comparing Object.keys order, insertion-order stringify, and the stdio wire string and its UTF-8 bytes. Identical in all 56.
On the wire question I raised for myself: stringifyJson = stringify(sort), so stdio sorts keys and field order was never wire-observable there anyway. The stdio proof is byte-identical base to head and still builds its expected lines from independent literals, so it stays an independent oracle.
Mutation testing came out better than equivalence. Ten mutations at base and head, and row 5 goes the good way: forcing id to null in the error constructor survived at base — mcp's own proofs missed it — and is killed at head by dispatch.handlerError plus the stdio proofs, because the one shared expression now sits under the dispatch, mcp and stdio proofs simultaneously. Nothing killed at base survives at head. Row 9 (dropping data from the error schema) survives on both sides — pre-existing gap, not a regression.
Coverage didn't move out of the callers: mcp 82 → 82 asserts, stdio 19 → 19 (file byte-identical), json_rpc 28 → 36. Layering holds — json_rpc's import block is identical base to head, no dependency on fjs/protocol/mcp/** or fjs/mcp/**, and jsonrpc is correctly dropped from both mcp files. The deleted todo's four tasks are all genuinely done, the retargeted references all point at symbols that exist, and I ran retired-issue-identifiers.md's own check script at both revisions — 18 bare citations across 13 files, unchanged, so its count still holds and the i665-mcp correction is right.
One thing to fix:
6455286cc exports two new public types — SuccessResponse and ErrorResponse — and neither changelog/unreleased/1720.md nor the body's Changelog: block mentions them. Both still describe only the two constructors. The file and the body match each other byte-for-byte, so the block isn't malformed; the scope is just understated. The body's fjs test — 3400 is stale too (3402 now).
Two nits, neither blocking. effectful-dispatch-skeleton.md:23 cites mcp/module.f.mjs:264-287 and map-step-combinator.md:26 cites :347-350; both were already wrong at base (264-287 is _noParams/uninitializedState, and mcpStep's body starts at 320) and this PR shifted them 6 further while editing the identifiers inside those very snippets — cheap to fix while the files are open. And the new responseOf.error proof uses id = null as its input, so it alone can't catch an id-forcing mutation; responseOf.success uses id = 1 and does.
Gates: npm test 3398 → 3400 on the branch's own work, 3402 at the current head with main merged in; tsc --noEmit exit 0; purge matched nothing; no casts or try/catch introduced. No **BREAKING CHANGES:** needed — purely additive, no caller output moved. No HTML entities in the body this time.
The changelog entry named only the constructors, not SuccessResponse and ErrorResponse. Also retargets two mcp line ranges that were already wrong before this branch, and gives responseOf.error a non-null id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
|
All three fixed in The changelog gap (the blocking one). You're right that The two line citations. Fixed rather than left: The On row 5: that a mutation surviving at base dies at head is the concrete payoff of the extraction — five sites collapsing to one expression means one expression sitting under the
Generated by Claude Code |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. Both findings are closed.
The entry file now carries the second bullet for the type exports, and the body's Changelog: block is byte-identical to it — same md5, same 413 bytes, last section before the trailer. The test count reads 3402 and npm test gives exactly 3402/0.
Both line citations are content-correct now, which I checked by reading the ranges rather than trusting the commit message. effectful-dispatch-skeleton.md:23 → mcp/module.f.mjs:314-322, and 314 really is const [t, message] = decodeRequest(value), the first line of mcpStep's body, with the invalidRequest arm and the destructure following in order. map-step-combinator.md:26 → :411-414 is exact, line for line.
The proof change does what it claims: swapping errorResponseOf(null) for errorResponseOf('abc') means the id-forcing mutation now fails responseOf.error by name — I ran it, 3396/6 with that proof among them, where at the previous head it was blind to it. Flipping the new assertion gives a single failure naming its own row, so it is load-bearing.
Nothing else moved on the branch side: git diff 6914572458ae..HEAD -- fjs/protocol/json_rpc/module.f.mjs is empty, so the constructor expressions are exactly what I already verified as an exact move. No casts, no try/catch introduced anywhere in the PR.
Two one-line residuals, neither blocking. The 314-322 range stops at the snippet's fourth line — the elided remainder maps to 323-346, so 314-346 would cover the whole quoted block; the start is exact either way. And both doc snippets write step/pure where the source says ioStep/pureOk — that predates this branch (origin/main has the same naming), so not yours, just adjacent.
Gates: npm test 3402/0, tsc --noEmit exit 0, purge matched nothing.
…r lines The snippet's elided remainder runs to 346; 314-322 stopped short. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
|
Thanks — and I took the first residual rather than leaving it, in You're right about where the block ends. I read the range rather than trusting the arithmetic: the snippet's last two quoted lines, The second residual I'm leaving deliberately, and it isn't only that it predates the branch. Head is Generated by Claude Code |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg
o2alexanderfedin
left a comment
There was a problem hiding this comment.
The retarget is right, verified line for line rather than by trusting the message: 314-346 maps exactly — 314 the decodeRequest destructure, 315-317 the error arm, 318 the destructure, 322 the id === undefined guard, 323-344 the body the snippet elides as { ... }, 345 return pureOk(null), 346 the closing brace. The sibling retarget in map-step-combinator.md (411-414) is exact too.
But there is a third stale citation in the same document, and this one the PR did cause. Line 12:
Pure `dispatch` (`fjs/protocol/json_rpc/module.f.mjs:95-110`):
At head, 95-110 is the JSDoc for errorResponseOf/successResponseOf — the constructors this PR adds. dispatch moved to 126, and the quoted snippet spans 126-141. On origin/main, 95-110 is exactly right; inserting the two constructors above dispatch shifted it 31 lines. It was already wrong at 739e54ecd, the SHA I approved — eeaf549ce fixed the two mcp citations and missed this one, 8c7ee62df fixed an endpoint. Third of three, and now the only one left.
Delta is docs-only on the branch side — one commit, one file, one line. The Rust files in the diffstat all arrive via #1699 from main.
On the step/pure naming I flagged: confirmed pre-existing on origin/main in both snippets, so not yours. Worth knowing they're real but different functions — pure at effects/module.f.mjs:113 and step at :342 both exist and differ from pureOk at :268 and ioStep, so a reader can follow them to the wrong place.
Gates: npm test 3402/0 matching the body, tsc --noEmit exit 0, purge matched nothing. Changelog: still byte-identical to the entry file and last before the trailer.
Fixes
fjs/protocol/json_rpc/todo/response-constructors.md, deleted here.json_rpcownsjsonrpc,Id,RpcErrorand theResponseschema — but not,until now, the two constructors that build a response from them. Both existed
privately here and were re-rolled downstream, four copies in all:
json_rpc/module.f.mjs:82errorResponseOf, privatejson_rpc/module.f.mjs:113dispatchmcp/module.f.mjs:254,257_errResponse/_okResponse, byte-identical rebuildsmcp/stdio/module.f.mjs:51,56parseErrorResponse,internalErrorResponseThe error values already crossed the module boundary —
mcpimportsjsonrpc,rpcError,invalidRequest,invalidParams,methodNotFound— soonly the envelope constructors had to be reinvented, and only because they were
private.
errorResponseOfandsuccessResponseOfare now exported and everycopy is gone.
The two stdio constants fold onto the same export exactly as the issue
predicted:
parseErrorResponseiserrorResponseOf(null)(parseError)evaluated once, and
internalErrorResponseiserrorResponseOfspecialised tointernalError.Naming
…Of, matching theerrorResponseOfthat already existed, and pairing with thesuccessResponse/errorResponseschemas that were already exported — soeach constructor is named after the schema whose shape it builds a value of.
The branch types
SuccessResponseandErrorResponseare now exported fromtypes.ts, derivedfrom those same schemas exactly as
Responseis fromresponse.The constructors still return the union, deliberately. Returning the branch
was tried:
resultStep(e, f)infers its result type fromf, andmcpStep'sinitializeand gated-method arms are multi-returnfunctions whose earlyreturns are all error responses and whose last is branch-dependent. The arm's
type then becomes a union of two
Effecttypes differing only in their valueparameter, which does not unify, and
tscrejects it. Widening only the mixedreturnis not enough — inference takes the first — so all 13 return sites inmcpStepwould need an explicit annotation. Of the four consumers in the tree,three (
dispatch,mcpStep, the stdio transport) are dispatchers that answereither arm, and
Handle's result type isResponse | null. The branch typesare there for a caller that genuinely holds one; both constructors' JSDoc
records the reasoning.
Proof
dispatchcovered both constructors transitively already. They are public APInow, so
responseOf.success/responseOf.errorprove them directly, and eachparses its result back through this module's own
responseschema. That is thefact worth pinning: the constructor and the schema describing it cannot drift
apart — a change to either that broke the correspondence would fail here rather
than at some consumer.
responseOf.erroruses a non-nullidso it can seean
id-forcing mutation;errorResponseOf(null)stays covered bydispatch.invalidRequest.Reference repairs
Deleting the issue file, and renaming the private constructors out of existence,
would have left stale references behind:
json_rpc/todo/effectful-dispatch-skeleton.mdcited the deleted issue as "theenvelope constructors" — now points at the exports themselves.
todo/retired-issue-identifiers.mdcountedi665-mcpas appearing "twice asa working link", one of them inside the deleted file. Corrected to once. That
document's own check script still reports its stated 18 bare citations.
_errResponse/_okResponsesurvived in 18 places:mcp/types.ts:70(theonly one in shipped source), 15 in
mcp/todo/README.md— whose header asserts"the code below is the handler as it stands" — and one snippet each in
map-step-combinator.mdandeffectful-dispatch-skeleton.md. All retargeted,and the 66D header now also warns against rebuilding a private pair.
mcp/module.f.mjsline ranges cited by those last two files were alreadywrong before this branch and had drifted 6 further; retargeted to
314-346(the whole quoted block, elision included) and
411-414.Checks
npx tsc— clean; declaration emit verified with--emitDeclarationOnly.fjs test— 3402 pass, 0 fail (3398 before; two new proofs here, two fromtypes/rtti/ts: render trailing omittable positions optional #1708 via the
mainmerge).npm run cov— 100% lines / branches / functions, thresholds enforced.No behaviour change: every construction site builds the same object it built
before.
Changelog:
protocol/json_rpc:errorResponseOfis now exported and joined bysuccessResponseOf, so protocols layered on this module build responseenvelopes with the owner's constructors instead of their own.
protocol/json_rpc/types: newSuccessResponseandErrorResponse— the twobranches of the
Responseunion, for a caller that holds one and wants.resultor.errorwithout anincheck.🤖 Generated with Claude Code
https://claude.ai/code/session_014jZKR1C2qxXCueDSBKBnFg