{"version":3,"file":"extract-json-lenient.mjs","names":[],"sources":["../../../../../../../ai/src/utils/extract-json-lenient.ts"],"sourcesContent":["import { extractJsonPayload } from \"./extract-json-payload\";\n\n/**\n * Lenient counterpart to {@link extractJsonPayload}, tuned for the\n * structured-output judges that emit *corrupted* JSON — notably the\n * Amazon Nova family, which routinely wraps its verdict in fenced\n * ` ```json ` blocks, prepends an explanation paragraph, or trails the\n * object with commentary.\n *\n * Where `extractJsonPayload` deliberately refuses the \"first `{` … last\n * `}`\" heuristic (it would corrupt strict callers when prose contains\n * stray braces), this helper *opts into* that resilience: after fence\n * stripping it scans for the first balanced JSON object (`{…}`) or array\n * (`[…]`) and returns just that slice. Brace/bracket counting is\n * string-aware (it ignores braces inside JSON string literals and honors\n * `\\\"` escapes), so prose-embedded braces inside the JSON's own strings\n * don't throw off the balance.\n *\n * Returns the fence-stripped, trimmed text unchanged when no balanced\n * structure is found, so the caller's `JSON.parse` still fails loudly on\n * genuine garbage rather than this helper inventing a value.\n *\n * **Trade-off:** resilience over strictness. Use it only where a tolerant\n * parse is wanted (the judge preset) — for normal structured output keep\n * `extractJsonPayload`, which fails fast on malformed responses so real\n * prompt/model defects surface instead of being silently papered over.\n *\n * @example\n * extractJsonLenient('Here is my verdict:\\n```json\\n{\"score\":0.9}\\n``` — done.');\n * // => '{\"score\":0.9}'\n *\n * @example\n * extractJsonLenient('The answer is {\"verdict\":\"pass\"} for sure.');\n * // => '{\"verdict\":\"pass\"}'\n *\n * @example\n * extractJsonLenient('{\"valid\":true}');\n * // => '{\"valid\":true}'   (clean JSON passes through)\n */\nexport function extractJsonLenient(text: string): string {\n  // First reuse the strict fence stripper. When the model produced a\n  // clean fenced block this already yields the exact payload, so the\n  // balanced scan below becomes a no-op pass-through.\n  const stripped = extractJsonPayload(text);\n\n  const sliced = sliceFirstBalanced(stripped);\n\n  return sliced ?? stripped;\n}\n\n/**\n * Scan for the first balanced JSON object or array and return its raw\n * slice. Returns `undefined` when no opening `{`/`[` is found or the\n * structure never closes (truncated / partial output) — the caller then\n * falls back to the fence-stripped text so the failure stays visible.\n *\n * String-literal aware: braces and brackets appearing *inside* a JSON\n * string are not counted toward the balance, and a backslash escapes the\n * next character so an escaped quote (`\\\"`) doesn't prematurely end the\n * string scan.\n */\nfunction sliceFirstBalanced(text: string): string | undefined {\n  const start = firstOpenerIndex(text);\n\n  if (start === -1) {\n    return undefined;\n  }\n\n  const opener = text[start];\n  const closer = opener === \"{\" ? \"}\" : \"]\";\n\n  let depth = 0;\n  let inString = false;\n  let escaped = false;\n\n  for (let index = start; index < text.length; index++) {\n    const char = text[index];\n\n    if (inString) {\n      if (escaped) {\n        escaped = false;\n      } else if (char === \"\\\\\") {\n        escaped = true;\n      } else if (char === '\"') {\n        inString = false;\n      }\n\n      continue;\n    }\n\n    if (char === '\"') {\n      inString = true;\n      continue;\n    }\n\n    if (char === opener) {\n      depth++;\n    } else if (char === closer) {\n      depth--;\n\n      if (depth === 0) {\n        return text.slice(start, index + 1);\n      }\n    }\n  }\n\n  // Opener with no matching close — truncated / partial output. Leave it\n  // to the caller's fallback (and its loud parse failure).\n  return undefined;\n}\n\n/**\n * Index of the first JSON structure opener (`{` or `[`), whichever\n * appears earliest, or `-1` when neither is present.\n */\nfunction firstOpenerIndex(text: string): number {\n  const brace = text.indexOf(\"{\");\n  const bracket = text.indexOf(\"[\");\n\n  if (brace === -1) {\n    return bracket;\n  }\n\n  if (bracket === -1) {\n    return brace;\n  }\n\n  return Math.min(brace, bracket);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,mBAAmB,MAAsB;CAIvD,MAAM,WAAW,mBAAmB,IAAI;CAIxC,OAFe,mBAAmB,QAEtB,KAAK;AACnB;;;;;;;;;;;;AAaA,SAAS,mBAAmB,MAAkC;CAC5D,MAAM,QAAQ,iBAAiB,IAAI;CAEnC,IAAI,UAAU,IACZ;CAGF,MAAM,SAAS,KAAK;CACpB,MAAM,SAAS,WAAW,MAAM,MAAM;CAEtC,IAAI,QAAQ;CACZ,IAAI,WAAW;CACf,IAAI,UAAU;CAEd,KAAK,IAAI,QAAQ,OAAO,QAAQ,KAAK,QAAQ,SAAS;EACpD,MAAM,OAAO,KAAK;EAElB,IAAI,UAAU;GACZ,IAAI,SACF,UAAU;QACL,IAAI,SAAS,MAClB,UAAU;QACL,IAAI,SAAS,MAClB,WAAW;GAGb;EACF;EAEA,IAAI,SAAS,MAAK;GAChB,WAAW;GACX;EACF;EAEA,IAAI,SAAS,QACX;OACK,IAAI,SAAS,QAAQ;GAC1B;GAEA,IAAI,UAAU,GACZ,OAAO,KAAK,MAAM,OAAO,QAAQ,CAAC;EAEtC;CACF;AAKF;;;;;AAMA,SAAS,iBAAiB,MAAsB;CAC9C,MAAM,QAAQ,KAAK,QAAQ,GAAG;CAC9B,MAAM,UAAU,KAAK,QAAQ,GAAG;CAEhC,IAAI,UAAU,IACZ,OAAO;CAGT,IAAI,YAAY,IACd,OAAO;CAGT,OAAO,KAAK,IAAI,OAAO,OAAO;AAChC"}