Merge branch 'codex/canonical-tool-output' into codex/code-mode-typed-results

This commit is contained in:
Tianyi Cui
2026-07-23 01:15:13 +08:00
18 changed files with 403 additions and 119 deletions
+13 -13
View File
@@ -1111,7 +1111,7 @@ export class ToolRegistry extends Service {
if (deferredContexts === undefined) throw new Error('tool registry scheduler invariant violated: unprepared execution')
const resultWithDeferredContexts: ToolExecutionResult = deferredContexts.length === 0
? normalized
: this.markCanonical({
: this.markCanonical(exec, {
...normalized,
additionalContexts: [
...deferredContexts,
@@ -1262,7 +1262,7 @@ export class ToolRegistry extends Service {
const decisionContexts = decision.additionalContexts ?? []
if (decision.kind === 'block') {
const message = failureMessageFromContent(decision.feedback)
return this.markCanonical({
return this.markCanonical(exec, {
content: decision.feedback,
isError: true,
error: { message },
@@ -1283,24 +1283,24 @@ export class ToolRegistry extends Service {
const tool = this.get(exec.name, exec.agent)
if (tool === undefined) throw new ToolNotFoundError(exec.name)
const replaced = this.createSuccessResult(exec, tool, decision.value)
return this.markCanonical({
return this.markCanonical(exec, {
...replaced,
...additionalContexts.length > 0 ? { additionalContexts } : {},
})
}
return this.markCanonical({
return this.markCanonical(exec, {
...result,
...decision.content !== undefined ? { content: decision.content } : {},
...additionalContexts.length > 0 ? { additionalContexts } : {},
})
}
/** Results created by the registry already own a validated, frozen canonical value. */
private readonly canonicalResults = new WeakSet<object>()
/** Registry-normalized results and the exact dispatch that validated each value. */
private readonly canonicalResults = new WeakMap<object, ToolExecutionToken>()
/** Mark a registry-normalized result without freezing presentation fields prematurely. */
private markCanonical<T extends ToolExecutionResult>(result: T): T {
this.canonicalResults.add(result)
/** Mark one registry-normalized result as canonical only for its owning dispatch. */
private markCanonical<T extends ToolExecutionResult>(exec: ToolExecution, result: T): T {
this.canonicalResults.set(result, exec.token)
return result
}
@@ -1327,7 +1327,7 @@ export class ToolRegistry extends Service {
}
meta = snapshotProjection(tool.name, 'presentationMeta', projected)
}
return this.markCanonical(this.materializeFinalResult({
return this.markCanonical(exec, this.materializeFinalResult({
isError: false,
value,
content,
@@ -1337,9 +1337,9 @@ export class ToolRegistry extends Service {
/** Normalize an around-dispatch wrapper's authored result through the owning output contract. */
private normalizeDispatchResult(exec: ToolExecution, result: ToolExecutionResult): ToolExecutionResult {
if (this.canonicalResults.has(result)) return result
if (this.canonicalResults.get(result) === exec.token) return result
if (result.isError) {
return this.markCanonical({
return this.markCanonical(exec, {
isError: true,
error: result.error,
content: result.content,
@@ -1350,7 +1350,7 @@ export class ToolRegistry extends Service {
const tool = this.get(exec.name, exec.agent)
if (tool === undefined) throw new ToolNotFoundError(exec.name)
const normalized = this.createSuccessResult(exec, tool, result.value)
return this.markCanonical({
return this.markCanonical(exec, {
...normalized,
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
})
+107 -27
View File
@@ -86,6 +86,26 @@ const CONSTRAINT_KEYWORDS = new Set([
const ANNOTATION_KEYWORDS = new Set(['description', 'title', 'default', 'examples'])
const SCHEMA_TYPES: readonly JsonSchemaType[] = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']
/* jscpd:ignore-start -- this realm boundary mirrors the session-owned lossless-JSON intrinsic test */
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): boolean {
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor')
const constructor: unknown = descriptor?.value
if (typeof constructor !== 'function') return false
try {
return constructor.name === name
&& constructor.prototype === prototype
&& Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`
} catch {
return false
}
}
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
function isIntrinsicObjectPrototype(value: object): boolean {
return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
}
/**
* Test for a realm-agnostic plain JSON record without accepting arrays or
* exotic objects.
@@ -94,8 +114,61 @@ const SCHEMA_TYPES: readonly JsonSchemaType[] = ['object', 'array', 'string', 'n
*/
export function isPlainJsonRecord(value: unknown): value is Record<string, unknown> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
const proto: unknown = Object.getPrototypeOf(value)
return proto === null || Object.getPrototypeOf(proto) === null
try {
const prototype: unknown = Object.getPrototypeOf(value)
return prototype === null
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype)
} catch {
return false
}
}
/** Whether an array uses one realm's intrinsic `Array.prototype`. */
function hasPlainArrayPrototype(value: unknown[]): boolean {
const prototype: unknown = Object.getPrototypeOf(value)
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
const objectPrototype: unknown = Object.getPrototypeOf(prototype)
return typeof objectPrototype === 'object'
&& objectPrototype !== null
&& isIntrinsicObjectPrototype(objectPrototype)
}
/* jscpd:ignore-end */
/** Return whether a record contains only own enumerable string keys. */
function hasOnlyEnumerableStringKeys(value: object): boolean {
try {
return Reflect.ownKeys(value)
.every(key => typeof key === 'string' && Object.prototype.propertyIsEnumerable.call(value, key))
} catch {
return false
}
}
/**
* Test for an ordinary schema record whose keys survive JSON projection.
* @param value - candidate record from any JavaScript realm.
* @returns Whether the record has an intrinsic prototype and only own enumerable string keys.
*/
export function isJsonSchemaRecord(value: unknown): value is Record<string, unknown> {
return isPlainJsonRecord(value) && hasOnlyEnumerableStringKeys(value)
}
/**
* Test for a dense ordinary array with no JSON-invisible decorations.
* @param value - candidate array from any JavaScript realm.
* @returns Whether the array is intrinsic, dense, and undecorated.
*/
export function isPlainJsonArray(value: unknown): value is unknown[] {
if (!Array.isArray(value)) return false
try {
if (!hasPlainArrayPrototype(value) || Reflect.ownKeys(value).length !== value.length + 1) return false
for (let index = 0; index < value.length; index++) {
if (!Object.hasOwn(value, index)) return false
}
return true
} catch {
return false
}
}
/** Lossless finite JSON number, excluding negative zero. */
@@ -133,12 +206,13 @@ function checkObjectSchemaTail(
properties: unknown,
violations: string[],
): void {
const required = node.required
if (Object.hasOwn(node, 'required')) {
if (!Array.isArray(required) || required.some(entry => typeof entry !== 'string')) {
const hasRequired = Object.hasOwn(node, 'required')
const required = hasRequired ? node.required : undefined
if (hasRequired) {
if (!isPlainJsonArray(required) || required.some(entry => typeof entry !== 'string')) {
violations.push(`${path}.required must be an array of strings`)
} else {
const declared = isPlainJsonRecord(properties) ? properties : {}
const declared = isJsonSchemaRecord(properties) ? properties : {}
for (const key of required as string[]) {
if (!Object.hasOwn(declared, key)) violations.push(`${path}.required names "${key}" which is not in properties`)
}
@@ -169,7 +243,7 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
}
const { node, path } = task
if (!isPlainJsonRecord(node)) {
if (!isJsonSchemaRecord(node)) {
violations.push(`${path} must be a schema object`)
continue
}
@@ -192,10 +266,10 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
}
violations.push(`${path}.${key} is not a supported keyword (subset: type/oneOf/properties/required/additionalProperties/items/enum/const + annotations)`)
}
if (node.description !== undefined && typeof node.description !== 'string') {
if (Object.hasOwn(node, 'description') && typeof node.description !== 'string') {
violations.push(`${path}.description must be a string`)
}
if (node.title !== undefined && typeof node.title !== 'string') {
if (Object.hasOwn(node, 'title') && typeof node.title !== 'string') {
violations.push(`${path}.title must be a string`)
}
@@ -215,7 +289,7 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
if (hasOneOf) {
const oneOf = node.oneOf
tasks.push({ kind: 'one-of-tail', node, path })
if (!Array.isArray(oneOf) || oneOf.length < 2) {
if (!isPlainJsonArray(oneOf) || oneOf.length < 2) {
violations.push(`${path}.oneOf must be an array of at least two schemas`)
} else {
for (let index = oneOf.length - 1; index >= 0; index--) {
@@ -249,10 +323,10 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
switch (schemaType) {
case 'object': {
const properties = node.properties
const properties = Object.hasOwn(node, 'properties') ? node.properties : undefined
tasks.push({ kind: 'object-tail', node, path, properties })
if (Object.hasOwn(node, 'properties')) {
if (!isPlainJsonRecord(properties)) {
if (!isJsonSchemaRecord(properties)) {
violations.push(`${path}.properties must be an object of schemas`)
} else {
const entries = Object.entries(properties)
@@ -275,18 +349,21 @@ function checkSchemaNode(root: unknown, rootPath: string, violations: string[],
case 'integer':
case 'boolean':
case 'null': {
const allowed = node.enum
const enumValid = Array.isArray(allowed)
const hasEnum = Object.hasOwn(node, 'enum')
const allowed = hasEnum ? node.enum : undefined
const enumValid = isPlainJsonArray(allowed)
&& allowed.length > 0
&& allowed.every(entry => scalarMatches(schemaType, entry))
if (Object.hasOwn(node, 'enum') && !enumValid) {
if (hasEnum && !enumValid) {
violations.push(`${path}.enum must be a non-empty array of ${schemaType} values`)
}
const constValid = scalarMatches(schemaType, node.const)
if (Object.hasOwn(node, 'const')) {
const hasConst = Object.hasOwn(node, 'const')
const declaredConst = hasConst ? node.const : undefined
const constValid = scalarMatches(schemaType, declaredConst)
if (hasConst) {
if (!constValid) {
violations.push(`${path}.const must be a ${schemaType} value`)
} else if (enumValid && !allowed.includes(node.const as JsonSchemaScalar)) {
} else if (enumValid && !allowed.includes(declaredConst)) {
violations.push(`${path}.const must be one of ${path}.enum when both are declared`)
}
}
@@ -320,7 +397,8 @@ export function assertSupportedJsonSchema(schema: unknown): asserts schema is Js
export function assertObjectJsonSchema(schema: unknown): asserts schema is ObjectJsonSchema {
const violations: string[] = []
checkSchemaNode(schema, 'schema', violations, new Set())
if (violations.length === 0 && (schema as JsonSchemaNode).type !== 'object') {
if (violations.length === 0
&& (!isJsonSchemaRecord(schema) || !Object.hasOwn(schema, 'type') || schema.type !== 'object')) {
violations.push('schema.type must be "object" (structured output is object-rooted)')
}
if (violations.length > 0) throw new JsonSchemaError(violations)
@@ -395,8 +473,9 @@ function valueFrame(node: JsonSchemaNode, value: unknown, path: string): ValueFr
/** Validate one scalar node after its primitive type check. */
function checkScalarValue(node: JsonSchemaNode, value: unknown, path: string): string[] {
if (node.enum !== undefined && !node.enum.includes(value as JsonSchemaScalar)) {
return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(node.enum)}`]
const allowed = Object.hasOwn(node, 'enum') ? node.enum : undefined
if (allowed !== undefined && !allowed.includes(value as JsonSchemaScalar)) {
return [`"${diagnosticPath(path)}" must be one of ${JSON.stringify(allowed)}`]
}
if (Object.hasOwn(node, 'const') && value !== node.const) {
return [`"${diagnosticPath(path)}" must be ${JSON.stringify(node.const)}`]
@@ -455,9 +534,9 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
continue
}
const nodeType = frame.node.type
const nodeType = Object.hasOwn(frame.node, 'type') ? frame.node.type : undefined
frame.catches = !(nodeType !== undefined && !(SCHEMA_TYPES as readonly unknown[]).includes(nodeType))
const oneOf = frame.node.oneOf
const oneOf = Object.hasOwn(frame.node, 'oneOf') ? frame.node.oneOf : undefined
if (oneOf !== undefined) {
frame.kind = 'oneOf'
frame.children = Array.from(oneOf, branch => ({ node: branch, value: frame.value, path: frame.path }))
@@ -477,9 +556,10 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
finish([`"${diagnosticPath(frame.path)}" must be an object`])
break
}
const properties = frame.node.properties ?? {}
const properties = Object.hasOwn(frame.node, 'properties') ? frame.node.properties ?? {} : {}
const violations: string[] = []
for (const key of frame.node.required ?? []) {
const required = Object.hasOwn(frame.node, 'required') ? frame.node.required ?? [] : []
for (const key of required) {
if (!Object.hasOwn(frame.value, key) || frame.value[key] === undefined) {
violations.push(`missing required property "${propertyPath(frame.path, key)}"`)
}
@@ -490,7 +570,7 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
children.push({ node: child, value: frame.value[key], path: propertyPath(frame.path, key) })
}
const tailViolations: string[] = []
if (frame.node.additionalProperties === false) {
if (Object.hasOwn(frame.node, 'additionalProperties') && frame.node.additionalProperties === false) {
for (const key of Object.keys(frame.value)) {
if (!Object.hasOwn(properties, key)) {
tailViolations.push(`"${propertyPath(frame.path, key)}" is not a declared property (additionalProperties: false)`)
@@ -510,7 +590,7 @@ function checkValue(schema: JsonSchemaNode, value: unknown, path: string): strin
finish([`"${diagnosticPath(frame.path)}" must be an array`])
break
}
const items = frame.node.items
const items = Object.hasOwn(frame.node, 'items') ? frame.node.items : undefined
const children = items === undefined
? []
: frame.value.flatMap((entry, index): ValueChild[] => [{ node: items, value: entry, path: `${frame.path}[${index}]` }])
+52 -42
View File
@@ -4,7 +4,7 @@ import { HarnessError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import type { ToolDefinition, ToolRunContext, ToolResult } from './index.ts'
import { assertSupportedJsonSchema, isPlainJsonRecord, JsonSchemaError, validateJsonSchemaValue } from './json-schema.ts'
import { assertSupportedJsonSchema, isJsonSchemaRecord, isPlainJsonArray, JsonSchemaError, validateJsonSchemaValue } from './json-schema.ts'
import type { JsonSchemaNode, JsonSchemaScalar, ObjectJsonSchema } from './json-schema.ts'
import type { ToolCallView, ToolResultView } from './presentation.ts'
@@ -100,7 +100,10 @@ export type ParameterPropertySpec = ValueSchemaSpec & { required?: true }
* Tool parameter schema. The map itself is an implicit open object root;
* requiredness remains a per-property `required: true` annotation.
*/
export type ParameterSchemaSpec = Record<string, ParameterPropertySpec>
export type ParameterSchemaSpec = {
[key: string]: ParameterPropertySpec
[key: symbol]: never
}
/** Raw JSON Schema projection of the implicit parameter object. */
export interface ParameterJsonSchema extends ObjectJsonSchema {
@@ -110,30 +113,29 @@ export interface ParameterJsonSchema extends ObjectJsonSchema {
/** Flatten an intersection into one object type for readable hovers. */
type Simplify<T> = { [K in keyof T]: T[K] } & {}
/** Keys of a property map marked `required: true`. */
type RequiredKeys<S extends ParameterSchemaSpec> = {
[K in keyof S]: S[K] extends { required: true } ? K : never
}[keyof S]
/** String keys of one property map; runtime compilation rejects symbol keys. */
type StringKeyOf<S> = Extract<keyof S, string>
/** Advance the bounded inference walk through one nested schema node. */
type NextDepth<D extends readonly unknown[]> = readonly [...D, unknown]
/** Keys of a property map marked `required: true`. */
type RequiredKeys<S> = {
[K in StringKeyOf<S>]: S[K] extends { required: true } ? K : never
}[StringKeyOf<S>]
/** Infer the declared value of one parameter property without key optionality. */
type InferProperty<P extends ParameterPropertySpec, D extends readonly unknown[]> =
P extends ValueSchemaSpec ? InferValue<P, D> : never
type InferProperty<P, Depth extends unknown[]> = InferValueAt<P, Depth>
/** Infer an implicit property map into required and optional object keys. */
type InferProperties<S extends ParameterSchemaSpec, D extends readonly unknown[]> = Simplify<
& { [K in RequiredKeys<S>]: InferProperty<S[K], D> }
& { [K in Exclude<keyof S, RequiredKeys<S>>]?: InferProperty<S[K], D> }
type InferProperties<S, Depth extends unknown[]> = Simplify<
& { [K in RequiredKeys<S>]: InferProperty<S[K], Depth> }
& { [K in Exclude<StringKeyOf<S>, RequiredKeys<S>>]?: InferProperty<S[K], Depth> }
>
/** Infer an explicit object node, including its declared openness. */
type InferObject<S extends ObjectValueSchemaSpec, D extends readonly unknown[]> =
S extends { properties: infer P extends ParameterSchemaSpec }
type InferObject<S extends { additionalProperties: boolean }, Depth extends unknown[]> =
S extends { properties: infer P }
? S['additionalProperties'] extends true
? InferProperties<P, D> & Record<string, JsonValue>
: InferProperties<P, D>
? InferProperties<P, Depth> & Record<string, JsonValue>
: InferProperties<P, Depth>
: S['additionalProperties'] extends true
? Record<string, JsonValue>
: Record<string, never>
@@ -144,25 +146,33 @@ type InferScalar<S, Fallback> =
S extends { enum: readonly (infer E)[] } ? E :
Fallback
/** Add one schema-container level to bounded compile-time inference. */
type NextInferenceDepth<Depth extends unknown[]> = [unknown, ...Depth]
/** Infer one node without recursively checking it against the full author union. */
type InferValueAt<S, Depth extends unknown[]> =
Depth['length'] extends 16 ? JsonValue :
S extends { type: 'string' } ? InferScalar<S, string> :
S extends { type: 'number' | 'integer' } ? InferScalar<S, number> :
S extends { type: 'boolean' } ? InferScalar<S, boolean> :
S extends { type: 'null' } ? null :
S extends { type: 'array' }
? S extends { items: infer I } ? InferValueAt<I, NextInferenceDepth<Depth>>[] : JsonValue[]
: S extends { type: 'object'; additionalProperties: boolean }
? InferObject<S, NextInferenceDepth<Depth>>
: S extends { type: 'json' } ? JsonValue :
S extends { oneOf: readonly unknown[] }
? InferValueAt<S['oneOf'][number], NextInferenceDepth<Depth>>
: never
/**
* Infer the TypeScript value accepted by an author-facing value schema.
* Output schemas may therefore infer object, array, scalar, or null roots.
* Infer the TypeScript value accepted by an author-facing value schema. Exact
* inference is bounded to 16 container levels, then falls back to `JsonValue`.
*/
export type InferValue<S extends ValueSchemaSpec, D extends readonly unknown[] = readonly []> =
D['length'] extends 12 ? JsonValue :
S extends StringValueSchemaSpec ? InferScalar<S, string> :
S extends NumberValueSchemaSpec | IntegerValueSchemaSpec ? InferScalar<S, number> :
S extends BooleanValueSchemaSpec ? InferScalar<S, boolean> :
S extends NullValueSchemaSpec ? null :
S extends ArrayValueSchemaSpec
? S extends { items: infer I extends ValueSchemaSpec } ? InferValue<I, NextDepth<D>>[] : JsonValue[]
: S extends ObjectValueSchemaSpec ? InferObject<S, NextDepth<D>> :
S extends JsonValueSchemaSpec ? JsonValue :
S extends OneOfValueSchemaSpec ? InferValue<S['oneOf'][number], NextDepth<D>> :
never
export type InferValue<S> = InferValueAt<S, []>
/** Infer the TypeScript argument object for an implicit parameter schema. */
export type InferArgs<S extends ParameterSchemaSpec> = InferProperties<S, readonly []>
export type InferArgs<S> = InferProperties<S, []>
const ANNOTATION_KEYS = ['description', 'title', 'default', 'examples'] as const
@@ -278,11 +288,11 @@ function runSchemaCompiler(initial: CompileTask): void {
continue
}
if (task.kind === 'property') {
if (!isPlainJsonRecord(task.property)) authorError(`${task.path} must be a value schema object`)
if (!isJsonSchemaRecord(task.property)) authorError(`${task.path} must be a value schema object`)
if (Object.hasOwn(task.property, 'required') && task.property.required !== true) {
authorError(`${task.path}.required must be true when present`)
}
if (task.property.required === true) task.required.push(task.key)
if (Object.hasOwn(task.property, 'required') && task.property.required === true) task.required.push(task.key)
tasks.push({
kind: 'value',
input: task.property,
@@ -293,7 +303,7 @@ function runSchemaCompiler(initial: CompileTask): void {
continue
}
if (task.kind === 'property-map') {
if (!isPlainJsonRecord(task.input)) authorError(`${task.path} must be an object of value schemas`)
if (!isJsonSchemaRecord(task.input)) authorError(`${task.path} must be an object of value schemas`)
if (seen.has(task.input)) authorError(`${task.path} is circular`)
seen.add(task.input)
const compiled: CompiledPropertyMap = { properties: {} }
@@ -319,7 +329,7 @@ function runSchemaCompiler(initial: CompileTask): void {
}
const { input, path } = task
if (!isPlainJsonRecord(input)) authorError(`${path} must be a value schema object`)
if (!isJsonSchemaRecord(input)) authorError(`${path} must be a value schema object`)
if (seen.has(input)) authorError(`${path} is circular`)
seen.add(input)
const authorKeys = [...ANNOTATION_KEYS, ...(task.allowRequired ? ['required'] : [])]
@@ -330,7 +340,7 @@ function runSchemaCompiler(initial: CompileTask): void {
if (Object.hasOwn(input, 'oneOf')) {
assertAuthorKeys(input, path, [...authorKeys, 'oneOf', 'type'])
if (Object.hasOwn(input, 'type')) authorError(`${path} cannot declare both type and oneOf`)
if (!Array.isArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`)
if (!isPlainJsonArray(input.oneOf)) authorError(`${path}.oneOf must be an array of at least two value schemas`)
const branches: JsonSchemaNode[] = []
node.oneOf = branches
copyAnnotations(input, node)
@@ -346,7 +356,8 @@ function runSchemaCompiler(initial: CompileTask): void {
continue
}
switch (input.type) {
const inputType = Object.hasOwn(input, 'type') ? input.type : undefined
switch (inputType) {
case 'json':
assertAuthorKeys(input, path, [...authorKeys, 'type'])
copyAnnotations(input, node)
@@ -388,12 +399,11 @@ function runSchemaCompiler(initial: CompileTask): void {
case 'boolean':
case 'null':
assertAuthorKeys(input, path, [...authorKeys, 'type', 'enum', 'const'])
node.type = input.type
node.type = inputType
copyAnnotations(input, node)
if (Object.hasOwn(input, 'enum')) {
node.enum = Array.isArray(input.enum)
? Array.from(input.enum as unknown[], entry => entry as JsonSchemaScalar)
: input.enum as JsonSchemaScalar[]
if (!isPlainJsonArray(input.enum)) authorError(`${path}.enum must be a non-empty array of scalar values`)
node.enum = Array.from(input.enum, entry => entry as JsonSchemaScalar)
}
if (Object.hasOwn(input, 'const')) node.const = input.const as JsonSchemaScalar
break