{"version":3,"file":"repair.d.ts","sourceRoot":"","sources":["../../../src/core/tool-call/repair.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,UAAU,EAEV,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EACX,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EAIb,MAAM,YAAY,CAAC;AAkBpB,MAAM,WAAW,aAAa;IAC7B,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,OAAO,CAAC,EAAE,aAAa,CAAC;CACxB;AAWD,0EAA0E;AAC1E,iBAAS,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAI9E;AAkLD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,aAAa,CAAC;CACxB,GAAG,kBAAkB,CA2FrB;AAcD,eAAO,MAAM,aAAa,mBAAa,CAAC;AAExC,6CAA6C;AAC7C,OAAO,EAAE,oBAAoB,EAAE,CAAC","sourcesContent":["import {\n\tcanonicalizeSchema,\n\tcanonicalStableStringify,\n\tsha256Hex,\n\tstableHash,\n\tvalidateAgainstSchema,\n} from \"./canonicalize.js\";\nimport { recoverTruncatedJson } from \"./truncated-json.js\";\nimport type {\n\tCanonicalJsonSchema,\n\tNormalizedToolCall,\n\tRepairOptions,\n\tRepairOutcomeKind,\n\tToolCallRepair,\n\tTruncatedJsonOutcome,\n} from \"./types.js\";\n\n/**\n * Conservative, deterministic argument repair.\n *\n * Rules:\n *  - Coerce unambiguous primitive representations (\"true\"→true, \"42\"→42).\n *  - Collapse a safe singleton into the array a schema requires.\n *  - Close a clearly truncated container (via truncated-json).\n *  - Normalize a tool-name alias to the canonical name.\n *  - NEVER invent semantic values: file paths, branch names, versions,\n *    commands, URLs, recipients, destructive flags, approval scopes, policy.\n *  - Ambiguous mutating calls fail closed.\n */\n\nconst FORBIDDEN_INVENTED_FIELDS =\n\t/\\b(path|file|dir|directory|branch|version|url|host|command|cmd|recipient|email|database|db|deploy|target|token|secret|key)\\b/i;\n\nexport interface RepairContext {\n\t/** Canonical args already available (parsed). When absent, parse rawArgs. */\n\tparsedArgs?: Record<string, unknown>;\n\t/** Raw args string (possibly truncated JSON). */\n\trawArgs?: string;\n\t/** Schema for validation (canonical). */\n\tschema: CanonicalJsonSchema;\n\toptions?: RepairOptions;\n}\n\n/** Generate a deterministic repair id from before/after hashes. */\nfunction makeRepairId(kind: string, before: string, after: string): string {\n\treturn sha256Hex(`${kind}|${before}|${after}`).slice(0, 24);\n}\n\nfunction hashOf(obj: unknown): string {\n\treturn sha256Hex(canonicalStableStringify(obj ?? {}));\n}\n\n/** Guard: would a repair for this field be an invented semantic value? */\nfunction isForbiddenInvention(field: string | undefined, kind: string): boolean {\n\tif (kind === \"invent_value\") return true; // never synthesize\n\tif (field && FORBIDDEN_INVENTED_FIELDS.test(field.toLowerCase()) && kind.startsWith(\"coerce-\")) return false;\n\treturn false;\n}\n\n/** Parse the raw args, attempting truncated-JSON closure when possible. */\nfunction parseArgs(ctx: RepairContext): {\n\tparsed: Record<string, unknown>;\n\ttruncated: TruncatedJsonOutcome;\n\treparsedRaw?: string;\n} {\n\tif (ctx.parsedArgs) return { parsed: ctx.parsedArgs, truncated: { status: \"not_truncated\" } };\n\tif (ctx.rawArgs === undefined) return { parsed: {}, truncated: { status: \"not_truncated\" } };\n\tconst raw = ctx.rawArgs.trim();\n\tif (raw === \"\") return { parsed: {}, truncated: { status: \"not_truncated\" } };\n\ttry {\n\t\tconst parsed = JSON.parse(raw);\n\t\treturn {\n\t\t\tparsed: parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? parsed : {},\n\t\t\ttruncated: { status: \"not_truncated\" },\n\t\t};\n\t} catch {\n\t\tconst rec = recoverTruncatedJson(raw, {\n\t\t\tmaxBytes: ctx.options?.maxTruncatedJsonBytes,\n\t\t});\n\t\tif (rec.value !== undefined) {\n\t\t\tconst parsed = rec.value as Record<string, unknown>;\n\t\t\treturn {\n\t\t\t\tparsed: parsed && typeof parsed === \"object\" && !Array.isArray(parsed) ? parsed : {},\n\t\t\t\ttruncated: rec.outcome,\n\t\t\t\treparsedRaw: JSON.stringify(parsed),\n\t\t\t};\n\t\t}\n\t\treturn { parsed: {}, truncated: rec.outcome };\n\t}\n}\n\nfunction coercePrimitive(\n\t_field: string,\n\tvalue: unknown,\n\tschema: CanonicalJsonSchema,\n): { value: unknown; kind?: string } {\n\tif (typeof value === \"string\") {\n\t\tif (schema.type === \"boolean\") {\n\t\t\tif (value === \"true\") return { value: true, kind: \"coerce-string-boolean\" };\n\t\t\tif (value === \"false\") return { value: false, kind: \"coerce-string-boolean\" };\n\t\t\treturn { value };\n\t\t}\n\t\tif (schema.type === \"integer\" || schema.type === \"number\") {\n\t\t\tconst trimmed = value.trim();\n\t\t\tif (/^-?\\d+$/.test(trimmed)) return { value: Number(trimmed), kind: \"coerce-string-integer\" };\n\t\t\tif (schema.type === \"number\" && /^-?\\d*\\.\\d+$/.test(trimmed)) {\n\t\t\t\treturn { value: Number(trimmed), kind: \"coerce-string-number\" };\n\t\t\t}\n\t\t\treturn { value };\n\t\t}\n\t}\n\treturn { value };\n}\n\nfunction canonicalName(name: string, aliasMap?: Record<string, string>): { name: string; repaired: boolean } {\n\tconst canonical = aliasMap?.[name];\n\tif (canonical && canonical !== name) return { name: canonical, repaired: true };\n\treturn { name, repaired: false };\n}\n\n/**\n * Recursively normalize `value` against `schema`. Returns the normalized value\n * and collects repairs. Ambiguity is reported via `ambiguous`.\n */\nfunction normalizeValue(\n\tfield: string,\n\tvalue: unknown,\n\tschema: CanonicalJsonSchema,\n\toptions: RepairOptions,\n\trepairs: ToolCallRepair[],\n\tambiguous: string[],\n\tdepth: number,\n): unknown {\n\tif (depth > 32) {\n\t\tambiguous.push(`${field}: repair depth exceeded`);\n\t\treturn value;\n\t}\n\tconst beforeHash = hashOf(value);\n\n\t// Primitive coercion.\n\tconst coerced = coercePrimitive(field, value, schema);\n\tif (coerced.kind) {\n\t\trepairs.push({\n\t\t\trepairId: makeRepairId(coerced.kind, beforeHash, hashOf(coerced.value)),\n\t\t\tfield,\n\t\t\trepairKind: coerced.kind,\n\t\t\tbeforeHash,\n\t\t\tafterHash: hashOf(coerced.value),\n\t\t\tconfidence: \"deterministic\",\n\t\t});\n\t\tvalue = coerced.value;\n\t}\n\n\t// Singleton → array when schema requires an array and ambiguity is absent.\n\tif (\n\t\toptions.allowSingletonToArray !== false &&\n\t\tschema.type === \"array\" &&\n\t\t!Array.isArray(value) &&\n\t\tvalue !== undefined &&\n\t\tvalue !== null\n\t) {\n\t\tconst wrapped = [value];\n\t\trepairs.push({\n\t\t\trepairId: makeRepairId(\"singleton-to-array\", beforeHash, hashOf(wrapped)),\n\t\t\tfield,\n\t\t\trepairKind: \"singleton-to-array\",\n\t\t\tbeforeHash,\n\t\t\tafterHash: hashOf(wrapped),\n\t\t\tconfidence: \"deterministic\",\n\t\t});\n\t\tvalue = wrapped;\n\t}\n\n\t// Recurse object properties.\n\tif (Array.isArray(value)) {\n\t\tconst itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;\n\t\tif (itemSchema) {\n\t\t\treturn value.map((item, i) =>\n\t\t\t\tnormalizeValue(\n\t\t\t\t\t`${field}[${i}]`,\n\t\t\t\t\titem,\n\t\t\t\t\titemSchema as CanonicalJsonSchema,\n\t\t\t\t\toptions,\n\t\t\t\t\trepairs,\n\t\t\t\t\tambiguous,\n\t\t\t\t\tdepth + 1,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn value;\n\t}\n\tif (value && typeof value === \"object\" && !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\trecord[key] = normalizeValue(\n\t\t\t\t\t\t`${field}.${key}`,\n\t\t\t\t\t\trecord[key],\n\t\t\t\t\t\tpropSchema as CanonicalJsonSchema,\n\t\t\t\t\t\toptions,\n\t\t\t\t\t\trepairs,\n\t\t\t\t\t\tambiguous,\n\t\t\t\t\t\tdepth + 1,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// Destructive-flag ambiguity detection: if a boolean destructive flag is a\n\t\t// string that couldn't be coerced, mark ambiguous.\n\t\tif (schema.type === \"object\" && schema.properties) {\n\t\t\tfor (const [key, propSchema] of Object.entries(schema.properties)) {\n\t\t\t\tconst ps = propSchema as CanonicalJsonSchema;\n\t\t\t\tif (record[key] === undefined) continue;\n\t\t\t\tif (\n\t\t\t\t\ttypeof record[key] === \"string\" &&\n\t\t\t\t\t(ps.type === \"boolean\" || ps.type === \"integer\" || ps.type === \"number\")\n\t\t\t\t) {\n\t\t\t\t\tif (coercePrimitive(key, record[key], ps).kind === undefined && record[key].trim() !== \"\") {\n\t\t\t\t\t\tambiguous.push(`${field}.${key}: ambiguous ${ps.type} value '${maskValue(record[key])}'`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn record;\n\t}\n\treturn value;\n}\n\nfunction maskValue(v: unknown): string {\n\tconst s = String(v);\n\tif (s.length > 16) return `${s.slice(0, 8)}…(${s.length})`;\n\treturn s;\n}\n\n/**\n * Run the full normalization + conservative repair pipeline for a single raw\n * tool call. Requires a canonical tool name (already alias-resolved by caller\n * or resolved here), a tool-call id, and the provider-emitted args.\n */\nexport function normalizeToolCall(input: {\n\tname: string;\n\trawName: string;\n\ttoolCallId: string;\n\trawArgs?: string;\n\tparsedArgs?: Record<string, unknown>;\n\tschema: CanonicalJsonSchema;\n\tcanonicalName?: string;\n\taliasMap?: Record<string, string>;\n\toptions?: RepairOptions;\n}): NormalizedToolCall {\n\tconst options = input.options ?? {};\n\tconst alias = canonicalName(input.canonicalName ?? input.name, input.aliasMap);\n\tconst repairs: ToolCallRepair[] = [];\n\tconst ambiguous: string[] = [];\n\n\tif (alias.repaired) {\n\t\trepairs.push({\n\t\t\trepairId: makeRepairId(\"tool-alias\", sha256Hex(input.rawName), sha256Hex(alias.name)),\n\t\t\tfield: \"name\",\n\t\t\trepairKind: \"tool-name-alias\",\n\t\t\tbeforeHash: sha256Hex(input.rawName),\n\t\t\tafterHash: sha256Hex(alias.name),\n\t\t\tconfidence: \"deterministic\",\n\t\t});\n\t}\n\n\tconst { parsed, truncated, reparsedRaw } = parseArgs({\n\t\trawArgs: input.rawArgs,\n\t\tparsedArgs: input.parsedArgs,\n\t\tschema: input.schema,\n\t\toptions,\n\t});\n\n\tif (truncated.status === \"unrecoverable\") {\n\t\treturn {\n\t\t\tname: alias.name,\n\t\t\trawName: input.rawName,\n\t\t\ttoolCallId: input.toolCallId,\n\t\t\targs: parsed,\n\t\t\toutcome: \"invalid_schema\",\n\t\t\trepairs,\n\t\t\trawHash: sha256Hex(input.rawArgs ?? canonicalStableStringify(input.parsedArgs ?? {})),\n\t\t\tcanonicalHash: hashOf(parsed),\n\t\t\ttruncated,\n\t\t};\n\t}\n\n\t// normalization base hash from the pre-repair canonical state\n\tconst preNormalized = parsed;\n\tconst preHash = hashOf(preNormalized);\n\n\tconst normalized = normalizeValue(\n\t\t\"$\",\n\t\tparsed,\n\t\tinput.schema ?? canonicalizeSchema({ type: \"object\" }),\n\t\toptions,\n\t\trepairs,\n\t\tambiguous,\n\t\t0,\n\t);\n\n\t// Validate against canonical schema.\n\tconst schemaErrors = validateAgainstSchema(normalized as Record<string, unknown>, input.schema);\n\n\tconst outcome: RepairOutcomeKind =\n\t\tambiguous.length > 0\n\t\t\t? \"ambiguous\"\n\t\t\t: schemaErrors.length > 0\n\t\t\t\t? \"invalid_schema\"\n\t\t\t\t: repairs.length > 0\n\t\t\t\t\t? \"repaired_and_valid\"\n\t\t\t\t\t: \"valid_without_repair\";\n\n\t// Deduplicate identical repair evidence.\n\tconst uniqueRepairs = dedupeRepairs(repairs);\n\n\t// Truncated recovery evidence is a repair.\n\tif (truncated.status === \"recovered\" && options.allowTruncatedContainerClose !== false) {\n\t\tuniqueRepairs.push({\n\t\t\trepairId: makeRepairId(\"truncated-json\", truncated.beforeHash, truncated.afterHash),\n\t\t\trepairKind: \"truncated-json-close\",\n\t\t\tbeforeHash: truncated.beforeHash,\n\t\t\tafterHash: truncated.afterHash,\n\t\t\tconfidence: \"deterministic\",\n\t\t});\n\t\tconst _rp = reparsedRaw;\n\t\tvoid _rp;\n\t}\n\n\treturn {\n\t\tname: alias.name,\n\t\trawName: input.rawName,\n\t\ttoolCallId: input.toolCallId,\n\t\targs: normalized as Record<string, unknown>,\n\t\toutcome,\n\t\trepairs: uniqueRepairs,\n\t\trawHash: preHash,\n\t\tcanonicalHash: hashOf(normalized),\n\t\ttruncated,\n\t};\n}\n\nfunction dedupeRepairs(repairs: ToolCallRepair[]): ToolCallRepair[] {\n\tconst seen = new Set<string>();\n\tconst out: ToolCallRepair[] = [];\n\tfor (const r of repairs) {\n\t\tconst key = `${r.repairKind}|${r.beforeHash}|${r.afterHash}`;\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tout.push(r);\n\t}\n\treturn out;\n}\n\nexport const stableObjHash = stableHash;\n\n/** Convenience re-export for diagnostics. */\nexport { isForbiddenInvention };\n"]}