@hviana/sema 0.4.4 → 0.4.7

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 (54) 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/rabitq-ivf/src/rabitq.js +31 -1
  24. package/dist/src/sema.d.ts +11 -1
  25. package/dist/src/sema.js +16 -2
  26. package/dist/src/store.d.ts +64 -1
  27. package/dist/src/store.js +107 -8
  28. package/index.html +2 -3
  29. package/package.json +1 -1
  30. package/src/geometry.ts +231 -43
  31. package/src/mind/attention.ts +366 -15
  32. package/src/mind/junction.ts +18 -2
  33. package/src/mind/match.ts +18 -2
  34. package/src/mind/mechanisms/cast.ts +376 -43
  35. package/src/mind/mechanisms/confluence.ts +16 -1
  36. package/src/mind/mechanisms/recall.ts +17 -2
  37. package/src/mind/pipeline-mechanism.ts +96 -36
  38. package/src/mind/pipeline.ts +33 -3
  39. package/src/mind/reasoning.ts +31 -4
  40. package/src/mind/recognition.ts +65 -2
  41. package/src/mind/resonance.ts +0 -0
  42. package/src/mind/types.ts +43 -1
  43. package/src/rabitq-ivf/src/rabitq.ts +31 -1
  44. package/src/sema.ts +21 -2
  45. package/src/store.ts +106 -5
  46. package/test/00-extract.test.mjs +28 -0
  47. package/test/15-decomposition-gap.test.mjs +0 -0
  48. package/test/24-generalization.test.mjs +67 -19
  49. package/test/29-counterfactual.test.mjs +106 -42
  50. package/test/33-multi-candidate.test.mjs +56 -12
  51. package/test/53-cross-region-probe-instrumentation.test.mjs +16 -1
  52. package/test/63-fold-invariants.test.mjs +489 -0
  53. package/test/64-two-ended-thresholds.test.mjs +76 -0
  54. package/test/65-ann-recall.test.mjs +331 -0
@@ -11,8 +11,8 @@
11
11
  // gate = the frame gate below + analogyStrength, projection = insert / project /
12
12
  // juxtapose.
13
13
  import { read } from "../primitives.js";
14
- import { argmaxBy, corpusN, hubBound } from "../traverse.js";
15
- import { analogyStrength, follow, project, reverseContext, } from "../match.js";
14
+ import { argmaxBy, corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "../traverse.js";
15
+ import { analogyStrength, follow, project, reverseContext, sharedFrameStrengthOf, } from "../match.js";
16
16
  import { joinWithBridge } from "../resonance.js";
17
17
  import { restatesQuery } from "../reasoning.js";
18
18
  import { CONCEPT, STEP } from "../graph-search.js";
@@ -21,6 +21,7 @@ import { consensusFloor, dominates } from "../../geometry.js";
21
21
  import { unexplainedLabel, unexplainedSpans, } from "../rationale.js";
22
22
  import { rItem, rNode } from "../trace.js";
23
23
  import { dismissedKnownContent } from "../bridge.js";
24
+ import { leafIdRun } from "../canonical.js";
24
25
  // ── CAST gates ────────────────────────────────────────────────────────────
25
26
  //
26
27
  // The frame gate has TWO components, both derived from the weave itself:
@@ -173,7 +174,94 @@ export async function counterfactualTransfer(ctx, query, pre) {
173
174
  const weave = await pre.weave();
174
175
  const points = weave.points;
175
176
  const depth = weave.depth;
176
- const aligned = points.length;
177
+ // CAST'S OWN SINGLE-VS-MULTI TEST, MEASURED FROM THE QUERY.
178
+ //
179
+ // `points.length >= 2` reads as "two structures to transfer between", but
180
+ // measured, it functions as "the query is about more than one thing" — and
181
+ // it only discriminates because the weave's exclusivity eliminates hard
182
+ // enough that a single-topic query cannot reach two points. The condition
183
+ // is carried by the elimination, not by anything CAST measures. Traced on
184
+ // test/24 3.1 ("the importance of gender equality in the workplace"): the
185
+ // climb is byte-identical either way (16 of 31 sub-regions, one context),
186
+ // and relaxing the weave alone makes CAST fire and answer about the 1992
187
+ // Dream Team.
188
+ //
189
+ // What actually separates 3.1 from a genuine comparison (test/29 C2, "How is
190
+ // Shakespeare like Leonardo da Vinci?") is CONTENT: C2's two points are
191
+ // evidenced by DIFFERENT query spans, while 3.1's extra points align to the
192
+ // same shared frame the first one already explains. So require two points
193
+ // that explain genuinely different parts of the query — a second point must
194
+ // contribute at least one perception quantum of query bytes the
195
+ // best-covered point does not. Derived from the runs themselves, order-free,
196
+ // and independent of how many points survived.
197
+ const coveredBy = (p) => {
198
+ const set = new Set();
199
+ for (const r of p.runs)
200
+ for (let i = r.qs; i < r.qe; i++)
201
+ set.add(i);
202
+ return set;
203
+ };
204
+ let widest = points[0];
205
+ let widestN = -1;
206
+ for (const p of points) {
207
+ const n = coveredBy(p).size;
208
+ if (n > widestN) {
209
+ widestN = n;
210
+ widest = p;
211
+ }
212
+ }
213
+ const widestSet = widest === undefined
214
+ ? new Set()
215
+ : coveredBy(widest);
216
+ let distinct = points.length === 0 ? 0 : 1;
217
+ for (const p of points) {
218
+ if (p === widest)
219
+ continue;
220
+ let own = 0;
221
+ for (const i of coveredBy(p))
222
+ if (!widestSet.has(i))
223
+ own++;
224
+ if (own >= quantum) {
225
+ distinct = 2;
226
+ break;
227
+ }
228
+ }
229
+ // THE CLIMB ANSWERS THE SAME QUESTION, AND IT ANSWERS IT ORDER-FREE. Runs
230
+ // are literal W-gram agreement, so two structures the query names in its own
231
+ // words can share no run at all: on `How is ice like steel?` the query's
232
+ // `ice` and the stored `Ice is cold` agree on nothing but the ` is `
233
+ // scaffolding `Steel is hard` also matches, and the run test above reads one
234
+ // topic. The climb had already read two — it elected `Ice is cold` from
235
+ // q4-9 and `Steel is hard` from q16-20, two disjoint places — and DISPERSION
236
+ // (Attention.clusters) is exactly that reading: not how much evidence, but
237
+ // how many separate places in the query corroborate it. Measured against
238
+ // the case this gate exists to refuse, test/24 3.1: a genuinely single-topic
239
+ // query reads clusters 1, while C1's single committed root reads 2.
240
+ //
241
+ // Either source is sufficient — bytes the other point does not explain, or
242
+ // places the climb found the query's evidence in — and neither is a count of
243
+ // weave survivors.
244
+ // Dispersion alone is a property of the QUERY, not of the pair being woven,
245
+ // so it is read together with the pair's own elected spans: two points count
246
+ // as two topics when the climb found the query dispersed AND it elected them
247
+ // from places at least a quantum apart. (Dispersion alone was measured and
248
+ // is too weak — it let CAST into test/33's near-tie and test/24's list
249
+ // skill, whose points the climb elects from the same place.)
250
+ const dispersed = roots.length >= MIN_WEAVE ||
251
+ roots.some((r) => r.clusters >= MIN_WEAVE);
252
+ const apart = points.some((a) => points.some((b) => a !== b &&
253
+ (b.start - a.end >= quantum || a.start - b.end >= quantum)));
254
+ // …and only where there is something left to transfer. When ONE point
255
+ // already explains the query down to the last quantum there is no analogy to
256
+ // draw — the query is that structure, restated or truncated — and the
257
+ // dispersion the climb reports is the SAME topic corroborated twice, not two
258
+ // topics. Measured on test/33's `steel is hard so steel is`, a prefix of one
259
+ // stored fact: its root disperses into 2 clusters purely because the fact
260
+ // repeats `steel is`, while that one point's runs cover all 25 query bytes.
261
+ const unexplained = query.length - widestN;
262
+ const aligned = distinct >= 2 || (dispersed && apart && unexplained >= quantum)
263
+ ? points.length
264
+ : 1;
177
265
  if (aligned < 2) {
178
266
  return fail(`only ${aligned} structure(s) aligned across the query — CAST needs ` +
179
267
  `at least two to transfer between`);
@@ -235,7 +323,39 @@ export async function counterfactualTransfer(ctx, query, pre) {
235
323
  });
236
324
  return [];
237
325
  }
238
- const woven = points.some((p) => p.runs.some((r) => !pre.rec.sites.some((s) => r.qs >= s.start && r.qe <= s.end)));
326
+ // WOVEN is anything actually brought TOGETHER? A run restating a site
327
+ // the query already contains is not, by itself, evidence of that; but TWO
328
+ // points restating DIFFERENT sites is exactly a comparison ("How is
329
+ // Michelangelo like Homer?" names both entities, recognition finds both,
330
+ // and the weave aligns each to its own stored structure). The escape
331
+ // clause alone called that unwoven — a reading that held only while
332
+ // recognition UNDER-reported sites, and test/29 A2 started failing the
333
+ // moment recognition's interior chains stopped dying mid-form.
334
+ //
335
+ // Both points must be evidenced in what the asker JUST SAID. A multi-turn
336
+ // query is the whole transcript, so the earlier turns' own questions are
337
+ // aligned points too — traced on test/48, the weave for `And what is the
338
+ // capital of Spain?` holds `What is the capital of France?` (runs q0-61,
339
+ // entirely inside the previous turn and its answer) beside the new question
340
+ // (q65-94). Two points, two named sites, and nothing woven at all: one of
341
+ // them is conversation history. The current turn is the bytes past the last
342
+ // answered span — the same `askerBytes` notion computeWeave prices its read
343
+ // budget with — so requiring both points to have evidence THERE separates a
344
+ // genuine two-place weave from a follow-up. Single-turn queries have no
345
+ // answered spans, so the current turn is the whole query and nothing changes.
346
+ const turnStart = ctx.answeredSpans.reduce((n, [, e]) => Math.max(n, e), 0);
347
+ const inTurn = points.filter((p) => p.runs.some((r) => r.qe > turnStart));
348
+ const siteAt = (r) => pre.rec.sites.findIndex((s) => r.qs >= s.start && r.qe <= s.end);
349
+ const namedSites = new Set();
350
+ for (const p of inTurn) {
351
+ for (const r of p.runs) {
352
+ const i = siteAt(r);
353
+ if (i >= 0)
354
+ namedSites.add(i);
355
+ }
356
+ }
357
+ const woven = points.some((p) => p.runs.some((r) => siteAt(r) < 0)) ||
358
+ (inTurn.length >= MIN_WEAVE && namedSites.size >= MIN_WEAVE);
239
359
  if (!woven) {
240
360
  return fail(`every aligned run restates a recognised query site — nothing was ` +
241
361
  `actually WOVEN across structures, so there is nothing to transfer`);
@@ -278,11 +398,30 @@ export async function counterfactualTransfer(ctx, query, pre) {
278
398
  const lastRun = (p) => p.runs[p.runs.length - 1];
279
399
  const qv = pre.guide;
280
400
  // ── SUBSTITUTION ──────────────────────────────────────────────────
281
- const fillerOf = (s) => {
282
- const r = s.runs[0];
283
- return r.cs < quantum
284
- ? s.ctx.subarray(0, r.cs + (r.qe - r.qs))
285
- : query.subarray(r.qs, r.qe);
401
+ const fillerOf = (s, r = s.runs[0]) => r.cs < quantum
402
+ ? s.ctx.subarray(0, r.cs + (r.qe - r.qs))
403
+ : query.subarray(r.qs, r.qe);
404
+ // THE FILLER IS WHAT THE SUBJECT CONTRIBUTES BEFORE THE SEAT — CLIPPED HERE,
405
+ // NOT ARBITRATED BY RANK. A subject whose alignment runs INTO the seat span
406
+ // agrees with the displaced structure there; those shared bytes are frame,
407
+ // and only the part before the seat is the subject's own contribution.
408
+ // Reading `runs[0]` whole made this schema depend on the weave having
409
+ // already cut that overlap away for it: on `steel is frigid` the weave's
410
+ // exclusivity handed `steel is hard so steel is strong` the run q0-5
411
+ // (`steel`) only because the seat's point ranked higher and took q5-15
412
+ // first. Read without that cut the same run is q0-9 (`steel is `), it ends
413
+ // PAST the seat at q5, and substitution found no subject at all — a schema
414
+ // silently reading a global elimination order as if it were local evidence.
415
+ // Clipping at the seat derives the same span from the two points actually
416
+ // involved, so the reading no longer moves when the weave's order does.
417
+ const fillerRun = (s, at) => {
418
+ const r0 = s.runs[0];
419
+ if (r0.qs >= at)
420
+ return null;
421
+ const qe = Math.min(r0.qe, at);
422
+ return qe - r0.qs >= Math.min(quantum, s.ctx.length)
423
+ ? (qe === r0.qe ? r0 : { ...r0, qe })
424
+ : null;
286
425
  };
287
426
  // The subject is the closest structure whose FILLER RUN precedes the seat.
288
427
  // The gate is on `runs[0]` — the run `fillerOf` actually reads — not on the
@@ -297,9 +436,14 @@ export async function counterfactualTransfer(ctx, query, pre) {
297
436
  // analogy — pushed lastRun past the seat and no substitution fired at all.
298
437
  // The ordering key follows the gate to the same run, so "closest preceding"
299
438
  // still means closest by the evidence actually used.
300
- const beforeOf = (p, r) => argmaxBy(points.filter((s) => s !== p && s.runs[0].qe <= r.qs &&
301
- s.runs[0].cs < quantum &&
302
- usable(s.runs[0].qs, s.runs[0].qe)), (s) => s.runs[0].qs, -Infinity, true)?.item;
439
+ const beforeOf = (p, r) => argmaxBy(points.flatMap((s) => {
440
+ if (s === p)
441
+ return [];
442
+ const f = fillerRun(s, r.qs);
443
+ return f !== null && f.cs < quantum && usable(f.qs, f.qe)
444
+ ? [{ point: s, run: f }]
445
+ : [];
446
+ }), (s) => s.run.qs, -Infinity, true)?.item;
303
447
  const displacement = points
304
448
  .map((p) => {
305
449
  const r = p.runs[0];
@@ -309,8 +453,9 @@ export async function counterfactualTransfer(ctx, query, pre) {
309
453
  const before = beforeOf(p, r);
310
454
  if (before === undefined)
311
455
  return null;
312
- if (r.cs > fillerOf(before).length + quantum)
456
+ if (r.cs > fillerOf(before.point, before.run).length + quantum) {
313
457
  return null;
458
+ }
314
459
  // SUBSTITUTION MUST ACTUALLY DISPLACE. The schema's premise is that the
315
460
  // displaced structure's seat is held by something ELSE, which the
316
461
  // subject then replaces. When the subject's filler already occurs in
@@ -322,8 +467,9 @@ export async function counterfactualTransfer(ctx, query, pre) {
322
467
  // produced `Michelangelo sculpted by Michelangelo.` — then outbid every
323
468
  // honest candidate with it (test/29 A2). Byte containment, the same
324
469
  // primitive the self-evidence and contradiction guards use.
325
- if (indexOf(p.ctx, fillerOf(before), 0) >= 0)
470
+ if (indexOf(p.ctx, fillerOf(before.point, before.run), 0) >= 0) {
326
471
  return null;
472
+ }
327
473
  return { p, before, depth: p.ctx.length - r.cs };
328
474
  })
329
475
  .filter((c) => c !== null);
@@ -332,7 +478,7 @@ export async function counterfactualTransfer(ctx, query, pre) {
332
478
  const subj = picked?.item.before ?? null;
333
479
  if (proj !== null && subj !== null) {
334
480
  const seat = proj.runs[0];
335
- const filler = fillerOf(subj);
481
+ const filler = fillerOf(subj.point, subj.run);
336
482
  const tail = proj.ctx.subarray(seat.cs);
337
483
  let answer = await joinWithBridge(ctx, filler, tail);
338
484
  const fwd = await follow(ctx, proj.anchor, qv);
@@ -341,10 +487,10 @@ export async function counterfactualTransfer(ctx, query, pre) {
341
487
  answer = concat2(answer, fwd);
342
488
  }
343
489
  ctx.trace?.step("projectCounterfactual", [
344
- rItem(filler, "filler", subj.anchor),
490
+ rItem(filler, "filler", subj.point.anchor),
345
491
  rNode(ctx, proj.anchor, "displaced-structure"),
346
492
  ], [rItem(answer, "projection")], "transfer the displaced structure onto the subject filler (seat substitution)");
347
- record(answer, "counterfactual substitution — the subject fills the analog's seat", new Set([subj.anchor, proj.anchor]),
493
+ record(answer, "counterfactual substitution — the subject fills the analog's seat", new Set([subj.point.anchor, proj.anchor]),
348
494
  // The acts performed: one seat INSERT projection + one edge FOLLOW.
349
495
  STEP + STEP,
350
496
  // What substitution actually READ: the two points it transfers
@@ -352,10 +498,34 @@ export async function counterfactualTransfer(ctx, query, pre) {
352
498
  // structure whose seat it fills — not every OTHER point the weave
353
499
  // happened to align (a third, unrelated point in the same weave
354
500
  // contributes nothing to what substitution itself explains).
355
- [...runSpans(subj), ...runSpans(proj)]);
501
+ [...runSpans(subj.point), ...runSpans(proj)]);
356
502
  }
357
503
  // ── REDIRECTION ────────────────────────────────────────────────────
358
- const last = points.reduce((a, b) => lastRun(b).qs > lastRun(a).qs ? b : a);
504
+ // REDIRECTION IS ABOUT THE SUBSTITUTE THE QUERY NAMES, SO IT LOOKS FOR THE
505
+ // RUN THAT NAMES ONE. A structure is named when the query quotes it from
506
+ // its own opening bytes (`cs === 0`) — `…were Lyon?` against `Lyon is a city
507
+ // in France`. Reading that off `runs[0]` assumed the weave had already
508
+ // eliminated everything the point shares with the dominant, which is the
509
+ // elimination deciding the schema again: relaxed, the same point also aligns
510
+ // the query's trailing ` France` (cs 17, frame it shares with `what is the
511
+ // capital of France?`), that run sorts FIRST, and redirection stopped seeing
512
+ // a named substitute at all. Scanning the point's runs for the naming one
513
+ // is the same reading, taken from the runs rather than from their order, and
514
+ // "latest named" then means latest by the run actually relied on.
515
+ const named = points.flatMap((p) => {
516
+ const r = p.runs.find((r) => r.cs === 0 && usable(r.qs, r.qe));
517
+ return r !== undefined ? [{ point: p, run: r }] : [];
518
+ });
519
+ // …and it must be named AFTER what it displaces. Redirection replaces the
520
+ // ANSWER, so the substitute is the newest thing the query says — `…of France
521
+ // were Lyon?` names Lyon past everything the displaced structure aligned.
522
+ // The old `latest last run` reduce encoded this implicitly and only held
523
+ // while trimming kept the dominant's runs latest; stated on the naming run
524
+ // it is the same reading without that dependency. Measured on test/29 D1
525
+ // (`steel is frigid`), where the point with a naming run is the SUBJECT at
526
+ // q0-9, ahead of the dominant's q5-15: redirection must not fire, and
527
+ // substitution — which is what that shape is — keeps the case.
528
+ const last = argmaxBy(named.filter((n) => n.run.qs > lastRun(dominant).qs), (n) => n.run.qs, -Infinity, true)?.item;
359
529
  // Displacement test, capped at the hub bound: a hub anchor can carry a
360
530
  // corpus-sized fan-out, and each continuation costs a full byte
361
531
  // reconstruction plus an O(|query|·|bytes|) scan. The first √N edges (the
@@ -364,16 +534,14 @@ export async function counterfactualTransfer(ctx, query, pre) {
364
534
  const domNext = ctx.store.nextFirst(dominant.anchor, hubBound(ctx));
365
535
  const displaced = domNext
366
536
  .every((n) => indexOf(query, read(ctx, n), 0) < 0);
367
- if (last !== dominant &&
368
- last.runs[0].cs === 0 && displaced &&
369
- usable(last.runs[0].qs, last.runs[0].qe)) {
370
- const g = await project(ctx, last.anchor, qv);
537
+ if (last !== undefined && last.point !== dominant && displaced) {
538
+ const g = await project(ctx, last.point.anchor, qv);
371
539
  if (g !== null) {
372
540
  ctx.trace?.step("projectCounterfactual", [
373
541
  rNode(ctx, dominant.anchor, "displaced-structure"),
374
- rNode(ctx, last.anchor, "substitute"),
542
+ rNode(ctx, last.point.anchor, "substitute"),
375
543
  ], [rItem(g, "projection")], "the substitute's own fact replaces the displaced structure's answer");
376
- record(g, "counterfactual redirection — the named substitute's fact is followed", new Set([dominant.anchor, last.anchor]),
544
+ record(g, "counterfactual redirection — the named substitute's fact is followed", new Set([dominant.anchor, last.point.anchor]),
377
545
  // One forward projection across the substitute's own fact.
378
546
  STEP,
379
547
  // What redirection READ: the displaced structure's own recognized
@@ -381,7 +549,7 @@ export async function counterfactualTransfer(ctx, query, pre) {
381
549
  // being overridden, it just doesn't answer from it) plus the named
382
550
  // substitute's own aligned run — not every OTHER point the weave
383
551
  // happened to align.
384
- [...runSpans(dominant), ...runSpans(last)]);
552
+ [...runSpans(dominant), ...runSpans(last.point)]);
385
553
  }
386
554
  }
387
555
  // ── COMPARISON ─────────────────────────────────────────────────────
@@ -408,7 +576,19 @@ export async function counterfactualTransfer(ctx, query, pre) {
408
576
  // distinction perception can make — the same quantum countClusters separates
409
577
  // neighbourhoods by — so a context within one quantum of the query's length
410
578
  // carries no independently perceivable unit beyond it and is the same scale.
411
- const queryScale = (n) => n - query.length < quantum;
579
+ //
580
+ // ONE QUANTUM OF EXCESS IS AN ABSOLUTE UNIT, AND SCALE IS NOT ABSOLUTE.
581
+ // `n - query.length < quantum` calls a 504-byte context the same scale as a
582
+ // 500-byte query while refusing a 47-byte context on a 42-byte one — the
583
+ // same 5 bytes, opposite verdicts, because the bar never looks at what it is
584
+ // measuring against. Measured on test/29 C3, whose query is C2's verbatim:
585
+ // the climb elects the exemplar SENTENCE (47) rather than the entity, five
586
+ // bytes past a 42-byte query, and comparison refused a pair it accepts at
587
+ // C2's grain. Read the excess against the query with `dominates` — the same
588
+ // half-dominance predicate this file uses for frame, and the one scale-free
589
+ // reading of "the seat sentence must not dominate the comparison" available
590
+ // without inventing a ratio.
591
+ const queryScale = (n) => !dominates(n - query.length, query.length);
412
592
  const analogs = [];
413
593
  for (const p of points) {
414
594
  if (p === dominant)
@@ -492,8 +672,85 @@ export async function counterfactualTransfer(ctx, query, pre) {
492
672
  // content the query never asked about. Computed once here; both the
493
673
  // hub fallback below and the comparison gate consume it.
494
674
  const rootTrusted = roots.some((r) => r.vote >= consensusFloor(corpusN(ctx)));
675
+ // The context that ESTABLISHES a filler — the same reverse context, under
676
+ // the same naming test, `seatOfNode` uses to VOICE an analog (a predecessor
677
+ // whose bytes CONTAIN the node's: it names or describes it, rather than
678
+ // merely having preceded it somewhere). Memoised: the analogy loop below
679
+ // asks about the same dominant every time, and only ever asks at all when
680
+ // the cheap tiers already read zero.
681
+ // A NODE NOTHING ESTABLISHES IS ITS OWN ESTABLISHING CONTEXT — the same
682
+ // reading `seatOfNode` takes one gate up: a bare filler was learnt as some
683
+ // context's answer and has a predecessor that NAMES it, so no establishing
684
+ // predecessor means the node already IS a learnt context. Returning null
685
+ // there made the tier depend on both sides being elected at the same GRAIN:
686
+ // test/29 C2's climb elects the entity `Leonardo da Vinci` (established by
687
+ // `The Mona Lisa was painted by…`) and reads 0.371, while C3's identical
688
+ // query elects that sentence ITSELF for the same side, whose own
689
+ // predecessor establishes nothing — the tier read 0.000 and comparison
690
+ // never fired, on a pair that is strictly MORE explicit about its frame.
691
+ const estMemo = new Map();
692
+ const establishing = (id) => {
693
+ const hit = estMemo.get(id);
694
+ if (hit !== undefined)
695
+ return hit;
696
+ const own = read(ctx, id);
697
+ const rev = reverseContext(ctx, id, pre.guide);
698
+ const out = rev !== null && indexOf(rev, own, 0) >= 0 ? rev : own;
699
+ estMemo.set(id, out);
700
+ return out;
701
+ };
702
+ // COMPARISON VOICES WHAT IT COMPARED. When the frame tier decided the
703
+ // analogy, the two establishing contexts it read ARE the roles being
704
+ // compared, so the schema below voices those same bytes instead of
705
+ // re-deriving a seat that can land somewhere else entirely. Measured on
706
+ // test/29 C3: the dominant is the exemplar sentence `The Mona Lisa was
707
+ // painted by Leonardo da Vinci.`, nothing establishes it, so `seatOf`
708
+ // took its FORWARD continuation and voiced `Leonardo was a Renaissance
709
+ // polymath` — the analog's own biography, exactly what C2 pins comparison
710
+ // must never leak, from the branch whose own doc says forward completion
711
+ // is right for a DOMINANT (true when the dominant is a bare name whose
712
+ // continuation establishes it; false when it already IS the establishing
713
+ // context). Only frame-tier pairs are affected: a halo-tier analogy was
714
+ // never measured on these bytes and keeps the seat it always had.
715
+ const frameSeats = new Map();
495
716
  for (const c of analogs) {
496
- const { score: sim, halo } = await analogyStrength(ctx, dominant.anchor, c.anchor);
717
+ const ev = await analogyStrength(ctx, dominant.anchor, c.anchor);
718
+ let sim = ev.score;
719
+ const halo = ev.halo;
720
+ // ROLE IS ESTABLISHED BY CONTEXT, NOT BY A NAME. When neither halo tier
721
+ // fired and the two anchors' own bytes share no learnt frame either, the
722
+ // anchors are FILLERS — bare entity names — not the frame-bearing
723
+ // structures the tier is about. Read the tier on what establishes each
724
+ // one instead: the aligned point's own context (or, for a hop-reached
725
+ // candidate, the point whose continuation edge reached it — the same
726
+ // context `cmpAccounted` already prices as that hop's query evidence).
727
+ // Both are ALREADY IN HAND, so this costs no extra read.
728
+ //
729
+ // Measured on test/29's corpus: "Michelangelo" vs "Homer" reads 0.000
730
+ // while "The David was sculpted by Michelangelo." vs "The Iliad was
731
+ // written by Homer." reads 0.452 — and a context in a different frame
732
+ // ("Water boils at one hundred degrees.") still reads 0.000. The tier
733
+ // was never failing to discriminate; it was reading the fillers.
734
+ //
735
+ // Still the FRAME tier (`halo` stays false), so this evidence remains
736
+ // subject to the naming / trusted-root bar the comparison gate holds all
737
+ // frame evidence to — a wider READING of the same tier, not a new licence.
738
+ // Containment is excluded for the same reason the generator excludes it:
739
+ // a context that contains the other establishes nothing independent.
740
+ if (!halo && sim === 0) {
741
+ // For a hop-reached candidate the thing whose ROLE is in question is
742
+ // the point the query named, not the fact one edge past it: "Homer"
743
+ // was named and "The Iliad was written by Homer." establishes it,
744
+ // while the hop's own destination ("Homer was an ancient Greek poet")
745
+ // has no establishing predecessor at all. The same reading
746
+ // `namedByQuery` and `cmpAccounted` already take of a hop.
747
+ const da = establishing(dominant.anchor);
748
+ const ca = establishing(c.point !== null ? c.anchor : c.src.anchor);
749
+ if (indexOf(da, ca, 0) < 0 && indexOf(ca, da, 0) < 0) {
750
+ sim = sharedFrameStrengthOf(ctx, da, ca);
751
+ frameSeats.set(c, [da, ca]);
752
+ }
753
+ }
497
754
  ctx.trace?.step("tryAnalog", [
498
755
  rNode(ctx, dominant.anchor, "dominant"),
499
756
  rNode(ctx, c.anchor, "candidate", sim),
@@ -649,7 +906,59 @@ export async function counterfactualTransfer(ctx, query, pre) {
649
906
  // FRAME-tier or fallback analog — whose "similarity" is an unbarred
650
907
  // coverage fraction or nothing — needs the query's naming or the climb's
651
908
  // trust.
652
- const analogNamed = bestAnalog !== null && namedByQuery(bestAnalog);
909
+ // A NAMING MUST NAME SOMETHING. `analogNamed` licences comparison on the
910
+ // claim that the query's own bytes evidence the analog — but that claim is
911
+ // only worth what those bytes discriminate. `edgeAncestors` already has the
912
+ // system's verdict for content that discriminates nothing: SATURATION, the
913
+ // √N parent-fan-out abstention `explainedSpan` (bridge.ts) and the climb
914
+ // both respect. A window in too many places to discriminate cannot be
915
+ // evidence that the query meant THIS analog rather than any other.
916
+ //
917
+ // So the naming must rest on at least ONE window that is not saturated —
918
+ // not every window, which would be far too strong: test/29 C1's naming is
919
+ // [" is "=SAT, "teel"=1, " is "=SAT], and the one discriminative run is
920
+ // exactly what makes it a naming. Measured over the accounted runs of
921
+ // every `analogNamed` comparison in the suite (contextsReached per window):
922
+ //
923
+ // C1 " cold"=1 "teel "=1 N=4 → names
924
+ // C2 "Leonardo da Vinci"=4 " Shakespeare"=5 N=22 → names
925
+ // C3 " Leonardo da Vinci"=3 " Shakespeare"=4 N=13 → names
926
+ // "what i"=2 " the capital of France"=2 "Lyon"=1 → names
927
+ // " is "=SAT "teel"=1 " is "=SAT → names
928
+ // "The "=3 " painted by "=3 "Michelangelo"=2 → names
929
+ // 50 " name"=SAT " the "=SAT "ing "=SAT
930
+ // " the "=SAT "he b"=SAT "ing "=SAT N=205 → names NOTHING
931
+ //
932
+ // test/50's junk comparison is the only one in the suite whose naming is
933
+ // saturated end to end: it "names" its analog with " the " and "ing ". The
934
+ // ignored-known principle cannot reach that case — the planet probe's gaps
935
+ // ("planet", "biggest", "sun") are genuinely untrained, so
936
+ // `dismissedKnownContent` correctly returns false and there is no ignored
937
+ // known content to find. This is a different question: not "did the
938
+ // comparison ignore what the store knows" but "did the query name this
939
+ // analog at all". Derived, never tuned — the saturation limit is
940
+ // `edgeAncestors`' own √N, computed nowhere new.
941
+ const namingDiscriminates = () => {
942
+ const N = corpusN(ctx);
943
+ const W = ctx.space.maxGroup;
944
+ const memo = sharedReachMemo(ctx);
945
+ for (const [from, to] of cmpAccounted) {
946
+ for (let o = from; o + W <= to; o++) {
947
+ const ids = leafIdRun(ctx, query, o, o + W);
948
+ if (ids === null)
949
+ continue;
950
+ const wid = ctx.store.findBranch(ids);
951
+ if (wid === null)
952
+ continue;
953
+ const r = edgeAncestors(ctx, wid, N, memo);
954
+ if (!r.saturated && r.roots.length > 0)
955
+ return true;
956
+ }
957
+ }
958
+ return false;
959
+ };
960
+ const analogNamed = bestAnalog !== null && namedByQuery(bestAnalog) &&
961
+ namingDiscriminates();
653
962
  // NOTE — two further gates were tried here and empirically REFUTED,
654
963
  // recorded so they are not re-tried:
655
964
  // • dominant self-coverage (dominant's aligned runs must dominate its
@@ -677,7 +986,27 @@ export async function counterfactualTransfer(ctx, query, pre) {
677
986
  // while a scrap-matched junk pair leaves the query's own trained content
678
987
  // ("…songs…times…", "…planet…sun.") dismissed as gaps. Halo-tier and
679
988
  // trusted-root comparisons are exempt — their evidence already stands.
680
- const cmpDismisses = !(bestHalo || rootTrusted) &&
989
+ // A TRUSTED ROOT IS NOT A LICENCE TO IGNORE WHAT THE STORE KNOWS. This
990
+ // exemption used to read `!(bestHalo || rootTrusted)`, so a root clearing
991
+ // consensusFloor discarded the ignored-known verdict entirely — and that
992
+ // verdict is the one piece of evidence in this gate that actually sees the
993
+ // failure: measured on test/50's probes, `dismissedKnownContent` returns
994
+ // TRUE for both ("songs"/"times"/"planet"-class trained content left in the
995
+ // comparison's gaps) while `rootTrusted` is also true, so the gate read
996
+ // false and comparison fired on a junk analog.
997
+ //
998
+ // The root's trust says the CLIMB settled on something; it says nothing
999
+ // about whether THIS comparison's own evidence covers the query's known
1000
+ // content, which is a different question about a different quantity. Halo
1001
+ // stays exempt — halo-tier company is independent evidence in its own right
1002
+ // (test/33 1b's nickname-corroborated analog), which is exactly what a
1003
+ // pooled consensus vote is not.
1004
+ //
1005
+ // Measured cost, and it is a candidate COUNT, not an answer: test/33 1b
1006
+ // ("expected at least two CAST candidates") loses one of its two, because
1007
+ // the comparison schema now honestly declines. The junk analogs it used to
1008
+ // supply were never the ones that test is about.
1009
+ const cmpDismisses = !bestHalo &&
681
1010
  dismissedKnownContent(ctx, query, cmpAccounted);
682
1011
  if (bestAnalog !== null &&
683
1012
  (bestHalo || analogNamed || rootTrusted) &&
@@ -690,7 +1019,8 @@ export async function counterfactualTransfer(ctx, query, pre) {
690
1019
  rNode(ctx, dominant.anchor, "analog", bestSim),
691
1020
  rNode(ctx, bestAnalog.anchor, "analog", bestSim),
692
1021
  ], [], "the two structures keep distributional company beyond chance — genuine analogs");
693
- const a = await seatOf(dominant);
1022
+ const seats = frameSeats.get(bestAnalog);
1023
+ const a = seats !== undefined ? seats[0] : await seatOf(dominant);
694
1024
  // The analog is only being CITED for comparison — the query never asked
695
1025
  // about it — so its seat never chases a FORWARD continuation (see
696
1026
  // seatOfNode's `allowForward`): only reverse (if a predecessor genuinely
@@ -703,9 +1033,11 @@ export async function counterfactualTransfer(ctx, query, pre) {
703
1033
  // [...] context will be the seat") — its own bytes ARE that seat
704
1034
  // directly, with no predecessor to even check (it was found by a
705
1035
  // forward edge, not matched in the query).
706
- const b = bestAnalog.point !== null
707
- ? await seatOf(bestAnalog.point, false)
708
- : read(ctx, bestAnalog.anchor);
1036
+ const b = seats !== undefined
1037
+ ? seats[1]
1038
+ : bestAnalog.point !== null
1039
+ ? await seatOf(bestAnalog.point, false)
1040
+ : read(ctx, bestAnalog.anchor);
709
1041
  const answer = await joinWithBridge(ctx, a, b);
710
1042
  record(answer, "analogical comparison — each analog voiced by the context that establishes its role", new Set([dominant.anchor, bestAnalog.anchor]),
711
1043
  // A halo-mediated act (the analogy gate) plus two seat projections.
@@ -188,9 +188,25 @@ export async function confluenceJoin(ctx, query, pre) {
188
188
  reach = Math.min(reach, reachOf(ctx, wid, N, reachMemo));
189
189
  }
190
190
  }
191
+ // ONE WINDOW IS NOT AN ENTITY. The reach above is read at the
192
+ // finest grain the fold can address, where content-defined window
193
+ // identity is at its most phase-sensitive: a scaffolding phrase
194
+ // whose cut happens to land in a rare phase reads as rare content.
195
+ // Measured on test/29 C3, a 13-context store: the meet of `The Mona
196
+ // Lisa was painted by Leonardo da Vinci.` and `Hamlet was written by
197
+ // William Shakespeare.` came out as ` by ` at reach 3 — pure frame,
198
+ // priced as the corpus's third-rarest content, and voiced as the
199
+ // entity where the two evidence streams meet. A single quantum
200
+ // agrees with half the corpus by accident (the same argument
201
+ // pipeline-mechanism.ts's proposed-run gate makes about a 4-byte
202
+ // span), so a meet must clear the two-quantum floor this file's own
203
+ // entry gate uses — the smallest span that carries a perceivable
204
+ // unit BEYOND the one being matched.
205
+ const len = e - s;
206
+ if (len < 2 * W)
207
+ continue;
191
208
  if (!isFinite(reach) || dominates(reach, N))
192
209
  continue;
193
- const len = e - s;
194
210
  if (met === null || reach < met.reach ||
195
211
  (reach === met.reach && len > met.len)) {
196
212
  met = { bytes: aBytes.subarray(s, e), reach, len, a, b };
@@ -4,7 +4,7 @@
4
4
  // index and grounds the nearest learned form. Four tiers, orderly degrading
5
5
  // from exact self-match to honest echo.
6
6
  import { cosine } from "../../vec.js";
7
- import { consensusFloor, identityBar, reachThreshold, significanceBar, } from "../../geometry.js";
7
+ import { conceptThreshold, consensusFloor, identityBar, reachThreshold, significanceBar, } from "../../geometry.js";
8
8
  import { gistOf, read, resolve } from "../primitives.js";
9
9
  import { bytesEqual, indexOf } from "../../bytes.js";
10
10
  import { corpusN, hubBound } from "../traverse.js";
@@ -240,8 +240,22 @@ export async function recallByResonance(ctx, query, pre) {
240
240
  // bridge's structural channels (junction walks, anchor climbs) are the
241
241
  // correct proposal source for a query whose gist has no clean match;
242
242
  // the ANN cannot propose what the gist cannot rank.
243
- const marketScale = k * ctx.space.maxGroup;
244
- if (corpusN(ctx) <= marketScale ** 3) {
243
+ // The condition above is the SCORE of the top hit, not the size of the
244
+ // corpus. It used to be spelled `corpusN(ctx) <= (k · W)³`, which asks
245
+ // a different question and answers it wrongly at exactly the scale the
246
+ // note was written from: on the trained store N = 325,608 with k = 24
247
+ // and W = 4 puts the cube at 884,736, so that store took the exhaustive
248
+ // branch — the very branch measured here as 38K–40K annVectorReads.
249
+ // Measured cost of the mismatch: substitutionBridge 8,544ms of a
250
+ // 19,548ms think (44%), against 1,248ms and 14,218ms without it, with
251
+ // every answer in the battery byte-identical and the suite unchanged
252
+ // at 445/445. Corpus size was never the discriminator; whether the
253
+ // gist ranks ANYTHING at concept level is.
254
+ //
255
+ // Reading it as the note states also removes a duplicated (k · W)³ —
256
+ // the same cube gates crossRegionVotes' walk budget, where it likewise
257
+ // never engages at real scale (see attention.ts).
258
+ if (whole.length > 0 && whole[0].score >= conceptThreshold(ctx.store.D)) {
245
259
  const exhaustive = await ctx.store.resonate(queryGist, hubBound(ctx), true);
246
260
  return exhaustive.map((h) => h.id);
247
261
  }
@@ -90,6 +90,10 @@ export interface WeaveInfo {
90
90
  vote: number;
91
91
  ctx: Uint8Array;
92
92
  runs: GradedRun[];
93
+ /** The query span the CLIMB elected this anchor from — its evidence,
94
+ * independent of any literal run alignment (see Attention.start/end). */
95
+ start: number;
96
+ end: number;
93
97
  }>;
94
98
  /** Weighted depth at each query byte — sum of alignment weights.
95
99
  * `depth[i]` is the total evidence that byte i is shared among the