margin-meter 0.1.0 → 0.3.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.
- package/README.md +353 -8
- package/dist/batching.d.ts +94 -0
- package/dist/batching.d.ts.map +1 -0
- package/dist/batching.js +181 -0
- package/dist/batching.js.map +1 -0
- package/dist/braintrust.d.ts +153 -0
- package/dist/braintrust.d.ts.map +1 -0
- package/dist/braintrust.js +424 -0
- package/dist/braintrust.js.map +1 -0
- package/dist/index.d.ts +428 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +927 -9
- package/dist/index.js.map +1 -1
- package/dist/instrument.d.ts +74 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +153 -0
- package/dist/instrument.js.map +1 -0
- package/dist/langsmith.d.ts +150 -0
- package/dist/langsmith.d.ts.map +1 -0
- package/dist/langsmith.js +415 -0
- package/dist/langsmith.js.map +1 -0
- package/dist/promptfoo.d.ts +161 -0
- package/dist/promptfoo.d.ts.map +1 -0
- package/dist/promptfoo.js +446 -0
- package/dist/promptfoo.js.map +1 -0
- package/dist/providers.d.ts +190 -0
- package/dist/providers.d.ts.map +1 -0
- package/dist/providers.js +611 -0
- package/dist/providers.js.map +1 -0
- package/dist/substrate.d.ts +51 -0
- package/dist/substrate.d.ts.map +1 -0
- package/dist/substrate.js +132 -0
- package/dist/substrate.js.map +1 -0
- package/dist/vitest.d.ts +232 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +502 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +25 -7
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SUBSTRATE a call travelled through to reach a model — the TypeScript twin
|
|
3
|
+
* of `margin_meter/substrate.py`.
|
|
4
|
+
*
|
|
5
|
+
* `provider` says who owns the model and who bills for it. `substrate` says which
|
|
6
|
+
* path the request actually took: the vendor's own API, a router like OpenRouter,
|
|
7
|
+
* a cloud reseller like Bedrock or Azure. The two are orthogonal, and comparing
|
|
8
|
+
* the same work across paths is the thing a single-substrate router structurally
|
|
9
|
+
* cannot do.
|
|
10
|
+
*
|
|
11
|
+
* Detection is EVIDENCE, not inference. A JS client carries the endpoint it will
|
|
12
|
+
* call (`baseURL` on the OpenAI and Anthropic SDKs), so an OpenAI client pointed
|
|
13
|
+
* at `openrouter.ai` is recorded as OpenRouter because that is literally where
|
|
14
|
+
* the request goes. Nothing is defaulted: a client whose endpoint cannot be read
|
|
15
|
+
* records NO path, which is the honest reading of an unmeasured dimension.
|
|
16
|
+
*
|
|
17
|
+
* Keep the host tables in step with the Python SDK and `margin/substrates.py`.
|
|
18
|
+
* Three copies of one rule is a real cost, and the alternative — a JS estate that
|
|
19
|
+
* cannot join the comparison at all — costs more.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DIRECT = "direct";
|
|
22
|
+
export declare const OPENROUTER = "openrouter";
|
|
23
|
+
export declare const LITELLM = "litellm";
|
|
24
|
+
export declare const AZURE = "azure";
|
|
25
|
+
export declare const BEDROCK = "bedrock";
|
|
26
|
+
export declare const VERTEX = "vertex";
|
|
27
|
+
export declare const GATEWAY = "gateway";
|
|
28
|
+
export type Substrate = "direct" | "openrouter" | "litellm" | "azure" | "bedrock" | "vertex" | "gateway";
|
|
29
|
+
/**
|
|
30
|
+
* Map a client's base URL to a substrate, or `undefined` if unreadable.
|
|
31
|
+
*
|
|
32
|
+
* Accepts anything stringable (the OpenAI SDK carries a string, others a `URL`).
|
|
33
|
+
* An unparseable value returns `undefined` rather than a default — no evidence,
|
|
34
|
+
* no claim. A host we cannot name reads `gateway`, which says the path was not
|
|
35
|
+
* the vendor's own without inventing a vendor for it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function fromBaseUrl(baseUrl: unknown): Substrate | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Detect the substrate from a live SDK client or one of its resources.
|
|
40
|
+
*
|
|
41
|
+
* The JS instrumenters patch a method on a specific client INSTANCE, so the
|
|
42
|
+
* endpoint is in hand at patch time — a process holding a vendor client and a
|
|
43
|
+
* router client instruments each separately and each reports correctly. Both
|
|
44
|
+
* `baseURL` (the JS SDKs' spelling) and `base_url` are read, and the resource's
|
|
45
|
+
* private `_client` is tried before the object itself so a patched
|
|
46
|
+
* `chat.completions` resolves as well as a bare client. Every lookup is
|
|
47
|
+
* defensive: a shape change in a vendor SDK must degrade to "no substrate
|
|
48
|
+
* recorded", never break the host's call.
|
|
49
|
+
*/
|
|
50
|
+
export declare function fromClient(obj: unknown): Substrate | undefined;
|
|
51
|
+
//# sourceMappingURL=substrate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"substrate.d.ts","sourceRoot":"","sources":["../src/substrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,UAAU,eAAe,CAAC;AACvC,eAAO,MAAM,OAAO,YAAY,CAAC;AACjC,eAAO,MAAM,KAAK,UAAU,CAAC;AAC7B,eAAO,MAAM,OAAO,YAAY,CAAC;AACjC,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,OAAO,YAAY,CAAC;AAEjC,MAAM,MAAM,SAAS,GACjB,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,OAAO,GACP,SAAS,GACT,QAAQ,GACR,SAAS,CAAC;AAoCd;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAuBnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAgB9D"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SUBSTRATE a call travelled through to reach a model — the TypeScript twin
|
|
3
|
+
* of `margin_meter/substrate.py`.
|
|
4
|
+
*
|
|
5
|
+
* `provider` says who owns the model and who bills for it. `substrate` says which
|
|
6
|
+
* path the request actually took: the vendor's own API, a router like OpenRouter,
|
|
7
|
+
* a cloud reseller like Bedrock or Azure. The two are orthogonal, and comparing
|
|
8
|
+
* the same work across paths is the thing a single-substrate router structurally
|
|
9
|
+
* cannot do.
|
|
10
|
+
*
|
|
11
|
+
* Detection is EVIDENCE, not inference. A JS client carries the endpoint it will
|
|
12
|
+
* call (`baseURL` on the OpenAI and Anthropic SDKs), so an OpenAI client pointed
|
|
13
|
+
* at `openrouter.ai` is recorded as OpenRouter because that is literally where
|
|
14
|
+
* the request goes. Nothing is defaulted: a client whose endpoint cannot be read
|
|
15
|
+
* records NO path, which is the honest reading of an unmeasured dimension.
|
|
16
|
+
*
|
|
17
|
+
* Keep the host tables in step with the Python SDK and `margin/substrates.py`.
|
|
18
|
+
* Three copies of one rule is a real cost, and the alternative — a JS estate that
|
|
19
|
+
* cannot join the comparison at all — costs more.
|
|
20
|
+
*/
|
|
21
|
+
export const DIRECT = "direct";
|
|
22
|
+
export const OPENROUTER = "openrouter";
|
|
23
|
+
export const LITELLM = "litellm";
|
|
24
|
+
export const AZURE = "azure";
|
|
25
|
+
export const BEDROCK = "bedrock";
|
|
26
|
+
export const VERTEX = "vertex";
|
|
27
|
+
export const GATEWAY = "gateway";
|
|
28
|
+
/** The vendors' own API hosts. A base URL on one of these is the direct path. */
|
|
29
|
+
const DIRECT_HOSTS = [
|
|
30
|
+
"api.openai.com",
|
|
31
|
+
"api.anthropic.com",
|
|
32
|
+
"generativelanguage.googleapis.com",
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Host patterns that name a substrate outright, most specific first. The third
|
|
36
|
+
* element allows a REGIONAL PREFIX joined by a hyphen — Vertex serves from
|
|
37
|
+
* `us-central1-aiplatform.googleapis.com`, a sibling label rather than a
|
|
38
|
+
* subdomain. Granted per entry, and only where the vendor controls the parent
|
|
39
|
+
* domain end to end.
|
|
40
|
+
*/
|
|
41
|
+
const HOST_SUBSTRATES = [
|
|
42
|
+
["openrouter.ai", OPENROUTER, false],
|
|
43
|
+
["openai.azure.com", AZURE, false],
|
|
44
|
+
["azure.com", AZURE, false],
|
|
45
|
+
["amazonaws.com", BEDROCK, false],
|
|
46
|
+
["aiplatform.googleapis.com", VERTEX, true],
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* True when `host` IS `domain` or a subdomain of it — never a substring.
|
|
50
|
+
* `api.openrouter.ai` matches `openrouter.ai`; `openrouter.ai.evil.test` does
|
|
51
|
+
* not. Substring matching would let any host anyone can register be recorded as
|
|
52
|
+
* a trusted substrate, corrupting the exact comparison this dimension exists to
|
|
53
|
+
* make.
|
|
54
|
+
*/
|
|
55
|
+
function hostMatches(host, domain, regionalPrefix) {
|
|
56
|
+
if (host === domain || host.endsWith("." + domain))
|
|
57
|
+
return true;
|
|
58
|
+
return regionalPrefix && host.endsWith("-" + domain);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Map a client's base URL to a substrate, or `undefined` if unreadable.
|
|
62
|
+
*
|
|
63
|
+
* Accepts anything stringable (the OpenAI SDK carries a string, others a `URL`).
|
|
64
|
+
* An unparseable value returns `undefined` rather than a default — no evidence,
|
|
65
|
+
* no claim. A host we cannot name reads `gateway`, which says the path was not
|
|
66
|
+
* the vendor's own without inventing a vendor for it.
|
|
67
|
+
*/
|
|
68
|
+
export function fromBaseUrl(baseUrl) {
|
|
69
|
+
if (baseUrl == null)
|
|
70
|
+
return undefined;
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = String(baseUrl).trim();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
if (!raw)
|
|
79
|
+
return undefined;
|
|
80
|
+
let host;
|
|
81
|
+
try {
|
|
82
|
+
host = new URL(raw).hostname.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (!host)
|
|
88
|
+
return undefined;
|
|
89
|
+
for (const [domain, name, regionalPrefix] of HOST_SUBSTRATES) {
|
|
90
|
+
if (hostMatches(host, domain, regionalPrefix))
|
|
91
|
+
return name;
|
|
92
|
+
}
|
|
93
|
+
for (const domain of DIRECT_HOSTS) {
|
|
94
|
+
if (hostMatches(host, domain, false))
|
|
95
|
+
return DIRECT;
|
|
96
|
+
}
|
|
97
|
+
return GATEWAY;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Detect the substrate from a live SDK client or one of its resources.
|
|
101
|
+
*
|
|
102
|
+
* The JS instrumenters patch a method on a specific client INSTANCE, so the
|
|
103
|
+
* endpoint is in hand at patch time — a process holding a vendor client and a
|
|
104
|
+
* router client instruments each separately and each reports correctly. Both
|
|
105
|
+
* `baseURL` (the JS SDKs' spelling) and `base_url` are read, and the resource's
|
|
106
|
+
* private `_client` is tried before the object itself so a patched
|
|
107
|
+
* `chat.completions` resolves as well as a bare client. Every lookup is
|
|
108
|
+
* defensive: a shape change in a vendor SDK must degrade to "no substrate
|
|
109
|
+
* recorded", never break the host's call.
|
|
110
|
+
*/
|
|
111
|
+
export function fromClient(obj) {
|
|
112
|
+
if (obj == null)
|
|
113
|
+
return undefined;
|
|
114
|
+
const o = obj;
|
|
115
|
+
for (const candidate of [o._client, o.client, o]) {
|
|
116
|
+
if (candidate == null)
|
|
117
|
+
continue;
|
|
118
|
+
let base;
|
|
119
|
+
try {
|
|
120
|
+
const c = candidate;
|
|
121
|
+
base = c.baseURL ?? c.base_url;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const found = fromBaseUrl(base);
|
|
127
|
+
if (found !== undefined)
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=substrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"substrate.js","sourceRoot":"","sources":["../src/substrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG,QAAQ,CAAC;AAC/B,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAC;AACvC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AACjC,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAC;AAC7B,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AACjC,MAAM,CAAC,MAAM,MAAM,GAAG,QAAQ,CAAC;AAC/B,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AAWjC,iFAAiF;AACjF,MAAM,YAAY,GAAG;IACnB,gBAAgB;IAChB,mBAAmB;IACnB,mCAAmC;CACpC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,eAAe,GAAwC;IAC3D,CAAC,eAAe,EAAE,UAAU,EAAE,KAAK,CAAC;IACpC,CAAC,kBAAkB,EAAE,KAAK,EAAE,KAAK,CAAC;IAClC,CAAC,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC;IAC3B,CAAC,eAAe,EAAE,OAAO,EAAE,KAAK,CAAC;IACjC,CAAC,2BAA2B,EAAE,MAAM,EAAE,IAAI,CAAC;CAC5C,CAAC;AAEF;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,IAAY,EAAE,MAAc,EAAE,cAAuB;IACxE,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,OAAO,cAAc,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC;AACvD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,OAAgB;IAC1C,IAAI,OAAO,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACtC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,cAAc,CAAC,IAAI,eAAe,EAAE,CAAC;QAC7D,IAAI,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,CAAC;YAAE,OAAO,IAAI,CAAC;IAC7D,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,YAAY,EAAE,CAAC;QAClC,IAAI,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,MAAM,CAAC;IACtD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,GAAY;IACrC,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IAClC,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,KAAK,MAAM,SAAS,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC;QACjD,IAAI,SAAS,IAAI,IAAI;YAAE,SAAS;QAChC,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,SAAoC,CAAC;YAC/C,IAAI,GAAG,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;IACxC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/dist/vitest.d.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* margin-meter/vitest — turn a vitest/jest JSON result file into outcomes.
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript test-runner adapter, the analog of the Python
|
|
5
|
+
* `margin_meter.pytest_plugin`. A unit-test suite is already an eval harness:
|
|
6
|
+
* each test is a pass/fail verdict graded by code assertions against a
|
|
7
|
+
* known-correct expectation — a *ground-truth*, *code* grade, which is exactly
|
|
8
|
+
* the honest denominator Margin's outcome intake wants
|
|
9
|
+
* (`docs/INTEGRATION_LADDER.md` rung 2, and rung 6's grading provenance). So the
|
|
10
|
+
* whole of rung 2 — "their evals report pass/fail" — is available for the most
|
|
11
|
+
* common JS/TS grading path with no hand-written `POST`.
|
|
12
|
+
*
|
|
13
|
+
* ## One adapter, two frameworks
|
|
14
|
+
*
|
|
15
|
+
* This is a JSON-results CONSUMER, not a runtime reporter, because that reads a
|
|
16
|
+
* file the customer already produces and drags in no test-framework dependency —
|
|
17
|
+
* the same design as the promptfoo/braintrust/langsmith adapters. Both dominant
|
|
18
|
+
* JS test runners emit the SAME jest-compatible JSON, so one adapter covers
|
|
19
|
+
* both:
|
|
20
|
+
*
|
|
21
|
+
* vitest run --reporter=json --outputFile=results.json # vitest
|
|
22
|
+
* jest --json --outputFile=results.json # jest
|
|
23
|
+
*
|
|
24
|
+
* import { MarginMeter, vitest } from "margin-meter";
|
|
25
|
+
* const meter = new MarginMeter(); // MARGIN_INGEST_URL/KEY from env
|
|
26
|
+
* const data = JSON.parse(await readFile("results.json", "utf8"));
|
|
27
|
+
* await vitest.recordResults(meter, data, "my-test-suite");
|
|
28
|
+
*
|
|
29
|
+
* The mapping mirrors the pytest plugin's, on purpose — the SDKs must not drift:
|
|
30
|
+
*
|
|
31
|
+
* - `passed` ← the assertion `status`: `passed` → true, `failed` →
|
|
32
|
+
* false. Any other status (`skipped`/`todo`/`pending`/`disabled`) is NOT a
|
|
33
|
+
* verdict and is SKIPPED — recording a skip as a pass or a fail would
|
|
34
|
+
* fabricate an outcome the suite never produced.
|
|
35
|
+
* - `qualityScore` ← `1.0` / `0.0` (a binary ground-truth grade). Always
|
|
36
|
+
* supplied: the ingest layer discards `quality_method`/`grader_kind` on a
|
|
37
|
+
* scoreless outcome, so the score is what lights the grading provenance.
|
|
38
|
+
* - `qualityMethod`/`graderKind` ← `ground_truth`/`code`, ASSERTED, never
|
|
39
|
+
* inferred — a code assertion IS a ground-truth code grade, exactly as the
|
|
40
|
+
* pytest plugin asserts. (Unlike promptfoo/braintrust, which grade several
|
|
41
|
+
* ways and must derive the signature; a unit test is unambiguously code.)
|
|
42
|
+
* - `link` ← the assertion `fullName`, else its ancestor titles + its
|
|
43
|
+
* title, else the file name + row index — for traceability back to the exact
|
|
44
|
+
* test. Truncated to 512 (a lossy link beats a lost outcome).
|
|
45
|
+
* - a test FILE that crashed on load/collection (a failing file status with no
|
|
46
|
+
* per-test results) records no outcome — there is no honest count of the
|
|
47
|
+
* tests it would have produced — but it WARNS, so a fully broken run is not
|
|
48
|
+
* silently read as a clean one.
|
|
49
|
+
*
|
|
50
|
+
* ## The honest limit — rung 8 is not reachable here, and that is stated, not defaulted
|
|
51
|
+
*
|
|
52
|
+
* The jest/vitest JSON format carries no per-test value field, so a
|
|
53
|
+
* value-per-outcome numerator (rung 8) cannot ride this FILE path. Absent is
|
|
54
|
+
* the honest cost-per-outcome default — never coerced to `0`. The route to
|
|
55
|
+
* rung 8 from a test suite is the RUNTIME path (`@pytest.mark.margin_value` in
|
|
56
|
+
* Python), and it lives in this same module: {@link MarginVitestReporter} runs
|
|
57
|
+
* inside the suite and reads `task.meta.marginValue`. Use the file consumer
|
|
58
|
+
* when you already have a results file; use the reporter when you want value.
|
|
59
|
+
*
|
|
60
|
+
* Two design promises, same as the sibling adapters: it consumes the file the
|
|
61
|
+
* customer already produces (no new harness), and it is fail-safe — a delivery
|
|
62
|
+
* fault on one row warns and is counted, never aborting the batch.
|
|
63
|
+
*/
|
|
64
|
+
import type { RecordOutcomeInput } from "./index.ts";
|
|
65
|
+
/** Where a warning goes. Defaults to `process.emitWarning` so a delivery
|
|
66
|
+
* problem is visible without crashing the host; a test injects a collector. */
|
|
67
|
+
export type WarnFn = (message: string) => void;
|
|
68
|
+
/** Map a jest/vitest assertion `status` to a pass/fail verdict, or `undefined`
|
|
69
|
+
* when the status is not a verdict at all (`skipped`/`todo`/`pending`/
|
|
70
|
+
* `disabled`/unknown). A skip is neither a pass nor a fail, so it is dropped
|
|
71
|
+
* rather than fabricated. */
|
|
72
|
+
export declare function verdictFromStatus(status: unknown): boolean | undefined;
|
|
73
|
+
/** A traceable link back to the exact test: the assertion's `fullName` if the
|
|
74
|
+
* runner wrote one, else its ancestor titles joined with its title, else the
|
|
75
|
+
* test file name plus the row index. */
|
|
76
|
+
export declare function linkFromAssertion(assertion: unknown, fileName: unknown, index: number): string;
|
|
77
|
+
/** One assertion result paired with the test file it came from (for the link
|
|
78
|
+
* fallback). {@link extractAssertions} produces these. */
|
|
79
|
+
export interface FlatAssertion {
|
|
80
|
+
assertion: Record<string, unknown>;
|
|
81
|
+
fileName?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Flatten a parsed vitest/jest JSON output to a per-test list. The canonical
|
|
84
|
+
* shape is `{ testResults: [{ name, assertionResults: [...] }] }` (jest's
|
|
85
|
+
* `--json`, vitest's `--reporter=json`); a bare array is treated as an
|
|
86
|
+
* already-flattened list of assertion results, for convenience and testing. */
|
|
87
|
+
export declare function extractAssertions(data: unknown): FlatAssertion[];
|
|
88
|
+
/** Names of test files that failed with no per-test results — a suite that
|
|
89
|
+
* crashed on load/collection. These carry no recordable verdict (there is no
|
|
90
|
+
* honest count of tests a crashed suite "would have" produced), so they are NOT
|
|
91
|
+
* turned into outcomes; {@link recordResults} warns for each so a fully broken
|
|
92
|
+
* run is not silently read as a clean one — the whole point of an honest
|
|
93
|
+
* denominator. */
|
|
94
|
+
export declare function failedEmptySuites(data: unknown): string[];
|
|
95
|
+
/** Fields for {@link outcomeInput}. */
|
|
96
|
+
export interface OutcomeInputArgs {
|
|
97
|
+
workflowId: string;
|
|
98
|
+
passed: boolean;
|
|
99
|
+
link: string;
|
|
100
|
+
isSimulated?: boolean;
|
|
101
|
+
/** Opt-in business value (rung 8). Reachable only from the runtime reporter
|
|
102
|
+
* (`task.meta.marginValue`); the file-consumer path never supplies it, since
|
|
103
|
+
* the jest/vitest JSON carries no value field. Absent stays absent. */
|
|
104
|
+
valueUsd?: number;
|
|
105
|
+
}
|
|
106
|
+
/** Translate one code-graded test verdict into `MarginMeter.recordOutcome`
|
|
107
|
+
* input. The grader signature is asserted (`ground_truth`/`code`) because a
|
|
108
|
+
* unit test is unambiguously a code grade. `valueUsd` rides only when supplied
|
|
109
|
+
* (never defaulted to 0). */
|
|
110
|
+
export declare function outcomeInput(args: OutcomeInputArgs): RecordOutcomeInput;
|
|
111
|
+
/** Options shared by the assertion-level translators. */
|
|
112
|
+
export interface TranslateOptions {
|
|
113
|
+
workflowId: string;
|
|
114
|
+
index?: number;
|
|
115
|
+
isSimulated?: boolean;
|
|
116
|
+
}
|
|
117
|
+
/** Full translation of one flattened assertion, or `undefined` if its status is
|
|
118
|
+
* not a real verdict (a skip/todo/pending is dropped, never fabricated). */
|
|
119
|
+
export declare function assertionToOutcomeInput(item: FlatAssertion, opts: TranslateOptions): RecordOutcomeInput | undefined;
|
|
120
|
+
/** Yield `recordOutcome` inputs for every real verdict in a parsed vitest/jest
|
|
121
|
+
* output (any envelope shape {@link extractAssertions} understands). */
|
|
122
|
+
export declare function iterOutcomes(data: unknown, opts: {
|
|
123
|
+
workflowId: string;
|
|
124
|
+
isSimulated?: boolean;
|
|
125
|
+
}): RecordOutcomeInput[];
|
|
126
|
+
/** The minimal meter surface {@link recordResults} needs — satisfied by
|
|
127
|
+
* `MarginMeter`, kept as a structural type so this module imports no runtime
|
|
128
|
+
* value from `index.ts`. */
|
|
129
|
+
export interface OutcomeSink {
|
|
130
|
+
recordOutcome(input: RecordOutcomeInput): Promise<unknown>;
|
|
131
|
+
flush(): Promise<void>;
|
|
132
|
+
}
|
|
133
|
+
/** How one `recordResults` batch landed. */
|
|
134
|
+
export interface RecordSummary {
|
|
135
|
+
recorded: number;
|
|
136
|
+
errors: number;
|
|
137
|
+
skipped: number;
|
|
138
|
+
}
|
|
139
|
+
/** Emit an outcome per verdict in a parsed vitest/jest output through `meter`.
|
|
140
|
+
* Fail-safe: a delivery fault on one row warns and counts an error; it never
|
|
141
|
+
* aborts the batch. Non-verdict rows (skips/todos/pending) are counted as
|
|
142
|
+
* skipped. */
|
|
143
|
+
export declare function recordResults(meter: OutcomeSink, data: unknown, workflowId: string, opts?: {
|
|
144
|
+
isSimulated?: boolean;
|
|
145
|
+
onWarn?: WarnFn;
|
|
146
|
+
}): Promise<RecordSummary>;
|
|
147
|
+
/** Read an opt-in `marginValue` off a task's `meta` — the value-per-outcome
|
|
148
|
+
* numerator (rung 8). Returns the USD amount, or `undefined` when the key is
|
|
149
|
+
* absent (the normal cost-per-outcome case). Throws on a malformed value
|
|
150
|
+
* (including an explicit `null`, i.e. present-but-malformed) so the caller can
|
|
151
|
+
* warn and drop it while keeping the pass/fail.
|
|
152
|
+
*
|
|
153
|
+
* An explicit `undefined` (`meta.marginValue = undefined`) is treated as absent
|
|
154
|
+
* — setting a meta key to `undefined` is not a value a customer meant to
|
|
155
|
+
* supply. This is the one intentional divergence from the file-adapters'
|
|
156
|
+
* `valueFrom*`, and it only ever drops-silently rather than fabricating. */
|
|
157
|
+
export declare function valueFromTaskMeta(meta: unknown): number | undefined;
|
|
158
|
+
/** A minimal structural view of a vitest task — duck-typed against vitest's
|
|
159
|
+
* documented reporter task tree so this module imports NOTHING from vitest and
|
|
160
|
+
* is not pinned to one of its versions. A suite carries child `tasks`; a test
|
|
161
|
+
* carries a `result.state`. */
|
|
162
|
+
export interface VitestTask {
|
|
163
|
+
type?: string;
|
|
164
|
+
name?: string;
|
|
165
|
+
result?: {
|
|
166
|
+
state?: string;
|
|
167
|
+
};
|
|
168
|
+
meta?: Record<string, unknown>;
|
|
169
|
+
tasks?: unknown[];
|
|
170
|
+
}
|
|
171
|
+
/** Map a vitest task result state to a pass/fail verdict, or `undefined` when it
|
|
172
|
+
* is not a verdict (`skip`/`todo`/`queued`/absent — a skipped or not-yet-run
|
|
173
|
+
* test is neither a pass nor a fail, so it is dropped, never fabricated). */
|
|
174
|
+
export declare function verdictFromState(state: unknown): boolean | undefined;
|
|
175
|
+
/** One leaf test found in the task tree, with the suite path that leads to it. */
|
|
176
|
+
export interface WalkedTest {
|
|
177
|
+
task: Record<string, unknown>;
|
|
178
|
+
path: string[];
|
|
179
|
+
}
|
|
180
|
+
/** Depth-first walk of a vitest task tree, yielding every LEAF test together
|
|
181
|
+
* with its ancestor suite names. A node is a CONTAINER (a suite, or a file
|
|
182
|
+
* whose `tasks` are its top-level tests) exactly when it has a `tasks` array —
|
|
183
|
+
* regardless of whether that array is empty — and is recursed into, never
|
|
184
|
+
* recorded. Everything else is a leaf test.
|
|
185
|
+
*
|
|
186
|
+
* ⛔ The container test is the PRESENCE of `tasks`, not its length. vitest emits
|
|
187
|
+
* a file that CRASHED on collection as a suite node with `tasks: []` AND
|
|
188
|
+
* `result.state: "fail"`; a length check would misread that as a failed leaf
|
|
189
|
+
* test and fabricate a `passed:false` code-graded outcome for a file that ran
|
|
190
|
+
* zero tests — the exact over-claim the file-consumer path refuses. A real test
|
|
191
|
+
* never carries a `tasks` property, so this classification keeps every genuine
|
|
192
|
+
* verdict while dropping the crashed/empty suite. */
|
|
193
|
+
export declare function walkTasks(nodes: unknown, ancestors?: string[]): WalkedTest[];
|
|
194
|
+
/** Yield `recordOutcome` inputs for every real verdict in a vitest `files` tree
|
|
195
|
+
* (the argument vitest passes a reporter's `onFinished`). A test's value rides
|
|
196
|
+
* from `task.meta.marginValue`; a malformed value warns and drops the value,
|
|
197
|
+
* keeping the pass/fail. */
|
|
198
|
+
export declare function iterTaskOutcomes(files: unknown, opts: {
|
|
199
|
+
workflowId: string;
|
|
200
|
+
isSimulated?: boolean;
|
|
201
|
+
onWarn?: WarnFn;
|
|
202
|
+
}): RecordOutcomeInput[];
|
|
203
|
+
/** Options for {@link MarginVitestReporter}. */
|
|
204
|
+
export interface MarginVitestReporterOptions {
|
|
205
|
+
/** The workflow every test in this suite is recorded under. Falls back to
|
|
206
|
+
* `MARGIN_VITEST_WORKFLOW` in the environment; with neither, the reporter is
|
|
207
|
+
* a no-op (and warns once) — installing it never silently records to nowhere. */
|
|
208
|
+
workflowId?: string;
|
|
209
|
+
/** The sink outcomes are emitted through. Injected in tests; in production it
|
|
210
|
+
* is left undefined and a real `MarginMeter` (reading `MARGIN_INGEST_URL`/
|
|
211
|
+
* `MARGIN_INGEST_KEY` from the env) is lazily constructed on first finish. */
|
|
212
|
+
meter?: OutcomeSink;
|
|
213
|
+
isSimulated?: boolean;
|
|
214
|
+
onWarn?: WarnFn;
|
|
215
|
+
}
|
|
216
|
+
/** A vitest reporter that records every test as a code-graded outcome and reads
|
|
217
|
+
* an opt-in per-test value off `task.meta.marginValue` (rung 8). It implements
|
|
218
|
+
* only the `onFinished(files)` hook — the one vitest calls with the completed
|
|
219
|
+
* task tree — and is fail-safe: a delivery or config fault warns, and NOTHING
|
|
220
|
+
* it does throws into the test run. */
|
|
221
|
+
export declare class MarginVitestReporter {
|
|
222
|
+
private readonly opts;
|
|
223
|
+
/** The last batch's summary — handy for a customer's own assertions and for
|
|
224
|
+
* the hermetic tests. */
|
|
225
|
+
summary: RecordSummary;
|
|
226
|
+
constructor(opts?: MarginVitestReporterOptions);
|
|
227
|
+
private resolveWorkflow;
|
|
228
|
+
private resolveMeter;
|
|
229
|
+
/** vitest's end-of-run hook. `files` is the completed task tree. */
|
|
230
|
+
onFinished(files?: unknown): Promise<void>;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=vitest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.d.ts","sourceRoot":"","sources":["../src/vitest.ts"],"names":[],"mappings":"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"}
|