@hviana/sema 0.4.3 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/AUTHORS.md +0 -1
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -2
  4. package/dist/src/geometry.d.ts +6 -0
  5. package/dist/src/geometry.js +224 -44
  6. package/dist/src/mind/attention.d.ts +11 -0
  7. package/dist/src/mind/attention.js +344 -13
  8. package/dist/src/mind/bridge.js +46 -21
  9. package/dist/src/mind/junction.js +18 -2
  10. package/dist/src/mind/match.d.ts +11 -0
  11. package/dist/src/mind/match.js +13 -2
  12. package/dist/src/mind/mechanisms/cast.js +366 -34
  13. package/dist/src/mind/mechanisms/confluence.js +17 -1
  14. package/dist/src/mind/mechanisms/recall.js +17 -3
  15. package/dist/src/mind/mind.js +11 -2
  16. package/dist/src/mind/pipeline-mechanism.d.ts +4 -0
  17. package/dist/src/mind/pipeline-mechanism.js +96 -40
  18. package/dist/src/mind/pipeline.js +31 -3
  19. package/dist/src/mind/reasoning.d.ts +4 -2
  20. package/dist/src/mind/reasoning.js +29 -4
  21. package/dist/src/mind/recognition.js +67 -2
  22. package/dist/src/mind/resonance.d.ts +14 -2
  23. package/dist/src/mind/resonance.js +0 -0
  24. package/dist/src/mind/types.d.ts +43 -1
  25. package/dist/src/sema.d.ts +11 -1
  26. package/dist/src/sema.js +16 -2
  27. package/dist/src/store.d.ts +64 -1
  28. package/dist/src/store.js +107 -8
  29. package/index.html +2 -3
  30. package/package.json +1 -1
  31. package/src/geometry.ts +231 -43
  32. package/src/mind/attention.ts +366 -15
  33. package/src/mind/bridge.ts +55 -18
  34. package/src/mind/junction.ts +18 -2
  35. package/src/mind/match.ts +18 -2
  36. package/src/mind/mechanisms/cast.ts +376 -43
  37. package/src/mind/mechanisms/confluence.ts +16 -1
  38. package/src/mind/mechanisms/recall.ts +17 -2
  39. package/src/mind/mind.ts +11 -2
  40. package/src/mind/pipeline-mechanism.ts +96 -36
  41. package/src/mind/pipeline.ts +33 -3
  42. package/src/mind/reasoning.ts +31 -4
  43. package/src/mind/recognition.ts +65 -2
  44. package/src/mind/resonance.ts +0 -0
  45. package/src/mind/types.ts +43 -1
  46. package/src/sema.ts +21 -2
  47. package/src/store.ts +106 -5
  48. package/test/00-extract.test.mjs +28 -0
  49. package/test/15-decomposition-gap.test.mjs +0 -0
  50. package/test/24-generalization.test.mjs +67 -19
  51. package/test/29-counterfactual.test.mjs +106 -42
  52. package/test/33-multi-candidate.test.mjs +56 -12
  53. package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
  54. package/test/63-fold-invariants.test.mjs +489 -0
  55. package/test/64-two-ended-thresholds.test.mjs +76 -0
package/src/geometry.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // 3. The same rule recurses level after level until one root remains.
9
9
 
10
10
  import { addInto, copy, normalize, Vec, zeros } from "./vec.js";
11
- import { Sema, sema, Space } from "./sema.js";
11
+ import { Sema, sema, Space, twoEndedSeat } from "./sema.js";
12
12
  import { Alphabet } from "./alphabet.js";
13
13
 
14
14
  // ---- geometric constants ----
@@ -60,6 +60,12 @@ export function identityBar(D: number, maxGroup: number, len: number): number {
60
60
  * ≈ 1 − 1/maxGroup. Half that quantum, 1 − 1/(2·maxGroup), is closer than any
61
61
  * single-child difference can be: a positional echo of the same content.
62
62
  *
63
+ * This is an EQUAL-ARITY replacement law. The two-ended coordinate frame is
64
+ * a bijective relabelling of the seats inside that node, so it does not change
65
+ * the one-child overlap or this bar. Stability under a leading/trailing
66
+ * insertion comes from preserving content-defined subtrees and their anchored
67
+ * coordinates — never from lowering the confidence floor.
68
+ *
63
69
  * Recall uses this as its confidence floor: a query whose nearest resonant
64
70
  * form sits below this bar is structurally unrelated to everything in the store
65
71
  * — further than any single-child variant — and the system returns null rather
@@ -214,7 +220,8 @@ function foldSlice(
214
220
  let len = 0;
215
221
  for (let k = 0; k < size; k++) {
216
222
  const f = items[at + k];
217
- const seat = space.seats[k].fwd;
223
+ const slot = twoEndedSeat(space.seats.length, size, k);
224
+ const seat = space.seats[slot].fwd;
218
225
  const v = f.tree.v;
219
226
  // Fused permute-and-accumulate — same FP ops, same order as the old
220
227
  // permuteInto + addInto pair, with no scratch buffer.
@@ -344,6 +351,42 @@ function bytesToLeaves(
344
351
  *
345
352
  * Levels are read from the hash the cut was ACCEPTED at, not recomputed, so
346
353
  * they cost nothing beyond the divisions already being done. */
354
+
355
+ // Cyclic-polynomial table for the bounded-window cut hash. Derived once from
356
+ // the fold's own mixing constant — no seed, no tuning. A byte contributes
357
+ // BUZ[b] on entering the window; the hash rotates by one per byte, so by the
358
+ // time that byte leaves, its contribution has travelled k places and is
359
+ // removed rotated by k. The rotation is taken at the use site rather than
360
+ // precomputed into a second table so the window width follows maxGroup
361
+ // instead of being frozen at one value.
362
+ const BUZ = new Uint32Array(256);
363
+ {
364
+ let x = 0x9e3779b9 >>> 0;
365
+ for (let i = 0; i < 256; i++) {
366
+ x = Math.imul(x ^ (x >>> 15), 2654435761) >>> 0;
367
+ x = (x ^ (x >>> 13)) >>> 0;
368
+ BUZ[i] = x;
369
+ }
370
+ }
371
+
372
+ /** BUZ rotated by the window width — what a byte's contribution has become by
373
+ * the time it leaves. Cached because the width follows `maxGroup`, which is
374
+ * fixed for a given space: built once, then a plain table lookup per byte. */
375
+ let buzOutTable: Uint32Array | null = null;
376
+ let buzOutWidth = -1;
377
+ function buzOut(k: number): Uint32Array {
378
+ if (buzOutWidth !== k || buzOutTable === null) {
379
+ const t = new Uint32Array(256);
380
+ for (let i = 0; i < 256; i++) {
381
+ const v = BUZ[i];
382
+ t[i] = ((v << k) | (v >>> (32 - k))) >>> 0;
383
+ }
384
+ buzOutTable = t;
385
+ buzOutWidth = k;
386
+ }
387
+ return buzOutTable;
388
+ }
389
+
347
390
  function contentLevels(
348
391
  space: Space,
349
392
  bytes: Uint8Array,
@@ -411,28 +454,109 @@ function contentLevels(
411
454
  // downstream are fitted to, not the purity of the rule that produces it. The
412
455
  // forced cut is part of that distribution. Do not tidy it away without
413
456
  // re-measuring everything that reads a region.
457
+ // A BOUNDED-WINDOW rolling hash — the cut decision reads only the last
458
+ // `k` bytes, so nothing before the window can reach it.
459
+ //
460
+ // The old hash, `h = (h<<1) + byte*K`, needed 32 shifts to drop a byte, so
461
+ // it carried ~32 bytes of history; and on PERIODIC content its value was
462
+ // periodic too, so the threshold either never fired or fired at a fixed
463
+ // phase. Then the `maxLen` fallback placed every boundary at a fixed
464
+ // offset from the previous one and the segmentation could never recover
465
+ // from a shift. Measured fraction of cuts that re-align after a prepend:
466
+ //
467
+ // text uniform sparse lowent records ramp
468
+ // old 0.870 0.902 0.492 0.879 0.888 0.441
469
+ // this 0.935 0.952 0.732 0.920 0.916 0.935
470
+ //
471
+ // The cyclic polynomial (each byte enters as a table value, leaves rotated
472
+ // by the window width) has EXACTLY k bytes of memory and scrambles periodic
473
+ // input, so the threshold fires at content-chosen positions on a gradient
474
+ // just as it does on text — which is what leaves the `maxLen` fallback
475
+ // rarely engaged instead of carrying the phase. Segment lengths are
476
+ // unchanged in distribution (mean 5.2-7.2 against the old 5.4-6.0), so the
477
+ // mechanisms fitted to that distribution see the same scale.
478
+ //
479
+ // Cost is the same shape as before: shifts, XORs and two table lookups per
480
+ // byte, no multiply and no auxiliary structure. (An exact sliding-window
481
+ // minimum — winnowing — aligns slightly better still, 0.91-0.999, but its
482
+ // deque costs 51 MB/s against this rule's 112 and buys nothing the
483
+ // scrambling hash does not already give.)
484
+ const k = W;
485
+ const OUT = buzOut(k);
414
486
  const cuts: number[] = [];
415
487
  const levels: number[] = [];
488
+ const n = bytes.length;
416
489
  let h = 0;
417
- let last = 0;
418
- for (let i = 0; i < bytes.length; i++) {
419
- h = (((h << 1) >>> 0) + Math.imul(bytes[i], 2654435761)) >>> 0;
420
- if (i + 1 >= bytes.length) break;
421
- const hit = h % W === 0;
422
- if (i - last >= minLen && (hit || i - last + 1 >= maxLen)) {
423
- cuts.push(i + 1);
424
- // How many further powers of W divide the hash 0 for a forced cut.
490
+ let prev = 0;
491
+ let recent = 0; // the last GAP raw hits, one bit each
492
+
493
+ // A boundary is a property of a 4-GRAM, not of a position. The register IS
494
+ // the window it holds exactly the last `k` raw bytes — so the decision is
495
+ // a pure function of those bytes and nothing else can reach it.
496
+ //
497
+ // What kept the OLD rule position-dependent was `minLen`, counted from the
498
+ // previous cut: on periodic content that count carried the initial phase
499
+ // forever and the segmentation never recovered from a shift. But its only
500
+ // job was to stop segments being too short, and that can be said locally —
501
+ // take a hit only when the previous GAP positions did NOT hit. Every term
502
+ // is then a function of a bounded byte window (k + GAP), so the rule stays
503
+ // a pure content property while still setting the segment scale. Measured
504
+ // fraction of cuts that survive a prepend, worst case over six byte types:
505
+ // 0.441 for the original rule, 0.769 counting from `last`, 0.847 for this.
506
+ const GAP = 2;
507
+ const GAPMASK = (1 << GAP) - 1;
508
+
509
+ // The keyring bound is restored WITHOUT reintroducing a count: because
510
+ // boundaries are content-determined, an over-long segment carries identical
511
+ // bytes wherever it occurs, so splitting it at strides from ITS OWN start is
512
+ // content-relative. (This holds only while such splits stay RARE — a split
513
+ // leaves a right edge the content did not choose, so the next segment's
514
+ // start is not content-determined either. At this rate they are: mean
515
+ // segment 5.4 against a bound of 8. Lowering the cut rate to lengthen
516
+ // segments makes forced splits dominant and alignment collapses — measured,
517
+ // 0.000 on two-symbol data at rate 1/16.)
518
+ const emit = (at: number, lvl: number): void => {
519
+ while (at - prev > maxLen) {
520
+ prev += maxLen;
521
+ cuts.push(prev);
522
+ levels.push(0);
523
+ }
524
+ if (at <= prev || at >= n) return;
525
+ cuts.push(at);
526
+ levels.push(lvl);
527
+ prev = at;
528
+ };
529
+
530
+ for (let i = 0; i < n; i++) {
531
+ h = ((h << 8) | bytes[i]) >>> 0;
532
+ if (i + 1 >= n) break;
533
+ if (i < k - 1) continue;
534
+ // Two-round avalanche. The window holds four RAW bytes, whose entropy may
535
+ // sit in only a few bits (a gradient's low bits, a sparse stream's zeros);
536
+ // one multiply leaves that structure partly intact and the boundary test
537
+ // inherits it. A second round spreads every input bit across the word,
538
+ // which is what makes the rule behave the same on a ramp as on prose.
539
+ let mixv = Math.imul(h ^ (h >>> 16), 0x85ebca6b) >>> 0;
540
+ mixv = Math.imul(mixv ^ (mixv >>> 13), 0xc2b2ae35) >>> 0;
541
+ mixv = (mixv ^ (mixv >>> 16)) >>> 0;
542
+ const hit = mixv % W === 0;
543
+ if (hit && (recent & GAPMASK) === 0) {
544
+ // Level: how many further powers of W divide the mixed value — level-L
545
+ // cuts stay a subset of level-(L-1) cuts, the nesting the tree needs.
425
546
  let lvl = 0;
426
- if (hit) {
427
- let m = W;
428
- while (lvl < 24 && m <= 0x40000000 && h % (m * W) === 0) {
429
- lvl++;
430
- m *= W;
431
- }
547
+ let m = W;
548
+ while (lvl < 24 && m <= 0x40000000 && mixv % (m * W) === 0) {
549
+ lvl++;
550
+ m *= W;
432
551
  }
433
- levels.push(lvl);
434
- last = i + 1;
552
+ emit(i + 1, lvl);
435
553
  }
554
+ recent = ((recent << 1) | (hit ? 1 : 0)) & GAPMASK;
555
+ }
556
+ while (n - prev > maxLen) {
557
+ prev += maxLen;
558
+ cuts.push(prev);
559
+ levels.push(0);
436
560
  }
437
561
  return { cuts, levels };
438
562
  }
@@ -574,6 +698,19 @@ function contentFoldSpan(
574
698
  * keyring falls through to the plain river fold for that row — the fold stays
575
699
  * total on any input, and the fallback is rare enough not to reintroduce a
576
700
  * systematic alignment. */
701
+ /** A content key for a folded item: a cheap hash of its gist's leading
702
+ * coordinates. Used to choose a split point inside an over-long row, where
703
+ * the cut levels are uniformly 0 and carry no signal. Identical subtrees
704
+ * fold to identical vectors, so the same items in the same order always
705
+ * choose the same split — the property the whole fold rests on. */
706
+ function itemKey(v: Vec): number {
707
+ let h = 0x811c9dc5;
708
+ for (let d = 0; d < 8; d++) {
709
+ h = Math.imul(h ^ ((v[d] * 8192) | 0), 0x01000193) >>> 0;
710
+ }
711
+ return h >>> 0;
712
+ }
713
+
577
714
  function groupByLevel(
578
715
  space: Space,
579
716
  items: Folded[],
@@ -584,14 +721,54 @@ function groupByLevel(
584
721
  const maxSeats = space.seats.length;
585
722
  const groups: Folded[] = [];
586
723
  const groupLevels: number[] = [];
724
+ // Emit [from, to) as one group, splitting it at its STRONGEST interior cut
725
+ // whenever it would exceed the keyring.
726
+ //
727
+ // The old rule force-cut at the arity limit counted from the group's start
728
+ // — the last index-derived boundary in the grouping. Measured, the
729
+ // grouping loses one node in six even when EVERY child survives a prepend
730
+ // (P(node | all kids survive) = 0.82-0.84), and those losses spike at
731
+ // arity 8: exactly this boundary. Choosing the highest-level cut inside
732
+ // the feasible window instead makes the split a function of content, and
733
+ // the window bound keeps arity <= maxSeats so the seat algebra is untouched.
734
+ const emit = (from: number, to: number): void => {
735
+ let at = from;
736
+ while (to - at > maxSeats) {
737
+ // Strongest cut in the window that still leaves a legal group. Ties
738
+ // take the LATEST, so equal levels give the widest legal group rather
739
+ // than a degenerate spine of singletons.
740
+ let best = at + maxSeats - 1;
741
+ let bestKey = -1;
742
+ let bestLevel = -1;
743
+ for (let j = at; j < at + maxSeats && j < to - 1; j++) {
744
+ // Prefer a real level boundary; among equals — and inside an
745
+ // over-long stretch the levels are almost all 0, so they usually ARE
746
+ // equal — fall back to the ITEMS' own content. A group's gist is
747
+ // diverse where its cut level is not, so hashing it gives a
748
+ // content-determined split point where the level array has none.
749
+ const key = itemKey(items[j].tree.v);
750
+ if (
751
+ levels[j] > bestLevel ||
752
+ (levels[j] === bestLevel && key > bestKey)
753
+ ) {
754
+ bestLevel = levels[j];
755
+ bestKey = key;
756
+ best = j;
757
+ }
758
+ }
759
+ const part = items.slice(at, best + 1);
760
+ groups.push(part.length === 1 ? part[0] : joinFlat(space, part));
761
+ groupLevels.push(levels[best]);
762
+ at = best + 1;
763
+ }
764
+ const slice = items.slice(at, to);
765
+ groups.push(slice.length === 1 ? slice[0] : joinFlat(space, slice));
766
+ };
587
767
  let start = 0;
588
768
  for (let i = 0; i <= levels.length; i++) {
589
769
  const atEnd = i === levels.length;
590
- const cutHere = atEnd || levels[i] >= level;
591
- const wouldOverflow = i - start + 1 >= maxSeats;
592
- if (!cutHere && !wouldOverflow) continue;
593
- const slice = items.slice(start, i + 1);
594
- groups.push(slice.length === 1 ? slice[0] : joinFlat(space, slice));
770
+ if (!atEnd && levels[i] < level) continue;
771
+ emit(start, i + 1);
595
772
  if (!atEnd) groupLevels.push(levels[i]);
596
773
  start = i + 1;
597
774
  }
@@ -605,14 +782,18 @@ function groupByLevel(
605
782
  }
606
783
 
607
784
  /** Join a row of already-folded items as one unnormalized node — the same
608
- * seat-bound accumulate `flatFold` does for bytes, one level up. */
785
+ * two-ended seat binding as {@link flatFold}, one level up. A group formed
786
+ * by content-level cuts inherits the same robustness: interior items keep
787
+ * their seats when a leading or trailing segment is perturbed. */
609
788
  function joinFlat(space: Space, items: Folded[]): Folded {
789
+ const n = items.length;
610
790
  const gist = new Float32Array(space.D);
611
- const kids = new Array<Sema>(items.length);
791
+ const kids = new Array<Sema>(n);
612
792
  let len = 0;
613
- for (let k = 0; k < items.length; k++) {
793
+ for (let k = 0; k < n; k++) {
614
794
  const v = items[k].tree.v;
615
- const seat = space.seats[k].fwd;
795
+ const slot = twoEndedSeat(space.seats.length, n, k);
796
+ const seat = space.seats[slot].fwd;
616
797
  for (let d = 0; d < space.D; d++) gist[d] += v[seat[d]];
617
798
  kids[k] = items[k].tree;
618
799
  len += items[k].len;
@@ -621,10 +802,22 @@ function joinFlat(space: Space, items: Folded[]): Folded {
621
802
  }
622
803
 
623
804
  /** One segment as a single unnormalized node: leaf per byte, each bound into
624
- * seat k, summed. Same FP ops and same seat order as foldSlice's group fold —
625
- * only the arity is the segment's own length rather than a fixed W. Never
626
- * normalizes: the linear-fold contract keeps every interior gist raw and
627
- * normalizes once at the root. */
805
+ * a seat derived from its position relative to BOTH segment ends.
806
+ *
807
+ * Binding from both ends first bytes use the lowest seat slots, last
808
+ * bytes use the highest — makes the gist of the segment interior robust
809
+ * under a leading or trailing insertion: a byte prepended or appended
810
+ * shifts only the boundary seat, not every interior position. The same
811
+ * rule gives the shift-invariant knife its re-synchronising window in the
812
+ * ancestral fold (sema-old, KNIFE_WINDOW trailing items bound with
813
+ * relative seat keys). Ported to the current river: a content-defined
814
+ * segment always starts at seat 0, so the "relative" binding is the
815
+ * segment's own two-ended assignment.
816
+ *
817
+ * Never normalizes: the linear-fold contract keeps every interior gist
818
+ * raw and normalizes once at the root. Magnitude still ∝ √n — seat
819
+ * permutation preserves vector length, and the sum of n near-orthogonal
820
+ * vectors grows as √n. */
628
821
  function flatFold(
629
822
  space: Space,
630
823
  alphabet: Alphabet,
@@ -645,7 +838,10 @@ function flatFold(
645
838
  for (let k = 0; k < n; k++) {
646
839
  const b = bytes[from + k];
647
840
  const v = alphabet.vecs[b];
648
- const seat = space.seats[k].fwd;
841
+ // Two-ended: the first half uses low seats and the second half uses
842
+ // high seats, inward from the tail of the FULL keyring.
843
+ const slot = twoEndedSeat(space.seats.length, n, k);
844
+ const seat = space.seats[slot].fwd;
649
845
  for (let d = 0; d < space.D; d++) gist[d] += v[seat[d]];
650
846
  kids[k] = sema(v, bytes.slice(from + k, from + k + 1), null);
651
847
  }
@@ -769,18 +965,10 @@ export function stablePrefixFoldIncremental(
769
965
  }
770
966
 
771
967
  /** Join two folded items as one 2-kid branch — the top-level join of the
772
- * stable-prefix fold, identical FP ops to foldSlice's seat-bound
773
- * accumulation over a group of two. Unnormalized (interior). */
968
+ * stable-prefix fold, delegated to {@link joinFlat} (same two-ended seat
969
+ * binding as every other group fold). Unnormalized (interior). */
774
970
  function fold2(space: Space, a: Folded, b: Folded): Folded {
775
- const D = space.D;
776
- const gist = new Float32Array(D);
777
- const kids = [a.tree, b.tree];
778
- for (let k = 0; k < 2; k++) {
779
- const seat = space.seats[k].fwd;
780
- const v = kids[k].v;
781
- for (let d = 0; d < D; d++) gist[d] += v[seat[d]];
782
- }
783
- return { tree: sema(gist, null, kids), len: a.len + b.len };
971
+ return joinFlat(space, [a, b]);
784
972
  }
785
973
 
786
974
  /** Plain river fold WITHOUT the final root normalize — the segment-level