margin-meter 0.2.0 → 0.4.0

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.
@@ -0,0 +1,415 @@
1
+ /**
2
+ * margin-meter/langsmith — turn a LangSmith `evaluate()` result into outcomes.
3
+ *
4
+ * The TypeScript twin of `sdk/python/margin_meter/langsmith.py`. LangSmith is the
5
+ * eval/observability layer of the LangChain ecosystem — one of the largest
6
+ * install bases in production LLM apps, and it ships a first-class JS/TS SDK, so a
7
+ * LangSmith customer is squarely in the TypeScript toolchain. `evaluate()`
8
+ * attaches one or more feedback scores to each example; a customer serializes that
9
+ * result — iterating `ExperimentResults` (each row carries
10
+ * `evaluation_results.results`) or via `result.to_pandas().to_dict(...)` (feedback
11
+ * lands in `feedback.<key>` columns) — and both are a match for
12
+ * `POST /api/ingest/outcomes` (`docs/INTEGRATION_LADDER.md` rung 2):
13
+ *
14
+ * import { MarginMeter, langsmith } from "margin-meter";
15
+ * const meter = new MarginMeter();
16
+ * const data = JSON.parse(await readFile("ls.json", "utf8"));
17
+ * await langsmith.recordResults(meter, data, "my-app", { threshold: 0.7 });
18
+ *
19
+ * The mapping is identical to the Python adapter's, on purpose — one LangSmith
20
+ * row → one outcome:
21
+ *
22
+ * - `qualityScore` ← the mean of the row's numeric feedback scores in [0, 1]
23
+ * (booleans count as 1.0 / 0.0). LangSmith attaches a score per evaluator, not
24
+ * one per row, so they are aggregated — the same rule the DeepEval, Braintrust
25
+ * and Ragas adapters apply. Both serializations are read: the object form
26
+ * (`evaluation_results.results = [{key, score}]`) and the pandas form
27
+ * (`feedback.<key>` columns).
28
+ * - `passed` ← LangSmith has NO single pass/fail verdict, so it is derived
29
+ * HONESTLY, never fabricated: (1) `threshold` given → mean ≥ threshold; (2) no
30
+ * threshold but every feedback is binary (0.0/1.0) → every evaluator passed;
31
+ * (3) no threshold and continuous scores → the row is SKIPPED with a warning.
32
+ * - `qualityMethod`/`graderKind` ← DERIVED from the evaluator KEYS, never assumed:
33
+ * a known LangChain LLM-judge evaluator (`correctness`, `qa`, `criteria`,
34
+ * `score_string`, …) → `llm_judge`/`judge`; a known code evaluator
35
+ * (`string_distance`, `exact_match`, `json_validity`, …) → `ground_truth`/`code`;
36
+ * anything else → `unknown`/`unknown` (the weaker-claim-wins honesty rule).
37
+ * Most LangSmith keys are user-named, so `unknown` is the honest common default.
38
+ * - `judgeModelId` ← an opt-in `margin_judge_model` field on the ROW (LangSmith
39
+ * rows have no metadata envelope), ridden only when the grade is an LLM judge
40
+ * (rung 6 provenance). A bare `model` field is NOT used.
41
+ * - `link` ← the row's `example_id`/`id`/`run_id`, else
42
+ * `langsmith:<index>` — never the input question (MAR-1548).
43
+ * - `valueUsd` ← opt-in `margin_value` on the ROW (rung 8); absent is the
44
+ * cost-per-outcome default, never 0.
45
+ *
46
+ * Same two promises as the other adapters: it consumes output the customer already
47
+ * produces (no new harness), and it never fabricates and is fail-safe.
48
+ */
49
+ // Grading provenance strings — mirror `src/margin/grading.py`, kept as literals.
50
+ const GROUND_TRUTH = "ground_truth";
51
+ const LLM_JUDGE = "llm_judge";
52
+ const UNKNOWN_METHOD = "unknown";
53
+ const GRADER_CODE = "code";
54
+ const GRADER_JUDGE = "judge";
55
+ const GRADER_UNKNOWN = "unknown";
56
+ const MAX_LINK_LEN = 512;
57
+ const MAX_JUDGE_ID_LEN = 128;
58
+ const VALUE_KEY = "margin_value";
59
+ // The explicit opt-in key for the judging model. A bare `model` is NOT used: on a
60
+ // LangSmith row it is the target model under evaluation, not the judge, and
61
+ // labelling it `judgeModelId` would misstate the provenance (rung 6).
62
+ const JUDGE_MODEL_KEY = "margin_judge_model";
63
+ // Mirror `src/margin/ingest.MAX_VALUE_USD`. A value above this is rejected by
64
+ // ingest and would take the whole row down with it, so the adapter drops the
65
+ // value and keeps the row — the same treatment a malformed value gets.
66
+ const MAX_VALUE_USD = 10_000_000.0;
67
+ // Model ids that name no real model. Normalised lower-case for the check.
68
+ const NON_MODELS = new Set(["", "n/a", "na", "none", "null"]);
69
+ // A finite DECIMAL number — the only string shape accepted for a value tag.
70
+ // Deliberately narrower than JS `Number()` (which parses `0x10`/`Infinity`/`""`):
71
+ // this is stricter than Python `float()` in the honest direction — it also
72
+ // rejects `inf`/`nan`, which Python would coerce into a garbage numerator.
73
+ const DECIMAL_RE = /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
74
+ // Feedback columns in the pandas serialization are prefixed with this.
75
+ const FEEDBACK_PREFIX = "feedback.";
76
+ /** LangChain's built-in LLM-as-judge string evaluators, plus common judge keys.
77
+ * Normalised lower-case, non-alphanumerics stripped so `score_string`/`scoreString`
78
+ * collapse together. */
79
+ const MODEL_EVALUATORS = new Set([
80
+ "correctness",
81
+ "qa",
82
+ "cotqa",
83
+ "contextqa",
84
+ "criteria",
85
+ "labeledcriteria",
86
+ "scorestring",
87
+ "labeledscorestring",
88
+ "embeddingdistance",
89
+ "helpfulness",
90
+ "relevance",
91
+ "coherence",
92
+ "conciseness",
93
+ "harmfulness",
94
+ "maliciousness",
95
+ "faithfulness",
96
+ "hallucination",
97
+ ]);
98
+ /** Deterministic, code-graded evaluators (no model in the grading loop). */
99
+ const CODE_EVALUATORS = new Set([
100
+ "stringdistance",
101
+ "exactmatch",
102
+ "regex",
103
+ "jsonvalidity",
104
+ "jsonequality",
105
+ "jsonedit",
106
+ "jsonschema",
107
+ "levenshtein",
108
+ ]);
109
+ const defaultWarn = (message) => {
110
+ const p = globalThis
111
+ .process;
112
+ if (p && typeof p.emitWarning === "function")
113
+ p.emitWarning(message);
114
+ else
115
+ console.warn(message);
116
+ };
117
+ function isObject(x) {
118
+ return typeof x === "object" && x !== null && !Array.isArray(x);
119
+ }
120
+ /** Normalise an evaluator key for set membership: lower-case, keep only
121
+ * alphanumerics (so `string_distance`/`String Distance` collapse), strip a
122
+ * leading `not` negation. Returns undefined for a missing/blank name. */
123
+ export function normKey(raw) {
124
+ if (typeof raw !== "string")
125
+ return undefined;
126
+ let t = raw
127
+ .trim()
128
+ .toLowerCase()
129
+ .replace(/[^a-z0-9]/g, "");
130
+ if (!t)
131
+ return undefined;
132
+ if (t.startsWith("not") && t !== "not")
133
+ t = t.slice(3);
134
+ return t || undefined;
135
+ }
136
+ /** A score coerced to [0, 1]: a bool becomes 1.0/0.0, a number is kept when in
137
+ * range, anything else (or out of range) is undefined (dropped). */
138
+ function numInRange(value) {
139
+ if (typeof value === "boolean")
140
+ return value ? 1.0 : 0.0;
141
+ if (typeof value === "number" && !Number.isNaN(value)) {
142
+ if (value >= 0.0 && value <= 1.0)
143
+ return value;
144
+ }
145
+ return undefined;
146
+ }
147
+ /** The per-evaluator feedback scores of a row, as `{ [key]: score }`.
148
+ *
149
+ * Reads both real serializations: the object form
150
+ * (`evaluation_results.results = [{key, score}]`, or a bare top-level `results`
151
+ * list) and the pandas form (`feedback.<key>` columns). A null/non-numeric/
152
+ * out-of-[0,1] score is dropped so the ingest layer never rejects the row for a
153
+ * bad decimal. */
154
+ export function feedbackScores(row) {
155
+ const out = {};
156
+ if (!isObject(row))
157
+ return out;
158
+ // Object form.
159
+ let results = undefined;
160
+ const er = row.evaluation_results;
161
+ if (isObject(er))
162
+ results = er.results;
163
+ // Python tests `results is None`, which is true both when `evaluation_results`
164
+ // was absent AND when its `results` was explicitly null — so a null there must
165
+ // also fall through to a top-level `results` list, not just an absent one.
166
+ if ((results === undefined || results === null) && Array.isArray(row.results))
167
+ results = row.results;
168
+ if (Array.isArray(results)) {
169
+ for (let i = 0; i < results.length; i++) {
170
+ const item = results[i];
171
+ if (!isObject(item))
172
+ continue;
173
+ const score = numInRange(item.score);
174
+ if (score === undefined)
175
+ continue;
176
+ const key = typeof item.key === "string" && item.key.trim() ? item.key : `eval_${i}`;
177
+ out[key] = score;
178
+ }
179
+ }
180
+ // Pandas form: feedback.<key> columns.
181
+ for (const [name, value] of Object.entries(row)) {
182
+ if (!name.startsWith(FEEDBACK_PREFIX))
183
+ continue;
184
+ const score = numInRange(value);
185
+ if (score === undefined)
186
+ continue;
187
+ out[name.slice(FEEDBACK_PREFIX.length) || name] = score;
188
+ }
189
+ return out;
190
+ }
191
+ /** Derive `(qualityMethod, graderKind)` from the evaluator keys behind a row,
192
+ * weaker-claim-wins:
193
+ * - any model evaluator → llm_judge/judge
194
+ * - only recognised code evaluators → ground_truth/code
195
+ * - anything else → unknown/unknown */
196
+ export function evaluatorSignature(keys) {
197
+ let hasModel = false;
198
+ let hasCode = false;
199
+ let hasUnknown = false;
200
+ for (const raw of keys) {
201
+ const t = normKey(raw);
202
+ if (t === undefined)
203
+ continue;
204
+ if (MODEL_EVALUATORS.has(t))
205
+ hasModel = true;
206
+ else if (CODE_EVALUATORS.has(t))
207
+ hasCode = true;
208
+ else
209
+ hasUnknown = true;
210
+ }
211
+ if (hasModel)
212
+ return { qualityMethod: LLM_JUDGE, graderKind: GRADER_JUDGE };
213
+ if (hasCode && !hasUnknown)
214
+ return { qualityMethod: GROUND_TRUTH, graderKind: GRADER_CODE };
215
+ return { qualityMethod: UNKNOWN_METHOD, graderKind: GRADER_UNKNOWN };
216
+ }
217
+ /** The judging model, from an explicit `margin_judge_model` opt-in on the row, or
218
+ * undefined. A bare `model` field is deliberately NOT used. */
219
+ export function judgeModelFromRow(row) {
220
+ if (!isObject(row))
221
+ return undefined;
222
+ const raw = row[JUDGE_MODEL_KEY];
223
+ if (typeof raw !== "string")
224
+ return undefined;
225
+ if (NON_MODELS.has(raw.trim().toLowerCase()))
226
+ return undefined;
227
+ return raw.trim().slice(0, MAX_JUDGE_ID_LEN);
228
+ }
229
+ function coerceValue(raw) {
230
+ if (typeof raw === "boolean")
231
+ throw new Error(`${VALUE_KEY} must be a number, got ${JSON.stringify(raw)}`);
232
+ let val;
233
+ if (typeof raw === "number") {
234
+ val = raw;
235
+ }
236
+ else if (typeof raw === "string") {
237
+ const s = raw.trim();
238
+ if (!DECIMAL_RE.test(s))
239
+ throw new Error(`${VALUE_KEY} must be a number, got ${JSON.stringify(raw)}`);
240
+ val = Number(s);
241
+ }
242
+ else {
243
+ throw new Error(`${VALUE_KEY} must be a number, got ${JSON.stringify(raw)}`);
244
+ }
245
+ if (!Number.isFinite(val))
246
+ throw new Error(`${VALUE_KEY} must be a finite number, got ${JSON.stringify(raw)}`);
247
+ // A loss is a real value (MAR-1198): ingest takes any finite amount within +/-MAX.
248
+ if (Math.abs(val) > MAX_VALUE_USD)
249
+ throw new Error(`${VALUE_KEY} implausible (|value| > ${MAX_VALUE_USD}), got ${val}`);
250
+ return val;
251
+ }
252
+ /** Read an opt-in `margin_value` off a row — the value-per-outcome numerator
253
+ * (rung 8). Returns the USD amount, or undefined when absent. Throws on a
254
+ * malformed/implausible value (a present-but-null value included) so the
255
+ * caller can warn and drop it, matching the Python twin. */
256
+ export function valueFromRow(row) {
257
+ if (!isObject(row))
258
+ return undefined;
259
+ if (!(VALUE_KEY in row))
260
+ return undefined;
261
+ return coerceValue(row[VALUE_KEY]);
262
+ }
263
+ /** A traceable link back to the example: its id, else the row index.
264
+ * ⛔ Never the input question (MAR-1548): /security says no prompt reaches us. */
265
+ export function linkFromRow(row, index) {
266
+ if (isObject(row)) {
267
+ for (const key of ["example_id", "id", "run_id"]) {
268
+ const val = row[key];
269
+ if (typeof val === "string" && val.trim())
270
+ return val.trim().slice(0, MAX_LINK_LEN);
271
+ }
272
+ }
273
+ return `langsmith:${index}`;
274
+ }
275
+ /** Derive the pass/fail for a row, or undefined when it cannot be derived
276
+ * honestly (no scores, or continuous scores with no threshold). */
277
+ export function passedFromScores(scores, threshold) {
278
+ const values = Object.values(scores);
279
+ if (values.length === 0)
280
+ return undefined;
281
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
282
+ if (threshold !== undefined)
283
+ return mean >= threshold;
284
+ if (values.every((v) => v === 0.0 || v === 1.0))
285
+ return values.every((v) => v === 1.0);
286
+ return undefined;
287
+ }
288
+ /** Translate one graded LangSmith row into `MarginMeter.recordOutcome` input.
289
+ * `judgeModelId` rides only when the grade is an LLM judge and a model was named;
290
+ * `valueUsd` rides only when a value was supplied. */
291
+ export function outcomeInput(args) {
292
+ const { qualityMethod, graderKind } = evaluatorSignature(args.evaluatorKeys);
293
+ const input = {
294
+ workflowId: args.workflowId,
295
+ passed: Boolean(args.passed),
296
+ qualityScore: Number(args.qualityScore),
297
+ qualityMethod,
298
+ graderKind,
299
+ link: String(args.link).slice(0, MAX_LINK_LEN),
300
+ isSimulated: Boolean(args.isSimulated),
301
+ };
302
+ if (qualityMethod === LLM_JUDGE && args.judgeModelId)
303
+ input.judgeModelId = args.judgeModelId.slice(0, MAX_JUDGE_ID_LEN);
304
+ if (args.valueUsd !== undefined)
305
+ input.valueUsd = Number(args.valueUsd);
306
+ return input;
307
+ }
308
+ /** Full translation of one LangSmith row, or undefined if it is not a real
309
+ * verdict (no feedback score, or an ambiguous continuous score with no
310
+ * threshold). A malformed `margin_value` warns and drops the value while keeping
311
+ * the pass/fail. */
312
+ export function rowToOutcomeInput(row, opts) {
313
+ const onWarn = opts.onWarn ?? defaultWarn;
314
+ const index = opts.index ?? 0;
315
+ const scores = feedbackScores(row);
316
+ const passed = passedFromScores(scores, opts.threshold);
317
+ if (passed === undefined) {
318
+ if (Object.keys(scores).length > 0) {
319
+ onWarn(`${linkFromRow(row, index)}: continuous scores and no threshold; ` +
320
+ "skipped (no verdict to record)");
321
+ }
322
+ return undefined;
323
+ }
324
+ const values = Object.values(scores);
325
+ const quality = values.reduce((a, b) => a + b, 0) / values.length;
326
+ let value;
327
+ try {
328
+ value = valueFromRow(row);
329
+ }
330
+ catch (exc) {
331
+ value = undefined;
332
+ onWarn(`${linkFromRow(row, index)}: ${exc.message}`);
333
+ }
334
+ return outcomeInput({
335
+ workflowId: opts.workflowId,
336
+ passed,
337
+ qualityScore: quality,
338
+ evaluatorKeys: Object.keys(scores),
339
+ link: linkFromRow(row, index),
340
+ judgeModelId: judgeModelFromRow(row),
341
+ valueUsd: value,
342
+ isSimulated: opts.isSimulated,
343
+ });
344
+ }
345
+ /** Pull the flat list of per-example rows out of parsed LangSmith output — a bare
346
+ * list, or a `{results|rows|records: [...]}` envelope. */
347
+ export function extractRows(data) {
348
+ if (Array.isArray(data))
349
+ return data.filter(isObject);
350
+ if (!isObject(data))
351
+ return [];
352
+ for (const key of ["results", "rows", "records"]) {
353
+ const block = data[key];
354
+ if (Array.isArray(block))
355
+ return block.filter(isObject);
356
+ }
357
+ return [];
358
+ }
359
+ /** Yield `recordOutcome` inputs for every real verdict in parsed LangSmith output
360
+ * (any envelope shape {@link extractRows} understands). */
361
+ export function iterOutcomes(data, opts) {
362
+ const out = [];
363
+ const rows = extractRows(data);
364
+ for (let i = 0; i < rows.length; i++) {
365
+ const input = rowToOutcomeInput(rows[i], {
366
+ workflowId: opts.workflowId,
367
+ index: i,
368
+ threshold: opts.threshold,
369
+ isSimulated: opts.isSimulated,
370
+ onWarn: opts.onWarn,
371
+ });
372
+ if (input !== undefined)
373
+ out.push(input);
374
+ }
375
+ return out;
376
+ }
377
+ /** Emit an outcome per verdict in parsed LangSmith output through `meter`.
378
+ * Fail-safe: a delivery fault on one row warns and counts an error; it never
379
+ * aborts the batch. Non-verdict rows are counted as skipped. */
380
+ export async function recordResults(meter, data, workflowId, opts = {}) {
381
+ const onWarn = opts.onWarn ?? defaultWarn;
382
+ const rows = extractRows(data);
383
+ let recorded = 0;
384
+ let errors = 0;
385
+ let skipped = 0;
386
+ for (let i = 0; i < rows.length; i++) {
387
+ const input = rowToOutcomeInput(rows[i], {
388
+ workflowId,
389
+ index: i,
390
+ threshold: opts.threshold,
391
+ isSimulated: opts.isSimulated,
392
+ onWarn,
393
+ });
394
+ if (input === undefined) {
395
+ skipped++;
396
+ continue;
397
+ }
398
+ try {
399
+ await meter.recordOutcome(input);
400
+ recorded++;
401
+ }
402
+ catch (exc) {
403
+ errors++;
404
+ onWarn(`failed to record outcome for ${input.link}: ${exc.message}`);
405
+ }
406
+ }
407
+ try {
408
+ await meter.flush();
409
+ }
410
+ catch (exc) {
411
+ onWarn(`flush failed: ${exc.message}`);
412
+ }
413
+ return { recorded, errors, skipped };
414
+ }
415
+ //# sourceMappingURL=langsmith.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"langsmith.js","sourceRoot":"","sources":["../src/langsmith.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * margin-meter/promptfoo — turn a `promptfoo eval` result file into outcomes.
3
+ *
4
+ * The TypeScript twin of `sdk/python/margin_meter/promptfoo.py`. promptfoo is a
5
+ * Node tool, so a promptfoo user is already in the JS toolchain and this SDK is
6
+ * their natural bridge. `promptfoo eval -o results.json` writes a code-graded
7
+ * pass/fail file that maps one-to-one onto `POST /api/ingest/outcomes`
8
+ * (`docs/INTEGRATION_LADDER.md` rung 2), so the whole of rung 2 — "their evals
9
+ * report pass/fail" — is available with no hand-written POST:
10
+ *
11
+ * promptfoo eval -o results.json
12
+ *
13
+ * import { MarginMeter, promptfoo } from "margin-meter";
14
+ * const meter = new MarginMeter(); // MARGIN_INGEST_URL/KEY from env
15
+ * const data = JSON.parse(await readFile("results.json", "utf8"));
16
+ * await promptfoo.recordResults(meter, data, "my-eval-suite");
17
+ *
18
+ * The mapping is identical to the Python adapter's, on purpose — the two SDKs
19
+ * must not drift:
20
+ *
21
+ * - `passed` ← `success` (the top-level aggregate verdict).
22
+ * - `qualityScore` ← `score` passed through when in [0, 1]; a score outside
23
+ * that range (a raw metric from a custom assert) falls back to the coherent
24
+ * binary, because the ingest layer rejects an out-of-range score and losing
25
+ * the row is worse than losing the decimal. Always supplied (1.0/0.0 at
26
+ * least): the ingest layer discards `quality_method`/`grader_kind` on a
27
+ * scoreless outcome, so the score is what lights the grading provenance.
28
+ * - `qualityMethod`/`graderKind` ← DERIVED from the assert types, never
29
+ * assumed (see the honesty rule below).
30
+ * - `link` ← `testCase.description`, else the prompt id + index — never
31
+ * `prompt.label`, which promptfoo defaults to the prompt text (MAR-1548).
32
+ * - `valueUsd` ← opt-in via `vars.margin_value` or
33
+ * `testCase.metadata.margin_value` — the only route to value-per-outcome
34
+ * (rung 8). Absent is the cost-per-outcome default, never coerced to 0.
35
+ * - a result with no boolean `success` is not a verdict and is SKIPPED —
36
+ * recording a non-verdict would fabricate an outcome the eval did not
37
+ * produce.
38
+ *
39
+ * ## The honesty rule
40
+ *
41
+ * promptfoo grades two different ways and the adapter must not conflate them:
42
+ * code-graded asserts (`contains`, `equals`, `is-json`, `javascript`, `regex`,
43
+ * …) are `ground_truth`/`code`; model-graded asserts (`llm-rubric`,
44
+ * `factuality`, `similar`, `classifier`, `g-eval`, …) are `llm_judge`/`judge`.
45
+ * The WEAKER honest claim wins: any model-graded assert makes the whole outcome
46
+ * a judge grade; a code assert mixed with an assert type we do not recognise is
47
+ * downgraded to `unknown` (we cannot prove the unknown one was code); nothing
48
+ * recognisable at all is `unknown`. A rubric-graded eval reported as a
49
+ * ground-truth code grade would be the exact productivity illusion Margin
50
+ * exists to puncture.
51
+ *
52
+ * Two design promises, same as the Python adapter: it consumes the file the
53
+ * customer already produces (no new harness), and it never fabricates and is
54
+ * fail-safe — a delivery fault on one row warns and is counted, never aborting
55
+ * the batch.
56
+ */
57
+ import type { RecordOutcomeInput } from "./index.ts";
58
+ /** Where a warning goes. Defaults to `process.emitWarning` so a delivery or
59
+ * tagging problem is visible without crashing the host; a test injects a
60
+ * collector. */
61
+ export type WarnFn = (message: string) => void;
62
+ /** The graded verdict behind one promptfoo result. */
63
+ export interface Verdict {
64
+ passed: boolean;
65
+ score: number;
66
+ }
67
+ /** Grading provenance for one outcome. */
68
+ export interface GraderSignature {
69
+ qualityMethod: string;
70
+ graderKind: string;
71
+ }
72
+ /** Normalise a promptfoo assert type for set membership: lower-case, drop
73
+ * dashes/underscores/spaces, strip a leading `not` negation (`not-contains`
74
+ * grades the same WAY as `contains`). Returns undefined for a missing/blank
75
+ * type. */
76
+ export declare function normAssertType(raw: unknown): string | undefined;
77
+ /** Derive `(qualityMethod, graderKind)` from the assert types behind a result,
78
+ * applying the weaker-claim-wins honesty rule:
79
+ * - any model-graded assert → llm_judge/judge
80
+ * - only recognised code asserts → ground_truth/code
81
+ * - anything else (no asserts, or a code assert mixed with an unrecognised
82
+ * type we cannot vouch for) → unknown/unknown */
83
+ export declare function gradingSignature(assertTypes: Iterable<unknown>): GraderSignature;
84
+ /** Collect the assert types behind one promptfoo result. Prefers
85
+ * `gradingResult.componentResults[].assertion.type` (what actually ran,
86
+ * including expanded asserts) and its single top-level `assertion`; falls back
87
+ * to the configured `testCase.assert[].type`. */
88
+ export declare function assertTypesFromResult(result: unknown): unknown[];
89
+ /** Read an opt-in `margin_value` off a result's `vars` or its test case's
90
+ * `metadata`/`vars` — the value-per-outcome numerator (rung 8). Returns the USD
91
+ * amount, or undefined when absent (the normal cost-per-outcome case). Throws
92
+ * on a malformed value so the caller can warn and drop it. */
93
+ export declare function valueFromResult(result: unknown): number | undefined;
94
+ /** A traceable link back to the exact eval case: the test description if the
95
+ * author wrote one, else the prompt id plus the row index. ⛔ Never
96
+ * `prompt.label` (MAR-1548): promptfoo defaults it to the raw prompt text. */
97
+ export declare function linkFromResult(result: unknown, index: number): string;
98
+ /** Return the `{ passed, score }` verdict for a real result, else undefined.
99
+ * `passed` comes from a boolean `success`; a result without one is not a
100
+ * verdict. `score` is passed through when in [0, 1]; out of that range (a raw
101
+ * metric) falls back to the coherent binary and warns, because the ingest layer
102
+ * rejects an out-of-range score and a lost row is worse than a lost decimal. */
103
+ export declare function passedAndScore(result: unknown, onWarn?: WarnFn): Verdict | undefined;
104
+ /** Fields for {@link outcomeInput}. */
105
+ export interface OutcomeInputArgs {
106
+ workflowId: string;
107
+ passed: boolean;
108
+ qualityScore: number;
109
+ assertTypes: Iterable<unknown>;
110
+ link: string;
111
+ valueUsd?: number;
112
+ isSimulated?: boolean;
113
+ }
114
+ /** Translate one graded promptfoo result into `MarginMeter.recordOutcome` input.
115
+ * The grader signature is derived from `assertTypes` (never assumed);
116
+ * `valueUsd` rides only when a value was supplied. */
117
+ export declare function outcomeInput(args: OutcomeInputArgs): RecordOutcomeInput;
118
+ /** Options shared by the result-level translators. */
119
+ export interface TranslateOptions {
120
+ workflowId: string;
121
+ index?: number;
122
+ isSimulated?: boolean;
123
+ onWarn?: WarnFn;
124
+ }
125
+ /** Full translation of one promptfoo result, or undefined if it is not a real
126
+ * verdict. A malformed `margin_value` warns and drops the value (keeping the
127
+ * pass/fail), never throwing into the caller. */
128
+ export declare function resultToOutcomeInput(result: unknown, opts: TranslateOptions): RecordOutcomeInput | undefined;
129
+ /** Pull the flat list of per-test results out of a parsed promptfoo output.
130
+ * promptfoo has shipped several envelope shapes (the top-level OutputFile with
131
+ * `results.results`, an older `results` list, a bare list); this walks the
132
+ * known shapes rather than pinning one version. */
133
+ export declare function extractResults(data: unknown): Record<string, unknown>[];
134
+ /** Yield `recordOutcome` inputs for every real verdict in a parsed promptfoo
135
+ * output (any envelope shape {@link extractResults} understands). */
136
+ export declare function iterOutcomes(data: unknown, opts: {
137
+ workflowId: string;
138
+ isSimulated?: boolean;
139
+ onWarn?: WarnFn;
140
+ }): RecordOutcomeInput[];
141
+ /** The minimal meter surface {@link recordResults} needs — satisfied by
142
+ * `MarginMeter`, kept as a structural type so this module imports no runtime
143
+ * value from `index.ts`. */
144
+ export interface OutcomeSink {
145
+ recordOutcome(input: RecordOutcomeInput): Promise<unknown>;
146
+ flush(): Promise<void>;
147
+ }
148
+ /** How one `recordResults` batch landed. */
149
+ export interface RecordSummary {
150
+ recorded: number;
151
+ errors: number;
152
+ skipped: number;
153
+ }
154
+ /** Emit an outcome per verdict in a parsed promptfoo output through `meter`.
155
+ * Fail-safe: a delivery fault on one row warns and counts an error; it never
156
+ * aborts the batch. Non-verdict rows are counted as skipped. */
157
+ export declare function recordResults(meter: OutcomeSink, data: unknown, workflowId: string, opts?: {
158
+ isSimulated?: boolean;
159
+ onWarn?: WarnFn;
160
+ }): Promise<RecordSummary>;
161
+ //# sourceMappingURL=promptfoo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promptfoo.d.ts","sourceRoot":"","sources":["../src/promptfoo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA8ErD;;iBAEiB;AACjB,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAU/C,sDAAsD;AACtD,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAMD;;;YAGY;AACZ,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAM/D;AAED;;;;;qDAKqD;AACrD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,eAAe,CAehF;AAED;;;kDAGkD;AAClD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,EAAE,CAuBhE;AAmCD;;;+DAG+D;AAC/D,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAmBnE;AAED;;+EAE+E;AAC/E,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAoBrE;AAED;;;;iFAIiF;AACjF,wBAAgB,cAAc,CAC5B,MAAM,EAAE,OAAO,EACf,MAAM,GAAE,MAAoB,GAC3B,OAAO,GAAG,SAAS,CAerB;AAED,uCAAuC;AACvC,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;uDAEuD;AACvD,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,kBAAkB,CAavE;AAED,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;kDAEkD;AAClD,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,gBAAgB,GACrB,kBAAkB,GAAG,SAAS,CAqBhC;AAED;;;oDAGoD;AACpD,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAWvE;AAED;sEACsE;AACtE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE,kBAAkB,EAAE,CAatB;AAED;;6BAE6B;AAC7B,MAAM,WAAW,WAAW;IAC1B,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;iEAEiE;AACjE,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,OAAO,CAAC,aAAa,CAAC,CAiCxB"}