@hviana/sema 0.1.5 → 0.1.7

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