Remove effect-zod bridge (#26956)
This commit is contained in:
@@ -4,8 +4,6 @@ import { EffectBridge } from "@/effect/bridge"
|
||||
import type { InstanceContext } from "@/project/instance"
|
||||
import { SessionID, MessageID } from "@/session/schema"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { ZodOverride } from "@opencode-ai/core/effect-zod"
|
||||
import { Config } from "@/config/config"
|
||||
import { MCP } from "../mcp"
|
||||
import { Skill } from "../skill"
|
||||
@@ -35,12 +33,11 @@ export const Info = Schema.Struct({
|
||||
model: Schema.optional(Schema.String),
|
||||
source: Schema.optional(Schema.Literals(["command", "mcp", "skill"])),
|
||||
// Some command templates are lazy promises from MCP prompt resolution.
|
||||
template: Schema.Unknown.annotate({ [ZodOverride]: z.promise(z.string()).or(z.string()) }),
|
||||
template: Schema.Unknown,
|
||||
subtask: Schema.optional(Schema.Boolean),
|
||||
hints: Schema.Array(Schema.String),
|
||||
}).annotate({ identifier: "Command" })
|
||||
|
||||
// for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it
|
||||
export type Info = Omit<Schema.Schema.Type<typeof Info>, "template"> & { template: Promise<string> | string }
|
||||
|
||||
export function hints(template: string) {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { ZodOverride } from "@opencode-ai/core/effect-zod"
|
||||
|
||||
// The original Zod schema carried an external $ref pointing at the models.dev
|
||||
// JSON schema. That external reference is not a named SDK component — it is a
|
||||
// literal pointer to an outside schema — so the walker cannot re-derive it
|
||||
// from AST metadata. Preserve the exact original Zod via ZodOverride.
|
||||
export const ConfigModelID = Schema.String.annotate({
|
||||
[ZodOverride]: z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" }),
|
||||
})
|
||||
export const ConfigModelID = Schema.String
|
||||
|
||||
export type ConfigModelID = Schema.Schema.Type<typeof ConfigModelID>
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as LSPClient from "./client"
|
||||
import path from "path"
|
||||
import { pathToFileURL, fileURLToPath } from "url"
|
||||
import * as LSPServer from "./server"
|
||||
import z from "zod"
|
||||
import { Config } from "@/config/config"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Process } from "@/util/process"
|
||||
@@ -14,7 +13,6 @@ import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { containsPath } from "@/project/instance-context"
|
||||
import { NonNegativeInt } from "@opencode-ai/core/schema"
|
||||
import { ZodOverride } from "@opencode-ai/core/effect-zod"
|
||||
|
||||
const log = Log.create({ service: "lsp" })
|
||||
|
||||
@@ -56,9 +54,7 @@ export const Status = Schema.Struct({
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
root: Schema.String,
|
||||
status: Schema.Literals(["connected", "error"]).annotate({
|
||||
[ZodOverride]: z.union([z.literal("connected"), z.literal("error")]),
|
||||
}),
|
||||
status: Schema.Literals(["connected", "error"]),
|
||||
}).annotate({ identifier: "LSPStatus" })
|
||||
export type Status = typeof Status.Type
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { MCP } from "@/mcp"
|
||||
import { Project } from "@/project/project"
|
||||
import { Session } from "@/session/session"
|
||||
import { ToolJsonSchema } from "@/tool/json-schema"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import * as EffectZod from "@opencode-ai/core/effect-zod"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { Effect, Option } from "effect"
|
||||
import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
|
||||
@@ -84,7 +84,7 @@ export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "exper
|
||||
return list.map((item) => ({
|
||||
id: item.id,
|
||||
description: item.description,
|
||||
parameters: EffectZod.toJsonSchema(item.parameters),
|
||||
parameters: ToolJsonSchema.fromTool(item),
|
||||
}))
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import z from "zod"
|
||||
import { NamedError } from "@opencode-ai/core/util/error"
|
||||
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
@@ -55,7 +54,7 @@ export const APIError = namedSchemaError("APIError", {
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||
})
|
||||
export type APIError = z.infer<typeof APIError.Schema>
|
||||
export type APIError = Schema.Schema.Type<typeof APIError.Schema>
|
||||
export const ContextOverflowError = namedSchemaError("ContextOverflowError", {
|
||||
message: Schema.String,
|
||||
responseBody: Schema.optional(Schema.String),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import * as EffectZod from "@opencode-ai/core/effect-zod"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
@@ -21,6 +20,7 @@ import PROMPT_PLAN from "../session/prompt/plan.txt"
|
||||
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
||||
import MAX_STEPS from "../session/prompt/max-steps.txt"
|
||||
import { ToolRegistry } from "@/tool/registry"
|
||||
import { ToolJsonSchema } from "@/tool/json-schema"
|
||||
import { MCP } from "../mcp"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
@@ -565,7 +565,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
providerID: input.model.providerID,
|
||||
agent: input.agent,
|
||||
})) {
|
||||
const schema = ProviderTransform.schema(input.model, EffectZod.toJsonSchema(item.parameters))
|
||||
const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item))
|
||||
tools[item.id] = tool({
|
||||
description: item.description,
|
||||
inputSchema: jsonSchema(schema),
|
||||
|
||||
164
packages/opencode/src/tool/json-schema.ts
Normal file
164
packages/opencode/src/tool/json-schema.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
import { JsonSchema, Schema } from "effect"
|
||||
import type * as Tool from "./tool"
|
||||
|
||||
type JsonObject = Record<string, unknown>
|
||||
const cache = new WeakMap<Schema.Top, JSONSchema7>()
|
||||
|
||||
export function fromSchema(schema: Schema.Top): JSONSchema7 {
|
||||
const cached = cache.get(schema)
|
||||
if (cached) return cached
|
||||
|
||||
const document = Schema.toJsonSchemaDocument(schema, { additionalProperties: true })
|
||||
const result = normalize({
|
||||
$schema: JsonSchema.META_SCHEMA_URI_DRAFT_2020_12,
|
||||
...document.schema,
|
||||
...(Object.keys(document.definitions).length > 0 ? { $defs: document.definitions } : {}),
|
||||
})
|
||||
const inlined = dropDefinitionsIfResolved(inlineLocalReferences(result))
|
||||
if (!isJsonSchema(inlined)) throw new Error("tool JSON Schema helper produced a non-schema value")
|
||||
cache.set(schema, inlined)
|
||||
return inlined
|
||||
}
|
||||
|
||||
export function fromTool(tool: Tool.Def): JSONSchema7 {
|
||||
return tool.jsonSchema ?? fromSchema(tool.parameters as Schema.Top)
|
||||
}
|
||||
|
||||
function normalize(value: unknown, options: { stripNull?: boolean } = {}): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => normalize(item))
|
||||
if (!isRecord(value)) return value
|
||||
|
||||
const required = Array.isArray(value.required)
|
||||
? new Set(value.required.filter((item) => typeof item === "string"))
|
||||
: undefined
|
||||
const schema = Object.fromEntries(
|
||||
Object.entries(value).map(([key, item]) => [
|
||||
key,
|
||||
key === "properties" && isRecord(item)
|
||||
? Object.fromEntries(
|
||||
Object.entries(item).map(([name, property]) => [
|
||||
name,
|
||||
normalize(property, { stripNull: !required?.has(name) }),
|
||||
]),
|
||||
)
|
||||
: normalize(item),
|
||||
]),
|
||||
)
|
||||
|
||||
if (schema.additionalProperties === true) delete schema.additionalProperties
|
||||
|
||||
if (options.stripNull && Array.isArray(schema.anyOf)) {
|
||||
const withoutNull = schema.anyOf.filter((item) => !isRecord(item) || item.type !== "null")
|
||||
if (withoutNull.length !== schema.anyOf.length) return normalize({ ...schema, anyOf: withoutNull })
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.anyOf)) {
|
||||
const withoutNull = schema.anyOf
|
||||
const number = withoutNull.find((item) => isRecord(item) && item.type === "number")
|
||||
const nonFinite = withoutNull.filter(
|
||||
(item) => isRecord(item) && Array.isArray(item.enum) && item.enum.every((entry) => isNonFiniteNumber(entry)),
|
||||
)
|
||||
if (number && nonFinite.length === withoutNull.length - 1) {
|
||||
const { anyOf: _, ...rest } = schema
|
||||
return normalize({ ...number, ...rest })
|
||||
}
|
||||
|
||||
if (isEmptyStructUnion(withoutNull)) {
|
||||
const { anyOf: _, ...rest } = schema
|
||||
return normalize({ type: "object", properties: {}, ...rest })
|
||||
}
|
||||
|
||||
if (withoutNull.length === 1 && isRecord(withoutNull[0])) {
|
||||
const { anyOf: _, ...rest } = schema
|
||||
return normalize({ ...withoutNull[0], ...rest })
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(schema.allOf) && schema.allOf.every(isRecord) && canFlattenAllOf(schema.allOf, schema)) {
|
||||
const { allOf, ...rest } = schema
|
||||
return normalize({ ...Object.assign({}, ...allOf), ...rest })
|
||||
}
|
||||
|
||||
if (schema.type === "integer" && schema.maximum === undefined) {
|
||||
return { minimum: Number.MIN_SAFE_INTEGER, ...schema, maximum: Number.MAX_SAFE_INTEGER }
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is JsonObject {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function isJsonSchema(value: unknown): value is JSONSchema7 {
|
||||
return typeof value === "boolean" || isRecord(value)
|
||||
}
|
||||
|
||||
function isNonFiniteNumber(value: unknown) {
|
||||
return value === "NaN" || value === "Infinity" || value === "-Infinity"
|
||||
}
|
||||
|
||||
function isEmptyStructUnion(items: unknown[]) {
|
||||
return (
|
||||
items.length === 2 &&
|
||||
items.some((item) => isRecord(item) && item.type === "object" && item.properties === undefined) &&
|
||||
items.some((item) => isRecord(item) && item.type === "array" && item.items === undefined)
|
||||
)
|
||||
}
|
||||
|
||||
function canFlattenAllOf(allOf: JsonObject[], parent: JsonObject) {
|
||||
const keys = new Set(Object.keys(parent).filter((key) => key !== "allOf"))
|
||||
return allOf.every((item) =>
|
||||
Object.keys(item).every((key) => {
|
||||
if (keys.has(key)) return false
|
||||
keys.add(key)
|
||||
return true
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function inlineLocalReferences(value: unknown, definitions?: JsonObject, seen = new Set<string>()): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => inlineLocalReferences(item, definitions, seen))
|
||||
if (!isRecord(value)) return value
|
||||
|
||||
const localDefinitions = definitions ?? (isRecord(value.$defs) ? value.$defs : undefined)
|
||||
if (typeof value.$ref === "string" && localDefinitions) {
|
||||
const name = value.$ref.match(/^#\/\$defs\/(.+)$/)?.[1] ?? value.$ref.match(/^#\/definitions\/(.+)$/)?.[1]
|
||||
if (name && !seen.has(name)) {
|
||||
const target = localDefinitions[name]
|
||||
if (target) {
|
||||
const { $ref: _, ...rest } = value
|
||||
return inlineLocalReferences(
|
||||
{ ...(isRecord(target) ? target : {}), ...rest },
|
||||
localDefinitions,
|
||||
new Set(seen).add(name),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, item]) => [key, inlineLocalReferences(item, localDefinitions, seen)]),
|
||||
)
|
||||
}
|
||||
|
||||
function dropDefinitionsIfResolved(value: unknown): unknown {
|
||||
if (!isRecord(value) || hasLocalReference(value)) return value
|
||||
const { $defs: _, definitions: __, ...rest } = value
|
||||
return rest
|
||||
}
|
||||
|
||||
function hasLocalReference(value: unknown): boolean {
|
||||
if (Array.isArray(value)) return value.some(hasLocalReference)
|
||||
if (!isRecord(value)) return false
|
||||
if (
|
||||
typeof value.$ref === "string" &&
|
||||
(value.$ref.startsWith("#/$defs/") || value.$ref.startsWith("#/definitions/"))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return Object.values(value).some(hasLocalReference)
|
||||
}
|
||||
|
||||
export * as ToolJsonSchema from "./json-schema"
|
||||
@@ -15,9 +15,9 @@ import { SkillTool } from "./skill"
|
||||
import * as Tool from "./tool"
|
||||
import { Config } from "@/config/config"
|
||||
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
|
||||
import type { JSONSchema7, JSONSchema7Definition } from "@ai-sdk/provider"
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { ZodOverride } from "@opencode-ai/core/effect-zod"
|
||||
import { Plugin } from "../plugin"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID, type ModelID } from "../provider/schema"
|
||||
@@ -137,17 +137,19 @@ export const layer: Layer.Layer<
|
||||
const custom: Tool.Def[] = []
|
||||
|
||||
function fromPlugin(id: string, def: ToolDefinition): Tool.Def {
|
||||
// Plugin tools define their args as a raw Zod shape. Wrap the
|
||||
// derived Zod object in a `Schema.declare` so it slots into the
|
||||
// Schema-typed framework, and annotate with `ZodOverride` so the
|
||||
// walker emits the original Zod object for LLM JSON Schema.
|
||||
const zodParams = z.object(def.args)
|
||||
const parameters = Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success).annotate({
|
||||
[ZodOverride]: zodParams,
|
||||
})
|
||||
// Plugin tools still expose Zod args publicly; keep that compatibility
|
||||
// boxed at the registry boundary and give the LLM the original JSON Schema.
|
||||
const entries = Object.entries(def.args)
|
||||
const allZod = entries.every((entry) => isZodType(entry[1]))
|
||||
const zodParams = allZod ? z.object(def.args) : undefined
|
||||
const jsonSchema = zodParams ? zodJsonSchema(zodParams) : legacyJsonSchema(entries)
|
||||
const parameters = zodParams
|
||||
? Schema.declare<unknown>((u): u is unknown => zodParams.safeParse(u).success)
|
||||
: Schema.Unknown
|
||||
return {
|
||||
id,
|
||||
parameters,
|
||||
jsonSchema,
|
||||
description: def.description,
|
||||
execute: (args, toolCtx) =>
|
||||
Effect.gen(function* () {
|
||||
@@ -323,8 +325,13 @@ export const layer: Layer.Layer<
|
||||
const output = {
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
jsonSchema: tool.jsonSchema,
|
||||
}
|
||||
yield* plugin.trigger("tool.definition", { toolID: tool.id }, output)
|
||||
const jsonSchema =
|
||||
output.parameters === tool.parameters || output.jsonSchema !== tool.jsonSchema
|
||||
? output.jsonSchema
|
||||
: undefined
|
||||
return {
|
||||
id: tool.id,
|
||||
description: [
|
||||
@@ -335,6 +342,7 @@ export const layer: Layer.Layer<
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
parameters: output.parameters,
|
||||
jsonSchema,
|
||||
execute: tool.execute,
|
||||
formatValidationError: tool.formatValidationError,
|
||||
}
|
||||
@@ -376,4 +384,50 @@ export const defaultLayer = Layer.suspend(() =>
|
||||
),
|
||||
)
|
||||
|
||||
function isZodType(value: unknown): value is z.ZodType {
|
||||
return typeof value === "object" && value !== null && "_zod" in value
|
||||
}
|
||||
|
||||
function isJsonSchemaDefinition(value: unknown): value is JSONSchema7Definition {
|
||||
return typeof value === "boolean" || (typeof value === "object" && value !== null && !Array.isArray(value))
|
||||
}
|
||||
|
||||
function legacyJsonSchema(entries: [string, unknown][]): JSONSchema7 {
|
||||
const properties = Object.fromEntries(
|
||||
entries.filter((entry): entry is [string, JSONSchema7Definition] => isJsonSchemaDefinition(entry[1])),
|
||||
)
|
||||
return {
|
||||
type: "object",
|
||||
properties,
|
||||
required: Object.keys(properties),
|
||||
}
|
||||
}
|
||||
|
||||
function zodJsonSchema(schema: z.ZodType): JSONSchema7 {
|
||||
const result = normalizeZodJsonSchema(z.toJSONSchema(schema, { io: "input" }))
|
||||
if (!isJsonSchemaObject(result)) throw new Error("plugin tool Zod schema produced a non-object JSON Schema")
|
||||
const { $defs, ...rest } = result
|
||||
return (
|
||||
$defs && isJsonSchemaObject($defs) ? { ...rest, definitions: $defs as JSONSchema7["definitions"] } : rest
|
||||
) as JSONSchema7
|
||||
}
|
||||
|
||||
function normalizeZodJsonSchema(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => normalizeZodJsonSchema(item))
|
||||
if (typeof value !== "object" || value === null) return value
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.filter((entry) =>
|
||||
(entry[0] === "exclusiveMaximum" || entry[0] === "exclusiveMinimum") && typeof entry[1] === "boolean"
|
||||
? false
|
||||
: true,
|
||||
)
|
||||
.map(([key, item]) => [key, normalizeZodJsonSchema(item)]),
|
||||
)
|
||||
}
|
||||
|
||||
function isJsonSchemaObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export * as ToolRegistry from "./registry"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Effect, Schema } from "effect"
|
||||
import type { JSONSchema7 } from "@ai-sdk/provider"
|
||||
import type { MessageV2 } from "../session/message-v2"
|
||||
import type { Permission } from "../permission"
|
||||
import type { SessionID, MessageID } from "../session/schema"
|
||||
@@ -38,6 +39,7 @@ export interface Def<
|
||||
id: string
|
||||
description: string
|
||||
parameters: Parameters
|
||||
jsonSchema?: JSONSchema7
|
||||
execute(args: Schema.Schema.Type<Parameters>, ctx: Context): Effect.Effect<ExecuteResult<M>>
|
||||
formatValidationError?(error: unknown): string
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||
export const Parameters = Schema.Struct({
|
||||
url: Schema.String.annotate({ description: "The URL to fetch content from" }),
|
||||
format: Schema.Literals(["text", "markdown", "html"])
|
||||
.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("markdown" as const)))
|
||||
.annotate({
|
||||
description: "The format to return the content in (text, markdown, or html). Defaults to markdown.",
|
||||
}),
|
||||
default: "markdown",
|
||||
})
|
||||
.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("markdown" as const))),
|
||||
timeout: Schema.optional(Schema.Number).annotate({ description: "Optional timeout in seconds (max 120)" }),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Schema } from "effect"
|
||||
import z from "zod"
|
||||
import { zod } from "@opencode-ai/core/effect-zod"
|
||||
|
||||
/**
|
||||
* Create a Schema-backed NamedError-shaped class.
|
||||
@@ -11,22 +9,14 @@ import { zod } from "@opencode-ai/core/effect-zod"
|
||||
* OpenAPI/SDK output is byte-identical to the original NamedError schema.
|
||||
*
|
||||
* Preserves the existing surface:
|
||||
* - static `Schema` (Zod schema of the wire shape)
|
||||
* - static `Schema` (Effect schema of the wire shape)
|
||||
* - static `isInstance(x)`
|
||||
* - instance `toObject()` returning `{ name, data }`
|
||||
* - `new X({ ...data }, { cause })`
|
||||
*/
|
||||
export function namedSchemaError<Tag extends string, Fields extends Schema.Struct.Fields>(tag: Tag, fields: Fields) {
|
||||
// Wire shape matches the original NamedError output so the SDK stays stable.
|
||||
const dataSchema = Schema.Struct(fields)
|
||||
const wire = z
|
||||
.object({
|
||||
name: z.literal(tag),
|
||||
data: zod(dataSchema),
|
||||
})
|
||||
.meta({ ref: tag })
|
||||
|
||||
// Effect Schema for the wire shape — used by HttpApi OpenAPI generation.
|
||||
// Wire shape matches the original NamedError output so the SDK stays stable.
|
||||
const effectSchema = Schema.Struct({
|
||||
name: Schema.Literal(tag),
|
||||
data: dataSchema,
|
||||
@@ -35,7 +25,7 @@ export function namedSchemaError<Tag extends string, Fields extends Schema.Struc
|
||||
type Data = Schema.Schema.Type<typeof dataSchema>
|
||||
|
||||
class NamedSchemaError extends Error {
|
||||
static readonly Schema = wire
|
||||
static readonly Schema = effectSchema
|
||||
static readonly EffectSchema = effectSchema
|
||||
static readonly tag = tag
|
||||
public static isInstance(input: unknown): input is NamedSchemaError {
|
||||
|
||||
Reference in New Issue
Block a user