@hviana/sema 0.2.2 → 0.2.4

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 (60) hide show
  1. package/dist/src/mind/articulation.js +1 -1
  2. package/dist/src/mind/attention.d.ts +18 -2
  3. package/dist/src/mind/attention.js +88 -18
  4. package/dist/src/mind/bridge.d.ts +30 -0
  5. package/dist/src/mind/bridge.js +569 -0
  6. package/dist/src/mind/graph-search.d.ts +16 -1
  7. package/dist/src/mind/graph-search.js +34 -5
  8. package/dist/src/mind/match.d.ts +15 -2
  9. package/dist/src/mind/match.js +3 -8
  10. package/dist/src/mind/mechanisms/alu.js +8 -1
  11. package/dist/src/mind/mechanisms/cast.d.ts +54 -0
  12. package/dist/src/mind/mechanisms/cast.js +303 -48
  13. package/dist/src/mind/mechanisms/cover.js +24 -32
  14. package/dist/src/mind/mechanisms/extraction.js +75 -30
  15. package/dist/src/mind/mechanisms/recall.js +66 -0
  16. package/dist/src/mind/mind.d.ts +1 -0
  17. package/dist/src/mind/mind.js +6 -1
  18. package/dist/src/mind/pipeline.js +34 -2
  19. package/dist/src/mind/reasoning.d.ts +20 -1
  20. package/dist/src/mind/reasoning.js +84 -6
  21. package/dist/src/mind/recognition.js +157 -13
  22. package/dist/src/mind/traverse.js +16 -0
  23. package/dist/src/mind/types.d.ts +65 -2
  24. package/dist/src/mind/types.js +53 -7
  25. package/dist/src/store.d.ts +12 -3
  26. package/dist/src/store.js +9 -3
  27. package/package.json +1 -1
  28. package/src/mind/articulation.ts +1 -0
  29. package/src/mind/attention.ts +105 -17
  30. package/src/mind/bridge.ts +596 -0
  31. package/src/mind/graph-search.ts +59 -2
  32. package/src/mind/match.ts +19 -5
  33. package/src/mind/mechanisms/alu.ts +8 -1
  34. package/src/mind/mechanisms/cast.ts +336 -46
  35. package/src/mind/mechanisms/cover.ts +31 -36
  36. package/src/mind/mechanisms/extraction.ts +101 -40
  37. package/src/mind/mechanisms/recall.ts +79 -0
  38. package/src/mind/mind.ts +7 -1
  39. package/src/mind/pipeline.ts +37 -2
  40. package/src/mind/reasoning.ts +97 -5
  41. package/src/mind/recognition.ts +160 -12
  42. package/src/mind/traverse.ts +17 -0
  43. package/src/mind/types.ts +110 -6
  44. package/src/store.ts +19 -5
  45. package/test/35-attention-confidence.test.mjs +139 -0
  46. package/test/36-already-answered-fusion.test.mjs +128 -0
  47. package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
  48. package/test/38-reason-restate-guard.test.mjs +94 -0
  49. package/test/39-cast-restate-guard.test.mjs +102 -0
  50. package/test/40-choosenext-scale-guard.test.mjs +75 -0
  51. package/test/41-seatofnode-direction.test.mjs +85 -0
  52. package/test/42-recognise-trace-idempotence.test.mjs +106 -0
  53. package/test/43-cast-analog-seat.test.mjs +244 -0
  54. package/test/44-recognise-edge-whitespace.test.mjs +63 -0
  55. package/test/45-liftanswer-restated-trim.test.mjs +60 -0
  56. package/test/46-recognise-multibyte-edge.test.mjs +85 -0
  57. package/test/47-cast-comparison-coverage.test.mjs +134 -0
  58. package/test/48-recognise-turn-connective.test.mjs +125 -0
  59. package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
  60. package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
@@ -12,8 +12,9 @@
12
12
  // juxtapose.
13
13
 
14
14
  import type { MindContext } from "../types.js";
15
+ import type { Vec } from "../../vec.js";
15
16
  import { read } from "../primitives.js";
16
- import { argmaxBy, hubBound } from "../traverse.js";
17
+ import { argmaxBy, corpusN, hubBound } from "../traverse.js";
17
18
  import {
18
19
  analogyStrength,
19
20
  follow,
@@ -22,11 +23,17 @@ import {
22
23
  reverseContext,
23
24
  } from "../match.js";
24
25
  import { joinWithBridge } from "../resonance.js";
26
+ import { restatesQuery } from "../reasoning.js";
25
27
  import { CONCEPT, STEP } from "../graph-search.js";
26
28
  import { concat2, indexOf } from "../../bytes.js";
27
- import { dominates } from "../../geometry.js";
28
- import { decodeText, unexplainedLabel } from "../rationale.js";
29
+ import { consensusFloor, dominates } from "../../geometry.js";
30
+ import {
31
+ decodeText,
32
+ unexplainedLabel,
33
+ unexplainedSpans,
34
+ } from "../rationale.js";
29
35
  import { rItem, rNode } from "../trace.js";
36
+ import { dismissedKnownContent } from "../bridge.js";
30
37
 
31
38
  // ── CAST gates ────────────────────────────────────────────────────────────
32
39
  //
@@ -80,6 +87,85 @@ export interface CastResult {
80
87
  unexplained: string;
81
88
  }
82
89
 
90
+ /** The seat that establishes a node's role in an analogical comparison:
91
+ * the REVERSE context (what leads to it) when a predecessor genuinely
92
+ * ESTABLISHES id — introduces or describes it by name — else the FORWARD
93
+ * continuation (what it leads to), else `fallback`.
94
+ *
95
+ * An earlier version gated this purely on `prevCount(id) > 0`: any
96
+ * predecessor at all was treated as proof of a genuine named ENTITY
97
+ * (seat it by what established it), while no predecessor meant a bare
98
+ * learnt CONTEXT (seat it by what it leads to, since voicing it verbatim
99
+ * would answer a question with a question). That test measured the wrong
100
+ * thing — a broad sample of this store's own question-shaped nodes showed
101
+ * the large majority (≈71%) have at least one predecessor, most of them a
102
+ * handful of generic, high-fan-out sentences that recur as an INCIDENTAL
103
+ * neighbour to dozens of otherwise-unrelated destinations (a SmolSent-
104
+ * style sentence-adjacency artifact, never naming or describing what
105
+ * follows). Traced live: "What is the capital of France?" — whose own
106
+ * forward edge unambiguously resolves to "The capital of France is
107
+ * Paris." — has exactly one such incidental predecessor ("Create an
108
+ * example of a types of questions a GPT model can answer.?"), wrongly
109
+ * read as disqualifying proof of "genuine entity."
110
+ *
111
+ * A plain forward-first swap (matching {@link project}'s universal
112
+ * priority) over-corrected: test/29's C2/C3 pin that a genuine entity
113
+ * analog (e.g. "Leonardo da Vinci", established by "The Mona Lisa was
114
+ * painted by Leonardo da Vinci.") must be seated by that establishing
115
+ * sentence, NOT by its own biography fact — voicing the bio leaks exactly
116
+ * what a comparison must keep out, and loses the embedded "Mona Lisa"
117
+ * term C3 relies on for a further hop.
118
+ *
119
+ * The distinguishing signal is content-addressed, not a count: a genuine
120
+ * establishing predecessor's bytes CONTAIN id's own bytes — it names or
121
+ * describes id ("...painted by Leonardo da Vinci." contains "Leonardo da
122
+ * Vinci"). An incidental adjacency predecessor never does — it merely
123
+ * preceded id in some unrelated document without ever mentioning it. No
124
+ * new tuned constant: containment is the same primitive `restatesQuery`
125
+ * and `dominates`-style checks already use throughout this codebase.
126
+ *
127
+ * `allowForward` (default true) gates the FORWARD branch specifically —
128
+ * see the call sites below: the DOMINANT is what the query is actually
129
+ * ASKING, so completing it forward is the whole point; an ANALOG is only
130
+ * being CITED for comparison; the query never asked about IT, so chasing
131
+ * its own further continuation drifts onto whatever coincidentally
132
+ * follows it in the corpus. Traced live: the analog "What is the capital
133
+ * of Japan?\nTokyo is the capital of Japan." is ALREADY a complete,
134
+ * self-answering unit (prevCount 0, so no establishing predecessor
135
+ * either) — its sole forward edge is "And what is the capital of the
136
+ * Moon?", an unrelated quiz question sharing nothing but corpus
137
+ * adjacency. With forward disallowed, an analog like this falls through
138
+ * to `fallback` — its own bytes, exactly the complete fact that made it a
139
+ * genuine analog in the first place. See
140
+ * test/41-seatofnode-direction.test.mjs and
141
+ * test/43-cast-analog-seat.test.mjs. */
142
+ export async function seatOfNode(
143
+ ctx: MindContext,
144
+ id: number,
145
+ guide: Vec | null | undefined,
146
+ fallback: Uint8Array,
147
+ allowForward = true,
148
+ ): Promise<Uint8Array> {
149
+ const rev = ctx.store.prevFirst(id, hubBound(ctx));
150
+ if (rev.length > 0) {
151
+ const own = read(ctx, id);
152
+ const establishing = rev.some((p) => indexOf(read(ctx, p), own, 0) >= 0);
153
+ if (establishing) {
154
+ const back = reverseContext(ctx, id, guide, rev);
155
+ if (back !== null) return back;
156
+ }
157
+ }
158
+ // The "last resort, non-establishing reverse" fallback below is itself a
159
+ // LESS CERTAIN projection (the same tier as forward) — an analog
160
+ // (allowForward: false) must stop at `fallback` (its own bytes) here
161
+ // rather than fall back to a predecessor that already failed the
162
+ // establishing check just above.
163
+ if (!allowForward) return fallback;
164
+ const fwd = await follow(ctx, id, guide);
165
+ if (fwd !== null) return fwd;
166
+ return reverseContext(ctx, id, guide, rev) ?? fallback;
167
+ }
168
+
83
169
  /** CAST's own entry gates, checked once here and reused by
84
170
  /** The main CAST entry point. Given a query and its pre-computed pre.rec.sites,
85
171
  * determine whether the query weaves together multiple independent learnt
@@ -102,18 +188,43 @@ export async function counterfactualTransfer(
102
188
  query: Uint8Array,
103
189
  pre: Precomputed,
104
190
  ): Promise<CastResult[]> {
191
+ // Opened unconditionally, at entry — the same convention recall.ts's
192
+ // recallByResonance and extraction.ts's extractBySkill use, so every exit
193
+ // path (five gates below, then the schemas themselves) closes through
194
+ // ONE scope and inspectRationale never hits a silent dead end. Only the
195
+ // first two gates duplicate floor()'s own admissible bound (query length,
196
+ // ranked anchor count) — required to stay in sync per this function's own
197
+ // doc comment above, and effectively dead through the ordinary pipeline
198
+ // (floor() returning null already stops run() from being called at all),
199
+ // but this function is also exported and callable directly, so they stay
200
+ // and get the same honest trace as everything past them.
201
+ const t = ctx.trace?.enter("counterfactual", [rItem(query, "query")]);
202
+ const fail = (note: string): CastResult[] => {
203
+ t?.done([], note);
204
+ return [];
205
+ };
206
+
105
207
  const quantum = ctx.space.maxGroup;
106
208
  if (query.length < 2 * quantum || ctx.store.edgeSourceCount() === 0) {
107
- return [];
209
+ return fail("query below the two-quantum floor, or no edges learnt yet");
108
210
  }
109
211
  const { roots, ranked } = await pre.attention();
110
- if (ranked.length < 2) return [];
212
+ if (ranked.length < 2) {
213
+ return fail(
214
+ `only ${ranked.length} ranked anchor(s) — CAST needs at least two`,
215
+ );
216
+ }
111
217
 
112
218
  const weave = await pre.weave();
113
219
  const points = weave.points;
114
220
  const depth = weave.depth;
115
221
  const aligned = points.length;
116
- if (aligned < 2) return [];
222
+ if (aligned < 2) {
223
+ return fail(
224
+ `only ${aligned} structure(s) aligned across the query — CAST needs ` +
225
+ `at least two to transfer between`,
226
+ );
227
+ }
117
228
 
118
229
  type Point = typeof points[0];
119
230
 
@@ -154,18 +265,31 @@ export async function counterfactualTransfer(
154
265
  const isRoot = (id: number) => roots.some((r) => r.anchor === id);
155
266
  // The weave must touch a COMMITTED point of attention: the dominant
156
267
  // structure itself, or another aligned point the climb committed to.
157
- if (!points.some((p) => isRoot(p.anchor))) return [];
268
+ if (!points.some((p) => isRoot(p.anchor))) {
269
+ t?.done(
270
+ [
271
+ ...points.map((p) => rNode(ctx, p.anchor, "aligned")),
272
+ ...roots.map((r) => rNode(ctx, r.anchor, "committed-root")),
273
+ ],
274
+ `${points.length} aligned structure(s), but none is one of the climb's ` +
275
+ `${roots.length} committed root(s) — CAST refuses to transfer through ` +
276
+ `content the climb itself never settled on`,
277
+ );
278
+ return [];
279
+ }
158
280
 
159
281
  const woven = points.some((p) =>
160
282
  p.runs.some((r) =>
161
283
  !pre.rec.sites.some((s) => r.qs >= s.start && r.qe <= s.end)
162
284
  )
163
285
  );
164
- if (!woven) return [];
286
+ if (!woven) {
287
+ return fail(
288
+ `every aligned run restates a recognised query site — nothing was ` +
289
+ `actually WOVEN across structures, so there is nothing to transfer`,
290
+ );
291
+ }
165
292
 
166
- const t = ctx.trace?.enter("counterfactual", [
167
- rItem(query, "query"),
168
- ]);
169
293
  // Each schema tried below RECORDS its candidate (when it fires) rather than
170
294
  // returning immediately — every schema that succeeds contributes its own
171
295
  // candidate, and the grounding decider's own weight comparison (not CAST's
@@ -259,7 +383,10 @@ export async function counterfactualTransfer(
259
383
  const tail = proj.ctx.subarray(seat.cs);
260
384
  let answer = await joinWithBridge(ctx, filler, tail);
261
385
  const fwd = await follow(ctx, proj.anchor, qv);
262
- if (fwd !== null && indexOf(answer, fwd, 0) < 0) {
386
+ if (
387
+ fwd !== null && indexOf(answer, fwd, 0) < 0 &&
388
+ !restatesQuery(query, fwd)
389
+ ) {
263
390
  answer = concat2(answer, fwd);
264
391
  }
265
392
  ctx.trace?.step(
@@ -337,36 +464,14 @@ export async function counterfactualTransfer(
337
464
  // seed-dependent failure where the climb ranks an exemplar above a
338
465
  // person node and the person node is excluded from points by run-
339
466
  // overlap trimming.
340
- // The seat that establishes a candidate's role: the REVERSE projection
341
- // (the context it follows), voiced by the query gist — falling back to the
342
- // candidate's own bytes when it follows nothing. DELIBERATE STRENGTHENING
343
- // over the pre-refactor code: reverseContext also returns null when the
344
- // picked context READS EMPTY (a dangling id degrades to zero bytes), so a
345
- // corrupted-store read now falls back here instead of voicing a hollow
346
- // seat into the comparison — the same "empty bytes are no grounding"
347
- // invariant project() has always enforced.
348
- // A node that only ever CONTINUES — an edge SOURCE with no predecessor —
349
- // is a learnt CONTEXT (a stored question-shaped frame), not an entity.
350
- // Voicing it verbatim answers a question with a question (the observed
351
- // cast echo: two stored questions concatenated as an "answer"). The
352
- // context's role is established by what it LEADS TO, so its seat is its
353
- // own continuation — the fact — with the reverse projection and the raw
354
- // bytes as the ordinary fallbacks for genuine entities.
355
- const seatOfNode = async (
356
- id: number,
357
- fallback: Uint8Array,
358
- ): Promise<Uint8Array> => {
359
- if (ctx.store.prevCount(id) === 0 && ctx.store.hasNext(id)) {
360
- const fwd = await follow(ctx, id, qv);
361
- if (fwd !== null) return fwd;
362
- }
363
- return reverseContext(ctx, id, qv) ?? fallback;
364
- };
365
- const seatOf = (p: Point): Promise<Uint8Array> => seatOfNode(p.anchor, p.ctx);
467
+ // The seat that establishes a candidate's role see {@link seatOfNode}.
468
+ const seatOf = (p: Point, allowForward = true): Promise<Uint8Array> =>
469
+ seatOfNode(ctx, p.anchor, qv, p.ctx, allowForward);
366
470
  interface AnalogCandidate {
367
471
  anchor: number;
368
472
  /** The point this candidate came from, or null when it is a nextOf
369
- * descendant — then seatOfNode supplies the seat. */
473
+ * descendant — then its own bytes ARE the seat (already one meaningful
474
+ * hop from `src`; see the comparison gate below). */
370
475
  point: Point | null;
371
476
  /** For a nextOf descendant: the aligned point whose continuation edge
372
477
  * named it. Its runs ARE the query evidence this analog rests on
@@ -409,8 +514,46 @@ export async function counterfactualTransfer(
409
514
  }
410
515
  let bestAnalog: AnalogCandidate | null = null;
411
516
  let bestSim = 0;
517
+ let bestHalo = false;
518
+ // Whether the query itself NAMES a candidate. A directly aligned point
519
+ // is named by construction — its runs ARE query bytes. A hop-reached
520
+ // candidate is named when its own bytes contain the query text of an
521
+ // aligned run of the point whose continuation edge reached it (that
522
+ // alignment IS the query evidence the hop rests on — the same reading
523
+ // cmpAccounted already prices): "William Shakespeare", reached off
524
+ // "Macbeth was written by William Shakespeare.", contains the src's
525
+ // 12-byte aligned run " Shakespeare" — test/29 C2/C3. The run must span
526
+ // at least TWO perception windows (2·W, the same two-quantum floor
527
+ // CAST's own entry gate holds the whole query to): a single shared
528
+ // W-window is exactly the frame tier's own evidence quantum — the level
529
+ // "half the corpus" shares — and stopword scraps (" the ", "he b",
530
+ // 4–5 bytes) never reach two windows, while a genuinely named entity
531
+ // does. NOT the weave's usable()/frame filter: weave depth counts every
532
+ // ranked exemplar, so a query's own named entity recurring across
533
+ // exemplars ("Shakespeare" in Hamlet+Macbeth+…) is wrongly classified as
534
+ // frame — measured live, it silently disqualified C3's genuine analog.
535
+ const namedByQuery = (c: AnalogCandidate): boolean => {
536
+ if (c.point !== null) return true;
537
+ const bytes = read(ctx, c.anchor);
538
+ return c.src.runs.some((r) =>
539
+ r.qe - r.qs >= 2 * quantum &&
540
+ indexOf(bytes, query.subarray(r.qs, r.qe), 0) >= 0
541
+ );
542
+ };
543
+ // Whether any committed root's consensus vote clears the SAME trust bar
544
+ // recallByResonance applies before grounding through a climb root:
545
+ // consensusFloor(N) = ln(N) + 1/2. The climb's FIRST root is
546
+ // deliberately floor-free (attention.ts: "the dominant one always
547
+ // grounds") — fine for ORIENTING mechanisms, not for voicing learnt
548
+ // content the query never asked about. Computed once here; both the
549
+ // hub fallback below and the comparison gate consume it.
550
+ const rootTrusted = roots.some((r) => r.vote >= consensusFloor(corpusN(ctx)));
412
551
  for (const c of analogs) {
413
- const sim = await analogyStrength(ctx, dominant.anchor, c.anchor);
552
+ const { score: sim, halo } = await analogyStrength(
553
+ ctx,
554
+ dominant.anchor,
555
+ c.anchor,
556
+ );
414
557
  ctx.trace?.step(
415
558
  "tryAnalog",
416
559
  [
@@ -418,11 +561,12 @@ export async function counterfactualTransfer(
418
561
  rNode(ctx, c.anchor, "candidate", sim),
419
562
  ],
420
563
  [],
421
- `analogy strength ${sim.toFixed(4)}`,
564
+ `analogy strength ${sim.toFixed(4)}${halo ? " (halo tier)" : ""}`,
422
565
  );
423
566
  if (sim > bestSim) {
424
567
  bestSim = sim;
425
568
  bestAnalog = c;
569
+ bestHalo = halo;
426
570
  }
427
571
  }
428
572
  // When every candidate fails the similarity gates (halo company — now
@@ -451,6 +595,17 @@ export async function counterfactualTransfer(
451
595
  let hubMass = -1;
452
596
  const fanClamp = hubBound(ctx) + 1;
453
597
  for (const c of analogs) {
598
+ // A fallback comparison carries NO similarity evidence at all. Its
599
+ // honesty rests on the grounding decider discounting it against
600
+ // richer candidates (the design note below) — an assumption that
601
+ // holds only when the climb itself settled on this query with real
602
+ // evidence. Under a root the consensus floor does not trust, an
603
+ // unnamed, hop-reached hub is pure corpus adjacency: refusing it is
604
+ // what kept the live wrong echo silent. A hub the query itself
605
+ // NAMED stays eligible either way (test/29 C2/C3's "William
606
+ // Shakespeare"); an unnamed one under a TRUSTED root stays eligible
607
+ // too (test/33 1b's deliberately weak second candidate).
608
+ if (!rootTrusted && !namedByQuery(c)) continue;
454
609
  // Evidence clamped at the hub bound: beyond √N + 1 the exact fan-out
455
610
  // no longer discriminates (every mega-hub ties at the clamp), and
456
611
  // counting it exactly would require the corpus-sized read.
@@ -500,10 +655,111 @@ export async function counterfactualTransfer(
500
655
  // climb's own forest, never tuned; substitution/redirection stay
501
656
  // unaffected — they orient around a displaced seat, not a whole-topic
502
657
  // analogy.
658
+ //
659
+ // roots.length <= 1 is a PROXY for "the query is about one thing" — it is
660
+ // only as good as the climb's own root-commitment, which depends on
661
+ // recognise() having found something to commit a root TO. When the
662
+ // query's newest content genuinely isn't recognised (not boundary noise —
663
+ // real, uncommitted content; see the session's own investigation of the
664
+ // France→Spain live trace), the climb under-commits roots and this proxy
665
+ // is fooled: comparison looks licensed to treat the query as one topic
666
+ // when it is not.
667
+ //
668
+ // The direct check is the SAME accounted spans comparison is about to
669
+ // cite as its evidence: unexplainedSpans (rationale.ts, the same gap
670
+ // computation the trace's own `unexplained` diagnostic uses) names every
671
+ // stretch of the query NEITHER the dominant NOR the analog's evidence
672
+ // touches. A short comparison query ("How is ice like steel?") legitimately
673
+ // accounts for only its two short entity spans — the surrounding "How is
674
+ // ... like ...?" framing is real but SHORT, split into several small gaps,
675
+ // none of them the bulk of the query. The live bug's shape is different in
676
+ // kind, not degree: ONE contiguous, substantial gap — a whole second
677
+ // question the query added that comparison's two spans never touch at all.
678
+ //
679
+ // Two bars, both derived, neither tuned:
680
+ // • the largest gap must not DOMINATE the whole query (the same
681
+ // predicate CAST's own frame gate uses) — rules out a gap that is
682
+ // most of the query outright;
683
+ // • the largest gap must be SMALLER than the dominant's own established
684
+ // context. A gap can't be dismissed as mere connective framing once
685
+ // it is at least as large as the topic being compared FROM — at that
686
+ // scale it isn't glue between two named things, it's substantial
687
+ // enough to be a second topic in its own right. This is what
688
+ // actually separates the live bug (a 47-byte gap against a 30-byte
689
+ // dominant — the ignored content is bigger than the topic itself)
690
+ // from ordinary short comparisons (a 9-byte gap against an 11-byte
691
+ // dominant — the gap is smaller than what's being compared): the two
692
+ // cases land on the same side of "half the query" often enough
693
+ // (both can exceed or clear it) that the query-relative bar alone
694
+ // does not reliably separate them — the topic-relative scale does.
695
+ const cmpAccounted: Array<[number, number]> = bestAnalog !== null
696
+ ? [...runSpans(dominant), ...runSpans(bestAnalog.point ?? bestAnalog.src)]
697
+ : [];
698
+ const cmpGaps = unexplainedSpans(query.length, cmpAccounted);
699
+ const cmpMaxGap = cmpGaps.reduce((n, [s, e]) => Math.max(n, e - s), 0);
700
+ // An analog that is not itself a directly ALIGNED point (point !== null —
701
+ // its own runs are query bytes, the query NAMED it) was only reached
702
+ // through a continuation hop or the structural-hub fallback. Voicing
703
+ // learnt content the query never named is the same act recallByResonance
704
+ // refuses to perform through a climb root whose consensus vote is below
705
+ // consensusFloor(N) = ln(N) + 1/2 (recall.ts's minVote), so comparison
706
+ // holds the climb to that SAME bar before citing a hop-reached analog:
707
+ // some committed root must clear the floor. The climb's FIRST root is
708
+ // deliberately floor-free (attention.ts: "the dominant one always
709
+ // grounds") — fine for ORIENTING mechanisms, not for transferring
710
+ // unnamed content through. The live bug this gates (real trained store,
711
+ // 325k edge sources, floor 13.2): the query's stopword scraps pooled a
712
+ // 1.92 vote that committed an unrelated haiku exemplar as the sole root,
713
+ // and comparison voiced that exemplar's continuation through a
714
+ // hop-reached analog while every other mechanism honestly refused. A
715
+ // directly aligned analog needs no floor — the query's own bytes are its
716
+ // evidence (test/29 C1's "Steel is hard" for "How is ice like steel?").
717
+ // See test/50-cast-analog-consensus-floor.
718
+ // A HALO-tier best analog needs neither: its similarity already cleared
719
+ // significanceBar-gated distributional company (analogyStrength's
720
+ // `halo`) — genuine evidence in its own right, the very case the halo
721
+ // gate exists for (test/33 1b's nickname-corroborated analog). Only a
722
+ // FRAME-tier or fallback analog — whose "similarity" is an unbarred
723
+ // coverage fraction or nothing — needs the query's naming or the climb's
724
+ // trust.
725
+ const analogNamed = bestAnalog !== null && namedByQuery(bestAnalog);
726
+ // NOTE — two further gates were tried here and empirically REFUTED,
727
+ // recorded so they are not re-tried:
728
+ // • dominant self-coverage (dominant's aligned runs must dominate its
729
+ // own ctx): legitimate dominants sit at the same coverage as junk
730
+ // ones ("The Mona Lisa was painted by…" 16/47 vs the live junk
731
+ // haiku ~10/54) — no separation.
732
+ // • denying the shared-frame similarity tier to hop-reached analogs:
733
+ // semantically right in isolation, but it merely promoted the next
734
+ // junk candidate — an ALIGNED scrap-matched point ("The affluence…",
735
+ // frame 0.157) — into bestAnalog on the live store, and the aligned
736
+ // configuration is byte-structurally IDENTICAL to test/29 C1's
737
+ // legitimate one ("Steel is hard", frame 0.364): every derived
738
+ // local separator measured (run length, site overlap, frame
739
+ // query-containment, weave-usable classification) falls on the same
740
+ // side for both. Only corpus-scale consensus separates them, which
741
+ // is exactly what `rootTrusted` prices.
742
+ // FRAME-tier evidence under an UNTRUSTED root is comparison's weakest
743
+ // licence (an unbarred coverage fraction, a climb the consensus floor
744
+ // does not trust). There it is additionally held to the IGNORED-KNOWN
745
+ // principle (dismissedKnownContent, bridge.ts): the two analogs' aligned
746
+ // runs must account for every STORED window of the query. This is the
747
+ // byte-structural separator the refuted-gates note below could not find
748
+ // locally: a legitimate small-corpus comparison ("How is ice like
749
+ // steel?") leaves only UNATTESTED spans ("How ", " like ") unexplained,
750
+ // while a scrap-matched junk pair leaves the query's own trained content
751
+ // ("…songs…times…", "…planet…sun.") dismissed as gaps. Halo-tier and
752
+ // trusted-root comparisons are exempt — their evidence already stands.
753
+ const cmpDismisses = !(bestHalo || rootTrusted) &&
754
+ dismissedKnownContent(ctx, query, cmpAccounted);
503
755
  if (
504
756
  bestAnalog !== null &&
757
+ (bestHalo || analogNamed || rootTrusted) &&
758
+ !cmpDismisses &&
505
759
  dominant.ctx.length <= query.length &&
506
- roots.length <= 1
760
+ roots.length <= 1 &&
761
+ !dominates(cmpMaxGap, query.length) &&
762
+ cmpMaxGap < dominant.ctx.length
507
763
  ) {
508
764
  ctx.trace?.step(
509
765
  "validateAnalogy",
@@ -515,9 +771,21 @@ export async function counterfactualTransfer(
515
771
  "the two structures keep distributional company beyond chance — genuine analogs",
516
772
  );
517
773
  const a = await seatOf(dominant);
774
+ // The analog is only being CITED for comparison — the query never asked
775
+ // about it — so its seat never chases a FORWARD continuation (see
776
+ // seatOfNode's `allowForward`): only reverse (if a predecessor genuinely
777
+ // establishes it) or its own bytes. A DIRECTLY aligned point
778
+ // (bestAnalog.point !== null) still goes through seatOfNode for that
779
+ // reverse check (a bare entity NAME like "Leonardo da Vinci" needs it —
780
+ // test/29's C2/C3). A nextOf DESCENDANT (point === null) was already
781
+ // reached by following ONE meaningful hop off another aligned point (the
782
+ // alignment loop above: "its nextOf is the hub... and the hub's own
783
+ // [...] context will be the seat") — its own bytes ARE that seat
784
+ // directly, with no predecessor to even check (it was found by a
785
+ // forward edge, not matched in the query).
518
786
  const b = bestAnalog.point !== null
519
- ? await seatOf(bestAnalog.point)
520
- : await seatOfNode(bestAnalog.anchor, read(ctx, bestAnalog.anchor));
787
+ ? await seatOf(bestAnalog.point, false)
788
+ : read(ctx, bestAnalog.anchor);
521
789
  const answer = await joinWithBridge(ctx, a, b);
522
790
  record(
523
791
  answer,
@@ -530,10 +798,32 @@ export async function counterfactualTransfer(
530
798
  // when it was an aligned point, else the source point whose
531
799
  // continuation edge reached it (that alignment IS the query evidence
532
800
  // the hop rests on).
801
+ cmpAccounted,
802
+ );
803
+ } else if (
804
+ bestAnalog !== null &&
805
+ dominant.ctx.length <= query.length &&
806
+ roots.length <= 1
807
+ ) {
808
+ ctx.trace?.step(
809
+ "validateAnalogy",
533
810
  [
534
- ...runSpans(dominant),
535
- ...runSpans(bestAnalog.point ?? bestAnalog.src),
811
+ rNode(ctx, dominant.anchor, "analog", bestSim),
812
+ rNode(ctx, bestAnalog.anchor, "analog", bestSim),
536
813
  ],
814
+ [],
815
+ !(bestHalo || analogNamed || rootTrusted)
816
+ ? `the best analog carries no halo-tier company evidence, was never ` +
817
+ `named by the query, and no committed root's consensus vote ` +
818
+ `clears the floor, so comparison refuses to voice it`
819
+ : cmpDismisses
820
+ ? `a frame-tier analog under an untrusted root dismisses stored ` +
821
+ `query content its alignment never accounted for — comparison ` +
822
+ `refuses to ignore what the store knows`
823
+ : `comparison's own accounted evidence leaves a ${cmpMaxGap}-byte gap in ` +
824
+ `a ${query.length}-byte query against a ${dominant.ctx.length}-byte ` +
825
+ `dominant — too large to be mere framing — so it refuses rather ` +
826
+ `than paper over it with an analog the query never asked about`,
537
827
  );
538
828
  }
539
829
  t?.done(
@@ -11,12 +11,11 @@
11
11
 
12
12
  import type { MindContext } from "../types.js";
13
13
  import type { ComputedResult, Site } from "../graph-search.js";
14
- import { bytesEqual, indexOf } from "../../bytes.js";
15
14
  import { read, resolve } from "../primitives.js";
16
15
  import { guidedFirst } from "../traverse.js";
17
16
  import { conceptHop } from "../match.js";
18
17
  import { bridge } from "../resonance.js";
19
- import { liftAnswer } from "../types.js";
18
+ import { liftAnswer, segRestatesQuery } from "../types.js";
20
19
  import { decodeText, unexplainedLabel } from "../rationale.js";
21
20
  import type { RationaleItem } from "../rationale.js";
22
21
  import { rItem, rNode, traceDerivation } from "../trace.js";
@@ -159,11 +158,18 @@ export const coverMechanism: PipelineMechanism = {
159
158
 
160
159
  const connectors = await resolveConnectors(ctx, sites);
161
160
  let splits = rec.splits;
161
+ let starts = rec.starts;
162
162
  if (computed.length > 0) {
163
163
  splits = new Set(rec.splits);
164
+ starts = new Set(rec.starts);
164
165
  for (const u of computed) {
165
166
  splits.add(u.i);
166
167
  splits.add(u.j);
168
+ // A computation's own boundaries carry the same fold-level evidence
169
+ // a chunk boundary does — "computation always wins" (see the header
170
+ // comment) extends to being trusted ground for cross-leaf recovery.
171
+ starts.add(u.i);
172
+ starts.add(u.j);
167
173
  }
168
174
  }
169
175
  const concepts = await resolveConcepts(ctx, sites);
@@ -199,6 +205,7 @@ export const coverMechanism: PipelineMechanism = {
199
205
  concepts,
200
206
  rec.leaves,
201
207
  splits,
208
+ starts,
202
209
  undefined,
203
210
  connectors,
204
211
  computedResults,
@@ -218,44 +225,31 @@ export const coverMechanism: PipelineMechanism = {
218
225
 
219
226
  if (segs === null) return [];
220
227
 
228
+ const W = ctx.space.maxGroup;
221
229
  // A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
222
230
  // site, not the site's own literal text read back) that equal a byte
223
231
  // span the query ALREADY CONTAINS elsewhere restates part of the
224
- // question — never an answer (the same principle recall.ts's tiers
225
- // apply to a whole-query projection: "a projection that is a proper
226
- // byte-subspan of the query restates part of the question"). A
232
+ // question — never an answer (see {@link segRestatesQuery}). A
227
233
  // recognised site that is itself an entire PRIOR TURN of a multi-turn
228
234
  // query is exactly this shape: it carries a genuine learnt
229
235
  // continuation, but that continuation is something the asker already
230
- // said moments later in the SAME query, not a new answer — the cover
231
- // search has no notion of "turn" to gate this itself, so the check
232
- // belongs here, over the derivation it already chose.
233
- const W = ctx.space.maxGroup;
234
- for (const s of segs) {
235
- if (!s.rec) continue;
236
- const literal = s.j - s.i === s.bytes.length &&
237
- bytesEqual(s.bytes, query.subarray(s.i, s.j));
238
- if (literal) continue;
239
- // A span shorter than one river window is exactly the "ice"/"c"
240
- // case: a chain hop's short terminal coincidentally recurring as a
241
- // substring of an unrelated longer word. Below the quantum, byte
242
- // overlap is chance, not evidence — the same floor identityBar and
243
- // reachThreshold hold every other structural-overlap claim to.
244
- if (
245
- s.bytes.length >= W && s.bytes.length < query.length &&
246
- indexOf(query, s.bytes, 0) >= 0
247
- ) {
248
- ctx.trace?.step(
249
- "restatedSpan",
250
- [rItem(s.bytes, "substituted", s.node, [s.i, s.j])],
251
- [],
252
- "the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer",
253
- );
254
- return [];
255
- }
236
+ // said moments later in the SAME query. liftAnswer TRIMS such spans
237
+ // out of both the framing decision and the final concatenation the
238
+ // OTHER spans a derivation chose are independent evidence and must not
239
+ // be discarded along with the stale one.
240
+ const restated = segs.filter((s) =>
241
+ segRestatesQuery(s, query, query.length, W)
242
+ );
243
+ if (restated.length > 0) {
244
+ ctx.trace?.step(
245
+ "restatedSpan",
246
+ restated.map((s) => rItem(s.bytes, "substituted", s.node, [s.i, s.j])),
247
+ [],
248
+ "the chosen span's substitution already occurs elsewhere in the query trimmed from the answer, not an answer itself",
249
+ );
256
250
  }
257
251
 
258
- const composed = liftAnswer(segs, query.length);
252
+ const composed = liftAnswer(segs, query.length, query, W);
259
253
  if (composed === null) return [];
260
254
 
261
255
  ctx.trace?.step(
@@ -268,11 +262,12 @@ export const coverMechanism: PipelineMechanism = {
268
262
  tCover ? [tCover.index] : undefined,
269
263
  );
270
264
 
271
- // accounted = RECOGNISED cover spans only — PASS-carried bytes
272
- // are priced in cost already; the diagnostic label reflects the
273
- // same distinction.
265
+ // accounted = RECOGNISED, non-restating cover spans only — a trimmed
266
+ // restated span contributes nothing to the composed answer, so it must
267
+ // not be priced as if it did (PASS-carried bytes are priced already;
268
+ // the diagnostic label reflects the same distinction).
274
269
  const accounted: Array<[number, number]> = segs
275
- .filter((s) => s.rec)
270
+ .filter((s) => s.rec && !segRestatesQuery(s, query, query.length, W))
276
271
  .map((s) => [s.i, s.j]);
277
272
 
278
273
  return [{