@far-world-labs/verblets 0.1.1 → 0.1.2

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 (300) hide show
  1. package/.cursor/launch.json +30 -0
  2. package/.cursor/settings.json +20 -0
  3. package/.github/workflows/branch-protection.yml +22 -0
  4. package/.github/workflows/ci.yml +117 -0
  5. package/.prettierrc +6 -0
  6. package/.release-it.json +4 -1
  7. package/.vscode/launch.json +31 -0
  8. package/AGENTS.md +220 -0
  9. package/DEVELOPING.md +105 -0
  10. package/README.md +671 -0
  11. package/eslint.config.js +80 -0
  12. package/package.json +28 -16
  13. package/scripts/generate-test/index.js +29 -3
  14. package/scripts/runner/index.js +26 -0
  15. package/scripts/simple-editor/index.js +29 -18
  16. package/scripts/summarize-files/index.js +28 -4
  17. package/src/chains/README.md +30 -0
  18. package/src/chains/anonymize/README.md +21 -0
  19. package/src/chains/anonymize/index.examples.js +75 -0
  20. package/src/chains/anonymize/index.js +121 -0
  21. package/src/chains/anonymize/index.spec.js +78 -0
  22. package/src/chains/bulk-central-tendency/index.examples.js +138 -0
  23. package/src/chains/bulk-central-tendency/index.js +91 -0
  24. package/src/chains/bulk-filter/README.md +21 -0
  25. package/src/chains/bulk-filter/index.examples.js +22 -0
  26. package/src/chains/bulk-filter/index.js +58 -0
  27. package/src/chains/bulk-filter/index.spec.js +38 -0
  28. package/src/chains/bulk-find/README.md +16 -0
  29. package/src/chains/bulk-find/index.examples.js +20 -0
  30. package/src/chains/bulk-find/index.js +30 -0
  31. package/src/chains/bulk-find/index.spec.js +26 -0
  32. package/src/chains/bulk-group/README.md +23 -0
  33. package/src/chains/bulk-group/index.examples.js +18 -0
  34. package/src/chains/bulk-group/index.js +34 -0
  35. package/src/chains/bulk-group/index.spec.js +41 -0
  36. package/src/chains/bulk-map/README.md +43 -0
  37. package/src/chains/bulk-map/index.examples.js +17 -0
  38. package/src/chains/bulk-map/index.js +86 -0
  39. package/src/chains/bulk-map/index.spec.js +44 -0
  40. package/src/chains/bulk-reduce/README.md +12 -0
  41. package/src/chains/bulk-reduce/index.examples.js +15 -0
  42. package/src/chains/bulk-reduce/index.js +13 -0
  43. package/src/chains/bulk-reduce/index.spec.js +25 -0
  44. package/src/chains/bulk-score/README.md +16 -0
  45. package/src/chains/bulk-score/bulk-score-result.json +18 -0
  46. package/src/chains/bulk-score/index.examples.js +22 -0
  47. package/src/chains/bulk-score/index.js +133 -0
  48. package/src/chains/bulk-score/index.spec.js +30 -0
  49. package/src/chains/category-samples/README.md +61 -0
  50. package/src/chains/category-samples/index.examples.js +103 -0
  51. package/src/chains/category-samples/index.js +134 -0
  52. package/src/chains/collect-terms/README.md +12 -0
  53. package/src/chains/collect-terms/index.examples.js +16 -0
  54. package/src/chains/collect-terms/index.js +44 -0
  55. package/src/chains/collect-terms/index.spec.js +25 -0
  56. package/src/chains/date/README.md +12 -0
  57. package/src/chains/date/index.examples.js +47 -0
  58. package/src/chains/date/index.js +74 -0
  59. package/src/chains/date/index.spec.js +62 -0
  60. package/src/chains/disambiguate/README.md +22 -0
  61. package/src/chains/disambiguate/disambiguate-meanings-result.json +16 -0
  62. package/src/chains/disambiguate/index.examples.js +18 -0
  63. package/src/chains/disambiguate/index.js +92 -0
  64. package/src/chains/disambiguate/index.spec.js +25 -0
  65. package/src/chains/dismantle/README.md +67 -0
  66. package/src/chains/dismantle/dismantle.examples.js +27 -0
  67. package/src/chains/dismantle/index.js +6 -17
  68. package/src/chains/dismantle/index.spec.js +1 -2
  69. package/src/chains/expect/README.md +171 -0
  70. package/src/chains/expect/index.examples.js +146 -0
  71. package/src/chains/expect/index.js +173 -0
  72. package/src/chains/expect/index.spec.js +324 -0
  73. package/src/chains/filter-ambiguous/README.md +11 -0
  74. package/src/chains/filter-ambiguous/index.examples.js +20 -0
  75. package/src/chains/filter-ambiguous/index.js +49 -0
  76. package/src/chains/filter-ambiguous/index.spec.js +31 -0
  77. package/src/chains/glossary/README.md +19 -0
  78. package/src/chains/glossary/index.examples.js +386 -0
  79. package/src/chains/glossary/index.js +75 -0
  80. package/src/chains/glossary/index.spec.js +19 -0
  81. package/src/chains/intersections/README.md +152 -0
  82. package/src/chains/intersections/index.examples.js +279 -0
  83. package/src/chains/intersections/index.js +366 -0
  84. package/src/chains/intersections/intersection-result.json +38 -0
  85. package/src/chains/list/index.examples.js +12 -16
  86. package/src/chains/list/index.js +106 -53
  87. package/src/chains/list/index.spec.js +3 -9
  88. package/src/chains/list/list-result.json +16 -0
  89. package/src/chains/llm-logger/README.md +208 -0
  90. package/src/chains/llm-logger/index.js +205 -0
  91. package/src/chains/llm-logger/index.spec.js +330 -0
  92. package/src/chains/questions/index.examples.js +2 -1
  93. package/src/chains/questions/index.js +14 -15
  94. package/src/chains/scan-js/index.js +6 -9
  95. package/src/chains/set-interval/README.md +81 -0
  96. package/src/chains/set-interval/index.examples.js +36 -0
  97. package/src/chains/set-interval/index.js +131 -0
  98. package/src/chains/set-interval/index.spec.js +70 -0
  99. package/src/chains/socratic/README.md +17 -0
  100. package/src/chains/socratic/index.js +64 -0
  101. package/src/chains/socratic/index.spec.js +24 -0
  102. package/src/chains/sort/index.examples.js +3 -7
  103. package/src/chains/sort/index.js +65 -15
  104. package/src/chains/sort/index.spec.js +5 -8
  105. package/src/chains/sort/sort-result.json +16 -0
  106. package/src/chains/summary-map/README.md +9 -1
  107. package/src/chains/summary-map/index.examples.js +9 -2
  108. package/src/chains/summary-map/index.js +43 -25
  109. package/src/chains/summary-map/index.spec.js +78 -3
  110. package/src/chains/test/index.js +9 -13
  111. package/src/chains/test-advice/index.js +4 -5
  112. package/src/chains/themes/README.md +20 -0
  113. package/src/chains/themes/index.examples.js +17 -0
  114. package/src/chains/themes/index.js +28 -0
  115. package/src/chains/themes/index.spec.js +19 -0
  116. package/src/chains/veiled-variants/index.examples.js +18 -0
  117. package/src/chains/veiled-variants/index.js +107 -0
  118. package/src/chains/veiled-variants/index.spec.js +40 -0
  119. package/src/constants/common.js +0 -2
  120. package/src/constants/models.js +172 -0
  121. package/src/index.js +178 -18
  122. package/src/json-schemas/README.md +13 -0
  123. package/src/json-schemas/index.js +8 -14
  124. package/src/json-schemas/schema-dot-org-photograph.json +11 -5
  125. package/src/json-schemas/schema-dot-org-place.json +78 -5
  126. package/src/lib/README.md +26 -0
  127. package/src/lib/bulk-filter/README.md +22 -0
  128. package/src/lib/bulk-filter/index.examples.js +27 -0
  129. package/src/lib/bulk-filter/index.js +63 -0
  130. package/src/lib/bulk-filter/index.spec.js +38 -0
  131. package/src/lib/bulk-find/README.md +18 -0
  132. package/src/lib/bulk-find/index.examples.js +19 -0
  133. package/src/lib/bulk-find/index.js +30 -0
  134. package/src/lib/bulk-find/index.spec.js +41 -0
  135. package/src/lib/chatgpt/index.js +63 -43
  136. package/src/lib/combinations/index.js +30 -0
  137. package/src/lib/combinations/index.spec.js +23 -0
  138. package/src/lib/functional/index.js +28 -0
  139. package/src/lib/logger-service/index.js +32 -0
  140. package/src/lib/parse-js-parts/index.js +9 -21
  141. package/src/lib/parse-llm-list/README.md +39 -0
  142. package/src/lib/parse-llm-list/index.js +54 -0
  143. package/src/lib/parse-llm-list/index.spec.js +59 -0
  144. package/src/lib/path-aliases/index.js +1 -3
  145. package/src/lib/path-aliases/index.spec.js +2 -8
  146. package/src/lib/pave/index.js +4 -4
  147. package/src/lib/pave/index.spec.js +6 -3
  148. package/src/lib/prompt-cache/index.js +14 -10
  149. package/src/lib/retry/index.js +11 -8
  150. package/src/lib/ring-buffer/README.md +460 -0
  151. package/src/lib/ring-buffer/index.js +1074 -0
  152. package/src/lib/search-best-first/city-walk.spec.js +37 -0
  153. package/src/lib/search-best-first/index.js +42 -11
  154. package/src/lib/search-best-first/index.spec.js +35 -0
  155. package/src/lib/search-js-files/index.js +21 -41
  156. package/src/lib/search-js-files/scan-file.js +10 -21
  157. package/src/lib/shorten-text/index.js +2 -7
  158. package/src/lib/shorten-text/index.spec.js +3 -3
  159. package/src/lib/strip-response/index.js +2 -7
  160. package/src/lib/template-replace/index.js +23 -0
  161. package/src/lib/template-replace/index.spec.js +60 -0
  162. package/src/lib/to-date/index.js +11 -0
  163. package/src/lib/to-number/index.js +1 -1
  164. package/src/lib/transcribe/index.js +4 -4
  165. package/src/prompts/README.md +3 -1
  166. package/src/prompts/as-object-with-schema.js +3 -8
  167. package/src/prompts/as-schema-org-text.js +10 -2
  168. package/src/prompts/code-features.js +1 -5
  169. package/src/prompts/constants.js +27 -27
  170. package/src/prompts/generate-collection.js +1 -1
  171. package/src/prompts/intent.js +11 -16
  172. package/src/prompts/select-from-threshold.js +1 -2
  173. package/src/prompts/sort.js +4 -8
  174. package/src/prompts/style.js +4 -7
  175. package/src/prompts/wrap-list.js +1 -4
  176. package/src/services/llm-model/global-overrides.spec.js +432 -0
  177. package/src/services/llm-model/index.js +234 -40
  178. package/src/services/llm-model/model.js +2 -2
  179. package/src/services/llm-model/negotiate.spec.js +447 -0
  180. package/src/services/redis/index.js +70 -7
  181. package/src/test/setup.js +20 -0
  182. package/src/verblets/README.md +26 -0
  183. package/src/verblets/auto/index.examples.js +12 -9
  184. package/src/verblets/auto/index.js +10 -10
  185. package/src/verblets/auto/index.spec.js +4 -6
  186. package/src/verblets/bool/README.md +36 -0
  187. package/src/verblets/bool/index.examples.js +53 -1
  188. package/src/verblets/bool/index.js +6 -9
  189. package/src/verblets/bool/index.spec.js +1 -3
  190. package/src/verblets/central-tendency/README.md +166 -0
  191. package/src/verblets/central-tendency/central-tendency-result.json +24 -0
  192. package/src/verblets/central-tendency/index.examples.js +196 -0
  193. package/src/verblets/central-tendency/index.js +171 -0
  194. package/src/verblets/central-tendency/index.spec.js +148 -0
  195. package/src/verblets/enum/index.examples.js +1 -4
  196. package/src/verblets/enum/index.js +7 -4
  197. package/src/verblets/expect/README.md +64 -0
  198. package/src/verblets/expect/index.examples.js +109 -0
  199. package/src/verblets/expect/index.js +75 -0
  200. package/src/verblets/expect/index.spec.js +127 -0
  201. package/src/verblets/intent/index.examples.js +84 -1
  202. package/src/verblets/intent/index.js +56 -68
  203. package/src/verblets/intersection/README.md +16 -0
  204. package/src/verblets/intersection/index.examples.js +89 -0
  205. package/src/verblets/intersection/index.js +84 -0
  206. package/src/verblets/intersection/index.spec.js +60 -0
  207. package/src/verblets/intersection/intersection-result.json +16 -0
  208. package/src/verblets/list-expand/README.md +10 -0
  209. package/src/verblets/list-expand/index.examples.js +14 -0
  210. package/src/verblets/list-expand/index.js +104 -0
  211. package/src/verblets/list-expand/index.spec.js +18 -0
  212. package/src/verblets/list-expand/list-expand-result.json +16 -0
  213. package/src/verblets/list-filter/README.md +22 -0
  214. package/src/verblets/list-filter/index.examples.js +26 -0
  215. package/src/verblets/list-filter/index.js +18 -0
  216. package/src/verblets/list-filter/index.spec.js +19 -0
  217. package/src/verblets/list-find/README.md +11 -0
  218. package/src/verblets/list-find/index.examples.js +15 -0
  219. package/src/verblets/list-find/index.js +17 -0
  220. package/src/verblets/list-find/index.spec.js +19 -0
  221. package/src/verblets/list-group/README.md +16 -0
  222. package/src/verblets/list-group/index.examples.js +16 -0
  223. package/src/verblets/list-group/index.js +112 -0
  224. package/src/verblets/list-group/index.spec.js +35 -0
  225. package/src/verblets/list-group/list-group-result.json +16 -0
  226. package/src/verblets/list-map/README.md +11 -0
  227. package/src/verblets/list-map/index.examples.js +15 -0
  228. package/src/verblets/list-map/index.js +26 -0
  229. package/src/verblets/list-map/index.spec.js +17 -0
  230. package/src/verblets/list-reduce/README.md +10 -0
  231. package/src/verblets/list-reduce/index.examples.js +14 -0
  232. package/src/verblets/list-reduce/index.js +21 -0
  233. package/src/verblets/list-reduce/index.spec.js +27 -0
  234. package/src/verblets/list-reduce/index.spec.jsx +27 -0
  235. package/src/verblets/name/README.md +15 -0
  236. package/src/verblets/name/index.examples.js +28 -0
  237. package/src/verblets/name/index.js +19 -0
  238. package/src/verblets/name/index.spec.js +33 -0
  239. package/src/verblets/name-similar-to/README.md +26 -0
  240. package/src/verblets/name-similar-to/index.examples.js +18 -0
  241. package/src/verblets/name-similar-to/index.js +20 -0
  242. package/src/verblets/name-similar-to/index.spec.js +13 -0
  243. package/src/verblets/number/index.examples.js +173 -7
  244. package/src/verblets/number/index.js +5 -2
  245. package/src/verblets/number/index.spec.js +1 -3
  246. package/src/verblets/number-with-units/index.examples.js +5 -1
  247. package/src/verblets/number-with-units/index.js +74 -9
  248. package/src/verblets/number-with-units/number-with-units-result.json +23 -0
  249. package/src/verblets/schema-org/index.examples.js +2 -7
  250. package/src/verblets/schema-org/index.js +32 -3
  251. package/src/verblets/sentiment/README.md +10 -0
  252. package/src/verblets/sentiment/index.examples.js +20 -0
  253. package/src/verblets/sentiment/index.js +9 -0
  254. package/src/verblets/sentiment/index.spec.js +20 -0
  255. package/src/verblets/to-object/index.js +10 -15
  256. package/src/verblets/to-object/index.spec.js +1 -4
  257. package/.eslintrc.json +0 -42
  258. package/docs/README.md +0 -41
  259. package/docs/babel.config.js +0 -3
  260. package/docs/blog/2019-05-28-first-blog-post.md +0 -12
  261. package/docs/blog/2019-05-29-long-blog-post.md +0 -44
  262. package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -20
  263. package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  264. package/docs/blog/2021-08-26-welcome/index.md +0 -25
  265. package/docs/blog/authors.yml +0 -17
  266. package/docs/docs/api/bool.md +0 -74
  267. package/docs/docs/api/search.md +0 -51
  268. package/docs/docs/intro.md +0 -47
  269. package/docs/docs/tutorial-basics/_category_.json +0 -8
  270. package/docs/docs/tutorial-basics/congratulations.md +0 -23
  271. package/docs/docs/tutorial-basics/create-a-blog-post.md +0 -34
  272. package/docs/docs/tutorial-basics/create-a-document.md +0 -57
  273. package/docs/docs/tutorial-basics/create-a-page.md +0 -43
  274. package/docs/docs/tutorial-basics/deploy-your-site.md +0 -31
  275. package/docs/docs/tutorial-basics/markdown-features.mdx +0 -152
  276. package/docs/docs/tutorial-extras/_category_.json +0 -7
  277. package/docs/docs/tutorial-extras/img/docsVersionDropdown.png +0 -0
  278. package/docs/docs/tutorial-extras/img/localeDropdown.png +0 -0
  279. package/docs/docs/tutorial-extras/manage-docs-versions.md +0 -55
  280. package/docs/docs/tutorial-extras/translate-your-site.md +0 -88
  281. package/docs/docusaurus.config.js +0 -120
  282. package/docs/package.json +0 -44
  283. package/docs/sidebars.js +0 -31
  284. package/docs/src/components/HomepageFeatures/index.js +0 -61
  285. package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  286. package/docs/src/css/custom.css +0 -30
  287. package/docs/src/pages/index.js +0 -43
  288. package/docs/src/pages/index.module.css +0 -23
  289. package/docs/src/pages/markdown-page.md +0 -7
  290. package/docs/static/.nojekyll +0 -0
  291. package/docs/static/img/docusaurus-social-card.jpg +0 -0
  292. package/docs/static/img/docusaurus.png +0 -0
  293. package/docs/static/img/favicon.ico +0 -0
  294. package/docs/static/img/logo.svg +0 -1
  295. package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  296. package/docs/static/img/undraw_docusaurus_react.svg +0 -170
  297. package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  298. package/src/constants/openai.js +0 -65
  299. /package/{.vite.config.examples.js → .vitest.config.examples.js} +0 -0
  300. /package/{.vite.config.js → .vitest.config.js} +0 -0
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { anonymize, anonymizeMethod } from './index.js';
3
+
4
+ vi.mock('./index.js', () => {
5
+ return {
6
+ anonymize: vi.fn(async (input) => {
7
+ if (!input || typeof input.text !== 'string' || !input.text.trim()) {
8
+ throw new Error('Text is required');
9
+ }
10
+ if (!input.method) {
11
+ throw new Error('Method is required');
12
+ }
13
+ if (!['STRICT', 'BALANCED', 'LIGHT'].includes(input.method)) {
14
+ throw new Error('Invalid method');
15
+ }
16
+ return {
17
+ text: 'anonymized',
18
+ stages: {
19
+ distinctiveContentRemoved: true,
20
+ structureNormalized: true,
21
+ patternsSuppressed: true,
22
+ },
23
+ };
24
+ }),
25
+ anonymizeMethod: { STRICT: 'STRICT', BALANCED: 'BALANCED', LIGHT: 'LIGHT' },
26
+ };
27
+ });
28
+
29
+ describe('anonymize', () => {
30
+ it('should return an object with text and stages properties', async () => {
31
+ const input = {
32
+ text: 'Test input',
33
+ method: anonymizeMethod.LIGHT,
34
+ };
35
+
36
+ const result = await anonymize(input);
37
+
38
+ expect(result).to.have.property('text');
39
+ expect(result).to.have.property('stages');
40
+ expect(result.stages).to.have.property('distinctiveContentRemoved');
41
+ expect(result.stages).to.have.property('structureNormalized');
42
+ expect(result.stages).to.have.property('patternsSuppressed');
43
+ });
44
+
45
+ it('should throw an error if method is not provided', async () => {
46
+ const input = {
47
+ text: 'Test input',
48
+ };
49
+
50
+ await expect(anonymize(input)).rejects.toThrow('Method is required');
51
+ });
52
+
53
+ it('should throw an error if method is invalid', async () => {
54
+ const input = {
55
+ text: 'Test input',
56
+ method: 'INVALID',
57
+ };
58
+
59
+ await expect(anonymize(input)).rejects.toThrow('Invalid method');
60
+ });
61
+
62
+ it('should throw an error if text is not provided', async () => {
63
+ const input = {
64
+ method: anonymizeMethod.LIGHT,
65
+ };
66
+
67
+ await expect(anonymize(input)).rejects.toThrow('Text is required');
68
+ });
69
+
70
+ it('should throw an error if text is empty', async () => {
71
+ const input = {
72
+ text: '',
73
+ method: anonymizeMethod.LIGHT,
74
+ };
75
+
76
+ await expect(anonymize(input)).rejects.toThrow('Text is required');
77
+ });
78
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { longTestTimeout } from '../../constants/common.js';
3
+ import { expect as aiExpect } from '../expect/index.js';
4
+ import bulkCentralTendency from './index.js';
5
+
6
+ describe('Bulk Central Tendency Chain', () => {
7
+ it(
8
+ 'processes multiple fruit items with consistent results',
9
+ async () => {
10
+ const items = ['apple', 'orange', 'durian', 'jackfruit', 'banana'];
11
+ const seedItems = ['apple', 'orange', 'banana', 'grape', 'strawberry'];
12
+
13
+ const results = await bulkCentralTendency(items, seedItems, {
14
+ context: 'Common fruits found in grocery stores',
15
+ });
16
+
17
+ expect(results).toHaveLength(5);
18
+ expect(results.every((r) => r && typeof r.score === 'number')).toBe(true);
19
+ expect(results.every((r) => r && r.score >= 0 && r.score <= 1)).toBe(true);
20
+ expect(results.every((r) => r && typeof r.reason === 'string')).toBe(true);
21
+ expect(results.every((r) => r && typeof r.confidence === 'number')).toBe(true);
22
+
23
+ // Use expect-chain for loose verification
24
+ const [isValidCentralityScoring] = await aiExpect(
25
+ results,
26
+ undefined,
27
+ 'Do these centrality scores make sense? Common fruits like apple, orange, banana should have higher scores than exotic fruits like durian and jackfruit.'
28
+ );
29
+ expect(isValidCentralityScoring).toBe(true);
30
+ },
31
+ longTestTimeout
32
+ );
33
+
34
+ it(
35
+ 'handles tool centrality with core features',
36
+ async () => {
37
+ const items = ['hammer', 'screwdriver', 'wrench', 'pliers', 'chainsaw'];
38
+ const seedItems = ['hammer', 'screwdriver', 'wrench', 'saw', 'drill'];
39
+
40
+ const results = await bulkCentralTendency(items, seedItems, {
41
+ context: 'Hand tools for construction and repair',
42
+ coreFeatures: ['handheld', 'mechanical', 'durable'],
43
+ });
44
+
45
+ expect(results).toHaveLength(5);
46
+ expect(results.every((r) => r && typeof r.score === 'number')).toBe(true);
47
+ expect(results.every((r) => r && r.score >= 0 && r.score <= 1)).toBe(true);
48
+
49
+ // Use expect-chain for loose verification
50
+ const [isValidToolScoring] = await aiExpect(
51
+ results,
52
+ undefined,
53
+ 'Do these tool centrality scores make sense? Basic hand tools like hammer, screwdriver, wrench should have high scores, while chainsaw (power tool) should have a lower score.'
54
+ );
55
+ expect(isValidToolScoring).toBe(true);
56
+ },
57
+ longTestTimeout
58
+ );
59
+
60
+ it(
61
+ 'demonstrates context effects on centrality',
62
+ async () => {
63
+ const items = ['robin', 'eagle', 'penguin', 'ostrich'];
64
+ const seedItems = ['robin', 'sparrow', 'cardinal', 'blue jay'];
65
+
66
+ const results = await bulkCentralTendency(items, seedItems, {
67
+ context: 'Small songbirds commonly seen in backyards',
68
+ });
69
+
70
+ expect(results).toHaveLength(4);
71
+ expect(results.every((r) => r && typeof r.score === 'number')).toBe(true);
72
+
73
+ // Use expect-chain for loose verification
74
+ const [isValidBirdScoring] = await aiExpect(
75
+ results,
76
+ undefined,
77
+ 'Given the context of "small songbirds commonly seen in backyards", does robin have the highest centrality score, while penguin and ostrich have much lower scores?'
78
+ );
79
+ expect(isValidBirdScoring).toBe(true);
80
+ },
81
+ longTestTimeout
82
+ );
83
+
84
+ it(
85
+ 'manages retry logic for failed items',
86
+ async () => {
87
+ const items = ['cat', 'dog', 'elephant'];
88
+ const seedItems = ['cat', 'dog', 'rabbit', 'hamster'];
89
+
90
+ const results = await bulkCentralTendency(items, seedItems, {
91
+ maxAttempts: 2,
92
+ });
93
+
94
+ expect(results).toHaveLength(3);
95
+ expect(results.every((r) => r && typeof r.score === 'number')).toBe(true);
96
+
97
+ // Use expect-chain for loose verification
98
+ const [isValidPetScoring] = await aiExpect(
99
+ results,
100
+ undefined,
101
+ 'Are these reasonable centrality scores for pets, with cat and dog having higher scores than elephant?'
102
+ );
103
+ expect(isValidPetScoring).toBe(true);
104
+ },
105
+ longTestTimeout
106
+ );
107
+
108
+ it('handles empty input', async () => {
109
+ const results = await bulkCentralTendency([], ['apple', 'orange']);
110
+ expect(results).toEqual([]);
111
+ });
112
+
113
+ it('throws error for invalid seed items', async () => {
114
+ await expect(bulkCentralTendency(['apple'], [])).rejects.toThrow(
115
+ 'seedItems must be a non-empty array'
116
+ );
117
+ await expect(bulkCentralTendency(['apple'], null)).rejects.toThrow(
118
+ 'seedItems must be a non-empty array'
119
+ );
120
+ });
121
+
122
+ it(
123
+ 'processes large batches efficiently',
124
+ async () => {
125
+ const items = Array.from({ length: 15 }, (_, i) => `item${i + 1}`);
126
+ const seedItems = ['item1', 'item2', 'item3', 'item4', 'item5'];
127
+
128
+ const results = await bulkCentralTendency(items, seedItems, {
129
+ chunkSize: 3,
130
+ });
131
+
132
+ expect(results).toHaveLength(15);
133
+ expect(results.every((r) => r && typeof r.score === 'number')).toBe(true);
134
+ expect(results.every((r) => r && r.score >= 0 && r.score <= 1)).toBe(true);
135
+ },
136
+ longTestTimeout
137
+ );
138
+ });
@@ -0,0 +1,91 @@
1
+ import { bulkMapRetry } from '../bulk-map/index.js';
2
+ import { CENTRAL_TENDENCY_PROMPT } from '../../verblets/central-tendency/index.js';
3
+
4
+ /**
5
+ * Build instructions for bulk central tendency evaluation using the core verblet prompt
6
+ * @param {string[]} seedItems - Array of seed items for comparison
7
+ * @param {Object} config - Configuration options
8
+ * @returns {string} Instructions for the bulk mapper
9
+ */
10
+ function buildBulkCentralTendencyInstructions(seedItems, { context = '', coreFeatures = [] } = {}) {
11
+ const contextLine = context ? `Context: ${context}` : '';
12
+ const coreFeaturesLine =
13
+ coreFeatures.length > 0 ? `Core Features: ${coreFeatures.join(', ')}` : '';
14
+ const outputRequirementsLine = `OUTPUT FORMAT: Return exactly one compact JSON object per line (no line breaks within the JSON):
15
+ {"score": <number>, "reason": "<brief explanation>", "confidence": <number>}`;
16
+
17
+ // Use the core prompt with all variables replaced
18
+ const corePrompt = CENTRAL_TENDENCY_PROMPT.replace('{context}', contextLine)
19
+ .replace('{coreFeatures}', coreFeaturesLine)
20
+ .replace('{outputRequirements}', outputRequirementsLine);
21
+
22
+ return `For each item, evaluate its centrality among these category members: ${seedItems.join(
23
+ ', '
24
+ )}
25
+
26
+ ${corePrompt}`;
27
+ }
28
+
29
+ /**
30
+ * Process multiple items for central tendency evaluation in bulk with retry support.
31
+ * Uses the existing bulkMapRetry infrastructure for efficiency and reliability.
32
+ *
33
+ * @param {string[]} items - Array of items to evaluate
34
+ * @param {string[]} seedItems - Array of seed items for comparison
35
+ * @param {Object} [config={}] - Configuration options
36
+ * @param {string} [config.context=''] - Context description for evaluation
37
+ * @param {string[]} [config.coreFeatures=[]] - Known core/definitional features
38
+ * @param {string|Object} [config.llm='fastGoodCheap'] - LLM model to use
39
+ * @param {number} [config.chunkSize=5] - Batch size for processing
40
+ * @param {number} [config.maxAttempts=3] - Max retry attempts for failed items
41
+ * @returns {Promise<Array>} Array of central tendency results
42
+ */
43
+ export default async function bulkCentralTendency(items, seedItems, config = {}) {
44
+ if (!Array.isArray(items)) {
45
+ throw new Error('Items must be an array');
46
+ }
47
+
48
+ if (items.length === 0) {
49
+ return [];
50
+ }
51
+
52
+ if (!Array.isArray(seedItems) || seedItems.length === 0) {
53
+ throw new Error('seedItems must be a non-empty array');
54
+ }
55
+
56
+ const { chunkSize = 5, maxAttempts = 3, ...otherConfig } = config;
57
+
58
+ // Build instructions for the bulk mapper
59
+ const instructions = buildBulkCentralTendencyInstructions(seedItems, otherConfig);
60
+
61
+ // Use bulkMapRetry to handle all the complexity
62
+ const results = await bulkMapRetry(items, instructions, { chunkSize, maxAttempts });
63
+
64
+ // Parse JSON responses and handle any parsing errors
65
+ return results.map((result, _index) => {
66
+ if (result === undefined) {
67
+ return undefined;
68
+ }
69
+
70
+ try {
71
+ const parsed = JSON.parse(result);
72
+
73
+ // Validate the structure
74
+ if (
75
+ parsed &&
76
+ typeof parsed.score === 'number' &&
77
+ typeof parsed.reason === 'string' &&
78
+ typeof parsed.confidence === 'number'
79
+ ) {
80
+ return parsed;
81
+ } else {
82
+ return undefined;
83
+ }
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ });
88
+ }
89
+
90
+ // Export the retry version as well for consistency with other bulk processors
91
+ export const bulkCentralTendencyRetry = bulkCentralTendency;
@@ -0,0 +1,21 @@
1
+ # bulk-filter
2
+
3
+ Filter very long lists in manageable chunks using `listFilter`. Failed batches can be retried.
4
+
5
+ ```javascript
6
+ import bulkFilter from './index.js';
7
+
8
+ const diary = [
9
+ 'Walked the dog and bought milk.',
10
+ 'One day I hope to sail across the Atlantic.',
11
+ 'Cleaned out the garage.',
12
+ "Maybe I'll start that bakery I keep dreaming about.",
13
+ ];
14
+
15
+ const aspirations = await bulkFilter(
16
+ diary,
17
+ 'Keep only lines about hopes or big dreams',
18
+ { chunkSize: 2 }
19
+ );
20
+ // => ['One day I hope to sail across the Atlantic.', "Maybe I'll start that bakery I keep dreaming about."]
21
+ ```
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import bulkFilter from './index.js';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulk-filter examples', () => {
6
+ it(
7
+ 'filters with listFilter',
8
+ async () => {
9
+ const notes = [
10
+ 'Saw a dolphin while surfing',
11
+ 'Finished laundry',
12
+ 'Dream of traveling to Iceland',
13
+ 'Paid the electricity bill',
14
+ ];
15
+ const dreams = await bulkFilter(notes, 'keep only lines about aspirations or dreams', {
16
+ chunkSize: 2,
17
+ });
18
+ expect(dreams.length).toBeGreaterThan(0);
19
+ },
20
+ longTestTimeout
21
+ );
22
+ });
@@ -0,0 +1,58 @@
1
+ import listFilter from '../../verblets/list-filter/index.js';
2
+
3
+ const buildMask = async (list, instructions, chunkSize, config = {}) => {
4
+ const mask = new Array(list.length);
5
+ for (let i = 0; i < list.length; i += chunkSize) {
6
+ const batch = list.slice(i, i + chunkSize);
7
+ try {
8
+ // eslint-disable-next-line no-await-in-loop
9
+ const result = await listFilter(batch, instructions, config);
10
+ const valid = result.every((item) => batch.includes(item));
11
+ if (!valid) {
12
+ for (let j = 0; j < batch.length; j += 1) {
13
+ mask[i + j] = undefined;
14
+ }
15
+ continue;
16
+ }
17
+ for (let j = 0; j < batch.length; j += 1) {
18
+ mask[i + j] = result.includes(batch[j]);
19
+ }
20
+ } catch {
21
+ for (let j = 0; j < batch.length; j += 1) {
22
+ mask[i + j] = undefined;
23
+ }
24
+ }
25
+ }
26
+ return mask;
27
+ };
28
+
29
+ export const bulkFilterRetry = async (list, instructions, config = {}) => {
30
+ const { chunkSize = 10, maxAttempts = 3, llm, ...options } = config;
31
+ const filterConfig = { llm, ...options };
32
+ let mask = await buildMask(list, instructions, chunkSize, filterConfig);
33
+ for (let attempt = 1; attempt < maxAttempts; attempt += 1) {
34
+ const missingIdx = [];
35
+ const missingItems = [];
36
+ mask.forEach((val, idx) => {
37
+ if (val === undefined) {
38
+ missingIdx.push(idx);
39
+ missingItems.push(list[idx]);
40
+ }
41
+ });
42
+ if (missingItems.length === 0) break;
43
+ // eslint-disable-next-line no-await-in-loop
44
+ const retryMask = await buildMask(missingItems, instructions, chunkSize, filterConfig);
45
+ retryMask.forEach((val, i) => {
46
+ if (val !== undefined) {
47
+ mask[missingIdx[i]] = val;
48
+ }
49
+ });
50
+ }
51
+ return list.filter((_, idx) => mask[idx]);
52
+ };
53
+
54
+ export default async function bulkFilter(list, instructions, config = {}) {
55
+ const { chunkSize = 10, llm, ...options } = config;
56
+ const mask = await buildMask(list, instructions, chunkSize, { llm, ...options });
57
+ return list.filter((_, idx) => mask[idx]);
58
+ }
@@ -0,0 +1,38 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import bulkFilter, { bulkFilterRetry } from './index.js';
3
+ import listFilter from '../../verblets/list-filter/index.js';
4
+
5
+ vi.mock('../../verblets/list-filter/index.js', () => ({
6
+ default: vi.fn(async (items, instructions) => {
7
+ if (items.includes('FAIL')) throw new Error('fail');
8
+ return items.filter((l) => l.includes(instructions));
9
+ }),
10
+ }));
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ describe('bulk-filter', () => {
17
+ it('filters items in batches', async () => {
18
+ const result = await bulkFilter(['a', 'b', 'c'], 'a', { chunkSize: 2 });
19
+ expect(result).toStrictEqual(['a']);
20
+ expect(listFilter).toHaveBeenCalledTimes(2);
21
+ });
22
+
23
+ it('retries failed batches', async () => {
24
+ let call = 0;
25
+ listFilter.mockImplementation(async (items) => {
26
+ call += 1;
27
+ if (call === 1) throw new Error('fail');
28
+ return items.filter((l) => l.includes('a'));
29
+ });
30
+
31
+ const result = await bulkFilterRetry(['FAIL', 'a', 'b'], 'a', {
32
+ chunkSize: 2,
33
+ maxAttempts: 2,
34
+ });
35
+ expect(result).toStrictEqual(['a']);
36
+ expect(listFilter).toHaveBeenCalledTimes(3);
37
+ });
38
+ });
@@ -0,0 +1,16 @@
1
+ # bulk-find
2
+
3
+ Scan long lists in manageable batches to locate the item that best matches your instructions.
4
+
5
+ ```javascript
6
+ import bulkFind from './index.js';
7
+
8
+ const emails = [
9
+ 'update from accounting',
10
+ 'party invitation',
11
+ 'weekly newsletter',
12
+ // ... potentially thousands more
13
+ ];
14
+ const best = await bulkFind(emails, 'Which email is most urgent?');
15
+ // => 'update from accounting'
16
+ ```
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import bulkFind from './index.js';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulk-find examples', () => {
6
+ it(
7
+ 'finds the best match across batches',
8
+ async () => {
9
+ const titles = [
10
+ 'ancient mystery',
11
+ 'space odyssey',
12
+ 'underwater adventure',
13
+ 'future tech thriller',
14
+ ];
15
+ const result = await bulkFind(titles, 'Which title feels most futuristic?', { chunkSize: 2 });
16
+ expect(result).toBeDefined();
17
+ },
18
+ longTestTimeout
19
+ );
20
+ });
@@ -0,0 +1,30 @@
1
+ import listFind from '../../verblets/list-find/index.js';
2
+
3
+ export const bulkFind = async function (list, instructions, config = {}) {
4
+ const { chunkSize = 10, llm, ...options } = config;
5
+ let candidate = '';
6
+ for (let i = 0; i < list.length; i += chunkSize) {
7
+ const batch = list.slice(i, i + chunkSize);
8
+ const combined = candidate ? [candidate, ...batch] : batch;
9
+ // eslint-disable-next-line no-await-in-loop
10
+ candidate = await listFind(combined, instructions, { llm, ...options });
11
+ }
12
+ return candidate;
13
+ };
14
+
15
+ export const bulkFindRetry = async function (list, instructions, config = {}) {
16
+ const { chunkSize = 10, maxAttempts = 3, llm, ...options } = config;
17
+ let result;
18
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
19
+ try {
20
+ // eslint-disable-next-line no-await-in-loop
21
+ result = await bulkFind(list, instructions, { chunkSize, llm, ...options });
22
+ if (result) break;
23
+ } catch {
24
+ // continue
25
+ }
26
+ }
27
+ return result;
28
+ };
29
+
30
+ export default bulkFind;
@@ -0,0 +1,26 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import bulkFind, { bulkFindRetry } from './index.js';
3
+ import listFind from '../../verblets/list-find/index.js';
4
+
5
+ vi.mock('../../verblets/list-find/index.js', () => ({
6
+ default: vi.fn(async (items) => items[items.length - 1]),
7
+ }));
8
+
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+ });
12
+
13
+ describe('bulk-find chain', () => {
14
+ it('scans batches to find best item', async () => {
15
+ const result = await bulkFind(['a', 'b', 'c', 'd'], 'find', { chunkSize: 2 });
16
+ expect(result).toBe('d');
17
+ expect(listFind).toHaveBeenCalledTimes(2);
18
+ });
19
+
20
+ it('retries on failure', async () => {
21
+ listFind.mockRejectedValueOnce(new Error('fail'));
22
+ const result = await bulkFindRetry(['x', 'y'], 'find', { chunkSize: 2, maxAttempts: 2 });
23
+ expect(result).toBe('y');
24
+ expect(listFind).toHaveBeenCalledTimes(2);
25
+ });
26
+ });
@@ -0,0 +1,23 @@
1
+ # bulk-group
2
+
3
+ Group long lists by first discovering the best categories and then grouping
4
+ items into those categories in smaller batches.
5
+
6
+ ```javascript
7
+ import bulkGroup from './index.js';
8
+
9
+ const feedback = [
10
+ 'Great interface and onboarding',
11
+ 'Price is a bit steep',
12
+ 'Love the mobile app',
13
+ 'Needs more integrations',
14
+ ];
15
+ const result = await bulkGroup(
16
+ feedback,
17
+ 'Is each line praise, criticism, or a feature request?',
18
+ { chunkSize: 2, topN: 3 }
19
+ );
20
+ // => { praise: ['Great interface and onboarding', 'Love the mobile app'],
21
+ // criticism: ['Price is a bit steep'],
22
+ // 'feature request': ['Needs more integrations'] }
23
+ ```
@@ -0,0 +1,18 @@
1
+ import bulkGroup from './index.js';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { longTestTimeout } from '../../constants/common.js';
4
+
5
+ describe('bulk-group examples', () => {
6
+ it(
7
+ 'groups a long list',
8
+ async () => {
9
+ const items = ['dog', 'fish', 'cat', 'whale', 'bird', 'shark', 'horse', 'dolphin'];
10
+ const result = await bulkGroup(items, 'Is each creature terrestrial or aquatic?', {
11
+ chunkSize: 4,
12
+ });
13
+ expect(typeof result).toBe('object');
14
+ expect(Object.keys(result).length).toBeGreaterThan(0);
15
+ },
16
+ longTestTimeout
17
+ );
18
+ });
@@ -0,0 +1,34 @@
1
+ import listGroup from '../../verblets/list-group/index.js';
2
+
3
+ export default async function bulkGroup(list, instructions, config = {}) {
4
+ const { chunkSize = 10, topN, llm, ...options } = config;
5
+ let categories;
6
+ const groups = {};
7
+
8
+ for (let i = 0; i < list.length; i += chunkSize) {
9
+ const batch = list.slice(i, i + chunkSize);
10
+
11
+ // eslint-disable-next-line no-await-in-loop
12
+ const result = await listGroup(batch, instructions, categories, { llm, ...options });
13
+
14
+ // Use categories from first batch for consistency
15
+ if (!categories) {
16
+ categories = Object.keys(result);
17
+ }
18
+
19
+ for (const [key, items] of Object.entries(result)) {
20
+ if (!groups[key]) groups[key] = [];
21
+ groups[key].push(...items);
22
+ }
23
+ }
24
+
25
+ // Apply topN filtering if specified
26
+ if (topN) {
27
+ const sortedEntries = Object.entries(groups)
28
+ .sort(([, a], [, b]) => b.length - a.length)
29
+ .slice(0, topN);
30
+ return Object.fromEntries(sortedEntries);
31
+ }
32
+
33
+ return groups;
34
+ }
@@ -0,0 +1,41 @@
1
+ import bulkGroup from './index.js';
2
+ import listGroup from '../../verblets/list-group/index.js';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+
5
+ vi.mock('../../verblets/list-group/index.js', () => ({
6
+ default: vi.fn(),
7
+ }));
8
+
9
+ describe('bulk-group chain', () => {
10
+ beforeEach(() => {
11
+ vi.clearAllMocks();
12
+ });
13
+
14
+ it('groups in batches', async () => {
15
+ const items = ['a', 'bb', 'ccc', 'dddd', 'eeeee'];
16
+
17
+ // Mock the calls in order - with chunkSize=2, we'll have 3 batches: [a,bb], [ccc,dddd], [eeeee]
18
+ listGroup
19
+ .mockResolvedValueOnce({ odd: ['a'], even: ['bb'] }) // First batch
20
+ .mockResolvedValueOnce({ odd: ['ccc'], even: ['dddd'] }) // Second batch
21
+ .mockResolvedValueOnce({ odd: ['eeeee'] }); // Third batch
22
+
23
+ const result = await bulkGroup(items, 'odd or even', {
24
+ chunkSize: 2,
25
+ });
26
+
27
+ expect(result).toStrictEqual({ odd: ['a', 'ccc', 'eeeee'], even: ['bb', 'dddd'] });
28
+ expect(listGroup).toHaveBeenCalledTimes(3);
29
+
30
+ // Verify the calls were made with the right parameters
31
+ expect(listGroup).toHaveBeenNthCalledWith(1, ['a', 'bb'], 'odd or even', undefined, {
32
+ llm: undefined,
33
+ });
34
+ expect(listGroup).toHaveBeenNthCalledWith(2, ['ccc', 'dddd'], 'odd or even', ['odd', 'even'], {
35
+ llm: undefined,
36
+ });
37
+ expect(listGroup).toHaveBeenNthCalledWith(3, ['eeeee'], 'odd or even', ['odd', 'even'], {
38
+ llm: undefined,
39
+ });
40
+ });
41
+ });