@hviana/sema 0.4.4 → 0.4.6

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 (51) hide show
  1. package/AUTHORS.md +0 -1
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -2
  4. package/dist/src/geometry.d.ts +6 -0
  5. package/dist/src/geometry.js +224 -44
  6. package/dist/src/mind/attention.d.ts +11 -0
  7. package/dist/src/mind/attention.js +344 -13
  8. package/dist/src/mind/junction.js +18 -2
  9. package/dist/src/mind/match.d.ts +11 -0
  10. package/dist/src/mind/match.js +13 -2
  11. package/dist/src/mind/mechanisms/cast.js +366 -34
  12. package/dist/src/mind/mechanisms/confluence.js +17 -1
  13. package/dist/src/mind/mechanisms/recall.js +17 -3
  14. package/dist/src/mind/pipeline-mechanism.d.ts +4 -0
  15. package/dist/src/mind/pipeline-mechanism.js +96 -40
  16. package/dist/src/mind/pipeline.js +31 -3
  17. package/dist/src/mind/reasoning.d.ts +4 -2
  18. package/dist/src/mind/reasoning.js +29 -4
  19. package/dist/src/mind/recognition.js +67 -2
  20. package/dist/src/mind/resonance.d.ts +14 -2
  21. package/dist/src/mind/resonance.js +0 -0
  22. package/dist/src/mind/types.d.ts +43 -1
  23. package/dist/src/sema.d.ts +11 -1
  24. package/dist/src/sema.js +16 -2
  25. package/dist/src/store.d.ts +64 -1
  26. package/dist/src/store.js +107 -8
  27. package/index.html +2 -3
  28. package/package.json +1 -1
  29. package/src/geometry.ts +231 -43
  30. package/src/mind/attention.ts +366 -15
  31. package/src/mind/junction.ts +18 -2
  32. package/src/mind/match.ts +18 -2
  33. package/src/mind/mechanisms/cast.ts +376 -43
  34. package/src/mind/mechanisms/confluence.ts +16 -1
  35. package/src/mind/mechanisms/recall.ts +17 -2
  36. package/src/mind/pipeline-mechanism.ts +96 -36
  37. package/src/mind/pipeline.ts +33 -3
  38. package/src/mind/reasoning.ts +31 -4
  39. package/src/mind/recognition.ts +65 -2
  40. package/src/mind/resonance.ts +0 -0
  41. package/src/mind/types.ts +43 -1
  42. package/src/sema.ts +21 -2
  43. package/src/store.ts +106 -5
  44. package/test/00-extract.test.mjs +28 -0
  45. package/test/15-decomposition-gap.test.mjs +0 -0
  46. package/test/24-generalization.test.mjs +67 -19
  47. package/test/29-counterfactual.test.mjs +106 -42
  48. package/test/33-multi-candidate.test.mjs +56 -12
  49. package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
  50. package/test/63-fold-invariants.test.mjs +489 -0
  51. package/test/64-two-ended-thresholds.test.mjs +76 -0
@@ -14,13 +14,20 @@
14
14
  import type { MindContext } from "../types.js";
15
15
  import type { Vec } from "../../vec.js";
16
16
  import { read } from "../primitives.js";
17
- import { argmaxBy, corpusN, hubBound } from "../traverse.js";
17
+ import {
18
+ argmaxBy,
19
+ corpusN,
20
+ edgeAncestors,
21
+ hubBound,
22
+ sharedReachMemo,
23
+ } from "../traverse.js";
18
24
  import {
19
25
  analogyStrength,
20
26
  follow,
21
27
  type GradedRun,
22
28
  project,
23
29
  reverseContext,
30
+ sharedFrameStrengthOf,
24
31
  } from "../match.js";
25
32
  import { joinWithBridge } from "../resonance.js";
26
33
  import { restatesQuery } from "../reasoning.js";
@@ -34,6 +41,7 @@ import {
34
41
  } from "../rationale.js";
35
42
  import { rItem, rNode } from "../trace.js";
36
43
  import { dismissedKnownContent } from "../bridge.js";
44
+ import { leafIdRun } from "../canonical.js";
37
45
 
38
46
  // ── CAST gates ────────────────────────────────────────────────────────────
39
47
  //
@@ -218,7 +226,94 @@ export async function counterfactualTransfer(
218
226
  const weave = await pre.weave();
219
227
  const points = weave.points;
220
228
  const depth = weave.depth;
221
- const aligned = points.length;
229
+ // CAST'S OWN SINGLE-VS-MULTI TEST, MEASURED FROM THE QUERY.
230
+ //
231
+ // `points.length >= 2` reads as "two structures to transfer between", but
232
+ // measured, it functions as "the query is about more than one thing" — and
233
+ // it only discriminates because the weave's exclusivity eliminates hard
234
+ // enough that a single-topic query cannot reach two points. The condition
235
+ // is carried by the elimination, not by anything CAST measures. Traced on
236
+ // test/24 3.1 ("the importance of gender equality in the workplace"): the
237
+ // climb is byte-identical either way (16 of 31 sub-regions, one context),
238
+ // and relaxing the weave alone makes CAST fire and answer about the 1992
239
+ // Dream Team.
240
+ //
241
+ // What actually separates 3.1 from a genuine comparison (test/29 C2, "How is
242
+ // Shakespeare like Leonardo da Vinci?") is CONTENT: C2's two points are
243
+ // evidenced by DIFFERENT query spans, while 3.1's extra points align to the
244
+ // same shared frame the first one already explains. So require two points
245
+ // that explain genuinely different parts of the query — a second point must
246
+ // contribute at least one perception quantum of query bytes the
247
+ // best-covered point does not. Derived from the runs themselves, order-free,
248
+ // and independent of how many points survived.
249
+ const coveredBy = (p: typeof points[0]): Set<number> => {
250
+ const set = new Set<number>();
251
+ for (const r of p.runs) for (let i = r.qs; i < r.qe; i++) set.add(i);
252
+ return set;
253
+ };
254
+ let widest = points[0];
255
+ let widestN = -1;
256
+ for (const p of points) {
257
+ const n = coveredBy(p).size;
258
+ if (n > widestN) {
259
+ widestN = n;
260
+ widest = p;
261
+ }
262
+ }
263
+ const widestSet = widest === undefined
264
+ ? new Set<number>()
265
+ : coveredBy(widest);
266
+ let distinct = points.length === 0 ? 0 : 1;
267
+ for (const p of points) {
268
+ if (p === widest) continue;
269
+ let own = 0;
270
+ for (const i of coveredBy(p)) if (!widestSet.has(i)) own++;
271
+ if (own >= quantum) {
272
+ distinct = 2;
273
+ break;
274
+ }
275
+ }
276
+ // THE CLIMB ANSWERS THE SAME QUESTION, AND IT ANSWERS IT ORDER-FREE. Runs
277
+ // are literal W-gram agreement, so two structures the query names in its own
278
+ // words can share no run at all: on `How is ice like steel?` the query's
279
+ // `ice` and the stored `Ice is cold` agree on nothing but the ` is `
280
+ // scaffolding `Steel is hard` also matches, and the run test above reads one
281
+ // topic. The climb had already read two — it elected `Ice is cold` from
282
+ // q4-9 and `Steel is hard` from q16-20, two disjoint places — and DISPERSION
283
+ // (Attention.clusters) is exactly that reading: not how much evidence, but
284
+ // how many separate places in the query corroborate it. Measured against
285
+ // the case this gate exists to refuse, test/24 3.1: a genuinely single-topic
286
+ // query reads clusters 1, while C1's single committed root reads 2.
287
+ //
288
+ // Either source is sufficient — bytes the other point does not explain, or
289
+ // places the climb found the query's evidence in — and neither is a count of
290
+ // weave survivors.
291
+ // Dispersion alone is a property of the QUERY, not of the pair being woven,
292
+ // so it is read together with the pair's own elected spans: two points count
293
+ // as two topics when the climb found the query dispersed AND it elected them
294
+ // from places at least a quantum apart. (Dispersion alone was measured and
295
+ // is too weak — it let CAST into test/33's near-tie and test/24's list
296
+ // skill, whose points the climb elects from the same place.)
297
+ const dispersed = roots.length >= MIN_WEAVE ||
298
+ roots.some((r) => r.clusters >= MIN_WEAVE);
299
+ const apart = points.some((a) =>
300
+ points.some((b) =>
301
+ a !== b &&
302
+ (b.start - a.end >= quantum || a.start - b.end >= quantum)
303
+ )
304
+ );
305
+ // …and only where there is something left to transfer. When ONE point
306
+ // already explains the query down to the last quantum there is no analogy to
307
+ // draw — the query is that structure, restated or truncated — and the
308
+ // dispersion the climb reports is the SAME topic corroborated twice, not two
309
+ // topics. Measured on test/33's `steel is hard so steel is`, a prefix of one
310
+ // stored fact: its root disperses into 2 clusters purely because the fact
311
+ // repeats `steel is`, while that one point's runs cover all 25 query bytes.
312
+ const unexplained = query.length - widestN;
313
+ const aligned =
314
+ distinct >= 2 || (dispersed && apart && unexplained >= quantum)
315
+ ? points.length
316
+ : 1;
222
317
  if (aligned < 2) {
223
318
  return fail(
224
319
  `only ${aligned} structure(s) aligned across the query — CAST needs ` +
@@ -290,11 +385,39 @@ export async function counterfactualTransfer(
290
385
  return [];
291
386
  }
292
387
 
293
- const woven = points.some((p) =>
294
- p.runs.some((r) =>
295
- !pre.rec.sites.some((s) => r.qs >= s.start && r.qe <= s.end)
296
- )
297
- );
388
+ // WOVEN is anything actually brought TOGETHER? A run restating a site
389
+ // the query already contains is not, by itself, evidence of that; but TWO
390
+ // points restating DIFFERENT sites is exactly a comparison ("How is
391
+ // Michelangelo like Homer?" names both entities, recognition finds both,
392
+ // and the weave aligns each to its own stored structure). The escape
393
+ // clause alone called that unwoven — a reading that held only while
394
+ // recognition UNDER-reported sites, and test/29 A2 started failing the
395
+ // moment recognition's interior chains stopped dying mid-form.
396
+ //
397
+ // Both points must be evidenced in what the asker JUST SAID. A multi-turn
398
+ // query is the whole transcript, so the earlier turns' own questions are
399
+ // aligned points too — traced on test/48, the weave for `And what is the
400
+ // capital of Spain?` holds `What is the capital of France?` (runs q0-61,
401
+ // entirely inside the previous turn and its answer) beside the new question
402
+ // (q65-94). Two points, two named sites, and nothing woven at all: one of
403
+ // them is conversation history. The current turn is the bytes past the last
404
+ // answered span — the same `askerBytes` notion computeWeave prices its read
405
+ // budget with — so requiring both points to have evidence THERE separates a
406
+ // genuine two-place weave from a follow-up. Single-turn queries have no
407
+ // answered spans, so the current turn is the whole query and nothing changes.
408
+ const turnStart = ctx.answeredSpans.reduce((n, [, e]) => Math.max(n, e), 0);
409
+ const inTurn = points.filter((p) => p.runs.some((r) => r.qe > turnStart));
410
+ const siteAt = (r: GradedRun): number =>
411
+ pre.rec.sites.findIndex((s) => r.qs >= s.start && r.qe <= s.end);
412
+ const namedSites = new Set<number>();
413
+ for (const p of inTurn) {
414
+ for (const r of p.runs) {
415
+ const i = siteAt(r);
416
+ if (i >= 0) namedSites.add(i);
417
+ }
418
+ }
419
+ const woven = points.some((p) => p.runs.some((r) => siteAt(r) < 0)) ||
420
+ (inTurn.length >= MIN_WEAVE && namedSites.size >= MIN_WEAVE);
298
421
  if (!woven) {
299
422
  return fail(
300
423
  `every aligned run restates a recognised query site — nothing was ` +
@@ -357,11 +480,30 @@ export async function counterfactualTransfer(
357
480
  const qv = pre.guide;
358
481
 
359
482
  // ── SUBSTITUTION ──────────────────────────────────────────────────
360
- const fillerOf = (s: Point): Uint8Array => {
361
- const r = s.runs[0];
362
- return r.cs < quantum
483
+ const fillerOf = (s: Point, r: GradedRun = s.runs[0]): Uint8Array =>
484
+ r.cs < quantum
363
485
  ? s.ctx.subarray(0, r.cs + (r.qe - r.qs))
364
486
  : query.subarray(r.qs, r.qe);
487
+ // THE FILLER IS WHAT THE SUBJECT CONTRIBUTES BEFORE THE SEAT — CLIPPED HERE,
488
+ // NOT ARBITRATED BY RANK. A subject whose alignment runs INTO the seat span
489
+ // agrees with the displaced structure there; those shared bytes are frame,
490
+ // and only the part before the seat is the subject's own contribution.
491
+ // Reading `runs[0]` whole made this schema depend on the weave having
492
+ // already cut that overlap away for it: on `steel is frigid` the weave's
493
+ // exclusivity handed `steel is hard so steel is strong` the run q0-5
494
+ // (`steel`) only because the seat's point ranked higher and took q5-15
495
+ // first. Read without that cut the same run is q0-9 (`steel is `), it ends
496
+ // PAST the seat at q5, and substitution found no subject at all — a schema
497
+ // silently reading a global elimination order as if it were local evidence.
498
+ // Clipping at the seat derives the same span from the two points actually
499
+ // involved, so the reading no longer moves when the weave's order does.
500
+ const fillerRun = (s: Point, at: number): GradedRun | null => {
501
+ const r0 = s.runs[0];
502
+ if (r0.qs >= at) return null;
503
+ const qe = Math.min(r0.qe, at);
504
+ return qe - r0.qs >= Math.min(quantum, s.ctx.length)
505
+ ? (qe === r0.qe ? r0 : { ...r0, qe })
506
+ : null;
365
507
  };
366
508
  // The subject is the closest structure whose FILLER RUN precedes the seat.
367
509
  // The gate is on `runs[0]` — the run `fillerOf` actually reads — not on the
@@ -376,14 +518,19 @@ export async function counterfactualTransfer(
376
518
  // analogy — pushed lastRun past the seat and no substitution fired at all.
377
519
  // The ordering key follows the gate to the same run, so "closest preceding"
378
520
  // still means closest by the evidence actually used.
379
- const beforeOf = (p: Point, r: GradedRun): Point | undefined =>
521
+ const beforeOf = (
522
+ p: Point,
523
+ r: GradedRun,
524
+ ): { point: Point; run: GradedRun } | undefined =>
380
525
  argmaxBy(
381
- points.filter((s) =>
382
- s !== p && s.runs[0].qe <= r.qs &&
383
- s.runs[0].cs < quantum &&
384
- usable(s.runs[0].qs, s.runs[0].qe)
385
- ),
386
- (s) => s.runs[0].qs,
526
+ points.flatMap((s) => {
527
+ if (s === p) return [];
528
+ const f = fillerRun(s, r.qs);
529
+ return f !== null && f.cs < quantum && usable(f.qs, f.qe)
530
+ ? [{ point: s, run: f }]
531
+ : [];
532
+ }),
533
+ (s) => s.run.qs,
387
534
  -Infinity,
388
535
  true,
389
536
  )?.item;
@@ -395,7 +542,9 @@ export async function counterfactualTransfer(
395
542
  }
396
543
  const before = beforeOf(p, r);
397
544
  if (before === undefined) return null;
398
- if (r.cs > fillerOf(before).length + quantum) return null;
545
+ if (r.cs > fillerOf(before.point, before.run).length + quantum) {
546
+ return null;
547
+ }
399
548
  // SUBSTITUTION MUST ACTUALLY DISPLACE. The schema's premise is that the
400
549
  // displaced structure's seat is held by something ELSE, which the
401
550
  // subject then replaces. When the subject's filler already occurs in
@@ -407,7 +556,9 @@ export async function counterfactualTransfer(
407
556
  // produced `Michelangelo sculpted by Michelangelo.` — then outbid every
408
557
  // honest candidate with it (test/29 A2). Byte containment, the same
409
558
  // primitive the self-evidence and contradiction guards use.
410
- if (indexOf(p.ctx, fillerOf(before), 0) >= 0) return null;
559
+ if (indexOf(p.ctx, fillerOf(before.point, before.run), 0) >= 0) {
560
+ return null;
561
+ }
411
562
  return { p, before, depth: p.ctx.length - r.cs };
412
563
  })
413
564
  .filter((c): c is NonNullable<typeof c> => c !== null);
@@ -416,7 +567,7 @@ export async function counterfactualTransfer(
416
567
  const subj = picked?.item.before ?? null;
417
568
  if (proj !== null && subj !== null) {
418
569
  const seat = proj.runs[0];
419
- const filler = fillerOf(subj);
570
+ const filler = fillerOf(subj.point, subj.run);
420
571
  const tail = proj.ctx.subarray(seat.cs);
421
572
  let answer = await joinWithBridge(ctx, filler, tail);
422
573
  const fwd = await follow(ctx, proj.anchor, qv);
@@ -429,7 +580,7 @@ export async function counterfactualTransfer(
429
580
  ctx.trace?.step(
430
581
  "projectCounterfactual",
431
582
  [
432
- rItem(filler, "filler", subj.anchor),
583
+ rItem(filler, "filler", subj.point.anchor),
433
584
  rNode(ctx, proj.anchor, "displaced-structure"),
434
585
  ],
435
586
  [rItem(answer, "projection")],
@@ -438,7 +589,7 @@ export async function counterfactualTransfer(
438
589
  record(
439
590
  answer,
440
591
  "counterfactual substitution — the subject fills the analog's seat",
441
- new Set([subj.anchor, proj.anchor]),
592
+ new Set([subj.point.anchor, proj.anchor]),
442
593
  // The acts performed: one seat INSERT projection + one edge FOLLOW.
443
594
  STEP + STEP,
444
595
  // What substitution actually READ: the two points it transfers
@@ -446,12 +597,41 @@ export async function counterfactualTransfer(
446
597
  // structure whose seat it fills — not every OTHER point the weave
447
598
  // happened to align (a third, unrelated point in the same weave
448
599
  // contributes nothing to what substitution itself explains).
449
- [...runSpans(subj), ...runSpans(proj)],
600
+ [...runSpans(subj.point), ...runSpans(proj)],
450
601
  );
451
602
  }
452
603
 
453
604
  // ── REDIRECTION ────────────────────────────────────────────────────
454
- const last = points.reduce((a, b) => lastRun(b).qs > lastRun(a).qs ? b : a);
605
+ // REDIRECTION IS ABOUT THE SUBSTITUTE THE QUERY NAMES, SO IT LOOKS FOR THE
606
+ // RUN THAT NAMES ONE. A structure is named when the query quotes it from
607
+ // its own opening bytes (`cs === 0`) — `…were Lyon?` against `Lyon is a city
608
+ // in France`. Reading that off `runs[0]` assumed the weave had already
609
+ // eliminated everything the point shares with the dominant, which is the
610
+ // elimination deciding the schema again: relaxed, the same point also aligns
611
+ // the query's trailing ` France` (cs 17, frame it shares with `what is the
612
+ // capital of France?`), that run sorts FIRST, and redirection stopped seeing
613
+ // a named substitute at all. Scanning the point's runs for the naming one
614
+ // is the same reading, taken from the runs rather than from their order, and
615
+ // "latest named" then means latest by the run actually relied on.
616
+ const named = points.flatMap((p) => {
617
+ const r = p.runs.find((r) => r.cs === 0 && usable(r.qs, r.qe));
618
+ return r !== undefined ? [{ point: p, run: r }] : [];
619
+ });
620
+ // …and it must be named AFTER what it displaces. Redirection replaces the
621
+ // ANSWER, so the substitute is the newest thing the query says — `…of France
622
+ // were Lyon?` names Lyon past everything the displaced structure aligned.
623
+ // The old `latest last run` reduce encoded this implicitly and only held
624
+ // while trimming kept the dominant's runs latest; stated on the naming run
625
+ // it is the same reading without that dependency. Measured on test/29 D1
626
+ // (`steel is frigid`), where the point with a naming run is the SUBJECT at
627
+ // q0-9, ahead of the dominant's q5-15: redirection must not fire, and
628
+ // substitution — which is what that shape is — keeps the case.
629
+ const last = argmaxBy(
630
+ named.filter((n) => n.run.qs > lastRun(dominant).qs),
631
+ (n) => n.run.qs,
632
+ -Infinity,
633
+ true,
634
+ )?.item;
455
635
  // Displacement test, capped at the hub bound: a hub anchor can carry a
456
636
  // corpus-sized fan-out, and each continuation costs a full byte
457
637
  // reconstruction plus an O(|query|·|bytes|) scan. The first √N edges (the
@@ -460,18 +640,14 @@ export async function counterfactualTransfer(
460
640
  const domNext = ctx.store.nextFirst(dominant.anchor, hubBound(ctx));
461
641
  const displaced = domNext
462
642
  .every((n) => indexOf(query, read(ctx, n), 0) < 0);
463
- if (
464
- last !== dominant &&
465
- last.runs[0].cs === 0 && displaced &&
466
- usable(last.runs[0].qs, last.runs[0].qe)
467
- ) {
468
- const g = await project(ctx, last.anchor, qv);
643
+ if (last !== undefined && last.point !== dominant && displaced) {
644
+ const g = await project(ctx, last.point.anchor, qv);
469
645
  if (g !== null) {
470
646
  ctx.trace?.step(
471
647
  "projectCounterfactual",
472
648
  [
473
649
  rNode(ctx, dominant.anchor, "displaced-structure"),
474
- rNode(ctx, last.anchor, "substitute"),
650
+ rNode(ctx, last.point.anchor, "substitute"),
475
651
  ],
476
652
  [rItem(g, "projection")],
477
653
  "the substitute's own fact replaces the displaced structure's answer",
@@ -479,7 +655,7 @@ export async function counterfactualTransfer(
479
655
  record(
480
656
  g,
481
657
  "counterfactual redirection — the named substitute's fact is followed",
482
- new Set([dominant.anchor, last.anchor]),
658
+ new Set([dominant.anchor, last.point.anchor]),
483
659
  // One forward projection across the substitute's own fact.
484
660
  STEP,
485
661
  // What redirection READ: the displaced structure's own recognized
@@ -487,7 +663,7 @@ export async function counterfactualTransfer(
487
663
  // being overridden, it just doesn't answer from it) plus the named
488
664
  // substitute's own aligned run — not every OTHER point the weave
489
665
  // happened to align.
490
- [...runSpans(dominant), ...runSpans(last)],
666
+ [...runSpans(dominant), ...runSpans(last.point)],
491
667
  );
492
668
  }
493
669
  }
@@ -529,7 +705,20 @@ export async function counterfactualTransfer(
529
705
  // distinction perception can make — the same quantum countClusters separates
530
706
  // neighbourhoods by — so a context within one quantum of the query's length
531
707
  // carries no independently perceivable unit beyond it and is the same scale.
532
- const queryScale = (n: number): boolean => n - query.length < quantum;
708
+ //
709
+ // ONE QUANTUM OF EXCESS IS AN ABSOLUTE UNIT, AND SCALE IS NOT ABSOLUTE.
710
+ // `n - query.length < quantum` calls a 504-byte context the same scale as a
711
+ // 500-byte query while refusing a 47-byte context on a 42-byte one — the
712
+ // same 5 bytes, opposite verdicts, because the bar never looks at what it is
713
+ // measuring against. Measured on test/29 C3, whose query is C2's verbatim:
714
+ // the climb elects the exemplar SENTENCE (47) rather than the entity, five
715
+ // bytes past a 42-byte query, and comparison refused a pair it accepts at
716
+ // C2's grain. Read the excess against the query with `dominates` — the same
717
+ // half-dominance predicate this file uses for frame, and the one scale-free
718
+ // reading of "the seat sentence must not dominate the comparison" available
719
+ // without inventing a ratio.
720
+ const queryScale = (n: number): boolean =>
721
+ !dominates(n - query.length, query.length);
533
722
  const analogs: AnalogCandidate[] = [];
534
723
  for (const p of points) {
535
724
  if (p === dominant) continue;
@@ -616,12 +805,84 @@ export async function counterfactualTransfer(
616
805
  // content the query never asked about. Computed once here; both the
617
806
  // hub fallback below and the comparison gate consume it.
618
807
  const rootTrusted = roots.some((r) => r.vote >= consensusFloor(corpusN(ctx)));
808
+ // The context that ESTABLISHES a filler — the same reverse context, under
809
+ // the same naming test, `seatOfNode` uses to VOICE an analog (a predecessor
810
+ // whose bytes CONTAIN the node's: it names or describes it, rather than
811
+ // merely having preceded it somewhere). Memoised: the analogy loop below
812
+ // asks about the same dominant every time, and only ever asks at all when
813
+ // the cheap tiers already read zero.
814
+ // A NODE NOTHING ESTABLISHES IS ITS OWN ESTABLISHING CONTEXT — the same
815
+ // reading `seatOfNode` takes one gate up: a bare filler was learnt as some
816
+ // context's answer and has a predecessor that NAMES it, so no establishing
817
+ // predecessor means the node already IS a learnt context. Returning null
818
+ // there made the tier depend on both sides being elected at the same GRAIN:
819
+ // test/29 C2's climb elects the entity `Leonardo da Vinci` (established by
820
+ // `The Mona Lisa was painted by…`) and reads 0.371, while C3's identical
821
+ // query elects that sentence ITSELF for the same side, whose own
822
+ // predecessor establishes nothing — the tier read 0.000 and comparison
823
+ // never fired, on a pair that is strictly MORE explicit about its frame.
824
+ const estMemo = new Map<number, Uint8Array>();
825
+ const establishing = (id: number): Uint8Array => {
826
+ const hit = estMemo.get(id);
827
+ if (hit !== undefined) return hit;
828
+ const own = read(ctx, id);
829
+ const rev = reverseContext(ctx, id, pre.guide);
830
+ const out = rev !== null && indexOf(rev, own, 0) >= 0 ? rev : own;
831
+ estMemo.set(id, out);
832
+ return out;
833
+ };
834
+ // COMPARISON VOICES WHAT IT COMPARED. When the frame tier decided the
835
+ // analogy, the two establishing contexts it read ARE the roles being
836
+ // compared, so the schema below voices those same bytes instead of
837
+ // re-deriving a seat that can land somewhere else entirely. Measured on
838
+ // test/29 C3: the dominant is the exemplar sentence `The Mona Lisa was
839
+ // painted by Leonardo da Vinci.`, nothing establishes it, so `seatOf`
840
+ // took its FORWARD continuation and voiced `Leonardo was a Renaissance
841
+ // polymath` — the analog's own biography, exactly what C2 pins comparison
842
+ // must never leak, from the branch whose own doc says forward completion
843
+ // is right for a DOMINANT (true when the dominant is a bare name whose
844
+ // continuation establishes it; false when it already IS the establishing
845
+ // context). Only frame-tier pairs are affected: a halo-tier analogy was
846
+ // never measured on these bytes and keeps the seat it always had.
847
+ const frameSeats = new Map<AnalogCandidate, [Uint8Array, Uint8Array]>();
619
848
  for (const c of analogs) {
620
- const { score: sim, halo } = await analogyStrength(
621
- ctx,
622
- dominant.anchor,
623
- c.anchor,
624
- );
849
+ const ev = await analogyStrength(ctx, dominant.anchor, c.anchor);
850
+ let sim = ev.score;
851
+ const halo = ev.halo;
852
+ // ROLE IS ESTABLISHED BY CONTEXT, NOT BY A NAME. When neither halo tier
853
+ // fired and the two anchors' own bytes share no learnt frame either, the
854
+ // anchors are FILLERS — bare entity names — not the frame-bearing
855
+ // structures the tier is about. Read the tier on what establishes each
856
+ // one instead: the aligned point's own context (or, for a hop-reached
857
+ // candidate, the point whose continuation edge reached it — the same
858
+ // context `cmpAccounted` already prices as that hop's query evidence).
859
+ // Both are ALREADY IN HAND, so this costs no extra read.
860
+ //
861
+ // Measured on test/29's corpus: "Michelangelo" vs "Homer" reads 0.000
862
+ // while "The David was sculpted by Michelangelo." vs "The Iliad was
863
+ // written by Homer." reads 0.452 — and a context in a different frame
864
+ // ("Water boils at one hundred degrees.") still reads 0.000. The tier
865
+ // was never failing to discriminate; it was reading the fillers.
866
+ //
867
+ // Still the FRAME tier (`halo` stays false), so this evidence remains
868
+ // subject to the naming / trusted-root bar the comparison gate holds all
869
+ // frame evidence to — a wider READING of the same tier, not a new licence.
870
+ // Containment is excluded for the same reason the generator excludes it:
871
+ // a context that contains the other establishes nothing independent.
872
+ if (!halo && sim === 0) {
873
+ // For a hop-reached candidate the thing whose ROLE is in question is
874
+ // the point the query named, not the fact one edge past it: "Homer"
875
+ // was named and "The Iliad was written by Homer." establishes it,
876
+ // while the hop's own destination ("Homer was an ancient Greek poet")
877
+ // has no establishing predecessor at all. The same reading
878
+ // `namedByQuery` and `cmpAccounted` already take of a hop.
879
+ const da = establishing(dominant.anchor);
880
+ const ca = establishing(c.point !== null ? c.anchor : c.src.anchor);
881
+ if (indexOf(da, ca, 0) < 0 && indexOf(ca, da, 0) < 0) {
882
+ sim = sharedFrameStrengthOf(ctx, da, ca);
883
+ frameSeats.set(c, [da, ca]);
884
+ }
885
+ }
625
886
  ctx.trace?.step(
626
887
  "tryAnalog",
627
888
  [
@@ -790,7 +1051,56 @@ export async function counterfactualTransfer(
790
1051
  // FRAME-tier or fallback analog — whose "similarity" is an unbarred
791
1052
  // coverage fraction or nothing — needs the query's naming or the climb's
792
1053
  // trust.
793
- const analogNamed = bestAnalog !== null && namedByQuery(bestAnalog);
1054
+ // A NAMING MUST NAME SOMETHING. `analogNamed` licences comparison on the
1055
+ // claim that the query's own bytes evidence the analog — but that claim is
1056
+ // only worth what those bytes discriminate. `edgeAncestors` already has the
1057
+ // system's verdict for content that discriminates nothing: SATURATION, the
1058
+ // √N parent-fan-out abstention `explainedSpan` (bridge.ts) and the climb
1059
+ // both respect. A window in too many places to discriminate cannot be
1060
+ // evidence that the query meant THIS analog rather than any other.
1061
+ //
1062
+ // So the naming must rest on at least ONE window that is not saturated —
1063
+ // not every window, which would be far too strong: test/29 C1's naming is
1064
+ // [" is "=SAT, "teel"=1, " is "=SAT], and the one discriminative run is
1065
+ // exactly what makes it a naming. Measured over the accounted runs of
1066
+ // every `analogNamed` comparison in the suite (contextsReached per window):
1067
+ //
1068
+ // C1 " cold"=1 "teel "=1 N=4 → names
1069
+ // C2 "Leonardo da Vinci"=4 " Shakespeare"=5 N=22 → names
1070
+ // C3 " Leonardo da Vinci"=3 " Shakespeare"=4 N=13 → names
1071
+ // "what i"=2 " the capital of France"=2 "Lyon"=1 → names
1072
+ // " is "=SAT "teel"=1 " is "=SAT → names
1073
+ // "The "=3 " painted by "=3 "Michelangelo"=2 → names
1074
+ // 50 " name"=SAT " the "=SAT "ing "=SAT
1075
+ // " the "=SAT "he b"=SAT "ing "=SAT N=205 → names NOTHING
1076
+ //
1077
+ // test/50's junk comparison is the only one in the suite whose naming is
1078
+ // saturated end to end: it "names" its analog with " the " and "ing ". The
1079
+ // ignored-known principle cannot reach that case — the planet probe's gaps
1080
+ // ("planet", "biggest", "sun") are genuinely untrained, so
1081
+ // `dismissedKnownContent` correctly returns false and there is no ignored
1082
+ // known content to find. This is a different question: not "did the
1083
+ // comparison ignore what the store knows" but "did the query name this
1084
+ // analog at all". Derived, never tuned — the saturation limit is
1085
+ // `edgeAncestors`' own √N, computed nowhere new.
1086
+ const namingDiscriminates = (): boolean => {
1087
+ const N = corpusN(ctx);
1088
+ const W = ctx.space.maxGroup;
1089
+ const memo = sharedReachMemo(ctx);
1090
+ for (const [from, to] of cmpAccounted) {
1091
+ for (let o = from; o + W <= to; o++) {
1092
+ const ids = leafIdRun(ctx, query, o, o + W);
1093
+ if (ids === null) continue;
1094
+ const wid = ctx.store.findBranch(ids);
1095
+ if (wid === null) continue;
1096
+ const r = edgeAncestors(ctx, wid, N, memo);
1097
+ if (!r.saturated && r.roots.length > 0) return true;
1098
+ }
1099
+ }
1100
+ return false;
1101
+ };
1102
+ const analogNamed = bestAnalog !== null && namedByQuery(bestAnalog) &&
1103
+ namingDiscriminates();
794
1104
  // NOTE — two further gates were tried here and empirically REFUTED,
795
1105
  // recorded so they are not re-tried:
796
1106
  // • dominant self-coverage (dominant's aligned runs must dominate its
@@ -818,7 +1128,27 @@ export async function counterfactualTransfer(
818
1128
  // while a scrap-matched junk pair leaves the query's own trained content
819
1129
  // ("…songs…times…", "…planet…sun.") dismissed as gaps. Halo-tier and
820
1130
  // trusted-root comparisons are exempt — their evidence already stands.
821
- const cmpDismisses = !(bestHalo || rootTrusted) &&
1131
+ // A TRUSTED ROOT IS NOT A LICENCE TO IGNORE WHAT THE STORE KNOWS. This
1132
+ // exemption used to read `!(bestHalo || rootTrusted)`, so a root clearing
1133
+ // consensusFloor discarded the ignored-known verdict entirely — and that
1134
+ // verdict is the one piece of evidence in this gate that actually sees the
1135
+ // failure: measured on test/50's probes, `dismissedKnownContent` returns
1136
+ // TRUE for both ("songs"/"times"/"planet"-class trained content left in the
1137
+ // comparison's gaps) while `rootTrusted` is also true, so the gate read
1138
+ // false and comparison fired on a junk analog.
1139
+ //
1140
+ // The root's trust says the CLIMB settled on something; it says nothing
1141
+ // about whether THIS comparison's own evidence covers the query's known
1142
+ // content, which is a different question about a different quantity. Halo
1143
+ // stays exempt — halo-tier company is independent evidence in its own right
1144
+ // (test/33 1b's nickname-corroborated analog), which is exactly what a
1145
+ // pooled consensus vote is not.
1146
+ //
1147
+ // Measured cost, and it is a candidate COUNT, not an answer: test/33 1b
1148
+ // ("expected at least two CAST candidates") loses one of its two, because
1149
+ // the comparison schema now honestly declines. The junk analogs it used to
1150
+ // supply were never the ones that test is about.
1151
+ const cmpDismisses = !bestHalo &&
822
1152
  dismissedKnownContent(ctx, query, cmpAccounted);
823
1153
  if (
824
1154
  bestAnalog !== null &&
@@ -838,7 +1168,8 @@ export async function counterfactualTransfer(
838
1168
  [],
839
1169
  "the two structures keep distributional company beyond chance — genuine analogs",
840
1170
  );
841
- const a = await seatOf(dominant);
1171
+ const seats = frameSeats.get(bestAnalog);
1172
+ const a = seats !== undefined ? seats[0] : await seatOf(dominant);
842
1173
  // The analog is only being CITED for comparison — the query never asked
843
1174
  // about it — so its seat never chases a FORWARD continuation (see
844
1175
  // seatOfNode's `allowForward`): only reverse (if a predecessor genuinely
@@ -851,7 +1182,9 @@ export async function counterfactualTransfer(
851
1182
  // [...] context will be the seat") — its own bytes ARE that seat
852
1183
  // directly, with no predecessor to even check (it was found by a
853
1184
  // forward edge, not matched in the query).
854
- const b = bestAnalog.point !== null
1185
+ const b = seats !== undefined
1186
+ ? seats[1]
1187
+ : bestAnalog.point !== null
855
1188
  ? await seatOf(bestAnalog.point, false)
856
1189
  : read(ctx, bestAnalog.anchor);
857
1190
  const answer = await joinWithBridge(ctx, a, b);
@@ -237,8 +237,23 @@ export async function confluenceJoin(
237
237
  reach = Math.min(reach, reachOf(ctx, wid, N, reachMemo));
238
238
  }
239
239
  }
240
- if (!isFinite(reach) || dominates(reach, N)) continue;
240
+ // ONE WINDOW IS NOT AN ENTITY. The reach above is read at the
241
+ // finest grain the fold can address, where content-defined window
242
+ // identity is at its most phase-sensitive: a scaffolding phrase
243
+ // whose cut happens to land in a rare phase reads as rare content.
244
+ // Measured on test/29 C3, a 13-context store: the meet of `The Mona
245
+ // Lisa was painted by Leonardo da Vinci.` and `Hamlet was written by
246
+ // William Shakespeare.` came out as ` by ` at reach 3 — pure frame,
247
+ // priced as the corpus's third-rarest content, and voiced as the
248
+ // entity where the two evidence streams meet. A single quantum
249
+ // agrees with half the corpus by accident (the same argument
250
+ // pipeline-mechanism.ts's proposed-run gate makes about a 4-byte
251
+ // span), so a meet must clear the two-quantum floor this file's own
252
+ // entry gate uses — the smallest span that carries a perceivable
253
+ // unit BEYOND the one being matched.
241
254
  const len = e - s;
255
+ if (len < 2 * W) continue;
256
+ if (!isFinite(reach) || dominates(reach, N)) continue;
242
257
  if (
243
258
  met === null || reach < met.reach ||
244
259
  (reach === met.reach && len > met.len)
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { cosine } from "../../vec.js";
8
8
  import {
9
+ conceptThreshold,
9
10
  consensusFloor,
10
11
  identityBar,
11
12
  reachThreshold,
@@ -328,8 +329,22 @@ export async function recallByResonance(
328
329
  // bridge's structural channels (junction walks, anchor climbs) are the
329
330
  // correct proposal source for a query whose gist has no clean match;
330
331
  // the ANN cannot propose what the gist cannot rank.
331
- const marketScale = k * ctx.space.maxGroup;
332
- if (corpusN(ctx) <= marketScale ** 3) {
332
+ // The condition above is the SCORE of the top hit, not the size of the
333
+ // corpus. It used to be spelled `corpusN(ctx) <= (k · W)³`, which asks
334
+ // a different question and answers it wrongly at exactly the scale the
335
+ // note was written from: on the trained store N = 325,608 with k = 24
336
+ // and W = 4 puts the cube at 884,736, so that store took the exhaustive
337
+ // branch — the very branch measured here as 38K–40K annVectorReads.
338
+ // Measured cost of the mismatch: substitutionBridge 8,544ms of a
339
+ // 19,548ms think (44%), against 1,248ms and 14,218ms without it, with
340
+ // every answer in the battery byte-identical and the suite unchanged
341
+ // at 445/445. Corpus size was never the discriminator; whether the
342
+ // gist ranks ANYTHING at concept level is.
343
+ //
344
+ // Reading it as the note states also removes a duplicated (k · W)³ —
345
+ // the same cube gates crossRegionVotes' walk budget, where it likewise
346
+ // never engages at real scale (see attention.ts).
347
+ if (whole.length > 0 && whole[0].score >= conceptThreshold(ctx.store.D)) {
333
348
  const exhaustive = await ctx.store.resonate(
334
349
  queryGist,
335
350
  hubBound(ctx),