@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.
Files changed (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +163 -0
  3. package/dist/advisories.d.ts +23 -0
  4. package/dist/advisories.d.ts.map +1 -0
  5. package/dist/advisories.js +376 -0
  6. package/dist/advisories.js.map +1 -0
  7. package/dist/aws-sigv4.d.ts +88 -0
  8. package/dist/aws-sigv4.d.ts.map +1 -0
  9. package/dist/aws-sigv4.js +117 -0
  10. package/dist/aws-sigv4.js.map +1 -0
  11. package/dist/baseline.d.ts +171 -0
  12. package/dist/baseline.d.ts.map +1 -0
  13. package/dist/baseline.js +273 -0
  14. package/dist/baseline.js.map +1 -0
  15. package/dist/cache.d.ts +26 -0
  16. package/dist/cache.d.ts.map +1 -0
  17. package/dist/cache.js +28 -0
  18. package/dist/cache.js.map +1 -0
  19. package/dist/changes.d.ts +29 -0
  20. package/dist/changes.d.ts.map +1 -0
  21. package/dist/changes.js +142 -0
  22. package/dist/changes.js.map +1 -0
  23. package/dist/compare.d.ts +65 -0
  24. package/dist/compare.d.ts.map +1 -0
  25. package/dist/compare.js +58 -0
  26. package/dist/compare.js.map +1 -0
  27. package/dist/config-schema.d.ts +118 -0
  28. package/dist/config-schema.d.ts.map +1 -0
  29. package/dist/config-schema.js +315 -0
  30. package/dist/config-schema.js.map +1 -0
  31. package/dist/config.d.ts +47 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +112 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/detect.d.ts +70 -0
  36. package/dist/detect.d.ts.map +1 -0
  37. package/dist/detect.js +228 -0
  38. package/dist/detect.js.map +1 -0
  39. package/dist/evaluate.d.ts +98 -0
  40. package/dist/evaluate.d.ts.map +1 -0
  41. package/dist/evaluate.js +110 -0
  42. package/dist/evaluate.js.map +1 -0
  43. package/dist/extract.d.ts +81 -0
  44. package/dist/extract.d.ts.map +1 -0
  45. package/dist/extract.js +280 -0
  46. package/dist/extract.js.map +1 -0
  47. package/dist/gcp-auth.d.ts +58 -0
  48. package/dist/gcp-auth.d.ts.map +1 -0
  49. package/dist/gcp-auth.js +113 -0
  50. package/dist/gcp-auth.js.map +1 -0
  51. package/dist/glob.d.ts +49 -0
  52. package/dist/glob.d.ts.map +1 -0
  53. package/dist/glob.js +154 -0
  54. package/dist/glob.js.map +1 -0
  55. package/dist/host.d.ts +30 -0
  56. package/dist/host.d.ts.map +1 -0
  57. package/dist/host.js +69 -0
  58. package/dist/host.js.map +1 -0
  59. package/dist/i18n/en.d.ts +4 -0
  60. package/dist/i18n/en.d.ts.map +1 -0
  61. package/dist/i18n/en.js +168 -0
  62. package/dist/i18n/en.js.map +1 -0
  63. package/dist/i18n/es.d.ts +4 -0
  64. package/dist/i18n/es.d.ts.map +1 -0
  65. package/dist/i18n/es.js +168 -0
  66. package/dist/i18n/es.js.map +1 -0
  67. package/dist/i18n/index.d.ts +36 -0
  68. package/dist/i18n/index.d.ts.map +1 -0
  69. package/dist/i18n/index.js +50 -0
  70. package/dist/i18n/index.js.map +1 -0
  71. package/dist/i18n/types.d.ts +180 -0
  72. package/dist/i18n/types.d.ts.map +1 -0
  73. package/dist/i18n/types.js +11 -0
  74. package/dist/i18n/types.js.map +1 -0
  75. package/dist/index.d.ts +66 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +69 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/llm.d.ts +226 -0
  80. package/dist/llm.d.ts.map +1 -0
  81. package/dist/llm.js +485 -0
  82. package/dist/llm.js.map +1 -0
  83. package/dist/nearest.d.ts +20 -0
  84. package/dist/nearest.d.ts.map +1 -0
  85. package/dist/nearest.js +54 -0
  86. package/dist/nearest.js.map +1 -0
  87. package/dist/net.d.ts +90 -0
  88. package/dist/net.d.ts.map +1 -0
  89. package/dist/net.js +203 -0
  90. package/dist/net.js.map +1 -0
  91. package/dist/node.d.ts +32 -0
  92. package/dist/node.d.ts.map +1 -0
  93. package/dist/node.js +33 -0
  94. package/dist/node.js.map +1 -0
  95. package/dist/openrouter.d.ts +25 -0
  96. package/dist/openrouter.d.ts.map +1 -0
  97. package/dist/openrouter.js +72 -0
  98. package/dist/openrouter.js.map +1 -0
  99. package/dist/optimize.d.ts +38 -0
  100. package/dist/optimize.d.ts.map +1 -0
  101. package/dist/optimize.js +183 -0
  102. package/dist/optimize.js.map +1 -0
  103. package/dist/otlp.d.ts +91 -0
  104. package/dist/otlp.d.ts.map +1 -0
  105. package/dist/otlp.js +102 -0
  106. package/dist/otlp.js.map +1 -0
  107. package/dist/phrases.d.ts +169 -0
  108. package/dist/phrases.d.ts.map +1 -0
  109. package/dist/phrases.js +939 -0
  110. package/dist/phrases.js.map +1 -0
  111. package/dist/pricing-overlay.d.ts +55 -0
  112. package/dist/pricing-overlay.d.ts.map +1 -0
  113. package/dist/pricing-overlay.js +241 -0
  114. package/dist/pricing-overlay.js.map +1 -0
  115. package/dist/pricing.d.ts +115 -0
  116. package/dist/pricing.d.ts.map +1 -0
  117. package/dist/pricing.js +400 -0
  118. package/dist/pricing.js.map +1 -0
  119. package/dist/profile.d.ts +71 -0
  120. package/dist/profile.d.ts.map +1 -0
  121. package/dist/profile.js +55 -0
  122. package/dist/profile.js.map +1 -0
  123. package/dist/promptfoo.d.ts +58 -0
  124. package/dist/promptfoo.d.ts.map +1 -0
  125. package/dist/promptfoo.js +149 -0
  126. package/dist/promptfoo.js.map +1 -0
  127. package/dist/prune.d.ts +91 -0
  128. package/dist/prune.d.ts.map +1 -0
  129. package/dist/prune.js +110 -0
  130. package/dist/prune.js.map +1 -0
  131. package/dist/reorder.d.ts +82 -0
  132. package/dist/reorder.d.ts.map +1 -0
  133. package/dist/reorder.js +215 -0
  134. package/dist/reorder.js.map +1 -0
  135. package/dist/review.d.ts +54 -0
  136. package/dist/review.d.ts.map +1 -0
  137. package/dist/review.js +131 -0
  138. package/dist/review.js.map +1 -0
  139. package/dist/rules.d.ts +5 -0
  140. package/dist/rules.d.ts.map +1 -0
  141. package/dist/rules.js +279 -0
  142. package/dist/rules.js.map +1 -0
  143. package/dist/savings.d.ts +36 -0
  144. package/dist/savings.d.ts.map +1 -0
  145. package/dist/savings.js +83 -0
  146. package/dist/savings.js.map +1 -0
  147. package/dist/segment.d.ts +8 -0
  148. package/dist/segment.d.ts.map +1 -0
  149. package/dist/segment.js +74 -0
  150. package/dist/segment.js.map +1 -0
  151. package/dist/shared-prefix.d.ts +63 -0
  152. package/dist/shared-prefix.d.ts.map +1 -0
  153. package/dist/shared-prefix.js +151 -0
  154. package/dist/shared-prefix.js.map +1 -0
  155. package/dist/similarity.d.ts +13 -0
  156. package/dist/similarity.d.ts.map +1 -0
  157. package/dist/similarity.js +30 -0
  158. package/dist/similarity.js.map +1 -0
  159. package/dist/structure.d.ts +144 -0
  160. package/dist/structure.d.ts.map +1 -0
  161. package/dist/structure.js +455 -0
  162. package/dist/structure.js.map +1 -0
  163. package/dist/suggest.d.ts +100 -0
  164. package/dist/suggest.d.ts.map +1 -0
  165. package/dist/suggest.js +151 -0
  166. package/dist/suggest.js.map +1 -0
  167. package/dist/tokenizer.d.ts +57 -0
  168. package/dist/tokenizer.d.ts.map +1 -0
  169. package/dist/tokenizer.js +157 -0
  170. package/dist/tokenizer.js.map +1 -0
  171. package/dist/types.d.ts +296 -0
  172. package/dist/types.d.ts.map +1 -0
  173. package/dist/types.js +3 -0
  174. package/dist/types.js.map +1 -0
  175. package/dist/walk.d.ts +40 -0
  176. package/dist/walk.d.ts.map +1 -0
  177. package/dist/walk.js +95 -0
  178. package/dist/walk.js.map +1 -0
  179. package/package.json +56 -0
  180. package/src/advisories.ts +431 -0
  181. package/src/aws-sigv4.ts +174 -0
  182. package/src/baseline.ts +390 -0
  183. package/src/cache.ts +54 -0
  184. package/src/changes.ts +158 -0
  185. package/src/compare.ts +131 -0
  186. package/src/config-schema.ts +451 -0
  187. package/src/config.ts +161 -0
  188. package/src/detect.ts +312 -0
  189. package/src/evaluate.ts +188 -0
  190. package/src/extract.ts +336 -0
  191. package/src/gcp-auth.ts +166 -0
  192. package/src/glob.ts +160 -0
  193. package/src/host.ts +90 -0
  194. package/src/i18n/en.ts +236 -0
  195. package/src/i18n/es.ts +236 -0
  196. package/src/i18n/index.ts +68 -0
  197. package/src/i18n/types.ts +230 -0
  198. package/src/index.ts +228 -0
  199. package/src/llm.ts +708 -0
  200. package/src/nearest.ts +61 -0
  201. package/src/net.ts +233 -0
  202. package/src/node.ts +63 -0
  203. package/src/openrouter.ts +125 -0
  204. package/src/optimize.ts +228 -0
  205. package/src/otlp.ts +179 -0
  206. package/src/phrases.ts +1047 -0
  207. package/src/pricing-overlay.ts +319 -0
  208. package/src/pricing.ts +468 -0
  209. package/src/profile.ts +124 -0
  210. package/src/promptfoo.ts +213 -0
  211. package/src/prune.ts +211 -0
  212. package/src/reorder.ts +307 -0
  213. package/src/review.ts +180 -0
  214. package/src/rules.ts +324 -0
  215. package/src/savings.ts +121 -0
  216. package/src/segment.ts +106 -0
  217. package/src/shared-prefix.ts +198 -0
  218. package/src/similarity.ts +28 -0
  219. package/src/structure.ts +652 -0
  220. package/src/suggest.ts +254 -0
  221. package/src/tokenizer.ts +190 -0
  222. package/src/types.ts +323 -0
  223. 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"}
@@ -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