@hviana/sema 0.4.0 → 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 (154) hide show
  1. package/CONTRIBUTING.md +92 -10
  2. package/LICENSE.md +2 -2
  3. package/package.json +1 -1
  4. package/src/ingest-cache.ts +4 -0
  5. package/src/meter.ts +5 -0
  6. package/src/mind/attention.ts +18 -1
  7. package/src/mind/bridge.ts +292 -54
  8. package/src/mind/junction.ts +21 -7
  9. package/src/mind/match.ts +92 -1
  10. package/src/mind/mechanisms/cast.ts +12 -0
  11. package/src/mind/mechanisms/confluence.ts +30 -1
  12. package/src/mind/mechanisms/cover.ts +36 -4
  13. package/src/mind/mechanisms/recall.ts +21 -44
  14. package/src/mind/mind.ts +39 -2
  15. package/src/mind/pipeline-mechanism.ts +86 -4
  16. package/src/mind/pipeline.ts +1 -1
  17. package/src/mind/reasoning.ts +15 -8
  18. package/src/mind/recognition.ts +40 -0
  19. package/src/mind/resonance.ts +0 -0
  20. package/src/mind/traverse.ts +17 -15
  21. package/src/mind/types.ts +10 -0
  22. package/test/49-natural-units-synonym-bridge.test.mjs +56 -15
  23. package/dist/example/demo.d.ts +0 -1
  24. package/dist/example/demo.js +0 -39
  25. package/dist/example/train_base.d.ts +0 -87
  26. package/dist/example/train_base.js +0 -2252
  27. package/dist/src/alphabet.d.ts +0 -7
  28. package/dist/src/alphabet.js +0 -33
  29. package/dist/src/alu/src/alu.d.ts +0 -185
  30. package/dist/src/alu/src/alu.js +0 -440
  31. package/dist/src/alu/src/expr.d.ts +0 -61
  32. package/dist/src/alu/src/expr.js +0 -318
  33. package/dist/src/alu/src/index.d.ts +0 -11
  34. package/dist/src/alu/src/index.js +0 -19
  35. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  36. package/dist/src/alu/src/kernel-arith.js +0 -264
  37. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  38. package/dist/src/alu/src/kernel-bits.js +0 -152
  39. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  40. package/dist/src/alu/src/kernel-logic.js +0 -60
  41. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  42. package/dist/src/alu/src/kernel-nd.js +0 -208
  43. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  44. package/dist/src/alu/src/kernel-numeric.js +0 -366
  45. package/dist/src/alu/src/operation.d.ts +0 -168
  46. package/dist/src/alu/src/operation.js +0 -189
  47. package/dist/src/alu/src/parser.d.ts +0 -221
  48. package/dist/src/alu/src/parser.js +0 -577
  49. package/dist/src/alu/src/resonance.d.ts +0 -55
  50. package/dist/src/alu/src/resonance.js +0 -126
  51. package/dist/src/alu/src/text.d.ts +0 -31
  52. package/dist/src/alu/src/text.js +0 -73
  53. package/dist/src/alu/src/value.d.ts +0 -109
  54. package/dist/src/alu/src/value.js +0 -300
  55. package/dist/src/alu/test/alu.test.d.ts +0 -1
  56. package/dist/src/alu/test/alu.test.js +0 -764
  57. package/dist/src/bytes.d.ts +0 -14
  58. package/dist/src/bytes.js +0 -59
  59. package/dist/src/canon.d.ts +0 -26
  60. package/dist/src/canon.js +0 -57
  61. package/dist/src/config.d.ts +0 -111
  62. package/dist/src/config.js +0 -91
  63. package/dist/src/derive/src/deduction.d.ts +0 -136
  64. package/dist/src/derive/src/deduction.js +0 -159
  65. package/dist/src/derive/src/index.d.ts +0 -8
  66. package/dist/src/derive/src/index.js +0 -11
  67. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  68. package/dist/src/derive/src/priority-queue.js +0 -73
  69. package/dist/src/derive/src/rewrite.d.ts +0 -56
  70. package/dist/src/derive/src/rewrite.js +0 -100
  71. package/dist/src/derive/src/trie.d.ts +0 -90
  72. package/dist/src/derive/src/trie.js +0 -217
  73. package/dist/src/derive/test/derive.test.d.ts +0 -1
  74. package/dist/src/derive/test/derive.test.js +0 -122
  75. package/dist/src/extension.d.ts +0 -37
  76. package/dist/src/extension.js +0 -7
  77. package/dist/src/geometry.d.ts +0 -175
  78. package/dist/src/geometry.js +0 -823
  79. package/dist/src/index.d.ts +0 -17
  80. package/dist/src/index.js +0 -19
  81. package/dist/src/ingest-cache.d.ts +0 -41
  82. package/dist/src/ingest-cache.js +0 -161
  83. package/dist/src/meter.d.ts +0 -171
  84. package/dist/src/meter.js +0 -269
  85. package/dist/src/mind/articulation.d.ts +0 -6
  86. package/dist/src/mind/articulation.js +0 -99
  87. package/dist/src/mind/attention.d.ts +0 -414
  88. package/dist/src/mind/attention.js +0 -2065
  89. package/dist/src/mind/bridge.d.ts +0 -39
  90. package/dist/src/mind/bridge.js +0 -738
  91. package/dist/src/mind/canonical.d.ts +0 -34
  92. package/dist/src/mind/canonical.js +0 -93
  93. package/dist/src/mind/graph-search.d.ts +0 -294
  94. package/dist/src/mind/graph-search.js +0 -996
  95. package/dist/src/mind/index.d.ts +0 -9
  96. package/dist/src/mind/index.js +0 -5
  97. package/dist/src/mind/junction.d.ts +0 -134
  98. package/dist/src/mind/junction.js +0 -331
  99. package/dist/src/mind/learning.d.ts +0 -75
  100. package/dist/src/mind/learning.js +0 -270
  101. package/dist/src/mind/match.d.ts +0 -166
  102. package/dist/src/mind/match.js +0 -565
  103. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  104. package/dist/src/mind/mechanisms/alu.js +0 -36
  105. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  106. package/dist/src/mind/mechanisms/cast.js +0 -773
  107. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  108. package/dist/src/mind/mechanisms/confluence.js +0 -225
  109. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  110. package/dist/src/mind/mechanisms/cover.js +0 -203
  111. package/dist/src/mind/mechanisms/extraction.d.ts +0 -33
  112. package/dist/src/mind/mechanisms/extraction.js +0 -300
  113. package/dist/src/mind/mechanisms/recall.d.ts +0 -16
  114. package/dist/src/mind/mechanisms/recall.js +0 -388
  115. package/dist/src/mind/mind.d.ts +0 -330
  116. package/dist/src/mind/mind.js +0 -594
  117. package/dist/src/mind/pipeline-mechanism.d.ts +0 -172
  118. package/dist/src/mind/pipeline-mechanism.js +0 -382
  119. package/dist/src/mind/pipeline.d.ts +0 -49
  120. package/dist/src/mind/pipeline.js +0 -275
  121. package/dist/src/mind/primitives.d.ts +0 -66
  122. package/dist/src/mind/primitives.js +0 -306
  123. package/dist/src/mind/rationale.d.ts +0 -139
  124. package/dist/src/mind/rationale.js +0 -163
  125. package/dist/src/mind/reasoning.d.ts +0 -40
  126. package/dist/src/mind/reasoning.js +0 -272
  127. package/dist/src/mind/recognition.d.ts +0 -20
  128. package/dist/src/mind/recognition.js +0 -463
  129. package/dist/src/mind/resonance.d.ts +0 -23
  130. package/dist/src/mind/resonance.js +0 -0
  131. package/dist/src/mind/trace.d.ts +0 -15
  132. package/dist/src/mind/trace.js +0 -73
  133. package/dist/src/mind/traverse.d.ts +0 -124
  134. package/dist/src/mind/traverse.js +0 -649
  135. package/dist/src/mind/types.d.ts +0 -323
  136. package/dist/src/mind/types.js +0 -130
  137. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  138. package/dist/src/rabitq-ivf/src/database.js +0 -201
  139. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  140. package/dist/src/rabitq-ivf/src/index.js +0 -4
  141. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  142. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  143. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  144. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  145. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  146. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  147. package/dist/src/sema.d.ts +0 -31
  148. package/dist/src/sema.js +0 -63
  149. package/dist/src/store-sqlite.d.ts +0 -184
  150. package/dist/src/store-sqlite.js +0 -942
  151. package/dist/src/store.d.ts +0 -678
  152. package/dist/src/store.js +0 -1703
  153. package/dist/src/vec.d.ts +0 -31
  154. package/dist/src/vec.js +0 -109
@@ -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];