@hviana/sema 0.5.2 → 0.5.4

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 (157) hide show
  1. package/AGENTS.md +115 -53
  2. package/HOW_IT_WORKS.md +291 -184
  3. package/jsr.json +6 -0
  4. package/package.json +1 -1
  5. package/src/mind/bridge.ts +10 -104
  6. package/src/mind/match.ts +497 -7
  7. package/src/mind/{prefix-completion.ts → mechanisms/prefix-completion.ts} +66 -92
  8. package/src/mind/mechanisms/recall.ts +38 -129
  9. package/src/mind/mechanisms/reference.ts +343 -0
  10. package/src/mind/mind.ts +12 -8
  11. package/src/mind/pipeline-mechanism.ts +120 -3
  12. package/src/mind/pipeline.ts +16 -2
  13. package/src/mind/traverse.ts +92 -1
  14. package/src/store.ts +13 -4
  15. package/test/33-multi-candidate.test.mjs +21 -11
  16. package/test/70-prefix-completion.test.mjs +1 -1
  17. package/test/72-prefix-candidate-supply.test.mjs +7 -9
  18. package/test/74-prefix-trap-not-sprung-early.test.mjs +1 -1
  19. package/test/76-reference-binding.test.mjs +543 -0
  20. package/dist/example/demo.d.ts +0 -1
  21. package/dist/example/demo.js +0 -39
  22. package/dist/example/train_base.d.ts +0 -93
  23. package/dist/example/train_base.js +0 -2269
  24. package/dist/src/alphabet.d.ts +0 -7
  25. package/dist/src/alphabet.js +0 -33
  26. package/dist/src/alu/src/alu.d.ts +0 -185
  27. package/dist/src/alu/src/alu.js +0 -440
  28. package/dist/src/alu/src/expr.d.ts +0 -61
  29. package/dist/src/alu/src/expr.js +0 -318
  30. package/dist/src/alu/src/index.d.ts +0 -11
  31. package/dist/src/alu/src/index.js +0 -19
  32. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  33. package/dist/src/alu/src/kernel-arith.js +0 -264
  34. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  35. package/dist/src/alu/src/kernel-bits.js +0 -152
  36. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  37. package/dist/src/alu/src/kernel-logic.js +0 -60
  38. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  39. package/dist/src/alu/src/kernel-nd.js +0 -208
  40. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  41. package/dist/src/alu/src/kernel-numeric.js +0 -366
  42. package/dist/src/alu/src/operation.d.ts +0 -168
  43. package/dist/src/alu/src/operation.js +0 -189
  44. package/dist/src/alu/src/parser.d.ts +0 -221
  45. package/dist/src/alu/src/parser.js +0 -577
  46. package/dist/src/alu/src/resonance.d.ts +0 -55
  47. package/dist/src/alu/src/resonance.js +0 -126
  48. package/dist/src/alu/src/text.d.ts +0 -31
  49. package/dist/src/alu/src/text.js +0 -73
  50. package/dist/src/alu/src/value.d.ts +0 -109
  51. package/dist/src/alu/src/value.js +0 -300
  52. package/dist/src/alu/test/alu.test.d.ts +0 -1
  53. package/dist/src/alu/test/alu.test.js +0 -764
  54. package/dist/src/bytes.d.ts +0 -14
  55. package/dist/src/bytes.js +0 -59
  56. package/dist/src/canon.d.ts +0 -45
  57. package/dist/src/canon.js +0 -85
  58. package/dist/src/config.d.ts +0 -111
  59. package/dist/src/config.js +0 -91
  60. package/dist/src/derive/src/deduction.d.ts +0 -136
  61. package/dist/src/derive/src/deduction.js +0 -159
  62. package/dist/src/derive/src/index.d.ts +0 -8
  63. package/dist/src/derive/src/index.js +0 -11
  64. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  65. package/dist/src/derive/src/priority-queue.js +0 -73
  66. package/dist/src/derive/src/rewrite.d.ts +0 -56
  67. package/dist/src/derive/src/rewrite.js +0 -100
  68. package/dist/src/derive/src/trie.d.ts +0 -90
  69. package/dist/src/derive/src/trie.js +0 -217
  70. package/dist/src/derive/test/derive.test.d.ts +0 -1
  71. package/dist/src/derive/test/derive.test.js +0 -122
  72. package/dist/src/extension.d.ts +0 -37
  73. package/dist/src/extension.js +0 -7
  74. package/dist/src/geometry.d.ts +0 -233
  75. package/dist/src/geometry.js +0 -1089
  76. package/dist/src/index.d.ts +0 -17
  77. package/dist/src/index.js +0 -19
  78. package/dist/src/ingest-cache.d.ts +0 -41
  79. package/dist/src/ingest-cache.js +0 -165
  80. package/dist/src/meter.d.ts +0 -176
  81. package/dist/src/meter.js +0 -274
  82. package/dist/src/mind/articulation.d.ts +0 -6
  83. package/dist/src/mind/articulation.js +0 -99
  84. package/dist/src/mind/attention.d.ts +0 -430
  85. package/dist/src/mind/attention.js +0 -2418
  86. package/dist/src/mind/bridge.d.ts +0 -39
  87. package/dist/src/mind/bridge.js +0 -998
  88. package/dist/src/mind/canonical.d.ts +0 -34
  89. package/dist/src/mind/canonical.js +0 -93
  90. package/dist/src/mind/frame-filler.d.ts +0 -15
  91. package/dist/src/mind/frame-filler.js +0 -535
  92. package/dist/src/mind/graph-search.d.ts +0 -294
  93. package/dist/src/mind/graph-search.js +0 -996
  94. package/dist/src/mind/index.d.ts +0 -9
  95. package/dist/src/mind/index.js +0 -5
  96. package/dist/src/mind/junction.d.ts +0 -137
  97. package/dist/src/mind/junction.js +0 -358
  98. package/dist/src/mind/learning.d.ts +0 -75
  99. package/dist/src/mind/learning.js +0 -265
  100. package/dist/src/mind/match.d.ts +0 -192
  101. package/dist/src/mind/match.js +0 -666
  102. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  103. package/dist/src/mind/mechanisms/alu.js +0 -36
  104. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  105. package/dist/src/mind/mechanisms/cast.js +0 -1186
  106. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  107. package/dist/src/mind/mechanisms/confluence.js +0 -271
  108. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  109. package/dist/src/mind/mechanisms/cover.js +0 -232
  110. package/dist/src/mind/mechanisms/extraction.d.ts +0 -33
  111. package/dist/src/mind/mechanisms/extraction.js +0 -327
  112. package/dist/src/mind/mechanisms/recall.d.ts +0 -16
  113. package/dist/src/mind/mechanisms/recall.js +0 -558
  114. package/dist/src/mind/mind.d.ts +0 -386
  115. package/dist/src/mind/mind.js +0 -754
  116. package/dist/src/mind/pipeline-mechanism.d.ts +0 -183
  117. package/dist/src/mind/pipeline-mechanism.js +0 -521
  118. package/dist/src/mind/pipeline.d.ts +0 -49
  119. package/dist/src/mind/pipeline.js +0 -331
  120. package/dist/src/mind/prefix-completion.d.ts +0 -59
  121. package/dist/src/mind/prefix-completion.js +0 -270
  122. package/dist/src/mind/primitives.d.ts +0 -85
  123. package/dist/src/mind/primitives.js +0 -333
  124. package/dist/src/mind/rationale.d.ts +0 -139
  125. package/dist/src/mind/rationale.js +0 -163
  126. package/dist/src/mind/reasoning.d.ts +0 -42
  127. package/dist/src/mind/reasoning.js +0 -305
  128. package/dist/src/mind/recognition.d.ts +0 -20
  129. package/dist/src/mind/recognition.js +0 -696
  130. package/dist/src/mind/resonance.d.ts +0 -35
  131. package/dist/src/mind/resonance.js +0 -365
  132. package/dist/src/mind/trace.d.ts +0 -15
  133. package/dist/src/mind/trace.js +0 -73
  134. package/dist/src/mind/traverse.d.ts +0 -158
  135. package/dist/src/mind/traverse.js +0 -716
  136. package/dist/src/mind/types.d.ts +0 -418
  137. package/dist/src/mind/types.js +0 -179
  138. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  139. package/dist/src/rabitq-ivf/src/database.js +0 -201
  140. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  141. package/dist/src/rabitq-ivf/src/index.js +0 -4
  142. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  143. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  144. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  145. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  146. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  147. package/dist/src/rabitq-ivf/src/rabitq.js +0 -313
  148. package/dist/src/sema.d.ts +0 -41
  149. package/dist/src/sema.js +0 -77
  150. package/dist/src/store-sqlite.d.ts +0 -184
  151. package/dist/src/store-sqlite.js +0 -942
  152. package/dist/src/store.d.ts +0 -762
  153. package/dist/src/store.js +0 -1823
  154. package/dist/src/vec.d.ts +0 -31
  155. package/dist/src/vec.js +0 -109
  156. package/src/mind/frame-filler.ts +0 -604
  157. package/test/69-frame-filler.test.mjs +0 -115
@@ -1,696 +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 { canonHash } from "../canon.js";
12
- import { isChunk } from "../sema.js";
13
- /** Decompose a byte stream into every stored form that leads somewhere
14
- * (has a continuation edge or a halo). Two complementary readings:
15
- *
16
- * • structural — walk the query's own perceived tree, naming each subtree
17
- * by findLeaf at the leaves and findBranch above. Catches every form
18
- * aligned to the query's segmentation.
19
- *
20
- * • canonical — re-derive the store's segmentation directly: at each byte,
21
- * the longest known leaf, chained into flat branches. Names forms the
22
- * query's own cut cannot, and records sub-leaf boundaries as `splits`.
23
- *
24
- * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
25
- export function recognise(ctx, bytes) {
26
- // Content-keyed memo — works for both single-turn respond() and multi-turn
27
- // respondTurn() (where the map persists across calls). ALWAYS consulted,
28
- // regardless of tracing — matching perceive()'s own memo, which carries no
29
- // trace gate at all.
30
- //
31
- // This memo is an accelerator, and that is now the whole of it: repeated
32
- // recognition of the same query is ordinary within one response (cover,
33
- // reason and articulate all recognise it) and recogniseImpl is O(n ·
34
- // maxGroup) probes each time.
35
- //
36
- // IT USED TO BE LOAD-BEARING FOR CORRECTNESS, and the history is worth
37
- // keeping because it explains why there is no trace gate here. foldTree's
38
- // subtree-resolution fast path (primitives.ts) once returned on a cache hit
39
- // WITHOUT recursing, so it skipped invoking `visit` — and therefore skipped
40
- // EMITTING SITES — for any subtree already in ctx._resolvedSubtrees. A
41
- // conversation's incremental fold deliberately shares node OBJECTS across
42
- // turns, so by the second call on the same bytes large swaths of the tree
43
- // were already cached and recogniseImpl silently found FEWER sites than the
44
- // first call (observed live: 31 → 5). Skipping this memo "only while
45
- // tracing" therefore meant every traced turn re-ran recogniseImpl at each of
46
- // those call sites, each result more incomplete than the last — changing
47
- // which mechanism grounded the answer, not merely costing time.
48
- //
49
- // foldTree no longer does that: it takes the fast path only when no `visit`
50
- // is supplied, so a walk that emits sites always walks in full and the id
51
- // cache is reduced to eliding store probes (see primitives.ts). recognise()
52
- // is idempotent on its own now — verified with the memo bypassed, the
53
- // subtree cache warm and the tree object shared: three consecutive calls on
54
- // the same 544-byte context returned sites=2 leaves=544 splits=0 starts=88,
55
- // identical every time.
56
- //
57
- // The unconditional consult STAYS regardless. A memo whose absence can only
58
- // cost time is still not something to gate on whether an audit happens to be
59
- // attached: tracing must not change what the pipeline computes, and the
60
- // cheapest way to guarantee that is for the trace flag to touch nothing but
61
- // the trace. The trace step must still fire on every call (a cache hit is
62
- // not silent), so it is emitted here directly rather than only inside
63
- // recogniseImpl.
64
- if (ctx.recogniseMemo) {
65
- const key = latin1Key(bytes);
66
- const hit = ctx.recogniseMemo.get(key);
67
- if (hit !== undefined) {
68
- if (ctx.meter)
69
- ctx.meter.recogniseHits++;
70
- ctx.trace?.step("recognise", [rItem(bytes, "query")], hit.sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
71
- s.start,
72
- s.end,
73
- ])), `decompose the query into ${hit.sites.length} learnt form(s) that ` +
74
- `lead somewhere (over ${hit.leaves.length} perceived leaves) [cached]`);
75
- return hit;
76
- }
77
- const fresh = recogniseImpl(ctx, bytes);
78
- ctx.recogniseMemo.set(key, fresh);
79
- return fresh;
80
- }
81
- return recogniseImpl(ctx, bytes);
82
- }
83
- function recogniseImpl(ctx, bytes) {
84
- if (ctx.meter) {
85
- ctx.meter.recognitions++;
86
- ctx.meter.recognisedBytes += bytes.length;
87
- }
88
- const store = ctx.store;
89
- const sites = [];
90
- const leaves = [];
91
- const splits = new Set();
92
- const starts = new Set();
93
- // The same cuts in ASCENDING order. The post-order walk below visits
94
- // leaf-parents left to right, so appending as they are added keeps this
95
- // sorted with no comparison — which is what lets the composite search find
96
- // its candidates by binary search instead of rescanning the whole set.
97
- const startList = [];
98
- if (bytes.length === 0)
99
- return { sites, leaves, splits, starts };
100
- // Span-resolve memo for THIS call: the structural pass (sub-runs inside
101
- // leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
102
- // spans, and each resolve() is a full fold of the sub-span (fresh subarray
103
- // objects — the per-response perceive memo cannot see them). Keyed
104
- // numerically by (start, end); resolve is pure and the store is read-only
105
- // here, so a hit is exact.
106
- const spanIds = new Map();
107
- const resolveSpan = (start, end) => {
108
- const key = start * (bytes.length + 1) + end;
109
- let id = spanIds.get(key);
110
- if (id === undefined) {
111
- id = resolve(ctx, bytes.subarray(start, end));
112
- spanIds.set(key, id);
113
- }
114
- return id;
115
- };
116
- // Byte atoms (implicit negative-id single-byte leaves) are admitted as
117
- // recognised sites only while atoms can still DISCRIMINATE at this corpus
118
- // scale (see {@link atomIsHub}). On a small store a single-letter fact
119
- // ("a" → "A") is genuine learnt content and its site is essential; on a
120
- // large one every letter of every query would otherwise become a
121
- // "recognised form" — the bridge then finds junction connectors between
122
- // bare letters, cover follows edges hanging off them, and pure noise
123
- // ("qq8f3kz9…") grounds to an arbitrary learnt sentence instead of
124
- // silence. Atoms stay available as leaves (PASS-carried literals) and
125
- // through exact tier-0 resolution regardless.
126
- const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
127
- // Distinct probes (structural exact match, canon fallback, edge trims at
128
- // several offsets) can legitimately re-derive the SAME (start, end, id)
129
- // site from different tree nodes — a wide edge-trim search is exactly
130
- // this on purpose (see below). Duplicate site entries are not wrong
131
- // evidence, but they double the weight cover's derivation search gives
132
- // that span, distorting its cost model — the same span must count once.
133
- const seen = new Set();
134
- const emit = (start, end, id) => {
135
- if (id < 0 && atomsAreHubs)
136
- return;
137
- // A SITE MUST SPAN ONE RIVER WINDOW. Below W, byte overlap is chance,
138
- // not evidence — the principle identityBar already states ("below one
139
- // river window, byte overlap is chance") and the bridge's attestedQ
140
- // already applies ("spans shorter than W carry no window of their own").
141
- // No new constant.
142
- //
143
- // This REPLACES the false premise it used to share with fuse() and
144
- // tryChain: those gates asked "does this offset sit on a fold boundary?"
145
- // and read the answer from `starts`, which is exactly {0, W, 2W, …}
146
- // because riverFold groups fixed-arity — arithmetic, not evidence.
147
- //
148
- // Measured on the 17.9M-node store, over the sites of 7 probes (1 good,
149
- // 11 junk by hand-labelling, corrected for whole-query forms):
150
- // len >= W rejects "hi"(2) "of"(2) "is"(2) "di"(2) "the"(3),
151
- // admits "Eiffel Tower"(12) and both whole-query forms
152
- // len >= W-1 admits "the" — W-1 is the write side's straddle
153
- // neighbour for RETRIEVAL, never a claim about units
154
- // §2.7 saturation admits 11/11 junk: edgeAncestors on a site node
155
- // reaches 1..48 contexts, so dominates(ctx, N) needs
156
- // ctx > 162805 and never fires; every site reads DISC
157
- // rarity does not separate: "hi" has 1 container, "the" 572
158
- //
159
- // A span covering the WHOLE query is exempt: then it is not a fragment of
160
- // something longer, it is the question ("hi" asked on its own).
161
- if (atomsAreHubs && end - start < ctx.space.maxGroup &&
162
- !(start === 0 && end === bytes.length))
163
- return;
164
- const key = start + "," + end + "," + id;
165
- if (seen.has(key))
166
- return;
167
- seen.add(key);
168
- if (leadsSomewhere(ctx, id)) {
169
- sites.push({ start, end, payload: id });
170
- }
171
- };
172
- // ── structural: the query's own perceived tree ──────────────────────
173
- starts.add(0);
174
- startList.push(0);
175
- foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
176
- if (n.kids === null) {
177
- leaves.push({ start, end, bytes: n.leaf ?? new Uint8Array(0), node });
178
- }
179
- if (node !== null)
180
- emit(start, end, node);
181
- // Canonical fallback: a subtree whose exact content-addressed lookup
182
- // missed may still be a stored form under the response's equivalence
183
- // (case, width, whitespace — whatever the injected canonicalizer says).
184
- // O(subtree bytes) per miss, memoised per response; a no-op when no
185
- // canonicalizer was injected or the store has no canon index. A raw
186
- // leaf (n.kids === null) is single-byte and handled by the byte-atom
187
- // path above instead — canon equivalence only applies to composites.
188
- else if (n.kids !== null) {
189
- const cid = canonResolve(ctx, bytes.subarray(start, end));
190
- if (cid !== null)
191
- emit(start, end, cid);
192
- // The edge-trim fallbacks below remove 1 byte from a side; the
193
- // remainder must still be a composite (>= 2 bytes, the same floor
194
- // n.kids !== null enforces above) rather than degenerate into
195
- // single-byte-atom territory, which atomIsHub already governs
196
- // separately.
197
- else if (end - start - 1 >= 2) {
198
- // The chunk's own boundary is drawn by content geometry, not by
199
- // any notion of "form" — it can include one edge byte the query's
200
- // fold happened to attach here that the trained span never had
201
- // (e.g. a separator from the preceding chunk). The core has no
202
- // idea what that byte means; it only knows resolve()/canonResolve
203
- // are self-verifying (hash-then-verify, same discipline as every
204
- // content lookup here), so a blind one-byte-shorter guess on
205
- // either edge costs nothing when wrong and is trustworthy when it
206
- // hits. Two extra probes, only on the already-failed miss path.
207
- const left = resolve(ctx, bytes.subarray(start + 1, end));
208
- if (left !== null)
209
- emit(start + 1, end, left);
210
- const right = resolve(ctx, bytes.subarray(start, end - 1));
211
- if (right !== null)
212
- emit(start, end - 1, right);
213
- // A misalignment wider than one byte (e.g. more than one edge
214
- // separator swallowed) is not itself geometry-quantized — the
215
- // WRITE side's canonical index (canonicalWindows) interns sliding
216
- // W−1/W-length windows over leaf ids at EVERY offset, not just
217
- // radix-aligned ones (see canonical.ts) — so the offset that
218
- // recovers a trained span can be anything, not a multiple of W.
219
- // What IS bounded is how far it's worth looking: chainReach(W)=W²,
220
- // the same reach the canonical pass (tryChain) trusts for a chain
221
- // rebuilt off the query's own fold. Every candidate offset is
222
- // gated by store.findBranch(leafIds) first — the SAME cheap,
223
- // fold-free existence check tryChain already uses — so the extra
224
- // resolve() fold (the real cost) is only paid when a branch could
225
- // plausibly exist there, not for every offset. The node itself is
226
- // also bounded to chunk-scale (end - start <= W²): widening this at
227
- // whole-query/root scale can rediscover a smaller subtree's own
228
- // content as a second, overlapping site the structural walk's own
229
- // finer recursion already emits correctly on its own — a duplicate
230
- // that downstream derivation can stitch into a wrong answer.
231
- const W = ctx.space.maxGroup;
232
- for (let k = 1; end - start <= W * W && k <= W * W && start + k < end - 1; k++) {
233
- const lIds = leafIdRun(ctx, bytes, start + k, end);
234
- if (lIds !== null && store.findBranch(lIds) !== null) {
235
- const eLeft = resolve(ctx, bytes.subarray(start + k, end));
236
- if (eLeft !== null)
237
- emit(start + k, end, eLeft);
238
- }
239
- const rIds = leafIdRun(ctx, bytes, start, end - k);
240
- if (rIds !== null && store.findBranch(rIds) !== null) {
241
- const eRight = resolve(ctx, bytes.subarray(start, end - k));
242
- if (eRight !== null)
243
- emit(start, end - k, eRight);
244
- }
245
- }
246
- // A trained form embedded at this span's left edge, past the
247
- // chunk-scale bound above. The loop above probes exactly this — trim
248
- // k leading bytes, verify the remainder is a stored branch — but only
249
- // for spans of at most W². A turn prefixed with a connective is
250
- // turn-scale, so it never qualified.
251
- //
252
- // Widening that loop's SIZE bound is what reopens test/46's
253
- // root-scale false positive. Widening only its LEFT trim, to a
254
- // bounded W offsets, does not: every candidate is still verified by
255
- // exact content addressing (the leaf-id run must BE a stored branch),
256
- // and the result always ends where this span ends, so it can never
257
- // introduce the smaller-subtree duplicate that regression was about.
258
- //
259
- // This replaces an assumption that no longer holds — that such a
260
- // form's left edge must be a cut the fold itself drew. It held while
261
- // cuts had long memory and a turn boundary reliably produced one; a
262
- // bounded-window rule re-synchronises a byte or two INTO the turn
263
- // instead, so the edge itself is often not a cut ("And " ends at 65,
264
- // and the fold's nearest cuts are 61 and 67).
265
- // No leaf-id prefilter here, unlike the loop above: a leaf id is the
266
- // LONGEST known leaf at a position, so the run itself is context
267
- // sensitive — measured, the embedded copy of a trained form yields a
268
- // different run from the standalone one and findBranch misses even
269
- // though the bytes resolve exactly (span [65,94): findBranch null,
270
- // resolve 91). With only W candidates the exact fold is affordable,
271
- // and it is the stronger evidence anyway: if it resolves, these exact
272
- // bytes ARE a stored node.
273
- for (let k = 1; k <= W && start + k < end - 1; k++) {
274
- const eLeft = resolve(ctx, bytes.subarray(start + k, end));
275
- if (eLeft !== null)
276
- emit(start + k, end, eLeft);
277
- }
278
- // THE SAME SEARCH ON THE OTHER EDGE. Everything above trims from the
279
- // LEFT and keeps this span's END fixed, so a stored form was findable
280
- // only when it ENDED where a fold node ends. Measured on a 12-context
281
- // store, probing for a trained 47-byte sentence wrapped in filler:
282
- // 1-4 bytes of LEFT padding kept it recognisable, while ONE byte of
283
- // right padding lost it. That asymmetry was never argued for — the
284
- // reasoning above is about a form's left edge landing on a cut, and it
285
- // says nothing about which side the noise is on.
286
- //
287
- // The stated hazard for widening this search is test/46's root-scale
288
- // false positive, and it is a hazard of the SIZE bound, not of the
289
- // direction: like its mirror this loop is bounded to W offsets and
290
- // every candidate is verified by exact content addressing, so it can
291
- // only ever emit spans that ARE stored nodes. Measured: neutral on
292
- // the suite, and the right-padded cases above become recognisable.
293
- for (let k = 1; k <= W && start < end - k - 1; k++) {
294
- const eRight = resolve(ctx, bytes.subarray(start, end - k));
295
- if (eRight !== null)
296
- emit(start, end - k, eRight);
297
- }
298
- // A REAL extra word at the left edge (a discourse connective like
299
- // "And " prepended to a follow-up turn — not boundary noise, actual
300
- // content the injected canonicalizer has no equivalence for) shows
301
- // up as a canon-miss too big for the chunk-scale search above: the
302
- // turn is its OWN segment, so it can be turn/segment-scale, not
303
- // chunk-scale. Widening the size bound itself reopens the root-scale
304
- // false-positive this module already fixed once (test/46); widening the
305
- // SEARCH instead does not, because every candidate is a cut the query's
306
- // OWN fold drew (`starts`, the same set the canonical pass privileges
307
- // with full chain reach) — fold EVIDENCE, never a blind guess.
308
- //
309
- // The candidates are the fold's own segment starts inside this span, in
310
- // order. They used to be probed at `start + k*W`, which assumed cuts
311
- // land on multiples of W; content-defined cuts do not, so that stride
312
- // tested offsets no segment ever began at and this search silently
313
- // never fired (test/44 pins it). Still bounded to W candidates, each
314
- // one O(1) from the sorted cut list before paying for a real
315
- // canonResolve fold — canonResolve, not resolve()/findBranch, because
316
- // the gap here is often exactly the kind of equivalence (case, in the
317
- // live trace) canon exists for, not an exact-content coincidence.
318
- // A deposit's ROOT is a whole-stream node, and a stream's ends are not
319
- // content cuts — so an embedded occurrence of a trained form reproduces
320
- // its SEGMENTS (which are offset-free) but never its root. What is
321
- // being looked for is therefore a suffix of this span that happens to be
322
- // a whole trained form, and its left edge can only be a cut the fold
323
- // itself drew. Candidates are taken from the RIGHT, nearest the end
324
- // first: the form ends where this node ends, so its start is near it.
325
- // Left-to-right was wrong — in test/44 the target's start is the 6th cut
326
- // from the end but the 12th from the beginning.
327
- //
328
- // `starts` is still filling (this runs inside the post-order walk), but
329
- // post-order guarantees every chunk BELOW this span is already in it —
330
- // exactly the set wanted. Bounded to chainReach(W) candidates, the same
331
- // reach the canonical pass trusts, so cost stays O(reach · span).
332
- let hi = startList.length; // first index past the last usable cut
333
- let lo = 0;
334
- while (lo < hi) {
335
- const mid = (lo + hi) >> 1;
336
- if (startList[mid] < end - 1)
337
- lo = mid + 1;
338
- else
339
- hi = mid;
340
- }
341
- const reach = chainReach(W);
342
- for (let k = 0; k < reach; k++) {
343
- const p = startList[lo - 1 - k];
344
- if (p === undefined || p <= start)
345
- break;
346
- const cid = canonResolve(ctx, bytes.subarray(p, end));
347
- if (cid !== null)
348
- emit(p, end, cid);
349
- }
350
- }
351
- }
352
- if (isChunk(n)) {
353
- starts.add(start);
354
- if (startList[startList.length - 1] !== start)
355
- startList.push(start);
356
- // Try every sub-span within this leaf-parent.
357
- const leafOffsets = [];
358
- let off = start;
359
- for (const k of n.kids) {
360
- leafOffsets.push(off);
361
- off += k.leaf?.length ?? 0;
362
- }
363
- // Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
364
- // query's own fold did not itself choose as a boundary — the same
365
- // opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
366
- // guards below (see its comment). Only the chunk's own left edge
367
- // (i === 0, already registered in `starts` above) carries the fold's
368
- // evidence; interior sub-starts are exempt from the guard only while
369
- // atoms themselves still discriminate at this corpus scale.
370
- for (let i = 0; i < n.kids.length; i++) {
371
- if (i > 0 && atomsAreHubs)
372
- break;
373
- const subIds = [];
374
- for (let j = i; j < n.kids.length; j++) {
375
- const kj = n.kids[j];
376
- if (kj.kids !== null || !kj.leaf)
377
- break;
378
- const lid = store.findLeaf(kj.leaf);
379
- if (lid === null)
380
- break;
381
- subIds.push(lid);
382
- const branch = store.findBranch(subIds);
383
- if (branch === null)
384
- continue;
385
- const subEnd = leafOffsets[j] + (kj.leaf?.length ?? 0);
386
- const resolved = resolveSpan(leafOffsets[i], subEnd);
387
- if (resolved !== null)
388
- emit(leafOffsets[i], subEnd, resolved);
389
- }
390
- }
391
- }
392
- });
393
- // ── canonical: longest-known-leaf re-segmentation ──────────────────
394
- const W = ctx.space.maxGroup;
395
- const singleLeaf = new Array(bytes.length).fill(null);
396
- for (let p = 0; p < bytes.length; p++) {
397
- const id = leafIdAt(ctx, bytes, p);
398
- if (id !== null)
399
- singleLeaf[p] = { id, end: p + 1 };
400
- }
401
- const leafFrom = (p) => {
402
- if (p >= bytes.length)
403
- return null;
404
- return singleLeaf[p];
405
- };
406
- // ── exact query-edge forms beyond the canonical chain reach ─────────
407
- //
408
- // At corpus scale off-boundary atom chains are deliberately suppressed,
409
- // but a whole trained form can be longer than chainReach(W) and sit at a
410
- // query edge without being a subtree of the query's larger root. Appending
411
- // another topic demonstrates the failure: the exact 30-byte trained
412
- // question `What is the capital of France?` ends inside the larger query's
413
- // [27,33) content segment, so neither the structural walk nor a W² chain
414
- // can name it.
415
- //
416
- // Probe only prefix/suffix endpoints within one maximum segment of the
417
- // query's own content cuts. The flat-branch lookup is byte-exact and runs
418
- // before resolveSpan pays for a fold; approximate evidence never enters.
419
- // This tier is needed only where atom chains are suppressed. Small stores
420
- // retain their existing decomposition unchanged.
421
- // ALWAYS ON, AND LINEAR. This used to be gated on `atomsAreHubs` — small
422
- // stores were said to "retain their existing decomposition unchanged", which
423
- // was true only while the query's fold was told where the turns were: every
424
- // turn was then a NODE, so the structural walk found it and this tier had
425
- // nothing to add. The fold no longer imposes turn boundaries (a turn start
426
- // is an ordinary interior offset now), so a trained form embedded in a
427
- // longer query is reachable ONLY here — the chain caps at chainReach(W)=W²
428
- // bytes and cannot span one. Measured: with the fold imposing boundaries
429
- // every turn is a node; without it, none is.
430
- //
431
- // Ungating it alone made inference QUADRATIC (test/14's constant-KB/s guard
432
- // went to 41.8s): every offset near a cut is an endpoint, and each probe
433
- // costs O(span) to slice the leaf-id run and hash it. The budget below is
434
- // what makes it affordable — see `spend`.
435
- {
436
- const allLeafIds = singleLeaf.map((x) => x?.id ?? null);
437
- if (allLeafIds.every((x) => x !== null)) {
438
- const radius = ctx.space.seats.length;
439
- const endpoints = new Set([0, bytes.length]);
440
- for (const cut of startList) {
441
- for (let p = Math.max(0, cut - radius); p <= Math.min(bytes.length, cut + radius); p++)
442
- endpoints.add(p);
443
- }
444
- const ordered = [...endpoints].sort((a, b) => a - b);
445
- // The leaf-id run is BYTE-EXACT, while `resolveSpan` behind it resolves
446
- // exactly OR canonically — so this gate was strictly narrower than its
447
- // own resolver, and every embedded form differing from its deposit only
448
- // by the response's equivalence (case, width) was dropped before the
449
- // resolver ever saw it. Rebuilding the run over canonicalized bytes
450
- // does NOT fix that: a differently-cased deposit's branch kid-ids are
451
- // not the query's leaf-id run under ANY canonicalization of the query,
452
- // so the second admission route has to be the canon INDEX itself — the
453
- // same candidate proposal `canonResolve` makes, and the same
454
- // cheap-probe-before-a-fold discipline the exact route already follows
455
- // (a hash and an indexed lookup; no fold, no vector, no scan). Both
456
- // routes only PROPOSE; `resolveSpan` still decides, so a hash-bucket
457
- // collision costs one fold and can never emit a wrong site (test/71).
458
- const canonAdmits = (start, end) => {
459
- const canon = ctx.canon;
460
- if (canon === null || !store.canonFind)
461
- return false;
462
- const key = canon(bytes.subarray(start, end));
463
- if (key.length === 0)
464
- return false;
465
- return store.canonFind(canonHash(key)).length > 0;
466
- };
467
- // The byte-exact route probes the SPAN ITSELF (see
468
- // Store.findFlatBranch): for a run of single-byte leaves the flat-kid
469
- // encoding is the identity, so the span's bytes ARE the branch key.
470
- // `subarray` is a view — this allocates nothing per probe, and the
471
- // bloom filter answers the misses without touching the database.
472
- const flatProbe = (start, end) => store.findFlatBranch
473
- ? store.findFlatBranch(bytes.subarray(start, end))
474
- : store.findBranch(allLeafIds.slice(start, end));
475
- // THE TWO ROUTES COST DIFFERENT THINGS, SO THEY ARE PRICED SEPARATELY.
476
- //
477
- // The exact route is a bloom-gated hash over a subarray VIEW: no
478
- // allocation, and a miss never reaches the database. It is cheap enough
479
- // to run on every endpoint, and that is what makes this tier able to
480
- // find a trained form embedded anywhere in the query.
481
- //
482
- // The canon route is not: it runs the canonicalizer over the span
483
- // (NFKC, case-fold, whitespace) and allocates a fresh key for every
484
- // probe. That is the O(span) cost with the heavy constant, and it is
485
- // the one worth a budget. Sharing ONE budget between them made the
486
- // cheap route starve on the expensive one's behalf — measured, test/71's
487
- // embedded differently-cased form needed 64x the budget to be found,
488
- // while the exact route it was competing with needed none of it.
489
- const probe = (start, end, canonBudget) => {
490
- if (end - start < W || end - start <= chainReach(W))
491
- return;
492
- if (flatProbe(start, end) === null) {
493
- if (!canonBudget)
494
- return;
495
- if (!canonAdmits(start, end))
496
- return;
497
- }
498
- const id = resolveSpan(start, end);
499
- if (id !== null)
500
- emit(start, end, id);
501
- };
502
- // A CUMULATIVE BYTE BUDGET, SPENT SHORTEST-SPAN-FIRST.
503
- //
504
- // Each probe costs O(span), and there are O(n) endpoints, so probing
505
- // them all is O(n²) — the quadratic this tier was gated to avoid. The
506
- // budget caps TOTAL probe bytes at a multiple of the query's own length,
507
- // which is what keeps whole-query inference linear.
508
- //
509
- // Spending it shortest-first is what makes the cap a scale bound rather
510
- // than a position bound: the tier recovers embedded forms up to roughly
511
- // √(2·budget) bytes ANYWHERE in the endpoint set, instead of walking the
512
- // endpoints in order and running out partway along the query. A form
513
- // longer than that is out of this tier's reach — but so is a form the
514
- // chain cannot span, and that is exactly the trade the budget prices.
515
- // The factor is chainReach(W), the same W² scale the chain already
516
- // trusts; no new constant.
517
- // The factor is chainReach(W) — the same W² scale the chain itself
518
- // trusts — so the cap is derived from the fold's geometry, never tuned.
519
- // (It was briefly an environment variable while the cost was being
520
- // measured; an env-read here would make inference non-reproducible,
521
- // which the determinism contract forbids outright.)
522
- // The factor is chainReach(W) — the same W² scale the chain itself
523
- // trusts — so the cap is derived from the fold's geometry, never tuned.
524
- // (It was briefly an environment variable while the cost was being
525
- // measured; an env-read here would make inference non-reproducible,
526
- // which the determinism contract forbids outright.)
527
- //
528
- // It now prices ONLY the canonicalizing route; the exact route runs on
529
- // every endpoint regardless, so exhausting this budget narrows which
530
- // equivalence-class forms are proposed, never which byte-exact ones.
531
- let budget = bytes.length * chainReach(W) * chainReach(W);
532
- const spend = (start, end) => {
533
- const span = end - start;
534
- const afford = span <= budget;
535
- if (afford)
536
- budget -= span;
537
- probe(start, end, afford);
538
- // Always keep walking: the exact route is unbudgeted, so running out
539
- // of canon budget must not stop the scan.
540
- return true;
541
- };
542
- const prefixes = ordered.filter((e) => e > 0).sort((a, b) => a - b);
543
- const suffixes = ordered
544
- .filter((s2) => s2 < bytes.length)
545
- .sort((a, b) => b - a);
546
- for (let i = 0; i < Math.max(prefixes.length, suffixes.length); i++) {
547
- // Interleaved so neither edge starves the other when the budget runs
548
- // out — a query can carry a trained form at either end.
549
- if (i < prefixes.length && !spend(0, prefixes[i]))
550
- break;
551
- if (i < suffixes.length && !spend(suffixes[i], bytes.length))
552
- break;
553
- }
554
- }
555
- }
556
- const chunkEnd = new Uint32Array(bytes.length);
557
- const chunkSpan = new Uint32Array(bytes.length);
558
- const sorted = [...starts].sort((a, b) => a - b);
559
- for (let si = 0; si < sorted.length; si++) {
560
- const chunkStart = sorted[si];
561
- const chunkLimit = si + 1 < sorted.length ? sorted[si + 1] : bytes.length;
562
- for (let p = chunkStart; p < chunkLimit; p++) {
563
- chunkEnd[p] = chunkLimit;
564
- chunkSpan[p] = chunkLimit - chunkStart;
565
- }
566
- }
567
- // A chain rebuilt from a NON-boundary offset (the query's own perceived
568
- // cut, `starts`, never chose to segment here) is opportunistic: the same
569
- // byte-atom coincidence the hub guard above already exists for, just
570
- // spelled over 2+ leaves instead of 1. At small corpus scale that's fine
571
- // — coincidence is rare and every chain is real evidence (see `atomIsHub`).
572
- // Past the scale where atoms themselves stop discriminating, the same
573
- // uniform-expectation argument bounds a CHAIN'S commonality too: it is at
574
- // least as rare as its rarest atom, so a store where atoms are hubs makes
575
- // interior chain reconstructions no more trustworthy than the atoms they
576
- // are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
577
- // hub-scale atoms, chained at an offset nothing in the query's own fold
578
- // selected). Chains that start ON a boundary carry the fold's own
579
- // evidence instead and are exempt.
580
- //
581
- // NOTE (2026-07-24): that last sentence is FALSE — `starts` is exactly
582
- // {0, W, 2W, …} (riverFold groups fixed-arity), so the exemption is
583
- // arithmetic, not evidence. Removing it wholesale was measured and
584
- // REVERTED: it also drops legitimate multi-byte chains (the 12-byte
585
- // "Eiffel Tower" site vanished with it). The premise is wrong but the
586
- // trust it stood in for is real; a replacement signal is still open work.
587
- // See bench/README.md.
588
- const tryChain = (p, maxIds, boundary) => {
589
- const first = leafFrom(p);
590
- if (!first)
591
- return;
592
- emit(p, first.end, first.id);
593
- const ids = [first.id];
594
- let pos = first.end;
595
- let prevId = null;
596
- for (let depth = 1; pos < bytes.length && ids.length <= maxIds; depth++) {
597
- const nx = leafFrom(pos);
598
- if (!nx)
599
- break;
600
- ids.push(nx.id);
601
- pos = nx.end;
602
- if (store.findBranch(ids) === null)
603
- continue;
604
- if (!boundary && atomsAreHubs)
605
- continue;
606
- const id = resolveSpan(p, pos);
607
- if (id === null || id === prevId)
608
- continue;
609
- prevId = id;
610
- emit(p, pos, id);
611
- }
612
- };
613
- for (let p = 0; p < bytes.length; p++) {
614
- if (starts.has(p)) {
615
- tryChain(p, chainReach(W), true); // boundary start — full reach
616
- }
617
- else {
618
- // THE INTERIOR BUDGET IS "ONE CHUNK PLUS A QUANTUM", MEASURED FROM THE
619
- // CHAIN'S OWN START. It used to be `chunkEnd[p] + W - p`, which counts
620
- // from the chunk's END, so the reach an interior chain gets depended on
621
- // WHERE INSIDE its chunk it happened to begin: measured on a composed
622
- // answer, a chunk spanning [0,6) gave offset 1 nine ids and offset 4
623
- // only six — and the 9-id trained form `Mona Lisa` starting at 4 died
624
- // three ids short of itself. The same form one byte earlier would have
625
- // been found. That is the position artifact this module has been
626
- // removing everywhere else, not a budget.
627
- //
628
- // Stated from `p` the trust is unchanged — a chain may span its own
629
- // chunk and one quantum beyond it — and it no longer varies with phase.
630
- tryChain(p, Math.min(chunkSpan[p] + W, chainReach(W)), false);
631
- }
632
- }
633
- // ── splits: a form boundary that does not fall on a leaf edge ────────
634
- const leafEdges = new Set([bytes.length]);
635
- for (const lf of leaves)
636
- leafEdges.add(lf.start);
637
- for (const s of sites) {
638
- if (!leafEdges.has(s.start))
639
- splits.add(s.start);
640
- if (!leafEdges.has(s.end))
641
- splits.add(s.end);
642
- }
643
- ctx.trace?.step("recognise", [rItem(bytes, "query")], sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
644
- s.start,
645
- s.end,
646
- ])), `decompose the query into ${sites.length} learnt form(s) that lead somewhere` +
647
- ` (over ${leaves.length} perceived leaves)`);
648
- return { sites, leaves, splits, starts };
649
- }
650
- /** Segment bytes using the geometry's own groupings — leaf-parent
651
- * nodes from the perceived tree, with consecutive bare leaves merged
652
- * into one segment. Each segment's gist is perceived from its bytes
653
- * IN ISOLATION, so the same content has the same gist regardless of
654
- * where it appears. */
655
- export function segment(ctx, bytes) {
656
- const tree = perceive(ctx, bytes);
657
- const out = [];
658
- let pendingStart = -1;
659
- let pendingEnd = -1;
660
- const flush = () => {
661
- if (pendingStart >= 0 && pendingEnd > pendingStart) {
662
- out.push({
663
- start: pendingStart,
664
- end: pendingEnd,
665
- v: gistOf(ctx, bytes.subarray(pendingStart, pendingEnd)),
666
- });
667
- }
668
- pendingStart = -1;
669
- pendingEnd = -1;
670
- };
671
- const walk = (n, start) => {
672
- if (n.kids === null) {
673
- const end = start + (n.leaf?.length ?? 0);
674
- if (pendingStart < 0)
675
- pendingStart = start;
676
- pendingEnd = end;
677
- return end;
678
- }
679
- if (isChunk(n)) {
680
- flush();
681
- let end = start;
682
- for (const c of n.kids)
683
- end += c.leaf?.length ?? 0;
684
- out.push({ start, end, v: gistOf(ctx, bytes.subarray(start, end)) });
685
- return end;
686
- }
687
- flush();
688
- let pos = start;
689
- for (const c of n.kids)
690
- pos = walk(c, pos);
691
- return pos;
692
- };
693
- walk(tree, 0);
694
- flush();
695
- return out;
696
- }