feat(typert): carry Remote absence without a second result envelope

Absence crosses the wire as a missing field: an omitted argument and a void
or undefined result both arrive as an absent JSON member, and the wide RPC
result slot accepts a success response without a value. Parameters declared
optional stay optional in the generated consumer declaration, so a business
signature is never widened to `T | undefined` to suit the wire. The weak SRC
descriptor reads parameter names from a JavaScript signature and cannot see
optionality, so a source-launched Host accepts an absent field and the strict
LIB pass owns rejecting a genuinely missing required parameter.
This commit is contained in:
imccyu
2026-08-11 19:09:13 +08:00
parent 8c31290abb
commit 027e5fe9a4
7 changed files with 149 additions and 77 deletions
+5 -3
View File
@@ -61,7 +61,6 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('settings-rejected'), message: z.string(), details: z.object({ ns: z.string() }) }),
z.object({ code: z.literal('settings-not-exposed'), message: z.string(), details: z.object({ ns: z.string() }) }),
z.object({ code: z.literal('settings-conflict'), message: z.string(), details: z.object({ ns: z.string(), expected: z.number(), actual: z.number() }) }),
z.object({ code: z.literal('credential-rejected'), message: z.string(), details: z.object({ ref: z.string() }) }),
z.object({ code: z.literal('model-discovery-failed'), message: z.string(), details: z.object({ settingsNs: z.string(), baseURL: z.string().optional() }) }),
z.object({ code: z.literal('title-invalid'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
z.object({ code: z.literal('fork-unavailable'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
@@ -91,6 +90,9 @@ export function rpcResultSchema<T>(value: z.ZodType<T>): z.ZodUnion<readonly [z.
}
// ---- The four wire full-form schemas (payload/result.value slots stay wide — business layer does the second parse) ----
// The wide value slot is optional: a void business result serializes with no
// `value` field at all. Each endpoint's own second parse still requires its
// declared value, so absence never passes for a method that returns data.
/** ClientRequest full form (payload stays wide — the business layer runs the second parse). */
export const clientRequestSchema = z.object({
@@ -104,7 +106,7 @@ export const clientRequestSchema = z.object({
export const serverResponseSchema = z.object({
type: z.literal('server-response'),
rpcId: rpcIdSchema,
result: rpcResultSchema(z.unknown()),
result: rpcResultSchema(z.unknown().optional()),
}) as unknown as z.ZodType<ServerResponse>
/** ServerRequest full form (payload stays wide). */
@@ -119,7 +121,7 @@ export const serverRequestSchema = z.object({
export const clientResponseSchema = z.object({
type: z.literal('client-response'),
rpcId: rpcIdSchema,
result: rpcResultSchema(z.unknown()),
result: rpcResultSchema(z.unknown().optional()),
}) as unknown as z.ZodType<ClientResponse>
/** Wire full-form union (discriminated by type). */