@trazum/core 1.8.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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/advisories.d.ts +23 -0
- package/dist/advisories.d.ts.map +1 -0
- package/dist/advisories.js +376 -0
- package/dist/advisories.js.map +1 -0
- package/dist/aws-sigv4.d.ts +88 -0
- package/dist/aws-sigv4.d.ts.map +1 -0
- package/dist/aws-sigv4.js +117 -0
- package/dist/aws-sigv4.js.map +1 -0
- package/dist/baseline.d.ts +171 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +273 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +28 -0
- package/dist/cache.js.map +1 -0
- package/dist/changes.d.ts +29 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +142 -0
- package/dist/changes.js.map +1 -0
- package/dist/compare.d.ts +65 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +58 -0
- package/dist/compare.js.map +1 -0
- package/dist/config-schema.d.ts +118 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +315 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +112 -0
- package/dist/config.js.map +1 -0
- package/dist/detect.d.ts +70 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +228 -0
- package/dist/detect.js.map +1 -0
- package/dist/evaluate.d.ts +98 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +110 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/extract.d.ts +81 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +280 -0
- package/dist/extract.js.map +1 -0
- package/dist/gcp-auth.d.ts +58 -0
- package/dist/gcp-auth.d.ts.map +1 -0
- package/dist/gcp-auth.js +113 -0
- package/dist/gcp-auth.js.map +1 -0
- package/dist/glob.d.ts +49 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +154 -0
- package/dist/glob.js.map +1 -0
- package/dist/host.d.ts +30 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +69 -0
- package/dist/host.js.map +1 -0
- package/dist/i18n/en.d.ts +4 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +168 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/es.d.ts +4 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +168 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/index.d.ts +36 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +50 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +180 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +11 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +226 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +485 -0
- package/dist/llm.js.map +1 -0
- package/dist/nearest.d.ts +20 -0
- package/dist/nearest.d.ts.map +1 -0
- package/dist/nearest.js +54 -0
- package/dist/nearest.js.map +1 -0
- package/dist/net.d.ts +90 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +203 -0
- package/dist/net.js.map +1 -0
- package/dist/node.d.ts +32 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +33 -0
- package/dist/node.js.map +1 -0
- package/dist/openrouter.d.ts +25 -0
- package/dist/openrouter.d.ts.map +1 -0
- package/dist/openrouter.js +72 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/optimize.d.ts +38 -0
- package/dist/optimize.d.ts.map +1 -0
- package/dist/optimize.js +183 -0
- package/dist/optimize.js.map +1 -0
- package/dist/otlp.d.ts +91 -0
- package/dist/otlp.d.ts.map +1 -0
- package/dist/otlp.js +102 -0
- package/dist/otlp.js.map +1 -0
- package/dist/phrases.d.ts +169 -0
- package/dist/phrases.d.ts.map +1 -0
- package/dist/phrases.js +939 -0
- package/dist/phrases.js.map +1 -0
- package/dist/pricing-overlay.d.ts +55 -0
- package/dist/pricing-overlay.d.ts.map +1 -0
- package/dist/pricing-overlay.js +241 -0
- package/dist/pricing-overlay.js.map +1 -0
- package/dist/pricing.d.ts +115 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +400 -0
- package/dist/pricing.js.map +1 -0
- package/dist/profile.d.ts +71 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +55 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptfoo.d.ts +58 -0
- package/dist/promptfoo.d.ts.map +1 -0
- package/dist/promptfoo.js +149 -0
- package/dist/promptfoo.js.map +1 -0
- package/dist/prune.d.ts +91 -0
- package/dist/prune.d.ts.map +1 -0
- package/dist/prune.js +110 -0
- package/dist/prune.js.map +1 -0
- package/dist/reorder.d.ts +82 -0
- package/dist/reorder.d.ts.map +1 -0
- package/dist/reorder.js +215 -0
- package/dist/reorder.js.map +1 -0
- package/dist/review.d.ts +54 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +131 -0
- package/dist/review.js.map +1 -0
- package/dist/rules.d.ts +5 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +279 -0
- package/dist/rules.js.map +1 -0
- package/dist/savings.d.ts +36 -0
- package/dist/savings.d.ts.map +1 -0
- package/dist/savings.js +83 -0
- package/dist/savings.js.map +1 -0
- package/dist/segment.d.ts +8 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/segment.js +74 -0
- package/dist/segment.js.map +1 -0
- package/dist/shared-prefix.d.ts +63 -0
- package/dist/shared-prefix.d.ts.map +1 -0
- package/dist/shared-prefix.js +151 -0
- package/dist/shared-prefix.js.map +1 -0
- package/dist/similarity.d.ts +13 -0
- package/dist/similarity.d.ts.map +1 -0
- package/dist/similarity.js +30 -0
- package/dist/similarity.js.map +1 -0
- package/dist/structure.d.ts +144 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +455 -0
- package/dist/structure.js.map +1 -0
- package/dist/suggest.d.ts +100 -0
- package/dist/suggest.d.ts.map +1 -0
- package/dist/suggest.js +151 -0
- package/dist/suggest.js.map +1 -0
- package/dist/tokenizer.d.ts +57 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +157 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/walk.d.ts +40 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +95 -0
- package/dist/walk.js.map +1 -0
- package/package.json +56 -0
- package/src/advisories.ts +431 -0
- package/src/aws-sigv4.ts +174 -0
- package/src/baseline.ts +390 -0
- package/src/cache.ts +54 -0
- package/src/changes.ts +158 -0
- package/src/compare.ts +131 -0
- package/src/config-schema.ts +451 -0
- package/src/config.ts +161 -0
- package/src/detect.ts +312 -0
- package/src/evaluate.ts +188 -0
- package/src/extract.ts +336 -0
- package/src/gcp-auth.ts +166 -0
- package/src/glob.ts +160 -0
- package/src/host.ts +90 -0
- package/src/i18n/en.ts +236 -0
- package/src/i18n/es.ts +236 -0
- package/src/i18n/index.ts +68 -0
- package/src/i18n/types.ts +230 -0
- package/src/index.ts +228 -0
- package/src/llm.ts +708 -0
- package/src/nearest.ts +61 -0
- package/src/net.ts +233 -0
- package/src/node.ts +63 -0
- package/src/openrouter.ts +125 -0
- package/src/optimize.ts +228 -0
- package/src/otlp.ts +179 -0
- package/src/phrases.ts +1047 -0
- package/src/pricing-overlay.ts +319 -0
- package/src/pricing.ts +468 -0
- package/src/profile.ts +124 -0
- package/src/promptfoo.ts +213 -0
- package/src/prune.ts +211 -0
- package/src/reorder.ts +307 -0
- package/src/review.ts +180 -0
- package/src/rules.ts +324 -0
- package/src/savings.ts +121 -0
- package/src/segment.ts +106 -0
- package/src/shared-prefix.ts +198 -0
- package/src/similarity.ts +28 -0
- package/src/structure.ts +652 -0
- package/src/suggest.ts +254 -0
- package/src/tokenizer.ts +190 -0
- package/src/types.ts +323 -0
- package/src/walk.ts +117 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Signature Version 4, by hand.
|
|
3
|
+
*
|
|
4
|
+
* **Why by hand.** `@trazum/core` has zero runtime dependencies and a test that
|
|
5
|
+
* fails the build if one appears. That is a security property rather than a
|
|
6
|
+
* packaging preference: this library reads people's prompts, and every
|
|
7
|
+
* dependency is somebody else's code reading them too. The AWS SDK is roughly
|
|
8
|
+
* a hundred packages to sign one request.
|
|
9
|
+
*
|
|
10
|
+
* **WebCrypto, not `node:crypto`.** The browser-safe entry point cannot reach a
|
|
11
|
+
* Node builtin — `apps/web` bundles this library, and a single `node:` import
|
|
12
|
+
* anywhere in that graph fails the build. `crypto.subtle` exists in both, so
|
|
13
|
+
* signing works in a browser and in the CLI without a second implementation.
|
|
14
|
+
* Everything here is therefore async, which HMAC-SHA256 does not need to be and
|
|
15
|
+
* `crypto.subtle` insists on anyway.
|
|
16
|
+
*
|
|
17
|
+
* **What is not asserted, and it matters.** There is no AWS-published
|
|
18
|
+
* known-answer vector in the tests. This environment cannot reach the internet
|
|
19
|
+
* to fetch one and cannot reach AWS to try a real call, so the tests check the
|
|
20
|
+
* canonical strings this builds — which are derivable from the specification by
|
|
21
|
+
* reading — plus the cryptographic properties that any correct signer has. A
|
|
22
|
+
* systematically wrong canonicalisation that is wrong *consistently* would pass
|
|
23
|
+
* all of them. The first real request against Bedrock is the proof, and until
|
|
24
|
+
* somebody makes one this is careful code rather than verified code.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* `20260810T140102Z` and `20260810`, from one instant.
|
|
28
|
+
*
|
|
29
|
+
* Both derive from the same `Date` rather than being formatted twice. The
|
|
30
|
+
* classic SigV4 bug is a request signed at 23:59:59.9 whose date stamp comes
|
|
31
|
+
* from the next day: the credential scope and the timestamp disagree by one
|
|
32
|
+
* day, AWS rejects it, and it happens roughly once per thousand requests at
|
|
33
|
+
* midnight UTC and never in a test.
|
|
34
|
+
*/
|
|
35
|
+
export declare function amzDates(now: Date): {
|
|
36
|
+
amzDate: string;
|
|
37
|
+
dateStamp: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The signing key: four chained HMACs, each keyed by the last.
|
|
41
|
+
*
|
|
42
|
+
* The chain is what scopes a signature to one day, one region and one service,
|
|
43
|
+
* so a leaked signature is not a leaked credential. `AWS4` prefixes the secret
|
|
44
|
+
* and is part of the specification rather than decoration.
|
|
45
|
+
*/
|
|
46
|
+
export declare function signingKey(secretAccessKey: string, dateStamp: string, region: string, service: string): Promise<ArrayBuffer>;
|
|
47
|
+
export interface SignInput {
|
|
48
|
+
method: string;
|
|
49
|
+
/** Already-encoded path, e.g. `/model/anthropic.claude-v2/invoke`. */
|
|
50
|
+
path: string;
|
|
51
|
+
host: string;
|
|
52
|
+
region: string;
|
|
53
|
+
service: string;
|
|
54
|
+
body: string;
|
|
55
|
+
accessKeyId: string;
|
|
56
|
+
secretAccessKey: string;
|
|
57
|
+
/** Present for temporary credentials, and signed when it is. */
|
|
58
|
+
sessionToken?: string;
|
|
59
|
+
now: Date;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The headers a signed request carries.
|
|
63
|
+
*
|
|
64
|
+
* Returned rather than mutated onto a request, so this is a pure function of its
|
|
65
|
+
* input and a test can read every byte of what it produced.
|
|
66
|
+
*/
|
|
67
|
+
export interface SignedHeaders {
|
|
68
|
+
authorization: string;
|
|
69
|
+
'x-amz-date': string;
|
|
70
|
+
'x-amz-content-sha256': string;
|
|
71
|
+
'x-amz-security-token'?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Builds the canonical request, exactly as the specification orders it.
|
|
75
|
+
*
|
|
76
|
+
* Exported for the tests, because this string is where a signer goes wrong.
|
|
77
|
+
* Every field is joined with a newline, headers are lowercased and sorted, and
|
|
78
|
+
* the trailing newline after the header block is required — omit it and every
|
|
79
|
+
* signature is wrong in a way whose only symptom is `403 SignatureDoesNotMatch`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function canonicalRequest(input: SignInput): Promise<{
|
|
82
|
+
canonical: string;
|
|
83
|
+
signedHeaderNames: string;
|
|
84
|
+
payloadHash: string;
|
|
85
|
+
}>;
|
|
86
|
+
/** The full `Authorization` header value, and the headers that go with it. */
|
|
87
|
+
export declare function signRequest(input: SignInput): Promise<SignedHeaders>;
|
|
88
|
+
//# sourceMappingURL=aws-sigv4.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aws-sigv4.d.ts","sourceRoot":"","sources":["../src/aws-sigv4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAyBH;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAG1E;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAKtB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,EAAE,IAAI,CAAC;CACX;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC,CA6BD;AAED,8EAA8E;AAC9E,wBAAsB,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,CAmB1E"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Signature Version 4, by hand.
|
|
3
|
+
*
|
|
4
|
+
* **Why by hand.** `@trazum/core` has zero runtime dependencies and a test that
|
|
5
|
+
* fails the build if one appears. That is a security property rather than a
|
|
6
|
+
* packaging preference: this library reads people's prompts, and every
|
|
7
|
+
* dependency is somebody else's code reading them too. The AWS SDK is roughly
|
|
8
|
+
* a hundred packages to sign one request.
|
|
9
|
+
*
|
|
10
|
+
* **WebCrypto, not `node:crypto`.** The browser-safe entry point cannot reach a
|
|
11
|
+
* Node builtin — `apps/web` bundles this library, and a single `node:` import
|
|
12
|
+
* anywhere in that graph fails the build. `crypto.subtle` exists in both, so
|
|
13
|
+
* signing works in a browser and in the CLI without a second implementation.
|
|
14
|
+
* Everything here is therefore async, which HMAC-SHA256 does not need to be and
|
|
15
|
+
* `crypto.subtle` insists on anyway.
|
|
16
|
+
*
|
|
17
|
+
* **What is not asserted, and it matters.** There is no AWS-published
|
|
18
|
+
* known-answer vector in the tests. This environment cannot reach the internet
|
|
19
|
+
* to fetch one and cannot reach AWS to try a real call, so the tests check the
|
|
20
|
+
* canonical strings this builds — which are derivable from the specification by
|
|
21
|
+
* reading — plus the cryptographic properties that any correct signer has. A
|
|
22
|
+
* systematically wrong canonicalisation that is wrong *consistently* would pass
|
|
23
|
+
* all of them. The first real request against Bedrock is the proof, and until
|
|
24
|
+
* somebody makes one this is careful code rather than verified code.
|
|
25
|
+
*/
|
|
26
|
+
const ALGORITHM = 'AWS4-HMAC-SHA256';
|
|
27
|
+
const encoder = new TextEncoder();
|
|
28
|
+
function hex(bytes) {
|
|
29
|
+
return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
30
|
+
}
|
|
31
|
+
async function sha256Hex(text) {
|
|
32
|
+
return hex(await crypto.subtle.digest('SHA-256', encoder.encode(text)));
|
|
33
|
+
}
|
|
34
|
+
async function hmac(key, message) {
|
|
35
|
+
const imported = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
36
|
+
return crypto.subtle.sign('HMAC', imported, encoder.encode(message));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `20260810T140102Z` and `20260810`, from one instant.
|
|
40
|
+
*
|
|
41
|
+
* Both derive from the same `Date` rather than being formatted twice. The
|
|
42
|
+
* classic SigV4 bug is a request signed at 23:59:59.9 whose date stamp comes
|
|
43
|
+
* from the next day: the credential scope and the timestamp disagree by one
|
|
44
|
+
* day, AWS rejects it, and it happens roughly once per thousand requests at
|
|
45
|
+
* midnight UTC and never in a test.
|
|
46
|
+
*/
|
|
47
|
+
export function amzDates(now) {
|
|
48
|
+
const amzDate = `${now.toISOString().replace(/[:-]|\.\d{3}/g, '')}`;
|
|
49
|
+
return { amzDate, dateStamp: amzDate.slice(0, 8) };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The signing key: four chained HMACs, each keyed by the last.
|
|
53
|
+
*
|
|
54
|
+
* The chain is what scopes a signature to one day, one region and one service,
|
|
55
|
+
* so a leaked signature is not a leaked credential. `AWS4` prefixes the secret
|
|
56
|
+
* and is part of the specification rather than decoration.
|
|
57
|
+
*/
|
|
58
|
+
export async function signingKey(secretAccessKey, dateStamp, region, service) {
|
|
59
|
+
const kDate = await hmac(encoder.encode(`AWS4${secretAccessKey}`), dateStamp);
|
|
60
|
+
const kRegion = await hmac(kDate, region);
|
|
61
|
+
const kService = await hmac(kRegion, service);
|
|
62
|
+
return hmac(kService, 'aws4_request');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Builds the canonical request, exactly as the specification orders it.
|
|
66
|
+
*
|
|
67
|
+
* Exported for the tests, because this string is where a signer goes wrong.
|
|
68
|
+
* Every field is joined with a newline, headers are lowercased and sorted, and
|
|
69
|
+
* the trailing newline after the header block is required — omit it and every
|
|
70
|
+
* signature is wrong in a way whose only symptom is `403 SignatureDoesNotMatch`.
|
|
71
|
+
*/
|
|
72
|
+
export async function canonicalRequest(input) {
|
|
73
|
+
const { amzDate } = amzDates(input.now);
|
|
74
|
+
const payloadHash = await sha256Hex(input.body);
|
|
75
|
+
// `host` and the two `x-amz-` headers are what Bedrock requires signed. Sorted
|
|
76
|
+
// by name because the specification says sorted, not because it looks tidier.
|
|
77
|
+
const headers = [
|
|
78
|
+
['host', input.host],
|
|
79
|
+
['x-amz-content-sha256', payloadHash],
|
|
80
|
+
['x-amz-date', amzDate],
|
|
81
|
+
];
|
|
82
|
+
if (input.sessionToken)
|
|
83
|
+
headers.push(['x-amz-security-token', input.sessionToken]);
|
|
84
|
+
headers.sort((a, b) => (a[0] < b[0] ? -1 : 1));
|
|
85
|
+
const canonicalHeaders = headers.map(([name, value]) => `${name}:${value.trim()}\n`).join('');
|
|
86
|
+
const signedHeaderNames = headers.map(([name]) => name).join(';');
|
|
87
|
+
const canonical = [
|
|
88
|
+
input.method,
|
|
89
|
+
input.path,
|
|
90
|
+
// No query string on any request this signs. Present and empty, because the
|
|
91
|
+
// field is positional: dropping it shifts everything below it up a line.
|
|
92
|
+
'',
|
|
93
|
+
canonicalHeaders,
|
|
94
|
+
signedHeaderNames,
|
|
95
|
+
payloadHash,
|
|
96
|
+
].join('\n');
|
|
97
|
+
return { canonical, signedHeaderNames, payloadHash };
|
|
98
|
+
}
|
|
99
|
+
/** The full `Authorization` header value, and the headers that go with it. */
|
|
100
|
+
export async function signRequest(input) {
|
|
101
|
+
const { amzDate, dateStamp } = amzDates(input.now);
|
|
102
|
+
const { canonical, signedHeaderNames, payloadHash } = await canonicalRequest(input);
|
|
103
|
+
const scope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
|
|
104
|
+
const stringToSign = [ALGORITHM, amzDate, scope, await sha256Hex(canonical)].join('\n');
|
|
105
|
+
const key = await signingKey(input.secretAccessKey, dateStamp, input.region, input.service);
|
|
106
|
+
const signature = hex(await hmac(key, stringToSign));
|
|
107
|
+
const headers = {
|
|
108
|
+
authorization: `${ALGORITHM} Credential=${input.accessKeyId}/${scope}, ` +
|
|
109
|
+
`SignedHeaders=${signedHeaderNames}, Signature=${signature}`,
|
|
110
|
+
'x-amz-date': amzDate,
|
|
111
|
+
'x-amz-content-sha256': payloadHash,
|
|
112
|
+
};
|
|
113
|
+
if (input.sessionToken)
|
|
114
|
+
headers['x-amz-security-token'] = input.sessionToken;
|
|
115
|
+
return headers;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=aws-sigv4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aws-sigv4.js","sourceRoot":"","sources":["../src/aws-sigv4.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { UsageProfile } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The cost baseline: what this repository's prompts cost as of a commit, and
|
|
4
|
+
* what has happened to them since.
|
|
5
|
+
*
|
|
6
|
+
* **Why this exists.** `budgets` in `trazum.config.json` is a ceiling — it
|
|
7
|
+
* answers "does this file fit". It cannot answer "did this change make things
|
|
8
|
+
* worse", and those are different questions with different failure modes. A
|
|
9
|
+
* repository sitting at 95% of every budget passes every gate forever while a
|
|
10
|
+
* pull request quietly adds four hundred tokens across a dozen files. A ceiling
|
|
11
|
+
* catches the absolute; only a baseline catches the drift.
|
|
12
|
+
*
|
|
13
|
+
* **Why the gate is in tokens and the money is only reported.** A dollar figure
|
|
14
|
+
* is derived from three things: the token count, the usage scenario, and the
|
|
15
|
+
* price list. Two of those change for reasons that have nothing to do with the
|
|
16
|
+
* prompts — a repriced model, an edited `callsPerMonth` — so a baseline holding
|
|
17
|
+
* dollars would fail a build the day the catalogue was updated, calling a price
|
|
18
|
+
* change a regression. A gate that cries wolf is a gate somebody deletes.
|
|
19
|
+
*
|
|
20
|
+
* Tokens depend on the text and nothing else. They are what is compared, they
|
|
21
|
+
* are what the threshold is written in, and the monthly figure is recomputed at
|
|
22
|
+
* comparison time and shown next to it — with an explicit note when the scenario
|
|
23
|
+
* or the price list moved, because a dollar delta across a reprice is two
|
|
24
|
+
* different measurements subtracted from each other.
|
|
25
|
+
*
|
|
26
|
+
* No filesystem access here, deliberately: `apps/web` bundles this package for
|
|
27
|
+
* the browser, and one `node:fs` import anywhere in the graph fails that build.
|
|
28
|
+
* Reading and writing the file is the CLI's job.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The document version.
|
|
32
|
+
*
|
|
33
|
+
* Written into every baseline and checked on read. A baseline is committed and
|
|
34
|
+
* outlives the version of Trazum that wrote it, so a future shape change has to
|
|
35
|
+
* be able to say "this file is from an older format, re-record it" rather than
|
|
36
|
+
* misreading fields that moved.
|
|
37
|
+
*/
|
|
38
|
+
export declare const BASELINE_VERSION = 1;
|
|
39
|
+
export declare const BASELINE_FILENAME = "trazum.baseline.json";
|
|
40
|
+
/**
|
|
41
|
+
* Largest baseline this will read.
|
|
42
|
+
*
|
|
43
|
+
* Bigger than the config limit because this one scales with the repository: a
|
|
44
|
+
* thousand prompts is a thousand entries. Still bounded, so a corrupt or hostile
|
|
45
|
+
* file is refused before `JSON.parse` is handed the whole thing.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MAX_BASELINE_BYTES: number;
|
|
48
|
+
export interface BaselineDocument {
|
|
49
|
+
version: number;
|
|
50
|
+
/** ISO date the baseline was recorded, for the report to cite. */
|
|
51
|
+
recorded: string;
|
|
52
|
+
/**
|
|
53
|
+
* The scenario the monthly figure was computed under. Recorded so a later
|
|
54
|
+
* comparison can say whether the money is comparable, not to gate on.
|
|
55
|
+
*/
|
|
56
|
+
scenario: UsageProfile;
|
|
57
|
+
/** `PRICING_LAST_REVIEWED` at the time, for the same reason. */
|
|
58
|
+
pricingReviewed: string;
|
|
59
|
+
totals: {
|
|
60
|
+
tokens: number;
|
|
61
|
+
monthlyUsd: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Repository-relative path to token count, forward slashes always.
|
|
65
|
+
*
|
|
66
|
+
* Per file rather than one total, because a gate that reports "the repository
|
|
67
|
+
* grew by 400 tokens" without naming the file is a gate people learn to
|
|
68
|
+
* ignore. Sorted on write so re-recording produces a reviewable diff instead
|
|
69
|
+
* of a reordered one.
|
|
70
|
+
*/
|
|
71
|
+
files: Record<string, number>;
|
|
72
|
+
}
|
|
73
|
+
export declare class BaselineError extends Error {
|
|
74
|
+
readonly source: string;
|
|
75
|
+
constructor(message: string, source: string);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Validates a baseline document.
|
|
79
|
+
*
|
|
80
|
+
* **Every failure throws**, for the reason the config parser gives: this file
|
|
81
|
+
* decides whether a build passes. A lenient read of a malformed baseline is a
|
|
82
|
+
* gate that measured nothing and reported success, which is worse than no gate,
|
|
83
|
+
* because the repository now believes it has one.
|
|
84
|
+
*/
|
|
85
|
+
export declare function parseBaseline(raw: string, source?: string): BaselineDocument;
|
|
86
|
+
/**
|
|
87
|
+
* The document as text, ready to commit.
|
|
88
|
+
*
|
|
89
|
+
* Keys are emitted in a fixed order and file paths sorted, so re-recording an
|
|
90
|
+
* unchanged repository produces a byte-identical file. A baseline that reshuffles
|
|
91
|
+
* itself on every write turns every pull request into an unreviewable diff, and
|
|
92
|
+
* the first thing anyone does with an unreviewable diff is stop reading it.
|
|
93
|
+
*/
|
|
94
|
+
export declare function formatBaseline(document: BaselineDocument): string;
|
|
95
|
+
export interface BaselineChange {
|
|
96
|
+
path: string;
|
|
97
|
+
before: number;
|
|
98
|
+
after: number;
|
|
99
|
+
/** `after - before`, so positive is growth — the direction that costs money. */
|
|
100
|
+
delta: number;
|
|
101
|
+
}
|
|
102
|
+
export interface BaselineComparison {
|
|
103
|
+
grown: BaselineChange[];
|
|
104
|
+
shrunk: BaselineChange[];
|
|
105
|
+
/**
|
|
106
|
+
* Present now, absent from the baseline.
|
|
107
|
+
*
|
|
108
|
+
* Counted toward the total, which is the whole reason this field exists: a new
|
|
109
|
+
* prompt is new cost, and a comparison over only the files present in both
|
|
110
|
+
* would let a five-thousand-token addition through every threshold.
|
|
111
|
+
*/
|
|
112
|
+
added: BaselineChange[];
|
|
113
|
+
/** In the baseline, gone from the tree. Never a regression — it is a saving. */
|
|
114
|
+
removed: BaselineChange[];
|
|
115
|
+
tokensBefore: number;
|
|
116
|
+
tokensAfter: number;
|
|
117
|
+
delta: number;
|
|
118
|
+
/** Growth as a percentage of the baseline, or 0 when the baseline was empty. */
|
|
119
|
+
deltaPct: number;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Compares a set of current token counts against a baseline.
|
|
123
|
+
*
|
|
124
|
+
* Pure arithmetic over two maps. It takes counts rather than file contents so
|
|
125
|
+
* the caller decides what a prompt is — directory mode, extracted markers, a
|
|
126
|
+
* hand-picked list — and this stays the one place the comparison is defined.
|
|
127
|
+
*/
|
|
128
|
+
export declare function compareToBaseline(baseline: BaselineDocument, current: Record<string, number>): BaselineComparison;
|
|
129
|
+
export interface BaselineThresholds {
|
|
130
|
+
/** Absolute token growth allowed. */
|
|
131
|
+
maxGrowthTokens?: number;
|
|
132
|
+
/** Growth allowed as a percentage of the baseline total. */
|
|
133
|
+
maxGrowthPct?: number;
|
|
134
|
+
}
|
|
135
|
+
/** Why a comparison failed. Structured, so the CLI owns the wording. */
|
|
136
|
+
export type BaselineBreach = {
|
|
137
|
+
kind: 'tokens';
|
|
138
|
+
limit: number;
|
|
139
|
+
actual: number;
|
|
140
|
+
} | {
|
|
141
|
+
kind: 'pct';
|
|
142
|
+
limit: number;
|
|
143
|
+
actual: number;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Whether a comparison breaches its thresholds.
|
|
147
|
+
*
|
|
148
|
+
* Both thresholds are checked and **either one failing fails the gate** — they
|
|
149
|
+
* are not alternatives to pick between. A percentage alone lets a small
|
|
150
|
+
* repository absorb a large absolute addition; an absolute number alone means a
|
|
151
|
+
* large repository never trips. Whichever is exceeded is reported, so the output
|
|
152
|
+
* names the limit that was actually crossed rather than a generic failure.
|
|
153
|
+
*
|
|
154
|
+
* Shrinking never fails. There is no such thing as a prompt that got too cheap.
|
|
155
|
+
*/
|
|
156
|
+
export declare function breaches(comparison: BaselineComparison, thresholds: BaselineThresholds): BaselineBreach[];
|
|
157
|
+
/**
|
|
158
|
+
* Whether the baseline's money is comparable to today's.
|
|
159
|
+
*
|
|
160
|
+
* Tokens are always comparable — they depend on the text and nothing else, which
|
|
161
|
+
* is why the gate is written in them. The monthly figure is not: a repriced model
|
|
162
|
+
* or an edited scenario changes it without a single prompt moving. When either
|
|
163
|
+
* has shifted the report says so instead of subtracting two different
|
|
164
|
+
* measurements and presenting the difference as a saving.
|
|
165
|
+
*/
|
|
166
|
+
export declare function moneyIsComparable(baseline: BaselineDocument, scenario: UsageProfile, pricingReviewed: string): {
|
|
167
|
+
comparable: boolean;
|
|
168
|
+
scenarioChanged: boolean;
|
|
169
|
+
pricingChanged: boolean;
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,eAAO,MAAM,iBAAiB,yBAAyB,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,QAAkB,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,EAAE,YAAY,CAAC;IACvB,gEAAgE;IAChE,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C;;;;;;;OAOG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,qBAAa,aAAc,SAAQ,KAAK;IAGpC,QAAQ,CAAC,MAAM,EAAE,MAAM;IAFzB,YACE,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EAIxB;CACF;AA2CD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAoB,GAAG,gBAAgB,CAgFvF;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,CAsBjE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB;;;;;;OAMG;IACH,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,gFAAgF;IAChF,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,kBAAkB,CAuCpB;AAED,MAAM,WAAW,kBAAkB;IACjC,qCAAqC;IACrC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CACtB,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,kBAAkB,GAC7B,cAAc,EAAE,CAWlB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,gBAAgB,EAC1B,QAAQ,EAAE,YAAY,EACtB,eAAe,EAAE,MAAM,GACtB;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,eAAe,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,CAU5E"}
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cost baseline: what this repository's prompts cost as of a commit, and
|
|
3
|
+
* what has happened to them since.
|
|
4
|
+
*
|
|
5
|
+
* **Why this exists.** `budgets` in `trazum.config.json` is a ceiling — it
|
|
6
|
+
* answers "does this file fit". It cannot answer "did this change make things
|
|
7
|
+
* worse", and those are different questions with different failure modes. A
|
|
8
|
+
* repository sitting at 95% of every budget passes every gate forever while a
|
|
9
|
+
* pull request quietly adds four hundred tokens across a dozen files. A ceiling
|
|
10
|
+
* catches the absolute; only a baseline catches the drift.
|
|
11
|
+
*
|
|
12
|
+
* **Why the gate is in tokens and the money is only reported.** A dollar figure
|
|
13
|
+
* is derived from three things: the token count, the usage scenario, and the
|
|
14
|
+
* price list. Two of those change for reasons that have nothing to do with the
|
|
15
|
+
* prompts — a repriced model, an edited `callsPerMonth` — so a baseline holding
|
|
16
|
+
* dollars would fail a build the day the catalogue was updated, calling a price
|
|
17
|
+
* change a regression. A gate that cries wolf is a gate somebody deletes.
|
|
18
|
+
*
|
|
19
|
+
* Tokens depend on the text and nothing else. They are what is compared, they
|
|
20
|
+
* are what the threshold is written in, and the monthly figure is recomputed at
|
|
21
|
+
* comparison time and shown next to it — with an explicit note when the scenario
|
|
22
|
+
* or the price list moved, because a dollar delta across a reprice is two
|
|
23
|
+
* different measurements subtracted from each other.
|
|
24
|
+
*
|
|
25
|
+
* No filesystem access here, deliberately: `apps/web` bundles this package for
|
|
26
|
+
* the browser, and one `node:fs` import anywhere in the graph fails that build.
|
|
27
|
+
* Reading and writing the file is the CLI's job.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The document version.
|
|
31
|
+
*
|
|
32
|
+
* Written into every baseline and checked on read. A baseline is committed and
|
|
33
|
+
* outlives the version of Trazum that wrote it, so a future shape change has to
|
|
34
|
+
* be able to say "this file is from an older format, re-record it" rather than
|
|
35
|
+
* misreading fields that moved.
|
|
36
|
+
*/
|
|
37
|
+
export const BASELINE_VERSION = 1;
|
|
38
|
+
export const BASELINE_FILENAME = 'trazum.baseline.json';
|
|
39
|
+
/**
|
|
40
|
+
* Largest baseline this will read.
|
|
41
|
+
*
|
|
42
|
+
* Bigger than the config limit because this one scales with the repository: a
|
|
43
|
+
* thousand prompts is a thousand entries. Still bounded, so a corrupt or hostile
|
|
44
|
+
* file is refused before `JSON.parse` is handed the whole thing.
|
|
45
|
+
*/
|
|
46
|
+
export const MAX_BASELINE_BYTES = 4 * 1024 * 1024;
|
|
47
|
+
export class BaselineError extends Error {
|
|
48
|
+
source;
|
|
49
|
+
constructor(message, source) {
|
|
50
|
+
super(`${source}: ${message}`);
|
|
51
|
+
this.source = source;
|
|
52
|
+
this.name = 'BaselineError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* An absolute path in any shape a committed file might carry one.
|
|
57
|
+
*
|
|
58
|
+
* Same regex, and the same reasoning, as `config-schema.ts`: written out rather
|
|
59
|
+
* than delegating to `path.isAbsolute` because that is platform-dependent, and a
|
|
60
|
+
* baseline recorded on Windows must be judged identically on a Linux runner or
|
|
61
|
+
* the gate silently matches nothing.
|
|
62
|
+
*/
|
|
63
|
+
const IS_ABSOLUTE = /^(?:[/\\]|[A-Za-z]:[/\\])/;
|
|
64
|
+
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
65
|
+
function requireWholeCount(value, label, source) {
|
|
66
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
67
|
+
throw new BaselineError(`"${label}" must be a whole number of 0 or more`, source);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function requireString(value, label, source) {
|
|
72
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
73
|
+
throw new BaselineError(`"${label}" must be a non-empty string`, source);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
function cacheHitRateOf(value, source) {
|
|
78
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
79
|
+
throw new BaselineError('"scenario.cacheHitRate" is a fraction between 0 and 1', source);
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
function batchEligibleOf(value, source) {
|
|
84
|
+
if (typeof value !== 'boolean') {
|
|
85
|
+
throw new BaselineError('"scenario.batchEligible" must be true or false', source);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Validates a baseline document.
|
|
91
|
+
*
|
|
92
|
+
* **Every failure throws**, for the reason the config parser gives: this file
|
|
93
|
+
* decides whether a build passes. A lenient read of a malformed baseline is a
|
|
94
|
+
* gate that measured nothing and reported success, which is worse than no gate,
|
|
95
|
+
* because the repository now believes it has one.
|
|
96
|
+
*/
|
|
97
|
+
export function parseBaseline(raw, source = BASELINE_FILENAME) {
|
|
98
|
+
let document;
|
|
99
|
+
try {
|
|
100
|
+
document = JSON.parse(raw);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
104
|
+
throw new BaselineError(`not valid JSON — ${detail}`, source);
|
|
105
|
+
}
|
|
106
|
+
if (!isPlainObject(document)) {
|
|
107
|
+
throw new BaselineError('the top level must be an object', source);
|
|
108
|
+
}
|
|
109
|
+
const version = document.version;
|
|
110
|
+
if (version !== BASELINE_VERSION) {
|
|
111
|
+
throw new BaselineError(`is version ${JSON.stringify(version)}, and this Trazum reads version ${BASELINE_VERSION}. ` +
|
|
112
|
+
're-record it with "trazum baseline"', source);
|
|
113
|
+
}
|
|
114
|
+
if (!isPlainObject(document.totals)) {
|
|
115
|
+
throw new BaselineError('"totals" must be an object', source);
|
|
116
|
+
}
|
|
117
|
+
if (!isPlainObject(document.files)) {
|
|
118
|
+
throw new BaselineError('"files" must be an object of path to token count', source);
|
|
119
|
+
}
|
|
120
|
+
if (!isPlainObject(document.scenario)) {
|
|
121
|
+
throw new BaselineError('"scenario" must be an object', source);
|
|
122
|
+
}
|
|
123
|
+
const files = {};
|
|
124
|
+
for (const [path, tokens] of Object.entries(document.files)) {
|
|
125
|
+
if (path.length === 0)
|
|
126
|
+
throw new BaselineError('"files" has an empty path', source);
|
|
127
|
+
if (IS_ABSOLUTE.test(path) || path.includes('..')) {
|
|
128
|
+
throw new BaselineError(`"files" path ${JSON.stringify(path)} must be relative to the project`, source);
|
|
129
|
+
}
|
|
130
|
+
files[path] = requireWholeCount(tokens, `files["${path}"]`, source);
|
|
131
|
+
}
|
|
132
|
+
const totalUsd = document.totals.monthlyUsd;
|
|
133
|
+
if (typeof totalUsd !== 'number' || !Number.isFinite(totalUsd) || totalUsd < 0) {
|
|
134
|
+
throw new BaselineError('"totals.monthlyUsd" must be a number of 0 or more', source);
|
|
135
|
+
}
|
|
136
|
+
const recorded = requireWholeCount(document.totals.tokens, 'totals.tokens', source);
|
|
137
|
+
const summed = Object.values(files).reduce((a, b) => a + b, 0);
|
|
138
|
+
if (recorded !== summed) {
|
|
139
|
+
// A hand-edited total is the one corruption that looks completely normal:
|
|
140
|
+
// the file parses, the gate runs, and it compares against a number nobody
|
|
141
|
+
// measured. Cheap to check, so it is checked.
|
|
142
|
+
throw new BaselineError(`"totals.tokens" is ${recorded} but the per-file counts sum to ${summed}`, source);
|
|
143
|
+
}
|
|
144
|
+
const scenario = document.scenario;
|
|
145
|
+
return {
|
|
146
|
+
version: BASELINE_VERSION,
|
|
147
|
+
recorded: requireString(document.recorded, 'recorded', source),
|
|
148
|
+
pricingReviewed: requireString(document.pricingReviewed, 'pricingReviewed', source),
|
|
149
|
+
scenario: {
|
|
150
|
+
model: requireString(scenario.model, 'scenario.model', source),
|
|
151
|
+
callsPerMonth: requireWholeCount(scenario.callsPerMonth, 'scenario.callsPerMonth', source),
|
|
152
|
+
avgOutputTokens: requireWholeCount(scenario.avgOutputTokens, 'scenario.avgOutputTokens', source),
|
|
153
|
+
cacheHitRate: cacheHitRateOf(scenario.cacheHitRate, source),
|
|
154
|
+
batchEligible: batchEligibleOf(scenario.batchEligible, source),
|
|
155
|
+
},
|
|
156
|
+
totals: { tokens: recorded, monthlyUsd: totalUsd },
|
|
157
|
+
files,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The document as text, ready to commit.
|
|
162
|
+
*
|
|
163
|
+
* Keys are emitted in a fixed order and file paths sorted, so re-recording an
|
|
164
|
+
* unchanged repository produces a byte-identical file. A baseline that reshuffles
|
|
165
|
+
* itself on every write turns every pull request into an unreviewable diff, and
|
|
166
|
+
* the first thing anyone does with an unreviewable diff is stop reading it.
|
|
167
|
+
*/
|
|
168
|
+
export function formatBaseline(document) {
|
|
169
|
+
const files = {};
|
|
170
|
+
for (const path of Object.keys(document.files).sort())
|
|
171
|
+
files[path] = document.files[path];
|
|
172
|
+
return `${JSON.stringify({
|
|
173
|
+
version: BASELINE_VERSION,
|
|
174
|
+
recorded: document.recorded,
|
|
175
|
+
scenario: {
|
|
176
|
+
model: document.scenario.model,
|
|
177
|
+
callsPerMonth: document.scenario.callsPerMonth,
|
|
178
|
+
avgOutputTokens: document.scenario.avgOutputTokens,
|
|
179
|
+
cacheHitRate: document.scenario.cacheHitRate,
|
|
180
|
+
batchEligible: document.scenario.batchEligible,
|
|
181
|
+
},
|
|
182
|
+
pricingReviewed: document.pricingReviewed,
|
|
183
|
+
totals: document.totals,
|
|
184
|
+
files,
|
|
185
|
+
}, null, 2)}\n`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Compares a set of current token counts against a baseline.
|
|
189
|
+
*
|
|
190
|
+
* Pure arithmetic over two maps. It takes counts rather than file contents so
|
|
191
|
+
* the caller decides what a prompt is — directory mode, extracted markers, a
|
|
192
|
+
* hand-picked list — and this stays the one place the comparison is defined.
|
|
193
|
+
*/
|
|
194
|
+
export function compareToBaseline(baseline, current) {
|
|
195
|
+
const grown = [];
|
|
196
|
+
const shrunk = [];
|
|
197
|
+
const added = [];
|
|
198
|
+
const removed = [];
|
|
199
|
+
for (const path of Object.keys(current).sort()) {
|
|
200
|
+
const after = current[path];
|
|
201
|
+
const before = baseline.files[path];
|
|
202
|
+
if (before === undefined) {
|
|
203
|
+
added.push({ path, before: 0, after, delta: after });
|
|
204
|
+
}
|
|
205
|
+
else if (after > before) {
|
|
206
|
+
grown.push({ path, before, after, delta: after - before });
|
|
207
|
+
}
|
|
208
|
+
else if (after < before) {
|
|
209
|
+
shrunk.push({ path, before, after, delta: after - before });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const path of Object.keys(baseline.files).sort()) {
|
|
213
|
+
if (current[path] === undefined) {
|
|
214
|
+
const before = baseline.files[path];
|
|
215
|
+
removed.push({ path, before, after: 0, delta: -before });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const tokensBefore = baseline.totals.tokens;
|
|
219
|
+
const tokensAfter = Object.values(current).reduce((a, b) => a + b, 0);
|
|
220
|
+
const delta = tokensAfter - tokensBefore;
|
|
221
|
+
return {
|
|
222
|
+
grown,
|
|
223
|
+
shrunk,
|
|
224
|
+
added,
|
|
225
|
+
removed,
|
|
226
|
+
tokensBefore,
|
|
227
|
+
tokensAfter,
|
|
228
|
+
delta,
|
|
229
|
+
deltaPct: tokensBefore === 0 ? 0 : (delta / tokensBefore) * 100,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Whether a comparison breaches its thresholds.
|
|
234
|
+
*
|
|
235
|
+
* Both thresholds are checked and **either one failing fails the gate** — they
|
|
236
|
+
* are not alternatives to pick between. A percentage alone lets a small
|
|
237
|
+
* repository absorb a large absolute addition; an absolute number alone means a
|
|
238
|
+
* large repository never trips. Whichever is exceeded is reported, so the output
|
|
239
|
+
* names the limit that was actually crossed rather than a generic failure.
|
|
240
|
+
*
|
|
241
|
+
* Shrinking never fails. There is no such thing as a prompt that got too cheap.
|
|
242
|
+
*/
|
|
243
|
+
export function breaches(comparison, thresholds) {
|
|
244
|
+
const found = [];
|
|
245
|
+
if (comparison.delta <= 0)
|
|
246
|
+
return found;
|
|
247
|
+
if (thresholds.maxGrowthTokens !== undefined && comparison.delta > thresholds.maxGrowthTokens) {
|
|
248
|
+
found.push({ kind: 'tokens', limit: thresholds.maxGrowthTokens, actual: comparison.delta });
|
|
249
|
+
}
|
|
250
|
+
if (thresholds.maxGrowthPct !== undefined && comparison.deltaPct > thresholds.maxGrowthPct) {
|
|
251
|
+
found.push({ kind: 'pct', limit: thresholds.maxGrowthPct, actual: comparison.deltaPct });
|
|
252
|
+
}
|
|
253
|
+
return found;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Whether the baseline's money is comparable to today's.
|
|
257
|
+
*
|
|
258
|
+
* Tokens are always comparable — they depend on the text and nothing else, which
|
|
259
|
+
* is why the gate is written in them. The monthly figure is not: a repriced model
|
|
260
|
+
* or an edited scenario changes it without a single prompt moving. When either
|
|
261
|
+
* has shifted the report says so instead of subtracting two different
|
|
262
|
+
* measurements and presenting the difference as a saving.
|
|
263
|
+
*/
|
|
264
|
+
export function moneyIsComparable(baseline, scenario, pricingReviewed) {
|
|
265
|
+
const scenarioChanged = baseline.scenario.model !== scenario.model ||
|
|
266
|
+
baseline.scenario.callsPerMonth !== scenario.callsPerMonth ||
|
|
267
|
+
baseline.scenario.avgOutputTokens !== scenario.avgOutputTokens ||
|
|
268
|
+
baseline.scenario.cacheHitRate !== scenario.cacheHitRate ||
|
|
269
|
+
baseline.scenario.batchEligible !== scenario.batchEligible;
|
|
270
|
+
const pricingChanged = baseline.pricingReviewed !== pricingReviewed;
|
|
271
|
+
return { comparable: !scenarioChanged && !pricingChanged, scenarioChanged, pricingChanged };
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=baseline.js.map
|