@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,198 @@
1
+ import { normalizeForCompare } from './similarity.js';
2
+ import { estimateTokens } from './tokenizer.js';
3
+ import type { TokenCounter } from './types.js';
4
+
5
+ /**
6
+ * The waste no single prompt can see.
7
+ *
8
+ * Every other analysis in this package reads one prompt. This one reads a
9
+ * workspace, because there is a kind of caching loss that only exists *between*
10
+ * prompts: forty prompts assembled from the same system preamble, byte-identical
11
+ * except that one has a trailing space, another reordered two bullets, and a
12
+ * third says "You are an assistant" where the rest say "You are an Assistant".
13
+ *
14
+ * Prompt caching is a byte-for-byte prefix match. Those forty prompts therefore
15
+ * occupy forty separate cache entries and share nothing. Unify the preamble and
16
+ * they occupy one. No amount of analysis of any one of those files can find that
17
+ * — each is individually fine — which is what makes this worth a module rather
18
+ * than another advisory.
19
+ *
20
+ * **It reports no dollar figure, and that is a finding rather than a gap.**
21
+ *
22
+ * The saving lives entirely in the cache hit rate: a prompt called ten times a
23
+ * month never warms an entry of its own and always reads a shared one that
24
+ * somebody else keeps hot. But `cacheHitRate` is an *input* to Trazum's cost
25
+ * model, not an output — `advisories.ts` takes it from `--cache-hit-rate` and
26
+ * applies the same value to every prompt. Under that model, splitting one prefix
27
+ * into forty changes nothing, because the model has no term for how many distinct
28
+ * cache entries exist.
29
+ *
30
+ * Pricing this would mean inventing a distribution of calls across the group,
31
+ * which is the one thing here that only the operator knows. So the report gives
32
+ * what it can establish — which prompts, how far the common prefix runs, and what
33
+ * stops it being byte-identical — and refuses the number. Naming a mechanism
34
+ * accurately beats attaching a figure to it that came from nowhere.
35
+ */
36
+
37
+ /** Blocks are blank-line separated, the same unit `reorder.ts` moves. */
38
+ function blocksOf(text: string): string[] {
39
+ return text.split(/\n[ \t]*\n/);
40
+ }
41
+
42
+ /**
43
+ * Whitespace collapsed, and nothing else touched.
44
+ *
45
+ * Used to separate the two kinds of drift, because they are different amounts of
46
+ * work to fix. A prefix that differs only here is a formatter away from sharing a
47
+ * cache entry; one that differs in wording needs somebody to decide which wording
48
+ * is right.
49
+ */
50
+ function collapseWhitespace(text: string): string {
51
+ return text.replace(/\s+/g, ' ').trim();
52
+ }
53
+
54
+ /**
55
+ * The prefix length below which unifying buys nothing, and `Infinity` when there
56
+ * is no answer.
57
+ *
58
+ * Lives here rather than in the CLI because it is the gate on this whole report
59
+ * and a gate with no test is decoration. It was a private function in the CLI
60
+ * first; a mutation run deleting the `unknown` branch survived, which is what a
61
+ * function nothing can reach looks like from the outside.
62
+ *
63
+ * `unknown` and a missing minimum both yield `Infinity`, so nothing is reported.
64
+ * That is the direction to fail: telling somebody to unify a preamble across
65
+ * twelve files to enable caching their provider may not offer spends their
66
+ * afternoon, and unlike a wrong number on a report nothing later corrects it.
67
+ */
68
+ export function cacheableMinimum(
69
+ model: { caching?: string | null; cacheMinTokens?: number | null } | undefined,
70
+ ): number {
71
+ if (!model) return Number.POSITIVE_INFINITY;
72
+ if (model.caching === 'none' || model.caching === 'unknown') return Number.POSITIVE_INFINITY;
73
+ return model.cacheMinTokens ?? Number.POSITIVE_INFINITY;
74
+ }
75
+
76
+ export interface PrefixCandidate {
77
+ /** How the report names it. A path, or a path with a marked prompt's id. */
78
+ path: string;
79
+ text: string;
80
+ }
81
+
82
+ export interface SharedPrefix {
83
+ /** Every prompt in the group, in the order they were given. */
84
+ paths: string[];
85
+ /** Blocks the group has in common, counted from the start. */
86
+ blocks: number;
87
+ /** Estimated tokens in that prefix, as the first prompt writes it. */
88
+ tokens: number;
89
+ /**
90
+ * What stops the prefixes being byte-identical today.
91
+ *
92
+ * `whitespace` — collapsing runs of whitespace makes them equal, so a
93
+ * formatter fixes it and no wording decision is needed.
94
+ * `wording` — something else differs: capitalisation, punctuation, word order,
95
+ * or genuinely different words. Someone has to choose.
96
+ */
97
+ drift: 'whitespace' | 'wording';
98
+ /** The shared prefix as the first prompt in the group writes it. */
99
+ sample: string;
100
+ }
101
+
102
+ export interface SharedPrefixOptions {
103
+ /**
104
+ * The model's cacheable minimum. A shared prefix below it buys nothing, so it
105
+ * is not reported — the same refusal `reorder.ts` makes, for the same reason:
106
+ * a change that recovers nothing is a diff for its own sake.
107
+ */
108
+ minTokens?: number;
109
+ countTokens?: TokenCounter;
110
+ }
111
+
112
+ /**
113
+ * Groups of prompts that could share a cache prefix and do not.
114
+ *
115
+ * Grouped by their **first** block, which is not a shortcut. Caching matches from
116
+ * the beginning of the request, so two prompts whose opening paragraphs differ
117
+ * share nothing no matter how identical the rest is — a group keyed on anything
118
+ * later would name prompts that cannot be made to share a prefix without
119
+ * reordering them, which is a different and far more dangerous change.
120
+ */
121
+ export function sharedPrefixes(
122
+ prompts: readonly PrefixCandidate[],
123
+ options: SharedPrefixOptions = {},
124
+ ): SharedPrefix[] {
125
+ const counter = options.countTokens ?? estimateTokens;
126
+ const minTokens = options.minTokens ?? 0;
127
+
128
+ const byOpening = new Map<string, PrefixCandidate[]>();
129
+ for (const prompt of prompts) {
130
+ const blocks = blocksOf(prompt.text);
131
+ const opening = normalizeForCompare(blocks[0] ?? '');
132
+ // An empty opening is not a match, it is an absence. Grouping on it would
133
+ // put every blank-led prompt in one group and report a prefix of nothing.
134
+ if (opening === '') continue;
135
+ const group = byOpening.get(opening);
136
+ if (group) group.push(prompt);
137
+ else byOpening.set(opening, [prompt]);
138
+ }
139
+
140
+ const found: SharedPrefix[] = [];
141
+
142
+ for (const group of byOpening.values()) {
143
+ if (group.length < 2) continue;
144
+
145
+ const blockLists = group.map((prompt) => blocksOf(prompt.text));
146
+ const first = blockLists[0];
147
+ if (!first) continue;
148
+
149
+ // How far the agreement runs, comparing normalised blocks position by
150
+ // position. Stops at the first block any member disagrees on, because a
151
+ // prefix is contiguous by definition.
152
+ let shared = 0;
153
+ for (let index = 0; index < first.length; index++) {
154
+ const target = normalizeForCompare(first[index] ?? '');
155
+ if (target === '') break;
156
+ const all = blockLists.every((blocks) => normalizeForCompare(blocks[index] ?? '') === target);
157
+ if (!all) break;
158
+ shared = index + 1;
159
+ }
160
+ if (shared === 0) continue;
161
+
162
+ /**
163
+ * The raw prefixes, joined the way they were written.
164
+ *
165
+ * `\n\n` rather than the original separator: the separator is itself part of
166
+ * what may differ, and reconstructing each prompt's exact bytes is not needed
167
+ * to answer whether they agree.
168
+ */
169
+ const rawPrefixes = blockLists.map((blocks) => blocks.slice(0, shared).join('\n\n'));
170
+ const sample = rawPrefixes[0] ?? '';
171
+
172
+ // Already byte-identical: these prompts share a cache entry today and there
173
+ // is nothing to recover. Reporting them would be noise that teaches people
174
+ // to stop reading this section.
175
+ if (rawPrefixes.every((prefix) => prefix === sample)) continue;
176
+
177
+ const collapsed = rawPrefixes.map(collapseWhitespace);
178
+ const flat = collapseWhitespace(sample);
179
+ const drift = collapsed.every((prefix) => prefix === flat) ? 'whitespace' : 'wording';
180
+
181
+ const tokens = counter(sample);
182
+ if (tokens < minTokens) continue;
183
+
184
+ found.push({
185
+ paths: group.map((prompt) => prompt.path),
186
+ blocks: shared,
187
+ tokens,
188
+ drift,
189
+ sample,
190
+ });
191
+ }
192
+
193
+ // Largest prefix first: it is the group where unifying recovers most, and the
194
+ // ordering is a total one so the output does not depend on Map iteration.
195
+ return found.sort(
196
+ (a, b) => b.tokens - a.tokens || (a.paths[0] ?? '').localeCompare(b.paths[0] ?? ''),
197
+ );
198
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Text-similarity helpers shared by the duplicate rules and the structural
3
+ * analysis.
4
+ *
5
+ * Both need to answer "are these two chunks saying the same thing?", and both
6
+ * need to answer it the same way — a rule that removes a near-duplicate and an
7
+ * advisory that reports one should never disagree about what "near" means.
8
+ */
9
+
10
+ /** Normalises a line or paragraph for duplicate comparison. */
11
+ export function normalizeForCompare(text: string): string {
12
+ return text
13
+ .toLowerCase()
14
+ .normalize('NFD')
15
+ .replace(/\p{Diacritic}/gu, '')
16
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
17
+ .trim();
18
+ }
19
+
20
+ /** Jaccard similarity over word sets. */
21
+ export function jaccard(a: string, b: string): number {
22
+ const setA = new Set(a.split(' ').filter(Boolean));
23
+ const setB = new Set(b.split(' ').filter(Boolean));
24
+ if (setA.size === 0 || setB.size === 0) return 0;
25
+ let intersection = 0;
26
+ for (const word of setA) if (setB.has(word)) intersection++;
27
+ return intersection / (setA.size + setB.size - intersection);
28
+ }