@hviana/sema 0.7.2 → 0.7.5
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 +95 -843
- package/README.md +11 -11
- package/dist/src/mind/mind.js +14 -0
- package/dist/src/store-sqlite.js +17 -0
- package/dist/src/store.d.ts +51 -4
- package/dist/src/store.js +81 -14
- package/docs/INDEX.md +71 -0
- package/docs/INVARIANTS.md +19 -0
- package/docs/architecture/bounded-reads.md +85 -0
- package/docs/architecture/caches.md +89 -0
- package/docs/architecture/commonality.md +45 -0
- package/docs/architecture/cost-model.md +71 -0
- package/docs/architecture/determinism.md +73 -0
- package/docs/architecture/exact-vs-approximate.md +47 -0
- package/docs/architecture/factored-machinery.md +28 -0
- package/docs/architecture/fold-contract.md +87 -0
- package/docs/architecture/halo-sketch.md +99 -0
- package/docs/architecture/match-project.md +62 -0
- package/docs/architecture/mechanism-market.md +95 -0
- package/docs/architecture/memoization.md +96 -0
- package/docs/architecture/meter.md +55 -0
- package/docs/architecture/saturation.md +92 -0
- package/docs/architecture/store.md +79 -0
- package/docs/architecture/thresholds.md +79 -0
- package/docs/failures/tempting-but-wrong.md +144 -0
- package/docs/harness/gates.md +56 -0
- package/docs/mechanisms/alu.md +75 -0
- package/docs/mechanisms/cast.md +75 -0
- package/docs/mechanisms/confluence.md +36 -0
- package/docs/mechanisms/cover.md +54 -0
- package/docs/mechanisms/extraction.md +53 -0
- package/docs/mechanisms/prefix-completion.md +54 -0
- package/docs/mechanisms/recall.md +69 -0
- package/docs/mechanisms/reference.md +58 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/mind/mind.ts +14 -0
- package/src/store-sqlite.ts +19 -0
- package/src/store.ts +92 -16
- package/test/89-completion-recursion.test.mjs +30 -10
- package/test/96-bytes-walk-termination.test.mjs +115 -0
- package/test/97-store-seed.test.mjs +105 -0
- package/HOW_IT_WORKS.md +0 -5836
package/src/store.ts
CHANGED
|
@@ -285,6 +285,17 @@ export class BoundedMap<K, V> {
|
|
|
285
285
|
export interface Store {
|
|
286
286
|
readonly D: number;
|
|
287
287
|
|
|
288
|
+
/** The seed the artifact was TRAINED with, recovered from the store's own
|
|
289
|
+
* `train.seed` metadata, or null for a store that was never trained.
|
|
290
|
+
*
|
|
291
|
+
* This is not decoration: the seed feeds the alphabet and the seat keyring
|
|
292
|
+
* (see the Mind constructor), so folding a query under any other seed lands
|
|
293
|
+
* in a different vector space than the one the artifact's nodes were folded
|
|
294
|
+
* into. A Mind opening a trained store MUST adopt this seed unless the
|
|
295
|
+
* caller explicitly overrides it — the same discipline that recovers
|
|
296
|
+
* `train.D` and `geometry.maxGroup` from the metadata. */
|
|
297
|
+
readonly trainSeed: number | null;
|
|
298
|
+
|
|
288
299
|
/** The work accumulator for the inference call in flight, or null. The
|
|
289
300
|
* Mind attaches one per profiled response and detaches it after (see
|
|
290
301
|
* src/meter.ts). A store MUST only ever write to it — no read may reach
|
|
@@ -917,6 +928,10 @@ export abstract class AbstractStore implements Store {
|
|
|
917
928
|
|
|
918
929
|
protected _D: number;
|
|
919
930
|
protected _maxGroup: number;
|
|
931
|
+
/** `train.seed` recovered by the backend at open, or null when the store was
|
|
932
|
+
* never trained. A backend that omits it simply reports null, which leaves
|
|
933
|
+
* the caller's configured seed in force. */
|
|
934
|
+
protected _trainSeed: number | null = null;
|
|
920
935
|
protected readonly minHaloMass: number;
|
|
921
936
|
protected readonly efSearch: number;
|
|
922
937
|
protected readonly overfetch: number;
|
|
@@ -1091,6 +1106,13 @@ export abstract class AbstractStore implements Store {
|
|
|
1091
1106
|
return this._D;
|
|
1092
1107
|
}
|
|
1093
1108
|
|
|
1109
|
+
/** The seed the artifact was trained with, recovered from `train.seed` at
|
|
1110
|
+
* open. Null for a store that was never trained. See
|
|
1111
|
+
* {@link Store.trainSeed} for why this must govern inference. */
|
|
1112
|
+
get trainSeed(): number | null {
|
|
1113
|
+
return this._trainSeed;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1094
1116
|
/** Await the async initialisation performed by the concrete constructor. */
|
|
1095
1117
|
protected async _ensureReady(): Promise<void> {
|
|
1096
1118
|
if (!this._ready) throw new Error("Store: not open");
|
|
@@ -1133,9 +1155,6 @@ export abstract class AbstractStore implements Store {
|
|
|
1133
1155
|
return rec;
|
|
1134
1156
|
}
|
|
1135
1157
|
|
|
1136
|
-
/** Reconstruct the bytes a node spans by traversing the DAG bottom-up.
|
|
1137
|
-
* Iterative post-order on an explicit stack — the call stack never sees the
|
|
1138
|
-
* tree depth, so even an adversarial chain of nodes stays safe. */
|
|
1139
1158
|
/** How many reads hit a MISSING node record this session (a dangling edge
|
|
1140
1159
|
* or kid id). Zero in a healthy store; a growing count means references
|
|
1141
1160
|
* outlive their records — the read degrades safely to empty bytes, this
|
|
@@ -1147,6 +1166,25 @@ export abstract class AbstractStore implements Store {
|
|
|
1147
1166
|
* unprofiled store pays one null check per read and allocates nothing. */
|
|
1148
1167
|
meter: Meter | null = null;
|
|
1149
1168
|
|
|
1169
|
+
/** Reconstruct the bytes a node spans by traversing the DAG bottom-up.
|
|
1170
|
+
* Iterative post-order on an explicit stack — the call stack never sees the
|
|
1171
|
+
* tree depth, so even an adversarial chain of nodes stays safe.
|
|
1172
|
+
*
|
|
1173
|
+
* TERMINATION. The walk memoizes into a LOCAL map, and `_bytesCache` is
|
|
1174
|
+
* consulted only as a warm hint whose hit is immediately promoted into that
|
|
1175
|
+
* map. It used to use `_bytesCache` itself as the memo, which is not a
|
|
1176
|
+
* memo at all: it EVICTS, and its `"smallest"` policy prefers precisely the
|
|
1177
|
+
* freshly-resolved small children that the pending parents on the stack are
|
|
1178
|
+
* waiting for. A parent then finds them uncached again, re-pushes them,
|
|
1179
|
+
* they are re-resolved, re-inserted, re-evicted — the loop makes no
|
|
1180
|
+
* progress and never exits. Latent until the cache saturates, then
|
|
1181
|
+
* unconditional: observed in the wild at 19.9M nodes with the 20 MB cache
|
|
1182
|
+
* pinned at 19,999,962/20,000,000 bytes, spinning 8h45m on a node whose
|
|
1183
|
+
* whole content was 124 bytes (5 kids, 2 of them perpetually re-evicted).
|
|
1184
|
+
* Because the loop is synchronous, no timer could fire — the trainer's stall
|
|
1185
|
+
* watchdog never got a turn either. A local map resolves each node at most
|
|
1186
|
+
* once per call, so the walk terminates by construction and `_bytesCache`
|
|
1187
|
+
* goes back to being a pure speed hint. */
|
|
1150
1188
|
bytes(id: NodeId): Uint8Array {
|
|
1151
1189
|
if (this.meter) {
|
|
1152
1190
|
this.meter.byteReads++;
|
|
@@ -1162,12 +1200,25 @@ export abstract class AbstractStore implements Store {
|
|
|
1162
1200
|
|
|
1163
1201
|
const stack: NodeId[] = [id];
|
|
1164
1202
|
const cache = this._bytesCache;
|
|
1203
|
+
// The walk's own memo. Entries are the same shared arrays `_bytesCache`
|
|
1204
|
+
// holds (no extra copy), and it lives exactly as long as this call.
|
|
1205
|
+
const done = new Map<NodeId, Uint8Array>();
|
|
1165
1206
|
|
|
1166
1207
|
while (stack.length > 0) {
|
|
1167
1208
|
const nid = stack[stack.length - 1]; // peek
|
|
1168
1209
|
|
|
1169
|
-
// Already resolved by
|
|
1170
|
-
|
|
1210
|
+
// Already resolved by this walk — the ONLY authority the readiness test
|
|
1211
|
+
// below trusts, because it cannot be evicted underneath us.
|
|
1212
|
+
if (done.has(nid)) {
|
|
1213
|
+
stack.pop();
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Warm hint: a hit is promoted into `done` in the same step, so from
|
|
1218
|
+
// here on the entry is pinned for the rest of the walk.
|
|
1219
|
+
const warm = cache.get(nid);
|
|
1220
|
+
if (warm !== undefined) {
|
|
1221
|
+
done.set(nid, warm);
|
|
1171
1222
|
stack.pop();
|
|
1172
1223
|
continue;
|
|
1173
1224
|
}
|
|
@@ -1180,22 +1231,27 @@ export abstract class AbstractStore implements Store {
|
|
|
1180
1231
|
// The cache makes the empty read permanent for the session; the
|
|
1181
1232
|
// counter survives as the visible trace.
|
|
1182
1233
|
this.danglingReads++;
|
|
1234
|
+
done.set(nid, _ZERO);
|
|
1183
1235
|
cache.set(nid, _ZERO);
|
|
1184
1236
|
stack.pop();
|
|
1185
1237
|
continue;
|
|
1186
1238
|
}
|
|
1187
1239
|
if (rec.leaf) {
|
|
1188
|
-
|
|
1240
|
+
// COPY before caching: rec.leaf is the node record's own buffer, and
|
|
1241
|
+
// handing it out would let one mutating caller corrupt the record.
|
|
1242
|
+
const leaf = new Uint8Array(rec.leaf);
|
|
1243
|
+
done.set(nid, leaf);
|
|
1244
|
+
cache.set(nid, leaf);
|
|
1189
1245
|
stack.pop();
|
|
1190
1246
|
continue;
|
|
1191
1247
|
}
|
|
1192
1248
|
|
|
1193
|
-
// Branch — push any
|
|
1194
|
-
// left-to-right). If every child is
|
|
1249
|
+
// Branch — push any unresolved children (reverse order so they resolve
|
|
1250
|
+
// left-to-right). If every child is resolved, concatenate now.
|
|
1195
1251
|
const kids = rec.kids ?? [];
|
|
1196
1252
|
let ready = true;
|
|
1197
1253
|
for (let i = kids.length - 1; i >= 0; i--) {
|
|
1198
|
-
if (!
|
|
1254
|
+
if (!done.has(kids[i])) {
|
|
1199
1255
|
stack.push(kids[i]);
|
|
1200
1256
|
ready = false;
|
|
1201
1257
|
}
|
|
@@ -1203,11 +1259,12 @@ export abstract class AbstractStore implements Store {
|
|
|
1203
1259
|
if (!ready) continue;
|
|
1204
1260
|
|
|
1205
1261
|
stack.pop();
|
|
1206
|
-
const out = concat(kids.map((k) =>
|
|
1262
|
+
const out = concat(kids.map((k) => done.get(k)!));
|
|
1263
|
+
done.set(nid, out);
|
|
1207
1264
|
cache.set(nid, out);
|
|
1208
1265
|
}
|
|
1209
1266
|
|
|
1210
|
-
const out =
|
|
1267
|
+
const out = done.get(id) ?? _ZERO;
|
|
1211
1268
|
if (this.meter) this.meter.bytesRead += out.length;
|
|
1212
1269
|
return out;
|
|
1213
1270
|
}
|
|
@@ -1730,16 +1787,35 @@ export abstract class AbstractStore implements Store {
|
|
|
1730
1787
|
* common-prefix / common-suffix trim: whatever remains after both trims is
|
|
1731
1788
|
* the single differing span (substitution, insertion or deletion), and both
|
|
1732
1789
|
* remainders must fit the budget. Scattered differences leave a wide
|
|
1733
|
-
* middle and are rejected.
|
|
1790
|
+
* middle and are rejected.
|
|
1791
|
+
*
|
|
1792
|
+
* Every read here is CAPPED (§2.8). It used to open with
|
|
1793
|
+
* `bytesPrefix(k, Number.MAX_SAFE_INTEGER)` — the ALL sentinel, i.e. the
|
|
1794
|
+
* full materialising `bytes()` read — on the deposit hot path, and only
|
|
1795
|
+
* then compare lengths. So a candidate the length test was about to reject
|
|
1796
|
+
* had already been reconstructed byte for byte. The LENGTHS decide first
|
|
1797
|
+
* instead, from the `contentLen` memo the interning order has already built
|
|
1798
|
+
* bottom-up, and the target's length is itself read under a cap: a target
|
|
1799
|
+
* longer than `la + W` is rejected without touching one of its bytes.
|
|
1800
|
+
* Same semantics — the old capped `b` read would have produced
|
|
1801
|
+
* `a.length + W + 1` here and failed the very same test — strictly fewer
|
|
1802
|
+
* byte reads. The `+ 1` on each byte cap keeps `_prefix`'s
|
|
1803
|
+
* "complete reconstruction" test true, so the results still cache. */
|
|
1734
1804
|
private differsByOneWindow(
|
|
1735
1805
|
kids: NodeId[],
|
|
1736
1806
|
targetId: NodeId,
|
|
1737
1807
|
W: number,
|
|
1738
1808
|
): boolean {
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
);
|
|
1742
|
-
const
|
|
1809
|
+
const lens = kids.map((k) => this.contentLen(k));
|
|
1810
|
+
let la = 0;
|
|
1811
|
+
for (const n of lens) la += n;
|
|
1812
|
+
const cap = la + W + 1;
|
|
1813
|
+
// `contentLen` under a cap returns a clamped LOWER BOUND once the partial
|
|
1814
|
+
// sum reaches it, so `>= cap` is exactly "longer than la + W".
|
|
1815
|
+
const lb = this.contentLen(targetId, cap);
|
|
1816
|
+
if (lb >= cap || Math.abs(la - lb) > W) return false;
|
|
1817
|
+
const a = concat(kids.map((k, i) => this.bytesPrefix(k, lens[i] + 1)));
|
|
1818
|
+
const b = this.bytesPrefix(targetId, lb + 1);
|
|
1743
1819
|
if (Math.abs(a.length - b.length) > W) return false;
|
|
1744
1820
|
const n = Math.min(a.length, b.length);
|
|
1745
1821
|
let i = 0;
|
|
@@ -77,15 +77,35 @@ const mix = (x) => {
|
|
|
77
77
|
return (x ^ (x >>> 15)) >>> 0;
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
/** Four-word windows of the repo's own
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
/** Four-word windows of the repo's own prose, taken from the CODE's comments
|
|
81
|
+
* under src (TypeScript) and test (the suites) — never from documentation. A
|
|
82
|
+
* test that reads docs is coupled to every documentation edit: deleting one
|
|
83
|
+
* root file once took its corpus below the non-vacuity guard and broke every
|
|
84
|
+
* release. Code comments are prose too, and the code is always here.
|
|
85
|
+
*
|
|
86
|
+
* Comment markers, code fences, inline code and link targets are stripped so
|
|
87
|
+
* what is left is language, which is where the fragment overlap lives. */
|
|
83
88
|
function fragments() {
|
|
84
89
|
const out = [];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
const files = [];
|
|
91
|
+
const walk = (dir, exts) => {
|
|
92
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
if (
|
|
94
|
+
e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist"
|
|
95
|
+
) continue;
|
|
96
|
+
const p = join(dir, e.name);
|
|
97
|
+
if (e.isDirectory()) walk(p, exts);
|
|
98
|
+
else if (exts.some((x) => e.name.endsWith(x))) files.push(p);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
walk(join(REPO, "src"), [".ts"]);
|
|
102
|
+
walk(join(REPO, "test"), [".mjs"]);
|
|
103
|
+
for (const f of files.sort()) {
|
|
104
|
+
const text = readFileSync(f, "utf8");
|
|
105
|
+
let t = "";
|
|
106
|
+
for (const m of text.matchAll(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g)) {
|
|
107
|
+
t += " " + m[0];
|
|
108
|
+
}
|
|
89
109
|
t = t.toLowerCase().replace(/[^a-z ]+/g, " ").replace(/\s+/g, " ");
|
|
90
110
|
const w = t.split(" ").filter(Boolean);
|
|
91
111
|
for (let i = 0; i + 4 < w.length; i += 2) {
|
|
@@ -140,9 +160,9 @@ const SIZES = [750, 1000, 1500];
|
|
|
140
160
|
test("completion recursion: per-query work does not grow with the corpus", async () => {
|
|
141
161
|
assert.ok(
|
|
142
162
|
FRAG.length > 4000,
|
|
143
|
-
`only ${FRAG.length} prose fragments found in
|
|
144
|
-
`draws its corpus from
|
|
145
|
-
`it can no longer exercise the completion recursion at all`,
|
|
163
|
+
`only ${FRAG.length} prose fragments found in the source comments — this ` +
|
|
164
|
+
`test draws its corpus from src/**/*.ts and test/**/*.mjs; with the ` +
|
|
165
|
+
`comments gone it can no longer exercise the completion recursion at all`,
|
|
146
166
|
);
|
|
147
167
|
|
|
148
168
|
const searches = [], pops = [], answers = [], secs = [];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// 96-bytes-walk-termination.test.mjs — `bytes()` must TERMINATE when its
|
|
2
|
+
// traversal memo cannot hold the reconstruction's working set.
|
|
3
|
+
//
|
|
4
|
+
// `bytes()` is an iterative post-order walk: it peeks the top of an explicit
|
|
5
|
+
// stack, pushes any child that is not yet resolved, and concatenates once every
|
|
6
|
+
// child is. Its termination argument needs a memo that only ever GROWS. It
|
|
7
|
+
// used `_bytesCache` — a byte-accounted BoundedMap that EVICTS, and whose
|
|
8
|
+
// `"smallest"` policy deliberately prefers the cheapest-to-rebuild entries,
|
|
9
|
+
// i.e. exactly the small, freshly-resolved children the parents still sitting
|
|
10
|
+
// on the stack are waiting for. The parent finds them unresolved again,
|
|
11
|
+
// re-pushes them, they are re-resolved, re-inserted and re-evicted. No
|
|
12
|
+
// progress. The loop never exits.
|
|
13
|
+
//
|
|
14
|
+
// It stayed hidden because it needs the cache to be SATURATED, which only a
|
|
15
|
+
// long run reaches. OBSERVED IN THE FIELD: a training run at 19.9M nodes with
|
|
16
|
+
// the 20 MB cache pinned at 19,999,962 bytes spun for 8h45m of 100% CPU on one
|
|
17
|
+
// node — whose entire content was 124 bytes, with 5 kids, 2 of them
|
|
18
|
+
// perpetually re-evicted. Sampled 45s apart through the V8 inspector, the
|
|
19
|
+
// walk's root id, the node's length and the cache's byte count were all
|
|
20
|
+
// identical; only the stack depth oscillated between 1 and 2.
|
|
21
|
+
//
|
|
22
|
+
// And the loop is SYNCHRONOUS, so nothing could observe it: the trainer's
|
|
23
|
+
// 15-minute stall watchdog is a timer, and a timer cannot fire while the
|
|
24
|
+
// microtask/JS stack is occupied. The run looked alive for 9 hours.
|
|
25
|
+
//
|
|
26
|
+
// The fix makes the walk memoize into a LOCAL map (`_bytesCache` demoted to a
|
|
27
|
+
// warm hint whose hit is promoted into that map immediately), so each node
|
|
28
|
+
// resolves at most once per call and termination is structural.
|
|
29
|
+
//
|
|
30
|
+
// This test does NOT depend on the eviction cursor's position — the field case
|
|
31
|
+
// reached the defect stochastically, via the cursor sweeping the map's
|
|
32
|
+
// recently-inserted tail. Here the subtree's children simply sum to more bytes
|
|
33
|
+
// than the ceiling, so no cursor position can save it: pre-fix this file hangs
|
|
34
|
+
// forever, and `node --test` reports it only as a timeout.
|
|
35
|
+
import { test } from "node:test";
|
|
36
|
+
import assert from "node:assert/strict";
|
|
37
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
38
|
+
|
|
39
|
+
const D = 64;
|
|
40
|
+
const gist = () => {
|
|
41
|
+
const v = new Float32Array(D);
|
|
42
|
+
v[0] = 1;
|
|
43
|
+
return v;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
test("bytes() terminates when its memo cannot hold the working set", async () => {
|
|
47
|
+
// A ceiling small enough to saturate in milliseconds. The mechanism is
|
|
48
|
+
// scale-free: this is the only thing scaled down from the field case.
|
|
49
|
+
const CEILING = 4096;
|
|
50
|
+
const store = new SQliteStore({
|
|
51
|
+
D,
|
|
52
|
+
bytesCacheMax: CEILING,
|
|
53
|
+
path: ":memory:",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// One ordinary 9,600-byte form: 300 distinct 32-byte children under a root.
|
|
57
|
+
const KIDS = 300, WIDTH = 32;
|
|
58
|
+
const kids = [];
|
|
59
|
+
for (let b = 0; b < KIDS; b++) {
|
|
60
|
+
const buf = new Uint8Array(WIDTH);
|
|
61
|
+
for (let i = 0; i < WIDTH; i++) buf[i] = (b * 131 + i * 17) & 0xff;
|
|
62
|
+
kids.push(await store.putLeaf(buf, gist()));
|
|
63
|
+
}
|
|
64
|
+
const root = await store.putBranch(kids, gist());
|
|
65
|
+
const expected = store.contentLen(root);
|
|
66
|
+
assert.equal(expected, KIDS * WIDTH);
|
|
67
|
+
|
|
68
|
+
// Churn the subtree out of the memo and leave it AT its ceiling — the steady
|
|
69
|
+
// state every long run reaches.
|
|
70
|
+
for (let k = 0; k < 4000; k++) {
|
|
71
|
+
const buf = new Uint8Array(48);
|
|
72
|
+
for (let i = 0; i < 48; i++) buf[i] = (k * 7919 + i * 251) & 0xff;
|
|
73
|
+
await store.putLeaf(buf, gist());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The premise the guard rests on: the memo genuinely cannot hold the working
|
|
77
|
+
// set. If a future change grows the ceiling or shrinks the fixture, this
|
|
78
|
+
// assertion fails LOUDLY rather than letting the test pass vacuously.
|
|
79
|
+
assert.ok(
|
|
80
|
+
KIDS * WIDTH > CEILING,
|
|
81
|
+
`fixture must exceed the memo ceiling (${KIDS * WIDTH} vs ${CEILING})`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Pre-fix this call never returns. Post-fix it is sub-millisecond.
|
|
85
|
+
const out = store.bytes(root);
|
|
86
|
+
assert.equal(out.length, expected);
|
|
87
|
+
for (let b = 0; b < KIDS; b++) {
|
|
88
|
+
for (let i = 0; i < WIDTH; i++) {
|
|
89
|
+
assert.equal(out[b * WIDTH + i], (b * 131 + i * 17) & 0xff);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("differsByOneWindow's reads are capped — no ALL-sentinel read on deposit", async () => {
|
|
95
|
+
// §2.8: the near-dedup byte check used to open with
|
|
96
|
+
// `bytesPrefix(k, Number.MAX_SAFE_INTEGER)` — the ALL sentinel, which routes
|
|
97
|
+
// to the full materialising `bytes()` — and only THEN compare lengths. A
|
|
98
|
+
// candidate the length test was about to reject had already been rebuilt byte
|
|
99
|
+
// for byte, and that read is what dragged the deposit path into the walk
|
|
100
|
+
// above. Lengths now decide first, from the `contentLen` memo.
|
|
101
|
+
const src = await import("node:fs").then((fs) =>
|
|
102
|
+
fs.readFileSync(new URL("../src/store.ts", import.meta.url), "utf8")
|
|
103
|
+
);
|
|
104
|
+
const body = src.slice(src.indexOf("private differsByOneWindow"));
|
|
105
|
+
const end = body.indexOf("\n }\n");
|
|
106
|
+
const fn = body.slice(0, end);
|
|
107
|
+
assert.ok(
|
|
108
|
+
!fn.includes("MAX_SAFE_INTEGER"),
|
|
109
|
+
"differsByOneWindow must not read with the ALL sentinel",
|
|
110
|
+
);
|
|
111
|
+
assert.ok(
|
|
112
|
+
fn.includes("this.contentLen("),
|
|
113
|
+
"differsByOneWindow must decide on lengths before reading bytes",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// 97-store-seed.test.mjs — a trained store's OWN seed governs the Mind that
|
|
2
|
+
// opens it.
|
|
3
|
+
//
|
|
4
|
+
// `train.seed` is persisted by the trainer (example/train_base/main.ts) and the
|
|
5
|
+
// trainer refuses to resume a store under a different seed, so the value is
|
|
6
|
+
// authoritative for the artifact. The seed feeds `makeKeyring`, `Space.rand`
|
|
7
|
+
// and the `Alphabet` in the Mind constructor: folding a query under any other
|
|
8
|
+
// seed lands in a DIFFERENT vector space than the one the artifact's nodes were
|
|
9
|
+
// folded into, so recognition and resonance read the wrong space and every
|
|
10
|
+
// answer degrades silently.
|
|
11
|
+
//
|
|
12
|
+
// The store recovers `train.D` and `geometry.maxGroup` from its own metadata at
|
|
13
|
+
// open; `train.seed` must be recovered the same way, and a Mind that did not
|
|
14
|
+
// receive an explicit seed must adopt it. An explicit caller seed still wins.
|
|
15
|
+
|
|
16
|
+
import { test } from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { DEFAULT_CONFIG, Mind } from "../dist/src/index.js";
|
|
22
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
23
|
+
|
|
24
|
+
/** A store that was never trained carries no seed and leaves the caller's
|
|
25
|
+
* configured default in force. */
|
|
26
|
+
test("an untrained store reports no trainSeed and keeps the default seed", async () => {
|
|
27
|
+
const store = new SQliteStore({ path: ":memory:", D: 256 });
|
|
28
|
+
const mind = new Mind({ store });
|
|
29
|
+
assert.equal(store.trainSeed, null);
|
|
30
|
+
assert.equal(mind.cfg.seed, DEFAULT_CONFIG.seed);
|
|
31
|
+
await store.close();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** The artifact's seed is recovered at open and adopted by a Mind that was not
|
|
35
|
+
* given one; an explicit seed still overrides it. */
|
|
36
|
+
test("a trained store's seed is recovered and adopted unless overridden", async () => {
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), "sema-seed-"));
|
|
38
|
+
const stem = join(dir, "trained");
|
|
39
|
+
const TRAIN_SEED = 7;
|
|
40
|
+
|
|
41
|
+
// Build the artifact: ingest under an explicit seed, then persist the seed
|
|
42
|
+
// exactly as the trainer does.
|
|
43
|
+
{
|
|
44
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
45
|
+
const mind = new Mind({ seed: TRAIN_SEED, store });
|
|
46
|
+
await mind.ingest([["the sky is blue", "blue"]]);
|
|
47
|
+
await store.setMeta("train.seed", String(TRAIN_SEED));
|
|
48
|
+
store.commit();
|
|
49
|
+
await store.close();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Reopen WITHOUT a seed: the store's own seed must stand.
|
|
53
|
+
{
|
|
54
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
55
|
+
assert.equal(store.trainSeed, TRAIN_SEED);
|
|
56
|
+
const adopted = new Mind({ store });
|
|
57
|
+
assert.equal(adopted.cfg.seed, TRAIN_SEED);
|
|
58
|
+
await store.close();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reopen WITH an explicit seed: the caller wins over the artifact.
|
|
62
|
+
{
|
|
63
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
64
|
+
const explicit = new Mind({ seed: 3, store });
|
|
65
|
+
assert.equal(explicit.cfg.seed, 3);
|
|
66
|
+
await store.close();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rmSync(dir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** The adopted seed is the one the answer is computed under: a store ingested
|
|
73
|
+
* under seed 7 answers a query identically when reopened without a seed and
|
|
74
|
+
* when reopened with seed 7 passed explicitly. */
|
|
75
|
+
test("adopting the artifact seed reproduces the artifact's answer", async () => {
|
|
76
|
+
const dir = mkdtempSync(join(tmpdir(), "sema-seed-"));
|
|
77
|
+
const stem = join(dir, "trained");
|
|
78
|
+
const TRAIN_SEED = 7;
|
|
79
|
+
const QUESTION = "the sky is blue";
|
|
80
|
+
|
|
81
|
+
let artifactAnswer;
|
|
82
|
+
{
|
|
83
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
84
|
+
const mind = new Mind({ seed: TRAIN_SEED, store });
|
|
85
|
+
await mind.ingest([
|
|
86
|
+
["the sky is blue", "blue"],
|
|
87
|
+
["the grass is green", "green"],
|
|
88
|
+
]);
|
|
89
|
+
artifactAnswer = (await mind.respondText(QUESTION)).trim();
|
|
90
|
+
await store.setMeta("train.seed", String(TRAIN_SEED));
|
|
91
|
+
store.commit();
|
|
92
|
+
await store.close();
|
|
93
|
+
}
|
|
94
|
+
assert.equal(artifactAnswer, "blue");
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
98
|
+
const adopted = new Mind({ store });
|
|
99
|
+
assert.equal(adopted.cfg.seed, TRAIN_SEED);
|
|
100
|
+
assert.equal((await adopted.respondText(QUESTION)).trim(), artifactAnswer);
|
|
101
|
+
await store.close();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
rmSync(dir, { recursive: true, force: true });
|
|
105
|
+
});
|