@hviana/sema 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/AGENTS.md +71 -5
  2. package/dist/src/derive/src/deduction.d.ts +12 -1
  3. package/dist/src/derive/src/deduction.js +5 -1
  4. package/dist/src/derive/src/index.d.ts +1 -0
  5. package/dist/src/geometry.d.ts +3 -30
  6. package/dist/src/geometry.js +330 -82
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/meter.d.ts +171 -0
  10. package/dist/src/meter.js +269 -0
  11. package/dist/src/mind/attention.d.ts +0 -4
  12. package/dist/src/mind/attention.js +251 -23
  13. package/dist/src/mind/bridge.d.ts +10 -1
  14. package/dist/src/mind/bridge.js +179 -10
  15. package/dist/src/mind/canonical.d.ts +6 -1
  16. package/dist/src/mind/canonical.js +6 -1
  17. package/dist/src/mind/graph-search.d.ts +9 -0
  18. package/dist/src/mind/graph-search.js +59 -19
  19. package/dist/src/mind/junction.d.ts +10 -0
  20. package/dist/src/mind/junction.js +14 -0
  21. package/dist/src/mind/match.d.ts +40 -0
  22. package/dist/src/mind/match.js +125 -1
  23. package/dist/src/mind/mechanisms/cast.js +63 -6
  24. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  25. package/dist/src/mind/mechanisms/extraction.js +1 -88
  26. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  27. package/dist/src/mind/mechanisms/recall.js +77 -14
  28. package/dist/src/mind/mind.d.ts +55 -4
  29. package/dist/src/mind/mind.js +110 -91
  30. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  31. package/dist/src/mind/pipeline-mechanism.js +179 -10
  32. package/dist/src/mind/pipeline.js +52 -10
  33. package/dist/src/mind/primitives.d.ts +11 -15
  34. package/dist/src/mind/primitives.js +47 -28
  35. package/dist/src/mind/reasoning.d.ts +7 -1
  36. package/dist/src/mind/reasoning.js +40 -8
  37. package/dist/src/mind/recognition.js +93 -20
  38. package/dist/src/mind/traverse.d.ts +11 -0
  39. package/dist/src/mind/traverse.js +58 -5
  40. package/dist/src/mind/types.d.ts +28 -5
  41. package/dist/src/store.d.ts +15 -0
  42. package/dist/src/store.js +91 -6
  43. package/package.json +1 -1
  44. package/src/derive/src/deduction.ts +15 -0
  45. package/src/derive/src/index.ts +1 -0
  46. package/src/geometry.ts +350 -122
  47. package/src/index.ts +1 -0
  48. package/src/meter.ts +333 -0
  49. package/src/mind/attention.ts +268 -31
  50. package/src/mind/bridge.ts +187 -10
  51. package/src/mind/canonical.ts +6 -1
  52. package/src/mind/graph-search.ts +60 -21
  53. package/src/mind/junction.ts +12 -0
  54. package/src/mind/match.ts +146 -1
  55. package/src/mind/mechanisms/cast.ts +62 -6
  56. package/src/mind/mechanisms/extraction.ts +2 -103
  57. package/src/mind/mechanisms/recall.ts +84 -17
  58. package/src/mind/mind.ts +139 -98
  59. package/src/mind/pipeline-mechanism.ts +203 -13
  60. package/src/mind/pipeline.ts +65 -8
  61. package/src/mind/primitives.ts +49 -33
  62. package/src/mind/reasoning.ts +39 -7
  63. package/src/mind/recognition.ts +89 -19
  64. package/src/mind/traverse.ts +59 -5
  65. package/src/mind/types.ts +28 -5
  66. package/src/store.ts +75 -6
  67. package/test/14-scaling.test.mjs +17 -7
  68. package/test/31-audit.test.mjs +4 -1
  69. package/test/33-multi-candidate.test.mjs +13 -1
  70. package/test/36-already-answered-fusion.test.mjs +10 -3
  71. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  72. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  73. package/test/55-cost-meter.test.mjs +284 -0
  74. package/test/56-bridge-identity-admission.test.mjs +209 -0
  75. package/test/57-fusion-order.test.mjs +104 -0
  76. package/test/58-subquantum-sites.test.mjs +112 -0
  77. package/test/59-fold-invariance.test.mjs +226 -0
@@ -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;
@@ -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) {
package/src/mind/match.ts CHANGED
@@ -31,7 +31,7 @@ import { conceptThreshold, identityBar, significanceBar } from "../geometry.js";
31
31
  import { indexOf } from "../bytes.js";
32
32
  import type { MindContext } from "./types.js";
33
33
  import { leafIdRun } from "./canonical.js";
34
- import { gistOf, read, resolve } from "./primitives.js";
34
+ import { foldTree, gistOf, perceive, read, resolve } from "./primitives.js";
35
35
  import {
36
36
  argmaxCosine,
37
37
  chooseAmong,
@@ -119,6 +119,25 @@ export function alignRuns(
119
119
  query: Uint8Array,
120
120
  ct: Uint8Array,
121
121
  ): Array<{ qs: number; qe: number; cs: number }> {
122
+ if (ctx.meter) {
123
+ ctx.meter.alignments++;
124
+ // The alignment family's honest unit: the seed index is O(|query|) but
125
+ // the run extension is O(|query|·|ct|) in the worst case, and a weave
126
+ // that starts scanning conversation-length contexts shows up HERE long
127
+ // before it shows up in a call count.
128
+ ctx.meter.alignCells += query.length * ct.length;
129
+ }
130
+ // MEASURED AND REFUTED — seeding at the write side's unit floor W−1 instead
131
+ // of W. `canonicalWindows` interns both lengths precisely so a form
132
+ // straddling a group boundary is reachable from either cut, and the runs
133
+ // found here are exactly such remnants: test/29 C1's query shares only `ce `
134
+ // with `Ice is cold` — three bytes, never seeded at W, so that structure
135
+ // enters the weave carrying nothing but the scaffolding run every exemplar
136
+ // shares. But this is a byte MATCHER between two streams, not an index, and
137
+ // at W−1 the corpus is dense with spurious 3-byte agreements: the extra runs
138
+ // reshuffle which point claims which span, and test/29 A2 loses its analog.
139
+ // C1 does not pass either way. The store's unit floor does not transfer to
140
+ // the aligner's seed length.
122
141
  const quantum = Math.min(ctx.space.maxGroup, ct.length);
123
142
  if (quantum < 1 || query.length < quantum) return [];
124
143
  const gram = (b: Uint8Array, at: number): string => {
@@ -167,6 +186,13 @@ export function alignRuns(
167
186
  * kind, so the substitution/redirection schemas work unchanged on conceptual
168
187
  * alignment. */
169
188
  export interface GradedRun {
189
+ /** True for a run the CLIMB proposed rather than the byte matcher found — see
190
+ * {@link computeWeave}'s phase 2. Provenance only: a proposed run reaches
191
+ * CAST already gated (dominant literal agreement, not frame, and over bytes
192
+ * no literal run claimed), so every consumer reads it exactly like any other
193
+ * run. Filtering seat-shaped reads by this flag was tried and is NOT
194
+ * needed — the gates, not the flag, are what make it safe. */
195
+ proposed?: boolean;
170
196
  qs: number;
171
197
  qe: number;
172
198
  cs: number;
@@ -542,3 +568,122 @@ export async function project(
542
568
  if (fc) return fc;
543
569
  return reverseContext(ctx, id, guide);
544
570
  }
571
+
572
+ // ── The span-shape family ───────────────────────────────────────────────────
573
+ //
574
+ // "Is this answer drawn from this context?" has TWO formally distinct
575
+ // readings, and the pair plus the anchor classifier built on them are SHARED
576
+ // machinery — extraction proposes span-shaped exemplars with them, the
577
+ // shared `Precomputed.spanShapedOf` container computes them, and fusion
578
+ // (reasoning.ts) gates on the strict one. They lived inside
579
+ // mechanisms/extraction.ts, so `pipeline-mechanism.ts` and `reasoning.ts`
580
+ // both had to import back OUT of a specific mechanism — an inversion the
581
+ // mechanism market forbids (AGENTS §2.6: the shared contract may not depend
582
+ // on any one mechanism; §2.5: a shared matcher belongs to this family, never
583
+ // to a mechanism's private helpers). Deleting extraction must not break the
584
+ // shared container, so they live here.
585
+ //
586
+ // • isSpanShaped — the OPEN reading (sparse in-order embedding).
587
+ // • containsSpan — the STRICT reading (contiguous run or resolved node).
588
+ // • skillExemplar — classify one anchor into (context, answer) using them.
589
+ //
590
+ // The two readings are NOT interchangeable; AGENTS §2.5 pins the distinction
591
+ // and each function's own doc states what breaks if it is substituted.
592
+
593
+ /** Check whether an anchor is a span-shaped skill exemplar: it represents a
594
+ * fact whose context and answer together form a span-in-context pattern.
595
+ * If the anchor has a nextOf continuation, that is the answer and the anchor
596
+ * itself is the context. Otherwise the anchor's prevOf parents provide
597
+ * candidate contexts, and the longest one whose span is span-shaped wins. */
598
+ export async function skillExemplar(
599
+ ctx: MindContext,
600
+ anchor: number,
601
+ guide?: Vec | null,
602
+ ): Promise<{ contextBytes: Uint8Array; answerBytes: Uint8Array } | null> {
603
+ if (ctx.store.hasNext(anchor)) {
604
+ const contextBytes = read(ctx, anchor);
605
+ const answerBytes = await follow(ctx, anchor, guide);
606
+ if (
607
+ answerBytes !== null && isSpanShaped(ctx, contextBytes, answerBytes)
608
+ ) {
609
+ return { contextBytes, answerBytes };
610
+ }
611
+ return null;
612
+ }
613
+ const answerBytes = read(ctx, anchor);
614
+ // Candidate contexts, capped at the hub bound (a common answer's reverse
615
+ // fan-in is corpus-sized).
616
+ const capped = ctx.store.prevFirst(anchor, hubBound(ctx));
617
+ const spanShaped: Array<{ id: number; bytes: Uint8Array }> = [];
618
+ for (const p of capped) {
619
+ const ctxB = read(ctx, p);
620
+ if (ctxB.length > 0 && isSpanShaped(ctx, ctxB, answerBytes)) {
621
+ spanShaped.push({ id: p, bytes: ctxB });
622
+ }
623
+ }
624
+ if (spanShaped.length === 0) return null;
625
+ // Among span-shaped contexts, the longest wins (the smallest spanning frame
626
+ // heuristic's dual: more frame to locate in the query); the query gist,
627
+ // when given, breaks LENGTH TIES via chooseAmong — the same reverse-regime
628
+ // disambiguator every context pick uses, whose gist cache spares the
629
+ // re-fold this block once paid per tied candidate. Same strict first-seen
630
+ // tie-break as the hand loop it replaces.
631
+ const maxLen = Math.max(...spanShaped.map((s) => s.bytes.length));
632
+ const longest = spanShaped.filter((s) => s.bytes.length === maxLen);
633
+ let contextBytes = longest[0].bytes;
634
+ if (guide && longest.length > 1) {
635
+ const pick = chooseAmong(ctx, longest.map((s) => s.id), guide).id;
636
+ contextBytes = longest.find((s) => s.id === pick)!.bytes;
637
+ }
638
+ return { contextBytes, answerBytes };
639
+ }
640
+
641
+ /** Whether the answer is a SPARSE subsequence of the context (bytes in
642
+ * order, arbitrary gaps) — the OPEN span-shape reading (see the section
643
+ * note above). This is what lets extraction validate a MULTI-PIECE
644
+ * exemplar whose answer is stitched from several context runs — but it is
645
+ * deliberately permissive, so it must never be used as evidence that one
646
+ * span was "drawn from" another (see {@link containsSpan} for that).
647
+ *
648
+ * There is deliberately NO containsSpan pre-check here: strict containment
649
+ * IMPLIES the subsequence embedding (a contiguous run, or a resolved node —
650
+ * whose content-addressed identity means its bytes occur contiguously — is
651
+ * an in-order embedding with zero gaps), so the scan below decides alone,
652
+ * with the same truth value. The old pre-check re-perceived the context
653
+ * (a full river fold) per CANDIDATE in skillExemplar's √N-capped loop —
654
+ * pure cost, no discrimination. */
655
+ export function isSpanShaped(
656
+ _ctx: MindContext,
657
+ context: Uint8Array,
658
+ answer: Uint8Array,
659
+ ): boolean {
660
+ let ai = 0;
661
+ for (let ci = 0; ci < context.length && ai < answer.length; ci++) {
662
+ if (context[ci] === answer[ai]) ai++;
663
+ }
664
+ return ai === answer.length;
665
+ }
666
+
667
+ /** STRICT containment: the answer's resolved node appears in the context's
668
+ * folded tree, or the answer occurs as one CONTIGUOUS byte run of the
669
+ * context. This is real evidence the answer was drawn from the context.
670
+ * Fusion gates on this — the sparse-subsequence reading of
671
+ * {@link isSpanShaped} is trivially satisfied by short answers over long
672
+ * queries ("cold" is a gap-tolerant subsequence of most sentences holding
673
+ * c…o…l…d in order), and gating fusion on it silently starved multi-topic
674
+ * queries of their further points of attention. */
675
+ export function containsSpan(
676
+ ctx: MindContext,
677
+ context: Uint8Array,
678
+ answer: Uint8Array,
679
+ ): boolean {
680
+ const ansId = resolve(ctx, answer);
681
+ if (ansId !== null) {
682
+ let found = false;
683
+ foldTree(ctx, perceive(ctx, context), 0, (_n, _s, _e, node) => {
684
+ if (node === ansId) found = true;
685
+ });
686
+ if (found) return true;
687
+ }
688
+ return indexOf(context, answer, 0) >= 0;
689
+ }