@hviana/sema 0.2.4 → 0.2.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/dist/src/geometry.d.ts +26 -0
- package/dist/src/geometry.js +32 -3
- package/dist/src/mind/attention.d.ts +232 -5
- package/dist/src/mind/attention.js +718 -166
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +30 -1
- package/dist/src/mind/junction.js +73 -18
- package/dist/src/mind/mind.d.ts +2 -0
- package/dist/src/mind/rationale.d.ts +7 -2
- package/dist/src/mind/rationale.js +6 -5
- package/dist/src/mind/traverse.js +74 -10
- package/dist/src/mind/types.d.ts +18 -0
- package/package.json +1 -1
- package/src/geometry.ts +52 -3
- package/src/mind/attention.ts +1134 -121
- package/src/mind/index.ts +16 -0
- package/src/mind/junction.ts +125 -16
- package/src/mind/mind.ts +15 -0
- package/src/mind/rationale.ts +12 -4
- package/src/mind/traverse.ts +75 -7
- package/src/mind/types.ts +25 -0
- package/test/51-structural-resonance-ladder.test.mjs +552 -0
- package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// 52-climb-consensus-instrumentation.test.mjs — structured instrumentation
|
|
2
|
+
// for the climbConsensus / inspectRationale step (spec §10).
|
|
3
|
+
//
|
|
4
|
+
// Purely additive: every assertion here checks the STRUCTURE of the `data`
|
|
5
|
+
// payload a traced "climbConsensus" RationaleStep now carries, alongside the
|
|
6
|
+
// existing human-readable `note` — never that inference itself changed
|
|
7
|
+
// (item 6 pins that explicitly: roots/ranked must be bit-identical whether
|
|
8
|
+
// or not a trace was requested).
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { Mind } from "../dist/src/index.js";
|
|
13
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
14
|
+
import { climbAttentionAll } from "../dist/src/mind/attention.js";
|
|
15
|
+
import { Rationale } from "../dist/src/mind/rationale.js";
|
|
16
|
+
|
|
17
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
18
|
+
const mk = (seed = 1) =>
|
|
19
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
20
|
+
|
|
21
|
+
/** Collect the full step stream for one traced query. */
|
|
22
|
+
async function trace(mind, q) {
|
|
23
|
+
const steps = [];
|
|
24
|
+
const ans = await mind.respondText(q, (s) => steps.push(s));
|
|
25
|
+
return { steps, ans };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The FIRST "climbConsensus" step — the one whose data reflects the actual
|
|
29
|
+
* per-region pipeline (a later same-response call, if any, would hit the
|
|
30
|
+
* content-keyed climb memo). */
|
|
31
|
+
function climbStep(steps) {
|
|
32
|
+
return steps.find((s) => s.mechanism.at(-1) === "climbConsensus");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SATURATION_REASONS = new Set([
|
|
36
|
+
"byte-atom-commonality",
|
|
37
|
+
"predecessor-fan-in",
|
|
38
|
+
"distinct-context-limit",
|
|
39
|
+
"parent-fan-out",
|
|
40
|
+
"lateral-cone-limit",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const ATTR_CORPUS = [
|
|
44
|
+
["red", "is a color"],
|
|
45
|
+
["blue", "is a color"],
|
|
46
|
+
["circle", "is a shape"],
|
|
47
|
+
["square", "is a shape"],
|
|
48
|
+
["red circle", "answer alpha"],
|
|
49
|
+
["red square", "answer beta"],
|
|
50
|
+
["blue circle", "answer gamma"],
|
|
51
|
+
["blue square", "answer delta"],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
55
|
+
// 1. every saturation stop reports one of the five reasons, with sound
|
|
56
|
+
// observed/limit provenance — read off the authoritative `reaches` list.
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
|
|
59
|
+
test("1. every reported saturation stop names a valid reason with sound provenance", async () => {
|
|
60
|
+
const m = mk(1);
|
|
61
|
+
await m.ingest(ATTR_CORPUS);
|
|
62
|
+
const { steps } = await trace(m, "red circle blue square");
|
|
63
|
+
await m.store.close();
|
|
64
|
+
|
|
65
|
+
const step = climbStep(steps);
|
|
66
|
+
assert.ok(step, "expected a climbConsensus step");
|
|
67
|
+
assert.ok(step.data, "climbConsensus step must carry a data payload");
|
|
68
|
+
assert.equal(step.data.version, 1);
|
|
69
|
+
assert.ok(Array.isArray(step.data.reaches), "expected a reaches list");
|
|
70
|
+
|
|
71
|
+
for (const r of step.data.reaches) {
|
|
72
|
+
assert.equal(typeof r.node, "number");
|
|
73
|
+
assert.ok(Array.isArray(r.roots));
|
|
74
|
+
assert.equal(typeof r.contextsReached, "number");
|
|
75
|
+
assert.equal(typeof r.saturated, "boolean");
|
|
76
|
+
if (r.saturated && r.saturation) {
|
|
77
|
+
assert.ok(
|
|
78
|
+
SATURATION_REASONS.has(r.saturation.reason),
|
|
79
|
+
`unexpected saturation reason "${r.saturation.reason}"`,
|
|
80
|
+
);
|
|
81
|
+
assert.equal(typeof r.saturation.node, "number");
|
|
82
|
+
assert.ok(r.saturation.observed > r.saturation.limit);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
88
|
+
// 2. canonical vs ANN/fallback regions report correct examined breadth.
|
|
89
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
90
|
+
|
|
91
|
+
test("2. an exact chunk region is selected canonically, without an ANN query", async () => {
|
|
92
|
+
const m = mk(2);
|
|
93
|
+
await m.ingest(ATTR_CORPUS);
|
|
94
|
+
// "red then circle" is not itself a trained fact (only "red circle" is),
|
|
95
|
+
// so recall cannot resolve it directly and the consensus climb runs.
|
|
96
|
+
const { steps } = await trace(m, "red then circle");
|
|
97
|
+
await m.store.close();
|
|
98
|
+
|
|
99
|
+
const step = climbStep(steps);
|
|
100
|
+
assert.ok(step, "expected a climbConsensus step");
|
|
101
|
+
assert.ok(step.data, "climbConsensus step must carry a data payload");
|
|
102
|
+
const regions = step.data.regions ?? [];
|
|
103
|
+
assert.ok(regions.length > 0, "expected per-region traces");
|
|
104
|
+
|
|
105
|
+
const votedRegions = regions.filter((r) => r.outcome === "voted");
|
|
106
|
+
assert.ok(votedRegions.length > 0, "expected at least one voted region");
|
|
107
|
+
for (const r of votedRegions) {
|
|
108
|
+
assert.ok(r.selected, "a voted region must report its selection");
|
|
109
|
+
assert.ok(
|
|
110
|
+
r.selected.source === "canonical" || r.selected.source === "ann",
|
|
111
|
+
"selected.source must be canonical or ann",
|
|
112
|
+
);
|
|
113
|
+
if (r.selected.source === "canonical") {
|
|
114
|
+
assert.equal(r.canonicalUsable, true);
|
|
115
|
+
}
|
|
116
|
+
if (r.selected.source === "ann" && !r.selected.fallback) {
|
|
117
|
+
assert.equal(r.annQueried, true);
|
|
118
|
+
assert.equal(typeof r.selected.rank, "number");
|
|
119
|
+
}
|
|
120
|
+
// annHitsExamined counts distinct CONSULTED hits, never more than what
|
|
121
|
+
// was returned.
|
|
122
|
+
assert.ok(r.annHitsExamined <= r.annHitsReturned || !r.annQueried);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
127
|
+
// 3/4. candidateBreadth / contributingVotes / contributingEvidence differ
|
|
128
|
+
// correctly under junction absorption, and superseded / saturation-
|
|
129
|
+
// masked votes never inflate contributingEvidence.
|
|
130
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
131
|
+
|
|
132
|
+
test("3/4. junction absorption widens contributingEvidence past contributingVotes", async () => {
|
|
133
|
+
const m = mk(3);
|
|
134
|
+
await m.ingest(ATTR_CORPUS);
|
|
135
|
+
const { steps } = await trace(m, "red then circle");
|
|
136
|
+
await m.store.close();
|
|
137
|
+
|
|
138
|
+
const step = climbStep(steps);
|
|
139
|
+
assert.ok(step.data.crossRegion, "expected a crossRegion summary");
|
|
140
|
+
assert.ok(
|
|
141
|
+
step.data.crossRegion.junctionVotes.length > 0,
|
|
142
|
+
"expected the exact junction to fire ('red circle' is a trained joint fact)",
|
|
143
|
+
);
|
|
144
|
+
const jv = step.data.crossRegion.junctionVotes[0];
|
|
145
|
+
assert.equal(jv.tier, "exact");
|
|
146
|
+
assert.ok(Array.isArray(jv.sourceRegionIndices));
|
|
147
|
+
assert.ok(Array.isArray(jv.explainedAwayRegionIndices));
|
|
148
|
+
assert.ok(jv.absorbed >= 1);
|
|
149
|
+
|
|
150
|
+
const anchors = step.data.anchors ?? [];
|
|
151
|
+
assert.ok(anchors.length > 0, "expected ranked anchor traces");
|
|
152
|
+
for (const a of anchors) {
|
|
153
|
+
// contributingEvidence is regionSupport (absorbed-weighted); it must
|
|
154
|
+
// never be LESS than the raw pooled-axiom count.
|
|
155
|
+
assert.ok(a.contributingEvidence >= a.contributingVotes);
|
|
156
|
+
assert.equal(typeof a.candidateBreadth, "number");
|
|
157
|
+
assert.ok(a.candidateBreadth >= a.contributingVotes);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// A region the junction explained away must be marked superseded on the
|
|
161
|
+
// per-region trace, and superseded regions never produced an ordinary
|
|
162
|
+
// vote that also counts toward contributingEvidence twice.
|
|
163
|
+
const regions = step.data.regions ?? [];
|
|
164
|
+
const supersededRegions = regions.filter((r) => r.superseded);
|
|
165
|
+
for (const r of supersededRegions) {
|
|
166
|
+
assert.equal(
|
|
167
|
+
r.ordinaryVoteProduced,
|
|
168
|
+
true,
|
|
169
|
+
"only a voted region can be superseded",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
175
|
+
// 5. commit reasons match the existing commitVotes loop.
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
177
|
+
|
|
178
|
+
test("5. commit decisions on the anchor trace match the roots actually returned", async () => {
|
|
179
|
+
const m = mk(4);
|
|
180
|
+
await m.ingest(ATTR_CORPUS);
|
|
181
|
+
const { steps } = await trace(m, "red circle blue square");
|
|
182
|
+
await m.store.close();
|
|
183
|
+
|
|
184
|
+
const step = climbStep(steps);
|
|
185
|
+
const anchors = step.data.anchors ?? [];
|
|
186
|
+
assert.ok(anchors.length > 0);
|
|
187
|
+
const roots = step.data.result.roots;
|
|
188
|
+
const rootAnchors = new Set(roots.map((r) => r.anchor));
|
|
189
|
+
|
|
190
|
+
for (const a of anchors) {
|
|
191
|
+
if (a.commit.status === "root") {
|
|
192
|
+
assert.ok(
|
|
193
|
+
rootAnchors.has(a.anchor),
|
|
194
|
+
`anchor ${a.anchor} marked root but absent from result.roots`,
|
|
195
|
+
);
|
|
196
|
+
assert.equal(a.commit.rejectionReasons.length, 0);
|
|
197
|
+
} else {
|
|
198
|
+
assert.ok(!rootAnchors.has(a.anchor) || a.commit.status === "overlap");
|
|
199
|
+
}
|
|
200
|
+
if (a.commit.status === "rejected") {
|
|
201
|
+
assert.ok(
|
|
202
|
+
a.commit.rejectionReasons.length > 0,
|
|
203
|
+
"a rejected anchor must name at least one reason",
|
|
204
|
+
);
|
|
205
|
+
for (const reason of a.commit.rejectionReasons) {
|
|
206
|
+
assert.ok(
|
|
207
|
+
["below-natural-break", "below-consensus-floor", "leading-saturation"]
|
|
208
|
+
.includes(reason),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// The dominant (first) root never carries the two vote-threshold gates.
|
|
214
|
+
const dominant = anchors.find((a) => a.commit.dominant);
|
|
215
|
+
if (dominant) {
|
|
216
|
+
assert.equal(dominant.commit.status, "root");
|
|
217
|
+
assert.equal(dominant.commit.passesNaturalBreak, undefined);
|
|
218
|
+
assert.equal(dominant.commit.passesConsensusFloor, undefined);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
223
|
+
// 6. roots and ranked results are bit-identical with and without tracing.
|
|
224
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
test("6. inspectRationale never changes the answer or the traced result.roots/ranked", async () => {
|
|
227
|
+
const queries = [
|
|
228
|
+
"red circle",
|
|
229
|
+
"red then circle",
|
|
230
|
+
"blue square",
|
|
231
|
+
"red",
|
|
232
|
+
"circle blue",
|
|
233
|
+
];
|
|
234
|
+
for (const q of queries) {
|
|
235
|
+
const plainMind = mk(5);
|
|
236
|
+
await plainMind.ingest(ATTR_CORPUS);
|
|
237
|
+
const plainAns = await plainMind.respondText(q);
|
|
238
|
+
await plainMind.store.close();
|
|
239
|
+
|
|
240
|
+
const tracedMind = mk(5);
|
|
241
|
+
await tracedMind.ingest(ATTR_CORPUS);
|
|
242
|
+
const { ans: tracedAns, steps } = await trace(tracedMind, q);
|
|
243
|
+
await tracedMind.store.close();
|
|
244
|
+
|
|
245
|
+
assert.equal(
|
|
246
|
+
tracedAns,
|
|
247
|
+
plainAns,
|
|
248
|
+
`answer differs for query "${q}" — tracing must be purely additive`,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Cross-check the climb itself: the untraced mind-level climbAttention
|
|
252
|
+
// convenience (never touches ctx.trace) must agree with the roots the
|
|
253
|
+
// traced climbConsensus step reported in its `data.result`.
|
|
254
|
+
const untracedMind = mk(5);
|
|
255
|
+
await untracedMind.ingest(ATTR_CORPUS);
|
|
256
|
+
const untracedRoots = await untracedMind.climbAttention(enc(q), 24);
|
|
257
|
+
await untracedMind.store.close();
|
|
258
|
+
|
|
259
|
+
const step = climbStep(steps);
|
|
260
|
+
if (step?.data) {
|
|
261
|
+
assert.deepEqual(
|
|
262
|
+
JSON.parse(JSON.stringify(step.data.result.roots)),
|
|
263
|
+
JSON.parse(JSON.stringify(untracedRoots)),
|
|
264
|
+
`traced result.roots differ from the untraced climb for query "${q}"`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
271
|
+
// 7. a repeated query produces the abbreviated cache-hit payload (§9).
|
|
272
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
273
|
+
|
|
274
|
+
test("7. a repeated query within one response emits the abbreviated cache trace", async () => {
|
|
275
|
+
const m = mk(7);
|
|
276
|
+
await m.ingest(ATTR_CORPUS);
|
|
277
|
+
// A top-level respond() gets a FRESH climbMemo every call (by design —
|
|
278
|
+
// beginResponse()), so a real cache hit is only observable by driving
|
|
279
|
+
// climbAttentionAll directly against a climbMemo that persists across
|
|
280
|
+
// two calls — exactly what respondTurn() does for a real conversation
|
|
281
|
+
// (see AttentionRead cache doc on climbAttentionAll).
|
|
282
|
+
m.climbMemo = new Map();
|
|
283
|
+
m.recogniseMemo = new Map();
|
|
284
|
+
m.perceiveMemo = new Map();
|
|
285
|
+
m.canon = null;
|
|
286
|
+
m.canonMemo = null;
|
|
287
|
+
|
|
288
|
+
const q = enc("red then circle");
|
|
289
|
+
const steps1 = [];
|
|
290
|
+
m.trace = new Rationale((s) => steps1.push(s));
|
|
291
|
+
await climbAttentionAll(m, q, 12, "inverse");
|
|
292
|
+
|
|
293
|
+
const steps2 = [];
|
|
294
|
+
m.trace = new Rationale((s) => steps2.push(s));
|
|
295
|
+
await climbAttentionAll(m, q, 12, "inverse");
|
|
296
|
+
await m.store.close();
|
|
297
|
+
|
|
298
|
+
const first = climbStep(steps1);
|
|
299
|
+
const second = climbStep(steps2);
|
|
300
|
+
assert.ok(first, "expected a climbConsensus step on the first call");
|
|
301
|
+
assert.ok(second, "expected a climbConsensus step on the repeated call");
|
|
302
|
+
|
|
303
|
+
assert.equal(first.data.cache.hit, false);
|
|
304
|
+
assert.equal(second.data.cache.hit, true);
|
|
305
|
+
assert.equal(second.data.cache.detailAvailable, false);
|
|
306
|
+
assert.equal(second.data.version, 1);
|
|
307
|
+
assert.deepEqual(second.data.config, {
|
|
308
|
+
annK: second.data.config.annK,
|
|
309
|
+
crossRegionProbeLimit: second.data.config.annK,
|
|
310
|
+
mode: second.data.config.mode,
|
|
311
|
+
});
|
|
312
|
+
assert.deepEqual(second.data.candidates, {
|
|
313
|
+
perceived: 0,
|
|
314
|
+
recognised: 0,
|
|
315
|
+
total: 0,
|
|
316
|
+
});
|
|
317
|
+
assert.equal(second.data.regions, undefined);
|
|
318
|
+
assert.equal(second.data.reaches, undefined);
|
|
319
|
+
assert.equal(second.data.crossRegion, undefined);
|
|
320
|
+
assert.equal(second.data.saturation, undefined);
|
|
321
|
+
assert.equal(second.data.pooling, undefined);
|
|
322
|
+
assert.equal(second.data.anchors, undefined);
|
|
323
|
+
assert.ok(second.data.result, "abbreviated payload must still carry result");
|
|
324
|
+
});
|