@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,315 @@
1
+ import { BASELINE_FILENAME } from './baseline.js';
2
+ import { mostSpecificMatch } from './glob.js';
3
+ import { isLocale } from './i18n/index.js';
4
+ import { nearestName } from './nearest.js';
5
+ import { RULES } from './rules.js';
6
+ /**
7
+ * The shape and validation of `trazum.config.json`. No filesystem access — the
8
+ * loader that reads the file lives in `config.ts`, which is only reachable via
9
+ * `@trazum/core/node`. Keeping the schema pure is what lets the browser bundle
10
+ * import the types and key lists without dragging `node:fs` into its graph.
11
+ *
12
+ * The problem this solves is small and real: every command in a repository's CI
13
+ * repeats the same four flags, and the one place they get out of step is the
14
+ * place the numbers stop meaning anything.
15
+ *
16
+ * **Every validation failure here throws.** That is the whole design. A config
17
+ * file is trusted to carry a budget, and the failure mode of a lenient parser
18
+ * is a typo'd key silently ignored, defaults quietly restored, and a green
19
+ * build for a prompt nobody measured. An unreadable config is a loud error; a
20
+ * config that half-applies is a lie.
21
+ */
22
+ export const CONFIG_FILENAME = 'trazum.config.json';
23
+ /**
24
+ * Largest config file this will read.
25
+ *
26
+ * A config is a couple of dozen lines. This exists so a hostile or accidental
27
+ * multi-megabyte file is refused before `JSON.parse` is handed the whole thing.
28
+ */
29
+ export const MAX_CONFIG_BYTES = 64 * 1024;
30
+ /** How far up the tree the search for a config file will walk. */
31
+ export const MAX_CONFIG_SEARCH_DEPTH = 32;
32
+ /**
33
+ * Every key the config accepts. Exported so the CLI's help can be tested
34
+ * against it rather than against a second hand-maintained list — a setting the
35
+ * help never mentions is one only the changelog knows about.
36
+ */
37
+ export const CONFIG_KEYS = [
38
+ 'level',
39
+ 'locale',
40
+ 'disable',
41
+ 'usage',
42
+ 'budgets',
43
+ 'maxGrowth',
44
+ 'baseline',
45
+ 'extensions',
46
+ 'pricing',
47
+ ];
48
+ export const CONFIG_BASELINE_KEYS = ['path', 'maxGrowthTokens', 'maxGrowthPct'];
49
+ export const CONFIG_USAGE_KEYS = [
50
+ 'model',
51
+ 'callsPerMonth',
52
+ 'avgOutputTokens',
53
+ 'cacheHitRate',
54
+ 'batchEligible',
55
+ ];
56
+ const TOP_LEVEL_KEYS = CONFIG_KEYS;
57
+ const USAGE_KEYS = CONFIG_USAGE_KEYS;
58
+ /** Extensions directory mode reads when the config does not say otherwise. */
59
+ export const DEFAULT_EXTENSIONS = ['.txt', '.md', '.prompt', '.tmpl'];
60
+ export class ConfigError extends Error {
61
+ source;
62
+ constructor(message, source) {
63
+ super(`${source}: ${message}`);
64
+ this.source = source;
65
+ this.name = 'ConfigError';
66
+ }
67
+ }
68
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
69
+ /**
70
+ * Rejects a key the schema does not have, naming the nearest one that it does.
71
+ *
72
+ * This is the same failure as an unrecognised CLI flag, and it gets the same
73
+ * treatment for the same reason: `"maxtokens"` where the schema says `budgets`
74
+ * is a budget that is never read, and silence about it means CI stays green.
75
+ */
76
+ function rejectUnknownKeys(object, allowed, source, path) {
77
+ for (const key of Object.keys(object)) {
78
+ if (allowed.includes(key))
79
+ continue;
80
+ const nearest = nearestName(key, allowed);
81
+ throw new ConfigError(nearest
82
+ ? `unknown key "${path}${key}" — did you mean "${nearest}"?`
83
+ : `unknown key "${path}${key}". Known keys: ${allowed.join(', ')}`, source);
84
+ }
85
+ }
86
+ function requireNonNegativeNumber(value, label, source) {
87
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
88
+ throw new ConfigError(`"${label}" must be a number of 0 or more`, source);
89
+ }
90
+ return value;
91
+ }
92
+ function parseUsage(raw, source) {
93
+ if (!isPlainObject(raw))
94
+ throw new ConfigError('"usage" must be an object', source);
95
+ rejectUnknownKeys(raw, USAGE_KEYS, source, 'usage.');
96
+ const usage = {};
97
+ if (raw.model !== undefined) {
98
+ if (typeof raw.model !== 'string' || raw.model.trim() === '') {
99
+ throw new ConfigError('"usage.model" must be a non-empty string', source);
100
+ }
101
+ // Deliberately NOT checked against the catalogue here. A `pricing` overlay can
102
+ // introduce a model, and the path to that overlay is a key of this very
103
+ // document — so the parser cannot know the catalogue yet. The membership check
104
+ // happens in `loadConfig`, once the overlay has been resolved, which is the
105
+ // only place with enough information to be right about it.
106
+ usage.model = raw.model;
107
+ }
108
+ if (raw.callsPerMonth !== undefined) {
109
+ usage.callsPerMonth = requireNonNegativeNumber(raw.callsPerMonth, 'usage.callsPerMonth', source);
110
+ }
111
+ if (raw.avgOutputTokens !== undefined) {
112
+ usage.avgOutputTokens = requireNonNegativeNumber(raw.avgOutputTokens, 'usage.avgOutputTokens', source);
113
+ }
114
+ if (raw.cacheHitRate !== undefined) {
115
+ const rate = requireNonNegativeNumber(raw.cacheHitRate, 'usage.cacheHitRate', source);
116
+ if (rate > 1) {
117
+ throw new ConfigError('"usage.cacheHitRate" is a fraction between 0 and 1', source);
118
+ }
119
+ usage.cacheHitRate = rate;
120
+ }
121
+ if (raw.batchEligible !== undefined) {
122
+ if (typeof raw.batchEligible !== 'boolean') {
123
+ throw new ConfigError('"usage.batchEligible" must be true or false', source);
124
+ }
125
+ usage.batchEligible = raw.batchEligible;
126
+ }
127
+ return usage;
128
+ }
129
+ /**
130
+ * An absolute path, in any of the shapes a config file might carry one.
131
+ *
132
+ * Written out rather than delegating to `path.isAbsolute` for two reasons. It
133
+ * keeps `node:path` out of this module — the browser bundle imports it, and a
134
+ * Node builtin anywhere in that graph fails the web build. And `isAbsolute` is
135
+ * platform-dependent: on Linux it reads `C:\prompts` as relative, so a pattern
136
+ * written on Windows would pass validation on a Linux CI runner and then match
137
+ * nothing. A config should be judged the same way everywhere it is checked.
138
+ */
139
+ const IS_ABSOLUTE = /^(?:[/\\]|[A-Za-z]:[/\\])/;
140
+ function parseBudgets(raw, source) {
141
+ if (!isPlainObject(raw))
142
+ throw new ConfigError('"budgets" must be an object', source);
143
+ const budgets = {};
144
+ for (const [pattern, value] of Object.entries(raw)) {
145
+ if (pattern.length === 0) {
146
+ throw new ConfigError('"budgets" has an empty pattern', source);
147
+ }
148
+ if (IS_ABSOLUTE.test(pattern) || pattern.includes('..')) {
149
+ // Budgets key files inside the repository. An absolute pattern, or one
150
+ // that climbs out with "..", either matches nothing or matches something
151
+ // outside the project — both are mistakes worth naming.
152
+ throw new ConfigError(`budget pattern "${pattern}" must be a relative path inside the project`, source);
153
+ }
154
+ const tokens = requireNonNegativeNumber(value, `budgets["${pattern}"]`, source);
155
+ if (!Number.isInteger(tokens)) {
156
+ throw new ConfigError(`budgets["${pattern}"] must be a whole number of tokens`, source);
157
+ }
158
+ budgets[pattern] = tokens;
159
+ }
160
+ return budgets;
161
+ }
162
+ /**
163
+ * Validates the `baseline` block.
164
+ *
165
+ * **A baseline with no threshold is an error, not a default.** Either default is
166
+ * wrong in a way that is silent: zero tolerance turns every honest addition into
167
+ * a failed build and gets the whole block deleted within a week, and a generous
168
+ * default is a gate that passes things nobody agreed to let through. The number
169
+ * is a policy decision, so the policy has to be written down.
170
+ *
171
+ * `path` defaults, because there is one sensible answer and repeating it in every
172
+ * config is noise.
173
+ */
174
+ function parseBaselineConfig(raw, source) {
175
+ if (!isPlainObject(raw))
176
+ throw new ConfigError('"baseline" must be an object', source);
177
+ rejectUnknownKeys(raw, CONFIG_BASELINE_KEYS, source, 'baseline.');
178
+ const baseline = { path: BASELINE_FILENAME };
179
+ if (raw.path !== undefined) {
180
+ if (typeof raw.path !== 'string' || raw.path.trim() === '') {
181
+ throw new ConfigError('"baseline.path" must be a path to a baseline file', source);
182
+ }
183
+ if (IS_ABSOLUTE.test(raw.path) || raw.path.includes('..')) {
184
+ throw new ConfigError(`"baseline.path" must be a relative path inside the project (got "${raw.path}")`, source);
185
+ }
186
+ baseline.path = raw.path;
187
+ }
188
+ if (raw.maxGrowthTokens !== undefined) {
189
+ const tokens = requireNonNegativeNumber(raw.maxGrowthTokens, 'baseline.maxGrowthTokens', source);
190
+ if (!Number.isInteger(tokens)) {
191
+ throw new ConfigError('"baseline.maxGrowthTokens" must be a whole number of tokens', source);
192
+ }
193
+ baseline.maxGrowthTokens = tokens;
194
+ }
195
+ if (raw.maxGrowthPct !== undefined) {
196
+ baseline.maxGrowthPct = requireNonNegativeNumber(raw.maxGrowthPct, 'baseline.maxGrowthPct', source);
197
+ }
198
+ if (baseline.maxGrowthTokens === undefined && baseline.maxGrowthPct === undefined) {
199
+ throw new ConfigError('"baseline" needs at least one of "maxGrowthTokens" or "maxGrowthPct" — ' +
200
+ 'a baseline with no threshold cannot fail, and a gate that cannot fail is not a gate', source);
201
+ }
202
+ return baseline;
203
+ }
204
+ /**
205
+ * Checks a config's `usage.model` against a resolved catalogue.
206
+ *
207
+ * Separate from `parseConfig` because it needs the catalogue, and the catalogue
208
+ * may be defined by the very document being parsed — `pricing` is a config key.
209
+ * `loadConfig` calls this once the overlay is in hand, so a typo'd model is still
210
+ * a loud error, just one raised at the point where "unknown model" can be
211
+ * answered truthfully.
212
+ */
213
+ export function validateConfigModel(config, catalogue, source) {
214
+ const model = config.usage?.model;
215
+ if (model === undefined || catalogue.byId.has(model))
216
+ return;
217
+ const ids = catalogue.models.map((m) => m.id);
218
+ const nearest = nearestName(model, ids);
219
+ throw new ConfigError(nearest
220
+ ? `"usage.model" names no such model: "${model}" — did you mean "${nearest}"?`
221
+ : `"usage.model" names no such model: "${model}". Available: ${ids.join(', ')}`, source);
222
+ }
223
+ /**
224
+ * Validates a config document.
225
+ *
226
+ * `source` names the file in every error, because a config error found while
227
+ * running `trazum check` in a monorepo is useless without knowing which of
228
+ * several config files it came from.
229
+ */
230
+ export function parseConfig(raw, source = CONFIG_FILENAME) {
231
+ let document;
232
+ try {
233
+ document = JSON.parse(raw);
234
+ }
235
+ catch (error) {
236
+ const detail = error instanceof Error ? error.message : String(error);
237
+ throw new ConfigError(`not valid JSON — ${detail}`, source);
238
+ }
239
+ if (!isPlainObject(document)) {
240
+ throw new ConfigError('the top level must be an object', source);
241
+ }
242
+ rejectUnknownKeys(document, TOP_LEVEL_KEYS, source, '');
243
+ const config = {};
244
+ if (document.level !== undefined) {
245
+ if (document.level !== 'safe' && document.level !== 'aggressive') {
246
+ throw new ConfigError('"level" must be "safe" or "aggressive"', source);
247
+ }
248
+ config.level = document.level;
249
+ }
250
+ if (document.locale !== undefined) {
251
+ if (typeof document.locale !== 'string' || !isLocale(document.locale)) {
252
+ throw new ConfigError(`"locale" is not a locale Trazum ships: ${String(document.locale)}`, source);
253
+ }
254
+ config.locale = document.locale;
255
+ }
256
+ if (document.disable !== undefined) {
257
+ if (!Array.isArray(document.disable)) {
258
+ throw new ConfigError('"disable" must be an array of rule ids', source);
259
+ }
260
+ const ids = RULES.map((rule) => rule.id);
261
+ config.disable = document.disable.map((value) => {
262
+ if (typeof value !== 'string' || !ids.includes(value)) {
263
+ const nearest = typeof value === 'string' ? nearestName(value, ids) : null;
264
+ throw new ConfigError(nearest
265
+ ? `"disable" names no such rule: "${String(value)}" — did you mean "${nearest}"?`
266
+ : `"disable" names no such rule: "${String(value)}". Run "trazum rules" for the list.`, source);
267
+ }
268
+ return value;
269
+ });
270
+ }
271
+ if (document.usage !== undefined)
272
+ config.usage = parseUsage(document.usage, source);
273
+ if (document.budgets !== undefined)
274
+ config.budgets = parseBudgets(document.budgets, source);
275
+ if (document.baseline !== undefined) {
276
+ config.baseline = parseBaselineConfig(document.baseline, source);
277
+ }
278
+ if (document.maxGrowth !== undefined) {
279
+ config.maxGrowth = requireNonNegativeNumber(document.maxGrowth, 'maxGrowth', source);
280
+ }
281
+ if (document.pricing !== undefined) {
282
+ if (typeof document.pricing !== 'string' || document.pricing.trim() === '') {
283
+ throw new ConfigError('"pricing" must be a path to a pricing overlay file', source);
284
+ }
285
+ if (IS_ABSOLUTE.test(document.pricing) || document.pricing.includes('..')) {
286
+ throw new ConfigError(`"pricing" must be a relative path inside the project (got "${document.pricing}")`, source);
287
+ }
288
+ config.pricing = document.pricing;
289
+ }
290
+ if (document.extensions !== undefined) {
291
+ if (!Array.isArray(document.extensions) || document.extensions.length === 0) {
292
+ throw new ConfigError('"extensions" must be a non-empty array of strings', source);
293
+ }
294
+ config.extensions = document.extensions.map((value) => {
295
+ if (typeof value !== 'string' || value.length < 2 || !value.startsWith('.')) {
296
+ throw new ConfigError(`"extensions" entries look like ".txt"; got ${JSON.stringify(value)}`, source);
297
+ }
298
+ return value.toLowerCase();
299
+ });
300
+ }
301
+ return config;
302
+ }
303
+ /**
304
+ * The budget that applies to a path, or null when no pattern matches.
305
+ *
306
+ * Returning the pattern alongside the number is the point: a file failing
307
+ * against a budget the reader cannot locate in their config is a bug report.
308
+ */
309
+ export function budgetFor(path, budgets) {
310
+ if (!budgets)
311
+ return null;
312
+ const pattern = mostSpecificMatch(Object.keys(budgets), path);
313
+ return pattern === null ? null : { pattern, maxTokens: budgets[pattern] };
314
+ }
315
+ //# sourceMappingURL=config-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-schema.js","sourceRoot":"","sources":["../src/config-schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { TrazumConfig } from './config-schema.js';
2
+ import type { PricingCatalogue } from './pricing.js';
3
+ /**
4
+ * Finding and reading `trazum.config.json`.
5
+ *
6
+ * Split from the schema in `config-schema.ts` because this is the only half
7
+ * that touches the filesystem, and the split is load-bearing: `apps/web`
8
+ * bundles `@trazum/core` for the browser, where a single `node:fs` import
9
+ * anywhere in the graph fails the build. Reachable only through
10
+ * `@trazum/core/node`, which the CLI imports and the web app does not.
11
+ */
12
+ export interface LoadedConfig {
13
+ config: TrazumConfig;
14
+ /** Absolute path the config came from, or null when none was found. */
15
+ path: string | null;
16
+ /**
17
+ * Prices to work from: the bundled catalogue, with the config's `pricing`
18
+ * overlay applied if it named one. Always present, so a caller never has to
19
+ * decide which of two sources to use.
20
+ */
21
+ pricing: PricingCatalogue;
22
+ /** Absolute path the overlay came from, or null when there was none. */
23
+ pricingPath: string | null;
24
+ }
25
+ export interface LoadConfigOptions {
26
+ /** Directory to start the upward search from. Defaults to the process cwd. */
27
+ from?: string;
28
+ /**
29
+ * An explicit path. Given one, no search happens and a missing file is an
30
+ * error: somebody who names a config file is not asking for defaults.
31
+ */
32
+ explicit?: string;
33
+ }
34
+ /**
35
+ * Finds and reads the nearest config file.
36
+ *
37
+ * The search walks **upward from the working directory**, which is what makes
38
+ * `cd packages/thing && trazum check prompt.txt` pick up the repository's
39
+ * config. It stops at the first hit, at a directory containing `.git`, or after
40
+ * `MAX_CONFIG_SEARCH_DEPTH` levels — a bound rather than an unlimited climb to
41
+ * the filesystem root.
42
+ *
43
+ * Finding nothing is not an error: the tool is useful with no config at all,
44
+ * and returning `{config: {}, path: null}` lets the caller say so.
45
+ */
46
+ export declare function loadConfig(options?: LoadConfigOptions): Promise<LoadedConfig>;
47
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,uEAAuE;IACvE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,gBAAgB,CAAC;IAC1B,wEAAwE;IACxE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AA4DD,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2BvF"}
package/dist/config.js ADDED
@@ -0,0 +1,112 @@
1
+ import { open, stat } from 'node:fs/promises';
2
+ import { dirname, join, resolve, sep } from 'node:path';
3
+ import { CONFIG_FILENAME, ConfigError, MAX_CONFIG_BYTES, MAX_CONFIG_SEARCH_DEPTH, parseConfig, validateConfigModel, } from './config-schema.js';
4
+ import { BUNDLED_CATALOGUE } from './pricing.js';
5
+ import { MAX_PRICING_BYTES, PricingOverlayError, applyPricingOverlay, parsePricingOverlay } from './pricing-overlay.js';
6
+ /**
7
+ * Resolves a config's `pricing` overlay, if it names one.
8
+ *
9
+ * The path is relative to **the config file**, not the working directory, so a
10
+ * config found by walking upward still finds its own overlay. Reusing
11
+ * `readIfPresent` means the overlay gets the same one-handle read and the same
12
+ * size limit as the config itself.
13
+ */
14
+ async function resolvePricing(config, configPath) {
15
+ if (!config.pricing)
16
+ return { pricing: BUNDLED_CATALOGUE, pricingPath: null };
17
+ const path = resolve(dirname(configPath), config.pricing);
18
+ const raw = await readIfPresent(path, MAX_PRICING_BYTES);
19
+ if (raw === null) {
20
+ // Named and missing is an error, for the same reason `--config` is: somebody
21
+ // who points at a price list is not asking for the bundled one.
22
+ throw new PricingOverlayError('no such pricing overlay', path);
23
+ }
24
+ return {
25
+ pricing: applyPricingOverlay(BUNDLED_CATALOGUE, parsePricingOverlay(raw, path), path),
26
+ pricingPath: path,
27
+ };
28
+ }
29
+ /**
30
+ * Reads a config file if it is there, or returns null.
31
+ *
32
+ * **The handle is opened once and everything is asked of the handle**, not of
33
+ * the path again. Checking the size with `stat(path)` and then reading with
34
+ * `readFile(path)` resolves the name twice, so what gets read is not
35
+ * necessarily what got measured — a symlink swapped in between the two calls
36
+ * defeats the size limit entirely, and on a CI runner checking out a pull
37
+ * request that is not a hypothetical attacker. One `open`, then `fh.stat()` and
38
+ * `fh.readFile()`, refer to the same file object throughout.
39
+ */
40
+ async function readIfPresent(path, limit = MAX_CONFIG_BYTES) {
41
+ let handle;
42
+ try {
43
+ handle = await open(path, 'r');
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ try {
49
+ const info = await handle.stat();
50
+ if (!info.isFile())
51
+ return null;
52
+ if (info.size > limit) {
53
+ throw new ConfigError(`is ${info.size} bytes, over the ${limit}-byte limit`, path);
54
+ }
55
+ return await handle.readFile('utf8');
56
+ }
57
+ finally {
58
+ await handle.close();
59
+ }
60
+ }
61
+ /**
62
+ * Finds and reads the nearest config file.
63
+ *
64
+ * The search walks **upward from the working directory**, which is what makes
65
+ * `cd packages/thing && trazum check prompt.txt` pick up the repository's
66
+ * config. It stops at the first hit, at a directory containing `.git`, or after
67
+ * `MAX_CONFIG_SEARCH_DEPTH` levels — a bound rather than an unlimited climb to
68
+ * the filesystem root.
69
+ *
70
+ * Finding nothing is not an error: the tool is useful with no config at all,
71
+ * and returning `{config: {}, path: null}` lets the caller say so.
72
+ */
73
+ export async function loadConfig(options = {}) {
74
+ if (options.explicit !== undefined) {
75
+ const path = resolve(options.explicit);
76
+ const raw = await readIfPresent(path);
77
+ if (raw === null)
78
+ throw new ConfigError('no such config file', path);
79
+ return finish(parseConfig(raw, path), path);
80
+ }
81
+ let directory = resolve(options.from ?? process.cwd());
82
+ for (let depth = 0; depth < MAX_CONFIG_SEARCH_DEPTH; depth++) {
83
+ const candidate = join(directory, CONFIG_FILENAME);
84
+ const raw = await readIfPresent(candidate);
85
+ if (raw !== null)
86
+ return finish(parseConfig(raw, candidate), candidate);
87
+ // A repository root is a deliberate stopping point: reaching past it would
88
+ // read a config belonging to whatever happens to be above the checkout,
89
+ // which on a CI runner is not the project's business.
90
+ const gitDirectory = await stat(join(directory, '.git')).catch(() => null);
91
+ if (gitDirectory)
92
+ break;
93
+ const parent = dirname(directory);
94
+ if (parent === directory || parent === sep)
95
+ break;
96
+ directory = parent;
97
+ }
98
+ return { config: {}, path: null, pricing: BUNDLED_CATALOGUE, pricingPath: null };
99
+ }
100
+ /**
101
+ * Resolves the overlay and checks the model, in that order.
102
+ *
103
+ * The order is the point: `usage.model` may name a model the overlay introduces,
104
+ * so validating it before the overlay is read would reject a config that is
105
+ * correct.
106
+ */
107
+ async function finish(config, path) {
108
+ const { pricing, pricingPath } = await resolvePricing(config, path);
109
+ validateConfigModel(config, pricing, path);
110
+ return { config, path, pricing, pricingPath };
111
+ }
112
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import type { ModelPricing } from './types.js';
2
+ /**
3
+ * Where a prompt is actually going.
4
+ *
5
+ * Trazum priced one vendor, so defaulting to Claude cost nothing. Pricing seven
6
+ * made that default a **wrong number**: `trazum check src/prompts.ts` billed a
7
+ * file that calls OpenAI against Claude Opus 5 and said so with a straight face.
8
+ * The fix is not a better default — it is reading what the code already says.
9
+ *
10
+ * Everything here is **evidence-first**. A detection that cannot name what it
11
+ * saw is a guess wearing a result's clothes, and this feeds a command used as a
12
+ * CI gate. Every answer carries the line it came from, so a wrong one is
13
+ * arguable rather than mysterious.
14
+ *
15
+ * **It declines when the file points two ways.** A module importing both
16
+ * `openai` and `@anthropic-ai/sdk` is a module Trazum cannot price without
17
+ * picking a side, and picking silently is how somebody budgets against the wrong
18
+ * provider for a month. The conflict is reported and the caller falls back to
19
+ * whatever they configured.
20
+ *
21
+ * Detection sits **between config and defaults** in the usual layering: a flag
22
+ * beats config, config beats detection, detection beats the built-in default.
23
+ * Reading the code is better than assuming, and worse than being told.
24
+ */
25
+ export type EvidenceKind =
26
+ /** `model: 'gpt-5'` — names the model outright, so nothing beats it. */
27
+ 'model-literal'
28
+ /** `from 'openai'` — names the provider but not which model. */
29
+ | 'sdk-import'
30
+ /** `https://api.deepseek.com` — a base URL pinned in the source. */
31
+ | 'base-url'
32
+ /** `// trazum:prompt name model=gpt-5` — the author said so directly. */
33
+ | 'marker';
34
+ export interface Evidence {
35
+ kind: EvidenceKind;
36
+ /** The text that produced it, so the reader can go and look. */
37
+ detail: string;
38
+ /** 1-based line, when it came from a specific place in the file. */
39
+ line?: number;
40
+ provider?: string;
41
+ model?: string;
42
+ }
43
+ export interface Detection {
44
+ /** The provider, or null when nothing was found or the file disagreed. */
45
+ provider: string | null;
46
+ /** The exact model, when something named one. */
47
+ model: string | null;
48
+ /** What was found, strongest first. Empty when nothing was. */
49
+ evidence: Evidence[];
50
+ /**
51
+ * Evidence pointing somewhere other than the answer.
52
+ *
53
+ * Non-empty means the file names more than one provider, and `provider` is
54
+ * null: two answers is not a weaker version of one answer.
55
+ */
56
+ conflicts: Evidence[];
57
+ }
58
+ export interface DetectOptions {
59
+ /** The catalogue to recognise model names from. */
60
+ models?: ModelPricing[];
61
+ }
62
+ /**
63
+ * Works out which provider a source file talks to, and says why.
64
+ *
65
+ * Returns `provider: null` both when nothing was found and when the file named
66
+ * more than one — the caller cannot act on either, and `conflicts` distinguishes
67
+ * them for the reader.
68
+ */
69
+ export declare function detectFromSource(source: string, options?: DetectOptions): Detection;
70
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,YAAY;AACtB,wEAAwE;AACtE,eAAe;AACjB,gEAAgE;GAC9D,YAAY;AACd,oEAAoE;GAClE,UAAU;AACZ,yEAAyE;GACvE,QAAQ,CAAC;AAEb,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,YAAY,CAAC;IACnB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iDAAiD;IACjD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,+DAA+D;IAC/D,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB;;;;;OAKG;IACH,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB;AA8JD,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,SAAS,CAiFvF"}