@hviana/sema 0.2.6 → 0.2.8

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/dist/example/demo.d.ts +1 -0
  2. package/dist/example/demo.js +39 -0
  3. package/dist/example/train_base.d.ts +87 -0
  4. package/dist/example/train_base.js +2252 -0
  5. package/dist/src/alphabet.d.ts +7 -0
  6. package/dist/src/alphabet.js +33 -0
  7. package/dist/src/alu/src/alu.d.ts +185 -0
  8. package/dist/src/alu/src/alu.js +440 -0
  9. package/dist/src/alu/src/expr.d.ts +61 -0
  10. package/dist/src/alu/src/expr.js +318 -0
  11. package/dist/src/alu/src/index.d.ts +11 -0
  12. package/dist/src/alu/src/index.js +19 -0
  13. package/dist/src/alu/src/kernel-arith.d.ts +16 -0
  14. package/dist/src/alu/src/kernel-arith.js +264 -0
  15. package/dist/src/alu/src/kernel-bits.d.ts +19 -0
  16. package/dist/src/alu/src/kernel-bits.js +152 -0
  17. package/dist/src/alu/src/kernel-logic.d.ts +4 -0
  18. package/dist/src/alu/src/kernel-logic.js +60 -0
  19. package/dist/src/alu/src/kernel-nd.d.ts +3 -0
  20. package/dist/src/alu/src/kernel-nd.js +208 -0
  21. package/dist/src/alu/src/kernel-numeric.d.ts +54 -0
  22. package/dist/src/alu/src/kernel-numeric.js +366 -0
  23. package/dist/src/alu/src/operation.d.ts +168 -0
  24. package/dist/src/alu/src/operation.js +189 -0
  25. package/dist/src/alu/src/parser.d.ts +221 -0
  26. package/dist/src/alu/src/parser.js +577 -0
  27. package/dist/src/alu/src/resonance.d.ts +55 -0
  28. package/dist/src/alu/src/resonance.js +126 -0
  29. package/dist/src/alu/src/text.d.ts +31 -0
  30. package/dist/src/alu/src/text.js +73 -0
  31. package/dist/src/alu/src/value.d.ts +109 -0
  32. package/dist/src/alu/src/value.js +300 -0
  33. package/dist/src/alu/test/alu.test.d.ts +1 -0
  34. package/dist/src/alu/test/alu.test.js +764 -0
  35. package/dist/src/bytes.d.ts +14 -0
  36. package/dist/src/bytes.js +59 -0
  37. package/dist/src/canon.d.ts +26 -0
  38. package/dist/src/canon.js +57 -0
  39. package/dist/src/config.d.ts +111 -0
  40. package/dist/src/config.js +91 -0
  41. package/dist/src/derive/src/deduction.d.ts +125 -0
  42. package/dist/src/derive/src/deduction.js +155 -0
  43. package/dist/src/derive/src/index.d.ts +7 -0
  44. package/dist/src/derive/src/index.js +11 -0
  45. package/dist/src/derive/src/priority-queue.d.ts +20 -0
  46. package/dist/src/derive/src/priority-queue.js +73 -0
  47. package/dist/src/derive/src/rewrite.d.ts +56 -0
  48. package/dist/src/derive/src/rewrite.js +100 -0
  49. package/dist/src/derive/src/trie.d.ts +90 -0
  50. package/dist/src/derive/src/trie.js +217 -0
  51. package/dist/src/derive/test/derive.test.d.ts +1 -0
  52. package/dist/src/derive/test/derive.test.js +122 -0
  53. package/dist/src/extension.d.ts +37 -0
  54. package/dist/src/extension.js +7 -0
  55. package/dist/src/geometry.d.ts +202 -0
  56. package/dist/src/geometry.js +575 -0
  57. package/dist/src/index.d.ts +16 -0
  58. package/dist/src/index.js +18 -0
  59. package/dist/src/ingest-cache.d.ts +41 -0
  60. package/dist/src/ingest-cache.js +161 -0
  61. package/dist/src/mind/articulation.d.ts +6 -0
  62. package/dist/src/mind/articulation.js +99 -0
  63. package/dist/src/mind/attention.d.ts +405 -0
  64. package/dist/src/mind/attention.js +1829 -0
  65. package/dist/src/mind/bridge.d.ts +30 -0
  66. package/dist/src/mind/bridge.js +569 -0
  67. package/dist/src/mind/canonical.d.ts +29 -0
  68. package/dist/src/mind/canonical.js +88 -0
  69. package/dist/src/mind/graph-search.d.ts +285 -0
  70. package/dist/src/mind/graph-search.js +956 -0
  71. package/dist/src/mind/index.d.ts +7 -0
  72. package/dist/src/mind/index.js +5 -0
  73. package/dist/src/mind/junction.d.ts +124 -0
  74. package/dist/src/mind/junction.js +317 -0
  75. package/dist/src/mind/learning.d.ts +47 -0
  76. package/dist/src/mind/learning.js +248 -0
  77. package/dist/src/mind/match.d.ts +126 -0
  78. package/dist/src/mind/match.js +441 -0
  79. package/dist/src/mind/mechanisms/alu.d.ts +4 -0
  80. package/dist/src/mind/mechanisms/alu.js +36 -0
  81. package/dist/src/mind/mechanisms/cast.d.ts +89 -0
  82. package/dist/src/mind/mechanisms/cast.js +716 -0
  83. package/dist/src/mind/mechanisms/confluence.d.ts +24 -0
  84. package/dist/src/mind/mechanisms/confluence.js +225 -0
  85. package/dist/src/mind/mechanisms/cover.d.ts +6 -0
  86. package/dist/src/mind/mechanisms/cover.js +203 -0
  87. package/dist/src/mind/mechanisms/extraction.d.ts +67 -0
  88. package/dist/src/mind/mechanisms/extraction.js +387 -0
  89. package/dist/src/mind/mechanisms/recall.d.ts +13 -0
  90. package/dist/src/mind/mechanisms/recall.js +325 -0
  91. package/dist/src/mind/mind.d.ts +276 -0
  92. package/dist/src/mind/mind.js +572 -0
  93. package/dist/src/mind/pipeline-mechanism.d.ts +142 -0
  94. package/dist/src/mind/pipeline-mechanism.js +213 -0
  95. package/dist/src/mind/pipeline.d.ts +20 -0
  96. package/dist/src/mind/pipeline.js +217 -0
  97. package/dist/src/mind/primitives.d.ts +70 -0
  98. package/dist/src/mind/primitives.js +287 -0
  99. package/dist/src/mind/rationale.d.ts +139 -0
  100. package/dist/src/mind/rationale.js +163 -0
  101. package/dist/src/mind/reasoning.d.ts +34 -0
  102. package/dist/src/mind/reasoning.js +240 -0
  103. package/dist/src/mind/recognition.d.ts +20 -0
  104. package/dist/src/mind/recognition.js +390 -0
  105. package/dist/src/mind/resonance.d.ts +23 -0
  106. package/dist/src/mind/resonance.js +0 -0
  107. package/dist/src/mind/trace.d.ts +15 -0
  108. package/dist/src/mind/trace.js +73 -0
  109. package/dist/src/mind/traverse.d.ts +113 -0
  110. package/dist/src/mind/traverse.js +568 -0
  111. package/dist/src/mind/types.d.ts +289 -0
  112. package/dist/src/mind/types.js +130 -0
  113. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  114. package/dist/src/rabitq-ivf/src/database.js +201 -0
  115. package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
  116. package/dist/src/rabitq-ivf/src/index.js +4 -0
  117. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  118. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  119. package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
  120. package/dist/src/rabitq-ivf/src/prng.js +36 -0
  121. package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
  122. package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
  123. package/dist/src/sema.d.ts +31 -0
  124. package/dist/src/sema.js +63 -0
  125. package/dist/src/store-sqlite.d.ts +184 -0
  126. package/dist/src/store-sqlite.js +942 -0
  127. package/dist/src/store.d.ts +663 -0
  128. package/dist/src/store.js +1618 -0
  129. package/dist/src/vec.d.ts +31 -0
  130. package/dist/src/vec.js +109 -0
  131. package/package.json +1 -1
  132. package/src/mind/attention.ts +178 -63
  133. package/test/51-structural-resonance-ladder.test.mjs +6 -3
@@ -0,0 +1,387 @@
1
+ // extraction.ts — Extraction (Skill) — Section 4 of the mind.
2
+ //
3
+ // Given a query and its consensus-ranked anchors, find the first span-shaped
4
+ // skill exemplar among the ranked anchors and read the analogous span of the
5
+ // query. A skill exemplar is a learnt fact whose context and answer together
6
+ // form a span-in-context pattern: the answer is a subsequence of the context
7
+ // (or one of its pieces is), and the context is the smallest spanning frame
8
+ // that contains it.
9
+ import { foldTree, perceive, read, resolve } from "../primitives.js";
10
+ import { follow, locate } from "../match.js";
11
+ import { chooseAmong, hubBound } from "../traverse.js";
12
+ import { concatBytes, indexOf } from "../../bytes.js";
13
+ import { decodeText, unexplainedLabel } from "../rationale.js";
14
+ import { CONCEPT, STEP } from "../graph-search.js";
15
+ import { rItem, rNode, traceFail } from "../trace.js";
16
+ // ── Extraction ────────────────────────────────────────────────────────────
17
+ /** Find the first span-shaped skill exemplar among the ranked anchors from
18
+ * climbAttentionAll and read the analogous span from the query. Returns
19
+ * the extracted bytes PLUS the query spans the skill ACCOUNTED FOR — the
20
+ * located frames AND any read span BOUNDED by located frames on both
21
+ * sides, the elementary evidence think's grounding decider weighs. A
22
+ * bounded read is explained: the skill located both its borders in the
23
+ * query and emitted exactly what sits between them. An OPEN-ENDED read
24
+ * (the exemplar's answer reaches the context's end, so the query is read
25
+ * to its own end with no located right border) remains a guess about where
26
+ * the span stops — it stays unaccounted, priced by exclusion like the
27
+ * cover's bridged bytes. (Accounting frames only — the earlier convention
28
+ * — let a CAST juxtaposition that merely echoed the query's exact site
29
+ * outweigh a correct bounded extraction: the same span counted as
30
+ * explained for one mechanism and not the other, and the asymmetry, not
31
+ * the answers' merits, decided the grounding.) Null when no skill
32
+ * applies. */
33
+ export async function extractBySkill(ctx, query, pre) {
34
+ const t = ctx.trace?.enter("extractBySkill", [
35
+ rItem(query, "query"),
36
+ ]);
37
+ const fail = traceFail(t);
38
+ // Use climbAttentionAll to get the FULL ranked list, not just the
39
+ // roots that cleared commitVotes' significance floor. The floor
40
+ // gates further points of attention for fusion, but extraction only
41
+ // needs ONE anchor that IS a span-shaped skill exemplar — and on
42
+ // some seeds the top-voted anchor is not one (e.g. a concept-merge
43
+ // nickname outvotes the painting exemplars on shared substrings,
44
+ // while the exemplars' votes fall below the floor). Iterating the
45
+ // ranked list instead of just the roots lets extraction reach the
46
+ // first painting-exemplar anchor regardless of its floor status.
47
+ //
48
+ const { ranked } = await pre.attention();
49
+ if (ranked.length === 0) {
50
+ return fail("no consensus anchor — no skill to apply");
51
+ }
52
+ // Try ranked anchors IN ORDER until one yields a USABLE extraction — not
53
+ // merely a span-shaped exemplar, but one whose extracted span clears the
54
+ // same one-river-fold quantum (W) cover.ts's restatedSpan gate already
55
+ // treats as the floor below which byte overlap is chance, not evidence.
56
+ // isSpanShaped (spanShapedOf) is a deliberately permissive sparse-
57
+ // subsequence check — see the section note below — so it accepts exemplars
58
+ // whose relation to the query is coincidental gap-matching, not genuine
59
+ // structure. Stopping at the FIRST such exemplar let a coincidental match
60
+ // early in the ranked list win outright and read out a sub-quantum
61
+ // fragment (observed: a 3-byte "Hel" pulled from an unrelated exemplar,
62
+ // while a later ranked anchor would have read the query's own "Hello…"
63
+ // correctly). Trying further anchors when one produces nothing usable is
64
+ // the same idiom this loop already uses for non-exemplars — extended to
65
+ // cover a bad extraction, not just a structural non-match.
66
+ //
67
+ // The retry is bounded at pre.k — the SAME evidence-breadth constant every
68
+ // other consumer of a ranked list already self-limits to (resonance, the
69
+ // weave, the climb itself; see Precomputed.k's own doc comment) — not the
70
+ // full ranked list. locate()'s frame match has an EXACT-byte tier with no
71
+ // significance correction of its own (short W-byte frames are cheap to
72
+ // match by pure chance), so trying every ranked anchor turns that per-
73
+ // anchor chance into a near-certainty over enough attempts: on a pure-
74
+ // gibberish query, 170 anchors deep found an unrelated Zulu exemplar whose
75
+ // short frame happened to byte-match, producing "xyzzy pl" — a coincidence
76
+ // no different in kind from the "RaBitQ estimate overshot the reach bar
77
+ // and grounded pure gibberish" failure recall.ts's own significance
78
+ // correction exists to prevent (see recallByResonance's reach-threshold
79
+ // comment). Bounding the search to the ranked list's own top-k restores
80
+ // the "genuinely relevant but not root-significant" exemplars this loop
81
+ // was built for, without the unbounded tail's chance collisions.
82
+ const W = ctx.space.maxGroup;
83
+ const searched = ranked.slice(0, pre.k);
84
+ let shapeMisses = 0;
85
+ let subQuantum = 0;
86
+ for (const cand of searched) {
87
+ const exemplar = await pre.spanShapedOf(cand.anchor);
88
+ if (!exemplar) {
89
+ shapeMisses++;
90
+ continue;
91
+ }
92
+ const built = buildFromExemplar(ctx, query, pre, exemplar);
93
+ if (built === null || built.bytes.length < W) {
94
+ subQuantum++;
95
+ continue;
96
+ }
97
+ if (shapeMisses > 0 || subQuantum > 0) {
98
+ ctx.trace?.step("trySkillAnchors", [
99
+ rItem(query.subarray(0, 0), `skipped ${shapeMisses + subQuantum}`),
100
+ rNode(ctx, cand.anchor, "chosen"),
101
+ ], [], `skipped ${shapeMisses} non-exemplar and ${subQuantum} sub-quantum ` +
102
+ `anchor(s) before one yielded a usable extraction`);
103
+ }
104
+ t?.done([rItem(built.bytes, "extracted")], built.pieces === 1
105
+ ? `apply a learnt extraction skill — read the analogous span of the query` +
106
+ ` framed like "${decodeText(exemplar.answerBytes)}" sits in its exemplar`
107
+ : `apply a learnt MULTI-PIECE skill — read ${built.pieces} analogous` +
108
+ ` pieces of the query and synthesize them like "${decodeText(exemplar.answerBytes)}"`);
109
+ return {
110
+ bytes: built.bytes,
111
+ accounted: built.accounted,
112
+ unexplained: unexplainedLabel(query, built.accounted),
113
+ };
114
+ }
115
+ if (shapeMisses === searched.length) {
116
+ ctx.trace?.step("trySkillAnchors", [], [], `none of the top ${searched.length} ranked anchor(s) (of ${ranked.length} total) ` +
117
+ `is a span-shaped skill exemplar`);
118
+ return fail("no consensus root is a span-shaped skill exemplar");
119
+ }
120
+ return fail("no ranked anchor yielded an extraction at or above the quantum floor");
121
+ }
122
+ /** Build the extracted bytes for ONE already-accepted span-shaped exemplar —
123
+ * factored out of {@link extractBySkill} so its anchor loop can try
124
+ * successive ranked candidates instead of committing to the first
125
+ * structural match. Null when the exemplar's answer does not decompose
126
+ * against its context, or no piece's frame locates in the query. */
127
+ function buildFromExemplar(ctx, query, pre, exemplar) {
128
+ const { contextBytes, answerBytes } = exemplar;
129
+ const ansCtxRuns = answerRunsInContext(ctx, contextBytes, answerBytes);
130
+ if (ansCtxRuns === null || ansCtxRuns.length === 0) {
131
+ return null;
132
+ }
133
+ if (ansCtxRuns.length > 1) {
134
+ ctx.trace?.step("decomposeAnswer", [rItem(answerBytes, "multi-piece-answer")], ansCtxRuns.map((r) => rItem(contextBytes.subarray(r.start, r.end), "piece", undefined, [
135
+ r.start,
136
+ r.end,
137
+ ])), `answer splits into ${ansCtxRuns.length} piece(s) within the exemplar context`);
138
+ }
139
+ const pieces = [];
140
+ const accounted = [];
141
+ for (let ri = 0; ri < ansCtxRuns.length; ri++) {
142
+ const run = ansCtxRuns[ri];
143
+ const isLast = ri === ansCtxRuns.length - 1;
144
+ const framePreLen = Math.min(run.start, ctx.space.maxGroup);
145
+ const framePre = run.start > 0
146
+ ? contextBytes.subarray(run.start - framePreLen, run.start)
147
+ : null;
148
+ const frames = [];
149
+ let start = 0;
150
+ if (framePre) {
151
+ const prePos = locate(ctx, query, framePre, 0, pre.rec.sites);
152
+ if (prePos < 0)
153
+ continue;
154
+ start = prePos + framePre.length;
155
+ frames.push([prePos, start]); // the located frame IS matched evidence
156
+ }
157
+ let end;
158
+ if (isLast) {
159
+ if (run.end < contextBytes.length) {
160
+ const framePostLen = Math.min(contextBytes.length - run.end, ctx.space.maxGroup);
161
+ const framePost = contextBytes.subarray(run.end, run.end + framePostLen);
162
+ const postPos = locate(ctx, query.subarray(start), framePost, 0, pre.rec.sites);
163
+ if (postPos < 0)
164
+ continue;
165
+ end = start + postPos;
166
+ frames.push([end, end + framePost.length]); // matched post-frame
167
+ }
168
+ else {
169
+ end = query.length;
170
+ }
171
+ }
172
+ else {
173
+ const nextRun = ansCtxRuns[ri + 1];
174
+ const nextPreLen = Math.min(nextRun.start, ctx.space.maxGroup);
175
+ const nextPre = contextBytes.subarray(nextRun.start - nextPreLen, nextRun.start);
176
+ const nextPos = locate(ctx, query.subarray(start), nextPre, 0, pre.rec.sites);
177
+ if (nextPos < 0) {
178
+ end = start + run.ansLen;
179
+ }
180
+ else {
181
+ end = start + nextPos;
182
+ frames.push([end, end + nextPre.length]); // matched next-frame
183
+ }
184
+ }
185
+ if (start >= end)
186
+ continue;
187
+ pieces.push(query.subarray(start, end));
188
+ accounted.push(...frames);
189
+ // Bounded on both sides ⇒ the read span itself is explained (see doc).
190
+ // frames carries the pre-border (when the answer is not at the context's
191
+ // start) and the located right border (post-frame or next piece's
192
+ // pre-frame); only when BOTH borders were located is the read bounded.
193
+ const preBounded = run.start === 0 || frames.some(([, e]) => e === start);
194
+ const postBounded = frames.some(([b]) => b === end);
195
+ if (preBounded && postBounded)
196
+ accounted.push([start, end]);
197
+ }
198
+ if (pieces.length === 0) {
199
+ return null;
200
+ }
201
+ const out = pieces.length === 1 ? pieces[0] : concatBytes(pieces);
202
+ return { bytes: out, accounted, pieces: pieces.length };
203
+ }
204
+ // ── The two span-shape readings: OPEN acceptance vs. STRONG decomposition ──
205
+ //
206
+ // isSpanShaped and answerRunsInContext read the SAME relation ("the answer is
207
+ // drawn from the context") at two deliberately different strengths, and they
208
+ // are NOT interchangeable:
209
+ //
210
+ // • isSpanShaped — the OPEN reading: any in-order embedding (a sparse
211
+ // subsequence, arbitrary gaps). O(|context|) byte scan. Used to ACCEPT
212
+ // an exemplar candidate.
213
+ // • answerRunsInContext — the STRONG reading: a greedy longest-run
214
+ // DECOMPOSITION into contiguous pieces. Greedy-longest is strictly
215
+ // stronger than subsequence (a long late match can consume context an
216
+ // earlier shorter choice needed), so an ACCEPTED exemplar can still fail
217
+ // to decompose — extractBySkill then fails with "answer is not a
218
+ // subsequence of the context" and think falls through to recall. That
219
+ // fall-through is BEHAVIOUR, pinned by the extraction suites: do not
220
+ // "unify" the two into one machine — replacing the open reading with the
221
+ // strong one silently rejects exemplars extraction today accepts, and
222
+ // replacing the strong one with a backtracking embedding changes which
223
+ // pieces are read out of the query.
224
+ /** Decompose an answer into substrings of its surrounding context, in order —
225
+ * the STRONG span-shape reading (see the section note above). Returns null
226
+ * when no greedy longest-run decomposition exists. Adjacent runs that
227
+ * connect contiguously are merged. */
228
+ export function answerRunsInContext(_ctx, context, answer) {
229
+ const pos = indexOf(context, answer, 0);
230
+ if (pos >= 0) {
231
+ return [{ start: pos, end: pos + answer.length, ansLen: answer.length }];
232
+ }
233
+ const runs = [];
234
+ let ai = 0;
235
+ let ci = 0;
236
+ while (ai < answer.length) {
237
+ // Longest match of the remaining answer at any position of the remaining
238
+ // context: one direct extend per context position — O(|ctx|·match) per
239
+ // run, replacing the previous longest-first indexOf countdown whose
240
+ // repeated scans were cubic on long sparse-subsequence answers.
241
+ let bestLen = 0;
242
+ let bestPos = -1;
243
+ for (let p = ci; p < context.length; p++) {
244
+ let l = 0;
245
+ const maxL = Math.min(context.length - p, answer.length - ai);
246
+ if (maxL <= bestLen)
247
+ break; // no later position can beat the best
248
+ while (l < maxL && context[p + l] === answer[ai + l])
249
+ l++;
250
+ if (l > bestLen) {
251
+ bestLen = l;
252
+ bestPos = p;
253
+ if (ai + l === answer.length)
254
+ break; // the whole remainder matched
255
+ }
256
+ }
257
+ if (bestLen === 0)
258
+ return null;
259
+ runs.push({ start: bestPos, end: bestPos + bestLen, ansLen: bestLen });
260
+ ai += bestLen;
261
+ ci = bestPos + bestLen;
262
+ }
263
+ const merged = [];
264
+ for (const r of runs) {
265
+ const last = merged[merged.length - 1];
266
+ if (last && r.start === last.end) {
267
+ last.end = r.end;
268
+ last.ansLen += r.ansLen;
269
+ }
270
+ else {
271
+ merged.push({ ...r });
272
+ }
273
+ }
274
+ return merged.length > 0 ? merged : null;
275
+ }
276
+ /** Check whether an anchor is a span-shaped skill exemplar: it represents a
277
+ * fact whose context and answer together form a span-in-context pattern.
278
+ * If the anchor has a nextOf continuation, that is the answer and the anchor
279
+ * itself is the context. Otherwise the anchor's prevOf parents provide
280
+ * candidate contexts, and the longest one whose span is span-shaped wins. */
281
+ export async function skillExemplar(ctx, anchor, guide) {
282
+ if (ctx.store.hasNext(anchor)) {
283
+ const contextBytes = read(ctx, anchor);
284
+ const answerBytes = await follow(ctx, anchor, guide);
285
+ if (answerBytes !== null && isSpanShaped(ctx, contextBytes, answerBytes)) {
286
+ return { contextBytes, answerBytes };
287
+ }
288
+ return null;
289
+ }
290
+ const answerBytes = read(ctx, anchor);
291
+ // Candidate contexts, capped at the hub bound (a common answer's reverse
292
+ // fan-in is corpus-sized).
293
+ const capped = ctx.store.prevFirst(anchor, hubBound(ctx));
294
+ const spanShaped = [];
295
+ for (const p of capped) {
296
+ const ctxB = read(ctx, p);
297
+ if (ctxB.length > 0 && isSpanShaped(ctx, ctxB, answerBytes)) {
298
+ spanShaped.push({ id: p, bytes: ctxB });
299
+ }
300
+ }
301
+ if (spanShaped.length === 0)
302
+ return null;
303
+ // Among span-shaped contexts, the longest wins (the smallest spanning frame
304
+ // heuristic's dual: more frame to locate in the query); the query gist,
305
+ // when given, breaks LENGTH TIES via chooseAmong — the same reverse-regime
306
+ // disambiguator every context pick uses, whose gist cache spares the
307
+ // re-fold this block once paid per tied candidate. Same strict first-seen
308
+ // tie-break as the hand loop it replaces.
309
+ const maxLen = Math.max(...spanShaped.map((s) => s.bytes.length));
310
+ const longest = spanShaped.filter((s) => s.bytes.length === maxLen);
311
+ let contextBytes = longest[0].bytes;
312
+ if (guide && longest.length > 1) {
313
+ const pick = chooseAmong(ctx, longest.map((s) => s.id), guide).id;
314
+ contextBytes = longest.find((s) => s.id === pick).bytes;
315
+ }
316
+ return { contextBytes, answerBytes };
317
+ }
318
+ /** Whether the answer is a SPARSE subsequence of the context (bytes in
319
+ * order, arbitrary gaps) — the OPEN span-shape reading (see the section
320
+ * note above). This is what lets extraction validate a MULTI-PIECE
321
+ * exemplar whose answer is stitched from several context runs — but it is
322
+ * deliberately permissive, so it must never be used as evidence that one
323
+ * span was "drawn from" another (see {@link containsSpan} for that).
324
+ *
325
+ * There is deliberately NO containsSpan pre-check here: strict containment
326
+ * IMPLIES the subsequence embedding (a contiguous run, or a resolved node —
327
+ * whose content-addressed identity means its bytes occur contiguously — is
328
+ * an in-order embedding with zero gaps), so the scan below decides alone,
329
+ * with the same truth value. The old pre-check re-perceived the context
330
+ * (a full river fold) per CANDIDATE in skillExemplar's √N-capped loop —
331
+ * pure cost, no discrimination. */
332
+ export function isSpanShaped(_ctx, context, answer) {
333
+ let ai = 0;
334
+ for (let ci = 0; ci < context.length && ai < answer.length; ci++) {
335
+ if (context[ci] === answer[ai])
336
+ ai++;
337
+ }
338
+ return ai === answer.length;
339
+ }
340
+ /** STRICT containment: the answer's resolved node appears in the context's
341
+ * folded tree, or the answer occurs as one CONTIGUOUS byte run of the
342
+ * context. This is real evidence the answer was drawn from the context.
343
+ * Fusion gates on this — the sparse-subsequence reading of
344
+ * {@link isSpanShaped} is trivially satisfied by short answers over long
345
+ * queries ("cold" is a gap-tolerant subsequence of most sentences holding
346
+ * c…o…l…d in order), and gating fusion on it silently starved multi-topic
347
+ * queries of their further points of attention. */
348
+ export function containsSpan(ctx, context, answer) {
349
+ const ansId = resolve(ctx, answer);
350
+ if (ansId !== null) {
351
+ let found = false;
352
+ foldTree(ctx, perceive(ctx, context), 0, (_n, _s, _e, node) => {
353
+ if (node === ansId)
354
+ found = true;
355
+ });
356
+ if (found)
357
+ return true;
358
+ }
359
+ return indexOf(context, answer, 0) >= 0;
360
+ }
361
+ // ── Pipeline mechanism ──────────────────────────────────────────────────────
362
+ export const extractionMechanism = {
363
+ name: "extraction",
364
+ provenance: "extract",
365
+ async floor(_ctx, _query, pre, worthRunning) {
366
+ // Extraction's floor is always exactly CONCEPT+STEP when it exists —
367
+ // same investment discipline as CAST's (see cast.ts): when the bound
368
+ // already cannot beat the incumbent, return it UNINVESTED (never
369
+ // first-touch the climb just to be pruned).
370
+ if (!worthRunning(CONCEPT + STEP))
371
+ return CONCEPT + STEP;
372
+ if ((await pre.attention()).ranked.length === 0)
373
+ return null;
374
+ return CONCEPT + STEP;
375
+ },
376
+ async run(ctx, query, pre) {
377
+ const ex = await extractBySkill(ctx, query, pre);
378
+ if (!ex)
379
+ return [];
380
+ return [{
381
+ bytes: ex.bytes,
382
+ accounted: ex.accounted,
383
+ moves: CONCEPT + STEP * ex.accounted.length,
384
+ unexplained: ex.unexplained,
385
+ }];
386
+ },
387
+ };
@@ -0,0 +1,13 @@
1
+ import type { MindContext } from "../types.js";
2
+ import type { PipelineMechanism, Precomputed } from "../pipeline-mechanism.js";
3
+ /** A recall result. */
4
+ export interface RecallResult {
5
+ bytes: Uint8Array;
6
+ echoed: boolean;
7
+ accounted: Array<[number, number]>;
8
+ moves: number;
9
+ unexplained: string;
10
+ }
11
+ /** Recall the answer by resonating the whole query against the content index. */
12
+ export declare function recallByResonance(ctx: MindContext, query: Uint8Array, pre: Precomputed): Promise<RecallResult | null>;
13
+ export declare const recallMechanism: PipelineMechanism;