@danielblomma/cortex-mcp 2.1.1 → 2.2.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 (150) hide show
  1. package/README.md +72 -24
  2. package/bin/cortex.mjs +50 -21
  3. package/mcp-registry-submission.json +63 -0
  4. package/package.json +19 -5
  5. package/scaffold/AGENTS.md +13 -9
  6. package/scaffold/CLAUDE.md +13 -9
  7. package/scaffold/docs/architecture.md +1 -1
  8. package/scaffold/mcp/build.mjs +92 -0
  9. package/scaffold/mcp/package.json +4 -4
  10. package/scaffold/mcp/src/cli/query.ts +455 -0
  11. package/scaffold/mcp/src/embed.ts +181 -77
  12. package/scaffold/mcp/src/embedScheduler.ts +509 -0
  13. package/scaffold/mcp/src/embeddings.ts +31 -30
  14. package/scaffold/mcp/src/graphCsv.ts +65 -0
  15. package/scaffold/mcp/src/jsonl.ts +69 -14
  16. package/scaffold/mcp/src/loadGraph.ts +276 -40
  17. package/scaffold/mcp/src/lruCache.ts +41 -0
  18. package/scaffold/mcp/src/searchCore.ts +1 -1
  19. package/scaffold/mcp/src/searchResults.ts +3 -3
  20. package/scaffold/mcp/src/types.ts +6 -1
  21. package/scaffold/mcp/tests/embed-scheduler.test.mjs +474 -0
  22. package/scaffold/mcp/tests/embedding-jsonl.test.mjs +39 -0
  23. package/scaffold/mcp/tests/graph-bulk-load.test.mjs +258 -0
  24. package/scaffold/mcp/tests/graph-csv.test.mjs +118 -0
  25. package/scaffold/mcp/tests/lru-cache.test.mjs +37 -0
  26. package/scaffold/mcp/tests/query-cli.test.mjs +115 -0
  27. package/scaffold/mcp/tests/vector-index.test.mjs +109 -0
  28. package/scaffold/mcp/tsconfig.json +3 -1
  29. package/scaffold/scripts/bootstrap.sh +37 -4
  30. package/scaffold/scripts/dashboard.mjs +1 -1
  31. package/scaffold/scripts/doctor.sh +9 -8
  32. package/scaffold/scripts/embed.sh +3 -2
  33. package/scaffold/scripts/ingest-parsers.mjs +387 -0
  34. package/scaffold/scripts/ingest-worker.mjs +52 -0
  35. package/scaffold/scripts/ingest.mjs +550 -378
  36. package/scaffold/scripts/load-ryu.sh +3 -2
  37. package/scaffold/scripts/memory-compile.mjs +5 -2
  38. package/scaffold/scripts/memory-lint.mjs +5 -2
  39. package/scaffold/scripts/parsers/csharp.mjs +2 -1
  40. package/scaffold/scripts/parsers/javascript/ast.mjs +160 -8
  41. package/scaffold/scripts/parsers/javascript/chunks.mjs +129 -22
  42. package/scaffold/scripts/parsers/javascript/imports.mjs +38 -4
  43. package/scaffold/scripts/parsers/vbnet.mjs +2 -1
  44. package/scaffold/scripts/status.sh +50 -14
  45. package/scaffold/scripts/parsers/node_modules/.package-lock.json +0 -56
  46. package/scaffold/scripts/parsers/node_modules/acorn/CHANGELOG.md +0 -972
  47. package/scaffold/scripts/parsers/node_modules/acorn/LICENSE +0 -21
  48. package/scaffold/scripts/parsers/node_modules/acorn/README.md +0 -301
  49. package/scaffold/scripts/parsers/node_modules/acorn/bin/acorn +0 -4
  50. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.mts +0 -883
  51. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.d.ts +0 -883
  52. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.js +0 -6295
  53. package/scaffold/scripts/parsers/node_modules/acorn/dist/acorn.mjs +0 -6266
  54. package/scaffold/scripts/parsers/node_modules/acorn/dist/bin.js +0 -90
  55. package/scaffold/scripts/parsers/node_modules/acorn/package.json +0 -50
  56. package/scaffold/scripts/parsers/node_modules/acorn-typescript/CHANGELOG.md +0 -421
  57. package/scaffold/scripts/parsers/node_modules/acorn-typescript/LICENSE +0 -21
  58. package/scaffold/scripts/parsers/node_modules/acorn-typescript/README.md +0 -81
  59. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.d.ts +0 -103
  60. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js +0 -78
  61. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/error.js.map +0 -1
  62. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.d.ts +0 -167
  63. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js +0 -75
  64. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/decorators.js.map +0 -1
  65. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.d.ts +0 -177
  66. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js +0 -56
  67. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/import-assertions.js.map +0 -1
  68. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.d.ts +0 -198
  69. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js +0 -327
  70. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/index.js.map +0 -1
  71. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.d.ts +0 -256
  72. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js +0 -256
  73. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/extentions/jsx/xhtml.js.map +0 -1
  74. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.d.ts +0 -472
  75. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js +0 -1
  76. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.js.map +0 -1
  77. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/index.mjs +0 -1
  78. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.d.ts +0 -159
  79. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js +0 -2
  80. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/middleware.js.map +0 -1
  81. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.d.ts +0 -10
  82. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js +0 -38
  83. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/parseutil.js.map +0 -1
  84. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.d.ts +0 -12
  85. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js +0 -29
  86. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/scopeflags.js.map +0 -1
  87. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.d.ts +0 -2
  88. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js +0 -118
  89. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/tokenType.js.map +0 -1
  90. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.d.ts +0 -60
  91. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js +0 -2
  92. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/types.js.map +0 -1
  93. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.d.ts +0 -2
  94. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js +0 -19
  95. package/scaffold/scripts/parsers/node_modules/acorn-typescript/lib/whitespace.js.map +0 -1
  96. package/scaffold/scripts/parsers/node_modules/acorn-typescript/package.json +0 -53
  97. package/scaffold/scripts/parsers/node_modules/acorn-typescript/tsconfig.json +0 -19
  98. package/scaffold/scripts/parsers/node_modules/acorn-walk/CHANGELOG.md +0 -209
  99. package/scaffold/scripts/parsers/node_modules/acorn-walk/LICENSE +0 -21
  100. package/scaffold/scripts/parsers/node_modules/acorn-walk/README.md +0 -124
  101. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.mts +0 -152
  102. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.d.ts +0 -152
  103. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.js +0 -485
  104. package/scaffold/scripts/parsers/node_modules/acorn-walk/dist/walk.mjs +0 -467
  105. package/scaffold/scripts/parsers/node_modules/acorn-walk/package.json +0 -50
  106. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/LICENSE +0 -24
  107. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/README.md +0 -23
  108. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-bash.wasm +0 -0
  109. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c.wasm +0 -0
  110. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-c_sharp.wasm +0 -0
  111. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-cpp.wasm +0 -0
  112. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-css.wasm +0 -0
  113. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-dart.wasm +0 -0
  114. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elisp.wasm +0 -0
  115. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elixir.wasm +0 -0
  116. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-elm.wasm +0 -0
  117. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-embedded_template.wasm +0 -0
  118. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-go.wasm +0 -0
  119. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-html.wasm +0 -0
  120. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-java.wasm +0 -0
  121. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-javascript.wasm +0 -0
  122. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-json.wasm +0 -0
  123. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-kotlin.wasm +0 -0
  124. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-lua.wasm +0 -0
  125. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-objc.wasm +0 -0
  126. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ocaml.wasm +0 -0
  127. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-php.wasm +0 -0
  128. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-python.wasm +0 -0
  129. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ql.wasm +0 -0
  130. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rescript.wasm +0 -0
  131. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm +0 -0
  132. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-rust.wasm +0 -0
  133. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-scala.wasm +0 -0
  134. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-solidity.wasm +0 -0
  135. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-swift.wasm +0 -0
  136. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-systemrdl.wasm +0 -0
  137. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tlaplus.wasm +0 -0
  138. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-toml.wasm +0 -0
  139. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-tsx.wasm +0 -0
  140. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-typescript.wasm +0 -0
  141. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-vue.wasm +0 -0
  142. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-yaml.wasm +0 -0
  143. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/out/tree-sitter-zig.wasm +0 -0
  144. package/scaffold/scripts/parsers/node_modules/tree-sitter-wasms/package.json +0 -64
  145. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/LICENSE +0 -21
  146. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/README.md +0 -198
  147. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/package.json +0 -37
  148. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter-web.d.ts +0 -242
  149. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.js +0 -1
  150. package/scaffold/scripts/parsers/node_modules/web-tree-sitter/tree-sitter.wasm +0 -0
@@ -0,0 +1,474 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ DEFAULT_SCHEDULER_OPTIONS,
6
+ groupDuplicates,
7
+ packWorkUnits,
8
+ resolvePoolConfig,
9
+ runWorkUnits,
10
+ sliceBatchVectors
11
+ } from "../dist/embedScheduler.js";
12
+
13
+ function measured(text, tokens, indexes) {
14
+ return { text, tokens, indexes };
15
+ }
16
+
17
+ function fakeTensor(rows) {
18
+ return { dims: [rows.length, rows[0]?.length ?? 0], data: Float32Array.from(rows.flat()) };
19
+ }
20
+
21
+ // ─── dedup ───────────────────────────────────────────────────────────────────
22
+
23
+ test("groupDuplicates: identical texts share one unique entry with all indexes", () => {
24
+ const unique = groupDuplicates([
25
+ { index: 0, text: "same" },
26
+ { index: 3, text: "other" },
27
+ { index: 7, text: "same" }
28
+ ]);
29
+ assert.equal(unique.length, 2);
30
+ const same = unique.find((u) => u.text === "same");
31
+ assert.deepEqual(same.indexes, [0, 7]);
32
+ });
33
+
34
+ // ─── packing ─────────────────────────────────────────────────────────────────
35
+
36
+ test("packWorkUnits: long texts become singles, shorts are batched when enabled", () => {
37
+ const units = packWorkUnits(
38
+ [
39
+ measured("long", 4000, [0]),
40
+ measured("s1", 20, [1]),
41
+ measured("s2", 21, [2]),
42
+ measured("s3", 22, [3])
43
+ ],
44
+ { ...DEFAULT_SCHEDULER_OPTIONS, batchMaxItems: 32 }
45
+ );
46
+ const single = units.find((u) => u.texts.includes("long"));
47
+ assert.equal(single.kind, "single");
48
+ const batch = units.find((u) => u.kind === "batch");
49
+ assert.deepEqual(batch.texts.sort(), ["s1", "s2", "s3"]);
50
+ });
51
+
52
+ test("packWorkUnits: respects the pad-waste bound", () => {
53
+ // 10-token and 100-token texts in one batch would waste 45% padding.
54
+ const units = packWorkUnits(
55
+ [measured("tiny", 10, [0]), measured("bigger", 100, [1])],
56
+ { ...DEFAULT_SCHEDULER_OPTIONS, shortMaxTokens: 128, batchMaxItems: 32 }
57
+ );
58
+ assert.equal(units.length, 2);
59
+ assert.ok(units.every((u) => u.texts.length === 1));
60
+ });
61
+
62
+ test("packWorkUnits: respects token budget and max items", () => {
63
+ const many = Array.from({ length: 100 }, (_, i) => measured(`t${i}`, 64, [i]));
64
+ const units = packWorkUnits(many, {
65
+ ...DEFAULT_SCHEDULER_OPTIONS,
66
+ batchTokenBudget: 64 * 8,
67
+ batchMaxItems: 8
68
+ });
69
+ assert.ok(units.every((u) => u.texts.length <= 8));
70
+ assert.ok(units.every((u) => u.texts.length * u.maxTokens <= 64 * 8));
71
+ const total = units.reduce((acc, u) => acc + u.texts.length, 0);
72
+ assert.equal(total, 100);
73
+ });
74
+
75
+ test("packWorkUnits: default options produce no batches (byte-identity default)", () => {
76
+ const units = packWorkUnits(
77
+ [measured("a", 10, [0]), measured("b", 11, [1]), measured("c", 12, [2])],
78
+ DEFAULT_SCHEDULER_OPTIONS
79
+ );
80
+ assert.ok(units.every((u) => u.kind === "single"));
81
+ });
82
+
83
+ test("packWorkUnits: batchMaxItems=1 disables batching entirely", () => {
84
+ const units = packWorkUnits(
85
+ [measured("a", 10, [0]), measured("b", 11, [1])],
86
+ { ...DEFAULT_SCHEDULER_OPTIONS, batchMaxItems: 1 }
87
+ );
88
+ assert.ok(units.every((u) => u.kind === "single"));
89
+ });
90
+
91
+ test("packWorkUnits: orders units by descending cost", () => {
92
+ const units = packWorkUnits(
93
+ [measured("small", 10, [0]), measured("huge", 5000, [1]), measured("mid", 500, [2])],
94
+ DEFAULT_SCHEDULER_OPTIONS
95
+ );
96
+ const costs = units.map((u) => u.cost);
97
+ assert.deepEqual(costs, [...costs].sort((a, b) => b - a));
98
+ assert.equal(units[0].texts[0], "huge");
99
+ });
100
+
101
+ // ─── pool config ─────────────────────────────────────────────────────────────
102
+
103
+ test("resolvePoolConfig: splits the budget into 4-thread sessions", () => {
104
+ assert.deepEqual(resolvePoolConfig({ threadBudget: 12, uniqueCount: 1000 }), {
105
+ sessions: 3,
106
+ threadsPerSession: 4
107
+ });
108
+ assert.deepEqual(resolvePoolConfig({ threadBudget: 4, uniqueCount: 1000 }), {
109
+ sessions: 1,
110
+ threadsPerSession: 4
111
+ });
112
+ assert.deepEqual(resolvePoolConfig({ threadBudget: 2, uniqueCount: 1000 }), {
113
+ sessions: 1,
114
+ threadsPerSession: 2
115
+ });
116
+ });
117
+
118
+ test("resolvePoolConfig: tiny workloads skip the pool", () => {
119
+ assert.deepEqual(resolvePoolConfig({ threadBudget: 12, uniqueCount: 10 }), {
120
+ sessions: 1,
121
+ threadsPerSession: 12
122
+ });
123
+ });
124
+
125
+ test("resolvePoolConfig: explicit override wins", () => {
126
+ assert.deepEqual(resolvePoolConfig({ threadBudget: 12, poolOverride: 2, uniqueCount: 10 }), {
127
+ sessions: 2,
128
+ threadsPerSession: 6
129
+ });
130
+ });
131
+
132
+ // ─── execution ───────────────────────────────────────────────────────────────
133
+
134
+ test("runWorkUnits: assigns vectors to every duplicate index", async () => {
135
+ const extractor = async (texts) => {
136
+ const list = Array.isArray(texts) ? texts : [texts];
137
+ return fakeTensor(list.map((t) => [t.length, 1]));
138
+ };
139
+ const units = packWorkUnits(
140
+ [measured("aaa", 3, [0, 5]), measured("bb", 2, [1])],
141
+ { ...DEFAULT_SCHEDULER_OPTIONS, batchMaxItems: 32 }
142
+ );
143
+ const result = await runWorkUnits(units, [extractor]);
144
+ assert.equal(result.failures.length, 0);
145
+ assert.deepEqual(result.vectors.get(0), [3, 1]);
146
+ assert.deepEqual(result.vectors.get(5), [3, 1]);
147
+ assert.deepEqual(result.vectors.get(1), [2, 1]);
148
+ });
149
+
150
+ test("runWorkUnits: onVector consumes vectors without retaining them", async () => {
151
+ const extractor = async (texts) => {
152
+ const list = Array.isArray(texts) ? texts : [texts];
153
+ return fakeTensor(list.map((t) => [t.length, 1]));
154
+ };
155
+ const units = packWorkUnits(
156
+ [measured("aaa", 3, [0, 5]), measured("bb", 2, [1])],
157
+ { ...DEFAULT_SCHEDULER_OPTIONS, batchMaxItems: 32 }
158
+ );
159
+ const consumed = new Map();
160
+ const result = await runWorkUnits(units, [extractor], {
161
+ onVector(index, vector) {
162
+ consumed.set(index, vector);
163
+ }
164
+ });
165
+ assert.equal(result.failures.length, 0);
166
+ assert.equal(result.vectors.size, 0);
167
+ assert.deepEqual(consumed.get(0), [3, 1]);
168
+ assert.deepEqual(consumed.get(5), [3, 1]);
169
+ assert.deepEqual(consumed.get(1), [2, 1]);
170
+ });
171
+
172
+ test("runWorkUnits: batch failure retries per item and isolates the poison text", async () => {
173
+ const extractor = async (texts) => {
174
+ if (Array.isArray(texts)) {
175
+ throw new Error("batch exploded");
176
+ }
177
+ if (texts === "bad") {
178
+ throw new Error("poison");
179
+ }
180
+ return fakeTensor([[texts.length]]);
181
+ };
182
+ const units = [
183
+ {
184
+ kind: "batch",
185
+ texts: ["ok", "bad", "fine"],
186
+ members: [[0], [1, 9], [2]],
187
+ maxTokens: 4,
188
+ cost: 48
189
+ }
190
+ ];
191
+ const result = await runWorkUnits(units, [extractor]);
192
+ assert.deepEqual(result.vectors.get(0), [2]);
193
+ assert.deepEqual(result.vectors.get(2), [4]);
194
+ assert.equal(result.vectors.has(1), false);
195
+ // the poison text fails for every index it owns
196
+ assert.deepEqual(result.failures.map((f) => f.index).sort(), [1, 9]);
197
+ });
198
+
199
+ test("runWorkUnits: distributes units across multiple extractors", async () => {
200
+ const calls = { a: 0, b: 0 };
201
+ const make = (name) => async (texts) => {
202
+ calls[name] += 1;
203
+ await new Promise((resolve) => setTimeout(resolve, 5));
204
+ const list = Array.isArray(texts) ? texts : [texts];
205
+ return fakeTensor(list.map((t) => [t.length]));
206
+ };
207
+ const units = Array.from({ length: 8 }, (_, i) => ({
208
+ kind: "single",
209
+ texts: [`t${i}`],
210
+ members: [[i]],
211
+ maxTokens: 2,
212
+ cost: 4
213
+ }));
214
+ const result = await runWorkUnits(units, [make("a"), make("b")]);
215
+ assert.equal(result.vectors.size, 8);
216
+ assert.ok(calls.a > 0 && calls.b > 0, `expected both lanes used, got ${JSON.stringify(calls)}`);
217
+ });
218
+
219
+ test("sliceBatchVectors: rejects shape mismatches", () => {
220
+ assert.throws(
221
+ () => sliceBatchVectors({ dims: [2, 3], data: Float32Array.from([1, 2, 3, 4, 5]) }, 2),
222
+ /shape mismatch/
223
+ );
224
+ });
225
+
226
+ test("runWorkUnits: in-flight token gate serializes giant units", async () => {
227
+ let concurrent = 0;
228
+ let peak = 0;
229
+ const extractor = async (texts) => {
230
+ concurrent += 1;
231
+ peak = Math.max(peak, concurrent);
232
+ await new Promise((resolve) => setTimeout(resolve, 10));
233
+ concurrent -= 1;
234
+ const list = Array.isArray(texts) ? texts : [texts];
235
+ return { dims: [list.length, 1], data: Float32Array.from(list.map((t) => t.length)) };
236
+ };
237
+ // three giants of 8000 tokens each; gate of 12288 allows only one at a time
238
+ const units = Array.from({ length: 3 }, (_, i) => ({
239
+ kind: "single",
240
+ texts: [`giant${i}`],
241
+ members: [[i]],
242
+ maxTokens: 8000,
243
+ cost: 8000 * 8000
244
+ }));
245
+ const result = await runWorkUnits(units, [extractor, extractor, extractor], {
246
+ maxInFlightTokens: 12288
247
+ });
248
+ assert.equal(result.vectors.size, 3);
249
+ assert.equal(peak, 1, `expected serialized giants, saw peak concurrency ${peak}`);
250
+ });
251
+
252
+ test("runWorkUnits: shorts flow while a giant occupies the budget", async () => {
253
+ const seen = [];
254
+ const extractor = async (texts) => {
255
+ const list = Array.isArray(texts) ? texts : [texts];
256
+ seen.push(list[0]);
257
+ await new Promise((resolve) => setTimeout(resolve, list[0].startsWith("giant") ? 40 : 5));
258
+ return { dims: [list.length, 1], data: Float32Array.from(list.map((t) => t.length)) };
259
+ };
260
+ const units = [
261
+ { kind: "single", texts: ["giant"], members: [[0]], maxTokens: 8000, cost: 64e6 },
262
+ ...Array.from({ length: 6 }, (_, i) => ({
263
+ kind: "single",
264
+ texts: [`s${i}`],
265
+ members: [[i + 1]],
266
+ maxTokens: 50,
267
+ cost: 2500
268
+ }))
269
+ ];
270
+ const result = await runWorkUnits(units, [extractor, extractor], { maxInFlightTokens: 12288 });
271
+ assert.equal(result.vectors.size, 7);
272
+ // shorts must have run while the giant was in flight (second lane busy)
273
+ assert.ok(seen.slice(0, 3).some((t) => t.startsWith("s")), `lane starvation: ${seen.join(",")}`);
274
+ });
275
+
276
+ test("runWorkUnits: gate never deadlocks on units larger than the budget", async () => {
277
+ const extractor = async (texts) => {
278
+ const list = Array.isArray(texts) ? texts : [texts];
279
+ return { dims: [list.length, 1], data: Float32Array.from(list.map((t) => t.length)) };
280
+ };
281
+ const units = [
282
+ { kind: "single", texts: ["mega"], members: [[0]], maxTokens: 50000, cost: 25e8 }
283
+ ];
284
+ const result = await runWorkUnits(units, [extractor], { maxInFlightTokens: 1000 });
285
+ assert.equal(result.vectors.size, 1);
286
+ });
287
+
288
+ // ─── review fixes ────────────────────────────────────────────────────────────
289
+
290
+ test("resolvePoolConfig: override never exceeds the thread budget", () => {
291
+ const config = resolvePoolConfig({ threadBudget: 4, poolOverride: 8, uniqueCount: 10 });
292
+ assert.ok(
293
+ config.sessions * config.threadsPerSession <= 4,
294
+ `cap violated: ${JSON.stringify(config)}`
295
+ );
296
+ assert.deepEqual(config, { sessions: 4, threadsPerSession: 1 });
297
+ // and an absurd override cannot demand absurd model copies
298
+ const huge = resolvePoolConfig({ threadBudget: 64, poolOverride: 100, uniqueCount: 1000 });
299
+ assert.ok(huge.sessions <= 8);
300
+ });
301
+
302
+ test("runWorkUnits: an oversized unit does not unlock the gate for others", async () => {
303
+ let concurrent = 0;
304
+ let peak = 0;
305
+ const extractor = async (texts) => {
306
+ concurrent += 1;
307
+ peak = Math.max(peak, concurrent);
308
+ await new Promise((resolve) => setTimeout(resolve, 8));
309
+ concurrent -= 1;
310
+ const list = Array.isArray(texts) ? texts : [texts];
311
+ return { dims: [list.length, 1], data: Float32Array.from(list.map((t) => t.length)) };
312
+ };
313
+ const units = [
314
+ { kind: "single", texts: ["mega"], members: [[0]], maxTokens: 50000, cost: 25e8 },
315
+ ...Array.from({ length: 3 }, (_, i) => ({
316
+ kind: "single",
317
+ texts: [`giant${i}`],
318
+ members: [[i + 1]],
319
+ maxTokens: 8000,
320
+ cost: 64e6
321
+ }))
322
+ ];
323
+ const result = await runWorkUnits(units, [extractor, extractor, extractor], {
324
+ maxInFlightTokens: 12288
325
+ });
326
+ assert.equal(result.vectors.size, 4);
327
+ assert.equal(peak, 1, `mega must not disable the gate; saw peak ${peak}`);
328
+ });
329
+
330
+ test("runWorkUnits: rejects units with invalid token counts", async () => {
331
+ const extractor = async () => ({ dims: [1, 1], data: Float32Array.from([1]) });
332
+ for (const bad of [NaN, 0, -5, Infinity]) {
333
+ await assert.rejects(
334
+ () =>
335
+ runWorkUnits(
336
+ [{ kind: "single", texts: ["x"], members: [[0]], maxTokens: bad, cost: 1 }],
337
+ [extractor]
338
+ ),
339
+ /invalid maxTokens/
340
+ );
341
+ }
342
+ });
343
+
344
+ test("runWorkUnits: empty units resolve, empty extractors throw", async () => {
345
+ const extractor = async () => ({ dims: [1, 1], data: Float32Array.from([1]) });
346
+ const empty = await runWorkUnits([], [extractor]);
347
+ assert.equal(empty.vectors.size, 0);
348
+ assert.equal(empty.failures.length, 0);
349
+ await assert.rejects(() => runWorkUnits([], []), /at least one extractor/);
350
+ });
351
+
352
+ test("runWorkUnits: failures are reported in slot-index order", async () => {
353
+ const extractor = async (texts) => {
354
+ const text = Array.isArray(texts) ? texts[0] : texts;
355
+ await new Promise((resolve) => setTimeout(resolve, text === "fail-late" ? 1 : 15));
356
+ throw new Error(`boom:${text}`);
357
+ };
358
+ const units = [
359
+ { kind: "single", texts: ["fail-early"], members: [[7]], maxTokens: 10, cost: 100 },
360
+ { kind: "single", texts: ["fail-late"], members: [[2]], maxTokens: 10, cost: 100 }
361
+ ];
362
+ const result = await runWorkUnits(units, [extractor, extractor]);
363
+ assert.deepEqual(result.failures.map((f) => f.index), [2, 7]);
364
+ });
365
+
366
+ test("sliceBatchVectors: infers dimensions when dims metadata is missing", () => {
367
+ const rows = sliceBatchVectors({ data: Float32Array.from([1, 2, 3, 4]) }, 2);
368
+ assert.deepEqual(rows, [
369
+ [1, 2],
370
+ [3, 4]
371
+ ]);
372
+ });
373
+
374
+ test("packWorkUnits: a lone short over the token budget still packs as batch-of-1", () => {
375
+ const units = packWorkUnits([measured("wide", 100, [0])], {
376
+ ...DEFAULT_SCHEDULER_OPTIONS,
377
+ shortMaxTokens: 128,
378
+ batchTokenBudget: 64,
379
+ batchMaxItems: 32
380
+ });
381
+ assert.equal(units.length, 1);
382
+ assert.equal(units[0].texts.length, 1);
383
+ });
384
+
385
+ test("resolvePoolConfig: derates sessions on low-memory machines", () => {
386
+ const lowMem = resolvePoolConfig({ threadBudget: 12, uniqueCount: 1000, memoryBytes: 4e9 });
387
+ assert.equal(lowMem.sessions, 1);
388
+ const midMem = resolvePoolConfig({ threadBudget: 12, uniqueCount: 1000, memoryBytes: 7e9 });
389
+ assert.equal(midMem.sessions, 2);
390
+ const bigMem = resolvePoolConfig({ threadBudget: 12, uniqueCount: 1000, memoryBytes: 64e9 });
391
+ assert.equal(bigMem.sessions, 3);
392
+ // unknown memory keeps the thread-derived shape
393
+ const unknown = resolvePoolConfig({ threadBudget: 12, uniqueCount: 1000 });
394
+ assert.equal(unknown.sessions, 3);
395
+ });
396
+
397
+ test("resolveInFlightTokens: adapts to memory within model-max bounds", async () => {
398
+ const { resolveInFlightTokens } = await import("../dist/embedScheduler.js");
399
+ // plenty of memory: capped at 8x model max
400
+ assert.equal(resolveInFlightTokens({ memoryBytes: 64e9, modelMaxTokens: 8192 }), 8 * 8192);
401
+ // tight memory: never below one model-max unit
402
+ assert.equal(resolveInFlightTokens({ memoryBytes: 1e9, modelMaxTokens: 8192 }), 8192);
403
+ // mid memory: proportional
404
+ const mid = resolveInFlightTokens({ memoryBytes: 12e9, modelMaxTokens: 8192 });
405
+ assert.ok(mid > 8192 && mid < 8 * 8192, `expected proportional gate, got ${mid}`);
406
+ // unknown memory: static default floor
407
+ assert.equal(resolveInFlightTokens({ modelMaxTokens: 512 }), 12288);
408
+ });
409
+
410
+ // ─── round-2 review fixes ────────────────────────────────────────────────────
411
+
412
+ test("resolveMemoryHeadroom: floors macOS-style tiny freemem at a share of total", async () => {
413
+ const { resolveMemoryHeadroom } = await import("../dist/embedScheduler.js");
414
+ // 24GB laptop reporting 0.5GB free (reclaimable cache counted as used)
415
+ const mac = resolveMemoryHeadroom({ freeMemory: 0.5e9, totalMemory: 24e9 });
416
+ assert.ok(mac >= 24e9 * 0.375 && mac <= 24e9 * 0.5, `got ${mac}`);
417
+ });
418
+
419
+ test("resolveMemoryHeadroom: container limits cap host-sized memory", async () => {
420
+ const { resolveMemoryHeadroom } = await import("../dist/embedScheduler.js");
421
+ // 4GB cgroup limit inside a 64GB host: headroom must reflect the limit
422
+ const container = resolveMemoryHeadroom({
423
+ freeMemory: 50e9,
424
+ totalMemory: 64e9,
425
+ constrainedMemory: 4e9,
426
+ availableMemory: 3e9
427
+ });
428
+ assert.ok(container <= 4e9 * 0.5, `container headroom leaked host memory: ${container}`);
429
+ });
430
+
431
+ test("resolveMemoryHeadroom: big idle host plans within half of total", async () => {
432
+ const { resolveMemoryHeadroom } = await import("../dist/embedScheduler.js");
433
+ const big = resolveMemoryHeadroom({ freeMemory: 50e9, totalMemory: 64e9 });
434
+ assert.equal(big, 32e9);
435
+ // garbage inputs stay sane
436
+ const garbage = resolveMemoryHeadroom({ freeMemory: NaN, totalMemory: -1 });
437
+ assert.ok(garbage > 0 && garbage <= 8e9);
438
+ });
439
+
440
+ test("resolveModelMaxTokens: sane values pass, sentinels and absurdities fall back", async () => {
441
+ const { resolveModelMaxTokens } = await import("../dist/embedScheduler.js");
442
+ assert.equal(resolveModelMaxTokens(512), 512);
443
+ assert.equal(resolveModelMaxTokens(8192), 8192);
444
+ assert.equal(resolveModelMaxTokens(131072), 131072);
445
+ assert.equal(resolveModelMaxTokens(1e8), 8192);
446
+ assert.equal(resolveModelMaxTokens(1e30), 8192);
447
+ assert.equal(resolveModelMaxTokens(undefined), 8192);
448
+ assert.equal(resolveModelMaxTokens(0), 8192);
449
+ });
450
+
451
+ test("createTokenCounter: uses the tokenizer, clamps at model max, survives failures", async () => {
452
+ const { createTokenCounter } = await import("../dist/embedScheduler.js");
453
+ const tokenizer = (text) => ({ input_ids: { dims: [1, text.length * 2] } });
454
+ const count = createTokenCounter(tokenizer, 100);
455
+ assert.equal(count("abc"), 6);
456
+ assert.equal(count("x".repeat(200)), 100); // clamped
457
+ const broken = createTokenCounter(() => {
458
+ throw new Error("boom");
459
+ }, 100);
460
+ assert.equal(broken("x".repeat(40)), 10); // chars/4 fallback
461
+ const missing = createTokenCounter(undefined, 100);
462
+ assert.equal(missing("x".repeat(40)), 10);
463
+ });
464
+
465
+ test("resolveMemoryHeadroom: constrainedMemory 0 means unconstrained (Node sentinel)", async () => {
466
+ const { resolveMemoryHeadroom } = await import("../dist/embedScheduler.js");
467
+ const unconstrained = resolveMemoryHeadroom({ freeMemory: 50e9, totalMemory: 64e9 });
468
+ const zeroSentinel = resolveMemoryHeadroom({
469
+ freeMemory: 50e9,
470
+ totalMemory: 64e9,
471
+ constrainedMemory: 0
472
+ });
473
+ assert.equal(zeroSentinel, unconstrained);
474
+ });
@@ -0,0 +1,39 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { readJsonlRecords, writeJsonlRecords } from "../dist/jsonl.js";
8
+
9
+ test("writeJsonlRecords writes canonical JSONL without building a joined body", () => {
10
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-jsonl-"));
11
+ const filePath = path.join(dir, "embeddings.jsonl");
12
+ const records = [
13
+ { id: "a", vector: [1, 2.5], model: "m" },
14
+ { id: "b", vector: [0], model: "m", label: "two" }
15
+ ];
16
+
17
+ const count = writeJsonlRecords(filePath, records);
18
+
19
+ assert.equal(count, 2);
20
+ assert.equal(fs.readFileSync(filePath, "utf8"), records.map(JSON.stringify).join("\n") + "\n");
21
+ });
22
+
23
+ test("readJsonlRecords streams valid records and skips blank or invalid lines", () => {
24
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-jsonl-"));
25
+ const filePath = path.join(dir, "embeddings.jsonl");
26
+ fs.writeFileSync(filePath, '{"id":"a"}\r\n\nnot json\n{"id":"b"}\n', "utf8");
27
+
28
+ assert.deepEqual(Array.from(readJsonlRecords(filePath)), [{ id: "a" }, { id: "b" }]);
29
+ });
30
+
31
+ test("writeJsonlRecords creates an empty file for empty output", () => {
32
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cortex-jsonl-"));
33
+ const filePath = path.join(dir, "empty.jsonl");
34
+
35
+ const count = writeJsonlRecords(filePath, []);
36
+
37
+ assert.equal(count, 0);
38
+ assert.equal(fs.readFileSync(filePath, "utf8"), "");
39
+ });