@hviana/sema 0.9.0 → 0.9.2

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 (118) hide show
  1. package/AGENTS.md +7 -7
  2. package/dist/src/alu/src/index.d.ts +1 -1
  3. package/dist/src/alu/src/index.js +1 -1
  4. package/dist/src/alu/src/parser.js +2 -6
  5. package/dist/src/alu/src/resonance.d.ts +13 -0
  6. package/dist/src/alu/src/resonance.js +41 -0
  7. package/dist/src/alu/test/alu.test.js +39 -0
  8. package/dist/src/bytes.d.ts +6 -2
  9. package/dist/src/bytes.js +10 -4
  10. package/dist/src/canon.js +44 -0
  11. package/dist/src/geometry.d.ts +19 -1
  12. package/dist/src/geometry.js +125 -141
  13. package/dist/src/meter.d.ts +33 -0
  14. package/dist/src/meter.js +34 -1
  15. package/dist/src/mind/articulation.js +14 -1
  16. package/dist/src/mind/attention.d.ts +12 -0
  17. package/dist/src/mind/attention.js +44 -16
  18. package/dist/src/mind/bridge.js +3 -3
  19. package/dist/src/mind/derivation.d.ts +40 -0
  20. package/dist/src/mind/derivation.js +34 -0
  21. package/dist/src/mind/evidence.d.ts +24 -0
  22. package/dist/src/mind/evidence.js +90 -0
  23. package/dist/src/mind/graph-search.d.ts +89 -15
  24. package/dist/src/mind/graph-search.js +345 -174
  25. package/dist/src/mind/learning.js +1 -1
  26. package/dist/src/mind/mechanisms/cover.d.ts +19 -3
  27. package/dist/src/mind/mechanisms/cover.js +142 -61
  28. package/dist/src/mind/mechanisms/recall.js +10 -3
  29. package/dist/src/mind/mind.d.ts +6 -0
  30. package/dist/src/mind/mind.js +5 -2
  31. package/dist/src/mind/pipeline.d.ts +5 -1
  32. package/dist/src/mind/pipeline.js +220 -90
  33. package/dist/src/mind/primitives.d.ts +25 -5
  34. package/dist/src/mind/primitives.js +107 -44
  35. package/dist/src/mind/reasoning.d.ts +18 -4
  36. package/dist/src/mind/reasoning.js +487 -328
  37. package/dist/src/mind/recognition.js +29 -13
  38. package/dist/src/mind/resonance.js +1 -11
  39. package/dist/src/mind/traverse.d.ts +45 -5
  40. package/dist/src/mind/traverse.js +285 -8
  41. package/dist/src/mind/types.d.ts +16 -1
  42. package/dist/src/store-sqlite.d.ts +25 -0
  43. package/dist/src/store-sqlite.js +89 -1
  44. package/dist/src/store.d.ts +48 -4
  45. package/dist/src/store.js +86 -6
  46. package/docs/INDEX.md +20 -19
  47. package/docs/INVARIANTS.md +17 -16
  48. package/docs/architecture/bounded-reads.md +1 -1
  49. package/docs/architecture/caches.md +5 -4
  50. package/docs/architecture/closure.md +45 -5
  51. package/docs/architecture/cost-model.md +16 -0
  52. package/docs/architecture/evidence.md +113 -0
  53. package/docs/architecture/exact-vs-approximate.md +10 -9
  54. package/docs/architecture/factored-machinery.md +14 -13
  55. package/docs/architecture/fold-contract.md +51 -1
  56. package/docs/architecture/mechanism-market.md +21 -0
  57. package/docs/architecture/memoization.md +3 -3
  58. package/docs/architecture/meter.md +2 -1
  59. package/docs/architecture/saturation.md +12 -0
  60. package/docs/architecture/store.md +25 -2
  61. package/docs/failures/tempting-but-wrong.md +13 -2
  62. package/docs/harness/gates.md +12 -10
  63. package/docs/mechanisms/cover.md +23 -6
  64. package/jsr.json +1 -1
  65. package/package.json +1 -1
  66. package/src/alu/README.md +10 -2
  67. package/src/alu/src/index.ts +1 -0
  68. package/src/alu/src/parser.ts +6 -6
  69. package/src/alu/src/resonance.ts +42 -0
  70. package/src/alu/test/alu.test.ts +40 -0
  71. package/src/bytes.ts +13 -3
  72. package/src/canon.ts +40 -0
  73. package/src/geometry.ts +183 -154
  74. package/src/meter.ts +34 -1
  75. package/src/mind/articulation.ts +14 -2
  76. package/src/mind/attention.ts +47 -25
  77. package/src/mind/bridge.ts +3 -3
  78. package/src/mind/derivation.ts +77 -0
  79. package/src/mind/evidence.ts +107 -0
  80. package/src/mind/graph-search.ts +449 -221
  81. package/src/mind/learning.ts +1 -7
  82. package/src/mind/match.ts +1 -2
  83. package/src/mind/mechanisms/cast.ts +1 -2
  84. package/src/mind/mechanisms/cover.ts +207 -87
  85. package/src/mind/mechanisms/extraction.ts +1 -2
  86. package/src/mind/mechanisms/prefix-completion.ts +1 -1
  87. package/src/mind/mechanisms/recall.ts +17 -5
  88. package/src/mind/mechanisms/reference.ts +1 -1
  89. package/src/mind/mind.ts +9 -30
  90. package/src/mind/pipeline.ts +263 -104
  91. package/src/mind/primitives.ts +119 -43
  92. package/src/mind/reasoning.ts +611 -419
  93. package/src/mind/recognition.ts +24 -9
  94. package/src/mind/resonance.ts +2 -16
  95. package/src/mind/trace.ts +1 -1
  96. package/src/mind/traverse.ts +321 -8
  97. package/src/mind/types.ts +15 -11
  98. package/src/store-sqlite.ts +92 -1
  99. package/src/store.ts +113 -7
  100. package/test/105-derive-through-reports-its-refusal.test.mjs +8 -5
  101. package/test/106-the-join-fires.test.mjs +21 -0
  102. package/test/111-the-cover-assembly-is-counted.test.mjs +8 -5
  103. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +18 -12
  104. package/test/136-the-two-named-limits.test.mjs +3 -2
  105. package/test/137-the-law-lives-once-and-below.test.mjs +21 -0
  106. package/test/148-exact-shortcuts-agree.test.mjs +188 -0
  107. package/test/149-the-closure-engine.test.mjs +138 -0
  108. package/test/150-the-join-is-output-sensitive.test.mjs +66 -0
  109. package/test/151-the-cover-pays-for-what-it-reaches.test.mjs +142 -0
  110. package/test/152-the-read-side-names-as-the-write-side.test.mjs +146 -0
  111. package/test/153-a-cheaper-bound-is-looked-at-first.test.mjs +155 -0
  112. package/test/154-the-question-names-the-step.test.mjs +281 -0
  113. package/test/24-generalization.test.mjs +32 -0
  114. package/test/36-bloom.test.mjs +53 -0
  115. package/test/37-cluster-dispersion-fusion.test.mjs +75 -0
  116. package/test/48-recognise-turn-connective.test.mjs +3 -2
  117. package/test/55-cost-meter.test.mjs +4 -4
  118. package/test/90-connector-read-cap.test.mjs +7 -7
@@ -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 inline loop per level (foldSlice): no per-group
177
- // function calls, no Array.slice per group, the permute and add FUSED
178
- // (`gist[d] += v[seat[d]]`, no scratch buffer), and subtree byte lengths
179
- // carried incrementally on Folded (the old boundary scan re-walked subtrees
180
- // every level — O(n log n)). The per-level SUPERPOSITION is byte-identical
181
- // to the original recursive foldGroup: the same FP additions in the same
182
- // order.
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
- // (riverFold's single normalize). This is a deliberate change of similarity
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(space, items, start, count, out, force) {
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
- const foldAt = (at, size) => {
209
- const gist = new Float32Array(D);
210
- const kids = new Array(size);
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
- foldAt(start + complete, leftover);
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 cyclic polynomial (each byte enters as a table value, leaves rotated
456
- // by the window width) has EXACTLY k bytes of memory and scrambles periodic
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 is the same shape as before: shifts, XORs and two table lookups per
464
- // byte, no multiply and no auxiliary structure. (An exact sliding-window
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 `latin1Key(bytes.subarray(0, L))` — the prefix's own bytes
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 < 8; 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 = itemKey(items[j].tree.v);
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] : joinFlat(space, part));
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] : joinFlat(space, slice));
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
- : riverFoldRaw(space, items);
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
- if (row.length === 1)
999
- return row[0];
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];
@@ -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
@@ -177,6 +197,19 @@ export declare class Meter {
177
197
  /** Times the reasoner pivoted on a span its answer contains and stepped
178
198
  * across that fact. */
179
199
  pivotSteps: number;
200
+ /** `chooseNext` picks the question NAMED — a continuation one of whose own
201
+ * establishing contexts the question (plus the node) wholly witnesses
202
+ * (traverse.ts, the exact tier). */
203
+ askedContinuations: number;
204
+ /** Predecessor rows that exact tier read, against its shared √N budget. */
205
+ askedPredecessorReads: number;
206
+ /** Times the tier abstained on a read it could not trust: the continuations
207
+ * came back at the √N cap, or its predecessor budget ran out before every
208
+ * continuation was asked about — the distributional ladder decided. */
209
+ askedReadsSaturated: number;
210
+ /** Cover sites dropped as FRAGMENTS whose several continuations the question
211
+ * names none of (mechanisms/cover.ts). */
212
+ unaskedFragments: number;
180
213
  /** Canon probes REFUSED because the canon budget ran out — the one thing the
181
214
  * budget does that nothing could see. The budget itself is derived
182
215
  * (`bytes.length · chainReach(W)²`, recognition.ts), and the cheap exact route
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 four counters are the untraced view — the
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
@@ -187,6 +207,19 @@ export class Meter {
187
207
  /** Times the reasoner pivoted on a span its answer contains and stepped
188
208
  * across that fact. */
189
209
  pivotSteps = 0;
210
+ /** `chooseNext` picks the question NAMED — a continuation one of whose own
211
+ * establishing contexts the question (plus the node) wholly witnesses
212
+ * (traverse.ts, the exact tier). */
213
+ askedContinuations = 0;
214
+ /** Predecessor rows that exact tier read, against its shared √N budget. */
215
+ askedPredecessorReads = 0;
216
+ /** Times the tier abstained on a read it could not trust: the continuations
217
+ * came back at the √N cap, or its predecessor budget ran out before every
218
+ * continuation was asked about — the distributional ladder decided. */
219
+ askedReadsSaturated = 0;
220
+ /** Cover sites dropped as FRAGMENTS whose several continuations the question
221
+ * names none of (mechanisms/cover.ts). */
222
+ unaskedFragments = 0;
190
223
  /** Canon probes REFUSED because the canon budget ran out — the one thing the
191
224
  * budget does that nothing could see. The budget itself is derived
192
225
  * (`bytes.length · chainReach(W)²`, recognition.ts), and the cheap exact route
@@ -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, new Map(), ans.leaves, ans.splits, substitutions, undefined, undefined, ctx.trace ? (steps) => traceDerivation(ctx, steps) : undefined);
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>;