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