@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,455 @@
1
+ import { segment } from './segment.js';
2
+ import { OUTPUT_CUES } from './phrases.js';
3
+ import { jaccard, normalizeForCompare } from './similarity.js';
4
+ /**
5
+ * Verbs that mark a sentence as being about the *response*.
6
+ *
7
+ * Requiring one is what keeps "translate this into English" from being read as
8
+ * "answer in English". Without it the language axis fires on any prompt that
9
+ * merely mentions a language, which is most translation prompts.
10
+ */
11
+ const RESPOND = String.raw `(?:respond|reply|answer|write|speak|output|responde|contesta|responder|escribe|redacta)`;
12
+ const AXES = [
13
+ {
14
+ axis: 'response-language',
15
+ values: [
16
+ [
17
+ 'fixed-language',
18
+ new RegExp(String.raw `\b${RESPOND}\b[^.!?\n]{0,40}?\bin\s+(?:english|spanish|french|german|italian|portuguese|ingl[ée]s|espa[nñ]ol|franc[ée]s|alem[áa]n)\b|\b${RESPOND}\b[^.!?\n]{0,40}?\ben\s+(?:ingl[ée]s|espa[nñ]ol|franc[ée]s|alem[áa]n)\b`, 'i'),
19
+ ],
20
+ [
21
+ 'mirror-language',
22
+ new RegExp(
23
+ // The qualifier slot ("own", "native", "preferred") is what makes
24
+ // this survive real prompts: "the customer's own language" is at
25
+ // least as common as the bare form.
26
+ String.raw `\bin\s+the\s+(?:same\s+)?language\s+(?:of|as|the\s+\w+\s+(?:used|wrote|speaks))|\bin\s+(?:the\s+(?:user|customer|client)'?s?|their)\s+(?:own\s+|native\s+|preferred\s+)?language\b|\bmatch\s+the\s+language\b|\ben\s+(?:el\s+)?(?:mismo\s+)?idioma\s+(?:del?\s+|en\s+que\s+)?(?:usuario|cliente|consulta|mensaje|escrib)`, 'i'),
27
+ ],
28
+ ],
29
+ },
30
+ {
31
+ axis: 'output-format',
32
+ values: [
33
+ [
34
+ 'format-json',
35
+ new RegExp(String.raw `\b(?:${RESPOND}|return|format|devuelve|formatea)\b[^.!?\n]{0,40}?\b(?:as|in|with|using|only|en|como)\s+(?:valid\s+|solo\s+|válido\s+)?json\b|\bonly\s+json\b|\bsolo\s+json\b`, 'i'),
36
+ ],
37
+ [
38
+ 'format-markdown',
39
+ new RegExp(String.raw `\b(?:${RESPOND}|return|format|devuelve|formatea)\b[^.!?\n]{0,40}?\b(?:as|in|with|using|en|como)\s+markdown\b|\buse\s+markdown\b|\busa\s+markdown\b`, 'i'),
40
+ ],
41
+ [
42
+ 'format-plain-text',
43
+ new RegExp(String.raw `\bplain\s+text\b|\bno\s+markdown\b|\bwithout\s+markdown\b|\btexto\s+plano\b|\bsin\s+markdown\b`, 'i'),
44
+ ],
45
+ ],
46
+ },
47
+ {
48
+ axis: 'response-length',
49
+ values: [
50
+ [
51
+ 'length-brief',
52
+ new RegExp(String.raw `\b(?:be\s+(?:brief|concise|succinct|terse)|keep\s+it\s+(?:short|brief)|as\s+short\s+as\s+possible|in\s+(?:one|a\s+single)\s+sentence|s[ée]\s+(?:breve|conciso)|de\s+forma\s+(?:breve|concisa)|brevemente)\b`, 'i'),
53
+ ],
54
+ [
55
+ 'length-detailed',
56
+ new RegExp(String.raw `\b(?:be\s+(?:detailed|comprehensive|thorough|exhaustive)|in\s+(?:great\s+)?depth|as\s+much\s+detail\s+as\s+possible|s[ée]\s+(?:exhaustivo|detallado)|de\s+forma\s+(?:detallada|exhaustiva)|con\s+todo\s+detalle|detalladamente)\b`, 'i'),
57
+ ],
58
+ ],
59
+ },
60
+ {
61
+ axis: 'reasoning-visibility',
62
+ values: [
63
+ [
64
+ 'reasoning-shown',
65
+ new RegExp(String.raw `\b(?:explain\s+your\s+(?:reasoning|answer|thinking)|show\s+your\s+(?:work|reasoning|thinking)|think\s+step[\s-]by[\s-]step|justify\s+your\s+answer|explica\s+tu\s+razonamiento|razona\s+paso\s+a\s+paso|justifica\s+tu\s+respuesta)\b`, 'i'),
66
+ ],
67
+ [
68
+ 'reasoning-hidden',
69
+ new RegExp(String.raw `\b(?:no\s+(?:explanation|commentary|preamble)|without\s+(?:explanation|commentary)|do\s+not\s+explain|don'?t\s+explain|no\s+expliques|sin\s+(?:explicaciones|comentarios)|sin\s+preámbulo)\b`, 'i'),
70
+ ],
71
+ ],
72
+ },
73
+ ];
74
+ /**
75
+ * Text the analysis is allowed to read.
76
+ *
77
+ * Protected segments are replaced by a space rather than kept: a JSON schema
78
+ * inside a code fence is the *specification* of the output format, not a
79
+ * second instruction about it, and reading it as one would report a
80
+ * contradiction in every well-written prompt that shows its schema.
81
+ */
82
+ function analysableText(prompt) {
83
+ return segment(prompt)
84
+ .map((s) => (s.kind === 'mutable' ? s.text : ' '))
85
+ .join('');
86
+ }
87
+ /** Splits into sentences, keeping line breaks as boundaries. */
88
+ function sentences(text) {
89
+ return text
90
+ .split(/(?<=[.!?;:])\s+|\n+/)
91
+ .map((s) => s.trim())
92
+ .filter(Boolean);
93
+ }
94
+ function trimSnippet(sentence, max = 90) {
95
+ const clean = sentence.replace(/\s+/g, ' ').trim();
96
+ return clean.length <= max ? clean : `${clean.slice(0, max - 1)}…`;
97
+ }
98
+ /**
99
+ * Finds instructions that pull in opposite directions on the same axis.
100
+ *
101
+ * Only the first occurrence of each value is kept, and an axis reports at most
102
+ * one contradiction: a prompt that says "be concise" four times and "be
103
+ * thorough" once has one problem, not four.
104
+ *
105
+ * Two matches inside the same sentence are ignored. "Be concise but complete"
106
+ * is a deliberate trade-off an author wrote on purpose; flagging it would
107
+ * train people to ignore the advisory.
108
+ */
109
+ export function findContradictions(prompt) {
110
+ const found = [];
111
+ const lines = sentences(analysableText(prompt));
112
+ for (const { axis, values } of AXES) {
113
+ const hits = new Map();
114
+ lines.forEach((sentence, index) => {
115
+ for (const [value, pattern] of values) {
116
+ if (!hits.has(value) && pattern.test(sentence)) {
117
+ hits.set(value, { snippet: trimSnippet(sentence), sentence: index });
118
+ }
119
+ }
120
+ });
121
+ if (hits.size < 2)
122
+ continue;
123
+ const [first, second] = [...hits.entries()];
124
+ if (!first || !second)
125
+ continue;
126
+ if (first[1].sentence === second[1].sentence)
127
+ continue;
128
+ found.push({
129
+ axis,
130
+ a: { value: first[0], snippet: first[1].snippet },
131
+ b: { value: second[0], snippet: second[1].snippet },
132
+ });
133
+ }
134
+ return found;
135
+ }
136
+ // --------------------------------------------------------------------------
137
+ // Redundant few-shot examples
138
+ // --------------------------------------------------------------------------
139
+ /**
140
+ * The two halves of an example label, as bounded fragments.
141
+ *
142
+ * Every quantifier here has an upper bound, and that is load-bearing rather
143
+ * than tidiness. The previous form ended `\s*\d*\s*[:.)]` — three adjacent
144
+ * unbounded quantifiers over overlapping character classes. On a line like
145
+ * `example` followed by 40 000 spaces and no terminator, the engine has to try
146
+ * every way of splitting that whitespace between the two `\s*` groups before
147
+ * it can fail: measured O(n²), 651 ms at 40 000 spaces, roughly a minute at
148
+ * the API's 400 KB cap — a denial of service in one request.
149
+ *
150
+ * Bounding turns the split count into a constant. A label reading
151
+ * `Example 12:` never needs more than a few spaces or six digits, so nothing
152
+ * real is lost.
153
+ *
154
+ * `[ \t]` rather than `\s` because these are matched per line, where `\s`
155
+ * matching newlines is wrong anyway.
156
+ */
157
+ const LABEL_PREFIX = String.raw `^[ \t]{0,16}(?:#{1,6}[ \t]{0,4})?(?:[-*][ \t]{0,4})?`;
158
+ const LABEL_SUFFIX = String.raw `[ \t]{0,4}\d{0,6}[ \t]{0,4}[:.)]`;
159
+ /**
160
+ * Header styles that open a new example, most explicit first.
161
+ *
162
+ * Tiered rather than combined because they nest: a prompt using "Example 1:"
163
+ * almost always puts "Input:"/"Output:" *inside* each example, so matching
164
+ * both at once would cut every example in half and compare the halves. The
165
+ * first tier that finds at least two blocks wins.
166
+ *
167
+ * Deliberately conservative overall: an unlabelled example is
168
+ * indistinguishable from ordinary prose, and guessing would make the advisory
169
+ * fire on prompts containing no examples at all.
170
+ */
171
+ const EXAMPLE_HEADER_TIERS = [
172
+ new RegExp(`${LABEL_PREFIX}(?:example|ejemplo)${LABEL_SUFFIX}`, 'i'),
173
+ new RegExp(`${LABEL_PREFIX}(?:input|entrada)${LABEL_SUFFIX}`, 'i'),
174
+ new RegExp(`${LABEL_PREFIX}(?:user|usuario|q)${LABEL_SUFFIX}`, 'i'),
175
+ ];
176
+ /**
177
+ * A labelled field inside a few-shot example — `Input:`, `Output:`, `A:`…
178
+ *
179
+ * Exported because the duplicate-line rule needs it: two examples that share
180
+ * an output line are demonstrating that two different inputs map to the *same*
181
+ * answer, which is often the whole point of including both. Deduplicating
182
+ * those lines leaves an example with no output at all — worse than the
183
+ * repetition it removed.
184
+ */
185
+ export const EXAMPLE_FIELD_LINE = new RegExp(`${LABEL_PREFIX}(?:input|output|entrada|salida|example|ejemplo|user|usuario|assistant|asistente|q|a|question|answer|pregunta|respuesta)${LABEL_SUFFIX}`, 'i');
186
+ /**
187
+ * Similarity at which two examples are treated as repeating each other.
188
+ *
189
+ * Lower than the 0.92 the near-duplicate *rule* uses, because that rule
190
+ * deletes and this only reports.
191
+ *
192
+ * What this threshold actually catches, measured on realistic pairs: a
193
+ * copy-pasted example with one field changed scores ~0.89, a lightly edited
194
+ * copy ~0.80, two genuinely different examples ~0.20. A *paraphrase* — the
195
+ * same lesson taught in different words — scores ~0.54 and is deliberately
196
+ * NOT caught, because it sits close enough to the distinct case that catching
197
+ * it would mean flagging examples that teach different things.
198
+ *
199
+ * So this finds copy-paste accumulation, which is how few-shot blocks actually
200
+ * grow, and not semantic redundancy. Recognising that "arrived quickly" and
201
+ * "arrived fast" teach the same thing needs a model, not a word-set overlap;
202
+ * that belongs to the optional LLM pass, not here.
203
+ */
204
+ const EXAMPLE_SIMILARITY = 0.7;
205
+ /** Splits the prompt on one header style. */
206
+ function splitOnHeader(lines, header) {
207
+ const blocks = [];
208
+ let current = null;
209
+ let inFence = false;
210
+ for (const line of lines) {
211
+ // A header inside a fenced block is part of the example, not a new one.
212
+ if (/^\s*(?:```|~~~)/.test(line))
213
+ inFence = !inFence;
214
+ if (!inFence && header.test(line)) {
215
+ current = [line];
216
+ blocks.push(current);
217
+ continue;
218
+ }
219
+ if (current)
220
+ current.push(line);
221
+ }
222
+ return blocks.map((block) => trimToExample(block.join('\n'))).filter(Boolean);
223
+ }
224
+ /**
225
+ * Cuts a block off where the example stops and ordinary prose resumes.
226
+ *
227
+ * Without this the final example runs to the end of the prompt and absorbs
228
+ * every instruction that follows it, which inflates its length and drags its
229
+ * similarity to the others below any sensible threshold — so the one block
230
+ * most likely to be a duplicate is the one that never gets reported.
231
+ *
232
+ * A paragraph belongs to the example while it still contains a labelled field;
233
+ * the first that does not ends the block.
234
+ */
235
+ function trimToExample(block) {
236
+ const paragraphs = block.split(/\n\s*\n/);
237
+ const kept = [paragraphs[0] ?? ''];
238
+ for (const paragraph of paragraphs.slice(1)) {
239
+ const isExampleContent = paragraph
240
+ .split('\n')
241
+ .some((line) => EXAMPLE_FIELD_LINE.test(line));
242
+ if (!isExampleContent)
243
+ break;
244
+ kept.push(paragraph);
245
+ }
246
+ return kept.join('\n\n').trim();
247
+ }
248
+ /** Splits the prompt into labelled example blocks. */
249
+ export function findExamples(prompt, count) {
250
+ const lines = prompt.split('\n');
251
+ for (const header of EXAMPLE_HEADER_TIERS) {
252
+ const blocks = splitOnHeader(lines, header);
253
+ if (blocks.length >= 2) {
254
+ return blocks.map((text) => ({ text, tokens: count(text) }));
255
+ }
256
+ }
257
+ return [];
258
+ }
259
+ /**
260
+ * Reports examples that repeat what an earlier example already demonstrates.
261
+ *
262
+ * Each example is compared against every earlier one, and blamed on the first
263
+ * match: in a run of three near-identical examples the second and third are
264
+ * both reported against the first, rather than chaining.
265
+ */
266
+ export function analyzeExamples(prompt, count) {
267
+ const examples = findExamples(prompt, count);
268
+ const normalized = examples.map((e) => normalizeForCompare(e.text));
269
+ const redundant = [];
270
+ for (let i = 1; i < examples.length; i++) {
271
+ for (let j = 0; j < i; j++) {
272
+ const similarity = jaccard(normalized[i], normalized[j]);
273
+ if (similarity >= EXAMPLE_SIMILARITY) {
274
+ redundant.push({
275
+ duplicateOf: j,
276
+ index: i,
277
+ similarity,
278
+ tokens: examples[i].tokens,
279
+ });
280
+ break;
281
+ }
282
+ }
283
+ }
284
+ return {
285
+ examples,
286
+ redundant,
287
+ redundantTokens: redundant.reduce((sum, r) => sum + r.tokens, 0),
288
+ };
289
+ }
290
+ /** Fenced blocks, with their info string. */
291
+ function fencedBlocks(prompt) {
292
+ const blocks = [];
293
+ const fence = /^([ \t]{0,3})(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)^\1\2[ \t]*$/gm;
294
+ let match;
295
+ while ((match = fence.exec(prompt)) !== null) {
296
+ blocks.push({ lang: (match[3] ?? '').trim().toLowerCase(), body: match[4] ?? '' });
297
+ }
298
+ return blocks;
299
+ }
300
+ /**
301
+ * Top-level keys of a JSON-ish block.
302
+ *
303
+ * Deliberately a scan rather than `JSON.parse`: schemas in prompts are
304
+ * routinely illustrative — trailing commas, `...`, comments, a placeholder
305
+ * where a value goes — and refusing to read those would skip exactly the
306
+ * prompts worth checking. Only keys at nesting depth 1 count, so a nested
307
+ * field name cannot be mistaken for a top-level one.
308
+ */
309
+ function topLevelKeys(body) {
310
+ const keys = [];
311
+ let depth = 0;
312
+ let inString = false;
313
+ let quote = '';
314
+ let current = '';
315
+ for (let i = 0; i < body.length; i++) {
316
+ const ch = body[i];
317
+ if (inString) {
318
+ if (ch === '\\') {
319
+ current += body[i + 1] ?? '';
320
+ i++;
321
+ continue;
322
+ }
323
+ if (ch === quote) {
324
+ inString = false;
325
+ // A string is a key only when the next non-space character is a colon.
326
+ const rest = body.slice(i + 1);
327
+ const colon = /^\s*:/.test(rest);
328
+ if (colon && depth === 1 && current)
329
+ keys.push(current);
330
+ current = '';
331
+ continue;
332
+ }
333
+ current += ch;
334
+ continue;
335
+ }
336
+ if (ch === '"' || ch === "'") {
337
+ inString = true;
338
+ quote = ch;
339
+ current = '';
340
+ continue;
341
+ }
342
+ if (ch === '{' || ch === '[')
343
+ depth++;
344
+ else if (ch === '}' || ch === ']')
345
+ depth--;
346
+ }
347
+ return [...new Set(keys)];
348
+ }
349
+ /** Minimum keys that must be restated before this is worth reporting. */
350
+ const RESTATED_KEY_MINIMUM = 3;
351
+ /**
352
+ * Finds an output schema that the prose repeats.
353
+ *
354
+ * The threshold is deliberately blunt: naming one or two fields in prose is
355
+ * ordinary ("set `escalate` to true when the customer asks for a human"), and
356
+ * flagging that would make the advisory noise. Three or more, and the prose is
357
+ * walking the schema.
358
+ */
359
+ export function findRestatedFormat(prompt, count) {
360
+ const blocks = fencedBlocks(prompt);
361
+ const keys = [
362
+ ...new Set(blocks
363
+ .filter((b) => b.lang === '' || /json|jsonc|json5|yaml|yml/.test(b.lang))
364
+ .flatMap((b) => topLevelKeys(b.body))),
365
+ ].filter((k) => k.length >= 3);
366
+ if (keys.length < RESTATED_KEY_MINIMUM)
367
+ return null;
368
+ // Prose only: the schema naming its own keys is not a restatement.
369
+ const prose = analysableText(prompt);
370
+ const proseSentences = sentences(prose);
371
+ const restatedKeys = new Set();
372
+ const guiltySentences = new Set();
373
+ proseSentences.forEach((sentence, index) => {
374
+ for (const key of keys) {
375
+ // Word boundary that also survives snake_case and kebab-case names.
376
+ const pattern = new RegExp(`(?<![\\p{L}\\p{N}_-])${escapeRegExp(key)}(?![\\p{L}\\p{N}_-])`, 'iu');
377
+ if (pattern.test(sentence)) {
378
+ restatedKeys.add(key);
379
+ guiltySentences.add(index);
380
+ }
381
+ }
382
+ });
383
+ if (restatedKeys.size < RESTATED_KEY_MINIMUM)
384
+ return null;
385
+ const restatedTokens = count([...guiltySentences].map((i) => proseSentences[i]).join(' '));
386
+ return {
387
+ keys,
388
+ restatedKeys: [...restatedKeys],
389
+ restatedTokens,
390
+ };
391
+ }
392
+ function escapeRegExp(text) {
393
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
394
+ }
395
+ /** How far back to look for a cue introducing a block. */
396
+ const CUE_WINDOW = 240;
397
+ /** Fenced blocks with their offset, needed to read what precedes them. */
398
+ function fencedBlocksAt(prompt) {
399
+ const blocks = [];
400
+ const fence = /^([ \t]{0,3})(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)^\1\2[ \t]*$/gm;
401
+ let match;
402
+ while ((match = fence.exec(prompt)) !== null) {
403
+ blocks.push({
404
+ lang: (match[3] ?? '').trim().toLowerCase(),
405
+ body: match[4] ?? '',
406
+ at: match.index,
407
+ raw: match[0],
408
+ });
409
+ }
410
+ return blocks;
411
+ }
412
+ export function findMovableSchema(prompt, count) {
413
+ const cued = [];
414
+ for (const block of fencedBlocksAt(prompt)) {
415
+ // Explicitly JSON-ish, or unlabelled but structured enough to be a shape.
416
+ // A labelled `python` block is somebody's code and never an output contract.
417
+ const jsonish = /^(json|jsonc|json5)$/.test(block.lang) || block.lang === '';
418
+ if (!jsonish)
419
+ continue;
420
+ /**
421
+ * No minimum key *length* here, unlike the restated-format detector.
422
+ * That filter exists there to stop a two-letter key matching a word in
423
+ * prose; nothing is matched against prose here, so all it would do is
424
+ * undercount schemas whose fields are called `id` or `ok`. It was copied
425
+ * across in the first draft and a mutation run found it: deleting it changed
426
+ * no test, which is what a line with no reason looks like.
427
+ */
428
+ const keys = topLevelKeys(block.body);
429
+ // Fewer than three keys is an illustration, not a contract worth moving —
430
+ // the same threshold the restated-format advisory uses, for the same reason.
431
+ if (keys.length < RESTATED_KEY_MINIMUM)
432
+ continue;
433
+ /**
434
+ * The window before the fence, normalised the way every other comparison in
435
+ * this package normalises: case folded, accents stripped, punctuation
436
+ * flattened. `Formato de salida:` and `FORMATO DE SALIDA —` are the same cue,
437
+ * and a dictionary that only matched one of them would cover Spanish on
438
+ * paper.
439
+ */
440
+ const lead = normalizeForCompare(prompt.slice(Math.max(0, block.at - CUE_WINDOW), block.at));
441
+ const cue = OUTPUT_CUES.find((phrase) => lead.includes(normalizeForCompare(phrase)));
442
+ if (cue === undefined)
443
+ continue;
444
+ cued.push({ raw: block.raw, keys, cue });
445
+ }
446
+ if (cued.length === 0)
447
+ return null;
448
+ return {
449
+ blocks: cued.length,
450
+ keys: [...new Set(cued.flatMap((block) => block.keys))],
451
+ tokens: count(cued.map((block) => block.raw).join('\n\n')),
452
+ cue: cued[0]?.cue ?? '',
453
+ };
454
+ }
455
+ //# sourceMappingURL=structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.js","sourceRoot":"","sources":["../src/structure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import type { Locale } from './i18n/types.js';
2
+ import type { LlmProvider, TokenCounter } from './types.js';
3
+ /**
4
+ * Rewrites the rules cannot do, proposed one phrase at a time.
5
+ *
6
+ * The existing LLM pass (`refineWithLlm`) hands the model the whole prompt and
7
+ * takes the whole answer back, which makes it all-or-nothing: when the result
8
+ * fails a safety check the author gets *nothing*, and when it passes they get a
9
+ * wholesale rewrite they have to read end to end to trust. Both halves of that
10
+ * are worse than they need to be.
11
+ *
12
+ * This asks a different question. "Which exact phrases in this prompt say
13
+ * something in more words than they need to?" — and the answer is a list of
14
+ * `before → after` pairs, each one small enough to judge on sight:
15
+ *
16
+ * You should always make sure to → Always
17
+ * It is important to note that → (removed)
18
+ *
19
+ * Nothing is applied unless the caller asks. Eight surviving suggestions out of
20
+ * ten is a useful result; a wholesale rewrite that fails one check is not.
21
+ *
22
+ * ## What makes a suggestion survive
23
+ *
24
+ * The model is a source of proposals, not of truth, so every one is checked
25
+ * against the prompt before it is shown:
26
+ *
27
+ * 1. **`before` must appear in the prompt, byte for byte.** A model that
28
+ * paraphrases what it is quoting has invented a suggestion about text that
29
+ * does not exist, and applying it would do nothing or, worse, match
30
+ * something else.
31
+ * 2. **It must not touch protected content.** Code, URLs, placeholders and XML
32
+ * tags are copied verbatim by every other part of this project, and a
33
+ * suggestion that edits one is refused rather than negotiated.
34
+ * 3. **`after` must not introduce protected content.** A replacement that adds
35
+ * a `{{placeholder}}` or a URL is proposing new semantics, not shorter
36
+ * phrasing.
37
+ * 4. **It must actually save tokens.** A rephrasing that costs the same is a
38
+ * change of style, and this tool is not a style guide.
39
+ * 5. **Overlapping suggestions are dropped, later ones first.** Applying two
40
+ * edits that share characters produces text neither of them described.
41
+ */
42
+ export interface RewriteSuggestion {
43
+ /** The exact text in the prompt, as it appears there. */
44
+ before: string;
45
+ /** What to put in its place. Empty means "delete this". */
46
+ after: string;
47
+ /** Character offsets in the prompt, one per surviving occurrence. */
48
+ offsets: number[];
49
+ /** Tokens saved if every occurrence is applied. */
50
+ tokensSaved: number;
51
+ }
52
+ export interface SuggestResult {
53
+ suggestions: RewriteSuggestion[];
54
+ /** Suggestions the model returned that did not survive, with the reason. */
55
+ rejected: Array<{
56
+ before: string;
57
+ after: string;
58
+ reason: RejectedReason;
59
+ }>;
60
+ provider: string;
61
+ model: string;
62
+ }
63
+ export type RejectedReason =
64
+ /** `before` is not in the prompt. The model paraphrased what it quoted. */
65
+ 'not-found'
66
+ /** It would edit a code block, URL, placeholder or tag. */
67
+ | 'touches-protected'
68
+ /** `after` introduces protected content that was not there. */
69
+ | 'introduces-protected'
70
+ /** No shorter than what it replaces. */
71
+ | 'no-saving'
72
+ /** It shares characters with a suggestion already accepted. */
73
+ | 'overlaps';
74
+ export declare const SUGGEST_SYSTEM_PROMPT = "You find phrases in a prompt that say something in more words than they need.\n\nReturn ONLY a JSON array. Each element is {\"before\": \"...\", \"after\": \"...\"}.\n\nRules:\n- \"before\" MUST be copied character for character from the prompt. Do not paraphrase it, do not fix its punctuation, do not change its capitalisation. If you cannot copy it exactly, leave it out.\n- \"after\" says the same thing in fewer words. Use \"\" to delete the phrase entirely.\n- Preserve meaning exactly. Never change what the prompt asks for, its constraints, its output format or its success criteria.\n- Never touch code, URLs, template placeholders ({{x}}, ${x}, {x}) or XML/HTML tags \u2014 do not include them in \"before\" at all.\n- Keep the original language of the prompt.\n- Prefer a few high-value rewrites to many trivial ones. Return [] if there is nothing worth changing.\n\nNo explanation, no code fences, no commentary. The array alone.";
75
+ export interface SuggestOptions {
76
+ tokenCounter?: TokenCounter;
77
+ locale?: Locale;
78
+ /** Cap on how many survive, highest saving first. Defaults to 20. */
79
+ max?: number;
80
+ }
81
+ /**
82
+ * Asks the provider for rewrites and returns the ones that survive checking.
83
+ *
84
+ * A provider that returns something unparseable yields an empty result rather
85
+ * than throwing: the deterministic rules have already run, and a malformed
86
+ * answer from an optional pass should cost the caller nothing.
87
+ */
88
+ export declare function suggestRewrites(prompt: string, provider: LlmProvider, options?: SuggestOptions): Promise<SuggestResult>;
89
+ /**
90
+ * Applies suggestions to the prompt.
91
+ *
92
+ * Right to left, so an earlier edit cannot move the offsets of a later one —
93
+ * the bug that makes every naive implementation of this corrupt long prompts,
94
+ * and one that shows up only when two suggestions are far enough apart that a
95
+ * short test never notices.
96
+ */
97
+ export declare function applyRewrites(prompt: string, suggestions: readonly RewriteSuggestion[]): string;
98
+ /** A reason code turned into a sentence, for a report. */
99
+ export declare function rejectionText(reason: RejectedReason, locale: Locale): string;
100
+ //# sourceMappingURL=suggest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../src/suggest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAG9C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,cAAc,CAAA;KAAE,CAAC,CAAC;IAC3E,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,cAAc;AACxB,2EAA2E;AACzE,WAAW;AACb,2DAA2D;GACzD,mBAAmB;AACrB,+DAA+D;GAC7D,sBAAsB;AACxB,wCAAwC;GACtC,WAAW;AACb,+DAA+D;GAC7D,UAAU,CAAC;AAEf,eAAO,MAAM,qBAAqB,k7BAY8B,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAwCD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,WAAW,EACrB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAiFxB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,iBAAiB,EAAE,GAAG,MAAM,CAU/F;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE5E"}