@hviana/sema 0.4.1 → 0.4.3
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/dist/src/ingest-cache.js +4 -0
- package/dist/src/meter.d.ts +5 -0
- package/dist/src/meter.js +5 -0
- package/dist/src/mind/attention.js +19 -2
- package/dist/src/mind/bridge.js +265 -56
- package/dist/src/mind/junction.d.ts +7 -4
- package/dist/src/mind/junction.js +16 -5
- package/dist/src/mind/match.d.ts +15 -0
- package/dist/src/mind/match.js +92 -2
- package/dist/src/mind/mechanisms/cast.js +12 -1
- package/dist/src/mind/mechanisms/confluence.js +31 -1
- package/dist/src/mind/mechanisms/cover.d.ts +1 -1
- package/dist/src/mind/mechanisms/cover.js +29 -5
- package/dist/src/mind/mechanisms/recall.js +17 -41
- package/dist/src/mind/mind.d.ts +7 -0
- package/dist/src/mind/mind.js +25 -2
- package/dist/src/mind/pipeline-mechanism.d.ts +2 -2
- package/dist/src/mind/pipeline-mechanism.js +87 -4
- package/dist/src/mind/pipeline.js +1 -1
- package/dist/src/mind/reasoning.js +19 -11
- package/dist/src/mind/recognition.js +41 -0
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/traverse.d.ts +3 -1
- package/dist/src/mind/traverse.js +14 -13
- package/dist/src/mind/types.d.ts +10 -0
- package/package.json +1 -1
- package/src/ingest-cache.ts +4 -0
- package/src/meter.ts +5 -0
- package/src/mind/attention.ts +18 -1
- package/src/mind/bridge.ts +292 -54
- package/src/mind/junction.ts +21 -7
- package/src/mind/match.ts +92 -1
- package/src/mind/mechanisms/cast.ts +12 -0
- package/src/mind/mechanisms/confluence.ts +30 -1
- package/src/mind/mechanisms/cover.ts +36 -4
- package/src/mind/mechanisms/recall.ts +21 -44
- package/src/mind/mind.ts +39 -2
- package/src/mind/pipeline-mechanism.ts +86 -4
- package/src/mind/pipeline.ts +1 -1
- package/src/mind/reasoning.ts +15 -8
- package/src/mind/recognition.ts +40 -0
- package/src/mind/resonance.ts +0 -0
- package/src/mind/traverse.ts +17 -15
- package/src/mind/types.ts +10 -0
- package/test/49-natural-units-synonym-bridge.test.mjs +56 -15
package/src/mind/traverse.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { cosine, Vec } from "../vec.js";
|
|
|
10
10
|
import type { AncestorReach, MindContext, SaturationStop } from "./types.js";
|
|
11
11
|
import { gistOf, read } from "./primitives.js";
|
|
12
12
|
|
|
13
|
-
// ──
|
|
13
|
+
// ── Session structural memo ─────────────────────────────────────────────
|
|
14
14
|
//
|
|
15
|
-
//
|
|
15
|
+
// Between ingests the store is read-only, so structural reads are pure
|
|
16
16
|
// functions of the node id. edgeAncestors climbs different start nodes that
|
|
17
17
|
// share ancestry (regions sharing a chunk, canonicalChunkId's prefix probes)
|
|
18
18
|
// — the reachMemo already caches whole-climb results, but a node visited as an
|
|
@@ -42,21 +42,17 @@ const structCaches = new WeakMap<object, StructCache>();
|
|
|
42
42
|
// every mechanism that prices commonality" — was reached only by confluence.
|
|
43
43
|
// The climb is by far the biggest consumer.
|
|
44
44
|
//
|
|
45
|
-
// Keyed
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// matters — the stable-prefix fold makes each turn's subtree independent of
|
|
50
|
-
// what follows, so 59–70% of a later turn's climb regions are byte-identical
|
|
51
|
-
// repeats of an earlier turn's (measured on a 4-turn session), and every one
|
|
52
|
-
// of them used to re-climb from cold.
|
|
45
|
+
// Keyed by the Mind's structural lifecycle identity: ordinary and
|
|
46
|
+
// conversational asks share it, and every ingest invalidates it. A real
|
|
47
|
+
// battery repeatedly reaches the same corpus scaffolding even when its
|
|
48
|
+
// surface questions differ.
|
|
53
49
|
//
|
|
54
50
|
// Budgeted, not unbounded (AGENTS §2.12): past the cap the whole map is
|
|
55
51
|
// dropped and re-derived, costing a cold climb and never a wrong answer.
|
|
56
52
|
const REACH_MEMO_MAX = 100_000;
|
|
57
53
|
const reachCaches = new WeakMap<object, Map<number, AncestorReach>>();
|
|
58
54
|
|
|
59
|
-
/** The reach memo this
|
|
55
|
+
/** The reach memo this ask should use — see the note above.
|
|
60
56
|
*
|
|
61
57
|
* A TRACED response always gets a fresh, empty one. `AncestorReach`'s
|
|
62
58
|
* `visited`/`maxDepth`/`saturation` fields are populated only when a trace
|
|
@@ -70,18 +66,18 @@ export function sharedReachMemo(
|
|
|
70
66
|
ctx: MindContext,
|
|
71
67
|
): Map<number, AncestorReach> {
|
|
72
68
|
if (ctx.trace !== null || ctx.climbMemo === null) return new Map();
|
|
73
|
-
let m = reachCaches.get(ctx.
|
|
74
|
-
if (m === undefined) reachCaches.set(ctx.
|
|
69
|
+
let m = reachCaches.get(ctx._structMemoKey);
|
|
70
|
+
if (m === undefined) reachCaches.set(ctx._structMemoKey, m = new Map());
|
|
75
71
|
else if (m.size >= REACH_MEMO_MAX) m.clear();
|
|
76
72
|
return m;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
function getStructCache(ctx: MindContext): StructCache | null {
|
|
80
76
|
if (ctx.climbMemo === null) return null;
|
|
81
|
-
let c = structCaches.get(ctx.
|
|
77
|
+
let c = structCaches.get(ctx._structMemoKey);
|
|
82
78
|
if (c === undefined) {
|
|
83
79
|
structCaches.set(
|
|
84
|
-
ctx.
|
|
80
|
+
ctx._structMemoKey,
|
|
85
81
|
c = {
|
|
86
82
|
hasNext: new Map(),
|
|
87
83
|
prevCount: new Map(),
|
|
@@ -92,6 +88,12 @@ function getStructCache(ctx: MindContext): StructCache | null {
|
|
|
92
88
|
return c;
|
|
93
89
|
}
|
|
94
90
|
|
|
91
|
+
/** Invalidate every session-lifetime structural read after a write. */
|
|
92
|
+
export function invalidateStructuralCaches(ctx: MindContext): void {
|
|
93
|
+
reachCaches.delete(ctx._structMemoKey);
|
|
94
|
+
structCaches.delete(ctx._structMemoKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
95
97
|
/** Cached {@link Store.hasNext} — pure during one respond(). */
|
|
96
98
|
function cachedHasNext(
|
|
97
99
|
ctx: MindContext,
|
package/src/mind/types.ts
CHANGED
|
@@ -279,6 +279,9 @@ export interface MindContext extends GraphSearchHost {
|
|
|
279
279
|
* Null outside respond(); during respondTurn() the conversation's
|
|
280
280
|
* persistent map is swapped in. */
|
|
281
281
|
climbMemo: Map<string, Map<string, AttentionRead>> | null;
|
|
282
|
+
/** Stable identity for session-lifetime, write-invalidated structural
|
|
283
|
+
* caches. Query-level climb results remain on climbMemo. */
|
|
284
|
+
_structMemoKey: object;
|
|
282
285
|
/** Memo of {@link recognise} — content-keyed (latin1) so recognised
|
|
283
286
|
* forms carry forward across conversation turns. Bypassed while a
|
|
284
287
|
* trace is attached. Null outside respond(). */
|
|
@@ -295,6 +298,13 @@ export interface MindContext extends GraphSearchHost {
|
|
|
295
298
|
* O(suffix) instead of O(context). Mind-lifetime (WeakMap keys are
|
|
296
299
|
* the Sema objects the pyramid keeps alive). */
|
|
297
300
|
_resolvedSubtrees: WeakMap<Sema, { id: number; len: number }> | null;
|
|
301
|
+
/** Completed assistant-turn byte spans in the current cumulative query.
|
|
302
|
+
* Empty for ordinary respond(); response-scoped structural context for
|
|
303
|
+
* mechanisms that must not re-derive already-produced replies. */
|
|
304
|
+
answeredSpans: ReadonlyArray<readonly [number, number]>;
|
|
305
|
+
/** Start offset of the user turn currently being answered. Zero for an
|
|
306
|
+
* ordinary respond() and for the first turn of a conversation. */
|
|
307
|
+
currentTurnStart: number;
|
|
298
308
|
_edgeGuide: Vec | null;
|
|
299
309
|
_edgeChoice: Map<number, number>;
|
|
300
310
|
_prevSeen: Set<number> | null;
|
|
@@ -14,20 +14,25 @@
|
|
|
14
14
|
// entry point to it — the halo system is real and works, it is simply
|
|
15
15
|
// never asked about a node that was never minted.
|
|
16
16
|
//
|
|
17
|
-
// Fix
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// same derived merge (existingUnits) plus a halo-corroborated substitution
|
|
24
|
-
// fallback in recognise().
|
|
17
|
+
// Fix: compose the distributional meaning of an arbitrary byte span from
|
|
18
|
+
// existing canonical-window containment and episode halos. No lexical unit
|
|
19
|
+
// index is introduced: each stored W-window ascends to the learned episodes
|
|
20
|
+
// containing it, their company halos are VSA-bundled, and the bridge admits
|
|
21
|
+
// the substitution only above the existing significanceBar plus its exact
|
|
22
|
+
// alignment/corroboration gates.
|
|
25
23
|
|
|
26
24
|
import { test } from "node:test";
|
|
27
25
|
import assert from "node:assert/strict";
|
|
28
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
conceptThreshold,
|
|
28
|
+
cosine,
|
|
29
|
+
Mind,
|
|
30
|
+
significanceBar,
|
|
31
|
+
} from "../dist/src/index.js";
|
|
29
32
|
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
30
|
-
import {
|
|
33
|
+
import { substitutionBridge } from "../dist/src/mind/bridge.js";
|
|
34
|
+
import { spanSynonymStrength } from "../dist/src/mind/match.js";
|
|
35
|
+
import { perceive, resolve } from "../dist/src/mind/primitives.js";
|
|
31
36
|
|
|
32
37
|
const enc = (s) => new TextEncoder().encode(s);
|
|
33
38
|
const dec = (b) => new TextDecoder().decode(b).replace(/\0+$/, "");
|
|
@@ -55,18 +60,17 @@ const TRAIN = [
|
|
|
55
60
|
["What is the biggest island on Earth?", "The biggest island is Greenland."],
|
|
56
61
|
];
|
|
57
62
|
|
|
58
|
-
test("
|
|
63
|
+
test("bare synonym words remain unaddressable without a lexical unit index", async () => {
|
|
59
64
|
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
60
65
|
await m.ingest(TRAIN);
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// bare word; it changes what recognise() can bridge to via halos).
|
|
66
|
+
// The capability composes meaning at read time; it deliberately does not
|
|
67
|
+
// mint or index modality-specific word nodes.
|
|
64
68
|
assert.equal(resolve(m, enc("biggest")), null);
|
|
65
69
|
assert.equal(resolve(m, enc("largest")), null);
|
|
66
70
|
await m.store.close();
|
|
67
71
|
});
|
|
68
72
|
|
|
69
|
-
test("
|
|
73
|
+
test("a trained fact is reachable through an untrained near-synonym", async () => {
|
|
70
74
|
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
71
75
|
await m.ingest(TRAIN);
|
|
72
76
|
|
|
@@ -173,3 +177,40 @@ test(
|
|
|
173
177
|
);
|
|
174
178
|
},
|
|
175
179
|
);
|
|
180
|
+
|
|
181
|
+
test("VSA company grounds a differently-spelled synonym without a lexical unit index", async () => {
|
|
182
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
183
|
+
await m.ingest([
|
|
184
|
+
["A physician treats illness.", "A healer treats illness."],
|
|
185
|
+
["A doctor treats illness.", "A healer treats illness."],
|
|
186
|
+
["The physician named Alice works today.", "Alice is on duty."],
|
|
187
|
+
["Which physician is on duty?", "Alice is on duty."],
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const doctor = enc(" doctor");
|
|
191
|
+
const physician = enc(" physician");
|
|
192
|
+
const geometric = cosine(
|
|
193
|
+
perceive(m, doctor).v,
|
|
194
|
+
perceive(m, physician).v,
|
|
195
|
+
);
|
|
196
|
+
const distributional = spanSynonymStrength(m, doctor, physician);
|
|
197
|
+
assert.ok(
|
|
198
|
+
geometric < conceptThreshold(m.store.D),
|
|
199
|
+
"the fixture must not pass through lexical/geometric similarity",
|
|
200
|
+
);
|
|
201
|
+
assert.ok(
|
|
202
|
+
distributional >= significanceBar(m.store.D),
|
|
203
|
+
"shared episode company must provide significant VSA synonym evidence",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const trained = resolve(m, enc("Which physician is on duty?"));
|
|
207
|
+
assert.notEqual(trained, null);
|
|
208
|
+
const hit = await substitutionBridge(
|
|
209
|
+
m,
|
|
210
|
+
enc("Which doctor is on duty?"),
|
|
211
|
+
async () => [trained],
|
|
212
|
+
);
|
|
213
|
+
assert.equal(hit?.id, trained);
|
|
214
|
+
assert.ok(hit?.subs.length > 0);
|
|
215
|
+
await m.store.close();
|
|
216
|
+
});
|