@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.
Files changed (84) 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 +5 -4
  12. package/dist/src/mind/attention.js +254 -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/index.d.ts +2 -0
  20. package/dist/src/mind/junction.d.ts +10 -0
  21. package/dist/src/mind/junction.js +14 -0
  22. package/dist/src/mind/learning.d.ts +32 -4
  23. package/dist/src/mind/learning.js +26 -4
  24. package/dist/src/mind/match.d.ts +40 -0
  25. package/dist/src/mind/match.js +125 -1
  26. package/dist/src/mind/mechanisms/cast.js +63 -6
  27. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  28. package/dist/src/mind/mechanisms/extraction.js +1 -88
  29. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  30. package/dist/src/mind/mechanisms/recall.js +77 -14
  31. package/dist/src/mind/mind.d.ts +59 -5
  32. package/dist/src/mind/mind.js +115 -93
  33. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  34. package/dist/src/mind/pipeline-mechanism.js +179 -10
  35. package/dist/src/mind/pipeline.d.ts +29 -0
  36. package/dist/src/mind/pipeline.js +79 -21
  37. package/dist/src/mind/primitives.d.ts +11 -15
  38. package/dist/src/mind/primitives.js +47 -28
  39. package/dist/src/mind/reasoning.d.ts +7 -1
  40. package/dist/src/mind/reasoning.js +40 -8
  41. package/dist/src/mind/recognition.js +93 -20
  42. package/dist/src/mind/traverse.d.ts +11 -0
  43. package/dist/src/mind/traverse.js +88 -7
  44. package/dist/src/mind/types.d.ts +39 -5
  45. package/dist/src/store.d.ts +15 -0
  46. package/dist/src/store.js +91 -6
  47. package/package.json +1 -1
  48. package/src/derive/src/deduction.ts +15 -0
  49. package/src/derive/src/index.ts +1 -0
  50. package/src/geometry.ts +350 -122
  51. package/src/index.ts +1 -0
  52. package/src/meter.ts +333 -0
  53. package/src/mind/attention.ts +276 -31
  54. package/src/mind/bridge.ts +187 -10
  55. package/src/mind/canonical.ts +6 -1
  56. package/src/mind/graph-search.ts +60 -21
  57. package/src/mind/index.ts +6 -0
  58. package/src/mind/junction.ts +12 -0
  59. package/src/mind/learning.ts +46 -5
  60. package/src/mind/match.ts +146 -1
  61. package/src/mind/mechanisms/cast.ts +62 -6
  62. package/src/mind/mechanisms/extraction.ts +2 -103
  63. package/src/mind/mechanisms/recall.ts +84 -17
  64. package/src/mind/mind.ts +144 -99
  65. package/src/mind/pipeline-mechanism.ts +203 -13
  66. package/src/mind/pipeline.ts +144 -36
  67. package/src/mind/primitives.ts +49 -33
  68. package/src/mind/reasoning.ts +39 -7
  69. package/src/mind/recognition.ts +89 -19
  70. package/src/mind/traverse.ts +89 -8
  71. package/src/mind/types.ts +39 -5
  72. package/src/store.ts +75 -6
  73. package/test/14-scaling.test.mjs +17 -7
  74. package/test/31-audit.test.mjs +4 -1
  75. package/test/33-multi-candidate.test.mjs +13 -1
  76. package/test/36-already-answered-fusion.test.mjs +10 -3
  77. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  78. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  79. package/test/54-evidence-k-instrumentation.test.mjs +175 -0
  80. package/test/55-cost-meter.test.mjs +284 -0
  81. package/test/56-bridge-identity-admission.test.mjs +209 -0
  82. package/test/57-fusion-order.test.mjs +104 -0
  83. package/test/58-subquantum-sites.test.mjs +112 -0
  84. package/test/59-fold-invariance.test.mjs +226 -0
@@ -12,11 +12,13 @@
12
12
  // 3. VISIBLE BUDGET — every mechanism carries its own caps internally (√N, k).
13
13
  // 4. TRAVELING EVIDENCE — run() returns MechanismResult with accounted, moves,
14
14
  // and unexplained. The pipeline computes the weight.
15
+ import { indexOf } from "../bytes.js";
16
+ import { dominates } from "../geometry.js";
15
17
  import { windowIds } from "./canonical.js";
16
18
  import { read, resolve } from "./primitives.js";
17
- import { alignGraded } from "./match.js";
19
+ import { alignGraded, skillExemplar } from "./match.js";
18
20
  import { climbAttentionAll } from "./attention.js";
19
- import { skillExemplar } from "./mechanisms/extraction.js";
21
+ import { sharedReachMemo } from "./traverse.js";
20
22
  // ── Precomputed ──────────────────────────────────────────────────────────────
21
23
  //
22
24
  // Precomputed is a LAZY container for structural analyses of the query — the
@@ -88,29 +90,46 @@ export class Precomputed {
88
90
  return w;
89
91
  }
90
92
  /** Shared memo for {@link reachOf} (structural-IDF reads): a window's
91
- * ancestor reach is a pure function of the read-only store, so one
92
- * response-scoped memo serves every mechanism that prices commonality. */
93
- reachMemo = new Map();
93
+ * ancestor reach is a pure function of the read-only store, so one memo
94
+ * serves every mechanism that prices commonality — AND the consensus
95
+ * climb, which is the largest consumer and used to build its own. The
96
+ * ONE definition of its lifetime lives in traverse.ts
97
+ * ({@link sharedReachMemo}): response-scoped for respond(),
98
+ * conversation-scoped across turns, always cold under a trace. */
99
+ _reach;
100
+ get reachMemo() {
101
+ return this._reach ??= sharedReachMemo(this.ctx);
102
+ }
94
103
  // ── Expensive lazy analyses ───────────────────────────────────────────
95
104
  //
96
105
  // Async, cached-by-promise: the first caller starts the computation, every
97
106
  // later caller (any mechanism, any phase) awaits the same promise. A
98
107
  // mechanism MUST check its cheap floor gates and the pipeline's
99
108
  // `worthRunning` predicate before first-touching one of these.
109
+ /** Charge a lazily-shared analysis to its OWN phase rather than to the
110
+ * mechanism that happened to first-touch it. Without this the profile
111
+ * reads as "cast.floor costs 2 s" when what actually cost 2 s is the
112
+ * consensus climb — which cast merely paid for on everyone's behalf, and
113
+ * which every later consumer then got free. Attribution must follow the
114
+ * work, not the caller. */
115
+ shared(phase, fn) {
116
+ const meter = this.ctx.meter;
117
+ return meter ? meter.time(phase, fn) : fn();
118
+ }
100
119
  _attention;
101
120
  /** The full consensus climb (roots + ranked anchors) — the query-level
102
121
  * evidence CAST, confluence, extraction, recall's scaffolding tier, and
103
122
  * fusion all share. Computed on first access; a query no mechanism
104
123
  * climbs for (e.g. one an extension decided outright) never pays for it. */
105
124
  attention() {
106
- return this._attention ??= climbAttentionAll(this.ctx, this.query, this.k);
125
+ return this._attention ??= this.shared("attention", () => climbAttentionAll(this.ctx, this.query, this.k));
107
126
  }
108
127
  _weave;
109
128
  /** Result of {@link alignGraded} for the first k ranked anchors —
110
129
  * O(k · |query| · |ctx|). Consumed by CAST; reusable by any future
111
130
  * mechanism doing analogical transfer. */
112
131
  weave() {
113
- return this._weave ??= this.attention().then((climb) => computeWeave(this.ctx, this.query, this, climb));
132
+ return this._weave ??= this.attention().then((climb) => this.shared("weave", async () => computeWeave(this.ctx, this.query, this, climb)));
114
133
  }
115
134
  /** Span-shaped classification of one ranked anchor, memoised per anchor id
116
135
  * so repeated calls (extraction's own early-exit scan, any future
@@ -124,7 +143,7 @@ export class Precomputed {
124
143
  spanShapedOf(anchor) {
125
144
  let p = this._spanShaped.get(anchor);
126
145
  if (p === undefined) {
127
- p = skillExemplar(this.ctx, anchor, this.guide);
146
+ p = this.shared("spanShaped", () => skillExemplar(this.ctx, anchor, this.guide));
128
147
  this._spanShaped.set(anchor, p);
129
148
  }
130
149
  return p;
@@ -152,6 +171,7 @@ function computeWeave(ctx, query, pre, climb) {
152
171
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
153
172
  const depth = new Float64Array(query.length);
154
173
  const points = [];
174
+ const byAnchor = new Map();
155
175
  // WEAVE-SCALE anchors only: CAST transfers structure between things the
156
176
  // QUERY weaves together — query-scale structures. A context an order of
157
177
  // magnitude beyond the query is not woven BY the query (the query can at
@@ -166,6 +186,34 @@ function computeWeave(ctx, query, pre, climb) {
166
186
  // uncapped weaves spent 5–8s per query recognising conversation-length
167
187
  // anchors that could never form a weave point.
168
188
  const capBytes = query.length * quantum;
189
+ // EXCLUSIVITY IS ARBITRATED BY THE CLIMB'S VOTE ORDER, DELIBERATELY. A query
190
+ // byte can only be independent evidence for ONE point, so points are built in
191
+ // ranked order and each new point's runs are trimmed against every point
192
+ // already accepted; a point left with no run of a full quantum drops out of
193
+ // the weave.
194
+ //
195
+ // That reads like first-come-wins — a point that merely ranked higher taking
196
+ // a span from the point that actually explains it — and arbitrating by LOCAL
197
+ // evidence instead (ownership of each byte to the longest covering run, then
198
+ // the heavier weight, then rank) was implemented and MEASURED: test/29 went
199
+ // 9/2 to 7/4, and the new failures name the reason. CAST requires the weave
200
+ // to touch a COMMITTED point of attention ("2 aligned structure(s), but none
201
+ // is one of the climb's 1 committed root(s)"), and it was precisely the vote
202
+ // order that kept the committed root's own point alive in the weave. Local
203
+ // run length knows nothing about what the climb settled on, so it evicted the
204
+ // root's evidence and left CAST refusing on its own consistency check.
205
+ //
206
+ // So the vote order here is not an accident of construction — it is what
207
+ // holds the weave and the climb to the same conclusion. Weave-local
208
+ // measures decide what is FRAME inside the weave (see the frame gates in
209
+ // cast.ts); which structures are in the weave at all stays the climb's call.
210
+ //
211
+ // TWO PASSES. `depth` — how much of the weave agrees on each query byte, and
212
+ // therefore what counts as FRAME — must be the whole weave's, not "whatever
213
+ // has been processed so far": read in one pass it made a candidate's own
214
+ // frame reading depend on its rank, and the proposed-run gate below needs the
215
+ // real thing.
216
+ const cands = [];
169
217
  for (const cand of rankedCapped) {
170
218
  const ctxBytes = read(ctx, cand.anchor, capBytes + 1);
171
219
  if (ctxBytes.length === 0 || ctxBytes.length > capBytes)
@@ -177,6 +225,9 @@ function computeWeave(ctx, query, pre, climb) {
177
225
  for (let i = r.qs; i < r.qe; i++)
178
226
  depth[i] += r.weight;
179
227
  }
228
+ cands.push({ cand, ctxBytes, raw });
229
+ }
230
+ for (const { cand, ctxBytes, raw } of cands) {
180
231
  const free = [];
181
232
  for (const r of raw) {
182
233
  let { qs, qe, cs, weight } = r;
@@ -201,12 +252,130 @@ function computeWeave(ctx, query, pre, climb) {
201
252
  }
202
253
  }
203
254
  if (free.length > 0) {
204
- points.push({
255
+ const pt = {
205
256
  anchor: cand.anchor,
206
257
  vote: cand.vote,
207
258
  ctx: ctxBytes,
208
259
  runs: free,
209
- });
260
+ };
261
+ byAnchor.set(cand.anchor, pt);
262
+ points.push(pt);
263
+ }
264
+ }
265
+ // A byte is FRAME when more than half the weave shares it, and a SPAN is
266
+ // frame when more than half its bytes are — the same two-level
267
+ // half-dominance reading cast.ts's own frame gate uses, over the same
268
+ // `depth`. Read against the accepted POINTS (as cast.ts does), so it is
269
+ // only meaningful once phase 1 has run.
270
+ const framed = (from, to) => {
271
+ let n = 0;
272
+ for (let i = from; i < to; i++)
273
+ if (dominates(depth[i], points.length))
274
+ n++;
275
+ return dominates(n, to - from);
276
+ };
277
+ // PHASE 2 — THE CLIMB'S OWN CONCLUSION IS AN ALIGNMENT THE LITERAL MATCHER
278
+ // CANNOT SEE. `alignRuns` seeds on W-grams, so two forms differing by a
279
+ // single byte share no run at all: on `How is ice like steel?` against a
280
+ // store holding `Ice is cold`, the query's `ice` and the stored `Ice` agree
281
+ // on only `ce ` — three bytes, never seeded — so that structure entered the
282
+ // weave carrying nothing but the ` is ` scaffolding every exemplar shares,
283
+ // lost it to the first point that claimed it, and vanished. The climb had
284
+ // ALREADY identified it: its resonance elected `Ice is cold` from the query
285
+ // span `ce l` and `Steel is hard` from `stee`, two disjoint spans each naming
286
+ // its own structure, weighed through the region's contrastive margin and its
287
+ // IDF — gates the aligner has no equivalent of.
288
+ //
289
+ // So the climb PROPOSES the pairing (which structure, which query span) and
290
+ // bytes DECIDE its terms (§2.3). Three gates, each one measured:
291
+ //
292
+ // • it may only take query bytes NO literal run claimed. Run inline with
293
+ // phase 1 this did the opposite of "exact decides" — a higher-ranked
294
+ // candidate's proposal trimmed a lower-ranked candidate's byte-for-byte
295
+ // match out of existence (`he W`, proposed for `a nickname meaning the
296
+ // divine one`, cut the literal `The ` out of `The Starry Night was
297
+ // painted by Vincent van Gogh.` and CAST's redirection lost its
298
+ // dominant — test/29 C4). Hence a second pass, after every literal run
299
+ // is placed.
300
+ // • the literal agreement must DOMINATE the span. A climb vote is not by
301
+ // itself an alignment: on `The Persistence of Memory was painted by
302
+ // Salvador Dali.` the climb elects `The Starry Night…` from the span
303
+ // ` Dali.`, which shares barely a byte with it — the resonance was
304
+ // carried by the frame those exemplars share. Admitting it let CAST
305
+ // weave points out of pure scaffolding and out-account the correct
306
+ // extraction (test/00, test/24). Where the proposal is real the
307
+ // agreement is overwhelming: both C1 spans agree on three of four bytes.
308
+ // • and the span must not be FRAME. Literal dominance alone is too weak
309
+ // at this scale — a 4-byte span agrees three-of-four with half the
310
+ // corpus by accident (`he W` against `a nickname meaning the divine
311
+ // one`). Frame is the weave-local measure of exactly that.
312
+ const claimed = new Uint8Array(query.length);
313
+ for (const p of points) {
314
+ for (const r of p.runs)
315
+ claimed.fill(1, r.qs, r.qe);
316
+ }
317
+ for (const { cand, ctxBytes } of cands) {
318
+ if (cand.end > cand.start) {
319
+ let qs = cand.start;
320
+ let qe = cand.end;
321
+ while (qs < qe && claimed[qs])
322
+ qs++;
323
+ while (qe > qs && claimed[qe - 1])
324
+ qe--;
325
+ let clear = true;
326
+ for (let i = qs; i < qe; i++)
327
+ if (claimed[i])
328
+ clear = false;
329
+ if (clear && qe - qs >= Math.min(quantum, ctxBytes.length)) {
330
+ // The gate only asks whether the agreement DOMINATES the span, so
331
+ // search DOWNWARD from the whole span and stop at the first hit: the
332
+ // first length found is both the longest agreement and, by
333
+ // construction, already past the dominance bar. At most O(W²) bounded
334
+ // substring probes — a span is one segment (≤ 2W) — where a full
335
+ // longest-common-substring scan would be O(|span|² · |ctx|) against a
336
+ // context that may be W× the query.
337
+ const span = query.subarray(qs, qe);
338
+ const bar = Math.floor(span.length / 2) + 1; // dominates(bar, length)
339
+ let bestLen = 0;
340
+ let bestCs = 0;
341
+ for (let len = span.length; len >= bar && bestLen === 0; len--) {
342
+ for (let off = 0; off + len <= span.length; off++) {
343
+ const at = indexOf(ctxBytes, span.subarray(off, off + len), 0);
344
+ if (at < 0)
345
+ continue;
346
+ bestLen = len;
347
+ // Where the span's FIRST byte lands, so `cs` means the same thing
348
+ // it does for a literal run: the context offset the run starts at.
349
+ bestCs = Math.max(0, at - off);
350
+ break;
351
+ }
352
+ }
353
+ if (bestLen > 0 && !framed(qs, qe)) {
354
+ const run = {
355
+ qs,
356
+ qe,
357
+ cs: bestCs,
358
+ weight: bestLen / (qe - qs),
359
+ proposed: true,
360
+ };
361
+ claimed.fill(1, qs, qe);
362
+ const pt = byAnchor.get(cand.anchor);
363
+ if (!pt) {
364
+ const made = {
365
+ anchor: cand.anchor,
366
+ vote: cand.vote,
367
+ ctx: ctxBytes,
368
+ runs: [run],
369
+ };
370
+ byAnchor.set(cand.anchor, made);
371
+ points.push(made);
372
+ }
373
+ else {
374
+ pt.runs.push(run);
375
+ pt.runs.sort((x, y) => x.qs - y.qs);
376
+ }
377
+ }
378
+ }
210
379
  }
211
380
  }
212
381
  return { points, depth };
@@ -8,6 +8,35 @@ export interface Thought {
8
8
  bytes: Uint8Array;
9
9
  provenance: Provenance;
10
10
  }
11
+ /** Structured payload of the "decideGrounding" rationale step — the same
12
+ * numbers the human-readable candidate labels already carry, exposed as
13
+ * data so a downstream tool need not parse free text. Purely additive
14
+ * instrumentation: built only under `ctx.trace?.` (optional chaining
15
+ * short-circuits its arguments), never read by inference. */
16
+ export interface DecideGroundingData {
17
+ version: 1;
18
+ /** Every grounding candidate weighed, in consideration order. */
19
+ candidates: Array<{
20
+ provenance: string;
21
+ /** The candidate's exact weight in the one cost ladder. */
22
+ weight: number;
23
+ /** The DISCRETE grade the decision actually compares (floor(weight/STEP)). */
24
+ grade: number;
25
+ /** Query bytes the candidate's accounted spans leave unexplained. */
26
+ unexplainedBytes: number;
27
+ /** Whether this candidate won the decision. */
28
+ decided: boolean;
29
+ }>;
30
+ /** Grade margin between the winner and the runner-up, when both exist —
31
+ * the same quantity the "narrowDecision" step reports as narrow when
32
+ * ≤ 1. Absent for a single-candidate decision. */
33
+ runnerUpMargin?: number;
34
+ }
35
+ /** Structured payload of the "narrowDecision" rationale step. */
36
+ export interface NarrowDecisionData {
37
+ version: 1;
38
+ margin: number;
39
+ }
11
40
  /** Think: a single lightest-derivation exploration of the Sema graph.
12
41
  *
13
42
  * Every answer travels the same path:
@@ -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
  }
@@ -132,9 +156,12 @@ export async function think(ctx, query, mechs) {
132
156
  // initial null, so the read-back needs the assertion.)
133
157
  const decided = best;
134
158
  if (candidates.length > 1) {
135
- ctx.trace?.step("decideGrounding", candidates.map((c) => rItem(c.bytes, `${c.provenance} (weight ${c.weight.toFixed(3)}${c.unexplained ? `, unexplained: "${c.unexplained}"` : ""})`)), decided ? [rItem(decided.bytes, decided.provenance)] : [], "the lightest grounding derivation wins — every mechanism weighed in the one cost ladder");
159
+ // The runner-up is computed BEFORE the decideGrounding step so its grade
160
+ // margin can ride along in the step's structured data payload; the
161
+ // computation itself is pure and was always unconditional — only its
162
+ // position moved.
163
+ let runnerUp = null;
136
164
  if (decided !== null) {
137
- let runnerUp = null;
138
165
  for (const c of candidates) {
139
166
  if (c === decided)
140
167
  continue;
@@ -142,15 +169,28 @@ export async function think(ctx, query, mechs) {
142
169
  runnerUp = c;
143
170
  }
144
171
  }
145
- if (runnerUp !== null) {
146
- const margin = grade(runnerUp.weight) - grade(decided.weight);
147
- if (margin <= 1) {
148
- ctx.trace?.step("narrowDecision", [
149
- rItem(decided.bytes, `${decided.provenance} (weight ${decided.weight.toFixed(3)})`),
150
- ], [
151
- rItem(runnerUp.bytes, `${runnerUp.provenance} (weight ${runnerUp.weight.toFixed(3)})`),
152
- ], `margin ${margin} grade-unit(s) — the decision could change with one more training fact`);
153
- }
172
+ }
173
+ const margin = decided !== null && runnerUp !== null
174
+ ? grade(runnerUp.weight) - grade(decided.weight)
175
+ : null;
176
+ ctx.trace?.step("decideGrounding", candidates.map((c) => rItem(c.bytes, `${c.provenance} (weight ${c.weight.toFixed(3)}${c.unexplained ? `, unexplained: "${c.unexplained}"` : ""})`)), decided ? [rItem(decided.bytes, decided.provenance)] : [], "the lightest grounding derivation wins — every mechanism weighed in the one cost ladder", undefined, {
177
+ version: 1,
178
+ candidates: candidates.map((c) => ({
179
+ provenance: c.provenance,
180
+ weight: c.weight,
181
+ grade: grade(c.weight),
182
+ unexplainedBytes: unaccounted(c.accounted),
183
+ decided: c === decided,
184
+ })),
185
+ ...(margin !== null ? { runnerUpMargin: margin } : {}),
186
+ });
187
+ if (decided !== null && runnerUp !== null && margin !== null) {
188
+ if (margin <= 1) {
189
+ ctx.trace?.step("narrowDecision", [
190
+ rItem(decided.bytes, `${decided.provenance} (weight ${decided.weight.toFixed(3)})`),
191
+ ], [
192
+ rItem(runnerUp.bytes, `${runnerUp.provenance} (weight ${runnerUp.weight.toFixed(3)})`),
193
+ ], `margin ${margin} grade-unit(s) — the decision could change with one more training fact`, undefined, { version: 1, margin });
154
194
  }
155
195
  }
156
196
  }
@@ -176,7 +216,13 @@ export async function think(ctx, query, mechs) {
176
216
  : provenance === "recall" || provenance === "recall-echo"
177
217
  ? new Set()
178
218
  : new Set(recognise(ctx, answer).sites.map((s) => s.payload));
179
- 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);
180
226
  // Fuse only when the query has a genuine REMAINDER no mechanism's
181
227
  // structural evidence touched at all. `decided.accounted` alone
182
228
  // undercounts this: it is a COST-LADDER quantity (cover.ts prices its
@@ -209,9 +255,21 @@ export async function think(ctx, query, mechs) {
209
255
  // rejected.
210
256
  const unclimbed = decided.accounted.length > 0 &&
211
257
  decided.accounted.every(([i, j]) => pre.computed.some((u) => u.i === i && u.j === j));
212
- const fused = remainder >= ctx.space.maxGroup
213
- ? await fuseAttention(ctx, query, reasoned, pre, unclimbed)
214
- : 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);
215
273
  done(fused, "grounded, reasoned forward, fused across points of attention");
216
274
  return { bytes: fused, provenance };
217
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>;