@hviana/sema 0.4.1 → 0.4.2

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 (152) hide show
  1. package/package.json +1 -1
  2. package/src/ingest-cache.ts +4 -0
  3. package/src/meter.ts +5 -0
  4. package/src/mind/attention.ts +18 -1
  5. package/src/mind/bridge.ts +292 -54
  6. package/src/mind/junction.ts +21 -7
  7. package/src/mind/match.ts +92 -1
  8. package/src/mind/mechanisms/cast.ts +12 -0
  9. package/src/mind/mechanisms/confluence.ts +30 -1
  10. package/src/mind/mechanisms/cover.ts +36 -4
  11. package/src/mind/mechanisms/recall.ts +21 -44
  12. package/src/mind/mind.ts +39 -2
  13. package/src/mind/pipeline-mechanism.ts +86 -4
  14. package/src/mind/pipeline.ts +1 -1
  15. package/src/mind/reasoning.ts +15 -8
  16. package/src/mind/recognition.ts +40 -0
  17. package/src/mind/resonance.ts +0 -0
  18. package/src/mind/traverse.ts +17 -15
  19. package/src/mind/types.ts +10 -0
  20. package/test/49-natural-units-synonym-bridge.test.mjs +56 -15
  21. package/dist/example/demo.d.ts +0 -1
  22. package/dist/example/demo.js +0 -39
  23. package/dist/example/train_base.d.ts +0 -87
  24. package/dist/example/train_base.js +0 -2252
  25. package/dist/src/alphabet.d.ts +0 -7
  26. package/dist/src/alphabet.js +0 -33
  27. package/dist/src/alu/src/alu.d.ts +0 -185
  28. package/dist/src/alu/src/alu.js +0 -440
  29. package/dist/src/alu/src/expr.d.ts +0 -61
  30. package/dist/src/alu/src/expr.js +0 -318
  31. package/dist/src/alu/src/index.d.ts +0 -11
  32. package/dist/src/alu/src/index.js +0 -19
  33. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  34. package/dist/src/alu/src/kernel-arith.js +0 -264
  35. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  36. package/dist/src/alu/src/kernel-bits.js +0 -152
  37. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  38. package/dist/src/alu/src/kernel-logic.js +0 -60
  39. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  40. package/dist/src/alu/src/kernel-nd.js +0 -208
  41. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  42. package/dist/src/alu/src/kernel-numeric.js +0 -366
  43. package/dist/src/alu/src/operation.d.ts +0 -168
  44. package/dist/src/alu/src/operation.js +0 -189
  45. package/dist/src/alu/src/parser.d.ts +0 -221
  46. package/dist/src/alu/src/parser.js +0 -577
  47. package/dist/src/alu/src/resonance.d.ts +0 -55
  48. package/dist/src/alu/src/resonance.js +0 -126
  49. package/dist/src/alu/src/text.d.ts +0 -31
  50. package/dist/src/alu/src/text.js +0 -73
  51. package/dist/src/alu/src/value.d.ts +0 -109
  52. package/dist/src/alu/src/value.js +0 -300
  53. package/dist/src/alu/test/alu.test.d.ts +0 -1
  54. package/dist/src/alu/test/alu.test.js +0 -764
  55. package/dist/src/bytes.d.ts +0 -14
  56. package/dist/src/bytes.js +0 -59
  57. package/dist/src/canon.d.ts +0 -26
  58. package/dist/src/canon.js +0 -57
  59. package/dist/src/config.d.ts +0 -111
  60. package/dist/src/config.js +0 -91
  61. package/dist/src/derive/src/deduction.d.ts +0 -136
  62. package/dist/src/derive/src/deduction.js +0 -159
  63. package/dist/src/derive/src/index.d.ts +0 -8
  64. package/dist/src/derive/src/index.js +0 -11
  65. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  66. package/dist/src/derive/src/priority-queue.js +0 -73
  67. package/dist/src/derive/src/rewrite.d.ts +0 -56
  68. package/dist/src/derive/src/rewrite.js +0 -100
  69. package/dist/src/derive/src/trie.d.ts +0 -90
  70. package/dist/src/derive/src/trie.js +0 -217
  71. package/dist/src/derive/test/derive.test.d.ts +0 -1
  72. package/dist/src/derive/test/derive.test.js +0 -122
  73. package/dist/src/extension.d.ts +0 -37
  74. package/dist/src/extension.js +0 -7
  75. package/dist/src/geometry.d.ts +0 -175
  76. package/dist/src/geometry.js +0 -823
  77. package/dist/src/index.d.ts +0 -17
  78. package/dist/src/index.js +0 -19
  79. package/dist/src/ingest-cache.d.ts +0 -41
  80. package/dist/src/ingest-cache.js +0 -161
  81. package/dist/src/meter.d.ts +0 -171
  82. package/dist/src/meter.js +0 -269
  83. package/dist/src/mind/articulation.d.ts +0 -6
  84. package/dist/src/mind/articulation.js +0 -99
  85. package/dist/src/mind/attention.d.ts +0 -414
  86. package/dist/src/mind/attention.js +0 -2065
  87. package/dist/src/mind/bridge.d.ts +0 -39
  88. package/dist/src/mind/bridge.js +0 -738
  89. package/dist/src/mind/canonical.d.ts +0 -34
  90. package/dist/src/mind/canonical.js +0 -93
  91. package/dist/src/mind/graph-search.d.ts +0 -294
  92. package/dist/src/mind/graph-search.js +0 -996
  93. package/dist/src/mind/index.d.ts +0 -9
  94. package/dist/src/mind/index.js +0 -5
  95. package/dist/src/mind/junction.d.ts +0 -134
  96. package/dist/src/mind/junction.js +0 -331
  97. package/dist/src/mind/learning.d.ts +0 -75
  98. package/dist/src/mind/learning.js +0 -270
  99. package/dist/src/mind/match.d.ts +0 -166
  100. package/dist/src/mind/match.js +0 -565
  101. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  102. package/dist/src/mind/mechanisms/alu.js +0 -36
  103. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  104. package/dist/src/mind/mechanisms/cast.js +0 -773
  105. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  106. package/dist/src/mind/mechanisms/confluence.js +0 -225
  107. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  108. package/dist/src/mind/mechanisms/cover.js +0 -203
  109. package/dist/src/mind/mechanisms/extraction.d.ts +0 -33
  110. package/dist/src/mind/mechanisms/extraction.js +0 -300
  111. package/dist/src/mind/mechanisms/recall.d.ts +0 -16
  112. package/dist/src/mind/mechanisms/recall.js +0 -388
  113. package/dist/src/mind/mind.d.ts +0 -330
  114. package/dist/src/mind/mind.js +0 -594
  115. package/dist/src/mind/pipeline-mechanism.d.ts +0 -172
  116. package/dist/src/mind/pipeline-mechanism.js +0 -382
  117. package/dist/src/mind/pipeline.d.ts +0 -49
  118. package/dist/src/mind/pipeline.js +0 -275
  119. package/dist/src/mind/primitives.d.ts +0 -66
  120. package/dist/src/mind/primitives.js +0 -306
  121. package/dist/src/mind/rationale.d.ts +0 -139
  122. package/dist/src/mind/rationale.js +0 -163
  123. package/dist/src/mind/reasoning.d.ts +0 -40
  124. package/dist/src/mind/reasoning.js +0 -272
  125. package/dist/src/mind/recognition.d.ts +0 -20
  126. package/dist/src/mind/recognition.js +0 -463
  127. package/dist/src/mind/resonance.d.ts +0 -23
  128. package/dist/src/mind/resonance.js +0 -0
  129. package/dist/src/mind/trace.d.ts +0 -15
  130. package/dist/src/mind/trace.js +0 -73
  131. package/dist/src/mind/traverse.d.ts +0 -124
  132. package/dist/src/mind/traverse.js +0 -649
  133. package/dist/src/mind/types.d.ts +0 -323
  134. package/dist/src/mind/types.js +0 -130
  135. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  136. package/dist/src/rabitq-ivf/src/database.js +0 -201
  137. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  138. package/dist/src/rabitq-ivf/src/index.js +0 -4
  139. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  140. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  141. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  142. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  143. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  144. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  145. package/dist/src/sema.d.ts +0 -31
  146. package/dist/src/sema.js +0 -63
  147. package/dist/src/store-sqlite.d.ts +0 -184
  148. package/dist/src/store-sqlite.js +0 -942
  149. package/dist/src/store.d.ts +0 -678
  150. package/dist/src/store.js +0 -1703
  151. package/dist/src/vec.d.ts +0 -31
  152. package/dist/src/vec.js +0 -109
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -42,6 +42,8 @@ import { bindSeat, companySignature, type Sema } from "./sema.js";
42
42
  import type { Input } from "./mind/index.js";
43
43
  import { BoundedMap } from "./store.js";
44
44
  import type { Vec } from "./vec.js";
45
+ import { invalidateStructuralCaches } from "./mind/traverse.js";
46
+ import { invalidateJunctionCache } from "./mind/junction.js";
45
47
 
46
48
  /** The interned result of perceiving + interning ONE input.
47
49
  *
@@ -122,6 +124,8 @@ export class CachedIngest {
122
124
  input: Input | (Input | [Input, Input])[],
123
125
  second?: Input,
124
126
  ): Promise<(Sema & { id: number }) | undefined> {
127
+ invalidateStructuralCaches(this.mind);
128
+ invalidateJunctionCache(this.mind);
125
129
  // One shape-reading for both ingest paths — see {@link dispatchIngest}.
126
130
  return dispatchIngest(
127
131
  input,
package/src/meter.ts CHANGED
@@ -176,6 +176,11 @@ export class Meter {
176
176
  /** Nodes popped by those ascents, against their √N·W budget — the counter
177
177
  * that shows whether the walks are deciding early or burning the budget. */
178
178
  junctionPops = 0;
179
+ /** Arbitrary byte spans whose distributional company was VSA-bundled from
180
+ * existing episode halos. */
181
+ spanHalos = 0;
182
+ /** Canonical W-windows examined while composing those span halos. */
183
+ spanHaloWindows = 0;
179
184
 
180
185
  /** `lightestDerivation` searches started. */
181
186
  searches = 0;
@@ -2275,6 +2275,16 @@ async function crossRegionVotes(
2275
2275
  // the same container (or a sub-container of it) twice.
2276
2276
  const consumed = new Set<number>();
2277
2277
  let probes = 0;
2278
+ // Once atoms themselves are hubs (N > W²), the cross-region analysis gets
2279
+ // one k·W walk allowance per evidence tier. Without a shared allowance,
2280
+ // each of k candidate pairs spends the full corpus-derived budget and a
2281
+ // cumulative dialogue multiplies bounded work into tens of seconds. Small
2282
+ // corpora retain exhaustive exact traversal: below this same scale the
2283
+ // budget would be smaller than the structures the tests deliberately build.
2284
+ const marketScale = k * ctx.space.maxGroup;
2285
+ const corpusScale = N > marketScale ** 3;
2286
+ const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
2287
+ const synonymBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
2278
2288
 
2279
2289
  for (let a = 0; a < cand.length && probes < k; a++) {
2280
2290
  if (consumed.has(cand[a])) continue;
@@ -2284,6 +2294,12 @@ async function crossRegionVotes(
2284
2294
  const rb = regions[cand[b]];
2285
2295
  if (!strong.has(cand[a]) && !strong.has(cand[b])) continue;
2286
2296
  if (ra.end >= rb.start) continue; // overlap or adjacent — nothing between
2297
+ // In a cumulative conversation, an old↔old interaction cannot explain
2298
+ // the user turn currently being answered; it was already available
2299
+ // before that turn existed. Keep old↔current pairs (the current turn may
2300
+ // refer to a prior answer), but do not repeatedly spend the junction
2301
+ // budget recomposing two regions wholly before the current boundary.
2302
+ if (ctx.currentTurnStart > 0 && rb.end <= ctx.currentTurnStart) continue;
2287
2303
  // Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
2288
2304
  // that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
2289
2305
  // genuine, individually-corroborated evidence about what fills the gap
@@ -2364,7 +2380,7 @@ async function crossRegionVotes(
2364
2380
  cap,
2365
2381
  seedsOf(cand[a]),
2366
2382
  seedsOf(cand[b]),
2367
- undefined,
2383
+ exactBudget,
2368
2384
  true,
2369
2385
  );
2370
2386
  if (probe) {
@@ -2384,6 +2400,7 @@ async function crossRegionVotes(
2384
2400
  maxInterior,
2385
2401
  true,
2386
2402
  sides,
2403
+ synonymBudget,
2387
2404
  );
2388
2405
  if (probe) {
2389
2406
  const singleAttempted = sides.leftSiblings.length > 0 ||
@@ -26,11 +26,11 @@
26
26
  // them reused across ≥ 2 containers (the same "≥ 2 structural parents"
27
27
  // bar propagateSuffixes gates suffix inheritance with). An untrained
28
28
  // word ("deadliest") has no stored windows and can never substitute.
29
- // • GEOMETRIC IDENTITY — the two spans' own perceived gists must clear
30
- // conceptThreshold(D), the same "same concept" bar haloSiblings and
31
- // articulation already gate on. This is what separates a synonym pair
32
- // the fold geometry genuinely identifies ("biggest"~"largest", sharing
33
- // most of their bytes and their role) from an arbitrary co-frame word.
29
+ // • GRADED IDENTITY — lexical geometry is tried first at
30
+ // conceptThreshold(D). Differently-spelled forms fall through to VSA
31
+ // company: their stored W-window occurrences ascend to learned episodes,
32
+ // whose bundled halos must clear significanceBar(D), the same
33
+ // distributional-evidence bar used by analogyStrength.
34
34
  //
35
35
  // A candidate context is accepted when its aligned-plus-substituted spans
36
36
  // DOMINATE the query (the same half-dominance predicate used throughout)
@@ -41,9 +41,9 @@
41
41
  //
42
42
  // COST: nothing on any answering path — the bridge runs only where the
43
43
  // alternative was silence. There it pays O(|query|) content-hash probes
44
- // (the propagateSuffixes trick), at most W anchor climbs and hubBound
45
- // candidate reads, and one O(|query|·|candidate|)-bounded alignment each —
46
- // all capped by existing derived bounds (W, chainReach, hubBound).
44
+ // (the propagateSuffixes trick), at most W anchor climbs and
45
+ // 2·recallQueryK candidate reads, and one
46
+ // O(|query|·|candidate|)-bounded alignment each.
47
47
  //
48
48
  // FIXED WRONG-ANSWER GAP (found and closed 2026-07-20): a proper-noun swap
49
49
  // could pass both derived gates above and voice a WRONG fact. Live case:
@@ -90,8 +90,8 @@
90
90
  // boiling-point and lowercase-France bridge wins are unaffected; full
91
91
  // suite green (358/358).
92
92
 
93
- import { cosine } from "../vec.js";
94
- import { conceptThreshold, dominates } from "../geometry.js";
93
+ import { cosine, type Vec } from "../vec.js";
94
+ import { conceptThreshold, dominates, significanceBar } from "../geometry.js";
95
95
  import { bytesEqual, indexOf } from "../bytes.js";
96
96
  import type { MindContext } from "./types.js";
97
97
  import { foldTree, perceive, read } from "./primitives.js";
@@ -103,6 +103,8 @@ import {
103
103
  sharedReachMemo,
104
104
  } from "./traverse.js";
105
105
  import { rItem, rNode } from "./trace.js";
106
+ import { junctionContainersFrom } from "./junction.js";
107
+ import { spanHalo } from "./match.js";
106
108
 
107
109
  /** One accepted substitution: query span [qs,qe) stands in for the
108
110
  * candidate context's span — recorded for the rationale trace. */
@@ -276,8 +278,45 @@ async function bridgeImpl(
276
278
  if (query.length < 2 * W) return null;
277
279
  const bound = hubBound(ctx);
278
280
  const N = corpusN(ctx);
281
+ const marketScale = ctx.cfg.recallQueryK * W;
282
+ const candidateCap = N <= marketScale ** 3 ? bound : 2 * ctx.cfg.recallQueryK;
279
283
  const bar = conceptThreshold(ctx.store.D);
284
+ const synonymBar = significanceBar(ctx.store.D);
280
285
  const reachCap = chainReach(W);
286
+ const diagnostics = ctx.trace
287
+ ? {
288
+ anchors: 0,
289
+ picked: 0,
290
+ proposed: 0,
291
+ structuralProposed: 0,
292
+ proposedGrounded: 0,
293
+ synonymChecks: 0,
294
+ bestSynonym: 0,
295
+ climbed: 0,
296
+ phraseScale: 0,
297
+ seeded: 0,
298
+ aligned: 0,
299
+ structurallyValid: 0,
300
+ coverageValid: 0,
301
+ identityValid: 0,
302
+ knownContentValid: 0,
303
+ bestCovered: 0,
304
+ bestRank: -1,
305
+ closest: [] as Array<{
306
+ id: number;
307
+ covered: number;
308
+ leading: number;
309
+ trailing: number;
310
+ gaps: number;
311
+ substitutions: number;
312
+ queryGapBytes: number;
313
+ candidateGapBytes: number;
314
+ gapRanges: Array<[number, number, number, number]>;
315
+ candidateSurplus: number;
316
+ gapsExplained: boolean;
317
+ }>,
318
+ }
319
+ : null;
281
320
 
282
321
  // PHRASE-SCALE CANDIDATE CAP — the same |content|·W bound the weave
283
322
  // (pipeline-mechanism.ts), the cross-region junction ladder's
@@ -332,7 +371,18 @@ async function bridgeImpl(
332
371
  if (rarity === 0) continue;
333
372
  anchors.push({ off: o, id, rarity });
334
373
  }
335
- if (anchors.length === 0) return null;
374
+ if (diagnostics) diagnostics.anchors = anchors.length;
375
+ if (anchors.length === 0) {
376
+ ctx.trace?.step(
377
+ "substitutionBridge",
378
+ [rItem(query, "query")],
379
+ [],
380
+ "no stored query window can anchor a corroborated substitution",
381
+ undefined,
382
+ diagnostics!,
383
+ );
384
+ return null;
385
+ }
336
386
  // CORROBORATION (see the module-level doc) over the precomputed window
337
387
  // facts: the query span [qs,qe) attests when every full W-window inside
338
388
  // it is a stored flat form and at least one is reused across ≥ 2
@@ -394,17 +444,33 @@ async function bridgeImpl(
394
444
  to: number,
395
445
  ): boolean => {
396
446
  if (to - from < W) return true;
397
- for (let o = from; o + W <= to; o++) {
398
- const ids = leafIdRun(ctx, bytes, o, o + W);
399
- if (ids === null) return false;
400
- const wid = ctx.store.findBranch(ids);
401
- if (wid === null) return false;
402
- const r = edgeAncestors(ctx, wid, N, reachMemo);
403
- if (r.saturated) continue; // in too many places to discriminate
404
- if (r.roots.length === 0) return false; // reaches nothing: novel content
405
- if (!dominates(r.contextsReached, N)) return false;
447
+ const common = (start: number, end: number): boolean => {
448
+ if (end - start < W) return false;
449
+ for (let o = start; o + W <= end; o++) {
450
+ const ids = leafIdRun(ctx, bytes, o, o + W);
451
+ if (ids === null) return false;
452
+ const wid = ctx.store.findBranch(ids);
453
+ if (wid === null) return false;
454
+ const r = edgeAncestors(ctx, wid, N, reachMemo);
455
+ if (r.saturated) continue; // in too many places to discriminate
456
+ if (r.roots.length === 0) return false; // reaches nothing: novel content
457
+ if (!dominates(r.contextsReached, N)) return false;
458
+ }
459
+ return true;
460
+ };
461
+ if (common(from, to)) return true;
462
+ // Alignment may attach the shared delimiter to either side of an inserted
463
+ // phrase. Up to W-1 boundary bytes are below the fold's identity scale;
464
+ // classify the phrase by a full-window interior core when one exists.
465
+ // This does not erase a short discriminative insertion: "heavy" still
466
+ // leaves the full `heav`/`eavy` windows for the corpus-global test.
467
+ for (let left = 0; left < W; left++) {
468
+ for (let right = 0; right < W; right++) {
469
+ if (left + right === 0 || left + right >= W) continue;
470
+ if (common(from + left, to - right)) return true;
471
+ }
406
472
  }
407
- return true;
473
+ return false;
408
474
  };
409
475
 
410
476
  anchors.sort((a, b) => a.rarity - b.rarity);
@@ -415,7 +481,7 @@ async function bridgeImpl(
415
481
  if (picked.some((p) => Math.abs(p.off - a.off) < W)) continue;
416
482
  picked.push(a);
417
483
  }
418
-
484
+ if (diagnostics) diagnostics.picked = picked.length;
419
485
  // 2. Candidate trained contexts. Two proposal channels, one verifier:
420
486
  // (a) the caller's PROPOSED hits — recall's whole-query resonance
421
487
  // ranking, the retrieval structure built to surface near-paraphrase
@@ -425,20 +491,56 @@ async function bridgeImpl(
425
491
  // candidate passes the same byte-exact alignment and gates below.
426
492
  const seen = new Set<number>();
427
493
  const candidates: number[] = [];
428
- // Proposal channel carries its caller's own bound (recall's resonance
429
- // k), so it neither consumes the climb's hub budget (on a small corpus
430
- // √N is a handful and the proposals would crowd the climb out entirely)
431
- // nor lets per-candidate byte work grow past that bound. A proposal may
432
- // be a FLAT content twin whose continuation edge lives on the
433
- // fold-shaped deposit node with the same bytes the same twin split
434
- // canonResolve bridges by re-folding (primitives.ts) but the re-fold
435
- // (a full perceive of the candidate's bytes) is paid only for proposals
436
- // that could align at all: alignment can only seed at a picked anchor
437
- // window occurring literally in the candidate (measured: unconditional
438
- // re-folds multiplied the refusal-path latency several-fold).
439
- // FIRST TOUCH of the caller's proposals — past every gate that could have
440
- // refused without them (see substitutionBridge's doc).
441
- for (const sid of await proposed()) {
494
+ // Exact co-occurrence proposes contexts the whole-form ANN can miss when a
495
+ // short insertion shifts every later fold boundary. The byte alignment below
496
+ // remains the decider. All pairs share one candidateCap·W junction
497
+ // allowance, ordered
498
+ // by their rarest side and then span: a rare content window joined to a
499
+ // distant frame boundary discriminates a whole question better than two
500
+ // neighbouring rare windows inside the same word.
501
+ if (query.length <= 2 * reachCap) {
502
+ const pairs: Array<[typeof picked[number], typeof picked[number]]> = [];
503
+ for (let i = 0; i < picked.length; i++) {
504
+ for (let j = i + 1; j < picked.length; j++) {
505
+ pairs.push([picked[i], picked[j]]);
506
+ }
507
+ }
508
+ pairs.sort((a, b) =>
509
+ Math.min(a[0].rarity, a[1].rarity) -
510
+ Math.min(b[0].rarity, b[1].rarity) ||
511
+ Math.abs(b[0].off - b[1].off) - Math.abs(a[0].off - a[1].off) ||
512
+ a[0].rarity + a[1].rarity - b[0].rarity - b[1].rarity
513
+ );
514
+ const structuralBudget = {
515
+ n: chainReach(W) * W * ctx.cfg.recallQueryK,
516
+ };
517
+ for (const [left, right] of pairs.slice(0, W)) {
518
+ const found = junctionContainersFrom(
519
+ ctx,
520
+ query.subarray(left.off, left.off + W),
521
+ query.subarray(right.off, right.off + W),
522
+ capBytes,
523
+ [left.id],
524
+ [right.id],
525
+ structuralBudget,
526
+ true,
527
+ );
528
+ for (const hit of found) {
529
+ if (candidates.length >= candidateCap) break;
530
+ if (seen.has(hit.id) || !ctx.store.hasNext(hit.id)) continue;
531
+ seen.add(hit.id);
532
+ candidates.push(hit.id);
533
+ if (diagnostics) diagnostics.structuralProposed++;
534
+ }
535
+ }
536
+ }
537
+ // Once exact structural proposals fill the shared cap, no caller proposal
538
+ // can enter the verifier. Do not evaluate the lazy ANN thunk merely to
539
+ // discard every result at the loop's first guard.
540
+ const proposedIds = candidates.length < candidateCap ? await proposed() : [];
541
+ if (diagnostics) diagnostics.proposed = proposedIds.length;
542
+ for (const sid of proposedIds) {
543
+ if (candidates.length >= candidateCap) break;
442
544
  if (seen.has(sid)) continue;
443
545
  seen.add(sid);
444
546
  const tb = candidateBytes(sid);
@@ -457,32 +559,62 @@ async function bridgeImpl(
457
559
  seen.add(use);
458
560
  }
459
561
  candidates.push(use);
562
+ if (diagnostics) diagnostics.proposedGrounded++;
460
563
  }
564
+ // Proposal channel — carries its caller's own bound (recall's resonance
565
+ // k), sharing the 2·recallQueryK candidate allowance
566
+ // with the structural and climb channels. A proposal may
567
+ // be a FLAT content twin whose continuation edge lives on the
568
+ // fold-shaped deposit node with the same bytes — the same twin split
569
+ // canonResolve bridges by re-folding (primitives.ts) — but the re-fold
570
+ // (a full perceive of the candidate's bytes) is paid only for proposals
571
+ // that could align at all: alignment can only seed at a picked anchor
572
+ // window occurring literally in the candidate (measured: unconditional
573
+ // re-folds multiplied the refusal-path latency several-fold).
574
+ // FIRST TOUCH of the caller's proposals — past every gate that could have
575
+ // refused without them (see substitutionBridge's doc).
461
576
  // Climb channel — edge-bearing ancestors only, decided by the indexed
462
577
  // O(1) hasNext; no byte is read here (the climb visits hundreds of
463
578
  // roots, and reading each was measured to dominate the refusal path).
464
- const proposedCount = candidates.length;
465
579
  for (const a of picked) {
466
- const reach = edgeAncestors(ctx, a.id, N);
580
+ const reach = edgeAncestors(ctx, a.id, N, reachMemo);
467
581
  for (const sid of reach.roots) {
468
- if (candidates.length - proposedCount >= bound) break;
582
+ if (candidates.length >= candidateCap) break;
469
583
  if (seen.has(sid)) continue;
470
584
  seen.add(sid);
471
585
  if (!ctx.store.hasNext(sid)) continue;
472
586
  candidates.push(sid);
587
+ if (diagnostics) diagnostics.climbed++;
473
588
  }
474
- if (candidates.length - proposedCount >= bound) break;
589
+ if (candidates.length >= candidateCap) break;
475
590
  }
476
-
477
591
  // 3. Align each candidate; gate its mismatches; keep the best.
478
592
  // Over-cap candidates are dropped here rather than earlier: the climb
479
593
  // channel deliberately reads no bytes while collecting (the climb visits
480
594
  // hundreds of roots), so this is where its proposals are first sized.
481
- const allBytes = new Map<number, Uint8Array>();
482
- for (const sid of candidates) {
595
+ //
596
+ // Candidate bytes are read LAZILY — on first access during the seed
597
+ // check — not eagerly for every collected id. On a 325K-context store
598
+ // the climb channel alone can propose hundreds of edge-bearing ancestors
599
+ // (hubBound = 571), most of which will never contain a picked anchor
600
+ // window and would be discarded at the seed check without their bytes
601
+ // ever being consulted. Eager reads for 500+ candidates each traversing
602
+ // the DAG (profiled at 12K node records and 73KB of bytes read per
603
+ // refusing query) is the dominant remaining bridge cost after the ANN
604
+ // gate. A Map stays available for the frame-unanimity scan below, which
605
+ // only needs bytes of candidates that actually seeded.
606
+ const seededBytes = new Map<number, Uint8Array>();
607
+ /** Read a candidate's bytes once; cache for the seed check AND for the
608
+ * frame-unanimity scan that follows alignment. Returns null when the
609
+ * candidate exceeds the phrase-scale cap or has no content. */
610
+ const bytesOfCandidate = (sid: number): Uint8Array | null => {
611
+ const hit = seededBytes.get(sid);
612
+ if (hit !== undefined) return hit;
483
613
  const b = candidateBytes(sid);
484
- if (b !== null) allBytes.set(sid, b);
485
- }
614
+ if (b !== null) seededBytes.set(sid, b);
615
+ return b;
616
+ };
617
+ if (diagnostics) diagnostics.phraseScale = seededBytes.size;
486
618
 
487
619
  // FRAME UNANIMITY: a substitution U → C inside the frame (Lf, Rf) is
488
620
  // groundable only when the collected candidates — the store's own sample
@@ -507,7 +639,7 @@ async function bridgeImpl(
507
639
  lf: Uint8Array,
508
640
  rf: Uint8Array,
509
641
  ): boolean => {
510
- for (const bytes of allBytes.values()) {
642
+ for (const bytes of seededBytes.values()) {
511
643
  let from = 0;
512
644
  for (;;) {
513
645
  const i = indexOf(bytes, lf, from);
@@ -540,9 +672,19 @@ async function bridgeImpl(
540
672
  // check — verified live).
541
673
  let best: BridgeHit | null = null;
542
674
  let bestAccounted = 0;
543
- for (const sid of candidates) {
544
- const cBytes = allBytes.get(sid);
545
- if (cBytes === undefined) continue;
675
+ const queryHaloMemo = new Map<string, Vec | null>();
676
+ const candidateHaloMemo = new Map<string, Vec | null>();
677
+ for (
678
+ let candidateIndex = 0;
679
+ candidateIndex < candidates.length;
680
+ candidateIndex++
681
+ ) {
682
+ const sid = candidates[candidateIndex];
683
+ // Read bytes lazily — most climb-proposed candidates have no picked
684
+ // anchor window and will never pass the seed check below, so their
685
+ // bytes are never read at all.
686
+ const cBytes = bytesOfCandidate(sid);
687
+ if (cBytes === null) continue;
546
688
  // Seed at the rarest picked anchor that literally occurs in this
547
689
  // candidate.
548
690
  let seed: { qo: number; co: number } | null = null;
@@ -554,7 +696,29 @@ async function bridgeImpl(
554
696
  }
555
697
  }
556
698
  if (seed === null) continue;
699
+ if (diagnostics) diagnostics.seeded++;
557
700
  const { matched, gaps } = align(ctx, query, cBytes, seed.qo, seed.co);
701
+ if (diagnostics) diagnostics.aligned++;
702
+
703
+ // Investment gate: even treating every two-sided mismatch as a valid
704
+ // synonym, can this alignment satisfy the bridge's final coverage rule?
705
+ // Distributional span composition performs bounded ancestor climbs; never
706
+ // pay for it on a candidate arithmetic already proves cannot win.
707
+ let matchStart = query.length;
708
+ let matchEnd = 0;
709
+ let potential = 0;
710
+ for (const [s, e] of matched) {
711
+ matchStart = Math.min(matchStart, s);
712
+ matchEnd = Math.max(matchEnd, e);
713
+ potential += e - s;
714
+ }
715
+ for (const g of gaps) {
716
+ if (g.qe > g.qs && g.ce > g.cs) potential += g.qe - g.qs;
717
+ }
718
+ if (
719
+ matchStart > W || query.length - matchEnd > W ||
720
+ !dominates(potential, query.length)
721
+ ) continue;
558
722
 
559
723
  // Gate each mismatch: a corroborated, geometrically-identified
560
724
  // substitution counts as accounted; anything else stays a gap.
@@ -594,12 +758,14 @@ async function bridgeImpl(
594
758
  // happen to share a few letters. Uses the SAME dominates() bar
595
759
  // (part*2 > whole) applied throughout the codebase, symmetrically:
596
760
  // the smaller raw side must be more than half the larger. Applies
597
- // to the RAW gap, before expansion — expansion only ever grows both
598
- // sides by IDENTICAL absorbed bytes, so it cannot fix an imbalance
599
- // that was already there.
761
+ // to the RAW gap for GEOMETRIC identity, before expansion — expansion
762
+ // only ever grows both sides by IDENTICAL absorbed bytes, so it cannot
763
+ // fix an imbalance that was already there. Distributional synonym
764
+ // evidence is exempt: two phrases may occupy the same role at very
765
+ // different lengths.
600
766
  let accepted = false;
601
767
  const balanced = dominates(Math.min(uLen, cLen), Math.max(uLen, cLen));
602
- const maxExtra = balanced ? reachCap - Math.max(uLen, cLen) : -1;
768
+ const maxExtra = reachCap - Math.max(uLen, cLen);
603
769
  outer:
604
770
  for (let extra = 0; extra <= maxExtra; extra++) {
605
771
  for (let a = 0; a <= extra; a++) {
@@ -622,7 +788,36 @@ async function bridgeImpl(
622
788
  if (!attestedQ(qs2, qe2)) continue;
623
789
  const u = query.subarray(qs2, qe2);
624
790
  const cSpan = cBytes.subarray(cs2, ce2);
625
- if (cosine(perceive(ctx, u).v, perceive(ctx, cSpan).v) < bar) {
791
+ const geometric = cosine(perceive(ctx, u).v, perceive(ctx, cSpan).v);
792
+ const qKey = `${qs2}:${qe2}`;
793
+ let qHalo = queryHaloMemo.get(qKey);
794
+ if (qHalo === undefined) {
795
+ qHalo = spanHalo(ctx, query, qs2, qe2);
796
+ queryHaloMemo.set(qKey, qHalo);
797
+ }
798
+ const cKey = `${sid}:${cs2}:${ce2}`;
799
+ let cHalo = candidateHaloMemo.get(cKey);
800
+ if (cHalo === undefined) {
801
+ cHalo = spanHalo(ctx, cBytes, cs2, ce2);
802
+ candidateHaloMemo.set(cKey, cHalo);
803
+ }
804
+ const distributional = qHalo !== null && cHalo !== null
805
+ ? cosine(qHalo, cHalo)
806
+ : 0;
807
+ if (diagnostics) {
808
+ diagnostics.synonymChecks++;
809
+ diagnostics.bestSynonym = Math.max(
810
+ diagnostics.bestSynonym,
811
+ distributional,
812
+ );
813
+ }
814
+ // Graded identity: byte geometry remains the cheap first tier;
815
+ // VSA company is the synonym tier when differently-spelled forms
816
+ // occupy the same learnt distributional role.
817
+ if (
818
+ (!balanced || geometric < bar) &&
819
+ distributional < synonymBar
820
+ ) {
626
821
  continue;
627
822
  }
628
823
  if (
@@ -655,6 +850,7 @@ async function bridgeImpl(
655
850
  // mechanism exists to explain SUBSTITUTIONS; a query needing none is
656
851
  // recall's job, not the bridge's.
657
852
  if (!ok) continue;
853
+ if (diagnostics) diagnostics.structurallyValid++;
658
854
 
659
855
  // Coverage: matched runs plus accepted substitutions must dominate the
660
856
  // query, every interior gap already proved ≤ W above, and the EDGES
@@ -675,8 +871,35 @@ async function bridgeImpl(
675
871
  covered += e - Math.max(s, reachEnd);
676
872
  reachEnd = Math.max(reachEnd, e);
677
873
  }
874
+ if (diagnostics) {
875
+ diagnostics.bestCovered = Math.max(diagnostics.bestCovered, covered);
876
+ const candidateGapBytes = gaps.reduce(
877
+ (n, g) => n + g.ce - g.cs,
878
+ 0,
879
+ );
880
+ diagnostics.closest.push({
881
+ id: sid,
882
+ covered,
883
+ leading: spans[0][0],
884
+ trailing: query.length - reachEnd,
885
+ gaps: gaps.length,
886
+ substitutions: subs.length,
887
+ queryGapBytes: gaps.reduce((n, g) => n + g.qe - g.qs, 0),
888
+ candidateGapBytes,
889
+ gapRanges: gaps.map((g) => [g.qs, g.qe, g.cs, g.ce]),
890
+ candidateSurplus: cBytes.length - covered - candidateGapBytes,
891
+ gapsExplained: gaps.every((g) => explainedSpan(cBytes, g.cs, g.ce)),
892
+ });
893
+ diagnostics.closest.sort((a, b) =>
894
+ b.covered - a.covered ||
895
+ a.leading + a.trailing - b.leading - b.trailing ||
896
+ a.id - b.id
897
+ );
898
+ if (diagnostics.closest.length > W) diagnostics.closest.length = W;
899
+ }
678
900
  if (spans[0][0] > W || query.length - reachEnd > W) continue;
679
901
  if (!dominates(covered, query.length)) continue;
902
+ if (diagnostics) diagnostics.coverageValid++;
680
903
 
681
904
  // ZERO-SUBSTITUTION ADMISSION — an IDENTITY claim, not a substitution.
682
905
  //
@@ -741,6 +964,7 @@ async function bridgeImpl(
741
964
  if (cBytes.length - covered - cGap > W) continue;
742
965
  if (!gaps.every((g) => explainedSpan(cBytes, g.cs, g.ce))) continue;
743
966
  }
967
+ if (diagnostics) diagnostics.identityValid++;
744
968
 
745
969
  // KNOWN content may never be dismissed — see dismissedKnownContent
746
970
  // (the live case: "what is the capital of france" aligning into a
@@ -748,10 +972,12 @@ async function bridgeImpl(
748
972
  // trained "France" — as a gap, while genuinely novel spans like
749
973
  // test/49's untrained "Name" remain tolerable).
750
974
  if (dismissedKnownQ(spans)) continue;
975
+ if (diagnostics) diagnostics.knownContentValid++;
751
976
 
752
977
  if (covered > bestAccounted) {
753
978
  bestAccounted = covered;
754
979
  best = { id: sid, accounted: spans, subs };
980
+ if (diagnostics) diagnostics.bestRank = candidateIndex;
755
981
  }
756
982
  }
757
983
 
@@ -767,6 +993,18 @@ async function bridgeImpl(
767
993
  ],
768
994
  `a trained context accounts for the query up to ${best.subs.length} ` +
769
995
  `corroborated substitution(s) — grounding through its learnt edges`,
996
+ undefined,
997
+ diagnostics!,
998
+ );
999
+ } else {
1000
+ ctx.trace?.step(
1001
+ "substitutionBridge",
1002
+ [rItem(query, "query")],
1003
+ [],
1004
+ "candidate contexts were proposed, but none passed the bridge's " +
1005
+ "structural identity and corroboration gates",
1006
+ undefined,
1007
+ diagnostics!,
770
1008
  );
771
1009
  }
772
1010
  return best;
@@ -95,9 +95,9 @@ export function junctionSeeds(ctx: MindContext, b: Uint8Array): number[] {
95
95
  return [wids[0], wids[wids.length - 1]];
96
96
  }
97
97
 
98
- /** Per-response cache of the identity walks' pure reads (capped bytes,
99
- * parent pages, container pages), keyed by the response lifecycle object
100
- * (ctx.climbMemo). One response issues many walks whose ancestries overlap
98
+ /** Session cache of the identity walks' pure reads (capped bytes,
99
+ * parent pages, container pages), keyed by the write-invalidated structural
100
+ * lifecycle object. One response issues many walks whose ancestries overlap
101
101
  * heavily (pair sides repeat across combos, and synonym walks revisit the
102
102
  * same neighbourhoods); the store is read-only while a response is in flight,
103
103
  * so every one of these reads is a pure function of the id — repeats cost a
@@ -110,17 +110,28 @@ export interface WalkCache {
110
110
  containers: Map<number, number[]>;
111
111
  }
112
112
  const walkCaches = new WeakMap<object, WalkCache>();
113
+ const WALK_CACHE_MAX = 100_000;
113
114
  export function walkCache(ctx: MindContext): WalkCache | null {
114
115
  if (ctx.climbMemo === null) return null;
115
- let c = walkCaches.get(ctx.climbMemo);
116
+ let c = walkCaches.get(ctx._structMemoKey);
116
117
  if (c === undefined) {
117
118
  walkCaches.set(
118
- ctx.climbMemo,
119
+ ctx._structMemoKey,
119
120
  c = { reads: new Map(), parents: new Map(), containers: new Map() },
120
121
  );
122
+ } else if (
123
+ c.reads.size + c.parents.size + c.containers.size >= WALK_CACHE_MAX
124
+ ) {
125
+ c.reads.clear();
126
+ c.parents.clear();
127
+ c.containers.clear();
121
128
  }
122
129
  return c;
123
130
  }
131
+
132
+ export function invalidateJunctionCache(ctx: MindContext): void {
133
+ walkCaches.delete(ctx._structMemoKey);
134
+ }
124
135
  export function cachedRead(
125
136
  ctx: MindContext,
126
137
  cache: WalkCache | null,
@@ -352,13 +363,15 @@ export async function junctionSynonyms(
352
363
  maxInterior: number,
353
364
  unordered = false,
354
365
  sides?: JunctionSynonymSides,
366
+ sharedBudget?: { n: number },
355
367
  ): Promise<SynonymJunction[]> {
356
368
  const s = sides ?? await loadJunctionSynonymSides(ctx, left, right);
357
369
  if (s.leftId === null && s.rightId === null) return [];
358
370
 
359
371
  // ── Tier 2.5a: single-synonym — one side replaced by a halo sibling ──────
360
372
  // ONE shared expansion budget across BOTH directions of this tier.
361
- const singleBudget = { n: hubBound(ctx) * ctx.space.maxGroup };
373
+ const singleBudget = sharedBudget ??
374
+ { n: hubBound(ctx) * ctx.space.maxGroup };
362
375
  const singleOut = new Map<number, SynonymJunction>();
363
376
  const keepBest = (
364
377
  map: Map<number, SynonymJunction>,
@@ -439,7 +452,8 @@ export async function junctionSynonyms(
439
452
  );
440
453
 
441
454
  const doubleOut = new Map<number, SynonymJunction>();
442
- const budget = { n: hubBound(ctx) * ctx.space.maxGroup };
455
+ const budget = sharedBudget ??
456
+ { n: hubBound(ctx) * ctx.space.maxGroup };
443
457
  const tries = Math.min(pairs.length, ctx.cfg.haloQueryK);
444
458
  for (let i = 0; i < tries; i++) {
445
459
  const { l, r, confidence } = pairs[i];