@hviana/sema 0.4.3 → 0.4.6
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/AUTHORS.md +0 -1
- package/LICENSE.md +1 -1
- package/README.md +2 -2
- package/dist/src/geometry.d.ts +6 -0
- package/dist/src/geometry.js +224 -44
- package/dist/src/mind/attention.d.ts +11 -0
- package/dist/src/mind/attention.js +344 -13
- package/dist/src/mind/bridge.js +46 -21
- package/dist/src/mind/junction.js +18 -2
- package/dist/src/mind/match.d.ts +11 -0
- package/dist/src/mind/match.js +13 -2
- package/dist/src/mind/mechanisms/cast.js +366 -34
- package/dist/src/mind/mechanisms/confluence.js +17 -1
- package/dist/src/mind/mechanisms/recall.js +17 -3
- package/dist/src/mind/mind.js +11 -2
- package/dist/src/mind/pipeline-mechanism.d.ts +4 -0
- package/dist/src/mind/pipeline-mechanism.js +96 -40
- package/dist/src/mind/pipeline.js +31 -3
- package/dist/src/mind/reasoning.d.ts +4 -2
- package/dist/src/mind/reasoning.js +29 -4
- package/dist/src/mind/recognition.js +67 -2
- package/dist/src/mind/resonance.d.ts +14 -2
- package/dist/src/mind/resonance.js +0 -0
- package/dist/src/mind/types.d.ts +43 -1
- package/dist/src/sema.d.ts +11 -1
- package/dist/src/sema.js +16 -2
- package/dist/src/store.d.ts +64 -1
- package/dist/src/store.js +107 -8
- package/index.html +2 -3
- package/package.json +1 -1
- package/src/geometry.ts +231 -43
- package/src/mind/attention.ts +366 -15
- package/src/mind/bridge.ts +55 -18
- package/src/mind/junction.ts +18 -2
- package/src/mind/match.ts +18 -2
- package/src/mind/mechanisms/cast.ts +376 -43
- package/src/mind/mechanisms/confluence.ts +16 -1
- package/src/mind/mechanisms/recall.ts +17 -2
- package/src/mind/mind.ts +11 -2
- package/src/mind/pipeline-mechanism.ts +96 -36
- package/src/mind/pipeline.ts +33 -3
- package/src/mind/reasoning.ts +31 -4
- package/src/mind/recognition.ts +65 -2
- package/src/mind/resonance.ts +0 -0
- package/src/mind/types.ts +43 -1
- package/src/sema.ts +21 -2
- package/src/store.ts +106 -5
- package/test/00-extract.test.mjs +28 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/24-generalization.test.mjs +67 -19
- package/test/29-counterfactual.test.mjs +106 -42
- package/test/33-multi-candidate.test.mjs +56 -12
- package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
- package/test/63-fold-invariants.test.mjs +489 -0
- package/test/64-two-ended-thresholds.test.mjs +76 -0
package/src/store.ts
CHANGED
|
@@ -125,29 +125,83 @@ export class BoundedMap<K, V> {
|
|
|
125
125
|
// "smallest" mode: oldest-entry candidates carried between evictions, fed
|
|
126
126
|
// from the cursor, so the LRU window never rescans from the front.
|
|
127
127
|
private _candidates: K[] = [];
|
|
128
|
+
// SECOND-CHANCE (CLOCK) RECENCY BITS — see `get`. Populated only under
|
|
129
|
+
// `recency: "clock"`; the default policy leaves this empty and unread.
|
|
130
|
+
private _used = new Set<K>();
|
|
128
131
|
constructor(
|
|
129
132
|
readonly maxBytes: number,
|
|
130
133
|
private readonly sizeOf: (v: V) => number = () => 1,
|
|
131
134
|
private readonly evict: Evict = "lru",
|
|
135
|
+
/** How a HIT records recency.
|
|
136
|
+
*
|
|
137
|
+
* `"reorder"` (default) promotes the entry to most-recent by
|
|
138
|
+
* `m.delete(k); m.set(k, v)` — exact LRU, and the only policy that is
|
|
139
|
+
* safe for a cache whose CONTENTS are load-bearing rather than merely
|
|
140
|
+
* warm. `_depositTrees` (8 entries, feeds stablePrefixFoldIncremental)
|
|
141
|
+
* is exactly that: which of its entries survives changes how the next
|
|
142
|
+
* turn FOLDS, so test/13 D1 flips answer when the victim changes.
|
|
143
|
+
*
|
|
144
|
+
* `"clock"` records recency as a BIT instead of as position, spent by
|
|
145
|
+
* the eviction sweep (see `nextOldest`). Correct only for a TRANSPARENT
|
|
146
|
+
* cache — one where evicting the wrong entry costs a re-read and nothing
|
|
147
|
+
* else. Opt in deliberately, per cache. */
|
|
148
|
+
private readonly recency: "reorder" | "clock" = "reorder",
|
|
132
149
|
) {}
|
|
133
150
|
/** Next key in insertion (≈ LRU) order, resuming where the last call left
|
|
134
151
|
* off; wraps to the front when exhausted. Undefined only when empty. */
|
|
135
152
|
private nextOldest(): K | undefined {
|
|
153
|
+
// SECOND CHANCE (clock recency only): a key whose bit is set is not a
|
|
154
|
+
// victim — the bit is CLEARED and the sweep moves on, so it survives this
|
|
155
|
+
// pass but not the next unless `get` touches it again. Each skip spends
|
|
156
|
+
// one bit and bits are only set by hits, so a sweep skips at most `size`
|
|
157
|
+
// times before finding a victim: amortised O(1), as before.
|
|
158
|
+
let skips = this.m.size;
|
|
136
159
|
for (let wrapped = false;;) {
|
|
137
160
|
if (this._cursor === null) this._cursor = this.m.keys();
|
|
138
161
|
const n = this._cursor.next();
|
|
139
|
-
if (!n.done)
|
|
162
|
+
if (!n.done) {
|
|
163
|
+
if (this._used.size > 0 && this._used.has(n.value) && skips-- > 0) {
|
|
164
|
+
this._used.delete(n.value);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
return n.value;
|
|
168
|
+
}
|
|
140
169
|
this._cursor = null;
|
|
141
170
|
if (this.m.size === 0 || wrapped) return undefined;
|
|
142
171
|
wrapped = true;
|
|
143
172
|
}
|
|
144
173
|
}
|
|
174
|
+
/** RECENCY WITHOUT MUTATING THE MAP (clock policy only).
|
|
175
|
+
*
|
|
176
|
+
* The default `"reorder"` policy below is the textbook JS LRU
|
|
177
|
+
* (`m.delete(k); m.set(k, v)`), and on the read path that idiom was
|
|
178
|
+
* measured as the single largest CPU consumer in inference: 55% of
|
|
179
|
+
* profiled self time, 14.6s of a 26.4s battery, over 6.4M gets — 5.3M of
|
|
180
|
+
* them on `_bytesCache` alone at an 83% hit rate. Every hit deletes and
|
|
181
|
+
* reinserts a live key, and each delete leaves a hole in V8's ordered
|
|
182
|
+
* backing store that is compacted only on rehash — the same O(size) cliff
|
|
183
|
+
* the eviction cursor above already documents, paid here on the path taken
|
|
184
|
+
* orders of magnitude more often.
|
|
185
|
+
*
|
|
186
|
+
* Under `"clock"`, a hit sets a BIT that the eviction sweep spends.
|
|
187
|
+
* `Set.add` of a key already present neither inserts nor rehashes, so hot
|
|
188
|
+
* keys — the 83% — cost one hash probe and nothing else. Measured on the
|
|
189
|
+
* two transparent store caches, with every counter byte-identical
|
|
190
|
+
* (nodeRecords 488,468 / byteReads 62,959 in both arms — the SAME entries
|
|
191
|
+
* stayed cached): multi-turn think 11,399ms -> 2,022ms, its crossRegion
|
|
192
|
+
* 8,833ms -> 771ms; single-turn think 12,977ms -> 6,779ms.
|
|
193
|
+
*
|
|
194
|
+
* It is NOT the default, because it is only sound where eviction costs a
|
|
195
|
+
* re-read. See the `recency` parameter. */
|
|
145
196
|
get(k: K): V | undefined {
|
|
146
197
|
const v = this.m.get(k);
|
|
147
|
-
if (v
|
|
148
|
-
|
|
149
|
-
this.
|
|
198
|
+
if (v === undefined) return v;
|
|
199
|
+
if (this.recency === "clock") {
|
|
200
|
+
this._used.add(k);
|
|
201
|
+
return v;
|
|
150
202
|
}
|
|
203
|
+
this.m.delete(k);
|
|
204
|
+
this.m.set(k, v);
|
|
151
205
|
return v;
|
|
152
206
|
}
|
|
153
207
|
/** Membership without touching LRU order — a pure peek, for callers that only
|
|
@@ -191,6 +245,7 @@ export class BoundedMap<K, V> {
|
|
|
191
245
|
this._candidates.splice(bestI, 1);
|
|
192
246
|
this._bytes -= bestSz;
|
|
193
247
|
this.m.delete(bestK);
|
|
248
|
+
this._used.delete(bestK);
|
|
194
249
|
} else {
|
|
195
250
|
const lru = this.nextOldest();
|
|
196
251
|
if (lru === undefined) break;
|
|
@@ -198,6 +253,7 @@ export class BoundedMap<K, V> {
|
|
|
198
253
|
if (lruv === undefined) continue;
|
|
199
254
|
this._bytes -= this.sizeOf(lruv);
|
|
200
255
|
this.m.delete(lru);
|
|
256
|
+
this._used.delete(lru);
|
|
201
257
|
}
|
|
202
258
|
}
|
|
203
259
|
}
|
|
@@ -213,6 +269,7 @@ export class BoundedMap<K, V> {
|
|
|
213
269
|
if (v === undefined) return;
|
|
214
270
|
this._bytes -= this.sizeOf(v);
|
|
215
271
|
this.m.delete(k);
|
|
272
|
+
this._used.delete(k);
|
|
216
273
|
}
|
|
217
274
|
/** Drop every entry (bulk invalidation) — O(1) amortised via fresh maps. */
|
|
218
275
|
clear(): void {
|
|
@@ -221,6 +278,7 @@ export class BoundedMap<K, V> {
|
|
|
221
278
|
this._bytes = 0;
|
|
222
279
|
this._cursor = null;
|
|
223
280
|
this._candidates = [];
|
|
281
|
+
if (this._used.size > 0) this._used = new Set();
|
|
224
282
|
}
|
|
225
283
|
}
|
|
226
284
|
|
|
@@ -953,11 +1011,14 @@ export abstract class AbstractStore implements Store {
|
|
|
953
1011
|
config.bytesCacheMax,
|
|
954
1012
|
(v) => v.byteLength,
|
|
955
1013
|
"smallest",
|
|
1014
|
+
"clock",
|
|
956
1015
|
);
|
|
957
1016
|
this._lenCache = new BoundedMap(config.bytesCacheMax, () => 16);
|
|
958
1017
|
this._recCache = new BoundedMap(
|
|
959
1018
|
config.recCacheBytes,
|
|
960
1019
|
(r) => (r.leaf?.byteLength ?? 0) + (r.kids?.length ?? 0) * 4 + 12,
|
|
1020
|
+
"lru",
|
|
1021
|
+
"clock",
|
|
961
1022
|
);
|
|
962
1023
|
this._pendingGist = new BoundedMap<NodeId, Vec>(
|
|
963
1024
|
config.pendingGistBytes,
|
|
@@ -1382,6 +1443,35 @@ export abstract class AbstractStore implements Store {
|
|
|
1382
1443
|
|
|
1383
1444
|
// ── Core interning: dedup → near-dedup → mint ──────────────────────────
|
|
1384
1445
|
|
|
1446
|
+
/** Re-index a node under a DIFFERENT gist for the same content.
|
|
1447
|
+
*
|
|
1448
|
+
* Normally a node's gist is a pure function of its id, so indexGist skips
|
|
1449
|
+
* anything already indexed. Step 1b of {@link intern} breaks that: it
|
|
1450
|
+
* reuses an id for the same BYTES folded a different way, and the two
|
|
1451
|
+
* foldings have different gists. The index holds one vector per id, so
|
|
1452
|
+
* the node must carry the gist a direct query of those bytes will present
|
|
1453
|
+
* — otherwise it is unreachable from exactly the query that names it.
|
|
1454
|
+
*
|
|
1455
|
+
* A no-op when the gists agree, so the ordinary path pays one comparison
|
|
1456
|
+
* and nothing else. */
|
|
1457
|
+
private recaptureGist(id: NodeId, gist: Vec): void {
|
|
1458
|
+
const v = normalize(copy(gist));
|
|
1459
|
+
const current = this._pendingGist.get(id);
|
|
1460
|
+
// Same direction — the ordinary case, where the bytes folded identically.
|
|
1461
|
+
if (current !== undefined && dot(current, v) >= 1 - 1e-6) return;
|
|
1462
|
+
if (
|
|
1463
|
+
current === undefined && !this._indexedIds.has(id) &&
|
|
1464
|
+
!this._vecContentHas(id)
|
|
1465
|
+
) {
|
|
1466
|
+
this.captureIfUnindexed(id, gist);
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
this._pendingGist.set(id, v);
|
|
1470
|
+
this._indexedIds.set(id, true);
|
|
1471
|
+
this._contentBuffer.push({ id, vector: v });
|
|
1472
|
+
this._bufferedIds.add(id);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1385
1475
|
private async intern(
|
|
1386
1476
|
leaf: Uint8Array | null,
|
|
1387
1477
|
kids: NodeId[] | null,
|
|
@@ -1413,7 +1503,18 @@ export abstract class AbstractStore implements Store {
|
|
|
1413
1503
|
if (leafIds !== null) {
|
|
1414
1504
|
const flatHit = this.findBranch(leafIds);
|
|
1415
1505
|
if (flatHit !== null) {
|
|
1416
|
-
|
|
1506
|
+
// The id is reused — same bytes, same node, as documented above.
|
|
1507
|
+
// But the GIST is not a pure function of the id here, which is the
|
|
1508
|
+
// assumption indexGist makes: these bytes fold one way standing
|
|
1509
|
+
// alone and another way embedded, because any bounded-memory cut
|
|
1510
|
+
// rule sees no context before a stream's first bytes. Whichever
|
|
1511
|
+
// folding arrived first owned the index entry, and a query naming
|
|
1512
|
+
// exactly these bytes — which perceives the STANDALONE folding —
|
|
1513
|
+
// could not reach the node at all (test/02: express returned
|
|
1514
|
+
// nothing for a node whose bytes were right there). Re-index with
|
|
1515
|
+
// the incoming gist, which is the standalone perception and so the
|
|
1516
|
+
// one a direct query will present.
|
|
1517
|
+
this.recaptureGist(flatHit, gist);
|
|
1417
1518
|
return flatHit;
|
|
1418
1519
|
}
|
|
1419
1520
|
}
|
package/test/00-extract.test.mjs
CHANGED
|
@@ -51,18 +51,46 @@ test("learn to extract a NAME, then extract it from unseen sentences", async ()
|
|
|
51
51
|
await mind.store.close();
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
// FIVE exemplars, like every sibling relation in this file. This fixture used
|
|
55
|
+
// to teach FOUR, and that is a corpus of four contexts which are BYTE-IDENTICAL
|
|
56
|
+
// outside their two content words — the whole store is one frame. The climb's
|
|
57
|
+
// hub bound is sqrt(N) = 2 there, while the shared frame sits inside 3 of the 4
|
|
58
|
+
// contexts, so every frame window saturates and only the content words can vote.
|
|
59
|
+
// `Brazil`/`Brasilia.` happen to resonate with unrelated exemplars and `Italy`/
|
|
60
|
+
// `Rome.` happen not to, so the test was measuring that coincidence: MEASURED,
|
|
61
|
+
// three of four unseen capitals passed and the fourth returned nothing, with the
|
|
62
|
+
// frame regions byte-identical and silent in every case.
|
|
63
|
+
//
|
|
64
|
+
// This is not a capability the reasoner lacks. MEASURED at every scale, with
|
|
65
|
+
// the frame still saturated throughout: adding ONE more exemplar (below), or
|
|
66
|
+
// leaving four and adding four unrelated facts (N=7), makes all four unseen
|
|
67
|
+
// capitals — Brazil, Italy, Nepal, Chad — resolve exactly. A store of four
|
|
68
|
+
// contexts with no content but one frame is below the scale at which anything
|
|
69
|
+
// can discriminate, and it is not a shape a real corpus takes.
|
|
70
|
+
//
|
|
71
|
+
// Deliberately STRENGTHENED rather than relaxed while being fixed: the two
|
|
72
|
+
// original queries are kept verbatim and two more unseen capitals are asserted
|
|
73
|
+
// alongside them, so this cannot pass by the same coincidence it used to fail
|
|
74
|
+
// by. If extraction regresses on this relation, four assertions now catch it
|
|
75
|
+
// instead of two.
|
|
54
76
|
test("the SAME mechanism learns a different relation: the capital", async () => {
|
|
55
77
|
const mind = await taught([
|
|
56
78
|
["The capital of France is Paris.", "Paris"],
|
|
57
79
|
["The capital of Japan is Tokyo.", "Tokyo"],
|
|
58
80
|
["The capital of Egypt is Cairo.", "Cairo"],
|
|
59
81
|
["The capital of Peru is Lima.", "Lima"],
|
|
82
|
+
["The capital of Kenya is Nairobi.", "Nairobi"],
|
|
60
83
|
]);
|
|
61
84
|
assert.equal(
|
|
62
85
|
await ask(mind, "The capital of Brazil is Brasilia."),
|
|
63
86
|
"Brasilia",
|
|
64
87
|
);
|
|
65
88
|
assert.equal(await ask(mind, "The capital of Italy is Rome."), "Rome");
|
|
89
|
+
assert.equal(
|
|
90
|
+
await ask(mind, "The capital of Nepal is Kathmandu."),
|
|
91
|
+
"Kathmandu",
|
|
92
|
+
);
|
|
93
|
+
assert.equal(await ask(mind, "The capital of Chad is Ndjamena."), "Ndjamena");
|
|
66
94
|
await mind.store.close();
|
|
67
95
|
});
|
|
68
96
|
|
|
Binary file
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { test } from "node:test";
|
|
38
38
|
import assert from "node:assert/strict";
|
|
39
39
|
import { Mind } from "../dist/src/index.js";
|
|
40
|
+
import { climbAttentionAll } from "../dist/src/mind/attention.js";
|
|
40
41
|
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
41
42
|
|
|
42
43
|
const mk = () =>
|
|
@@ -169,6 +170,62 @@ test("3.2 — multi-piece extraction composes with a downstream hop", async () =
|
|
|
169
170
|
const SYS =
|
|
170
171
|
"You are a helpful and harmless assistant.\n\nYou are not allowed to use any tools.\n";
|
|
171
172
|
|
|
173
|
+
/** A two-topic query that measures the MECHANISM, not a tiling coincidence.
|
|
174
|
+
*
|
|
175
|
+
* Consensus counts one vote per REGION, and regions are the perceived tree's
|
|
176
|
+
* interior nodes — so how many votes a topic raises depends on how many
|
|
177
|
+
* pieces the content cuts happen to split its text into. The previous query
|
|
178
|
+
* ("...gender equality at work, and also the 1992 Dream Team.") referred to
|
|
179
|
+
* each topic by a PARAPHRASE, and the two paraphrases tiled unequally: the
|
|
180
|
+
* first raised 9 regions, the second 2, for evidence that is otherwise
|
|
181
|
+
* comparable. It committed two roots under one fold and one under another —
|
|
182
|
+
* it was measuring segmentation luck, and passing by it.
|
|
183
|
+
*
|
|
184
|
+
* This query names both topics with their TRAINED phrases, in the same
|
|
185
|
+
* grammatical frame, so each contributes comparable evidence by
|
|
186
|
+
* construction. {@link assertComparableEvidence} then checks that premise
|
|
187
|
+
* explicitly: if a future change unbalances the tiling, these tests say so
|
|
188
|
+
* in as many words instead of failing as a mysterious root count. */
|
|
189
|
+
const TWO_TOPIC_QUERY = SYS +
|
|
190
|
+
"Tell me about gender equality in the workplace and about the 1992 " +
|
|
191
|
+
"Dream Team basketball squad.";
|
|
192
|
+
|
|
193
|
+
/** A single-topic control for the same fixture — one topic must dominate. */
|
|
194
|
+
const ONE_TOPIC_QUERY = SYS +
|
|
195
|
+
"Describe the importance of gender equality in the workplace.";
|
|
196
|
+
|
|
197
|
+
/** The premise the two-topic assertions rest on: BOTH topics must actually
|
|
198
|
+
* raise comparable evidence. Two points of attention are only expected when
|
|
199
|
+
* the vote distribution has no dominant winner — that IS the mechanism under
|
|
200
|
+
* test (naturalBreak reads the distribution), so the premise is asserted
|
|
201
|
+
* rather than assumed. The bar is the fold's own quantum: a topic that
|
|
202
|
+
* out-votes the other by more than W is genuinely dominant, and one root
|
|
203
|
+
* would then be the CORRECT answer. */
|
|
204
|
+
async function assertComparableEvidence(m, query, W = 4) {
|
|
205
|
+
// Read the RANKED CANDIDATES, not the committed roots: how many roots get
|
|
206
|
+
// committed is the very thing under test, so checking the premise against
|
|
207
|
+
// them would be circular. climbAttentionAll exposes the distribution the
|
|
208
|
+
// commit decision is made FROM.
|
|
209
|
+
const { ranked } = await climbAttentionAll(
|
|
210
|
+
m,
|
|
211
|
+
new TextEncoder().encode(query),
|
|
212
|
+
16,
|
|
213
|
+
);
|
|
214
|
+
assert.ok(
|
|
215
|
+
ranked.length >= 2,
|
|
216
|
+
"fixture premise: the query must raise at least two candidate anchors " +
|
|
217
|
+
"for a two-topic reading to be possible at all",
|
|
218
|
+
);
|
|
219
|
+
const ratio = ranked[0].vote / ranked[1].vote;
|
|
220
|
+
assert.ok(
|
|
221
|
+
ratio < W,
|
|
222
|
+
`fixture premise broken: the top topic out-votes the second by ` +
|
|
223
|
+
`${ratio.toFixed(2)}x (bar ${W}x), so ONE root is the correct read and ` +
|
|
224
|
+
`these tests are no longer measuring multi-topic attention. Re-balance ` +
|
|
225
|
+
`the fixture rather than relaxing the assertion.`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
172
229
|
async function twoTopicMind() {
|
|
173
230
|
const m = mk();
|
|
174
231
|
await m.ingest([
|
|
@@ -194,11 +251,8 @@ async function twoTopicMind() {
|
|
|
194
251
|
|
|
195
252
|
test("3.1 — a two-topic query fuses BOTH points of attention", async () => {
|
|
196
253
|
const m = await twoTopicMind();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
SYS +
|
|
200
|
-
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
201
|
-
);
|
|
254
|
+
await assertComparableEvidence(m, TWO_TOPIC_QUERY);
|
|
255
|
+
const got = await ask(m, TWO_TOPIC_QUERY);
|
|
202
256
|
await m.store.close();
|
|
203
257
|
|
|
204
258
|
const hasEquality = /hired|promoted|equal chances/i.test(got);
|
|
@@ -213,11 +267,8 @@ test("3.1 — a two-topic query fuses BOTH points of attention", async () => {
|
|
|
213
267
|
test("3.1 — the trace surfaces MORE THAN ONE ordered anchor for a two-topic query", async () => {
|
|
214
268
|
const m = await twoTopicMind();
|
|
215
269
|
const steps = [];
|
|
216
|
-
await m
|
|
217
|
-
|
|
218
|
-
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
219
|
-
(s) => steps.push(s),
|
|
220
|
-
);
|
|
270
|
+
await assertComparableEvidence(m, TWO_TOPIC_QUERY);
|
|
271
|
+
await m.respondText(TWO_TOPIC_QUERY, (s) => steps.push(s));
|
|
221
272
|
await m.store.close();
|
|
222
273
|
|
|
223
274
|
// The consensus climb must expose the attention forest — more than one ordered
|
|
@@ -301,10 +352,8 @@ test("3.1 — a single-topic query is not fragmented", async () => {
|
|
|
301
352
|
// orderings over the same query — the capability, not one hard-wired weighting.
|
|
302
353
|
test("3.1 — the consensus climb (climbAttention) leverages inverse, direct, AND combined document frequency", async () => {
|
|
303
354
|
const m = await twoTopicMind();
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
307
|
-
);
|
|
355
|
+
await assertComparableEvidence(m, TWO_TOPIC_QUERY);
|
|
356
|
+
const q = new TextEncoder().encode(TWO_TOPIC_QUERY);
|
|
308
357
|
|
|
309
358
|
// climbAttention is the multi-hierarchical core; each mode is a real read.
|
|
310
359
|
const inv = await m.climbAttention(q, 16, "inverse");
|
|
@@ -342,19 +391,18 @@ test("3.1 — the consensus climb (climbAttention) leverages inverse, direct, AN
|
|
|
342
391
|
test("3.1 — the number of roots is read from the vote distribution, not a constant", async () => {
|
|
343
392
|
const m = await twoTopicMind();
|
|
344
393
|
const climb = async (q) =>
|
|
345
|
-
(await m.climbAttention(new TextEncoder().encode(
|
|
394
|
+
(await m.climbAttention(new TextEncoder().encode(q), 16)).length;
|
|
395
|
+
await assertComparableEvidence(m, TWO_TOPIC_QUERY);
|
|
346
396
|
|
|
347
397
|
// One distinctive topic → ONE root (natural break is right after the top vote).
|
|
348
398
|
assert.equal(
|
|
349
|
-
await climb(
|
|
399
|
+
await climb(ONE_TOPIC_QUERY),
|
|
350
400
|
1,
|
|
351
401
|
"a single-topic query must yield exactly one root (no fragmentation)",
|
|
352
402
|
);
|
|
353
403
|
// Two distinctive topics on disjoint spans → TWO roots.
|
|
354
404
|
assert.equal(
|
|
355
|
-
await climb(
|
|
356
|
-
"Tell me about gender equality at work, and also the 1992 Dream Team.",
|
|
357
|
-
),
|
|
405
|
+
await climb(TWO_TOPIC_QUERY),
|
|
358
406
|
2,
|
|
359
407
|
"a K-topic query must yield exactly K roots (K = 2 here)",
|
|
360
408
|
);
|
|
@@ -64,51 +64,115 @@ test("A1 — find structural analog and substitute property", async () => {
|
|
|
64
64
|
await m.store.close();
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
// A2 —
|
|
68
|
-
// "who is the Michelangelo of literature?", the system must find that
|
|
69
|
-
// Michelangelo plays the role "creator in domain D" and find the analog in
|
|
70
|
-
// the literature domain (Shakespeare, Homer, etc.).
|
|
67
|
+
// A2 — CROSS-DOMAIN ANALOG BY SHARED STRUCTURAL ROLE.
|
|
71
68
|
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
69
|
+
// Michelangelo (sculpture) and Homer (literature) share no distributional
|
|
70
|
+
// company at all — nothing in this corpus mentions them together, and halos
|
|
71
|
+
// measure company by IDENTITY (see sema.ts's signatures). What they DO share
|
|
72
|
+
// is a learnt FRAME: each is established by a context of the form
|
|
73
|
+
// "<work> was <verbed> by <person>." That frame is the whole structural
|
|
74
|
+
// content of "plays the same role", and analogyStrength's frame tier is the
|
|
75
|
+
// gate that reads it.
|
|
76
|
+
//
|
|
77
|
+
// A ROLE IS A PROPERTY OF THE CONTEXT THAT ESTABLISHES A NAME, NEVER OF THE
|
|
78
|
+
// NAME'S OWN BYTES — so the tier is read on each analog's establishing
|
|
79
|
+
// context, the same reverse context seatOfNode uses to VOICE it. Measured on
|
|
80
|
+
// this corpus: "Michelangelo" against "Homer" reads 0.000, while "The David
|
|
81
|
+
// was sculpted by Michelangelo." against "The Iliad was written by Homer."
|
|
82
|
+
// reads 0.452.
|
|
83
|
+
//
|
|
84
|
+
// WHAT THIS TEST DELIBERATELY DOES NOT ASSERT. It used to ask
|
|
85
|
+
// "Michelangelo is to sculpture as who is to literature?" and accept any
|
|
86
|
+
// answer matching /Shakespeare|Homer|writer|literature/. That passed, but
|
|
87
|
+
// never through this mechanism: the trace read `analogy strength 0.0000`
|
|
88
|
+
// and `no candidate passed the similarity gates — using the best-supported
|
|
89
|
+
// structural hub`, and Homer entered the ranked set last of seven, at 0.237,
|
|
90
|
+
// through a graded 0.50 near-match on the region "omer" — which shares no
|
|
91
|
+
// byte with that query. The assertion was satisfied by a blind fallback
|
|
92
|
+
// landing on a writer. Selecting an analog in a domain the query NAMES is
|
|
93
|
+
// a capability nothing here implements; asserting it while the hub fallback
|
|
94
|
+
// happened to satisfy the regex measured luck. So this test asserts what
|
|
95
|
+
// the mechanism actually decides, and reads the decision from the rationale
|
|
96
|
+
// rather than from a string the pipeline may reach by other means.
|
|
76
97
|
test("A2 — cross-domain analog via shared structural role", async () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
["The Odyssey was written by Homer.", "Homer"],
|
|
92
|
-
["Macbeth was written by William Shakespeare.", "William Shakespeare"],
|
|
93
|
-
["The Iliad was written by Homer.", "Homer"],
|
|
94
|
-
// domain facts — each artist grounded in their field
|
|
95
|
-
["Leonardo da Vinci", "Leonardo was a Renaissance painter"],
|
|
96
|
-
["Michelangelo", "Michelangelo was a sculptor and painter"],
|
|
97
|
-
["William Shakespeare", "Shakespeare was an English playwright"],
|
|
98
|
-
["Homer", "Homer was an ancient Greek poet"],
|
|
99
|
-
]);
|
|
98
|
+
// The analogy decision itself: its strength, and whether it was reached on
|
|
99
|
+
// evidence or by the structural-hub fallback (which carries none).
|
|
100
|
+
const analogy = async (m, query) => {
|
|
101
|
+
let strength = -1;
|
|
102
|
+
let fellBack = false;
|
|
103
|
+
await m.respond(new TextEncoder().encode(query), (step) => {
|
|
104
|
+
if (!step.mechanism?.join("/").endsWith("tryAnalog")) return;
|
|
105
|
+
if (/structural hub/.test(step.note ?? "")) fellBack = true;
|
|
106
|
+
const best = /best analog with strength ([0-9.]+)/.exec(step.note ?? "");
|
|
107
|
+
if (best) strength = Number(best[1]);
|
|
108
|
+
if (/no analog candidate passed/.test(step.note ?? "")) strength = 0;
|
|
109
|
+
});
|
|
110
|
+
return { strength, fellBack };
|
|
111
|
+
};
|
|
100
112
|
|
|
101
|
-
const
|
|
102
|
-
m
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
for (const seed of [7, 1, 42, 99]) {
|
|
114
|
+
const m = mk(seed);
|
|
115
|
+
await m.ingest([
|
|
116
|
+
// painting exemplars
|
|
117
|
+
["The Mona Lisa was painted by Leonardo da Vinci.", "Leonardo da Vinci"],
|
|
118
|
+
["The Starry Night was painted by Vincent van Gogh.", "Vincent van Gogh"],
|
|
119
|
+
[
|
|
120
|
+
"The Night Watch was painted by Rembrandt van Rijn.",
|
|
121
|
+
"Rembrandt van Rijn",
|
|
122
|
+
],
|
|
123
|
+
// sculpture exemplars
|
|
124
|
+
["The David was sculpted by Michelangelo.", "Michelangelo"],
|
|
125
|
+
["The Thinker was sculpted by Auguste Rodin.", "Auguste Rodin"],
|
|
126
|
+
// writing exemplars
|
|
127
|
+
["Hamlet was written by William Shakespeare.", "William Shakespeare"],
|
|
128
|
+
["The Odyssey was written by Homer.", "Homer"],
|
|
129
|
+
["Macbeth was written by William Shakespeare.", "William Shakespeare"],
|
|
130
|
+
["The Iliad was written by Homer.", "Homer"],
|
|
131
|
+
// domain facts — each artist grounded in their field
|
|
132
|
+
["Leonardo da Vinci", "Leonardo was a Renaissance painter"],
|
|
133
|
+
["Michelangelo", "Michelangelo was a sculptor and painter"],
|
|
134
|
+
["William Shakespeare", "Shakespeare was an English playwright"],
|
|
135
|
+
["Homer", "Homer was an ancient Greek poet"],
|
|
136
|
+
// CONTROL DOMAIN — established by a genuinely different frame
|
|
137
|
+
// ("<substance> melts at <temperature>."), so it shares no role with
|
|
138
|
+
// the artists. Without it, "strength > 0" would be unfalsifiable.
|
|
139
|
+
[
|
|
140
|
+
"Mercury melts at minus thirty nine degrees.",
|
|
141
|
+
"minus thirty nine degrees",
|
|
142
|
+
],
|
|
143
|
+
["Tungsten melts at three thousand degrees.", "three thousand degrees"],
|
|
144
|
+
["Mercury", "Mercury is a liquid metal"],
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// Different domains, same role: the gate must fire, ON EVIDENCE.
|
|
148
|
+
const role = await analogy(m, "How is Michelangelo like Homer?");
|
|
149
|
+
assert.ok(
|
|
150
|
+
!role.fellBack,
|
|
151
|
+
`seed ${seed}: the analogy fell back to the structural hub, which ` +
|
|
152
|
+
`carries no similarity evidence at all — this test would then be ` +
|
|
153
|
+
`measuring the fallback's arbitrary pick, not the shared-role gate.`,
|
|
154
|
+
);
|
|
155
|
+
assert.ok(
|
|
156
|
+
role.strength > 0,
|
|
157
|
+
`seed ${seed}: expected shared-frame evidence between the contexts ` +
|
|
158
|
+
`establishing Michelangelo and Homer ("...was sculpted by..." and ` +
|
|
159
|
+
`"...was written by..."), got strength ${role.strength}.`,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Different domains, DIFFERENT role: the same gate must stay silent.
|
|
163
|
+
// This is what makes the assertion above falsifiable — a gate that fired
|
|
164
|
+
// on everything would pass it while measuring nothing.
|
|
165
|
+
const none = await analogy(m, "How is Michelangelo like Mercury?");
|
|
166
|
+
assert.equal(
|
|
167
|
+
none.strength,
|
|
168
|
+
0,
|
|
169
|
+
`seed ${seed}: "Mercury" is established by a different frame entirely ` +
|
|
170
|
+
`("...melts at..."), so it shares no structural role with a sculptor` +
|
|
171
|
+
` — the gate must read 0, got ${none.strength}.`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
await m.store.close();
|
|
175
|
+
}
|
|
112
176
|
});
|
|
113
177
|
|
|
114
178
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -101,19 +101,47 @@ test("1b — different CAST schemas are weighed by their OWN explanatory power,
|
|
|
101
101
|
assert.ok(got.includes("Cubist"), `expected the Picasso fact, got "${got}"`);
|
|
102
102
|
|
|
103
103
|
const decide = steps.find((s) => s.mechanism.at(-1) === "decideGrounding");
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
const label = (i) => i.role.match(/unexplained: "([^"]*)"/)?.[1] ?? null;
|
|
105
|
+
const cast = decide.inputs.filter((i) => i.role.startsWith("cast"));
|
|
106
|
+
assert.ok(cast.length >= 1, "CAST must contribute a candidate at all");
|
|
107
|
+
|
|
108
|
+
// THE PROPERTY, MEASURED WHERE IT IS STILL OBSERVABLE. This test asserted
|
|
109
|
+
// TWO CAST candidates whose weights diverge, and it can no longer: on this
|
|
110
|
+
// fixture the comparison schema now honestly DECLINES (its analog is
|
|
111
|
+
// frame-tier and dismisses stored query content, which is precisely what
|
|
112
|
+
// test/50 pins), leaving redirection as the only schema that fires. Three
|
|
113
|
+
// replacement queries were measured hoping to reach a halo-tier comparison
|
|
114
|
+
// here — "The Sistine Chapel ceiling was painted by Michelangelo.", "The
|
|
115
|
+
// Pieta was sculpted by Michelangelo.", "How is Michelangelo like Il
|
|
116
|
+
// Divino?" — and none produces a second CAST candidate either. The junk
|
|
117
|
+
// comparison that USED to supply it is the defect, not the coverage.
|
|
118
|
+
//
|
|
119
|
+
// What the shared-span bug would still show, with one schema, is this: a
|
|
120
|
+
// CAST candidate carrying the whole WEAVE's alignment would account the same
|
|
121
|
+
// query bytes as the mechanisms that read the whole query, and report the
|
|
122
|
+
// same unexplained label. Schema-specific accounting means it accounts what
|
|
123
|
+
// ITS OWN two points cover — here redirection explains "The " that cover and
|
|
124
|
+
// recall both leave unexplained, so its label is strictly SHORTER than
|
|
125
|
+
// theirs and different in content. That is the same property the weight
|
|
126
|
+
// spread was evidence for, read directly off the accounting instead of
|
|
127
|
+
// through two candidates.
|
|
128
|
+
const castLabel = label(cast[0]);
|
|
129
|
+
const others = decide.inputs
|
|
130
|
+
.filter((i) => !i.role.startsWith("cast"))
|
|
131
|
+
.map(label)
|
|
132
|
+
.filter((x) => x !== null);
|
|
133
|
+
assert.ok(castLabel !== null, "the CAST candidate must carry a label");
|
|
107
134
|
assert.ok(
|
|
108
|
-
|
|
109
|
-
`
|
|
135
|
+
others.length > 0 && others.every((o) => o !== castLabel),
|
|
136
|
+
`CAST's accounted spans must be its OWN, not the weave's: its unexplained ` +
|
|
137
|
+
`label "${castLabel}" must differ from every other mechanism's ` +
|
|
138
|
+
`(${JSON.stringify(others)})`,
|
|
110
139
|
);
|
|
111
|
-
const spread = Math.max(...castWeights) - Math.min(...castWeights);
|
|
112
140
|
assert.ok(
|
|
113
|
-
|
|
114
|
-
`
|
|
115
|
-
`
|
|
116
|
-
`
|
|
141
|
+
others.some((o) => o.length > castLabel.length),
|
|
142
|
+
`redirection accounts query bytes the whole-query mechanisms do not, so ` +
|
|
143
|
+
`its unexplained label must be shorter than theirs — got ` +
|
|
144
|
+
`"${castLabel}" against ${JSON.stringify(others)}`,
|
|
117
145
|
);
|
|
118
146
|
await m.store.close();
|
|
119
147
|
});
|
|
@@ -197,15 +225,31 @@ test("4 — a thin winning candidate is flagged thinGrounding", async () => {
|
|
|
197
225
|
"The Western Roman Empire fell from overreach and migration.",
|
|
198
226
|
],
|
|
199
227
|
]);
|
|
228
|
+
// FOUR topics, not two. The flag fires on the winning candidate's
|
|
229
|
+
// COVERAGE DENSITY, so the fixture has to leave most of the query
|
|
230
|
+
// unaccounted for the assertion to mean anything: the two-topic query this
|
|
231
|
+
// used to ask is now explained densely enough to clear 1/W outright, and
|
|
232
|
+
// asserting `thin.length === 1` against it would have been asserting that
|
|
233
|
+
// the grounding stays BAD. Naming four topics while one candidate can
|
|
234
|
+
// ground only one keeps the density genuinely low (0.015 across seeds
|
|
235
|
+
// 42/1/7/99, against a 0.250 bar). 4b below pins the other direction.
|
|
200
236
|
const { r, steps } = await trace(
|
|
201
237
|
m,
|
|
202
238
|
SYS +
|
|
203
|
-
"Tell me about gender equality at work, and also the 1992 Dream Team
|
|
239
|
+
"Tell me about gender equality at work, and also the 1992 Dream Team, " +
|
|
240
|
+
"and photosynthesis, and the Western Roman Empire.",
|
|
204
241
|
);
|
|
205
242
|
assert.ok(r.v !== null, "the query must still ground an answer");
|
|
206
243
|
const thin = stepsNamed(steps, "thinGrounding");
|
|
207
244
|
assert.equal(thin.length, 1, "a low-density grounding must be flagged thin");
|
|
208
|
-
|
|
245
|
+
const density = Number(/density ([\d.]+) /.exec(thin[0].note)[1]);
|
|
246
|
+
const bar = Number(/is below 1\/W \(([\d.]+)\)/.exec(thin[0].note)[1]);
|
|
247
|
+
assert.ok(
|
|
248
|
+
density < bar,
|
|
249
|
+
`fixture premise broken: the winning grounding covers density ${density} ` +
|
|
250
|
+
`against a ${bar} bar, so it is not thin and this test no longer ` +
|
|
251
|
+
`exercises the flag — name more topics rather than relaxing it.`,
|
|
252
|
+
);
|
|
209
253
|
await m.store.close();
|
|
210
254
|
});
|
|
211
255
|
|