@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
@@ -5,10 +5,11 @@
5
5
  // that leads somewhere (has a continuation edge or a halo).
6
6
  // segment — leaf-parent segmentation using the geometry's own groupings.
7
7
  import { rItem } from "./trace.js";
8
- import { canonResolve, foldTree, gistOf, latin1Key, perceive, resolve, } from "./primitives.js";
8
+ import { canonResolve, foldTree, gistOf, perceive, resolve, } from "./primitives.js";
9
9
  import { atomIsHub, bearsEdge, corpusN, leadsSomewhere } from "./traverse.js";
10
10
  import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
11
11
  import { canonHash } from "../canon.js";
12
+ import { latin1 } from "../bytes.js";
12
13
  import { isChunk } from "../sema.js";
13
14
  /** Decompose a byte stream into every stored form that leads somewhere
14
15
  * (has a continuation edge or a halo). Two complementary readings:
@@ -77,7 +78,7 @@ export function recognise(ctx, bytes) {
77
78
  // not silent), so it is emitted here directly rather than only inside
78
79
  // recogniseImpl.
79
80
  if (ctx.recogniseMemo) {
80
- const key = latin1Key(bytes);
81
+ const key = latin1(bytes);
81
82
  const hit = ctx.recogniseMemo.get(key);
82
83
  if (hit !== undefined) {
83
84
  if (ctx.meter)
@@ -483,9 +484,21 @@ function recogniseImpl(ctx, bytes) {
483
484
  // encoding is the identity, so the span's bytes ARE the branch key.
484
485
  // `subarray` is a view — this allocates nothing per probe, and the
485
486
  // bloom filter answers the misses without touching the database.
486
- const flatProbe = (start, end) => store.findFlatBranch
487
- ? store.findFlatBranch(bytes.subarray(start, end))
488
- : store.findBranch(allLeafIds.slice(start, end));
487
+ //
488
+ // Through ONE span prober (Store.flatSpans), because hashing the key was
489
+ // itself the quadratic: every probe hashed its span from the start, and
490
+ // the interior pass below sweeps up to `reach` ends past each endpoint —
491
+ // O(n · reach²) bytes hashed. Measured on the 31.7M-node store, one
492
+ // composition-regime response (#97 of the battery) probed 2,079,400
493
+ // spans and hashed 251,660,406 bytes for them. The prober extends each
494
+ // start's hash instead, so the same probes, answered identically, cost
495
+ // O(n · reach).
496
+ const spans = store.flatSpans?.(bytes) ?? null;
497
+ const flatProbe = (start, end) => spans !== null
498
+ ? spans(start, end)
499
+ : store.findFlatBranch
500
+ ? store.findFlatBranch(bytes.subarray(start, end))
501
+ : store.findBranch(allLeafIds.slice(start, end));
489
502
  // THE TWO ROUTES COST DIFFERENT THINGS, SO THEY ARE PRICED SEPARATELY.
490
503
  //
491
504
  // The exact route is a bloom-gated hash over a subarray VIEW: no
@@ -627,13 +640,17 @@ function recogniseImpl(ctx, bytes) {
627
640
  const reach = chainReach(W) * W * W + 2 * radius;
628
641
  if (ctx.meter)
629
642
  ctx.meter.recogniseInteriorGaps += ordered.length;
643
+ // Each end pairs only with the starts in [end − reach, end − W], in
644
+ // ascending order — the pairs, and the order the budget is spent in,
645
+ // of the all-pairs scan, without its O(n²) enumeration.
646
+ let lo = 0;
630
647
  for (const end of ordered) {
631
- for (const start of ordered) {
632
- if (start >= end)
633
- continue;
634
- const span = end - start;
635
- if (span < W || span > reach)
636
- continue;
648
+ while (ordered[lo] < end - reach)
649
+ lo++;
650
+ for (let i = lo; i < ordered.length; i++) {
651
+ const start = ordered[i];
652
+ if (end - start < W)
653
+ break;
637
654
  if (ctx.meter)
638
655
  ctx.meter.recogniseInteriorPairs++;
639
656
  spend(start, end);
@@ -672,8 +689,7 @@ function recogniseImpl(ctx, bytes) {
672
689
  // arithmetic, not evidence. Removing it wholesale was measured and
673
690
  // REVERTED: it also drops legitimate multi-byte chains (the 12-byte
674
691
  // "Eiffel Tower" site vanished with it). The premise is wrong but the
675
- // trust it stood in for is real; a replacement signal is still open work.
676
- // See bench/README.md.
692
+ // trust it stood in for is real; the replacement signal follows.
677
693
  //
678
694
  // THE REPLACEMENT SIGNAL (2026-08-13): `leadsSomewhere` on the BYTE-EXACT
679
695
  // branch the chain already found. The blanket off-boundary suppression is
@@ -6,7 +6,7 @@
6
6
  import { rItem } from "./trace.js";
7
7
  import { cosine } from "../vec.js";
8
8
  import { mergeThreshold } from "../geometry.js";
9
- import { concat2, concatBytes, indexOf } from "../bytes.js";
9
+ import { concat2, concatBytes, indexOf, latin1 } from "../bytes.js";
10
10
  import { gistOf, read, resolve, walkTree } from "./primitives.js";
11
11
  import { perceive } from "./primitives.js";
12
12
  import { argmaxCosine, candidateGist, hubBound } from "./traverse.js";
@@ -107,16 +107,6 @@ function junctionEdges(ctx, left, right, maxContainer) {
107
107
  }
108
108
  return out;
109
109
  }
110
- /** A byte string as a string, ONE code unit per byte — injective, so it is
111
- * safe to build a cache key from. Chunked to keep the spread within the
112
- * engine's argument limit on long contexts. */
113
- function latin1(b) {
114
- let s = "";
115
- for (let i = 0; i < b.length; i += 4096) {
116
- s += String.fromCharCode(...b.subarray(i, i + 4096));
117
- }
118
- return s;
119
- }
120
110
  /** Per-response memo of bridge results, keyed by the response's lifecycle
121
111
  * object (ctx.climbMemo — created fresh by respond() and nulled after, so
122
112
  * entries can never outlive the read-only window they are valid in). The
@@ -1,5 +1,6 @@
1
1
  import { Vec } from "../vec.js";
2
2
  import type { AncestorReach, MindContext } from "./types.js";
3
+ import { type WindowIndex } from "./evidence.js";
3
4
  /** The reach memo this ask should use — see the note above.
4
5
  *
5
6
  * A TRACED response always gets a fresh, empty one. `AncestorReach`'s
@@ -58,9 +59,9 @@ export declare function atomIsHub(ctx: MindContext, contextCount: number): boole
58
59
  * it is sound as a pre-filter before a consumer that applies the full
59
60
  * predicate, and never as a replacement for it. */
60
61
  export declare function bearsEdge(ctx: MindContext, id: number): boolean;
61
- /** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
62
- * The admission predicate recognition filters sites with (cover.md): a form
63
- * that
62
+ /** Whether a node LEADS SOMEWHERE — the store's admission predicate
63
+ * ({@link Store.leadsSomewhere}: edge or halo) with its edge tier memoised for
64
+ * the response. Recognition filters sites with it (cover.md): a form that
64
65
  * leads nowhere contributes nothing to any derivation. Runs once per candidate
65
66
  * span on the recognition hot path — `hasNext` is cached per response (the same
66
67
  * flat-branch ids are probed across prefix variants by canonicalChunkId).
@@ -140,8 +141,10 @@ export declare function argmaxCosine<T>(query: Vec, items: Iterable<T>, vecOf: (
140
141
  * formRules all share. undefined when the node has no continuation. */
141
142
  export declare function guidedFirst(ctx: MindContext, id: number): number | undefined;
142
143
  export declare function guidedNext(ctx: MindContext, node: number): number | undefined;
143
- /** Disambiguate among a node's learnt continuations by distributional
144
- * support. NOTE the `guide` contract: its VALUE is deliberately unused —
144
+ /** Disambiguate among a node's learnt continuations: first by the question's
145
+ * own witness of an establishing context (the exact tier, see
146
+ * {@link askedContinuations}), then by distributional support. NOTE the
147
+ * `guide` contract: its VALUE is deliberately unused —
145
148
  * only its PRESENCE gates disambiguation (a null guide means no query is in
146
149
  * flight, so structural walkers keep plain first-edge behaviour). The
147
150
  * gist-cosine of short answer candidates against a query guide is dominated
@@ -153,6 +156,26 @@ export declare function guidedNext(ctx: MindContext, node: number): number | und
153
156
  * therefore scores by guide cosine. The two directions consult different
154
157
  * halves of the evidence on purpose. */
155
158
  export declare function chooseNext(ctx: MindContext, id: number, guide?: Vec | null): number | undefined;
159
+ /** The response canonicalizer's reading of `bytes` when it keeps every offset
160
+ * — so a window found in the canonical bytes sits at the same place in the
161
+ * asker's — else the bytes themselves. Text canon is offset-preserving on
162
+ * ASCII without interior whitespace runs; where it is not, the raw bytes are
163
+ * read and a case-variant window simply does not match. */
164
+ export declare function offsetCanon(ctx: MindContext, bytes: Uint8Array): Uint8Array;
165
+ /** The continuations of `id` that `asked` NAMES (see {@link
166
+ * askedContinuations}) — for a caller holding material other than the whole
167
+ * question: the multi-hop walk asks with what of the question no product has
168
+ * restated yet. null when none is named. */
169
+ export declare function namedContinuations(ctx: MindContext, id: number, asked: {
170
+ bytes: Uint8Array;
171
+ index: WindowIndex;
172
+ }): number[] | null;
173
+ /** The question spans that NAMED `pick` among `id`'s continuations — the
174
+ * evidence a projection through that pick stands on, so a mechanism can
175
+ * account for what the question said about it (mechanism-market.md:
176
+ * evidence travels). Empty when the question names no continuation of `id`
177
+ * or names others. */
178
+ export declare function askedEvidence(ctx: MindContext, id: number, pick: number): Array<[number, number]>;
156
179
  /** The perceived gist of a candidate node, through the session gist cache.
157
180
  * Re-gisting a candidate is a full river fold of its bytes — the measured
158
181
  * recall bottleneck (a hub context offers up to √N continuations, EACH
@@ -198,6 +221,23 @@ export declare function chooseAmong(ctx: MindContext, candidates: readonly numbe
198
221
  * NOT scaffolding-only — it has no evidence either way, and its callers already
199
222
  * refuse it on their own terms. */
200
223
  export declare function allWindowsAreScaffolding(ctx: MindContext, query: Uint8Array): boolean;
224
+ /** Per offset of `bytes`: 1 when the W-window there is a stored form contained
225
+ * in more than √N places (corpus-global scaffolding; see the floor below),
226
+ * else 0. Memoised per
227
+ * byte array for the life of the store's read-only response. */
228
+ export declare function hubWindows(ctx: MindContext, bytes: Uint8Array): Uint8Array;
229
+ /** The query's SCAFFOLDING CORE, as spans: the bytes every W-window over which
230
+ * is a hub (see {@link hubWindows}) — what is nothing but frame, where
231
+ * {@link scaffoldExtents} is what a frame window reaches. */
232
+ export declare function scaffoldSpans(ctx: MindContext, query: Uint8Array): Array<[number, number]>;
233
+ /** The EXTENTS of the query's SCAFFOLDING windows, merged: every byte some
234
+ * W-window reaches that is a stored form contained in more than √N places —
235
+ * corpus-global commonality (commonality.md), the same "hub" reading as
236
+ * {@link allWindowsAreScaffolding} and the bridge's `explainedSpan`. A window
237
+ * the store never saw is NOT scaffolding. The extent, not the core, is what
238
+ * a coverage test needs: a span that still holds one hub window can be
239
+ * "carried" by any fact that holds that window. */
240
+ export declare function scaffoldExtents(ctx: MindContext, query: Uint8Array): Array<[number, number]>;
201
241
  /** Trained forms the query may OPEN, proposed from the write side's own
202
242
  * leaf-id window index — the supply of last resort for prefix completion.
203
243
  *
@@ -7,13 +7,15 @@
7
7
  // project) live in match.ts — the elementary match-and-project operation.
8
8
  import { cosine } from "../vec.js";
9
9
  import { gistOf, read } from "./primitives.js";
10
- import { canonicalWindows, leafIdPrefix, leafIdRun } from "./canonical.js";
10
+ import { canonicalWindows, chainReach, leafIdPrefix, leafIdRun, } from "./canonical.js";
11
11
  // Imported at the TOP, where every other import is. They used to sit 800 lines
12
12
  // down under a note claiming the position mattered ("before trace module is
13
13
  // loaded") — it does not: an ES module's static imports are HOISTED, so the
14
14
  // file's line order never decides load order. The note described an intention
15
15
  // the runtime does not honour; the imports move and the claim goes.
16
16
  import { decodeText } from "./rationale.js";
17
+ import { latin1 } from "../bytes.js";
18
+ import { windowIndex, witness } from "./evidence.js";
17
19
  //
18
20
  // Budgeted on the same terms as the reach memo below (caches.md): these three
19
21
  // maps are cleared on every write, but a long read-only session over a large
@@ -432,9 +434,9 @@ export function atomIsHub(ctx, contextCount) {
432
434
  export function bearsEdge(ctx, id) {
433
435
  return cachedHasNext(ctx, id, getStructCache(ctx));
434
436
  }
435
- /** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
436
- * The admission predicate recognition filters sites with (cover.md): a form
437
- * that
437
+ /** Whether a node LEADS SOMEWHERE — the store's admission predicate
438
+ * ({@link Store.leadsSomewhere}: edge or halo) with its edge tier memoised for
439
+ * the response. Recognition filters sites with it (cover.md): a form that
438
440
  * leads nowhere contributes nothing to any derivation. Runs once per candidate
439
441
  * span on the recognition hot path — `hasNext` is cached per response (the same
440
442
  * flat-branch ids are probed across prefix variants by canonicalChunkId).
@@ -599,8 +601,10 @@ export function guidedNext(ctx, node) {
599
601
  ctx._edgeChoice.set(node, pick ?? -1);
600
602
  return pick;
601
603
  }
602
- /** Disambiguate among a node's learnt continuations by distributional
603
- * support. NOTE the `guide` contract: its VALUE is deliberately unused —
604
+ /** Disambiguate among a node's learnt continuations: first by the question's
605
+ * own witness of an establishing context (the exact tier, see
606
+ * {@link askedContinuations}), then by distributional support. NOTE the
607
+ * `guide` contract: its VALUE is deliberately unused —
604
608
  * only its PRESENCE gates disambiguation (a null guide means no query is in
605
609
  * flight, so structural walkers keep plain first-edge behaviour). The
606
610
  * gist-cosine of short answer candidates against a query guide is dominated
@@ -621,13 +625,30 @@ export function chooseNext(ctx, id, guide) {
621
625
  return undefined;
622
626
  if (nx.length === 1 || !guide)
623
627
  return nx[0];
628
+ // THE EXACT TIER — the continuation the QUESTION names. Every other
629
+ // disambiguation below reads popularity, and is right to refuse the gist (see
630
+ // the doc above); but the corpus also wrote down, for each continuation,
631
+ // WHICH QUESTIONS IT ANSWERS — its establishing contexts — and a question is
632
+ // not a gist. When one of them is witnessed by the asker's bytes plus the
633
+ // node's own, that continuation is the one being asked for. Measured on the
634
+ // 31.7M-node store: `Who is the father of Frederick II?` answered the
635
+ // citizenship fact (the most-poured of eight) while `Frederick II father` —
636
+ // one of the father fact's own establishing contexts — lay wholly inside the
637
+ // question. Exact, so it ranks first (exact-vs-approximate.md); when nothing
638
+ // is witnessed the ladder below decides exactly as before.
639
+ const asked = ctx._edgeAsked;
640
+ const named = asked === null ? null : askedContinuations(ctx, id, nx, asked);
641
+ if (named !== null && named.length === 1)
642
+ return named[0];
624
643
  // Cap candidates at √N — the same bound the original chooseAmong used.
625
644
  // A hub context can accumulate thousands of continuations; the best-fit
626
645
  // one is among the first √N by insertion order (edges are never deleted,
627
646
  // so the oldest are the most established). A strongly-supported edge
628
647
  // inserted beyond the cap is invisible here — the deliberate trade
629
- // against paying O(fan-out) count reads on every disambiguation.
630
- const capped = nx; // already the hub-capped prefix, by the read above
648
+ // against paying O(fan-out) count reads on every disambiguation. Several
649
+ // continuations named EQUALLY by the question are told apart by the same
650
+ // ladder, over them alone.
651
+ const capped = named ?? nx; // already the hub-capped prefix, by the read above
631
652
  // Distributional-evidence disambiguation, consulting BOTH read-outs of the
632
653
  // evidence the training poured:
633
654
  // 1. prevCount — how many DISTINCT contexts predict this candidate (one
@@ -691,6 +712,184 @@ export function chooseNext(ctx, id, guide) {
691
712
  }
692
713
  return best;
693
714
  }
715
+ /** The response canonicalizer's reading of `bytes` when it keeps every offset
716
+ * — so a window found in the canonical bytes sits at the same place in the
717
+ * asker's — else the bytes themselves. Text canon is offset-preserving on
718
+ * ASCII without interior whitespace runs; where it is not, the raw bytes are
719
+ * read and a case-variant window simply does not match. */
720
+ export function offsetCanon(ctx, bytes) {
721
+ if (ctx.canon === null)
722
+ return bytes;
723
+ const c = ctx.canon(bytes);
724
+ return c.length === bytes.length ? c : bytes;
725
+ }
726
+ /** The continuations of `id` that `asked` NAMES (see {@link
727
+ * askedContinuations}) — for a caller holding material other than the whole
728
+ * question: the multi-hop walk asks with what of the question no product has
729
+ * restated yet. null when none is named. */
730
+ export function namedContinuations(ctx, id, asked) {
731
+ const nx = ctx.store.nextFirst(id, hubBound(ctx));
732
+ return nx.length === 0 ? null : askedContinuations(ctx, id, nx, asked);
733
+ }
734
+ /** The continuations of `id` (among `nx`) that the question NAMES: one of
735
+ * their establishing contexts — a predecessor other than `id` itself — is
736
+ * wholly witnessed by the question plus `id`'s own bytes (evidence.ts), with
737
+ * the question supplying at least one window the node does not. Ranked by
738
+ * how much of the question witnesses it; null when none is named.
739
+ *
740
+ * THE NODE'S OWN BYTES ARE MATERIAL because a derivation stands on them. On
741
+ * the second hop of `Where was the place of death of the director of film
742
+ * Beat Girl?` the node is `Edmond T. Gréville` — reached, never written — and
743
+ * its fact's establishing context `Edmond T. Gréville place of death` is held
744
+ * by neither the question nor the node, only by both. Measured over 5,236
745
+ * held-out 2Wiki questions: such a context is wholly witnessed by the
746
+ * question alone 69 times, by the question and the first hop 2,153 times.
747
+ *
748
+ * BOUNDED: predecessor reads share one √N budget across the candidates (see
749
+ * the loop below), asked cheapest first; past it the tier abstains for the
750
+ * rest, metered, and the ladder decides as before.
751
+ * Each form is read at most to the material's length — a form longer than
752
+ * everything at hand cannot be wholly witnessed without repeating it. */
753
+ function askedContinuations(ctx, id, nx, asked) {
754
+ return askedEntry(ctx, id, nx, asked).named;
755
+ }
756
+ /** The question spans that NAMED `pick` among `id`'s continuations — the
757
+ * evidence a projection through that pick stands on, so a mechanism can
758
+ * account for what the question said about it (mechanism-market.md:
759
+ * evidence travels). Empty when the question names no continuation of `id`
760
+ * or names others. */
761
+ export function askedEvidence(ctx, id, pick) {
762
+ const asked = ctx._edgeAsked;
763
+ if (asked === null)
764
+ return [];
765
+ const nx = ctx.store.nextFirst(id, hubBound(ctx));
766
+ const entry = askedEntry(ctx, id, nx, asked);
767
+ return entry.named?.includes(pick) ? entry.spans.get(pick) ?? [] : [];
768
+ }
769
+ function askedEntry(ctx, id, nx, asked) {
770
+ let memo = askedMemo.get(asked);
771
+ if (memo === undefined)
772
+ askedMemo.set(asked, memo = new Map());
773
+ const hit = memo.get(id);
774
+ if (hit !== undefined && !ctx.trace)
775
+ return hit;
776
+ const entry = askedContinuationsImpl(ctx, id, nx, asked);
777
+ memo.set(id, entry);
778
+ return entry;
779
+ }
780
+ /** One pick per node per question — every mechanism of a response asks the
781
+ * same node about the same question (the guided-pick memo's own reason). */
782
+ const askedMemo = new WeakMap();
783
+ function askedContinuationsImpl(ctx, id, nx, asked) {
784
+ const none = { named: null, spans: new Map() };
785
+ const W = ctx.space.maxGroup;
786
+ // A SATURATED READ IS NOT A CANDIDATE SET. When the continuations came back
787
+ // at the √N cap the read may have cut the named one off, so "none of these is
788
+ // named" and "this is the named one" are both unfounded — and this is exactly
789
+ // where witnessing would read most. The tier abstains, metered, and the
790
+ // distributional ladder decides as it always has.
791
+ if (nx.length >= hubBound(ctx)) {
792
+ if (ctx.meter)
793
+ ctx.meter.askedReadsSaturated++;
794
+ return none;
795
+ }
796
+ const cache = getStructCache(ctx);
797
+ const ownCap = asked.bytes.length * W;
798
+ const own = offsetCanon(ctx, read(ctx, id, ownCap));
799
+ const ownIndex = windowIndex(own, W);
800
+ // Naming needs the question to say at least one window the node does not:
801
+ // when the node already holds every window of the question (the question IS
802
+ // this context, or a piece of it), nothing can be named, and nothing is read.
803
+ let beyond = false;
804
+ for (const key of asked.index.keys()) {
805
+ if (!ownIndex.has(key)) {
806
+ beyond = true;
807
+ break;
808
+ }
809
+ }
810
+ if (!beyond)
811
+ return none;
812
+ const indexes = [asked.index, ownIndex];
813
+ const formCap = asked.bytes.length + own.length;
814
+ // BOUNDED READS (bounded-reads.md): the decision reads at most √N
815
+ // establishing contexts — floored at the write side's own arity `chainReach(W)`
816
+ // so a store too small for √N to cover one fact's questions still decides.
817
+ // Candidates are asked CHEAPEST FIRST (fewest establishing contexts): a common
818
+ // reply established by hundreds of contexts would otherwise spend the whole
819
+ // allowance alone. The order changes what is READ, never what wins: scores
820
+ // are compared afterwards in the continuations' own order.
821
+ let budget = Math.max(hubBound(ctx), chainReach(W));
822
+ const order = nx
823
+ .map((n, at) => ({ n, at, support: cachedPrevCount(ctx, n, cache) }))
824
+ .filter((c) => c.support >= 2) // only `id` establishes the rest
825
+ .sort((a, b) => a.support - b.support || a.at - b.at);
826
+ const scored = [];
827
+ for (const { n, at, support } of order) {
828
+ if (support > budget) {
829
+ if (ctx.meter)
830
+ ctx.meter.askedReadsSaturated++;
831
+ break;
832
+ }
833
+ budget -= support;
834
+ if (ctx.meter)
835
+ ctx.meter.askedPredecessorReads += support;
836
+ let score = 0;
837
+ let by = -1;
838
+ let spans = [];
839
+ for (const c of ctx.store.prevFirst(n, support)) {
840
+ if (c === id)
841
+ continue;
842
+ // The form's FIRST window decides most refusals: one short prefix read
843
+ // before the whole form is reconstructed (a conversation-length
844
+ // predecessor would otherwise be read in full to fail on its opening).
845
+ const head = offsetCanon(ctx, read(ctx, c, W));
846
+ if (head.length < W)
847
+ continue;
848
+ if (!indexes.some((ix) => ix.has(latin1(head))))
849
+ continue;
850
+ const form = read(ctx, c, formCap + 1);
851
+ if (form.length < W || form.length > formCap)
852
+ continue;
853
+ const w = witness(offsetCanon(ctx, form), indexes, W);
854
+ if (!w.complete || w.bytes < W)
855
+ continue;
856
+ if (w.bytes > score) {
857
+ score = w.bytes;
858
+ by = c;
859
+ spans = w.spans;
860
+ }
861
+ }
862
+ if (score > 0)
863
+ scored.push({ n, at, score, by, spans });
864
+ }
865
+ scored.sort((a, b) => a.at - b.at);
866
+ let best = [];
867
+ let bestBytes = 0;
868
+ let witnessed = null;
869
+ for (const { n, score, by } of scored) {
870
+ if (score > bestBytes) {
871
+ best = [n];
872
+ bestBytes = score;
873
+ witnessed = by;
874
+ }
875
+ else if (score === bestBytes)
876
+ best.push(n);
877
+ }
878
+ if (best.length === 0)
879
+ return none;
880
+ if (ctx.meter)
881
+ ctx.meter.askedContinuations++;
882
+ if (ctx.trace && witnessed !== null) {
883
+ ctx.trace.step("askedContinuation", [rItemShort(ctx, id, "node"), rItemShort(ctx, witnessed, "asked")], best.map((n) => rItemShort(ctx, n, "named")), `${nx.length} continuations — the question witnesses ` +
884
+ `${best.length === 1 ? "one's" : `${best.length}'`} own establishing ` +
885
+ `context (${bestBytes} question byte(s) beyond the node)`);
886
+ }
887
+ const evidence = new Map();
888
+ for (const c of scored)
889
+ if (best.includes(c.n))
890
+ evidence.set(c.n, c.spans);
891
+ return { named: best, spans: evidence };
892
+ }
694
893
  /** The perceived gist of a candidate node, through the session gist cache.
695
894
  * Re-gisting a candidate is a full river fold of its bytes — the measured
696
895
  * recall bottleneck (a hub context offers up to √N continuations, EACH
@@ -776,6 +975,84 @@ export function allWindowsAreScaffolding(ctx, query) {
776
975
  }
777
976
  return sawOne;
778
977
  }
978
+ /** Per offset of `bytes`: 1 when the W-window there is a stored form contained
979
+ * in more than √N places (corpus-global scaffolding; see the floor below),
980
+ * else 0. Memoised per
981
+ * byte array for the life of the store's read-only response. */
982
+ export function hubWindows(ctx, bytes) {
983
+ const hit = hubWindowMemo.get(bytes);
984
+ if (hit !== undefined)
985
+ return hit;
986
+ const W = ctx.space.maxGroup;
987
+ // Floored at the write side's arity: inside ONE deposit's fold a window is
988
+ // already contained by up to `chainReach(W)` chunks and branches, so on a
989
+ // store of a few facts the √N reading would call every window frame — that
990
+ // is fold structure, not corpus commonality.
991
+ const bound = Math.max(hubBound(ctx), chainReach(W));
992
+ const hub = new Uint8Array(Math.max(0, bytes.length - W + 1));
993
+ for (let o = 0; o < hub.length; o++) {
994
+ const ids = leafIdRun(ctx, bytes, o, o + W);
995
+ const id = ids === null ? null : ctx.store.findBranch(ids);
996
+ if (id !== null && ctx.store.containersSlice(id, bound, 1).length > 0) {
997
+ hub[o] = 1;
998
+ }
999
+ }
1000
+ hubWindowMemo.set(bytes, hub);
1001
+ return hub;
1002
+ }
1003
+ const hubWindowMemo = new WeakMap();
1004
+ /** The query's SCAFFOLDING CORE, as spans: the bytes every W-window over which
1005
+ * is a hub (see {@link hubWindows}) — what is nothing but frame, where
1006
+ * {@link scaffoldExtents} is what a frame window reaches. */
1007
+ export function scaffoldSpans(ctx, query) {
1008
+ const W = ctx.space.maxGroup;
1009
+ const hub = hubWindows(ctx, query);
1010
+ const n = hub.length;
1011
+ if (n <= 0)
1012
+ return [];
1013
+ const spans = [];
1014
+ let start = -1;
1015
+ for (let i = 0; i < query.length; i++) {
1016
+ let all = true;
1017
+ for (let o = Math.max(0, i - W + 1); o <= Math.min(i, n - 1); o++) {
1018
+ if (!hub[o]) {
1019
+ all = false;
1020
+ break;
1021
+ }
1022
+ }
1023
+ if (all && start < 0)
1024
+ start = i;
1025
+ if (!all && start >= 0) {
1026
+ spans.push([start, i]);
1027
+ start = -1;
1028
+ }
1029
+ }
1030
+ if (start >= 0)
1031
+ spans.push([start, query.length]);
1032
+ return spans;
1033
+ }
1034
+ /** The EXTENTS of the query's SCAFFOLDING windows, merged: every byte some
1035
+ * W-window reaches that is a stored form contained in more than √N places —
1036
+ * corpus-global commonality (commonality.md), the same "hub" reading as
1037
+ * {@link allWindowsAreScaffolding} and the bridge's `explainedSpan`. A window
1038
+ * the store never saw is NOT scaffolding. The extent, not the core, is what
1039
+ * a coverage test needs: a span that still holds one hub window can be
1040
+ * "carried" by any fact that holds that window. */
1041
+ export function scaffoldExtents(ctx, query) {
1042
+ const W = ctx.space.maxGroup;
1043
+ const hub = hubWindows(ctx, query);
1044
+ const spans = [];
1045
+ for (let o = 0; o < hub.length; o++) {
1046
+ if (!hub[o])
1047
+ continue;
1048
+ const last = spans[spans.length - 1];
1049
+ if (last !== undefined && o <= last[1])
1050
+ last[1] = o + W;
1051
+ else
1052
+ spans.push([o, o + W]);
1053
+ }
1054
+ return spans;
1055
+ }
779
1056
  // ── THE PREFIX SUPPLY ───────────────────────────────────────────────────────
780
1057
  //
781
1058
  // A RETRIEVAL capability, not a grounding one: "which trained forms does this
@@ -7,6 +7,7 @@ import type { MindConfig } from "../config.js";
7
7
  import type { Meter } from "../meter.js";
8
8
  import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
9
9
  import type { Rationale } from "./rationale.js";
10
+ import type { WindowIndex } from "./evidence.js";
10
11
  import type { ContentFold, Grid } from "../geometry.js";
11
12
  /** One {@link MindContext._depositTrees} entry — see that field's doc.
12
13
  *
@@ -157,7 +158,13 @@ export interface Attention {
157
158
  * a genuine further topic is named in its own distinctive wording
158
159
  * somewhere the query's scaffolding does not reach, always a SEPARATE
159
160
  * cluster from whatever else corroborates it. See
160
- * test/37-cluster-dispersion-fusion.test.mjs. */
161
+ * test/37-cluster-dispersion-fusion.test.mjs.
162
+ *
163
+ * Read from the VOTES, this is a lossy witness: a region votes once, for its
164
+ * top anchor, so a place can be lost to a tie or won through an accident.
165
+ * Fusion therefore also asks the root's CONTEXT the same question at window
166
+ * scale (reasoning.ts `sharedPlaces`) and trusts a root that either reading
167
+ * finds in two places. */
161
168
  clusters: number;
162
169
  }
163
170
  /** Both read-outs of one consensus climb. */
@@ -364,6 +371,14 @@ export interface MindContext extends GraphSearchHost {
364
371
  * ordinary respond() and for the first turn of a conversation. */
365
372
  currentTurnStart: number;
366
373
  _edgeGuide: Vec | null;
374
+ /** The question currently being answered, as the material `chooseNext`'s
375
+ * exact tier witnesses a continuation's establishing contexts against —
376
+ * its bytes (canonical when the response's canon preserves offsets) and
377
+ * their window index. Set and cleared with `_edgeGuide`. */
378
+ _edgeAsked: {
379
+ bytes: Uint8Array;
380
+ index: WindowIndex;
381
+ } | null;
367
382
  _edgeChoice: Map<number, number>;
368
383
  _prevSeen: Set<number> | null;
369
384
  /** Session cache of node-id → perceived gist, for candidate scoring
@@ -28,6 +28,20 @@ export declare class SQliteStore extends AbstractStore implements Store {
28
28
  private _bloom;
29
29
  /** Dedup probes answered by the filter alone this session (observability). */
30
30
  bloomSkips: number;
31
+ /** The same negative filter over the CANON index's key hashes. Recognition's
32
+ * canonical admission and the join's canonical entity scan ask `canonFind`
33
+ * once per probed span, and almost every answer is "no such key" — on an
34
+ * index that is often EMPTY (it is built only by `buildCanonIndex`).
35
+ * Loaded on first use from `canon_bloom` when its stamp matches the meta,
36
+ * else built by one sequential scan of the h column; kept exact on
37
+ * `canonAdd` (rebuilt bigger from the table, uncommitted rows included,
38
+ * when growth saturates it) and persisted with the commit that wrote the
39
+ * rows. The canon table is never deleted from, so the filter can never
40
+ * hold a false negative: a miss it reports is a miss the query would have
41
+ * returned. */
42
+ private _canonBloom;
43
+ /** The in-memory canon filter differs from the persisted one. */
44
+ private _canonBloomDirty;
31
45
  private _insertNode;
32
46
  private _insertKid;
33
47
  private _selContain;
@@ -79,6 +93,9 @@ export declare class SQliteStore extends AbstractStore implements Store {
79
93
  protected _dbGetNode(id: NodeId): NodeRec | null;
80
94
  protected _dbFindLeaf(h: number, bytes: Uint8Array): NodeId | null;
81
95
  protected _dbFindBranchByLeaf(h: number, bytes: Uint8Array): NodeId | null;
96
+ /** The node filter alone: never a false negative (every inserted hash is
97
+ * added before any probe can see it), so `false` is exact. */
98
+ protected _dbFlatMayExist(h: number, bytes: Uint8Array): boolean;
82
99
  protected _dbFindBranchByKids(h: number, packed: Uint8Array): NodeId | null;
83
100
  protected _dbInsertKid(child: NodeId, parent: NodeId): void;
84
101
  protected _dbGetParents(id: NodeId): NodeId[];
@@ -137,6 +154,14 @@ export declare class SQliteStore extends AbstractStore implements Store {
137
154
  protected _dbSetMeta(key: string, val: string): void;
138
155
  protected _dbDeleteMeta(key: string): void;
139
156
  canonAdd(h: number, id: number): void;
157
+ /** The canon filter: the persisted one when its stamp matches the meta,
158
+ * else built from the index as it stands. */
159
+ private _canonFilter;
160
+ private _canonScan;
161
+ /** What a persisted canon filter is stamped with: the incremental build's
162
+ * cursor, which every canon writer advances in the transaction it writes. */
163
+ private _canonStamp;
164
+ private _canonBloomPersist;
140
165
  canonFind(h: number): number[];
141
166
  sketchGet(id: number): number[] | null;
142
167
  sketchPut(id: number, ids: readonly number[]): void;