@hviana/sema 0.2.3 → 0.2.5

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/geometry.d.ts +26 -0
  2. package/dist/src/geometry.js +32 -3
  3. package/dist/src/mind/attention.d.ts +246 -7
  4. package/dist/src/mind/attention.js +771 -173
  5. package/dist/src/mind/bridge.d.ts +30 -0
  6. package/dist/src/mind/bridge.js +569 -0
  7. package/dist/src/mind/index.d.ts +2 -0
  8. package/dist/src/mind/junction.d.ts +30 -1
  9. package/dist/src/mind/junction.js +73 -18
  10. package/dist/src/mind/match.d.ts +15 -2
  11. package/dist/src/mind/match.js +3 -8
  12. package/dist/src/mind/mechanisms/cast.d.ts +54 -0
  13. package/dist/src/mind/mechanisms/cast.js +268 -37
  14. package/dist/src/mind/mechanisms/cover.js +16 -31
  15. package/dist/src/mind/mechanisms/recall.js +66 -0
  16. package/dist/src/mind/mind.d.ts +2 -0
  17. package/dist/src/mind/rationale.d.ts +7 -2
  18. package/dist/src/mind/rationale.js +6 -5
  19. package/dist/src/mind/reasoning.d.ts +11 -0
  20. package/dist/src/mind/reasoning.js +58 -2
  21. package/dist/src/mind/recognition.js +127 -7
  22. package/dist/src/mind/traverse.js +90 -25
  23. package/dist/src/mind/types.d.ts +57 -2
  24. package/dist/src/mind/types.js +38 -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/geometry.ts +52 -3
  29. package/src/mind/attention.ts +1199 -128
  30. package/src/mind/bridge.ts +596 -0
  31. package/src/mind/index.ts +16 -0
  32. package/src/mind/junction.ts +125 -16
  33. package/src/mind/match.ts +19 -5
  34. package/src/mind/mechanisms/cast.ts +290 -38
  35. package/src/mind/mechanisms/cover.ts +23 -36
  36. package/src/mind/mechanisms/recall.ts +79 -0
  37. package/src/mind/mind.ts +15 -0
  38. package/src/mind/rationale.ts +12 -4
  39. package/src/mind/reasoning.ts +71 -2
  40. package/src/mind/recognition.ts +132 -7
  41. package/src/mind/traverse.ts +91 -24
  42. package/src/mind/types.ts +95 -6
  43. package/src/store.ts +19 -5
  44. package/test/36-already-answered-fusion.test.mjs +128 -0
  45. package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
  46. package/test/38-reason-restate-guard.test.mjs +94 -0
  47. package/test/39-cast-restate-guard.test.mjs +102 -0
  48. package/test/40-choosenext-scale-guard.test.mjs +75 -0
  49. package/test/41-seatofnode-direction.test.mjs +85 -0
  50. package/test/42-recognise-trace-idempotence.test.mjs +106 -0
  51. package/test/43-cast-analog-seat.test.mjs +244 -0
  52. package/test/44-recognise-edge-whitespace.test.mjs +63 -0
  53. package/test/45-liftanswer-restated-trim.test.mjs +60 -0
  54. package/test/46-recognise-multibyte-edge.test.mjs +85 -0
  55. package/test/47-cast-comparison-coverage.test.mjs +134 -0
  56. package/test/48-recognise-turn-connective.test.mjs +125 -0
  57. package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
  58. package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
  59. package/test/51-structural-resonance-ladder.test.mjs +552 -0
  60. package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
@@ -16,6 +16,20 @@ import { windowIds } from "./canonical.js";
16
16
  import { hubBound } from "./traverse.js";
17
17
  import { haloSiblings } from "./match.js";
18
18
  import { indexOf } from "../bytes.js";
19
+ /** Resolve `left`/`right` to their exact node ids (when known) and load each
20
+ * resolved side's halo siblings once — deterministic (haloSiblings already
21
+ * ranks nearest-first) and shared by every ladder rung that consults
22
+ * siblings, so no ladder rung repeats a halo ANN query the previous one
23
+ * already paid for. */
24
+ export async function loadJunctionSynonymSides(ctx, left, right) {
25
+ const leftId = resolve(ctx, left);
26
+ const rightId = resolve(ctx, right);
27
+ const leftSiblings = leftId !== null ? await haloSiblings(ctx, leftId) : [];
28
+ const rightSiblings = rightId !== null
29
+ ? await haloSiblings(ctx, rightId)
30
+ : [];
31
+ return { leftId, rightId, leftSiblings, rightSiblings };
32
+ }
19
33
  /** Seed node ids to ascend from for one side of a junction: the side's own
20
34
  * node when it is a stored form, plus — when the node has no structural
21
35
  * parents — its canonical window ids. A non-W-aligned node may have no
@@ -225,38 +239,79 @@ export function junctionContainers(ctx, left, right, maxContainer, unordered = f
225
239
  * cost is bounded at √N·W pops total regardless of how many siblings are
226
240
  * tried. A sibling whose bytes exceed `maxInterior` is skipped (it
227
241
  * cannot be junction-sized). */
228
- export async function junctionSynonyms(ctx, left, right, maxInterior, unordered = false) {
229
- const out = [];
230
- const lId = resolve(ctx, left);
231
- const rId = resolve(ctx, right);
232
- if (lId === null && rId === null)
233
- return out;
234
- const budget = { n: hubBound(ctx) * ctx.space.maxGroup };
242
+ export async function junctionSynonyms(ctx, left, right, maxInterior, unordered = false, sides) {
243
+ const s = sides ?? await loadJunctionSynonymSides(ctx, left, right);
244
+ if (s.leftId === null && s.rightId === null)
245
+ return [];
246
+ // ── Tier 2.5a: single-synonym one side replaced by a halo sibling ──────
247
+ // ONE shared expansion budget across BOTH directions of this tier.
248
+ const singleBudget = { n: hubBound(ctx) * ctx.space.maxGroup };
249
+ const singleOut = new Map();
250
+ const keepBest = (map, j, tier, confidence) => {
251
+ const prev = map.get(j.id);
252
+ if (prev === undefined || confidence > prev.confidence) {
253
+ map.set(j.id, { ...j, tier, confidence });
254
+ }
255
+ };
235
256
  // Left-side synonyms: containers of sibling+right. `right`'s seeds are
236
257
  // FIXED across every sibling this loop tries.
237
- if (lId !== null) {
258
+ if (s.leftId !== null) {
238
259
  const rightSeeds = junctionSeeds(ctx, right);
239
- for (const sib of await haloSiblings(ctx, lId)) {
260
+ for (const sib of s.leftSiblings) {
240
261
  const sibBytes = read(ctx, sib.id, maxInterior + 1);
241
262
  if (sibBytes.length === 0 || sibBytes.length > maxInterior)
242
263
  continue;
243
- const containers = junctionContainersFrom(ctx, sibBytes, right, sibBytes.length + right.length + maxInterior, junctionSeeds(ctx, sibBytes), rightSeeds, budget, unordered);
244
- for (const c of containers)
245
- out.push(c);
264
+ const containers = junctionContainersFrom(ctx, sibBytes, right, sibBytes.length + right.length + maxInterior, junctionSeeds(ctx, sibBytes), rightSeeds, singleBudget, unordered);
265
+ for (const c of containers) {
266
+ keepBest(singleOut, c, "single-synonym", sib.score);
267
+ }
246
268
  }
247
269
  }
248
270
  // Right-side synonyms: containers of left+sibling. `left`'s seeds are
249
271
  // likewise fixed across this loop.
250
- if (rId !== null) {
272
+ if (s.rightId !== null) {
251
273
  const leftSeeds = junctionSeeds(ctx, left);
252
- for (const sib of await haloSiblings(ctx, rId)) {
274
+ for (const sib of s.rightSiblings) {
253
275
  const sibBytes = read(ctx, sib.id, maxInterior + 1);
254
276
  if (sibBytes.length === 0 || sibBytes.length > maxInterior)
255
277
  continue;
256
- const containers = junctionContainersFrom(ctx, left, sibBytes, left.length + sibBytes.length + maxInterior, leftSeeds, junctionSeeds(ctx, sibBytes), budget, unordered);
257
- for (const c of containers)
258
- out.push(c);
278
+ const containers = junctionContainersFrom(ctx, left, sibBytes, left.length + sibBytes.length + maxInterior, leftSeeds, junctionSeeds(ctx, sibBytes), singleBudget, unordered);
279
+ for (const c of containers) {
280
+ keepBest(singleOut, c, "single-synonym", sib.score);
281
+ }
259
282
  }
260
283
  }
261
- return out;
284
+ if (singleOut.size > 0)
285
+ return [...singleOut.values()];
286
+ // ── Tier 2.5b: double-synonym — BOTH sides replaced, tried only when
287
+ // single-synonym found NOTHING. Every (leftSibling, rightSibling) pair,
288
+ // sorted deterministically, bounded to haloQueryK pairs total, ONE fresh
289
+ // shared budget for the whole tier. ─────────────────────────────────────
290
+ if (s.leftSiblings.length === 0 || s.rightSiblings.length === 0)
291
+ return [];
292
+ const pairs = [];
293
+ for (const l of s.leftSiblings) {
294
+ for (const r of s.rightSiblings) {
295
+ pairs.push({ l, r, confidence: Math.min(l.score, r.score) });
296
+ }
297
+ }
298
+ pairs.sort((a, b) => b.confidence - a.confidence ||
299
+ a.l.id - b.l.id ||
300
+ a.r.id - b.r.id);
301
+ const doubleOut = new Map();
302
+ const budget = { n: hubBound(ctx) * ctx.space.maxGroup };
303
+ const tries = Math.min(pairs.length, ctx.cfg.haloQueryK);
304
+ for (let i = 0; i < tries; i++) {
305
+ const { l, r, confidence } = pairs[i];
306
+ const lBytes = read(ctx, l.id, maxInterior + 1);
307
+ const rBytes = read(ctx, r.id, maxInterior + 1);
308
+ if (lBytes.length === 0 || lBytes.length > maxInterior ||
309
+ rBytes.length === 0 || rBytes.length > maxInterior)
310
+ continue;
311
+ const containers = junctionContainersFrom(ctx, lBytes, rBytes, lBytes.length + rBytes.length + maxInterior, junctionSeeds(ctx, lBytes), junctionSeeds(ctx, rBytes), budget, unordered);
312
+ for (const c of containers) {
313
+ keepBest(doubleOut, c, "double-synonym", confidence);
314
+ }
315
+ }
316
+ return [...doubleOut.values()];
262
317
  }
@@ -64,8 +64,21 @@ export declare function haloSiblings(ctx: MindContext, id: number, halo?: Vec |
64
64
  * strength, not a pick. Returns the direct halo cosine, or failing that the
65
65
  * highest mutual-halo-sibling min-score (second-order analogy), or failing
66
66
  * that the SHARED-FRAME strength (below) — the gate CAST's comparison
67
- * schema validates genuine analogs with (bar: significanceBar). */
68
- export declare function analogyStrength(ctx: MindContext, a: number, b: number): Promise<number>;
67
+ * schema validates genuine analogs with (bar: significanceBar).
68
+ *
69
+ * The result names its TIER alongside the score: `halo: true` means the
70
+ * score cleared a significanceBar-gated HALO tier (direct company cosine
71
+ * or mutual-sibling) — genuine distributional evidence; `halo: false`
72
+ * means only the structural shared-frame fallback matched, a coverage
73
+ * fraction with no bar of its own. CAST's comparison gate treats the two
74
+ * differently (see cast.ts): halo evidence stands alone, frame evidence
75
+ * needs the query to have named the analog or the climb root to be
76
+ * trusted. */
77
+ export interface AnalogyEvidence {
78
+ score: number;
79
+ halo: boolean;
80
+ }
81
+ export declare function analogyStrength(ctx: MindContext, a: number, b: number): Promise<AnalogyEvidence>;
69
82
  /** The STRUCTURAL analogy tier: two nodes are analogs when their byte
70
83
  * streams share a LEARNT frame — a content-addressed flat form of at least
71
84
  * one full river window (W bytes, the perception quantum) that occurs in
@@ -257,11 +257,6 @@ export async function haloSiblings(ctx, id, halo, bar = conceptThreshold(ctx.sto
257
257
  memo.set(id, out);
258
258
  return out;
259
259
  }
260
- /** The DISTRIBUTIONAL matcher between two nodes: mutual-nearest-neighbour
261
- * strength, not a pick. Returns the direct halo cosine, or failing that the
262
- * highest mutual-halo-sibling min-score (second-order analogy), or failing
263
- * that the SHARED-FRAME strength (below) — the gate CAST's comparison
264
- * schema validates genuine analogs with (bar: significanceBar). */
265
260
  export async function analogyStrength(ctx, a, b) {
266
261
  const ha = ctx.store.halo(a);
267
262
  const hb = ctx.store.halo(b);
@@ -269,7 +264,7 @@ export async function analogyStrength(ctx, a, b) {
269
264
  const bar = significanceBar(ctx.store.D);
270
265
  const direct = cosine(ha, hb);
271
266
  if (direct >= bar)
272
- return direct;
267
+ return { score: direct, halo: true };
273
268
  const sibsA = await haloSiblings(ctx, a, ha, bar);
274
269
  const sibsB = await haloSiblings(ctx, b, hb, bar);
275
270
  let best = 0;
@@ -282,9 +277,9 @@ export async function analogyStrength(ctx, a, b) {
282
277
  }
283
278
  }
284
279
  if (best > 0)
285
- return best;
280
+ return { score: best, halo: true };
286
281
  }
287
- return sharedFrameStrength(ctx, a, b);
282
+ return { score: sharedFrameStrength(ctx, a, b), halo: false };
288
283
  }
289
284
  /** The STRUCTURAL analogy tier: two nodes are analogs when their byte
290
285
  * streams share a LEARNT frame — a content-addressed flat form of at least
@@ -1,4 +1,5 @@
1
1
  import type { MindContext } from "../types.js";
2
+ import type { Vec } from "../../vec.js";
2
3
  /** A CAST answer plus its elementary evidence for think's grounding decider:
3
4
  * `accounted` — the query spans the weave's aligned runs explain; `moves` —
4
5
  * the ladder cost of the acts the taken branch performed (STEP per
@@ -13,6 +14,59 @@ export interface CastResult {
13
14
  * Task 2 note in pipeline.ts's Candidate interface). */
14
15
  unexplained: string;
15
16
  }
17
+ /** The seat that establishes a node's role in an analogical comparison:
18
+ * the REVERSE context (what leads to it) when a predecessor genuinely
19
+ * ESTABLISHES id — introduces or describes it by name — else the FORWARD
20
+ * continuation (what it leads to), else `fallback`.
21
+ *
22
+ * An earlier version gated this purely on `prevCount(id) > 0`: any
23
+ * predecessor at all was treated as proof of a genuine named ENTITY
24
+ * (seat it by what established it), while no predecessor meant a bare
25
+ * learnt CONTEXT (seat it by what it leads to, since voicing it verbatim
26
+ * would answer a question with a question). That test measured the wrong
27
+ * thing — a broad sample of this store's own question-shaped nodes showed
28
+ * the large majority (≈71%) have at least one predecessor, most of them a
29
+ * handful of generic, high-fan-out sentences that recur as an INCIDENTAL
30
+ * neighbour to dozens of otherwise-unrelated destinations (a SmolSent-
31
+ * style sentence-adjacency artifact, never naming or describing what
32
+ * follows). Traced live: "What is the capital of France?" — whose own
33
+ * forward edge unambiguously resolves to "The capital of France is
34
+ * Paris." — has exactly one such incidental predecessor ("Create an
35
+ * example of a types of questions a GPT model can answer.?"), wrongly
36
+ * read as disqualifying proof of "genuine entity."
37
+ *
38
+ * A plain forward-first swap (matching {@link project}'s universal
39
+ * priority) over-corrected: test/29's C2/C3 pin that a genuine entity
40
+ * analog (e.g. "Leonardo da Vinci", established by "The Mona Lisa was
41
+ * painted by Leonardo da Vinci.") must be seated by that establishing
42
+ * sentence, NOT by its own biography fact — voicing the bio leaks exactly
43
+ * what a comparison must keep out, and loses the embedded "Mona Lisa"
44
+ * term C3 relies on for a further hop.
45
+ *
46
+ * The distinguishing signal is content-addressed, not a count: a genuine
47
+ * establishing predecessor's bytes CONTAIN id's own bytes — it names or
48
+ * describes id ("...painted by Leonardo da Vinci." contains "Leonardo da
49
+ * Vinci"). An incidental adjacency predecessor never does — it merely
50
+ * preceded id in some unrelated document without ever mentioning it. No
51
+ * new tuned constant: containment is the same primitive `restatesQuery`
52
+ * and `dominates`-style checks already use throughout this codebase.
53
+ *
54
+ * `allowForward` (default true) gates the FORWARD branch specifically —
55
+ * see the call sites below: the DOMINANT is what the query is actually
56
+ * ASKING, so completing it forward is the whole point; an ANALOG is only
57
+ * being CITED for comparison; the query never asked about IT, so chasing
58
+ * its own further continuation drifts onto whatever coincidentally
59
+ * follows it in the corpus. Traced live: the analog "What is the capital
60
+ * of Japan?\nTokyo is the capital of Japan." is ALREADY a complete,
61
+ * self-answering unit (prevCount 0, so no establishing predecessor
62
+ * either) — its sole forward edge is "And what is the capital of the
63
+ * Moon?", an unrelated quiz question sharing nothing but corpus
64
+ * adjacency. With forward disallowed, an analog like this falls through
65
+ * to `fallback` — its own bytes, exactly the complete fact that made it a
66
+ * genuine analog in the first place. See
67
+ * test/41-seatofnode-direction.test.mjs and
68
+ * test/43-cast-analog-seat.test.mjs. */
69
+ export declare function seatOfNode(ctx: MindContext, id: number, guide: Vec | null | undefined, fallback: Uint8Array, allowForward?: boolean): Promise<Uint8Array>;
16
70
  /** CAST's own entry gates, checked once here and reused by
17
71
  /** The main CAST entry point. Given a query and its pre-computed pre.rec.sites,
18
72
  * determine whether the query weaves together multiple independent learnt
@@ -11,14 +11,16 @@
11
11
  // gate = the frame gate below + analogyStrength, projection = insert / project /
12
12
  // juxtapose.
13
13
  import { read } from "../primitives.js";
14
- import { argmaxBy, hubBound } from "../traverse.js";
14
+ import { argmaxBy, corpusN, hubBound } from "../traverse.js";
15
15
  import { analogyStrength, follow, project, reverseContext, } from "../match.js";
16
16
  import { joinWithBridge } from "../resonance.js";
17
+ import { restatesQuery } from "../reasoning.js";
17
18
  import { CONCEPT, STEP } from "../graph-search.js";
18
19
  import { concat2, indexOf } from "../../bytes.js";
19
- import { dominates } from "../../geometry.js";
20
- import { unexplainedLabel } from "../rationale.js";
20
+ import { consensusFloor, dominates } from "../../geometry.js";
21
+ import { unexplainedLabel, unexplainedSpans, } from "../rationale.js";
21
22
  import { rItem, rNode } from "../trace.js";
23
+ import { dismissedKnownContent } from "../bridge.js";
22
24
  // ── CAST gates ────────────────────────────────────────────────────────────
23
25
  //
24
26
  // The frame gate has TWO components, both derived from the weave itself:
@@ -52,6 +54,81 @@ import { rItem, rNode } from "../trace.js";
52
54
  * minimum: `depth > MIN_WEAVE` means at least three structures agree on a
53
55
  * byte, so no byte is frame when only the minimum pair exists. */
54
56
  const MIN_WEAVE = 2;
57
+ /** The seat that establishes a node's role in an analogical comparison:
58
+ * the REVERSE context (what leads to it) when a predecessor genuinely
59
+ * ESTABLISHES id — introduces or describes it by name — else the FORWARD
60
+ * continuation (what it leads to), else `fallback`.
61
+ *
62
+ * An earlier version gated this purely on `prevCount(id) > 0`: any
63
+ * predecessor at all was treated as proof of a genuine named ENTITY
64
+ * (seat it by what established it), while no predecessor meant a bare
65
+ * learnt CONTEXT (seat it by what it leads to, since voicing it verbatim
66
+ * would answer a question with a question). That test measured the wrong
67
+ * thing — a broad sample of this store's own question-shaped nodes showed
68
+ * the large majority (≈71%) have at least one predecessor, most of them a
69
+ * handful of generic, high-fan-out sentences that recur as an INCIDENTAL
70
+ * neighbour to dozens of otherwise-unrelated destinations (a SmolSent-
71
+ * style sentence-adjacency artifact, never naming or describing what
72
+ * follows). Traced live: "What is the capital of France?" — whose own
73
+ * forward edge unambiguously resolves to "The capital of France is
74
+ * Paris." — has exactly one such incidental predecessor ("Create an
75
+ * example of a types of questions a GPT model can answer.?"), wrongly
76
+ * read as disqualifying proof of "genuine entity."
77
+ *
78
+ * A plain forward-first swap (matching {@link project}'s universal
79
+ * priority) over-corrected: test/29's C2/C3 pin that a genuine entity
80
+ * analog (e.g. "Leonardo da Vinci", established by "The Mona Lisa was
81
+ * painted by Leonardo da Vinci.") must be seated by that establishing
82
+ * sentence, NOT by its own biography fact — voicing the bio leaks exactly
83
+ * what a comparison must keep out, and loses the embedded "Mona Lisa"
84
+ * term C3 relies on for a further hop.
85
+ *
86
+ * The distinguishing signal is content-addressed, not a count: a genuine
87
+ * establishing predecessor's bytes CONTAIN id's own bytes — it names or
88
+ * describes id ("...painted by Leonardo da Vinci." contains "Leonardo da
89
+ * Vinci"). An incidental adjacency predecessor never does — it merely
90
+ * preceded id in some unrelated document without ever mentioning it. No
91
+ * new tuned constant: containment is the same primitive `restatesQuery`
92
+ * and `dominates`-style checks already use throughout this codebase.
93
+ *
94
+ * `allowForward` (default true) gates the FORWARD branch specifically —
95
+ * see the call sites below: the DOMINANT is what the query is actually
96
+ * ASKING, so completing it forward is the whole point; an ANALOG is only
97
+ * being CITED for comparison; the query never asked about IT, so chasing
98
+ * its own further continuation drifts onto whatever coincidentally
99
+ * follows it in the corpus. Traced live: the analog "What is the capital
100
+ * of Japan?\nTokyo is the capital of Japan." is ALREADY a complete,
101
+ * self-answering unit (prevCount 0, so no establishing predecessor
102
+ * either) — its sole forward edge is "And what is the capital of the
103
+ * Moon?", an unrelated quiz question sharing nothing but corpus
104
+ * adjacency. With forward disallowed, an analog like this falls through
105
+ * to `fallback` — its own bytes, exactly the complete fact that made it a
106
+ * genuine analog in the first place. See
107
+ * test/41-seatofnode-direction.test.mjs and
108
+ * test/43-cast-analog-seat.test.mjs. */
109
+ export async function seatOfNode(ctx, id, guide, fallback, allowForward = true) {
110
+ const rev = ctx.store.prevFirst(id, hubBound(ctx));
111
+ if (rev.length > 0) {
112
+ const own = read(ctx, id);
113
+ const establishing = rev.some((p) => indexOf(read(ctx, p), own, 0) >= 0);
114
+ if (establishing) {
115
+ const back = reverseContext(ctx, id, guide, rev);
116
+ if (back !== null)
117
+ return back;
118
+ }
119
+ }
120
+ // The "last resort, non-establishing reverse" fallback below is itself a
121
+ // LESS CERTAIN projection (the same tier as forward) — an analog
122
+ // (allowForward: false) must stop at `fallback` (its own bytes) here
123
+ // rather than fall back to a predecessor that already failed the
124
+ // establishing check just above.
125
+ if (!allowForward)
126
+ return fallback;
127
+ const fwd = await follow(ctx, id, guide);
128
+ if (fwd !== null)
129
+ return fwd;
130
+ return reverseContext(ctx, id, guide, rev) ?? fallback;
131
+ }
55
132
  /** CAST's own entry gates, checked once here and reused by
56
133
  /** The main CAST entry point. Given a query and its pre-computed pre.rec.sites,
57
134
  * determine whether the query weaves together multiple independent learnt
@@ -222,7 +299,8 @@ export async function counterfactualTransfer(ctx, query, pre) {
222
299
  const tail = proj.ctx.subarray(seat.cs);
223
300
  let answer = await joinWithBridge(ctx, filler, tail);
224
301
  const fwd = await follow(ctx, proj.anchor, qv);
225
- if (fwd !== null && indexOf(answer, fwd, 0) < 0) {
302
+ if (fwd !== null && indexOf(answer, fwd, 0) < 0 &&
303
+ !restatesQuery(query, fwd)) {
226
304
  answer = concat2(answer, fwd);
227
305
  }
228
306
  ctx.trace?.step("projectCounterfactual", [
@@ -278,30 +356,8 @@ export async function counterfactualTransfer(ctx, query, pre) {
278
356
  // seed-dependent failure where the climb ranks an exemplar above a
279
357
  // person node and the person node is excluded from points by run-
280
358
  // overlap trimming.
281
- // The seat that establishes a candidate's role: the REVERSE projection
282
- // (the context it follows), voiced by the query gist falling back to the
283
- // candidate's own bytes when it follows nothing. DELIBERATE STRENGTHENING
284
- // over the pre-refactor code: reverseContext also returns null when the
285
- // picked context READS EMPTY (a dangling id degrades to zero bytes), so a
286
- // corrupted-store read now falls back here instead of voicing a hollow
287
- // seat into the comparison — the same "empty bytes are no grounding"
288
- // invariant project() has always enforced.
289
- // A node that only ever CONTINUES — an edge SOURCE with no predecessor —
290
- // is a learnt CONTEXT (a stored question-shaped frame), not an entity.
291
- // Voicing it verbatim answers a question with a question (the observed
292
- // cast echo: two stored questions concatenated as an "answer"). The
293
- // context's role is established by what it LEADS TO, so its seat is its
294
- // own continuation — the fact — with the reverse projection and the raw
295
- // bytes as the ordinary fallbacks for genuine entities.
296
- const seatOfNode = async (id, fallback) => {
297
- if (ctx.store.prevCount(id) === 0 && ctx.store.hasNext(id)) {
298
- const fwd = await follow(ctx, id, qv);
299
- if (fwd !== null)
300
- return fwd;
301
- }
302
- return reverseContext(ctx, id, qv) ?? fallback;
303
- };
304
- const seatOf = (p) => seatOfNode(p.anchor, p.ctx);
359
+ // The seat that establishes a candidate's role see {@link seatOfNode}.
360
+ const seatOf = (p, allowForward = true) => seatOfNode(ctx, p.anchor, qv, p.ctx, allowForward);
305
361
  const analogs = [];
306
362
  for (const p of points) {
307
363
  if (p === dominant)
@@ -335,15 +391,49 @@ export async function counterfactualTransfer(ctx, query, pre) {
335
391
  }
336
392
  let bestAnalog = null;
337
393
  let bestSim = 0;
394
+ let bestHalo = false;
395
+ // Whether the query itself NAMES a candidate. A directly aligned point
396
+ // is named by construction — its runs ARE query bytes. A hop-reached
397
+ // candidate is named when its own bytes contain the query text of an
398
+ // aligned run of the point whose continuation edge reached it (that
399
+ // alignment IS the query evidence the hop rests on — the same reading
400
+ // cmpAccounted already prices): "William Shakespeare", reached off
401
+ // "Macbeth was written by William Shakespeare.", contains the src's
402
+ // 12-byte aligned run " Shakespeare" — test/29 C2/C3. The run must span
403
+ // at least TWO perception windows (2·W, the same two-quantum floor
404
+ // CAST's own entry gate holds the whole query to): a single shared
405
+ // W-window is exactly the frame tier's own evidence quantum — the level
406
+ // "half the corpus" shares — and stopword scraps (" the ", "he b",
407
+ // 4–5 bytes) never reach two windows, while a genuinely named entity
408
+ // does. NOT the weave's usable()/frame filter: weave depth counts every
409
+ // ranked exemplar, so a query's own named entity recurring across
410
+ // exemplars ("Shakespeare" in Hamlet+Macbeth+…) is wrongly classified as
411
+ // frame — measured live, it silently disqualified C3's genuine analog.
412
+ const namedByQuery = (c) => {
413
+ if (c.point !== null)
414
+ return true;
415
+ const bytes = read(ctx, c.anchor);
416
+ return c.src.runs.some((r) => r.qe - r.qs >= 2 * quantum &&
417
+ indexOf(bytes, query.subarray(r.qs, r.qe), 0) >= 0);
418
+ };
419
+ // Whether any committed root's consensus vote clears the SAME trust bar
420
+ // recallByResonance applies before grounding through a climb root:
421
+ // consensusFloor(N) = ln(N) + 1/2. The climb's FIRST root is
422
+ // deliberately floor-free (attention.ts: "the dominant one always
423
+ // grounds") — fine for ORIENTING mechanisms, not for voicing learnt
424
+ // content the query never asked about. Computed once here; both the
425
+ // hub fallback below and the comparison gate consume it.
426
+ const rootTrusted = roots.some((r) => r.vote >= consensusFloor(corpusN(ctx)));
338
427
  for (const c of analogs) {
339
- const sim = await analogyStrength(ctx, dominant.anchor, c.anchor);
428
+ const { score: sim, halo } = await analogyStrength(ctx, dominant.anchor, c.anchor);
340
429
  ctx.trace?.step("tryAnalog", [
341
430
  rNode(ctx, dominant.anchor, "dominant"),
342
431
  rNode(ctx, c.anchor, "candidate", sim),
343
- ], [], `analogy strength ${sim.toFixed(4)}`);
432
+ ], [], `analogy strength ${sim.toFixed(4)}${halo ? " (halo tier)" : ""}`);
344
433
  if (sim > bestSim) {
345
434
  bestSim = sim;
346
435
  bestAnalog = c;
436
+ bestHalo = halo;
347
437
  }
348
438
  }
349
439
  // When every candidate fails the similarity gates (halo company — now
@@ -372,6 +462,18 @@ export async function counterfactualTransfer(ctx, query, pre) {
372
462
  let hubMass = -1;
373
463
  const fanClamp = hubBound(ctx) + 1;
374
464
  for (const c of analogs) {
465
+ // A fallback comparison carries NO similarity evidence at all. Its
466
+ // honesty rests on the grounding decider discounting it against
467
+ // richer candidates (the design note below) — an assumption that
468
+ // holds only when the climb itself settled on this query with real
469
+ // evidence. Under a root the consensus floor does not trust, an
470
+ // unnamed, hop-reached hub is pure corpus adjacency: refusing it is
471
+ // what kept the live wrong echo silent. A hub the query itself
472
+ // NAMED stays eligible either way (test/29 C2/C3's "William
473
+ // Shakespeare"); an unnamed one under a TRUSTED root stays eligible
474
+ // too (test/33 1b's deliberately weak second candidate).
475
+ if (!rootTrusted && !namedByQuery(c))
476
+ continue;
375
477
  // Evidence clamped at the hub bound: beyond √N + 1 the exact fan-out
376
478
  // no longer discriminates (every mega-hub ties at the clamp), and
377
479
  // counting it exactly would require the corpus-sized read.
@@ -412,17 +514,130 @@ export async function counterfactualTransfer(ctx, query, pre) {
412
514
  // climb's own forest, never tuned; substitution/redirection stay
413
515
  // unaffected — they orient around a displaced seat, not a whole-topic
414
516
  // analogy.
517
+ //
518
+ // roots.length <= 1 is a PROXY for "the query is about one thing" — it is
519
+ // only as good as the climb's own root-commitment, which depends on
520
+ // recognise() having found something to commit a root TO. When the
521
+ // query's newest content genuinely isn't recognised (not boundary noise —
522
+ // real, uncommitted content; see the session's own investigation of the
523
+ // France→Spain live trace), the climb under-commits roots and this proxy
524
+ // is fooled: comparison looks licensed to treat the query as one topic
525
+ // when it is not.
526
+ //
527
+ // The direct check is the SAME accounted spans comparison is about to
528
+ // cite as its evidence: unexplainedSpans (rationale.ts, the same gap
529
+ // computation the trace's own `unexplained` diagnostic uses) names every
530
+ // stretch of the query NEITHER the dominant NOR the analog's evidence
531
+ // touches. A short comparison query ("How is ice like steel?") legitimately
532
+ // accounts for only its two short entity spans — the surrounding "How is
533
+ // ... like ...?" framing is real but SHORT, split into several small gaps,
534
+ // none of them the bulk of the query. The live bug's shape is different in
535
+ // kind, not degree: ONE contiguous, substantial gap — a whole second
536
+ // question the query added that comparison's two spans never touch at all.
537
+ //
538
+ // Two bars, both derived, neither tuned:
539
+ // • the largest gap must not DOMINATE the whole query (the same
540
+ // predicate CAST's own frame gate uses) — rules out a gap that is
541
+ // most of the query outright;
542
+ // • the largest gap must be SMALLER than the dominant's own established
543
+ // context. A gap can't be dismissed as mere connective framing once
544
+ // it is at least as large as the topic being compared FROM — at that
545
+ // scale it isn't glue between two named things, it's substantial
546
+ // enough to be a second topic in its own right. This is what
547
+ // actually separates the live bug (a 47-byte gap against a 30-byte
548
+ // dominant — the ignored content is bigger than the topic itself)
549
+ // from ordinary short comparisons (a 9-byte gap against an 11-byte
550
+ // dominant — the gap is smaller than what's being compared): the two
551
+ // cases land on the same side of "half the query" often enough
552
+ // (both can exceed or clear it) that the query-relative bar alone
553
+ // does not reliably separate them — the topic-relative scale does.
554
+ const cmpAccounted = bestAnalog !== null
555
+ ? [...runSpans(dominant), ...runSpans(bestAnalog.point ?? bestAnalog.src)]
556
+ : [];
557
+ const cmpGaps = unexplainedSpans(query.length, cmpAccounted);
558
+ const cmpMaxGap = cmpGaps.reduce((n, [s, e]) => Math.max(n, e - s), 0);
559
+ // An analog that is not itself a directly ALIGNED point (point !== null —
560
+ // its own runs are query bytes, the query NAMED it) was only reached
561
+ // through a continuation hop or the structural-hub fallback. Voicing
562
+ // learnt content the query never named is the same act recallByResonance
563
+ // refuses to perform through a climb root whose consensus vote is below
564
+ // consensusFloor(N) = ln(N) + 1/2 (recall.ts's minVote), so comparison
565
+ // holds the climb to that SAME bar before citing a hop-reached analog:
566
+ // some committed root must clear the floor. The climb's FIRST root is
567
+ // deliberately floor-free (attention.ts: "the dominant one always
568
+ // grounds") — fine for ORIENTING mechanisms, not for transferring
569
+ // unnamed content through. The live bug this gates (real trained store,
570
+ // 325k edge sources, floor 13.2): the query's stopword scraps pooled a
571
+ // 1.92 vote that committed an unrelated haiku exemplar as the sole root,
572
+ // and comparison voiced that exemplar's continuation through a
573
+ // hop-reached analog while every other mechanism honestly refused. A
574
+ // directly aligned analog needs no floor — the query's own bytes are its
575
+ // evidence (test/29 C1's "Steel is hard" for "How is ice like steel?").
576
+ // See test/50-cast-analog-consensus-floor.
577
+ // A HALO-tier best analog needs neither: its similarity already cleared
578
+ // significanceBar-gated distributional company (analogyStrength's
579
+ // `halo`) — genuine evidence in its own right, the very case the halo
580
+ // gate exists for (test/33 1b's nickname-corroborated analog). Only a
581
+ // FRAME-tier or fallback analog — whose "similarity" is an unbarred
582
+ // coverage fraction or nothing — needs the query's naming or the climb's
583
+ // trust.
584
+ const analogNamed = bestAnalog !== null && namedByQuery(bestAnalog);
585
+ // NOTE — two further gates were tried here and empirically REFUTED,
586
+ // recorded so they are not re-tried:
587
+ // • dominant self-coverage (dominant's aligned runs must dominate its
588
+ // own ctx): legitimate dominants sit at the same coverage as junk
589
+ // ones ("The Mona Lisa was painted by…" 16/47 vs the live junk
590
+ // haiku ~10/54) — no separation.
591
+ // • denying the shared-frame similarity tier to hop-reached analogs:
592
+ // semantically right in isolation, but it merely promoted the next
593
+ // junk candidate — an ALIGNED scrap-matched point ("The affluence…",
594
+ // frame 0.157) — into bestAnalog on the live store, and the aligned
595
+ // configuration is byte-structurally IDENTICAL to test/29 C1's
596
+ // legitimate one ("Steel is hard", frame 0.364): every derived
597
+ // local separator measured (run length, site overlap, frame
598
+ // query-containment, weave-usable classification) falls on the same
599
+ // side for both. Only corpus-scale consensus separates them, which
600
+ // is exactly what `rootTrusted` prices.
601
+ // FRAME-tier evidence under an UNTRUSTED root is comparison's weakest
602
+ // licence (an unbarred coverage fraction, a climb the consensus floor
603
+ // does not trust). There it is additionally held to the IGNORED-KNOWN
604
+ // principle (dismissedKnownContent, bridge.ts): the two analogs' aligned
605
+ // runs must account for every STORED window of the query. This is the
606
+ // byte-structural separator the refuted-gates note below could not find
607
+ // locally: a legitimate small-corpus comparison ("How is ice like
608
+ // steel?") leaves only UNATTESTED spans ("How ", " like ") unexplained,
609
+ // while a scrap-matched junk pair leaves the query's own trained content
610
+ // ("…songs…times…", "…planet…sun.") dismissed as gaps. Halo-tier and
611
+ // trusted-root comparisons are exempt — their evidence already stands.
612
+ const cmpDismisses = !(bestHalo || rootTrusted) &&
613
+ dismissedKnownContent(ctx, query, cmpAccounted);
415
614
  if (bestAnalog !== null &&
615
+ (bestHalo || analogNamed || rootTrusted) &&
616
+ !cmpDismisses &&
416
617
  dominant.ctx.length <= query.length &&
417
- roots.length <= 1) {
618
+ roots.length <= 1 &&
619
+ !dominates(cmpMaxGap, query.length) &&
620
+ cmpMaxGap < dominant.ctx.length) {
418
621
  ctx.trace?.step("validateAnalogy", [
419
622
  rNode(ctx, dominant.anchor, "analog", bestSim),
420
623
  rNode(ctx, bestAnalog.anchor, "analog", bestSim),
421
624
  ], [], "the two structures keep distributional company beyond chance — genuine analogs");
422
625
  const a = await seatOf(dominant);
626
+ // The analog is only being CITED for comparison — the query never asked
627
+ // about it — so its seat never chases a FORWARD continuation (see
628
+ // seatOfNode's `allowForward`): only reverse (if a predecessor genuinely
629
+ // establishes it) or its own bytes. A DIRECTLY aligned point
630
+ // (bestAnalog.point !== null) still goes through seatOfNode for that
631
+ // reverse check (a bare entity NAME like "Leonardo da Vinci" needs it —
632
+ // test/29's C2/C3). A nextOf DESCENDANT (point === null) was already
633
+ // reached by following ONE meaningful hop off another aligned point (the
634
+ // alignment loop above: "its nextOf is the hub... and the hub's own
635
+ // [...] context will be the seat") — its own bytes ARE that seat
636
+ // directly, with no predecessor to even check (it was found by a
637
+ // forward edge, not matched in the query).
423
638
  const b = bestAnalog.point !== null
424
- ? await seatOf(bestAnalog.point)
425
- : await seatOfNode(bestAnalog.anchor, read(ctx, bestAnalog.anchor));
639
+ ? await seatOf(bestAnalog.point, false)
640
+ : read(ctx, bestAnalog.anchor);
426
641
  const answer = await joinWithBridge(ctx, a, b);
427
642
  record(answer, "analogical comparison — each analog voiced by the context that establishes its role", new Set([dominant.anchor, bestAnalog.anchor]),
428
643
  // A halo-mediated act (the analogy gate) plus two seat projections.
@@ -432,10 +647,26 @@ export async function counterfactualTransfer(ctx, query, pre) {
432
647
  // when it was an aligned point, else the source point whose
433
648
  // continuation edge reached it (that alignment IS the query evidence
434
649
  // the hop rests on).
435
- [
436
- ...runSpans(dominant),
437
- ...runSpans(bestAnalog.point ?? bestAnalog.src),
438
- ]);
650
+ cmpAccounted);
651
+ }
652
+ else if (bestAnalog !== null &&
653
+ dominant.ctx.length <= query.length &&
654
+ roots.length <= 1) {
655
+ ctx.trace?.step("validateAnalogy", [
656
+ rNode(ctx, dominant.anchor, "analog", bestSim),
657
+ rNode(ctx, bestAnalog.anchor, "analog", bestSim),
658
+ ], [], !(bestHalo || analogNamed || rootTrusted)
659
+ ? `the best analog carries no halo-tier company evidence, was never ` +
660
+ `named by the query, and no committed root's consensus vote ` +
661
+ `clears the floor, so comparison refuses to voice it`
662
+ : cmpDismisses
663
+ ? `a frame-tier analog under an untrusted root dismisses stored ` +
664
+ `query content its alignment never accounted for — comparison ` +
665
+ `refuses to ignore what the store knows`
666
+ : `comparison's own accounted evidence leaves a ${cmpMaxGap}-byte gap in ` +
667
+ `a ${query.length}-byte query against a ${dominant.ctx.length}-byte ` +
668
+ `dominant — too large to be mere framing — so it refuses rather ` +
669
+ `than paper over it with an analog the query never asked about`);
439
670
  }
440
671
  t?.done(results.map((r) => rItem(r.bytes, "answer")), results.length > 0
441
672
  ? `${results.length} counterfactual schema(s) fired — the grounding decider weighs them`