@hviana/sema 0.3.0 → 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.
Files changed (77) hide show
  1. package/AGENTS.md +71 -5
  2. package/dist/src/derive/src/deduction.d.ts +12 -1
  3. package/dist/src/derive/src/deduction.js +5 -1
  4. package/dist/src/derive/src/index.d.ts +1 -0
  5. package/dist/src/geometry.d.ts +3 -30
  6. package/dist/src/geometry.js +330 -82
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/meter.d.ts +171 -0
  10. package/dist/src/meter.js +269 -0
  11. package/dist/src/mind/attention.d.ts +0 -4
  12. package/dist/src/mind/attention.js +251 -23
  13. package/dist/src/mind/bridge.d.ts +10 -1
  14. package/dist/src/mind/bridge.js +179 -10
  15. package/dist/src/mind/canonical.d.ts +6 -1
  16. package/dist/src/mind/canonical.js +6 -1
  17. package/dist/src/mind/graph-search.d.ts +9 -0
  18. package/dist/src/mind/graph-search.js +59 -19
  19. package/dist/src/mind/junction.d.ts +10 -0
  20. package/dist/src/mind/junction.js +14 -0
  21. package/dist/src/mind/match.d.ts +40 -0
  22. package/dist/src/mind/match.js +125 -1
  23. package/dist/src/mind/mechanisms/cast.js +63 -6
  24. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  25. package/dist/src/mind/mechanisms/extraction.js +1 -88
  26. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  27. package/dist/src/mind/mechanisms/recall.js +77 -14
  28. package/dist/src/mind/mind.d.ts +55 -4
  29. package/dist/src/mind/mind.js +110 -91
  30. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  31. package/dist/src/mind/pipeline-mechanism.js +179 -10
  32. package/dist/src/mind/pipeline.js +52 -10
  33. package/dist/src/mind/primitives.d.ts +11 -15
  34. package/dist/src/mind/primitives.js +47 -28
  35. package/dist/src/mind/reasoning.d.ts +7 -1
  36. package/dist/src/mind/reasoning.js +40 -8
  37. package/dist/src/mind/recognition.js +93 -20
  38. package/dist/src/mind/traverse.d.ts +11 -0
  39. package/dist/src/mind/traverse.js +58 -5
  40. package/dist/src/mind/types.d.ts +28 -5
  41. package/dist/src/store.d.ts +15 -0
  42. package/dist/src/store.js +91 -6
  43. package/package.json +1 -1
  44. package/src/derive/src/deduction.ts +15 -0
  45. package/src/derive/src/index.ts +1 -0
  46. package/src/geometry.ts +350 -122
  47. package/src/index.ts +1 -0
  48. package/src/meter.ts +333 -0
  49. package/src/mind/attention.ts +268 -31
  50. package/src/mind/bridge.ts +187 -10
  51. package/src/mind/canonical.ts +6 -1
  52. package/src/mind/graph-search.ts +60 -21
  53. package/src/mind/junction.ts +12 -0
  54. package/src/mind/match.ts +146 -1
  55. package/src/mind/mechanisms/cast.ts +62 -6
  56. package/src/mind/mechanisms/extraction.ts +2 -103
  57. package/src/mind/mechanisms/recall.ts +84 -17
  58. package/src/mind/mind.ts +139 -98
  59. package/src/mind/pipeline-mechanism.ts +203 -13
  60. package/src/mind/pipeline.ts +65 -8
  61. package/src/mind/primitives.ts +49 -33
  62. package/src/mind/reasoning.ts +39 -7
  63. package/src/mind/recognition.ts +89 -19
  64. package/src/mind/traverse.ts +59 -5
  65. package/src/mind/types.ts +28 -5
  66. package/src/store.ts +75 -6
  67. package/test/14-scaling.test.mjs +17 -7
  68. package/test/31-audit.test.mjs +4 -1
  69. package/test/33-multi-candidate.test.mjs +13 -1
  70. package/test/36-already-answered-fusion.test.mjs +10 -3
  71. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  72. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  73. package/test/55-cost-meter.test.mjs +284 -0
  74. package/test/56-bridge-identity-admission.test.mjs +209 -0
  75. package/test/57-fusion-order.test.mjs +104 -0
  76. package/test/58-subquantum-sites.test.mjs +112 -0
  77. package/test/59-fold-invariance.test.mjs +226 -0
@@ -25,11 +25,16 @@ import { recallMechanism } from "./mechanisms/recall.js";
25
25
  export { resolveConcepts, resolveConnectors } from "./mechanisms/cover.js";
26
26
  export { aluToMechanism } from "./mechanisms/alu.js";
27
27
  // ── Extension dispatch (pre-loop parse) ─────────────────────────────────────
28
- async function collectComputed(mechanisms, query) {
28
+ async function collectComputed(ctx, mechanisms, query) {
29
29
  const out = [];
30
+ const meter = ctx.meter;
30
31
  for (const m of mechanisms) {
31
- if (m.parse)
32
- out.push(...await m.parse(query));
32
+ if (!m.parse)
33
+ continue;
34
+ const spans = meter
35
+ ? await meter.time(`${m.name}.parse`, () => m.parse(query))
36
+ : await m.parse(query);
37
+ out.push(...spans);
33
38
  }
34
39
  return out;
35
40
  }
@@ -76,7 +81,7 @@ export async function think(ctx, query, mechs) {
76
81
  const mechanisms = mechs ?? defaultMechanisms;
77
82
  const rec = recognise(ctx, query);
78
83
  // Phase 1: collect computed spans from mechanisms that implement parse()
79
- const computed = await collectComputed(mechanisms, query);
84
+ const computed = await collectComputed(ctx, mechanisms, query);
80
85
  if (computed.length > 0) {
81
86
  ctx.trace?.step("computeExtensions", [rItem(query, "query")], computed.map((u) => rItem(query.subarray(u.i, u.j), "operand", undefined, [u.i, u.j])), `extensions recognised and evaluated ${computed.length} computation(s)`);
82
87
  for (const u of computed) {
@@ -99,14 +104,28 @@ export async function think(ctx, query, mechs) {
99
104
  const consider = (c) => {
100
105
  if (c.bytes.length === 0)
101
106
  return;
107
+ if (ctx.meter)
108
+ ctx.meter.candidates++;
102
109
  candidates.push(c);
103
110
  if (best === null || grade(c.weight) < grade(best.weight))
104
111
  best = c;
105
112
  };
106
113
  const worthRunning = (floor) => best === null || grade(floor) < grade(best.weight);
107
114
  // Phase 3: grounding loop
115
+ // Per-mechanism accounting (src/meter.ts). The market's whole premise is
116
+ // that mechanisms compete on one cost scale — so the profiling read-out is
117
+ // also per-mechanism, uniformly: the loop never asks which one it holds.
118
+ const meter = ctx.meter;
108
119
  for (const mech of mechanisms) {
109
- const floor = await mech.floor(ctx, query, pre, worthRunning);
120
+ const floor = meter
121
+ ? await meter.time(`${mech.name}.floor`, () => mech.floor(ctx, query, pre, worthRunning))
122
+ : await mech.floor(ctx, query, pre, worthRunning);
123
+ if (meter) {
124
+ if (floor === null)
125
+ meter.mechanismSkips++;
126
+ else
127
+ meter.mechanismFloors++;
128
+ }
110
129
  if (floor === null) {
111
130
  ctx.trace?.step("skipMechanism", [], [], `${mech.name} skipped — structural precondition failed`);
112
131
  continue;
@@ -115,7 +134,11 @@ export async function think(ctx, query, mechs) {
115
134
  ctx.trace?.step("skipMechanism", [], [], `${mech.name} skipped — floor ${floor} cannot beat incumbent (grade ${grade(best.weight)})`);
116
135
  continue;
117
136
  }
118
- const results = await mech.run(ctx, query, pre);
137
+ if (meter)
138
+ meter.mechanismRuns++;
139
+ const results = meter
140
+ ? await meter.time(`${mech.name}.run`, () => mech.run(ctx, query, pre))
141
+ : await mech.run(ctx, query, pre);
119
142
  for (const r of results) {
120
143
  const weight = r.weight ?? weigh(r.accounted, r.moves);
121
144
  consider({
@@ -125,6 +148,7 @@ export async function think(ctx, query, mechs) {
125
148
  used: r.used,
126
149
  accounted: r.accounted,
127
150
  unexplained: r.unexplained,
151
+ complete: r.complete,
128
152
  });
129
153
  }
130
154
  }
@@ -192,7 +216,13 @@ export async function think(ctx, query, mechs) {
192
216
  : provenance === "recall" || provenance === "recall-echo"
193
217
  ? new Set()
194
218
  : new Set(recognise(ctx, answer).sites.map((s) => s.payload));
195
- const reasoned = await reason(ctx, query, answer, preConsumed, pre);
219
+ // A grounding that DECLARED itself complete is not extended: the answer is
220
+ // already a trained form's own continuation, reached through an identity
221
+ // claim about the query, so a multi-hop pivot could only chain past the
222
+ // fact that produced it (see MechanismResult.complete).
223
+ const reasoned = decided.complete ? answer : meter
224
+ ? await meter.time("reason", () => reason(ctx, query, answer, preConsumed, pre))
225
+ : await reason(ctx, query, answer, preConsumed, pre);
196
226
  // Fuse only when the query has a genuine REMAINDER no mechanism's
197
227
  // structural evidence touched at all. `decided.accounted` alone
198
228
  // undercounts this: it is a COST-LADDER quantity (cover.ts prices its
@@ -225,9 +255,21 @@ export async function think(ctx, query, mechs) {
225
255
  // rejected.
226
256
  const unclimbed = decided.accounted.length > 0 &&
227
257
  decided.accounted.every(([i, j]) => pre.computed.some((u) => u.i === i && u.j === j));
228
- const fused = remainder >= ctx.space.maxGroup
229
- ? await fuseAttention(ctx, query, reasoned, pre, unclimbed)
230
- : reasoned;
258
+ // Where the winning grounding stands in the query — fusion places primary
259
+ // by it (see fuseAttention's `primarySpans`). `accounted` is the
260
+ // cost-ladder read and is authoritative when non-empty; when it is empty
261
+ // the grounding is a pure COMPUTATION, whose evidence is its computed span.
262
+ // Exactly the cost-ladder-vs-coverage distinction `explained` above draws,
263
+ // read here for POSITION instead of for coverage — and resolved here, where
264
+ // both readings are in hand, rather than inside fuseAttention.
265
+ const primarySpans = decided.accounted.length > 0
266
+ ? decided.accounted
267
+ : pre.computed.map((u) => [u.i, u.j]);
268
+ const fused = remainder < ctx.space.maxGroup
269
+ ? reasoned
270
+ : meter
271
+ ? await meter.time("fuse", () => fuseAttention(ctx, query, reasoned, pre, unclimbed, primarySpans))
272
+ : await fuseAttention(ctx, query, reasoned, pre, unclimbed, decided.accounted);
231
273
  done(fused, "grounded, reasoned forward, fused across points of attention");
232
274
  return { bytes: fused, provenance };
233
275
  }
@@ -15,21 +15,17 @@ export declare function latin1Key(bytes: Uint8Array): string;
15
15
  * CALLER — who assembled the multi-turn context — knows where those
16
16
  * boundaries are; the geometry never guesses them from the bytes. */
17
17
  export declare function perceive(ctx: MindContext, input: Input, leafAt?: (i: number) => number | null, lookup?: (ids: number[]) => number | null, boundaries?: readonly number[]): Sema;
18
- /** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
19
- * (bit-identical to inference perception of a standalone query that
20
- * structural train/inference agreement is load-bearing for exact recall),
21
- * computed incrementally via the fold's level pyramid
22
- * ({@link bytesToTreePyramid}). An input that EXTENDS a previously
23
- * deposited one is a conversation context grown by one turn the cached
24
- * prefix length IS the turn boundary (derived from the deposit sequence
25
- * itself, never from content conventions) — and takes the STABLE-PREFIX
26
- * fold over the accumulated boundaries, bit-identical to the boundary
27
- * fold query-time conversation perception uses, so the trained context
28
- * node and the query's context subtree are the SAME node. Segment folds
29
- * reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
30
- * instead of O(context) per turn. The fold state is purely a cache; the
31
- * boundary accumulation is what an evicted chain loses (falling back to
32
- * the plain fold, the pre-boundary shape — a warm replay restores it). */
18
+ /** The DEPOSIT-shaped perceive. Folds over the stream's own content cuts —
19
+ * bit-identical to what inference computes for the same bytes, and that
20
+ * train/inference agreement is load-bearing for exact recall. An input that
21
+ * EXTENDS a previously deposited one is a conversation context grown by one
22
+ * turn; the cached prefix length IS the turn boundary (derived from the deposit
23
+ * sequence itself, never from a content convention) and joins the cut set, so
24
+ * the trained context node and the query's context subtree are the SAME node.
25
+ * Segment folds reuse across deposits ({@link stablePrefixFoldIncremental}) —
26
+ * O(turn) instead of O(context) per turn. All of it is purely a cache: an
27
+ * evicted chain loses only the turn boundaries, and since the content cuts do
28
+ * not depend on the cache, the segments themselves are unchanged. */
33
29
  export declare function perceiveDeposit(ctx: MindContext, bytes: Uint8Array, conversational?: boolean): Sema;
34
30
  /** The raw bytes of an input — modality-neutral conversion. */
35
31
  export declare function inputBytes(ctx: MindContext, input: Input): Uint8Array;
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Address — bytes → node (perceive, foldTree, resolve)
4
4
  // Read — node → bytes (read)
5
- import { bytesToTree, bytesToTreePyramid, gridToTree, hilbertBytes, stablePrefixFoldIncremental, stackGrids, } from "../geometry.js";
5
+ import { bytesToTree, gridToTree, hilbertBytes, stablePrefixFoldIncremental, stackGrids, } from "../geometry.js";
6
6
  import { canonHash } from "../canon.js";
7
7
  import { bytesEqual } from "../bytes.js";
8
8
  import { ALL } from "./types.js";
@@ -43,12 +43,23 @@ export function perceive(ctx, input, leafAt, lookup, boundaries) {
43
43
  if (memo) {
44
44
  const key = latin1Key(bytes);
45
45
  const hit = memo.get(key);
46
- if (hit !== undefined)
46
+ if (hit !== undefined) {
47
+ if (ctx.meter)
48
+ ctx.meter.perceiveHits++;
47
49
  return hit;
50
+ }
51
+ if (ctx.meter) {
52
+ ctx.meter.perceptions++;
53
+ ctx.meter.perceivedBytes += bytes.length;
54
+ }
48
55
  const tree = bytesToTree(ctx.space, ctx.alphabet, bytes, undefined, undefined, boundaries);
49
56
  memo.set(key, tree);
50
57
  return tree;
51
58
  }
59
+ if (ctx.meter) {
60
+ ctx.meter.perceptions++;
61
+ ctx.meter.perceivedBytes += bytes.length;
62
+ }
52
63
  return bytesToTree(ctx.space, ctx.alphabet, bytes, undefined, undefined, boundaries);
53
64
  }
54
65
  return bytesToTree(ctx.space, ctx.alphabet, bytes, leafAt, lookup);
@@ -58,21 +69,17 @@ export function perceive(ctx, input, leafAt, lookup, boundaries) {
58
69
  }
59
70
  return gridToTree(ctx.space, ctx.alphabet, input);
60
71
  }
61
- /** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
62
- * (bit-identical to inference perception of a standalone query that
63
- * structural train/inference agreement is load-bearing for exact recall),
64
- * computed incrementally via the fold's level pyramid
65
- * ({@link bytesToTreePyramid}). An input that EXTENDS a previously
66
- * deposited one is a conversation context grown by one turn the cached
67
- * prefix length IS the turn boundary (derived from the deposit sequence
68
- * itself, never from content conventions) — and takes the STABLE-PREFIX
69
- * fold over the accumulated boundaries, bit-identical to the boundary
70
- * fold query-time conversation perception uses, so the trained context
71
- * node and the query's context subtree are the SAME node. Segment folds
72
- * reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
73
- * instead of O(context) per turn. The fold state is purely a cache; the
74
- * boundary accumulation is what an evicted chain loses (falling back to
75
- * the plain fold, the pre-boundary shape — a warm replay restores it). */
72
+ /** The DEPOSIT-shaped perceive. Folds over the stream's own content cuts —
73
+ * bit-identical to what inference computes for the same bytes, and that
74
+ * train/inference agreement is load-bearing for exact recall. An input that
75
+ * EXTENDS a previously deposited one is a conversation context grown by one
76
+ * turn; the cached prefix length IS the turn boundary (derived from the deposit
77
+ * sequence itself, never from a content convention) and joins the cut set, so
78
+ * the trained context node and the query's context subtree are the SAME node.
79
+ * Segment folds reuse across deposits ({@link stablePrefixFoldIncremental}) —
80
+ * O(turn) instead of O(context) per turn. All of it is purely a cache: an
81
+ * evicted chain loses only the turn boundaries, and since the content cuts do
82
+ * not depend on the cache, the segments themselves are unchanged. */
76
83
  export function perceiveDeposit(ctx, bytes, conversational = false) {
77
84
  let prev;
78
85
  let prefixLen = 0;
@@ -99,19 +106,27 @@ export function perceiveDeposit(ctx, bytes, conversational = false) {
99
106
  }
100
107
  }
101
108
  }
102
- let tree;
103
- let entry;
109
+ // ONLY turn boundaries belong here. The stream's own content cuts are NOT
110
+ // passed in: `bytesToTree` and `stablePrefixFoldIncremental` both derive them
111
+ // per span, at every level, and handing the level-0 cuts in as stable-prefix
112
+ // boundaries instead produces a LEFT-NESTED join of flat segments — a
113
+ // different tree from the one inference builds for the same bytes. When that
114
+ // happened, a deposit's context root and `resolve(question)` were different
115
+ // nodes, so the trained edge hung off a node inference never reached and
116
+ // recall went silent (test/44 caught it as a site that could not be emitted
117
+ // because the resolved node led nowhere). Train and infer must fold
118
+ // identically; the way to guarantee that is to give this function nothing
119
+ // extra to say.
120
+ const cuts = new Set();
104
121
  if (prev !== undefined) {
105
- const boundaries = [...prev.boundaries, prefixLen];
106
- const folded = stablePrefixFoldIncremental(ctx.space, ctx.alphabet, bytes, boundaries, prev.stable);
107
- tree = folded.tree;
108
- entry = { boundaries, stable: folded.fold };
109
- }
110
- else {
111
- const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
112
- tree = plain.tree;
113
- entry = { boundaries: [], pyramid: plain.pyramid };
122
+ for (const b of prev.boundaries)
123
+ cuts.add(b);
124
+ cuts.add(prefixLen);
114
125
  }
126
+ const boundaries = [...cuts].sort((a, b) => a - b);
127
+ const folded = stablePrefixFoldIncremental(ctx.space, ctx.alphabet, bytes, boundaries, prev?.stable);
128
+ const tree = folded.tree;
129
+ const entry = { boundaries, stable: folded.fold };
115
130
  // Only a conversational deposit writes the cache too — otherwise a bare
116
131
  // fact's plain fold could later be misread as a conversation's turn-zero
117
132
  // boundary by an unrelated conversational deposit that happens to extend
@@ -193,6 +208,8 @@ export function foldTree(ctx, n, start, visit) {
193
208
  export function resolve(ctx, bytes) {
194
209
  if (bytes.length === 0)
195
210
  return null;
211
+ if (ctx.meter)
212
+ ctx.meter.resolves++;
196
213
  const exact = foldTree(ctx, perceive(ctx, bytes), 0).node;
197
214
  if (exact !== null)
198
215
  return exact;
@@ -235,6 +252,8 @@ export function canonResolve(ctx, bytes) {
235
252
  if (direct !== null)
236
253
  return set(direct);
237
254
  }
255
+ if (ctx.meter)
256
+ ctx.meter.canonLookups++;
238
257
  const candidates = store.canonFind(canonHash(key));
239
258
  if (candidates.length === 0)
240
259
  return set(null);
@@ -31,4 +31,10 @@ export declare function fuseAttention(ctx: MindContext, query: Uint8Array, prima
31
31
  * primary's own source; that assumption is exactly backwards when
32
32
  * primary is unclimbed. Absent or false preserves the original
33
33
  * behaviour exactly. */
34
- unclimbed?: boolean): Promise<Uint8Array>;
34
+ unclimbed?: boolean,
35
+ /** The query spans `primary`'s own grounding stands on — used ONLY to place
36
+ * primary in the fused reading order (see below). Resolved by the caller,
37
+ * which is the layer that knows how a given grounding records its evidence;
38
+ * fuseAttention just reads a position from it. Empty or absent preserves
39
+ * the original behaviour exactly. */
40
+ primarySpans?: ReadonlyArray<readonly [number, number]>): Promise<Uint8Array>;
@@ -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 { corpusN } from "./traverse.js";
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 hubBound = Math.ceil(Math.sqrt(corpusN(ctx)));
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, hubBound))
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, hubBound))
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, hubBound).some((n) => !consumed.has(n))) {
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: forest[0].start, bytes: primary },
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 "./mechanisms/extraction.js";
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 (stable-prefix folded independently see
194
- // mind.ts's _growContext), so it can be turn/segment-scale, not
195
- // chunk-scale. Widening the size bound itself reopens the root-
196
- // scale false-positive this module already fixed once (test/46);
197
- // widening the SEARCH instead trying every position up to W
198
- // chunk-widths from the left edge that the query's OWN fold treats
199
- // as a chunk boundary (`starts`, the same set the canonical pass
200
- // privileges with full chain reach) does not: `starts.has(p)` is
201
- // fold EVIDENCE the query produced on its own (a leaf-parent chunk
202
- // like "And " is visited, and its start added to `starts`, before
203
- // this composite ever runs foldTree is post-order), never a blind
204
- // guess. Bounded to W candidates, each an O(1) set lookup before
205
- // paying for the real canonResolve fold canonResolve, not
206
- // resolve()/findBranch, because the gap here is often exactly the
207
- // kind of equivalence (case, in the live trace) canon exists for,
208
- // not just an exact-content coincidence.
209
- for (let k = 1; k <= W; k++) {
210
- const p = start + k * W;
211
- if (p >= end - 1 || !starts.has(p))
212
- continue;
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