@hviana/sema 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +71 -5
- package/CONTRIBUTING.md +92 -10
- package/LICENSE.md +2 -2
- package/dist/src/derive/src/deduction.d.ts +12 -1
- package/dist/src/derive/src/deduction.js +5 -1
- package/dist/src/derive/src/index.d.ts +1 -0
- package/dist/src/geometry.d.ts +3 -30
- package/dist/src/geometry.js +330 -82
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/meter.d.ts +171 -0
- package/dist/src/meter.js +269 -0
- package/dist/src/mind/attention.d.ts +0 -4
- package/dist/src/mind/attention.js +251 -23
- package/dist/src/mind/bridge.d.ts +10 -1
- package/dist/src/mind/bridge.js +179 -10
- package/dist/src/mind/canonical.d.ts +6 -1
- package/dist/src/mind/canonical.js +6 -1
- package/dist/src/mind/graph-search.d.ts +9 -0
- package/dist/src/mind/graph-search.js +59 -19
- package/dist/src/mind/junction.d.ts +10 -0
- package/dist/src/mind/junction.js +14 -0
- package/dist/src/mind/match.d.ts +40 -0
- package/dist/src/mind/match.js +125 -1
- package/dist/src/mind/mechanisms/cast.js +63 -6
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
- package/dist/src/mind/mechanisms/extraction.js +1 -88
- package/dist/src/mind/mechanisms/recall.d.ts +3 -0
- package/dist/src/mind/mechanisms/recall.js +77 -14
- package/dist/src/mind/mind.d.ts +55 -4
- package/dist/src/mind/mind.js +110 -91
- package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
- package/dist/src/mind/pipeline-mechanism.js +179 -10
- package/dist/src/mind/pipeline.js +52 -10
- package/dist/src/mind/primitives.d.ts +11 -15
- package/dist/src/mind/primitives.js +47 -28
- package/dist/src/mind/reasoning.d.ts +7 -1
- package/dist/src/mind/reasoning.js +40 -8
- package/dist/src/mind/recognition.js +93 -20
- package/dist/src/mind/traverse.d.ts +11 -0
- package/dist/src/mind/traverse.js +58 -5
- package/dist/src/mind/types.d.ts +28 -5
- package/dist/src/store.d.ts +15 -0
- package/dist/src/store.js +91 -6
- package/package.json +1 -1
- package/src/derive/src/deduction.ts +15 -0
- package/src/derive/src/index.ts +1 -0
- package/src/geometry.ts +350 -122
- package/src/index.ts +1 -0
- package/src/meter.ts +333 -0
- package/src/mind/attention.ts +268 -31
- package/src/mind/bridge.ts +187 -10
- package/src/mind/canonical.ts +6 -1
- package/src/mind/graph-search.ts +60 -21
- package/src/mind/junction.ts +12 -0
- package/src/mind/match.ts +146 -1
- package/src/mind/mechanisms/cast.ts +62 -6
- package/src/mind/mechanisms/extraction.ts +2 -103
- package/src/mind/mechanisms/recall.ts +84 -17
- package/src/mind/mind.ts +139 -98
- package/src/mind/pipeline-mechanism.ts +203 -13
- package/src/mind/pipeline.ts +65 -8
- package/src/mind/primitives.ts +49 -33
- package/src/mind/reasoning.ts +39 -7
- package/src/mind/recognition.ts +89 -19
- package/src/mind/traverse.ts +59 -5
- package/src/mind/types.ts +28 -5
- package/src/store.ts +75 -6
- package/test/14-scaling.test.mjs +17 -7
- package/test/31-audit.test.mjs +4 -1
- package/test/33-multi-candidate.test.mjs +13 -1
- package/test/36-already-answered-fusion.test.mjs +10 -3
- package/test/46-recognise-multibyte-edge.test.mjs +3 -3
- package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
- package/test/55-cost-meter.test.mjs +284 -0
- package/test/56-bridge-identity-admission.test.mjs +209 -0
- package/test/57-fusion-order.test.mjs +104 -0
- package/test/58-subquantum-sites.test.mjs +112 -0
- package/test/59-fold-invariance.test.mjs +226 -0
package/src/mind/pipeline.ts
CHANGED
|
@@ -32,12 +32,18 @@ export { aluToMechanism } from "./mechanisms/alu.js";
|
|
|
32
32
|
// ── Extension dispatch (pre-loop parse) ─────────────────────────────────────
|
|
33
33
|
|
|
34
34
|
async function collectComputed(
|
|
35
|
+
ctx: MindContext,
|
|
35
36
|
mechanisms: readonly PipelineMechanism[],
|
|
36
37
|
query: Uint8Array,
|
|
37
38
|
): Promise<ComputedSpan[]> {
|
|
38
39
|
const out: ComputedSpan[] = [];
|
|
40
|
+
const meter = ctx.meter;
|
|
39
41
|
for (const m of mechanisms) {
|
|
40
|
-
if (m.parse)
|
|
42
|
+
if (!m.parse) continue;
|
|
43
|
+
const spans = meter
|
|
44
|
+
? await meter.time(`${m.name}.parse`, () => m.parse!(query))
|
|
45
|
+
: await m.parse(query);
|
|
46
|
+
out.push(...spans);
|
|
41
47
|
}
|
|
42
48
|
return out;
|
|
43
49
|
}
|
|
@@ -143,7 +149,7 @@ export async function think(
|
|
|
143
149
|
const rec = recognise(ctx, query);
|
|
144
150
|
|
|
145
151
|
// Phase 1: collect computed spans from mechanisms that implement parse()
|
|
146
|
-
const computed = await collectComputed(mechanisms, query);
|
|
152
|
+
const computed = await collectComputed(ctx, mechanisms, query);
|
|
147
153
|
|
|
148
154
|
if (computed.length > 0) {
|
|
149
155
|
ctx.trace?.step(
|
|
@@ -181,6 +187,7 @@ export async function think(
|
|
|
181
187
|
used?: ReadonlySet<number>;
|
|
182
188
|
accounted: ReadonlyArray<[number, number]>;
|
|
183
189
|
unexplained: string;
|
|
190
|
+
complete?: boolean;
|
|
184
191
|
}
|
|
185
192
|
const grade = (w: number) => Math.floor(w / STEP);
|
|
186
193
|
const unaccounted = (spans: ReadonlyArray<[number, number]>): number =>
|
|
@@ -195,6 +202,7 @@ export async function think(
|
|
|
195
202
|
let best: Candidate | null = null;
|
|
196
203
|
const consider = (c: Candidate) => {
|
|
197
204
|
if (c.bytes.length === 0) return;
|
|
205
|
+
if (ctx.meter) ctx.meter.candidates++;
|
|
198
206
|
candidates.push(c);
|
|
199
207
|
if (best === null || grade(c.weight) < grade(best.weight)) best = c;
|
|
200
208
|
};
|
|
@@ -202,8 +210,21 @@ export async function think(
|
|
|
202
210
|
best === null || grade(floor) < grade(best.weight);
|
|
203
211
|
|
|
204
212
|
// Phase 3: grounding loop
|
|
213
|
+
// Per-mechanism accounting (src/meter.ts). The market's whole premise is
|
|
214
|
+
// that mechanisms compete on one cost scale — so the profiling read-out is
|
|
215
|
+
// also per-mechanism, uniformly: the loop never asks which one it holds.
|
|
216
|
+
const meter = ctx.meter;
|
|
205
217
|
for (const mech of mechanisms) {
|
|
206
|
-
const floor =
|
|
218
|
+
const floor = meter
|
|
219
|
+
? await meter.time(
|
|
220
|
+
`${mech.name}.floor`,
|
|
221
|
+
() => mech.floor(ctx, query, pre, worthRunning),
|
|
222
|
+
)
|
|
223
|
+
: await mech.floor(ctx, query, pre, worthRunning);
|
|
224
|
+
if (meter) {
|
|
225
|
+
if (floor === null) meter.mechanismSkips++;
|
|
226
|
+
else meter.mechanismFloors++;
|
|
227
|
+
}
|
|
207
228
|
if (floor === null) {
|
|
208
229
|
ctx.trace?.step(
|
|
209
230
|
"skipMechanism",
|
|
@@ -224,7 +245,10 @@ export async function think(
|
|
|
224
245
|
);
|
|
225
246
|
continue;
|
|
226
247
|
}
|
|
227
|
-
|
|
248
|
+
if (meter) meter.mechanismRuns++;
|
|
249
|
+
const results = meter
|
|
250
|
+
? await meter.time(`${mech.name}.run`, () => mech.run(ctx, query, pre))
|
|
251
|
+
: await mech.run(ctx, query, pre);
|
|
228
252
|
for (const r of results) {
|
|
229
253
|
const weight = r.weight ?? weigh(r.accounted, r.moves);
|
|
230
254
|
consider({
|
|
@@ -234,6 +258,7 @@ export async function think(
|
|
|
234
258
|
used: r.used,
|
|
235
259
|
accounted: r.accounted,
|
|
236
260
|
unexplained: r.unexplained,
|
|
261
|
+
complete: r.complete,
|
|
237
262
|
});
|
|
238
263
|
}
|
|
239
264
|
}
|
|
@@ -338,7 +363,16 @@ export async function think(
|
|
|
338
363
|
: provenance === "recall" || provenance === "recall-echo"
|
|
339
364
|
? new Set<number>()
|
|
340
365
|
: new Set(recognise(ctx, answer).sites.map((s) => s.payload));
|
|
341
|
-
|
|
366
|
+
// A grounding that DECLARED itself complete is not extended: the answer is
|
|
367
|
+
// already a trained form's own continuation, reached through an identity
|
|
368
|
+
// claim about the query, so a multi-hop pivot could only chain past the
|
|
369
|
+
// fact that produced it (see MechanismResult.complete).
|
|
370
|
+
const reasoned = decided.complete ? answer : meter
|
|
371
|
+
? await meter.time(
|
|
372
|
+
"reason",
|
|
373
|
+
() => reason(ctx, query, answer, preConsumed, pre),
|
|
374
|
+
)
|
|
375
|
+
: await reason(ctx, query, answer, preConsumed, pre);
|
|
342
376
|
|
|
343
377
|
// Fuse only when the query has a genuine REMAINDER no mechanism's
|
|
344
378
|
// structural evidence touched at all. `decided.accounted` alone
|
|
@@ -374,9 +408,32 @@ export async function think(
|
|
|
374
408
|
decided.accounted.every(([i, j]) =>
|
|
375
409
|
pre.computed.some((u) => u.i === i && u.j === j)
|
|
376
410
|
);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
411
|
+
// Where the winning grounding stands in the query — fusion places primary
|
|
412
|
+
// by it (see fuseAttention's `primarySpans`). `accounted` is the
|
|
413
|
+
// cost-ladder read and is authoritative when non-empty; when it is empty
|
|
414
|
+
// the grounding is a pure COMPUTATION, whose evidence is its computed span.
|
|
415
|
+
// Exactly the cost-ladder-vs-coverage distinction `explained` above draws,
|
|
416
|
+
// read here for POSITION instead of for coverage — and resolved here, where
|
|
417
|
+
// both readings are in hand, rather than inside fuseAttention.
|
|
418
|
+
const primarySpans: ReadonlyArray<[number, number]> =
|
|
419
|
+
decided.accounted.length > 0
|
|
420
|
+
? decided.accounted
|
|
421
|
+
: pre.computed.map((u): [number, number] => [u.i, u.j]);
|
|
422
|
+
const fused = remainder < ctx.space.maxGroup
|
|
423
|
+
? reasoned
|
|
424
|
+
: meter
|
|
425
|
+
? await meter.time(
|
|
426
|
+
"fuse",
|
|
427
|
+
() => fuseAttention(ctx, query, reasoned, pre, unclimbed, primarySpans),
|
|
428
|
+
)
|
|
429
|
+
: await fuseAttention(
|
|
430
|
+
ctx,
|
|
431
|
+
query,
|
|
432
|
+
reasoned,
|
|
433
|
+
pre,
|
|
434
|
+
unclimbed,
|
|
435
|
+
decided.accounted,
|
|
436
|
+
);
|
|
380
437
|
|
|
381
438
|
done(
|
|
382
439
|
fused,
|
package/src/mind/primitives.ts
CHANGED
|
@@ -7,7 +7,6 @@ import { Vec } from "../vec.js";
|
|
|
7
7
|
import { Sema } from "../sema.js";
|
|
8
8
|
import {
|
|
9
9
|
bytesToTree,
|
|
10
|
-
bytesToTreePyramid,
|
|
11
10
|
Grid,
|
|
12
11
|
gridToTree,
|
|
13
12
|
hilbertBytes,
|
|
@@ -64,7 +63,14 @@ export function perceive(
|
|
|
64
63
|
if (memo) {
|
|
65
64
|
const key = latin1Key(bytes);
|
|
66
65
|
const hit = memo.get(key);
|
|
67
|
-
if (hit !== undefined)
|
|
66
|
+
if (hit !== undefined) {
|
|
67
|
+
if (ctx.meter) ctx.meter.perceiveHits++;
|
|
68
|
+
return hit;
|
|
69
|
+
}
|
|
70
|
+
if (ctx.meter) {
|
|
71
|
+
ctx.meter.perceptions++;
|
|
72
|
+
ctx.meter.perceivedBytes += bytes.length;
|
|
73
|
+
}
|
|
68
74
|
const tree = bytesToTree(
|
|
69
75
|
ctx.space,
|
|
70
76
|
ctx.alphabet,
|
|
@@ -76,6 +82,10 @@ export function perceive(
|
|
|
76
82
|
memo.set(key, tree);
|
|
77
83
|
return tree;
|
|
78
84
|
}
|
|
85
|
+
if (ctx.meter) {
|
|
86
|
+
ctx.meter.perceptions++;
|
|
87
|
+
ctx.meter.perceivedBytes += bytes.length;
|
|
88
|
+
}
|
|
79
89
|
return bytesToTree(
|
|
80
90
|
ctx.space,
|
|
81
91
|
ctx.alphabet,
|
|
@@ -93,21 +103,17 @@ export function perceive(
|
|
|
93
103
|
return gridToTree(ctx.space, ctx.alphabet, input as Grid);
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
/** The DEPOSIT-shaped perceive.
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
|
|
108
|
-
* instead of O(context) per turn. The fold state is purely a cache; the
|
|
109
|
-
* boundary accumulation is what an evicted chain loses (falling back to
|
|
110
|
-
* the plain fold, the pre-boundary shape — a warm replay restores it). */
|
|
106
|
+
/** The DEPOSIT-shaped perceive. Folds over the stream's own content cuts —
|
|
107
|
+
* bit-identical to what inference computes for the same bytes, and that
|
|
108
|
+
* train/inference agreement is load-bearing for exact recall. An input that
|
|
109
|
+
* EXTENDS a previously deposited one is a conversation context grown by one
|
|
110
|
+
* turn; the cached prefix length IS the turn boundary (derived from the deposit
|
|
111
|
+
* sequence itself, never from a content convention) and joins the cut set, so
|
|
112
|
+
* the trained context node and the query's context subtree are the SAME node.
|
|
113
|
+
* Segment folds reuse across deposits ({@link stablePrefixFoldIncremental}) —
|
|
114
|
+
* O(turn) instead of O(context) per turn. All of it is purely a cache: an
|
|
115
|
+
* evicted chain loses only the turn boundaries, and since the content cuts do
|
|
116
|
+
* not depend on the cache, the segments themselves are unchanged. */
|
|
111
117
|
export function perceiveDeposit(
|
|
112
118
|
ctx: MindContext,
|
|
113
119
|
bytes: Uint8Array,
|
|
@@ -140,24 +146,32 @@ export function perceiveDeposit(
|
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
}
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
// ONLY turn boundaries belong here. The stream's own content cuts are NOT
|
|
150
|
+
// passed in: `bytesToTree` and `stablePrefixFoldIncremental` both derive them
|
|
151
|
+
// per span, at every level, and handing the level-0 cuts in as stable-prefix
|
|
152
|
+
// boundaries instead produces a LEFT-NESTED join of flat segments — a
|
|
153
|
+
// different tree from the one inference builds for the same bytes. When that
|
|
154
|
+
// happened, a deposit's context root and `resolve(question)` were different
|
|
155
|
+
// nodes, so the trained edge hung off a node inference never reached and
|
|
156
|
+
// recall went silent (test/44 caught it as a site that could not be emitted
|
|
157
|
+
// because the resolved node led nowhere). Train and infer must fold
|
|
158
|
+
// identically; the way to guarantee that is to give this function nothing
|
|
159
|
+
// extra to say.
|
|
160
|
+
const cuts = new Set<number>();
|
|
145
161
|
if (prev !== undefined) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
ctx.space,
|
|
149
|
-
ctx.alphabet,
|
|
150
|
-
bytes,
|
|
151
|
-
boundaries,
|
|
152
|
-
prev.stable,
|
|
153
|
-
);
|
|
154
|
-
tree = folded.tree;
|
|
155
|
-
entry = { boundaries, stable: folded.fold };
|
|
156
|
-
} else {
|
|
157
|
-
const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
|
|
158
|
-
tree = plain.tree;
|
|
159
|
-
entry = { boundaries: [], pyramid: plain.pyramid };
|
|
162
|
+
for (const b of prev.boundaries) cuts.add(b);
|
|
163
|
+
cuts.add(prefixLen);
|
|
160
164
|
}
|
|
165
|
+
const boundaries = [...cuts].sort((a, b) => a - b);
|
|
166
|
+
const folded = stablePrefixFoldIncremental(
|
|
167
|
+
ctx.space,
|
|
168
|
+
ctx.alphabet,
|
|
169
|
+
bytes,
|
|
170
|
+
boundaries,
|
|
171
|
+
prev?.stable,
|
|
172
|
+
);
|
|
173
|
+
const tree = folded.tree;
|
|
174
|
+
const entry: DepositCacheEntry = { boundaries, stable: folded.fold };
|
|
161
175
|
// Only a conversational deposit writes the cache too — otherwise a bare
|
|
162
176
|
// fact's plain fold could later be misread as a conversation's turn-zero
|
|
163
177
|
// boundary by an unrelated conversational deposit that happens to extend
|
|
@@ -243,6 +257,7 @@ export function foldTree(
|
|
|
243
257
|
* unknown. */
|
|
244
258
|
export function resolve(ctx: MindContext, bytes: Uint8Array): number | null {
|
|
245
259
|
if (bytes.length === 0) return null;
|
|
260
|
+
if (ctx.meter) ctx.meter.resolves++;
|
|
246
261
|
const exact = foldTree(ctx, perceive(ctx, bytes), 0).node;
|
|
247
262
|
if (exact !== null) return exact;
|
|
248
263
|
return canonResolve(ctx, bytes);
|
|
@@ -283,6 +298,7 @@ export function canonResolve(
|
|
|
283
298
|
const direct = foldTree(ctx, perceive(ctx, key), 0).node;
|
|
284
299
|
if (direct !== null) return set(direct);
|
|
285
300
|
}
|
|
301
|
+
if (ctx.meter) ctx.meter.canonLookups++;
|
|
286
302
|
const candidates = store.canonFind(canonHash(key));
|
|
287
303
|
if (candidates.length === 0) return set(null);
|
|
288
304
|
let best: number | null = null;
|
package/src/mind/reasoning.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { rItem, rNode } from "./trace.js";
|
|
|
7
7
|
import { bytesEqual, indexOf } from "../bytes.js";
|
|
8
8
|
import type { MindContext } from "./types.js";
|
|
9
9
|
import { resolve } from "./primitives.js";
|
|
10
|
-
import { corpusN } from "./traverse.js";
|
|
10
|
+
import { corpusN, hubBound } from "./traverse.js";
|
|
11
11
|
import { follow, haloSiblings, project } from "./match.js";
|
|
12
12
|
import { joinWithBridge, pivotInto } from "./resonance.js";
|
|
13
13
|
import type { Precomputed } from "./pipeline-mechanism.js";
|
|
@@ -57,16 +57,16 @@ export async function reason(
|
|
|
57
57
|
// convention every fan-out decision uses (first √N in the relation's own
|
|
58
58
|
// read order); a pivot suppressed only by a beyond-cap neighbour may now
|
|
59
59
|
// fire — the same visibility trade chooseNext documents.
|
|
60
|
-
const
|
|
60
|
+
const bound = hubBound(ctx);
|
|
61
61
|
const consumeNode = (id: number | null) => {
|
|
62
62
|
if (id === null) return;
|
|
63
63
|
consumed.add(id);
|
|
64
|
-
for (const p of ctx.store.prevFirst(id,
|
|
64
|
+
for (const p of ctx.store.prevFirst(id, bound)) consumed.add(p);
|
|
65
65
|
};
|
|
66
66
|
const consumeAll = (id: number | null) => {
|
|
67
67
|
if (id === null) return;
|
|
68
68
|
consumeNode(id);
|
|
69
|
-
for (const n of ctx.store.nextFirst(id,
|
|
69
|
+
for (const n of ctx.store.nextFirst(id, bound)) consumed.add(n);
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
// Pre-consume whatever the grounding stage already spoke for. The halo
|
|
@@ -99,7 +99,7 @@ export async function reason(
|
|
|
99
99
|
// for, so a consumed fixpoint falls through to the pivot step instead.
|
|
100
100
|
if (
|
|
101
101
|
curId !== null &&
|
|
102
|
-
ctx.store.nextFirst(curId,
|
|
102
|
+
ctx.store.nextFirst(curId, bound).some((n) => !consumed.has(n))
|
|
103
103
|
) {
|
|
104
104
|
const fwd = await follow(ctx, curId, qv);
|
|
105
105
|
const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
|
|
@@ -164,6 +164,12 @@ export async function fuseAttention(
|
|
|
164
164
|
* primary is unclimbed. Absent or false preserves the original
|
|
165
165
|
* behaviour exactly. */
|
|
166
166
|
unclimbed = false,
|
|
167
|
+
/** The query spans `primary`'s own grounding stands on — used ONLY to place
|
|
168
|
+
* primary in the fused reading order (see below). Resolved by the caller,
|
|
169
|
+
* which is the layer that knows how a given grounding records its evidence;
|
|
170
|
+
* fuseAttention just reads a position from it. Empty or absent preserves
|
|
171
|
+
* the original behaviour exactly. */
|
|
172
|
+
primarySpans: ReadonlyArray<readonly [number, number]> = [],
|
|
167
173
|
): Promise<Uint8Array> {
|
|
168
174
|
// When the answer is structurally drawn from the query itself
|
|
169
175
|
// (extraction), it already spans all the query's pieces — fusion
|
|
@@ -194,8 +200,34 @@ export async function fuseAttention(
|
|
|
194
200
|
return primary;
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
// WHERE THE QUERY ASKED FOR IT. The sort below orders the fused pieces by
|
|
204
|
+
// query position, which is the whole point of the `start` field: a
|
|
205
|
+
// multi-topic answer should read in the order the question posed its
|
|
206
|
+
// topics. Every ROOT carries its own start. `primary` did not — it was
|
|
207
|
+
// given forest[0].start, the FIRST attention root's position, which is
|
|
208
|
+
// primary's own source only when primary happens to come from that root.
|
|
209
|
+
// When it does not, primary is sorted to a position it never occupied.
|
|
210
|
+
//
|
|
211
|
+
// Observed live: "What is the capital of France? And what is 2 + 2?"
|
|
212
|
+
// answered "4The capital city of France is Paris." — the ALU result, whose
|
|
213
|
+
// evidence is the "2 + 2" span near the END of the query, inherited the
|
|
214
|
+
// France root's start of 0 and sorted ahead of the France answer. Both
|
|
215
|
+
// pieces were right; only the order was.
|
|
216
|
+
//
|
|
217
|
+
// primary's own position is the earliest query byte its grounding stands
|
|
218
|
+
// on. `accounted` is the cost-ladder read of that and is authoritative
|
|
219
|
+
// when non-empty; when it is empty the grounding is a pure COMPUTATION,
|
|
220
|
+
// whose evidence is its computed span — the same cost-ladder-vs-coverage
|
|
221
|
+
// distinction think() already draws for the fusion remainder ("`accounted`
|
|
222
|
+
// alone undercounts this ... cover prices its computed spans at near-zero
|
|
223
|
+
// and deliberately leaves them out"), read here for position instead of
|
|
224
|
+
// for coverage. With neither, nothing is known and the old behaviour
|
|
225
|
+
// (forest[0].start) stands.
|
|
226
|
+
const primaryStart = primarySpans.length > 0
|
|
227
|
+
? primarySpans.reduce((m, [s]) => Math.min(m, s), Infinity)
|
|
228
|
+
: forest[0].start;
|
|
197
229
|
const pieces: Array<{ start: number; bytes: Uint8Array }> = [
|
|
198
|
-
{ start:
|
|
230
|
+
{ start: primaryStart, bytes: primary },
|
|
199
231
|
];
|
|
200
232
|
const qv = pre.guide; // once, not per root
|
|
201
233
|
const rest = lonePromotes ? forest : forest.slice(1);
|
|
@@ -287,4 +319,4 @@ export async function fuseAttention(
|
|
|
287
319
|
|
|
288
320
|
// (resonance.js is already a static dependency above — `bridge` — so the old
|
|
289
321
|
// dynamic import of pivotInto guarded against a cycle that does not exist.)
|
|
290
|
-
import { containsSpan } from "./
|
|
322
|
+
import { containsSpan } from "./match.js";
|
package/src/mind/recognition.ts
CHANGED
|
@@ -58,6 +58,7 @@ export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
58
58
|
const key = latin1Key(bytes);
|
|
59
59
|
const hit = ctx.recogniseMemo.get(key);
|
|
60
60
|
if (hit !== undefined) {
|
|
61
|
+
if (ctx.meter) ctx.meter.recogniseHits++;
|
|
61
62
|
ctx.trace?.step(
|
|
62
63
|
"recognise",
|
|
63
64
|
[rItem(bytes, "query")],
|
|
@@ -80,11 +81,20 @@ export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
84
|
+
if (ctx.meter) {
|
|
85
|
+
ctx.meter.recognitions++;
|
|
86
|
+
ctx.meter.recognisedBytes += bytes.length;
|
|
87
|
+
}
|
|
83
88
|
const store = ctx.store;
|
|
84
89
|
const sites: Site[] = [];
|
|
85
90
|
const leaves: Leaf[] = [];
|
|
86
91
|
const splits = new Set<number>();
|
|
87
92
|
const starts = new Set<number>();
|
|
93
|
+
// The same cuts in ASCENDING order. The post-order walk below visits
|
|
94
|
+
// leaf-parents left to right, so appending as they are added keeps this
|
|
95
|
+
// sorted with no comparison — which is what lets the composite search find
|
|
96
|
+
// its candidates by binary search instead of rescanning the whole set.
|
|
97
|
+
const startList: number[] = [];
|
|
88
98
|
if (bytes.length === 0) return { sites, leaves, splits, starts };
|
|
89
99
|
|
|
90
100
|
// Span-resolve memo for THIS call: the structural pass (sub-runs inside
|
|
@@ -124,6 +134,34 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
124
134
|
const seen = new Set<string>();
|
|
125
135
|
const emit = (start: number, end: number, id: number) => {
|
|
126
136
|
if (id < 0 && atomsAreHubs) return;
|
|
137
|
+
// A SITE MUST SPAN ONE RIVER WINDOW. Below W, byte overlap is chance,
|
|
138
|
+
// not evidence — the principle identityBar already states ("below one
|
|
139
|
+
// river window, byte overlap is chance") and the bridge's attestedQ
|
|
140
|
+
// already applies ("spans shorter than W carry no window of their own").
|
|
141
|
+
// No new constant.
|
|
142
|
+
//
|
|
143
|
+
// This REPLACES the false premise it used to share with fuse() and
|
|
144
|
+
// tryChain: those gates asked "does this offset sit on a fold boundary?"
|
|
145
|
+
// and read the answer from `starts`, which is exactly {0, W, 2W, …}
|
|
146
|
+
// because riverFold groups fixed-arity — arithmetic, not evidence.
|
|
147
|
+
//
|
|
148
|
+
// Measured on the 17.9M-node store, over the sites of 7 probes (1 good,
|
|
149
|
+
// 11 junk by hand-labelling, corrected for whole-query forms):
|
|
150
|
+
// len >= W rejects "hi"(2) "of"(2) "is"(2) "di"(2) "the"(3),
|
|
151
|
+
// admits "Eiffel Tower"(12) and both whole-query forms
|
|
152
|
+
// len >= W-1 admits "the" — W-1 is the write side's straddle
|
|
153
|
+
// neighbour for RETRIEVAL, never a claim about units
|
|
154
|
+
// §2.7 saturation admits 11/11 junk: edgeAncestors on a site node
|
|
155
|
+
// reaches 1..48 contexts, so dominates(ctx, N) needs
|
|
156
|
+
// ctx > 162805 and never fires; every site reads DISC
|
|
157
|
+
// rarity does not separate: "hi" has 1 container, "the" 572
|
|
158
|
+
//
|
|
159
|
+
// A span covering the WHOLE query is exempt: then it is not a fragment of
|
|
160
|
+
// something longer, it is the question ("hi" asked on its own).
|
|
161
|
+
if (
|
|
162
|
+
atomsAreHubs && end - start < ctx.space.maxGroup &&
|
|
163
|
+
!(start === 0 && end === bytes.length)
|
|
164
|
+
) return;
|
|
127
165
|
const key = start + "," + end + "," + id;
|
|
128
166
|
if (seen.has(key)) return;
|
|
129
167
|
seen.add(key);
|
|
@@ -134,6 +172,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
134
172
|
|
|
135
173
|
// ── structural: the query's own perceived tree ──────────────────────
|
|
136
174
|
starts.add(0);
|
|
175
|
+
startList.push(0);
|
|
137
176
|
foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
|
|
138
177
|
if (n.kids === null) {
|
|
139
178
|
leaves.push({ start, end, bytes: n.leaf ?? new Uint8Array(0), node });
|
|
@@ -207,25 +246,47 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
207
246
|
// "And " prepended to a follow-up turn — not boundary noise, actual
|
|
208
247
|
// content the injected canonicalizer has no equivalence for) shows
|
|
209
248
|
// up as a canon-miss too big for the chunk-scale search above: the
|
|
210
|
-
// turn is its OWN segment
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// resolve()/findBranch, because
|
|
224
|
-
// kind of equivalence (case, in the
|
|
225
|
-
// not
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
// turn is its OWN segment, so it can be turn/segment-scale, not
|
|
250
|
+
// chunk-scale. Widening the size bound itself reopens the root-scale
|
|
251
|
+
// false-positive this module already fixed once (test/46); widening the
|
|
252
|
+
// SEARCH instead does not, because every candidate is a cut the query's
|
|
253
|
+
// OWN fold drew (`starts`, the same set the canonical pass privileges
|
|
254
|
+
// with full chain reach) — fold EVIDENCE, never a blind guess.
|
|
255
|
+
//
|
|
256
|
+
// The candidates are the fold's own segment starts inside this span, in
|
|
257
|
+
// order. They used to be probed at `start + k*W`, which assumed cuts
|
|
258
|
+
// land on multiples of W; content-defined cuts do not, so that stride
|
|
259
|
+
// tested offsets no segment ever began at and this search silently
|
|
260
|
+
// never fired (test/44 pins it). Still bounded to W candidates, each
|
|
261
|
+
// one O(1) from the sorted cut list before paying for a real
|
|
262
|
+
// canonResolve fold — canonResolve, not resolve()/findBranch, because
|
|
263
|
+
// the gap here is often exactly the kind of equivalence (case, in the
|
|
264
|
+
// live trace) canon exists for, not an exact-content coincidence.
|
|
265
|
+
// A deposit's ROOT is a whole-stream node, and a stream's ends are not
|
|
266
|
+
// content cuts — so an embedded occurrence of a trained form reproduces
|
|
267
|
+
// its SEGMENTS (which are offset-free) but never its root. What is
|
|
268
|
+
// being looked for is therefore a suffix of this span that happens to be
|
|
269
|
+
// a whole trained form, and its left edge can only be a cut the fold
|
|
270
|
+
// itself drew. Candidates are taken from the RIGHT, nearest the end
|
|
271
|
+
// first: the form ends where this node ends, so its start is near it.
|
|
272
|
+
// Left-to-right was wrong — in test/44 the target's start is the 6th cut
|
|
273
|
+
// from the end but the 12th from the beginning.
|
|
274
|
+
//
|
|
275
|
+
// `starts` is still filling (this runs inside the post-order walk), but
|
|
276
|
+
// post-order guarantees every chunk BELOW this span is already in it —
|
|
277
|
+
// exactly the set wanted. Bounded to chainReach(W) candidates, the same
|
|
278
|
+
// reach the canonical pass trusts, so cost stays O(reach · span).
|
|
279
|
+
let hi = startList.length; // first index past the last usable cut
|
|
280
|
+
let lo = 0;
|
|
281
|
+
while (lo < hi) {
|
|
282
|
+
const mid = (lo + hi) >> 1;
|
|
283
|
+
if (startList[mid] < end - 1) lo = mid + 1;
|
|
284
|
+
else hi = mid;
|
|
285
|
+
}
|
|
286
|
+
const reach = chainReach(W);
|
|
287
|
+
for (let k = 0; k < reach; k++) {
|
|
288
|
+
const p = startList[lo - 1 - k];
|
|
289
|
+
if (p === undefined || p <= start) break;
|
|
229
290
|
const cid = canonResolve(ctx, bytes.subarray(p, end));
|
|
230
291
|
if (cid !== null) emit(p, end, cid);
|
|
231
292
|
}
|
|
@@ -233,6 +294,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
233
294
|
}
|
|
234
295
|
if (isChunk(n)) {
|
|
235
296
|
starts.add(start);
|
|
297
|
+
if (startList[startList.length - 1] !== start) startList.push(start);
|
|
236
298
|
// Try every sub-span within this leaf-parent.
|
|
237
299
|
const leafOffsets: number[] = [];
|
|
238
300
|
let off = start;
|
|
@@ -304,6 +366,14 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
304
366
|
// hub-scale atoms, chained at an offset nothing in the query's own fold
|
|
305
367
|
// selected). Chains that start ON a boundary carry the fold's own
|
|
306
368
|
// evidence instead and are exempt.
|
|
369
|
+
//
|
|
370
|
+
// NOTE (2026-07-24): that last sentence is FALSE — `starts` is exactly
|
|
371
|
+
// {0, W, 2W, …} (riverFold groups fixed-arity), so the exemption is
|
|
372
|
+
// arithmetic, not evidence. Removing it wholesale was measured and
|
|
373
|
+
// REVERTED: it also drops legitimate multi-byte chains (the 12-byte
|
|
374
|
+
// "Eiffel Tower" site vanished with it). The premise is wrong but the
|
|
375
|
+
// trust it stood in for is real; a replacement signal is still open work.
|
|
376
|
+
// See bench/README.md.
|
|
307
377
|
const tryChain = (
|
|
308
378
|
p: number,
|
|
309
379
|
maxIds: number,
|
package/src/mind/traverse.ts
CHANGED
|
@@ -32,6 +32,50 @@ interface StructCache {
|
|
|
32
32
|
}
|
|
33
33
|
const structCaches = new WeakMap<object, StructCache>();
|
|
34
34
|
|
|
35
|
+
// ── The shared ancestor-reach memo ──────────────────────────────────────
|
|
36
|
+
//
|
|
37
|
+
// `edgeAncestors` is a pure function of (node, N) over a read-only store —
|
|
38
|
+
// asking never writes — so its result is reusable for as long as the store
|
|
39
|
+
// is not written to. There used to be TWO memos and they never met: the
|
|
40
|
+
// climb built a private one per call (computeAttention), while
|
|
41
|
+
// `Precomputed.reachMemo` — documented as "one response-scoped memo serves
|
|
42
|
+
// every mechanism that prices commonality" — was reached only by confluence.
|
|
43
|
+
// The climb is by far the biggest consumer.
|
|
44
|
+
//
|
|
45
|
+
// Keyed off `ctx.climbMemo`'s OBJECT IDENTITY, exactly like the struct cache
|
|
46
|
+
// above, which buys the right lifetime for free: a plain respond() has a
|
|
47
|
+
// fresh climbMemo, so the memo is response-scoped; a conversation turn has
|
|
48
|
+
// the conversation's persistent one, so it is conversation-scoped. That
|
|
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.
|
|
53
|
+
//
|
|
54
|
+
// Budgeted, not unbounded (AGENTS §2.12): past the cap the whole map is
|
|
55
|
+
// dropped and re-derived, costing a cold climb and never a wrong answer.
|
|
56
|
+
const REACH_MEMO_MAX = 100_000;
|
|
57
|
+
const reachCaches = new WeakMap<object, Map<number, AncestorReach>>();
|
|
58
|
+
|
|
59
|
+
/** The reach memo this response should use — see the note above.
|
|
60
|
+
*
|
|
61
|
+
* A TRACED response always gets a fresh, empty one. `AncestorReach`'s
|
|
62
|
+
* `visited`/`maxDepth`/`saturation` fields are populated only when a trace
|
|
63
|
+
* is attached, so an entry deposited by an untraced earlier turn would
|
|
64
|
+
* silently black out the reach detail of a later traced one; and the trace's
|
|
65
|
+
* reach payload is serialised by ITERATING this map, which must therefore
|
|
66
|
+
* hold what THIS climb consulted, not the whole conversation's history.
|
|
67
|
+
* Consistent with AGENTS §2.11: a traced response is a different machine —
|
|
68
|
+
* never benchmark with a trace attached. */
|
|
69
|
+
export function sharedReachMemo(
|
|
70
|
+
ctx: MindContext,
|
|
71
|
+
): Map<number, AncestorReach> {
|
|
72
|
+
if (ctx.trace !== null || ctx.climbMemo === null) return new Map();
|
|
73
|
+
let m = reachCaches.get(ctx.climbMemo);
|
|
74
|
+
if (m === undefined) reachCaches.set(ctx.climbMemo, m = new Map());
|
|
75
|
+
else if (m.size >= REACH_MEMO_MAX) m.clear();
|
|
76
|
+
return m;
|
|
77
|
+
}
|
|
78
|
+
|
|
35
79
|
function getStructCache(ctx: MindContext): StructCache | null {
|
|
36
80
|
if (ctx.climbMemo === null) return null;
|
|
37
81
|
let c = structCaches.get(ctx.climbMemo);
|
|
@@ -134,7 +178,7 @@ export function edgeAncestors(
|
|
|
134
178
|
// is withdrawn. On a small store the floor stays ≤ √N and the atom
|
|
135
179
|
// climbs exactly as before, so single-letter facts keep working.
|
|
136
180
|
if (id < 0 && atomIsHub(ctx, contextCount)) {
|
|
137
|
-
const bound0 =
|
|
181
|
+
const bound0 = boundFor(contextCount);
|
|
138
182
|
const reach: AncestorReach = {
|
|
139
183
|
roots: [],
|
|
140
184
|
contextsReached: 0,
|
|
@@ -156,7 +200,7 @@ export function edgeAncestors(
|
|
|
156
200
|
return reach;
|
|
157
201
|
}
|
|
158
202
|
|
|
159
|
-
const bound =
|
|
203
|
+
const bound = boundFor(contextCount);
|
|
160
204
|
const roots: number[] = [];
|
|
161
205
|
const seen = new Set<number>([id]);
|
|
162
206
|
const ctxSeen = new Set<number>();
|
|
@@ -213,6 +257,7 @@ export function edgeAncestors(
|
|
|
213
257
|
let maxDepth = 0;
|
|
214
258
|
|
|
215
259
|
const visit = (x: number): boolean => {
|
|
260
|
+
if (ctx.meter) ctx.meter.ancestorVisits++;
|
|
216
261
|
if (depths) {
|
|
217
262
|
visitedCount++;
|
|
218
263
|
if (curDepth > maxDepth) maxDepth = curDepth;
|
|
@@ -395,8 +440,7 @@ export function atomReach(ctx: MindContext, contextCount: number): number {
|
|
|
395
440
|
* atom votes and is recognised exactly as any stored form; above it the
|
|
396
441
|
* alphabet is scaffolding everywhere and abstains. */
|
|
397
442
|
export function atomIsHub(ctx: MindContext, contextCount: number): boolean {
|
|
398
|
-
return atomReach(ctx, contextCount) >
|
|
399
|
-
Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
443
|
+
return atomReach(ctx, contextCount) > boundFor(contextCount);
|
|
400
444
|
}
|
|
401
445
|
|
|
402
446
|
/** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
|
|
@@ -444,7 +488,17 @@ export function corpusN(ctx: MindContext): number {
|
|
|
444
488
|
* materialised list. {@link hubCap} is the list-side reading of the same
|
|
445
489
|
* convention. */
|
|
446
490
|
export function hubBound(ctx: MindContext): number {
|
|
447
|
-
return
|
|
491
|
+
return boundFor(corpusN(ctx));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** √N for an EXPLICIT context count — the ctx-free reading of {@link
|
|
495
|
+
* hubBound}, for the callers inside this module that are handed a count
|
|
496
|
+
* rather than a context ({@link edgeAncestors}, {@link atomIsHub}). The
|
|
497
|
+
* floor at 2 matches {@link corpusN}'s, so both readings agree for every
|
|
498
|
+
* input: the two used to be spelled out inline, once WITH the floor and
|
|
499
|
+
* once without, in the same function. */
|
|
500
|
+
function boundFor(contextCount: number): number {
|
|
501
|
+
return Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
448
502
|
}
|
|
449
503
|
|
|
450
504
|
/** Cap a candidate list at the hub bound √N (insertion order) — the ONE
|