@hviana/sema 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/src/geometry.d.ts +26 -0
  2. package/dist/src/geometry.js +32 -3
  3. package/dist/src/mind/attention.d.ts +246 -7
  4. package/dist/src/mind/attention.js +771 -173
  5. package/dist/src/mind/bridge.d.ts +30 -0
  6. package/dist/src/mind/bridge.js +569 -0
  7. package/dist/src/mind/index.d.ts +2 -0
  8. package/dist/src/mind/junction.d.ts +30 -1
  9. package/dist/src/mind/junction.js +73 -18
  10. package/dist/src/mind/match.d.ts +15 -2
  11. package/dist/src/mind/match.js +3 -8
  12. package/dist/src/mind/mechanisms/cast.d.ts +54 -0
  13. package/dist/src/mind/mechanisms/cast.js +268 -37
  14. package/dist/src/mind/mechanisms/cover.js +16 -31
  15. package/dist/src/mind/mechanisms/recall.js +66 -0
  16. package/dist/src/mind/mind.d.ts +2 -0
  17. package/dist/src/mind/rationale.d.ts +7 -2
  18. package/dist/src/mind/rationale.js +6 -5
  19. package/dist/src/mind/reasoning.d.ts +11 -0
  20. package/dist/src/mind/reasoning.js +58 -2
  21. package/dist/src/mind/recognition.js +127 -7
  22. package/dist/src/mind/traverse.js +90 -25
  23. package/dist/src/mind/types.d.ts +57 -2
  24. package/dist/src/mind/types.js +38 -7
  25. package/dist/src/store.d.ts +12 -3
  26. package/dist/src/store.js +9 -3
  27. package/package.json +1 -1
  28. package/src/geometry.ts +52 -3
  29. package/src/mind/attention.ts +1199 -128
  30. package/src/mind/bridge.ts +596 -0
  31. package/src/mind/index.ts +16 -0
  32. package/src/mind/junction.ts +125 -16
  33. package/src/mind/match.ts +19 -5
  34. package/src/mind/mechanisms/cast.ts +290 -38
  35. package/src/mind/mechanisms/cover.ts +23 -36
  36. package/src/mind/mechanisms/recall.ts +79 -0
  37. package/src/mind/mind.ts +15 -0
  38. package/src/mind/rationale.ts +12 -4
  39. package/src/mind/reasoning.ts +71 -2
  40. package/src/mind/recognition.ts +132 -7
  41. package/src/mind/traverse.ts +91 -24
  42. package/src/mind/types.ts +95 -6
  43. package/src/store.ts +19 -5
  44. package/test/36-already-answered-fusion.test.mjs +128 -0
  45. package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
  46. package/test/38-reason-restate-guard.test.mjs +94 -0
  47. package/test/39-cast-restate-guard.test.mjs +102 -0
  48. package/test/40-choosenext-scale-guard.test.mjs +75 -0
  49. package/test/41-seatofnode-direction.test.mjs +85 -0
  50. package/test/42-recognise-trace-idempotence.test.mjs +106 -0
  51. package/test/43-cast-analog-seat.test.mjs +244 -0
  52. package/test/44-recognise-edge-whitespace.test.mjs +63 -0
  53. package/test/45-liftanswer-restated-trim.test.mjs +60 -0
  54. package/test/46-recognise-multibyte-edge.test.mjs +85 -0
  55. package/test/47-cast-comparison-coverage.test.mjs +134 -0
  56. package/test/48-recognise-turn-connective.test.mjs +125 -0
  57. package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
  58. package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
  59. package/test/51-structural-resonance-ladder.test.mjs +552 -0
  60. package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
@@ -11,12 +11,11 @@
11
11
 
12
12
  import type { MindContext } from "../types.js";
13
13
  import type { ComputedResult, Site } from "../graph-search.js";
14
- import { bytesEqual, indexOf } from "../../bytes.js";
15
14
  import { read, resolve } from "../primitives.js";
16
15
  import { guidedFirst } from "../traverse.js";
17
16
  import { conceptHop } from "../match.js";
18
17
  import { bridge } from "../resonance.js";
19
- import { liftAnswer } from "../types.js";
18
+ import { liftAnswer, segRestatesQuery } from "../types.js";
20
19
  import { decodeText, unexplainedLabel } from "../rationale.js";
21
20
  import type { RationaleItem } from "../rationale.js";
22
21
  import { rItem, rNode, traceDerivation } from "../trace.js";
@@ -226,44 +225,31 @@ export const coverMechanism: PipelineMechanism = {
226
225
 
227
226
  if (segs === null) return [];
228
227
 
228
+ const W = ctx.space.maxGroup;
229
229
  // A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
230
230
  // site, not the site's own literal text read back) that equal a byte
231
231
  // span the query ALREADY CONTAINS elsewhere restates part of the
232
- // question — never an answer (the same principle recall.ts's tiers
233
- // apply to a whole-query projection: "a projection that is a proper
234
- // byte-subspan of the query restates part of the question"). A
232
+ // question — never an answer (see {@link segRestatesQuery}). A
235
233
  // recognised site that is itself an entire PRIOR TURN of a multi-turn
236
234
  // query is exactly this shape: it carries a genuine learnt
237
235
  // continuation, but that continuation is something the asker already
238
- // said moments later in the SAME query, not a new answer — the cover
239
- // search has no notion of "turn" to gate this itself, so the check
240
- // belongs here, over the derivation it already chose.
241
- const W = ctx.space.maxGroup;
242
- for (const s of segs) {
243
- if (!s.rec) continue;
244
- const literal = s.j - s.i === s.bytes.length &&
245
- bytesEqual(s.bytes, query.subarray(s.i, s.j));
246
- if (literal) continue;
247
- // A span shorter than one river window is exactly the "ice"/"c"
248
- // case: a chain hop's short terminal coincidentally recurring as a
249
- // substring of an unrelated longer word. Below the quantum, byte
250
- // overlap is chance, not evidence — the same floor identityBar and
251
- // reachThreshold hold every other structural-overlap claim to.
252
- if (
253
- s.bytes.length >= W && s.bytes.length < query.length &&
254
- indexOf(query, s.bytes, 0) >= 0
255
- ) {
256
- ctx.trace?.step(
257
- "restatedSpan",
258
- [rItem(s.bytes, "substituted", s.node, [s.i, s.j])],
259
- [],
260
- "the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer",
261
- );
262
- return [];
263
- }
236
+ // said moments later in the SAME query. liftAnswer TRIMS such spans
237
+ // out of both the framing decision and the final concatenation the
238
+ // OTHER spans a derivation chose are independent evidence and must not
239
+ // be discarded along with the stale one.
240
+ const restated = segs.filter((s) =>
241
+ segRestatesQuery(s, query, query.length, W)
242
+ );
243
+ if (restated.length > 0) {
244
+ ctx.trace?.step(
245
+ "restatedSpan",
246
+ restated.map((s) => rItem(s.bytes, "substituted", s.node, [s.i, s.j])),
247
+ [],
248
+ "the chosen span's substitution already occurs elsewhere in the query trimmed from the answer, not an answer itself",
249
+ );
264
250
  }
265
251
 
266
- const composed = liftAnswer(segs, query.length);
252
+ const composed = liftAnswer(segs, query.length, query, W);
267
253
  if (composed === null) return [];
268
254
 
269
255
  ctx.trace?.step(
@@ -276,11 +262,12 @@ export const coverMechanism: PipelineMechanism = {
276
262
  tCover ? [tCover.index] : undefined,
277
263
  );
278
264
 
279
- // accounted = RECOGNISED cover spans only — PASS-carried bytes
280
- // are priced in cost already; the diagnostic label reflects the
281
- // same distinction.
265
+ // accounted = RECOGNISED, non-restating cover spans only — a trimmed
266
+ // restated span contributes nothing to the composed answer, so it must
267
+ // not be priced as if it did (PASS-carried bytes are priced already;
268
+ // the diagnostic label reflects the same distinction).
282
269
  const accounted: Array<[number, number]> = segs
283
- .filter((s) => s.rec)
270
+ .filter((s) => s.rec && !segRestatesQuery(s, query, query.length, W))
284
271
  .map((s) => [s.i, s.j]);
285
272
 
286
273
  return [{
@@ -20,6 +20,7 @@ import { CONCEPT, STEP } from "../graph-search.js";
20
20
  import { unexplainedLabel } from "../rationale.js";
21
21
  import type { PipelineMechanism, Precomputed } from "../pipeline-mechanism.js";
22
22
  import { rItem, rNode } from "../trace.js";
23
+ import { substitutionBridge } from "../bridge.js";
23
24
 
24
25
  /** A recall result. */
25
26
  export interface RecallResult {
@@ -293,6 +294,84 @@ export async function recallByResonance(
293
294
  }
294
295
  }
295
296
  }
297
+ // 3b. Corroborated-substitution bridge — refusal-path only (bridge.ts).
298
+ // Every gist-based tier has failed; before refusing, align the query
299
+ // byte-for-byte against the trained contexts its own stored windows
300
+ // anchor, accepting mismatches only as corpus-attested, concept-bar
301
+ // substitutions. A bridged context grounds exactly like any hit —
302
+ // projected through its learnt edges — under the same restated-fragment
303
+ // guard tiers 0b/2 apply. Costs nothing on any answering path.
304
+ {
305
+ // The resonance hits already ranked above are handed to the bridge as
306
+ // PROPOSED candidates alongside its own anchor climbs: on a corpus this
307
+ // size a W-byte window is far too common for the clamped climb to
308
+ // single out the right trained context, while the whole-query gist
309
+ // already ranked it nearest (observed live: "what is the capital of
310
+ // france" resonating straight to "What is the capital of France?" yet
311
+ // refusing on the reach bar). Approximate scores propose; the bridge's
312
+ // byte-exact alignment and attestation gates decide.
313
+ //
314
+ // The proposal breadth here is widened PAST `k` — first by requesting
315
+ // hubBound(ctx) candidates instead of `k` (recall's own tiers above
316
+ // stay at `k`; this re-resonates only on the refusal path, exactly
317
+ // where the bridge itself already runs), AND by asking the index to
318
+ // search EXHAUSTIVELY. Both matter: the IVF only ever probes
319
+ // ⌈√clusters⌉ of them (store.ts's efFor) REGARDLESS of k — widening k
320
+ // alone just returns more hits from the SAME already-probed clusters,
321
+ // never a hit whose vector lives in an unprobed one. Measured live:
322
+ // "What is the chemical symbol for water?" needs "What is the
323
+ // chemical formula for water?", scoring only 0.58 against the
324
+ // query's gist (a MIDDLE-of-string word swap perturbs the river-fold
325
+ // tree hash far more than a same-length TAIL swap like the "carbon"/
326
+ // "oxygen" neighbours that outrank it at 0.87+) — absent from the
327
+ // resonance list even at k=5000, present and byte-exact-verified the
328
+ // moment it's force-fed to the bridge directly. `exhaustive` is the
329
+ // natural, tuning-free ceiling (probe every cluster) for a call that
330
+ // is ALREADY refusal-path-only and must not miss a candidate hiding
331
+ // behind an unlucky structural distance.
332
+ const wide = k < hubBound(ctx)
333
+ ? await ctx.store.resonate(queryGist, hubBound(ctx), true)
334
+ : whole;
335
+ const bridged = await substitutionBridge(
336
+ ctx,
337
+ query,
338
+ wide.map((h) => h.id),
339
+ );
340
+ if (bridged !== null) {
341
+ const g = await project(ctx, bridged.id, queryGist);
342
+ // A projection contained in a substituted candidate-side span is the
343
+ // substitution RESTATED as if it were knowledge — the exact failure
344
+ // observed live: "Darwin was born in England." bridged to the
345
+ // Einstein fact through " England." → " Germany." and would have
346
+ // voiced "Germany", an answer the substitution itself manufactured.
347
+ // The same principle as the restated-fragment guards above, extended
348
+ // to the bridge's own substitutions.
349
+ const cBytes = ctx.store.bytes(bridged.id);
350
+ const manufactured = g !== null &&
351
+ bridged.subs.some((s) =>
352
+ indexOf(cBytes.subarray(s.cs, s.ce), g!, 0) >= 0
353
+ );
354
+ if (
355
+ g !== null && g.length > 0 && !restates(g) && !manufactured &&
356
+ !(g.length < query.length && indexOf(query, g, 0) >= 0)
357
+ ) {
358
+ return ground(
359
+ g,
360
+ `substitution bridge — a trained context accounts for the query ` +
361
+ `up to ${bridged.subs.length} corroborated substitution(s)`,
362
+ // Accounted NOTHING — the same epistemic humility as the echo
363
+ // tier below: a substitution-bridged grounding is a last resort
364
+ // that must lose to ANY mechanism that actually explained the
365
+ // query (observed: pricing the aligned spans here outweighed
366
+ // extraction's correct answer in the grounding decider), while
367
+ // still beating silence when everything else refused.
368
+ [],
369
+ CONCEPT * bridged.subs.length + STEP,
370
+ );
371
+ }
372
+ }
373
+ }
374
+
296
375
  // The refusal/echo decision. The echo returns a stored form's bytes AS
297
376
  // the answer — a near-identity claim about the query — and identity-grade
298
377
  // decisions are never made on an estimated score ("approximate scores may
package/src/mind/mind.ts CHANGED
@@ -136,6 +136,21 @@ import {
136
136
  climbAttention as climbAttentionFn,
137
137
  naturalBreak as naturalBreakFn,
138
138
  } from "./attention.js";
139
+ export type {
140
+ AnchorRejectionReason,
141
+ ClimbConsensusData,
142
+ ConsensusAnchorTrace,
143
+ ConsensusReachTrace,
144
+ ConsensusRegionTrace,
145
+ CrossRegionTier,
146
+ JunctionVoteTrace,
147
+ RegionOutcome,
148
+ } from "./attention.js";
149
+ export type {
150
+ AncestorReach,
151
+ SaturationReason,
152
+ SaturationStop,
153
+ } from "./types.js";
139
154
  import { aluToMechanism, defaultMechanisms, think } from "./pipeline.js";
140
155
  import { articulate } from "./articulation.js";
141
156
  import { ingest } from "./learning.js";
@@ -83,6 +83,11 @@ export interface RationaleStep {
83
83
  /** A one-line, human account of what the mechanism did and why — the sentence
84
84
  * that turns the data into an explanation. */
85
85
  note?: string;
86
+ /** Optional structured payload — a mechanism-specific, plain-serialisable
87
+ * shape (no Map/Set/vectors/mutable internals) that carries more than the
88
+ * human-readable `note` can, for a debugger or downstream tool to consume
89
+ * programmatically. Never read by inference; purely additive. */
90
+ data?: unknown;
86
91
  }
87
92
 
88
93
  /** The callback {@link Mind.respond} / {@link Mind.respondText} accept. It is
@@ -142,7 +147,7 @@ export interface Scope {
142
147
  /** Close the mechanism: emit its step with these outputs and pop it off the
143
148
  * nesting stack. Idempotent — a second call is ignored, so a `finally` that
144
149
  * closes after an early return is safe. */
145
- done(outputs: RationaleItem[], note?: string): void;
150
+ done(outputs: RationaleItem[], note?: string, data?: unknown): void;
146
151
  }
147
152
 
148
153
  /** The live tracer: a stack of open mechanisms over one {@link Mind.respond}.
@@ -212,6 +217,7 @@ export class Rationale {
212
217
  outputs: RationaleItem[],
213
218
  deps: number[] | undefined,
214
219
  note: string | undefined,
220
+ data?: unknown,
215
221
  ): void {
216
222
  this.sink({
217
223
  index,
@@ -223,6 +229,7 @@ export class Rationale {
223
229
  inputs,
224
230
  outputs,
225
231
  note,
232
+ data,
226
233
  });
227
234
  }
228
235
 
@@ -248,11 +255,11 @@ export class Rationale {
248
255
  };
249
256
  return {
250
257
  index,
251
- done: (outputs, note) => {
258
+ done: (outputs, note, data) => {
252
259
  if (closed) return;
253
260
  closed = true;
254
261
  pop();
255
- emit(index, mechanism, inputs, outputs, resolvedDeps, note);
262
+ emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
256
263
  },
257
264
  };
258
265
  }
@@ -265,11 +272,12 @@ export class Rationale {
265
272
  outputs: RationaleItem[],
266
273
  note?: string,
267
274
  deps?: number[],
275
+ data?: unknown,
268
276
  ): number {
269
277
  const mechanism = this.path(name);
270
278
  const resolvedDeps = deps ?? this.defaultDeps();
271
279
  const index = this.reserve(name);
272
- this.emit(index, mechanism, inputs, outputs, resolvedDeps, note);
280
+ this.emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
273
281
  return index;
274
282
  }
275
283
  }
@@ -13,6 +13,20 @@ import { joinWithBridge, pivotInto } from "./resonance.js";
13
13
  import type { Precomputed } from "./pipeline-mechanism.js";
14
14
  import type { Rationale } from "./rationale.js";
15
15
 
16
+ /** Whether `bytes` is a proper byte-subspan of `query` — already present in
17
+ * the question, so voicing it back only restates part of what was asked,
18
+ * never answers it. The exact guard recallByResonance already applies to
19
+ * its OWN grounding candidates (tier 1's `restates`, tier 2's subspan
20
+ * check, tier 0b's argument-binding subspan check) — every mechanism that
21
+ * walks a LEARNT CONTINUATION EDGE past an already-vetted grounding
22
+ * (reason()'s own hops below, and CAST's `projectCounterfactual` seat
23
+ * substitution — see cast.ts) needs the same guard applied to what the
24
+ * walk turns up, since `follow()`/`chooseNext`/`pivotInto` know nothing of
25
+ * the query at all — only of what structurally continues what. */
26
+ export function restatesQuery(query: Uint8Array, bytes: Uint8Array): boolean {
27
+ return bytes.length < query.length && indexOf(query, bytes, 0) >= 0;
28
+ }
29
+
16
30
  /** Extend a grounded answer forward across facts (multi-hop reasoning).
17
31
  * Pivots on the longest unconsumed learnt context each answer contains,
18
32
  * then follows the pivot's continuation to the next fact. Repeats up
@@ -91,7 +105,8 @@ export async function reason(
91
105
  const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
92
106
  if (
93
107
  fwd !== null && !bytesEqual(fwd, cur) &&
94
- (fwdId === null || !consumed.has(fwdId))
108
+ (fwdId === null || !consumed.has(fwdId)) &&
109
+ !restatesQuery(query, fwd)
95
110
  ) {
96
111
  consumeAll(curId);
97
112
  t ??= ctx.trace?.enter("reason", [
@@ -115,7 +130,7 @@ export async function reason(
115
130
 
116
131
  const fc = await follow(ctx, pivot, qv);
117
132
  consumeAll(pivot);
118
- if (fc === null || bytesEqual(fc, cur)) break;
133
+ if (fc === null || bytesEqual(fc, cur) || restatesQuery(query, fc)) break;
119
134
  t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
120
135
  ctx.trace?.step(
121
136
  "pivotStep",
@@ -189,6 +204,60 @@ export async function fuseAttention(
189
204
  ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
190
205
  ]);
191
206
  for (const root of rest) {
207
+ // DISPERSION: this root's contributing regions are confined to a
208
+ // single cluster (see Attention.clusters) — one local neighbourhood of
209
+ // the query, not several separate places. Raw region count already
210
+ // failed to discriminate a coincidental match from a genuine further
211
+ // topic (test/24 gap 3.1 vs test/35's echo); dispersion is a different
212
+ // question — not how MUCH evidence, but how many separate PLACES in the
213
+ // query corroborate it — and a coincidental match is structurally
214
+ // confined to one cluster no matter how strongly it resonates.
215
+ //
216
+ // EXCEPTION: crossRegionVotes' own joint conclusions (a query naming
217
+ // two attributes that were only ever learnt TOGETHER — test/34's own
218
+ // binding corpus) are inherently ONE fused context and are pooled from
219
+ // a single synthetic region, so they always read as one cluster even
220
+ // though they already, by construction, account for both original
221
+ // mentions. `breadth` (dominates — the same > half-the-query bar used
222
+ // everywhere else) still correctly recognises these: a genuine joint
223
+ // binding explains the MAJORITY of the query's regions on its own,
224
+ // which a coincidental echo never does (verified: test/35's echo tops
225
+ // out at 0.40). So a root is trusted when EITHER measure alone
226
+ // indicates real signal — excluded only when BOTH are weak. Cheap and
227
+ // synchronous — checked before the async already-answered walk below.
228
+ if (root.clusters < 2 && root.breadth <= 0.5) {
229
+ ctx.trace?.step(
230
+ "singleCluster",
231
+ [rNode(ctx, root.anchor, "point", root.vote)],
232
+ [],
233
+ "this point's evidence is confined to one local neighbourhood of the query — not trusted as an independent topic",
234
+ );
235
+ continue;
236
+ }
237
+ // ALREADY ANSWERED: this root's own learnt continuation — the same
238
+ // content-addressed walk reason()'s echo guard already trusts
239
+ // (`ctx.store.prevCount(qId) > 0`), here applied per-candidate instead
240
+ // of to the whole query — is VERBATIM present later in the query. A
241
+ // query that embeds both an exchange's ask and its own already-given
242
+ // reply (a conversation's turn plus its own prior answer, concatenated
243
+ // raw by addTurn — or any caller pasting a transcript into one
244
+ // respond() call; the check is Mind-bookkeeping-free, so it treats both
245
+ // identically) has already spoken this root's answer — fusing it in
246
+ // again would only restate it. Deliberately NOT a magnitude measure:
247
+ // it fires on exact content-addressed recurrence, not on how strongly
248
+ // the root resonates.
249
+ const cont = await follow(ctx, root.anchor, qv);
250
+ if (
251
+ cont !== null && cont.length > 0 && indexOf(query, cont, root.end) >= 0
252
+ ) {
253
+ ctx.trace?.step(
254
+ "alreadyAnswered",
255
+ [rNode(ctx, root.anchor, "point", root.vote)],
256
+ [rItem(cont, "continuation")],
257
+ "this point's own learnt continuation already appears later in the query — already answered, not fused",
258
+ );
259
+ continue;
260
+ }
192
261
  const g = await project(ctx, root.anchor, qv);
193
262
  if (g === null || g.length === 0) continue;
194
263
  if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0)) continue;
@@ -16,7 +16,7 @@ import {
16
16
  resolve,
17
17
  } from "./primitives.js";
18
18
  import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
19
- import { chainReach, leafIdAt } from "./canonical.js";
19
+ import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
20
20
  import { isChunk, type Sema } from "../sema.js";
21
21
  import type { Leaf, Site } from "./graph-search.js";
22
22
 
@@ -34,12 +34,44 @@ import type { Leaf, Site } from "./graph-search.js";
34
34
  * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
35
35
  export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
36
36
  // Content-keyed memo — works for both single-turn respond() and multi-turn
37
- // respondTurn() (where the map persists across calls). Skipped while
38
- // tracing so every call still emits its rationale step.
39
- if (ctx.recogniseMemo && !ctx.trace) {
37
+ // respondTurn() (where the map persists across calls). ALWAYS consulted,
38
+ // regardless of tracing matching perceive()'s own memo, which carries no
39
+ // trace gate at all. This memo is NOT an optional accelerator: recogniseImpl
40
+ // walks the query's perceived tree via foldTree, whose subtree-resolution
41
+ // fast path (see primitives.ts) skips invoking `visit` — and therefore
42
+ // skips EMITTING SITES — for any subtree already cached in
43
+ // ctx._resolvedSubtrees. A multi-turn conversation's stable-prefix fold
44
+ // deliberately shares node OBJECTS across turns, so by the second call on
45
+ // the exact same bytes, large swaths of the tree are already cached and
46
+ // foldTree stops short of recursing into them — a second recogniseImpl
47
+ // call on the SAME bytes is not idempotent; it silently finds FEWER sites
48
+ // than the first (observed live: 31 sites → 5 on an immediate repeat
49
+ // call). Skipping this memo "only while tracing" used to mean every
50
+ // traced turn re-ran recogniseImpl from scratch at every one of the many
51
+ // call sites that recognise the same query (cover, reason, articulate...),
52
+ // each subsequent call silently more incomplete than the last — measurably
53
+ // changing which mechanism grounds the answer, not just costing time. The
54
+ // trace step must still fire on every call regardless (a cache hit is not
55
+ // silent), so it is emitted here directly instead of only inside
56
+ // recogniseImpl.
57
+ if (ctx.recogniseMemo) {
40
58
  const key = latin1Key(bytes);
41
59
  const hit = ctx.recogniseMemo.get(key);
42
- if (hit !== undefined) return hit;
60
+ if (hit !== undefined) {
61
+ ctx.trace?.step(
62
+ "recognise",
63
+ [rItem(bytes, "query")],
64
+ hit.sites.map((s) =>
65
+ rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
66
+ s.start,
67
+ s.end,
68
+ ])
69
+ ),
70
+ `decompose the query into ${hit.sites.length} learnt form(s) that ` +
71
+ `lead somewhere (over ${hit.leaves.length} perceived leaves) [cached]`,
72
+ );
73
+ return hit;
74
+ }
43
75
  const fresh = recogniseImpl(ctx, bytes);
44
76
  ctx.recogniseMemo.set(key, fresh);
45
77
  return fresh;
@@ -83,8 +115,18 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
83
115
  // silence. Atoms stay available as leaves (PASS-carried literals) and
84
116
  // through exact tier-0 resolution regardless.
85
117
  const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
118
+ // Distinct probes (structural exact match, canon fallback, edge trims at
119
+ // several offsets) can legitimately re-derive the SAME (start, end, id)
120
+ // site from different tree nodes — a wide edge-trim search is exactly
121
+ // this on purpose (see below). Duplicate site entries are not wrong
122
+ // evidence, but they double the weight cover's derivation search gives
123
+ // that span, distorting its cost model — the same span must count once.
124
+ const seen = new Set<string>();
86
125
  const emit = (start: number, end: number, id: number) => {
87
126
  if (id < 0 && atomsAreHubs) return;
127
+ const key = start + "," + end + "," + id;
128
+ if (seen.has(key)) return;
129
+ seen.add(key);
88
130
  if (leadsSomewhere(ctx, id)) {
89
131
  sites.push({ start, end, payload: id });
90
132
  }
@@ -101,10 +143,93 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
101
143
  // missed may still be a stored form under the response's equivalence
102
144
  // (case, width, whitespace — whatever the injected canonicalizer says).
103
145
  // O(subtree bytes) per miss, memoised per response; a no-op when no
104
- // canonicalizer was injected or the store has no canon index.
105
- else if (end - start >= 2) {
146
+ // canonicalizer was injected or the store has no canon index. A raw
147
+ // leaf (n.kids === null) is single-byte and handled by the byte-atom
148
+ // path above instead — canon equivalence only applies to composites.
149
+ else if (n.kids !== null) {
106
150
  const cid = canonResolve(ctx, bytes.subarray(start, end));
107
151
  if (cid !== null) emit(start, end, cid);
152
+ // The edge-trim fallbacks below remove 1 byte from a side; the
153
+ // remainder must still be a composite (>= 2 bytes, the same floor
154
+ // n.kids !== null enforces above) rather than degenerate into
155
+ // single-byte-atom territory, which atomIsHub already governs
156
+ // separately.
157
+ else if (end - start - 1 >= 2) {
158
+ // The chunk's own boundary is drawn by content geometry, not by
159
+ // any notion of "form" — it can include one edge byte the query's
160
+ // fold happened to attach here that the trained span never had
161
+ // (e.g. a separator from the preceding chunk). The core has no
162
+ // idea what that byte means; it only knows resolve()/canonResolve
163
+ // are self-verifying (hash-then-verify, same discipline as every
164
+ // content lookup here), so a blind one-byte-shorter guess on
165
+ // either edge costs nothing when wrong and is trustworthy when it
166
+ // hits. Two extra probes, only on the already-failed miss path.
167
+ const left = resolve(ctx, bytes.subarray(start + 1, end));
168
+ if (left !== null) emit(start + 1, end, left);
169
+ const right = resolve(ctx, bytes.subarray(start, end - 1));
170
+ if (right !== null) emit(start, end - 1, right);
171
+ // A misalignment wider than one byte (e.g. more than one edge
172
+ // separator swallowed) is not itself geometry-quantized — the
173
+ // WRITE side's canonical index (canonicalWindows) interns sliding
174
+ // W−1/W-length windows over leaf ids at EVERY offset, not just
175
+ // radix-aligned ones (see canonical.ts) — so the offset that
176
+ // recovers a trained span can be anything, not a multiple of W.
177
+ // What IS bounded is how far it's worth looking: chainReach(W)=W²,
178
+ // the same reach the canonical pass (tryChain) trusts for a chain
179
+ // rebuilt off the query's own fold. Every candidate offset is
180
+ // gated by store.findBranch(leafIds) first — the SAME cheap,
181
+ // fold-free existence check tryChain already uses — so the extra
182
+ // resolve() fold (the real cost) is only paid when a branch could
183
+ // plausibly exist there, not for every offset. The node itself is
184
+ // also bounded to chunk-scale (end - start <= W²): widening this at
185
+ // whole-query/root scale can rediscover a smaller subtree's own
186
+ // content as a second, overlapping site the structural walk's own
187
+ // finer recursion already emits correctly on its own — a duplicate
188
+ // that downstream derivation can stitch into a wrong answer.
189
+ const W = ctx.space.maxGroup;
190
+ for (
191
+ let k = 1;
192
+ end - start <= W * W && k <= W * W && start + k < end - 1;
193
+ k++
194
+ ) {
195
+ const lIds = leafIdRun(ctx, bytes, start + k, end);
196
+ if (lIds !== null && store.findBranch(lIds) !== null) {
197
+ const eLeft = resolve(ctx, bytes.subarray(start + k, end));
198
+ if (eLeft !== null) emit(start + k, end, eLeft);
199
+ }
200
+ const rIds = leafIdRun(ctx, bytes, start, end - k);
201
+ if (rIds !== null && store.findBranch(rIds) !== null) {
202
+ const eRight = resolve(ctx, bytes.subarray(start, end - k));
203
+ if (eRight !== null) emit(start, end - k, eRight);
204
+ }
205
+ }
206
+ // A REAL extra word at the left edge (a discourse connective like
207
+ // "And " prepended to a follow-up turn — not boundary noise, actual
208
+ // content the injected canonicalizer has no equivalence for) shows
209
+ // up as a canon-miss too big for the chunk-scale search above: the
210
+ // turn is its OWN segment (stable-prefix folded independently — see
211
+ // mind.ts's _growContext), so it can be turn/segment-scale, not
212
+ // chunk-scale. Widening the size bound itself reopens the root-
213
+ // scale false-positive this module already fixed once (test/46);
214
+ // widening the SEARCH instead — trying every position up to W
215
+ // chunk-widths from the left edge that the query's OWN fold treats
216
+ // as a chunk boundary (`starts`, the same set the canonical pass
217
+ // privileges with full chain reach) — does not: `starts.has(p)` is
218
+ // fold EVIDENCE the query produced on its own (a leaf-parent chunk
219
+ // like "And " is visited, and its start added to `starts`, before
220
+ // this composite ever runs — foldTree is post-order), never a blind
221
+ // guess. Bounded to W candidates, each an O(1) set lookup before
222
+ // paying for the real canonResolve fold — canonResolve, not
223
+ // resolve()/findBranch, because the gap here is often exactly the
224
+ // kind of equivalence (case, in the live trace) canon exists for,
225
+ // not just an exact-content coincidence.
226
+ for (let k = 1; k <= W; k++) {
227
+ const p = start + k * W;
228
+ if (p >= end - 1 || !starts.has(p)) continue;
229
+ const cid = canonResolve(ctx, bytes.subarray(p, end));
230
+ if (cid !== null) emit(p, end, cid);
231
+ }
232
+ }
108
233
  }
109
234
  if (isChunk(n)) {
110
235
  starts.add(start);