@hviana/sema 0.2.5 → 0.2.6

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 (133) hide show
  1. package/package.json +1 -1
  2. package/src/mind/attention.ts +355 -19
  3. package/test/53-cross-region-probe-instrumentation.test.mjs +356 -0
  4. package/dist/example/demo.d.ts +0 -1
  5. package/dist/example/demo.js +0 -39
  6. package/dist/example/train_base.d.ts +0 -87
  7. package/dist/example/train_base.js +0 -2252
  8. package/dist/src/alphabet.d.ts +0 -7
  9. package/dist/src/alphabet.js +0 -33
  10. package/dist/src/alu/src/alu.d.ts +0 -185
  11. package/dist/src/alu/src/alu.js +0 -440
  12. package/dist/src/alu/src/expr.d.ts +0 -61
  13. package/dist/src/alu/src/expr.js +0 -318
  14. package/dist/src/alu/src/index.d.ts +0 -11
  15. package/dist/src/alu/src/index.js +0 -19
  16. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  17. package/dist/src/alu/src/kernel-arith.js +0 -264
  18. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  19. package/dist/src/alu/src/kernel-bits.js +0 -152
  20. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  21. package/dist/src/alu/src/kernel-logic.js +0 -60
  22. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  23. package/dist/src/alu/src/kernel-nd.js +0 -208
  24. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  25. package/dist/src/alu/src/kernel-numeric.js +0 -366
  26. package/dist/src/alu/src/operation.d.ts +0 -168
  27. package/dist/src/alu/src/operation.js +0 -189
  28. package/dist/src/alu/src/parser.d.ts +0 -221
  29. package/dist/src/alu/src/parser.js +0 -577
  30. package/dist/src/alu/src/resonance.d.ts +0 -55
  31. package/dist/src/alu/src/resonance.js +0 -126
  32. package/dist/src/alu/src/text.d.ts +0 -31
  33. package/dist/src/alu/src/text.js +0 -73
  34. package/dist/src/alu/src/value.d.ts +0 -109
  35. package/dist/src/alu/src/value.js +0 -300
  36. package/dist/src/alu/test/alu.test.d.ts +0 -1
  37. package/dist/src/alu/test/alu.test.js +0 -764
  38. package/dist/src/bytes.d.ts +0 -14
  39. package/dist/src/bytes.js +0 -59
  40. package/dist/src/canon.d.ts +0 -26
  41. package/dist/src/canon.js +0 -57
  42. package/dist/src/config.d.ts +0 -111
  43. package/dist/src/config.js +0 -91
  44. package/dist/src/derive/src/deduction.d.ts +0 -125
  45. package/dist/src/derive/src/deduction.js +0 -155
  46. package/dist/src/derive/src/index.d.ts +0 -7
  47. package/dist/src/derive/src/index.js +0 -11
  48. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  49. package/dist/src/derive/src/priority-queue.js +0 -73
  50. package/dist/src/derive/src/rewrite.d.ts +0 -56
  51. package/dist/src/derive/src/rewrite.js +0 -100
  52. package/dist/src/derive/src/trie.d.ts +0 -90
  53. package/dist/src/derive/src/trie.js +0 -217
  54. package/dist/src/derive/test/derive.test.d.ts +0 -1
  55. package/dist/src/derive/test/derive.test.js +0 -122
  56. package/dist/src/extension.d.ts +0 -37
  57. package/dist/src/extension.js +0 -7
  58. package/dist/src/geometry.d.ts +0 -202
  59. package/dist/src/geometry.js +0 -575
  60. package/dist/src/index.d.ts +0 -16
  61. package/dist/src/index.js +0 -18
  62. package/dist/src/ingest-cache.d.ts +0 -41
  63. package/dist/src/ingest-cache.js +0 -161
  64. package/dist/src/mind/articulation.d.ts +0 -6
  65. package/dist/src/mind/articulation.js +0 -99
  66. package/dist/src/mind/attention.d.ts +0 -315
  67. package/dist/src/mind/attention.js +0 -1549
  68. package/dist/src/mind/bridge.d.ts +0 -30
  69. package/dist/src/mind/bridge.js +0 -569
  70. package/dist/src/mind/canonical.d.ts +0 -29
  71. package/dist/src/mind/canonical.js +0 -88
  72. package/dist/src/mind/graph-search.d.ts +0 -285
  73. package/dist/src/mind/graph-search.js +0 -956
  74. package/dist/src/mind/index.d.ts +0 -7
  75. package/dist/src/mind/index.js +0 -5
  76. package/dist/src/mind/junction.d.ts +0 -124
  77. package/dist/src/mind/junction.js +0 -317
  78. package/dist/src/mind/learning.d.ts +0 -47
  79. package/dist/src/mind/learning.js +0 -248
  80. package/dist/src/mind/match.d.ts +0 -126
  81. package/dist/src/mind/match.js +0 -441
  82. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  83. package/dist/src/mind/mechanisms/alu.js +0 -36
  84. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  85. package/dist/src/mind/mechanisms/cast.js +0 -716
  86. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  87. package/dist/src/mind/mechanisms/confluence.js +0 -225
  88. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  89. package/dist/src/mind/mechanisms/cover.js +0 -203
  90. package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
  91. package/dist/src/mind/mechanisms/extraction.js +0 -387
  92. package/dist/src/mind/mechanisms/recall.d.ts +0 -13
  93. package/dist/src/mind/mechanisms/recall.js +0 -325
  94. package/dist/src/mind/mind.d.ts +0 -276
  95. package/dist/src/mind/mind.js +0 -572
  96. package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
  97. package/dist/src/mind/pipeline-mechanism.js +0 -213
  98. package/dist/src/mind/pipeline.d.ts +0 -20
  99. package/dist/src/mind/pipeline.js +0 -217
  100. package/dist/src/mind/primitives.d.ts +0 -70
  101. package/dist/src/mind/primitives.js +0 -287
  102. package/dist/src/mind/rationale.d.ts +0 -139
  103. package/dist/src/mind/rationale.js +0 -163
  104. package/dist/src/mind/reasoning.d.ts +0 -34
  105. package/dist/src/mind/reasoning.js +0 -240
  106. package/dist/src/mind/recognition.d.ts +0 -20
  107. package/dist/src/mind/recognition.js +0 -390
  108. package/dist/src/mind/resonance.d.ts +0 -23
  109. package/dist/src/mind/resonance.js +0 -0
  110. package/dist/src/mind/trace.d.ts +0 -15
  111. package/dist/src/mind/trace.js +0 -73
  112. package/dist/src/mind/traverse.d.ts +0 -113
  113. package/dist/src/mind/traverse.js +0 -568
  114. package/dist/src/mind/types.d.ts +0 -289
  115. package/dist/src/mind/types.js +0 -130
  116. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  117. package/dist/src/rabitq-ivf/src/database.js +0 -201
  118. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  119. package/dist/src/rabitq-ivf/src/index.js +0 -4
  120. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  121. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  122. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  123. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  124. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  125. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  126. package/dist/src/sema.d.ts +0 -31
  127. package/dist/src/sema.js +0 -63
  128. package/dist/src/store-sqlite.d.ts +0 -184
  129. package/dist/src/store-sqlite.js +0 -942
  130. package/dist/src/store.d.ts +0 -663
  131. package/dist/src/store.js +0 -1618
  132. package/dist/src/vec.d.ts +0 -31
  133. package/dist/src/vec.js +0 -109
@@ -1,240 +0,0 @@
1
- // reasoning.ts — multi-hop reasoning + multi-topic fusion (Section 4 of the mind).
2
- //
3
- // reason — extend an answer forward across facts (multi-hop)
4
- // fuseAttention — fuse independent points of attention (multi-topic)
5
- import { rItem, rNode } from "./trace.js";
6
- import { bytesEqual, indexOf } from "../bytes.js";
7
- import { resolve } from "./primitives.js";
8
- import { corpusN } from "./traverse.js";
9
- import { follow, haloSiblings, project } from "./match.js";
10
- import { joinWithBridge, pivotInto } from "./resonance.js";
11
- /** Whether `bytes` is a proper byte-subspan of `query` — already present in
12
- * the question, so voicing it back only restates part of what was asked,
13
- * never answers it. The exact guard recallByResonance already applies to
14
- * its OWN grounding candidates (tier 1's `restates`, tier 2's subspan
15
- * check, tier 0b's argument-binding subspan check) — every mechanism that
16
- * walks a LEARNT CONTINUATION EDGE past an already-vetted grounding
17
- * (reason()'s own hops below, and CAST's `projectCounterfactual` seat
18
- * substitution — see cast.ts) needs the same guard applied to what the
19
- * walk turns up, since `follow()`/`chooseNext`/`pivotInto` know nothing of
20
- * the query at all — only of what structurally continues what. */
21
- export function restatesQuery(query, bytes) {
22
- return bytes.length < query.length && indexOf(query, bytes, 0) >= 0;
23
- }
24
- /** Extend a grounded answer forward across facts (multi-hop reasoning).
25
- * Pivots on the longest unconsumed learnt context each answer contains,
26
- * then follows the pivot's continuation to the next fact. Repeats up
27
- * to `cfg.recallQueryK` hops. `preConsumed` carries node ids already
28
- * spoken for by the grounding stage (cover/extract/CAST). `pre` is the
29
- * response's shared pre-computation — the post-grounding stages read the
30
- * same container the mechanisms did. */
31
- export async function reason(ctx, query, answer, preConsumed, pre) {
32
- // Echo guard: a query that is ITSELF a learnt continuation (some context's
33
- // answer) is being asked back at the system — hopping forward from it would
34
- // chain through the very fact that produced it and echo the conversation
35
- // back. The grounded answer alone is the honest read-out. Deliberately a
36
- // broad structural gate; pinned by test/31-audit.
37
- const qId = pre.queryResolved;
38
- if (qId !== null && ctx.store.prevCount(qId) > 0)
39
- return answer;
40
- const consumed = new Set();
41
- // Consume a node and its neighbours for pivot-cycle prevention — CAPPED at
42
- // the hub bound, via the store's LIMITed edge reads: a common continuation's
43
- // reverse fan-in (and a hub context's forward fan-out) is corpus-sized, and
44
- // no per-hop operation may grow with the corpus. The cap follows the one
45
- // convention every fan-out decision uses (first √N in the relation's own
46
- // read order); a pivot suppressed only by a beyond-cap neighbour may now
47
- // fire — the same visibility trade chooseNext documents.
48
- const hubBound = Math.ceil(Math.sqrt(corpusN(ctx)));
49
- const consumeNode = (id) => {
50
- if (id === null)
51
- return;
52
- consumed.add(id);
53
- for (const p of ctx.store.prevFirst(id, hubBound))
54
- consumed.add(p);
55
- };
56
- const consumeAll = (id) => {
57
- if (id === null)
58
- return;
59
- consumeNode(id);
60
- for (const n of ctx.store.nextFirst(id, hubBound))
61
- consumed.add(n);
62
- };
63
- // Pre-consume whatever the grounding stage already spoke for. The halo
64
- // sweep is one ANN query per node — cap it at haloQueryK sweeps (cover
65
- // grounding can pre-consume one node per recognised site, O(query length));
66
- // nodes past the cap are still consumed directly, they just skip the
67
- // synonym expansion.
68
- let haloSweeps = 0;
69
- for (const id of preConsumed) {
70
- consumeNode(id);
71
- if (haloSweeps >= ctx.cfg.haloQueryK)
72
- continue;
73
- const h = ctx.store.halo(id);
74
- if (!h)
75
- continue;
76
- haloSweeps++;
77
- for (const sib of await haloSiblings(ctx, id, h))
78
- consumeNode(sib.id);
79
- }
80
- let cur = answer;
81
- const qv = pre.guide; // the response-wide guide IS the query's gist
82
- let t;
83
- const startedFrom = answer;
84
- for (let hop = 0; hop < ctx.cfg.recallQueryK; hop++) {
85
- const curId = resolve(ctx, cur);
86
- consumeNode(curId);
87
- // Forward-absorb: follow only UNCONSUMED continuations. The gate below
88
- // checks an unconsumed edge EXISTS, but follow()'s chooseNext knows
89
- // nothing of `consumed` and may still walk to a consumed fixpoint —
90
- // absorbing it would repeat content the grounding stage already spoke
91
- // for, so a consumed fixpoint falls through to the pivot step instead.
92
- if (curId !== null &&
93
- ctx.store.nextFirst(curId, hubBound).some((n) => !consumed.has(n))) {
94
- const fwd = await follow(ctx, curId, qv);
95
- const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
96
- if (fwd !== null && !bytesEqual(fwd, cur) &&
97
- (fwdId === null || !consumed.has(fwdId)) &&
98
- !restatesQuery(query, fwd)) {
99
- consumeAll(curId);
100
- t ??= ctx.trace?.enter("reason", [
101
- rItem(startedFrom, "grounded"),
102
- ]);
103
- ctx.trace?.step("absorbForward", [rItem(cur, "answer", curId)], [rItem(fwd, "answer", resolve(ctx, fwd) ?? undefined)], "the answer is itself a learnt fact — follow its continuation to the fixpoint");
104
- cur = fwd;
105
- continue;
106
- }
107
- }
108
- // Pivot: find the longest unconsumed learnt context the answer contains.
109
- consumeAll(curId);
110
- const pivot = await pivotInto(ctx, cur, consumed);
111
- if (pivot === null)
112
- break;
113
- const fc = await follow(ctx, pivot, qv);
114
- consumeAll(pivot);
115
- if (fc === null || bytesEqual(fc, cur) || restatesQuery(query, fc))
116
- break;
117
- t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
118
- ctx.trace?.step("pivotStep", [rItem(cur, "answer"), rNode(ctx, pivot, "pivot")], [rItem(fc, "answer", resolve(ctx, fc) ?? undefined)], "pivot on the shared span this answer contains, then step forward across that fact");
119
- cur = fc;
120
- }
121
- t?.done([rItem(cur, "answer", resolve(ctx, cur) ?? undefined)], "the multi-hop chain's fixpoint");
122
- return cur;
123
- }
124
- /** Fuse independent points of attention into one answer (multi-topic).
125
- * When the consensus climb finds more than one dominant point, each
126
- * independent point grounds its own answer; they are bridged together
127
- * by any learnt connector the graph holds between them. */
128
- export async function fuseAttention(ctx, query, primary, pre,
129
- /** True when `primary` never touched the consensus climb at all — e.g. a
130
- * pure ALU computation, which has no anchor of its own. commitVotes
131
- * ALWAYS admits the dominant root regardless of its vote (attention.ts:
132
- * "roots.length === 0 || …") on the assumption a lone root already IS
133
- * primary's own source; that assumption is exactly backwards when
134
- * primary is unclimbed. Absent or false preserves the original
135
- * behaviour exactly. */
136
- unclimbed = false) {
137
- // When the answer is structurally drawn from the query itself
138
- // (extraction), it already spans all the query's pieces — fusion
139
- // would only add noise from unrelated stored contexts. The gate is
140
- // STRICT containment (resolved node in the query's tree, or a contiguous
141
- // byte run): the old sparse-subsequence test was trivially satisfied by
142
- // short answers over long queries, silently starving multi-topic queries
143
- // of fusion.
144
- if (containsSpan(ctx, query, primary))
145
- return primary;
146
- // The committed points of attention ARE the shared climb's roots (same
147
- // query, same k, same DF mode) — read them from Precomputed instead of
148
- // re-climbing, so even a traced response pays for the climb once.
149
- const forest = (await pre.attention()).roots;
150
- // A LONE root is ordinarily primary's own source — nothing to fuse. But
151
- // when primary is unclimbed, the lone root was never checked against
152
- // anything: it is admitted by commitVotes unconditionally, so it may be
153
- // genuine consensus (Attention.breadth dominates — most of the query's
154
- // OWN regions corroborate it) or a coincidental echo (breadth does not
155
- // dominate — see test/35-attention-confidence). breadth is the SCALE-
156
- // INVARIANT read of exactly this question: the raw IDF vote cannot serve
157
- // here, since it is an absolute ln(N)-scaled quantity (a genuine root on
158
- // a large store can score BELOW its own floor while a coincidental echo
159
- // on a small one scores comfortably above its own, smaller, floor).
160
- const lonePromotes = unclimbed && forest.length === 1 &&
161
- forest[0].breadth > 0.5;
162
- if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
163
- return primary;
164
- }
165
- const pieces = [
166
- { start: forest[0].start, bytes: primary },
167
- ];
168
- const qv = pre.guide; // once, not per root
169
- const rest = lonePromotes ? forest : forest.slice(1);
170
- const t = ctx.trace?.enter("fuseAttention", [
171
- rItem(primary, "primary"),
172
- ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
173
- ]);
174
- for (const root of rest) {
175
- // DISPERSION: this root's contributing regions are confined to a
176
- // single cluster (see Attention.clusters) — one local neighbourhood of
177
- // the query, not several separate places. Raw region count already
178
- // failed to discriminate a coincidental match from a genuine further
179
- // topic (test/24 gap 3.1 vs test/35's echo); dispersion is a different
180
- // question — not how MUCH evidence, but how many separate PLACES in the
181
- // query corroborate it — and a coincidental match is structurally
182
- // confined to one cluster no matter how strongly it resonates.
183
- //
184
- // EXCEPTION: crossRegionVotes' own joint conclusions (a query naming
185
- // two attributes that were only ever learnt TOGETHER — test/34's own
186
- // binding corpus) are inherently ONE fused context and are pooled from
187
- // a single synthetic region, so they always read as one cluster even
188
- // though they already, by construction, account for both original
189
- // mentions. `breadth` (dominates — the same > half-the-query bar used
190
- // everywhere else) still correctly recognises these: a genuine joint
191
- // binding explains the MAJORITY of the query's regions on its own,
192
- // which a coincidental echo never does (verified: test/35's echo tops
193
- // out at 0.40). So a root is trusted when EITHER measure alone
194
- // indicates real signal — excluded only when BOTH are weak. Cheap and
195
- // synchronous — checked before the async already-answered walk below.
196
- if (root.clusters < 2 && root.breadth <= 0.5) {
197
- ctx.trace?.step("singleCluster", [rNode(ctx, root.anchor, "point", root.vote)], [], "this point's evidence is confined to one local neighbourhood of the query — not trusted as an independent topic");
198
- continue;
199
- }
200
- // ALREADY ANSWERED: this root's own learnt continuation — the same
201
- // content-addressed walk reason()'s echo guard already trusts
202
- // (`ctx.store.prevCount(qId) > 0`), here applied per-candidate instead
203
- // of to the whole query — is VERBATIM present later in the query. A
204
- // query that embeds both an exchange's ask and its own already-given
205
- // reply (a conversation's turn plus its own prior answer, concatenated
206
- // raw by addTurn — or any caller pasting a transcript into one
207
- // respond() call; the check is Mind-bookkeeping-free, so it treats both
208
- // identically) has already spoken this root's answer — fusing it in
209
- // again would only restate it. Deliberately NOT a magnitude measure:
210
- // it fires on exact content-addressed recurrence, not on how strongly
211
- // the root resonates.
212
- const cont = await follow(ctx, root.anchor, qv);
213
- if (cont !== null && cont.length > 0 && indexOf(query, cont, root.end) >= 0) {
214
- ctx.trace?.step("alreadyAnswered", [rNode(ctx, root.anchor, "point", root.vote)], [rItem(cont, "continuation")], "this point's own learnt continuation already appears later in the query — already answered, not fused");
215
- continue;
216
- }
217
- const g = await project(ctx, root.anchor, qv);
218
- if (g === null || g.length === 0)
219
- continue;
220
- if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0))
221
- continue;
222
- pieces.push({ start: root.start, bytes: g });
223
- }
224
- if (pieces.length === 1) {
225
- t?.done([rItem(primary, "answer")], "no further independent point grounded");
226
- return primary;
227
- }
228
- pieces.sort((a, b) => a.start - b.start);
229
- let out = pieces[0].bytes;
230
- for (let i = 1; i < pieces.length; i++) {
231
- // An approximate-resonance miss (or a genuinely unlearnt junction) joins
232
- // the pieces bare — joinWithBridge surfaces it as a bridgeMiss step.
233
- out = await joinWithBridge(ctx, out, pieces[i].bytes);
234
- }
235
- t?.done([rItem(out, "answer", resolve(ctx, out) ?? undefined)], `fused ${pieces.length} independent points of attention into one answer`);
236
- return out;
237
- }
238
- // (resonance.js is already a static dependency above — `bridge` — so the old
239
- // dynamic import of pivotInto guarded against a cycle that does not exist.)
240
- import { containsSpan } from "./mechanisms/extraction.js";
@@ -1,20 +0,0 @@
1
- import type { MindContext, Recognition, Segment } from "./types.js";
2
- /** Decompose a byte stream into every stored form that leads somewhere
3
- * (has a continuation edge or a halo). Two complementary readings:
4
- *
5
- * • structural — walk the query's own perceived tree, naming each subtree
6
- * by findLeaf at the leaves and findBranch above. Catches every form
7
- * aligned to the query's segmentation.
8
- *
9
- * • canonical — re-derive the store's segmentation directly: at each byte,
10
- * the longest known leaf, chained into flat branches. Names forms the
11
- * query's own cut cannot, and records sub-leaf boundaries as `splits`.
12
- *
13
- * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
14
- export declare function recognise(ctx: MindContext, bytes: Uint8Array): Recognition;
15
- /** Segment bytes using the geometry's own groupings — leaf-parent
16
- * nodes from the perceived tree, with consecutive bare leaves merged
17
- * into one segment. Each segment's gist is perceived from its bytes
18
- * IN ISOLATION, so the same content has the same gist regardless of
19
- * where it appears. */
20
- export declare function segment(ctx: MindContext, bytes: Uint8Array): Segment[];
@@ -1,390 +0,0 @@
1
- // recognition.ts — Section 2 of the mind:
2
- // Address + Read over byte streams — decompose a query into its known forms.
3
- //
4
- // recognise — structural + canonical decomposition into every stored form
5
- // that leads somewhere (has a continuation edge or a halo).
6
- // segment — leaf-parent segmentation using the geometry's own groupings.
7
- import { rItem } from "./trace.js";
8
- import { canonResolve, foldTree, gistOf, latin1Key, perceive, resolve, } from "./primitives.js";
9
- import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
10
- import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
11
- import { isChunk } from "../sema.js";
12
- /** Decompose a byte stream into every stored form that leads somewhere
13
- * (has a continuation edge or a halo). Two complementary readings:
14
- *
15
- * • structural — walk the query's own perceived tree, naming each subtree
16
- * by findLeaf at the leaves and findBranch above. Catches every form
17
- * aligned to the query's segmentation.
18
- *
19
- * • canonical — re-derive the store's segmentation directly: at each byte,
20
- * the longest known leaf, chained into flat branches. Names forms the
21
- * query's own cut cannot, and records sub-leaf boundaries as `splits`.
22
- *
23
- * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
24
- export function recognise(ctx, bytes) {
25
- // Content-keyed memo — works for both single-turn respond() and multi-turn
26
- // respondTurn() (where the map persists across calls). ALWAYS consulted,
27
- // regardless of tracing — matching perceive()'s own memo, which carries no
28
- // trace gate at all. This memo is NOT an optional accelerator: recogniseImpl
29
- // walks the query's perceived tree via foldTree, whose subtree-resolution
30
- // fast path (see primitives.ts) skips invoking `visit` — and therefore
31
- // skips EMITTING SITES — for any subtree already cached in
32
- // ctx._resolvedSubtrees. A multi-turn conversation's stable-prefix fold
33
- // deliberately shares node OBJECTS across turns, so by the second call on
34
- // the exact same bytes, large swaths of the tree are already cached and
35
- // foldTree stops short of recursing into them — a second recogniseImpl
36
- // call on the SAME bytes is not idempotent; it silently finds FEWER sites
37
- // than the first (observed live: 31 sites → 5 on an immediate repeat
38
- // call). Skipping this memo "only while tracing" used to mean every
39
- // traced turn re-ran recogniseImpl from scratch at every one of the many
40
- // call sites that recognise the same query (cover, reason, articulate...),
41
- // each subsequent call silently more incomplete than the last — measurably
42
- // changing which mechanism grounds the answer, not just costing time. The
43
- // trace step must still fire on every call regardless (a cache hit is not
44
- // silent), so it is emitted here directly instead of only inside
45
- // recogniseImpl.
46
- if (ctx.recogniseMemo) {
47
- const key = latin1Key(bytes);
48
- const hit = ctx.recogniseMemo.get(key);
49
- if (hit !== undefined) {
50
- ctx.trace?.step("recognise", [rItem(bytes, "query")], hit.sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
51
- s.start,
52
- s.end,
53
- ])), `decompose the query into ${hit.sites.length} learnt form(s) that ` +
54
- `lead somewhere (over ${hit.leaves.length} perceived leaves) [cached]`);
55
- return hit;
56
- }
57
- const fresh = recogniseImpl(ctx, bytes);
58
- ctx.recogniseMemo.set(key, fresh);
59
- return fresh;
60
- }
61
- return recogniseImpl(ctx, bytes);
62
- }
63
- function recogniseImpl(ctx, bytes) {
64
- const store = ctx.store;
65
- const sites = [];
66
- const leaves = [];
67
- const splits = new Set();
68
- const starts = new Set();
69
- if (bytes.length === 0)
70
- return { sites, leaves, splits, starts };
71
- // Span-resolve memo for THIS call: the structural pass (sub-runs inside
72
- // leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
73
- // spans, and each resolve() is a full fold of the sub-span (fresh subarray
74
- // objects — the per-response perceive memo cannot see them). Keyed
75
- // numerically by (start, end); resolve is pure and the store is read-only
76
- // here, so a hit is exact.
77
- const spanIds = new Map();
78
- const resolveSpan = (start, end) => {
79
- const key = start * (bytes.length + 1) + end;
80
- let id = spanIds.get(key);
81
- if (id === undefined) {
82
- id = resolve(ctx, bytes.subarray(start, end));
83
- spanIds.set(key, id);
84
- }
85
- return id;
86
- };
87
- // Byte atoms (implicit negative-id single-byte leaves) are admitted as
88
- // recognised sites only while atoms can still DISCRIMINATE at this corpus
89
- // scale (see {@link atomIsHub}). On a small store a single-letter fact
90
- // ("a" → "A") is genuine learnt content and its site is essential; on a
91
- // large one every letter of every query would otherwise become a
92
- // "recognised form" — the bridge then finds junction connectors between
93
- // bare letters, cover follows edges hanging off them, and pure noise
94
- // ("qq8f3kz9…") grounds to an arbitrary learnt sentence instead of
95
- // silence. Atoms stay available as leaves (PASS-carried literals) and
96
- // through exact tier-0 resolution regardless.
97
- const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
98
- // Distinct probes (structural exact match, canon fallback, edge trims at
99
- // several offsets) can legitimately re-derive the SAME (start, end, id)
100
- // site from different tree nodes — a wide edge-trim search is exactly
101
- // this on purpose (see below). Duplicate site entries are not wrong
102
- // evidence, but they double the weight cover's derivation search gives
103
- // that span, distorting its cost model — the same span must count once.
104
- const seen = new Set();
105
- const emit = (start, end, id) => {
106
- if (id < 0 && atomsAreHubs)
107
- return;
108
- const key = start + "," + end + "," + id;
109
- if (seen.has(key))
110
- return;
111
- seen.add(key);
112
- if (leadsSomewhere(ctx, id)) {
113
- sites.push({ start, end, payload: id });
114
- }
115
- };
116
- // ── structural: the query's own perceived tree ──────────────────────
117
- starts.add(0);
118
- foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
119
- if (n.kids === null) {
120
- leaves.push({ start, end, bytes: n.leaf ?? new Uint8Array(0), node });
121
- }
122
- if (node !== null)
123
- emit(start, end, node);
124
- // Canonical fallback: a subtree whose exact content-addressed lookup
125
- // missed may still be a stored form under the response's equivalence
126
- // (case, width, whitespace — whatever the injected canonicalizer says).
127
- // O(subtree bytes) per miss, memoised per response; a no-op when no
128
- // canonicalizer was injected or the store has no canon index. A raw
129
- // leaf (n.kids === null) is single-byte and handled by the byte-atom
130
- // path above instead — canon equivalence only applies to composites.
131
- else if (n.kids !== null) {
132
- const cid = canonResolve(ctx, bytes.subarray(start, end));
133
- if (cid !== null)
134
- emit(start, end, cid);
135
- // The edge-trim fallbacks below remove 1 byte from a side; the
136
- // remainder must still be a composite (>= 2 bytes, the same floor
137
- // n.kids !== null enforces above) rather than degenerate into
138
- // single-byte-atom territory, which atomIsHub already governs
139
- // separately.
140
- else if (end - start - 1 >= 2) {
141
- // The chunk's own boundary is drawn by content geometry, not by
142
- // any notion of "form" — it can include one edge byte the query's
143
- // fold happened to attach here that the trained span never had
144
- // (e.g. a separator from the preceding chunk). The core has no
145
- // idea what that byte means; it only knows resolve()/canonResolve
146
- // are self-verifying (hash-then-verify, same discipline as every
147
- // content lookup here), so a blind one-byte-shorter guess on
148
- // either edge costs nothing when wrong and is trustworthy when it
149
- // hits. Two extra probes, only on the already-failed miss path.
150
- const left = resolve(ctx, bytes.subarray(start + 1, end));
151
- if (left !== null)
152
- emit(start + 1, end, left);
153
- const right = resolve(ctx, bytes.subarray(start, end - 1));
154
- if (right !== null)
155
- emit(start, end - 1, right);
156
- // A misalignment wider than one byte (e.g. more than one edge
157
- // separator swallowed) is not itself geometry-quantized — the
158
- // WRITE side's canonical index (canonicalWindows) interns sliding
159
- // W−1/W-length windows over leaf ids at EVERY offset, not just
160
- // radix-aligned ones (see canonical.ts) — so the offset that
161
- // recovers a trained span can be anything, not a multiple of W.
162
- // What IS bounded is how far it's worth looking: chainReach(W)=W²,
163
- // the same reach the canonical pass (tryChain) trusts for a chain
164
- // rebuilt off the query's own fold. Every candidate offset is
165
- // gated by store.findBranch(leafIds) first — the SAME cheap,
166
- // fold-free existence check tryChain already uses — so the extra
167
- // resolve() fold (the real cost) is only paid when a branch could
168
- // plausibly exist there, not for every offset. The node itself is
169
- // also bounded to chunk-scale (end - start <= W²): widening this at
170
- // whole-query/root scale can rediscover a smaller subtree's own
171
- // content as a second, overlapping site the structural walk's own
172
- // finer recursion already emits correctly on its own — a duplicate
173
- // that downstream derivation can stitch into a wrong answer.
174
- const W = ctx.space.maxGroup;
175
- for (let k = 1; end - start <= W * W && k <= W * W && start + k < end - 1; k++) {
176
- const lIds = leafIdRun(ctx, bytes, start + k, end);
177
- if (lIds !== null && store.findBranch(lIds) !== null) {
178
- const eLeft = resolve(ctx, bytes.subarray(start + k, end));
179
- if (eLeft !== null)
180
- emit(start + k, end, eLeft);
181
- }
182
- const rIds = leafIdRun(ctx, bytes, start, end - k);
183
- if (rIds !== null && store.findBranch(rIds) !== null) {
184
- const eRight = resolve(ctx, bytes.subarray(start, end - k));
185
- if (eRight !== null)
186
- emit(start, end - k, eRight);
187
- }
188
- }
189
- // A REAL extra word at the left edge (a discourse connective like
190
- // "And " prepended to a follow-up turn — not boundary noise, actual
191
- // content the injected canonicalizer has no equivalence for) shows
192
- // up as a canon-miss too big for the chunk-scale search above: the
193
- // turn is its OWN segment (stable-prefix folded independently — see
194
- // mind.ts's _growContext), so it can be turn/segment-scale, not
195
- // chunk-scale. Widening the size bound itself reopens the root-
196
- // scale false-positive this module already fixed once (test/46);
197
- // widening the SEARCH instead — trying every position up to W
198
- // chunk-widths from the left edge that the query's OWN fold treats
199
- // as a chunk boundary (`starts`, the same set the canonical pass
200
- // privileges with full chain reach) — does not: `starts.has(p)` is
201
- // fold EVIDENCE the query produced on its own (a leaf-parent chunk
202
- // like "And " is visited, and its start added to `starts`, before
203
- // this composite ever runs — foldTree is post-order), never a blind
204
- // guess. Bounded to W candidates, each an O(1) set lookup before
205
- // paying for the real canonResolve fold — canonResolve, not
206
- // resolve()/findBranch, because the gap here is often exactly the
207
- // kind of equivalence (case, in the live trace) canon exists for,
208
- // not just an exact-content coincidence.
209
- for (let k = 1; k <= W; k++) {
210
- const p = start + k * W;
211
- if (p >= end - 1 || !starts.has(p))
212
- continue;
213
- const cid = canonResolve(ctx, bytes.subarray(p, end));
214
- if (cid !== null)
215
- emit(p, end, cid);
216
- }
217
- }
218
- }
219
- if (isChunk(n)) {
220
- starts.add(start);
221
- // Try every sub-span within this leaf-parent.
222
- const leafOffsets = [];
223
- let off = start;
224
- for (const k of n.kids) {
225
- leafOffsets.push(off);
226
- off += k.leaf?.length ?? 0;
227
- }
228
- // Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
229
- // query's own fold did not itself choose as a boundary — the same
230
- // opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
231
- // guards below (see its comment). Only the chunk's own left edge
232
- // (i === 0, already registered in `starts` above) carries the fold's
233
- // evidence; interior sub-starts are exempt from the guard only while
234
- // atoms themselves still discriminate at this corpus scale.
235
- for (let i = 0; i < n.kids.length; i++) {
236
- if (i > 0 && atomsAreHubs)
237
- break;
238
- const subIds = [];
239
- for (let j = i; j < n.kids.length; j++) {
240
- const kj = n.kids[j];
241
- if (kj.kids !== null || !kj.leaf)
242
- break;
243
- const lid = store.findLeaf(kj.leaf);
244
- if (lid === null)
245
- break;
246
- subIds.push(lid);
247
- const branch = store.findBranch(subIds);
248
- if (branch === null)
249
- continue;
250
- const subEnd = leafOffsets[j] + (kj.leaf?.length ?? 0);
251
- const resolved = resolveSpan(leafOffsets[i], subEnd);
252
- if (resolved !== null)
253
- emit(leafOffsets[i], subEnd, resolved);
254
- }
255
- }
256
- }
257
- });
258
- // ── canonical: longest-known-leaf re-segmentation ──────────────────
259
- const W = ctx.space.maxGroup;
260
- const singleLeaf = new Array(bytes.length).fill(null);
261
- for (let p = 0; p < bytes.length; p++) {
262
- const id = leafIdAt(ctx, bytes, p);
263
- if (id !== null)
264
- singleLeaf[p] = { id, end: p + 1 };
265
- }
266
- const leafFrom = (p) => {
267
- if (p >= bytes.length)
268
- return null;
269
- return singleLeaf[p];
270
- };
271
- const chunkEnd = new Uint32Array(bytes.length);
272
- const sorted = [...starts].sort((a, b) => a - b);
273
- for (let si = 0; si < sorted.length; si++) {
274
- const chunkStart = sorted[si];
275
- const chunkLimit = si + 1 < sorted.length ? sorted[si + 1] : bytes.length;
276
- for (let p = chunkStart; p < chunkLimit; p++) {
277
- chunkEnd[p] = chunkLimit;
278
- }
279
- }
280
- // A chain rebuilt from a NON-boundary offset (the query's own perceived
281
- // cut, `starts`, never chose to segment here) is opportunistic: the same
282
- // byte-atom coincidence the hub guard above already exists for, just
283
- // spelled over 2+ leaves instead of 1. At small corpus scale that's fine
284
- // — coincidence is rare and every chain is real evidence (see `atomIsHub`).
285
- // Past the scale where atoms themselves stop discriminating, the same
286
- // uniform-expectation argument bounds a CHAIN'S commonality too: it is at
287
- // least as rare as its rarest atom, so a store where atoms are hubs makes
288
- // interior chain reconstructions no more trustworthy than the atoms they
289
- // are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
290
- // hub-scale atoms, chained at an offset nothing in the query's own fold
291
- // selected). Chains that start ON a boundary carry the fold's own
292
- // evidence instead and are exempt.
293
- const tryChain = (p, maxIds, boundary) => {
294
- const first = leafFrom(p);
295
- if (!first)
296
- return;
297
- emit(p, first.end, first.id);
298
- const ids = [first.id];
299
- let pos = first.end;
300
- let prevId = null;
301
- for (let depth = 1; pos < bytes.length && ids.length <= maxIds; depth++) {
302
- const nx = leafFrom(pos);
303
- if (!nx)
304
- break;
305
- ids.push(nx.id);
306
- pos = nx.end;
307
- if (store.findBranch(ids) === null)
308
- continue;
309
- if (!boundary && atomsAreHubs)
310
- continue;
311
- const id = resolveSpan(p, pos);
312
- if (id === null || id === prevId)
313
- continue;
314
- prevId = id;
315
- emit(p, pos, id);
316
- }
317
- };
318
- for (let p = 0; p < bytes.length; p++) {
319
- if (starts.has(p)) {
320
- tryChain(p, chainReach(W), true); // boundary start — full reach
321
- }
322
- else {
323
- const limit = chunkEnd[p] + W;
324
- tryChain(p, Math.min(limit - p, chainReach(W)), false);
325
- }
326
- }
327
- // ── splits: a form boundary that does not fall on a leaf edge ────────
328
- const leafEdges = new Set([bytes.length]);
329
- for (const lf of leaves)
330
- leafEdges.add(lf.start);
331
- for (const s of sites) {
332
- if (!leafEdges.has(s.start))
333
- splits.add(s.start);
334
- if (!leafEdges.has(s.end))
335
- splits.add(s.end);
336
- }
337
- ctx.trace?.step("recognise", [rItem(bytes, "query")], sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
338
- s.start,
339
- s.end,
340
- ])), `decompose the query into ${sites.length} learnt form(s) that lead somewhere` +
341
- ` (over ${leaves.length} perceived leaves)`);
342
- return { sites, leaves, splits, starts };
343
- }
344
- /** Segment bytes using the geometry's own groupings — leaf-parent
345
- * nodes from the perceived tree, with consecutive bare leaves merged
346
- * into one segment. Each segment's gist is perceived from its bytes
347
- * IN ISOLATION, so the same content has the same gist regardless of
348
- * where it appears. */
349
- export function segment(ctx, bytes) {
350
- const tree = perceive(ctx, bytes);
351
- const out = [];
352
- let pendingStart = -1;
353
- let pendingEnd = -1;
354
- const flush = () => {
355
- if (pendingStart >= 0 && pendingEnd > pendingStart) {
356
- out.push({
357
- start: pendingStart,
358
- end: pendingEnd,
359
- v: gistOf(ctx, bytes.subarray(pendingStart, pendingEnd)),
360
- });
361
- }
362
- pendingStart = -1;
363
- pendingEnd = -1;
364
- };
365
- const walk = (n, start) => {
366
- if (n.kids === null) {
367
- const end = start + (n.leaf?.length ?? 0);
368
- if (pendingStart < 0)
369
- pendingStart = start;
370
- pendingEnd = end;
371
- return end;
372
- }
373
- if (isChunk(n)) {
374
- flush();
375
- let end = start;
376
- for (const c of n.kids)
377
- end += c.leaf?.length ?? 0;
378
- out.push({ start, end, v: gistOf(ctx, bytes.subarray(start, end)) });
379
- return end;
380
- }
381
- flush();
382
- let pos = start;
383
- for (const c of n.kids)
384
- pos = walk(c, pos);
385
- return pos;
386
- };
387
- walk(tree, 0);
388
- flush();
389
- return out;
390
- }