@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.
Files changed (84) 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 +5 -4
  12. package/dist/src/mind/attention.js +254 -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/index.d.ts +2 -0
  20. package/dist/src/mind/junction.d.ts +10 -0
  21. package/dist/src/mind/junction.js +14 -0
  22. package/dist/src/mind/learning.d.ts +32 -4
  23. package/dist/src/mind/learning.js +26 -4
  24. package/dist/src/mind/match.d.ts +40 -0
  25. package/dist/src/mind/match.js +125 -1
  26. package/dist/src/mind/mechanisms/cast.js +63 -6
  27. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  28. package/dist/src/mind/mechanisms/extraction.js +1 -88
  29. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  30. package/dist/src/mind/mechanisms/recall.js +77 -14
  31. package/dist/src/mind/mind.d.ts +59 -5
  32. package/dist/src/mind/mind.js +115 -93
  33. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  34. package/dist/src/mind/pipeline-mechanism.js +179 -10
  35. package/dist/src/mind/pipeline.d.ts +29 -0
  36. package/dist/src/mind/pipeline.js +79 -21
  37. package/dist/src/mind/primitives.d.ts +11 -15
  38. package/dist/src/mind/primitives.js +47 -28
  39. package/dist/src/mind/reasoning.d.ts +7 -1
  40. package/dist/src/mind/reasoning.js +40 -8
  41. package/dist/src/mind/recognition.js +93 -20
  42. package/dist/src/mind/traverse.d.ts +11 -0
  43. package/dist/src/mind/traverse.js +88 -7
  44. package/dist/src/mind/types.d.ts +39 -5
  45. package/dist/src/store.d.ts +15 -0
  46. package/dist/src/store.js +91 -6
  47. package/package.json +1 -1
  48. package/src/derive/src/deduction.ts +15 -0
  49. package/src/derive/src/index.ts +1 -0
  50. package/src/geometry.ts +350 -122
  51. package/src/index.ts +1 -0
  52. package/src/meter.ts +333 -0
  53. package/src/mind/attention.ts +276 -31
  54. package/src/mind/bridge.ts +187 -10
  55. package/src/mind/canonical.ts +6 -1
  56. package/src/mind/graph-search.ts +60 -21
  57. package/src/mind/index.ts +6 -0
  58. package/src/mind/junction.ts +12 -0
  59. package/src/mind/learning.ts +46 -5
  60. package/src/mind/match.ts +146 -1
  61. package/src/mind/mechanisms/cast.ts +62 -6
  62. package/src/mind/mechanisms/extraction.ts +2 -103
  63. package/src/mind/mechanisms/recall.ts +84 -17
  64. package/src/mind/mind.ts +144 -99
  65. package/src/mind/pipeline-mechanism.ts +203 -13
  66. package/src/mind/pipeline.ts +144 -36
  67. package/src/mind/primitives.ts +49 -33
  68. package/src/mind/reasoning.ts +39 -7
  69. package/src/mind/recognition.ts +89 -19
  70. package/src/mind/traverse.ts +89 -8
  71. package/src/mind/types.ts +39 -5
  72. package/src/store.ts +75 -6
  73. package/test/14-scaling.test.mjs +17 -7
  74. package/test/31-audit.test.mjs +4 -1
  75. package/test/33-multi-candidate.test.mjs +13 -1
  76. package/test/36-already-answered-fusion.test.mjs +10 -3
  77. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  78. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  79. package/test/54-evidence-k-instrumentation.test.mjs +175 -0
  80. package/test/55-cost-meter.test.mjs +284 -0
  81. package/test/56-bridge-identity-admission.test.mjs +209 -0
  82. package/test/57-fusion-order.test.mjs +104 -0
  83. package/test/58-subquantum-sites.test.mjs +112 -0
  84. package/test/59-fold-invariance.test.mjs +226 -0
@@ -94,9 +94,14 @@ import { cosine } from "../vec.js";
94
94
  import { conceptThreshold, dominates } from "../geometry.js";
95
95
  import { bytesEqual, indexOf } from "../bytes.js";
96
96
  import type { MindContext } from "./types.js";
97
- import { foldTree, perceive } from "./primitives.js";
97
+ import { foldTree, perceive, read } from "./primitives.js";
98
98
  import { chainReach, leafIdRun } from "./canonical.js";
99
- import { corpusN, edgeAncestors, hubBound } from "./traverse.js";
99
+ import {
100
+ corpusN,
101
+ edgeAncestors,
102
+ hubBound,
103
+ sharedReachMemo,
104
+ } from "./traverse.js";
100
105
  import { rItem, rNode } from "./trace.js";
101
106
 
102
107
  /** One accepted substitution: query span [qs,qe) stands in for the
@@ -242,10 +247,30 @@ function align(
242
247
 
243
248
  /** Recall's corroborated-substitution bridge — see the module comment.
244
249
  * Returns the best bridged grounding proposal, or null. */
250
+ /** `proposed` is a THUNK, not a list: the bridge's own cheap gates (the
251
+ * two-quantum query floor and the O(|query|) stored-window anchor scan)
252
+ * decide whether ANY candidate can be aligned, and they need no proposals
253
+ * to do it. Resolving the caller's proposals eagerly meant recall paid its
254
+ * exhaustive whole-index resonance — the most expensive single act on the
255
+ * refusal path — for every query, including the ones whose windows the
256
+ * store has never seen and which the anchor scan rejects outright. Same
257
+ * investment discipline the mechanism floors follow (AGENTS §2.6): never
258
+ * compute a shared analysis just to discard it. */
245
259
  export async function substitutionBridge(
246
260
  ctx: MindContext,
247
261
  query: Uint8Array,
248
- proposed: ReadonlyArray<number> = [],
262
+ proposed: () => Promise<ReadonlyArray<number>> = async () => [],
263
+ ): Promise<BridgeHit | null> {
264
+ const meter = ctx.meter;
265
+ return meter
266
+ ? meter.time("substitutionBridge", () => bridgeImpl(ctx, query, proposed))
267
+ : bridgeImpl(ctx, query, proposed);
268
+ }
269
+
270
+ async function bridgeImpl(
271
+ ctx: MindContext,
272
+ query: Uint8Array,
273
+ proposed: () => Promise<ReadonlyArray<number>>,
249
274
  ): Promise<BridgeHit | null> {
250
275
  const W = ctx.space.maxGroup;
251
276
  if (query.length < 2 * W) return null;
@@ -254,6 +279,38 @@ export async function substitutionBridge(
254
279
  const bar = conceptThreshold(ctx.store.D);
255
280
  const reachCap = chainReach(W);
256
281
 
282
+ // PHRASE-SCALE CANDIDATE CAP — the same |content|·W bound the weave
283
+ // (pipeline-mechanism.ts), the cross-region junction ladder's
284
+ // `maxInterior`, and structural resonance's `maxSiblingBytes` all apply,
285
+ // for the same reason and now at the one remaining place that read
286
+ // candidate contexts WHOLE.
287
+ //
288
+ // The bridge accepts a candidate only when the query is DOMINATED by its
289
+ // matched runs plus substitutions, with at most one window W of slack at
290
+ // each edge and at most one chain reach (W²) per interior gap — so the
291
+ // candidate region an accepted alignment can ever consume is bounded by
292
+ // |query|·W. Content beyond that cannot participate in any alignment
293
+ // this function would accept; reading it is pure cost. And a candidate
294
+ // an order of magnitude past the query is not a paraphrase of it: it is a
295
+ // document or a whole conversation that merely quotes a phrase, and
296
+ // grounding through ITS learnt edge voices that document's continuation,
297
+ // not a phrase answer.
298
+ //
299
+ // Measured on the 17.7M-node / 325K-context store: uncapped, the refusal
300
+ // path materialised up to ~1 MB of candidate bytes per query (up to √N
301
+ // proposals plus √N climbed contexts, each read in full), and the frame-
302
+ // unanimity scan — which walks EVERY collected candidate's bytes, inside
303
+ // the per-gap expansion loop — paid that volume back tens of times per
304
+ // substitution. Recall's run() was 0.7–2.6 s per refusing query.
305
+ const capBytes = query.length * W;
306
+ /** A candidate's bytes, phrase-scale capped: null when it exceeds the cap
307
+ * (read one byte past it, so "too long" is decided without materialising
308
+ * the rest) or has no content. */
309
+ const candidateBytes = (sid: number): Uint8Array | null => {
310
+ const b = read(ctx, sid, capBytes + 1);
311
+ return b.length === 0 || b.length > capBytes ? null : b;
312
+ };
313
+
257
314
  // 1. The query's stored windows, rarest first (fewest containers — the
258
315
  // most discriminative anchors; hub-clamped like every fan-out read).
259
316
  // The scan doubles as the ONE store probe of every query window: the
@@ -302,6 +359,54 @@ export async function substitutionBridge(
302
359
  }
303
360
  return false;
304
361
  };
362
+ // ── EXPLAINED SPANS — the scaffolding judgement, corpus-global ──────────
363
+ //
364
+ // The question every gap poses is "may the two forms differ HERE without
365
+ // differing in what they SAY?", and that is the discriminative-vs-
366
+ // scaffolding question AGENTS §2.7 names, over the CORPUS-GLOBAL
367
+ // population. It already has one definition — `dominates(reachOf(...), N)`,
368
+ // the same gate confluence's filler test uses ("scaffolding never binds").
369
+ // Nothing new is derived here; the bar is read, not invented.
370
+ //
371
+ // A span is explained when EITHER
372
+ // • it is sub-quantum (< W) — typographic glue, the tolerance identityBar
373
+ // already prices ("below one river window, byte overlap is chance"); or
374
+ // • every full W-window inside it is COMMON by the store's own climb:
375
+ // the ascent SATURATES (the window sits in more places than √N — the
376
+ // climb's own definition of non-discriminative), or it resolves to a
377
+ // majority of the corpus's contexts. "the process of ", " is the ".
378
+ //
379
+ // THE READING MATTERS, not just the population (AGENTS §2.7). This
380
+ // deliberately does NOT go through `reachOf`, which maps BOTH "saturated"
381
+ // and "reaches nothing" to Infinity. For IDF weighting those are the same
382
+ // thing (no usable identity evidence); for THIS question they are
383
+ // opposites — a window reaching nothing is novel content, the most
384
+ // discriminative material there is, and reading it as Infinity would call
385
+ // it scaffolding. Measured: with `reachOf`, "Is water wet?" was answered
386
+ // with "No, heavy water is not wet." — "heav"/"eavy" occur once, reach no
387
+ // edge-bearing ancestor, and were written off as filler. So an
388
+ // empty-rooted window is NEVER explained, and neither is an untrained one
389
+ // (the same principle attestedQ applies to the query side).
390
+ const reachMemo = sharedReachMemo(ctx);
391
+ const explainedSpan = (
392
+ bytes: Uint8Array,
393
+ from: number,
394
+ to: number,
395
+ ): boolean => {
396
+ if (to - from < W) return true;
397
+ for (let o = from; o + W <= to; o++) {
398
+ const ids = leafIdRun(ctx, bytes, o, o + W);
399
+ if (ids === null) return false;
400
+ const wid = ctx.store.findBranch(ids);
401
+ if (wid === null) return false;
402
+ const r = edgeAncestors(ctx, wid, N, reachMemo);
403
+ if (r.saturated) continue; // in too many places to discriminate
404
+ if (r.roots.length === 0) return false; // reaches nothing: novel content
405
+ if (!dominates(r.contextsReached, N)) return false;
406
+ }
407
+ return true;
408
+ };
409
+
305
410
  anchors.sort((a, b) => a.rarity - b.rarity);
306
411
  // Up to W anchors, at least one window apart — the quantum's own count.
307
412
  const picked: typeof anchors = [];
@@ -331,11 +436,13 @@ export async function substitutionBridge(
331
436
  // that could align at all: alignment can only seed at a picked anchor
332
437
  // window occurring literally in the candidate (measured: unconditional
333
438
  // re-folds multiplied the refusal-path latency several-fold).
334
- for (const sid of proposed) {
439
+ // FIRST TOUCH of the caller's proposals — past every gate that could have
440
+ // refused without them (see substitutionBridge's doc).
441
+ for (const sid of await proposed()) {
335
442
  if (seen.has(sid)) continue;
336
443
  seen.add(sid);
337
- const tb = ctx.store.bytes(sid);
338
- if (tb.length === 0) continue;
444
+ const tb = candidateBytes(sid);
445
+ if (tb === null) continue;
339
446
  if (
340
447
  !picked.some((a) => indexOf(tb, query.subarray(a.off, a.off + W), 0) >= 0)
341
448
  ) continue;
@@ -368,8 +475,14 @@ export async function substitutionBridge(
368
475
  }
369
476
 
370
477
  // 3. Align each candidate; gate its mismatches; keep the best.
478
+ // Over-cap candidates are dropped here rather than earlier: the climb
479
+ // channel deliberately reads no bytes while collecting (the climb visits
480
+ // hundreds of roots), so this is where its proposals are first sized.
371
481
  const allBytes = new Map<number, Uint8Array>();
372
- for (const sid of candidates) allBytes.set(sid, ctx.store.bytes(sid));
482
+ for (const sid of candidates) {
483
+ const b = candidateBytes(sid);
484
+ if (b !== null) allBytes.set(sid, b);
485
+ }
373
486
 
374
487
  // FRAME UNANIMITY: a substitution U → C inside the frame (Lf, Rf) is
375
488
  // groundable only when the collected candidates — the store's own sample
@@ -428,8 +541,8 @@ export async function substitutionBridge(
428
541
  let best: BridgeHit | null = null;
429
542
  let bestAccounted = 0;
430
543
  for (const sid of candidates) {
431
- const cBytes = allBytes.get(sid)!;
432
- if (cBytes.length === 0) continue;
544
+ const cBytes = allBytes.get(sid);
545
+ if (cBytes === undefined) continue;
433
546
  // Seed at the rarest picked anchor that literally occurs in this
434
547
  // candidate.
435
548
  let seed: { qo: number; co: number } | null = null;
@@ -541,7 +654,7 @@ export async function substitutionBridge(
541
654
  // resonance rank but has no outgoing edge to bridge through). This
542
655
  // mechanism exists to explain SUBSTITUTIONS; a query needing none is
543
656
  // recall's job, not the bridge's.
544
- if (!ok || subs.length === 0) continue;
657
+ if (!ok) continue;
545
658
 
546
659
  // Coverage: matched runs plus accepted substitutions must dominate the
547
660
  // query, every interior gap already proved ≤ W above, and the EDGES
@@ -565,6 +678,70 @@ export async function substitutionBridge(
565
678
  if (spans[0][0] > W || query.length - reachEnd > W) continue;
566
679
  if (!dominates(covered, query.length)) continue;
567
680
 
681
+ // ZERO-SUBSTITUTION ADMISSION — an IDENTITY claim, not a substitution.
682
+ //
683
+ // A candidate needing no substitution is normally refused (see the trap
684
+ // above), and that refusal is right for the case it was written for: the
685
+ // query is a strict byte-PREFIX of several candidates, each of which
686
+ // continues differently, and nothing here corroborates picking one
687
+ // continuation over another. But that trap has a signature — the
688
+ // candidate carries substantial content BEYOND the alignment, and that
689
+ // surplus is exactly the "answer" the bridge would be inventing.
690
+ //
691
+ // The opposite shape is not ambiguous at all: the alignment explains BOTH
692
+ // strings end to end, and the only thing between them is sub-quantum glue
693
+ // — typographic punctuation the fold treats as structure. Then the two
694
+ // are the SAME learnt form, and grounding through its edge returns that
695
+ // form's own trained answer, never a chosen-among-many completion.
696
+ //
697
+ // Why the ladder cannot reach these otherwise: the gist is a STRUCTURAL
698
+ // signature, so a mid-string insertion shifts every fold boundary after
699
+ // it. Measured: `Who wrote Romeo and Juliet?` against the trained
700
+ // `Who wrote "Romeo and Juliet"?` — two inserted quote characters — scores
701
+ // cos 0.377, BELOW unrelated neighbours like "Who wrote the opera
702
+ // Carmen??" (0.603). Recall's identity tiers gate on identityBar (0.969
703
+ // here) and its reach tiers on 0.875, so no gist-based tier can ever see
704
+ // it; only byte-exact alignment can, which is what this function does.
705
+ //
706
+ // The claim is deliberately strict, in three parts:
707
+ //
708
+ // • QUERY SIDE — EXACT. Every byte of the query must be a literal
709
+ // match against the candidate: covered === query.length, no slack at
710
+ // all, not even sub-quantum. The query is what we are answering, so
711
+ // an identity claim about it may write off NOTHING. This is stricter
712
+ // than the ≤ W edge tolerance the substituted path uses, and it has
713
+ // to be: with a one-window allowance, `what is 2^10?` matched the
714
+ // trained `what is 2+2?` — "^10" against "+2", four bytes, both sides
715
+ // below W — and answered "2+2 is 4.", outweighing cover's authoritative
716
+ // ALU result. Below W, byte OVERLAP is chance rather than evidence;
717
+ // that never made a below-W DIFFERENCE meaningless, and digits are the
718
+ // case that proves it.
719
+ // • CANDIDATE SIDE, INTERIOR — each gap must be an EXPLAINED span (see
720
+ // explainedSpan): sub-quantum glue, or corpus-global scaffolding.
721
+ // This side is asymmetric ON PURPOSE. Material the CANDIDATE has and
722
+ // the query omits is not something the asker asked about: if it is
723
+ // scaffolding, dropping it changes nothing ("What is *the process of*
724
+ // photosynthesis?"); if it is discriminative, the candidate answers a
725
+ // DIFFERENT, narrower question ("Is *heavy* water wet?") and must be
726
+ // refused. Only the corpus can tell those apart, and it does.
727
+ // • CANDIDATE SIDE, SURPLUS — its bytes are the matched runs
728
+ // (byte-identical to the query's, hence the same total length) plus
729
+ // its own gap spans; anything past that is surplus, and surplus is
730
+ // the prefix trap. A prefix-completion candidate fails here by the
731
+ // whole length of the completion it wanted to supply — which is why
732
+ // admitting scaffolding interiors does not reopen that trap.
733
+ //
734
+ // Ordered cheapest-first: the two arithmetic tests run before
735
+ // explainedSpan, whose per-window `reachOf` climbs are the only costly
736
+ // part (shared through the response/conversation reach memo, and reached
737
+ // only by a candidate that already survived every structural gate).
738
+ if (subs.length === 0) {
739
+ if (covered !== query.length) continue;
740
+ const cGap = gaps.reduce((n, g) => n + (g.ce - g.cs), 0);
741
+ if (cBytes.length - covered - cGap > W) continue;
742
+ if (!gaps.every((g) => explainedSpan(cBytes, g.cs, g.ce))) continue;
743
+ }
744
+
568
745
  // KNOWN content may never be dismissed — see dismissedKnownContent
569
746
  // (the live case: "what is the capital of france" aligning into a
570
747
  // Matrix synopsis by writing off "ance" — a stored window of the
@@ -27,7 +27,12 @@ import type { MindContext } from "./types.js";
27
27
 
28
28
  /** The two sliding-window lengths the WRITE side interns over a stream's leaf
29
29
  * ids: W−1 and W (the river's grouping quantum and its off-by-one neighbour,
30
- * so a form straddling a group boundary is reachable from either cut). */
30
+ * so a form straddling a group boundary is reachable from either cut).
31
+ *
32
+ * Widening this to cover the reader's full segment scale (W−1..2W) was
33
+ * measured and REFUTED: it tripled the store (3,160 → 8,980 nodes on a
34
+ * 200-pair corpus), slowed ingest 80%, and fixed not one test. Unknown
35
+ * regions are not caused by the index's scale. */
31
36
  export function canonicalWindows(W: number): [number, number] {
32
37
  return [W - 1, W];
33
38
  }
@@ -350,6 +350,18 @@ export class GraphSearch {
350
350
  private readonly host: GraphSearchHost,
351
351
  ) {}
352
352
 
353
+ /** The hub bound √N (AGENTS §2.8) — the ONE fan-out cap, stated here
354
+ * rather than imported from `traverse.ts` because this module is
355
+ * deliberately host-based (it holds a bare Store, never a MindContext).
356
+ * That is the same write/read-side duplication convention canonical.ts's
357
+ * header documents: if the formula changes it must change in BOTH places.
358
+ * It is stated ONCE per side, though — the expression used to be spelled
359
+ * out at three call sites here, one of them inside a per-item rules
360
+ * generator, and they had already drifted on the `Math.max(2, …)` floor. */
361
+ private hubBound(): number {
362
+ return Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
363
+ }
364
+
353
365
  /** Explore the Sema graph for the lightest cover of the query and return its
354
366
  * chosen spans left-to-right — WITH the derivation's total weight (the g
355
367
  * value of the goal item, in the exported cost ladder), which think's
@@ -442,7 +454,17 @@ export class GraphSearch {
442
454
  connectors,
443
455
  computedResults,
444
456
  );
445
- const derivation = lightestDerivation(system);
457
+ // Search-effort accounting (src/meter.ts): the chart's pops/pushes are
458
+ // the cover's real cost, and a heuristic that stops being admissible
459
+ // shows up as a pop count that explodes while the answer stays the same.
460
+ const meter = this.host.meter;
461
+ const stats = meter ? { pops: 0, pushes: 0 } : undefined;
462
+ const derivation = lightestDerivation(system, stats);
463
+ if (meter && stats) {
464
+ meter.searches++;
465
+ meter.searchPops += stats.pops;
466
+ meter.searchPushes += stats.pushes;
467
+ }
446
468
  // When covering under a substitution map (articulation), a form→out rule is
447
469
  // the form EMITTING the asker's voice, not grounding to its own answer — so
448
470
  // tell the reader to name those moves `voice` rather than `ground`.
@@ -475,15 +497,12 @@ export class GraphSearch {
475
497
  ): DeductionSystem<GItem> {
476
498
  const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
477
499
  // Same corpus-scale hub floor {@link atomIsHub}/{@link atomReach} (traverse.ts)
478
- // derive for byte atoms — duplicated here rather than imported because this
479
- // module is deliberately host-based (no MindContext), and both inputs
480
- // (maxGroup, edgeSourceCount) are already in scope with no ctx needed. If
481
- // the formula ever changes, it must change in BOTH places (see canonical.ts's
482
- // header for the same write/read-side duplication convention).
500
+ // derive for byte atoms — see {@link hubBound} below for why this module
501
+ // states the formula itself instead of importing it.
483
502
  const atomsAreHubs = Math.max(
484
503
  1,
485
504
  Math.ceil((this.store.edgeSourceCount() * W) / 256),
486
- ) > Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
505
+ ) > this.hubBound();
487
506
  const nodeBytes = (n: number) => this.store.bytesPrefix(n, ALL);
488
507
  // Content-addressed probes over the store's hash-cons maps — the same keys
489
508
  // training filled. No byte-by-byte trie walk.
@@ -753,10 +772,7 @@ export class GraphSearch {
753
772
  // guard then dead-ends it) with no way to reach the forward edge.
754
773
  // Forking offers every continuation as its own rule so the one that
755
774
  // genuinely advances (not a duplicate) is still reachable.
756
- const bound = Math.ceil(
757
- Math.sqrt(Math.max(2, this.store.edgeSourceCount())),
758
- );
759
- const nx = this.store.nextFirst(it.node, bound);
775
+ const nx = this.store.nextFirst(it.node, this.hubBound());
760
776
  if (nx.length) {
761
777
  // The SAME evidence-weighted disambiguation the first hop uses
762
778
  // (below) identifies the most-corroborated continuation. Yielding
@@ -1142,16 +1158,39 @@ export class GraphSearch {
1142
1158
  // is opportunistic cross-leaf recovery exactly like recognition.ts's own
1143
1159
  // canonical chain — findLeaf/findBranch here have no idea WHY these two
1144
1160
  // leaves are adjacent, only that their concatenation happens to spell a
1145
- // trained form ("hi" recovered from "W[hi]ch"). The same corpus-scale
1146
- // caution recognition.ts's `boundary` gate applies: trust it fully when
1147
- // `l.i` is a position the query's OWN fold chose as a boundary (real
1148
- // structural evidence); past the scale where atoms themselves stop
1149
- // discriminating, an interior offset's opportunistic match is noise, not
1150
- // a genuine cross-leaf recovery. A completion-involved fuse (l.rec ||
1151
- // r.rec) is a different, legitimate case — a rewrite growing into its
1152
- // neighbour and is exempt, same as recognition.ts's rec-derived sites.
1153
- const trusted = l.rec || r.rec || ctx.starts.has(l.i) ||
1154
- !ctx.atomsAreHubs;
1161
+ // trained form ("hi" recovered from "W[hi]ch"). At hub scale, where
1162
+ // atoms themselves no longer discriminate, that coincidence is noise.
1163
+ //
1164
+ // A FOLD BOUNDARY IS NOT EVIDENCE. This gate used to exempt any fuse
1165
+ // starting at `starts.has(l.i)` documented as "a position the query's
1166
+ // OWN fold chose as a boundary (real structural evidence)". The plain
1167
+ // river fold CHOOSES NOTHING: `riverFold` groups fixed-arity
1168
+ // (`for (i = 0; i < complete; i += mg)`), so `starts` is exactly
1169
+ // {0, W, 2W, 3W, …} for every query. Measured on three unrelated
1170
+ // queries: starts = 0,4,8,12,16,20,24,28[,32] in every case — the set
1171
+ // carries ZERO content information, and the exemption therefore fired at
1172
+ // a quarter of all offsets by arithmetic alone.
1173
+ //
1174
+ // What it cost (17.9M-node store, W=4): "In which country is the Eiffel
1175
+ // Tower?" fused the atoms "h"+"i" at offset 4 — trusted only because
1176
+ // 4 ≡ 0 (mod 4) — into the trained form "hi", followed its continuation
1177
+ // edge, and grounded "Hello there. If you are looking for Open
1178
+ // Assistant, look no further." as a FACT, explaining 2 of 37 bytes
1179
+ // (density 0.054, a fifth of the 1/W honesty bar `thinGrounding`
1180
+ // reports and does not enforce).
1181
+ //
1182
+ // So the exemption is removed rather than replaced: there is no cheap
1183
+ // signal here that means what it claimed to mean, and inventing one
1184
+ // would be worse than admitting the absence. Genuine cross-leaf forms
1185
+ // are not lost — recognition.ts's canonical pass already probes EVERY
1186
+ // byte offset for the write side's interned windows and emits them as
1187
+ // sites, which arrive here as `l.rec`/`r.rec` and stay exempt. What
1188
+ // remains excluded at hub scale is precisely the case with no evidence
1189
+ // behind it: two byte atoms that happen to spell something.
1190
+ //
1191
+ // Below hub scale nothing changes (`!atomsAreHubs`): on a small store
1192
+ // coincidence is rare and every chain is real evidence.
1193
+ const trusted = l.rec || r.rec || !ctx.atomsAreHubs;
1155
1194
  let node = (trusted && bytes.length <= ctx.W)
1156
1195
  ? ctx.findLeafU(bytes)
1157
1196
  : undefined;
package/src/mind/index.ts CHANGED
@@ -32,3 +32,9 @@ export type {
32
32
  SaturationReason,
33
33
  SaturationStop,
34
34
  } from "./types.js";
35
+ export type { DepositReport } from "./learning.js";
36
+ export type {
37
+ DecideGroundingData,
38
+ NarrowDecisionData,
39
+ Provenance,
40
+ } from "./pipeline.js";
@@ -186,6 +186,16 @@ function cachedContainers(
186
186
  * few levels of its parts). A side too common to decide within the
187
187
  * budget abstains here and falls through to the resonance tier (the
188
188
  * climb's own saturation semantics).
189
+ *
190
+ * REFUTED TIGHTENING (measured, 325K contexts): giving this walk
191
+ * edgeAncestors' cumulative LATERAL-CONE limit — abstain once the
192
+ * accumulated lateral entries pass √N — would cut most of the pops (47
193
+ * of 56 walks exceed √N lateral, and 47 exhaust the budget), but TWO OF
194
+ * THE FOUR walks that actually found a container had lateral spread of
195
+ * 1425 and 1426, far past √N. The cone limit is sound for
196
+ * edgeAncestors' question and wrong for this one: a junction container
197
+ * is legitimately reached across many containing structures. Half the
198
+ * successful junctions would be lost.
189
199
  * • per-node hub guards — parent fan-outs beyond √N are hubs (not
190
200
  * expanded); each node contributes at most one √N page of containers;
191
201
  * √N collected candidates decide. */
@@ -215,6 +225,7 @@ export function junctionContainersFrom(
215
225
  if (seeds.length === 0) return [];
216
226
 
217
227
  const b = budget ?? { n: bound * ctx.space.maxGroup };
228
+ if (ctx.meter) ctx.meter.junctionWalks++;
218
229
  // DEPTH CAP: perception trees are W-ary and a junction container is
219
230
  // phrase-scale, so it sits within ~log_W(maxContainer) structural levels
220
231
  // of its parts — at most W levels for any practical W (plus the
@@ -231,6 +242,7 @@ export function junctionContainersFrom(
231
242
  }));
232
243
  while (stack.length > 0 && out.length < bound && b.n-- > 0) {
233
244
  const { id: x, d } = stack.pop()!;
245
+ if (ctx.meter) ctx.meter.junctionPops++;
234
246
  const f = cachedRead(ctx, cache, x, maxContainer);
235
247
  if (f.length > maxContainer) continue; // beyond phrase scale: prune branch
236
248
  if (unordered) {
@@ -215,12 +215,32 @@ async function propagateSuffixes(
215
215
  }
216
216
  }
217
217
 
218
- /** Ingest a pair (context, continuation)learn an edge and pour halos. */
218
+ /** What one ingested item depositedreported through {@link ingest}'s
219
+ * optional `onDeposit` callback. Pure provenance read-out: node ids are
220
+ * content-addressed, so two byte-identical items report the SAME ids (that
221
+ * is content addressing working, not an error), and the callback observes
222
+ * the deposit without influencing it. */
223
+ export interface DepositReport {
224
+ /** Zero-based position of the item in the ingested input (0 for a scalar
225
+ * or single pair). */
226
+ index: number;
227
+ /** Whether the item was a bare experience or a (context, continuation)
228
+ * pair. */
229
+ kind: "one" | "pair";
230
+ /** Root node id of the item's (context) bytes. */
231
+ contextId: number;
232
+ /** Root node id of the continuation bytes — pairs only. */
233
+ continuationId?: number;
234
+ }
235
+
236
+ /** Ingest a pair (context, continuation) — learn an edge and pour halos.
237
+ * Returns the deposited root ids (context, continuation) — a pure
238
+ * read-out; callers that ignore it behave exactly as before. */
219
239
  export async function ingestPair(
220
240
  ctx: MindContext,
221
241
  ctxInput: Input,
222
242
  cont: Input,
223
- ): Promise<void> {
243
+ ): Promise<{ ctxId: number; contId: number }> {
224
244
  const c = await deposit(ctx, ctxInput, true, true);
225
245
  const cont_ = await deposit(ctx, cont, false);
226
246
  const ctxId = c.rootId, contId = cont_.rootId;
@@ -249,6 +269,7 @@ export async function ingestPair(
249
269
  bindSeat(ctx.space, companySignature(ctx.space, partId), 0),
250
270
  );
251
271
  }
272
+ return { ctxId, contId };
252
273
  }
253
274
 
254
275
  /** Dispatch the public ingest input shapes onto one-input / pair handlers —
@@ -286,16 +307,36 @@ export async function dispatchIngest(
286
307
  return undefined;
287
308
  }
288
309
 
289
- /** Ingest an input or array of inputs/pairs. The public ingest entry point. */
310
+ /** Ingest an input or array of inputs/pairs. The public ingest entry point.
311
+ *
312
+ * `onDeposit`, when given, is invoked once per ingested item with the
313
+ * deposited root node ids ({@link DepositReport}) — item-level provenance
314
+ * for tooling that needs to know which stored node an ingested item became.
315
+ * Purely observational: the callback runs after the item's deposit
316
+ * completed and nothing reads its result. */
290
317
  export async function ingest(
291
318
  ctx: MindContext,
292
319
  input: Input | (Input | [Input, Input])[],
293
320
  second?: Input,
321
+ onDeposit?: (report: DepositReport) => void,
294
322
  ): Promise<(Sema & { id: number }) | undefined> {
323
+ let index = 0;
295
324
  return dispatchIngest(
296
325
  input,
297
326
  second,
298
- (i) => ingestOne(ctx, i),
299
- (a, b) => ingestPair(ctx, a, b),
327
+ async (i) => {
328
+ const r = await ingestOne(ctx, i);
329
+ onDeposit?.({ index: index++, kind: "one", contextId: r.id });
330
+ return r;
331
+ },
332
+ async (a, b) => {
333
+ const { ctxId, contId } = await ingestPair(ctx, a, b);
334
+ onDeposit?.({
335
+ index: index++,
336
+ kind: "pair",
337
+ contextId: ctxId,
338
+ continuationId: contId,
339
+ });
340
+ },
300
341
  );
301
342
  }