@hviana/sema 0.9.0 → 0.9.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 (118) hide show
  1. package/AGENTS.md +7 -7
  2. package/dist/src/alu/src/index.d.ts +1 -1
  3. package/dist/src/alu/src/index.js +1 -1
  4. package/dist/src/alu/src/parser.js +2 -6
  5. package/dist/src/alu/src/resonance.d.ts +13 -0
  6. package/dist/src/alu/src/resonance.js +41 -0
  7. package/dist/src/alu/test/alu.test.js +39 -0
  8. package/dist/src/bytes.d.ts +6 -2
  9. package/dist/src/bytes.js +10 -4
  10. package/dist/src/canon.js +44 -0
  11. package/dist/src/geometry.d.ts +19 -1
  12. package/dist/src/geometry.js +125 -141
  13. package/dist/src/meter.d.ts +33 -0
  14. package/dist/src/meter.js +34 -1
  15. package/dist/src/mind/articulation.js +14 -1
  16. package/dist/src/mind/attention.d.ts +12 -0
  17. package/dist/src/mind/attention.js +44 -16
  18. package/dist/src/mind/bridge.js +3 -3
  19. package/dist/src/mind/derivation.d.ts +40 -0
  20. package/dist/src/mind/derivation.js +34 -0
  21. package/dist/src/mind/evidence.d.ts +24 -0
  22. package/dist/src/mind/evidence.js +90 -0
  23. package/dist/src/mind/graph-search.d.ts +89 -15
  24. package/dist/src/mind/graph-search.js +345 -174
  25. package/dist/src/mind/learning.js +1 -1
  26. package/dist/src/mind/mechanisms/cover.d.ts +19 -3
  27. package/dist/src/mind/mechanisms/cover.js +142 -61
  28. package/dist/src/mind/mechanisms/recall.js +10 -3
  29. package/dist/src/mind/mind.d.ts +6 -0
  30. package/dist/src/mind/mind.js +5 -2
  31. package/dist/src/mind/pipeline.d.ts +5 -1
  32. package/dist/src/mind/pipeline.js +220 -90
  33. package/dist/src/mind/primitives.d.ts +25 -5
  34. package/dist/src/mind/primitives.js +107 -44
  35. package/dist/src/mind/reasoning.d.ts +18 -4
  36. package/dist/src/mind/reasoning.js +487 -328
  37. package/dist/src/mind/recognition.js +29 -13
  38. package/dist/src/mind/resonance.js +1 -11
  39. package/dist/src/mind/traverse.d.ts +45 -5
  40. package/dist/src/mind/traverse.js +285 -8
  41. package/dist/src/mind/types.d.ts +16 -1
  42. package/dist/src/store-sqlite.d.ts +25 -0
  43. package/dist/src/store-sqlite.js +89 -1
  44. package/dist/src/store.d.ts +48 -4
  45. package/dist/src/store.js +86 -6
  46. package/docs/INDEX.md +20 -19
  47. package/docs/INVARIANTS.md +17 -16
  48. package/docs/architecture/bounded-reads.md +1 -1
  49. package/docs/architecture/caches.md +5 -4
  50. package/docs/architecture/closure.md +45 -5
  51. package/docs/architecture/cost-model.md +16 -0
  52. package/docs/architecture/evidence.md +113 -0
  53. package/docs/architecture/exact-vs-approximate.md +10 -9
  54. package/docs/architecture/factored-machinery.md +14 -13
  55. package/docs/architecture/fold-contract.md +51 -1
  56. package/docs/architecture/mechanism-market.md +21 -0
  57. package/docs/architecture/memoization.md +3 -3
  58. package/docs/architecture/meter.md +2 -1
  59. package/docs/architecture/saturation.md +12 -0
  60. package/docs/architecture/store.md +25 -2
  61. package/docs/failures/tempting-but-wrong.md +13 -2
  62. package/docs/harness/gates.md +12 -10
  63. package/docs/mechanisms/cover.md +23 -6
  64. package/jsr.json +1 -1
  65. package/package.json +1 -1
  66. package/src/alu/README.md +10 -2
  67. package/src/alu/src/index.ts +1 -0
  68. package/src/alu/src/parser.ts +6 -6
  69. package/src/alu/src/resonance.ts +42 -0
  70. package/src/alu/test/alu.test.ts +40 -0
  71. package/src/bytes.ts +13 -3
  72. package/src/canon.ts +40 -0
  73. package/src/geometry.ts +183 -154
  74. package/src/meter.ts +34 -1
  75. package/src/mind/articulation.ts +14 -2
  76. package/src/mind/attention.ts +47 -25
  77. package/src/mind/bridge.ts +3 -3
  78. package/src/mind/derivation.ts +77 -0
  79. package/src/mind/evidence.ts +107 -0
  80. package/src/mind/graph-search.ts +449 -221
  81. package/src/mind/learning.ts +1 -7
  82. package/src/mind/match.ts +1 -2
  83. package/src/mind/mechanisms/cast.ts +1 -2
  84. package/src/mind/mechanisms/cover.ts +207 -87
  85. package/src/mind/mechanisms/extraction.ts +1 -2
  86. package/src/mind/mechanisms/prefix-completion.ts +1 -1
  87. package/src/mind/mechanisms/recall.ts +17 -5
  88. package/src/mind/mechanisms/reference.ts +1 -1
  89. package/src/mind/mind.ts +9 -30
  90. package/src/mind/pipeline.ts +263 -104
  91. package/src/mind/primitives.ts +119 -43
  92. package/src/mind/reasoning.ts +611 -419
  93. package/src/mind/recognition.ts +24 -9
  94. package/src/mind/resonance.ts +2 -16
  95. package/src/mind/trace.ts +1 -1
  96. package/src/mind/traverse.ts +321 -8
  97. package/src/mind/types.ts +15 -11
  98. package/src/store-sqlite.ts +92 -1
  99. package/src/store.ts +113 -7
  100. package/test/105-derive-through-reports-its-refusal.test.mjs +8 -5
  101. package/test/106-the-join-fires.test.mjs +21 -0
  102. package/test/111-the-cover-assembly-is-counted.test.mjs +8 -5
  103. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +18 -12
  104. package/test/136-the-two-named-limits.test.mjs +3 -2
  105. package/test/137-the-law-lives-once-and-below.test.mjs +21 -0
  106. package/test/148-exact-shortcuts-agree.test.mjs +188 -0
  107. package/test/149-the-closure-engine.test.mjs +138 -0
  108. package/test/150-the-join-is-output-sensitive.test.mjs +66 -0
  109. package/test/151-the-cover-pays-for-what-it-reaches.test.mjs +142 -0
  110. package/test/152-the-read-side-names-as-the-write-side.test.mjs +146 -0
  111. package/test/153-a-cheaper-bound-is-looked-at-first.test.mjs +155 -0
  112. package/test/154-the-question-names-the-step.test.mjs +281 -0
  113. package/test/24-generalization.test.mjs +32 -0
  114. package/test/36-bloom.test.mjs +53 -0
  115. package/test/37-cluster-dispersion-fusion.test.mjs +75 -0
  116. package/test/48-recognise-turn-connective.test.mjs +3 -2
  117. package/test/55-cost-meter.test.mjs +4 -4
  118. package/test/90-connector-read-cap.test.mjs +7 -7
@@ -11,13 +11,13 @@ import {
11
11
  canonResolve,
12
12
  foldTree,
13
13
  gistOf,
14
- latin1Key,
15
14
  perceive,
16
15
  resolve,
17
16
  } from "./primitives.js";
18
17
  import { atomIsHub, bearsEdge, corpusN, leadsSomewhere } from "./traverse.js";
19
18
  import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
20
19
  import { canonHash } from "../canon.js";
20
+ import { latin1 } from "../bytes.js";
21
21
  import { isChunk, type Sema } from "../sema.js";
22
22
  import type { Leaf, Site } from "./graph-search.js";
23
23
 
@@ -91,7 +91,7 @@ export function recognise(
91
91
  // not silent), so it is emitted here directly rather than only inside
92
92
  // recogniseImpl.
93
93
  if (ctx.recogniseMemo) {
94
- const key = latin1Key(bytes);
94
+ const key = latin1(bytes);
95
95
  const hit = ctx.recogniseMemo.get(key);
96
96
  if (hit !== undefined) {
97
97
  if (ctx.meter) ctx.meter.recogniseHits++;
@@ -495,8 +495,20 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
495
495
  // encoding is the identity, so the span's bytes ARE the branch key.
496
496
  // `subarray` is a view — this allocates nothing per probe, and the
497
497
  // bloom filter answers the misses without touching the database.
498
+ //
499
+ // Through ONE span prober (Store.flatSpans), because hashing the key was
500
+ // itself the quadratic: every probe hashed its span from the start, and
501
+ // the interior pass below sweeps up to `reach` ends past each endpoint —
502
+ // O(n · reach²) bytes hashed. Measured on the 31.7M-node store, one
503
+ // composition-regime response (#97 of the battery) probed 2,079,400
504
+ // spans and hashed 251,660,406 bytes for them. The prober extends each
505
+ // start's hash instead, so the same probes, answered identically, cost
506
+ // O(n · reach).
507
+ const spans = store.flatSpans?.(bytes) ?? null;
498
508
  const flatProbe = (start: number, end: number): number | null =>
499
- store.findFlatBranch
509
+ spans !== null
510
+ ? spans(start, end)
511
+ : store.findFlatBranch
500
512
  ? store.findFlatBranch(bytes.subarray(start, end))
501
513
  : store.findBranch(allLeafIds.slice(start, end));
502
514
  // THE TWO ROUTES COST DIFFERENT THINGS, SO THEY ARE PRICED SEPARATELY.
@@ -635,11 +647,15 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
635
647
  // simpler form won on that measurement.
636
648
  const reach = chainReach(W) * W * W + 2 * radius;
637
649
  if (ctx.meter) ctx.meter.recogniseInteriorGaps += ordered.length;
650
+ // Each end pairs only with the starts in [end − reach, end − W], in
651
+ // ascending order — the pairs, and the order the budget is spent in,
652
+ // of the all-pairs scan, without its O(n²) enumeration.
653
+ let lo = 0;
638
654
  for (const end of ordered) {
639
- for (const start of ordered) {
640
- if (start >= end) continue;
641
- const span = end - start;
642
- if (span < W || span > reach) continue;
655
+ while (ordered[lo] < end - reach) lo++;
656
+ for (let i = lo; i < ordered.length; i++) {
657
+ const start = ordered[i];
658
+ if (end - start < W) break;
643
659
  if (ctx.meter) ctx.meter.recogniseInteriorPairs++;
644
660
  spend(start, end);
645
661
  }
@@ -679,8 +695,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
679
695
  // arithmetic, not evidence. Removing it wholesale was measured and
680
696
  // REVERTED: it also drops legitimate multi-byte chains (the 12-byte
681
697
  // "Eiffel Tower" site vanished with it). The premise is wrong but the
682
- // trust it stood in for is real; a replacement signal is still open work.
683
- // See bench/README.md.
698
+ // trust it stood in for is real; the replacement signal follows.
684
699
  //
685
700
  // THE REPLACEMENT SIGNAL (2026-08-13): `leadsSomewhere` on the BYTE-EXACT
686
701
  // branch the chain already found. The blanket off-boundary suppression is
@@ -3,12 +3,11 @@
3
3
  // Address → Resonate → filter by Traverse/Read predicates → transform.
4
4
  // Used by bridge, recallByResonance, pivotInto, meaningOf.
5
5
  // (The graded locate() matcher formerly here lives in match.ts.)
6
- import { rItem, rNode } from "./trace.js";
7
- import { decodeText } from "./rationale.js";
6
+ import { rItem } from "./trace.js";
8
7
 
9
8
  import { cosine, Vec } from "../vec.js";
10
9
  import { mergeThreshold } from "../geometry.js";
11
- import { concat2, concatBytes, indexOf } from "../bytes.js";
10
+ import { concat2, concatBytes, indexOf, latin1 } from "../bytes.js";
12
11
  import type { MindContext } from "./types.js";
13
12
  import { gistOf, read, resolve, walkTree } from "./primitives.js";
14
13
  import { perceive } from "./primitives.js";
@@ -17,8 +16,6 @@ import {
17
16
  cachedRead,
18
17
  type Junction,
19
18
  junctionContainers,
20
- junctionContainersFrom,
21
- junctionSeeds,
22
19
  junctionSynonyms,
23
20
  walkCache,
24
21
  } from "./junction.js";
@@ -128,17 +125,6 @@ function junctionEdges(
128
125
  return out;
129
126
  }
130
127
 
131
- /** A byte string as a string, ONE code unit per byte — injective, so it is
132
- * safe to build a cache key from. Chunked to keep the spread within the
133
- * engine's argument limit on long contexts. */
134
- function latin1(b: Uint8Array): string {
135
- let s = "";
136
- for (let i = 0; i < b.length; i += 4096) {
137
- s += String.fromCharCode(...b.subarray(i, i + 4096));
138
- }
139
- return s;
140
- }
141
-
142
128
  /** Per-response memo of bridge results, keyed by the response's lifecycle
143
129
  * object (ctx.climbMemo — created fresh by respond() and nulled after, so
144
130
  * entries can never outlive the read-only window they are valid in). The
package/src/mind/trace.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import type { MindContext } from "./types.js";
8
8
  import { read } from "./primitives.js";
9
- import type { DerivationItem, DerivationStep } from "./graph-search.js";
9
+ import type { DerivationStep } from "./graph-search.js";
10
10
  import { decodeText } from "./rationale.js";
11
11
  import type { RationaleItem } from "./rationale.js";
12
12
 
@@ -9,13 +9,20 @@
9
9
  import { cosine, Vec } from "../vec.js";
10
10
  import type { AncestorReach, MindContext, SaturationStop } from "./types.js";
11
11
  import { gistOf, read } from "./primitives.js";
12
- import { canonicalWindows, leafIdPrefix, leafIdRun } from "./canonical.js";
12
+ import {
13
+ canonicalWindows,
14
+ chainReach,
15
+ leafIdPrefix,
16
+ leafIdRun,
17
+ } from "./canonical.js";
13
18
  // Imported at the TOP, where every other import is. They used to sit 800 lines
14
19
  // down under a note claiming the position mattered ("before trace module is
15
20
  // loaded") — it does not: an ES module's static imports are HOISTED, so the
16
21
  // file's line order never decides load order. The note described an intention
17
22
  // the runtime does not honour; the imports move and the claim goes.
18
23
  import { decodeText } from "./rationale.js";
24
+ import { latin1 } from "../bytes.js";
25
+ import { type WindowIndex, windowIndex, witness } from "./evidence.js";
19
26
  import type { RationaleItem } from "./rationale.js";
20
27
 
21
28
  // ── Session structural memo ─────────────────────────────────────────────
@@ -488,9 +495,9 @@ export function bearsEdge(ctx: MindContext, id: number): boolean {
488
495
  return cachedHasNext(ctx, id, getStructCache(ctx));
489
496
  }
490
497
 
491
- /** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
492
- * The admission predicate recognition filters sites with (cover.md): a form
493
- * that
498
+ /** Whether a node LEADS SOMEWHERE — the store's admission predicate
499
+ * ({@link Store.leadsSomewhere}: edge or halo) with its edge tier memoised for
500
+ * the response. Recognition filters sites with it (cover.md): a form that
494
501
  * leads nowhere contributes nothing to any derivation. Runs once per candidate
495
502
  * span on the recognition hot path — `hasNext` is cached per response (the same
496
503
  * flat-branch ids are probed across prefix variants by canonicalChunkId).
@@ -696,8 +703,10 @@ export function guidedNext(
696
703
  return pick;
697
704
  }
698
705
 
699
- /** Disambiguate among a node's learnt continuations by distributional
700
- * support. NOTE the `guide` contract: its VALUE is deliberately unused —
706
+ /** Disambiguate among a node's learnt continuations: first by the question's
707
+ * own witness of an establishing context (the exact tier, see
708
+ * {@link askedContinuations}), then by distributional support. NOTE the
709
+ * `guide` contract: its VALUE is deliberately unused —
701
710
  * only its PRESENCE gates disambiguation (a null guide means no query is in
702
711
  * flight, so structural walkers keep plain first-edge behaviour). The
703
712
  * gist-cosine of short answer candidates against a query guide is dominated
@@ -721,13 +730,30 @@ export function chooseNext(
721
730
  if (nx.length === 0) return undefined;
722
731
  if (nx.length === 1 || !guide) return nx[0];
723
732
 
733
+ // THE EXACT TIER — the continuation the QUESTION names. Every other
734
+ // disambiguation below reads popularity, and is right to refuse the gist (see
735
+ // the doc above); but the corpus also wrote down, for each continuation,
736
+ // WHICH QUESTIONS IT ANSWERS — its establishing contexts — and a question is
737
+ // not a gist. When one of them is witnessed by the asker's bytes plus the
738
+ // node's own, that continuation is the one being asked for. Measured on the
739
+ // 31.7M-node store: `Who is the father of Frederick II?` answered the
740
+ // citizenship fact (the most-poured of eight) while `Frederick II father` —
741
+ // one of the father fact's own establishing contexts — lay wholly inside the
742
+ // question. Exact, so it ranks first (exact-vs-approximate.md); when nothing
743
+ // is witnessed the ladder below decides exactly as before.
744
+ const asked = ctx._edgeAsked;
745
+ const named = asked === null ? null : askedContinuations(ctx, id, nx, asked);
746
+ if (named !== null && named.length === 1) return named[0];
747
+
724
748
  // Cap candidates at √N — the same bound the original chooseAmong used.
725
749
  // A hub context can accumulate thousands of continuations; the best-fit
726
750
  // one is among the first √N by insertion order (edges are never deleted,
727
751
  // so the oldest are the most established). A strongly-supported edge
728
752
  // inserted beyond the cap is invisible here — the deliberate trade
729
- // against paying O(fan-out) count reads on every disambiguation.
730
- const capped = nx; // already the hub-capped prefix, by the read above
753
+ // against paying O(fan-out) count reads on every disambiguation. Several
754
+ // continuations named EQUALLY by the question are told apart by the same
755
+ // ladder, over them alone.
756
+ const capped = named ?? nx; // already the hub-capped prefix, by the read above
731
757
 
732
758
  // Distributional-evidence disambiguation, consulting BOTH read-outs of the
733
759
  // evidence the training poured:
@@ -802,6 +828,213 @@ export function chooseNext(
802
828
  return best;
803
829
  }
804
830
 
831
+ /** The response canonicalizer's reading of `bytes` when it keeps every offset
832
+ * — so a window found in the canonical bytes sits at the same place in the
833
+ * asker's — else the bytes themselves. Text canon is offset-preserving on
834
+ * ASCII without interior whitespace runs; where it is not, the raw bytes are
835
+ * read and a case-variant window simply does not match. */
836
+ export function offsetCanon(ctx: MindContext, bytes: Uint8Array): Uint8Array {
837
+ if (ctx.canon === null) return bytes;
838
+ const c = ctx.canon(bytes);
839
+ return c.length === bytes.length ? c : bytes;
840
+ }
841
+
842
+ /** The continuations of `id` that `asked` NAMES (see {@link
843
+ * askedContinuations}) — for a caller holding material other than the whole
844
+ * question: the multi-hop walk asks with what of the question no product has
845
+ * restated yet. null when none is named. */
846
+ export function namedContinuations(
847
+ ctx: MindContext,
848
+ id: number,
849
+ asked: { bytes: Uint8Array; index: WindowIndex },
850
+ ): number[] | null {
851
+ const nx = ctx.store.nextFirst(id, hubBound(ctx));
852
+ return nx.length === 0 ? null : askedContinuations(ctx, id, nx, asked);
853
+ }
854
+
855
+ /** The continuations of `id` (among `nx`) that the question NAMES: one of
856
+ * their establishing contexts — a predecessor other than `id` itself — is
857
+ * wholly witnessed by the question plus `id`'s own bytes (evidence.ts), with
858
+ * the question supplying at least one window the node does not. Ranked by
859
+ * how much of the question witnesses it; null when none is named.
860
+ *
861
+ * THE NODE'S OWN BYTES ARE MATERIAL because a derivation stands on them. On
862
+ * the second hop of `Where was the place of death of the director of film
863
+ * Beat Girl?` the node is `Edmond T. Gréville` — reached, never written — and
864
+ * its fact's establishing context `Edmond T. Gréville place of death` is held
865
+ * by neither the question nor the node, only by both. Measured over 5,236
866
+ * held-out 2Wiki questions: such a context is wholly witnessed by the
867
+ * question alone 69 times, by the question and the first hop 2,153 times.
868
+ *
869
+ * BOUNDED: predecessor reads share one √N budget across the candidates (see
870
+ * the loop below), asked cheapest first; past it the tier abstains for the
871
+ * rest, metered, and the ladder decides as before.
872
+ * Each form is read at most to the material's length — a form longer than
873
+ * everything at hand cannot be wholly witnessed without repeating it. */
874
+ function askedContinuations(
875
+ ctx: MindContext,
876
+ id: number,
877
+ nx: readonly number[],
878
+ asked: { bytes: Uint8Array; index: WindowIndex },
879
+ ): number[] | null {
880
+ return askedEntry(ctx, id, nx, asked).named;
881
+ }
882
+
883
+ /** The question spans that NAMED `pick` among `id`'s continuations — the
884
+ * evidence a projection through that pick stands on, so a mechanism can
885
+ * account for what the question said about it (mechanism-market.md:
886
+ * evidence travels). Empty when the question names no continuation of `id`
887
+ * or names others. */
888
+ export function askedEvidence(
889
+ ctx: MindContext,
890
+ id: number,
891
+ pick: number,
892
+ ): Array<[number, number]> {
893
+ const asked = ctx._edgeAsked;
894
+ if (asked === null) return [];
895
+ const nx = ctx.store.nextFirst(id, hubBound(ctx));
896
+ const entry = askedEntry(ctx, id, nx, asked);
897
+ return entry.named?.includes(pick) ? entry.spans.get(pick) ?? [] : [];
898
+ }
899
+
900
+ interface AskedEntry {
901
+ named: number[] | null;
902
+ /** Per named continuation, the question spans that witnessed it. */
903
+ spans: Map<number, Array<[number, number]>>;
904
+ }
905
+
906
+ function askedEntry(
907
+ ctx: MindContext,
908
+ id: number,
909
+ nx: readonly number[],
910
+ asked: { bytes: Uint8Array; index: WindowIndex },
911
+ ): AskedEntry {
912
+ let memo = askedMemo.get(asked);
913
+ if (memo === undefined) askedMemo.set(asked, memo = new Map());
914
+ const hit = memo.get(id);
915
+ if (hit !== undefined && !ctx.trace) return hit;
916
+ const entry = askedContinuationsImpl(ctx, id, nx, asked);
917
+ memo.set(id, entry);
918
+ return entry;
919
+ }
920
+
921
+ /** One pick per node per question — every mechanism of a response asks the
922
+ * same node about the same question (the guided-pick memo's own reason). */
923
+ const askedMemo = new WeakMap<object, Map<number, AskedEntry>>();
924
+
925
+ function askedContinuationsImpl(
926
+ ctx: MindContext,
927
+ id: number,
928
+ nx: readonly number[],
929
+ asked: { bytes: Uint8Array; index: WindowIndex },
930
+ ): AskedEntry {
931
+ const none: AskedEntry = { named: null, spans: new Map() };
932
+ const W = ctx.space.maxGroup;
933
+ // A SATURATED READ IS NOT A CANDIDATE SET. When the continuations came back
934
+ // at the √N cap the read may have cut the named one off, so "none of these is
935
+ // named" and "this is the named one" are both unfounded — and this is exactly
936
+ // where witnessing would read most. The tier abstains, metered, and the
937
+ // distributional ladder decides as it always has.
938
+ if (nx.length >= hubBound(ctx)) {
939
+ if (ctx.meter) ctx.meter.askedReadsSaturated++;
940
+ return none;
941
+ }
942
+ const cache = getStructCache(ctx);
943
+ const ownCap = asked.bytes.length * W;
944
+ const own = offsetCanon(ctx, read(ctx, id, ownCap));
945
+ const ownIndex = windowIndex(own, W);
946
+ // Naming needs the question to say at least one window the node does not:
947
+ // when the node already holds every window of the question (the question IS
948
+ // this context, or a piece of it), nothing can be named, and nothing is read.
949
+ let beyond = false;
950
+ for (const key of asked.index.keys()) {
951
+ if (!ownIndex.has(key)) {
952
+ beyond = true;
953
+ break;
954
+ }
955
+ }
956
+ if (!beyond) return none;
957
+ const indexes = [asked.index, ownIndex];
958
+ const formCap = asked.bytes.length + own.length;
959
+ // BOUNDED READS (bounded-reads.md): the decision reads at most √N
960
+ // establishing contexts — floored at the write side's own arity `chainReach(W)`
961
+ // so a store too small for √N to cover one fact's questions still decides.
962
+ // Candidates are asked CHEAPEST FIRST (fewest establishing contexts): a common
963
+ // reply established by hundreds of contexts would otherwise spend the whole
964
+ // allowance alone. The order changes what is READ, never what wins: scores
965
+ // are compared afterwards in the continuations' own order.
966
+ let budget = Math.max(hubBound(ctx), chainReach(W));
967
+ const order = nx
968
+ .map((n, at) => ({ n, at, support: cachedPrevCount(ctx, n, cache) }))
969
+ .filter((c) => c.support >= 2) // only `id` establishes the rest
970
+ .sort((a, b) => a.support - b.support || a.at - b.at);
971
+ const scored: Array<
972
+ {
973
+ n: number;
974
+ at: number;
975
+ score: number;
976
+ by: number;
977
+ spans: Array<[number, number]>;
978
+ }
979
+ > = [];
980
+ for (const { n, at, support } of order) {
981
+ if (support > budget) {
982
+ if (ctx.meter) ctx.meter.askedReadsSaturated++;
983
+ break;
984
+ }
985
+ budget -= support;
986
+ if (ctx.meter) ctx.meter.askedPredecessorReads += support;
987
+ let score = 0;
988
+ let by = -1;
989
+ let spans: Array<[number, number]> = [];
990
+ for (const c of ctx.store.prevFirst(n, support)) {
991
+ if (c === id) continue;
992
+ // The form's FIRST window decides most refusals: one short prefix read
993
+ // before the whole form is reconstructed (a conversation-length
994
+ // predecessor would otherwise be read in full to fail on its opening).
995
+ const head = offsetCanon(ctx, read(ctx, c, W));
996
+ if (head.length < W) continue;
997
+ if (!indexes.some((ix) => ix.has(latin1(head)))) continue;
998
+ const form = read(ctx, c, formCap + 1);
999
+ if (form.length < W || form.length > formCap) continue;
1000
+ const w = witness(offsetCanon(ctx, form), indexes, W);
1001
+ if (!w.complete || w.bytes < W) continue;
1002
+ if (w.bytes > score) {
1003
+ score = w.bytes;
1004
+ by = c;
1005
+ spans = w.spans;
1006
+ }
1007
+ }
1008
+ if (score > 0) scored.push({ n, at, score, by, spans });
1009
+ }
1010
+ scored.sort((a, b) => a.at - b.at);
1011
+ let best: number[] = [];
1012
+ let bestBytes = 0;
1013
+ let witnessed: number | null = null;
1014
+ for (const { n, score, by } of scored) {
1015
+ if (score > bestBytes) {
1016
+ best = [n];
1017
+ bestBytes = score;
1018
+ witnessed = by;
1019
+ } else if (score === bestBytes) best.push(n);
1020
+ }
1021
+ if (best.length === 0) return none;
1022
+ if (ctx.meter) ctx.meter.askedContinuations++;
1023
+ if (ctx.trace && witnessed !== null) {
1024
+ ctx.trace.step(
1025
+ "askedContinuation",
1026
+ [rItemShort(ctx, id, "node"), rItemShort(ctx, witnessed, "asked")],
1027
+ best.map((n) => rItemShort(ctx, n, "named")),
1028
+ `${nx.length} continuations — the question witnesses ` +
1029
+ `${best.length === 1 ? "one's" : `${best.length}'`} own establishing ` +
1030
+ `context (${bestBytes} question byte(s) beyond the node)`,
1031
+ );
1032
+ }
1033
+ const evidence = new Map<number, Array<[number, number]>>();
1034
+ for (const c of scored) if (best.includes(c.n)) evidence.set(c.n, c.spans);
1035
+ return { named: best, spans: evidence };
1036
+ }
1037
+
805
1038
  /** The perceived gist of a candidate node, through the session gist cache.
806
1039
  * Re-gisting a candidate is a full river fold of its bytes — the measured
807
1040
  * recall bottleneck (a hub context offers up to √N continuations, EACH
@@ -903,6 +1136,86 @@ export function allWindowsAreScaffolding(
903
1136
  return sawOne;
904
1137
  }
905
1138
 
1139
+ /** Per offset of `bytes`: 1 when the W-window there is a stored form contained
1140
+ * in more than √N places (corpus-global scaffolding; see the floor below),
1141
+ * else 0. Memoised per
1142
+ * byte array for the life of the store's read-only response. */
1143
+ export function hubWindows(ctx: MindContext, bytes: Uint8Array): Uint8Array {
1144
+ const hit = hubWindowMemo.get(bytes);
1145
+ if (hit !== undefined) return hit;
1146
+ const W = ctx.space.maxGroup;
1147
+ // Floored at the write side's arity: inside ONE deposit's fold a window is
1148
+ // already contained by up to `chainReach(W)` chunks and branches, so on a
1149
+ // store of a few facts the √N reading would call every window frame — that
1150
+ // is fold structure, not corpus commonality.
1151
+ const bound = Math.max(hubBound(ctx), chainReach(W));
1152
+ const hub = new Uint8Array(Math.max(0, bytes.length - W + 1));
1153
+ for (let o = 0; o < hub.length; o++) {
1154
+ const ids = leafIdRun(ctx, bytes, o, o + W);
1155
+ const id = ids === null ? null : ctx.store.findBranch(ids);
1156
+ if (id !== null && ctx.store.containersSlice(id, bound, 1).length > 0) {
1157
+ hub[o] = 1;
1158
+ }
1159
+ }
1160
+ hubWindowMemo.set(bytes, hub);
1161
+ return hub;
1162
+ }
1163
+ const hubWindowMemo = new WeakMap<Uint8Array, Uint8Array>();
1164
+
1165
+ /** The query's SCAFFOLDING CORE, as spans: the bytes every W-window over which
1166
+ * is a hub (see {@link hubWindows}) — what is nothing but frame, where
1167
+ * {@link scaffoldExtents} is what a frame window reaches. */
1168
+ export function scaffoldSpans(
1169
+ ctx: MindContext,
1170
+ query: Uint8Array,
1171
+ ): Array<[number, number]> {
1172
+ const W = ctx.space.maxGroup;
1173
+ const hub = hubWindows(ctx, query);
1174
+ const n = hub.length;
1175
+ if (n <= 0) return [];
1176
+ const spans: Array<[number, number]> = [];
1177
+ let start = -1;
1178
+ for (let i = 0; i < query.length; i++) {
1179
+ let all = true;
1180
+ for (let o = Math.max(0, i - W + 1); o <= Math.min(i, n - 1); o++) {
1181
+ if (!hub[o]) {
1182
+ all = false;
1183
+ break;
1184
+ }
1185
+ }
1186
+ if (all && start < 0) start = i;
1187
+ if (!all && start >= 0) {
1188
+ spans.push([start, i]);
1189
+ start = -1;
1190
+ }
1191
+ }
1192
+ if (start >= 0) spans.push([start, query.length]);
1193
+ return spans;
1194
+ }
1195
+
1196
+ /** The EXTENTS of the query's SCAFFOLDING windows, merged: every byte some
1197
+ * W-window reaches that is a stored form contained in more than √N places —
1198
+ * corpus-global commonality (commonality.md), the same "hub" reading as
1199
+ * {@link allWindowsAreScaffolding} and the bridge's `explainedSpan`. A window
1200
+ * the store never saw is NOT scaffolding. The extent, not the core, is what
1201
+ * a coverage test needs: a span that still holds one hub window can be
1202
+ * "carried" by any fact that holds that window. */
1203
+ export function scaffoldExtents(
1204
+ ctx: MindContext,
1205
+ query: Uint8Array,
1206
+ ): Array<[number, number]> {
1207
+ const W = ctx.space.maxGroup;
1208
+ const hub = hubWindows(ctx, query);
1209
+ const spans: Array<[number, number]> = [];
1210
+ for (let o = 0; o < hub.length; o++) {
1211
+ if (!hub[o]) continue;
1212
+ const last = spans[spans.length - 1];
1213
+ if (last !== undefined && o <= last[1]) last[1] = o + W;
1214
+ else spans.push([o, o + W]);
1215
+ }
1216
+ return spans;
1217
+ }
1218
+
906
1219
  // ── THE PREFIX SUPPLY ───────────────────────────────────────────────────────
907
1220
  //
908
1221
  // A RETRIEVAL capability, not a grounding one: "which trained forms does this
package/src/mind/types.ts CHANGED
@@ -10,16 +10,9 @@ import type { Space } from "../sema.js";
10
10
  import type { Alphabet } from "../alphabet.js";
11
11
  import type { MindConfig } from "../config.js";
12
12
  import type { Meter } from "../meter.js";
13
- import type {
14
- ComputedResult,
15
- DerivationItem,
16
- DerivationStep,
17
- GraphSearch,
18
- Leaf,
19
- Seg,
20
- Site,
21
- } from "./graph-search.js";
13
+ import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
22
14
  import type { Rationale } from "./rationale.js";
15
+ import type { WindowIndex } from "./evidence.js";
23
16
  import type { ContentFold, Grid } from "../geometry.js";
24
17
 
25
18
  /** One {@link MindContext._depositTrees} entry — see that field's doc.
@@ -33,7 +26,7 @@ export interface DepositCacheEntry {
33
26
  /** The plain content fold's reusable segment state. */
34
27
  content: ContentFold;
35
28
  }
36
- import { bytesEqual, concatBytes, indexOf } from "../bytes.js";
29
+ import { bytesEqual, concatBytes } from "../bytes.js";
37
30
  import { restates } from "./derivation.js";
38
31
  import { dominates } from "../geometry.js";
39
32
 
@@ -199,7 +192,13 @@ export interface Attention {
199
192
  * a genuine further topic is named in its own distinctive wording
200
193
  * somewhere the query's scaffolding does not reach, always a SEPARATE
201
194
  * cluster from whatever else corroborates it. See
202
- * test/37-cluster-dispersion-fusion.test.mjs. */
195
+ * test/37-cluster-dispersion-fusion.test.mjs.
196
+ *
197
+ * Read from the VOTES, this is a lossy witness: a region votes once, for its
198
+ * top anchor, so a place can be lost to a tie or won through an accident.
199
+ * Fusion therefore also asks the root's CONTEXT the same question at window
200
+ * scale (reasoning.ts `sharedPlaces`) and trusts a root that either reading
201
+ * finds in two places. */
203
202
  clusters: number;
204
203
  }
205
204
 
@@ -413,6 +412,11 @@ export interface MindContext extends GraphSearchHost {
413
412
  * ordinary respond() and for the first turn of a conversation. */
414
413
  currentTurnStart: number;
415
414
  _edgeGuide: Vec | null;
415
+ /** The question currently being answered, as the material `chooseNext`'s
416
+ * exact tier witnesses a continuation's establishing contexts against —
417
+ * its bytes (canonical when the response's canon preserves offsets) and
418
+ * their window index. Set and cleared with `_edgeGuide`. */
419
+ _edgeAsked: { bytes: Uint8Array; index: WindowIndex } | null;
416
420
  _edgeChoice: Map<number, number>;
417
421
  _prevSeen: Set<number> | null;
418
422
  /** Session cache of node-id → perceived gist, for candidate scoring