@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
@@ -92,9 +92,9 @@
92
92
  import { cosine } from "../vec.js";
93
93
  import { conceptThreshold, dominates } from "../geometry.js";
94
94
  import { bytesEqual, indexOf } from "../bytes.js";
95
- import { foldTree, perceive } from "./primitives.js";
95
+ import { foldTree, perceive, read } from "./primitives.js";
96
96
  import { chainReach, leafIdRun } from "./canonical.js";
97
- import { corpusN, edgeAncestors, hubBound } from "./traverse.js";
97
+ import { corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
98
98
  import { rItem, rNode } from "./trace.js";
99
99
  /** True when some query byte-range left UNACCOUNTED by `spans` contains a
100
100
  * STORED window — content the store has seen that the proposed reading
@@ -217,7 +217,22 @@ function align(ctx, q, c, qo, co) {
217
217
  }
218
218
  /** Recall's corroborated-substitution bridge — see the module comment.
219
219
  * Returns the best bridged grounding proposal, or null. */
220
- export async function substitutionBridge(ctx, query, proposed = []) {
220
+ /** `proposed` is a THUNK, not a list: the bridge's own cheap gates (the
221
+ * two-quantum query floor and the O(|query|) stored-window anchor scan)
222
+ * decide whether ANY candidate can be aligned, and they need no proposals
223
+ * to do it. Resolving the caller's proposals eagerly meant recall paid its
224
+ * exhaustive whole-index resonance — the most expensive single act on the
225
+ * refusal path — for every query, including the ones whose windows the
226
+ * store has never seen and which the anchor scan rejects outright. Same
227
+ * investment discipline the mechanism floors follow (AGENTS §2.6): never
228
+ * compute a shared analysis just to discard it. */
229
+ export async function substitutionBridge(ctx, query, proposed = async () => []) {
230
+ const meter = ctx.meter;
231
+ return meter
232
+ ? meter.time("substitutionBridge", () => bridgeImpl(ctx, query, proposed))
233
+ : bridgeImpl(ctx, query, proposed);
234
+ }
235
+ async function bridgeImpl(ctx, query, proposed) {
221
236
  const W = ctx.space.maxGroup;
222
237
  if (query.length < 2 * W)
223
238
  return null;
@@ -225,6 +240,37 @@ export async function substitutionBridge(ctx, query, proposed = []) {
225
240
  const N = corpusN(ctx);
226
241
  const bar = conceptThreshold(ctx.store.D);
227
242
  const reachCap = chainReach(W);
243
+ // PHRASE-SCALE CANDIDATE CAP — the same |content|·W bound the weave
244
+ // (pipeline-mechanism.ts), the cross-region junction ladder's
245
+ // `maxInterior`, and structural resonance's `maxSiblingBytes` all apply,
246
+ // for the same reason and now at the one remaining place that read
247
+ // candidate contexts WHOLE.
248
+ //
249
+ // The bridge accepts a candidate only when the query is DOMINATED by its
250
+ // matched runs plus substitutions, with at most one window W of slack at
251
+ // each edge and at most one chain reach (W²) per interior gap — so the
252
+ // candidate region an accepted alignment can ever consume is bounded by
253
+ // |query|·W. Content beyond that cannot participate in any alignment
254
+ // this function would accept; reading it is pure cost. And a candidate
255
+ // an order of magnitude past the query is not a paraphrase of it: it is a
256
+ // document or a whole conversation that merely quotes a phrase, and
257
+ // grounding through ITS learnt edge voices that document's continuation,
258
+ // not a phrase answer.
259
+ //
260
+ // Measured on the 17.7M-node / 325K-context store: uncapped, the refusal
261
+ // path materialised up to ~1 MB of candidate bytes per query (up to √N
262
+ // proposals plus √N climbed contexts, each read in full), and the frame-
263
+ // unanimity scan — which walks EVERY collected candidate's bytes, inside
264
+ // the per-gap expansion loop — paid that volume back tens of times per
265
+ // substitution. Recall's run() was 0.7–2.6 s per refusing query.
266
+ const capBytes = query.length * W;
267
+ /** A candidate's bytes, phrase-scale capped: null when it exceeds the cap
268
+ * (read one byte past it, so "too long" is decided without materialising
269
+ * the rest) or has no content. */
270
+ const candidateBytes = (sid) => {
271
+ const b = read(ctx, sid, capBytes + 1);
272
+ return b.length === 0 || b.length > capBytes ? null : b;
273
+ };
228
274
  // 1. The query's stored windows, rarest first (fewest containers — the
229
275
  // most discriminative anchors; hub-clamped like every fan-out read).
230
276
  // The scan doubles as the ONE store probe of every query window: the
@@ -281,6 +327,55 @@ export async function substitutionBridge(ctx, query, proposed = []) {
281
327
  }
282
328
  return false;
283
329
  };
330
+ // ── EXPLAINED SPANS — the scaffolding judgement, corpus-global ──────────
331
+ //
332
+ // The question every gap poses is "may the two forms differ HERE without
333
+ // differing in what they SAY?", and that is the discriminative-vs-
334
+ // scaffolding question AGENTS §2.7 names, over the CORPUS-GLOBAL
335
+ // population. It already has one definition — `dominates(reachOf(...), N)`,
336
+ // the same gate confluence's filler test uses ("scaffolding never binds").
337
+ // Nothing new is derived here; the bar is read, not invented.
338
+ //
339
+ // A span is explained when EITHER
340
+ // • it is sub-quantum (< W) — typographic glue, the tolerance identityBar
341
+ // already prices ("below one river window, byte overlap is chance"); or
342
+ // • every full W-window inside it is COMMON by the store's own climb:
343
+ // the ascent SATURATES (the window sits in more places than √N — the
344
+ // climb's own definition of non-discriminative), or it resolves to a
345
+ // majority of the corpus's contexts. "the process of ", " is the ".
346
+ //
347
+ // THE READING MATTERS, not just the population (AGENTS §2.7). This
348
+ // deliberately does NOT go through `reachOf`, which maps BOTH "saturated"
349
+ // and "reaches nothing" to Infinity. For IDF weighting those are the same
350
+ // thing (no usable identity evidence); for THIS question they are
351
+ // opposites — a window reaching nothing is novel content, the most
352
+ // discriminative material there is, and reading it as Infinity would call
353
+ // it scaffolding. Measured: with `reachOf`, "Is water wet?" was answered
354
+ // with "No, heavy water is not wet." — "heav"/"eavy" occur once, reach no
355
+ // edge-bearing ancestor, and were written off as filler. So an
356
+ // empty-rooted window is NEVER explained, and neither is an untrained one
357
+ // (the same principle attestedQ applies to the query side).
358
+ const reachMemo = sharedReachMemo(ctx);
359
+ const explainedSpan = (bytes, from, to) => {
360
+ if (to - from < W)
361
+ return true;
362
+ for (let o = from; o + W <= to; o++) {
363
+ const ids = leafIdRun(ctx, bytes, o, o + W);
364
+ if (ids === null)
365
+ return false;
366
+ const wid = ctx.store.findBranch(ids);
367
+ if (wid === null)
368
+ return false;
369
+ const r = edgeAncestors(ctx, wid, N, reachMemo);
370
+ if (r.saturated)
371
+ continue; // in too many places to discriminate
372
+ if (r.roots.length === 0)
373
+ return false; // reaches nothing: novel content
374
+ if (!dominates(r.contextsReached, N))
375
+ return false;
376
+ }
377
+ return true;
378
+ };
284
379
  anchors.sort((a, b) => a.rarity - b.rarity);
285
380
  // Up to W anchors, at least one window apart — the quantum's own count.
286
381
  const picked = [];
@@ -311,12 +406,14 @@ export async function substitutionBridge(ctx, query, proposed = []) {
311
406
  // that could align at all: alignment can only seed at a picked anchor
312
407
  // window occurring literally in the candidate (measured: unconditional
313
408
  // re-folds multiplied the refusal-path latency several-fold).
314
- for (const sid of proposed) {
409
+ // FIRST TOUCH of the caller's proposals — past every gate that could have
410
+ // refused without them (see substitutionBridge's doc).
411
+ for (const sid of await proposed()) {
315
412
  if (seen.has(sid))
316
413
  continue;
317
414
  seen.add(sid);
318
- const tb = ctx.store.bytes(sid);
319
- if (tb.length === 0)
415
+ const tb = candidateBytes(sid);
416
+ if (tb === null)
320
417
  continue;
321
418
  if (!picked.some((a) => indexOf(tb, query.subarray(a.off, a.off + W), 0) >= 0))
322
419
  continue;
@@ -353,9 +450,15 @@ export async function substitutionBridge(ctx, query, proposed = []) {
353
450
  break;
354
451
  }
355
452
  // 3. Align each candidate; gate its mismatches; keep the best.
453
+ // Over-cap candidates are dropped here rather than earlier: the climb
454
+ // channel deliberately reads no bytes while collecting (the climb visits
455
+ // hundreds of roots), so this is where its proposals are first sized.
356
456
  const allBytes = new Map();
357
- for (const sid of candidates)
358
- allBytes.set(sid, ctx.store.bytes(sid));
457
+ for (const sid of candidates) {
458
+ const b = candidateBytes(sid);
459
+ if (b !== null)
460
+ allBytes.set(sid, b);
461
+ }
359
462
  // FRAME UNANIMITY: a substitution U → C inside the frame (Lf, Rf) is
360
463
  // groundable only when the collected candidates — the store's own sample
361
464
  // of contexts sharing the query's content — are unanimous about the
@@ -408,7 +511,7 @@ export async function substitutionBridge(ctx, query, proposed = []) {
408
511
  let bestAccounted = 0;
409
512
  for (const sid of candidates) {
410
513
  const cBytes = allBytes.get(sid);
411
- if (cBytes.length === 0)
514
+ if (cBytes === undefined)
412
515
  continue;
413
516
  // Seed at the rarest picked anchor that literally occurs in this
414
517
  // candidate.
@@ -520,7 +623,7 @@ export async function substitutionBridge(ctx, query, proposed = []) {
520
623
  // resonance rank but has no outgoing edge to bridge through). This
521
624
  // mechanism exists to explain SUBSTITUTIONS; a query needing none is
522
625
  // recall's job, not the bridge's.
523
- if (!ok || subs.length === 0)
626
+ if (!ok)
524
627
  continue;
525
628
  // Coverage: matched runs plus accepted substitutions must dominate the
526
629
  // query, every interior gap already proved ≤ W above, and the EDGES
@@ -546,6 +649,72 @@ export async function substitutionBridge(ctx, query, proposed = []) {
546
649
  continue;
547
650
  if (!dominates(covered, query.length))
548
651
  continue;
652
+ // ZERO-SUBSTITUTION ADMISSION — an IDENTITY claim, not a substitution.
653
+ //
654
+ // A candidate needing no substitution is normally refused (see the trap
655
+ // above), and that refusal is right for the case it was written for: the
656
+ // query is a strict byte-PREFIX of several candidates, each of which
657
+ // continues differently, and nothing here corroborates picking one
658
+ // continuation over another. But that trap has a signature — the
659
+ // candidate carries substantial content BEYOND the alignment, and that
660
+ // surplus is exactly the "answer" the bridge would be inventing.
661
+ //
662
+ // The opposite shape is not ambiguous at all: the alignment explains BOTH
663
+ // strings end to end, and the only thing between them is sub-quantum glue
664
+ // — typographic punctuation the fold treats as structure. Then the two
665
+ // are the SAME learnt form, and grounding through its edge returns that
666
+ // form's own trained answer, never a chosen-among-many completion.
667
+ //
668
+ // Why the ladder cannot reach these otherwise: the gist is a STRUCTURAL
669
+ // signature, so a mid-string insertion shifts every fold boundary after
670
+ // it. Measured: `Who wrote Romeo and Juliet?` against the trained
671
+ // `Who wrote "Romeo and Juliet"?` — two inserted quote characters — scores
672
+ // cos 0.377, BELOW unrelated neighbours like "Who wrote the opera
673
+ // Carmen??" (0.603). Recall's identity tiers gate on identityBar (0.969
674
+ // here) and its reach tiers on 0.875, so no gist-based tier can ever see
675
+ // it; only byte-exact alignment can, which is what this function does.
676
+ //
677
+ // The claim is deliberately strict, in three parts:
678
+ //
679
+ // • QUERY SIDE — EXACT. Every byte of the query must be a literal
680
+ // match against the candidate: covered === query.length, no slack at
681
+ // all, not even sub-quantum. The query is what we are answering, so
682
+ // an identity claim about it may write off NOTHING. This is stricter
683
+ // than the ≤ W edge tolerance the substituted path uses, and it has
684
+ // to be: with a one-window allowance, `what is 2^10?` matched the
685
+ // trained `what is 2+2?` — "^10" against "+2", four bytes, both sides
686
+ // below W — and answered "2+2 is 4.", outweighing cover's authoritative
687
+ // ALU result. Below W, byte OVERLAP is chance rather than evidence;
688
+ // that never made a below-W DIFFERENCE meaningless, and digits are the
689
+ // case that proves it.
690
+ // • CANDIDATE SIDE, INTERIOR — each gap must be an EXPLAINED span (see
691
+ // explainedSpan): sub-quantum glue, or corpus-global scaffolding.
692
+ // This side is asymmetric ON PURPOSE. Material the CANDIDATE has and
693
+ // the query omits is not something the asker asked about: if it is
694
+ // scaffolding, dropping it changes nothing ("What is *the process of*
695
+ // photosynthesis?"); if it is discriminative, the candidate answers a
696
+ // DIFFERENT, narrower question ("Is *heavy* water wet?") and must be
697
+ // refused. Only the corpus can tell those apart, and it does.
698
+ // • CANDIDATE SIDE, SURPLUS — its bytes are the matched runs
699
+ // (byte-identical to the query's, hence the same total length) plus
700
+ // its own gap spans; anything past that is surplus, and surplus is
701
+ // the prefix trap. A prefix-completion candidate fails here by the
702
+ // whole length of the completion it wanted to supply — which is why
703
+ // admitting scaffolding interiors does not reopen that trap.
704
+ //
705
+ // Ordered cheapest-first: the two arithmetic tests run before
706
+ // explainedSpan, whose per-window `reachOf` climbs are the only costly
707
+ // part (shared through the response/conversation reach memo, and reached
708
+ // only by a candidate that already survived every structural gate).
709
+ if (subs.length === 0) {
710
+ if (covered !== query.length)
711
+ continue;
712
+ const cGap = gaps.reduce((n, g) => n + (g.ce - g.cs), 0);
713
+ if (cBytes.length - covered - cGap > W)
714
+ continue;
715
+ if (!gaps.every((g) => explainedSpan(cBytes, g.cs, g.ce)))
716
+ continue;
717
+ }
549
718
  // KNOWN content may never be dismissed — see dismissedKnownContent
550
719
  // (the live case: "what is the capital of france" aligning into a
551
720
  // Matrix synopsis by writing off "ance" — a stored window of the
@@ -1,7 +1,12 @@
1
1
  import type { MindContext } from "./types.js";
2
2
  /** The two sliding-window lengths the WRITE side interns over a stream's leaf
3
3
  * ids: W−1 and W (the river's grouping quantum and its off-by-one neighbour,
4
- * so a form straddling a group boundary is reachable from either cut). */
4
+ * so a form straddling a group boundary is reachable from either cut).
5
+ *
6
+ * Widening this to cover the reader's full segment scale (W−1..2W) was
7
+ * measured and REFUTED: it tripled the store (3,160 → 8,980 nodes on a
8
+ * 200-pair corpus), slowed ingest 80%, and fixed not one test. Unknown
9
+ * regions are not caused by the index's scale. */
5
10
  export declare function canonicalWindows(W: number): [number, number];
6
11
  /** The READ side's chain reach: how many leaf ids a canonical chain may grow
7
12
  * to from one position — W², the deepest two-level composite the write side's
@@ -24,7 +24,12 @@
24
24
  // what training indexed, which no type checker catches.
25
25
  /** The two sliding-window lengths the WRITE side interns over a stream's leaf
26
26
  * ids: W−1 and W (the river's grouping quantum and its off-by-one neighbour,
27
- * so a form straddling a group boundary is reachable from either cut). */
27
+ * so a form straddling a group boundary is reachable from either cut).
28
+ *
29
+ * Widening this to cover the reader's full segment scale (W−1..2W) was
30
+ * measured and REFUTED: it tripled the store (3,160 → 8,980 nodes on a
31
+ * 200-pair corpus), slowed ingest 80%, and fixed not one test. Unknown
32
+ * regions are not caused by the index's scale. */
28
33
  export function canonicalWindows(W) {
29
34
  return [W - 1, W];
30
35
  }
@@ -143,6 +143,15 @@ export declare class GraphSearch {
143
143
  * recursive completion), and chooseNext (distributional-evidence edge
144
144
  * disambiguation when a recognised form has multiple continuations). */
145
145
  host: GraphSearchHost);
146
+ /** The hub bound √N (AGENTS §2.8) — the ONE fan-out cap, stated here
147
+ * rather than imported from `traverse.ts` because this module is
148
+ * deliberately host-based (it holds a bare Store, never a MindContext).
149
+ * That is the same write/read-side duplication convention canonical.ts's
150
+ * header documents: if the formula changes it must change in BOTH places.
151
+ * It is stated ONCE per side, though — the expression used to be spelled
152
+ * out at three call sites here, one of them inside a per-item rules
153
+ * generator, and they had already drifted on the `Math.max(2, …)` floor. */
154
+ private hubBound;
146
155
  /** Explore the Sema graph for the lightest cover of the query and return its
147
156
  * chosen spans left-to-right — WITH the derivation's total weight (the g
148
157
  * value of the goal item, in the exported cost ladder), which think's
@@ -190,6 +190,17 @@ export class GraphSearch {
190
190
  this.maxGroup = maxGroup;
191
191
  this.host = host;
192
192
  }
193
+ /** The hub bound √N (AGENTS §2.8) — the ONE fan-out cap, stated here
194
+ * rather than imported from `traverse.ts` because this module is
195
+ * deliberately host-based (it holds a bare Store, never a MindContext).
196
+ * That is the same write/read-side duplication convention canonical.ts's
197
+ * header documents: if the formula changes it must change in BOTH places.
198
+ * It is stated ONCE per side, though — the expression used to be spelled
199
+ * out at three call sites here, one of them inside a per-item rules
200
+ * generator, and they had already drifted on the `Math.max(2, …)` floor. */
201
+ hubBound() {
202
+ return Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
203
+ }
193
204
  /** Explore the Sema graph for the lightest cover of the query and return its
194
205
  * chosen spans left-to-right — WITH the derivation's total weight (the g
195
206
  * value of the goal item, in the exported cost ladder), which think's
@@ -240,7 +251,17 @@ export class GraphSearch {
240
251
  * that leads nowhere new, never at an arbitrary count. */
241
252
  solve(spanLen, recognition, conceptTarget, substitutions, connectors, computedResults, onDerivation) {
242
253
  const system = this.buildSearch(spanLen, recognition.sites, conceptTarget, recognition.leaves, recognition.splits, recognition.starts, substitutions, connectors, computedResults);
243
- const derivation = lightestDerivation(system);
254
+ // Search-effort accounting (src/meter.ts): the chart's pops/pushes are
255
+ // the cover's real cost, and a heuristic that stops being admissible
256
+ // shows up as a pop count that explodes while the answer stays the same.
257
+ const meter = this.host.meter;
258
+ const stats = meter ? { pops: 0, pushes: 0 } : undefined;
259
+ const derivation = lightestDerivation(system, stats);
260
+ if (meter && stats) {
261
+ meter.searches++;
262
+ meter.searchPops += stats.pops;
263
+ meter.searchPushes += stats.pushes;
264
+ }
244
265
  // When covering under a substitution map (articulation), a form→out rule is
245
266
  // the form EMITTING the asker's voice, not grounding to its own answer — so
246
267
  // tell the reader to name those moves `voice` rather than `ground`.
@@ -262,12 +283,9 @@ export class GraphSearch {
262
283
  buildSearch(queryLen, sites, conceptTarget, leaves, splits, starts, substitutions, connectors, computedResults) {
263
284
  const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
264
285
  // Same corpus-scale hub floor {@link atomIsHub}/{@link atomReach} (traverse.ts)
265
- // derive for byte atoms — duplicated here rather than imported because this
266
- // module is deliberately host-based (no MindContext), and both inputs
267
- // (maxGroup, edgeSourceCount) are already in scope with no ctx needed. If
268
- // the formula ever changes, it must change in BOTH places (see canonical.ts's
269
- // header for the same write/read-side duplication convention).
270
- const atomsAreHubs = Math.max(1, Math.ceil((this.store.edgeSourceCount() * W) / 256)) > Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
286
+ // derive for byte atoms — see {@link hubBound} below for why this module
287
+ // states the formula itself instead of importing it.
288
+ const atomsAreHubs = Math.max(1, Math.ceil((this.store.edgeSourceCount() * W) / 256)) > this.hubBound();
271
289
  const nodeBytes = (n) => this.store.bytesPrefix(n, ALL);
272
290
  // Content-addressed probes over the store's hash-cons maps — the same keys
273
291
  // training filled. No byte-by-byte trie walk.
@@ -511,8 +529,7 @@ export class GraphSearch {
511
529
  // guard then dead-ends it) with no way to reach the forward edge.
512
530
  // Forking offers every continuation as its own rule so the one that
513
531
  // genuinely advances (not a duplicate) is still reachable.
514
- const bound = Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
515
- const nx = this.store.nextFirst(it.node, bound);
532
+ const nx = this.store.nextFirst(it.node, this.hubBound());
516
533
  if (nx.length) {
517
534
  // The SAME evidence-weighted disambiguation the first hop uses
518
535
  // (below) identifies the most-corroborated continuation. Yielding
@@ -861,16 +878,39 @@ export class GraphSearch {
861
878
  // is opportunistic cross-leaf recovery exactly like recognition.ts's own
862
879
  // canonical chain — findLeaf/findBranch here have no idea WHY these two
863
880
  // leaves are adjacent, only that their concatenation happens to spell a
864
- // trained form ("hi" recovered from "W[hi]ch"). The same corpus-scale
865
- // caution recognition.ts's `boundary` gate applies: trust it fully when
866
- // `l.i` is a position the query's OWN fold chose as a boundary (real
867
- // structural evidence); past the scale where atoms themselves stop
868
- // discriminating, an interior offset's opportunistic match is noise, not
869
- // a genuine cross-leaf recovery. A completion-involved fuse (l.rec ||
870
- // r.rec) is a different, legitimate case — a rewrite growing into its
871
- // neighbour and is exempt, same as recognition.ts's rec-derived sites.
872
- const trusted = l.rec || r.rec || ctx.starts.has(l.i) ||
873
- !ctx.atomsAreHubs;
881
+ // trained form ("hi" recovered from "W[hi]ch"). At hub scale, where
882
+ // atoms themselves no longer discriminate, that coincidence is noise.
883
+ //
884
+ // A FOLD BOUNDARY IS NOT EVIDENCE. This gate used to exempt any fuse
885
+ // starting at `starts.has(l.i)` documented as "a position the query's
886
+ // OWN fold chose as a boundary (real structural evidence)". The plain
887
+ // river fold CHOOSES NOTHING: `riverFold` groups fixed-arity
888
+ // (`for (i = 0; i < complete; i += mg)`), so `starts` is exactly
889
+ // {0, W, 2W, 3W, …} for every query. Measured on three unrelated
890
+ // queries: starts = 0,4,8,12,16,20,24,28[,32] in every case — the set
891
+ // carries ZERO content information, and the exemption therefore fired at
892
+ // a quarter of all offsets by arithmetic alone.
893
+ //
894
+ // What it cost (17.9M-node store, W=4): "In which country is the Eiffel
895
+ // Tower?" fused the atoms "h"+"i" at offset 4 — trusted only because
896
+ // 4 ≡ 0 (mod 4) — into the trained form "hi", followed its continuation
897
+ // edge, and grounded "Hello there. If you are looking for Open
898
+ // Assistant, look no further." as a FACT, explaining 2 of 37 bytes
899
+ // (density 0.054, a fifth of the 1/W honesty bar `thinGrounding`
900
+ // reports and does not enforce).
901
+ //
902
+ // So the exemption is removed rather than replaced: there is no cheap
903
+ // signal here that means what it claimed to mean, and inventing one
904
+ // would be worse than admitting the absence. Genuine cross-leaf forms
905
+ // are not lost — recognition.ts's canonical pass already probes EVERY
906
+ // byte offset for the write side's interned windows and emits them as
907
+ // sites, which arrive here as `l.rec`/`r.rec` and stay exempt. What
908
+ // remains excluded at hub scale is precisely the case with no evidence
909
+ // behind it: two byte atoms that happen to spell something.
910
+ //
911
+ // Below hub scale nothing changes (`!atomsAreHubs`): on a small store
912
+ // coincidence is rare and every chain is real evidence.
913
+ const trusted = l.rec || r.rec || !ctx.atomsAreHubs;
874
914
  let node = (trusted && bytes.length <= ctx.W)
875
915
  ? ctx.findLeafU(bytes)
876
916
  : undefined;
@@ -5,3 +5,5 @@ export type { MechanismResult, PipelineMechanism, Precomputed, } from "./pipelin
5
5
  export type { InspectRationale, RationaleItem, RationaleStep, } from "./rationale.js";
6
6
  export type { AnchorRejectionReason, ClimbConsensusData, ConsensusAnchorTrace, ConsensusReachTrace, ConsensusRegionTrace, CrossRegionTier, JunctionVoteTrace, RegionOutcome, } from "./attention.js";
7
7
  export type { AncestorReach, AttentionRead, SaturationReason, SaturationStop, } from "./types.js";
8
+ export type { DepositReport } from "./learning.js";
9
+ export type { DecideGroundingData, NarrowDecisionData, Provenance, } from "./pipeline.js";
@@ -85,6 +85,16 @@ export declare function cachedRead(ctx: MindContext, cache: WalkCache | null, id
85
85
  * few levels of its parts). A side too common to decide within the
86
86
  * budget abstains here and falls through to the resonance tier (the
87
87
  * climb's own saturation semantics).
88
+ *
89
+ * REFUTED TIGHTENING (measured, 325K contexts): giving this walk
90
+ * edgeAncestors' cumulative LATERAL-CONE limit — abstain once the
91
+ * accumulated lateral entries pass √N — would cut most of the pops (47
92
+ * of 56 walks exceed √N lateral, and 47 exhaust the budget), but TWO OF
93
+ * THE FOUR walks that actually found a container had lateral spread of
94
+ * 1425 and 1426, far past √N. The cone limit is sound for
95
+ * edgeAncestors' question and wrong for this one: a junction container
96
+ * is legitimately reached across many containing structures. Half the
97
+ * successful junctions would be lost.
88
98
  * • per-node hub guards — parent fan-outs beyond √N are hubs (not
89
99
  * expanded); each node contributes at most one √N page of containers;
90
100
  * √N collected candidates decide. */
@@ -115,6 +115,16 @@ function cachedContainers(ctx, cache, id, limit) {
115
115
  * few levels of its parts). A side too common to decide within the
116
116
  * budget abstains here and falls through to the resonance tier (the
117
117
  * climb's own saturation semantics).
118
+ *
119
+ * REFUTED TIGHTENING (measured, 325K contexts): giving this walk
120
+ * edgeAncestors' cumulative LATERAL-CONE limit — abstain once the
121
+ * accumulated lateral entries pass √N — would cut most of the pops (47
122
+ * of 56 walks exceed √N lateral, and 47 exhaust the budget), but TWO OF
123
+ * THE FOUR walks that actually found a container had lateral spread of
124
+ * 1425 and 1426, far past √N. The cone limit is sound for
125
+ * edgeAncestors' question and wrong for this one: a junction container
126
+ * is legitimately reached across many containing structures. Half the
127
+ * successful junctions would be lost.
118
128
  * • per-node hub guards — parent fan-outs beyond √N are hubs (not
119
129
  * expanded); each node contributes at most one √N page of containers;
120
130
  * √N collected candidates decide. */
@@ -137,6 +147,8 @@ unordered = false) {
137
147
  if (seeds.length === 0)
138
148
  return [];
139
149
  const b = budget ?? { n: bound * ctx.space.maxGroup };
150
+ if (ctx.meter)
151
+ ctx.meter.junctionWalks++;
140
152
  // DEPTH CAP: perception trees are W-ary and a junction container is
141
153
  // phrase-scale, so it sits within ~log_W(maxContainer) structural levels
142
154
  // of its parts — at most W levels for any practical W (plus the
@@ -153,6 +165,8 @@ unordered = false) {
153
165
  }));
154
166
  while (stack.length > 0 && out.length < bound && b.n-- > 0) {
155
167
  const { id: x, d } = stack.pop();
168
+ if (ctx.meter)
169
+ ctx.meter.junctionPops++;
156
170
  const f = cachedRead(ctx, cache, x, maxContainer);
157
171
  if (f.length > maxContainer)
158
172
  continue; // beyond phrase scale: prune branch
@@ -28,8 +28,30 @@ export declare function deposit(ctx: MindContext, input: Input, track: boolean,
28
28
  export declare function ingestOne(ctx: MindContext, input: Input): Promise<Sema & {
29
29
  id: number;
30
30
  }>;
31
- /** Ingest a pair (context, continuation)learn an edge and pour halos. */
32
- export declare function ingestPair(ctx: MindContext, ctxInput: Input, cont: Input): Promise<void>;
31
+ /** What one ingested item depositedreported through {@link ingest}'s
32
+ * optional `onDeposit` callback. Pure provenance read-out: node ids are
33
+ * content-addressed, so two byte-identical items report the SAME ids (that
34
+ * is content addressing working, not an error), and the callback observes
35
+ * the deposit without influencing it. */
36
+ export interface DepositReport {
37
+ /** Zero-based position of the item in the ingested input (0 for a scalar
38
+ * or single pair). */
39
+ index: number;
40
+ /** Whether the item was a bare experience or a (context, continuation)
41
+ * pair. */
42
+ kind: "one" | "pair";
43
+ /** Root node id of the item's (context) bytes. */
44
+ contextId: number;
45
+ /** Root node id of the continuation bytes — pairs only. */
46
+ continuationId?: number;
47
+ }
48
+ /** Ingest a pair (context, continuation) — learn an edge and pour halos.
49
+ * Returns the deposited root ids (context, continuation) — a pure
50
+ * read-out; callers that ignore it behave exactly as before. */
51
+ export declare function ingestPair(ctx: MindContext, ctxInput: Input, cont: Input): Promise<{
52
+ ctxId: number;
53
+ contId: number;
54
+ }>;
33
55
  /** Dispatch the public ingest input shapes onto one-input / pair handlers —
34
56
  * THE one reading of ingest's polymorphic surface (scalar, (context,
35
57
  * continuation) pair, or a list mixing bare inputs and pairs). Both ingest
@@ -41,7 +63,13 @@ export declare function dispatchIngest(input: Input | (Input | [Input, Input])[]
41
63
  }>, onPair: (ctxInput: Input, cont: Input) => Promise<void>): Promise<(Sema & {
42
64
  id: number;
43
65
  }) | undefined>;
44
- /** Ingest an input or array of inputs/pairs. The public ingest entry point. */
45
- export declare function ingest(ctx: MindContext, input: Input | (Input | [Input, Input])[], second?: Input): Promise<(Sema & {
66
+ /** Ingest an input or array of inputs/pairs. The public ingest entry point.
67
+ *
68
+ * `onDeposit`, when given, is invoked once per ingested item with the
69
+ * deposited root node ids ({@link DepositReport}) — item-level provenance
70
+ * for tooling that needs to know which stored node an ingested item became.
71
+ * Purely observational: the callback runs after the item's deposit
72
+ * completed and nothing reads its result. */
73
+ export declare function ingest(ctx: MindContext, input: Input | (Input | [Input, Input])[], second?: Input, onDeposit?: (report: DepositReport) => void): Promise<(Sema & {
46
74
  id: number;
47
75
  }) | undefined>;
@@ -188,7 +188,9 @@ async function propagateSuffixes(ctx, src, dst) {
188
188
  await ctx.store.link(id, dst);
189
189
  }
190
190
  }
191
- /** Ingest a pair (context, continuation) — learn an edge and pour halos. */
191
+ /** Ingest a pair (context, continuation) — learn an edge and pour halos.
192
+ * Returns the deposited root ids (context, continuation) — a pure
193
+ * read-out; callers that ignore it behave exactly as before. */
192
194
  export async function ingestPair(ctx, ctxInput, cont) {
193
195
  const c = await deposit(ctx, ctxInput, true, true);
194
196
  const cont_ = await deposit(ctx, cont, false);
@@ -213,6 +215,7 @@ export async function ingestPair(ctx, ctxInput, cont) {
213
215
  await ctx.store.pourHalo(partId, contSeat);
214
216
  await ctx.store.pourHalo(contId, bindSeat(ctx.space, companySignature(ctx.space, partId), 0));
215
217
  }
218
+ return { ctxId, contId };
216
219
  }
217
220
  /** Dispatch the public ingest input shapes onto one-input / pair handlers —
218
221
  * THE one reading of ingest's polymorphic surface (scalar, (context,
@@ -242,7 +245,26 @@ export async function dispatchIngest(input, second, onOne, onPair) {
242
245
  await onPair(input, second);
243
246
  return undefined;
244
247
  }
245
- /** Ingest an input or array of inputs/pairs. The public ingest entry point. */
246
- export async function ingest(ctx, input, second) {
247
- return dispatchIngest(input, second, (i) => ingestOne(ctx, i), (a, b) => ingestPair(ctx, a, b));
248
+ /** Ingest an input or array of inputs/pairs. The public ingest entry point.
249
+ *
250
+ * `onDeposit`, when given, is invoked once per ingested item with the
251
+ * deposited root node ids ({@link DepositReport}) — item-level provenance
252
+ * for tooling that needs to know which stored node an ingested item became.
253
+ * Purely observational: the callback runs after the item's deposit
254
+ * completed and nothing reads its result. */
255
+ export async function ingest(ctx, input, second, onDeposit) {
256
+ let index = 0;
257
+ return dispatchIngest(input, second, async (i) => {
258
+ const r = await ingestOne(ctx, i);
259
+ onDeposit?.({ index: index++, kind: "one", contextId: r.id });
260
+ return r;
261
+ }, async (a, b) => {
262
+ const { ctxId, contId } = await ingestPair(ctx, a, b);
263
+ onDeposit?.({
264
+ index: index++,
265
+ kind: "pair",
266
+ contextId: ctxId,
267
+ continuationId: contId,
268
+ });
269
+ });
248
270
  }
@@ -34,6 +34,13 @@ export declare function alignRuns(ctx: MindContext, query: Uint8Array, ct: Uint8
34
34
  * kind, so the substitution/redirection schemas work unchanged on conceptual
35
35
  * alignment. */
36
36
  export interface GradedRun {
37
+ /** True for a run the CLIMB proposed rather than the byte matcher found — see
38
+ * {@link computeWeave}'s phase 2. Provenance only: a proposed run reaches
39
+ * CAST already gated (dominant literal agreement, not frame, and over bytes
40
+ * no literal run claimed), so every consumer reads it exactly like any other
41
+ * run. Filtering seat-shaped reads by this flag was tried and is NOT
42
+ * needed — the gates, not the flag, are what make it safe. */
43
+ proposed?: boolean;
37
44
  qs: number;
38
45
  qe: number;
39
46
  cs: number;
@@ -124,3 +131,36 @@ export declare function reverseContext(ctx: MindContext, id: number, guide?: Vec
124
131
  * the context it follows. This is the direction ladder every mechanism's
125
132
  * final grounding step reduces to. */
126
133
  export declare function project(ctx: MindContext, id: number, guide?: Vec | null): Promise<Uint8Array | null>;
134
+ /** Check whether an anchor is a span-shaped skill exemplar: it represents a
135
+ * fact whose context and answer together form a span-in-context pattern.
136
+ * If the anchor has a nextOf continuation, that is the answer and the anchor
137
+ * itself is the context. Otherwise the anchor's prevOf parents provide
138
+ * candidate contexts, and the longest one whose span is span-shaped wins. */
139
+ export declare function skillExemplar(ctx: MindContext, anchor: number, guide?: Vec | null): Promise<{
140
+ contextBytes: Uint8Array;
141
+ answerBytes: Uint8Array;
142
+ } | null>;
143
+ /** Whether the answer is a SPARSE subsequence of the context (bytes in
144
+ * order, arbitrary gaps) — the OPEN span-shape reading (see the section
145
+ * note above). This is what lets extraction validate a MULTI-PIECE
146
+ * exemplar whose answer is stitched from several context runs — but it is
147
+ * deliberately permissive, so it must never be used as evidence that one
148
+ * span was "drawn from" another (see {@link containsSpan} for that).
149
+ *
150
+ * There is deliberately NO containsSpan pre-check here: strict containment
151
+ * IMPLIES the subsequence embedding (a contiguous run, or a resolved node —
152
+ * whose content-addressed identity means its bytes occur contiguously — is
153
+ * an in-order embedding with zero gaps), so the scan below decides alone,
154
+ * with the same truth value. The old pre-check re-perceived the context
155
+ * (a full river fold) per CANDIDATE in skillExemplar's √N-capped loop —
156
+ * pure cost, no discrimination. */
157
+ export declare function isSpanShaped(_ctx: MindContext, context: Uint8Array, answer: Uint8Array): boolean;
158
+ /** STRICT containment: the answer's resolved node appears in the context's
159
+ * folded tree, or the answer occurs as one CONTIGUOUS byte run of the
160
+ * context. This is real evidence the answer was drawn from the context.
161
+ * Fusion gates on this — the sparse-subsequence reading of
162
+ * {@link isSpanShaped} is trivially satisfied by short answers over long
163
+ * queries ("cold" is a gap-tolerant subsequence of most sentences holding
164
+ * c…o…l…d in order), and gating fusion on it silently starved multi-topic
165
+ * queries of their further points of attention. */
166
+ export declare function containsSpan(ctx: MindContext, context: Uint8Array, answer: Uint8Array): boolean;