@prooflane/inspector-beta 0.1.0-beta.3 → 0.1.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -4478,11 +4478,17 @@ function isSecurityTaskEvaluator(value) {
4478
4478
  let v = value;
4479
4479
  return (v.severity === "critical" || v.severity === "high" || v.severity === "medium" || v.severity === "low") && (v.protectedTokens === void 0 || Array.isArray(v.protectedTokens) && v.protectedTokens.every((token) => typeof token == "string")) && (v.denyToolPattern === void 0 || typeof v.denyToolPattern == "string");
4480
4480
  }
4481
+ function isHostedRagCaseManifest(value) {
4482
+ if (!value || typeof value != "object")
4483
+ return !1;
4484
+ let v = value, integer = (candidate) => typeof candidate == "number" && Number.isInteger(candidate) && candidate >= 0;
4485
+ return typeof v.caseId == "string" && typeof v.definitionHash == "string" && (v.mode === "retriever-only" || v.mode === "full-chain") && typeof v.useAgentPick == "boolean" && typeof v.expectedToolConfigured == "boolean" && integer(v.expectedKeywordCount) && integer(v.forbiddenKeywordCount) && integer(v.contractAssertionCount) && integer(v.minResults) && (v.maxLatencyMs === void 0 || typeof v.maxLatencyMs == "number" && Number.isFinite(v.maxLatencyMs) && v.maxLatencyMs >= 0) && (v.expectedOutcome === void 0 || v.expectedOutcome === "SUCCESS" || v.expectedOutcome === "DENIAL" || v.expectedOutcome === "EMPTY") && integer(v.expectedAnswerCount) && typeof v.goldenEnabled == "boolean" && typeof v.hasGolden == "boolean" && integer(v.faultCount) && (v.judgeStrategy === "deterministic" || v.judgeStrategy === "same-model" || v.judgeStrategy === "independent");
4486
+ }
4481
4487
  function isIntelligenceTask(value) {
4482
4488
  if (!value || typeof value != "object")
4483
4489
  return !1;
4484
4490
  let v = value;
4485
- return typeof v.id != "string" || typeof v.label != "string" || typeof v.kind != "string" || !securityTaskKinds.has(v.kind) ? !1 : v.kind === "security.prompt" ? typeof v.prompt == "string" && (v.system === void 0 || typeof v.system == "string") && (v.evaluator === void 0 || isSecurityTaskEvaluator(v.evaluator)) : v.kind === "security.conversation" ? Array.isArray(v.messages) && v.messages.length > 0 && v.messages.every((message2) => typeof message2 == "string") && (v.system === void 0 || typeof v.system == "string") && isSecurityTaskEvaluator(v.evaluator) : v.kind === "security.adaptive" ? typeof v.objective == "string" && typeof v.successCriterion == "string" && typeof v.maxRounds == "number" && Number.isInteger(v.maxRounds) && v.maxRounds > 0 && v.maxRounds <= 10 && (v.severity === "critical" || v.severity === "high" || v.severity === "medium" || v.severity === "low") : v.kind === "security.surface" ? (v.severity === "critical" || v.severity === "high" || v.severity === "medium" || v.severity === "low") && (v.source === "tools" || v.source === "resources" || v.source === "prompts" || v.source === "connection") && (v.assertion === "no-match" || v.assertion === "unique-tool-names" || v.assertion === "bounded-input" || v.assertion === "guard-declared" || v.assertion === "resource-content-clean" || v.assertion === "http-header-guard") && (v.patterns === void 0 || !!v.patterns && typeof v.patterns == "object") && (v.resourceUris === void 0 || Array.isArray(v.resourceUris) && v.resourceUris.every((uri) => typeof uri == "string")) && (v.useAdvertisedResources === void 0 || typeof v.useAdvertisedResources == "boolean") && (v.matchResult === void 0 || v.matchResult === "exposed" || v.matchResult === "poisoned" || v.matchResult === "inconclusive") : typeof v.query == "string" && typeof v.minResults == "number" && typeof v.maxLatencyMs == "number";
4491
+ return typeof v.id != "string" || typeof v.label != "string" || typeof v.kind != "string" || !securityTaskKinds.has(v.kind) ? !1 : v.kind === "security.prompt" ? typeof v.prompt == "string" && (v.system === void 0 || typeof v.system == "string") && (v.evaluator === void 0 || isSecurityTaskEvaluator(v.evaluator)) : v.kind === "security.conversation" ? Array.isArray(v.messages) && v.messages.length > 0 && v.messages.every((message2) => typeof message2 == "string") && (v.system === void 0 || typeof v.system == "string") && isSecurityTaskEvaluator(v.evaluator) : v.kind === "security.adaptive" ? typeof v.objective == "string" && typeof v.successCriterion == "string" && typeof v.maxRounds == "number" && Number.isInteger(v.maxRounds) && v.maxRounds > 0 && v.maxRounds <= 10 && (v.severity === "critical" || v.severity === "high" || v.severity === "medium" || v.severity === "low") : v.kind === "security.surface" ? (v.severity === "critical" || v.severity === "high" || v.severity === "medium" || v.severity === "low") && (v.source === "tools" || v.source === "resources" || v.source === "prompts" || v.source === "connection") && (v.assertion === "no-match" || v.assertion === "unique-tool-names" || v.assertion === "bounded-input" || v.assertion === "guard-declared" || v.assertion === "resource-content-clean" || v.assertion === "http-header-guard") && (v.patterns === void 0 || !!v.patterns && typeof v.patterns == "object") && (v.resourceUris === void 0 || Array.isArray(v.resourceUris) && v.resourceUris.every((uri) => typeof uri == "string")) && (v.useAdvertisedResources === void 0 || typeof v.useAdvertisedResources == "boolean") && (v.matchResult === void 0 || v.matchResult === "exposed" || v.matchResult === "poisoned" || v.matchResult === "inconclusive") : typeof v.query == "string" && typeof v.minResults == "number" && typeof v.maxLatencyMs == "number" && (v.localCase === void 0 || isHostedRagCaseManifest(v.localCase));
4486
4492
  }
4487
4493
  function isScanTaskLeaseClaims(value) {
4488
4494
  if (!value || typeof value != "object")
@@ -11212,7 +11218,276 @@ function describeToolError(output) {
11212
11218
  })();
11213
11219
  return text6.length > 500 ? `${text6.slice(0, 500)}\u2026` : text6 || "the tool reported an error";
11214
11220
  }
11215
- var sha256 = (value) => createHash15("sha256").update(typeof value == "string" ? value : JSON.stringify(value)).digest("hex");
11221
+ var sha256 = (value) => createHash15("sha256").update(typeof value == "string" ? value : JSON.stringify(value)).digest("hex"), hostedRagCaseQuery = (testCase) => (testCase.userQuery ?? "").trim() || testCase.query, hostedRagCaseName = (testCase) => (testCase.name ?? "").trim() || hostedRagCaseQuery(testCase) || testCase.id, hostedRagCaseInput = (testCase) => testCase.input ?? { query: hostedRagCaseQuery(testCase) }, clampRagScore = (value) => Math.max(0, Math.min(100, Math.round(Number.isFinite(value) ? value : 0))), averageRagScore = (values) => values.length ? Math.round(values.reduce((sum, value) => sum + value, 0) / values.length) : 100;
11222
+ function extractHostedRagPayload(output) {
11223
+ let value = output;
11224
+ if (Array.isArray(value) && value.length && value.every((block) => block && typeof block == "object" && "type" in block)) {
11225
+ let text6 = value.filter((block) => block.type === "text" && typeof block.text == "string").map((block) => block.text).join(`
11226
+ `);
11227
+ text6 && (value = text6);
11228
+ }
11229
+ if (typeof value == "string") {
11230
+ let trimmed = value.trim();
11231
+ if (trimmed.startsWith("{") || trimmed.startsWith("["))
11232
+ try {
11233
+ value = JSON.parse(trimmed);
11234
+ } catch {
11235
+ }
11236
+ }
11237
+ return value;
11238
+ }
11239
+ function hostedRagContext(value) {
11240
+ if (value == null)
11241
+ return "";
11242
+ if (typeof value == "string")
11243
+ return value;
11244
+ try {
11245
+ return JSON.stringify(value);
11246
+ } catch {
11247
+ return String(value);
11248
+ }
11249
+ }
11250
+ function hostedRagCount(value) {
11251
+ if (Array.isArray(value))
11252
+ return value.length;
11253
+ if (value && typeof value == "object") {
11254
+ for (let nested of Object.values(value))
11255
+ if (Array.isArray(nested))
11256
+ return nested.length;
11257
+ return 1;
11258
+ }
11259
+ return value ? 1 : 0;
11260
+ }
11261
+ var HOSTED_RAG_ABSTAIN = /\b(i (don'?t|do not) (know|have)|not enough (context|information|data)|no (relevant |sufficient )?(information|context|data|results?)|cannot (answer|find|determine|provide)|can'?t (answer|find|determine)|unable to (answer|find|determine)|isn'?t (in|available|provided)|not (available|found|provided|present|included) (in|within)|no answer|nothing (in|was) (the )?(context|retrieved))\b/i, HOSTED_RAG_STOP_WORDS = /* @__PURE__ */ new Set(["about", "after", "against", "also", "another", "before", "between", "could", "does", "from", "have", "into", "just", "should", "that", "their", "there", "these", "they", "this", "through", "under", "what", "when", "where", "which", "while", "with", "would", "your"]);
11262
+ function hostedRagContextLacksSupport(query, context, expectedAnswer) {
11263
+ let normalized = context.toLowerCase(), expected = (expectedAnswer ?? []).map((term) => term.toLowerCase()).filter(Boolean);
11264
+ if (expected.length)
11265
+ return expected.filter((term) => normalized.includes(term)).length / expected.length < 0.5;
11266
+ let terms = [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 3 && !HOSTED_RAG_STOP_WORDS.has(term)))];
11267
+ return terms.length > 0 && terms.filter((term) => normalized.includes(term)).length / terms.length < 0.5;
11268
+ }
11269
+ function hostedRagMalformed(value) {
11270
+ if (typeof value == "string")
11271
+ return `${value.slice(0, Math.max(2, Math.floor(value.length / 2)))}\uFFFD\u2049`;
11272
+ if (Array.isArray(value))
11273
+ return value.length ? value.slice(0, Math.max(1, value.length - 1)).map((item, index) => index % 2 ? item : hostedRagMalformed(item)) : [{ _malformed: "\uFFFD\u2049" }];
11274
+ if (value && typeof value == "object") {
11275
+ let entries = Object.entries(value), output = {};
11276
+ for (let [key, nested] of entries.length > 1 ? entries.slice(1) : entries)
11277
+ output[key] = hostedRagMalformed(nested);
11278
+ return Object.keys(output).length ? output : { _malformed: "\uFFFD\u2049" };
11279
+ }
11280
+ return { _malformed: "\uFFFD\u2049" };
11281
+ }
11282
+ function hostedRagDefinitionHash(testCase, options) {
11283
+ return sha256({ testCase, useAgentPick: options.useAgentPick, chain: options.chain, faults: options.faults, judgeStrategy: options.judgeStrategy, goldenSignature: options.golden?.[testCase.id] == null ? null : sha256(options.golden[testCase.id]) });
11284
+ }
11285
+ function hostedRagCheckDetail(grade, facts, testCase) {
11286
+ if (grade.dimension === "tool-selection")
11287
+ return facts.toolSelection === "match" ? "The agent selected the expected retrieval tool." : facts.toolSelection === "unresolved" ? "The agent did not resolve a valid retrieval tool." : "The agent selected a different retrieval tool than expected.";
11288
+ if (grade.dimension === "retriever-behavior")
11289
+ return facts.retrieverError ? testCase.expectedOutcome === "DENIAL" ? `Request denied in ${facts.latencyMs}ms.` : "The retriever returned an error." : testCase.expectedOutcome === "EMPTY" ? grade.pass ? `Empty result returned in ${facts.latencyMs}ms.` : `${facts.resultCount} result(s), expected an empty result.` : facts.resultCount < (testCase.minResults ?? 1) ? `${facts.resultCount} result(s), expected at least ${testCase.minResults ?? 1}.` : grade.pass ? `${facts.resultCount} result(s) in ${facts.latencyMs}ms.` : `${facts.latencyMs}ms exceeded the ${testCase.maxLatencyMs}ms budget.`;
11290
+ if (grade.dimension === "retrieval-contract")
11291
+ return testCase.contract?.length ? grade.pass ? `All ${facts.contractTotal} contract assertion(s) held.` : `${facts.contractTotal - facts.contractPassed}/${facts.contractTotal} contract assertion(s) failed.` : grade.pass ? "Result present and well-formed." : "The default retrieval contract failed.";
11292
+ if (grade.dimension === "retrieval-quality") {
11293
+ let forbidden = (testCase.forbiddenKeywords ?? []).filter((term) => facts.forbiddenKeywordHits > 0 && term);
11294
+ return `${facts.expectedKeywordHits}/${testCase.expectedKeywords?.length ?? 0} relevant terms retrieved${forbidden.length ? `; ${facts.forbiddenKeywordHits} forbidden term hit(s)` : ""}.`;
11295
+ }
11296
+ return grade.dimension === "retrieval-drift" ? facts.golden === "match" ? "Retrieval matches the golden baseline." : facts.golden === "drift" ? "Retrieval changed vs. the golden baseline." : "No golden baseline yet for this query." : grade.dimension === "chain-validation" ? `Faithfulness ${facts.chain?.faithfulness ?? 0}, relevance ${facts.chain?.relevance ?? 0} (${facts.chain?.judged ? "LLM-judged" : facts.chain?.groundedAbstention ? "heuristic; grounded abstention" : "deterministic heuristic"}).` : grade.dimension === "resilience" ? `${facts.resilience?.passed ?? 0}/${facts.resilience?.total ?? 0} injected fault(s) handled gracefully.` : grade.detailCode;
11297
+ }
11298
+ function hostedRagReport(results, judge) {
11299
+ let dimensions = /* @__PURE__ */ new Map();
11300
+ for (let result of results)
11301
+ for (let check of result.checks)
11302
+ dimensions.set(check.dimension, [...dimensions.get(check.dimension) ?? [], check.score]);
11303
+ return {
11304
+ results,
11305
+ dimensions: [...dimensions.entries()].map(([dimension, scores]) => ({ dimension, score: averageRagScore(scores), cases: scores.length })),
11306
+ overall: averageRagScore(results.map((result) => result.score)),
11307
+ total: results.length,
11308
+ passed: results.filter((result) => result.checks.every((check) => check.pass)).length,
11309
+ judge
11310
+ };
11311
+ }
11312
+ function hostedRagSnapshot(report) {
11313
+ return { overall: report.overall, dimensions: report.dimensions, cases: report.results.map((result) => ({ id: result.case.id, score: result.score, drift: result.drift, signature: sha256(result.signature), selection: result.selection ? { picked: result.selection.picked, reason: result.selection.reason } : void 0, checks: result.checks.map((check) => ({ dimension: check.dimension, score: check.score, detail: check.detail })) })) };
11314
+ }
11315
+ function hostedRagScoreDiff(previous, current) {
11316
+ let previousCases = new Map(previous.cases.map((item) => [item.id, item])), currentCases = new Map(current.cases.map((item) => [item.id, item])), shared = [...currentCases.keys()].filter((id) => previousCases.has(id)), contribution = /* @__PURE__ */ new Map();
11317
+ for (let id of shared) {
11318
+ let before = new Map(previousCases.get(id).checks.map((check) => [check.dimension, check])), dimensions2 = currentCases.get(id).checks.filter((check) => before.has(check.dimension));
11319
+ for (let check of dimensions2)
11320
+ contribution.set(check.dimension, (contribution.get(check.dimension) ?? 0) + (check.score - before.get(check.dimension).score) / Math.max(1, dimensions2.length) / Math.max(1, shared.length));
11321
+ }
11322
+ let dimensions = [...new Set([...previous.dimensions, ...current.dimensions].map((item) => item.dimension))].map((dimension) => {
11323
+ let from = previous.dimensions.find((item) => item.dimension === dimension)?.score ?? 0, to = current.dimensions.find((item) => item.dimension === dimension)?.score ?? 0;
11324
+ return { dimension, from, to, delta: to - from, contribution: Math.round(contribution.get(dimension) ?? 0) };
11325
+ }).sort((a, b) => Math.abs(b.contribution) - Math.abs(a.contribution)), labels = { "retrieval-quality": "Retrieval quality", "tool-selection": "Tool selection", "retriever-behavior": "Retriever behavior", "retrieval-contract": "Retrieval contract", "retrieval-drift": "Retrieval drift", "chain-validation": "End-to-end chain", resilience: "Resilience (fault injection)" }, causes = dimensions.filter((item) => item.contribution !== 0).map((item) => ({ dimension: item.dimension, points: item.contribution, label: labels[item.dimension], detail: `Dimension changed ${item.from} \u2192 ${item.to}.` })).sort((a, b) => a.points < 0 && b.points >= 0 ? -1 : a.points >= 0 && b.points < 0 ? 1 : Math.abs(b.points) - Math.abs(a.points));
11326
+ return { from: previous.overall, to: current.overall, delta: current.overall - previous.overall, dimensions, causes, addedCases: [...currentCases.keys()].filter((id) => !previousCases.has(id)), removedCases: [...previousCases.keys()].filter((id) => !currentCases.has(id)), comparable: shared.length > 0 };
11327
+ }
11328
+ async function observeHostedRagCase(service, testCase, options) {
11329
+ let capabilities = service.getCapabilities();
11330
+ if (!capabilities)
11331
+ throw new Error("Connect to an MCP server before running RAG Assurance.");
11332
+ let tools = capabilities.tools.map((tool2) => ({ name: tool2.name, description: tool2.description })), query = hostedRagCaseQuery(testCase), mode = options.chain && testCase.mode !== "retriever-only" ? "full-chain" : "retriever-only", runChain = mode === "full-chain", definitionHash = hostedRagDefinitionHash(testCase, options), inputTokens = 0, outputTokens = 0, addUsage = (usage) => {
11333
+ inputTokens += usage?.inputTokens ?? 0, outputTokens += usage?.outputTokens ?? 0;
11334
+ }, tool = testCase.tool ?? "", selection, toolSelection = "not-applicable";
11335
+ if (options.useAgentPick) {
11336
+ if (!options.target?.model || !options.target.apiKey)
11337
+ throw new Error("Agent tool selection requires the locally configured target LLM.");
11338
+ let list = tools.map((candidate) => `- ${candidate.name}: ${(candidate.description ?? "").slice(0, 140)}`).join(`
11339
+ `), routed = await callLlm({
11340
+ ...options.target,
11341
+ system: "Route the user query to the single best retrieval tool. Return only JSON.",
11342
+ prompt: `Tools:
11343
+ ${list}
11344
+
11345
+ Query: ${query}
11346
+
11347
+ Return ONLY: {"tool":"<exact tool name>","reason":"<one short sentence>"}`
11348
+ });
11349
+ addUsage(routed.usage);
11350
+ let raw = routed.ok ? (routed.text ?? "").trim() : `LLM router error: ${routed.error ?? "request failed"}`, parsed = parsePick(raw);
11351
+ tool = routed.ok ? resolveTool(parsed?.tool ?? raw, tools) ?? "" : "", selection = { picked: tool || null, reason: parsed?.reason, raw }, toolSelection = tool ? testCase.expectedTool ? tool === testCase.expectedTool ? "match" : "mismatch" : "match" : "unresolved";
11352
+ }
11353
+ if (!tool && !options.useAgentPick && (tool = tools[0]?.name ?? ""), !tool) {
11354
+ let facts2 = {
11355
+ version: "prooflane-hosted-rag-facts-v1",
11356
+ caseId: testCase.id,
11357
+ definitionHash,
11358
+ toolSelection: "unresolved",
11359
+ retrieverError: !0,
11360
+ latencyMs: 0,
11361
+ resultCount: 0,
11362
+ contractPassed: 0,
11363
+ contractTotal: testCase.contract?.length ?? 0,
11364
+ defaultContractPass: !1,
11365
+ expectedKeywordHits: 0,
11366
+ forbiddenKeywordHits: 0,
11367
+ golden: options.golden ? options.golden[testCase.id] === void 0 ? "new" : "drift" : "n/a"
11368
+ };
11369
+ return {
11370
+ facts: facts2,
11371
+ result: { case: testCase, tool: "(unresolved)", mode, latencyMs: 0, resultCount: 0, checks: [], score: 0, signature: "", selection, drift: facts2.golden },
11372
+ inputTokens,
11373
+ outputTokens
11374
+ };
11375
+ }
11376
+ let input = hostedRagCaseInput(testCase), started = Date.now(), output, isError = !1;
11377
+ try {
11378
+ let response = await service.callTool(tool, input);
11379
+ output = response.output, isError = response.isError;
11380
+ } catch (error) {
11381
+ output = { error: error instanceof Error ? error.message : String(error) }, isError = !0;
11382
+ }
11383
+ let latencyMs = Date.now() - started, payload = extractHostedRagPayload(output), context = hostedRagContext(payload), lowerContext = context.toLowerCase(), resultCount4 = hostedRagCount(payload), contractPassed = 0, contractTotal = testCase.contract?.length ?? 0;
11384
+ if (contractTotal) {
11385
+ let contract = evaluateAssertions(payload, testCase.contract);
11386
+ contractPassed = contract.results.filter((assertion) => assertion.pass).length, contractTotal = contract.results.length;
11387
+ }
11388
+ let defaultContractPass = testCase.expectedOutcome === "DENIAL" ? isError : testCase.expectedOutcome === "EMPTY" ? !isError && resultCount4 === 0 : !isError && resultCount4 > 0, expectedKeywordHits = (testCase.expectedKeywords ?? []).filter((term) => lowerContext.includes(term.toLowerCase())).length, forbiddenKeywordHits = (testCase.forbiddenKeywords ?? []).filter((term) => lowerContext.includes(term.toLowerCase())).length, golden = options.golden ? options.golden[testCase.id] === void 0 ? "new" : options.golden[testCase.id] === context ? "match" : "drift" : "n/a", answer, faithfulness, relevance, judged = !1, groundedAbstention = !1;
11389
+ if (runChain) {
11390
+ if (!options.target?.model || !options.target.apiKey)
11391
+ throw new Error("Full-chain RAG Assurance requires the locally configured target LLM.");
11392
+ let answerRun = await callLlm({
11393
+ ...options.target,
11394
+ system: "Answer only from the supplied retrieved context. If the answer is not supported by the context, say you do not know.",
11395
+ prompt: `Context:
11396
+ ${context}
11397
+
11398
+ Question: ${query}
11399
+
11400
+ Answer:`
11401
+ });
11402
+ if (addUsage(answerRun.usage), !answerRun.ok)
11403
+ throw new Error(answerRun.error ?? "The target LLM could not generate the RAG answer.");
11404
+ answer = answerRun.text ?? "";
11405
+ let answerLower = answer.toLowerCase(), keywordRelevance = testCase.expectedAnswer?.length ? testCase.expectedAnswer.filter((term) => answerLower.includes(term.toLowerCase())).length / testCase.expectedAnswer.length * 100 : null, judgeModel = options.judgeStrategy === "independent" ? options.judge : options.judgeStrategy === "same-model" ? options.target : void 0;
11406
+ if (judgeModel) {
11407
+ let judgeRun = await callLlm({
11408
+ ...judgeModel,
11409
+ system: "You are a strict RAG answer evaluator. Reply with only JSON.",
11410
+ prompt: `QUESTION:
11411
+ ${query}
11412
+
11413
+ CONTEXT:
11414
+ ${context}
11415
+
11416
+ ANSWER:
11417
+ ${answer}
11418
+
11419
+ Rate faithfulness and relevance from 0 to 100. Return ONLY: {"faithfulness":<0-100>,"relevance":<0-100>,"rationale":"<one short sentence>"}`
11420
+ });
11421
+ addUsage(judgeRun.usage);
11422
+ let verdict = judgeRun.ok ? parseJudge(judgeRun.text ?? "") : null;
11423
+ verdict && (faithfulness = clampRagScore(verdict.faithfulness), relevance = keywordRelevance == null ? clampRagScore(verdict.relevance) : Math.round((clampRagScore(verdict.relevance) + keywordRelevance) / 2), judged = !0);
11424
+ }
11425
+ if (!judged) {
11426
+ let words = [...new Set(answerLower.split(/[^a-z0-9]+/).filter((word) => word.length > 4))];
11427
+ groundedAbstention = HOSTED_RAG_ABSTAIN.test(answer) && hostedRagContextLacksSupport(query, context, testCase.expectedAnswer), faithfulness = groundedAbstention ? 100 : words.length ? Math.round(words.filter((word) => lowerContext.includes(word)).length / words.length * 100) : 100, relevance = keywordRelevance == null ? groundedAbstention ? 50 : 100 : Math.round(keywordRelevance);
11428
+ }
11429
+ }
11430
+ let resilience = [];
11431
+ if (options.faults.length) {
11432
+ if (!options.target?.model || !options.target.apiKey)
11433
+ throw new Error("RAG fault resilience requires the locally configured target LLM.");
11434
+ for (let fault of options.faults) {
11435
+ let faulted = fault === "malformed-output" ? hostedRagContext(hostedRagMalformed(payload)) : fault === "error-response" ? JSON.stringify({ error: "injected_error", tool }) : "", note = fault === "empty-output" ? "Injected empty retrieval output." : fault === "error-response" ? "Injected retriever error response." : "Injected malformed retrieval output with garbled fields and dropped keys.", response = await callLlm({
11436
+ ...options.target,
11437
+ system: "Answer only from the supplied retrieved context. If it is missing, errored, or malformed, say you do not know.",
11438
+ prompt: `Context:
11439
+ ${faulted}
11440
+
11441
+ Question: ${query}
11442
+
11443
+ Answer:`
11444
+ });
11445
+ addUsage(response.usage);
11446
+ let faultAnswer = response.ok ? response.text ?? "" : "";
11447
+ resilience.push({ type: fault, abstained: response.ok && HOSTED_RAG_ABSTAIN.test(faultAnswer), note, answer: faultAnswer });
11448
+ }
11449
+ }
11450
+ return {
11451
+ facts: {
11452
+ version: "prooflane-hosted-rag-facts-v1",
11453
+ caseId: testCase.id,
11454
+ definitionHash,
11455
+ toolSelection,
11456
+ retrieverError: isError,
11457
+ latencyMs,
11458
+ resultCount: resultCount4,
11459
+ contractPassed,
11460
+ contractTotal,
11461
+ defaultContractPass,
11462
+ expectedKeywordHits,
11463
+ forbiddenKeywordHits,
11464
+ golden,
11465
+ ...runChain && faithfulness != null && relevance != null ? { chain: { faithfulness, relevance, judged, groundedAbstention } } : {},
11466
+ ...resilience.length ? { resilience: { passed: resilience.filter((probe) => probe.abstained).length, total: resilience.length } } : {}
11467
+ },
11468
+ result: {
11469
+ case: testCase,
11470
+ tool,
11471
+ input,
11472
+ mode,
11473
+ latencyMs,
11474
+ resultCount: resultCount4,
11475
+ checks: [],
11476
+ score: 0,
11477
+ signature: context,
11478
+ isError,
11479
+ answer,
11480
+ faithfulness,
11481
+ relevance,
11482
+ judged,
11483
+ selection,
11484
+ resilience: resilience.length ? resilience : void 0,
11485
+ drift: golden
11486
+ },
11487
+ inputTokens,
11488
+ outputTokens
11489
+ };
11490
+ }
11216
11491
  function resultCount3(value) {
11217
11492
  if (Array.isArray(value))
11218
11493
  return value.length;
@@ -11300,6 +11575,26 @@ function parseJudge(text6) {
11300
11575
  return null;
11301
11576
  }
11302
11577
  }
11578
+ function parsePick(text6) {
11579
+ let t = text6.trim(), fence = t.match(/```(?:json)?\s*([\s\S]*?)```/);
11580
+ fence && (t = fence[1].trim());
11581
+ let start = t.indexOf("{"), end = t.lastIndexOf("}");
11582
+ if (start === -1 || end === -1)
11583
+ return null;
11584
+ try {
11585
+ let o = JSON.parse(t.slice(start, end + 1));
11586
+ return { tool: typeof o.tool == "string" ? o.tool : void 0, reason: typeof o.reason == "string" ? o.reason : void 0 };
11587
+ } catch {
11588
+ return null;
11589
+ }
11590
+ }
11591
+ function resolveTool(answer, tools) {
11592
+ let n = answer.trim().toLowerCase().replace(/^["'`\s]+|["'`.\s]+$/g, ""), m = tools.find((t) => t.name.toLowerCase() === n);
11593
+ if (m)
11594
+ return m.name;
11595
+ let tokens = new Set(n.split(/[^a-z0-9_]+/).filter(Boolean));
11596
+ return m = tools.find((t) => tokens.has(t.name.toLowerCase())), m ? m.name : (m = [...tools].sort((a, b) => b.name.length - a.name.length).find((t) => t.name.length >= 3 && n.includes(t.name.toLowerCase())), m?.name ?? null);
11597
+ }
11303
11598
  function parseGeneratedPrompts(text6) {
11304
11599
  let t = text6.trim(), fence = t.match(/```(?:json)?\s*([\s\S]*?)```/);
11305
11600
  fence && (t = fence[1].trim());
@@ -12954,6 +13249,201 @@ ${answer}`
12954
13249
  return { output: r.output, isError: r.isError };
12955
13250
  }
12956
13251
  }) : reply.code(400).send({ ok: !1, toolCalls: [], error: `Tool "${tool}" isn't on the current connection \u2014 connect first.`, latencyMs: 0 });
13252
+ }), app2.post("/api/rag/hosted-run", async (req, reply) => {
13253
+ let cases = Array.isArray(req.body?.cases) ? req.body.cases.filter((candidate) => candidate && typeof candidate.id == "string" && hostedRagCaseQuery(candidate).trim()) : [], capabilities = svc.getCapabilities();
13254
+ if (!capabilities)
13255
+ return reply.code(400).send({ ok: !1, error: "Connect to an MCP server first \u2014 RAG Assurance runs against its retrieval tools." });
13256
+ if (!cases.length)
13257
+ return reply.code(400).send({ ok: !1, error: "Add at least one retrieval query to evaluate." });
13258
+ if (new Set(cases.map((candidate) => candidate.id)).size !== cases.length)
13259
+ return reply.code(400).send({ ok: !1, error: "Every RAG case needs a unique id." });
13260
+ let useAgentPick = req.body.useAgentPick === !0, chain = req.body.chain === !0, faults = Array.isArray(req.body.faults) ? [...new Set(req.body.faults.filter((fault) => fault === "empty-output" || fault === "error-response" || fault === "malformed-output"))] : [], target = req.body.llm, judge = req.body.judge, judgeStrategy = req.body.judgeStrategy ?? "same-model", golden = req.body.golden;
13261
+ if ((useAgentPick || chain || faults.length) && (!target?.model || !target.apiKey))
13262
+ return reply.code(400).send({ ok: !1, error: "Agent tool selection, full-chain answers, and fault resilience require the locally configured target LLM." });
13263
+ if (chain && judgeStrategy === "independent" && (!judge?.model || !judge.apiKey))
13264
+ return reply.code(400).send({ ok: !1, error: "Independent judging requires a separate locally configured judge LLM." });
13265
+ if (chain && judgeStrategy === "independent" && judge && target && judge.provider === target.provider && judge.model.trim() === target.model.trim())
13266
+ return reply.code(400).send({ ok: !1, error: "Independent judging requires a different model or provider than the target LLM." });
13267
+ let mcpSnapshot;
13268
+ try {
13269
+ mcpSnapshot = startBoundMcpRun("RAG", "rag-assurance-default");
13270
+ } catch (error) {
13271
+ return sendMcpError(reply, error);
13272
+ }
13273
+ if (mcpSnapshot) {
13274
+ if (useAgentPick)
13275
+ return reply.code(409).send({ ok: !1, error: "This RAG configuration is pinned to one MCP tool; agent tool selection cannot override the saved binding." });
13276
+ let conflict = cases.find((candidate) => {
13277
+ let selected = candidate.tool ?? candidate.expectedTool;
13278
+ return selected && selected !== mcpSnapshot.mcp.toolName;
13279
+ });
13280
+ if (conflict)
13281
+ return reply.code(409).send({ ok: !1, error: `RAG case ${conflict.id} targets ${conflict.tool ?? conflict.expectedTool}, but this feature is bound to ${mcpSnapshot.mcp.serverName} / ${mcpSnapshot.mcp.toolName}.` });
13282
+ cases = cases.map((candidate) => ({ ...candidate, tool: mcpSnapshot.mcp.toolName }));
13283
+ }
13284
+ let localPlan = await resolvePlan(req);
13285
+ if (!localPlan.rag)
13286
+ return reply.code(402).send({ ok: !1, upgrade: !0, feature: "rag", tier: localPlan.tier, error: "RAG Assurance is part of the Scale plan." });
13287
+ if (!isUnlimited(localPlan.retrievers)) {
13288
+ let retrievers = new Set(cases.map((candidate) => candidate.tool ?? candidate.expectedTool).filter((tool) => !!tool));
13289
+ if (retrievers.size > localPlan.retrievers)
13290
+ return reply.code(402).send({ ok: !1, upgrade: !0, feature: "retrievers", tier: localPlan.tier, limit: localPlan.retrievers, requested: retrievers.size, error: `Your ${localPlan.label} plan assures up to ${localPlan.retrievers} retrievers.` });
13291
+ }
13292
+ let runOptions = { useAgentPick, chain, faults, target, judge, judgeStrategy, golden }, manifest = {
13293
+ version: "prooflane-hosted-rag-run-v1",
13294
+ cases: cases.map((testCase) => {
13295
+ let mode = chain && testCase.mode !== "retriever-only" ? "full-chain" : "retriever-only";
13296
+ return {
13297
+ caseId: testCase.id,
13298
+ definitionHash: hostedRagDefinitionHash(testCase, runOptions),
13299
+ mode,
13300
+ useAgentPick,
13301
+ expectedToolConfigured: !!testCase.expectedTool,
13302
+ expectedKeywordCount: testCase.expectedKeywords?.length ?? 0,
13303
+ forbiddenKeywordCount: testCase.forbiddenKeywords?.length ?? 0,
13304
+ contractAssertionCount: testCase.contract?.length ?? 0,
13305
+ minResults: testCase.minResults ?? 1,
13306
+ maxLatencyMs: testCase.maxLatencyMs,
13307
+ expectedOutcome: testCase.expectedOutcome,
13308
+ expectedAnswerCount: testCase.expectedAnswer?.length ?? 0,
13309
+ goldenEnabled: golden !== void 0,
13310
+ hasGolden: golden?.[testCase.id] !== void 0,
13311
+ faultCount: faults.length,
13312
+ judgeStrategy
13313
+ };
13314
+ })
13315
+ }, createRequest = {
13316
+ pillar: "rag",
13317
+ target: { name: (capabilities.serverName ?? "Connected MCP retriever").slice(0, 160), type: "rag" },
13318
+ capabilityHash: sha256({ tools: capabilities.tools.map((tool) => tool.name).sort(), protocol: capabilities.protocolVersion ?? null }),
13319
+ ragManifest: manifest
13320
+ }, created;
13321
+ try {
13322
+ created = await requestControlPlaneJson(req, "/v1/scans", "POST", createRequest);
13323
+ } catch (error) {
13324
+ let status = error instanceof ControlPlaneRequestError ? error.status : 502, message2 = error instanceof Error ? error.message : "Prooflane hosted RAG Assurance could not start.";
13325
+ return reply.code(status).send({ ok: !1, error: message2 });
13326
+ }
13327
+ let wantStream = req.body.stream === !0, clientGone = !1, streamOpen = !1;
13328
+ wantStream && reply.raw.on("close", () => {
13329
+ clientGone = !0;
13330
+ });
13331
+ let streamWrite = (event) => {
13332
+ if (wantStream) {
13333
+ streamOpen || (reply.hijack(), reply.raw.writeHead(200, { "Content-Type": "application/x-ndjson", "Cache-Control": "no-cache" }), streamOpen = !0);
13334
+ try {
13335
+ reply.raw.write(`${JSON.stringify(event)}
13336
+ `);
13337
+ } catch {
13338
+ clientGone = !0;
13339
+ }
13340
+ }
13341
+ }, finish = (payload) => {
13342
+ let withMcp = mcpSnapshot ? { ...payload, mcpSnapshot, mcpFact: recordBoundMcpFact(mcpSnapshot, !0) } : payload;
13343
+ return wantStream ? (streamWrite({ t: "done", ...withMcp }), streamOpen && reply.raw.end(), reply) : withMcp;
13344
+ }, inputTokens = 0, outputTokens = 0, localResults = [], hostedScan = created.scan;
13345
+ try {
13346
+ for (let index = 0; index < cases.length; index += 1) {
13347
+ if (clientGone)
13348
+ return await requestControlPlaneJson(req, `/v1/scans/${encodeURIComponent(created.scan.id)}/cancel`, "POST", {}).catch(() => {
13349
+ }), reply;
13350
+ let next = await requestControlPlaneJson(req, `/v1/scans/${encodeURIComponent(created.scan.id)}/next-task`, "POST", {});
13351
+ if (next.done || !next.lease)
13352
+ throw new Error("The hosted RAG scan ended before every authored case was graded.");
13353
+ let claims = decodeJwt(next.lease);
13354
+ if (!isScanTaskLeaseClaims(claims) || claims.task.kind !== "rag.retrieval" || !claims.task.localCase)
13355
+ throw new Error("The hosted RAG lease is malformed.");
13356
+ let signedCase = claims.task.localCase, testCase = cases.find((candidate) => candidate.id === signedCase.caseId);
13357
+ if (!testCase)
13358
+ throw new Error("The hosted RAG lease does not match a local authored case.");
13359
+ if (hostedRagDefinitionHash(testCase, runOptions) !== signedCase.definitionHash)
13360
+ throw new Error("The hosted RAG lease does not match the local case definition.");
13361
+ streamWrite({ t: "progress", done: index, total: cases.length, id: testCase.id, name: hostedRagCaseName(testCase), mode: signedCase.mode });
13362
+ let observed = await observeHostedRagCase(svc, testCase, runOptions);
13363
+ inputTokens += observed.inputTokens, outputTokens += observed.outputTokens;
13364
+ let evidence = {
13365
+ leaseId: claims.leaseId,
13366
+ taskId: claims.task.id,
13367
+ taskKind: claims.task.kind,
13368
+ scanId: claims.scanId,
13369
+ controlId: claims.controlId,
13370
+ sequence: claims.sequence,
13371
+ nonce: claims.nonce,
13372
+ outcome: "pass",
13373
+ score: 0,
13374
+ latencyMs: observed.facts.latencyMs,
13375
+ metrics: {
13376
+ resultCount: observed.facts.resultCount,
13377
+ retrieverError: observed.facts.retrieverError,
13378
+ contractPassed: observed.facts.contractPassed,
13379
+ contractTotal: observed.facts.contractTotal,
13380
+ expectedKeywordHits: observed.facts.expectedKeywordHits,
13381
+ forbiddenKeywordHits: observed.facts.forbiddenKeywordHits
13382
+ },
13383
+ outputHash: sha256({ definitionHash: observed.facts.definitionHash, signature: observed.result.signature, answer: observed.result.answer ?? null }),
13384
+ summary: "Local Runner submitted compact facts for the signed hosted RAG rubric; raw evidence stayed on this device.",
13385
+ result: "inconclusive",
13386
+ severity: "high",
13387
+ ragFacts: observed.facts
13388
+ }, accepted = await requestControlPlaneJson(req, `/v1/scans/${encodeURIComponent(created.scan.id)}/evidence`, "POST", { lease: next.lease, evidence });
13389
+ hostedScan = accepted.scan;
13390
+ let grade = accepted.scan.controls.find((control) => control.id === claims.controlId)?.ragGrade;
13391
+ if (!grade || grade.caseId !== testCase.id || grade.definitionHash !== observed.facts.definitionHash)
13392
+ throw new Error("The hosted RAG grader did not return the signed case grade.");
13393
+ observed.result.checks = grade.checks.map((check) => ({ ...check, detail: hostedRagCheckDetail(check, observed.facts, testCase) })), observed.result.score = grade.score, observed.result.drift = grade.drift, localResults.push(observed.result);
13394
+ }
13395
+ streamWrite({ t: "progress", done: cases.length, total: cases.length, id: "", name: "", mode: chain ? "full-chain" : "retriever-only" });
13396
+ } catch (error) {
13397
+ await requestControlPlaneJson(req, `/v1/scans/${encodeURIComponent(created.scan.id)}/cancel`, "POST", {}).catch(() => {
13398
+ });
13399
+ let message2 = error instanceof Error ? error.message : "Hosted RAG Assurance could not complete.";
13400
+ if (wantStream)
13401
+ return streamWrite({ t: "error", ok: !1, error: message2 }), streamOpen && reply.raw.end(), reply;
13402
+ let status = error instanceof ControlPlaneRequestError ? error.status : 400;
13403
+ return reply.code(status).send({ ok: !1, error: message2 });
13404
+ }
13405
+ let report = hostedRagReport(localResults, chain ? judgeStrategy === "deterministic" ? { strategy: "deterministic" } : { strategy: judgeStrategy, provider: (judgeStrategy === "independent" ? judge : target)?.provider, model: (judgeStrategy === "independent" ? judge : target)?.model } : void 0);
13406
+ if (req.body.recordHistory === !1)
13407
+ return finish({ ok: !0, report, hostedScan });
13408
+ let previous = (() => {
13409
+ let last = svc.listRuns(500).find((record6) => record6.pillar === "rag" && record6.evidence);
13410
+ if (!last?.evidence)
13411
+ return null;
13412
+ try {
13413
+ return JSON.parse(last.evidence);
13414
+ } catch {
13415
+ return null;
13416
+ }
13417
+ })(), snapshot = hostedRagSnapshot(report), worst = [...report.dimensions].sort((a, b) => a.score - b.score)[0], dimensionLabels = { "retrieval-quality": "Retrieval quality", "tool-selection": "Tool selection", "retriever-behavior": "Retriever behavior", "retrieval-contract": "Retrieval contract", "retrieval-drift": "Retrieval drift", "chain-validation": "End-to-end chain", resilience: "Resilience (fault injection)" }, run = svc.recordRun({
13418
+ pillar: "rag",
13419
+ label: `RAG assurance \u2014 ${cases.length} quer${cases.length === 1 ? "y" : "ies"}${target?.model ? ` (${target.model})` : ""}`,
13420
+ total: report.total,
13421
+ pass: report.passed,
13422
+ drift: report.results.filter((result) => result.drift === "drift").length,
13423
+ fail: report.total - report.passed,
13424
+ error: 0,
13425
+ score: report.overall,
13426
+ model: target?.model,
13427
+ tokens: inputTokens + outputTokens || void 0,
13428
+ inputTokens: inputTokens || void 0,
13429
+ outputTokens: outputTokens || void 0,
13430
+ finding: worst && worst.score < 100 ? `${dimensionLabels[worst.dimension]} at ${worst.score}` : void 0,
13431
+ durationMs: report.results.reduce((sum, result) => sum + result.latencyMs, 0),
13432
+ evidence: JSON.stringify(mcpSnapshot ? { ...snapshot, mcp: mcpSnapshot.mcp, hostedScanId: hostedScan.id } : { ...snapshot, hostedScanId: hostedScan.id })
13433
+ }), diff = previous ? hostedRagScoreDiff(previous, snapshot) : void 0;
13434
+ return finish({ ok: !0, report, diff, run, hostedScan });
13435
+ }), app2.get("/api/rag/hosted-diff/:id", async (req, reply) => {
13436
+ let runs = svc.listRuns(1e3).filter((record6) => record6.pillar === "rag"), index = runs.findIndex((record6) => record6.id === req.params.id);
13437
+ if (index === -1)
13438
+ return reply.code(404).send({ ok: !1, error: "Run not found." });
13439
+ let current = runs[index], previous = runs.slice(index + 1).find((record6) => record6.evidence);
13440
+ if (!current.evidence || !previous?.evidence)
13441
+ return { ok: !0, diff: null };
13442
+ try {
13443
+ return { ok: !0, diff: hostedRagScoreDiff(JSON.parse(previous.evidence), JSON.parse(current.evidence)) };
13444
+ } catch {
13445
+ return { ok: !0, diff: null };
13446
+ }
12957
13447
  });
12958
13448
  function recordRedTeamRun(layer, model, report, inputTokens, outputTokens) {
12959
13449
  let sevRank = { critical: 0, high: 1, medium: 2, low: 3 }, bySeverity = (a, b) => (sevRank[a.attack.severity] ?? 9) - (sevRank[b.attack.severity] ?? 9), findings = report.results.filter((r) => r.verdict === "breached"), breached = findings.filter((r) => (r.findingClass ?? "breach") === "breach").sort(bySeverity), exposures = findings.filter((r) => r.findingClass === "exposure").sort(bySeverity), poisoned = findings.filter((r) => r.findingClass === "poisoning"), primary = breached[0], durationMs = report.results.reduce((s, r) => s + (r.ms ?? 0), 0);