@hviana/sema 0.4.1 → 0.4.3

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 (45) hide show
  1. package/dist/src/ingest-cache.js +4 -0
  2. package/dist/src/meter.d.ts +5 -0
  3. package/dist/src/meter.js +5 -0
  4. package/dist/src/mind/attention.js +19 -2
  5. package/dist/src/mind/bridge.js +265 -56
  6. package/dist/src/mind/junction.d.ts +7 -4
  7. package/dist/src/mind/junction.js +16 -5
  8. package/dist/src/mind/match.d.ts +15 -0
  9. package/dist/src/mind/match.js +92 -2
  10. package/dist/src/mind/mechanisms/cast.js +12 -1
  11. package/dist/src/mind/mechanisms/confluence.js +31 -1
  12. package/dist/src/mind/mechanisms/cover.d.ts +1 -1
  13. package/dist/src/mind/mechanisms/cover.js +29 -5
  14. package/dist/src/mind/mechanisms/recall.js +17 -41
  15. package/dist/src/mind/mind.d.ts +7 -0
  16. package/dist/src/mind/mind.js +25 -2
  17. package/dist/src/mind/pipeline-mechanism.d.ts +2 -2
  18. package/dist/src/mind/pipeline-mechanism.js +87 -4
  19. package/dist/src/mind/pipeline.js +1 -1
  20. package/dist/src/mind/reasoning.js +19 -11
  21. package/dist/src/mind/recognition.js +41 -0
  22. package/dist/src/mind/resonance.js +0 -0
  23. package/dist/src/mind/traverse.d.ts +3 -1
  24. package/dist/src/mind/traverse.js +14 -13
  25. package/dist/src/mind/types.d.ts +10 -0
  26. package/package.json +1 -1
  27. package/src/ingest-cache.ts +4 -0
  28. package/src/meter.ts +5 -0
  29. package/src/mind/attention.ts +18 -1
  30. package/src/mind/bridge.ts +292 -54
  31. package/src/mind/junction.ts +21 -7
  32. package/src/mind/match.ts +92 -1
  33. package/src/mind/mechanisms/cast.ts +12 -0
  34. package/src/mind/mechanisms/confluence.ts +30 -1
  35. package/src/mind/mechanisms/cover.ts +36 -4
  36. package/src/mind/mechanisms/recall.ts +21 -44
  37. package/src/mind/mind.ts +39 -2
  38. package/src/mind/pipeline-mechanism.ts +86 -4
  39. package/src/mind/pipeline.ts +1 -1
  40. package/src/mind/reasoning.ts +15 -8
  41. package/src/mind/recognition.ts +40 -0
  42. package/src/mind/resonance.ts +0 -0
  43. package/src/mind/traverse.ts +17 -15
  44. package/src/mind/types.ts +10 -0
  45. package/test/49-natural-units-synonym-bridge.test.mjs +56 -15
@@ -53,15 +53,24 @@ export function junctionSeeds(ctx, b) {
53
53
  return [wids[0], wids[wids.length - 1]];
54
54
  }
55
55
  const walkCaches = new WeakMap();
56
+ const WALK_CACHE_MAX = 100_000;
56
57
  export function walkCache(ctx) {
57
58
  if (ctx.climbMemo === null)
58
59
  return null;
59
- let c = walkCaches.get(ctx.climbMemo);
60
+ let c = walkCaches.get(ctx._structMemoKey);
60
61
  if (c === undefined) {
61
- walkCaches.set(ctx.climbMemo, c = { reads: new Map(), parents: new Map(), containers: new Map() });
62
+ walkCaches.set(ctx._structMemoKey, c = { reads: new Map(), parents: new Map(), containers: new Map() });
63
+ }
64
+ else if (c.reads.size + c.parents.size + c.containers.size >= WALK_CACHE_MAX) {
65
+ c.reads.clear();
66
+ c.parents.clear();
67
+ c.containers.clear();
62
68
  }
63
69
  return c;
64
70
  }
71
+ export function invalidateJunctionCache(ctx) {
72
+ walkCaches.delete(ctx._structMemoKey);
73
+ }
65
74
  export function cachedRead(ctx, cache, id, cap) {
66
75
  if (cache === null)
67
76
  return read(ctx, id, cap + 1);
@@ -253,13 +262,14 @@ export function junctionContainers(ctx, left, right, maxContainer, unordered = f
253
262
  * cost is bounded at √N·W pops total regardless of how many siblings are
254
263
  * tried. A sibling whose bytes exceed `maxInterior` is skipped (it
255
264
  * cannot be junction-sized). */
256
- export async function junctionSynonyms(ctx, left, right, maxInterior, unordered = false, sides) {
265
+ export async function junctionSynonyms(ctx, left, right, maxInterior, unordered = false, sides, sharedBudget) {
257
266
  const s = sides ?? await loadJunctionSynonymSides(ctx, left, right);
258
267
  if (s.leftId === null && s.rightId === null)
259
268
  return [];
260
269
  // ── Tier 2.5a: single-synonym — one side replaced by a halo sibling ──────
261
270
  // ONE shared expansion budget across BOTH directions of this tier.
262
- const singleBudget = { n: hubBound(ctx) * ctx.space.maxGroup };
271
+ const singleBudget = sharedBudget ??
272
+ { n: hubBound(ctx) * ctx.space.maxGroup };
263
273
  const singleOut = new Map();
264
274
  const keepBest = (map, j, tier, confidence) => {
265
275
  const prev = map.get(j.id);
@@ -313,7 +323,8 @@ export async function junctionSynonyms(ctx, left, right, maxInterior, unordered
313
323
  a.l.id - b.l.id ||
314
324
  a.r.id - b.r.id);
315
325
  const doubleOut = new Map();
316
- const budget = { n: hubBound(ctx) * ctx.space.maxGroup };
326
+ const budget = sharedBudget ??
327
+ { n: hubBound(ctx) * ctx.space.maxGroup };
317
328
  const tries = Math.min(pairs.length, ctx.cfg.haloQueryK);
318
329
  for (let i = 0; i < tries; i++) {
319
330
  const { l, r, confidence } = pairs[i];
@@ -67,6 +67,21 @@ export declare function bestHaloMate<T>(ctx: MindContext, halo: Vec, items: Iter
67
67
  score: number;
68
68
  } | null;
69
69
  export declare function haloSiblings(ctx: MindContext, id: number, halo?: Vec | null, bar?: number): Promise<Hit[]>;
70
+ /** Bundle the distributional company of every addressable W-window in a
71
+ * byte span. This is the query-time counterpart of the write-side halo
72
+ * pours: no lexical unit or storage row is invented; the span is represented
73
+ * by VSA superposition of the window concepts the store already knows.
74
+ *
75
+ * Components are normalized before bundling so repetition mass remains
76
+ * evidence about each stored node, not an accidental weight on one window
77
+ * inside the composed phrase. Returns null when the corpus provides no
78
+ * distributional evidence for the span. */
79
+ export declare function spanHalo(ctx: MindContext, bytes: Uint8Array, from?: number, to?: number): Vec | null;
80
+ /** Distributional synonym evidence between arbitrary byte spans. Whole words
81
+ * need not be independently interned: their stored W-window occurrences are
82
+ * lifted to episode halos, bundled, and compared. The caller chooses the
83
+ * derived gate appropriate to its claim (concept identity or analogy). */
84
+ export declare function spanSynonymStrength(ctx: MindContext, a: Uint8Array, b: Uint8Array): number;
70
85
  /** The DISTRIBUTIONAL matcher between two nodes: mutual-nearest-neighbour
71
86
  * strength, not a pick. Returns the direct halo cosine, or failing that the
72
87
  * highest mutual-halo-sibling min-score (second-order analogy), or failing
@@ -24,12 +24,12 @@
24
24
  // the PROJECTIONS (follow, conceptHop, reverseContext, project) — so each
25
25
  // mechanism file states only its configuration, never its own copy of the
26
26
  // machinery. The gates all live in geometry.ts (derived, never tuned).
27
- import { cosine } from "../vec.js";
27
+ import { addInto, cosine, dot, normalize, zeros } from "../vec.js";
28
28
  import { conceptThreshold, identityBar, significanceBar } from "../geometry.js";
29
29
  import { indexOf } from "../bytes.js";
30
30
  import { leafIdRun } from "./canonical.js";
31
31
  import { foldTree, gistOf, perceive, read, resolve } from "./primitives.js";
32
- import { argmaxCosine, chooseAmong, chooseNext, guidedFirst, hubBound, hubCap, } from "./traverse.js";
32
+ import { argmaxCosine, chooseAmong, chooseNext, corpusN, edgeAncestors, guidedFirst, hubBound, hubCap, sharedReachMemo, } from "./traverse.js";
33
33
  import { recognise, segment } from "./recognition.js";
34
34
  // ═══════════════════════════════════════════════════════════════════════════
35
35
  // MATCHERS — locating learned structure in/against bytes, by graded strictness
@@ -276,6 +276,96 @@ export async function haloSiblings(ctx, id, halo, bar = conceptThreshold(ctx.sto
276
276
  memo.set(id, out);
277
277
  return out;
278
278
  }
279
+ /** Bundle the distributional company of every addressable W-window in a
280
+ * byte span. This is the query-time counterpart of the write-side halo
281
+ * pours: no lexical unit or storage row is invented; the span is represented
282
+ * by VSA superposition of the window concepts the store already knows.
283
+ *
284
+ * Components are normalized before bundling so repetition mass remains
285
+ * evidence about each stored node, not an accidental weight on one window
286
+ * inside the composed phrase. Returns null when the corpus provides no
287
+ * distributional evidence for the span. */
288
+ export function spanHalo(ctx, bytes, from = 0, to = bytes.length) {
289
+ const W = ctx.space.maxGroup;
290
+ if (to - from < W)
291
+ return null;
292
+ if (ctx.meter)
293
+ ctx.meter.spanHalos++;
294
+ const out = zeros(ctx.store.D);
295
+ let found = false;
296
+ const added = new Set();
297
+ const episodeRoots = [];
298
+ const N = corpusN(ctx);
299
+ const reachMemo = sharedReachMemo(ctx);
300
+ const addHalo = (id) => {
301
+ if (added.has(id))
302
+ return;
303
+ const halo = ctx.store.halo(id);
304
+ if (halo === null)
305
+ return;
306
+ const norm = Math.sqrt(dot(halo, halo));
307
+ if (norm === 0)
308
+ return;
309
+ added.add(id);
310
+ addInto(out, halo, 1 / norm);
311
+ found = true;
312
+ };
313
+ const windowCount = to - from - W + 1;
314
+ const offsets = [];
315
+ const samples = Math.min(W, windowCount);
316
+ for (let i = 0; i < samples; i++) {
317
+ const relative = samples === 1
318
+ ? 0
319
+ : Math.floor((i * (windowCount - 1)) / (samples - 1));
320
+ const off = from + relative;
321
+ if (offsets[offsets.length - 1] !== off)
322
+ offsets.push(off);
323
+ }
324
+ for (const off of offsets) {
325
+ if (ctx.meter)
326
+ ctx.meter.spanHaloWindows++;
327
+ const ids = leafIdRun(ctx, bytes, off, off + W);
328
+ if (ids === null)
329
+ continue;
330
+ const id = ctx.store.findBranch(ids);
331
+ if (id === null)
332
+ continue;
333
+ addHalo(id);
334
+ // Canonical flat windows are retrieval addresses and normally carry no
335
+ // halo themselves. Their bounded structural ascent reaches the learned
336
+ // episode forms that contain them; bundling those forms' company is the
337
+ // distributional meaning of the window, derived entirely from existing
338
+ // containment and halo state.
339
+ if (!added.has(id)) {
340
+ episodeRoots.push(edgeAncestors(ctx, id, N, reachMemo).roots);
341
+ }
342
+ }
343
+ for (let rank = 0; added.size < ctx.cfg.haloQueryK; rank++) {
344
+ let any = false;
345
+ for (const roots of episodeRoots) {
346
+ if (rank >= roots.length)
347
+ continue;
348
+ any = true;
349
+ addHalo(roots[rank]);
350
+ if (added.size >= ctx.cfg.haloQueryK)
351
+ break;
352
+ }
353
+ if (!any)
354
+ break;
355
+ }
356
+ return found ? normalize(out) : null;
357
+ }
358
+ /** Distributional synonym evidence between arbitrary byte spans. Whole words
359
+ * need not be independently interned: their stored W-window occurrences are
360
+ * lifted to episode halos, bundled, and compared. The caller chooses the
361
+ * derived gate appropriate to its claim (concept identity or analogy). */
362
+ export function spanSynonymStrength(ctx, a, b) {
363
+ const ah = spanHalo(ctx, a);
364
+ const bh = spanHalo(ctx, b);
365
+ if (ah === null || bh === null)
366
+ return 0;
367
+ return cosine(ah, bh);
368
+ }
279
369
  export async function analogyStrength(ctx, a, b) {
280
370
  const ha = ctx.store.halo(a);
281
371
  const hb = ctx.store.halo(b);
@@ -221,7 +221,18 @@ export async function counterfactualTransfer(ctx, query, pre) {
221
221
  ...roots.map((r) => rNode(ctx, r.anchor, "committed-root")),
222
222
  ], `${points.length} aligned structure(s), but none is one of the climb's ` +
223
223
  `${roots.length} committed root(s) — CAST refuses to transfer through ` +
224
- `content the climb itself never settled on`);
224
+ `content the climb itself never settled on`, {
225
+ aligned: points.map((p) => ({
226
+ anchor: p.anchor,
227
+ vote: p.vote,
228
+ runs: p.runs.map((r) => ({ ...r })),
229
+ coveredBytes: p.runs.reduce((n, r) => n + r.qe - r.qs, 0),
230
+ })),
231
+ committedRoots: roots.map((r) => ({
232
+ anchor: r.anchor,
233
+ vote: r.vote,
234
+ })),
235
+ });
225
236
  return [];
226
237
  }
227
238
  const woven = points.some((p) => p.runs.some((r) => !pre.rec.sites.some((s) => r.qs >= s.start && r.qe <= s.end)));
@@ -68,7 +68,23 @@ export async function confluenceJoin(ctx, query, pre) {
68
68
  // with the query is the CONSTRAINT being re-named (or its scaffolding),
69
69
  // never the open seat the question asks for — subtracted by identity,
70
70
  // below.
71
- const queryWin = pre.queryWindows;
71
+ // Completed assistant turns are context the engine already produced, not
72
+ // independent constraints asserted by the asker. Treating their windows as
73
+ // fresh constraints makes a cumulative dialogue's confluence work grow with
74
+ // every prior answer and can join the engine's own prose back into a reply.
75
+ // Recognition and attention still see the full transcript; only this
76
+ // mechanism's constraint population excludes answered spans.
77
+ const queryWin = new Map();
78
+ let answered = 0;
79
+ for (const [off, id] of pre.queryWindows) {
80
+ while (answered < ctx.answeredSpans.length &&
81
+ ctx.answeredSpans[answered][1] <= off)
82
+ answered++;
83
+ const span = ctx.answeredSpans[answered];
84
+ if (span && span[0] <= off && off + W <= span[1])
85
+ continue;
86
+ queryWin.set(off, id);
87
+ }
72
88
  const queryIds = new Set(queryWin.values());
73
89
  // A constraint must bind a CONSTITUENT, not a shard. A genuinely shared
74
90
  // form weaves a contiguous RUN of shared discriminative windows — its
@@ -84,6 +100,17 @@ export async function confluenceJoin(ctx, query, pre) {
84
100
  const bindsAConstituent = (cover) => cover.some(([cs, ce]) => ce - cs >= 2 * W);
85
101
  const streams = [];
86
102
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
103
+ // CONJUNCTIVITY EARLY-EXIT: a conjunctive query's top-ranked anchors
104
+ // (largest vote weight) must already form at least two independent
105
+ // constraint streams. When the first W anchors yield fewer than 2, the
106
+ // query has at most one topic — no join to compute. The full pre.k scan
107
+ // would produce the same null result after reading every anchor's bytes
108
+ // and computing window identities (profiled at 18K–50K leaf lookups per
109
+ // refusing query), so cutting the scan short here saves 50–70% of
110
+ // confluence cost on non-conjunctive queries while preserving every
111
+ // genuinely conjunctive case (whose top anchors ARE its constraints).
112
+ const earlyExit = Math.min(rankedCapped.length, ctx.space.maxGroup * 2);
113
+ let exitAfter = earlyExit;
87
114
  for (const cand of rankedCapped) {
88
115
  if (streams.some((s) => s.anchor === cand.anchor))
89
116
  continue;
@@ -111,6 +138,9 @@ export async function confluenceJoin(ctx, query, pre) {
111
138
  if (cover.length > 0 && bindsAConstituent(cover)) {
112
139
  streams.push({ anchor: cand.anchor, vote: cand.vote, ids, cover, held });
113
140
  }
141
+ // Early-exit: after 2W anchors, a non-conjunctive query is decided.
142
+ if (--exitAfter <= 0 && streams.length < 2)
143
+ return null;
114
144
  }
115
145
  if (streams.length < 2)
116
146
  return null;
@@ -2,5 +2,5 @@ import type { MindContext } from "../types.js";
2
2
  import type { Site } from "../graph-search.js";
3
3
  import type { PipelineMechanism } from "../pipeline-mechanism.js";
4
4
  export declare function resolveConcepts(ctx: MindContext, sites: Site[]): Promise<Map<number, number>>;
5
- export declare function resolveConnectors(ctx: MindContext, sites: ReadonlyArray<Site>): Promise<Map<string, Uint8Array>>;
5
+ export declare function resolveConnectors(ctx: MindContext, sites: ReadonlyArray<Site>, query?: Uint8Array): Promise<Map<string, Uint8Array>>;
6
6
  export declare const coverMechanism: PipelineMechanism;
@@ -9,11 +9,12 @@
9
9
  // near-zero-cost incumbent that prunes the other mechanisms through the
10
10
  // ordinary admissible-floor check, with no extension special-case anywhere.
11
11
  import { read, resolve } from "../primitives.js";
12
- import { guidedFirst } from "../traverse.js";
12
+ import { guidedFirst, hubBound } from "../traverse.js";
13
13
  import { conceptHop } from "../match.js";
14
14
  import { bridge } from "../resonance.js";
15
15
  import { liftAnswer, segRestatesQuery } from "../types.js";
16
16
  import { decodeText, unexplainedLabel } from "../rationale.js";
17
+ import { indexOf } from "../../bytes.js";
17
18
  import { rItem, rNode, traceDerivation } from "../trace.js";
18
19
  // ── Concept / connector pre-resolution ──────────────────────────────────────
19
20
  export async function resolveConcepts(ctx, sites) {
@@ -34,10 +35,29 @@ export async function resolveConcepts(ctx, sites) {
34
35
  }
35
36
  return target;
36
37
  }
37
- export async function resolveConnectors(ctx, sites) {
38
+ export async function resolveConnectors(ctx, sites, query) {
38
39
  const links = new Map();
39
- const ordered = [...sites].sort((a, b) => a.start - b.start);
40
40
  const answerOf = (n) => guidedFirst(ctx, n) ?? n;
41
+ // A site's continuation already present elsewhere in the query is stale
42
+ // transcript evidence: cover still needs the site for structural context,
43
+ // but liftAnswer will trim that continuation as already answered. Building
44
+ // pairwise/n-ary bridges for it can only create connectors that are later
45
+ // discarded, and on cumulative dialogue that dominated the whole search.
46
+ let answered = 0;
47
+ const ordered = [...sites]
48
+ .sort((a, b) => a.start - b.start)
49
+ .filter((s) => {
50
+ while (answered < ctx.answeredSpans.length &&
51
+ ctx.answeredSpans[answered][1] <= s.start)
52
+ answered++;
53
+ const span = ctx.answeredSpans[answered];
54
+ if (span && span[0] <= s.start && s.end <= span[1])
55
+ return false;
56
+ if (query === undefined || ctx.answeredSpans.length === 0)
57
+ return true;
58
+ const continuations = ctx.store.nextFirst(s.payload, hubBound(ctx));
59
+ return !continuations.some((answer) => indexOf(query, read(ctx, answer), 0) >= 0);
60
+ });
41
61
  const bridgePair = async (l, r) => {
42
62
  if (l === r || links.has(l + "," + r))
43
63
  return;
@@ -119,7 +139,9 @@ export const coverMechanism = {
119
139
  }
120
140
  if (sites.length === 0 && computed.length === 0)
121
141
  return [];
122
- const connectors = await resolveConnectors(ctx, sites);
142
+ const connectors = ctx.meter
143
+ ? await ctx.meter.time("cover.resolveConnectors", () => resolveConnectors(ctx, sites, query))
144
+ : await resolveConnectors(ctx, sites, query);
123
145
  let splits = rec.splits;
124
146
  let starts = rec.starts;
125
147
  if (computed.length > 0) {
@@ -135,7 +157,9 @@ export const coverMechanism = {
135
157
  starts.add(u.j);
136
158
  }
137
159
  }
138
- const concepts = await resolveConcepts(ctx, sites);
160
+ const concepts = ctx.meter
161
+ ? await ctx.meter.time("cover.resolveConcepts", () => resolveConcepts(ctx, sites))
162
+ : await resolveConcepts(ctx, sites);
139
163
  const coverDeps = [
140
164
  ctx.trace?.lastIndex("recognise"),
141
165
  ctx.trace?.lastIndex("computeExtensions"),
@@ -228,48 +228,24 @@ export async function recallByResonance(ctx, query, pre) {
228
228
  // refusing on the reach bar). Approximate scores propose; the bridge's
229
229
  // byte-exact alignment and attestation gates decide.
230
230
  //
231
- // The proposal breadth here is widened PAST `k` — first by requesting
232
- // hubBound(ctx) candidates instead of `k` (recall's own tiers above
233
- // stay at `k`; this re-resonates only on the refusal path, exactly
234
- // where the bridge itself already runs), AND by asking the index to
235
- // search EXHAUSTIVELY. Both matter: the IVF only ever probes
236
- // ⌈√clusters⌉ of them (store.ts's efFor) REGARDLESS of k — widening k
237
- // alone just returns more hits from the SAME already-probed clusters,
238
- // never a hit whose vector lives in an unprobed one. Measured live:
239
- // "What is the chemical symbol for water?" needs "What is the
240
- // chemical formula for water?", scoring only 0.58 against the
241
- // query's gist (a MIDDLE-of-string word swap perturbs the river-fold
242
- // tree hash far more than a same-length TAIL swap like the "carbon"/
243
- // "oxygen" neighbours that outrank it at 0.87+) — absent from the
244
- // resonance list even at k=5000, present and byte-exact-verified the
245
- // moment it's force-fed to the bridge directly. `exhaustive` is the
246
- // natural, tuning-free ceiling (probe every cluster) for a call that
247
- // is ALREADY refusal-path-only and must not miss a candidate hiding
248
- // behind an unlucky structural distance.
249
- //
250
- // MEASURED COST, AND WHY IT STAYS (17.9M vectors / 325K contexts):
251
- // this one call is ~570 ms and ~45% of all inference time on a refusing
252
- // query. The cost is entirely `exhaustive` (nprobe = every cluster),
253
- // NOT the widened k — timed on that store: k=571 exhaustive 632 ms,
254
- // k=24 exhaustive 536 ms, k=571 NON-exhaustive 12 ms. So narrowing k
255
- // buys nothing and the 50x is the whole-index scan itself.
256
- // It is load-bearing: over an 18-query battery the bridge produced a
257
- // winner 4 times, and ALL FOUR winners came from this proposal channel
258
- // — the anchor-climb channel won nothing on its own. Reordering the
259
- // channels (climb first, resonate only on failure) would therefore pay
260
- // the climb, fail, and pay this anyway. Do not weaken it without
261
- // re-running that measurement.
262
- // Handed to the bridge as a THUNK: this exhaustive whole-index probe is
263
- // the most expensive single act on the refusal path, and the bridge's
264
- // own cheap gates (query length, the O(|query|) stored-window anchor
265
- // scan) can refuse without any proposal at all. See substitutionBridge.
231
+ // Reuse recall's already-ranked proposals. Never scan every IVF cluster:
232
+ // exact co-occurrence and bounded anchor ascent are the bridge's structural
233
+ // proposal channels, while an exhaustive ANN call made every honest
234
+ // refusal cost hundreds of milliseconds regardless of k.
266
235
  const wideIds = async () => {
267
- const wide = k >= hubBound(ctx)
268
- ? whole
269
- : ctx.meter
270
- ? await ctx.meter.time("recall.exhaustiveResonate", () => ctx.store.resonate(queryGist, hubBound(ctx), true))
271
- : await ctx.store.resonate(queryGist, hubBound(ctx), true);
272
- return wide.map((h) => h.id);
236
+ // When the top resonance hit is below the concept threshold, the query
237
+ // gist has no concept-level match to any stored form — an exhaustive √N
238
+ // ANN would only score more vectors below the bar (profiled at 38K–40K
239
+ // annVectorReads per refusing query on a 325K-context store). The
240
+ // bridge's structural channels (junction walks, anchor climbs) are the
241
+ // correct proposal source for a query whose gist has no clean match;
242
+ // the ANN cannot propose what the gist cannot rank.
243
+ const marketScale = k * ctx.space.maxGroup;
244
+ if (corpusN(ctx) <= marketScale ** 3) {
245
+ const exhaustive = await ctx.store.resonate(queryGist, hubBound(ctx), true);
246
+ return exhaustive.map((h) => h.id);
247
+ }
248
+ return whole.map((h) => h.id);
273
249
  };
274
250
  const bridged = await substitutionBridge(ctx, query, wideIds);
275
251
  if (bridged !== null) {
@@ -34,6 +34,10 @@ export interface ConversationState {
34
34
  * is `boundaries[0]`; the second turn starts at that offset, and so
35
35
  * on. Empty for a single-turn or new conversation. */
36
36
  boundaries: number[];
37
+ /** Byte spans occupied by replies produced by this Mind. Unlike boundary
38
+ * parity, this remains exact when a turn receives an empty reply. Optional
39
+ * so states saved before the field existed remain restorable. */
40
+ answeredSpans?: Array<[number, number]>;
37
41
  }
38
42
  /** An active conversation handle. Opaque — interact through the Mind's
39
43
  * conversation methods ({@link Mind.beginConversation},
@@ -111,6 +115,7 @@ export declare class Mind implements MindContext {
111
115
  lastCost: CostReport | null;
112
116
  /** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
113
117
  climbMemo: Map<string, Map<string, AttentionRead>> | null;
118
+ _structMemoKey: object;
114
119
  /** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
115
120
  recogniseMemo: Map<string, Recognition> | null;
116
121
  /** Memo of perceive() — content-keyed. See {@link MindContext.perceiveMemo}. */
@@ -120,6 +125,8 @@ export declare class Mind implements MindContext {
120
125
  id: number;
121
126
  len: number;
122
127
  }> | null;
128
+ answeredSpans: ReadonlyArray<readonly [number, number]>;
129
+ currentTurnStart: number;
123
130
  /** The perceived gist of the query currently being answered. Set by `think`
124
131
  * before the graph search runs; `chooseNext` consults it as a gate (a null
125
132
  * guide means no query is in flight, so structural walkers keep plain
@@ -20,7 +20,8 @@ import { GraphSearch, } from "./graph-search.js";
20
20
  import { Alu } from "../alu/src/index.js";
21
21
  import { decodeText, Rationale, } from "./rationale.js";
22
22
  import { gistOf, inputBytes, latin1Key, perceive as perceiveImpl, resolve as resolveImpl, } from "./primitives.js";
23
- import { chooseNext, edgeAncestors as edgeAncestorsFn } from "./traverse.js";
23
+ import { chooseNext, edgeAncestors as edgeAncestorsFn, invalidateStructuralCaches, } from "./traverse.js";
24
+ import { invalidateJunctionCache } from "./junction.js";
24
25
  import { follow } from "./match.js";
25
26
  import { recognise, segment } from "./recognition.js";
26
27
  import { meaningOf } from "./resonance.js";
@@ -71,12 +72,15 @@ export class Mind {
71
72
  lastCost = null;
72
73
  /** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
73
74
  climbMemo = null;
75
+ _structMemoKey = {};
74
76
  /** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
75
77
  recogniseMemo = null;
76
78
  /** Memo of perceive() — content-keyed. See {@link MindContext.perceiveMemo}. */
77
79
  perceiveMemo = null;
78
80
  /** Subtree-resolution cache. See {@link MindContext._resolvedSubtrees}. */
79
81
  _resolvedSubtrees = null;
82
+ answeredSpans = [];
83
+ currentTurnStart = 0;
80
84
  /** The perceived gist of the query currently being answered. Set by `think`
81
85
  * before the graph search runs; `chooseNext` consults it as a gate (a null
82
86
  * guide means no query is in flight, so structural walkers keep plain
@@ -212,6 +216,11 @@ export class Mind {
212
216
  this.recogniseMemo = conv ? conv.recogniseMemo : new Map();
213
217
  this.perceiveMemo = conv ? conv.perceiveMemo : new Map();
214
218
  this._resolvedSubtrees = conv ? conv.resolvedSubtrees : null;
219
+ // Inference is a pure function of cumulative bytes. Conversation
220
+ // boundaries remain persistence/API metadata and must not select a
221
+ // different mechanism path than respond() on the identical byte stream.
222
+ this.answeredSpans = [];
223
+ this.currentTurnStart = 0;
215
224
  this.canon = canon ?? null;
216
225
  this.canonMemo = canon ? new Map() : null;
217
226
  this._beginMeter();
@@ -259,6 +268,8 @@ export class Mind {
259
268
  this.recogniseMemo = null;
260
269
  this.perceiveMemo = null;
261
270
  this._resolvedSubtrees = null;
271
+ this.answeredSpans = [];
272
+ this.currentTurnStart = 0;
262
273
  this.canon = null;
263
274
  this.canonMemo = null;
264
275
  this._edgeGuide = null;
@@ -335,11 +346,17 @@ export class Mind {
335
346
  const id = this._nextConvId++;
336
347
  const initBytes = state?.context ?? new Uint8Array(0);
337
348
  const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
349
+ const initAnswered = state?.answeredSpans
350
+ ? state.answeredSpans.map(([start, end]) => [start, end])
351
+ : initBoundaries.flatMap((start, i, cuts) => i % 2 === 0 && i + 1 < cuts.length
352
+ ? [[start, cuts[i + 1]]]
353
+ : []);
338
354
  const tree = bytesToTree(this.space, this.alphabet, initBytes, undefined, undefined, initBoundaries.length > 0 ? initBoundaries : undefined);
339
355
  this._conversations.set(id, {
340
356
  tree,
341
357
  bytes: initBytes,
342
358
  boundaries: initBoundaries,
359
+ answeredSpans: initAnswered,
343
360
  perceiveMemo: new Map(),
344
361
  recogniseMemo: new Map(),
345
362
  climbMemo: new Map(),
@@ -361,6 +378,7 @@ export class Mind {
361
378
  return {
362
379
  context: data.bytes,
363
380
  boundaries: [...data.boundaries],
381
+ answeredSpans: data.answeredSpans.map(([start, end]) => [start, end]),
364
382
  };
365
383
  }
366
384
  /** Append a turn to a conversation's accumulated context WITHOUT
@@ -447,8 +465,11 @@ export class Mind {
447
465
  // the cumulative continuous shape multi-turn training deposits, so a
448
466
  // later turn can refer to what was ANSWERED ("which of those two…"),
449
467
  // not only to what was asked.
450
- if (response.bytes.length > 0)
468
+ if (response.bytes.length > 0) {
469
+ const start = data.bytes.length;
451
470
  this.addTurn(conv, response.bytes);
471
+ data.answeredSpans.push([start, data.bytes.length]);
472
+ }
452
473
  return { response, state: this.conversationState(conv) };
453
474
  }
454
475
  finally {
@@ -487,6 +508,8 @@ export class Mind {
487
508
  * reports each ingested item's deposited root node ids
488
509
  * ({@link DepositReport}); purely observational. */
489
510
  async ingest(input, second, onDeposit) {
511
+ invalidateStructuralCaches(this);
512
+ invalidateJunctionCache(this);
490
513
  return ingest(this, input, second, onDeposit);
491
514
  }
492
515
  // ── Extension Surface ────────────────────────────────────────────────────
@@ -41,8 +41,8 @@ export declare class Precomputed {
41
41
  * serves every mechanism that prices commonality — AND the consensus
42
42
  * climb, which is the largest consumer and used to build its own. The
43
43
  * ONE definition of its lifetime lives in traverse.ts
44
- * ({@link sharedReachMemo}): response-scoped for respond(),
45
- * conversation-scoped across turns, always cold under a trace. */
44
+ * ({@link sharedReachMemo}): session-scoped between writes and always cold
45
+ * under a trace. */
46
46
  private _reach?;
47
47
  get reachMemo(): Map<number, AncestorReach>;
48
48
  /** Charge a lazily-shared analysis to its OWN phase rather than to the
@@ -94,8 +94,8 @@ export class Precomputed {
94
94
  * serves every mechanism that prices commonality — AND the consensus
95
95
  * climb, which is the largest consumer and used to build its own. The
96
96
  * ONE definition of its lifetime lives in traverse.ts
97
- * ({@link sharedReachMemo}): response-scoped for respond(),
98
- * conversation-scoped across turns, always cold under a trace. */
97
+ * ({@link sharedReachMemo}): session-scoped between writes and always cold
98
+ * under a trace. */
99
99
  _reach;
100
100
  get reachMemo() {
101
101
  return this._reach ??= sharedReachMemo(this.ctx);
@@ -185,7 +185,9 @@ function computeWeave(ctx, query, pre, climb) {
185
185
  // recognising) a corpus-sized deposit: profiled on a 17.7M-node store,
186
186
  // uncapped weaves spent 5–8s per query recognising conversation-length
187
187
  // anchors that could never form a weave point.
188
- const capBytes = query.length * quantum;
188
+ const askerBytes = query.length -
189
+ ctx.answeredSpans.reduce((n, [start, end]) => n + end - start, 0);
190
+ const capBytes = askerBytes * quantum;
189
191
  // EXCLUSIVITY IS ARBITRATED BY THE CLIMB'S VOTE ORDER, DELIBERATELY. A query
190
192
  // byte can only be independent evidence for ONE point, so points are built in
191
193
  // ranked order and each new point's runs are trimmed against every point
@@ -214,11 +216,92 @@ function computeWeave(ctx, query, pre, climb) {
214
216
  // frame reading depend on its rank, and the proposed-run gate below needs the
215
217
  // real thing.
216
218
  const cands = [];
219
+ const querySegments = [];
220
+ let segmentStart = 0;
221
+ for (const [start, end] of ctx.answeredSpans) {
222
+ if (segmentStart < start)
223
+ querySegments.push([segmentStart, start]);
224
+ segmentStart = Math.max(segmentStart, end);
225
+ }
226
+ if (segmentStart < query.length) {
227
+ querySegments.push([segmentStart, query.length]);
228
+ }
229
+ const weaveLength = querySegments.reduce((n, [s, e]) => n + e - s, 0);
230
+ const weaveQuery = new Uint8Array(weaveLength);
231
+ const weaveMap = [];
232
+ let compactStart = 0;
233
+ for (const [start, end] of querySegments) {
234
+ weaveQuery.set(query.subarray(start, end), compactStart);
235
+ weaveMap.push({
236
+ compactStart,
237
+ originalStart: start,
238
+ length: end - start,
239
+ });
240
+ compactStart += end - start;
241
+ }
242
+ const segmentOf = (start, end) => {
243
+ let lo = 0;
244
+ let hi = weaveMap.length;
245
+ while (lo < hi) {
246
+ const mid = (lo + hi) >>> 1;
247
+ if (weaveMap[mid].originalStart <= start)
248
+ lo = mid + 1;
249
+ else
250
+ hi = mid;
251
+ }
252
+ const part = lo > 0 ? weaveMap[lo - 1] : undefined;
253
+ return part && end <= part.originalStart + part.length ? part : undefined;
254
+ };
255
+ const weaveSites = pre.rec.sites.flatMap((s) => {
256
+ const part = segmentOf(s.start, s.end);
257
+ return part
258
+ ? [{
259
+ ...s,
260
+ start: part.compactStart + s.start - part.originalStart,
261
+ end: part.compactStart + s.end - part.originalStart,
262
+ }]
263
+ : [];
264
+ });
217
265
  for (const cand of rankedCapped) {
218
266
  const ctxBytes = read(ctx, cand.anchor, capBytes + 1);
219
267
  if (ctxBytes.length === 0 || ctxBytes.length > capBytes)
220
268
  continue;
221
- const raw = alignGraded(ctx, query, ctxBytes, pre.rec.sites);
269
+ // CAST compares structures stated by the asker. Completed assistant turns
270
+ // remain available to recognition and the climb as conversation context,
271
+ // but aligning every candidate across their full prose makes weave work
272
+ // grow with answer length and lets the engine analogise against its own
273
+ // previous output. The compact asker stream is aligned once (so candidate
274
+ // windows are not rebuilt per turn), then every run is split back across
275
+ // the original turn segments so no evidence crosses an omitted boundary.
276
+ const raw = alignGraded(ctx, weaveQuery, ctxBytes, weaveSites).flatMap((r) => {
277
+ let lo = 0;
278
+ let hi = weaveMap.length;
279
+ while (lo < hi) {
280
+ const mid = (lo + hi) >>> 1;
281
+ if (weaveMap[mid].compactStart <= r.qs)
282
+ lo = mid + 1;
283
+ else
284
+ hi = mid;
285
+ }
286
+ const out = [];
287
+ for (let pi = Math.max(0, lo - 1); pi < weaveMap.length; pi++) {
288
+ const part = weaveMap[pi];
289
+ if (part.compactStart >= r.qe)
290
+ break;
291
+ const partEnd = part.compactStart + part.length;
292
+ const start = Math.max(r.qs, part.compactStart);
293
+ const end = Math.min(r.qe, partEnd);
294
+ if (start >= end)
295
+ continue;
296
+ out.push({
297
+ ...r,
298
+ qs: part.originalStart + start - part.compactStart,
299
+ qe: part.originalStart + end - part.compactStart,
300
+ cs: r.cs + start - r.qs,
301
+ });
302
+ }
303
+ return out;
304
+ });
222
305
  if (raw.length === 0)
223
306
  continue;
224
307
  for (const r of raw) {