@hviana/sema 0.2.9 → 0.4.0
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/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 +5 -4
- package/dist/src/mind/attention.js +254 -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/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +10 -0
- package/dist/src/mind/junction.js +14 -0
- package/dist/src/mind/learning.d.ts +32 -4
- package/dist/src/mind/learning.js +26 -4
- 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 +59 -5
- package/dist/src/mind/mind.js +115 -93
- package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
- package/dist/src/mind/pipeline-mechanism.js +179 -10
- package/dist/src/mind/pipeline.d.ts +29 -0
- package/dist/src/mind/pipeline.js +79 -21
- 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 +88 -7
- package/dist/src/mind/types.d.ts +39 -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 +276 -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/index.ts +6 -0
- package/src/mind/junction.ts +12 -0
- package/src/mind/learning.ts +46 -5
- 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 +144 -99
- package/src/mind/pipeline-mechanism.ts +203 -13
- package/src/mind/pipeline.ts +144 -36
- 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 +89 -8
- package/src/mind/types.ts +39 -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/54-evidence-k-instrumentation.test.mjs +175 -0
- 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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { rItem, rNode } from "./trace.js";
|
|
6
6
|
import { bytesEqual, indexOf } from "../bytes.js";
|
|
7
7
|
import { resolve } from "./primitives.js";
|
|
8
|
-
import {
|
|
8
|
+
import { hubBound } from "./traverse.js";
|
|
9
9
|
import { follow, haloSiblings, project } from "./match.js";
|
|
10
10
|
import { joinWithBridge, pivotInto } from "./resonance.js";
|
|
11
11
|
/** Whether `bytes` is a proper byte-subspan of `query` — already present in
|
|
@@ -45,19 +45,19 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
|
|
|
45
45
|
// convention every fan-out decision uses (first √N in the relation's own
|
|
46
46
|
// read order); a pivot suppressed only by a beyond-cap neighbour may now
|
|
47
47
|
// fire — the same visibility trade chooseNext documents.
|
|
48
|
-
const
|
|
48
|
+
const bound = hubBound(ctx);
|
|
49
49
|
const consumeNode = (id) => {
|
|
50
50
|
if (id === null)
|
|
51
51
|
return;
|
|
52
52
|
consumed.add(id);
|
|
53
|
-
for (const p of ctx.store.prevFirst(id,
|
|
53
|
+
for (const p of ctx.store.prevFirst(id, bound))
|
|
54
54
|
consumed.add(p);
|
|
55
55
|
};
|
|
56
56
|
const consumeAll = (id) => {
|
|
57
57
|
if (id === null)
|
|
58
58
|
return;
|
|
59
59
|
consumeNode(id);
|
|
60
|
-
for (const n of ctx.store.nextFirst(id,
|
|
60
|
+
for (const n of ctx.store.nextFirst(id, bound))
|
|
61
61
|
consumed.add(n);
|
|
62
62
|
};
|
|
63
63
|
// Pre-consume whatever the grounding stage already spoke for. The halo
|
|
@@ -90,7 +90,7 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
|
|
|
90
90
|
// absorbing it would repeat content the grounding stage already spoke
|
|
91
91
|
// for, so a consumed fixpoint falls through to the pivot step instead.
|
|
92
92
|
if (curId !== null &&
|
|
93
|
-
ctx.store.nextFirst(curId,
|
|
93
|
+
ctx.store.nextFirst(curId, bound).some((n) => !consumed.has(n))) {
|
|
94
94
|
const fwd = await follow(ctx, curId, qv);
|
|
95
95
|
const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
|
|
96
96
|
if (fwd !== null && !bytesEqual(fwd, cur) &&
|
|
@@ -133,7 +133,13 @@ export async function fuseAttention(ctx, query, primary, pre,
|
|
|
133
133
|
* primary's own source; that assumption is exactly backwards when
|
|
134
134
|
* primary is unclimbed. Absent or false preserves the original
|
|
135
135
|
* behaviour exactly. */
|
|
136
|
-
unclimbed = false
|
|
136
|
+
unclimbed = false,
|
|
137
|
+
/** The query spans `primary`'s own grounding stands on — used ONLY to place
|
|
138
|
+
* primary in the fused reading order (see below). Resolved by the caller,
|
|
139
|
+
* which is the layer that knows how a given grounding records its evidence;
|
|
140
|
+
* fuseAttention just reads a position from it. Empty or absent preserves
|
|
141
|
+
* the original behaviour exactly. */
|
|
142
|
+
primarySpans = []) {
|
|
137
143
|
// When the answer is structurally drawn from the query itself
|
|
138
144
|
// (extraction), it already spans all the query's pieces — fusion
|
|
139
145
|
// would only add noise from unrelated stored contexts. The gate is
|
|
@@ -162,8 +168,34 @@ unclimbed = false) {
|
|
|
162
168
|
if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
|
|
163
169
|
return primary;
|
|
164
170
|
}
|
|
171
|
+
// WHERE THE QUERY ASKED FOR IT. The sort below orders the fused pieces by
|
|
172
|
+
// query position, which is the whole point of the `start` field: a
|
|
173
|
+
// multi-topic answer should read in the order the question posed its
|
|
174
|
+
// topics. Every ROOT carries its own start. `primary` did not — it was
|
|
175
|
+
// given forest[0].start, the FIRST attention root's position, which is
|
|
176
|
+
// primary's own source only when primary happens to come from that root.
|
|
177
|
+
// When it does not, primary is sorted to a position it never occupied.
|
|
178
|
+
//
|
|
179
|
+
// Observed live: "What is the capital of France? And what is 2 + 2?"
|
|
180
|
+
// answered "4The capital city of France is Paris." — the ALU result, whose
|
|
181
|
+
// evidence is the "2 + 2" span near the END of the query, inherited the
|
|
182
|
+
// France root's start of 0 and sorted ahead of the France answer. Both
|
|
183
|
+
// pieces were right; only the order was.
|
|
184
|
+
//
|
|
185
|
+
// primary's own position is the earliest query byte its grounding stands
|
|
186
|
+
// on. `accounted` is the cost-ladder read of that and is authoritative
|
|
187
|
+
// when non-empty; when it is empty the grounding is a pure COMPUTATION,
|
|
188
|
+
// whose evidence is its computed span — the same cost-ladder-vs-coverage
|
|
189
|
+
// distinction think() already draws for the fusion remainder ("`accounted`
|
|
190
|
+
// alone undercounts this ... cover prices its computed spans at near-zero
|
|
191
|
+
// and deliberately leaves them out"), read here for position instead of
|
|
192
|
+
// for coverage. With neither, nothing is known and the old behaviour
|
|
193
|
+
// (forest[0].start) stands.
|
|
194
|
+
const primaryStart = primarySpans.length > 0
|
|
195
|
+
? primarySpans.reduce((m, [s]) => Math.min(m, s), Infinity)
|
|
196
|
+
: forest[0].start;
|
|
165
197
|
const pieces = [
|
|
166
|
-
{ start:
|
|
198
|
+
{ start: primaryStart, bytes: primary },
|
|
167
199
|
];
|
|
168
200
|
const qv = pre.guide; // once, not per root
|
|
169
201
|
const rest = lonePromotes ? forest : forest.slice(1);
|
|
@@ -237,4 +269,4 @@ unclimbed = false) {
|
|
|
237
269
|
}
|
|
238
270
|
// (resonance.js is already a static dependency above — `bridge` — so the old
|
|
239
271
|
// dynamic import of pivotInto guarded against a cycle that does not exist.)
|
|
240
|
-
import { containsSpan } from "./
|
|
272
|
+
import { containsSpan } from "./match.js";
|
|
@@ -47,6 +47,8 @@ export function recognise(ctx, bytes) {
|
|
|
47
47
|
const key = latin1Key(bytes);
|
|
48
48
|
const hit = ctx.recogniseMemo.get(key);
|
|
49
49
|
if (hit !== undefined) {
|
|
50
|
+
if (ctx.meter)
|
|
51
|
+
ctx.meter.recogniseHits++;
|
|
50
52
|
ctx.trace?.step("recognise", [rItem(bytes, "query")], hit.sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
|
|
51
53
|
s.start,
|
|
52
54
|
s.end,
|
|
@@ -61,11 +63,20 @@ export function recognise(ctx, bytes) {
|
|
|
61
63
|
return recogniseImpl(ctx, bytes);
|
|
62
64
|
}
|
|
63
65
|
function recogniseImpl(ctx, bytes) {
|
|
66
|
+
if (ctx.meter) {
|
|
67
|
+
ctx.meter.recognitions++;
|
|
68
|
+
ctx.meter.recognisedBytes += bytes.length;
|
|
69
|
+
}
|
|
64
70
|
const store = ctx.store;
|
|
65
71
|
const sites = [];
|
|
66
72
|
const leaves = [];
|
|
67
73
|
const splits = new Set();
|
|
68
74
|
const starts = new Set();
|
|
75
|
+
// The same cuts in ASCENDING order. The post-order walk below visits
|
|
76
|
+
// leaf-parents left to right, so appending as they are added keeps this
|
|
77
|
+
// sorted with no comparison — which is what lets the composite search find
|
|
78
|
+
// its candidates by binary search instead of rescanning the whole set.
|
|
79
|
+
const startList = [];
|
|
69
80
|
if (bytes.length === 0)
|
|
70
81
|
return { sites, leaves, splits, starts };
|
|
71
82
|
// Span-resolve memo for THIS call: the structural pass (sub-runs inside
|
|
@@ -105,6 +116,33 @@ function recogniseImpl(ctx, bytes) {
|
|
|
105
116
|
const emit = (start, end, id) => {
|
|
106
117
|
if (id < 0 && atomsAreHubs)
|
|
107
118
|
return;
|
|
119
|
+
// A SITE MUST SPAN ONE RIVER WINDOW. Below W, byte overlap is chance,
|
|
120
|
+
// not evidence — the principle identityBar already states ("below one
|
|
121
|
+
// river window, byte overlap is chance") and the bridge's attestedQ
|
|
122
|
+
// already applies ("spans shorter than W carry no window of their own").
|
|
123
|
+
// No new constant.
|
|
124
|
+
//
|
|
125
|
+
// This REPLACES the false premise it used to share with fuse() and
|
|
126
|
+
// tryChain: those gates asked "does this offset sit on a fold boundary?"
|
|
127
|
+
// and read the answer from `starts`, which is exactly {0, W, 2W, …}
|
|
128
|
+
// because riverFold groups fixed-arity — arithmetic, not evidence.
|
|
129
|
+
//
|
|
130
|
+
// Measured on the 17.9M-node store, over the sites of 7 probes (1 good,
|
|
131
|
+
// 11 junk by hand-labelling, corrected for whole-query forms):
|
|
132
|
+
// len >= W rejects "hi"(2) "of"(2) "is"(2) "di"(2) "the"(3),
|
|
133
|
+
// admits "Eiffel Tower"(12) and both whole-query forms
|
|
134
|
+
// len >= W-1 admits "the" — W-1 is the write side's straddle
|
|
135
|
+
// neighbour for RETRIEVAL, never a claim about units
|
|
136
|
+
// §2.7 saturation admits 11/11 junk: edgeAncestors on a site node
|
|
137
|
+
// reaches 1..48 contexts, so dominates(ctx, N) needs
|
|
138
|
+
// ctx > 162805 and never fires; every site reads DISC
|
|
139
|
+
// rarity does not separate: "hi" has 1 container, "the" 572
|
|
140
|
+
//
|
|
141
|
+
// A span covering the WHOLE query is exempt: then it is not a fragment of
|
|
142
|
+
// something longer, it is the question ("hi" asked on its own).
|
|
143
|
+
if (atomsAreHubs && end - start < ctx.space.maxGroup &&
|
|
144
|
+
!(start === 0 && end === bytes.length))
|
|
145
|
+
return;
|
|
108
146
|
const key = start + "," + end + "," + id;
|
|
109
147
|
if (seen.has(key))
|
|
110
148
|
return;
|
|
@@ -115,6 +153,7 @@ function recogniseImpl(ctx, bytes) {
|
|
|
115
153
|
};
|
|
116
154
|
// ── structural: the query's own perceived tree ──────────────────────
|
|
117
155
|
starts.add(0);
|
|
156
|
+
startList.push(0);
|
|
118
157
|
foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
|
|
119
158
|
if (n.kids === null) {
|
|
120
159
|
leaves.push({ start, end, bytes: n.leaf ?? new Uint8Array(0), node });
|
|
@@ -190,26 +229,50 @@ function recogniseImpl(ctx, bytes) {
|
|
|
190
229
|
// "And " prepended to a follow-up turn — not boundary noise, actual
|
|
191
230
|
// content the injected canonicalizer has no equivalence for) shows
|
|
192
231
|
// up as a canon-miss too big for the chunk-scale search above: the
|
|
193
|
-
// turn is its OWN segment
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// resolve()/findBranch, because
|
|
207
|
-
// kind of equivalence (case, in the
|
|
208
|
-
// not
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
232
|
+
// turn is its OWN segment, so it can be turn/segment-scale, not
|
|
233
|
+
// chunk-scale. Widening the size bound itself reopens the root-scale
|
|
234
|
+
// false-positive this module already fixed once (test/46); widening the
|
|
235
|
+
// SEARCH instead does not, because every candidate is a cut the query's
|
|
236
|
+
// OWN fold drew (`starts`, the same set the canonical pass privileges
|
|
237
|
+
// with full chain reach) — fold EVIDENCE, never a blind guess.
|
|
238
|
+
//
|
|
239
|
+
// The candidates are the fold's own segment starts inside this span, in
|
|
240
|
+
// order. They used to be probed at `start + k*W`, which assumed cuts
|
|
241
|
+
// land on multiples of W; content-defined cuts do not, so that stride
|
|
242
|
+
// tested offsets no segment ever began at and this search silently
|
|
243
|
+
// never fired (test/44 pins it). Still bounded to W candidates, each
|
|
244
|
+
// one O(1) from the sorted cut list before paying for a real
|
|
245
|
+
// canonResolve fold — canonResolve, not resolve()/findBranch, because
|
|
246
|
+
// the gap here is often exactly the kind of equivalence (case, in the
|
|
247
|
+
// live trace) canon exists for, not an exact-content coincidence.
|
|
248
|
+
// A deposit's ROOT is a whole-stream node, and a stream's ends are not
|
|
249
|
+
// content cuts — so an embedded occurrence of a trained form reproduces
|
|
250
|
+
// its SEGMENTS (which are offset-free) but never its root. What is
|
|
251
|
+
// being looked for is therefore a suffix of this span that happens to be
|
|
252
|
+
// a whole trained form, and its left edge can only be a cut the fold
|
|
253
|
+
// itself drew. Candidates are taken from the RIGHT, nearest the end
|
|
254
|
+
// first: the form ends where this node ends, so its start is near it.
|
|
255
|
+
// Left-to-right was wrong — in test/44 the target's start is the 6th cut
|
|
256
|
+
// from the end but the 12th from the beginning.
|
|
257
|
+
//
|
|
258
|
+
// `starts` is still filling (this runs inside the post-order walk), but
|
|
259
|
+
// post-order guarantees every chunk BELOW this span is already in it —
|
|
260
|
+
// exactly the set wanted. Bounded to chainReach(W) candidates, the same
|
|
261
|
+
// reach the canonical pass trusts, so cost stays O(reach · span).
|
|
262
|
+
let hi = startList.length; // first index past the last usable cut
|
|
263
|
+
let lo = 0;
|
|
264
|
+
while (lo < hi) {
|
|
265
|
+
const mid = (lo + hi) >> 1;
|
|
266
|
+
if (startList[mid] < end - 1)
|
|
267
|
+
lo = mid + 1;
|
|
268
|
+
else
|
|
269
|
+
hi = mid;
|
|
270
|
+
}
|
|
271
|
+
const reach = chainReach(W);
|
|
272
|
+
for (let k = 0; k < reach; k++) {
|
|
273
|
+
const p = startList[lo - 1 - k];
|
|
274
|
+
if (p === undefined || p <= start)
|
|
275
|
+
break;
|
|
213
276
|
const cid = canonResolve(ctx, bytes.subarray(p, end));
|
|
214
277
|
if (cid !== null)
|
|
215
278
|
emit(p, end, cid);
|
|
@@ -218,6 +281,8 @@ function recogniseImpl(ctx, bytes) {
|
|
|
218
281
|
}
|
|
219
282
|
if (isChunk(n)) {
|
|
220
283
|
starts.add(start);
|
|
284
|
+
if (startList[startList.length - 1] !== start)
|
|
285
|
+
startList.push(start);
|
|
221
286
|
// Try every sub-span within this leaf-parent.
|
|
222
287
|
const leafOffsets = [];
|
|
223
288
|
let off = start;
|
|
@@ -290,6 +355,14 @@ function recogniseImpl(ctx, bytes) {
|
|
|
290
355
|
// hub-scale atoms, chained at an offset nothing in the query's own fold
|
|
291
356
|
// selected). Chains that start ON a boundary carry the fold's own
|
|
292
357
|
// evidence instead and are exempt.
|
|
358
|
+
//
|
|
359
|
+
// NOTE (2026-07-24): that last sentence is FALSE — `starts` is exactly
|
|
360
|
+
// {0, W, 2W, …} (riverFold groups fixed-arity), so the exemption is
|
|
361
|
+
// arithmetic, not evidence. Removing it wholesale was measured and
|
|
362
|
+
// REVERTED: it also drops legitimate multi-byte chains (the 12-byte
|
|
363
|
+
// "Eiffel Tower" site vanished with it). The premise is wrong but the
|
|
364
|
+
// trust it stood in for is real; a replacement signal is still open work.
|
|
365
|
+
// See bench/README.md.
|
|
293
366
|
const tryChain = (p, maxIds, boundary) => {
|
|
294
367
|
const first = leafFrom(p);
|
|
295
368
|
if (!first)
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { Vec } from "../vec.js";
|
|
2
2
|
import type { AncestorReach, MindContext } from "./types.js";
|
|
3
|
+
/** The reach memo this response should use — see the note above.
|
|
4
|
+
*
|
|
5
|
+
* A TRACED response always gets a fresh, empty one. `AncestorReach`'s
|
|
6
|
+
* `visited`/`maxDepth`/`saturation` fields are populated only when a trace
|
|
7
|
+
* is attached, so an entry deposited by an untraced earlier turn would
|
|
8
|
+
* silently black out the reach detail of a later traced one; and the trace's
|
|
9
|
+
* reach payload is serialised by ITERATING this map, which must therefore
|
|
10
|
+
* hold what THIS climb consulted, not the whole conversation's history.
|
|
11
|
+
* Consistent with AGENTS §2.11: a traced response is a different machine —
|
|
12
|
+
* never benchmark with a trace attached. */
|
|
13
|
+
export declare function sharedReachMemo(ctx: MindContext): Map<number, AncestorReach>;
|
|
3
14
|
/** Climb the structural DAG from a node to its edge-bearing ancestor contexts.
|
|
4
15
|
* Ascent stops at hub nodes (parents > √N) — their reach is non-discriminative.
|
|
5
16
|
* When the start node has no structural parents, climbs from containment parents
|
|
@@ -8,6 +8,49 @@
|
|
|
8
8
|
import { cosine } from "../vec.js";
|
|
9
9
|
import { gistOf, read } from "./primitives.js";
|
|
10
10
|
const structCaches = new WeakMap();
|
|
11
|
+
// ── The shared ancestor-reach memo ──────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// `edgeAncestors` is a pure function of (node, N) over a read-only store —
|
|
14
|
+
// asking never writes — so its result is reusable for as long as the store
|
|
15
|
+
// is not written to. There used to be TWO memos and they never met: the
|
|
16
|
+
// climb built a private one per call (computeAttention), while
|
|
17
|
+
// `Precomputed.reachMemo` — documented as "one response-scoped memo serves
|
|
18
|
+
// every mechanism that prices commonality" — was reached only by confluence.
|
|
19
|
+
// The climb is by far the biggest consumer.
|
|
20
|
+
//
|
|
21
|
+
// Keyed off `ctx.climbMemo`'s OBJECT IDENTITY, exactly like the struct cache
|
|
22
|
+
// above, which buys the right lifetime for free: a plain respond() has a
|
|
23
|
+
// fresh climbMemo, so the memo is response-scoped; a conversation turn has
|
|
24
|
+
// the conversation's persistent one, so it is conversation-scoped. That
|
|
25
|
+
// matters — the stable-prefix fold makes each turn's subtree independent of
|
|
26
|
+
// what follows, so 59–70% of a later turn's climb regions are byte-identical
|
|
27
|
+
// repeats of an earlier turn's (measured on a 4-turn session), and every one
|
|
28
|
+
// of them used to re-climb from cold.
|
|
29
|
+
//
|
|
30
|
+
// Budgeted, not unbounded (AGENTS §2.12): past the cap the whole map is
|
|
31
|
+
// dropped and re-derived, costing a cold climb and never a wrong answer.
|
|
32
|
+
const REACH_MEMO_MAX = 100_000;
|
|
33
|
+
const reachCaches = new WeakMap();
|
|
34
|
+
/** The reach memo this response should use — see the note above.
|
|
35
|
+
*
|
|
36
|
+
* A TRACED response always gets a fresh, empty one. `AncestorReach`'s
|
|
37
|
+
* `visited`/`maxDepth`/`saturation` fields are populated only when a trace
|
|
38
|
+
* is attached, so an entry deposited by an untraced earlier turn would
|
|
39
|
+
* silently black out the reach detail of a later traced one; and the trace's
|
|
40
|
+
* reach payload is serialised by ITERATING this map, which must therefore
|
|
41
|
+
* hold what THIS climb consulted, not the whole conversation's history.
|
|
42
|
+
* Consistent with AGENTS §2.11: a traced response is a different machine —
|
|
43
|
+
* never benchmark with a trace attached. */
|
|
44
|
+
export function sharedReachMemo(ctx) {
|
|
45
|
+
if (ctx.trace !== null || ctx.climbMemo === null)
|
|
46
|
+
return new Map();
|
|
47
|
+
let m = reachCaches.get(ctx.climbMemo);
|
|
48
|
+
if (m === undefined)
|
|
49
|
+
reachCaches.set(ctx.climbMemo, m = new Map());
|
|
50
|
+
else if (m.size >= REACH_MEMO_MAX)
|
|
51
|
+
m.clear();
|
|
52
|
+
return m;
|
|
53
|
+
}
|
|
11
54
|
function getStructCache(ctx) {
|
|
12
55
|
if (ctx.climbMemo === null)
|
|
13
56
|
return null;
|
|
@@ -89,7 +132,7 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
89
132
|
// is withdrawn. On a small store the floor stays ≤ √N and the atom
|
|
90
133
|
// climbs exactly as before, so single-letter facts keep working.
|
|
91
134
|
if (id < 0 && atomIsHub(ctx, contextCount)) {
|
|
92
|
-
const bound0 =
|
|
135
|
+
const bound0 = boundFor(contextCount);
|
|
93
136
|
const reach = {
|
|
94
137
|
roots: [],
|
|
95
138
|
contextsReached: 0,
|
|
@@ -102,13 +145,15 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
102
145
|
observed: atomReach(ctx, contextCount),
|
|
103
146
|
limit: bound0,
|
|
104
147
|
},
|
|
148
|
+
visited: 0,
|
|
149
|
+
maxDepth: 0,
|
|
105
150
|
}
|
|
106
151
|
: {}),
|
|
107
152
|
};
|
|
108
153
|
memo?.set(id, reach);
|
|
109
154
|
return reach;
|
|
110
155
|
}
|
|
111
|
-
const bound =
|
|
156
|
+
const bound = boundFor(contextCount);
|
|
112
157
|
const roots = [];
|
|
113
158
|
const seen = new Set([id]);
|
|
114
159
|
const ctxSeen = new Set();
|
|
@@ -151,7 +196,22 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
151
196
|
// ~20K distinct nodes per climb family, >95% unique — not memoisable)
|
|
152
197
|
// while the context account never decided.
|
|
153
198
|
let lateral = 0;
|
|
199
|
+
// CLIMB READ-OUT (pure instrumentation, same contract as satStop): the
|
|
200
|
+
// parallel `depths` stack mirrors every push/pop of `stack`, so a node's
|
|
201
|
+
// ascent distance is known at its pop. Allocated only when a trace is
|
|
202
|
+
// requested; the climb itself never reads any of these back.
|
|
203
|
+
const depths = ctx.trace ? [] : null;
|
|
204
|
+
let curDepth = 0;
|
|
205
|
+
let visitedCount = 0;
|
|
206
|
+
let maxDepth = 0;
|
|
154
207
|
const visit = (x) => {
|
|
208
|
+
if (ctx.meter)
|
|
209
|
+
ctx.meter.ancestorVisits++;
|
|
210
|
+
if (depths) {
|
|
211
|
+
visitedCount++;
|
|
212
|
+
if (curDepth > maxDepth)
|
|
213
|
+
maxDepth = curDepth;
|
|
214
|
+
}
|
|
155
215
|
const hasNx = cachedHasNext(ctx, x, structCache);
|
|
156
216
|
const pc = cachedPrevCount(ctx, x, structCache);
|
|
157
217
|
if (hasNx || pc > 0) {
|
|
@@ -203,6 +263,7 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
203
263
|
if (!seen.has(p)) {
|
|
204
264
|
seen.add(p);
|
|
205
265
|
stack.push(p);
|
|
266
|
+
depths?.push(curDepth + 1);
|
|
206
267
|
fresh++;
|
|
207
268
|
}
|
|
208
269
|
}
|
|
@@ -225,8 +286,10 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
225
286
|
};
|
|
226
287
|
const stack = [];
|
|
227
288
|
const containment = !cachedHasParents(ctx, id, structCache);
|
|
228
|
-
if (!containment)
|
|
289
|
+
if (!containment) {
|
|
229
290
|
stack.push(id);
|
|
291
|
+
depths?.push(0);
|
|
292
|
+
}
|
|
230
293
|
// The containment seed is STREAMED in pages of √N: a distinctive window's
|
|
231
294
|
// containers (which converge on one or two contexts, however many chunks
|
|
232
295
|
// of one deposit repeat it) are walked IN FULL — exact — while a common
|
|
@@ -247,17 +310,22 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
247
310
|
if (!seen.has(c)) {
|
|
248
311
|
seen.add(c);
|
|
249
312
|
stack.push(c);
|
|
313
|
+
depths?.push(1);
|
|
250
314
|
}
|
|
251
315
|
}
|
|
252
316
|
if (stack.length === 0) {
|
|
253
|
-
if (containerOff === 0)
|
|
317
|
+
if (containerOff === 0) {
|
|
254
318
|
stack.push(id); // no containers at all
|
|
319
|
+
depths?.push(0);
|
|
320
|
+
}
|
|
255
321
|
else
|
|
256
322
|
break;
|
|
257
323
|
}
|
|
258
324
|
}
|
|
259
325
|
while (stack.length > 0) {
|
|
260
326
|
let x = stack.pop();
|
|
327
|
+
if (depths)
|
|
328
|
+
curDepth = depths.pop();
|
|
261
329
|
// TRANSPARENT-CHAIN HOP: a node with no edges in or out and exactly one
|
|
262
330
|
// parent contributes nothing here — no root, no context, no lateral
|
|
263
331
|
// entry — so the run to its first non-transparent ancestor is skipped
|
|
@@ -277,6 +345,10 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
277
345
|
if (dup)
|
|
278
346
|
continue;
|
|
279
347
|
x = top;
|
|
348
|
+
// The chain's interior hops are part of the terminal's ascent
|
|
349
|
+
// distance — count them exactly as a node-at-a-time ascent would.
|
|
350
|
+
if (depths)
|
|
351
|
+
curDepth += run.length - 1;
|
|
280
352
|
}
|
|
281
353
|
if (!visit(x)) {
|
|
282
354
|
saturated = true;
|
|
@@ -289,6 +361,7 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
289
361
|
contextsReached: ctxSeen.size,
|
|
290
362
|
saturated,
|
|
291
363
|
...(saturated && satStop ? { saturation: satStop } : {}),
|
|
364
|
+
...(depths ? { visited: visitedCount, maxDepth } : {}),
|
|
292
365
|
};
|
|
293
366
|
memo?.set(id, reach);
|
|
294
367
|
return reach;
|
|
@@ -316,8 +389,7 @@ export function atomReach(ctx, contextCount) {
|
|
|
316
389
|
* atom votes and is recognised exactly as any stored form; above it the
|
|
317
390
|
* alphabet is scaffolding everywhere and abstains. */
|
|
318
391
|
export function atomIsHub(ctx, contextCount) {
|
|
319
|
-
return atomReach(ctx, contextCount) >
|
|
320
|
-
Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
392
|
+
return atomReach(ctx, contextCount) > boundFor(contextCount);
|
|
321
393
|
}
|
|
322
394
|
/** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
|
|
323
395
|
* The admission predicate recognition filters sites with (HOW_IT_WORKS
|
|
@@ -358,7 +430,16 @@ export function corpusN(ctx) {
|
|
|
358
430
|
* materialised list. {@link hubCap} is the list-side reading of the same
|
|
359
431
|
* convention. */
|
|
360
432
|
export function hubBound(ctx) {
|
|
361
|
-
return
|
|
433
|
+
return boundFor(corpusN(ctx));
|
|
434
|
+
}
|
|
435
|
+
/** √N for an EXPLICIT context count — the ctx-free reading of {@link
|
|
436
|
+
* hubBound}, for the callers inside this module that are handed a count
|
|
437
|
+
* rather than a context ({@link edgeAncestors}, {@link atomIsHub}). The
|
|
438
|
+
* floor at 2 matches {@link corpusN}'s, so both readings agree for every
|
|
439
|
+
* input: the two used to be spelled out inline, once WITH the floor and
|
|
440
|
+
* once without, in the same function. */
|
|
441
|
+
function boundFor(contextCount) {
|
|
442
|
+
return Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
362
443
|
}
|
|
363
444
|
/** Cap a candidate list at the hub bound √N (insertion order) — the ONE
|
|
364
445
|
* fan-out convention every walk and disambiguation uses (see HOW_IT_WORKS
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -4,17 +4,16 @@ import type { BoundedMap, Store } from "../store.js";
|
|
|
4
4
|
import type { Space } from "../sema.js";
|
|
5
5
|
import type { Alphabet } from "../alphabet.js";
|
|
6
6
|
import type { MindConfig } from "../config.js";
|
|
7
|
+
import type { Meter } from "../meter.js";
|
|
7
8
|
import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
|
|
8
9
|
import type { Rationale } from "./rationale.js";
|
|
9
|
-
import type {
|
|
10
|
+
import type { Grid, StableFold } from "../geometry.js";
|
|
10
11
|
/** One {@link MindContext._depositTrees} entry — see that field's doc. */
|
|
11
12
|
export interface DepositCacheEntry {
|
|
12
13
|
/** Turn boundaries accumulated over this content's deposit chain —
|
|
13
14
|
* strictly increasing proper offsets, each a previously-deposited
|
|
14
15
|
* whole-context length. Empty for a first-seen (single-turn) input. */
|
|
15
16
|
boundaries: number[];
|
|
16
|
-
/** Plain-fold pyramid (first-seen inputs only). */
|
|
17
|
-
pyramid?: FoldPyramid;
|
|
18
17
|
/** Stable-prefix segment folds (grown-context inputs only). */
|
|
19
18
|
stable?: StableFold;
|
|
20
19
|
/** The continuation bytes this ctxInput was paired with in ingestPair, if
|
|
@@ -29,6 +28,10 @@ export type Input = string | Uint8Array | Grid | Grid[];
|
|
|
29
28
|
/** The host capabilities GraphSearch consults during a cover. MindContext
|
|
30
29
|
* extends this so the Mind can pass itself as the host. */
|
|
31
30
|
export interface GraphSearchHost {
|
|
31
|
+
/** Work accumulator, or null/absent when nothing is profiling — see
|
|
32
|
+
* src/meter.ts. Declared here (not only on MindContext) so the graph
|
|
33
|
+
* search can report its chart effort without importing mind code. */
|
|
34
|
+
readonly meter?: Meter | null;
|
|
32
35
|
resolve(bytes: Uint8Array): number | null;
|
|
33
36
|
recogniseSpan?(bytes: Uint8Array): {
|
|
34
37
|
sites: ReadonlyArray<Site>;
|
|
@@ -132,6 +135,19 @@ export interface RegionVote {
|
|
|
132
135
|
* to "one region" just because it collapsed to one pooled axiom.
|
|
133
136
|
* Defaults to 1 when absent. */
|
|
134
137
|
absorbed?: number;
|
|
138
|
+
/** The SEPARATE query places this vote's evidence occupies, when that is
|
|
139
|
+
* more than the one contiguous run [start, end]. A cross-region junction
|
|
140
|
+
* vote is pooled as a single synthetic region spanning its endpoints and
|
|
141
|
+
* the gap between them, so `[start, end]` reads as ONE place — yet the
|
|
142
|
+
* vote exists precisely because two non-adjacent regions each voted and
|
|
143
|
+
* only their conjunction resolved. Cluster counting (Attention.clusters)
|
|
144
|
+
* asks "how many separate places in the query corroborate this?", and
|
|
145
|
+
* answering it from the merged span makes every joint binding look like a
|
|
146
|
+
* single local neighbourhood; fusion's dispersion gate then drops it
|
|
147
|
+
* unless it also explains most of the whole query, which a binding inside
|
|
148
|
+
* a MULTI-topic query structurally cannot. Absent for an ordinary
|
|
149
|
+
* per-region vote, where the merged span already is the truth. */
|
|
150
|
+
parts?: readonly (readonly [number, number])[];
|
|
135
151
|
}
|
|
136
152
|
/** The structural gate that first decided an {@link edgeAncestors} climb was
|
|
137
153
|
* saturated (an abstention, not a discriminative conclusion) — pure
|
|
@@ -156,6 +172,17 @@ export interface AncestorReach {
|
|
|
156
172
|
* a non-saturated reach, and absent (even when saturated) when no trace
|
|
157
173
|
* was requested — instrumentation must not allocate when tracing is off. */
|
|
158
174
|
saturation?: SaturationStop;
|
|
175
|
+
/** The number of nodes the climb actually PROCESSED (popped and examined
|
|
176
|
+
* by its visit step; a transparent chain counts as its one terminal).
|
|
177
|
+
* Present only when a trace was requested — same contract as
|
|
178
|
+
* {@link saturation}: instrumentation must not allocate when tracing is
|
|
179
|
+
* off. Purely a read-out; the climb never consults it. */
|
|
180
|
+
visited?: number;
|
|
181
|
+
/** The maximum structural ascent distance (in parent/containment hops,
|
|
182
|
+
* transparent-chain interiors counted) from the start node among the
|
|
183
|
+
* processed nodes. Present only when a trace was requested — see
|
|
184
|
+
* {@link visited}. */
|
|
185
|
+
maxDepth?: number;
|
|
159
186
|
}
|
|
160
187
|
/** Saturated-interval information for the noise-drop gate. */
|
|
161
188
|
export interface SaturationInfo {
|
|
@@ -179,6 +206,12 @@ export type AItem = {
|
|
|
179
206
|
};
|
|
180
207
|
export interface MindContext extends GraphSearchHost {
|
|
181
208
|
store: Store;
|
|
209
|
+
/** The work accumulator for the inference call in flight, or null when
|
|
210
|
+
* nothing is profiling — see src/meter.ts. WRITE-ONLY from the engine's
|
|
211
|
+
* point of view: no inference decision may read a counter, or the
|
|
212
|
+
* determinism contract (AGENTS §2.1) is gone. Every call site is
|
|
213
|
+
* `ctx.meter?.x++`, so an unprofiled response allocates nothing. */
|
|
214
|
+
meter: Meter | null;
|
|
182
215
|
space: Space;
|
|
183
216
|
alphabet: Alphabet;
|
|
184
217
|
cfg: MindConfig;
|
|
@@ -239,8 +272,9 @@ export interface MindContext extends GraphSearchHost {
|
|
|
239
272
|
* folds with the SAME stable-prefix fold query-time perception uses
|
|
240
273
|
* (structural train/inference agreement, load-bearing for recall),
|
|
241
274
|
* reusing every already-folded segment via `stable` (see StableFold) —
|
|
242
|
-
* O(turn) per deposit instead of O(context). A first-seen input
|
|
243
|
-
*
|
|
275
|
+
* O(turn) per deposit instead of O(context). A first-seen input takes the
|
|
276
|
+
* same fold with no boundaries at all, and caches the segments it produced
|
|
277
|
+
* so a later turn of the same conversation reuses them. Purely a
|
|
244
278
|
* performance cache for the FOLD STATE; the boundaries are semantic but
|
|
245
279
|
* derived only from the deposit sequence itself (an evicted chain falls
|
|
246
280
|
* back to plain-fold behavior, exactly the pre-boundary shape). */
|
package/dist/src/store.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Vec } from "./vec.js";
|
|
2
2
|
import { type StoreConfig } from "./config.js";
|
|
3
|
+
import type { Meter } from "./meter.js";
|
|
3
4
|
/** A node id: a dense, non-negative integer assigned in creation order. */
|
|
4
5
|
export type NodeId = number;
|
|
5
6
|
/** A node in the graph. Exactly one of `leaf` / `kids` is set. */
|
|
@@ -62,6 +63,13 @@ export declare class BoundedMap<K, V> {
|
|
|
62
63
|
}
|
|
63
64
|
export interface Store {
|
|
64
65
|
readonly D: number;
|
|
66
|
+
/** The work accumulator for the inference call in flight, or null. The
|
|
67
|
+
* Mind attaches one per profiled response and detaches it after (see
|
|
68
|
+
* src/meter.ts). A store MUST only ever write to it — no read may reach
|
|
69
|
+
* a decision, or determinism is gone. An adapter that adds reads of its
|
|
70
|
+
* own should bump the matching counter; one that does not simply reports
|
|
71
|
+
* less, never wrongly. */
|
|
72
|
+
meter: Meter | null;
|
|
65
73
|
/** Insert a leaf, returning its content id. Idempotent. */
|
|
66
74
|
putLeaf(bytes: Uint8Array, gist: Vec): Promise<NodeId>;
|
|
67
75
|
/** Insert a branch over child ids, returning its content id. Idempotent. */
|
|
@@ -508,6 +516,10 @@ export declare abstract class AbstractStore implements Store {
|
|
|
508
516
|
* outlive their records — the read degrades safely to empty bytes, this
|
|
509
517
|
* counter is what keeps that degradation observable. */
|
|
510
518
|
danglingReads: number;
|
|
519
|
+
/** {@link Store.meter} — the per-response work accumulator, or null when
|
|
520
|
+
* nothing is profiling. Every read below bumps it through `?.`, so an
|
|
521
|
+
* unprofiled store pays one null check per read and allocates nothing. */
|
|
522
|
+
meter: Meter | null;
|
|
511
523
|
bytes(id: NodeId): Uint8Array;
|
|
512
524
|
/** First `maxLen` bytes of a node. Walks only the leftmost branch,
|
|
513
525
|
* stopping at `maxLen` — so a 1 MB document root costs the same as a
|
|
@@ -517,6 +529,9 @@ export declare abstract class AbstractStore implements Store {
|
|
|
517
529
|
* may be shared with the byte cache and with other callers — treat them
|
|
518
530
|
* as read-only. Mutating one would corrupt every subsequent read. */
|
|
519
531
|
bytesPrefix(id: NodeId, maxLen: number): Uint8Array;
|
|
532
|
+
/** {@link bytesPrefix}'s recursive body — uncharged; see the metering
|
|
533
|
+
* boundary note there. */
|
|
534
|
+
private _prefix;
|
|
520
535
|
contentLen(id: NodeId, cap?: number): number;
|
|
521
536
|
findLeaf(bytes: Uint8Array): NodeId | null;
|
|
522
537
|
findBranch(kids: NodeId[]): NodeId | null;
|