@lunora/testing 1.0.0-alpha.110 → 1.0.0-alpha.112

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/index.d.mts CHANGED
@@ -578,6 +578,20 @@ interface Scorer {
578
578
  name: string;
579
579
  score: (sample: ScorerSample) => Promise<ScoreResult | number> | ScoreResult | number;
580
580
  }
581
+ /**
582
+ * What a `produce` runner may return: the output text alone, or the text plus
583
+ * metadata describing what the run actually did.
584
+ *
585
+ * The metadata form exists for scorers that judge more than the final string —
586
+ * a retrieval scorer needs the ranked ids the run retrieved, which only the run
587
+ * can know. It is merged OVER the case's own metadata before scoring.
588
+ */
589
+ interface ProducedOutput {
590
+ /** Merged over the case's `metadata`, then handed to every scorer. */
591
+ metadata?: Record<string, unknown>;
592
+ /** The output text under test. */
593
+ output: string;
594
+ }
581
595
  /** One dataset case: an input and its optional gold answer/metadata. */
582
596
  interface EvalCase {
583
597
  expected?: string;
@@ -628,5 +642,70 @@ declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>
628
642
  * give each its own thread/key inside `produce` if the producer is stateful.
629
643
  * Returns per-case results plus the mean of their averages.
630
644
  */
631
- declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
632
- export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type RecordEvaluationInput, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
645
+ declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<ProducedOutput | string> | ProducedOutput | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
646
+ /** Where a retrieval scorer reads its two id lists from. */
647
+ interface RetrievalScorerOptions {
648
+ /** Metadata key holding the gold relevant ids. Default `"relevant"`. */
649
+ relevantKey?: string;
650
+ /** Metadata key holding the run's ranked retrieved ids. Default `"retrieved"`. */
651
+ retrievedKey?: string;
652
+ }
653
+ /**
654
+ * Recall@k scorer — what fraction of the gold passages made it into the top
655
+ * `k`.
656
+ *
657
+ * This is the ceiling on everything downstream: a passage retrieval never
658
+ * returned is one no reranker can promote and no prompt can cite. Omit `k` to
659
+ * score the whole retrieved list.
660
+ */
661
+ declare const recallAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
662
+ /**
663
+ * Precision@k scorer — what fraction of the top `k` retrieved passages are
664
+ * gold.
665
+ *
666
+ * This is the counterweight to recall: padding `topK` raises recall for free
667
+ * while burying the answer in noise the model has to read past, and pay for.
668
+ * Omit `k` to score the whole retrieved list.
669
+ */
670
+ declare const precisionAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
671
+ /**
672
+ * Mean Reciprocal Rank scorer — `1 / rank` of the first gold passage: 1 if it
673
+ * is first, 0.5 if second, 0 if absent.
674
+ *
675
+ * This is the metric that notices ordering, which recall cannot. A gold passage
676
+ * at rank 20 counts the same as rank 1 for recall@20, but only one of those
677
+ * survives a context-window trim or a model that skims the top of its prompt.
678
+ */
679
+ declare const mrrScorer: (options?: RetrievalScorerOptions) => Scorer;
680
+ /**
681
+ * Normalized Discounted Cumulative Gain scorer — relevance discounted
682
+ * logarithmically by rank, divided by the best achievable arrangement.
683
+ *
684
+ * This is the one to gate on when comparing retrieval strategies. Unlike recall
685
+ * it is sensitive to order, and unlike MRR it credits every gold passage rather
686
+ * than only the first — so it is the metric that can actually say whether a
687
+ * reranker or a hybrid leg helped.
688
+ *
689
+ * Relevance is binary: an id is gold or it is not, which is what a gold-id set
690
+ * expresses. Graded relevance would need per-id weights.
691
+ */
692
+ declare const ndcgAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
693
+ /**
694
+ * Groundedness scorer — does the answer assert only what the retrieved context
695
+ * supports?
696
+ *
697
+ * This is the generation-side counterpart to the metrics above: perfect
698
+ * retrieval still fails if the model answers from its own weights. It scores
699
+ * the output against the context via an injected `judge`, the same shape
700
+ * `llmScorer` takes, so this stays model-agnostic and mockable.
701
+ *
702
+ * Reads the context from `metadata.context` by default — the `context` string
703
+ * `retrieve()` already returns. Fails closed: no context means nothing could
704
+ * have grounded the answer.
705
+ */
706
+ declare const groundednessScorer: (options: {
707
+ contextKey?: string;
708
+ judge: (prompt: string) => Promise<string>;
709
+ name?: string;
710
+ }) => Scorer;
711
+ export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type ProducedOutput, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
package/dist/index.d.ts CHANGED
@@ -578,6 +578,20 @@ interface Scorer {
578
578
  name: string;
579
579
  score: (sample: ScorerSample) => Promise<ScoreResult | number> | ScoreResult | number;
580
580
  }
581
+ /**
582
+ * What a `produce` runner may return: the output text alone, or the text plus
583
+ * metadata describing what the run actually did.
584
+ *
585
+ * The metadata form exists for scorers that judge more than the final string —
586
+ * a retrieval scorer needs the ranked ids the run retrieved, which only the run
587
+ * can know. It is merged OVER the case's own metadata before scoring.
588
+ */
589
+ interface ProducedOutput {
590
+ /** Merged over the case's `metadata`, then handed to every scorer. */
591
+ metadata?: Record<string, unknown>;
592
+ /** The output text under test. */
593
+ output: string;
594
+ }
581
595
  /** One dataset case: an input and its optional gold answer/metadata. */
582
596
  interface EvalCase {
583
597
  expected?: string;
@@ -628,5 +642,70 @@ declare const scoreSample: (sample: ScorerSample, scorers: ReadonlyArray<Scorer>
628
642
  * give each its own thread/key inside `produce` if the producer is stateful.
629
643
  * Returns per-case results plus the mean of their averages.
630
644
  */
631
- declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<string> | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
632
- export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type RecordEvaluationInput, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, keywordScorer, llmScorer, lunoraTest, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
645
+ declare const evaluate: (cases: ReadonlyArray<EvalCase>, produce: (input: string) => Promise<ProducedOutput | string> | ProducedOutput | string, scorers: ReadonlyArray<Scorer>) => Promise<EvalResult>;
646
+ /** Where a retrieval scorer reads its two id lists from. */
647
+ interface RetrievalScorerOptions {
648
+ /** Metadata key holding the gold relevant ids. Default `"relevant"`. */
649
+ relevantKey?: string;
650
+ /** Metadata key holding the run's ranked retrieved ids. Default `"retrieved"`. */
651
+ retrievedKey?: string;
652
+ }
653
+ /**
654
+ * Recall@k scorer — what fraction of the gold passages made it into the top
655
+ * `k`.
656
+ *
657
+ * This is the ceiling on everything downstream: a passage retrieval never
658
+ * returned is one no reranker can promote and no prompt can cite. Omit `k` to
659
+ * score the whole retrieved list.
660
+ */
661
+ declare const recallAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
662
+ /**
663
+ * Precision@k scorer — what fraction of the top `k` retrieved passages are
664
+ * gold.
665
+ *
666
+ * This is the counterweight to recall: padding `topK` raises recall for free
667
+ * while burying the answer in noise the model has to read past, and pay for.
668
+ * Omit `k` to score the whole retrieved list.
669
+ */
670
+ declare const precisionAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
671
+ /**
672
+ * Mean Reciprocal Rank scorer — `1 / rank` of the first gold passage: 1 if it
673
+ * is first, 0.5 if second, 0 if absent.
674
+ *
675
+ * This is the metric that notices ordering, which recall cannot. A gold passage
676
+ * at rank 20 counts the same as rank 1 for recall@20, but only one of those
677
+ * survives a context-window trim or a model that skims the top of its prompt.
678
+ */
679
+ declare const mrrScorer: (options?: RetrievalScorerOptions) => Scorer;
680
+ /**
681
+ * Normalized Discounted Cumulative Gain scorer — relevance discounted
682
+ * logarithmically by rank, divided by the best achievable arrangement.
683
+ *
684
+ * This is the one to gate on when comparing retrieval strategies. Unlike recall
685
+ * it is sensitive to order, and unlike MRR it credits every gold passage rather
686
+ * than only the first — so it is the metric that can actually say whether a
687
+ * reranker or a hybrid leg helped.
688
+ *
689
+ * Relevance is binary: an id is gold or it is not, which is what a gold-id set
690
+ * expresses. Graded relevance would need per-id weights.
691
+ */
692
+ declare const ndcgAtK: (k?: number, options?: RetrievalScorerOptions) => Scorer;
693
+ /**
694
+ * Groundedness scorer — does the answer assert only what the retrieved context
695
+ * supports?
696
+ *
697
+ * This is the generation-side counterpart to the metrics above: perfect
698
+ * retrieval still fails if the model answers from its own weights. It scores
699
+ * the output against the context via an injected `judge`, the same shape
700
+ * `llmScorer` takes, so this stays model-agnostic and mockable.
701
+ *
702
+ * Reads the context from `metadata.context` by default — the `context` string
703
+ * `retrieve()` already returns. Fails closed: no context means nothing could
704
+ * have grounded the answer.
705
+ */
706
+ declare const groundednessScorer: (options: {
707
+ contextKey?: string;
708
+ judge: (prompt: string) => Promise<string>;
709
+ name?: string;
710
+ }) => Scorer;
711
+ export { type AgentHarness, type AgentHarnessOptions, type AgentRunOverrides, type EvalCase, type EvalItemResult, type EvalResult, type EvaluationAttributeValue, type EvaluationMetrics, type EvaluationSpanHandle, type FakeScheduledJob, type FakeSchedulerControls, type FunctionRegistry, type HarnessDispatch, type HarnessMessage, type HarnessThread, type LunoraTestOptions, type ProducedOutput, type RecordEvaluationInput, type RetrievalScorerOptions, type ScheduledJobFailure, type ScoreResult, type Scorer, type ScorerSample, type SweepOptions, type TestHarness, type TestIdentity, type TestSubscription, agentHarness, containsScorer, evaluate, evaluationAttributes, exactMatchScorer, finalTurn, groundednessScorer, keywordScorer, llmScorer, lunoraTest, mrrScorer, ndcgAtK, precisionAtK, recallAtK, recordEvaluation, regexScorer, scoreSample, toolCallTurn };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-DA9onGVO.mjs";import{evaluationAttributes as c,recordEvaluation as n}from"./packem_shared/evaluationAttributes-BA1uUP-z.mjs";import{lunoraTest as u}from"./packem_shared/lunoraTest-CC-mCvor.mjs";import{containsScorer as m,evaluate as p,exactMatchScorer as s,keywordScorer as f,llmScorer as S,regexScorer as d,scoreSample as v}from"./packem_shared/containsScorer-DDK7WEuL.mjs";import{extractLink as T,listCapturedMail as g,waitForMail as k}from"@lunora/mail/testing";export{o as agentHarness,m as containsScorer,p as evaluate,c as evaluationAttributes,s as exactMatchScorer,T as extractLink,t as finalTurn,f as keywordScorer,g as listCapturedMail,S as llmScorer,u as lunoraTest,n as recordEvaluation,d as regexScorer,v as scoreSample,a as toolCallTurn,k as waitForMail};
1
+ import{agentHarness as o,finalTurn as t,toolCallTurn as a}from"./packem_shared/agentHarness-DA9onGVO.mjs";import{evaluationAttributes as l,recordEvaluation as n}from"./packem_shared/evaluationAttributes-BA1uUP-z.mjs";import{lunoraTest as s}from"./packem_shared/lunoraTest-CC-mCvor.mjs";import{groundednessScorer as p,mrrScorer as u,ndcgAtK as x,precisionAtK as S,recallAtK as f}from"./packem_shared/groundednessScorer-CznJBIjn.mjs";import{containsScorer as g,evaluate as A,exactMatchScorer as v,keywordScorer as K,llmScorer as M,regexScorer as T,scoreSample as k}from"./packem_shared/containsScorer-gv0Sdd86.mjs";import{extractLink as C,listCapturedMail as b,waitForMail as h}from"@lunora/mail/testing";export{o as agentHarness,g as containsScorer,A as evaluate,l as evaluationAttributes,v as exactMatchScorer,C as extractLink,t as finalTurn,p as groundednessScorer,K as keywordScorer,b as listCapturedMail,M as llmScorer,s as lunoraTest,u as mrrScorer,x as ndcgAtK,S as precisionAtK,f as recallAtK,n as recordEvaluation,T as regexScorer,k as scoreSample,a as toolCallTurn,h as waitForMail};
@@ -0,0 +1,2 @@
1
+ import{LunoraError as g}from"@lunora/errors";const l=/^\s*(-?\d+(?:\.\d+)?)/u,i=e=>Number.isFinite(e)?Math.min(1,Math.max(0,e)):0,h=e=>typeof e=="number"?{score:i(e)}:{score:i(e.score),...e.reason===void 0?{}:{reason:e.reason}},u=e=>e.length===0?0:e.reduce((t,o)=>t+o,0)/e.length,y=(e,t={})=>({name:`contains:${e}`,score:({output:o})=>{const a=t.caseSensitive?o:o.toLowerCase(),r=t.caseSensitive?e:e.toLowerCase();return a.includes(r)?1:0}}),v=(e,t="regex")=>({name:t,score:({output:o})=>e.test(o)?1:0}),$=()=>({name:"exact-match",score:({expected:e,output:t})=>t.trim()===e?.trim()?1:0}),L=e=>{if(e.length===0)throw new g("BAD_REQUEST","@lunora/testing: keywordScorer requires at least one keyword");return{name:"keyword-coverage",score:({output:t})=>{const o=t.toLowerCase(),a=e.filter(r=>o.includes(r.toLowerCase())).length;return{reason:`${String(a)}/${String(e.length)} keywords present`,score:a/e.length}}}},S=(e,t)=>[`Rate the ASSISTANT OUTPUT against this criterion: ${e}`,"Respond with a single number from 0 (fails) to 1 (fully meets), then a dash and a one-line reason.",...t.input===void 0?[]:["",`Input: ${t.input}`],...t.expected===void 0?[]:["",`Reference answer: ${t.expected}`],"",`Assistant output: ${t.output}`].join(`
2
+ `),w=e=>{const t=l.exec(e);return{reason:e.trim(),score:t?i(Number(t[1])):0}},b=e=>({name:e.name??"llm-judge",score:async t=>w(await e.judge(S(e.criteria,t)))}),f=async(e,t)=>{const o=await Promise.all(t.map(async n=>({name:n.name,result:h(await n.score(e))}))),a={},r=new Map;for(const{name:n,result:s}of o){const c=r.get(n)??0;r.set(n,c+1),a[c===0?n:`${n}#${String(c+1)}`]=s}return{average:u(o.map(({result:n})=>n.score)),scores:a}},A=async(e,t,o)=>{const a=await Promise.all(e.map(async r=>{const n=await t(r.input),s=typeof n=="string"?n:n.output,c=typeof n=="string"||n.metadata===void 0?r.metadata:{...r.metadata,...n.metadata},m={input:r.input,output:s,...r.expected===void 0?{}:{expected:r.expected},...c===void 0?{}:{metadata:c}},{average:d,scores:p}=await f(m,o);return{average:d,input:r.input,output:s,scores:p}}));return{average:u(a.map(r=>r.average)),items:a}};export{y as containsScorer,A as evaluate,$ as exactMatchScorer,L as keywordScorer,b as llmScorer,w as parseJudgeScore,v as regexScorer,f as scoreSample};
@@ -0,0 +1,3 @@
1
+ import{LunoraError as g}from"@lunora/errors";import{parseJudgeScore as h}from"./containsScorer-gv0Sdd86.mjs";const w="retrieved",v="relevant",l=(e,t)=>{const n=e.metadata?.[t];return Array.isArray(n)?n.filter(r=>typeof r=="string"&&r.length>0):[]},s=(e,t)=>{const n=l(e,t?.relevantKey??v);if(n.length!==0)return{relevant:new Set(n),retrieved:l(e,t?.retrievedKey??w)}},a=e=>({reason:`no gold ids under metadata.${e?.relevantKey??v} — cannot score retrieval`,score:0}),c=(e,t)=>{if(e!==void 0&&(!Number.isInteger(e)||e<1))throw new g("BAD_REQUEST",`@lunora/testing: ${t} \`k\` must be a positive integer`)},u=(e,t)=>t===void 0?e.retrieved:e.retrieved.slice(0,t),f=(e,t)=>{const n=new Set;for(const r of e)t.has(r)&&n.add(r);return n.size},S=(e,t)=>{const n=new Set;let r=0;for(const[o,i]of e.entries())t.has(i)&&!n.has(i)&&(n.add(i),r+=1/Math.log2(o+2));return r},m=e=>{let t=0;for(let n=0;n<e;n+=1)t+=1/Math.log2(n+2);return t},p=(e,t)=>(c(e,"recallAtK"),{name:e===void 0?"recall":`recall@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=f(u(r,e),r.relevant);return{reason:`${String(o)}/${String(r.relevant.size)} gold ids retrieved`,score:o/r.relevant.size}}}),x=(e,t)=>(c(e,"precisionAtK"),{name:e===void 0?"precision":`precision@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=u(r,e);if(o.length===0)return{reason:"nothing retrieved",score:0};const i=f(o,r.relevant);return{reason:`${String(i)}/${String(o.length)} retrieved ids are gold`,score:i/o.length}}}),y=e=>({name:"mrr",score:t=>{const n=s(t,e);if(n===void 0)return a(e);const r=n.retrieved.findIndex(o=>n.relevant.has(o));return r===-1?{reason:"no gold id retrieved",score:0}:{reason:`first gold id at rank ${String(r+1)}`,score:1/(r+1)}}}),$=(e,t)=>(c(e,"ndcgAtK"),{name:e===void 0?"ndcg":`ndcg@${String(e)}`,score:n=>{const r=s(n,t);if(r===void 0)return a(t);const o=u(r,e);if(o.length===0)return{reason:"nothing retrieved",score:0};const i=S(o,r.relevant),d=m(e===void 0?r.relevant.size:Math.min(r.relevant.size,e));return{reason:`dcg ${i.toFixed(3)} / ideal ${d.toFixed(3)}`,score:d===0?0:i/d}}}),K=e=>{if(typeof e.judge!="function")throw new g("BAD_REQUEST","@lunora/testing: groundednessScorer requires an injected `judge` function");const t=e.contextKey??"context";return{name:e.name??"groundedness",score:async n=>{const r=n.metadata?.[t];if(typeof r!="string"||r.trim().length===0)return{reason:`no retrieved context under metadata.${t}`,score:0};const o=await e.judge(["Rate how well the ASSISTANT ANSWER is supported by the RETRIEVED CONTEXT below.","Score 1 if every claim in the answer is supported by the context, 0 if the answer asserts","anything the context does not support. Judge support only — do NOT reward correctness","the context does not contain.","Respond with a single number from 0 to 1, then a dash and a one-line reason.","",`Retrieved context:
2
+ ${r}`,"",`Assistant answer: ${n.output}`].join(`
3
+ `));return h(o)}}};export{K as groundednessScorer,y as mrrScorer,$ as ndcgAtK,x as precisionAtK,p as recallAtK};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/testing",
3
- "version": "1.0.0-alpha.110",
3
+ "version": "1.0.0-alpha.112",
4
4
  "description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -50,11 +50,11 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/agent": "1.0.0-alpha.56",
53
+ "@lunora/agent": "1.0.0-alpha.58",
54
54
  "@lunora/errors": "1.0.0-alpha.22",
55
55
  "@lunora/mail": "1.0.0-alpha.50",
56
- "@lunora/server": "1.0.0-alpha.75",
57
- "@lunora/shard-engine": "1.0.0-alpha.29"
56
+ "@lunora/server": "1.0.0-alpha.77",
57
+ "@lunora/shard-engine": "1.0.0-alpha.30"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@playwright/test": "^1.61.1"
@@ -1,2 +0,0 @@
1
- import{LunoraError as d}from"@lunora/errors";const g=/^\s*(-?\d+(?:\.\d+)?)/u,i=e=>Number.isFinite(e)?Math.min(1,Math.max(0,e)):0,p=e=>typeof e=="number"?{score:i(e)}:{score:i(e.score),...e.reason===void 0?{}:{reason:e.reason}},u=e=>e.length===0?0:e.reduce((t,n)=>t+n,0)/e.length,x=(e,t={})=>({name:`contains:${e}`,score:({output:n})=>{const a=t.caseSensitive?n:n.toLowerCase(),r=t.caseSensitive?e:e.toLowerCase();return a.includes(r)?1:0}}),f=(e,t="regex")=>({name:t,score:({output:n})=>e.test(n)?1:0}),v=()=>({name:"exact-match",score:({expected:e,output:t})=>t.trim()===e?.trim()?1:0}),y=e=>{if(e.length===0)throw new d("BAD_REQUEST","@lunora/testing: keywordScorer requires at least one keyword");return{name:"keyword-coverage",score:({output:t})=>{const n=t.toLowerCase(),a=e.filter(r=>n.includes(r.toLowerCase())).length;return{reason:`${String(a)}/${String(e.length)} keywords present`,score:a/e.length}}}},l=(e,t)=>[`Rate the ASSISTANT OUTPUT against this criterion: ${e}`,"Respond with a single number from 0 (fails) to 1 (fully meets), then a dash and a one-line reason.",...t.input===void 0?[]:["",`Input: ${t.input}`],...t.expected===void 0?[]:["",`Reference answer: ${t.expected}`],"",`Assistant output: ${t.output}`].join(`
2
- `),h=e=>{const t=g.exec(e);return{reason:e.trim(),score:t?i(Number(t[1])):0}},$=e=>({name:e.name??"llm-judge",score:async t=>h(await e.judge(l(e.criteria,t)))}),S=async(e,t)=>{const n=await Promise.all(t.map(async o=>({name:o.name,result:p(await o.score(e))}))),a={},r=new Map;for(const{name:o,result:s}of n){const c=r.get(o)??0;r.set(o,c+1),a[c===0?o:`${o}#${String(c+1)}`]=s}return{average:u(n.map(({result:o})=>o.score)),scores:a}},L=async(e,t,n)=>{const a=await Promise.all(e.map(async r=>{const o=await t(r.input),s={input:r.input,output:o,...r.expected===void 0?{}:{expected:r.expected},...r.metadata===void 0?{}:{metadata:r.metadata}},{average:c,scores:m}=await S(s,n);return{average:c,input:r.input,output:o,scores:m}}));return{average:u(a.map(r=>r.average)),items:a}};export{x as containsScorer,L as evaluate,v as exactMatchScorer,y as keywordScorer,$ as llmScorer,f as regexScorer,S as scoreSample};