@hviana/sema 0.2.9 → 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.
- package/AGENTS.md +71 -5
- package/dist/src/derive/src/deduction.d.ts +12 -1
- package/dist/src/derive/src/deduction.js +5 -1
- package/dist/src/derive/src/index.d.ts +1 -0
- package/dist/src/geometry.d.ts +3 -30
- package/dist/src/geometry.js +330 -82
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/meter.d.ts +171 -0
- package/dist/src/meter.js +269 -0
- package/dist/src/mind/attention.d.ts +5 -4
- package/dist/src/mind/attention.js +254 -23
- package/dist/src/mind/bridge.d.ts +10 -1
- package/dist/src/mind/bridge.js +179 -10
- package/dist/src/mind/canonical.d.ts +6 -1
- package/dist/src/mind/canonical.js +6 -1
- package/dist/src/mind/graph-search.d.ts +9 -0
- package/dist/src/mind/graph-search.js +59 -19
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +10 -0
- package/dist/src/mind/junction.js +14 -0
- package/dist/src/mind/learning.d.ts +32 -4
- package/dist/src/mind/learning.js +26 -4
- package/dist/src/mind/match.d.ts +40 -0
- package/dist/src/mind/match.js +125 -1
- package/dist/src/mind/mechanisms/cast.js +63 -6
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
- package/dist/src/mind/mechanisms/extraction.js +1 -88
- package/dist/src/mind/mechanisms/recall.d.ts +3 -0
- package/dist/src/mind/mechanisms/recall.js +77 -14
- package/dist/src/mind/mind.d.ts +59 -5
- package/dist/src/mind/mind.js +115 -93
- package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
- package/dist/src/mind/pipeline-mechanism.js +179 -10
- package/dist/src/mind/pipeline.d.ts +29 -0
- package/dist/src/mind/pipeline.js +79 -21
- package/dist/src/mind/primitives.d.ts +11 -15
- package/dist/src/mind/primitives.js +47 -28
- package/dist/src/mind/reasoning.d.ts +7 -1
- package/dist/src/mind/reasoning.js +40 -8
- package/dist/src/mind/recognition.js +93 -20
- package/dist/src/mind/traverse.d.ts +11 -0
- package/dist/src/mind/traverse.js +88 -7
- package/dist/src/mind/types.d.ts +39 -5
- package/dist/src/store.d.ts +15 -0
- package/dist/src/store.js +91 -6
- package/package.json +1 -1
- package/src/derive/src/deduction.ts +15 -0
- package/src/derive/src/index.ts +1 -0
- package/src/geometry.ts +350 -122
- package/src/index.ts +1 -0
- package/src/meter.ts +333 -0
- package/src/mind/attention.ts +276 -31
- package/src/mind/bridge.ts +187 -10
- package/src/mind/canonical.ts +6 -1
- package/src/mind/graph-search.ts +60 -21
- package/src/mind/index.ts +6 -0
- package/src/mind/junction.ts +12 -0
- package/src/mind/learning.ts +46 -5
- package/src/mind/match.ts +146 -1
- package/src/mind/mechanisms/cast.ts +62 -6
- package/src/mind/mechanisms/extraction.ts +2 -103
- package/src/mind/mechanisms/recall.ts +84 -17
- package/src/mind/mind.ts +144 -99
- package/src/mind/pipeline-mechanism.ts +203 -13
- package/src/mind/pipeline.ts +144 -36
- package/src/mind/primitives.ts +49 -33
- package/src/mind/reasoning.ts +39 -7
- package/src/mind/recognition.ts +89 -19
- package/src/mind/traverse.ts +89 -8
- package/src/mind/types.ts +39 -5
- package/src/store.ts +75 -6
- package/test/14-scaling.test.mjs +17 -7
- package/test/31-audit.test.mjs +4 -1
- package/test/33-multi-candidate.test.mjs +13 -1
- package/test/36-already-answered-fusion.test.mjs +10 -3
- package/test/46-recognise-multibyte-edge.test.mjs +3 -3
- package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
- package/test/54-evidence-k-instrumentation.test.mjs +175 -0
- package/test/55-cost-meter.test.mjs +284 -0
- package/test/56-bridge-identity-admission.test.mjs +209 -0
- package/test/57-fusion-order.test.mjs +104 -0
- package/test/58-subquantum-sites.test.mjs +112 -0
- 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
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
392
|
-
*
|
|
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
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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";
|