@hviana/sema 0.9.0 → 0.9.1
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 +7 -7
- package/dist/src/alu/src/index.d.ts +1 -1
- package/dist/src/alu/src/index.js +1 -1
- package/dist/src/alu/src/parser.js +2 -6
- package/dist/src/alu/src/resonance.d.ts +13 -0
- package/dist/src/alu/src/resonance.js +41 -0
- package/dist/src/alu/test/alu.test.js +39 -0
- package/dist/src/bytes.d.ts +6 -2
- package/dist/src/bytes.js +10 -4
- package/dist/src/canon.js +44 -0
- package/dist/src/geometry.d.ts +19 -1
- package/dist/src/geometry.js +125 -141
- package/dist/src/meter.d.ts +20 -0
- package/dist/src/meter.js +21 -1
- package/dist/src/mind/articulation.js +14 -1
- package/dist/src/mind/attention.d.ts +12 -0
- package/dist/src/mind/attention.js +44 -16
- package/dist/src/mind/bridge.js +3 -3
- package/dist/src/mind/derivation.d.ts +40 -0
- package/dist/src/mind/derivation.js +34 -0
- package/dist/src/mind/graph-search.d.ts +89 -15
- package/dist/src/mind/graph-search.js +345 -174
- package/dist/src/mind/learning.js +1 -1
- package/dist/src/mind/mechanisms/cover.d.ts +19 -3
- package/dist/src/mind/mechanisms/cover.js +101 -58
- package/dist/src/mind/mechanisms/recall.js +0 -1
- package/dist/src/mind/mind.js +2 -2
- package/dist/src/mind/pipeline.d.ts +5 -1
- package/dist/src/mind/pipeline.js +175 -87
- package/dist/src/mind/primitives.d.ts +25 -5
- package/dist/src/mind/primitives.js +107 -44
- package/dist/src/mind/reasoning.d.ts +18 -4
- package/dist/src/mind/reasoning.js +445 -321
- package/dist/src/mind/recognition.js +29 -13
- package/dist/src/mind/resonance.js +1 -11
- package/dist/src/mind/traverse.d.ts +3 -3
- package/dist/src/mind/traverse.js +3 -3
- package/dist/src/mind/types.d.ts +7 -1
- package/dist/src/store-sqlite.d.ts +25 -0
- package/dist/src/store-sqlite.js +89 -1
- package/dist/src/store.d.ts +48 -4
- package/dist/src/store.js +86 -6
- package/docs/INDEX.md +18 -18
- package/docs/INVARIANTS.md +16 -16
- package/docs/architecture/bounded-reads.md +1 -1
- package/docs/architecture/caches.md +5 -4
- package/docs/architecture/closure.md +45 -5
- package/docs/architecture/cost-model.md +16 -0
- package/docs/architecture/factored-machinery.md +14 -13
- package/docs/architecture/fold-contract.md +51 -1
- package/docs/architecture/mechanism-market.md +21 -0
- package/docs/architecture/memoization.md +3 -3
- package/docs/architecture/meter.md +2 -1
- package/docs/architecture/saturation.md +12 -0
- package/docs/architecture/store.md +25 -2
- package/docs/failures/tempting-but-wrong.md +13 -2
- package/docs/harness/gates.md +12 -10
- package/docs/mechanisms/cover.md +23 -6
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alu/README.md +10 -2
- package/src/alu/src/index.ts +1 -0
- package/src/alu/src/parser.ts +6 -6
- package/src/alu/src/resonance.ts +42 -0
- package/src/alu/test/alu.test.ts +40 -0
- package/src/bytes.ts +13 -3
- package/src/canon.ts +40 -0
- package/src/geometry.ts +183 -154
- package/src/meter.ts +21 -1
- package/src/mind/articulation.ts +14 -2
- package/src/mind/attention.ts +47 -25
- package/src/mind/bridge.ts +3 -3
- package/src/mind/derivation.ts +77 -0
- package/src/mind/graph-search.ts +449 -221
- package/src/mind/learning.ts +1 -7
- package/src/mind/match.ts +1 -2
- package/src/mind/mechanisms/cast.ts +1 -2
- package/src/mind/mechanisms/cover.ts +149 -84
- package/src/mind/mechanisms/extraction.ts +1 -2
- package/src/mind/mechanisms/prefix-completion.ts +1 -1
- package/src/mind/mechanisms/recall.ts +1 -3
- package/src/mind/mechanisms/reference.ts +1 -1
- package/src/mind/mind.ts +5 -30
- package/src/mind/pipeline.ts +206 -102
- package/src/mind/primitives.ts +119 -43
- package/src/mind/reasoning.ts +558 -413
- package/src/mind/recognition.ts +24 -9
- package/src/mind/resonance.ts +2 -16
- package/src/mind/trace.ts +1 -1
- package/src/mind/traverse.ts +3 -3
- package/src/mind/types.ts +9 -11
- package/src/store-sqlite.ts +92 -1
- package/src/store.ts +113 -7
- package/test/105-derive-through-reports-its-refusal.test.mjs +8 -5
- package/test/106-the-join-fires.test.mjs +21 -0
- package/test/111-the-cover-assembly-is-counted.test.mjs +8 -5
- package/test/128-the-leads-somewhere-pair-agrees.test.mjs +18 -12
- package/test/136-the-two-named-limits.test.mjs +3 -2
- package/test/137-the-law-lives-once-and-below.test.mjs +21 -0
- package/test/148-exact-shortcuts-agree.test.mjs +188 -0
- package/test/149-the-closure-engine.test.mjs +138 -0
- package/test/150-the-join-is-output-sensitive.test.mjs +66 -0
- package/test/151-the-cover-pays-for-what-it-reaches.test.mjs +142 -0
- package/test/152-the-read-side-names-as-the-write-side.test.mjs +146 -0
- package/test/153-a-cheaper-bound-is-looked-at-first.test.mjs +155 -0
- package/test/24-generalization.test.mjs +32 -0
- package/test/36-bloom.test.mjs +53 -0
- package/test/37-cluster-dispersion-fusion.test.mjs +75 -0
- package/test/48-recognise-turn-connective.test.mjs +3 -2
- package/test/55-cost-meter.test.mjs +4 -4
- package/test/90-connector-read-cap.test.mjs +7 -7
package/dist/src/geometry.js
CHANGED
|
@@ -173,16 +173,23 @@ export function consensusFloor(N) {
|
|
|
173
173
|
// groups of `maxGroup` adjacent items into one via permute-then-add
|
|
174
174
|
// (positional seat binding), recursing until one root remains.
|
|
175
175
|
//
|
|
176
|
-
// FLAT per-level fold — one
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
176
|
+
// FLAT per-level fold — one loop per level (foldSlice), each group joined by
|
|
177
|
+
// joinFlat with the permute and add FUSED (`gist[d] += v[seat[d]]`, no scratch
|
|
178
|
+
// buffer), and subtree byte lengths carried incrementally on Folded (the old
|
|
179
|
+
// boundary scan re-walked subtrees every level — O(n log n)). The per-level
|
|
180
|
+
// SUPERPOSITION is byte-identical to the original recursive foldGroup: the
|
|
181
|
+
// same FP additions in the same order.
|
|
182
|
+
//
|
|
183
|
+
// THE SHAPE IS WRITTEN ONCE, OVER A FOLD ALGEBRA. Which items group under
|
|
184
|
+
// which parent is decided by the cut levels, the keyring and, inside an
|
|
185
|
+
// over-long row, the items' content key — never by what an item IS. So the
|
|
186
|
+
// grouping (foldSlice, groupByLevel) takes the two things it needs from an
|
|
187
|
+
// item, `join` and `key`, and runs unchanged over the vector fold (perception)
|
|
188
|
+
// and over the identity fold ({@link contentIdentity}), which can therefore
|
|
189
|
+
// never disagree about the tree.
|
|
183
190
|
//
|
|
184
191
|
// LINEAR fold — intermediate gists are NOT normalized; only the final root is
|
|
185
|
-
// (
|
|
192
|
+
// (`rootOf`'s single normalize). This is a deliberate change of similarity
|
|
186
193
|
// semantics from the original per-group normalize, not a cached optimization:
|
|
187
194
|
// the fold is now a pure linear operator — a superposition of positionally-
|
|
188
195
|
// bound leaf vectors — so an interior node carries its span's natural
|
|
@@ -201,85 +208,22 @@ export function consensusFloor(N) {
|
|
|
201
208
|
* each extra level applies another seat permutation to the whole gist —
|
|
202
209
|
* near-identical inputs straddling such a cliff read as orthogonal
|
|
203
210
|
* (measured: 33-byte-identical prefixes at cos ≈ 0). */
|
|
204
|
-
function foldSlice(
|
|
205
|
-
const mg = space.maxGroup;
|
|
206
|
-
const D = space.D;
|
|
211
|
+
function foldSlice(alg, mg, items, start, count, out, force) {
|
|
207
212
|
const complete = count - (count % mg);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
let len = 0;
|
|
212
|
-
for (let k = 0; k < size; k++) {
|
|
213
|
-
const f = items[at + k];
|
|
214
|
-
const slot = twoEndedSeat(space.seats.length, size, k);
|
|
215
|
-
const seat = space.seats[slot].fwd;
|
|
216
|
-
const v = f.tree.v;
|
|
217
|
-
// Fused permute-and-accumulate — same FP ops, same order as the old
|
|
218
|
-
// permuteInto + addInto pair, with no scratch buffer.
|
|
219
|
-
for (let d = 0; d < D; d++)
|
|
220
|
-
gist[d] += v[seat[d]];
|
|
221
|
-
kids[k] = f.tree;
|
|
222
|
-
len += f.len;
|
|
223
|
-
}
|
|
224
|
-
out.push({ tree: sema(gist, null, kids), len });
|
|
225
|
-
};
|
|
226
|
-
for (let i = 0; i < complete; i += mg)
|
|
227
|
-
foldAt(start + i, mg);
|
|
213
|
+
for (let i = 0; i < complete; i += mg) {
|
|
214
|
+
out.push(alg.join(items.slice(start + i, start + i + mg)));
|
|
215
|
+
}
|
|
228
216
|
const leftover = count - complete;
|
|
229
217
|
if (leftover === 0)
|
|
230
218
|
return;
|
|
231
|
-
if (force && leftover >= 2)
|
|
232
|
-
|
|
219
|
+
if (force && leftover >= 2) {
|
|
220
|
+
out.push(alg.join(items.slice(start + complete, start + count)));
|
|
221
|
+
}
|
|
233
222
|
else
|
|
234
223
|
for (let i = complete; i < count; i++)
|
|
235
224
|
out.push(items[start + i]);
|
|
236
225
|
}
|
|
237
|
-
function riverFold(space, row, stableBytes) {
|
|
238
|
-
if (row.length === 0) {
|
|
239
|
-
const z = new Float32Array(space.D);
|
|
240
|
-
return { tree: sema(z, new Uint8Array(0), null), len: 0 };
|
|
241
|
-
}
|
|
242
|
-
let level = row;
|
|
243
|
-
while (level.length > 1) {
|
|
244
|
-
// Find the item index where accumulated bytes reaches stableBytes.
|
|
245
|
-
let boundary = level.length;
|
|
246
|
-
if (stableBytes > 0) {
|
|
247
|
-
let acc = 0;
|
|
248
|
-
for (let i = 0; i < level.length; i++) {
|
|
249
|
-
acc += level[i].len;
|
|
250
|
-
if (acc >= stableBytes) {
|
|
251
|
-
boundary = i + 1;
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
const next = [];
|
|
257
|
-
if (boundary < level.length) {
|
|
258
|
-
// Prefix folds independently of the suffix — structural stability.
|
|
259
|
-
foldSlice(space, level, 0, boundary, next, true);
|
|
260
|
-
foldSlice(space, level, boundary, level.length - boundary, next, true);
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
foldSlice(space, level, 0, level.length, next, true);
|
|
264
|
-
}
|
|
265
|
-
level = next;
|
|
266
|
-
}
|
|
267
|
-
// LINEAR fold — this root normalize is the ONLY normalize of the entire
|
|
268
|
-
// fold; every intermediate gist stays unnormalized (see the folding
|
|
269
|
-
// header). Skipped for a single-leaf input: that root IS the shared
|
|
270
|
-
// alphabet vector (already unit), and normalizing in place would mutate the
|
|
271
|
-
// alphabet itself.
|
|
272
|
-
if (row.length > 1)
|
|
273
|
-
normalize(level[0].tree.v);
|
|
274
|
-
return level[0];
|
|
275
|
-
}
|
|
276
226
|
// ---- public API ----
|
|
277
|
-
function bytesToLeaves(alphabet, bytes) {
|
|
278
|
-
return Array.from(bytes, (b, i) => {
|
|
279
|
-
const v = alphabet.vecs[b];
|
|
280
|
-
return { tree: sema(v, bytes.slice(i, i + 1), null), len: 1 };
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
227
|
/** CONTENT-DEFINED FOLD BOUNDARIES — where a byte stream segments, chosen by
|
|
284
228
|
* the bytes rather than by arithmetic.
|
|
285
229
|
*
|
|
@@ -341,42 +285,8 @@ function bytesToLeaves(alphabet, bytes) {
|
|
|
341
285
|
*
|
|
342
286
|
* Levels are read from the hash the cut was ACCEPTED at, not recomputed, so
|
|
343
287
|
* they cost nothing beyond the divisions already being done. */
|
|
344
|
-
// Cyclic-polynomial table for the bounded-window cut hash. Derived once from
|
|
345
|
-
// the fold's own mixing constant — no seed, no tuning. A byte contributes
|
|
346
|
-
// BUZ[b] on entering the window; the hash rotates by one per byte, so by the
|
|
347
|
-
// time that byte leaves, its contribution has travelled k places and is
|
|
348
|
-
// removed rotated by k. The rotation is taken at the use site rather than
|
|
349
|
-
// precomputed into a second table so the window width follows maxGroup
|
|
350
|
-
// instead of being frozen at one value.
|
|
351
|
-
const BUZ = new Uint32Array(256);
|
|
352
|
-
{
|
|
353
|
-
let x = 0x9e3779b9 >>> 0;
|
|
354
|
-
for (let i = 0; i < 256; i++) {
|
|
355
|
-
x = Math.imul(x ^ (x >>> 15), 2654435761) >>> 0;
|
|
356
|
-
x = (x ^ (x >>> 13)) >>> 0;
|
|
357
|
-
BUZ[i] = x;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
/** BUZ rotated by the window width — what a byte's contribution has become by
|
|
361
|
-
* the time it leaves. Cached because the width follows `maxGroup`, which is
|
|
362
|
-
* fixed for a given space: built once, then a plain table lookup per byte. */
|
|
363
|
-
let buzOutTable = null;
|
|
364
|
-
let buzOutWidth = -1;
|
|
365
|
-
function buzOut(k) {
|
|
366
|
-
if (buzOutWidth !== k || buzOutTable === null) {
|
|
367
|
-
const t = new Uint32Array(256);
|
|
368
|
-
for (let i = 0; i < 256; i++) {
|
|
369
|
-
const v = BUZ[i];
|
|
370
|
-
t[i] = ((v << k) | (v >>> (32 - k))) >>> 0;
|
|
371
|
-
}
|
|
372
|
-
buzOutTable = t;
|
|
373
|
-
buzOutWidth = k;
|
|
374
|
-
}
|
|
375
|
-
return buzOutTable;
|
|
376
|
-
}
|
|
377
288
|
function contentLevels(space, bytes) {
|
|
378
289
|
const W = space.maxGroup;
|
|
379
|
-
const minLen = W - 1;
|
|
380
290
|
const maxLen = space.seats.length;
|
|
381
291
|
// MEASURED AND REFUTED — making E[segment] equal W. A segment is at least
|
|
382
292
|
// `minLen` bytes and then cuts with probability p, so E[len] = minLen +
|
|
@@ -452,21 +362,20 @@ function contentLevels(space, bytes) {
|
|
|
452
362
|
// old 0.870 0.902 0.492 0.879 0.888 0.441
|
|
453
363
|
// this 0.935 0.952 0.732 0.920 0.916 0.935
|
|
454
364
|
//
|
|
455
|
-
// The
|
|
456
|
-
//
|
|
457
|
-
// input, so the threshold fires at content-chosen positions on a gradient
|
|
365
|
+
// The register (the last k raw bytes, `h = h << 8 | byte`, mixed by the
|
|
366
|
+
// two-round avalanche below) has EXACTLY k bytes of memory and scrambles
|
|
367
|
+
// periodic input, so the threshold fires at content-chosen positions on a gradient
|
|
458
368
|
// just as it does on text — which is what leaves the `maxLen` fallback
|
|
459
369
|
// rarely engaged instead of carrying the phase. Segment lengths are
|
|
460
370
|
// unchanged in distribution (mean 5.2-7.2 against the old 5.4-6.0), so the
|
|
461
371
|
// mechanisms fitted to that distribution see the same scale.
|
|
462
372
|
//
|
|
463
|
-
// Cost
|
|
464
|
-
//
|
|
373
|
+
// Cost per byte: a shift, an OR and the avalanche's two multiplies — no
|
|
374
|
+
// table and no auxiliary structure. (An exact sliding-window
|
|
465
375
|
// minimum — winnowing — aligns slightly better still, 0.91-0.999, but its
|
|
466
376
|
// deque costs 51 MB/s against this rule's 112 and buys nothing the
|
|
467
377
|
// scrambling hash does not already give.)
|
|
468
378
|
const k = W;
|
|
469
|
-
const OUT = buzOut(k);
|
|
470
379
|
const cuts = [];
|
|
471
380
|
const levels = [];
|
|
472
381
|
const n = bytes.length;
|
|
@@ -648,8 +557,9 @@ function contentFoldSpan(space, alphabet, bytes, from, to) {
|
|
|
648
557
|
for (let i = 0; i + 1 < edges.length; i++) {
|
|
649
558
|
segs.push(flatFold(space, alphabet, span, edges[i], edges[i + 1]));
|
|
650
559
|
}
|
|
651
|
-
if (segs.length > 1)
|
|
652
|
-
return groupByLevel(space, segs, levels, 1);
|
|
560
|
+
if (segs.length > 1) {
|
|
561
|
+
return groupByLevel(vectorFold(space), space, segs, levels, 1);
|
|
562
|
+
}
|
|
653
563
|
return segs[0];
|
|
654
564
|
}
|
|
655
565
|
/** {@link contentFoldSpan} over a WHOLE stream, reusing the segments a previous
|
|
@@ -687,7 +597,7 @@ function contentFoldSpan(space, alphabet, bytes, from, to) {
|
|
|
687
597
|
* streams. Verifying the bytes here would cost O(prefix) and defeat the
|
|
688
598
|
* whole point, so the obligation sits with the caller, and every caller
|
|
689
599
|
* discharges it structurally rather than by care: `perceiveDeposit` looks the
|
|
690
|
-
* entry up under `
|
|
600
|
+
* entry up under `latin1(bytes.subarray(0, L))` — the prefix's own bytes
|
|
691
601
|
* ARE the cache key — and a conversation's fold state advances only by
|
|
692
602
|
* append. A new caller that cannot make the same structural argument must
|
|
693
603
|
* pass no `prev` at all; the cold path is always correct.
|
|
@@ -711,7 +621,7 @@ export function contentFoldIncremental(space, alphabet, bytes, prev) {
|
|
|
711
621
|
segs.push(hit ?? flatFold(space, alphabet, bytes, edges[i], edges[i + 1]));
|
|
712
622
|
}
|
|
713
623
|
const folded = segs.length > 1
|
|
714
|
-
? groupByLevel(space, segs, levels, 1)
|
|
624
|
+
? groupByLevel(vectorFold(space), space, segs, levels, 1)
|
|
715
625
|
: segs[0];
|
|
716
626
|
// THE ROOT IS NORMALIZED IN PLACE, A CACHED SEGMENT NEVER IS. With one
|
|
717
627
|
// segment — or with a grouping that passes a lone item through — `folded`
|
|
@@ -745,14 +655,15 @@ export function contentFoldIncremental(space, alphabet, bytes, prev) {
|
|
|
745
655
|
* the cut levels are uniformly 0 and carry no signal. Identical subtrees
|
|
746
656
|
* fold to identical vectors, so the same items in the same order always
|
|
747
657
|
* choose the same split — the property the whole fold rests on. */
|
|
658
|
+
const ITEM_KEY_COORDS = 8;
|
|
748
659
|
function itemKey(v) {
|
|
749
660
|
let h = 0x811c9dc5;
|
|
750
|
-
for (let d = 0; d <
|
|
661
|
+
for (let d = 0; d < ITEM_KEY_COORDS; d++) {
|
|
751
662
|
h = Math.imul(h ^ ((v[d] * 8192) | 0), 0x01000193) >>> 0;
|
|
752
663
|
}
|
|
753
664
|
return h >>> 0;
|
|
754
665
|
}
|
|
755
|
-
function groupByLevel(space, items, levels, level) {
|
|
666
|
+
function groupByLevel(alg, space, items, levels, level) {
|
|
756
667
|
if (items.length === 1)
|
|
757
668
|
return items[0];
|
|
758
669
|
const maxSeats = space.seats.length;
|
|
@@ -783,7 +694,7 @@ function groupByLevel(space, items, levels, level) {
|
|
|
783
694
|
// equal — fall back to the ITEMS' own content. A group's gist is
|
|
784
695
|
// diverse where its cut level is not, so hashing it gives a
|
|
785
696
|
// content-determined split point where the level array has none.
|
|
786
|
-
const key =
|
|
697
|
+
const key = alg.key(items[j]);
|
|
787
698
|
if (levels[j] > bestLevel ||
|
|
788
699
|
(levels[j] === bestLevel && key > bestKey)) {
|
|
789
700
|
bestLevel = levels[j];
|
|
@@ -792,12 +703,12 @@ function groupByLevel(space, items, levels, level) {
|
|
|
792
703
|
}
|
|
793
704
|
}
|
|
794
705
|
const part = items.slice(at, best + 1);
|
|
795
|
-
groups.push(part.length === 1 ? part[0] :
|
|
706
|
+
groups.push(part.length === 1 ? part[0] : alg.join(part));
|
|
796
707
|
groupLevels.push(levels[best]);
|
|
797
708
|
at = best + 1;
|
|
798
709
|
}
|
|
799
710
|
const slice = items.slice(at, to);
|
|
800
|
-
groups.push(slice.length === 1 ? slice[0] :
|
|
711
|
+
groups.push(slice.length === 1 ? slice[0] : alg.join(slice));
|
|
801
712
|
};
|
|
802
713
|
let start = 0;
|
|
803
714
|
for (let i = 0; i <= levels.length; i++) {
|
|
@@ -812,10 +723,10 @@ function groupByLevel(space, items, levels, level) {
|
|
|
812
723
|
if (groups.length === items.length) {
|
|
813
724
|
// This level split nothing — climb rather than spin.
|
|
814
725
|
return level < 24
|
|
815
|
-
? groupByLevel(space, items, levels, level + 1)
|
|
816
|
-
:
|
|
726
|
+
? groupByLevel(alg, space, items, levels, level + 1)
|
|
727
|
+
: riverFold(alg, space, items);
|
|
817
728
|
}
|
|
818
|
-
return groupByLevel(space, groups, groupLevels, level + 1);
|
|
729
|
+
return groupByLevel(alg, space, groups, groupLevels, level + 1);
|
|
819
730
|
}
|
|
820
731
|
/** Join a row of already-folded items as one unnormalized node — the same
|
|
821
732
|
* two-ended seat binding as {@link flatFold}, one level up. A group formed
|
|
@@ -837,6 +748,76 @@ function joinFlat(space, items) {
|
|
|
837
748
|
}
|
|
838
749
|
return { tree: sema(gist, null, kids), len };
|
|
839
750
|
}
|
|
751
|
+
/** The vector fold — perception's algebra. */
|
|
752
|
+
const vectorFolds = new WeakMap();
|
|
753
|
+
function vectorFold(space) {
|
|
754
|
+
let alg = vectorFolds.get(space);
|
|
755
|
+
if (alg === undefined) {
|
|
756
|
+
alg = {
|
|
757
|
+
join: (items) => joinFlat(space, items),
|
|
758
|
+
key: (item) => itemKey(item.tree.v),
|
|
759
|
+
};
|
|
760
|
+
vectorFolds.set(space, alg);
|
|
761
|
+
}
|
|
762
|
+
return alg;
|
|
763
|
+
}
|
|
764
|
+
/** The raw gist coordinate `p` of a node whose children are `kids` — the
|
|
765
|
+
* coordinate {@link flatFold}/{@link joinFlat} would have written: the same
|
|
766
|
+
* float32 additions, in the same order, from a zero start. */
|
|
767
|
+
function boundCoord(space, n, kid, p) {
|
|
768
|
+
let acc = 0;
|
|
769
|
+
for (let k = 0; k < n; k++) {
|
|
770
|
+
const seat = space.seats[twoEndedSeat(space.seats.length, n, k)].fwd;
|
|
771
|
+
acc = Math.fround(acc + kid(k, seat[p]));
|
|
772
|
+
}
|
|
773
|
+
return acc;
|
|
774
|
+
}
|
|
775
|
+
/** The node a byte stream's content fold NAMES — `foldTree` over
|
|
776
|
+
* {@link contentFoldSpan}'s tree, without building a single vector.
|
|
777
|
+
*
|
|
778
|
+
* A fold names a node only when every child is named, so identity needs the
|
|
779
|
+
* tree's SHAPE and the store's answer for each node — and the shape is a
|
|
780
|
+
* function of the bytes (cuts and levels) plus, inside an over-long row, each
|
|
781
|
+
* item's {@link itemKey}: eight coordinates of its raw gist. Those are read
|
|
782
|
+
* lazily through {@link boundCoord}, bit-identical to the coordinates the
|
|
783
|
+
* vector fold computes, so the grouping (the SAME {@link groupByLevel}) cannot
|
|
784
|
+
* differ. `segment(from, to)` names a level-0 segment (one flat node over
|
|
785
|
+
* single-byte atoms, or the atom itself); `branch(kids, from, to)` names the
|
|
786
|
+
* group covering [from, to) — `kids` holds null for an unnamed child, because
|
|
787
|
+
* the store names a branch by its BYTES when its children do not name it
|
|
788
|
+
* (the write side's step 1b, store.ts `intern`), so an unnamed child does not
|
|
789
|
+
* settle its ancestors. Every item's grouping key is read from its content,
|
|
790
|
+
* named or not, for the same reason. An empty stream is the caller's: its
|
|
791
|
+
* fold is the alphabet's zero-byte leaf, not a segment. */
|
|
792
|
+
export function contentIdentity(space, alphabet, bytes, segment, branch) {
|
|
793
|
+
const { cuts, levels } = contentLevels(space, bytes);
|
|
794
|
+
const edges = [0, ...cuts, bytes.length];
|
|
795
|
+
const segs = [];
|
|
796
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
797
|
+
const from = edges[i], n = edges[i + 1] - from;
|
|
798
|
+
segs.push({
|
|
799
|
+
id: segment(from, edges[i + 1]),
|
|
800
|
+
from,
|
|
801
|
+
to: edges[i + 1],
|
|
802
|
+
coord: n === 1 ? (p) => alphabet.vecs[bytes[from]][p] : (p) => boundCoord(space, n, (k, at) => alphabet.vecs[bytes[from + k]][at], p),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
if (segs.length === 1)
|
|
806
|
+
return segs[0].id;
|
|
807
|
+
const alg = {
|
|
808
|
+
join: (items) => {
|
|
809
|
+
const from = items[0].from, to = items[items.length - 1].to;
|
|
810
|
+
return {
|
|
811
|
+
id: branch(items.map((it) => it.id), from, to),
|
|
812
|
+
from,
|
|
813
|
+
to,
|
|
814
|
+
coord: (p) => boundCoord(space, items.length, (k, at) => items[k].coord(at), p),
|
|
815
|
+
};
|
|
816
|
+
},
|
|
817
|
+
key: (item) => item.key ??= itemKey(Array.from({ length: ITEM_KEY_COORDS }, (_, d) => item.coord(d))),
|
|
818
|
+
};
|
|
819
|
+
return groupByLevel(alg, space, segs, levels, 1).id;
|
|
820
|
+
}
|
|
840
821
|
/** One segment as a single unnormalized node: leaf per byte, each bound into
|
|
841
822
|
* a seat derived from its position relative to BOTH segment ends.
|
|
842
823
|
*
|
|
@@ -878,14 +859,6 @@ function flatFold(space, alphabet, bytes, from, to) {
|
|
|
878
859
|
}
|
|
879
860
|
return { tree: sema(gist, null, kids), len: n };
|
|
880
861
|
}
|
|
881
|
-
/* * The stable-prefix segmented fold (fold-contract.md). Each segment between
|
|
882
|
-
* consecutive boundaries folds PLAINLY and independently; segment roots
|
|
883
|
-
* join left-nested, and only the final root is normalized (the linear-fold
|
|
884
|
-
* contract: one normalize per perception). A segment's own inner splits
|
|
885
|
-
* need no recursion here: a nested learnt prefix is itself an earlier
|
|
886
|
-
* boundary, so the left-nested join reproduces every intermediate learnt
|
|
887
|
-
* root ((s₀·s₁) IS the root the store learnt for the first two segments'
|
|
888
|
-
* bytes, and so on). */
|
|
889
862
|
/** A fold's ROOT: ONE normalize per perception, at the root, exactly as
|
|
890
863
|
* riverFold did — the interior stays raw (the linear-fold contract).
|
|
891
864
|
*
|
|
@@ -902,6 +875,14 @@ function rootOf(f) {
|
|
|
902
875
|
normalize(f.tree.v);
|
|
903
876
|
return f.tree;
|
|
904
877
|
}
|
|
878
|
+
/** The stable-prefix segmented fold (fold-contract.md). Each segment between
|
|
879
|
+
* consecutive boundaries folds PLAINLY and independently; segment roots
|
|
880
|
+
* join left-nested, and only the final root is normalized (the linear-fold
|
|
881
|
+
* contract: one normalize per perception). A segment's own inner splits
|
|
882
|
+
* need no recursion here: a nested learnt prefix is itself an earlier
|
|
883
|
+
* boundary, so the left-nested join reproduces every intermediate learnt
|
|
884
|
+
* root ((s₀·s₁) IS the root the store learnt for the first two segments'
|
|
885
|
+
* bytes, and so on). */
|
|
905
886
|
function stablePrefixFold(space, alphabet, bytes, boundaries) {
|
|
906
887
|
const cuts = [];
|
|
907
888
|
let prev = 0;
|
|
@@ -995,12 +976,15 @@ export function riverFoldRaw(space, row) {
|
|
|
995
976
|
const z = new Float32Array(space.D);
|
|
996
977
|
return { tree: sema(z, new Uint8Array(0), null), len: 0 };
|
|
997
978
|
}
|
|
998
|
-
|
|
999
|
-
|
|
979
|
+
return riverFold(vectorFold(space), space, row);
|
|
980
|
+
}
|
|
981
|
+
/** The river's fixed-arity shape over any fold algebra: groups of maxGroup,
|
|
982
|
+
* the trailing partial group forced, level after level to one root. */
|
|
983
|
+
function riverFold(alg, space, row) {
|
|
1000
984
|
let level = row;
|
|
1001
985
|
while (level.length > 1) {
|
|
1002
986
|
const next = [];
|
|
1003
|
-
foldSlice(space, level, 0, level.length, next, true);
|
|
987
|
+
foldSlice(alg, space.maxGroup, level, 0, level.length, next, true);
|
|
1004
988
|
level = next;
|
|
1005
989
|
}
|
|
1006
990
|
return level[0];
|
package/dist/src/meter.d.ts
CHANGED
|
@@ -97,6 +97,13 @@ export declare class Meter {
|
|
|
97
97
|
perceivedBytes: number;
|
|
98
98
|
/** `perceive` calls served from the per-response / conversation memo. */
|
|
99
99
|
perceiveHits: number;
|
|
100
|
+
/** Bytes walked by the IDENTITY fold (`exactNode` — the fold's shape read
|
|
101
|
+
* for its node id, no vectors). Perception's content-addressed half: what
|
|
102
|
+
* used to show up in `perceivedBytes` when every resolve folded vectors. */
|
|
103
|
+
identityBytes: number;
|
|
104
|
+
/** Branches the read side named by their BYTES — the flat node the write
|
|
105
|
+
* side reused when the children named none (primitives.ts `branchNaming`). */
|
|
106
|
+
flatBranchNames: number;
|
|
100
107
|
/** `recognise` calls that actually ran. */
|
|
101
108
|
recognitions: number;
|
|
102
109
|
/** Bytes recognised by those calls. */
|
|
@@ -150,12 +157,20 @@ export declare class Meter {
|
|
|
150
157
|
searchPops: number;
|
|
151
158
|
/** Chart items pushed by those searches. */
|
|
152
159
|
searchPushes: number;
|
|
160
|
+
/** Chart items popped DOMINATED — a span's form or completion that could
|
|
161
|
+
* only yield completions of that span costing at least one already yielded
|
|
162
|
+
* (graph-search.ts `buildSearch`), so no rule was generated from it. */
|
|
163
|
+
searchDominated: number;
|
|
153
164
|
/** `floor()` calls that returned a bound (the mechanism could fire). */
|
|
154
165
|
mechanismFloors: number;
|
|
155
166
|
/** `floor()` calls that returned null (structurally impossible). */
|
|
156
167
|
mechanismSkips: number;
|
|
157
168
|
/** `run()` calls — the ones the floor pruning let through. */
|
|
158
169
|
mechanismRuns: number;
|
|
170
|
+
/** Mechanisms skipped because a mechanism floored lower, run AHEAD of them
|
|
171
|
+
* (pipeline.ts, "a cheaper bound is looked at before a dearer one is paid
|
|
172
|
+
* for"), already reached a grade their floor cannot beat. */
|
|
173
|
+
mechanismsBounded: number;
|
|
159
174
|
/** Candidates the decider weighed. */
|
|
160
175
|
candidates: number;
|
|
161
176
|
/** `deriveThrough` yielded — a fact was reached through the subject the query
|
|
@@ -168,6 +183,11 @@ export declare class Meter {
|
|
|
168
183
|
joinNoKey: number;
|
|
169
184
|
/** Refused: the fact contains no entity that leads anywhere. */
|
|
170
185
|
joinNoEntity: number;
|
|
186
|
+
/** Facts the join was priced for — the facts a lightest derivation stood on
|
|
187
|
+
* (graph-search.ts `solve`), each once. The output-sensitivity of the join
|
|
188
|
+
* in one number: it tracks the answer's facts, never how many facts the
|
|
189
|
+
* exploration reached (`test/150`). */
|
|
190
|
+
joinFacts: number;
|
|
171
191
|
/** `recompleteNode` re-covered a produced form — the descent that decomposes
|
|
172
192
|
* a completion by ITS OWN kids. Without this the descent is invisible: a
|
|
173
193
|
* caller could see the chain's result but not whether the recomposition
|
package/dist/src/meter.js
CHANGED
|
@@ -91,6 +91,13 @@ export class Meter {
|
|
|
91
91
|
perceivedBytes = 0;
|
|
92
92
|
/** `perceive` calls served from the per-response / conversation memo. */
|
|
93
93
|
perceiveHits = 0;
|
|
94
|
+
/** Bytes walked by the IDENTITY fold (`exactNode` — the fold's shape read
|
|
95
|
+
* for its node id, no vectors). Perception's content-addressed half: what
|
|
96
|
+
* used to show up in `perceivedBytes` when every resolve folded vectors. */
|
|
97
|
+
identityBytes = 0;
|
|
98
|
+
/** Branches the read side named by their BYTES — the flat node the write
|
|
99
|
+
* side reused when the children named none (primitives.ts `branchNaming`). */
|
|
100
|
+
flatBranchNames = 0;
|
|
94
101
|
/** `recognise` calls that actually ran. */
|
|
95
102
|
recognitions = 0;
|
|
96
103
|
/** Bytes recognised by those calls. */
|
|
@@ -146,6 +153,10 @@ export class Meter {
|
|
|
146
153
|
searchPops = 0;
|
|
147
154
|
/** Chart items pushed by those searches. */
|
|
148
155
|
searchPushes = 0;
|
|
156
|
+
/** Chart items popped DOMINATED — a span's form or completion that could
|
|
157
|
+
* only yield completions of that span costing at least one already yielded
|
|
158
|
+
* (graph-search.ts `buildSearch`), so no rule was generated from it. */
|
|
159
|
+
searchDominated = 0;
|
|
149
160
|
// ── Mind: the mechanism market ──────────────────────────────────────────
|
|
150
161
|
/** `floor()` calls that returned a bound (the mechanism could fire). */
|
|
151
162
|
mechanismFloors = 0;
|
|
@@ -153,13 +164,17 @@ export class Meter {
|
|
|
153
164
|
mechanismSkips = 0;
|
|
154
165
|
/** `run()` calls — the ones the floor pruning let through. */
|
|
155
166
|
mechanismRuns = 0;
|
|
167
|
+
/** Mechanisms skipped because a mechanism floored lower, run AHEAD of them
|
|
168
|
+
* (pipeline.ts, "a cheaper bound is looked at before a dearer one is paid
|
|
169
|
+
* for"), already reached a grade their floor cannot beat. */
|
|
170
|
+
mechanismsBounded = 0;
|
|
156
171
|
/** Candidates the decider weighed. */
|
|
157
172
|
candidates = 0;
|
|
158
173
|
// ── Graph search: the fact join (DIRECTION) ─────────────────────────────
|
|
159
174
|
//
|
|
160
175
|
// The join's outcome was observable ONLY through the rationale, and the
|
|
161
176
|
// rationale PERTURBS the search (measured: appending text to a refusal note
|
|
162
|
-
// changed a traced answer). These
|
|
177
|
+
// changed a traced answer). These five counters are the untraced view — the
|
|
163
178
|
// same surface every other work counter uses, incremented where the decision
|
|
164
179
|
// is made, never behind a trace guard.
|
|
165
180
|
/** `deriveThrough` yielded — a fact was reached through the subject the query
|
|
@@ -172,6 +187,11 @@ export class Meter {
|
|
|
172
187
|
joinNoKey = 0;
|
|
173
188
|
/** Refused: the fact contains no entity that leads anywhere. */
|
|
174
189
|
joinNoEntity = 0;
|
|
190
|
+
/** Facts the join was priced for — the facts a lightest derivation stood on
|
|
191
|
+
* (graph-search.ts `solve`), each once. The output-sensitivity of the join
|
|
192
|
+
* in one number: it tracks the answer's facts, never how many facts the
|
|
193
|
+
* exploration reached (`test/150`). */
|
|
194
|
+
joinFacts = 0;
|
|
175
195
|
/** `recompleteNode` re-covered a produced form — the descent that decomposes
|
|
176
196
|
* a completion by ITS OWN kids. Without this the descent is invisible: a
|
|
177
197
|
* caller could see the chain's result but not whether the recomposition
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
//
|
|
3
3
|
// articulate — substitute answer forms with the asker's synonyms,
|
|
4
4
|
// using concept (halo) resonance to match the voices.
|
|
5
|
+
import { indexOf } from "../bytes.js";
|
|
5
6
|
import { spliceAll } from "./types.js";
|
|
6
7
|
import { recognise } from "./recognition.js";
|
|
7
8
|
import { answers, contains } from "./traverse.js";
|
|
8
9
|
import { bestHaloMate } from "./match.js";
|
|
10
|
+
import { noLicence } from "./graph-search.js";
|
|
9
11
|
import { coverSequence } from "../derive/src/index.js";
|
|
10
12
|
import { rItem, rNode, traceDerivation } from "./trace.js";
|
|
11
13
|
/** Re-voice an answer in the asker's own words. For each recognised form
|
|
@@ -67,7 +69,18 @@ export async function articulate(ctx, answer, query) {
|
|
|
67
69
|
if (!found)
|
|
68
70
|
continue;
|
|
69
71
|
const voice = found.item;
|
|
72
|
+
// THE ASKER'S WORDING ALREADY HOLDING THE FORM is not a re-voicing of it:
|
|
73
|
+
// splicing it in would add the asker's other words to the answer. That is
|
|
74
|
+
// what `contains` asks of the DAG, and the DAG can miss it — the fold cuts
|
|
75
|
+
// `eva director` as `eva d|irector`, so `eva` is no structural child of it
|
|
76
|
+
// — so the question is also asked of the bytes, the reading that cannot
|
|
77
|
+
// miss. Measured (test/106's chain): the answer's `eva`, a halo mate of the
|
|
78
|
+
// asker's `eva director` because both lead to the same fact, was revoiced
|
|
79
|
+
// as "The director of eva director is Gustaf Molander."
|
|
80
|
+
const formBytes = store.bytesPrefix(s.payload, voice.bytes.length + 1);
|
|
70
81
|
if (voice.node === s.payload || contains(ctx, voice.node, s.payload) ||
|
|
82
|
+
(formBytes.length <= voice.bytes.length &&
|
|
83
|
+
indexOf(voice.bytes, formBytes, 0) >= 0) ||
|
|
71
84
|
answers(ctx, voice.node, s.payload)) {
|
|
72
85
|
continue;
|
|
73
86
|
}
|
|
@@ -96,7 +109,7 @@ export async function articulate(ctx, answer, query) {
|
|
|
96
109
|
s.end,
|
|
97
110
|
])),
|
|
98
111
|
]);
|
|
99
|
-
const solved = ctx.search.cover(answer.length, voicedSites,
|
|
112
|
+
const solved = ctx.search.cover(answer.length, voicedSites, noLicence(), ans.leaves, ans.splits, substitutions, undefined, undefined, ctx.trace ? (steps) => traceDerivation(ctx, steps) : undefined);
|
|
100
113
|
const segs = solved && solved.segs;
|
|
101
114
|
tArtCover?.done(segs === null
|
|
102
115
|
? []
|
|
@@ -350,6 +350,18 @@ export declare function poolVotes(ctx: MindContext, regionVotes: readonly Region
|
|
|
350
350
|
anchored: Set<number>;
|
|
351
351
|
steps: DerivationStep[];
|
|
352
352
|
};
|
|
353
|
+
/** The number of DISTINCT clusters a root's contributing regions form —
|
|
354
|
+
* see Attention.clusters. Two regions belong to the same cluster iff the
|
|
355
|
+
* gap between them is strictly less than one river-fold quantum W: at
|
|
356
|
+
* that distance there is no room for a genuinely separate, independently
|
|
357
|
+
* perceivable unit of content between them (the same "smallest meaningful
|
|
358
|
+
* distinction" quantum {@link reachThreshold}'s own doc invokes). A gap
|
|
359
|
+
* of a full quantum or more means real, separate structure could sit
|
|
360
|
+
* between the two spans, so they count as independent corroboration.
|
|
361
|
+
* Strict `<` (not `<=`): verified against gap 3.1's own "gender equality"
|
|
362
|
+
* root, whose two genuine clusters sit EXACTLY W bytes apart — `<= W`
|
|
363
|
+
* would wrongly merge them into one and break that pinned requirement. */
|
|
364
|
+
export declare function countClusters(spans: readonly [number, number][], W: number): number;
|
|
353
365
|
export declare function commitVotes(ctx: MindContext, pooled: {
|
|
354
366
|
votes: Map<number, number>;
|
|
355
367
|
votesIdf: Map<number, number>;
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
import { isChunk } from "../sema.js";
|
|
10
10
|
import { lightestDerivation, } from "../derive/src/index.js";
|
|
11
11
|
import { composeStructuralGist, consensusFloor, dominates, estimatorNoise, } from "../geometry.js";
|
|
12
|
-
import { foldTree, gistOf,
|
|
12
|
+
import { foldTree, gistOf, perceive, read, resolve } from "./primitives.js";
|
|
13
13
|
import { recognise } from "./recognition.js";
|
|
14
14
|
import { leafIdRun } from "./canonical.js";
|
|
15
15
|
import { atomIsHub, corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
|
|
16
16
|
import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, loadJunctionSynonymSides, walkCache, } from "./junction.js";
|
|
17
|
-
import { indexOf } from "../bytes.js";
|
|
17
|
+
import { indexOf, latin1 } from "../bytes.js";
|
|
18
18
|
import { restates } from "./derivation.js";
|
|
19
19
|
import { rItem, rNode, traceDerivation } from "./trace.js";
|
|
20
20
|
function newTraceDraft(perceivedCount) {
|
|
@@ -74,7 +74,7 @@ export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
|
|
|
74
74
|
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
75
75
|
// respondTurn().
|
|
76
76
|
if (ctx.climbMemo) {
|
|
77
|
-
const contentKey =
|
|
77
|
+
const contentKey = latin1(query);
|
|
78
78
|
const modeKey = `${k}:${mode}`;
|
|
79
79
|
let byRead = ctx.climbMemo.get(contentKey);
|
|
80
80
|
if (byRead === undefined) {
|
|
@@ -1125,7 +1125,7 @@ export function poolVotes(ctx, regionVotes, sat, N, td) {
|
|
|
1125
1125
|
* Strict `<` (not `<=`): verified against gap 3.1's own "gender equality"
|
|
1126
1126
|
* root, whose two genuine clusters sit EXACTLY W bytes apart — `<= W`
|
|
1127
1127
|
* would wrongly merge them into one and break that pinned requirement. */
|
|
1128
|
-
function countClusters(spans, W) {
|
|
1128
|
+
export function countClusters(spans, W) {
|
|
1129
1129
|
if (spans.length === 0)
|
|
1130
1130
|
return 0;
|
|
1131
1131
|
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
@@ -1521,19 +1521,47 @@ export function canonicalChunkId(ctx, regionBytes, N, reachMemo) {
|
|
|
1521
1521
|
if (len < 2)
|
|
1522
1522
|
return flatId;
|
|
1523
1523
|
// Within one window, the widest reach is still the right CANONICAL
|
|
1524
|
-
// identity — a chunk's anchor should be its most general stable form.
|
|
1525
|
-
|
|
1526
|
-
|
|
1524
|
+
// identity — a chunk's anchor should be its most general stable form. A
|
|
1525
|
+
// SATURATED reach is the widest there is: its count is wherever the climb
|
|
1526
|
+
// happened to stop deciding, so an unsaturated prefix never out-widens it
|
|
1527
|
+
// by comparing against that count. (It used to: on test/24's gap-3.1
|
|
1528
|
+
// fixture the region `ace and ` had its saturated window replaced by the
|
|
1529
|
+
// unsaturated 2-byte prefix `e `, and that sub-window anchor voted.)
|
|
1530
|
+
//
|
|
1531
|
+
// So a window's anchor is its SHORTEST saturated form when any saturates,
|
|
1532
|
+
// and its widest-reaching form otherwise — and the forms are climbed
|
|
1533
|
+
// shortest first, stopping at the first that saturates: the forms longer
|
|
1534
|
+
// than it cannot change the anchor. Measured over 8 queries on the
|
|
1535
|
+
// 31.7M-node store: 339 of 347 windows saturate, 312 already at their
|
|
1536
|
+
// 2-byte prefix, while the full window and the 3-byte prefix — climbed
|
|
1537
|
+
// first, to √N contexts each — were 89% of the visits.
|
|
1538
|
+
const forms = [flatId];
|
|
1527
1539
|
for (let k2 = 1; k2 < len; k2++) {
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1540
|
+
const shortId = ctx.store.findBranch(ids.slice(0, len - k2));
|
|
1541
|
+
if (shortId !== null)
|
|
1542
|
+
forms.push(shortId);
|
|
1543
|
+
}
|
|
1544
|
+
const reaches = new Array(forms.length);
|
|
1545
|
+
let bestId = null;
|
|
1546
|
+
let bestReach = null;
|
|
1547
|
+
for (let f = forms.length - 1; f >= 0; f--) {
|
|
1548
|
+
const r = edgeAncestors(ctx, forms[f], N, reachMemo);
|
|
1549
|
+
if (r.saturated) {
|
|
1550
|
+
bestId = forms[f];
|
|
1551
|
+
bestReach = r;
|
|
1552
|
+
break;
|
|
1553
|
+
}
|
|
1554
|
+
reaches[f] = r;
|
|
1555
|
+
}
|
|
1556
|
+
if (bestId === null || bestReach === null) {
|
|
1557
|
+
// None saturates: the widest reach, the longer form on a tie.
|
|
1558
|
+
bestId = forms[0];
|
|
1559
|
+
bestReach = reaches[0];
|
|
1560
|
+
for (let f = 1; f < forms.length; f++) {
|
|
1561
|
+
if (reaches[f].contextsReached > bestReach.contextsReached) {
|
|
1562
|
+
bestId = forms[f];
|
|
1563
|
+
bestReach = reaches[f];
|
|
1564
|
+
}
|
|
1537
1565
|
}
|
|
1538
1566
|
}
|
|
1539
1567
|
if (fallback === null)
|