{"version":3,"file":"canonicalize.d.ts","sourceRoot":"","sources":["../../../src/core/tool-call/canonicalize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE/D;AA6BD,yCAAyC;AACzC,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,mBAAmB,GAAG,mBAAmB,CAuCnF;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,mBAAmB,GAAG,MAAM,EAAE,CAE1G","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { CanonicalJsonSchema } from \"./types.js\";\n\n/**\n * Deterministic JSON serialization. Keys are sorted, numbers are compacted,\n * and primitive whitespace is normalized so identical logical objects produce\n * identical bytes. This is the source of stable, replay-safe hashes and the\n * foundation of the storm-breaker call fingerprint.\n */\nexport function canonicalStableStringify(value: unknown): string {\n\treturn stableStringify(value);\n}\n\nfunction stableStringify(value: unknown): string {\n\tif (value === null || value === undefined) return \"null\";\n\tif (typeof value === \"string\") return JSON.stringify(value);\n\tif (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n\tif (typeof value === \"number\") return Number.isFinite(value) ? String(value) : \"null\";\n\tif (typeof value === \"bigint\") return String(value);\n\tif (typeof value === \"function\" || typeof value === \"symbol\") {\n\t\tthrow new Error(\"cannot stable stringify non-serializable value\");\n\t}\n\tif (Array.isArray(value)) {\n\t\tconst items = value.map((v) => stableStringify(v)).join(\",\");\n\t\treturn `[${items}]`;\n\t}\n\tif (typeof value === \"object\") {\n\t\tconst record = value as Record<string, unknown>;\n\t\tconst keys = Object.keys(record).sort();\n\t\tconst parts: string[] = [];\n\t\tfor (const key of keys) {\n\t\t\tconst serialized = stableStringify(record[key]);\n\t\t\tif (serialized === undefined) continue;\n\t\t\tparts.push(`${JSON.stringify(key)}:${serialized}`);\n\t\t}\n\t\treturn `{${parts.join(\",\")}}`;\n\t}\n\tthrow new Error(`cannot stable stringify value of type ${typeof value}`);\n}\n\n/** SHA-256 hex digest of UTF-8 bytes. */\nexport function sha256Hex(input: string): string {\n\treturn createHash(\"sha256\").update(input, \"utf-8\").digest(\"hex\");\n}\n\n/** Stable hash over any value via canonical serialization. */\nexport function stableHash(value: unknown): string {\n\treturn sha256Hex(canonicalStableStringify(value));\n}\n\n/**\n * Deterministically canonicalize a JSON Schema so that semantically identical\n * schemas (eg. OpenAPI-style nullable vs JSON-Schema nullable) collapse to the\n * same logical shape. This is used for stable compatibility hashing and to\n * make `additionalProperties`/`required` handling predictable. It never drops\n * restrictions; it only normalizes representation.\n */\nexport function canonicalizeSchema(schema: CanonicalJsonSchema): CanonicalJsonSchema {\n\tif (schema == null || typeof schema !== \"object\" || Array.isArray(schema)) {\n\t\t// Unsupported root: preserve the object wrapper if possible.\n\t\treturn { type: \"object\" };\n\t}\n\tconst out: CanonicalJsonSchema = {};\n\tif (schema.type !== undefined) out.type = schema.type;\n\tif (Array.isArray(schema.required)) {\n\t\tout.required = [...new Set(schema.required)].sort();\n\t} else if (typeof schema.required === \"string\") {\n\t\t// OpenAPI `required: true` on a field is meaningless at root; drop.\n\t}\n\tif (schema.properties && typeof schema.properties === \"object\") {\n\t\tconst props: Record<string, CanonicalJsonSchema> = {};\n\t\tfor (const [k, v] of Object.entries(schema.properties)) {\n\t\t\tif (v && typeof v === \"object\") props[k] = canonicalizeSchema(v as CanonicalJsonSchema);\n\t\t}\n\t\tout.properties = props;\n\t}\n\tif (schema.items !== undefined) {\n\t\tif (Array.isArray(schema.items)) {\n\t\t\tout.items = schema.items.map((i) =>\n\t\t\t\ti && typeof i === \"object\" ? canonicalizeSchema(i as CanonicalJsonSchema) : { type: \"object\" },\n\t\t\t);\n\t\t} else if (schema.items && typeof schema.items === \"object\") {\n\t\t\tout.items = canonicalizeSchema(schema.items as CanonicalJsonSchema);\n\t\t}\n\t}\n\tif (schema.enum !== undefined) out.enum = schema.enum;\n\tif (Array.isArray(schema.oneOf)) out.oneOf = schema.oneOf.map((s) => canonicalizeSchema(s));\n\tif (Array.isArray(schema.anyOf)) out.anyOf = schema.anyOf.map((s) => canonicalizeSchema(s));\n\tif (schema.nullable !== undefined) out.nullable = !!schema.nullable;\n\tif (schema.additionalProperties !== undefined) {\n\t\tif (typeof schema.additionalProperties === \"boolean\") out.additionalProperties = schema.additionalProperties;\n\t\telse if (schema.additionalProperties && typeof schema.additionalProperties === \"object\")\n\t\t\tout.additionalProperties = canonicalizeSchema(schema.additionalProperties);\n\t}\n\tif (schema.$ref !== undefined) out.$ref = schema.$ref;\n\treturn out;\n}\n\nfunction unionHasString(type: string | string[] | undefined): boolean {\n\tif (!type) return false;\n\treturn Array.isArray(type) ? type.includes(\"string\") : type === \"string\";\n}\n\n/**\n * Minimal structural validation of normalized arguments against a canonical\n * schema. This is intentionally conservative: it checks types, required\n * presence, enums, and additionalProperties=false restrictions. It is NOT a\n * full JSON-Schema engine; unsupported constructs fall back to permissive\n * (never to stricter) behavior so we never block a genuinely valid call, while\n * the repair layer separately refuses ambiguous destructive calls.\n */\nexport function validateAgainstSchema(args: Record<string, unknown>, schema: CanonicalJsonSchema): string[] {\n\treturn validateObject(args, schema, \"$\", 0);\n}\n\nfunction typeMatches(value: unknown, type: string): boolean {\n\tswitch (type) {\n\t\tcase \"object\":\n\t\t\treturn value !== null && typeof value === \"object\" && !Array.isArray(value);\n\t\tcase \"array\":\n\t\t\treturn Array.isArray(value);\n\t\tcase \"string\":\n\t\t\treturn typeof value === \"string\";\n\t\tcase \"boolean\":\n\t\t\treturn typeof value === \"boolean\";\n\t\tcase \"integer\":\n\t\t\treturn typeof value === \"number\" && Number.isInteger(value);\n\t\tcase \"number\":\n\t\t\treturn typeof value === \"number\";\n\t\tcase \"null\":\n\t\t\treturn value === null;\n\t\tdefault:\n\t\t\treturn true; // unknown keyword: permissive\n\t}\n}\n\nfunction validateObject(value: unknown, schema: CanonicalJsonSchema, path: string, depth: number): string[] {\n\tconst errors: string[] = [];\n\tif (depth > 64) return errors;\n\tconst types = Array.isArray(schema.type) ? schema.type : schema.type ? [schema.type] : [];\n\tconst allowsNull = !!schema.nullable || (types.length === 0 && value === null);\n\tif (value === null && !allowsNull && types.includes(\"null\")) return errors;\n\n\tif (types.length > 0) {\n\t\tconst matches = types.some((t) => typeMatches(value, t));\n\t\tif (!matches && value !== null) {\n\t\t\tconst wantsObject =\n\t\t\t\ttypes.some((t) => t === \"object\" || t === \"array\") && typeof value === \"object\" && value !== null;\n\t\t\t// Arrays/objects are structurally validated below regardless of keyword.\n\t\t\tif (!wantsObject && value !== null) {\n\t\t\t\terrors.push(`${path}: expected ${types.join(\"|\")} but got ${jsonTypeOf(value)}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (schema.enum !== undefined) {\n\t\tconst ok = schema.enum.some((e) => canonicalStableStringify(e) === canonicalStableStringify(value));\n\t\tif (!ok) errors.push(`${path}: value not in enum`);\n\t\treturn errors;\n\t}\n\n\tif (typeof value === \"object\" && value !== null && !Array.isArray(value)) {\n\t\tconst record = value as Record<string, unknown>;\n\t\tif (schema.properties) {\n\t\t\tfor (const [key, propSchema] of Object.entries(schema.properties)) {\n\t\t\t\tif (record[key] !== undefined) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\t...validateObject(record[key], propSchema as CanonicalJsonSchema, `${path}.${key}`, depth + 1),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (schema.required) {\n\t\t\tfor (const req of schema.required) {\n\t\t\t\tif (record[req] === undefined) errors.push(`${path}: missing required field '${req}'`);\n\t\t\t}\n\t\t}\n\t\tif (!unionHasString(types) && schema.additionalProperties === false && schema.properties) {\n\t\t\tfor (const key of Object.keys(record)) {\n\t\t\t\tif (!Object.hasOwn(schema.properties, key)) {\n\t\t\t\t\terrors.push(`${path}: unexpected property '${key}' (additionalProperties=false)`);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif (Array.isArray(value) && schema.items) {\n\t\tconst items = Array.isArray(schema.items) ? schema.items : [schema.items];\n\t\tfor (let i = 0; i < value.length; i++) {\n\t\t\tconst itemSchema = items[Math.min(i, items.length - 1)];\n\t\t\tif (itemSchema)\n\t\t\t\terrors.push(...validateObject(value[i], itemSchema as CanonicalJsonSchema, `${path}[${i}]`, depth + 1));\n\t\t}\n\t}\n\n\tif (schema.oneOf) {\n\t\tconst matches = schema.oneOf.filter((s) => validateObject(value, s, path, depth + 1).length === 0).length;\n\t\tif (matches !== 1) errors.push(`${path}: must match exactly one of oneOf`);\n\t}\n\tif (schema.anyOf) {\n\t\tconst matches = schema.anyOf.filter((s) => validateObject(value, s, path, depth + 1).length === 0).length;\n\t\tif (matches === 0) errors.push(`${path}: must match at least one of anyOf`);\n\t}\n\treturn errors;\n}\n\nfunction jsonTypeOf(value: unknown): string {\n\tif (value === null) return \"null\";\n\tif (Array.isArray(value)) return \"array\";\n\treturn typeof value;\n}\n"]}