@hviana/sema 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/package.json +1 -1
  2. package/src/mind/attention.ts +355 -19
  3. package/test/53-cross-region-probe-instrumentation.test.mjs +356 -0
  4. package/dist/example/demo.d.ts +0 -1
  5. package/dist/example/demo.js +0 -39
  6. package/dist/example/train_base.d.ts +0 -87
  7. package/dist/example/train_base.js +0 -2252
  8. package/dist/src/alphabet.d.ts +0 -7
  9. package/dist/src/alphabet.js +0 -33
  10. package/dist/src/alu/src/alu.d.ts +0 -185
  11. package/dist/src/alu/src/alu.js +0 -440
  12. package/dist/src/alu/src/expr.d.ts +0 -61
  13. package/dist/src/alu/src/expr.js +0 -318
  14. package/dist/src/alu/src/index.d.ts +0 -11
  15. package/dist/src/alu/src/index.js +0 -19
  16. package/dist/src/alu/src/kernel-arith.d.ts +0 -16
  17. package/dist/src/alu/src/kernel-arith.js +0 -264
  18. package/dist/src/alu/src/kernel-bits.d.ts +0 -19
  19. package/dist/src/alu/src/kernel-bits.js +0 -152
  20. package/dist/src/alu/src/kernel-logic.d.ts +0 -4
  21. package/dist/src/alu/src/kernel-logic.js +0 -60
  22. package/dist/src/alu/src/kernel-nd.d.ts +0 -3
  23. package/dist/src/alu/src/kernel-nd.js +0 -208
  24. package/dist/src/alu/src/kernel-numeric.d.ts +0 -54
  25. package/dist/src/alu/src/kernel-numeric.js +0 -366
  26. package/dist/src/alu/src/operation.d.ts +0 -168
  27. package/dist/src/alu/src/operation.js +0 -189
  28. package/dist/src/alu/src/parser.d.ts +0 -221
  29. package/dist/src/alu/src/parser.js +0 -577
  30. package/dist/src/alu/src/resonance.d.ts +0 -55
  31. package/dist/src/alu/src/resonance.js +0 -126
  32. package/dist/src/alu/src/text.d.ts +0 -31
  33. package/dist/src/alu/src/text.js +0 -73
  34. package/dist/src/alu/src/value.d.ts +0 -109
  35. package/dist/src/alu/src/value.js +0 -300
  36. package/dist/src/alu/test/alu.test.d.ts +0 -1
  37. package/dist/src/alu/test/alu.test.js +0 -764
  38. package/dist/src/bytes.d.ts +0 -14
  39. package/dist/src/bytes.js +0 -59
  40. package/dist/src/canon.d.ts +0 -26
  41. package/dist/src/canon.js +0 -57
  42. package/dist/src/config.d.ts +0 -111
  43. package/dist/src/config.js +0 -91
  44. package/dist/src/derive/src/deduction.d.ts +0 -125
  45. package/dist/src/derive/src/deduction.js +0 -155
  46. package/dist/src/derive/src/index.d.ts +0 -7
  47. package/dist/src/derive/src/index.js +0 -11
  48. package/dist/src/derive/src/priority-queue.d.ts +0 -20
  49. package/dist/src/derive/src/priority-queue.js +0 -73
  50. package/dist/src/derive/src/rewrite.d.ts +0 -56
  51. package/dist/src/derive/src/rewrite.js +0 -100
  52. package/dist/src/derive/src/trie.d.ts +0 -90
  53. package/dist/src/derive/src/trie.js +0 -217
  54. package/dist/src/derive/test/derive.test.d.ts +0 -1
  55. package/dist/src/derive/test/derive.test.js +0 -122
  56. package/dist/src/extension.d.ts +0 -37
  57. package/dist/src/extension.js +0 -7
  58. package/dist/src/geometry.d.ts +0 -202
  59. package/dist/src/geometry.js +0 -575
  60. package/dist/src/index.d.ts +0 -16
  61. package/dist/src/index.js +0 -18
  62. package/dist/src/ingest-cache.d.ts +0 -41
  63. package/dist/src/ingest-cache.js +0 -161
  64. package/dist/src/mind/articulation.d.ts +0 -6
  65. package/dist/src/mind/articulation.js +0 -99
  66. package/dist/src/mind/attention.d.ts +0 -315
  67. package/dist/src/mind/attention.js +0 -1549
  68. package/dist/src/mind/bridge.d.ts +0 -30
  69. package/dist/src/mind/bridge.js +0 -569
  70. package/dist/src/mind/canonical.d.ts +0 -29
  71. package/dist/src/mind/canonical.js +0 -88
  72. package/dist/src/mind/graph-search.d.ts +0 -285
  73. package/dist/src/mind/graph-search.js +0 -956
  74. package/dist/src/mind/index.d.ts +0 -7
  75. package/dist/src/mind/index.js +0 -5
  76. package/dist/src/mind/junction.d.ts +0 -124
  77. package/dist/src/mind/junction.js +0 -317
  78. package/dist/src/mind/learning.d.ts +0 -47
  79. package/dist/src/mind/learning.js +0 -248
  80. package/dist/src/mind/match.d.ts +0 -126
  81. package/dist/src/mind/match.js +0 -441
  82. package/dist/src/mind/mechanisms/alu.d.ts +0 -4
  83. package/dist/src/mind/mechanisms/alu.js +0 -36
  84. package/dist/src/mind/mechanisms/cast.d.ts +0 -89
  85. package/dist/src/mind/mechanisms/cast.js +0 -716
  86. package/dist/src/mind/mechanisms/confluence.d.ts +0 -24
  87. package/dist/src/mind/mechanisms/confluence.js +0 -225
  88. package/dist/src/mind/mechanisms/cover.d.ts +0 -6
  89. package/dist/src/mind/mechanisms/cover.js +0 -203
  90. package/dist/src/mind/mechanisms/extraction.d.ts +0 -67
  91. package/dist/src/mind/mechanisms/extraction.js +0 -387
  92. package/dist/src/mind/mechanisms/recall.d.ts +0 -13
  93. package/dist/src/mind/mechanisms/recall.js +0 -325
  94. package/dist/src/mind/mind.d.ts +0 -276
  95. package/dist/src/mind/mind.js +0 -572
  96. package/dist/src/mind/pipeline-mechanism.d.ts +0 -142
  97. package/dist/src/mind/pipeline-mechanism.js +0 -213
  98. package/dist/src/mind/pipeline.d.ts +0 -20
  99. package/dist/src/mind/pipeline.js +0 -217
  100. package/dist/src/mind/primitives.d.ts +0 -70
  101. package/dist/src/mind/primitives.js +0 -287
  102. package/dist/src/mind/rationale.d.ts +0 -139
  103. package/dist/src/mind/rationale.js +0 -163
  104. package/dist/src/mind/reasoning.d.ts +0 -34
  105. package/dist/src/mind/reasoning.js +0 -240
  106. package/dist/src/mind/recognition.d.ts +0 -20
  107. package/dist/src/mind/recognition.js +0 -390
  108. package/dist/src/mind/resonance.d.ts +0 -23
  109. package/dist/src/mind/resonance.js +0 -0
  110. package/dist/src/mind/trace.d.ts +0 -15
  111. package/dist/src/mind/trace.js +0 -73
  112. package/dist/src/mind/traverse.d.ts +0 -113
  113. package/dist/src/mind/traverse.js +0 -568
  114. package/dist/src/mind/types.d.ts +0 -289
  115. package/dist/src/mind/types.js +0 -130
  116. package/dist/src/rabitq-ivf/src/database.d.ts +0 -113
  117. package/dist/src/rabitq-ivf/src/database.js +0 -201
  118. package/dist/src/rabitq-ivf/src/index.d.ts +0 -7
  119. package/dist/src/rabitq-ivf/src/index.js +0 -4
  120. package/dist/src/rabitq-ivf/src/ivf.d.ts +0 -200
  121. package/dist/src/rabitq-ivf/src/ivf.js +0 -1165
  122. package/dist/src/rabitq-ivf/src/prng.d.ts +0 -19
  123. package/dist/src/rabitq-ivf/src/prng.js +0 -36
  124. package/dist/src/rabitq-ivf/src/rabitq.d.ts +0 -95
  125. package/dist/src/rabitq-ivf/src/rabitq.js +0 -283
  126. package/dist/src/sema.d.ts +0 -31
  127. package/dist/src/sema.js +0 -63
  128. package/dist/src/store-sqlite.d.ts +0 -184
  129. package/dist/src/store-sqlite.js +0 -942
  130. package/dist/src/store.d.ts +0 -663
  131. package/dist/src/store.js +0 -1618
  132. package/dist/src/vec.d.ts +0 -31
  133. package/dist/src/vec.js +0 -109
@@ -1,956 +0,0 @@
1
- // graph-search.ts — the weighted deduction system that thinking runs on.
2
- //
3
- // Thinking is ONE thing: a lightest derivation over the Sema graph (derive's
4
- // adapted A*LD engine). This file is that one thing, factored out of the mind: it
5
- // states the items, axioms, goal, and weighted rules, and returns the chosen
6
- // spans of the lightest cover. Every behaviour — covering the query, following
7
- // edges to a fixpoint, jumping a concept (halo) link, fusing fragments into a
8
- // deeper learned form, splicing a learnt connector between two rewrites, and
9
- // re-voicing an answer in the asker's words — is a rule here, not a separate
10
- // algorithm.
11
- //
12
- // It reaches Sema only through the {@link Store} interface and one injected
13
- // callback, {@link GraphSearch.resolve} (canonical node id of a byte span). The
14
- // async hints the synchronous search cannot gather for itself — concept targets
15
- // and connectors — are pre-resolved by the caller (see src/mind/pipeline.ts) and handed in.
16
- // So this engine stays decoupled: it knows the graph's shape, never how the
17
- // graph was learnt.
18
- import { lightestDerivation, } from "../derive/src/index.js";
19
- import { bytesEqual, concat2, concatBytes, latin1 } from "../bytes.js";
20
- import { ALL } from "./types.js";
21
- // The cost ladder is a strict ORDERING, not tuned magic:
22
- // • Coverage dominates everything: leaving one query byte unrecognised (PASS)
23
- // outweighs any chain of graph steps a covering derivation could take, so
24
- // the search always prefers to recognise.
25
- // • A direct edge (STEP) is the cheapest move; fusing adjacent fragments is
26
- // free (cost 0) — it only re-NAMES a span, it does not advance the cover.
27
- // • A concept jump (CONCEPT) is dearer than a direct edge, so a literal
28
- // continuation is preferred to a synonym's.
29
- // Any constants with this ordering give the same lightest derivations.
30
- //
31
- // EXPORTED because the ladder is the ONE cost currency of the whole mind:
32
- // think's grounding decider (pipeline.ts) weighs every mechanism's candidate
33
- // answer in these same units, so a mechanism-level choice and a byte-level
34
- // choice are the same kind of decision — a lightest derivation.
35
- export const STEP = 1;
36
- export const CONCEPT = 10;
37
- export const PASS = 1000;
38
- /** The cheapest local cost in the ladder: a recognised completion bridging into
39
- * the cover. Far below STEP, so connecting two recognised spans never disturbs
40
- * the ordering — and, being the minimum per-position cost, it is the per-byte
41
- * unit of the admissible search heuristic (see {@link GraphSearch.buildSearch}). */
42
- export const MICRO = 1e-3;
43
- /** Append `v` to the list at `mp[k]`, creating the list on first use. */
44
- function pushInto(mp, k, v) {
45
- const a = mp.get(k);
46
- if (a)
47
- a.push(v);
48
- else
49
- mp.set(k, [v]);
50
- }
51
- /** Read the chosen spans back off a derivation: the goal is a chain of bridge
52
- * steps, each whose second premise is the `out` it crossed. Walk the chain to
53
- * the axiom and reverse into left-to-right order. */
54
- function readCover(derivation) {
55
- const segs = [];
56
- let node = derivation;
57
- while (node && node.rule) {
58
- const out = node.premises[1].item;
59
- if (out.kind === "out") {
60
- segs.push({
61
- i: out.i,
62
- j: out.j,
63
- bytes: out.bytes,
64
- rec: out.rec,
65
- node: out.node,
66
- computed: out.computed,
67
- });
68
- }
69
- node = node.premises[0];
70
- }
71
- segs.reverse();
72
- return segs;
73
- }
74
- /** Flatten a {@link GItem} into the {@link DerivationItem} a rationale shows. */
75
- function derivationItem(it) {
76
- if (it.kind === "cover")
77
- return { kind: "cover", span: [it.p, it.p] };
78
- if (it.kind === "form") {
79
- return { kind: "form", span: [it.i, it.j], node: it.node };
80
- }
81
- return { kind: "out", span: [it.i, it.j], bytes: it.bytes, node: it.node };
82
- }
83
- /** Name the reasoning act a rule performed, from the shape of what it consumed
84
- * and produced — the one mapping from rule geometry to a human move. Mirrors
85
- * the rule set in {@link GraphSearch.coverRules}/{@link formRules}/{@link
86
- * outRules}/{@link fuse}; kept here beside {@link readCover} so the two readers
87
- * of a derivation sit together. */
88
- function classifyMove(premises, conclusion, articulating) {
89
- if (premises.length === 1) {
90
- const [p] = premises;
91
- if (p.kind === "form" && conclusion.kind === "form") {
92
- // form→form is an edge step; CONCEPT cost marks the synonym hop, but the
93
- // cost is on the rule, not the item — the caller passes it to refine this.
94
- return "follow-edge";
95
- }
96
- if (p.kind === "form" && conclusion.kind === "out") {
97
- // form→out: grounding to a terminal answer, or — under a substitution map —
98
- // the form emitting the asker's own wording (articulation's `voice`).
99
- if (!conclusion.rec)
100
- return "step";
101
- return articulating ? "voice" : "ground";
102
- }
103
- if (p.kind === "out" && conclusion.kind === "out")
104
- return "split";
105
- return "step";
106
- }
107
- if (premises.length === 2) {
108
- const [a, b] = premises;
109
- if (a.kind === "cover" || b.kind === "cover")
110
- return "bridge";
111
- if (conclusion.kind === "form")
112
- return "recompose";
113
- if (conclusion.kind === "out") {
114
- // A connector splice concatenates THREE pieces (l + link + r); a plain
115
- // fuse concatenates two (concat2). Distinguish by BYTE width: a splice's
116
- // conclusion is wider than its two premises summed (the link sits between
117
- // them), whereas a fuse's conclusion is exactly their sum. Position can't
118
- // tell them apart — in "icefire" the two rewrites are positionally
119
- // adjacent yet a connector is still spliced into the bytes.
120
- if (a.kind === "out" && b.kind === "out") {
121
- const summed = (a.bytes?.length ?? 0) + (b.bytes?.length ?? 0);
122
- return (conclusion.bytes?.length ?? 0) > summed
123
- ? "splice-connector"
124
- : "fuse";
125
- }
126
- return "fuse";
127
- }
128
- }
129
- return "step";
130
- }
131
- /** Walk a finished derivation into its rule applications, in post-order
132
- * (premises before the conclusion they feed), deduplicating shared
133
- * sub-derivations so a node reached by two rules is reported once. This is the
134
- * whole proof tree — every STEP, CONCEPT, fuse and bridge the lightest cover
135
- * was built from — the finest granularity a rationale can reach. */
136
- function readDerivation(root, articulating = false) {
137
- const steps = [];
138
- // The `order` assigned to each already-emitted derivation node, so a later
139
- // step that consumes it as a premise can name it as a producer — the proof
140
- // tree's data-flow edge, preserved exactly. Only RULE-bearing nodes (the
141
- // emitted steps) get an entry; an axiom premise (a seed leaf/form/computed
142
- // result) has no producer and contributes no edge. `order` therefore stays
143
- // contiguous with `steps`, so producers index directly into the emitted list.
144
- const orderOf = new Map();
145
- let order = 0;
146
- const walk = (d) => {
147
- if (orderOf.has(d))
148
- return; // a shared sub-derivation, already emitted
149
- for (const p of d.premises)
150
- walk(p);
151
- if (!d.rule)
152
- return; // an axiom: a seed, reported as its consumer's premise
153
- const move = classifyMove(d.rule.premises, d.rule.conclusion, articulating);
154
- // Refine an edge step to a concept hop by its cost (CONCEPT > STEP).
155
- const refined = move === "follow-edge" && d.rule.cost >= CONCEPT ? "concept-hop" : move;
156
- const producers = [];
157
- for (const p of d.premises) {
158
- const o = orderOf.get(p); // defined iff p was itself an emitted rule step
159
- if (o !== undefined)
160
- producers.push(o);
161
- }
162
- const myOrder = order++;
163
- orderOf.set(d, myOrder);
164
- steps.push({
165
- order: myOrder,
166
- move: refined,
167
- premises: d.premises.map((p) => derivationItem(p.item)),
168
- conclusion: derivationItem(d.item),
169
- cost: d.rule.cost,
170
- producers,
171
- });
172
- };
173
- walk(root);
174
- return steps;
175
- }
176
- /** The lightest-derivation search over the Sema graph. One instance binds the
177
- * store, `maxGroup` (the fusible span ceiling), and the canonical
178
- * {@link resolve} callback; {@link cover} then solves one query. */
179
- export class GraphSearch {
180
- store;
181
- maxGroup;
182
- host;
183
- constructor(store, maxGroup,
184
- /** The host whose capabilities the search consults: resolve (canonical node
185
- * id of a byte span), recogniseSpan (content-addressed graph lookup for
186
- * recursive completion), and chooseNext (distributional-evidence edge
187
- * disambiguation when a recognised form has multiple continuations). */
188
- host) {
189
- this.store = store;
190
- this.maxGroup = maxGroup;
191
- this.host = host;
192
- }
193
- /** Explore the Sema graph for the lightest cover of the query and return its
194
- * chosen spans left-to-right — WITH the derivation's total weight (the g
195
- * value of the goal item, in the exported cost ladder), which think's
196
- * grounding decider compares against the other mechanisms' candidates —
197
- * or null if the query cannot be covered.
198
- *
199
- * The search runs on the query's tree leaves, not flat bytes — leaf-level
200
- * cover axioms, recognised forms as graph entry points — and discovers
201
- * cross-leaf forms by fusing adjacent fragments on demand (the only
202
- * byte-processing it does, and only where a derivation needs it). When
203
- * `substitutions` is given, recognised forms emit substitute bytes directly
204
- * (cost 0) — articulation splicing the asker's wording in.
205
- *
206
- * Any learnt connector between two rewrites is spliced IN by the in-search
207
- * connector rule (see {@link outRules}), so the returned spans already carry
208
- * it — there is no post-pass. */
209
- cover(queryLen, sites, conceptTarget, leaves, splits, starts, substitutions, connectors, computedResults,
210
- /** When given, receives the lightest derivation's rule applications — the
211
- * full adapted A*LD proof tree as classified {@link DerivationStep}s — for the TOP
212
- * cover only (a recursive recompletion solves its own sub-cover and is not
213
- * reported here, to keep the trace one layer per think). Off by default,
214
- * so the search pays nothing when no one inspects. */
215
- onDerivation) {
216
- // Top-level entry: reset the per-call recursion state, then run the one
217
- // {@link solve} routine that both the query and any produced composite go
218
- // through (completion is cover, recursively — see {@link recompleteNode}).
219
- this.recompleteOpen.clear();
220
- this.recompleteMemo = new Map();
221
- return this.solve(queryLen, {
222
- sites,
223
- leaves,
224
- splits,
225
- starts,
226
- }, conceptTarget, substitutions, connectors, computedResults, onDerivation);
227
- }
228
- /** Build the deduction system for one span and return its lightest cover's
229
- * chosen spans — the SINGLE routine the query and every produced composite
230
- * run through. `recognition` carries the span's recognised forms; the query
231
- * brings its own (with pre-resolved concepts/connectors), a recursive
232
- * completion re-recognises the produced bytes (edge/fuse only).
233
- *
234
- * No depth limit governs nesting — convergence is INTRINSIC, exactly as in the
235
- * adapted A*LD chart and {@link completeForward}: a completion only recurses into a
236
- * node it has not already entered ({@link recompleteNode}'s cycle guard), and
237
- * the node ids are finite, so the recursion must terminate on its own. A
238
- * decomposition may therefore run as deep as the graph licenses — three
239
- * decomposes, two recomposes, any mix — and stops only when it reaches a node
240
- * that leads nowhere new, never at an arbitrary count. */
241
- solve(spanLen, recognition, conceptTarget, substitutions, connectors, computedResults, onDerivation) {
242
- const system = this.buildSearch(spanLen, recognition.sites, conceptTarget, recognition.leaves, recognition.splits, recognition.starts, substitutions, connectors, computedResults);
243
- const derivation = lightestDerivation(system);
244
- // When covering under a substitution map (articulation), a form→out rule is
245
- // the form EMITTING the asker's voice, not grounding to its own answer — so
246
- // tell the reader to name those moves `voice` rather than `ground`.
247
- if (derivation && onDerivation) {
248
- onDerivation(readDerivation(derivation, substitutions !== undefined));
249
- }
250
- return derivation
251
- ? { segs: readCover(derivation), cost: derivation.cost }
252
- : null;
253
- }
254
- /** The weighted deduction system the graph exploration solves (the four
255
- * reductions of adapted A*LD live in {@link lightestDerivation}; this only states the
256
- * items, axioms, goal, and rules — see {@link GItem} for the item kinds).
257
- *
258
- * Forms that span across leaves are discovered BY the rules, which fuse
259
- * adjacent fragments toward a known leaf (findLeaf) or branch (findBranch);
260
- * a completion fused with its neighbour may spell a deeper learned form the
261
- * flat probes can't name, recovered canonically by {@link resolve}. */
262
- buildSearch(queryLen, sites, conceptTarget, leaves, splits, starts, substitutions, connectors, computedResults) {
263
- const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
264
- // Same corpus-scale hub floor {@link atomIsHub}/{@link atomReach} (traverse.ts)
265
- // derive for byte atoms — duplicated here rather than imported because this
266
- // module is deliberately host-based (no MindContext), and both inputs
267
- // (maxGroup, edgeSourceCount) are already in scope with no ctx needed. If
268
- // the formula ever changes, it must change in BOTH places (see canonical.ts's
269
- // header for the same write/read-side duplication convention).
270
- const atomsAreHubs = Math.max(1, Math.ceil((this.store.edgeSourceCount() * W) / 256)) > Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
271
- const nodeBytes = (n) => this.store.bytesPrefix(n, ALL);
272
- // Content-addressed probes over the store's hash-cons maps — the same keys
273
- // training filled. No byte-by-byte trie walk.
274
- const findLeafU = (b) => this.store.findLeaf(b) ?? undefined;
275
- const findBranchU = (k) => this.store.findBranch(k) ?? undefined;
276
- // Finalised `out` items, indexed for the binary (bridge, fuse) rules.
277
- const coversDone = new Set();
278
- const outsByStart = new Map();
279
- const outsByEnd = new Map();
280
- const outsByNode = new Map();
281
- const coverableByStart = new Map();
282
- // Index the connectors by their left and right answer-node, so the connector
283
- // rule iterates only this out's FEW resolved partners (selective, and for the
284
- // N-ary case O(parts) keys) instead of scanning every position pair — what
285
- // keeps the in-search bridge bounded when many parts are recognised at once.
286
- const linksByLeft = new Map();
287
- const linksByRight = new Map();
288
- if (connectors) {
289
- for (const [key, bytes] of connectors) {
290
- const comma = key.indexOf(",");
291
- const l = Number(key.slice(0, comma));
292
- const r = Number(key.slice(comma + 1));
293
- pushInto(linksByLeft, l, [r, bytes]);
294
- pushInto(linksByRight, r, [l, bytes]);
295
- }
296
- }
297
- return {
298
- key(it) {
299
- if (it.kind === "cover")
300
- return "c" + it.p;
301
- if (it.kind === "form") {
302
- return `f${it.i}.${it.j}.${it.node}.${it.via ? 1 : 0}.${it.rcmp ? 1 : 0}`;
303
- }
304
- return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
305
- },
306
- *axioms() {
307
- yield { item: { kind: "cover", p: 0 }, cost: 0 };
308
- // One out per tree leaf — content-defined chunks, far fewer than bytes.
309
- // Each carries the node id it resolves to (when known) so it can compose
310
- // toward a known branch by findBranch.
311
- for (const lf of leaves) {
312
- yield {
313
- item: {
314
- kind: "out",
315
- i: lf.start,
316
- j: lf.end,
317
- bytes: lf.bytes,
318
- cover: true,
319
- rec: false,
320
- node: lf.node ?? undefined,
321
- },
322
- cost: 0,
323
- };
324
- }
325
- for (const s of sites) {
326
- yield {
327
- item: {
328
- kind: "form",
329
- i: s.start,
330
- j: s.end,
331
- node: s.payload,
332
- via: false,
333
- },
334
- cost: 0,
335
- };
336
- }
337
- // Computed (extension) results — see {@link ComputedResult}. Each enters as a
338
- // RECOGNISED covering completion, exactly like a learned terminal answer:
339
- // its bytes are the computed result, it bridges the cover at MICRO (rec),
340
- // and it carries the result's canonical node (when the store holds it) so
341
- // it can fuse as an operand of an outer form. The STEP base cost marks a
342
- // computation as a unit of work — a derived fact, on par with following a
343
- // learned edge (STEP), so it decisively beats leaving the span
344
- // unrecognised (PASS) but never masquerades as a free perceived leaf.
345
- // Because this cost EQUALS a learned edge's, the search would tie a
346
- // computation against a colliding recall; the computation-always-wins policy lives
347
- // in the caller (src/mind/pipeline.ts think), which masks any recognised site a result
348
- // overlaps so the computation is the cover's sole completion there — the
349
- // search stays a neutral cost engine with no computation-vs-recall precedence baked
350
- // in. When `computedResults` is empty (every non-arithmetic query) this loop
351
- // emits nothing, so the search is byte-identical to one with no extension at all.
352
- for (const u of computedResults ?? []) {
353
- yield {
354
- item: {
355
- kind: "out",
356
- i: u.i,
357
- j: u.j,
358
- bytes: u.bytes,
359
- cover: true,
360
- rec: true,
361
- node: u.node,
362
- computed: true,
363
- },
364
- cost: STEP,
365
- };
366
- }
367
- },
368
- isGoal: (it) => it.kind === "cover" && it.p === queryLen,
369
- // Admissible, consistent lower bound on the cost remaining to the goal
370
- // cover(queryLen) — this is what makes the adapted A*LD search OUTPUT-SENSITIVE
371
- // (work proportional to the answer, not to how densely the corpus enriched
372
- // the query's sub-forms). Without it the engine runs as uninformed
373
- // Dijkstra and pops every zero-cost fuse before the goal; with it the
374
- // agenda is ordered g + h, so the frontier is driven FORWARD toward full
375
- // coverage instead of wallowing in low-position fragment fusions.
376
- //
377
- // The bound: whatever an item is, reaching the goal still requires the
378
- // cover frontier to advance to queryLen, and the CHEAPEST any single
379
- // covered position can be is the recognised-completion bridge (ε = MICRO,
380
- // the minimum local cost in the ladder — every other move costs ≥ STEP).
381
- // So the remaining query past the item's right edge is a guaranteed
382
- // ≥ ε-per-byte cost. cover(p) still owes [p,queryLen); a form/out at
383
- // [i,j) can contribute coverage no further than j, so it still owes
384
- // [j,queryLen). Using ε (≤ every real per-byte cost, incl. PASS) keeps it
385
- // a true lower bound; counting only the suffix past the right edge keeps it
386
- // consistent (each forward rule pays ≥ ε per byte it advances the edge).
387
- heuristic: (it) => {
388
- const right = it.kind === "cover" ? it.p : it.j;
389
- return (queryLen - right) * MICRO;
390
- },
391
- rules: (it) => {
392
- if (it.kind === "cover") {
393
- return this.coverRules(it, coversDone, coverableByStart);
394
- }
395
- if (it.kind === "form") {
396
- return this.formRules(it, conceptTarget, substitutions, nodeBytes);
397
- }
398
- return this.outRules(it, {
399
- W,
400
- splits,
401
- starts,
402
- atomsAreHubs,
403
- coversDone,
404
- outsByStart,
405
- outsByEnd,
406
- outsByNode,
407
- coverableByStart,
408
- findLeafU,
409
- findBranchU,
410
- linksByLeft,
411
- linksByRight,
412
- });
413
- },
414
- };
415
- }
416
- /** cover(p): the BRIDGE rule — extend the cover across any coverable out that
417
- * begins at p, stepping the frontier from p to that out's end.
418
- *
419
- * This is what links the rewritten parts of a multi-form answer. An out is
420
- * either a recognised completion (`rec`, free) or a literal span the query
421
- * carried between known forms — the connective: a space, comma, period,
422
- * newline, or any run of bytes that was never recognised. Bridging a literal
423
- * costs PASS per byte (so the search still prefers to recognise), but it is
424
- * the cheapest — indeed only — way to cross a gap that has no learned form,
425
- * and it KEEPS that connective in the cover chain, so the asker's own linking
426
- * material reappears when it still coheres ("ice, fire" → "cold, hot", not
427
- * "coldhot"). A recognised completion bridges for a tiny ε (1e-3, far below
428
- * STEP), so a single connected span beats two separate ones on coherence
429
- * without disturbing the cost ladder's ordering.
430
- *
431
- * Marks p finalised so the symmetric out-side bridge ({@link outRules}) can
432
- * fire for outs that arrive after their start position is covered. */
433
- *coverRules(it, coversDone, coverableByStart) {
434
- coversDone.add(it.p);
435
- for (const o of coverableByStart.get(it.p) ?? []) {
436
- yield this.bridgeRule(it, o);
437
- }
438
- }
439
- /** The BRIDGE rule, built once for its two arrival orders (cover-first in
440
- * {@link coverRules}, out-first in {@link outRules}): ε for a recognised
441
- * completion; PASS per byte for a literal connective — kept in the cover
442
- * so the asker's own connector survives where it fits. ONE definition of
443
- * the cost expression, so the ladder's application cannot drift between
444
- * the two sides. */
445
- bridgeRule(cover, o) {
446
- return {
447
- premises: [cover, o],
448
- conclusion: { kind: "cover", p: o.j },
449
- cost: o.rec ? MICRO : PASS * (o.j - o.i),
450
- };
451
- }
452
- /** The connector-SPLICE rule for an oriented (l, r) pair, or null when the
453
- * pair does not qualify — the ONE body behind {@link outRules}' two
454
- * mirror loops (this-as-left over resolved right partners, this-as-right
455
- * over resolved left partners). Fires only when both sides are
456
- * recognised, r starts at or after l ends, and the gap between them is
457
- * empty or wholly recognised — never across the asker's own literal
458
- * separator. */
459
- trySplice(l, r, link, outsByEnd) {
460
- if (!l.rec || !r.rec || r.i < l.j)
461
- return null;
462
- if (!this.gapRecognised(l.j, r.i, outsByEnd))
463
- return null;
464
- return {
465
- premises: [l, r],
466
- conclusion: {
467
- kind: "out",
468
- i: l.i,
469
- j: r.j,
470
- bytes: concatBytes([l.bytes, link, r.bytes]),
471
- cover: true,
472
- rec: true,
473
- node: r.node,
474
- },
475
- cost: 0,
476
- };
477
- }
478
- /** form(i,j,node,via): follow the graph out of `node`, or (in articulation)
479
- * emit its substitute voice directly. */
480
- *formRules(it, conceptTarget, substitutions, nodeBytes) {
481
- // Articulation: emit voice bytes at the recognised span; the hop/concept/
482
- // emit chain is suppressed — the form contributes only its substitute.
483
- if (substitutions) {
484
- const voice = substitutions.get(it.node);
485
- if (voice !== undefined) {
486
- yield {
487
- premises: [it],
488
- conclusion: {
489
- kind: "out",
490
- i: it.i,
491
- j: it.j,
492
- bytes: voice,
493
- cover: true,
494
- rec: true,
495
- node: it.node,
496
- },
497
- cost: 0,
498
- };
499
- }
500
- return;
501
- }
502
- if (it.via) {
503
- // A CHAIN hop (this form was itself reached by following an edge):
504
- // read up to the hub bound — the SAME √(max(2,N)) convention {@link
505
- // hubBound} in traverse.ts derives, replicated here because
506
- // GraphSearch holds only a bare Store, not a MindContext — and fork
507
- // across EVERY continuation. A bidirectional pair (SmolSent trains
508
- // both directions) puts the back-edge to where we came from ahead of
509
- // the forward edge in insertion order; a single first-inserted pick
510
- // would walk the search straight into a cycle (the A*LD duplicate-key
511
- // guard then dead-ends it) with no way to reach the forward edge.
512
- // Forking offers every continuation as its own rule so the one that
513
- // genuinely advances (not a duplicate) is still reachable.
514
- const bound = Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
515
- const nx = this.store.nextFirst(it.node, bound);
516
- if (nx.length) {
517
- // The SAME evidence-weighted disambiguation the first hop uses
518
- // (below) identifies the most-corroborated continuation. Yielding
519
- // it FIRST matters: {@link lightestDerivation}'s chart keeps the
520
- // FIRST rule to reach a given cost for an item and discards later
521
- // arrivals at an EQUAL cost (`cost < current`, strictly) — so
522
- // among same-depth sibling forks that tie in cost, the
523
- // evidence-backed edge wins deterministically, never by
524
- // exploration-order luck. `preferred`, when set, is necessarily
525
- // an element of `nx` (chooseNext reads the identical hub-bounded
526
- // set — see traverse.ts), so a plain skip-in-place suffices; no
527
- // second array need be allocated to reorder it to the front.
528
- const preferred = nx.length > 1
529
- ? this.host.chooseNext?.(it.node)
530
- : undefined;
531
- const fork = (node) => ({
532
- premises: [it],
533
- conclusion: { kind: "form", i: it.i, j: it.j, node, via: true },
534
- // A real edge always costs STEP, whether it is the first hop or
535
- // the fifth — so a chain's total cost is proportional to its
536
- // length and the lightest derivation is always the SHORTEST
537
- // successful one. (Charging every hop after the first for
538
- // FREE, as an earlier design did, made every stopping point at
539
- // any depth tie at the same cost — indistinguishable to the
540
- // search, decided by whichever arrived first.)
541
- cost: STEP,
542
- });
543
- if (preferred !== undefined)
544
- yield fork(preferred);
545
- for (const c of nx) {
546
- if (c !== preferred)
547
- yield fork(c);
548
- }
549
- // Also offer stopping HERE, at CONCEPT above whatever the chain
550
- // has already cost — the same "a synonym jump is dearer than a
551
- // direct edge" ordering the ladder already uses elsewhere,
552
- // repurposed as "giving up early is dearer than one more real
553
- // hop". A premature stop at depth D costs D·STEP + CONCEPT, so a
554
- // SHORTER premature stop always beats a longer one, and a genuine
555
- // fixpoint (below, costing +0) always beats any premature stop at
556
- // the same depth — the search only settles here when continuing
557
- // genuinely dead-ends (every fork above revisits an already-
558
- // explored key) or grows costlier than giving up.
559
- //
560
- // The stop-here bytes are the node's OWN, as-is — NOT recompleteNode's
561
- // recursive re-cover. recompleteNode re-runs the full recognition
562
- // + edge/fuse search over the node's bytes, an O(node's own
563
- // substructure) cost independent of chain depth; offering it at
564
- // EVERY premature stop makes a query's total cost scale with how
565
- // many nodes the corpus happens to interconnect here (corpus
566
- // density), not with the answer's own hop count — the exact
567
- // output-sensitivity the rest of this search is built to keep.
568
- // Reserved below for the ONE place it is load-bearing: the true
569
- // fixpoint, checked once, at the chain's actual end.
570
- yield {
571
- premises: [it],
572
- conclusion: {
573
- kind: "out",
574
- i: it.i,
575
- j: it.j,
576
- bytes: nodeBytes(it.node),
577
- cover: true,
578
- rec: true,
579
- node: it.node,
580
- },
581
- cost: CONCEPT,
582
- };
583
- return;
584
- }
585
- // The chain reached a node with no WHOLE-node continuation anywhere
586
- // — a genuine fixpoint, not a premature stop. Before emitting it as
587
- // terminal, CONTINUE THE EXPLORATION into its own structure: a
588
- // composite answer like "p1 p2" leads nowhere as a whole, yet
589
- // recognising it surfaces p1, p2 — each of which continues (→ R1,
590
- // R2) and recomposes into a deeper learnt form (→ FINAL). {@link
591
- // recompleteNode} re-covers the node's bytes through the SAME
592
- // recognition + edge/fuse machinery the top query uses (a continued
593
- // graph exploration, not a re-perception), and returns the deeper
594
- // completion's bytes when it genuinely leads somewhere new. Emit
595
- // that; else emit the node itself as the terminal answer. This is
596
- // the ONE place the recursive re-cover runs — once, at the chain's
597
- // actual end, never per intermediate stop — so its cost tracks the
598
- // ANSWER's own structure, not how densely the corpus interconnects
599
- // the nodes passed through on the way there.
600
- const deeper = this.recompleteNode(it.node);
601
- yield {
602
- premises: [it],
603
- conclusion: {
604
- kind: "out",
605
- i: it.i,
606
- j: it.j,
607
- bytes: deeper ?? nodeBytes(it.node),
608
- cover: true,
609
- rec: true,
610
- node: it.node,
611
- },
612
- cost: 0,
613
- };
614
- }
615
- else {
616
- // The FIRST hop out of a recognised site — unchanged from the
617
- // original design. LIMIT 2 only senses PLURALITY (does this node
618
- // have more than one learnt continuation?); the actual bounded read
619
- // and evidence-weighted pick both live inside {@link
620
- // Mind.chooseNext} (see traverse.ts), which floors its own hub bound
621
- // at √(max(2,N)) regardless of what we read here.
622
- const nx = this.store.nextFirst(it.node, 2);
623
- if (nx.length) {
624
- yield {
625
- premises: [it],
626
- conclusion: {
627
- kind: "form",
628
- i: it.i,
629
- j: it.j,
630
- node: (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
631
- nx[0],
632
- via: true,
633
- rcmp: it.rcmp,
634
- },
635
- // A recomposed form's continuation is FREE: the two (or more) parts
636
- // were already paid for as their own rewrites, and the single
637
- // consolidated span saves one cover-bridge versus leaving them
638
- // split — so charging 0 here makes the grounded whole (e.g.
639
- // "DE"→F) strictly beat the split ("D","E") by exactly that saved
640
- // bridge, deterministically.
641
- cost: it.rcmp ? 0 : STEP,
642
- };
643
- return;
644
- }
645
- // Recognised but edge-less: borrow a concept (halo) sibling's edge. No
646
- // edge and no concept means the form leads nowhere — it yields no rule, so
647
- // a query of only such forms produces no derivation, and think is silent.
648
- const target = conceptTarget.get(it.node);
649
- if (target !== undefined) {
650
- yield {
651
- premises: [it],
652
- conclusion: {
653
- kind: "form",
654
- i: it.i,
655
- j: it.j,
656
- node: target,
657
- via: true,
658
- },
659
- cost: CONCEPT,
660
- };
661
- }
662
- }
663
- }
664
- /** Complete a node that an edge produced but that bears no further whole-edge,
665
- * by COVERING ITS OWN BYTES — the very same operation the top query runs.
666
- * Completion is not a special pass: a produced composite ("p1 p2") is just
667
- * another span to cover, and covering it re-applies recognition, edge-follow,
668
- * fusion and recomposition to discover that its parts continue (p1→R1, p2→R2)
669
- * and recompose into a deeper learnt form (→ FINAL). This is why a single
670
- * edge-target needs no bespoke logic — it routes back through {@link solve}.
671
- *
672
- * Re-recognition (not the node's tree children) is what surfaces the learnt
673
- * parts: content-defined chunking may cut "p1 p2" as "p1 p"|"2", so only
674
- * recognising the bytes recovers p1 and p2 as the forms the graph knows.
675
- *
676
- * The recovered answer is accepted only when it MOVED and names a LEARNT node
677
- * ({@link resolve}) — the graph itself gates against re-expanding a contained
678
- * form ("ice is cold" ⊅→ "ice is cold is cold").
679
- *
680
- * Termination is INTRINSIC, not a depth limit: a node already on the
681
- * completion stack ({@link recompleteOpen}) is not re-entered — a self-
682
- * referential recomposition is a cycle that can yield nothing new, so it
683
- * stops there, exactly as {@link completeForward} stops on a revisited edge.
684
- * Distinct node ids are finite and each finished completion is memoised, so a
685
- * legitimate chain runs as deep as the graph licenses and no further. */
686
- recompleteNode(node) {
687
- if (!this.host.recogniseSpan)
688
- return null;
689
- const memo = this.recompleteMemo;
690
- if (memo.has(node))
691
- return memo.get(node) ?? null;
692
- // Cycle guard: a node being completed must not recurse back into itself.
693
- if (this.recompleteOpen.has(node))
694
- return null;
695
- // A leaf or single-child node has no parts to recompose; skip before the
696
- // costly recognition so a plain terminal answer pays nothing.
697
- const nrec = this.store.get(node);
698
- if (!nrec || nrec.kids === null || nrec.kids.length < 2) {
699
- memo.set(node, null);
700
- return null;
701
- }
702
- const bytes = this.store.bytesPrefix(node, ALL);
703
- this.recompleteOpen.add(node);
704
- try {
705
- // Completion is cover: re-cover the produced bytes through the SAME solve
706
- // routine, recognising them afresh. No concepts/connectors (those need the
707
- // caller's async pre-resolution) — the recursion explores edges and fusion,
708
- // which is what a deeper rewrite chain is made of.
709
- const solved = this.solve(bytes.length, this.host.recogniseSpan(bytes), new Map());
710
- const answer = solved && concatBytes(solved.segs.map((s) => s.bytes));
711
- const out = (answer !== null && !bytesEqual(answer, bytes) &&
712
- this.host.resolve(answer) !== null)
713
- ? answer
714
- : null;
715
- memo.set(node, out);
716
- return out;
717
- }
718
- finally {
719
- this.recompleteOpen.delete(node);
720
- }
721
- }
722
- /** Per-cover memo of each produced node's completion (so the many terminal
723
- * outs of a long query re-cover each distinct node at most once); reset at the
724
- * top of {@link cover}. */
725
- recompleteMemo = new Map();
726
- /** The nodes currently being re-completed — the recursion stack. A node in
727
- * this set is not re-entered, so a cyclic recomposition terminates naturally
728
- * (the same cycle guard {@link completeForward} uses), with no depth cap. */
729
- recompleteOpen = new Set();
730
- /** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
731
- * learnt connector (the in-search bridge), splitting (at a sub-leaf form
732
- * boundary), bridging (cover(i) ∧ this → cover(j)), and fusing with an
733
- * adjacent finalised out. */
734
- *outRules(it, ctx) {
735
- const { splits, coversDone, outsByStart, outsByEnd, coverableByStart } = ctx;
736
- const outsByNode = ctx.outsByNode;
737
- const byRight = ctx.linksByRight ?? new Map();
738
- pushInto(outsByStart, it.i, it);
739
- pushInto(outsByEnd, it.j, it);
740
- if (it.rec && it.node !== undefined)
741
- pushInto(outsByNode, it.node, it);
742
- if (it.cover)
743
- pushInto(coverableByStart, it.i, it);
744
- // ── connector rule (the BRIDGE, in-search) ──────────────────────────
745
- // A connector keyed `L,R` carries everything a learnt whole holds BETWEEN
746
- // answer L and answer R — for an N-ary whole, that includes the interior
747
- // answers (see {@link Mind.resolveGroupConnectors}). Splicing it joins L and
748
- // R into one recognised span L+connector+R, priced inside the search. The
749
- // rule fires for ADJACENT parts (R begins where L ends) AND across a gap that
750
- // is ITSELF wholly recognised (an interior answer absorbed into the whole,
751
- // Points 2 & 5) — but NEVER across the asker's own unrecognised separator (a
752
- // space, comma), so "ice fire" stays "cold hot", never "cold or hot".
753
- //
754
- // Cost stays bounded by iterating only the FEW resolved connector targets of
755
- // this out's node (links are selective and, for the N-ary case, keyed first→
756
- // later — O(parts), not O(parts²)), and matching them against finalised outs
757
- // by node id, rather than scanning every position pair.
758
- const byLeft = ctx.linksByLeft;
759
- if (byLeft && it.rec && it.node !== undefined) {
760
- // L = this out, R = a later out whose node is a resolved target.
761
- for (const [rNode, link] of byLeft.get(it.node) ?? []) {
762
- for (const r of outsByNode.get(rNode) ?? []) {
763
- const rule = this.trySplice(it, r, link, outsByEnd);
764
- if (rule)
765
- yield rule;
766
- }
767
- }
768
- // R = this out, L = an earlier out whose node has a resolved target here.
769
- for (const [lNode, link] of byRight.get(it.node) ?? []) {
770
- for (const l of outsByNode.get(lNode) ?? []) {
771
- const rule = this.trySplice(l, it, link, outsByEnd);
772
- if (rule)
773
- yield rule;
774
- }
775
- }
776
- }
777
- // Split an unrecognised out at a sub-leaf form boundary — demand-driven,
778
- // only when a split point sits inside this out's range. Each half carries
779
- // its own resolved node id so it can still anchor a fusion.
780
- if (it.cover && !it.rec && it.j - it.i > 1) {
781
- for (const k of splits) {
782
- if (k > it.i && k < it.j) {
783
- const lb = it.bytes.subarray(0, k - it.i);
784
- const rb = it.bytes.subarray(k - it.i);
785
- yield {
786
- premises: [it],
787
- conclusion: {
788
- kind: "out",
789
- i: it.i,
790
- j: k,
791
- bytes: lb,
792
- cover: true,
793
- rec: false,
794
- node: ctx.findLeafU(lb),
795
- },
796
- cost: 0,
797
- };
798
- yield {
799
- premises: [it],
800
- conclusion: {
801
- kind: "out",
802
- i: k,
803
- j: it.j,
804
- bytes: rb,
805
- cover: true,
806
- rec: false,
807
- node: ctx.findLeafU(rb),
808
- },
809
- cost: 0,
810
- };
811
- }
812
- }
813
- }
814
- // The BRIDGE, fired from the out side: cover(i) ∧ this → cover(j). Same
815
- // rule as coverRules, the other order of arrival — an out finalised after
816
- // its start was already covered. Either way the connective (this out, when
817
- // unrecognised) is carried into the cover chain so the asker's own link
818
- // survives between the rewritten parts.
819
- if (it.cover && coversDone.has(it.i)) {
820
- yield this.bridgeRule({ kind: "cover", p: it.i }, it);
821
- }
822
- for (const r of outsByStart.get(it.j) ?? [])
823
- yield* this.fuse(it, r, ctx);
824
- for (const l of outsByEnd.get(it.i) ?? [])
825
- yield* this.fuse(l, it, ctx);
826
- }
827
- /** Whether the query span [from, to) is wholly covered by RECOGNISED outs —
828
- * the test that lets a connector jump across INTERIOR answers (an N-ary whole)
829
- * but never across the asker's own unrecognised framing (a space or comma the
830
- * asker wrote between parts). Empty span (from === to, the adjacent case) is
831
- * trivially recognised. Otherwise step right-to-left: from `to`, find a
832
- * recognised out ending there and continue from its start, until reaching
833
- * `from`. Greedy-longest is sufficient here — the spans in play are the few
834
- * recognised answers of one query, not a general interval cover. */
835
- gapRecognised(from, to, outsByEnd) {
836
- let pos = to;
837
- while (pos > from) {
838
- let stepped = -1;
839
- for (const o of outsByEnd.get(pos) ?? []) {
840
- if (o.rec && o.i >= from && o.i < pos) {
841
- stepped = Math.min(stepped === -1 ? o.i : stepped, o.i);
842
- }
843
- }
844
- if (stepped < 0)
845
- return false; // a position not spanned by a recognised out
846
- pos = stepped;
847
- }
848
- return true;
849
- }
850
- /** Fuse two adjacent finalised outs — the search's own discovery of forms
851
- * that cross leaf boundaries. The concatenation may be a known leaf
852
- * (findLeaf, when short enough), or — when both sides resolved — their pair a
853
- * known branch (findBranch); a completion fused with its neighbour may spell
854
- * a deeper learned form, recovered canonically by {@link resolve} (gated on a
855
- * completion being present, so it only runs along chains). The fused span
856
- * lives on as an intermediate out while it could still grow into a form, and
857
- * enters the graph as a form the moment it names a node. */
858
- *fuse(l, r, ctx) {
859
- const bytes = concat2(l.bytes, r.bytes);
860
- // A PURE leaf-leaf fuse (neither side already a recognised completion)
861
- // is opportunistic cross-leaf recovery exactly like recognition.ts's own
862
- // canonical chain — findLeaf/findBranch here have no idea WHY these two
863
- // leaves are adjacent, only that their concatenation happens to spell a
864
- // trained form ("hi" recovered from "W[hi]ch"). The same corpus-scale
865
- // caution recognition.ts's `boundary` gate applies: trust it fully when
866
- // `l.i` is a position the query's OWN fold chose as a boundary (real
867
- // structural evidence); past the scale where atoms themselves stop
868
- // discriminating, an interior offset's opportunistic match is noise, not
869
- // a genuine cross-leaf recovery. A completion-involved fuse (l.rec ||
870
- // r.rec) is a different, legitimate case — a rewrite growing into its
871
- // neighbour — and is exempt, same as recognition.ts's rec-derived sites.
872
- const trusted = l.rec || r.rec || ctx.starts.has(l.i) ||
873
- !ctx.atomsAreHubs;
874
- let node = (trusted && bytes.length <= ctx.W)
875
- ? ctx.findLeafU(bytes)
876
- : undefined;
877
- // Whether this pair ACTUALLY forms a 2-child branch — the hard evidence
878
- // that the fused bytes are a learned form worth keeping alive. Derived
879
- // from the same findBranchU probe that sets `node`; when false, the pair
880
- // is structurally unrecognised and an intermediate span that carries no
881
- // node cannot contribute to any further fusion (findBranch needs two
882
- // nodes, resolve needs a completion, and findLeaf already had its chance).
883
- let pairFormsBranch = false;
884
- if (trusted && node === undefined && l.node !== undefined &&
885
- r.node !== undefined) {
886
- node = ctx.findBranchU([l.node, r.node]);
887
- pairFormsBranch = node !== undefined;
888
- }
889
- if (node === undefined && (l.rec || r.rec)) {
890
- // Canonical recovery of a deeper learned form fused from a completion and
891
- // its neighbour.
892
- const id = this.host.resolve(bytes);
893
- if (id !== null)
894
- node = id;
895
- }
896
- // A completed rewrite (rec) must not be absorbed into an unrelated INTERIOR
897
- // chunk of a one-shot phrase: that lets the chunk's continuation swallow the
898
- // inter-part gap and corrupt the answer ("cold"+" " → "cold " ⊂ "cold or
899
- // hot"; "Y"+" " → "Y " ⊂ "X then Y then Z"). A node learnt as a meaningful
900
- // unit bears a halo (it took part in an episode); a bare phrase-interior
901
- // chunk does not. So when a completion fuses into a node, require that node
902
- // to be halo-bearing — a real fused form (a learnt fact context like "4+3")
903
- // carries a halo and still passes.
904
- if (node !== undefined && (l.rec || r.rec) && !this.store.hasHalo(node)) {
905
- node = undefined;
906
- }
907
- // A node-less fused span is kept alive ONLY while it can still grow INTO a
908
- // learned form: it's still ≤ W bytes (so a wider fuse might yet name it via
909
- // findLeaf), or the pair ACTUALLY forms a branch (so the fused bytes are
910
- // a real learned form, even if the halo gate cleared its node above). It is
911
- // NOT kept merely because both sides carry a node — that "potential" gate
912
- // let every pair of adjacent recognised forms produce an intermediate span
913
- // regardless of whether they name a branch together, generating O(N²) chart
914
- // items for N abutted forms where only O(N) pairs actually form branches.
915
- // The earlier O(2ⁿ) gate (kept alive whenever a side was a completion) is
916
- // already superseded by this one — a completion that genuinely deepens names
917
- // a node via findBranch or resolve and is yielded as a form regardless.
918
- const couldGrow = bytes.length <= ctx.W || pairFormsBranch;
919
- if (node === undefined && !couldGrow)
920
- return;
921
- yield {
922
- premises: [l, r],
923
- conclusion: {
924
- kind: "out",
925
- i: l.i,
926
- j: r.j,
927
- bytes,
928
- cover: false,
929
- rec: false,
930
- node,
931
- },
932
- cost: 0,
933
- };
934
- if (node !== undefined) {
935
- // A RECOMPOSITION: two already-rewritten parts (both rec completions)
936
- // fused into a node that itself CONTINUES. Tag the form so its onward
937
- // step is charged at MICRO (see formRules) — the graph learned this whole,
938
- // so following it to its grounded answer must win over leaving the parts
939
- // split. An ordinary cross-leaf fuse (not from rewrites) is not tagged and
940
- // keeps the normal STEP cost.
941
- const recomposed = l.rec && r.rec && this.store.hasNext(node);
942
- yield {
943
- premises: [l, r],
944
- conclusion: {
945
- kind: "form",
946
- i: l.i,
947
- j: r.j,
948
- node,
949
- via: false,
950
- rcmp: recomposed,
951
- },
952
- cost: 0,
953
- };
954
- }
955
- }
956
- }