@trazum/core 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/advisories.d.ts +23 -0
- package/dist/advisories.d.ts.map +1 -0
- package/dist/advisories.js +376 -0
- package/dist/advisories.js.map +1 -0
- package/dist/aws-sigv4.d.ts +88 -0
- package/dist/aws-sigv4.d.ts.map +1 -0
- package/dist/aws-sigv4.js +117 -0
- package/dist/aws-sigv4.js.map +1 -0
- package/dist/baseline.d.ts +171 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +273 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +28 -0
- package/dist/cache.js.map +1 -0
- package/dist/changes.d.ts +29 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +142 -0
- package/dist/changes.js.map +1 -0
- package/dist/compare.d.ts +65 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +58 -0
- package/dist/compare.js.map +1 -0
- package/dist/config-schema.d.ts +118 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +315 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +112 -0
- package/dist/config.js.map +1 -0
- package/dist/detect.d.ts +70 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +228 -0
- package/dist/detect.js.map +1 -0
- package/dist/evaluate.d.ts +98 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +110 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/extract.d.ts +81 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +280 -0
- package/dist/extract.js.map +1 -0
- package/dist/gcp-auth.d.ts +58 -0
- package/dist/gcp-auth.d.ts.map +1 -0
- package/dist/gcp-auth.js +113 -0
- package/dist/gcp-auth.js.map +1 -0
- package/dist/glob.d.ts +49 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +154 -0
- package/dist/glob.js.map +1 -0
- package/dist/host.d.ts +30 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +69 -0
- package/dist/host.js.map +1 -0
- package/dist/i18n/en.d.ts +4 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +168 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/es.d.ts +4 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +168 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/index.d.ts +36 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +50 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +180 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +11 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/llm.d.ts +226 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +485 -0
- package/dist/llm.js.map +1 -0
- package/dist/nearest.d.ts +20 -0
- package/dist/nearest.d.ts.map +1 -0
- package/dist/nearest.js +54 -0
- package/dist/nearest.js.map +1 -0
- package/dist/net.d.ts +90 -0
- package/dist/net.d.ts.map +1 -0
- package/dist/net.js +203 -0
- package/dist/net.js.map +1 -0
- package/dist/node.d.ts +32 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +33 -0
- package/dist/node.js.map +1 -0
- package/dist/openrouter.d.ts +25 -0
- package/dist/openrouter.d.ts.map +1 -0
- package/dist/openrouter.js +72 -0
- package/dist/openrouter.js.map +1 -0
- package/dist/optimize.d.ts +38 -0
- package/dist/optimize.d.ts.map +1 -0
- package/dist/optimize.js +183 -0
- package/dist/optimize.js.map +1 -0
- package/dist/otlp.d.ts +91 -0
- package/dist/otlp.d.ts.map +1 -0
- package/dist/otlp.js +102 -0
- package/dist/otlp.js.map +1 -0
- package/dist/phrases.d.ts +169 -0
- package/dist/phrases.d.ts.map +1 -0
- package/dist/phrases.js +939 -0
- package/dist/phrases.js.map +1 -0
- package/dist/pricing-overlay.d.ts +55 -0
- package/dist/pricing-overlay.d.ts.map +1 -0
- package/dist/pricing-overlay.js +241 -0
- package/dist/pricing-overlay.js.map +1 -0
- package/dist/pricing.d.ts +115 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +400 -0
- package/dist/pricing.js.map +1 -0
- package/dist/profile.d.ts +71 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +55 -0
- package/dist/profile.js.map +1 -0
- package/dist/promptfoo.d.ts +58 -0
- package/dist/promptfoo.d.ts.map +1 -0
- package/dist/promptfoo.js +149 -0
- package/dist/promptfoo.js.map +1 -0
- package/dist/prune.d.ts +91 -0
- package/dist/prune.d.ts.map +1 -0
- package/dist/prune.js +110 -0
- package/dist/prune.js.map +1 -0
- package/dist/reorder.d.ts +82 -0
- package/dist/reorder.d.ts.map +1 -0
- package/dist/reorder.js +215 -0
- package/dist/reorder.js.map +1 -0
- package/dist/review.d.ts +54 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +131 -0
- package/dist/review.js.map +1 -0
- package/dist/rules.d.ts +5 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +279 -0
- package/dist/rules.js.map +1 -0
- package/dist/savings.d.ts +36 -0
- package/dist/savings.d.ts.map +1 -0
- package/dist/savings.js +83 -0
- package/dist/savings.js.map +1 -0
- package/dist/segment.d.ts +8 -0
- package/dist/segment.d.ts.map +1 -0
- package/dist/segment.js +74 -0
- package/dist/segment.js.map +1 -0
- package/dist/shared-prefix.d.ts +63 -0
- package/dist/shared-prefix.d.ts.map +1 -0
- package/dist/shared-prefix.js +151 -0
- package/dist/shared-prefix.js.map +1 -0
- package/dist/similarity.d.ts +13 -0
- package/dist/similarity.d.ts.map +1 -0
- package/dist/similarity.js +30 -0
- package/dist/similarity.js.map +1 -0
- package/dist/structure.d.ts +144 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +455 -0
- package/dist/structure.js.map +1 -0
- package/dist/suggest.d.ts +100 -0
- package/dist/suggest.d.ts.map +1 -0
- package/dist/suggest.js +151 -0
- package/dist/suggest.js.map +1 -0
- package/dist/tokenizer.d.ts +57 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +157 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/walk.d.ts +40 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +95 -0
- package/dist/walk.js.map +1 -0
- package/package.json +56 -0
- package/src/advisories.ts +431 -0
- package/src/aws-sigv4.ts +174 -0
- package/src/baseline.ts +390 -0
- package/src/cache.ts +54 -0
- package/src/changes.ts +158 -0
- package/src/compare.ts +131 -0
- package/src/config-schema.ts +451 -0
- package/src/config.ts +161 -0
- package/src/detect.ts +312 -0
- package/src/evaluate.ts +188 -0
- package/src/extract.ts +336 -0
- package/src/gcp-auth.ts +166 -0
- package/src/glob.ts +160 -0
- package/src/host.ts +90 -0
- package/src/i18n/en.ts +236 -0
- package/src/i18n/es.ts +236 -0
- package/src/i18n/index.ts +68 -0
- package/src/i18n/types.ts +230 -0
- package/src/index.ts +228 -0
- package/src/llm.ts +708 -0
- package/src/nearest.ts +61 -0
- package/src/net.ts +233 -0
- package/src/node.ts +63 -0
- package/src/openrouter.ts +125 -0
- package/src/optimize.ts +228 -0
- package/src/otlp.ts +179 -0
- package/src/phrases.ts +1047 -0
- package/src/pricing-overlay.ts +319 -0
- package/src/pricing.ts +468 -0
- package/src/profile.ts +124 -0
- package/src/promptfoo.ts +213 -0
- package/src/prune.ts +211 -0
- package/src/reorder.ts +307 -0
- package/src/review.ts +180 -0
- package/src/rules.ts +324 -0
- package/src/savings.ts +121 -0
- package/src/segment.ts +106 -0
- package/src/shared-prefix.ts +198 -0
- package/src/similarity.ts +28 -0
- package/src/structure.ts +652 -0
- package/src/suggest.ts +254 -0
- package/src/tokenizer.ts +190 -0
- package/src/types.ts +323 -0
- package/src/walk.ts +117 -0
|
@@ -0,0 +1,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
|
+
}
|