// reasoning.ts — multi-hop reasoning + multi-topic fusion. // // reason — extend an answer forward across facts (multi-hop) // fuseAttention — fuse independent points of attention (multi-topic) import { rItem, rNode } from "./trace.js"; import { bytesEqual, indexOf, latin1 } from "../bytes.js"; import type { Attention, MindContext } from "./types.js"; import { read, resolve } from "./primitives.js"; import { countClusters } from "./attention.js"; import { guidedFirst, hubBound, namedContinuations, offsetCanon, otherInstance, } from "./traverse.js"; import { unspoken, type WindowIndex, windowIndex } from "./evidence.js"; import { containsSpan, follow, haloSiblings, project } from "./match.js"; import { joinWithBridge, pivotInto } from "./resonance.js"; import { closed, closeOver, type ClosureLayer, type Continuation, type DerivationState, type Offer, restates, type Span, unaccountedBytes, type Witness, } from "./derivation.js"; import type { Precomputed } from "./pipeline-mechanism.js"; import type { Rationale } from "./rationale.js"; import { STEP } from "./graph-search.js"; /** What one ADMITTED transition did to the question, published on the meter: * the bytes its witnesses CARRIED and the remainder the move DRAINED. The two * transitions that go through the law — the walk's steps and the fusion — * report through this one function, so the two counters have one spelling. * Write-only (meter.ts contract 1). */ function meterTransition( ctx: MindContext, before: DerivationState, after: DerivationState, witnesses: ReadonlyArray, ): void { if (!ctx.meter) return; for (const w of witnesses) { if (w.window) ctx.meter.reasonCarriedBytes += w.window[1] - w.window[0]; } ctx.meter.closureDrainedBytes += unaccountedBytes(before.remainder) - unaccountedBytes(after.remainder); } /** Extend a grounded answer forward across facts (multi-hop reasoning) — the * WALK, run as one layer of the closure engine ({@link walkLayer}). * Pivots on the longest unconsumed learnt context each answer contains, * then follows the pivot's continuation to the next fact. **The chain ends * when it STOPS, never when a count runs out**: every exit is a refusal (no * pivot, no forward step, no question material carried) and the walk is bounded * by the material and the graph — `consumed` refuses to revisit a node. There * is no hop allowance, so this doc deliberately names no `cfg` capacity: the * cover prices every hop at `STEP` and lets the search decide the depth, and a * second count here would be a second decision about the same thing. * `preConsumed` carries node ids already * spoken for by the grounding stage (cover/extract/CAST). `voiced` carries * the BYTES of the anchors a mechanism declared it voiced (its `used` set), * when it declared one — see the pivot's own containment rule. `pre` is the * response's shared pre-computation — the post-grounding stages read the * same container the mechanisms did. */ export function reason( ctx: MindContext, query: Uint8Array, d0: DerivationState, preConsumed: ReadonlySet, pre: Precomputed, voiced: readonly Uint8Array[] = [], ): Promise { return closeOver(d0, query, ctx.space.maxGroup, [ walkLayer(ctx, query, preConsumed, pre, voiced), ]); } /** The multi-hop walk as a {@link ClosureLayer}: it OFFERS a forward absorb or a * pivot, the law admits. Its preamble — the two guards that say the question * was already answered by the query's own position in the graph, and the * pre-consumption of what the grounding spoke for — runs on the FIRST offer, * against the state the engine hands it; a guard that fires makes that first * offer `null`, so the walk ends where it begins and reports nothing more. */ export function walkLayer( ctx: MindContext, query: Uint8Array, preConsumed: ReadonlySet, pre: Precomputed, voiced: readonly Uint8Array[] = [], ): ClosureLayer { /** The preamble ran (on the first offer), and whether a guard stopped it. */ let begun = false; let stopped = false; let groundedId: number | null = null; let groundedPrev: number[] | null = null; let startedFrom: Uint8Array = new Uint8Array(0); // Consume a node and its neighbours for pivot-cycle prevention — CAPPED at // the hub bound, via the store's LIMITed edge reads: a common continuation's // reverse fan-in (and a hub context's forward fan-out) is corpus-sized, and // no per-hop operation may grow with the corpus. The cap follows the one // convention every fan-out decision uses (first √N in the relation's own // read order); a pivot suppressed only by a beyond-cap neighbour may now // fire — the same visibility trade chooseNext documents. const bound = hubBound(ctx); const consumed = new Set(); /** The windows each product of this derivation holds — what of the question * the derivation has already restated, so a later step is NAMED only by * question material none of them said. */ const spoken: WindowIndex[] = []; const speak = (product: Uint8Array) => spoken.push(windowIndex(offsetCanon(ctx, product), ctx.space.maxGroup)); /** `prev` lets a caller hand in an already-read reverse-edge list — hop 0 * reuses the guard's, below, instead of re-reading it. */ const consumeNode = ( id: number | null, prev?: readonly number[], ) => { if (id === null) return; consumed.add(id); for (const p of prev ?? ctx.store.prevFirst(id, bound)) consumed.add(p); }; const consumeAll = (id: number | null) => { if (id === null) return; consumeNode(id); for (const n of ctx.store.nextFirst(id, bound)) consumed.add(n); }; /** THE PREAMBLE — true when the walk may proceed. */ const begin = async (d0: DerivationState): Promise => { const answer = d0.product; startedFrom = answer; speak(answer); // Echo guard: a query that is ITSELF a learnt continuation (some context's // answer) is being asked back at the system — hopping forward from it would // chain through the very fact that produced it and echo the conversation // back. The grounded answer alone is the honest read-out. Deliberately a // broad structural gate; pinned by test/31-audit. const qId = pre.queryResolved; if (qId !== null && ctx.store.prevCount(qId) > 0) { // THE ECHO GUARD, NAMED — the audit's point 1 could not attribute two real // walks that stopped with no note at all, and this is where one of them went: // the query IS a learnt continuation, so the reasoner returns the grounded // read-out and never offers anything. Without this note the walk looks like // an unexplained stop; with it, "no note" can only mean the offer was never // asked. No decision changes: the guard returns exactly where it did. ctx.trace?.step( "echoGuard", [rItem(query, "query")], [], "the query is itself a learnt continuation — answering the grounded read-out without walking", ); return false; } // ANSWERED DIRECTLY — the echo guard's other half, and the same principle: // the QUERY's own position in the graph, not the answer's content, says the // read-out is complete. Above: the query is itself a learnt CONTINUATION. // Here: the query is a learnt CONTEXT and the grounded answer is one of ITS // OWN continuations. Either way the question was answered directly and there // is nothing left to chain for. // // Every stopping condition in the loop below judges the ANSWER (`consumed` / // the law's `restates` / `bytesEqual`); none asks whether the QUESTION was // satisfied. So a single-hop question whose answer happens to name another // learnt context extends past a correct answer and REPLACES it: // // asked " father" // hop 1 "The father of is Ernest I of Anhalt-Dessau." <- correct // pivot "Ernest I of Anhalt-Dessau" <- a learnt context too // got "The date of death of Ernest I of Anhalt-Dessau is 12 June 1516." // // Any store holding a bare-entity context alongside a relation fact has that // shape; it is not exotic. // // Checked ONCE, before the loop, and ahead of BOTH extension branches: // `absorbForward` extends the answer too, and nothing about the defect is // specific to pivoting, so a guard between them would gate one arbitrary half // of the same step. Hop 0 is also the only hop at which the question can be // answered directly at all — after a hop, `cur` is no longer the query's own // continuation, so re-testing per hop could only cost reads. // // Read from the ANSWER's side (`prevFirst`) rather than the query's // (`nextFirst`). Same relation, but a CONTEXT's fan-out is hub-sized while // this is one answer's establishing-context fan-in. Both the resolve and the // reverse read are exactly what hop 0 of the loop below would perform, so // they are computed ONCE here and handed down (`groundedId`, `groundedPrev`) // — the guard then costs nothing when it does not fire. Stated because the // naive placement does NOT: `resolve` re-folds the answer bytes on every call // (no memo) and `prevFirst` is a direct read (no memo), so a guard that // recomputed them would add one fold plus one √N-bounded read per ask. // The √N cap carries the file-wide visibility trade, and fails SAFE in the // direction that matters: a missed guard costs an over-extended answer, never // a suppressed chain. // // A genuine multi-hop query is not a deposited context at all ("What is the // capital of the country of Eiffel Tower?" resolves to nothing), so this can // never gate a real chain. groundedId = resolve(ctx, answer); groundedPrev = groundedId === null ? null : ctx.store.prevFirst(groundedId, bound); if (qId !== null && groundedPrev !== null && groundedPrev.includes(qId)) { return false; } // Pre-consume whatever the grounding stage already spoke for. The halo // sweep is one ANN query per node — cap it at haloQueryK sweeps (cover // grounding can pre-consume one node per recognised site, O(query length)); // nodes past the cap are still consumed directly, they just skip the // synonym expansion. const preconsume = async () => { let haloSweeps = 0; for (const id of preConsumed) { consumeNode(id); if (haloSweeps >= ctx.cfg.haloQueryK) continue; const h = ctx.store.halo(id); if (!h) continue; haloSweeps++; for (const sib of await haloSiblings(ctx, id, h)) consumeNode(sib.id); } }; if (ctx.meter) { await ctx.meter.time("reason.preconsumeHalos", preconsume); } else { await preconsume(); } return true; }; // ── THE WALK, AS THE LAW'S ─────────────────────────────────────────── // // This function no longer decides whether a step may be taken: it OFFERS a // continuation — a forward step from the answer's own learnt edge, or a pivot // on a learnt context the answer contains — and `derivation.ts`'s law admits // or refuses it. The state it offers against is the pipeline's own: the // grounding's remainder (what the question still owes), its accounting, its // cost. // // NO ALLOWANCE: THE CHAIN ENDS WHEN IT STOPS. Every exit below is the law — // no pivot, no forward step, no question material carried — and the walk is // bounded by the material and the graph rather than by a count: the law // requires each step to engage what is left (or to move to new structure), // and `consumed` refuses to revisit a node. `recallQueryK` no longer bounds // the reasoner here; it keeps its other roles (the bridge's candidate reads, // the pivot's probe budget, the resonance limits). // // Measured before removing the allowance: test/89 — the corpus-cost guard, the // heaviest case in the suite — is green and no slower without it (27 s against // 30 s); and raising it from 12 to 200 changed neither the answer nor // `pivotSteps` on the chain fixtures. const qv = pre.guide; // the response-wide guide IS the query's gist // NO PRODUCER OWNS THE NEXT STEP. A grounding that declared what it voiced // (cast/join) used to MOVE through any further term inside its answer, // unasked — `Who is the director of The Jerk?` grounded by CAST walked on to // `The country of citizenship of Carl Reiner is American.` What it voiced // still bounds what may be pivoted through (`voiced`, pivotInto); whether a // step is taken is the naming rule below, the same for every mechanism. let hop = 0; let t: ReturnType | undefined; // WHAT THE OFFERED STEP WOULD BE. The law decides, so the accepted step is // traced where the decision went (`onTaken`) and a refused one is reported as // the law's refusal (`onRefused`) — a step is never reported before it is // admitted. let pending: | { kind: "absorb"; cur: Uint8Array; curId: number | null; fwd: Uint8Array } | { kind: "pivot"; cur: Uint8Array; pivot: number; fc: Uint8Array } | null = null; const offer: Offer = async (d) => { if (!begun) { begun = true; if (!(await begin(d))) { stopped = true; return null; } } if (ctx.meter) ctx.meter.offerRuns += 1; const cur = d.product; // The first step's `cur` IS the grounding's product, so the guard above // already resolved it and read its reverse edges — reuse both. const curId = hop === 0 ? groundedId : resolve(ctx, cur); consumeNode(curId, hop === 0 ? groundedPrev ?? undefined : undefined); hop++; // Forward-absorb: follow only UNCONSUMED continuations. The gate below // checks an unconsumed edge EXISTS, but follow()'s chooseNext knows nothing // of `consumed` and may still walk to a consumed fixpoint — absorbing it // would repeat content the grounding stage already spoke for, so a consumed // fixpoint falls through to the pivot step instead. Offered with `moves`: // completing the answer's OWN learnt form is an identity step, not a claim // about the asker's material. // THE READ, KEPT — its LENGTH is free and it is the only instrument the // bound has: a read that came back full may have been cut off, and a read // that came back short did not. No second read is paid for this. const outs = curId !== null ? ctx.store.nextFirst(curId, bound) : []; const saturated = outs.length >= bound; // THE RE-REACH, NAMED — the audit's structural-identity case (its test G), and // the silent path it hid in. MEASURED, and this is the point's closure: across // thirteen real questions on the trained store — the roteiro's ten plus three — // this note fires ZERO times. The case, if it exists in this engine, is not on // that material. It is not unimplementable, it is unobserved, and the audit's // own rule is that an unobserved claim stays declared as unobserved: a // distinction between legitimate convergence and a cycle cannot be implemented // responsibly before it is seen, and the brief's section 13 puts measurement // before code. What exists here is the instrument that will show it the moment // it happens. When every successor of the current product has already // been spoken for, the absorb above is skipped WITHOUT a word and the walk // falls to the pivot: the same structure reached again is indistinguishable // from having none. This note does not change the decision — it makes the // state observable first, because the distinction between a legitimate // convergence and a cycle cannot be implemented responsibly until it is seen. if ( curId !== null && outs.length > 0 && !outs.some((n) => !consumed.has(n)) ) { ctx.trace?.step( "reachAlreadySpokenFor", [rItem(cur, "answer")], [], `every continuation of this product (${outs.length}) has already been spoken for — the same structure, reached again`, ); } if (curId !== null && outs.some((n) => !consumed.has(n))) { const fwd = await follow(ctx, curId, qv); const fwdId = fwd !== null ? resolve(ctx, fwd) : null; if ( fwd !== null && !bytesEqual(fwd, cur) && (fwdId === null || !consumed.has(fwdId)) && !restates(query, fwd, 0, { proper: true }) ) { consumeAll(curId); pending = { kind: "absorb", cur, curId, fwd }; return { product: fwd, contains: true, reaches: true, cost: STEP }; } // THE ABSORB'S OWN REFUSALS, TOLD APART — a learnt continuation existed // and was declined, so the walk falls through to the pivot rather than // ending. Which of the four reasons it was is exactly what the rationale // could not say before: `null` at the end of the walk is one state, and // this is another (AGENTS §6 — the note belongs where the mechanism emits // it, not in a counter beside it). const whyAbsorb = fwd === null ? "the unconsumed edge led nowhere" : bytesEqual(fwd, cur) ? "the continuation reached was the product already in hand" : fwdId !== null && consumed.has(fwdId) ? "the continuation reached had already been spoken for" : "this continuation only restates the question"; ctx.trace?.step( "absorbCandidateRefused", [rItem(cur, "answer")], [], `${whyAbsorb} — falling through to the pivot`, ); } if (saturated) { // THE BUDGET, NAMED — the audit's point 2. `nextFirst` is read with a cap // (`hubBound`), so a saturated read may have hidden a continuation, and the // walk would end as if none existed. The note says MAY, because the length // cannot tell a full read from a truncated one; it can only tell that this // is where a cut would have happened. ctx.trace?.step( "readBoundSaturated", [rItem(cur, "answer")], [], "the read came back full — a continuation may have been cut off by the bound", ); } // Pivot: the longest unconsumed learnt context the answer contains. consumeAll(curId); const pivot = await pivotInto(ctx, cur, consumed, voiced); if (pivot === null) { // THE ENUMERATION, DECLARED — the one duty the law cannot perform for the // layer: only the layer knows that it has nothing left to offer. Before // this note the walk ended in a bare `null`, indistinguishable from the // causes below it; a reader of the rationale could not tell "exhausted" // from "the candidate was refused" (AGENTS §6: a gap in instrumentation // is a defect IN the instrumentation). ctx.trace?.step( "enumerationExhausted", [rItem(cur, "answer")], [], "the layer has no further candidate — its enumeration is exhausted", ); return null; } const fc = await follow(ctx, pivot, qv); consumeAll(pivot); if ( fc === null || bytesEqual(fc, cur) || restates(query, fc, 0, { proper: true }) ) { // THE THREE REFUSALS, TOLD APART — a candidate was reached and declined, // which is NOT the same state as having none: the law reads both as "no // continuation", but the rationale should not. const why = fc === null ? "the pivot's context carried no continuation" : bytesEqual(fc, cur) ? "the continuation reached was the product already in hand" : "this continuation only restates the question"; ctx.trace?.step( "pivotCandidateRefused", [rItem(cur, "answer")], [], `${why} — refused`, ); return null; } // THE STEP THE QUESTION ASKS FOR. The hop `follow` took is NAMED when one // of its establishing contexts is witnessed by what of the question no // product has restated yet, plus the pivot itself (traverse.ts, // `namedContinuations`): `Where was the place of death of the director of // film Beat Girl?` names `Edmond T. Gréville place of death` once the first // hop has restated `director` and `Beat Girl` — and `Who is the father of // Frederick II?` names nothing past `Peter III of Aragon`, because `father` // was restated by the hop that reached him. A named step MOVES to structure // the question asked for, so it is admitted on that ground; an unnamed one // must carry the question's remainder as before, and from a CLOSED // derivation — nothing left owed — it is not offered at all: the question // asks for no further step, and the law would otherwise admit any. const asked = ctx._edgeAsked; let named = false; if (asked !== null) { const unsaid = { bytes: asked.bytes, index: unspoken(asked.index, spoken), }; const names = namedContinuations(ctx, pivot, unsaid); const hop = guidedFirst(ctx, pivot); named = names !== null && hop !== undefined && names.includes(hop); } if (!named && closed(d)) { ctx.trace?.step( "pivotUnasked", [rItem(cur, "answer"), rNode(ctx, pivot, "pivot")], [rItem(fc, "withheld")], "the derivation owes nothing and the question names no further step — not offered", ); return null; } pending = { kind: "pivot", cur, pivot, fc }; // Offered with the identity species when the question names the step; // otherwise the law requires this step to carry question material the grounding left // unaccounted, which is the drift the extension tests pin — refused by the // law's own measure, not by a private window test. return { product: fc, contains: true, reaches: named, cost: STEP, }; }; const onTaken = ( before: DerivationState, after: DerivationState, witnesses: ReadonlyArray, ): void => { meterTransition(ctx, before, after, witnesses); speak(after.product); const p = pending; pending = null; if (p === null) return; t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]); if (p.kind === "absorb") { ctx.trace?.step( "absorbForward", [rItem(p.cur, "answer", p.curId ?? undefined)], [rItem(p.fwd, "answer", resolve(ctx, p.fwd) ?? undefined)], "the answer is itself a learnt fact — follow its continuation to the fixpoint", ); } else { if (ctx.meter) ctx.meter.pivotSteps++; ctx.trace?.step( "pivotStep", [rItem(p.cur, "answer"), rNode(ctx, p.pivot, "pivot")], [rItem(p.fc, "answer", resolve(ctx, p.fc) ?? undefined)], "pivot on the shared span this answer contains, then step forward across that fact", ); } }; const onRefused = (at: DerivationState): void => { // THE LAW REFUSED — counted where it happened, before the bookkeeping // below decides whether there is a step to report. if (ctx.meter) ctx.meter.lawRejects += 1; const p = pending; pending = null; if (p === null || p.kind !== "pivot") return; // THE BRAKE, MADE VISIBLE — and it is now the LAW's refusal, reported // where it happened. The reasoner declines a step that carries none of // the material the grounding left unaccounted. A refusal that leaves no // trace is the kind of silent cut AGENTS §6 forbids: the rationale is // where a reader learns that an extension was declined for want of // question material, and where the next person sees why the chain stopped // here. Measured with the check disabled, test/110 and test/116 fail. const left = unaccountedBytes(at.remainder); ctx.trace?.step( "pivotRefused", [rItem(p.cur, "answer"), rItem(query, "query")], at.remainder.map(([a, b]) => rItem(query.subarray(a, b), "uncovered")), `the step carries none of the question material the grounding left ` + `unaccounted (${left} byte(s) in ${at.remainder.length} span(s)) — refused`, ); }; // THE WALK'S OWN ENDING, NAMED — the audit's point 1. Every stop INSIDE the // offer now has a note, but a walk can also end without the offer ever being // asked, or after a step the law took and then found nothing to follow: those // ended as "no note at all", indistinguishable from a mechanism that never ran. // Two endings cover every remaining case, and together they make a silent stop // impossible — which is what the `Offer` contract needs, because `null` is read // as exhaustion and exhaustion must be a statement, not an absence. A walk a // guard stopped reports nothing here: the guard already said why (or, for the // answered-directly guard, the answer IS the read-out), exactly as before the // walk became a layer. const onEnd = (d0: DerivationState, closed_: DerivationState): void => { if (stopped) return; t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]); if (closed_.remainder.length === 0) { ctx.trace?.step( "walkClosed", [rItem(closed_.product, "answer")], [], "the derivation is closed — nothing is left unaccounted for", ); } else { ctx.trace?.step( "walkEndedWithoutOffer", [rItem(closed_.product, "answer")], closed_.remainder.map(([a, b]) => rItem(query.subarray(a, b), "uncovered") ), `the walk ended with ${ unaccountedBytes(closed_.remainder) } byte(s) unaccounted and no further offer — no structural continuation`, ); } // INSTRUMENTATION ONLY — the extension's two facts, untraced (meter.ts // contract 1: a counter never reaches a decision). They are what a caller // needs to PRICE the extension instead of taking it unconditionally, and both // are now read off the state the law advanced rather than accumulated beside // it: the work it did is the cost it accumulated, and the material it carried // is the accounting the law's witnesses added. const steps = closed_.cost - d0.cost; if (ctx.meter) { ctx.meter.reasonSteps += steps; // The accounting the law's witnesses added, summed by the law's own function // over the tail of the state's list — and computed ONLY when a meter is // attached: this used to slice and MAP a fresh array on every response, for a // counter that usually does not exist. One allocation, under the meter. ctx.meter.reasonAccountedBytes += unaccountedBytes( closed_.accounted.slice(d0.accounted.length), ); } t?.done( [rItem( closed_.product, "answer", resolve(ctx, closed_.product) ?? undefined, )], // A FIXPOINT: no further step was offered, or the law refused the one that // was. There is no allowance to exhaust, so the note is true by // construction. "the multi-hop chain's fixpoint", ); }; return { name: "reason", offer, onTaken, onRefused, onEnd }; } /** In how many separate PLACES of the query a stored context shares content at * window scale — the query's W-byte windows the context's bytes contain, * clustered by {@link countClusters} at the same quantum. Below one window * byte identity is chance (identityBar), so only whole windows count. * * It is the dispersion question asked of the CONTEXT rather than of the votes * that happened to land on it. A region votes once, for its top anchor, so * where a root's votes come from is a lossy witness of where its context and * the query agree — measured on test/24's gap-3.1 fixture: the second topic's * second cluster was the region `ace and `, in the OTHER topic's words, * voting through a 2-byte sub-window anchor `e ` (attention.ts * canonicalChunkId); once that stopped voting, the genuine topic read as one * cluster. Its context still shares the system prompt AND its own wording * with the query — two places, the two test/37 documents — while a * coincidental echo (one closing phrase byte-identical to an unrelated * conversation's end, test/37) shares one. */ function sharedPlaces( context: Uint8Array, query: Uint8Array, quantum: number, ): number { if (context.length < quantum || query.length < quantum) return 0; const windows = new Set(); for (let o = 0; o + quantum <= context.length; o++) { windows.add(latin1(context.subarray(o, o + quantum))); } const spans: Array<[number, number]> = []; for (let o = 0; o + quantum <= query.length; o++) { if (windows.has(latin1(query.subarray(o, o + quantum)))) { spans.push([o, o + quantum]); } } return countClusters(spans, quantum); } /** Whether any query byte lies at least `quantum` away from every span — the * only place a root independent of those spans could stand. The spans, * widened by the quantum on each side, either cover [0, len) or leave a gap. */ function roomBeyond( len: number, spans: ReadonlyArray, quantum: number, ): boolean { if (spans.length === 0) return len > 0; const widened = spans .map(([s, e]): [number, number] => [s - quantum, e + quantum]) .sort((a, b) => a[0] - b[0]); let reach = 0; // every byte below `reach` is within a quantum of a span for (const [s, e] of widened) { if (s > reach) return true; reach = Math.max(reach, e); if (reach >= len) return false; } return reach < len; } /** Fuse independent points of attention into one answer (multi-topic). * When the consensus climb finds more than one dominant point, each * independent point grounds its own answer; they are bridged together * by any learnt connector the graph holds between them. */ export function fuseAttention( ctx: MindContext, query: Uint8Array, state: DerivationState, pre: Precomputed, unclimbed = false, primarySpans: ReadonlyArray = [], ): Promise { return closeOver(state, query, ctx.space.maxGroup, [ fusionLayer(ctx, query, pre, unclimbed, primarySpans), ]); } /** Fusion as a {@link ClosureLayer}: ONE transition, offered once — the fused * product of the independent points of attention, or nothing. The law admits * it (`reaches`: the fusion composed structure this derivation had not stood * on); the layer's own structural gates below decide whether there is a * fusion to offer at all. */ export function fusionLayer( ctx: MindContext, query: Uint8Array, pre: Precomputed, /** True when `primary` never touched the consensus climb at all — e.g. a * pure ALU computation, which has no anchor of its own. commitVotes * ALWAYS admits the dominant root regardless of its vote (attention.ts: * "roots.length === 0 || …") on the assumption a lone root already IS * primary's own source; that assumption is exactly backwards when * primary is unclimbed. Absent or false preserves the original * behaviour exactly. */ unclimbed = false, /** The query spans `primary`'s own grounding stands on — used ONLY to place * primary in the fused reading order (see below). Resolved by the caller, * which is the layer that knows how a given grounding records its evidence; * fuseAttention just reads a position from it. Empty or absent preserves * the original behaviour exactly. */ primarySpans: ReadonlyArray = [], ): ClosureLayer { let offered = false; const offer: Offer = async (state) => { if (offered) return null; offered = true; // THE STATE, NOT BARE BYTES: fusion is one more transition of the derivation // the walk returned, so it reads that state's product and hands back a state. // Its own structural gates stay here — this is the layer that can resolve // those witnesses, and the law never re-derives one. const primary = state.product; // When the answer is structurally drawn from the query itself // (extraction), it already spans all the query's pieces — fusion // would only add noise from unrelated stored contexts. The gate is // STRICT containment (resolved node in the query's tree, or a contiguous // byte run): the old sparse-subsequence test was trivially satisfied by // short answers over long queries, silently starving multi-topic queries // of fusion. if (containsSpan(ctx, query, primary)) return null; // A SECOND POINT OF ATTENTION MUST STAND ON EVIDENCE STRUCTURALLY SEPARATE // FROM PRIMARY'S: at least one perceptual quantum of query between them, // the same separation countClusters uses to tell independent evidence // neighbourhoods apart. Not a score, and not a tuned bar — the fold's own // quantum. A root standing on primary's own evidence is primary's topic // read again, never a further one, so it is not fused (measured on the // 31.7M-node store: an exact dialogue turn grounded whole, `[0,79]`, paid a // 3.7 s consensus climb whose only root lay inside that span). // // With no primarySpans (the caller did not resolve them) every span // vacuously qualifies, preserving the original behaviour exactly. const quantum = ctx.space.maxGroup; const independentOfPrimary = (root: Attention): boolean => primarySpans.every(([s, e]) => { const gap = root.end <= s ? s - root.end : e <= root.start ? root.start - e : 0; return gap >= quantum; }); // Decided BEFORE the climb when it is already decided: a root spans at // least one query byte, so where every byte lies within a quantum of // primary's evidence no root can be independent, and there is nothing the // climb could hand this layer. if (!roomBeyond(query.length, primarySpans, quantum)) return null; // The committed points of attention ARE the shared climb's roots (same // query, same k, same DF mode) — read them from Precomputed instead of // re-climbing, so even a traced response pays for the climb once. const forest = (await pre.attention()).roots; // A LONE root is ordinarily primary's own source — nothing to fuse. But // when primary is unclimbed, the lone root was never checked against // anything: it is admitted by commitVotes unconditionally, so it may be // genuine consensus (Attention.breadth dominates — most of the query's // OWN regions corroborate it) or a coincidental echo (breadth does not // dominate — see test/35-attention-confidence). breadth is the SCALE- // INVARIANT read of exactly this question: the raw IDF vote cannot serve // here, since it is an absolute ln(N)-scaled quantity (a genuine root on // a large store can score BELOW its own floor while a coincidental echo // on a small one scores comfortably above its own, smaller, floor). // // Breadth alone is not enough when primary is a pure COMPUTATION. The ALU // answers "2+2 equals what?" with 4, and the store's own arithmetic table // then supplies a lone root — an exemplar like "1+2" — whose breadth // dominates because it is corroborated by the computation's OWN bytes. // Fusing it projected that exemplar's continuation and the bridge voiced // "4+3" (test/11 seed 99) — the separation above is what refuses it. const lonePromotes = unclimbed && forest.length === 1 && forest[0].breadth > 0.5 && independentOfPrimary(forest[0]); if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) { return null; } // WHERE THE QUERY ASKED FOR IT. The sort below orders the fused pieces by // query position, which is the whole point of the `start` field: a // multi-topic answer should read in the order the question posed its // topics. Every ROOT carries its own start. `primary` did not — it was // given forest[0].start, the FIRST attention root's position, which is // primary's own source only when primary happens to come from that root. // When it does not, primary is sorted to a position it never occupied. // // Observed live: "What is the capital of France? And what is 2 + 2?" // answered "4The capital city of France is Paris." — the ALU result, whose // evidence is the "2 + 2" span near the END of the query, inherited the // France root's start of 0 and sorted ahead of the France answer. Both // pieces were right; only the order was. // // primary's own position is the earliest query byte its grounding stands // on. `accounted` is the cost-ladder read of that and is authoritative // when non-empty; when it is empty the grounding is a pure COMPUTATION, // whose evidence is its computed span — the same cost-ladder-vs-coverage // distinction think() already draws for the fusion remainder ("`accounted` // alone undercounts this ... cover prices its computed spans at near-zero // and deliberately leaves them out"), read here for position instead of // for coverage. With neither, nothing is known and the old behaviour // (forest[0].start) stands. const primaryStart = primarySpans.length > 0 ? primarySpans.reduce((m, [s]) => Math.min(m, s), Infinity) : forest[0].start; const pieces: Array<{ start: number; bytes: Uint8Array }> = [ { start: primaryStart, bytes: primary }, ]; const qv = pre.guide; // once, not per root const rest = (lonePromotes ? forest : forest.slice(1)).filter( independentOfPrimary, ); if (rest.length === 0) return null; const t = ctx.trace?.enter("fuseAttention", [ rItem(primary, "primary"), ...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)), ]); for (const root of rest) { // DISPERSION: this root's contributing regions are confined to a // single cluster (see Attention.clusters) — one local neighbourhood of // the query, not several separate places. Raw region count already // failed to discriminate a coincidental match from a genuine further // topic (test/24 gap 3.1 vs test/35's echo); dispersion is a different // question — not how MUCH evidence, but how many separate PLACES in the // query corroborate it — and a coincidental match is structurally // confined to one cluster no matter how strongly it resonates. // // EXCEPTION: crossRegionVotes' own joint conclusions (a query naming // two attributes that were only ever learnt TOGETHER — test/34's own // binding corpus) are inherently ONE fused context and are pooled from // a single synthetic region, so they always read as one cluster even // though they already, by construction, account for both original // mentions. `breadth` (dominates — the same > half-the-query bar used // everywhere else) still correctly recognises these: a genuine joint // binding explains the MAJORITY of the query's regions on its own, // which a coincidental echo never does (verified: test/35's echo tops // out at 0.40). So a root is trusted when EITHER measure alone // indicates real signal — excluded only when BOTH are weak. Cheap and // synchronous — checked before the async already-answered walk below. // // BELOW ONE WINDOW, BYTE IDENTITY IS NOT EVIDENCE (attention.ts): a // confined, narrow point whose strongest evidence is shorter than a // window — the `)?` closing `… (1971 Film)?`, which also closes a stored // `Who is the uncle of Auberon Herbert (Landowner)?` — is no further topic // however many scaffolding windows (`the `, ` of `) it also shares. if ( root.clusters < 2 && root.breadth <= 0.5 && (root.end - root.start < quantum || sharedPlaces(read(ctx, root.anchor), query, quantum) < 2) ) { ctx.trace?.step( "singleCluster", [rNode(ctx, root.anchor, "point", root.vote)], [], "this point's evidence is confined to one local neighbourhood of the query — not trusted as an independent topic", ); continue; } // ALREADY ANSWERED: this root's own learnt continuation — the same // content-addressed walk reason()'s echo guard already trusts // (`ctx.store.prevCount(qId) > 0`), here applied per-candidate instead // of to the whole query — is VERBATIM present later in the query. A // query that embeds both an exchange's ask and its own already-given // reply (a conversation's turn plus its own prior answer, concatenated // raw by addTurn — or any caller pasting a transcript into one // respond() call; the check is Mind-bookkeeping-free, so it treats both // identically) has already spoken this root's answer — fusing it in // again would only restate it. Deliberately NOT a magnitude measure: // it fires on exact content-addressed recurrence, not on how strongly // the root resonates. // ANOTHER INSTANCE IS NOT ANOTHER TOPIC. A root the question witnesses // in every byte but one contiguous span is the question's own frame // around a different filler (traverse.ts, `otherInstance`): the climb // committed `Where was Peter Jackson born?` as a second point of // `Where was the director of film Beat Girl born?` on the frame windows // alone, and fusing it voiced Peter Jackson's birthplace beside the // answer. Such a root says what the question's relation IS (traverse.ts, // `byCoInstance`), never what it asks about. { const form = read(ctx, root.anchor, 2 * query.length); const co = otherInstance(ctx, form); if (co !== null) { ctx.trace?.step( "coInstanceRoot", [rNode(ctx, root.anchor, "point", root.vote)], [rItem(form.subarray(co.span[0], co.span[1]), "filler")], "the question witnesses this point in every byte but one filler of its own — another instance of the question's frame, not a further topic", ); continue; } } const cont = await follow(ctx, root.anchor, qv); if ( cont !== null && cont.length > 0 && // The law's positional reading: the caller knows the material it is // looking for lies AT OR AFTER this root, which is the witness; the law // owns the containment test itself. The `cont.length > 0` guard stays // here because an EMPTY continuation indexes at every offset — the law // has no opinion about whether "nothing" counts as said. restates(query, cont, 0, { from: root.end }) ) { ctx.trace?.step( "alreadyAnswered", [rNode(ctx, root.anchor, "point", root.vote)], [rItem(cont, "continuation")], "this point's own learnt continuation already appears later in the query — already answered, not fused", ); continue; } const g = await project(ctx, root.anchor, qv); if (g === null || g.length === 0) continue; if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0)) continue; pieces.push({ start: root.start, bytes: g }); } if (pieces.length === 1) { t?.done( [rItem(primary, "answer")], "no further independent point grounded", ); return null; } pieces.sort((a, b) => a.start - b.start); let out = pieces[0].bytes; for (let i = 1; i < pieces.length; i++) { // An approximate-resonance miss (or a genuinely unlearnt junction) joins // the pieces bare — joinWithBridge surfaces it as a bridgeMiss step. out = await joinWithBridge(ctx, out, pieces[i].bytes); } t?.done( [rItem(out, "answer", resolve(ctx, out) ?? undefined)], `fused ${pieces.length} independent points of attention into one answer`, ); // THE FACT IS THE FUSED ANSWER, not the call: every early return above hands // back `primary` untouched. Untraced on purpose (meter.ts contract 1). if (ctx.meter) ctx.meter.fuseRuns++; // A FUSION IS A TRANSITION, and the only one in the engine that can splice the // QUESTION'S OWN material into the product. So it is OFFERED to the law rather // than built by hand: the law reads the window the fused product holds — the // same reading the carries admission uses, one definition — accounts for the // span it carried, and lets the question's remainder drain on EVIDENCE. A // fusion that carries nothing consumes nothing, because the reading finds no // window; and `reaches` is declared only when the fusion composed another root, // which is structure this derivation had not stood on. // // THE LAW'S REFUSAL IS FINAL. The only state it refuses a fusion against is a // supplied fixed point (`reaches` is always declared here), and the engine // enters no layer for one — so there is no hand-built fallback: a fusion the // law refuses is not taken (measured: no response in the suite reaches the // refusal). const fusedT: Continuation = { product: out, contains: true, reaches: rest.length > 0, cost: STEP, }; return fusedT; }; return { name: "fuse", offer, onTaken: (before, after, witnesses) => meterTransition(ctx, before, after, witnesses), }; }