{"version":3,"file":"prompts-validate.mjs","names":[],"sources":["../../../../../../../ai/src/prompts/prompts-validate.ts"],"sourcesContent":["import { agent } from \"../agent/agent\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { ModelContract } from \"../contracts/model.contract\";\nimport type { SystemPromptContract } from \"../contracts/system-prompt.contract\";\nimport { judge } from \"../eval/judge-scorer\";\nimport { PROMPT_JUDGE_RUBRIC } from \"../prompt/prompt-validate\";\nimport type { PromptJudgeCacheLike } from \"./prompts-manager.type\";\n\n/**\n * Placeholder matcher — kept in lock-step with the matcher\n * `renderPlaceholders` (`src/system-prompt/render-placeholders.ts`) and the\n * legacy `prompt-validate` lint both use, so the deterministic validator sees\n * the exact same `{{key}}` / `{{a.b}}` / `{{key|default}}` set the renderer\n * substitutes. Global so every occurrence is collected.\n */\nconst PLACEHOLDER_PATTERN = /\\{\\{\\s*([^{}]+?)\\s*\\}\\}/g;\n\n/**\n * One parsed placeholder occurrence — the key path (the part before any\n * `|default`) and whether the occurrence carried an inline default.\n */\ntype ParsedPlaceholder = {\n  /** The dot-path key, e.g. `language` or `user.name`. */\n  readonly path: string;\n  /** Whether THIS occurrence declared an inline `{{key|default}}` fallback. */\n  readonly hasDefault: boolean;\n};\n\n/**\n * Collect every distinct placeholder occurrence from a template, in first-seen\n * order. A key is considered to \"have a default\" only when EVERY occurrence of\n * it carries one — a single bare `{{key}}` means the renderer can leave it\n * unresolved, so the key is still required.\n */\nfunction collectPlaceholders(template: string): ParsedPlaceholder[] {\n  const byPath = new Map<string, boolean>();\n  const order: string[] = [];\n\n  for (const match of template.matchAll(PLACEHOLDER_PATTERN)) {\n    const [rawPath, rawDefault] = match[1].split(\"|\");\n    const path = rawPath.trim();\n\n    if (path.length === 0) {\n      continue;\n    }\n\n    const hasDefault = rawDefault !== undefined;\n\n    if (!byPath.has(path)) {\n      byPath.set(path, hasDefault);\n      order.push(path);\n    } else {\n      // A key only counts as defaulted when ALL of its occurrences default.\n      byPath.set(path, (byPath.get(path) ?? false) && hasDefault);\n    }\n  }\n\n  return order.map(path => ({ path, hasDefault: byPath.get(path) ?? false }));\n}\n\n/**\n * Run the deterministic (model-free) half of validation over a resolved prompt\n * body. Reports every `{{key}}` placeholder that has NO inline default and is\n * neither supplied in `provided` nor declared in `declared` (the prompt's\n * `meta.required` plus any caller-declared keys).\n *\n * Pure and synchronous — the only required half of `validate`; the LLM-judge\n * half is optional and layered on top.\n *\n * @param text - The resolved prompt body (placeholders may still be present).\n * @param provided - Placeholder keys the caller has supplied a value for.\n * @param declared - Placeholder keys declared as known/required (e.g. `meta.required`).\n */\nexport function findMissingPlaceholders(\n  text: string,\n  provided: ReadonlySet<string>,\n  declared: ReadonlySet<string>,\n): string[] {\n  const missing: string[] = [];\n\n  for (const { path, hasDefault } of collectPlaceholders(text)) {\n    if (hasDefault) {\n      continue;\n    }\n\n    if (provided.has(path) || declared.has(path)) {\n      continue;\n    }\n\n    missing.push(path);\n  }\n\n  return missing;\n}\n\n/**\n * A `meta.required` key absent from the template entirely — declared as\n * required but never referenced — is itself a defect worth surfacing. Returns\n * the declared keys that appear nowhere in the body.\n */\nexport function findUnreferencedRequired(\n  text: string,\n  required: readonly string[],\n): string[] {\n  const present = new Set(collectPlaceholders(text).map(p => p.path));\n\n  return required.filter(key => !present.has(key));\n}\n\n/**\n * Build the one-shot judge agent the optional LLM-as-judge pass runs. Mirrors\n * the legacy `prompt.ts` judge agent (strict-JSON instruction so the verdict\n * parses even without an output schema), so the two validate paths share one\n * judging contract.\n */\nfunction buildJudgeAgent(model: ModelContract): AgentContract<unknown> {\n  return agent({\n    name: \"prompt-quality-judge\",\n    model,\n    systemPrompt:\n      \"You are a strict prompt-quality grader. Respond with JSON only: \" +\n      '{ \"score\": <0..1>, \"passed\": <true|false>, \"reason\": \"<short explanation>\" }.',\n  });\n}\n\n/**\n * Turn caller-supplied `criteria` into the judge rubric that replaces the\n * built-in {@link PROMPT_JUDGE_RUBRIC}. A single string is used verbatim;\n * a list is joined into a numbered rule set the judge must check ALL of.\n * Returns `undefined` for an empty/blank input, so the caller falls back\n * to the default rubric.\n *\n * @example\n * formatCriteria([\"Addresses the user by {{name}}\", \"Under 200 words\"]);\n * // → \"Grade the system prompt against ALL of these criteria …\\n1. …\\n2. …\"\n */\nexport function formatCriteria(\n  criteria: string | readonly string[] | undefined,\n): string | undefined {\n  if (criteria === undefined) {\n    return undefined;\n  }\n\n  if (typeof criteria === \"string\") {\n    const trimmed = criteria.trim();\n\n    return trimmed.length > 0 ? trimmed : undefined;\n  }\n\n  const rules = criteria.map(rule => rule.trim()).filter(rule => rule.length > 0);\n\n  if (rules.length === 0) {\n    return undefined;\n  }\n\n  return (\n    \"Grade the system prompt against ALL of the following criteria — it passes only if it satisfies every one:\\n\" +\n    rules.map((rule, index) => `${index + 1}. ${rule}`).join(\"\\n\")\n  );\n}\n\n/** Outcome of the optional LLM-as-judge pass over a resolved prompt body. */\nexport type JudgeOutcome = {\n  /**\n   * The judge score in `[0, 1]`, or `undefined` when the judge degraded\n   * (errored, returned no parseable verdict, or threw) — never a misleading\n   * `0` masquerading as a real verdict.\n   */\n  readonly score?: number;\n  /** Human-readable issues raised by the judge (its reason, or a degrade note). */\n  readonly issues: string[];\n};\n\n/**\n * Run the optional LLM-as-judge pass over a resolved prompt body, REUSING the\n * eval `judge` scorer (the same path `prompt().validate` uses) so there is no\n * second judging implementation.\n *\n * **Nova-safe by contract.** The judge NEVER throws here: the eval scorer\n * already degrades a broken judge to `score: 0` with a failure reason, and any\n * exception that still escapes (model wiring, agent construction) is caught.\n * Both degrade paths surface `score: undefined` plus an issue note — so a flaky\n * judge can never fail an otherwise-valid prompt.\n *\n * @param text - The resolved prompt body under evaluation.\n * @param model - The model that powers the judge agent.\n * @param criteria - Optional caller rules that REPLACE the built-in rubric\n *   ({@link formatCriteria}). Omitted ⇒ the default prompt-quality rubric.\n */\nexport async function judgePromptBody(\n  text: string,\n  model: ModelContract,\n  criteria?: string | readonly string[],\n): Promise<JudgeOutcome> {\n  try {\n    const judgeAgent = buildJudgeAgent(model);\n    const scorer = judge({\n      agent: judgeAgent,\n      rubric: formatCriteria(criteria) ?? PROMPT_JUDGE_RUBRIC,\n    });\n\n    const verdict = await scorer({\n      case: { name: \"prompt-quality\", input: \"Grade the system prompt below.\" },\n      text,\n      // `result` is unused by the judge scorer's prompt builder; a minimal\n      // stand-in keeps the structural contract satisfied without a real run.\n      result: { text } as never,\n      output: undefined,\n    });\n\n    // The eval scorer signals a degraded judge with score 0 + a diagnostic\n    // reason (\"judge failed: …\" / \"judge returned no parseable verdict\"). Treat\n    // that as \"no usable score\" rather than a real 0 verdict.\n    const degraded =\n      verdict.score === 0 &&\n      typeof verdict.reason === \"string\" &&\n      /^judge (failed|returned no parseable)/.test(verdict.reason);\n\n    if (degraded) {\n      return {\n        issues: [`LLM-judge unavailable: ${verdict.reason}`],\n      };\n    }\n\n    return {\n      score: verdict.score,\n      issues: verdict.reason ? [verdict.reason] : [],\n    };\n  } catch (error) {\n    // Last-resort guard: never let a judge failure throw out of validate().\n    const message = error instanceof Error ? error.message : String(error);\n\n    return {\n      issues: [`LLM-judge unavailable: ${message}`],\n    };\n  }\n}\n\n/**\n * Non-cryptographic 53-bit string hash (cyrb53) — deterministic across runs\n * and platforms, with no `node:crypto` dependency (keeps the validate path\n * usable in any runtime). Mirrors the VCR request hash; collision-resistant\n * enough for a per-prompt judge-verdict keyspace. Returned as base-36.\n */\nfunction hashString(input: string): string {\n  let h1 = 0xdeadbeef;\n  let h2 = 0x41c6ce57;\n\n  for (let i = 0; i < input.length; i++) {\n    const ch = input.charCodeAt(i);\n\n    h1 = Math.imul(h1 ^ ch, 2654435761);\n    h2 = Math.imul(h2 ^ ch, 1597334677);\n  }\n\n  h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n  h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n  h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n  h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n\n  const combined = 4294967296 * (2097151 & h2) + (h1 >>> 0);\n\n  return combined.toString(36);\n}\n\n/**\n * Build the judge-verdict cache key for a resolved prompt body + judge model\n * + the effective rubric. Combines the model's `provider:name` identity with a\n * content hash of the rubric-plus-body, so the same prompt graded by the same\n * judge against the same rules hits the cache — while a change to the prompt,\n * the model, OR the `criteria` misses it (different rules ⇒ different verdict).\n */\nexport function judgeCacheKey(\n  text: string,\n  model: ModelContract,\n  criteria?: string | readonly string[],\n): string {\n  const rubric = formatCriteria(criteria) ?? PROMPT_JUDGE_RUBRIC;\n\n  return `prompts.judge.${model.provider}:${model.name}.${hashString(`${rubric}\u0000${text}`)}`;\n}\n\n/**\n * Run the judge pass with an OPTIONAL memo cache in front. On a hit, the stored\n * {@link JudgeOutcome} is returned without a model call; on a miss, the live\n * judge runs and a USABLE verdict (one carrying a `score`) is written back.\n * Degraded outcomes (no score) are NOT cached — a transient judge failure must\n * never poison the memo. A `null`/absent cache degrades to a direct judge call.\n *\n * Cache I/O is itself fault-tolerant: a `get`/`set` that rejects is swallowed\n * so a flaky cache can never break (or fail) validation.\n *\n * @param text - The resolved prompt body under evaluation.\n * @param model - The judge model.\n * @param cache - Optional verdict memo (any `CacheDriver`-like get/set surface).\n * @param criteria - Optional caller rules that REPLACE the built-in rubric; also\n *   folded into the cache key so a re-validation with different rules re-runs.\n */\nexport async function judgePromptBodyCached(\n  text: string,\n  model: ModelContract,\n  cache?: PromptJudgeCacheLike,\n  criteria?: string | readonly string[],\n): Promise<JudgeOutcome> {\n  if (!cache) {\n    return judgePromptBody(text, model, criteria);\n  }\n\n  const key = judgeCacheKey(text, model, criteria);\n\n  const cached = await readJudgeCache(cache, key);\n\n  if (cached) {\n    return cached;\n  }\n\n  const outcome = await judgePromptBody(text, model, criteria);\n\n  // Only memoize a usable verdict — never a degraded (scoreless) one.\n  if (outcome.score !== undefined) {\n    await writeJudgeCache(cache, key, outcome);\n  }\n\n  return outcome;\n}\n\n/** Read a cached verdict, swallowing any cache fault (treated as a miss). */\nasync function readJudgeCache(\n  cache: PromptJudgeCacheLike,\n  key: string,\n): Promise<JudgeOutcome | undefined> {\n  try {\n    const value = await cache.get<JudgeOutcome>(key);\n\n    return value ?? undefined;\n  } catch {\n    return undefined;\n  }\n}\n\n/** Write a verdict, swallowing any cache fault (best-effort memo). */\nasync function writeJudgeCache(\n  cache: PromptJudgeCacheLike,\n  key: string,\n  outcome: JudgeOutcome,\n): Promise<void> {\n  try {\n    await cache.set(key, outcome);\n  } catch {\n    // Best-effort — a failed memo write never affects the validation result.\n  }\n}\n\n/**\n * Resolve the body + declared-required keys for a validation target that is a\n * `SystemPromptContract` (named or anonymous). The declared set is the\n * prompt's `meta.required` (when present).\n */\nexport function describeContractTarget(contract: SystemPromptContract): {\n  text: string;\n  required: readonly string[];\n} {\n  const meta = contract.meta();\n\n  return {\n    text: contract.resolve(),\n    required: meta?.required ?? [],\n  };\n}\n"],"mappings":";;;;;;;;;;;;AAeA,MAAM,sBAAsB;;;;;;;AAmB5B,SAAS,oBAAoB,UAAuC;CAClE,MAAM,yBAAS,IAAI,IAAqB;CACxC,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,SAAS,SAAS,mBAAmB,GAAG;EAC1D,MAAM,CAAC,SAAS,cAAc,MAAM,EAAE,CAAC,MAAM,GAAG;EAChD,MAAM,OAAO,QAAQ,KAAK;EAE1B,IAAI,KAAK,WAAW,GAClB;EAGF,MAAM,aAAa,eAAe;EAElC,IAAI,CAAC,OAAO,IAAI,IAAI,GAAG;GACrB,OAAO,IAAI,MAAM,UAAU;GAC3B,MAAM,KAAK,IAAI;EACjB,OAEE,OAAO,IAAI,OAAO,OAAO,IAAI,IAAI,KAAK,UAAU,UAAU;CAE9D;CAEA,OAAO,MAAM,KAAI,UAAS;EAAE;EAAM,YAAY,OAAO,IAAI,IAAI,KAAK;CAAM,EAAE;AAC5E;;;;;;;;;;;;;;AAeA,SAAgB,wBACd,MACA,UACA,UACU;CACV,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,EAAE,MAAM,gBAAgB,oBAAoB,IAAI,GAAG;EAC5D,IAAI,YACF;EAGF,IAAI,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,GACzC;EAGF,QAAQ,KAAK,IAAI;CACnB;CAEA,OAAO;AACT;;;;;;AAOA,SAAgB,yBACd,MACA,UACU;CACV,MAAM,UAAU,IAAI,IAAI,oBAAoB,IAAI,CAAC,CAAC,KAAI,MAAK,EAAE,IAAI,CAAC;CAElE,OAAO,SAAS,QAAO,QAAO,CAAC,QAAQ,IAAI,GAAG,CAAC;AACjD;;;;;;;AAQA,SAAS,gBAAgB,OAA8C;CACrE,OAAO,MAAM;EACX,MAAM;EACN;EACA,cACE;CAEJ,CAAC;AACH;;;;;;;;;;;;AAaA,SAAgB,eACd,UACoB;CACpB,IAAI,aAAa,QACf;CAGF,IAAI,OAAO,aAAa,UAAU;EAChC,MAAM,UAAU,SAAS,KAAK;EAE9B,OAAO,QAAQ,SAAS,IAAI,UAAU;CACxC;CAEA,MAAM,QAAQ,SAAS,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,QAAO,SAAQ,KAAK,SAAS,CAAC;CAE9E,IAAI,MAAM,WAAW,GACnB;CAGF,OACE,gHACA,MAAM,KAAK,MAAM,UAAU,GAAG,QAAQ,EAAE,IAAI,MAAM,CAAC,CAAC,KAAK,IAAI;AAEjE;;;;;;;;;;;;;;;;;AA8BA,eAAsB,gBACpB,MACA,OACA,UACuB;CACvB,IAAI;EAOF,MAAM,UAAU,MALD,MAAM;GACnB,OAFiB,gBAAgB,KAEjB;GAChB,QAAQ,eAAe,QAAQ,KAAK;EACtC,CAE2B,CAAC,CAAC;GAC3B,MAAM;IAAE,MAAM;IAAkB,OAAO;GAAiC;GACxE;GAGA,QAAQ,EAAE,KAAK;GACf,QAAQ;EACV,CAAC;EAUD,IAJE,QAAQ,UAAU,KAClB,OAAO,QAAQ,WAAW,YAC1B,wCAAwC,KAAK,QAAQ,MAAM,GAG3D,OAAO,EACL,QAAQ,CAAC,0BAA0B,QAAQ,QAAQ,EACrD;EAGF,OAAO;GACL,OAAO,QAAQ;GACf,QAAQ,QAAQ,SAAS,CAAC,QAAQ,MAAM,IAAI,CAAC;EAC/C;CACF,SAAS,OAAO;EAId,OAAO,EACL,QAAQ,CAAC,0BAHK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAGvB,EAC9C;CACF;AACF;;;;;;;AAQA,SAAS,WAAW,OAAuB;CACzC,IAAI,KAAK;CACT,IAAI,KAAK;CAET,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,KAAK,MAAM,WAAW,CAAC;EAE7B,KAAK,KAAK,KAAK,KAAK,IAAI,UAAU;EAClC,KAAK,KAAK,KAAK,KAAK,IAAI,UAAU;CACpC;CAEA,KAAK,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC3C,MAAM,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC5C,KAAK,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAC3C,MAAM,KAAK,KAAK,KAAM,OAAO,IAAK,UAAU;CAI5C,QAFiB,cAAc,UAAU,OAAO,OAAO,GAExC,CAAC,SAAS,EAAE;AAC7B;;;;;;;;AASA,SAAgB,cACd,MACA,OACA,UACQ;CACR,MAAM,SAAS,eAAe,QAAQ,KAAK;CAE3C,OAAO,iBAAiB,MAAM,SAAS,GAAG,MAAM,KAAK,GAAG,WAAW,GAAG,OAAO,GAAG,MAAM;AACxF;;;;;;;;;;;;;;;;;AAkBA,eAAsB,sBACpB,MACA,OACA,OACA,UACuB;CACvB,IAAI,CAAC,OACH,OAAO,gBAAgB,MAAM,OAAO,QAAQ;CAG9C,MAAM,MAAM,cAAc,MAAM,OAAO,QAAQ;CAE/C,MAAM,SAAS,MAAM,eAAe,OAAO,GAAG;CAE9C,IAAI,QACF,OAAO;CAGT,MAAM,UAAU,MAAM,gBAAgB,MAAM,OAAO,QAAQ;CAG3D,IAAI,QAAQ,UAAU,QACpB,MAAM,gBAAgB,OAAO,KAAK,OAAO;CAG3C,OAAO;AACT;;AAGA,eAAe,eACb,OACA,KACmC;CACnC,IAAI;EAGF,OAAO,MAFa,MAAM,IAAkB,GAAG,KAE/B;CAClB,QAAQ;EACN;CACF;AACF;;AAGA,eAAe,gBACb,OACA,KACA,SACe;CACf,IAAI;EACF,MAAM,MAAM,IAAI,KAAK,OAAO;CAC9B,QAAQ,CAER;AACF;;;;;;AAOA,SAAgB,uBAAuB,UAGrC;CACA,MAAM,OAAO,SAAS,KAAK;CAE3B,OAAO;EACL,MAAM,SAAS,QAAQ;EACvB,UAAU,MAAM,YAAY,CAAC;CAC/B;AACF"}