@theoremjs/react 0.3.0 → 0.3.1

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 (258) hide show
  1. package/README.md +728 -202
  2. package/client/composer-hints.js +1 -1
  3. package/client/decision-images.d.ts +76 -0
  4. package/client/decision-images.js +139 -0
  5. package/client/decision-transport.d.ts +26 -4
  6. package/client/decision-transport.js +79 -6
  7. package/client/embedding-input.d.ts +84 -0
  8. package/client/embedding-input.js +194 -0
  9. package/client/embedding-space.d.ts +82 -0
  10. package/client/embedding-space.js +389 -0
  11. package/client/embedding-transport.d.ts +40 -0
  12. package/client/embedding-transport.js +71 -0
  13. package/client/failure.d.ts +7 -10
  14. package/client/failure.js +9 -9
  15. package/client/host-call.d.ts +1 -1
  16. package/client/host-call.js +7 -2
  17. package/client/host-transport.d.ts +2 -0
  18. package/client/host-transport.js +3 -2
  19. package/client/index.d.ts +19 -11
  20. package/client/index.js +14 -10
  21. package/client/ink-waveform.d.ts +1 -1
  22. package/client/ink-waveform.js +1 -2
  23. package/client/live/apply-live-turn-tool-event.js +1 -1
  24. package/client/live/live-captions.d.ts +30 -0
  25. package/client/live/live-captions.js +105 -1
  26. package/client/live/live-mic-forward.d.ts +1 -0
  27. package/client/live/live-mic-forward.js +11 -2
  28. package/client/live/live-page-tool.d.ts +34 -0
  29. package/client/live/live-page-tool.js +11 -0
  30. package/client/live/live-state.d.ts +4 -2
  31. package/client/live/live-state.js +2 -0
  32. package/client/live/live-video.js +1 -1
  33. package/client/live/run-live-tool-call.d.ts +7 -2
  34. package/client/live/run-live-tool-call.js +48 -10
  35. package/client/live/use-live-caption-log.d.ts +17 -0
  36. package/client/live/use-live-caption-log.js +51 -0
  37. package/client/live-client.d.ts +52 -4
  38. package/client/live-client.js +250 -59
  39. package/client/live-messages.d.ts +51 -13
  40. package/client/live-messages.js +42 -9
  41. package/client/page-dispatch.d.ts +15 -0
  42. package/client/page-dispatch.js +63 -0
  43. package/client/run-commit.d.ts +16 -43
  44. package/client/run-commit.js +26 -101
  45. package/client/run-session.d.ts +40 -41
  46. package/client/run-session.js +104 -112
  47. package/client/schema-fields.js +27 -6
  48. package/client/shaped-data.js +37 -8
  49. package/client/source-citations.js +1 -1
  50. package/client/space-geometry.d.ts +39 -0
  51. package/client/space-geometry.js +183 -0
  52. package/client/state-fields.js +1 -1
  53. package/client/tool-result.js +30 -12
  54. package/client/tool-resume.d.ts +18 -1
  55. package/client/tool-resume.js +4 -1
  56. package/client/trace-feed.d.ts +1 -1
  57. package/client/trace-story.d.ts +2 -2
  58. package/client/trace-story.js +43 -19
  59. package/client/trace-view.d.ts +2 -2
  60. package/client/trace-view.js +56 -16
  61. package/client/transcript-groups.d.ts +28 -2
  62. package/client/transcript-groups.js +63 -13
  63. package/client/transcript-schema.d.ts +10 -0
  64. package/client/transcript-schema.js +120 -0
  65. package/client/transport.d.ts +114 -26
  66. package/client/transport.js +106 -32
  67. package/client/turn-client.d.ts +17 -8
  68. package/client/turn-client.js +26 -31
  69. package/client/voice-recorder.d.ts +1 -1
  70. package/client/voice-recorder.js +2 -5
  71. package/client/wire-line.js +5 -5
  72. package/client/worklets/mic-capture.js +9 -9
  73. package/components/InkWaveform.d.ts +12 -1
  74. package/components/InkWaveform.js +65 -42
  75. package/components/live/use-live-runner-controls.d.ts +3 -4
  76. package/components/live/use-live-runner-controls.js +6 -10
  77. package/components/live/use-live-runner-model.d.ts +10 -6
  78. package/components/live/use-live-runner-model.js +29 -14
  79. package/components/live/use-live-runner-ui.d.ts +10 -8
  80. package/components/live/use-live-runner-ui.js +19 -11
  81. package/components/live/use-live-session-client.d.ts +4 -4
  82. package/components/live/use-live-session-client.js +11 -8
  83. package/components/use-composer-voice.d.ts +4 -4
  84. package/components/use-composer-voice.js +8 -8
  85. package/components/use-editor-selection.js +4 -1
  86. package/components/use-voice-playback.d.ts +2 -1
  87. package/components/use-voice-playback.js +4 -4
  88. package/hooks/use-described.d.ts +5 -1
  89. package/hooks/use-described.js +11 -9
  90. package/hooks/use-embedding-index.d.ts +27 -0
  91. package/hooks/use-embedding-index.js +43 -0
  92. package/hooks/use-one-call.d.ts +31 -0
  93. package/hooks/use-one-call.js +76 -0
  94. package/hooks/use-page-tool-warnings.d.ts +6 -0
  95. package/hooks/use-page-tool-warnings.js +25 -0
  96. package/hooks/use-second-ticker.d.ts +2 -0
  97. package/hooks/use-second-ticker.js +13 -0
  98. package/hooks/use-theorem-chat-actions.d.ts +25 -21
  99. package/hooks/use-theorem-chat-actions.js +67 -45
  100. package/hooks/use-theorem-chat-state.d.ts +6 -6
  101. package/hooks/use-theorem-chat-state.js +13 -9
  102. package/hooks/use-theorem-chat.d.ts +48 -14
  103. package/hooks/use-theorem-chat.js +329 -141
  104. package/hooks/use-theorem-decision.d.ts +5 -10
  105. package/hooks/use-theorem-decision.js +4 -67
  106. package/hooks/use-theorem-embedding.d.ts +18 -0
  107. package/hooks/use-theorem-embedding.js +12 -0
  108. package/hooks/use-theorem-host.d.ts +5 -4
  109. package/hooks/use-theorem-host.js +26 -8
  110. package/hooks/use-theorem-interface.d.ts +6 -3
  111. package/hooks/use-theorem-interface.js +12 -22
  112. package/index.d.ts +6 -3
  113. package/index.js +5 -2
  114. package/live.d.ts +1 -0
  115. package/live.js +1 -1
  116. package/package.json +6 -5
  117. package/server/checkpoint-ownership.d.ts +6 -0
  118. package/server/checkpoint-ownership.js +42 -0
  119. package/server/conversation-store.d.ts +92 -0
  120. package/server/conversation-store.js +131 -0
  121. package/server/conversation.d.ts +193 -0
  122. package/server/conversation.js +552 -0
  123. package/server/decision-handler.d.ts +12 -4
  124. package/server/decision-handler.js +29 -38
  125. package/server/embedding-handler.d.ts +33 -0
  126. package/server/embedding-handler.js +45 -0
  127. package/server/handler.d.ts +58 -20
  128. package/server/handler.js +441 -238
  129. package/server/mod.d.ts +15 -6
  130. package/server/mod.js +13 -5
  131. package/server/page-dispatch.d.ts +22 -0
  132. package/server/page-dispatch.js +127 -0
  133. package/server/request-body.d.ts +2 -0
  134. package/server/request-body.js +42 -0
  135. package/server/request-check.d.ts +6 -1
  136. package/server/request-check.js +27 -12
  137. package/server/session-authority.d.ts +8 -0
  138. package/server/session-authority.js +78 -0
  139. package/server/session-boundary.d.ts +26 -0
  140. package/server/session-boundary.js +64 -0
  141. package/server/session-identity.d.ts +16 -0
  142. package/server/session-identity.js +41 -0
  143. package/server/session-limits.d.ts +12 -0
  144. package/server/session-limits.js +59 -0
  145. package/server/session-store.d.ts +69 -20
  146. package/server/session-store.js +220 -5
  147. package/server/single-action.d.ts +21 -0
  148. package/server/single-action.js +30 -0
  149. package/server/steer-inbox.js +1 -1
  150. package/server/tool-operations.d.ts +6 -0
  151. package/server/tool-operations.js +132 -0
  152. package/server/turn-input.d.ts +10 -0
  153. package/server/turn-input.js +29 -0
  154. package/styles/ink-controls.css +78 -78
  155. package/ui/CardWall.d.ts +10 -0
  156. package/ui/CardWall.js +61 -0
  157. package/ui/ChatComposerBar.d.ts +28 -5
  158. package/ui/ChatComposerBar.js +85 -30
  159. package/ui/ChatTranscript.d.ts +14 -1
  160. package/ui/ChatTranscript.js +110 -57
  161. package/ui/ConsoleFrame.d.ts +61 -4
  162. package/ui/ConsoleFrame.js +109 -9
  163. package/ui/DecisionAnswers.d.ts +40 -2
  164. package/ui/DecisionAnswers.js +166 -35
  165. package/ui/DecisionImages.d.ts +16 -0
  166. package/ui/DecisionImages.js +46 -0
  167. package/ui/EmbeddingDocuments.d.ts +78 -0
  168. package/ui/EmbeddingDocuments.js +278 -0
  169. package/ui/EmbeddingQuery.d.ts +27 -0
  170. package/ui/EmbeddingQuery.js +95 -0
  171. package/ui/EmbeddingResults.d.ts +41 -0
  172. package/ui/EmbeddingResults.js +165 -0
  173. package/ui/EmbeddingSpace.d.ts +26 -0
  174. package/ui/EmbeddingSpace.js +106 -0
  175. package/ui/LiveCaptionsPanel.d.ts +22 -0
  176. package/ui/LiveCaptionsPanel.js +27 -0
  177. package/ui/LiveRunner.d.ts +6 -4
  178. package/ui/LiveRunner.js +42 -55
  179. package/ui/PaneState.d.ts +33 -0
  180. package/ui/PaneState.js +50 -0
  181. package/ui/SchemaFields.d.ts +1 -1
  182. package/ui/SchemaFields.js +54 -26
  183. package/ui/ShapedData.js +73 -28
  184. package/ui/SidePanel.d.ts +74 -5
  185. package/ui/SidePanel.js +82 -9
  186. package/ui/StateFields.d.ts +10 -2
  187. package/ui/StateFields.js +56 -13
  188. package/ui/TheoremChat.d.ts +65 -14
  189. package/ui/TheoremChat.js +66 -78
  190. package/ui/TheoremDecision.d.ts +12 -20
  191. package/ui/TheoremDecision.js +78 -35
  192. package/ui/TheoremEmbedding.d.ts +27 -0
  193. package/ui/TheoremEmbedding.js +122 -0
  194. package/ui/TheoremHost.d.ts +22 -5
  195. package/ui/TheoremHost.js +127 -37
  196. package/ui/ToolGateCard.d.ts +5 -1
  197. package/ui/ToolGateCard.js +30 -23
  198. package/ui/ToolResult.js +12 -10
  199. package/ui/ToolResultCharts.js +29 -12
  200. package/ui/TraceCharts.d.ts +2 -2
  201. package/ui/TraceCharts.js +48 -17
  202. package/ui/TraceGuardrails.d.ts +3 -2
  203. package/ui/TraceGuardrails.js +24 -6
  204. package/ui/TraceInspector.d.ts +11 -1
  205. package/ui/TraceInspector.js +45 -28
  206. package/ui/TraceInspectorPanel.d.ts +31 -9
  207. package/ui/TraceInspectorPanel.js +77 -39
  208. package/ui/TraceOverview.d.ts +7 -3
  209. package/ui/TraceOverview.js +44 -14
  210. package/ui/TraceSpanDetail.js +24 -22
  211. package/ui/TraceStory.js +9 -8
  212. package/ui/TraceValues.js +19 -9
  213. package/ui/TraceWaterfall.js +12 -9
  214. package/ui/VoiceNote.js +4 -4
  215. package/ui/approval-heading.d.ts +4 -0
  216. package/ui/approval-heading.js +9 -0
  217. package/ui/arrive.d.ts +18 -0
  218. package/ui/arrive.js +46 -0
  219. package/ui/built/theme.css +86 -68
  220. package/ui/built/theme.css.d.ts +9 -0
  221. package/ui/built/theorem.d.ts +3 -3
  222. package/ui/built/theorem.js +34 -5
  223. package/ui/built/theorem.variants.d.ts +8 -2
  224. package/ui/code-language.d.ts +12 -0
  225. package/ui/code-language.js +50 -0
  226. package/ui/disclosure-motion.js +53 -9
  227. package/ui/embedding-space-gl.d.ts +37 -0
  228. package/ui/embedding-space-gl.js +663 -0
  229. package/ui/embedding-space-view.d.ts +8 -0
  230. package/ui/embedding-space-view.js +8 -0
  231. package/ui/glide.d.ts +7 -0
  232. package/ui/glide.js +44 -0
  233. package/ui/icons.js +2 -1
  234. package/ui/index.d.ts +20 -9
  235. package/ui/index.js +20 -10
  236. package/ui/labels-provider.js +5 -2
  237. package/ui/labels.d.ts +374 -85
  238. package/ui/labels.js +1028 -188
  239. package/ui/markdown-plugins.d.ts +9 -0
  240. package/ui/markdown-plugins.js +13 -0
  241. package/ui/mcp-icon.d.ts +6 -0
  242. package/ui/mcp-icon.js +8 -0
  243. package/ui/row-keys.d.ts +19 -0
  244. package/ui/row-keys.js +33 -0
  245. package/ui/theme.d.ts +1 -1
  246. package/ui/theme.js +2 -2
  247. package/ui/theorem-theme.js +32 -10
  248. package/ui/trace-palette.d.ts +2 -16
  249. package/ui/trace-palette.js +2 -16
  250. package/ui/trace-patterns.d.ts +11 -3
  251. package/ui/trace-patterns.js +16 -3
  252. package/ui/transcript-copy-text.js +1 -0
  253. package/ui/wall-columns.d.ts +8 -0
  254. package/ui/wall-columns.js +19 -0
  255. package/client/walk-away.d.ts +0 -34
  256. package/client/walk-away.js +0 -48
  257. package/server/walk-away.d.ts +0 -22
  258. package/server/walk-away.js +0 -41
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The rules behind an embedding console: the inputs as the JSON a request
3
+ * sends, text split into documents, documents packed into the requests the
4
+ * profile allows, all checked the way the kernel checks them, and documents
5
+ * ranked by how alike they point to a query.
6
+ *
7
+ * @module
8
+ */
9
+ import { base64Bytes, embeddingMediaIssues, } from '@theoremjs/agents';
10
+ function isPart(value) {
11
+ if (typeof value !== 'object' || value === null)
12
+ return false;
13
+ const part = value;
14
+ if (part.type === 'text')
15
+ return typeof part.text === 'string';
16
+ if (typeof part.mimeType !== 'string')
17
+ return false;
18
+ if (part.type === 'media')
19
+ return typeof part.data === 'string';
20
+ return part.type === 'media-ref' && typeof part.uri === 'string';
21
+ }
22
+ function isInput(value) {
23
+ if (typeof value === 'string')
24
+ return true;
25
+ if (typeof value !== 'object' || value === null)
26
+ return false;
27
+ const parts = value.parts;
28
+ return Array.isArray(parts) && parts.every(isPart);
29
+ }
30
+ /** The request's inputs, read from its JSON; `undefined` when it is not a list of inputs. */
31
+ export function readEmbeddingInputs(text) {
32
+ let value;
33
+ try {
34
+ value = JSON.parse(text);
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ return Array.isArray(value) && value.every(isInput) ? value : undefined;
40
+ }
41
+ function partsOf(input) {
42
+ return typeof input === 'string' ? [{ type: 'text', text: input }] : input.parts;
43
+ }
44
+ /** One input, read from its JSON; `undefined` when it is not one. */
45
+ export function readEmbeddingInput(text) {
46
+ let value;
47
+ try {
48
+ value = JSON.parse(text);
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ return isInput(value) ? value : undefined;
54
+ }
55
+ /** The input as fields, or `null` when it holds more than one text part and fields would lose it. */
56
+ export function inputFields(input) {
57
+ const parts = partsOf(input);
58
+ const texts = parts.filter((part) => part.type === 'text');
59
+ if (texts.length > 1)
60
+ return null;
61
+ return {
62
+ text: texts[0]?.text ?? '',
63
+ media: parts.filter((part) => part.type !== 'text'),
64
+ };
65
+ }
66
+ /** Fields as the input they send: plain text alone, else its parts with the text first. */
67
+ export function fieldsInput(fields) {
68
+ if (fields.media.length === 0)
69
+ return fields.text;
70
+ return {
71
+ parts: [
72
+ ...(fields.text ? [{ type: 'text', text: fields.text }] : []),
73
+ ...fields.media,
74
+ ],
75
+ };
76
+ }
77
+ function mediaFacts(part) {
78
+ return part.type === 'media'
79
+ ? { mimeType: part.mimeType, sizeBytes: base64Bytes(part.data) }
80
+ : { mimeType: part.mimeType };
81
+ }
82
+ /** The inputs, held to the profile's rules and its model's limits as the kernel holds them in one request. */
83
+ export function checkInputs(inputs, rules) {
84
+ const list = [...inputs];
85
+ if (list.length === 0)
86
+ return { ok: false, reason: 'no_inputs' };
87
+ if (rules.maxInputs !== undefined && list.length > rules.maxInputs)
88
+ return { ok: false, reason: 'too_many_inputs', max: rules.maxInputs, inputs: list };
89
+ const media = [];
90
+ for (const [index, input] of list.entries()) {
91
+ const parts = partsOf(input);
92
+ if (parts.length === 0 || parts.every((part) => part.type === 'text' && !part.text.trim()))
93
+ return { ok: false, reason: 'empty_input', index, inputs: list };
94
+ if (!rules.text && parts.some((part) => part.type === 'text' && part.text.trim()))
95
+ return { ok: false, reason: 'text_off', index, inputs: list };
96
+ const files = [];
97
+ for (const part of parts) {
98
+ if (part.type === 'text')
99
+ continue;
100
+ try {
101
+ files.push(mediaFacts(part));
102
+ }
103
+ catch {
104
+ return { ok: false, reason: 'invalid_json' };
105
+ }
106
+ }
107
+ media.push(files);
108
+ }
109
+ const issues = embeddingMediaIssues(rules, media);
110
+ return issues.length > 0
111
+ ? { ok: false, reason: 'attachments', issues, inputs: list }
112
+ : { ok: true, inputs: list };
113
+ }
114
+ /** The request's JSON as the inputs it sends, held to the profile's rules. */
115
+ export function checkEmbeddingInputs(text, rules) {
116
+ const inputs = readEmbeddingInputs(text);
117
+ return inputs === undefined ? { ok: false, reason: 'invalid_json' } : checkInputs(inputs, rules);
118
+ }
119
+ /** The text as documents, each trimmed, none empty. */
120
+ export function splitText(text, split) {
121
+ const pieces = split === 'whole'
122
+ ? [text]
123
+ : split === 'lines'
124
+ ? text.split(/\r?\n/)
125
+ : split === 'paragraphs'
126
+ ? text.split(/\r?\n[^\S\r\n]*\r?\n/)
127
+ : split === 'commas'
128
+ ? text.split(',')
129
+ : split.separator
130
+ ? text.split(split.separator)
131
+ : [text];
132
+ return pieces.map((piece) => piece.trim()).filter((piece) => piece !== '');
133
+ }
134
+ /** A file read as words and split, not sent as media: plain text, Markdown, CSV, and JSON. */
135
+ export function readsAsText(file) {
136
+ return (file.type.startsWith('text/') ||
137
+ file.type === 'application/json' ||
138
+ /\.(txt|md|markdown|csv|tsv|json)$/i.test(file.name));
139
+ }
140
+ // why: OpenRouter caps no batch (it took 101 inputs); 100 is Gemini's cap, a size every provider checked carries.
141
+ /** The most inputs the console sends in one request when the model sets no cap. */
142
+ export const MAX_BATCH_INPUTS = 100;
143
+ /**
144
+ * The inputs, in order, as the fewest requests the profile and its model
145
+ * allow: each holds at most `most` inputs and passes {@linkcode checkInputs}.
146
+ * An input refused on its own goes alone, for the kernel to refuse.
147
+ */
148
+ export function embeddingBatches(inputs, rules, most = MAX_BATCH_INPUTS) {
149
+ const batches = [];
150
+ let current = [];
151
+ for (const input of inputs) {
152
+ const next = [...current, input];
153
+ if (current.length > 0 && (next.length > most || !checkInputs(next, rules).ok)) {
154
+ batches.push(current);
155
+ current = [input];
156
+ }
157
+ else
158
+ current = next;
159
+ }
160
+ if (current.length > 0)
161
+ batches.push(current);
162
+ return batches;
163
+ }
164
+ /** Two calls' usage as one: token counts and costs added, estimated when either was. */
165
+ export function addUsage(a, b) {
166
+ if (!a || !b)
167
+ return a ?? b;
168
+ const sum = (x, y) => x === undefined && y === undefined ? undefined : (x ?? 0) + (y ?? 0);
169
+ const inputTokens = sum(a.inputTokens, b.inputTokens);
170
+ const costUsd = sum(a.costUsd, b.costUsd);
171
+ return {
172
+ ...(inputTokens === undefined ? {} : { inputTokens }),
173
+ ...(costUsd === undefined ? {} : { costUsd }),
174
+ ...(a.estimated || b.estimated ? { estimated: true } : {}),
175
+ };
176
+ }
177
+ /** How alike two vectors point, from -1 to 1. */
178
+ export function cosineSimilarity(a, b) {
179
+ let dot = 0;
180
+ let aa = 0;
181
+ let bb = 0;
182
+ for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
183
+ dot += a[i] * b[i];
184
+ aa += a[i] * a[i];
185
+ bb += b[i] * b[i];
186
+ }
187
+ return aa === 0 || bb === 0 ? 0 : dot / Math.sqrt(aa * bb);
188
+ }
189
+ /** The documents, most alike to the query first; a vector of another width is left out. */
190
+ export function rankMatches(query, documents) {
191
+ return documents
192
+ .flatMap((values, index) => values.length === query.length ? [{ index, score: cosineSimilarity(query, values) }] : [])
193
+ .sort((a, b) => b.score - a.score);
194
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The index as a space: each document a point in a ball, placed by the
3
+ * three directions its vectors differ most along, each measured in its own
4
+ * spread so the space is as deep as it is wide. Its direction from the
5
+ * centre is its direction along those three; its distance is how far it sits
6
+ * from the index's mean along them, as `r / (r + median)`, scaled so the
7
+ * farthest document is on the rim: the order of distances is kept, and no
8
+ * one far document squeezes the rest into the centre. A pair is alike when
9
+ * its cosine similarity stands out against every other pair in the index:
10
+ * more than {@link OUTLIER} robust deviations above the median pair. Most
11
+ * pairs in an index are unrelated, so the median and its deviation say what
12
+ * unrelated looks like for this model and these documents. Each document is
13
+ * linked to its nearest neighbour, the one its cosine similarity is highest
14
+ * with, when that pair is alike. A cluster is documents alike pairs connect,
15
+ * directly or through each other. Positions are a projection and only
16
+ * approximate; links and clusters are the vectors' own cosine similarity.
17
+ *
18
+ * @module
19
+ */
20
+ /** A point in the space, inside the unit ball. */
21
+ export type SpacePoint = readonly [number, number, number];
22
+ /** One document in the space. */
23
+ export type SpaceDocument = {
24
+ /** Its index among the documents. */
25
+ readonly index: number;
26
+ /** Where it sits in the unit ball. */
27
+ readonly position: SpacePoint;
28
+ /** The cluster it belongs to. */
29
+ readonly cluster: number;
30
+ };
31
+ /** Documents alike pairs connect, directly or through each other. */
32
+ export type SpaceCluster = {
33
+ /** Its documents' indexes, in order. */
34
+ readonly members: readonly number[];
35
+ /** Its hue, from 0 to 1 around the colour wheel: where its members' mean vector lands, as an angle around the space's two widest directions, so neighbouring clusters share a family. */
36
+ readonly tone: number;
37
+ };
38
+ /** A document and its nearest neighbour, whose cosine similarity is above the space's threshold. */
39
+ export type SpaceLink = {
40
+ readonly a: number;
41
+ readonly b: number;
42
+ /** Their cosine similarity. */
43
+ readonly similarity: number;
44
+ };
45
+ /** The three directions the space is drawn along, kept so a query lands consistently. */
46
+ export type SpaceBasis = {
47
+ readonly mean: Float64Array;
48
+ /** Each a unit direction in the vectors' own space; fewer than three when the documents span fewer. */
49
+ readonly axes: readonly Float64Array[];
50
+ /** Each axis's spread over the documents; a point is measured along each in its own spread, so the space is as deep as it is wide. */
51
+ readonly spreads: readonly number[];
52
+ /** The documents' median distance from the mean, so measured. */
53
+ readonly scale: number;
54
+ /** The farthest document's `r / (r + scale)`; dividing by it puts that document on the rim. */
55
+ readonly reach: number;
56
+ };
57
+ export type EmbeddingSpace = {
58
+ readonly documents: readonly SpaceDocument[];
59
+ readonly clusters: readonly SpaceCluster[];
60
+ /** Each document's nearest neighbour, where that pair is above the threshold; a pair that is each other's nearest appears once. */
61
+ readonly links: readonly SpaceLink[];
62
+ /** The cosine similarity a pair must pass to be linked; `Infinity` when the index has too few pairs, or too alike, to say what unrelated looks like. */
63
+ readonly threshold: number;
64
+ readonly basis: SpaceBasis;
65
+ /** How wide the vectors are. */
66
+ readonly dimensions: number;
67
+ };
68
+ /**
69
+ * The similarity one must pass to stand out among `similarities`: their
70
+ * median plus {@link OUTLIER} robust deviations. `Infinity` when they have no
71
+ * spread, since then nothing stands out.
72
+ */
73
+ export declare function outlierThreshold(similarities: ArrayLike<number>): number;
74
+ /** The space for a set of document vectors, all at once. */
75
+ export declare function layoutSpace(vectors: readonly (readonly number[])[]): EmbeddingSpace;
76
+ /**
77
+ * The same space, laid out in slices between which the page goes on, so a
78
+ * large index never freezes it. Rejects with the signal's reason once aborted.
79
+ */
80
+ export declare function layoutSpaceSoon(vectors: readonly (readonly number[])[], signal?: AbortSignal): Promise<EmbeddingSpace>;
81
+ /** Where a query lands in the space: projected along the same directions as the documents. `null` when its width differs. */
82
+ export declare function placeQuery(space: EmbeddingSpace, query: readonly number[]): SpacePoint | null;
@@ -0,0 +1,389 @@
1
+ /**
2
+ * The index as a space: each document a point in a ball, placed by the
3
+ * three directions its vectors differ most along, each measured in its own
4
+ * spread so the space is as deep as it is wide. Its direction from the
5
+ * centre is its direction along those three; its distance is how far it sits
6
+ * from the index's mean along them, as `r / (r + median)`, scaled so the
7
+ * farthest document is on the rim: the order of distances is kept, and no
8
+ * one far document squeezes the rest into the centre. A pair is alike when
9
+ * its cosine similarity stands out against every other pair in the index:
10
+ * more than {@link OUTLIER} robust deviations above the median pair. Most
11
+ * pairs in an index are unrelated, so the median and its deviation say what
12
+ * unrelated looks like for this model and these documents. Each document is
13
+ * linked to its nearest neighbour, the one its cosine similarity is highest
14
+ * with, when that pair is alike. A cluster is documents alike pairs connect,
15
+ * directly or through each other. Positions are a projection and only
16
+ * approximate; links and clusters are the vectors' own cosine similarity.
17
+ *
18
+ * @module
19
+ */
20
+ /** Power-iteration rounds per axis; the Gram matrix is small, so this converges well inside it. */
21
+ const ROUNDS = 200;
22
+ /** Where a point lands when the projection says nothing about it. */
23
+ const CENTRE = [0, 0, 0];
24
+ /** How many robust deviations above the median pair a pair must be to count as alike: the usual cut for an outlier. */
25
+ const OUTLIER = 3;
26
+ /** Scales the median absolute deviation to a standard deviation, for normally spread values. */
27
+ const MAD_SCALE = 1.4826;
28
+ function dot(a, b, length) {
29
+ let sum = 0;
30
+ for (let i = 0; i < length; i += 1)
31
+ sum += a[i] * b[i];
32
+ return sum;
33
+ }
34
+ /** A point's offset from the mean along each axis, in that axis's own spread; 0 along an axis with none. */
35
+ function along(axes, mean, spreads, vector) {
36
+ const offset = Array.from(vector, (v, d) => v - mean[d]);
37
+ return [0, 1, 2].map((k) => axes[k] && spreads[k] > 1e-12 ? dot(axes[k], offset, offset.length) / spreads[k] : 0);
38
+ }
39
+ /** Each axis's spread: the root mean square of the documents' offsets along it. */
40
+ function spreadsOf(axes, mean, vectors) {
41
+ return [0, 1, 2].map((k) => {
42
+ const axis = axes[k];
43
+ if (!axis || vectors.length === 0)
44
+ return 0;
45
+ const squares = vectors.reduce((sum, v) => {
46
+ const at = dot(axis, Array.from(v, (x, d) => x - mean[d]), v.length);
47
+ return sum + at * at;
48
+ }, 0);
49
+ return Math.sqrt(squares / vectors.length);
50
+ });
51
+ }
52
+ /** Coordinates in the axes, drawn into the ball: same direction, distance `r / (r + scale) / reach`. */
53
+ function inBall(point, scale, reach) {
54
+ const length = Math.hypot(point[0], point[1], point[2]);
55
+ if (length < 1e-9 || scale <= 0 || reach <= 0)
56
+ return CENTRE;
57
+ const radius = length / (length + scale) / reach / length;
58
+ return [point[0] * radius, point[1] * radius, point[2] * radius];
59
+ }
60
+ /** The documents packed row after row, so the loops over them stay on one typed array. */
61
+ function packed(vectors, dimensions) {
62
+ const rows = new Float64Array(vectors.length * dimensions);
63
+ vectors.forEach((vector, i) => {
64
+ rows.set(vector, i * dimensions);
65
+ });
66
+ return rows;
67
+ }
68
+ /** Every document against every other: the raw dot products, once. Yields after each row, so a large index can share the thread. */
69
+ function* gramOf(rows, n, dimensions) {
70
+ const gram = new Float64Array(n * n);
71
+ const tail = dimensions - (dimensions % 4);
72
+ for (let i = 0; i < n; i += 1) {
73
+ const a = i * dimensions;
74
+ for (let j = i; j < n; j += 1) {
75
+ const b = j * dimensions;
76
+ let s0 = 0;
77
+ let s1 = 0;
78
+ let s2 = 0;
79
+ let s3 = 0;
80
+ // why: four running sums let the engine pipeline the multiplies; this loop is most of the layout's time.
81
+ for (let d = 0; d < tail; d += 4) {
82
+ s0 += rows[a + d] * rows[b + d];
83
+ s1 += rows[a + d + 1] * rows[b + d + 1];
84
+ s2 += rows[a + d + 2] * rows[b + d + 2];
85
+ s3 += rows[a + d + 3] * rows[b + d + 3];
86
+ }
87
+ for (let d = tail; d < dimensions; d += 1)
88
+ s0 += rows[a + d] * rows[b + d];
89
+ const sum = s0 + s1 + s2 + s3;
90
+ gram[i * n + j] = sum;
91
+ gram[j * n + i] = sum;
92
+ }
93
+ yield;
94
+ }
95
+ return gram;
96
+ }
97
+ /** The same products about the documents' mean, from the raw ones. */
98
+ function centred(gram, n) {
99
+ const rows = new Float64Array(n);
100
+ let all = 0;
101
+ for (let i = 0; i < n; i += 1) {
102
+ for (let j = 0; j < n; j += 1)
103
+ rows[i] += gram[i * n + j];
104
+ rows[i] /= n;
105
+ all += rows[i];
106
+ }
107
+ all /= n;
108
+ const out = new Float64Array(n * n);
109
+ for (let i = 0; i < n; i += 1)
110
+ for (let j = 0; j < n; j += 1)
111
+ out[i * n + j] = gram[i * n + j] - rows[i] - rows[j] + all;
112
+ return out;
113
+ }
114
+ /** One power-iteration round: `matrix · vector`, normalised, with its length and how far it moved. */
115
+ function powerRound(matrix, n, vector) {
116
+ const next = new Float64Array(n);
117
+ let length = 0;
118
+ for (let i = 0; i < n; i += 1) {
119
+ let sum = 0;
120
+ for (let j = 0; j < n; j += 1)
121
+ sum += matrix[i * n + j] * vector[j];
122
+ next[i] = sum;
123
+ length += sum * sum;
124
+ }
125
+ length = Math.sqrt(length);
126
+ if (length < 1e-12)
127
+ return { next: vector, length: 0, moved: 0 };
128
+ let moved = 0;
129
+ for (let i = 0; i < n; i += 1) {
130
+ next[i] /= length;
131
+ moved = Math.max(moved, Math.abs(next[i] - vector[i]));
132
+ }
133
+ return { next, length, moved };
134
+ }
135
+ /** Flips a vector so its largest entry is positive. */
136
+ function signed(vector) {
137
+ let lead = 0;
138
+ for (let i = 1; i < vector.length; i += 1)
139
+ if (Math.abs(vector[i]) > Math.abs(vector[lead]))
140
+ lead = i;
141
+ return vector[lead] < 0 ? vector.map((v) => -v) : vector;
142
+ }
143
+ /** The largest eigenvector left in a matrix, by power iteration. */
144
+ function* largestEigen(matrix, n, k) {
145
+ // invariant: a fixed start, so the same documents always draw the same space.
146
+ let vector = Float64Array.from({ length: n }, (_, i) => 1 + ((i * 7919 + k * 104729) % 97) / 97);
147
+ let value = 0;
148
+ for (let round = 0; round < ROUNDS; round += 1) {
149
+ const step = powerRound(matrix, n, vector);
150
+ if (step.length === 0)
151
+ break;
152
+ value = step.length;
153
+ vector = step.next;
154
+ if (step.moved < 1e-12)
155
+ break;
156
+ yield;
157
+ }
158
+ return { vector: signed(vector), value };
159
+ }
160
+ /** The largest eigenvectors of a symmetric matrix, by power iteration with deflation, each signed so its largest entry is positive. */
161
+ function* topEigen(matrix, n, count) {
162
+ const work = Float64Array.from(matrix);
163
+ const found = [];
164
+ const scale = Math.max(...Array.from({ length: n }, (_, i) => Math.abs(work[i * n + i])), 0);
165
+ for (let k = 0; k < Math.min(count, n); k += 1) {
166
+ const { vector, value } = yield* largestEigen(work, n, k);
167
+ if (value <= scale * 1e-9)
168
+ break;
169
+ found.push({ vector, value });
170
+ for (let i = 0; i < n; i += 1)
171
+ for (let j = 0; j < n; j += 1)
172
+ work[i * n + j] -= value * vector[i] * vector[j];
173
+ }
174
+ return found;
175
+ }
176
+ function meanOf(rows, n, dimensions) {
177
+ const mean = new Float64Array(dimensions);
178
+ for (let i = 0; i < n; i += 1)
179
+ for (let d = 0; d < dimensions; d += 1)
180
+ mean[d] += rows[i * dimensions + d];
181
+ for (let d = 0; d < dimensions; d += 1)
182
+ mean[d] /= Math.max(n, 1);
183
+ return mean;
184
+ }
185
+ /** One axis in the vectors' own space: the documents weighted by an eigenvector of their Gram matrix. */
186
+ function axisOf(rows, mean, weights) {
187
+ const dimensions = mean.length;
188
+ const axis = new Float64Array(dimensions);
189
+ for (let i = 0; i < weights.length; i += 1)
190
+ for (let d = 0; d < dimensions; d += 1)
191
+ axis[d] += weights[i] * (rows[i * dimensions + d] - mean[d]);
192
+ let length = 0;
193
+ for (let d = 0; d < dimensions; d += 1)
194
+ length += axis[d] * axis[d];
195
+ length = Math.sqrt(length);
196
+ if (length > 0)
197
+ for (let d = 0; d < dimensions; d += 1)
198
+ axis[d] /= length;
199
+ return axis;
200
+ }
201
+ /** Every pair's cosine similarity, from the raw products. */
202
+ function cosineOf(gram, n) {
203
+ const norms = Array.from({ length: n }, (_, i) => Math.sqrt(Math.max(gram[i * n + i], 0)));
204
+ const cosine = new Float64Array(n * n);
205
+ for (let i = 0; i < n; i += 1)
206
+ for (let j = 0; j < n; j += 1)
207
+ cosine[i * n + j] =
208
+ norms[i] === 0 || norms[j] === 0 ? 0 : gram[i * n + j] / (norms[i] * norms[j]);
209
+ return cosine;
210
+ }
211
+ /** The median of some values; it sorts them. */
212
+ function medianOf(values) {
213
+ values.sort();
214
+ const middle = values.length >> 1;
215
+ return values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
216
+ }
217
+ /**
218
+ * The similarity one must pass to stand out among `similarities`: their
219
+ * median plus {@link OUTLIER} robust deviations. `Infinity` when they have no
220
+ * spread, since then nothing stands out.
221
+ */
222
+ export function outlierThreshold(similarities) {
223
+ const values = Float64Array.from(similarities);
224
+ if (values.length === 0)
225
+ return Infinity;
226
+ const median = medianOf(values.slice());
227
+ const deviation = MAD_SCALE * medianOf(values.map((v) => Math.abs(v - median)));
228
+ return deviation > 0 ? median + OUTLIER * deviation : Infinity;
229
+ }
230
+ /** Every pair's similarity, once each. */
231
+ function pairsOf(cosine, n) {
232
+ const pairs = new Float64Array((n * (n - 1)) / 2);
233
+ let at = 0;
234
+ for (let i = 0; i < n; i += 1)
235
+ for (let j = i + 1; j < n; j += 1)
236
+ pairs[at++] = cosine[i * n + j];
237
+ return pairs;
238
+ }
239
+ /** Every pair above the threshold, a row at a time: what clusters are made of. */
240
+ function* linksOf(cosine, n, threshold) {
241
+ const links = [];
242
+ for (let i = 0; i < n; i += 1) {
243
+ for (let j = i + 1; j < n; j += 1)
244
+ if (cosine[i * n + j] > threshold)
245
+ links.push({ a: i, b: j, similarity: cosine[i * n + j] });
246
+ yield;
247
+ }
248
+ return links;
249
+ }
250
+ /** The document whose cosine similarity with `i` is highest, the first on a tie; -1 when there is no other. */
251
+ function nearestTo(cosine, n, i) {
252
+ let best = -1;
253
+ for (let j = 0; j < n; j += 1)
254
+ if (j !== i && (best < 0 || cosine[i * n + j] > cosine[i * n + best]))
255
+ best = j;
256
+ return best;
257
+ }
258
+ /** Each document's nearest neighbour where that pair passes the threshold, each pair once, a row at a time. */
259
+ function* nearestOf(cosine, n, threshold) {
260
+ const seen = new Set();
261
+ const links = [];
262
+ for (let i = 0; i < n; i += 1) {
263
+ const best = nearestTo(cosine, n, i);
264
+ const [a, b] = i < best ? [i, best] : [best, i];
265
+ if (best >= 0 && cosine[i * n + best] > threshold && !seen.has(a * n + b)) {
266
+ seen.add(a * n + b);
267
+ links.push({ a, b, similarity: cosine[i * n + best] });
268
+ }
269
+ yield;
270
+ }
271
+ return links.sort((x, y) => x.a - y.a || x.b - y.b);
272
+ }
273
+ /** The groups the links connect, each in document order. */
274
+ function groupsOf(n, links) {
275
+ const root = Array.from({ length: n }, (_, i) => i);
276
+ const find = (i) => {
277
+ while (root[i] !== i) {
278
+ root[i] = root[root[i]];
279
+ i = root[i];
280
+ }
281
+ return i;
282
+ };
283
+ for (const link of links)
284
+ root[find(link.a)] = find(link.b);
285
+ const groups = new Map();
286
+ for (let i = 0; i < n; i += 1) {
287
+ const top = find(i);
288
+ const group = groups.get(top);
289
+ if (group)
290
+ group.push(i);
291
+ else
292
+ groups.set(top, [i]);
293
+ }
294
+ return [...groups.values()];
295
+ }
296
+ /** The mean of some rows. */
297
+ function meanOfRows(rows, members, dimensions) {
298
+ const mean = new Array(dimensions).fill(0);
299
+ for (const i of members)
300
+ for (let d = 0; d < dimensions; d += 1)
301
+ mean[d] += rows[i * dimensions + d];
302
+ return mean.map((v) => v / Math.max(members.length, 1));
303
+ }
304
+ /** A position's hue: its angle around the space's two widest directions, from 0 to 1. */
305
+ function toneOf(position) {
306
+ return (Math.atan2(position[1], position[0]) / (2 * Math.PI) + 1) % 1;
307
+ }
308
+ /**
309
+ * The layout, a slice at a time. Vectors whose width differs from the first
310
+ * are placed at the centre, each a cluster of its own.
311
+ */
312
+ function* layoutSteps(vectors) {
313
+ const dimensions = vectors[0]?.length ?? 0;
314
+ const original = vectors.flatMap((vector, i) => (vector.length === dimensions ? [i] : []));
315
+ const n = original.length;
316
+ const rows = packed(original.map((i) => vectors[i]), dimensions);
317
+ const mean = meanOf(rows, n, dimensions);
318
+ const gram = yield* gramOf(rows, n, dimensions);
319
+ const axes = (yield* topEigen(centred(gram, n), n, 3)).map(({ vector }) => axisOf(rows, mean, vector));
320
+ const kept = original.map((i) => vectors[i]);
321
+ const spreads = spreadsOf(axes, mean, kept);
322
+ const coordinates = kept.map((vector) => along(axes, mean, spreads, vector));
323
+ const lengths = Float64Array.from(coordinates, (p) => Math.hypot(p[0], p[1], p[2]));
324
+ // why: when more than half sit at the mean, the farthest sets the scale, so the rest still spread out.
325
+ const median = n > 0 ? medianOf(lengths.slice()) : 0;
326
+ const scale = median > 1e-9 ? median : lengths.reduce((most, v) => Math.max(most, v), 0);
327
+ const reach = lengths.reduce((most, r) => Math.max(most, scale > 0 ? r / (r + scale) : 0), 0);
328
+ const basis = { mean, axes, spreads, scale, reach };
329
+ const cosine = cosineOf(gram, n);
330
+ const threshold = outlierThreshold(pairsOf(cosine, n));
331
+ const found = yield* linksOf(cosine, n, threshold);
332
+ const groups = groupsOf(n, found);
333
+ const nearest = yield* nearestOf(cosine, n, threshold);
334
+ const clusters = groups.map((group) => ({
335
+ members: group.map((i) => original[i]),
336
+ tone: toneOf(project(basis, meanOfRows(rows, group, dimensions))),
337
+ }));
338
+ for (const [i, vector] of vectors.entries())
339
+ if (vector.length !== dimensions)
340
+ clusters.push({ members: [i], tone: toneOf(CENTRE) });
341
+ const clusterOf = new Map(clusters.flatMap((c, k) => c.members.map((i) => [i, k])));
342
+ const documents = vectors.map((vector, index) => ({
343
+ index,
344
+ // invariant: documents and queries go through the one projection, so a query lands on its own document.
345
+ position: vector.length === dimensions ? project(basis, vector) : CENTRE,
346
+ cluster: clusterOf.get(index) ?? 0,
347
+ }));
348
+ const links = nearest.map((link) => ({ ...link, a: original[link.a], b: original[link.b] }));
349
+ return { documents, clusters, links, threshold, basis, dimensions };
350
+ }
351
+ /** The space for a set of document vectors, all at once. */
352
+ export function layoutSpace(vectors) {
353
+ const steps = layoutSteps(vectors);
354
+ for (;;) {
355
+ const step = steps.next();
356
+ if (step.done)
357
+ return step.value;
358
+ }
359
+ }
360
+ /** Time the layout works before it lets the page go on, in milliseconds. */
361
+ const SLICE_MS = 8;
362
+ /**
363
+ * The same space, laid out in slices between which the page goes on, so a
364
+ * large index never freezes it. Rejects with the signal's reason once aborted.
365
+ */
366
+ export async function layoutSpaceSoon(vectors, signal) {
367
+ const steps = layoutSteps(vectors);
368
+ for (;;) {
369
+ const until = performance.now() + SLICE_MS;
370
+ for (;;) {
371
+ const step = steps.next();
372
+ if (step.done)
373
+ return step.value;
374
+ if (performance.now() >= until)
375
+ break;
376
+ }
377
+ await new Promise((resolve) => setTimeout(resolve, 0));
378
+ signal?.throwIfAborted();
379
+ }
380
+ }
381
+ /** Where a query lands in the space: projected along the same directions as the documents. `null` when its width differs. */
382
+ export function placeQuery(space, query) {
383
+ if (query.length !== space.dimensions || space.dimensions === 0)
384
+ return null;
385
+ return project(space.basis, query);
386
+ }
387
+ function project(basis, vector) {
388
+ return inBall(along(basis.axes, basis.mean, basis.spreads, vector), basis.scale, basis.reach);
389
+ }