@hviana/sema 0.9.0 → 0.9.2

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 (118) hide show
  1. package/AGENTS.md +7 -7
  2. package/dist/src/alu/src/index.d.ts +1 -1
  3. package/dist/src/alu/src/index.js +1 -1
  4. package/dist/src/alu/src/parser.js +2 -6
  5. package/dist/src/alu/src/resonance.d.ts +13 -0
  6. package/dist/src/alu/src/resonance.js +41 -0
  7. package/dist/src/alu/test/alu.test.js +39 -0
  8. package/dist/src/bytes.d.ts +6 -2
  9. package/dist/src/bytes.js +10 -4
  10. package/dist/src/canon.js +44 -0
  11. package/dist/src/geometry.d.ts +19 -1
  12. package/dist/src/geometry.js +125 -141
  13. package/dist/src/meter.d.ts +33 -0
  14. package/dist/src/meter.js +34 -1
  15. package/dist/src/mind/articulation.js +14 -1
  16. package/dist/src/mind/attention.d.ts +12 -0
  17. package/dist/src/mind/attention.js +44 -16
  18. package/dist/src/mind/bridge.js +3 -3
  19. package/dist/src/mind/derivation.d.ts +40 -0
  20. package/dist/src/mind/derivation.js +34 -0
  21. package/dist/src/mind/evidence.d.ts +24 -0
  22. package/dist/src/mind/evidence.js +90 -0
  23. package/dist/src/mind/graph-search.d.ts +89 -15
  24. package/dist/src/mind/graph-search.js +345 -174
  25. package/dist/src/mind/learning.js +1 -1
  26. package/dist/src/mind/mechanisms/cover.d.ts +19 -3
  27. package/dist/src/mind/mechanisms/cover.js +142 -61
  28. package/dist/src/mind/mechanisms/recall.js +10 -3
  29. package/dist/src/mind/mind.d.ts +6 -0
  30. package/dist/src/mind/mind.js +5 -2
  31. package/dist/src/mind/pipeline.d.ts +5 -1
  32. package/dist/src/mind/pipeline.js +220 -90
  33. package/dist/src/mind/primitives.d.ts +25 -5
  34. package/dist/src/mind/primitives.js +107 -44
  35. package/dist/src/mind/reasoning.d.ts +18 -4
  36. package/dist/src/mind/reasoning.js +487 -328
  37. package/dist/src/mind/recognition.js +29 -13
  38. package/dist/src/mind/resonance.js +1 -11
  39. package/dist/src/mind/traverse.d.ts +45 -5
  40. package/dist/src/mind/traverse.js +285 -8
  41. package/dist/src/mind/types.d.ts +16 -1
  42. package/dist/src/store-sqlite.d.ts +25 -0
  43. package/dist/src/store-sqlite.js +89 -1
  44. package/dist/src/store.d.ts +48 -4
  45. package/dist/src/store.js +86 -6
  46. package/docs/INDEX.md +20 -19
  47. package/docs/INVARIANTS.md +17 -16
  48. package/docs/architecture/bounded-reads.md +1 -1
  49. package/docs/architecture/caches.md +5 -4
  50. package/docs/architecture/closure.md +45 -5
  51. package/docs/architecture/cost-model.md +16 -0
  52. package/docs/architecture/evidence.md +113 -0
  53. package/docs/architecture/exact-vs-approximate.md +10 -9
  54. package/docs/architecture/factored-machinery.md +14 -13
  55. package/docs/architecture/fold-contract.md +51 -1
  56. package/docs/architecture/mechanism-market.md +21 -0
  57. package/docs/architecture/memoization.md +3 -3
  58. package/docs/architecture/meter.md +2 -1
  59. package/docs/architecture/saturation.md +12 -0
  60. package/docs/architecture/store.md +25 -2
  61. package/docs/failures/tempting-but-wrong.md +13 -2
  62. package/docs/harness/gates.md +12 -10
  63. package/docs/mechanisms/cover.md +23 -6
  64. package/jsr.json +1 -1
  65. package/package.json +1 -1
  66. package/src/alu/README.md +10 -2
  67. package/src/alu/src/index.ts +1 -0
  68. package/src/alu/src/parser.ts +6 -6
  69. package/src/alu/src/resonance.ts +42 -0
  70. package/src/alu/test/alu.test.ts +40 -0
  71. package/src/bytes.ts +13 -3
  72. package/src/canon.ts +40 -0
  73. package/src/geometry.ts +183 -154
  74. package/src/meter.ts +34 -1
  75. package/src/mind/articulation.ts +14 -2
  76. package/src/mind/attention.ts +47 -25
  77. package/src/mind/bridge.ts +3 -3
  78. package/src/mind/derivation.ts +77 -0
  79. package/src/mind/evidence.ts +107 -0
  80. package/src/mind/graph-search.ts +449 -221
  81. package/src/mind/learning.ts +1 -7
  82. package/src/mind/match.ts +1 -2
  83. package/src/mind/mechanisms/cast.ts +1 -2
  84. package/src/mind/mechanisms/cover.ts +207 -87
  85. package/src/mind/mechanisms/extraction.ts +1 -2
  86. package/src/mind/mechanisms/prefix-completion.ts +1 -1
  87. package/src/mind/mechanisms/recall.ts +17 -5
  88. package/src/mind/mechanisms/reference.ts +1 -1
  89. package/src/mind/mind.ts +9 -30
  90. package/src/mind/pipeline.ts +263 -104
  91. package/src/mind/primitives.ts +119 -43
  92. package/src/mind/reasoning.ts +611 -419
  93. package/src/mind/recognition.ts +24 -9
  94. package/src/mind/resonance.ts +2 -16
  95. package/src/mind/trace.ts +1 -1
  96. package/src/mind/traverse.ts +321 -8
  97. package/src/mind/types.ts +15 -11
  98. package/src/store-sqlite.ts +92 -1
  99. package/src/store.ts +113 -7
  100. package/test/105-derive-through-reports-its-refusal.test.mjs +8 -5
  101. package/test/106-the-join-fires.test.mjs +21 -0
  102. package/test/111-the-cover-assembly-is-counted.test.mjs +8 -5
  103. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +18 -12
  104. package/test/136-the-two-named-limits.test.mjs +3 -2
  105. package/test/137-the-law-lives-once-and-below.test.mjs +21 -0
  106. package/test/148-exact-shortcuts-agree.test.mjs +188 -0
  107. package/test/149-the-closure-engine.test.mjs +138 -0
  108. package/test/150-the-join-is-output-sensitive.test.mjs +66 -0
  109. package/test/151-the-cover-pays-for-what-it-reaches.test.mjs +142 -0
  110. package/test/152-the-read-side-names-as-the-write-side.test.mjs +146 -0
  111. package/test/153-a-cheaper-bound-is-looked-at-first.test.mjs +155 -0
  112. package/test/154-the-question-names-the-step.test.mjs +281 -0
  113. package/test/24-generalization.test.mjs +32 -0
  114. package/test/36-bloom.test.mjs +53 -0
  115. package/test/37-cluster-dispersion-fusion.test.mjs +75 -0
  116. package/test/48-recognise-turn-connective.test.mjs +3 -2
  117. package/test/55-cost-meter.test.mjs +4 -4
  118. package/test/90-connector-read-cap.test.mjs +7 -7
@@ -0,0 +1,188 @@
1
+ // 148-exact-shortcuts-agree.test.mjs — two shortcuts that are EXACT, pinned
2
+ // against the definitions they shortcut.
3
+ //
4
+ // 148.1 THE SEGMENT PROBE DECIDES A MISS EXACTLY. `exactNode` (primitives.ts)
5
+ // answers "which stored node are these bytes?" without building the fold
6
+ // when a level-0 content segment is not a stored flat branch: a fold
7
+ // names a branch only when every child is named, and a segment is one
8
+ // flat node over single-byte atoms. So the answer must equal
9
+ // `foldTree(perceive(bytes))` on EVERY span — hits, misses, edges — and
10
+ // this walks every sub-span of real deposits and of noise to show it.
11
+ //
12
+ // 148.3 THE IDENTITY FOLD HAS THE VECTOR FOLD'S SHAPE. `exactNode` names a
13
+ // stream through `contentIdentity` (geometry.ts), which groups the same
14
+ // way the vector fold does but reads an item's gist only where the shape
15
+ // does — the eight coordinates `itemKey` hashes inside an over-long row —
16
+ // and computes those lazily. Pinned with an interning namer, so every
17
+ // node is named and the two folds must agree on EVERY node of the tree,
18
+ // over streams long and repetitive enough to force over-long rows (low
19
+ // entropy keeps the cut levels flat, so the split falls to `itemKey`).
20
+ //
21
+ // 148.2 THE ASCII CANON IS THE UNICODE CANON. `textCanon` takes a byte loop
22
+ // for ASCII input instead of decode → NFKC → lowercase → regex → encode.
23
+ // NFKC is the identity on ASCII and the regex's ASCII whitespace is
24
+ // TAB..CR and SPACE, so the two must agree byte for byte; the reference
25
+ // here is the general reading, spelled out, over random ASCII drawn from
26
+ // an alphabet that is mostly whitespace and case.
27
+
28
+ import { test } from "node:test";
29
+ import assert from "node:assert/strict";
30
+ import { Mind } from "../dist/src/index.js";
31
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
32
+ import { exactNode, foldTree, perceive } from "../dist/src/mind/primitives.js";
33
+ import { textCanon } from "../dist/src/canon.js";
34
+ import { bytesToTree, contentIdentity } from "../dist/src/geometry.js";
35
+
36
+ const enc = new TextEncoder();
37
+ const dec = new TextDecoder("utf-8", { fatal: false });
38
+
39
+ /** Deterministic generator — the suite forbids Math.random in fixtures. */
40
+ function lcg(seed) {
41
+ let x = seed >>> 0;
42
+ return () => {
43
+ x = (Math.imul(x, 1664525) + 1013904223) >>> 0;
44
+ return x / 0x100000000;
45
+ };
46
+ }
47
+
48
+ test("148.1 exactNode agrees with the full fold on every sub-span", async () => {
49
+ const mind = new Mind({
50
+ seed: 3,
51
+ store: new SQliteStore({ path: ":memory:" }),
52
+ });
53
+ const facts = [
54
+ ["The director of Eva is Gustaf Molander.", "Gustaf Molander"],
55
+ ["gustaf molander country", "The country of Gustaf Molander is Sweden."],
56
+ ["What is the capital of France", "The capital of France is Paris"],
57
+ ["ice", "cold"],
58
+ ];
59
+ await mind.ingest(facts);
60
+
61
+ const rand = lcg(148);
62
+ const noise = Array.from(
63
+ { length: 6 },
64
+ () =>
65
+ Array.from(
66
+ { length: 24 },
67
+ () => "abcdefghij ETAOIN.,"[Math.floor(rand() * 19)],
68
+ ).join(""),
69
+ );
70
+ const streams = [...facts.flat(), ...noise].map((t) => enc.encode(t));
71
+
72
+ let hits = 0, misses = 0;
73
+ for (const bytes of streams) {
74
+ for (let a = 0; a < bytes.length; a++) {
75
+ for (let b = a + 1; b <= bytes.length; b++) {
76
+ const span = bytes.subarray(a, b);
77
+ const full = foldTree(mind, perceive(mind, span), 0).node;
78
+ assert.equal(
79
+ exactNode(mind, span),
80
+ full,
81
+ `span "${
82
+ dec.decode(span)
83
+ }": the segment probe disagrees with the fold`,
84
+ );
85
+ if (full === null) misses++;
86
+ else hits++;
87
+ }
88
+ }
89
+ }
90
+ // Non-vacuity: both outcomes must be exercised, or agreement proves nothing.
91
+ assert.ok(hits > 50, `too few hits to pin the agreement (${hits})`);
92
+ assert.ok(misses > 1000, `too few misses to pin the agreement (${misses})`);
93
+ await mind.store.close();
94
+ });
95
+
96
+ test("148.2 the ASCII canon equals the Unicode canon, byte for byte", () => {
97
+ const reference = (bytes) =>
98
+ enc.encode(
99
+ dec.decode(bytes).normalize("NFKC").toLowerCase().replace(
100
+ /(\S)\s+(?=\S)/g,
101
+ "$1 ",
102
+ ),
103
+ );
104
+ // Mostly the characters the rule is about: every ASCII whitespace, both
105
+ // cases, punctuation, plus the control bytes JS's \s does NOT include.
106
+ const alphabet = [
107
+ 0x20,
108
+ 0x09,
109
+ 0x0a,
110
+ 0x0b,
111
+ 0x0c,
112
+ 0x0d,
113
+ 0x1c,
114
+ 0x1f,
115
+ 0x00,
116
+ 0x7f,
117
+ ..."aZ.,'Q9 ".split("").map((c) => c.charCodeAt(0)),
118
+ ];
119
+ const rand = lcg(2);
120
+ const cases = [
121
+ "",
122
+ " ",
123
+ " lead",
124
+ "trail ",
125
+ "a b\t\tc\n\nD",
126
+ "\t\v\f\r",
127
+ "ALL CAPS",
128
+ ].map((t) => enc.encode(t));
129
+ for (let i = 0; i < 4000; i++) {
130
+ const n = Math.floor(rand() * 14);
131
+ cases.push(
132
+ Uint8Array.from(
133
+ { length: n },
134
+ () => alphabet[Math.floor(rand() * alphabet.length)],
135
+ ),
136
+ );
137
+ }
138
+ for (const c of cases) {
139
+ assert.deepEqual(
140
+ [...textCanon(c)],
141
+ [...reference(c)],
142
+ `canon of ${JSON.stringify(dec.decode(c))} diverged`,
143
+ );
144
+ }
145
+ });
146
+
147
+ test("148.3 the identity fold groups exactly as the vector fold", () => {
148
+ const mind = new Mind({
149
+ seed: 5,
150
+ store: new SQliteStore({ path: ":memory:" }),
151
+ });
152
+ const ids = new Map();
153
+ const intern = (key) => {
154
+ let id = ids.get(key);
155
+ if (id === undefined) ids.set(key, id = ids.size);
156
+ return id;
157
+ };
158
+ const atoms = (bytes) => Array.from(bytes, (b) => -(b + 1)).join(",");
159
+ /** foldTree's naming, over the vector fold's tree. */
160
+ const name = (n) =>
161
+ n.kids === null ? -(n.leaf[0] + 1) : intern(n.kids.map(name).join(","));
162
+ const rand = lcg(1483);
163
+ const alphabets = ["ab", "aab", "abc ", "0123456789", "the quick brown fox "];
164
+ let streams = 0;
165
+ for (const alpha of alphabets) {
166
+ for (let t = 0; t < 40; t++) {
167
+ const n = 2 + Math.floor(rand() * 1500);
168
+ const bytes = Uint8Array.from(
169
+ { length: n },
170
+ () => alpha.charCodeAt(Math.floor(rand() * alpha.length)),
171
+ );
172
+ const viaVectors = name(bytesToTree(mind.space, mind.alphabet, bytes));
173
+ const viaIdentity = contentIdentity(
174
+ mind.space,
175
+ mind.alphabet,
176
+ bytes,
177
+ (from, to) =>
178
+ to - from === 1
179
+ ? -(bytes[from] + 1)
180
+ : intern(atoms(bytes.subarray(from, to))),
181
+ (kids) => intern(kids.join(",")),
182
+ );
183
+ assert.equal(viaIdentity, viaVectors, `${alpha} stream of ${n} bytes`);
184
+ streams++;
185
+ }
186
+ }
187
+ assert.equal(streams, 200);
188
+ });
@@ -0,0 +1,138 @@
1
+ // 149-the-closure-engine.test.mjs — one closure, over layers.
2
+ //
3
+ // `closeOver` (derivation.ts) closes a derivation under the law, layer by layer:
4
+ // each layer OFFERS, the law admits, and the state a layer reaches is the state
5
+ // the next one is offered against. The pipeline's post-grounding stage is that
6
+ // engine with two layers (the walk, the fusion), so the gates it used to spell
7
+ // by hand are now the law's or the layer's. Pinned:
8
+ //
9
+ // 149.1 ¬FIXED is the engine's, applied before any layer pays: a fixed state
10
+ // enters no layer — no offer is asked, nothing is wrapped.
11
+ // 149.2 layers are PHASES, in order, never revisited: the second layer is
12
+ // offered the first one's final state, and the first is not asked again.
13
+ // 149.3 a layer that does not ENGAGE is not entered at all.
14
+ // 149.4 the law's refusal ends THAT layer's walk, not the closure.
15
+ // 149.5 the pipeline asks the engine, and sequences no layer by hand.
16
+
17
+ import { test } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { readFile } from "node:fs/promises";
20
+ import { dirname, join } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { closeOver } from "../dist/src/mind/derivation.js";
23
+
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const enc = new TextEncoder();
26
+ const QUERY = enc.encode("ABCDEFGHIJKL");
27
+ const W = 4;
28
+ const state = (extra = {}) => ({
29
+ product: new Uint8Array(0),
30
+ accounted: [],
31
+ remainder: [[0, 12]],
32
+ cost: 0,
33
+ ...extra,
34
+ });
35
+ /** A layer that offers the given products, one per ask, then nothing. */
36
+ const layer = (name, products, log, extra = {}) => {
37
+ let i = 0;
38
+ return {
39
+ name,
40
+ offer: async (d) => {
41
+ log.push(`${name}:ask:${new TextDecoder().decode(d.product)}`);
42
+ if (i >= products.length) return null;
43
+ return {
44
+ product: enc.encode(products[i++]),
45
+ contains: true,
46
+ reaches: true,
47
+ cost: 1,
48
+ };
49
+ },
50
+ ...extra,
51
+ };
52
+ };
53
+
54
+ test("149.1 a fixed state enters no layer", async () => {
55
+ const log = [];
56
+ const entered = [];
57
+ const out = await closeOver(
58
+ state({ fixed: true }),
59
+ QUERY,
60
+ W,
61
+ [layer("a", ["x"], log)],
62
+ async (name, walk) => {
63
+ entered.push(name);
64
+ return walk();
65
+ },
66
+ );
67
+ assert.deepEqual(log, [], "no offer may be asked of a fixed state");
68
+ assert.deepEqual(entered, [], "no layer may be entered for a fixed state");
69
+ assert.equal(out.fixed, true);
70
+ });
71
+
72
+ test("149.2 layers are phases, in order, never revisited", async () => {
73
+ const log = [];
74
+ const out = await closeOver(state(), QUERY, W, [
75
+ layer("a", ["a1", "a2"], log),
76
+ layer("b", ["b1"], log),
77
+ ]);
78
+ assert.deepEqual(log, [
79
+ "a:ask:",
80
+ "a:ask:a1",
81
+ "a:ask:a2",
82
+ "b:ask:a2",
83
+ "b:ask:b1",
84
+ ]);
85
+ assert.equal(new TextDecoder().decode(out.product), "b1");
86
+ assert.equal(out.cost, 3, "every admitted step is priced");
87
+ });
88
+
89
+ test("149.3 a layer that does not engage is not entered", async () => {
90
+ const log = [];
91
+ const entered = [];
92
+ await closeOver(
93
+ state(),
94
+ QUERY,
95
+ W,
96
+ [layer("a", ["a1"], log, { engages: () => false }), layer("b", [], log)],
97
+ async (name, walk) => {
98
+ entered.push(name);
99
+ return walk();
100
+ },
101
+ );
102
+ assert.deepEqual(entered, ["b"]);
103
+ assert.deepEqual(log, ["b:ask:"]);
104
+ });
105
+
106
+ test("149.4 the law's refusal ends that layer, not the closure", async () => {
107
+ const log = [];
108
+ const refused = [];
109
+ const ends = [];
110
+ // `a` offers a step that neither carries nor reaches: the law refuses it.
111
+ const a = {
112
+ name: "a",
113
+ offer: async () => ({ product: enc.encode("zz"), contains: true, cost: 1 }),
114
+ onRefused: () => refused.push("a"),
115
+ onEnd: (from, to) => ends.push(["a", from === to]),
116
+ };
117
+ const out = await closeOver(state(), QUERY, W, [a, layer("b", ["b1"], log)]);
118
+ assert.deepEqual(refused, ["a"]);
119
+ assert.deepEqual(ends, [["a", true]], "a refused layer ends where it began");
120
+ assert.equal(new TextDecoder().decode(out.product), "b1");
121
+ });
122
+
123
+ test("149.5 the pipeline asks the engine and sequences no layer by hand", async () => {
124
+ const src = await readFile(
125
+ join(here, "..", "src", "mind", "pipeline.ts"),
126
+ "utf8",
127
+ );
128
+ const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
129
+ assert.match(code, /closeOver\(/, "the post-grounding stage is the engine");
130
+ assert.ok(
131
+ !/\breason\(|\bfuseAttention\(/.test(code),
132
+ "the pipeline calls a layer's walk directly again — offer it to the engine",
133
+ );
134
+ assert.ok(
135
+ !/decided\.complete\s*\?/.test(code),
136
+ "a complete grounding is the law's FIXED clause, applied by the engine",
137
+ );
138
+ });
@@ -0,0 +1,66 @@
1
+ // 150-the-join-is-output-sensitive.test.mjs — the join is priced for the facts
2
+ // a derivation STANDS ON, never for every fact the exploration REACHES.
3
+ //
4
+ // THE TRAP. `deriveThrough` used to fire for every finalized out with a node.
5
+ // A chain hop forks across every continuation (trap 13: the offer is the
6
+ // corpus's own structure), so a form whose continuation is a hub reaches every
7
+ // one of that hub's fixpoints — and each paid an entity scan, and each join it
8
+ // fired was a new fact to scan. Measured on the 31.7M-node store: a 46-byte
9
+ // dialogue query reached 2,000+ fixpoints in a minute and passed 10 GB. It is
10
+ // the trap `recompleteNode` records, and the cure is `deepen`'s: the solve runs
11
+ // to its lightest derivation, licenses the facts THAT derivation stood on, and
12
+ // re-runs with their joins as ordinary rules, until no licensed fact joins.
13
+ //
14
+ // Pinned:
15
+ // 150.1 the facts priced do not grow with the hub's degree — the same query
16
+ // over a hub of 8 continuations and of 32 prices the same facts;
17
+ // 150.2 the answer is unchanged by the degree (the derivation is the same).
18
+
19
+ import { test } from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { Mind } from "../dist/src/index.js";
22
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
23
+
24
+ /** "hello" → "hi there" (one first hop), then "hi there" forks across `n`
25
+ * distinct replies — each a fixpoint fact the search reaches. The filler
26
+ * raises the corpus scale N so the hop's READ cap (√N) admits all `n`: on a
27
+ * tiny store √N is 1–2 and no hub can be read at all. Measured before the
28
+ * license: 35 facts scanned at n = 8, 59 at n = 32. */
29
+ async function hub(n) {
30
+ const mind = new Mind({
31
+ seed: 7,
32
+ profile: true,
33
+ store: new SQliteStore({ path: ":memory:" }),
34
+ });
35
+ const pairs = [["hello", "hi there"]];
36
+ for (let k = 0; k < 1200; k++) {
37
+ pairs.push([`filler question ${k}`, `filler answer ${k}`]);
38
+ }
39
+ for (let k = 0; k < n; k++) {
40
+ pairs.push(["hi there", `reply number ${k} talks about topic ${k}.`]);
41
+ }
42
+ await mind.ingest(pairs);
43
+ return mind;
44
+ }
45
+
46
+ test("150.1 the join prices the derivation's facts, not the hub's", async () => {
47
+ const priced = [];
48
+ const answers = [];
49
+ for (const n of [8, 32]) {
50
+ const mind = await hub(n);
51
+ // The tail is bytes nothing learnt, so the cover is expensive and the
52
+ // search reaches every fixpoint of the hub before it settles.
53
+ answers.push(await mind.respondText("hello zqxv wbjk"));
54
+ const c = mind.lastCost.counters;
55
+ assert.ok(c.joinFacts > 0, "the derivation's facts are priced");
56
+ priced.push(c.joinFacts);
57
+ await mind.store.close();
58
+ }
59
+ assert.equal(
60
+ priced[1],
61
+ priced[0],
62
+ `facts priced grew with the hub's degree: ${priced.join(" → ")}`,
63
+ );
64
+ // 150.2 — the degree changes what the search REACHES, not what it chose.
65
+ assert.equal(answers[1], answers[0]);
66
+ });
@@ -0,0 +1,142 @@
1
+ // 151-the-cover-pays-for-what-it-reaches.test.mjs — the cover's expensive
2
+ // premises are resolved where the search REACHES them, and a span's cheapest
3
+ // completion dominates the rest.
4
+ //
5
+ // THE TRAP, four times. Each of these paid for every candidate the search
6
+ // could have used, before (or regardless of whether) it used it:
7
+ // • connectors — every touching pair of recognised sites was bridged before
8
+ // the search ran (measured on the 31.7M-node store: a 262-byte query
9
+ // recognised whole paid 23 bridges, 143,520 junction pops, 7.3 s of 7.9 s);
10
+ // • concept hops — every edge-less site paid a halo lookup, though the hop is
11
+ // priced at CONCEPT and the whole's own edge closes the cover at one STEP;
12
+ // • fusion — the post-grounding fusion climbed for a second point of
13
+ // attention even when primary's evidence spans the whole query, where no
14
+ // root can stand a quantum apart from it (3.7 s for an exact dialogue turn);
15
+ // • alternatives — a greeting's thousands of replies were each a completion of
16
+ // the same span, each fused and canonically resolved with its neighbours
17
+ // (122,096 pops, 5.4 s of an 8.6 s cover, to stand in the end on two hops).
18
+ //
19
+ // Pinned:
20
+ // 151.1 a query recognised whole bridges no connector for its parts;
21
+ // 151.2 an edge-less part's concept hop is not looked up when the whole
22
+ // closes the cover first;
23
+ // 151.3 a grounding standing on the whole query pays no fusion climb;
24
+ // 151.4 the work the cover generates does not grow with a hub's degree — the
25
+ // replies past the first are dominated, and the answer is unchanged.
26
+
27
+ import { test } from "node:test";
28
+ import assert from "node:assert/strict";
29
+ import { Mind } from "../dist/src/index.js";
30
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
31
+
32
+ async function fixture(pairs) {
33
+ const mind = new Mind({
34
+ seed: 7,
35
+ profile: true,
36
+ store: new SQliteStore({ path: ":memory:" }),
37
+ });
38
+ await mind.ingest(pairs);
39
+ return mind;
40
+ }
41
+
42
+ const sitesOf = (mind, text) => {
43
+ mind.beginResponse();
44
+ try {
45
+ return mind.recogniseSpan(new TextEncoder().encode(text)).sites.map((
46
+ s,
47
+ ) => [s.start, s.end]);
48
+ } finally {
49
+ mind.endResponse();
50
+ }
51
+ };
52
+
53
+ test("151.1 a query recognised whole bridges no connector for its parts", async () => {
54
+ // `ab` and `cd` both continue and TOUCH in the query, so they are an offered
55
+ // connector pair; `abcd` is learnt whole and closes the cover at one STEP,
56
+ // before either part's rewrite (a STEP each) leaves the agenda.
57
+ const mind = await fixture([
58
+ ["ab", "x"],
59
+ ["cd", "y"],
60
+ ["abcd", "the whole"],
61
+ ]);
62
+ const sites = sitesOf(mind, "abcd");
63
+ assert.ok(
64
+ sites.some(([s, e]) => s === 0 && e === 2) &&
65
+ sites.some(([s, e]) => s === 2 && e === 4),
66
+ `the touching parts must be recognised for the pair to be offered: ${
67
+ JSON.stringify(sites)
68
+ }`,
69
+ );
70
+ assert.equal(await mind.respondText("abcd"), "the whole");
71
+ assert.equal(mind.lastCost.counters.coverBridges ?? 0, 0);
72
+ await mind.store.close();
73
+ });
74
+
75
+ test("151.2 an unreached concept hop is not looked up", async () => {
76
+ // `ab` and `cd` are learnt only as CONTINUATIONS: they lead somewhere (a halo)
77
+ // but have no edge of their own, so each is offered a concept hop.
78
+ const mind = await fixture([
79
+ ["abcd", "the whole"],
80
+ ["first", "ab"],
81
+ ["second", "cd"],
82
+ ]);
83
+ const sites = sitesOf(mind, "abcd");
84
+ assert.ok(
85
+ sites.length > 1,
86
+ `the edge-less parts must be recognised: ${JSON.stringify(sites)}`,
87
+ );
88
+ assert.equal(await mind.respondText("abcd"), "the whole");
89
+ assert.equal(mind.lastCost.counters.haloQueries ?? 0, 0);
90
+ await mind.store.close();
91
+ });
92
+
93
+ test("151.3 a grounding standing on the whole query pays no fusion climb", async () => {
94
+ // The answer carries none of the query's bytes, so the derivation stays open
95
+ // and the fusion layer engages — but primary's evidence is the whole query.
96
+ const mind = await fixture([
97
+ ["ab", "x"],
98
+ ["cd", "y"],
99
+ ["abcd", "the whole"],
100
+ ]);
101
+ assert.equal(await mind.respondText("abcd"), "the whole");
102
+ assert.equal(mind.lastCost.counters.climbs ?? 0, 0);
103
+ await mind.store.close();
104
+ });
105
+
106
+ /** "hello" → "hi there", then "hi there" forks across `n` replies — each a
107
+ * fixpoint completing the same span. The filler raises N so the hop's READ
108
+ * cap (√N) admits all `n`. */
109
+ async function hub(n) {
110
+ const pairs = [["hello", "hi there"]];
111
+ for (let k = 0; k < 1200; k++) {
112
+ pairs.push([`filler question ${k}`, `filler answer ${k}`]);
113
+ }
114
+ for (let k = 0; k < n; k++) {
115
+ pairs.push(["hi there", `reply number ${k} talks about topic ${k}.`]);
116
+ }
117
+ return await fixture(pairs);
118
+ }
119
+
120
+ test("151.4 a hub's degree does not grow the work the cover generates", async () => {
121
+ const generated = [];
122
+ const answers = [];
123
+ for (const n of [8, 32]) {
124
+ const mind = await hub(n);
125
+ // The tail is bytes nothing learnt, so the goal is PASS-priced and, before
126
+ // dominance, the search fused every reply with its neighbours.
127
+ answers.push(await mind.respondText("hello zqxv wbjk"));
128
+ const c = mind.lastCost.counters;
129
+ assert.ok(
130
+ c.searchDominated > 0,
131
+ "the replies past the first are dominated",
132
+ );
133
+ generated.push(c.searchPops - c.searchDominated);
134
+ await mind.store.close();
135
+ }
136
+ assert.equal(
137
+ generated[1],
138
+ generated[0],
139
+ `rule-generating pops grew with the hub's degree: ${generated.join(" → ")}`,
140
+ );
141
+ assert.equal(answers[1], answers[0]);
142
+ });
@@ -0,0 +1,146 @@
1
+ // 152-the-read-side-names-as-the-write-side.test.mjs — a stored deposit asked
2
+ // verbatim resolves to itself, however the store named its branches.
3
+ //
4
+ // THE DEFECT. `intern` (store.ts) names a branch by its children; when they
5
+ // name none, it looks up the FLAT node over the same bytes and reuses it (step
6
+ // 1b, "same bytes, same node"). Every deposit interns flat nodes — its whole
7
+ // input and each canonical window — so a later deposit whose fold grouped
8
+ // `ver` + `!` was stored with an earlier deposit's window `ver!` as that
9
+ // child. The read side (`exactNode`, `foldTree`) named branches by their
10
+ // children alone, so it could never name such a deposit again: measured on the
11
+ // 31.7M-node store, 7 of 80 dialogue turns asked verbatim resolved to nothing
12
+ // and fell to the composition path (seconds instead of milliseconds).
13
+ //
14
+ // Pinned:
15
+ // 152.1 the deposit resolves to its own context node, which leads to its
16
+ // continuation — through a branch named by its bytes;
17
+ // 152.2 the identity fold and the vector fold name the same node;
18
+ // 152.3 it is answered on the exact path — the cover alone, no consensus
19
+ // climb — not after the whole market (which reached the same reply
20
+ // through recall, 6 mechanism runs and a climb, before the fix).
21
+ // 152.4 a name found ONLY through the bytes (a flat index entry, no learnt
22
+ // structure) is where the exact lookup used to miss, so `resolve`
23
+ // still asks the canonical class there — which holds the learnt
24
+ // member that continues;
25
+ // 152.5 recognition does the same, so a query named only by such an entry
26
+ // is answered through the equivalent form that continues.
27
+
28
+ import { test } from "node:test";
29
+ import assert from "node:assert/strict";
30
+ import { Mind } from "../dist/src/index.js";
31
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
32
+ import {
33
+ exactNaming,
34
+ exactNode,
35
+ foldTree,
36
+ perceive,
37
+ resolve,
38
+ } from "../dist/src/mind/primitives.js";
39
+ import { textCanon } from "../dist/src/canon.js";
40
+
41
+ const QUERY = "Hey, Sarah. Come check out my new river!";
42
+ const REPLY = "Wow, you dug a trench.";
43
+ const enc = (s) => new TextEncoder().encode(s);
44
+ const dec = (b) => new TextDecoder().decode(b);
45
+
46
+ async function fixture() {
47
+ const mind = new Mind({
48
+ seed: 7,
49
+ profile: true,
50
+ store: new SQliteStore({ path: ":memory:" }),
51
+ });
52
+ // `Clever!` interns the canonical window `ver!` FIRST, so the pair's own
53
+ // fold — which groups `ver` + `!` — is stored with that window as the child.
54
+ await mind.ingest([["Clever!", "a word"], [QUERY, REPLY]]);
55
+ return mind;
56
+ }
57
+
58
+ test("152.1 a deposit stored through a flat window resolves to its own context", async () => {
59
+ const mind = await fixture();
60
+ mind.beginResponse();
61
+ try {
62
+ const id = resolve(mind, enc(QUERY));
63
+ assert.notEqual(id, null, "the stored deposit must resolve");
64
+ const next = mind.store.nextFirst(id, 1);
65
+ assert.equal(dec(mind.store.bytes(next[0])), REPLY);
66
+ assert.ok(
67
+ mind.meter.flatBranchNames > 0,
68
+ "the fixture must exercise a branch the store named by its bytes",
69
+ );
70
+ } finally {
71
+ mind.endResponse();
72
+ }
73
+ await mind.store.close();
74
+ });
75
+
76
+ test("152.2 the identity fold and the vector fold name the same node", async () => {
77
+ const mind = await fixture();
78
+ mind.beginResponse();
79
+ try {
80
+ const bytes = enc(QUERY);
81
+ const viaIdentity = exactNode(mind, bytes);
82
+ const viaVectors = foldTree(mind, perceive(mind, bytes), 0).node;
83
+ assert.notEqual(viaIdentity, null);
84
+ assert.equal(viaIdentity, viaVectors);
85
+ } finally {
86
+ mind.endResponse();
87
+ }
88
+ await mind.store.close();
89
+ });
90
+
91
+ test("152.3 the deposit asked verbatim is answered on the exact path", async () => {
92
+ const mind = await fixture();
93
+ assert.equal(await mind.respondText(QUERY), REPLY);
94
+ const c = mind.lastCost.counters;
95
+ assert.equal(c.mechanismRuns, 1, "the cover alone must ground it");
96
+ assert.equal(c.climbs ?? 0, 0, "no consensus climb may run");
97
+ await mind.store.close();
98
+ });
99
+
100
+ /** A flat index entry for `tonight` with NO learnt structure over those bytes —
101
+ * the state the 31.7M-node store holds for it (written by an earlier deposit
102
+ * path; today's deposits always intern the structure beside the flat copy, so
103
+ * the state is built through the store's own write API) — and the case-folded
104
+ * `Tonight`, learnt with a continuation. */
105
+ async function byteOnly() {
106
+ const mind = new Mind({
107
+ seed: 7,
108
+ profile: true,
109
+ store: new SQliteStore({ path: ":memory:", D: 1024, maxGroup: 4 }),
110
+ });
111
+ await mind.ingest([[
112
+ "Tonight",
113
+ "The composer of Tonight is Leonard Bernstein.",
114
+ ]]);
115
+ await mind.store.putBranch(
116
+ Array.from(enc("tonight"), (b) => -(b + 1)),
117
+ new Float32Array(1024).fill(1 / 32),
118
+ );
119
+ await mind.buildCanonIndex();
120
+ return mind;
121
+ }
122
+
123
+ test("152.4 a name found only through the bytes still asks the canonical class", async () => {
124
+ const mind = await byteOnly();
125
+ mind.beginResponse(undefined, textCanon);
126
+ try {
127
+ const named = exactNaming(mind, enc("tonight"));
128
+ assert.notEqual(named.id, null, "the flat entry names the bytes");
129
+ assert.equal(named.byBytes, true, "…and nothing but the bytes names them");
130
+ const id = resolve(mind, enc("tonight"));
131
+ assert.equal(dec(mind.store.bytes(id)), "Tonight");
132
+ assert.ok(mind.store.hasNext(id), "the class member that continues");
133
+ } finally {
134
+ mind.endResponse();
135
+ }
136
+ await mind.store.close();
137
+ });
138
+
139
+ test("152.5 a query named only by bytes is answered through the form that continues", async () => {
140
+ const mind = await byteOnly();
141
+ assert.equal(
142
+ await mind.respondText("tonight"),
143
+ "The composer of Tonight is Leonard Bernstein.",
144
+ );
145
+ await mind.store.close();
146
+ });