@hviana/sema 0.3.0 → 0.4.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.
Files changed (77) hide show
  1. package/AGENTS.md +71 -5
  2. package/dist/src/derive/src/deduction.d.ts +12 -1
  3. package/dist/src/derive/src/deduction.js +5 -1
  4. package/dist/src/derive/src/index.d.ts +1 -0
  5. package/dist/src/geometry.d.ts +3 -30
  6. package/dist/src/geometry.js +330 -82
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/meter.d.ts +171 -0
  10. package/dist/src/meter.js +269 -0
  11. package/dist/src/mind/attention.d.ts +0 -4
  12. package/dist/src/mind/attention.js +251 -23
  13. package/dist/src/mind/bridge.d.ts +10 -1
  14. package/dist/src/mind/bridge.js +179 -10
  15. package/dist/src/mind/canonical.d.ts +6 -1
  16. package/dist/src/mind/canonical.js +6 -1
  17. package/dist/src/mind/graph-search.d.ts +9 -0
  18. package/dist/src/mind/graph-search.js +59 -19
  19. package/dist/src/mind/junction.d.ts +10 -0
  20. package/dist/src/mind/junction.js +14 -0
  21. package/dist/src/mind/match.d.ts +40 -0
  22. package/dist/src/mind/match.js +125 -1
  23. package/dist/src/mind/mechanisms/cast.js +63 -6
  24. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  25. package/dist/src/mind/mechanisms/extraction.js +1 -88
  26. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  27. package/dist/src/mind/mechanisms/recall.js +77 -14
  28. package/dist/src/mind/mind.d.ts +55 -4
  29. package/dist/src/mind/mind.js +110 -91
  30. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  31. package/dist/src/mind/pipeline-mechanism.js +179 -10
  32. package/dist/src/mind/pipeline.js +52 -10
  33. package/dist/src/mind/primitives.d.ts +11 -15
  34. package/dist/src/mind/primitives.js +47 -28
  35. package/dist/src/mind/reasoning.d.ts +7 -1
  36. package/dist/src/mind/reasoning.js +40 -8
  37. package/dist/src/mind/recognition.js +93 -20
  38. package/dist/src/mind/traverse.d.ts +11 -0
  39. package/dist/src/mind/traverse.js +58 -5
  40. package/dist/src/mind/types.d.ts +28 -5
  41. package/dist/src/store.d.ts +15 -0
  42. package/dist/src/store.js +91 -6
  43. package/package.json +1 -1
  44. package/src/derive/src/deduction.ts +15 -0
  45. package/src/derive/src/index.ts +1 -0
  46. package/src/geometry.ts +350 -122
  47. package/src/index.ts +1 -0
  48. package/src/meter.ts +333 -0
  49. package/src/mind/attention.ts +268 -31
  50. package/src/mind/bridge.ts +187 -10
  51. package/src/mind/canonical.ts +6 -1
  52. package/src/mind/graph-search.ts +60 -21
  53. package/src/mind/junction.ts +12 -0
  54. package/src/mind/match.ts +146 -1
  55. package/src/mind/mechanisms/cast.ts +62 -6
  56. package/src/mind/mechanisms/extraction.ts +2 -103
  57. package/src/mind/mechanisms/recall.ts +84 -17
  58. package/src/mind/mind.ts +139 -98
  59. package/src/mind/pipeline-mechanism.ts +203 -13
  60. package/src/mind/pipeline.ts +65 -8
  61. package/src/mind/primitives.ts +49 -33
  62. package/src/mind/reasoning.ts +39 -7
  63. package/src/mind/recognition.ts +89 -19
  64. package/src/mind/traverse.ts +59 -5
  65. package/src/mind/types.ts +28 -5
  66. package/src/store.ts +75 -6
  67. package/test/14-scaling.test.mjs +17 -7
  68. package/test/31-audit.test.mjs +4 -1
  69. package/test/33-multi-candidate.test.mjs +13 -1
  70. package/test/36-already-answered-fusion.test.mjs +10 -3
  71. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  72. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  73. package/test/55-cost-meter.test.mjs +284 -0
  74. package/test/56-bridge-identity-admission.test.mjs +209 -0
  75. package/test/57-fusion-order.test.mjs +104 -0
  76. package/test/58-subquantum-sites.test.mjs +112 -0
  77. package/test/59-fold-invariance.test.mjs +226 -0
package/src/geometry.ts CHANGED
@@ -284,6 +284,167 @@ function bytesToLeaves(
284
284
  });
285
285
  }
286
286
 
287
+ /** CONTENT-DEFINED FOLD BOUNDARIES — where a byte stream segments, chosen by
288
+ * the bytes rather than by arithmetic.
289
+ *
290
+ * `riverFold` groups FIXED arity from byte 0 and permutes item k by
291
+ * `seats[k]`, k = index mod W, so a byte's contribution is a function of its
292
+ * ABSOLUTE OFFSET: the same byte run is a different vector at a different
293
+ * position, and the same content is a different SUBTREE. W's size has nothing
294
+ * to do with it — any fixed modulus does this, and identity must not depend on
295
+ * W at all.
296
+ *
297
+ * A rolling hash removes the dependence without touching the fold's arity: the
298
+ * cut lands where the hash of the recent bytes vanishes mod W, so a change
299
+ * upstream moves only the cut it falls inside — every downstream boundary, and
300
+ * therefore every downstream segment, is unchanged. Because
301
+ * {@link stablePrefixFold} folds each segment independently from its own slot
302
+ * 0, byte-identical content then produces byte-identical subtrees, and
303
+ * hash-consing makes it the SAME NODE ID wherever it appears. That — not the
304
+ * root cosine, which should and does still move when content is added — is
305
+ * what recognition, cover and resolve need.
306
+ *
307
+ * Measured over 400 real deposits, shifts of 1..7 bytes, downstream cuts
308
+ * preserved / segments byte-identical:
309
+ *
310
+ * content-defined, deposit text 99.7% / 98.4% mean seg 6.61 B
311
+ * content-defined, non-Latin scripts 99.6% / 98.3% mean seg 6.43 B
312
+ * content-defined, random binary 99.9% / 99.2% mean seg 6.92 B
313
+ * the arithmetic grid, same corpus 14.3% (only the k≡0 mod W
314
+ * shifts survive)
315
+ *
316
+ * The three content-defined rows agreeing is the load-bearing part: this reads
317
+ * BYTES, never text. Mind is not a text engine — the same fold carries grids
318
+ * and any other modality — so a boundary rule justified by where words or
319
+ * sentences fall would be importing an assumption the architecture rejects.
320
+ * Random binary must, and does, behave exactly like prose.
321
+ *
322
+ * Every constant is derived (§2.2): the cut mask is W, so a cut is offered once
323
+ * per quantum of bytes — which, composed with the minimum below, puts the
324
+ * expected segment at minLen + W − 1 ≈ 6 B rather than at W, deliberately (see
325
+ * the refutation recorded at `cutRate` in {@link contentLevels}: a segment is
326
+ * the flat PHRASE-scale unit the W-ary groups are built from, not a group of W
327
+ * children, and forcing E[len] = W costs 15 tests). The minimum is W−1, `canonicalWindows`'s
328
+ * straddle neighbour and the write side's own floor for a unit; and the maximum
329
+ * is the KEYRING's seat count, because a segment folds as ONE flat node and
330
+ * `fold` has exactly that many seats to bind children into. Capping there is
331
+ * what keeps the fold light: a segment of 3..seats leaves is a single node,
332
+ * where splitting it into W-groups plus a remainder would cost two or three
333
+ * and the remainders barely share (measured: partial-arity nodes 504 → 3,590,
334
+ * and total distinct nodes 8,142 → 9,712, when segments folded as [W][rest]). */
335
+ /** {@link contentBoundaries} plus, for each cut, its LEVEL — how deep in the
336
+ * tree that cut reaches.
337
+ *
338
+ * One rolling hash serves every scale. A cut is level 0 when its hash vanishes
339
+ * mod W, level 1 when mod W², and so on: level-L cuts are by construction a
340
+ * subset of level-(L−1) cuts, which is exactly the nesting a tree needs. The
341
+ * expected span of a level-L node is therefore W^(L+1) bytes — the same growth
342
+ * the grid fold had, but with boundaries the content chose, so a shift moves
343
+ * one node at each level instead of all of them.
344
+ *
345
+ * Levels are read from the hash the cut was ACCEPTED at, not recomputed, so
346
+ * they cost nothing beyond the divisions already being done. */
347
+ function contentLevels(
348
+ space: Space,
349
+ bytes: Uint8Array,
350
+ ): { cuts: number[]; levels: number[] } {
351
+ const W = space.maxGroup;
352
+ const minLen = W - 1;
353
+ const maxLen = space.seats.length;
354
+ // MEASURED AND REFUTED — making E[segment] equal W. A segment is at least
355
+ // `minLen` bytes and then cuts with probability p, so E[len] = minLen +
356
+ // (1−p)/p; with `h % W === 0` that is minLen + W − 1 = 6 for W = 4 (measured
357
+ // mean 6.61 B on real deposits), half again coarser than the fold's own
358
+ // arity, and the doc above used to claim the mask made the two equal. It
359
+ // does not, and the mismatch looked like the cause of a real deficit: the
360
+ // climb perceives one region per segment, so on `Michelangelo is to sculpture
361
+ // as who is to literature?` it sees 9 regions where the grid saw 19, reaches
362
+ // one ranked anchor instead of two, and CAST's weave never includes the
363
+ // writing-domain exemplar it needs (test/29 A2, C1).
364
+ //
365
+ // Fixing the rate to hit E[len] = W (a threshold `h % W < W − minLen + 1` on
366
+ // the same rolling value, keeping the two bytes of history) does exactly what
367
+ // it says — mean segment 3.60 B on text, 3.93 B on random binary, 14 segments
368
+ // on that query instead of 9 — and takes the suite from 3 failures to 18,
369
+ // across think, universality, scaling, decomposition, bridge, generalization,
370
+ // saturation, audit and recognition. The coarser-than-arity scale is
371
+ // load-bearing: W is the arity `riverFold` groups CHILDREN at, and a segment
372
+ // is not a group of W children but the flat unit those groups are built from
373
+ // — a phrase-scale unit, and the mechanisms above all read it that way.
374
+ // Do not re-derive the rate from W.
375
+ //
376
+ // MEASURED AND REFUTED — reading the hash's HIGH bits instead of its low
377
+ // ones. The accumulator shifts LEFT, so `h % W` is the part the mixing has
378
+ // had no chance to reach: with `h = (h << 1) + byte·K` and K odd, `h mod 4`
379
+ // reduces to `(2·(prev mod 2) + byte) mod 4` — two bytes of history, and only
380
+ // their parity. It reads like a hash test that is really a byte-parity test,
381
+ // and it shows: on `Michelangelo is to sculpture as who is to literature?`
382
+ // the segments come out 8,8,8,8,8,7,4,2 — almost every boundary the FORCED
383
+ // one at maxLen, which is the one offset-dependent cut in the rule.
384
+ //
385
+ // Testing the top bits instead (`h < 2^32 / W`, the same 1/W rate, with the
386
+ // level test falling out as `h < 2^32 / W^(L+1)` — cleaner nesting than
387
+ // divisibility) takes the suite from 2 failures to 21 AND breaks the
388
+ // invariance floor in test/59. The reason is the whole point: in a
389
+ // shift-accumulator the LOW bits have a short effective window and the HIGH
390
+ // bits a long one, so a disturbance upstream perturbs the high bits for the
391
+ // next ~32 bytes while the low bits re-sync within two. The narrow window is
392
+ // not a defect in this hash — it IS the invariance. A boundary rule here is
393
+ // choosing how far a change may propagate, not how well the bits mix.
394
+ //
395
+ // MEASURED AND REFUTED — normalized chunking, to shrink the forced cut. The
396
+ // hard cut at maxLen is the ONE offset-dependent boundary left in the rule,
397
+ // and it is not rare: 32% of segments on a mixed sample end there, five of
398
+ // the eight in the query above, so for those streams the fold IS a grid of 2W
399
+ // with all of the grid's phase. The standard remedy applies cleanly here —
400
+ // past a target length, relax the mask by one power of the same radix
401
+ // (W → W/2), still content-defined and so still invariant, just coarser
402
+ // evidence for a boundary; both quantities are already in the rule, nothing
403
+ // introduced. It works as advertised: forced cuts fall from 32% to 8.3% and
404
+ // test/59's invariance floors still hold.
405
+ //
406
+ // And the suite goes from 2 failures to 5 relaxing at the expected length
407
+ // (minLen + W − 1), or to 6 relaxing at the last opportunity (maxLen − 1) —
408
+ // 22-multihop, 24-generalization, 29, 36. Taken with the two results above
409
+ // (changing the rate costs 15 tests; changing which bits are read costs 19),
410
+ // the reading is that the segment DISTRIBUTION is what the mechanisms
411
+ // downstream are fitted to, not the purity of the rule that produces it. The
412
+ // forced cut is part of that distribution. Do not tidy it away without
413
+ // re-measuring everything that reads a region.
414
+ const cuts: number[] = [];
415
+ const levels: number[] = [];
416
+ 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.
425
+ 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
+ }
432
+ }
433
+ levels.push(lvl);
434
+ last = i + 1;
435
+ }
436
+ }
437
+ return { cuts, levels };
438
+ }
439
+
440
+ export function contentBoundaries(space: Space, bytes: Uint8Array): number[] {
441
+ // ONE implementation of the rule. This used to carry its own copy of the
442
+ // rolling-hash loop, which is exactly how a write side and a read side drift
443
+ // apart without a type error; the levels are computed from the hash the cut
444
+ // was accepted at, so asking for them costs nothing but an array.
445
+ return contentLevels(space, bytes).cuts;
446
+ }
447
+
287
448
  /** Find the longest prefix of `bytes` whose leaf-id signature matches a
288
449
  * known branch via `lookup`. Returns the byte-length of that prefix, or 0. */
289
450
  export function knownPrefixLength(
@@ -335,15 +496,160 @@ export function bytesToTree(
335
496
  if (bytes.length === 0) {
336
497
  return sema(alphabet.vecs[0], new Uint8Array(0), null);
337
498
  }
338
- if (boundaries !== undefined && boundaries.length > 0) {
339
- return stablePrefixFold(space, alphabet, bytes, boundaries);
340
- }
499
+ // WHERE A STREAM SEGMENTS IS DECIDED BY ITS BYTES ({@link contentBoundaries}),
500
+ // and the segments then fold BALANCED, W-ary, like any other row of items.
501
+ //
502
+ // The two must not be confused. Content cuts are what make identity
503
+ // offset-free: a segment folds from its own slot 0, so the same bytes give the
504
+ // same subtree wherever they sit. The shape ABOVE the segments is a separate
505
+ // question, and it must stay the river's own — grouping W at a time, depth
506
+ // log_W(n). Joining segments left-nested instead (as the stable-prefix fold
507
+ // does, for its own good reason) costs a node per segment on a single spine:
508
+ // at a cut every ~6.6 B a 3 KB deposit becomes a 450-deep spine of 450 fresh
509
+ // D-vectors, ~1.8 MB for one deposit, and every walker above inherits the
510
+ // depth. That is an implementation blunder, not a property of content-defined
511
+ // folding, and it is what riverFold below avoids.
512
+ //
513
+ // Caller-supplied boundaries stay left-nested (see stablePrefixFold): there are
514
+ // a handful of them, one per conversation turn, and the cumulative-context-root
515
+ // contract depends on that shape. Each SPAN between them content-folds.
341
516
  const sb = (leafAt && lookup) ? knownPrefixLength(bytes, leafAt, lookup) : 0;
342
- return riverFold(
517
+ const outer = new Set<number>();
518
+ if (boundaries !== undefined) { for (const b of boundaries) outer.add(b); }
519
+ if (sb > 0) outer.add(sb);
520
+ if (outer.size === 0) {
521
+ return rootOf(contentFoldSpan(space, alphabet, bytes, 0, bytes.length));
522
+ }
523
+ return stablePrefixFold(
343
524
  space,
344
- bytesToLeaves(alphabet, bytes),
345
- sb > 0 ? sb : bytes.length,
346
- ).tree;
525
+ alphabet,
526
+ bytes,
527
+ [...outer].sort((a, b) => a - b),
528
+ );
529
+ }
530
+
531
+ /** One span, folded over its own content cuts — AT EVERY LEVEL.
532
+ *
533
+ * A segment becomes ONE FLAT NODE: every leaf bound into its own seat and
534
+ * summed, arity = the segment's length. `contentBoundaries` caps a segment at
535
+ * the keyring's seat count so this is always possible, and the flat form is
536
+ * both lighter (one node per segment instead of a [W][remainder] pair) and the
537
+ * natural unit — a segment IS the smallest thing the cuts claim is a unit.
538
+ *
539
+ * Above the segments the cutting RECURSES rather than reverting to the grid.
540
+ * Grouping segment roots W-at-a-time from index 0 would reintroduce the very
541
+ * bug content cuts exist to remove, one level up: a form spanning segments
542
+ * 12..17 straddles the [12-15] and [16-17] groups and is no node at all, so
543
+ * recognition can only reach it by an alignment accident (test/44 pins exactly
544
+ * this — at HEAD the grid happened to put a node one byte before the target).
545
+ * {@link contentLevels} assigns each cut a level from how divisible its hash
546
+ * is, so level-L cuts are a subset of level-(L−1) cuts and every node at every
547
+ * scale is delimited by content. Identity is then offset-free at all scales,
548
+ * which is the whole requirement — it must not depend on W. */
549
+ function contentFoldSpan(
550
+ space: Space,
551
+ alphabet: Alphabet,
552
+ bytes: Uint8Array,
553
+ from: number,
554
+ to: number,
555
+ ): Folded {
556
+ const span = bytes.subarray(from, to);
557
+ const { cuts, levels } = contentLevels(space, span);
558
+ const edges = [0, ...cuts, span.length];
559
+ const segs: Folded[] = [];
560
+ for (let i = 0; i + 1 < edges.length; i++) {
561
+ segs.push(flatFold(space, alphabet, span, edges[i], edges[i + 1]));
562
+ }
563
+ if (segs.length > 1) return groupByLevel(space, segs, levels, 1);
564
+ return segs[0];
565
+ }
566
+
567
+ /** Group a row of items by the level of the cut BETWEEN them: items separated
568
+ * by a cut of level < L belong to the same parent, and a cut of level ≥ L ends
569
+ * it. Recurses upward until one root remains, so the shape at every level is
570
+ * the content's, not an index's. `levels[i]` is the level of the cut that
571
+ * precedes item i+1 (there are items.length − 1 of them).
572
+ *
573
+ * A level that fails to split (every cut below L) or that would exceed the
574
+ * keyring falls through to the plain river fold for that row — the fold stays
575
+ * total on any input, and the fallback is rare enough not to reintroduce a
576
+ * systematic alignment. */
577
+ function groupByLevel(
578
+ space: Space,
579
+ items: Folded[],
580
+ levels: number[],
581
+ level: number,
582
+ ): Folded {
583
+ if (items.length === 1) return items[0];
584
+ const maxSeats = space.seats.length;
585
+ const groups: Folded[] = [];
586
+ const groupLevels: number[] = [];
587
+ let start = 0;
588
+ for (let i = 0; i <= levels.length; i++) {
589
+ 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));
595
+ if (!atEnd) groupLevels.push(levels[i]);
596
+ start = i + 1;
597
+ }
598
+ if (groups.length === items.length) {
599
+ // This level split nothing — climb rather than spin.
600
+ return level < 24
601
+ ? groupByLevel(space, items, levels, level + 1)
602
+ : riverFoldRaw(space, items);
603
+ }
604
+ return groupByLevel(space, groups, groupLevels, level + 1);
605
+ }
606
+
607
+ /** Join a row of already-folded items as one unnormalized node — the same
608
+ * seat-bound accumulate `flatFold` does for bytes, one level up. */
609
+ function joinFlat(space: Space, items: Folded[]): Folded {
610
+ const gist = new Float32Array(space.D);
611
+ const kids = new Array<Sema>(items.length);
612
+ let len = 0;
613
+ for (let k = 0; k < items.length; k++) {
614
+ const v = items[k].tree.v;
615
+ const seat = space.seats[k].fwd;
616
+ for (let d = 0; d < space.D; d++) gist[d] += v[seat[d]];
617
+ kids[k] = items[k].tree;
618
+ len += items[k].len;
619
+ }
620
+ return { tree: sema(gist, null, kids), len };
621
+ }
622
+
623
+ /** 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. */
628
+ function flatFold(
629
+ space: Space,
630
+ alphabet: Alphabet,
631
+ bytes: Uint8Array,
632
+ from: number,
633
+ to: number,
634
+ ): Folded {
635
+ const n = to - from;
636
+ if (n === 1) {
637
+ const b = bytes[from];
638
+ return {
639
+ tree: sema(alphabet.vecs[b], bytes.slice(from, to), null),
640
+ len: 1,
641
+ };
642
+ }
643
+ const gist = new Float32Array(space.D);
644
+ const kids = new Array<Sema>(n);
645
+ for (let k = 0; k < n; k++) {
646
+ const b = bytes[from + k];
647
+ const v = alphabet.vecs[b];
648
+ const seat = space.seats[k].fwd;
649
+ for (let d = 0; d < space.D; d++) gist[d] += v[seat[d]];
650
+ kids[k] = sema(v, bytes.slice(from + k, from + k + 1), null);
651
+ }
652
+ return { tree: sema(gist, null, kids), len: n };
347
653
  }
348
654
 
349
655
  /** The stable-prefix segmented fold (§10.3). Each segment between
@@ -354,6 +660,22 @@ export function bytesToTree(
354
660
  * boundary, so the left-nested join reproduces every intermediate learnt
355
661
  * root ((s₀·s₁) IS the root the store learnt for the first two segments'
356
662
  * bytes, and so on). */
663
+ /** A fold's ROOT: ONE normalize per perception, at the root, exactly as
664
+ * riverFold did — the interior stays raw (the linear-fold contract).
665
+ *
666
+ * Normalizes EXCEPT when the whole stream folded to a single
667
+ * leaf: a leaf's vector IS the alphabet's own, shared by every occurrence of
668
+ * that byte, and `normalize` writes in place. Every fold entry point returns
669
+ * through here, because the guard is exactly the kind that gets written at one
670
+ * site and missed at the next two — which is what had happened: only
671
+ * {@link bytesToTree} carried it, while `stablePrefixFold` and its incremental
672
+ * twin normalized unconditionally, reachable by a one-byte stream whose only
673
+ * boundary is its own length. */
674
+ function rootOf(f: Folded): Sema {
675
+ if (f.tree.kids !== null) normalize(f.tree.v);
676
+ return f.tree;
677
+ }
678
+
357
679
  function stablePrefixFold(
358
680
  space: Space,
359
681
  alphabet: Alphabet,
@@ -369,18 +691,16 @@ function stablePrefixFold(
369
691
  }
370
692
  }
371
693
  if (cuts.length === 0) {
372
- return riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
694
+ return rootOf(contentFoldSpan(space, alphabet, bytes, 0, bytes.length));
373
695
  }
374
696
  const edges = [0, ...cuts, bytes.length];
375
697
  const segs: Folded[] = [];
376
698
  for (let i = 0; i + 1 < edges.length; i++) {
377
- const seg = bytes.subarray(edges[i], edges[i + 1]);
378
- segs.push(riverFoldRaw(space, bytesToLeaves(alphabet, seg)));
699
+ segs.push(contentFoldSpan(space, alphabet, bytes, edges[i], edges[i + 1]));
379
700
  }
380
701
  let cur = segs[0];
381
702
  for (let i = 1; i < segs.length; i++) cur = fold2(space, cur, segs[i]);
382
- normalize(cur.tree.v);
383
- return cur.tree;
703
+ return rootOf(cur);
384
704
  }
385
705
 
386
706
  /** A stable-prefix fold's reusable state: the segment edge offsets and each
@@ -388,9 +708,8 @@ function stablePrefixFold(
388
708
  * grown stream whose boundary set EXTENDS a previous fold's reuses every
389
709
  * matching segment's Folded unchanged (segments fold independently by
390
710
  * construction, so reuse is bit-identical to refolding) and folds only the
391
- * new right-edge segment — O(turn) per extension, the stable-prefix
392
- * counterpart of {@link FoldPyramid}. Purely a cache: the produced tree
393
- * never depends on cache state. */
711
+ * new right-edge segment — O(turn) per extension. Purely a cache: the
712
+ * produced tree never depends on cache state. */
394
713
  export interface StableFold {
395
714
  edges: number[];
396
715
  segs: Folded[];
@@ -424,23 +743,29 @@ export function stablePrefixFoldIncremental(
424
743
  ? prev.segs[i]
425
744
  : undefined;
426
745
  segs.push(
427
- hit ??
428
- riverFoldRaw(
429
- space,
430
- bytesToLeaves(alphabet, bytes.subarray(edges[i], edges[i + 1])),
431
- ),
746
+ hit ?? contentFoldSpan(space, alphabet, bytes, edges[i], edges[i + 1]),
432
747
  );
433
748
  }
434
749
  if (segs.length === 1) {
435
- // Degenerate boundary set — the plain fold, as stablePrefixFold does.
436
- const tree =
437
- riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
750
+ // Degenerate boundary set — one span, which IS the whole stream, and it was
751
+ // just folded (or reused from `prev`) right above. It cannot go through
752
+ // `rootOf`: the ROOT is normalized, a cached SEGMENT never is (a later turn
753
+ // reuses it as one), and `normalize` writes in place. Re-folding `bytes`
754
+ // to get a separate object is what this used to do — and a first-seen
755
+ // deposit has no boundaries, so it always lands here, paying the fold
756
+ // twice. Copying the gist is the same result for one vector copy. A
757
+ // single LEAF needs neither: its vector is the shared alphabet entry and
758
+ // must not be written at all.
759
+ const only = segs[0].tree;
760
+ const tree = only.kids === null
761
+ ? only
762
+ : sema(Float32Array.from(only.v), null, only.kids);
763
+ if (tree.kids !== null) normalize(tree.v);
438
764
  return { tree, fold: { edges, segs } };
439
765
  }
440
766
  let cur = segs[0];
441
767
  for (let i = 1; i < segs.length; i++) cur = fold2(space, cur, segs[i]);
442
- normalize(cur.tree.v);
443
- return { tree: cur.tree, fold: { edges, segs } };
768
+ return { tree: rootOf(cur), fold: { edges, segs } };
444
769
  }
445
770
 
446
771
  /** Join two folded items as one 2-kid branch — the top-level join of the
@@ -526,103 +851,6 @@ export function composeStructuralGist(
526
851
  return result;
527
852
  }
528
853
 
529
- // ---- pyramid fold (incremental plain perception) ----
530
-
531
- /** The PLAIN fold's full level pyramid — every level's item list, bottom
532
- * (leaves) to top (root). Left-grouped folding is RADIX-ALIGNED: the item
533
- * at level L, index i, covers exactly bytes [i·mg^L, (i+1)·mg^L) whenever
534
- * it is a FULL block, and a full block folds bit-identically in ANY byte
535
- * string that contains it at that offset. So a string extended by a
536
- * suffix (a conversation's accumulated context) reuses every full block of
537
- * its prefix's pyramid and refolds only the right edge of each level —
538
- * O(suffix + depth·mg) per extension instead of O(whole), with the
539
- * produced tree BIT-IDENTICAL to a from-scratch plain fold (same nodes,
540
- * same FP ops; reused subtrees are shared objects, and Sema nodes are
541
- * never mutated). Purely an implementation cache: structure and numerics
542
- * never depend on whether a pyramid was available. */
543
- export interface FoldPyramid {
544
- levels: Array<Array<{ tree: Sema; len: number }>>;
545
- bytes: number;
546
- }
547
-
548
- /** Plain bytes→tree (identical to capability-less {@link bytesToTree}) that
549
- * also RETURNS its pyramid, reusing `prev` — the pyramid of a PROPER
550
- * prefix of `bytes` (caller guarantees content match and
551
- * prev.bytes < bytes.length). */
552
- export function bytesToTreePyramid(
553
- space: Space,
554
- alphabet: Alphabet,
555
- bytes: Uint8Array,
556
- prev?: FoldPyramid,
557
- ): { tree: Sema; pyramid: FoldPyramid } {
558
- if (bytes.length === 0) {
559
- return {
560
- tree: sema(alphabet.vecs[0], new Uint8Array(0), null),
561
- pyramid: { levels: [], bytes: 0 },
562
- };
563
- }
564
- // ZERO GROWTH: the previous pyramid already covers every byte — its top
565
- // level holds the finished, normalized root. Refolding here would not
566
- // only waste the work: when the whole span is one full block, the refold's
567
- // top item is a REUSED raw interior of `prev`, and the final normalize
568
- // below would mutate that shared raw block in place, corrupting every
569
- // later incremental fold built on `prev`.
570
- if (prev && prev.bytes === bytes.length && prev.levels.length > 0) {
571
- return {
572
- tree: prev.levels[prev.levels.length - 1][0].tree,
573
- pyramid: prev,
574
- };
575
- }
576
- const mg = space.maxGroup;
577
- const reusable = (L: number): ReadonlyArray<Folded> | null => {
578
- // prev's TOPMOST level holds its normalized ROOT — reusable blocks must
579
- // be raw interiors, so the top level is always excluded.
580
- if (!prev || L > prev.levels.length - 2) return null;
581
- return prev.levels[L];
582
- };
583
- // Level 0: reuse the prefix's leaf items wholesale (all are full blocks).
584
- const lv0 = reusable(0);
585
- const row: Folded[] = lv0
586
- ? [...lv0, ...bytesToLeaves(alphabet, bytes.subarray(prev!.bytes))]
587
- : bytesToLeaves(alphabet, bytes);
588
- const levels: Folded[][] = [row];
589
- let level = row;
590
- let L = 0;
591
- while (level.length > 1) {
592
- let next: Folded[] = [];
593
- // Reuse the leading FULL blocks of prev's level L+1 (len == mg^(L+1),
594
- // wholly inside the prefix); the rule below folds the rest from this
595
- // level's items exactly as the from-scratch fold would.
596
- const blockLen = mg ** (L + 1);
597
- let reused = 0;
598
- const cand = reusable(L + 1);
599
- if (cand) {
600
- const maxFull = Math.floor(prev!.bytes / blockLen);
601
- while (
602
- reused < maxFull && reused < cand.length &&
603
- cand[reused].len === blockLen
604
- ) reused++;
605
- for (let i = 0; i < reused; i++) next.push(cand[i]);
606
- }
607
- foldSlice(
608
- space,
609
- level,
610
- reused * mg,
611
- level.length - reused * mg,
612
- next,
613
- true,
614
- );
615
- levels.push(next);
616
- level = next;
617
- L++;
618
- }
619
- if (row.length > 1) normalize(level[0].tree.v);
620
- return {
621
- tree: level[0].tree,
622
- pyramid: { levels, bytes: bytes.length },
623
- };
624
- }
625
-
626
854
  // ---- n-D Hilbert curve ----
627
855
 
628
856
  function gridDims(grid: Grid): number[] {
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./sema.js";
7
7
  export * from "./alphabet.js";
8
8
  export * from "./geometry.js";
9
9
  export * from "./store.js";
10
+ export * from "./meter.js";
10
11
  export * from "./mind/rationale.js";
11
12
  export * from "./mind/index.js";
12
13
  export * from "./store-sqlite.js";