@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.
- package/AGENTS.md +470 -0
- package/AUTHORS.md +12 -0
- package/COMMERCIAL-LICENSE.md +19 -0
- package/CONTRIBUTING.md +15 -0
- package/HOW_IT_WORKS.md +4210 -0
- package/LICENSE.md +117 -0
- package/README.md +290 -0
- package/TRADEMARKS.md +19 -0
- package/example/demo.ts +46 -0
- package/example/train_base.ts +2675 -0
- package/index.html +893 -0
- package/package.json +25 -0
- package/src/alphabet.ts +34 -0
- package/src/alu/README.md +332 -0
- package/src/alu/src/alu.ts +541 -0
- package/src/alu/src/expr.ts +339 -0
- package/src/alu/src/index.ts +115 -0
- package/src/alu/src/kernel-arith.ts +377 -0
- package/src/alu/src/kernel-bits.ts +199 -0
- package/src/alu/src/kernel-logic.ts +102 -0
- package/src/alu/src/kernel-nd.ts +235 -0
- package/src/alu/src/kernel-numeric.ts +466 -0
- package/src/alu/src/operation.ts +344 -0
- package/src/alu/src/parser.ts +574 -0
- package/src/alu/src/resonance.ts +161 -0
- package/src/alu/src/text.ts +83 -0
- package/src/alu/src/value.ts +327 -0
- package/src/alu/test/alu.test.ts +1004 -0
- package/src/bytes.ts +62 -0
- package/src/config.ts +227 -0
- package/src/derive/README.md +295 -0
- package/src/derive/src/deduction.ts +263 -0
- package/src/derive/src/index.ts +24 -0
- package/src/derive/src/priority-queue.ts +70 -0
- package/src/derive/src/rewrite.ts +127 -0
- package/src/derive/src/trie.ts +242 -0
- package/src/derive/test/derive.test.ts +137 -0
- package/src/extension.ts +42 -0
- package/src/geometry.ts +511 -0
- package/src/index.ts +38 -0
- package/src/ingest-cache.ts +224 -0
- package/src/mind/articulation.ts +137 -0
- package/src/mind/attention.ts +1061 -0
- package/src/mind/canonical.ts +107 -0
- package/src/mind/graph-search.ts +1100 -0
- package/src/mind/index.ts +18 -0
- package/src/mind/junction.ts +347 -0
- package/src/mind/learning.ts +246 -0
- package/src/mind/match.ts +507 -0
- package/src/mind/mechanisms/alu.ts +33 -0
- package/src/mind/mechanisms/cast.ts +568 -0
- package/src/mind/mechanisms/confluence.ts +268 -0
- package/src/mind/mechanisms/cover.ts +248 -0
- package/src/mind/mechanisms/extraction.ts +422 -0
- package/src/mind/mechanisms/recall.ts +241 -0
- package/src/mind/mind.ts +483 -0
- package/src/mind/pipeline-mechanism.ts +326 -0
- package/src/mind/pipeline.ts +300 -0
- package/src/mind/primitives.ts +201 -0
- package/src/mind/rationale.ts +275 -0
- package/src/mind/reasoning.ts +198 -0
- package/src/mind/recognition.ts +234 -0
- package/src/mind/resonance.ts +0 -0
- package/src/mind/trace.ts +108 -0
- package/src/mind/traverse.ts +556 -0
- package/src/mind/types.ts +281 -0
- package/src/rabitq-hnsw/README.md +303 -0
- package/src/rabitq-hnsw/src/database.ts +492 -0
- package/src/rabitq-hnsw/src/heap.ts +90 -0
- package/src/rabitq-hnsw/src/hnsw.ts +514 -0
- package/src/rabitq-hnsw/src/index.ts +15 -0
- package/src/rabitq-hnsw/src/prng.ts +39 -0
- package/src/rabitq-hnsw/src/rabitq.ts +308 -0
- package/src/rabitq-hnsw/src/store.ts +994 -0
- package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
- package/src/store-sqlite.ts +928 -0
- package/src/store.ts +2148 -0
- package/src/vec.ts +124 -0
- package/test/00-extract.test.mjs +151 -0
- package/test/01-floor.test.mjs +20 -0
- package/test/02-roundtrip.test.mjs +83 -0
- package/test/03-recall.test.mjs +98 -0
- package/test/04-think.test.mjs +627 -0
- package/test/05-concepts.test.mjs +84 -0
- package/test/08-storage.test.mjs +948 -0
- package/test/09-edges.test.mjs +65 -0
- package/test/11-universality.test.mjs +180 -0
- package/test/13-conversation.test.mjs +228 -0
- package/test/14-scaling.test.mjs +503 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/16-bridge.test.mjs +250 -0
- package/test/17-intelligence.test.mjs +240 -0
- package/test/18-alu.test.mjs +209 -0
- package/test/19-nd.test.mjs +254 -0
- package/test/20-stability.test.mjs +489 -0
- package/test/21-partial.test.mjs +158 -0
- package/test/22-multihop.test.mjs +130 -0
- package/test/23-rationale.test.mjs +316 -0
- package/test/24-generalization.test.mjs +416 -0
- package/test/25-embedding.test.mjs +75 -0
- package/test/26-cooperative.test.mjs +89 -0
- package/test/27-saturation-drop.test.mjs +637 -0
- package/test/28-unknowable.test.mjs +88 -0
- package/test/29-counterfactual.test.mjs +476 -0
- package/test/30-conflict-resolution.test.mjs +166 -0
- package/test/31-audit.test.mjs +288 -0
- package/test/32-confluence.test.mjs +173 -0
- package/test/33-multi-candidate.test.mjs +222 -0
- package/test/34-cross-region.test.mjs +252 -0
- 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
|
+
});
|
|
Binary file
|