@hviana/sema 0.1.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 (110) hide show
  1. package/AGENTS.md +470 -0
  2. package/AUTHORS.md +12 -0
  3. package/COMMERCIAL-LICENSE.md +19 -0
  4. package/CONTRIBUTING.md +15 -0
  5. package/HOW_IT_WORKS.md +4210 -0
  6. package/LICENSE.md +117 -0
  7. package/README.md +290 -0
  8. package/TRADEMARKS.md +19 -0
  9. package/example/demo.ts +46 -0
  10. package/example/train_base.ts +2675 -0
  11. package/index.html +893 -0
  12. package/package.json +25 -0
  13. package/src/alphabet.ts +34 -0
  14. package/src/alu/README.md +332 -0
  15. package/src/alu/src/alu.ts +541 -0
  16. package/src/alu/src/expr.ts +339 -0
  17. package/src/alu/src/index.ts +115 -0
  18. package/src/alu/src/kernel-arith.ts +377 -0
  19. package/src/alu/src/kernel-bits.ts +199 -0
  20. package/src/alu/src/kernel-logic.ts +102 -0
  21. package/src/alu/src/kernel-nd.ts +235 -0
  22. package/src/alu/src/kernel-numeric.ts +466 -0
  23. package/src/alu/src/operation.ts +344 -0
  24. package/src/alu/src/parser.ts +574 -0
  25. package/src/alu/src/resonance.ts +161 -0
  26. package/src/alu/src/text.ts +83 -0
  27. package/src/alu/src/value.ts +327 -0
  28. package/src/alu/test/alu.test.ts +1004 -0
  29. package/src/bytes.ts +62 -0
  30. package/src/config.ts +227 -0
  31. package/src/derive/README.md +295 -0
  32. package/src/derive/src/deduction.ts +263 -0
  33. package/src/derive/src/index.ts +24 -0
  34. package/src/derive/src/priority-queue.ts +70 -0
  35. package/src/derive/src/rewrite.ts +127 -0
  36. package/src/derive/src/trie.ts +242 -0
  37. package/src/derive/test/derive.test.ts +137 -0
  38. package/src/extension.ts +42 -0
  39. package/src/geometry.ts +511 -0
  40. package/src/index.ts +38 -0
  41. package/src/ingest-cache.ts +224 -0
  42. package/src/mind/articulation.ts +137 -0
  43. package/src/mind/attention.ts +1061 -0
  44. package/src/mind/canonical.ts +107 -0
  45. package/src/mind/graph-search.ts +1100 -0
  46. package/src/mind/index.ts +18 -0
  47. package/src/mind/junction.ts +347 -0
  48. package/src/mind/learning.ts +246 -0
  49. package/src/mind/match.ts +507 -0
  50. package/src/mind/mechanisms/alu.ts +33 -0
  51. package/src/mind/mechanisms/cast.ts +568 -0
  52. package/src/mind/mechanisms/confluence.ts +268 -0
  53. package/src/mind/mechanisms/cover.ts +248 -0
  54. package/src/mind/mechanisms/extraction.ts +422 -0
  55. package/src/mind/mechanisms/recall.ts +241 -0
  56. package/src/mind/mind.ts +483 -0
  57. package/src/mind/pipeline-mechanism.ts +326 -0
  58. package/src/mind/pipeline.ts +300 -0
  59. package/src/mind/primitives.ts +201 -0
  60. package/src/mind/rationale.ts +275 -0
  61. package/src/mind/reasoning.ts +198 -0
  62. package/src/mind/recognition.ts +234 -0
  63. package/src/mind/resonance.ts +0 -0
  64. package/src/mind/trace.ts +108 -0
  65. package/src/mind/traverse.ts +556 -0
  66. package/src/mind/types.ts +281 -0
  67. package/src/rabitq-hnsw/README.md +303 -0
  68. package/src/rabitq-hnsw/src/database.ts +492 -0
  69. package/src/rabitq-hnsw/src/heap.ts +90 -0
  70. package/src/rabitq-hnsw/src/hnsw.ts +514 -0
  71. package/src/rabitq-hnsw/src/index.ts +15 -0
  72. package/src/rabitq-hnsw/src/prng.ts +39 -0
  73. package/src/rabitq-hnsw/src/rabitq.ts +308 -0
  74. package/src/rabitq-hnsw/src/store.ts +994 -0
  75. package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
  76. package/src/store-sqlite.ts +928 -0
  77. package/src/store.ts +2148 -0
  78. package/src/vec.ts +124 -0
  79. package/test/00-extract.test.mjs +151 -0
  80. package/test/01-floor.test.mjs +20 -0
  81. package/test/02-roundtrip.test.mjs +83 -0
  82. package/test/03-recall.test.mjs +98 -0
  83. package/test/04-think.test.mjs +627 -0
  84. package/test/05-concepts.test.mjs +84 -0
  85. package/test/08-storage.test.mjs +948 -0
  86. package/test/09-edges.test.mjs +65 -0
  87. package/test/11-universality.test.mjs +180 -0
  88. package/test/13-conversation.test.mjs +228 -0
  89. package/test/14-scaling.test.mjs +503 -0
  90. package/test/15-decomposition-gap.test.mjs +0 -0
  91. package/test/16-bridge.test.mjs +250 -0
  92. package/test/17-intelligence.test.mjs +240 -0
  93. package/test/18-alu.test.mjs +209 -0
  94. package/test/19-nd.test.mjs +254 -0
  95. package/test/20-stability.test.mjs +489 -0
  96. package/test/21-partial.test.mjs +158 -0
  97. package/test/22-multihop.test.mjs +130 -0
  98. package/test/23-rationale.test.mjs +316 -0
  99. package/test/24-generalization.test.mjs +416 -0
  100. package/test/25-embedding.test.mjs +75 -0
  101. package/test/26-cooperative.test.mjs +89 -0
  102. package/test/27-saturation-drop.test.mjs +637 -0
  103. package/test/28-unknowable.test.mjs +88 -0
  104. package/test/29-counterfactual.test.mjs +476 -0
  105. package/test/30-conflict-resolution.test.mjs +166 -0
  106. package/test/31-audit.test.mjs +288 -0
  107. package/test/32-confluence.test.mjs +173 -0
  108. package/test/33-multi-candidate.test.mjs +222 -0
  109. package/test/34-cross-region.test.mjs +252 -0
  110. package/tsconfig.json +16 -0
@@ -0,0 +1,503 @@
1
+ // 14-scaling.test.mjs — TRAINING and INFERENCE must scale.
2
+ //
3
+ // This file is the single home for sema's scaling contracts (it absorbs the old
4
+ // 10-scale.test.mjs). It measures the two lifecycle halves separately and states,
5
+ // for each, the law it must obey:
6
+ //
7
+ // TRAINING (ingestion)
8
+ // • Constant rate: depositing data runs at a steady KB/s that does NOT
9
+ // collapse as the store grows. We measure the STEADY-STATE incremental
10
+ // rate (fresh batches into an already-warm, already-growing store), because
11
+ // a cold first batch pays one-time setup amortised over few bytes and would
12
+ // otherwise flatter small corpora.
13
+ // • Correctness at scale: exact recall is preserved as N grows.
14
+ //
15
+ // INFERENCE (answering)
16
+ // • Sublinear in CORPUS size: the cost of one query barely moves as the store
17
+ // grows by orders of magnitude — work is proportional to the query, not to
18
+ // how much was learned. (exponent in corpus size ≪ 1.)
19
+ // • Constant rate in INPUT length: a query of n bytes costs ~c·n, i.e. a
20
+ // steady KB/s of input processed, regardless of how long the input is.
21
+ // (Linear time ⟺ constant throughput; the guard is that it is not
22
+ // SUPER-linear — no quadratic blow-up.)
23
+ //
24
+ // INDEPENDENT CORPORA: every size point is built from its OWN disjoint corpus
25
+ // (a distinct salt → distinct tokens, no shared forms across points). So a growth
26
+ // curve reflects genuine scaling, never reuse of a warm cache or shared nodes —
27
+ // the growth exponent is the proof.
28
+ //
29
+ // Method: medians of repeated runs; a power law t ≈ c·n^k fit by log–log least
30
+ // squares; throughput as KB processed per second.
31
+
32
+ import { test } from "node:test";
33
+ import assert from "node:assert/strict";
34
+ import { Mind } from "../dist/src/index.js";
35
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
36
+
37
+ // ── measurement helpers ──────────────────────────────────────────────────
38
+ const median = (xs) => {
39
+ const s = [...xs].sort((a, b) => a - b);
40
+ return s[s.length >> 1];
41
+ };
42
+ const fastest = (xs) => Math.min(...xs); // least-noisy lower bound on cost
43
+
44
+ /** Power-law exponent k in t ≈ c·n^k, by log–log least squares.
45
+ * k≈0 flat · k≈1 linear · k≈2 quadratic. */
46
+ function logLogSlope(sizes, times) {
47
+ const n = sizes.length;
48
+ const xs = sizes.map(Math.log), ys = times.map(Math.log);
49
+ const mx = xs.reduce((a, b) => a + b, 0) / n;
50
+ const my = ys.reduce((a, b) => a + b, 0) / n;
51
+ let num = 0, den = 0;
52
+ for (let i = 0; i < n; i++) {
53
+ num += (xs[i] - mx) * (ys[i] - my);
54
+ den += (xs[i] - mx) ** 2;
55
+ }
56
+ return num / den;
57
+ }
58
+
59
+ const bytesOf = (pairs) => {
60
+ let b = 0;
61
+ for (const [q, a] of pairs) {
62
+ b += new TextEncoder().encode(q).length +
63
+ new TextEncoder().encode(a).length;
64
+ }
65
+ return b;
66
+ };
67
+
68
+ // ── INDEPENDENT corpora ────────────────────────────────────────────────────
69
+ // `salt` makes every corpus disjoint from every other: distinct ids, distinct
70
+ // answers, no shared learned forms. A "fact" is a question→answer pair built
71
+ // from the salt and index alone.
72
+ function corpus(n, salt) {
73
+ const pairs = [];
74
+ for (let i = 0; i < n; i++) {
75
+ // The index appears in BOTH question and answer, so every pair is unique and
76
+ // distinguishable — no two questions fold toward a colliding answer (which
77
+ // would make exact recall ambiguous through no fault of the store).
78
+ const id = `${salt}n${i}`;
79
+ pairs.push([`query ${id} please?`, `answer for ${id} is value ${i}`]);
80
+ }
81
+ return pairs;
82
+ }
83
+
84
+ // All-unknown filler whose tokens are substrings of no learned form — isolates
85
+ // the segmentation/scan cost of inference from any recall work.
86
+ function unknownInput(targetBytes) {
87
+ const out = [];
88
+ let len = 0, i = 0;
89
+ while (len < targetBytes) {
90
+ const tok = `zq${i}wx`;
91
+ out.push(tok);
92
+ len += tok.length + 1;
93
+ i++;
94
+ }
95
+ return out.join(" ");
96
+ }
97
+
98
+ // =========================================================================
99
+ // TRAINING — constant-rate ingestion + exact recall at scale
100
+ // =========================================================================
101
+
102
+ // ── why the OLD constant-rate test missed the real regression ───────────────
103
+ // The earlier guard poured several `corpus()` batches into one growing store and
104
+ // compared early-vs-late wall-clock KB/s. It missed the real regression for two
105
+ // reasons this rewrite removes:
106
+ //
107
+ // 1. It timed BATCHES of scaffolding-sharing pairs. `corpus()` pairs share a fixed
108
+ // frame ("query … please?"), so most of each later batch DEDUPS — the costly
109
+ // path (intern + INDEX a new node) is rarely hit, and per-batch KB/s stays
110
+ // flat regardless of store size.
111
+ // 2. WALL-CLOCK at test scale is too noisy to assert on: GC and scheduling
112
+ // jitter swamp the signal over a few thousand deposits, so any strict
113
+ // degradation bound is either flaky or so loose it catches nothing.
114
+ //
115
+ // The real degradation lived in the vector index (rabitq-hnsw) and is now guarded
116
+ // DETERMINISTICALLY at its own layer (testScalabilityWithoutCache, on the
117
+ // cache-independent storage-read count). HERE we assert the same contract end to
118
+ // end through the Mind, also by STORAGE READS rather than time: the work one
119
+ // recall touch does in the content index must be set by the query, not by how
120
+ // many vectors the store holds. Reads are exact and jitter-free, so the bound can
121
+ // be strict and the test fast.
122
+
123
+ // A genuinely-novel experience of distinctive words — every call interns fresh
124
+ // leaves/branches and indexes new gists (the expensive path), so the store grows
125
+ // with real, non-dedup content.
126
+ function novelExperience(i, salt) {
127
+ const w = [];
128
+ for (let k = 0; k < 8; k++) w.push(`${salt}w${i}x${k}q${(i * 7 + k * 3)}`);
129
+ return w.join(" ");
130
+ }
131
+
132
+ // DEGRADATION GUARD (the headline), measured by STORAGE READS, not time. Build a
133
+ // SMALL store and a ~10x LARGER one from disjoint novel content, then resonate
134
+ // the SAME fixed query against each and compare how many content-index rows the
135
+ // lookup read. A recall touch visits ~log N nodes, so reads may rise a little
136
+ // with N — but a store that degraded (re-reading a working set that grows with
137
+ // the corpus) would show reads climbing roughly with N. The bound is strict
138
+ // because the count is exact and cache-independent.
139
+ test("training: recall work does NOT grow with the store (storage reads, not time)", async () => {
140
+ const probe = "fixed distinctive probe alpha beta gamma delta epsilon";
141
+ const store = new SQliteStore({ path: ":memory:" });
142
+ const mind = new Mind({ seed: 7, store });
143
+
144
+ // Reads the SAME fixed query touches in the content index at the current store
145
+ // size. The page cache does not matter — reads are counted below it — so we
146
+ // run with the default cache for a fast build and still measure the honest,
147
+ // cache-independent cost.
148
+ const readsNow = async () => {
149
+ await mind.respond(probe); // warm the scan automaton
150
+ await mind.respond(probe); // the measured resonate
151
+ return store.lastResonateReads();
152
+ };
153
+
154
+ // Grow ONE store from a small N to a ~6x larger N (disjoint novel content each
155
+ // step), measuring the fixed query's reads at each size. One store keeps the
156
+ // build cheap; the only variable across the two measurements is corpus size.
157
+ const grow = async (from, to) => {
158
+ for (let i = from; i < to; i++) await mind.ingest(novelExperience(i, "g"));
159
+ };
160
+
161
+ await grow(0, 300);
162
+ const small = await readsNow();
163
+ await grow(300, 1800); // 6x the corpus
164
+ const large = await readsNow();
165
+ await store.close();
166
+
167
+ console.log(
168
+ ` content-index reads for one recall: N=300 → ${small}, N=1800 → ${large}`,
169
+ );
170
+
171
+ // 6x the corpus must not cost anywhere near 6x the reads. A genuinely
172
+ // sublinear (~log N) lookup barely moves; a per-recall cost that tracked the
173
+ // corpus would roughly 6x. Allow a generous 3x band for HNSW's log-N growth
174
+ // and graph-shape variation, far below the ~6x a true O(N) regression shows.
175
+ assert.ok(
176
+ large < small * 3 + 50,
177
+ `one recall read ${small} content-index rows at N=300 but ${large} at ` +
178
+ `N=1800 (6x corpus) — recall work is growing with the store; training/` +
179
+ `inference must stay sublinear in corpus size`,
180
+ );
181
+ });
182
+
183
+ // ABSOLUTE THROUGHPUT FLOOR (catches plain slowness, independent of N). Even a
184
+ // perfectly flat curve is useless if the constant is tiny. Deposit novel
185
+ // experiences into a small, warm store and require a sane KB/s floor, so a
186
+ // regression that makes EVERY deposit slow (not just large stores) also fails.
187
+ // This is a coarse smoke floor, set well below a healthy build, so machine speed
188
+ // and jitter never make it flaky — it only trips on a genuine slowdown.
189
+ test("training: absolute deposition throughput clears a sane floor", async () => {
190
+ const store = new SQliteStore({ path: ":memory:" });
191
+ const mind = new Mind({ seed: 7, store });
192
+
193
+ // Warm up (JIT, index init) so this measures steady-state per-deposit cost,
194
+ // not one-time setup.
195
+ for (let i = 0; i < 200; i++) await mind.ingest(novelExperience(i, "warm"));
196
+
197
+ const N = 1000;
198
+ let bytes = 0;
199
+ const t0 = performance.now();
200
+ for (let i = 0; i < N; i++) {
201
+ const x = novelExperience(i, "floor");
202
+ bytes += new TextEncoder().encode(x).length;
203
+ await mind.ingest(x);
204
+ }
205
+ const secs = (performance.now() - t0) / 1000;
206
+ const kbps = bytes / 1024 / secs;
207
+ await store.close();
208
+
209
+ console.log(
210
+ ` absolute deposition throughput: ${kbps.toFixed(1)} KB/s ` +
211
+ `(${N} novel experiences, ${(bytes / 1024).toFixed(0)} KB)`,
212
+ );
213
+
214
+ // A generous floor: well below what a healthy build sustains, but far above a
215
+ // regressed one. Deposition is CPU-bound perception + a bounded number of
216
+ // index writes; it should clear several KB/s even on a slow machine.
217
+ const FLOOR = 5;
218
+ assert.ok(
219
+ kbps > FLOOR,
220
+ `deposition ran at ${
221
+ kbps.toFixed(1)
222
+ } KB/s — below the ${FLOOR} KB/s floor; training is too slow`,
223
+ );
224
+ });
225
+
226
+ // Distinctive facts — every pair has its own adjective·noun·verb·number content,
227
+ // so each is genuinely separable (NOT the scaffolding-dominated near-duplicates of
228
+ // `corpus()`, whose collisions are the intelligence file's concern). This is the
229
+ // corpus on which exact recall SHOULD be perfect.
230
+ const ADJ = [
231
+ "crystal",
232
+ "ancient",
233
+ "silent",
234
+ "burning",
235
+ "hollow",
236
+ "gilded",
237
+ "frozen",
238
+ "wandering",
239
+ ];
240
+ const NOUN = [
241
+ "river",
242
+ "archive",
243
+ "compass",
244
+ "lantern",
245
+ "garden",
246
+ "engine",
247
+ "harbor",
248
+ "mirror",
249
+ ];
250
+ const VERB = [
251
+ "holds",
252
+ "guards",
253
+ "reveals",
254
+ "feeds",
255
+ "remembers",
256
+ "follows",
257
+ "shelters",
258
+ "names",
259
+ ];
260
+ function distinctiveCorpus(n) {
261
+ const pairs = [];
262
+ for (let i = 0; i < n; i++) {
263
+ const a = ADJ[i % 8], no = NOUN[(i >> 3) % 8], v = VERB[(i >> 6) % 8];
264
+ const id = `${a} ${no} ${i}`;
265
+ pairs.push([
266
+ `what does the ${id} ${v}?`,
267
+ `the ${id} ${v} the ${ADJ[(i + 3) % 8]} ${NOUN[(i + 5) % 8]} of ${i * 7}`,
268
+ ]);
269
+ }
270
+ return pairs;
271
+ }
272
+
273
+ // Training must not blur or drop a learned fact as the corpus grows: a fact that
274
+ // was deposited must recall EXACTLY. This is the deposition correctness contract.
275
+ //
276
+ // KNOWN REGRESSION (2026-06-27): on the current build this FAILS — exact recall
277
+ // is only ~50% at N=400 (and degrades with N), because the resonance/near-dedup
278
+ // merge + 1-bit codec collapses distinct-but-similar gists at scale. The bar is
279
+ // strict ON PURPOSE so the regression stays visible until the at-scale recall
280
+ // accuracy is fixed; it is NOT relaxed to hide the loss. (The old N=400 recall
281
+ // assertion existed but was SEMA_SCALE-skipped, so this went unnoticed.)
282
+ test("training: exact recall is preserved at scale", async () => {
283
+ const N = process.env.SEMA_SCALE ? Number(process.env.SEMA_SCALE) : 120;
284
+ const store = new SQliteStore({ path: ":memory:" });
285
+ const mind = new Mind({ seed: 7, store });
286
+ const pairs = distinctiveCorpus(N);
287
+ await mind.ingest(pairs);
288
+
289
+ const sample = Math.min(40, N);
290
+ let exact = 0;
291
+ const misses = [];
292
+ for (let s = 0; s < sample; s++) {
293
+ const i = Math.floor((s * N) / sample);
294
+ const got = await mind.respondText(pairs[i][0]);
295
+ if (got === pairs[i][1]) exact++;
296
+ else if (misses.length < 3) {
297
+ misses.push(`"${pairs[i][0]}" → "${got.slice(0, 40)}"`);
298
+ }
299
+ }
300
+ const entries = await mind.store.size();
301
+ await mind.store.close();
302
+ console.log(
303
+ ` N=${N}: ${entries} entries, exact recall ${exact}/${sample}`,
304
+ );
305
+ assert.equal(
306
+ exact,
307
+ sample,
308
+ `only ${exact}/${sample} distinctive facts recalled exactly at N=${N} — ` +
309
+ `every deposited fact must recall exactly. KNOWN REGRESSION: at-scale ` +
310
+ `recall is degraded by gist over-merge + lossy codec; fix the store's ` +
311
+ `merge/recall accuracy, do not relax this bar.\n ` +
312
+ misses.join("\n "),
313
+ );
314
+ });
315
+
316
+ // =========================================================================
317
+ // INFERENCE — sublinear in corpus, constant-rate in input length
318
+ // =========================================================================
319
+
320
+ // One fixed query, INDEPENDENT corpora of growing size. The cost must barely
321
+ // move: inference work is proportional to the QUERY, not to the corpus. The
322
+ // growth exponent in corpus size is the proof — it must be well below linear.
323
+ test("inference: cost is sublinear in corpus size (independent corpora)", async () => {
324
+ const query = unknownInput(1024);
325
+ const sizes = [50, 200, 800, 3200];
326
+ const times = [];
327
+
328
+ for (const n of sizes) {
329
+ const store = new SQliteStore({ path: ":memory:" });
330
+ const mind = new Mind({ seed: 7, store });
331
+ await mind.ingest(corpus(n, `infcorp${n}`)); // disjoint corpus per point
332
+ await mind.respond(query); // warm the scan automaton
333
+ const samples = [];
334
+ for (let r = 0; r < 5; r++) {
335
+ const t0 = performance.now();
336
+ await mind.respond(query);
337
+ samples.push(performance.now() - t0);
338
+ }
339
+ times.push(fastest(samples));
340
+ await mind.store.close();
341
+ }
342
+
343
+ const k = logLogSlope(sizes, times);
344
+ console.log(" inference vs corpus size (fixed 1KB query):");
345
+ sizes.forEach((n, i) =>
346
+ console.log(
347
+ ` corpus=${String(n).padStart(4)} pairs ${times[i].toFixed(1)} ms`,
348
+ )
349
+ );
350
+ console.log(
351
+ ` growth exponent k ≈ ${k.toFixed(2)} (corpus axis; ≪ 1 = sublinear)`,
352
+ );
353
+
354
+ // 64× the corpus must not cost anywhere near 64× the time. Sublinear: exponent
355
+ // well under 1 (a flat-ish ~log curve), so the per-query cost is governed by
356
+ // the query, not the store.
357
+ assert.ok(
358
+ k < 0.6,
359
+ `inference grew with exponent k=${k.toFixed(2)} in corpus size — ` +
360
+ `expected ≪ 1 (sublinear; cost set by the query, not the corpus)`,
361
+ );
362
+ });
363
+
364
+ // One fixed corpus, query LENGTH growing. Time must rise about LINEARLY with
365
+ // input bytes — a steady KB/s of input processed — and never super-linearly
366
+ // (no quadratic scan, no exponential fuse). Constant throughput is the headline
367
+ // number; the exponent guards against blow-up.
368
+ test("inference: input is processed at a roughly constant KB/s (linear, not quadratic)", async () => {
369
+ const store = new SQliteStore({ path: ":memory:" });
370
+ const mind = new Mind({ seed: 7, store });
371
+ await mind.ingest(corpus(200, "inflen"));
372
+
373
+ const kbs = [0.5, 1, 2, 4, 8];
374
+ const queries = kbs.map((kb) => unknownInput(kb * 1024));
375
+ const bytes = queries.map((q) => new TextEncoder().encode(q).length);
376
+
377
+ await mind.respond(queries[queries.length - 1]); // warm up
378
+
379
+ const times = [];
380
+ for (const q of queries) {
381
+ const samples = [];
382
+ for (let r = 0; r < 5; r++) {
383
+ const t0 = performance.now();
384
+ await mind.respond(q);
385
+ samples.push(performance.now() - t0);
386
+ }
387
+ times.push(fastest(samples));
388
+ }
389
+ await mind.store.close();
390
+
391
+ const rates = bytes.map((b, i) => (b / 1024) / (times[i] / 1000)); // KB/s
392
+ const k = logLogSlope(bytes, times);
393
+ console.log(" inference vs input length (fixed corpus):");
394
+ bytes.forEach((b, i) =>
395
+ console.log(
396
+ ` in=${(b / 1024).toFixed(1)}KB ${
397
+ times[i].toFixed(1).padStart(7)
398
+ } ms ${rates[i].toFixed(1)} KB/s`,
399
+ )
400
+ );
401
+ // Measure the throughput band over the LARGER inputs only: the smallest query
402
+ // is dominated by fixed per-query overhead (one climb + resonate, tens of ms
403
+ // on a handful of bytes), so its KB/s legitimately reads low and is noise-
404
+ // sensitive — it says nothing about how INPUT LENGTH scales. From 1KB up,
405
+ // throughput is steady.
406
+ const steadyRates = rates.slice(1);
407
+ const loR = Math.min(...steadyRates), hiR = Math.max(...steadyRates);
408
+ console.log(
409
+ ` throughput band (≥1KB) ${loR.toFixed(1)}–${
410
+ hiR.toFixed(1)
411
+ } KB/s · length exponent k ≈ ${k.toFixed(2)}`,
412
+ );
413
+
414
+ // PRIMARY guard — not super-linear: time grows about linearly (or better),
415
+ // never quadratically. This is the load-independent contract; an exponential
416
+ // fuse or quadratic scan would push k well past 1.
417
+ assert.ok(
418
+ k < 1.3,
419
+ `input-length exponent k=${
420
+ k.toFixed(2)
421
+ } — expected ≤ ~1 (linear or sublinear), never quadratic`,
422
+ );
423
+ // Secondary — constant rate: past the fixed-overhead floor, throughput does
424
+ // not fall off as the input grows (a generous band absorbs scheduling jitter).
425
+ assert.ok(
426
+ hiR < loR * 3,
427
+ `throughput fanned out ${
428
+ (hiR / loR).toFixed(1)
429
+ }× across ≥1KB inputs — expected a roughly constant KB/s`,
430
+ );
431
+ });
432
+
433
+ // =========================================================================
434
+ // INFERENCE — correctness preserved while scaling
435
+ // =========================================================================
436
+
437
+ // Completion must still fire correctly when the learned form is buried in a long
438
+ // unknown input — scaling the input must not lose the recall.
439
+ test("inference: completion still fires inside long inputs", async () => {
440
+ const mind = new Mind({
441
+ seed: 7,
442
+ });
443
+ await mind.ingest([["ice", "cold"], ["fire", "hot"], ["2+2", "4"]]);
444
+
445
+ for (const pad of [16, 64, 256, 1024]) {
446
+ const filler = unknownInput(pad);
447
+ const mid = filler.slice(0, filler.length >> 1);
448
+ const end = filler.slice(filler.length >> 1);
449
+ assert.equal(await mind.respondText(`${mid} ice ${end}`), "cold");
450
+ assert.equal(await mind.respondText(`${mid} 2+2 ${end}`), "4");
451
+ }
452
+ await mind.store.close();
453
+ });
454
+
455
+ // Worst case for the cover: MANY distinct learned forms abutted with no
456
+ // separators ("w0w1w2…"). A node-less fused span is kept alive only while
457
+ // it can still grow INTO a learned form — bytes ≤ W (findLeaf), or the pair
458
+ // ACTUALLY forms a branch (findBranch). It is NOT kept merely because both
459
+ // sides carry a node — that "potential" gate generated O(N²) chart items for
460
+ // N abutted forms where only O(N) pairs actually form branches. The GROWTH
461
+ // RATIO is the load-independent proof.
462
+ test("inference: many abutted learned forms stay polynomial, not exponential", async () => {
463
+ const mind = new Mind({
464
+ seed: 7,
465
+ });
466
+ const pairs = [];
467
+ for (let i = 0; i < 16; i++) pairs.push([`w${i}`, `a${i}`]);
468
+ await mind.ingest(pairs);
469
+
470
+ const timeParts = async (np) => {
471
+ const q = Array.from({ length: np }, (_, i) => `w${i}`).join("");
472
+ const t0 = performance.now();
473
+ await mind.respondText(q);
474
+ return performance.now() - t0;
475
+ };
476
+
477
+ await timeParts(4); // warm up
478
+ const small = median(await Promise.all([4, 4, 4].map(timeParts)));
479
+ const big = median(await Promise.all([12, 12, 12].map(timeParts)));
480
+ const ratio = big / Math.max(small, 0.5);
481
+ console.log(
482
+ ` abutted forms: 4 parts ${small.toFixed(1)}ms → 12 parts ${
483
+ big.toFixed(1)
484
+ }ms (${ratio.toFixed(1)}×)`,
485
+ );
486
+
487
+ // The old O(N²)-potential gate let 3× the parts blow up the chart; with the
488
+ // actual-branch-evidence gate growth stays near-linear (roughly proportional
489
+ // to parts). The ratio is the precise, load-independent guard.
490
+ assert.ok(
491
+ ratio < 25,
492
+ `cost grew ${
493
+ ratio.toFixed(1)
494
+ }× for 3× the parts — expected polynomial (≪ 25×), not exponential`,
495
+ );
496
+ assert.ok(
497
+ big < 1500,
498
+ `12 abutted forms took ${
499
+ big.toFixed(0)
500
+ }ms — expected < 1500ms (no 2ⁿ fuse)`,
501
+ );
502
+ await mind.store.close();
503
+ });