@hviana/sema 0.5.4 → 0.5.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 (140) hide show
  1. package/README.md +34 -7
  2. package/dist/example/demo.d.ts +1 -0
  3. package/dist/example/demo.js +39 -0
  4. package/dist/example/train_base.d.ts +93 -0
  5. package/dist/example/train_base.js +2269 -0
  6. package/dist/src/alphabet.d.ts +7 -0
  7. package/dist/src/alphabet.js +33 -0
  8. package/dist/src/alu/src/alu.d.ts +185 -0
  9. package/dist/src/alu/src/alu.js +440 -0
  10. package/dist/src/alu/src/expr.d.ts +61 -0
  11. package/dist/src/alu/src/expr.js +318 -0
  12. package/dist/src/alu/src/index.d.ts +11 -0
  13. package/dist/src/alu/src/index.js +19 -0
  14. package/dist/src/alu/src/kernel-arith.d.ts +16 -0
  15. package/dist/src/alu/src/kernel-arith.js +264 -0
  16. package/dist/src/alu/src/kernel-bits.d.ts +19 -0
  17. package/dist/src/alu/src/kernel-bits.js +152 -0
  18. package/dist/src/alu/src/kernel-logic.d.ts +4 -0
  19. package/dist/src/alu/src/kernel-logic.js +60 -0
  20. package/dist/src/alu/src/kernel-nd.d.ts +3 -0
  21. package/dist/src/alu/src/kernel-nd.js +208 -0
  22. package/dist/src/alu/src/kernel-numeric.d.ts +54 -0
  23. package/dist/src/alu/src/kernel-numeric.js +366 -0
  24. package/dist/src/alu/src/operation.d.ts +168 -0
  25. package/dist/src/alu/src/operation.js +189 -0
  26. package/dist/src/alu/src/parser.d.ts +221 -0
  27. package/dist/src/alu/src/parser.js +577 -0
  28. package/dist/src/alu/src/resonance.d.ts +55 -0
  29. package/dist/src/alu/src/resonance.js +126 -0
  30. package/dist/src/alu/src/text.d.ts +31 -0
  31. package/dist/src/alu/src/text.js +73 -0
  32. package/dist/src/alu/src/value.d.ts +109 -0
  33. package/dist/src/alu/src/value.js +300 -0
  34. package/dist/src/alu/test/alu.test.d.ts +1 -0
  35. package/dist/src/alu/test/alu.test.js +764 -0
  36. package/dist/src/bytes.d.ts +14 -0
  37. package/dist/src/bytes.js +59 -0
  38. package/dist/src/canon.d.ts +45 -0
  39. package/dist/src/canon.js +85 -0
  40. package/dist/src/config.d.ts +111 -0
  41. package/dist/src/config.js +91 -0
  42. package/dist/src/derive/src/deduction.d.ts +136 -0
  43. package/dist/src/derive/src/deduction.js +159 -0
  44. package/dist/src/derive/src/index.d.ts +8 -0
  45. package/dist/src/derive/src/index.js +11 -0
  46. package/dist/src/derive/src/priority-queue.d.ts +20 -0
  47. package/dist/src/derive/src/priority-queue.js +73 -0
  48. package/dist/src/derive/src/rewrite.d.ts +56 -0
  49. package/dist/src/derive/src/rewrite.js +100 -0
  50. package/dist/src/derive/src/trie.d.ts +90 -0
  51. package/dist/src/derive/src/trie.js +217 -0
  52. package/dist/src/derive/test/derive.test.d.ts +1 -0
  53. package/dist/src/derive/test/derive.test.js +122 -0
  54. package/dist/src/extension.d.ts +37 -0
  55. package/dist/src/extension.js +7 -0
  56. package/dist/src/geometry.d.ts +233 -0
  57. package/dist/src/geometry.js +1089 -0
  58. package/dist/src/index.d.ts +17 -0
  59. package/dist/src/index.js +19 -0
  60. package/dist/src/ingest-cache.d.ts +41 -0
  61. package/dist/src/ingest-cache.js +165 -0
  62. package/dist/src/meter.d.ts +176 -0
  63. package/dist/src/meter.js +274 -0
  64. package/dist/src/mind/articulation.d.ts +6 -0
  65. package/dist/src/mind/articulation.js +99 -0
  66. package/dist/src/mind/attention.d.ts +430 -0
  67. package/dist/src/mind/attention.js +2418 -0
  68. package/dist/src/mind/bridge.d.ts +37 -0
  69. package/dist/src/mind/bridge.js +907 -0
  70. package/dist/src/mind/canonical.d.ts +34 -0
  71. package/dist/src/mind/canonical.js +93 -0
  72. package/dist/src/mind/graph-search.d.ts +294 -0
  73. package/dist/src/mind/graph-search.js +996 -0
  74. package/dist/src/mind/index.d.ts +9 -0
  75. package/dist/src/mind/index.js +5 -0
  76. package/dist/src/mind/junction.d.ts +137 -0
  77. package/dist/src/mind/junction.js +358 -0
  78. package/dist/src/mind/learning.d.ts +75 -0
  79. package/dist/src/mind/learning.js +265 -0
  80. package/dist/src/mind/match.d.ts +404 -0
  81. package/dist/src/mind/match.js +1039 -0
  82. package/dist/src/mind/mechanisms/alu.d.ts +4 -0
  83. package/dist/src/mind/mechanisms/alu.js +36 -0
  84. package/dist/src/mind/mechanisms/cast.d.ts +89 -0
  85. package/dist/src/mind/mechanisms/cast.js +1186 -0
  86. package/dist/src/mind/mechanisms/confluence.d.ts +24 -0
  87. package/dist/src/mind/mechanisms/confluence.js +271 -0
  88. package/dist/src/mind/mechanisms/cover.d.ts +6 -0
  89. package/dist/src/mind/mechanisms/cover.js +232 -0
  90. package/dist/src/mind/mechanisms/extraction.d.ts +33 -0
  91. package/dist/src/mind/mechanisms/extraction.js +327 -0
  92. package/dist/src/mind/mechanisms/prefix-completion.d.ts +22 -0
  93. package/dist/src/mind/mechanisms/prefix-completion.js +243 -0
  94. package/dist/src/mind/mechanisms/recall.d.ts +16 -0
  95. package/dist/src/mind/mechanisms/recall.js +472 -0
  96. package/dist/src/mind/mechanisms/reference.d.ts +6 -0
  97. package/dist/src/mind/mechanisms/reference.js +296 -0
  98. package/dist/src/mind/mind.d.ts +386 -0
  99. package/dist/src/mind/mind.js +754 -0
  100. package/dist/src/mind/pipeline-mechanism.d.ts +238 -0
  101. package/dist/src/mind/pipeline-mechanism.js +622 -0
  102. package/dist/src/mind/pipeline.d.ts +49 -0
  103. package/dist/src/mind/pipeline.js +343 -0
  104. package/dist/src/mind/primitives.d.ts +85 -0
  105. package/dist/src/mind/primitives.js +333 -0
  106. package/dist/src/mind/rationale.d.ts +139 -0
  107. package/dist/src/mind/rationale.js +163 -0
  108. package/dist/src/mind/reasoning.d.ts +42 -0
  109. package/dist/src/mind/reasoning.js +305 -0
  110. package/dist/src/mind/recognition.d.ts +20 -0
  111. package/dist/src/mind/recognition.js +696 -0
  112. package/dist/src/mind/resonance.d.ts +35 -0
  113. package/dist/src/mind/resonance.js +365 -0
  114. package/dist/src/mind/trace.d.ts +15 -0
  115. package/dist/src/mind/trace.js +73 -0
  116. package/dist/src/mind/traverse.d.ts +196 -0
  117. package/dist/src/mind/traverse.js +806 -0
  118. package/dist/src/mind/types.d.ts +418 -0
  119. package/dist/src/mind/types.js +179 -0
  120. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  121. package/dist/src/rabitq-ivf/src/database.js +201 -0
  122. package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
  123. package/dist/src/rabitq-ivf/src/index.js +4 -0
  124. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  125. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  126. package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
  127. package/dist/src/rabitq-ivf/src/prng.js +36 -0
  128. package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
  129. package/dist/src/rabitq-ivf/src/rabitq.js +313 -0
  130. package/dist/src/sema.d.ts +41 -0
  131. package/dist/src/sema.js +77 -0
  132. package/dist/src/store-sqlite.d.ts +184 -0
  133. package/dist/src/store-sqlite.js +942 -0
  134. package/dist/src/store.d.ts +762 -0
  135. package/dist/src/store.js +1823 -0
  136. package/dist/src/vec.d.ts +31 -0
  137. package/dist/src/vec.js +109 -0
  138. package/index.html +796 -688
  139. package/jsr.json +1 -1
  140. package/package.json +1 -1
@@ -0,0 +1,1039 @@
1
+ // match.ts — the ONE elementary operation behind every generalising mechanism:
2
+ // MATCH a learned structure against bytes, then PROJECT along a learned
3
+ // relation, gated by a derived threshold.
4
+ //
5
+ // Every grounding/generalisation mechanism in the mind is a configuration of
6
+ // this single (matcher, direction, gate) operation:
7
+ //
8
+ // mechanism matcher direction gate
9
+ // ─────────────────── ────────────────────────────── ───────────── ────────────────
10
+ // cover follow-edge exact (content-addressed) forward —
11
+ // concept hop halo sibling forward conceptThreshold
12
+ // recall tier 0–1 identity / whole-query gist fwd/reverse identityBar
13
+ // skill extraction locate() ladder (exact→halo→ read-out per-step gates
14
+ // gist) on the exemplar's frames
15
+ // CAST substitution alignGraded() (graded ladder: insert frame shapes
16
+ // literal W-grams → halo sites)
17
+ // CAST comparison analogyStrength() (halo, juxtapose significanceBar
18
+ // direct or mutual-sibling)
19
+ // multi-hop pivot byte containment forward —
20
+ // articulation halo sibling substitute conceptThreshold
21
+ // reference frameSlots() (the shared carry into carriesFillers
22
+ // aligner, gaps contracted) the answer
23
+ //
24
+ // This module holds the shared vocabulary those configurations are built
25
+ // from — the MATCHERS (locate, alignRuns, alignGraded, alignAround/frameSlots,
26
+ // analogyStrength) and the PROJECTIONS (follow, conceptHop, reverseContext,
27
+ // project) — so each mechanism file states only its configuration, never its
28
+ // own copy of the machinery. Most gates live in geometry.ts (derived, never
29
+ // tuned); the two STRUCTURAL gates that are byte predicates rather than
30
+ // thresholds — isSpanShaped and carriesFillers — live here beside the matchers
31
+ // they gate.
32
+ import { addInto, cosine, dot, normalize, zeros } from "../vec.js";
33
+ import { conceptThreshold, dominates, identityBar, significanceBar, } from "../geometry.js";
34
+ import { bytesEqual, indexOf } from "../bytes.js";
35
+ import { chainReach, leafIdRun } from "./canonical.js";
36
+ import { foldTree, gistOf, perceive, read, resolve } from "./primitives.js";
37
+ import { argmaxCosine, chooseAmong, chooseNext, corpusN, edgeAncestors, guidedFirst, hubBound, hubCap, sharedReachMemo, } from "./traverse.js";
38
+ import { recognise, segment } from "./recognition.js";
39
+ // ═══════════════════════════════════════════════════════════════════════════
40
+ // MATCHERS — locating learned structure in/against bytes, by graded strictness
41
+ // ═══════════════════════════════════════════════════════════════════════════
42
+ /** The graded LOCATE ladder: find `needle` in `haystack` starting at
43
+ * `fromPos`, strictest matcher first, relaxing only when the stricter one
44
+ * fails. This is the read-out matcher skill extraction locates exemplar
45
+ * frames with.
46
+ *
47
+ * 1. exact — literal byte match (the fast path).
48
+ * 2. halo — the needle's distributional role matches a recognised query
49
+ * form (gate: conceptThreshold).
50
+ * 3. gist — the needle's perceived gist matches a query segment
51
+ * (gate: identityBar — scale-aware).
52
+ *
53
+ * Returns the absolute byte position, or −1. */
54
+ export function locate(ctx, haystack, needle, fromPos, sites) {
55
+ // 1. Exact match — fast, preserves backward compatibility.
56
+ const exact = indexOf(haystack.subarray(fromPos), needle, 0);
57
+ if (exact >= 0)
58
+ return fromPos + exact;
59
+ // 2. Halo-based: the frame bytes' distributional role matches a query form.
60
+ if (sites && sites.length > 0) {
61
+ const frameId = resolve(ctx, needle);
62
+ if (frameId !== null) {
63
+ const frameHalo = ctx.store.halo(frameId);
64
+ if (frameHalo) {
65
+ const bestSite = bestHaloMate(ctx, frameHalo, sites.filter((s) => s.start >= fromPos), (s) => ctx.store.halo(s.payload));
66
+ if (bestSite !== null)
67
+ return bestSite.item.start;
68
+ }
69
+ }
70
+ }
71
+ // 3. Gist resonance: the frame's perceived gist against query segments.
72
+ const frameGist = gistOf(ctx, needle);
73
+ const segments = segment(ctx, haystack.subarray(fromPos));
74
+ // The gist tier claims the WHOLE needle appears as a segment — an
75
+ // identity claim over `needle.length` bytes, so its bar is the
76
+ // scale-aware {@link identityBar} (one river window of tolerated foreign
77
+ // bytes), not the fixed estimator floor. For quantum-sized frames the
78
+ // two coincide; for long needles the fixed bar accepted segments that
79
+ // differed by whole windows.
80
+ const bestSeg = argmaxCosine(frameGist, segments, (s) => s.v, identityBar(ctx.store.D, ctx.space.maxGroup, needle.length), true);
81
+ if (bestSeg !== null)
82
+ return fromPos + bestSeg.item.start;
83
+ return -1;
84
+ }
85
+ /** The ALIGNED matcher: maximal literal matching runs between `query` and
86
+ * `ct` (a learned context's bytes), by seed-and-extend over
87
+ * `space.maxGroup`-sized n-gram seeds. Where locate() finds ONE position of
88
+ * a short frame, this finds EVERY run two whole structures share — the
89
+ * matcher CAST detects a woven query with. Returns non-overlapping runs
90
+ * sorted by query position. */
91
+ export function alignRuns(ctx, query, ct) {
92
+ if (ctx.meter) {
93
+ ctx.meter.alignments++;
94
+ // The alignment family's honest unit: the seed index is O(|query|) but
95
+ // the run extension is O(|query|·|ct|) in the worst case, and a weave
96
+ // that starts scanning conversation-length contexts shows up HERE long
97
+ // before it shows up in a call count.
98
+ ctx.meter.alignCells += query.length * ct.length;
99
+ }
100
+ // MEASURED AND REFUTED — seeding at the write side's unit floor W−1 instead
101
+ // of W. `canonicalWindows` interns both lengths precisely so a form
102
+ // straddling a group boundary is reachable from either cut, and the runs
103
+ // found here are exactly such remnants: test/29 C1's query shares only `ce `
104
+ // with `Ice is cold` — three bytes, never seeded at W, so that structure
105
+ // enters the weave carrying nothing but the scaffolding run every exemplar
106
+ // shares. But this is a byte MATCHER between two streams, not an index, and
107
+ // at W−1 the corpus is dense with spurious 3-byte agreements: the extra runs
108
+ // reshuffle which point claims which span, and test/29 A2 loses its analog.
109
+ // C1 does not pass either way. The store's unit floor does not transfer to
110
+ // the aligner's seed length.
111
+ const quantum = Math.min(ctx.space.maxGroup, ct.length);
112
+ if (quantum < 1 || query.length < quantum)
113
+ return [];
114
+ const gram = (b, at) => {
115
+ let s = "";
116
+ for (let i = 0; i < quantum; i++)
117
+ s += String.fromCharCode(b[at + i]);
118
+ return s;
119
+ };
120
+ const seeds = new Map();
121
+ for (let i = 0; i + quantum <= query.length; i++) {
122
+ const k2 = gram(query, i);
123
+ const bucket = seeds.get(k2);
124
+ if (bucket === undefined)
125
+ seeds.set(k2, [i]);
126
+ else
127
+ bucket.push(i);
128
+ }
129
+ const found = [];
130
+ for (let j = 0; j + quantum <= ct.length; j++) {
131
+ const bucket = seeds.get(gram(ct, j));
132
+ if (bucket === undefined)
133
+ continue;
134
+ for (const i of bucket) {
135
+ if (i > 0 && j > 0 && query[i - 1] === ct[j - 1])
136
+ continue;
137
+ let len = quantum;
138
+ while (i + len < query.length && j + len < ct.length &&
139
+ query[i + len] === ct[j + len])
140
+ len++;
141
+ found.push({ qs: i, qe: i + len, cs: j, len });
142
+ }
143
+ }
144
+ found.sort((a, b) => b.len - a.len);
145
+ const runs = [];
146
+ for (const r of found) {
147
+ const clash = runs.some((o) => (r.qs < o.qe && o.qs < r.qe) ||
148
+ (r.cs < o.cs + (o.qe - o.qs) && o.cs < r.cs + r.len));
149
+ if (!clash)
150
+ runs.push({ qs: r.qs, qe: r.qe, cs: r.cs });
151
+ }
152
+ return runs.sort((a, b) => a.qs - b.qs);
153
+ }
154
+ /** The GRADED alignment matcher: extends literal W-gram alignment
155
+ * ({@link alignRuns}) with halo-matched recognised sites in query regions
156
+ * that have no literal coverage. Same ladder as {@link locate}: literal
157
+ * first, then distributional role (halo-matched sites, gate:
158
+ * conceptThreshold, enforced by {@link bestHaloMate}). Returns weighted
159
+ * runs sorted by query position.
160
+ *
161
+ * `querySites` are the pre-computed recognition sites for the query
162
+ * (optional — when absent, only literal alignment fires and graded degrades
163
+ * to the original behaviour). Context sites are recognised internally. */
164
+ export function alignGraded(ctx, query, contextBytes, querySites) {
165
+ const lit = alignRuns(ctx, query, contextBytes);
166
+ const out = lit.map((r) => ({ ...r, weight: 1 }));
167
+ if (!querySites || querySites.length === 0)
168
+ return out;
169
+ // Mark query positions ALREADY covered by literal runs — halo fills gaps.
170
+ // If literal coverage is already complete, skip the halo step entirely
171
+ // (recognise is O(|ctx|·W) — wasted when every byte is accounted for).
172
+ const covered = new Uint8Array(query.length);
173
+ let gaps = false;
174
+ for (const r of lit) {
175
+ for (let i = r.qs; i < r.qe; i++)
176
+ covered[i] = 1;
177
+ }
178
+ for (let i = 0; i < query.length; i++) {
179
+ if (!covered[i]) {
180
+ gaps = true;
181
+ break;
182
+ }
183
+ }
184
+ if (!gaps)
185
+ return out;
186
+ // Recognise sites in the exemplar context — structural positions for halo
187
+ // matching. (Circular import with recognition.ts is safe: recognise() is
188
+ // called lazily, never at module load — the same pattern `segment` uses.)
189
+ const ctxSites = recognise(ctx, contextBytes).sites;
190
+ if (ctxSites.length === 0)
191
+ return out;
192
+ // Context sites with halos, hoisted: the same set serves every query site.
193
+ const ctxCands = ctxSites.filter((cs) => ctx.store.hasHalo(cs.payload));
194
+ if (ctxCands.length === 0)
195
+ return out;
196
+ // Candidate halos, also hoisted (lazily, first query site that needs them):
197
+ // bestHaloMate consults every candidate's halo PER QUERY SITE, and sites
198
+ // share the candidate set — without this memo the same few dozen halos were
199
+ // re-fetched thousands of times per response. Distinct payloads can repeat
200
+ // across sites, hence the map by payload id.
201
+ const ctxHalos = new Map();
202
+ const ctxHaloOf = (cs) => {
203
+ let h = ctxHalos.get(cs.payload);
204
+ if (h === undefined) {
205
+ h = ctx.store.halo(cs.payload);
206
+ ctxHalos.set(cs.payload, h);
207
+ }
208
+ return h;
209
+ };
210
+ for (const qs of querySites) {
211
+ // Only sites that overlap UNCOVERED query regions add new evidence.
212
+ let touchesGap = false;
213
+ for (let i = qs.start; i < qs.end; i++) {
214
+ if (!covered[i]) {
215
+ touchesGap = true;
216
+ break;
217
+ }
218
+ }
219
+ if (!touchesGap)
220
+ continue;
221
+ const qHalo = ctx.store.halo(qs.payload);
222
+ if (!qHalo)
223
+ continue;
224
+ // bestHaloMate already gates at conceptThreshold — no second check needed.
225
+ const match = bestHaloMate(ctx, qHalo, ctxCands, ctxHaloOf);
226
+ if (match === null)
227
+ continue;
228
+ out.push({
229
+ qs: qs.start,
230
+ qe: qs.end,
231
+ cs: match.item.start,
232
+ weight: match.score,
233
+ });
234
+ }
235
+ out.sort((a, b) => a.qs - b.qs);
236
+ return out;
237
+ }
238
+ /** Extend a seed match (query offset qo ↔ candidate offset co) to its maximal
239
+ * common run, then walk outward in both directions collecting further common
240
+ * runs of at least W bytes across bounded mismatch gaps (each side ≤
241
+ * chainReach). Returns the matched query spans and the mismatch pairs
242
+ * between consecutive runs.
243
+ *
244
+ * This is the SEEDED aligner, distinct from {@link alignRuns}: that one finds
245
+ * every run two structures share anywhere (a weave), this one reads two
246
+ * streams as ONE structure that diverges in bounded places (a frame with
247
+ * slots).
248
+ *
249
+ * Gaps come back in SWEEP order (right sweep, then left), not query order,
250
+ * and only the INTERIOR ones are reported — a consumer that needs the query's
251
+ * unmatched head or tail derives it from `matched`. Both are the bridge's
252
+ * contract, which prices its edges separately (see its matchStart/matchEnd
253
+ * window test); {@link frameSlots} takes the other reading. */
254
+ export function alignAround(ctx, q, c, qo, co) {
255
+ const W = ctx.space.maxGroup;
256
+ const reachCap = chainReach(W);
257
+ // Maximal run around the seed.
258
+ let qs = qo, ss = co;
259
+ while (qs > 0 && ss > 0 && q[qs - 1] === c[ss - 1]) {
260
+ qs--;
261
+ ss--;
262
+ }
263
+ let qe = qo, se = co;
264
+ while (qe < q.length && se < c.length && q[qe] === c[se]) {
265
+ qe++;
266
+ se++;
267
+ }
268
+ const matched = [[qs, qe]];
269
+ const gaps = [];
270
+ // The next common run of ≥ W bytes past (qi, si), with each side's gap
271
+ // bounded by chainReach; smallest total gap wins (nearest continuation).
272
+ const runLenAt = (qi, si) => {
273
+ let n = 0;
274
+ while (qi + n < q.length && si + n < c.length && q[qi + n] === c[si + n]) {
275
+ n++;
276
+ }
277
+ return n;
278
+ };
279
+ // RIGHT sweep.
280
+ let qi = qe, si = se;
281
+ for (;;) {
282
+ let found = false;
283
+ for (let total = 1; total <= 2 * reachCap && !found; total++) {
284
+ for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
285
+ const gs = total - gq;
286
+ if (gs > reachCap)
287
+ continue;
288
+ if (qi + gq >= q.length || si + gs >= c.length)
289
+ continue;
290
+ const n = runLenAt(qi + gq, si + gs);
291
+ if (n >= W || qi + gq + n === q.length) {
292
+ if (n === 0)
293
+ continue;
294
+ if (gq > 0 || gs > 0) {
295
+ gaps.push({ qs: qi, qe: qi + gq, cs: si, ce: si + gs });
296
+ }
297
+ matched.push([qi + gq, qi + gq + n]);
298
+ qi = qi + gq + n;
299
+ si = si + gs + n;
300
+ found = true;
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ if (!found)
306
+ break;
307
+ }
308
+ // LEFT sweep (mirror).
309
+ qi = qs;
310
+ si = ss;
311
+ for (;;) {
312
+ let found = false;
313
+ for (let total = 1; total <= 2 * reachCap && !found; total++) {
314
+ for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
315
+ const gs = total - gq;
316
+ if (gs > reachCap)
317
+ continue;
318
+ if (qi - gq <= 0 || si - gs <= 0)
319
+ continue;
320
+ // Run ENDING at (qi - gq, si - gs).
321
+ let n = 0;
322
+ while (n < qi - gq && n < si - gs &&
323
+ q[qi - gq - 1 - n] === c[si - gs - 1 - n]) {
324
+ n++;
325
+ }
326
+ if (n >= W || n === qi - gq) {
327
+ if (n === 0)
328
+ continue;
329
+ if (gq > 0 || gs > 0) {
330
+ gaps.push({ qs: qi - gq, qe: qi, cs: si - gs, ce: si });
331
+ }
332
+ matched.push([qi - gq - n, qi - gq]);
333
+ qi = qi - gq - n;
334
+ si = si - gs - n;
335
+ found = true;
336
+ break;
337
+ }
338
+ }
339
+ }
340
+ if (!found)
341
+ break;
342
+ }
343
+ return { matched, gaps };
344
+ }
345
+ /** Contract a gap to its VARYING CORE: strip the prefix and suffix the two
346
+ * sides share. {@link alignAround} cannot match a shared affix shorter than
347
+ * W, so that affix lands INSIDE the gap — measured, the slot of
348
+ * `How do I compile main.c?` against `…hello.c?` comes back as
349
+ * `main.c?`/`hello.c?`, three bytes of which (`.c?`) both sides hold.
350
+ *
351
+ * Splicing the uncontracted gap carries the query's own punctuation into the
352
+ * answer; worse, it hides what actually VARIES, which is the only thing a
353
+ * cohort can agree about. Returns null when nothing is left on either side —
354
+ * a pure insertion or deletion, which names no slot. */
355
+ export function contractGap(q, c, g) {
356
+ let { qs, qe, cs, ce } = g;
357
+ while (qs < qe && cs < ce && q[qs] === c[cs]) {
358
+ qs++;
359
+ cs++;
360
+ }
361
+ while (qe > qs && ce > cs && q[qe - 1] === c[ce - 1]) {
362
+ qe--;
363
+ ce--;
364
+ }
365
+ return qe > qs && ce > cs ? { qs, qe, cs, ce } : null;
366
+ }
367
+ /** THE SLOT MATCHER: read one query ↔ context pairing as one structure with
368
+ * variable positions.
369
+ *
370
+ * IT REPORTS; IT DOES NOT JUDGE. This returns every gap the aligner found,
371
+ * contracted to its varying core and tagged with its kind, plus the shared
372
+ * coverage — and rejects nothing. That is the whole point of the split, and
373
+ * it was got WRONG first: four VOICING gates (the frame must dominate the
374
+ * query, each slot must reach one window on both sides, an insertion or
375
+ * deletion disqualifies the pairing, fillers must be pairwise distinct) were
376
+ * applied here, and every one of them is a requirement for SUBSTITUTING AND
377
+ * SPEAKING, not for knowing where a pairing varies. With them in place the
378
+ * shared reading was reference-shaped: measured over four real pairings, three
379
+ * were hidden from every consumer —
380
+ *
381
+ * `What is the capital of the country where the Eiffel Tower is?`
382
+ * against `What is the capital of France?` (covered 23/61) HIDDEN
383
+ * `What is the capital of France, really?` (an insertion) HIDDEN
384
+ * `What is the capital of Fran?` (sub-window) HIDDEN
385
+ *
386
+ * — including the case of the one consumer that most obviously needed it. A
387
+ * shared layer with one usable consumer is private code at a public address.
388
+ * Each gate now lives with the mechanism that needs it (see reference.ts).
389
+ *
390
+ * Seeded at the origin, because a frame is shared structure the query and its
391
+ * instances both OPEN with: the maximal run around (0,0) is the frame's head
392
+ * and the sweeps find the rest.
393
+ *
394
+ * Null only for a degenerate pairing (either side empty). */
395
+ export function frameSlots(ctx, query, cand, id) {
396
+ if (query.length === 0 || cand.length === 0)
397
+ return null;
398
+ const { matched, gaps } = alignAround(ctx, query, cand, 0, 0);
399
+ const spans = [...matched].sort((a, b) => a[0] - b[0]);
400
+ // Where the alignment RAN OUT on each side. Seeded at the origin there is
401
+ // no leading gap, so both cursors are everything consumed so far: the
402
+ // matched runs (equal length on both sides by construction) plus what each
403
+ // interior gap ate of its own side. Counting only the runs reads the
404
+ // candidate cursor short by exactly the fillers already seen, and invents a
405
+ // trailing gap on every well-aligned instance.
406
+ const all = [...gaps];
407
+ let qEnd = 0, cEnd = 0;
408
+ for (const [s, e] of spans) {
409
+ cEnd += e - s;
410
+ qEnd = Math.max(qEnd, e);
411
+ }
412
+ for (const g of gaps)
413
+ cEnd += g.ce - g.cs;
414
+ if (qEnd < query.length || cEnd < cand.length) {
415
+ all.push({ qs: qEnd, qe: query.length, cs: cEnd, ce: cand.length });
416
+ }
417
+ const slots = [];
418
+ for (const gap of all.sort((a, b) => a.qs - b.qs)) {
419
+ if (gap.qe <= gap.qs && gap.ce <= gap.cs)
420
+ continue;
421
+ // Contract to the varying core. contractGap returns null when one side is
422
+ // wholly shared with the other — a pure insertion or deletion, which is a
423
+ // real variation and is reported AS ONE, not discarded.
424
+ const core = contractGap(query, cand, gap);
425
+ const g = core ?? gap;
426
+ const kind = g.qe > g.qs && g.ce > g.cs
427
+ ? "substitution"
428
+ : g.qe > g.qs
429
+ ? "insertion"
430
+ : "deletion";
431
+ slots.push({
432
+ qs: g.qs,
433
+ qe: g.qe,
434
+ cs: g.cs,
435
+ ce: g.ce,
436
+ kind,
437
+ filler: cand.slice(g.cs, g.ce),
438
+ });
439
+ }
440
+ const covered = spans.reduce((n, [s, e]) => n + e - s, 0);
441
+ return { id, slots, matched: spans, covered };
442
+ }
443
+ /** THE DISPLACED-FILLER GATE: does `projection` speak the ANCHOR's occupant of
444
+ * a position the query fills differently?
445
+ *
446
+ * A mechanism grounding through an anchor voices that anchor's continuation.
447
+ * When the query is the same structure as the anchor with one position filled
448
+ * differently — a different filename, a different word — the anchor's
449
+ * continuation is ABOUT THE ANCHOR'S occupant, and voicing it answers a
450
+ * question the asker did not ask. It is worse than silence, because it is
451
+ * fluent and specific and wrong:
452
+ *
453
+ * trained `How do I compile hello.c?` -> `Run gcc hello.c`
454
+ * asked `How do I compile main.c?`
455
+ * voiced `Run gcc hello.c` <- the corpus's file, not the asker's
456
+ *
457
+ * The same shape on the trained 15.7M-node store: `How do you say 'flurbish'
458
+ * in French?` answers "the way to say hello is \"Bonjour\"".
459
+ *
460
+ * THIS IS NOT THE RESTATED-FRAGMENT GUARD. That one asks whether the
461
+ * projection is a piece of the QUERY; this asks whether it is a piece of the
462
+ * ANCHOR that the query displaced. Neither implies the other, and the
463
+ * observed failures pass the restatement guard cleanly.
464
+ *
465
+ * Three conditions, all byte-exact and all necessary:
466
+ *
467
+ * 1. the query and the anchor must be ONE STRUCTURE — what they share has to
468
+ * dominate the query, or the query is not a variant of the anchor at all
469
+ * and the anchor's occupant of anything is beside the point;
470
+ * 2. both sides of the position must reach one river window — below it byte
471
+ * overlap is chance, not evidence (the floor identityBar and the bridge's
472
+ * attestedQ both draw);
473
+ * 3. the projection must voice the anchor's filler and NOT the query's
474
+ * referent. Voicing both is a projection that carried the asker's own
475
+ * occupant through, which is exactly what a licensed reference does and
476
+ * must stay allowed;
477
+ * 4. and the projection must share NO perceivable content with the query
478
+ * outside that position — no run of one river window.
479
+ *
480
+ * GATE 4 IS WHAT SEPARATES A DIFFERENT THING FROM A DIFFERENT WORD, and
481
+ * without it this refuses correct answers. A displaced slot alone cannot
482
+ * tell them apart: `symbol` <- `formula` and `main` <- `hello` are the same
483
+ * shape to the matcher — one substitution slot, frame dominating. Measured
484
+ * on the trained store, gates 1-3 alone silenced
485
+ *
486
+ * Q `What is the chemical symbol for water?`
487
+ * A `The chemical formula for water is H2O.`
488
+ *
489
+ * which is right, and merely phrased in the corpus's own words. The answer
490
+ * shares `the chemical ` and ` for water` with the question, so it is plainly
491
+ * about what was asked. `Run gcc hello.c` against `How do I compile main.c?`
492
+ * shares nothing but `.c` — two bytes, below the window where overlap stops
493
+ * being chance — so it is not about what was asked at all. No new constant:
494
+ * W is the same floor identityBar, attestedQ and the site test already draw. */
495
+ export function voicesDisplacedFiller(ctx, query, anchor, projection) {
496
+ const W = ctx.space.maxGroup;
497
+ const inst = frameSlots(ctx, query, anchor, 0);
498
+ if (inst === null)
499
+ return false;
500
+ if (!dominates(inst.covered, query.length))
501
+ return false;
502
+ for (const slot of inst.slots) {
503
+ if (slot.kind !== "substitution")
504
+ continue;
505
+ if (slot.qe - slot.qs < W || slot.filler.length < W)
506
+ continue;
507
+ if (indexOf(projection, slot.filler, 0) < 0)
508
+ continue;
509
+ const referent = query.subarray(slot.qs, slot.qe);
510
+ if (indexOf(projection, referent, 0) >= 0)
511
+ continue;
512
+ // Gate 4: does the projection still speak about the query's FRAME? A run
513
+ // of one window anywhere outside the displaced position is enough — the
514
+ // answer is then about the thing that was asked, in the corpus's own
515
+ // wording. Sharing nothing means it is about something else.
516
+ const shared = alignRuns(ctx, query, projection).some((r) => r.qe - r.qs >= W && (r.qe <= slot.qs || r.qs >= slot.qe));
517
+ if (shared)
518
+ continue;
519
+ return true;
520
+ }
521
+ return false;
522
+ }
523
+ /** Whether every member is byte-distinct from the others. */
524
+ export function distinct(items) {
525
+ for (let i = 0; i < items.length; i++) {
526
+ for (let j = i + 1; j < items.length; j++) {
527
+ if (bytesEqual(items[i], items[j]))
528
+ return false;
529
+ }
530
+ }
531
+ return true;
532
+ }
533
+ /** Substitute every `needle -> repl` pair SIMULTANEOUSLY: one left-to-right
534
+ * pass, longest needle first at each position, and a replacement is never
535
+ * re-examined.
536
+ *
537
+ * SIMULTANEOUS IS NOT A DETAIL. Applying the pairs in sequence lets one
538
+ * substitution's OUTPUT be another's input: with slots `gcc -> zig` and
539
+ * `hello.c -> zig.c` a sequential pass rewrites bytes it had just written,
540
+ * and the result depends on the order the slots happened to be found in.
541
+ * Longest-first at each position makes the pass independent of pair order,
542
+ * which is what keeps {@link carriesFillers} and the binding it licenses the
543
+ * SAME operation — if they could disagree, the licence would not be testing
544
+ * what is voiced. */
545
+ export function substituteAll(hay, pairs) {
546
+ const usable = pairs.filter((p) => p.needle.length > 0);
547
+ if (usable.length === 0)
548
+ return hay;
549
+ // Longest needle first, so a needle that is a prefix of another can never
550
+ // pre-empt it. Ties cannot arise: an instance whose fillers are not
551
+ // pairwise distinct is refused by frameSlots.
552
+ const order = [...usable].sort((a, b) => b.needle.length - a.needle.length);
553
+ const out = [];
554
+ let i = 0;
555
+ let hit = false;
556
+ outer: while (i < hay.length) {
557
+ for (const p of order) {
558
+ if (i + p.needle.length > hay.length)
559
+ continue;
560
+ let k = 0;
561
+ while (k < p.needle.length && hay[i + k] === p.needle[k])
562
+ k++;
563
+ if (k < p.needle.length)
564
+ continue;
565
+ for (const b of p.repl)
566
+ out.push(b);
567
+ i += p.needle.length;
568
+ hit = true;
569
+ continue outer;
570
+ }
571
+ out.push(hay[i]);
572
+ i++;
573
+ }
574
+ return hit ? Uint8Array.from(out) : hay;
575
+ }
576
+ /** THE CARRIAGE LICENCE — the gate that decides whether a slot may be VOICED
577
+ * through. Given two instances of one frame and what each one continues to,
578
+ * it asks one byte question:
579
+ *
580
+ * substituteAll(contA, fillersA -> fillersB) == contB
581
+ *
582
+ * When it holds, the corpus attests byte-exactly that the continuation is a
583
+ * function of the fillers and nothing else, so putting a NEW occupant through
584
+ * the same carriage is derivation rather than invention. No threshold, no
585
+ * similarity, no new constant: the store's own instances decide, exactly as
586
+ * the bridge's `unanimous` decides whether a frame is a value slot.
587
+ *
588
+ * Its FAILURE is what this is really for. A frame whose continuation carries
589
+ * filler-DEPENDENT content — `What is the capital of X?` answering a different
590
+ * city per X — fails it, and that failure is the only thing between a slot
591
+ * and an invented fact. Measured on the trained 15.7M-node store (325,615
592
+ * contexts): `What is the capital of Zamunda?` resonates to a PURE cohort,
593
+ * every one of the top 14 hits an instance of that frame, with an unambiguous
594
+ * slot; every structural gate passes and only this one refuses, on
595
+ * `replace("Tokyo", "Japan" -> "France") != "Paris"`.
596
+ *
597
+ * With SEVERAL slots the test is unchanged, which is the point of testing the
598
+ * whole substitution at once: a frame whose answer tracks one slot but
599
+ * invents around another fails exactly as a single-slot value slot does. */
600
+ export function carriesFillers(contA, fillersA, contB, fillersB) {
601
+ if (fillersA.length !== fillersB.length)
602
+ return false;
603
+ const projected = substituteAll(contA, fillersA.map((needle, s) => ({ needle, repl: fillersB[s] })));
604
+ return bytesEqual(projected, contB);
605
+ }
606
+ /** The IN-LIST halo matcher: the best halo-mate for `halo` among EXPLICIT
607
+ * candidates, above the concept threshold — the list counterpart of
608
+ * {@link haloSiblings}, which asks the halo INDEX for candidates instead.
609
+ * Behind locate()'s halo step and articulation's voice matching; a third
610
+ * "best halo among these" decision must come here, not inline. */
611
+ export function bestHaloMate(ctx, halo, items, haloOf) {
612
+ return argmaxCosine(halo, items, haloOf, conceptThreshold(ctx.store.D));
613
+ }
614
+ /** The HALO-SIBLING matcher: the nodes that keep the same distributional
615
+ * company as `id`, nearest first — `resonateHalo` filtered to exclude the
616
+ * node itself and everything below `bar` (default: the concept threshold).
617
+ * `halo`, when the caller has already read the node's halo row, is reused
618
+ * instead of refetched (one read per relation). Returns [] for a node with
619
+ * no halo. The one sibling enumeration behind the concept hop, the
620
+ * reasoning stage's synonym expansion, and the analogy matcher below. */
621
+ const haloSiblingMemo = new WeakMap();
622
+ export async function haloSiblings(ctx, id, halo, bar = conceptThreshold(ctx.store.D)) {
623
+ // Per-response memo for the DEFAULT-ARGUMENT reading (the one the concept
624
+ // hop, the bridge's synonym tier, and reasoning's synonym expansion all
625
+ // use): the same node's siblings are asked for repeatedly within one
626
+ // response (bridge pairs share sides), each a full halo-ANN query, and the
627
+ // store is read-only while a response is in flight. Keyed by the response
628
+ // lifecycle object (ctx.climbMemo — fresh per respond, nulled after).
629
+ // Calls with an explicit halo or bar (analogyStrength's gated reading)
630
+ // bypass the memo — their filter differs.
631
+ const memoable = halo === undefined &&
632
+ bar === conceptThreshold(ctx.store.D) && ctx.climbMemo !== null;
633
+ let memo;
634
+ if (memoable) {
635
+ memo = haloSiblingMemo.get(ctx.climbMemo);
636
+ if (memo === undefined) {
637
+ haloSiblingMemo.set(ctx.climbMemo, memo = new Map());
638
+ }
639
+ const hit = memo.get(id);
640
+ if (hit !== undefined)
641
+ return hit;
642
+ }
643
+ const h = halo ?? ctx.store.halo(id);
644
+ const out = h
645
+ ? (await ctx.store.resonateHalo(h, ctx.cfg.haloQueryK))
646
+ .filter((sib) => sib.id !== id && sib.score >= bar)
647
+ : [];
648
+ if (memo !== undefined)
649
+ memo.set(id, out);
650
+ return out;
651
+ }
652
+ /** Bundle the distributional company of every addressable W-window in a
653
+ * byte span. This is the query-time counterpart of the write-side halo
654
+ * pours: no lexical unit or storage row is invented; the span is represented
655
+ * by VSA superposition of the window concepts the store already knows.
656
+ *
657
+ * Components are normalized before bundling so repetition mass remains
658
+ * evidence about each stored node, not an accidental weight on one window
659
+ * inside the composed phrase. Returns null when the corpus provides no
660
+ * distributional evidence for the span. */
661
+ export function spanHalo(ctx, bytes, from = 0, to = bytes.length) {
662
+ const W = ctx.space.maxGroup;
663
+ if (to - from < W)
664
+ return null;
665
+ if (ctx.meter)
666
+ ctx.meter.spanHalos++;
667
+ const out = zeros(ctx.store.D);
668
+ let found = false;
669
+ const added = new Set();
670
+ const episodeRoots = [];
671
+ const N = corpusN(ctx);
672
+ const reachMemo = sharedReachMemo(ctx);
673
+ const addHalo = (id) => {
674
+ if (added.has(id))
675
+ return;
676
+ const halo = ctx.store.halo(id);
677
+ if (halo === null)
678
+ return;
679
+ const norm = Math.sqrt(dot(halo, halo));
680
+ if (norm === 0)
681
+ return;
682
+ added.add(id);
683
+ addInto(out, halo, 1 / norm);
684
+ found = true;
685
+ };
686
+ const windowCount = to - from - W + 1;
687
+ const offsets = [];
688
+ const samples = Math.min(W, windowCount);
689
+ for (let i = 0; i < samples; i++) {
690
+ const relative = samples === 1
691
+ ? 0
692
+ : Math.floor((i * (windowCount - 1)) / (samples - 1));
693
+ const off = from + relative;
694
+ if (offsets[offsets.length - 1] !== off)
695
+ offsets.push(off);
696
+ }
697
+ for (const off of offsets) {
698
+ if (ctx.meter)
699
+ ctx.meter.spanHaloWindows++;
700
+ const ids = leafIdRun(ctx, bytes, off, off + W);
701
+ if (ids === null)
702
+ continue;
703
+ const id = ctx.store.findBranch(ids);
704
+ if (id === null)
705
+ continue;
706
+ addHalo(id);
707
+ // Canonical flat windows are retrieval addresses and normally carry no
708
+ // halo themselves. Their bounded structural ascent reaches the learned
709
+ // episode forms that contain them; bundling those forms' company is the
710
+ // distributional meaning of the window, derived entirely from existing
711
+ // containment and halo state.
712
+ if (!added.has(id)) {
713
+ episodeRoots.push(edgeAncestors(ctx, id, N, reachMemo).roots);
714
+ }
715
+ }
716
+ for (let rank = 0; added.size < ctx.cfg.haloQueryK; rank++) {
717
+ let any = false;
718
+ for (const roots of episodeRoots) {
719
+ if (rank >= roots.length)
720
+ continue;
721
+ any = true;
722
+ addHalo(roots[rank]);
723
+ if (added.size >= ctx.cfg.haloQueryK)
724
+ break;
725
+ }
726
+ if (!any)
727
+ break;
728
+ }
729
+ return found ? normalize(out) : null;
730
+ }
731
+ /** Distributional synonym evidence between arbitrary byte spans. Whole words
732
+ * need not be independently interned: their stored W-window occurrences are
733
+ * lifted to episode halos, bundled, and compared. The caller chooses the
734
+ * derived gate appropriate to its claim (concept identity or analogy). */
735
+ export function spanSynonymStrength(ctx, a, b) {
736
+ const ah = spanHalo(ctx, a);
737
+ const bh = spanHalo(ctx, b);
738
+ if (ah === null || bh === null)
739
+ return 0;
740
+ return cosine(ah, bh);
741
+ }
742
+ export async function analogyStrength(ctx, a, b) {
743
+ const ha = ctx.store.halo(a);
744
+ const hb = ctx.store.halo(b);
745
+ if (ha && hb) {
746
+ const bar = significanceBar(ctx.store.D);
747
+ const direct = cosine(ha, hb);
748
+ if (direct >= bar)
749
+ return { score: direct, halo: true };
750
+ const sibsA = await haloSiblings(ctx, a, ha, bar);
751
+ const sibsB = await haloSiblings(ctx, b, hb, bar);
752
+ let best = 0;
753
+ for (const x of sibsA) {
754
+ if (x.id === b)
755
+ continue;
756
+ const y = sibsB.find((s) => s.id === x.id);
757
+ if (y !== undefined) {
758
+ best = Math.max(best, Math.min(x.score, y.score));
759
+ }
760
+ }
761
+ if (best > 0)
762
+ return { score: best, halo: true };
763
+ }
764
+ return { score: sharedFrameStrength(ctx, a, b), halo: false };
765
+ }
766
+ /** The STRUCTURAL analogy tier: two nodes are analogs when their byte
767
+ * streams share a LEARNT frame — a content-addressed flat form of at least
768
+ * one full river window (W bytes, the perception quantum) that occurs in
769
+ * BOTH. This is what "playing the same role" means structurally: "Ice is
770
+ * cold" and "Steel is hard" share the learnt " is " frame even though they
771
+ * keep disjoint distributional company. Halos measure company by IDENTITY
772
+ * (company signatures — see sema.ts), so unrelated-company analogs must be
773
+ * validated by the frame itself, not by content leaking through halo
774
+ * vectors. Strength is the shared learnt coverage of the SHORTER side —
775
+ * a fraction, comparable to the cosine tiers above. Derived: the window
776
+ * is maxGroup, the same quantum differsByOneWindow and canonicalChunkId
777
+ * measure by; no tuned constants. */
778
+ export function sharedFrameStrength(ctx, a, b) {
779
+ return sharedFrameStrengthOf(ctx, read(ctx, a), read(ctx, b));
780
+ }
781
+ /** The same measure over BYTES, for callers holding a role-establishing
782
+ * CONTEXT rather than the node whose role it establishes — CAST's comparison
783
+ * reads the tier this way when two candidate analogs are fillers (bare entity
784
+ * names) rather than frame-bearing structures themselves. A role is a
785
+ * property of the context that establishes a filler, never of the filler's
786
+ * own bytes: measured on test/29's corpus, "Michelangelo" against "Homer"
787
+ * reads 0.000 while their establishing contexts ("The David was sculpted
788
+ * by…" against "The Iliad was written by…") read 0.452, and a context in a
789
+ * genuinely different frame ("Water boils at…") still reads 0.000 — the tier
790
+ * discriminates, it was simply being asked about the wrong bytes. */
791
+ export function sharedFrameStrengthOf(ctx, A, B) {
792
+ const W = ctx.space.maxGroup;
793
+ if (A.length < W || B.length < W)
794
+ return 0;
795
+ // Mark every byte of the shorter side covered by a learnt W-window that
796
+ // also occurs in the longer side.
797
+ const [s, l] = A.length <= B.length ? [A, B] : [B, A];
798
+ const covered = new Uint8Array(s.length);
799
+ for (let off = 0; off + W <= s.length; off++) {
800
+ const win = s.subarray(off, off + W);
801
+ // Learnt: the window resolves as a content-addressed flat form.
802
+ const ids = leafIdRun(ctx, s, off, off + W);
803
+ if (ids === null || ctx.store.findBranch(ids) === null)
804
+ continue;
805
+ if (indexOf(l, win, 0) < 0)
806
+ continue;
807
+ covered.fill(1, off, off + W);
808
+ }
809
+ let n = 0;
810
+ for (let i = 0; i < s.length; i++)
811
+ n += covered[i];
812
+ return n >= W ? n / s.length : 0;
813
+ }
814
+ // ═══════════════════════════════════════════════════════════════════════════
815
+ // PROJECTIONS — what a matched node is projected ALONG (the direction)
816
+ // ═══════════════════════════════════════════════════════════════════════════
817
+ /** FORWARD through a synonym: the continuation an edge-less node borrows from
818
+ * a concept (halo) sibling — resonate the node's halo, take the first
819
+ * sibling above the concept threshold that itself has a direct edge. */
820
+ export async function conceptHop(ctx, id) {
821
+ for (const sib of await haloSiblings(ctx, id)) {
822
+ const hop = guidedFirst(ctx, sib.id);
823
+ if (hop !== undefined)
824
+ return hop;
825
+ }
826
+ return null;
827
+ }
828
+ /** FORWARD projection: follow continuation edges from a node to its fixpoint.
829
+ * The first hop may cross a concept (halo) link — a synonym. The rest
830
+ * follow direct edges. Convergence is intrinsic: the seen set guards
831
+ * against cycles. `guide` disambiguates multi-continuation nodes by
832
+ * resonance. */
833
+ export async function follow(ctx, id, guide) {
834
+ const seen = new Set([id]);
835
+ // First hop: a direct edge, else a concept sibling's edge (the synonym).
836
+ let next = chooseNext(ctx, id, guide);
837
+ if (next === undefined) {
838
+ const hop = await conceptHop(ctx, id);
839
+ if (hop === null)
840
+ return null;
841
+ next = hop;
842
+ }
843
+ // Direct successors to the fixpoint. Only the FIXPOINT's bytes are
844
+ // returned, so the walk tracks node ids and reads bytes exactly once at
845
+ // the end — a K-hop chain used to pay K full reconstructions and discard
846
+ // K−1 of them.
847
+ while (!seen.has(next)) {
848
+ seen.add(next);
849
+ const fwd = chooseNext(ctx, next, guide);
850
+ if (fwd === undefined || seen.has(fwd))
851
+ break;
852
+ next = fwd;
853
+ }
854
+ return read(ctx, next);
855
+ }
856
+ /** REVERSE projection: the context a learnt continuation follows, voiced as
857
+ * bytes. A common continuation ("Yes.") follows MANY contexts; with a
858
+ * `guide` the context whose gist resonates with the query wins (seat
859
+ * symmetry) — without one, the most-corroborated context wins (poured halo
860
+ * MASS, the direct measure of how many episodes established it), falling
861
+ * back to first-learnt on equal mass. Among many predecessors RECIPROCAL
862
+ * ones (mutual edges) are preferred when any exist (RC5). Callers that
863
+ * HAVE a query gist must pass it, or they silently change disambiguation
864
+ * regime.
865
+ *
866
+ * `rev`, when the caller has already materialised prevOf (one read per
867
+ * relation — a hub's reverse fan-in is corpus-sized), is reused instead of
868
+ * refetched. Returns null when there is no predecessor or the picked
869
+ * context reads empty (a zero-length context is no grounding: an empty
870
+ * Uint8Array is truthy, and returning it would flow a hollow "answer"
871
+ * onward). */
872
+ export function reverseContext(ctx, id, guide, rev) {
873
+ // CAPPED default read: only the first √N predecessors are ever candidates
874
+ // (hubCap below / in chooseAmong), so only they are read. hubBound ≥ 2
875
+ // keeps the single-predecessor shortcut exact.
876
+ const candidates = rev ?? ctx.store.prevFirst(id, hubBound(ctx));
877
+ if (candidates.length === 0)
878
+ return null;
879
+ // RECIPROCAL PREFERENCE: among many predecessors, one that `id` also
880
+ // continues TO (cand → id AND id → cand both learnt) is a mutually
881
+ // established pairing — the strongest structural evidence a predecessor
882
+ // can carry (bidirectional training deposits both directions of a genuine
883
+ // pair). A bare predecessor is one episode's adjacency; guide-resonance
884
+ // over bare predecessors favours whichever stored document merely
885
+ // CONTAINS the query's bytes (the linear fold's cosine is byte overlap —
886
+ // the observed "merci → unrelated French document" failure). One capped
887
+ // forward read decides; when no reciprocal exists, behaviour is unchanged
888
+ // — bare predecessors ARE the honest answer for a shared deposited
889
+ // continuation (two questions → one answer; audited by 31-audit C1), and
890
+ // this arm serves every mechanism's reverse projection, so abstaining
891
+ // here starves far more than the one containment failure it would fix.
892
+ let pool = candidates;
893
+ if (candidates.length > 1) {
894
+ const fwd = new Set(ctx.store.nextFirst(id, hubBound(ctx)));
895
+ if (fwd.size > 0) {
896
+ const mutual = candidates.filter((c) => fwd.has(c));
897
+ if (mutual.length > 0)
898
+ pool = mutual;
899
+ }
900
+ }
901
+ const pick = pool.length === 1
902
+ ? pool[0]
903
+ : guide
904
+ ? chooseAmong(ctx, pool, guide).id
905
+ : pickByMass(ctx, pool);
906
+ const g = read(ctx, pick);
907
+ return g.length > 0 ? g : null;
908
+ }
909
+ /** The most-corroborated candidate by poured halo mass (first-seen wins a
910
+ * tie). Capped at √N candidates by insertion order — the same hub bound
911
+ * every fan-out walk uses. */
912
+ function pickByMass(ctx, ids) {
913
+ const capped = hubCap(ctx, ids);
914
+ let best = capped[0];
915
+ let bestMass = ctx.store.haloMass(best);
916
+ for (let i = 1; i < capped.length; i++) {
917
+ const mass = ctx.store.haloMass(capped[i]);
918
+ if (mass > bestMass) {
919
+ best = capped[i];
920
+ bestMass = mass;
921
+ }
922
+ }
923
+ return best;
924
+ }
925
+ /** THE projection: ground a matched node to answer bytes — FORWARD to its
926
+ * continuation fixpoint (which may cross a concept hop), else REVERSE to
927
+ * the context it follows. This is the direction ladder every mechanism's
928
+ * final grounding step reduces to. */
929
+ export async function project(ctx, id, guide) {
930
+ const fc = await follow(ctx, id, guide);
931
+ if (fc)
932
+ return fc;
933
+ return reverseContext(ctx, id, guide);
934
+ }
935
+ // ── The span-shape family ───────────────────────────────────────────────────
936
+ //
937
+ // "Is this answer drawn from this context?" has TWO formally distinct
938
+ // readings, and the pair plus the anchor classifier built on them are SHARED
939
+ // machinery — extraction proposes span-shaped exemplars with them, the
940
+ // shared `Precomputed.spanShapedOf` container computes them, and fusion
941
+ // (reasoning.ts) gates on the strict one. They lived inside
942
+ // mechanisms/extraction.ts, so `pipeline-mechanism.ts` and `reasoning.ts`
943
+ // both had to import back OUT of a specific mechanism — an inversion the
944
+ // mechanism market forbids (AGENTS §2.6: the shared contract may not depend
945
+ // on any one mechanism; §2.5: a shared matcher belongs to this family, never
946
+ // to a mechanism's private helpers). Deleting extraction must not break the
947
+ // shared container, so they live here.
948
+ //
949
+ // • isSpanShaped — the OPEN reading (sparse in-order embedding).
950
+ // • containsSpan — the STRICT reading (contiguous run or resolved node).
951
+ // • skillExemplar — classify one anchor into (context, answer) using them.
952
+ //
953
+ // The two readings are NOT interchangeable; AGENTS §2.5 pins the distinction
954
+ // and each function's own doc states what breaks if it is substituted.
955
+ /** Check whether an anchor is a span-shaped skill exemplar: it represents a
956
+ * fact whose context and answer together form a span-in-context pattern.
957
+ * If the anchor has a nextOf continuation, that is the answer and the anchor
958
+ * itself is the context. Otherwise the anchor's prevOf parents provide
959
+ * candidate contexts, and the longest one whose span is span-shaped wins. */
960
+ export async function skillExemplar(ctx, anchor, guide) {
961
+ if (ctx.store.hasNext(anchor)) {
962
+ const contextBytes = read(ctx, anchor);
963
+ const answerBytes = await follow(ctx, anchor, guide);
964
+ if (answerBytes !== null && isSpanShaped(ctx, contextBytes, answerBytes)) {
965
+ return { contextBytes, answerBytes };
966
+ }
967
+ return null;
968
+ }
969
+ const answerBytes = read(ctx, anchor);
970
+ // Candidate contexts, capped at the hub bound (a common answer's reverse
971
+ // fan-in is corpus-sized).
972
+ const capped = ctx.store.prevFirst(anchor, hubBound(ctx));
973
+ const spanShaped = [];
974
+ for (const p of capped) {
975
+ const ctxB = read(ctx, p);
976
+ if (ctxB.length > 0 && isSpanShaped(ctx, ctxB, answerBytes)) {
977
+ spanShaped.push({ id: p, bytes: ctxB });
978
+ }
979
+ }
980
+ if (spanShaped.length === 0)
981
+ return null;
982
+ // Among span-shaped contexts, the longest wins (the smallest spanning frame
983
+ // heuristic's dual: more frame to locate in the query); the query gist,
984
+ // when given, breaks LENGTH TIES via chooseAmong — the same reverse-regime
985
+ // disambiguator every context pick uses, whose gist cache spares the
986
+ // re-fold this block once paid per tied candidate. Same strict first-seen
987
+ // tie-break as the hand loop it replaces.
988
+ const maxLen = Math.max(...spanShaped.map((s) => s.bytes.length));
989
+ const longest = spanShaped.filter((s) => s.bytes.length === maxLen);
990
+ let contextBytes = longest[0].bytes;
991
+ if (guide && longest.length > 1) {
992
+ const pick = chooseAmong(ctx, longest.map((s) => s.id), guide).id;
993
+ contextBytes = longest.find((s) => s.id === pick).bytes;
994
+ }
995
+ return { contextBytes, answerBytes };
996
+ }
997
+ /** Whether the answer is a SPARSE subsequence of the context (bytes in
998
+ * order, arbitrary gaps) — the OPEN span-shape reading (see the section
999
+ * note above). This is what lets extraction validate a MULTI-PIECE
1000
+ * exemplar whose answer is stitched from several context runs — but it is
1001
+ * deliberately permissive, so it must never be used as evidence that one
1002
+ * span was "drawn from" another (see {@link containsSpan} for that).
1003
+ *
1004
+ * There is deliberately NO containsSpan pre-check here: strict containment
1005
+ * IMPLIES the subsequence embedding (a contiguous run, or a resolved node —
1006
+ * whose content-addressed identity means its bytes occur contiguously — is
1007
+ * an in-order embedding with zero gaps), so the scan below decides alone,
1008
+ * with the same truth value. The old pre-check re-perceived the context
1009
+ * (a full river fold) per CANDIDATE in skillExemplar's √N-capped loop —
1010
+ * pure cost, no discrimination. */
1011
+ export function isSpanShaped(_ctx, context, answer) {
1012
+ let ai = 0;
1013
+ for (let ci = 0; ci < context.length && ai < answer.length; ci++) {
1014
+ if (context[ci] === answer[ai])
1015
+ ai++;
1016
+ }
1017
+ return ai === answer.length;
1018
+ }
1019
+ /** STRICT containment: the answer's resolved node appears in the context's
1020
+ * folded tree, or the answer occurs as one CONTIGUOUS byte run of the
1021
+ * context. This is real evidence the answer was drawn from the context.
1022
+ * Fusion gates on this — the sparse-subsequence reading of
1023
+ * {@link isSpanShaped} is trivially satisfied by short answers over long
1024
+ * queries ("cold" is a gap-tolerant subsequence of most sentences holding
1025
+ * c…o…l…d in order), and gating fusion on it silently starved multi-topic
1026
+ * queries of their further points of attention. */
1027
+ export function containsSpan(ctx, context, answer) {
1028
+ const ansId = resolve(ctx, answer);
1029
+ if (ansId !== null) {
1030
+ let found = false;
1031
+ foldTree(ctx, perceive(ctx, context), 0, (_n, _s, _e, node) => {
1032
+ if (node === ansId)
1033
+ found = true;
1034
+ });
1035
+ if (found)
1036
+ return true;
1037
+ }
1038
+ return indexOf(context, answer, 0) >= 0;
1039
+ }