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.
@@ -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":"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"}
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAarD;gFACgF;AAChF,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAc/C;;;8BAG8B;AAC9B,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAItE;AAED;;yCAEyC;AACzC,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,OAAO,EAClB,QAAQ,EAAE,OAAO,EACjB,KAAK,EAAE,MAAM,GACZ,MAAM,CAiBR;AAED;2DAC2D;AAC3D,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;gFAGgF;AAChF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,EAAE,CAmBhE;AAQD;;;;;mBAKmB;AACnB,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,CAsBzD;AAED,uCAAuC;AACvC,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;4EAEwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;8BAG8B;AAC9B,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,kBAAkB,CAYvE;AAED,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;6EAC6E;AAC7E,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE,gBAAgB,GACrB,kBAAkB,GAAG,SAAS,CAUhC;AAED;yEACyE;AACzE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,GAClD,kBAAkB,EAAE,CAYtB;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;;;eAGe;AACf,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,CA0CxB;AAsDD;;;;;;;;;6EAS6E;AAC7E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMnE;AAED;;;gCAGgC;AAChC,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;CACnB;AAED;;8EAE8E;AAC9E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAIpE;AAED,kFAAkF;AAClF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;;;;;;;sDAYsD;AACtD,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,GAAE,MAAM,EAAO,GAAG,UAAU,EAAE,CAgBhF;AAED;;;6BAG6B;AAC7B,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACnE,kBAAkB,EAAE,CA4BtB;AAED,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C;;sFAEkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;mFAE+E;IAC/E,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;wCAIwC;AACxC,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IACnD;8BAC0B;IAC1B,OAAO,EAAE,aAAa,CAA0C;gBAEpD,IAAI,GAAE,2BAAgC;IAIlD,OAAO,CAAC,eAAe;YAQT,YAAY;IAS1B,oEAAoE;IAC9D,UAAU,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CA4CjD"}