@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.
- package/dist/src/geometry.d.ts +26 -0
- package/dist/src/geometry.js +32 -3
- package/dist/src/mind/attention.d.ts +246 -7
- package/dist/src/mind/attention.js +771 -173
- package/dist/src/mind/bridge.d.ts +30 -0
- package/dist/src/mind/bridge.js +569 -0
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +30 -1
- package/dist/src/mind/junction.js +73 -18
- package/dist/src/mind/match.d.ts +15 -2
- package/dist/src/mind/match.js +3 -8
- package/dist/src/mind/mechanisms/cast.d.ts +54 -0
- package/dist/src/mind/mechanisms/cast.js +268 -37
- package/dist/src/mind/mechanisms/cover.js +16 -31
- package/dist/src/mind/mechanisms/recall.js +66 -0
- package/dist/src/mind/mind.d.ts +2 -0
- package/dist/src/mind/rationale.d.ts +7 -2
- package/dist/src/mind/rationale.js +6 -5
- package/dist/src/mind/reasoning.d.ts +11 -0
- package/dist/src/mind/reasoning.js +58 -2
- package/dist/src/mind/recognition.js +127 -7
- package/dist/src/mind/traverse.js +90 -25
- package/dist/src/mind/types.d.ts +57 -2
- package/dist/src/mind/types.js +38 -7
- package/dist/src/store.d.ts +12 -3
- package/dist/src/store.js +9 -3
- package/package.json +1 -1
- package/src/geometry.ts +52 -3
- package/src/mind/attention.ts +1199 -128
- package/src/mind/bridge.ts +596 -0
- package/src/mind/index.ts +16 -0
- package/src/mind/junction.ts +125 -16
- package/src/mind/match.ts +19 -5
- package/src/mind/mechanisms/cast.ts +290 -38
- package/src/mind/mechanisms/cover.ts +23 -36
- package/src/mind/mechanisms/recall.ts +79 -0
- package/src/mind/mind.ts +15 -0
- package/src/mind/rationale.ts +12 -4
- package/src/mind/reasoning.ts +71 -2
- package/src/mind/recognition.ts +132 -7
- package/src/mind/traverse.ts +91 -24
- package/src/mind/types.ts +95 -6
- package/src/store.ts +19 -5
- package/test/36-already-answered-fusion.test.mjs +128 -0
- package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
- package/test/38-reason-restate-guard.test.mjs +94 -0
- package/test/39-cast-restate-guard.test.mjs +102 -0
- package/test/40-choosenext-scale-guard.test.mjs +75 -0
- package/test/41-seatofnode-direction.test.mjs +85 -0
- package/test/42-recognise-trace-idempotence.test.mjs +106 -0
- package/test/43-cast-analog-seat.test.mjs +244 -0
- package/test/44-recognise-edge-whitespace.test.mjs +63 -0
- package/test/45-liftanswer-restated-trim.test.mjs +60 -0
- package/test/46-recognise-multibyte-edge.test.mjs +85 -0
- package/test/47-cast-comparison-coverage.test.mjs +134 -0
- package/test/48-recognise-turn-connective.test.mjs +125 -0
- package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
- package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
- package/test/51-structural-resonance-ladder.test.mjs +552 -0
- package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
// cover runs FIRST in defaultMechanisms: a computed-backed cover becomes a
|
|
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
|
-
import { bytesEqual, indexOf } from "../../bytes.js";
|
|
12
11
|
import { read, resolve } from "../primitives.js";
|
|
13
12
|
import { guidedFirst } from "../traverse.js";
|
|
14
13
|
import { conceptHop } from "../match.js";
|
|
15
14
|
import { bridge } from "../resonance.js";
|
|
16
|
-
import { liftAnswer } from "../types.js";
|
|
15
|
+
import { liftAnswer, segRestatesQuery } from "../types.js";
|
|
17
16
|
import { decodeText, unexplainedLabel } from "../rationale.js";
|
|
18
17
|
import { rItem, rNode, traceDerivation } from "../trace.js";
|
|
19
18
|
// ── Concept / connector pre-resolution ──────────────────────────────────────
|
|
@@ -166,46 +165,32 @@ export const coverMechanism = {
|
|
|
166
165
|
: "lightest derivation: the chosen spans, left to right");
|
|
167
166
|
if (segs === null)
|
|
168
167
|
return [];
|
|
168
|
+
const W = ctx.space.maxGroup;
|
|
169
169
|
// A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
|
|
170
170
|
// site, not the site's own literal text read back) that equal a byte
|
|
171
171
|
// span the query ALREADY CONTAINS elsewhere restates part of the
|
|
172
|
-
// question — never an answer (
|
|
173
|
-
// apply to a whole-query projection: "a projection that is a proper
|
|
174
|
-
// byte-subspan of the query restates part of the question"). A
|
|
172
|
+
// question — never an answer (see {@link segRestatesQuery}). A
|
|
175
173
|
// recognised site that is itself an entire PRIOR TURN of a multi-turn
|
|
176
174
|
// query is exactly this shape: it carries a genuine learnt
|
|
177
175
|
// continuation, but that continuation is something the asker already
|
|
178
|
-
// said moments later in the SAME query
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const literal = s.j - s.i === s.bytes.length &&
|
|
186
|
-
bytesEqual(s.bytes, query.subarray(s.i, s.j));
|
|
187
|
-
if (literal)
|
|
188
|
-
continue;
|
|
189
|
-
// A span shorter than one river window is exactly the "ice"/"c"
|
|
190
|
-
// case: a chain hop's short terminal coincidentally recurring as a
|
|
191
|
-
// substring of an unrelated longer word. Below the quantum, byte
|
|
192
|
-
// overlap is chance, not evidence — the same floor identityBar and
|
|
193
|
-
// reachThreshold hold every other structural-overlap claim to.
|
|
194
|
-
if (s.bytes.length >= W && s.bytes.length < query.length &&
|
|
195
|
-
indexOf(query, s.bytes, 0) >= 0) {
|
|
196
|
-
ctx.trace?.step("restatedSpan", [rItem(s.bytes, "substituted", s.node, [s.i, s.j])], [], "the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer");
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
176
|
+
// said moments later in the SAME query. liftAnswer TRIMS such spans
|
|
177
|
+
// out of both the framing decision and the final concatenation — the
|
|
178
|
+
// OTHER spans a derivation chose are independent evidence and must not
|
|
179
|
+
// be discarded along with the stale one.
|
|
180
|
+
const restated = segs.filter((s) => segRestatesQuery(s, query, query.length, W));
|
|
181
|
+
if (restated.length > 0) {
|
|
182
|
+
ctx.trace?.step("restatedSpan", restated.map((s) => rItem(s.bytes, "substituted", s.node, [s.i, s.j])), [], "the chosen span's substitution already occurs elsewhere in the query — trimmed from the answer, not an answer itself");
|
|
199
183
|
}
|
|
200
|
-
const composed = liftAnswer(segs, query.length);
|
|
184
|
+
const composed = liftAnswer(segs, query.length, query, W);
|
|
201
185
|
if (composed === null)
|
|
202
186
|
return [];
|
|
203
187
|
ctx.trace?.step("liftAnswer", segs.map((s) => rItem(s.bytes, s.rec ? "chosen" : "scaffolding", s.node, [s.i, s.j])), [rItem(composed, "answer", resolve(ctx, composed) ?? undefined)], "lift the recognised region out of the asker's framing", tCover ? [tCover.index] : undefined);
|
|
204
|
-
// accounted = RECOGNISED cover spans only —
|
|
205
|
-
//
|
|
206
|
-
//
|
|
188
|
+
// accounted = RECOGNISED, non-restating cover spans only — a trimmed
|
|
189
|
+
// restated span contributes nothing to the composed answer, so it must
|
|
190
|
+
// not be priced as if it did (PASS-carried bytes are priced already;
|
|
191
|
+
// the diagnostic label reflects the same distinction).
|
|
207
192
|
const accounted = segs
|
|
208
|
-
.filter((s) => s.rec)
|
|
193
|
+
.filter((s) => s.rec && !segRestatesQuery(s, query, query.length, W))
|
|
209
194
|
.map((s) => [s.i, s.j]);
|
|
210
195
|
return [{
|
|
211
196
|
bytes: composed,
|
|
@@ -12,6 +12,7 @@ import { follow, project, reverseContext } from "../match.js";
|
|
|
12
12
|
import { CONCEPT, STEP } from "../graph-search.js";
|
|
13
13
|
import { unexplainedLabel } from "../rationale.js";
|
|
14
14
|
import { rItem, rNode } from "../trace.js";
|
|
15
|
+
import { substitutionBridge } from "../bridge.js";
|
|
15
16
|
/** Recall the answer by resonating the whole query against the content index. */
|
|
16
17
|
export async function recallByResonance(ctx, query, pre) {
|
|
17
18
|
const t = ctx.trace?.enter("recallByResonance", [
|
|
@@ -209,6 +210,71 @@ export async function recallByResonance(ctx, query, pre) {
|
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
}
|
|
213
|
+
// 3b. Corroborated-substitution bridge — refusal-path only (bridge.ts).
|
|
214
|
+
// Every gist-based tier has failed; before refusing, align the query
|
|
215
|
+
// byte-for-byte against the trained contexts its own stored windows
|
|
216
|
+
// anchor, accepting mismatches only as corpus-attested, concept-bar
|
|
217
|
+
// substitutions. A bridged context grounds exactly like any hit —
|
|
218
|
+
// projected through its learnt edges — under the same restated-fragment
|
|
219
|
+
// guard tiers 0b/2 apply. Costs nothing on any answering path.
|
|
220
|
+
{
|
|
221
|
+
// The resonance hits already ranked above are handed to the bridge as
|
|
222
|
+
// PROPOSED candidates alongside its own anchor climbs: on a corpus this
|
|
223
|
+
// size a W-byte window is far too common for the clamped climb to
|
|
224
|
+
// single out the right trained context, while the whole-query gist
|
|
225
|
+
// already ranked it nearest (observed live: "what is the capital of
|
|
226
|
+
// france" resonating straight to "What is the capital of France?" yet
|
|
227
|
+
// refusing on the reach bar). Approximate scores propose; the bridge's
|
|
228
|
+
// byte-exact alignment and attestation gates decide.
|
|
229
|
+
//
|
|
230
|
+
// The proposal breadth here is widened PAST `k` — first by requesting
|
|
231
|
+
// hubBound(ctx) candidates instead of `k` (recall's own tiers above
|
|
232
|
+
// stay at `k`; this re-resonates only on the refusal path, exactly
|
|
233
|
+
// where the bridge itself already runs), AND by asking the index to
|
|
234
|
+
// search EXHAUSTIVELY. Both matter: the IVF only ever probes
|
|
235
|
+
// ⌈√clusters⌉ of them (store.ts's efFor) REGARDLESS of k — widening k
|
|
236
|
+
// alone just returns more hits from the SAME already-probed clusters,
|
|
237
|
+
// never a hit whose vector lives in an unprobed one. Measured live:
|
|
238
|
+
// "What is the chemical symbol for water?" needs "What is the
|
|
239
|
+
// chemical formula for water?", scoring only 0.58 against the
|
|
240
|
+
// query's gist (a MIDDLE-of-string word swap perturbs the river-fold
|
|
241
|
+
// tree hash far more than a same-length TAIL swap like the "carbon"/
|
|
242
|
+
// "oxygen" neighbours that outrank it at 0.87+) — absent from the
|
|
243
|
+
// resonance list even at k=5000, present and byte-exact-verified the
|
|
244
|
+
// moment it's force-fed to the bridge directly. `exhaustive` is the
|
|
245
|
+
// natural, tuning-free ceiling (probe every cluster) for a call that
|
|
246
|
+
// is ALREADY refusal-path-only and must not miss a candidate hiding
|
|
247
|
+
// behind an unlucky structural distance.
|
|
248
|
+
const wide = k < hubBound(ctx)
|
|
249
|
+
? await ctx.store.resonate(queryGist, hubBound(ctx), true)
|
|
250
|
+
: whole;
|
|
251
|
+
const bridged = await substitutionBridge(ctx, query, wide.map((h) => h.id));
|
|
252
|
+
if (bridged !== null) {
|
|
253
|
+
const g = await project(ctx, bridged.id, queryGist);
|
|
254
|
+
// A projection contained in a substituted candidate-side span is the
|
|
255
|
+
// substitution RESTATED as if it were knowledge — the exact failure
|
|
256
|
+
// observed live: "Darwin was born in England." bridged to the
|
|
257
|
+
// Einstein fact through " England." → " Germany." and would have
|
|
258
|
+
// voiced "Germany", an answer the substitution itself manufactured.
|
|
259
|
+
// The same principle as the restated-fragment guards above, extended
|
|
260
|
+
// to the bridge's own substitutions.
|
|
261
|
+
const cBytes = ctx.store.bytes(bridged.id);
|
|
262
|
+
const manufactured = g !== null &&
|
|
263
|
+
bridged.subs.some((s) => indexOf(cBytes.subarray(s.cs, s.ce), g, 0) >= 0);
|
|
264
|
+
if (g !== null && g.length > 0 && !restates(g) && !manufactured &&
|
|
265
|
+
!(g.length < query.length && indexOf(query, g, 0) >= 0)) {
|
|
266
|
+
return ground(g, `substitution bridge — a trained context accounts for the query ` +
|
|
267
|
+
`up to ${bridged.subs.length} corroborated substitution(s)`,
|
|
268
|
+
// Accounted NOTHING — the same epistemic humility as the echo
|
|
269
|
+
// tier below: a substitution-bridged grounding is a last resort
|
|
270
|
+
// that must lose to ANY mechanism that actually explained the
|
|
271
|
+
// query (observed: pricing the aligned spans here outweighed
|
|
272
|
+
// extraction's correct answer in the grounding decider), while
|
|
273
|
+
// still beating silence when everything else refused.
|
|
274
|
+
[], CONCEPT * bridged.subs.length + STEP);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
212
278
|
// The refusal/echo decision. The echo returns a stored form's bytes AS
|
|
213
279
|
// the answer — a near-identity claim about the query — and identity-grade
|
|
214
280
|
// decisions are never made on an estimated score ("approximate scores may
|
package/dist/src/mind/mind.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface Conversation {
|
|
|
42
42
|
readonly id: number;
|
|
43
43
|
}
|
|
44
44
|
import type { AttentionRead, MindContext, Recognition } from "./types.js";
|
|
45
|
+
export type { AnchorRejectionReason, ClimbConsensusData, ConsensusAnchorTrace, ConsensusReachTrace, ConsensusRegionTrace, CrossRegionTier, JunctionVoteTrace, RegionOutcome, } from "./attention.js";
|
|
46
|
+
export type { AncestorReach, SaturationReason, SaturationStop, } from "./types.js";
|
|
45
47
|
export interface MindOptions {
|
|
46
48
|
seed?: number;
|
|
47
49
|
recallQueryK?: number;
|
|
@@ -59,6 +59,11 @@ export interface RationaleStep {
|
|
|
59
59
|
/** A one-line, human account of what the mechanism did and why — the sentence
|
|
60
60
|
* that turns the data into an explanation. */
|
|
61
61
|
note?: string;
|
|
62
|
+
/** Optional structured payload — a mechanism-specific, plain-serialisable
|
|
63
|
+
* shape (no Map/Set/vectors/mutable internals) that carries more than the
|
|
64
|
+
* human-readable `note` can, for a debugger or downstream tool to consume
|
|
65
|
+
* programmatically. Never read by inference; purely additive. */
|
|
66
|
+
data?: unknown;
|
|
62
67
|
}
|
|
63
68
|
/** The callback {@link Mind.respond} / {@link Mind.respondText} accept. It is
|
|
64
69
|
* invoked once per completed step, AS the inference unfolds — never batched at
|
|
@@ -86,7 +91,7 @@ export interface Scope {
|
|
|
86
91
|
/** Close the mechanism: emit its step with these outputs and pop it off the
|
|
87
92
|
* nesting stack. Idempotent — a second call is ignored, so a `finally` that
|
|
88
93
|
* closes after an early return is safe. */
|
|
89
|
-
done(outputs: RationaleItem[], note?: string): void;
|
|
94
|
+
done(outputs: RationaleItem[], note?: string, data?: unknown): void;
|
|
90
95
|
}
|
|
91
96
|
/** The live tracer: a stack of open mechanisms over one {@link Mind.respond}.
|
|
92
97
|
*
|
|
@@ -130,5 +135,5 @@ export declare class Rationale {
|
|
|
130
135
|
enter(name: string, inputs: RationaleItem[], deps?: number[]): Scope;
|
|
131
136
|
/** Record a mechanism that has no sub-steps — its inputs and outputs are both
|
|
132
137
|
* known at the call site. Returns its index, for a later step to depend on. */
|
|
133
|
-
step(name: string, inputs: RationaleItem[], outputs: RationaleItem[], note?: string, deps?: number[]): number;
|
|
138
|
+
step(name: string, inputs: RationaleItem[], outputs: RationaleItem[], note?: string, deps?: number[], data?: unknown): number;
|
|
134
139
|
}
|
|
@@ -109,7 +109,7 @@ export class Rationale {
|
|
|
109
109
|
this.lastByName.set(name, index);
|
|
110
110
|
return index;
|
|
111
111
|
}
|
|
112
|
-
emit(index, mechanism, inputs, outputs, deps, note) {
|
|
112
|
+
emit(index, mechanism, inputs, outputs, deps, note, data) {
|
|
113
113
|
this.sink({
|
|
114
114
|
index,
|
|
115
115
|
mechanism,
|
|
@@ -120,6 +120,7 @@ export class Rationale {
|
|
|
120
120
|
inputs,
|
|
121
121
|
outputs,
|
|
122
122
|
note,
|
|
123
|
+
data,
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
/** Enter a mechanism that has sub-steps. Captures its inputs and the nesting
|
|
@@ -141,22 +142,22 @@ export class Rationale {
|
|
|
141
142
|
};
|
|
142
143
|
return {
|
|
143
144
|
index,
|
|
144
|
-
done: (outputs, note) => {
|
|
145
|
+
done: (outputs, note, data) => {
|
|
145
146
|
if (closed)
|
|
146
147
|
return;
|
|
147
148
|
closed = true;
|
|
148
149
|
pop();
|
|
149
|
-
emit(index, mechanism, inputs, outputs, resolvedDeps, note);
|
|
150
|
+
emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
|
|
150
151
|
},
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
/** Record a mechanism that has no sub-steps — its inputs and outputs are both
|
|
154
155
|
* known at the call site. Returns its index, for a later step to depend on. */
|
|
155
|
-
step(name, inputs, outputs, note, deps) {
|
|
156
|
+
step(name, inputs, outputs, note, deps, data) {
|
|
156
157
|
const mechanism = this.path(name);
|
|
157
158
|
const resolvedDeps = deps ?? this.defaultDeps();
|
|
158
159
|
const index = this.reserve(name);
|
|
159
|
-
this.emit(index, mechanism, inputs, outputs, resolvedDeps, note);
|
|
160
|
+
this.emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
|
|
160
161
|
return index;
|
|
161
162
|
}
|
|
162
163
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { MindContext } from "./types.js";
|
|
2
2
|
import type { Precomputed } from "./pipeline-mechanism.js";
|
|
3
|
+
/** Whether `bytes` is a proper byte-subspan of `query` — already present in
|
|
4
|
+
* the question, so voicing it back only restates part of what was asked,
|
|
5
|
+
* never answers it. The exact guard recallByResonance already applies to
|
|
6
|
+
* its OWN grounding candidates (tier 1's `restates`, tier 2's subspan
|
|
7
|
+
* check, tier 0b's argument-binding subspan check) — every mechanism that
|
|
8
|
+
* walks a LEARNT CONTINUATION EDGE past an already-vetted grounding
|
|
9
|
+
* (reason()'s own hops below, and CAST's `projectCounterfactual` seat
|
|
10
|
+
* substitution — see cast.ts) needs the same guard applied to what the
|
|
11
|
+
* walk turns up, since `follow()`/`chooseNext`/`pivotInto` know nothing of
|
|
12
|
+
* the query at all — only of what structurally continues what. */
|
|
13
|
+
export declare function restatesQuery(query: Uint8Array, bytes: Uint8Array): boolean;
|
|
3
14
|
/** Extend a grounded answer forward across facts (multi-hop reasoning).
|
|
4
15
|
* Pivots on the longest unconsumed learnt context each answer contains,
|
|
5
16
|
* then follows the pivot's continuation to the next fact. Repeats up
|
|
@@ -8,6 +8,19 @@ import { resolve } from "./primitives.js";
|
|
|
8
8
|
import { corpusN } from "./traverse.js";
|
|
9
9
|
import { follow, haloSiblings, project } from "./match.js";
|
|
10
10
|
import { joinWithBridge, pivotInto } from "./resonance.js";
|
|
11
|
+
/** Whether `bytes` is a proper byte-subspan of `query` — already present in
|
|
12
|
+
* the question, so voicing it back only restates part of what was asked,
|
|
13
|
+
* never answers it. The exact guard recallByResonance already applies to
|
|
14
|
+
* its OWN grounding candidates (tier 1's `restates`, tier 2's subspan
|
|
15
|
+
* check, tier 0b's argument-binding subspan check) — every mechanism that
|
|
16
|
+
* walks a LEARNT CONTINUATION EDGE past an already-vetted grounding
|
|
17
|
+
* (reason()'s own hops below, and CAST's `projectCounterfactual` seat
|
|
18
|
+
* substitution — see cast.ts) needs the same guard applied to what the
|
|
19
|
+
* walk turns up, since `follow()`/`chooseNext`/`pivotInto` know nothing of
|
|
20
|
+
* the query at all — only of what structurally continues what. */
|
|
21
|
+
export function restatesQuery(query, bytes) {
|
|
22
|
+
return bytes.length < query.length && indexOf(query, bytes, 0) >= 0;
|
|
23
|
+
}
|
|
11
24
|
/** Extend a grounded answer forward across facts (multi-hop reasoning).
|
|
12
25
|
* Pivots on the longest unconsumed learnt context each answer contains,
|
|
13
26
|
* then follows the pivot's continuation to the next fact. Repeats up
|
|
@@ -81,7 +94,8 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
|
|
|
81
94
|
const fwd = await follow(ctx, curId, qv);
|
|
82
95
|
const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
|
|
83
96
|
if (fwd !== null && !bytesEqual(fwd, cur) &&
|
|
84
|
-
(fwdId === null || !consumed.has(fwdId))
|
|
97
|
+
(fwdId === null || !consumed.has(fwdId)) &&
|
|
98
|
+
!restatesQuery(query, fwd)) {
|
|
85
99
|
consumeAll(curId);
|
|
86
100
|
t ??= ctx.trace?.enter("reason", [
|
|
87
101
|
rItem(startedFrom, "grounded"),
|
|
@@ -98,7 +112,7 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
|
|
|
98
112
|
break;
|
|
99
113
|
const fc = await follow(ctx, pivot, qv);
|
|
100
114
|
consumeAll(pivot);
|
|
101
|
-
if (fc === null || bytesEqual(fc, cur))
|
|
115
|
+
if (fc === null || bytesEqual(fc, cur) || restatesQuery(query, fc))
|
|
102
116
|
break;
|
|
103
117
|
t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
|
|
104
118
|
ctx.trace?.step("pivotStep", [rItem(cur, "answer"), rNode(ctx, pivot, "pivot")], [rItem(fc, "answer", resolve(ctx, fc) ?? undefined)], "pivot on the shared span this answer contains, then step forward across that fact");
|
|
@@ -158,6 +172,48 @@ unclimbed = false) {
|
|
|
158
172
|
...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
|
|
159
173
|
]);
|
|
160
174
|
for (const root of rest) {
|
|
175
|
+
// DISPERSION: this root's contributing regions are confined to a
|
|
176
|
+
// single cluster (see Attention.clusters) — one local neighbourhood of
|
|
177
|
+
// the query, not several separate places. Raw region count already
|
|
178
|
+
// failed to discriminate a coincidental match from a genuine further
|
|
179
|
+
// topic (test/24 gap 3.1 vs test/35's echo); dispersion is a different
|
|
180
|
+
// question — not how MUCH evidence, but how many separate PLACES in the
|
|
181
|
+
// query corroborate it — and a coincidental match is structurally
|
|
182
|
+
// confined to one cluster no matter how strongly it resonates.
|
|
183
|
+
//
|
|
184
|
+
// EXCEPTION: crossRegionVotes' own joint conclusions (a query naming
|
|
185
|
+
// two attributes that were only ever learnt TOGETHER — test/34's own
|
|
186
|
+
// binding corpus) are inherently ONE fused context and are pooled from
|
|
187
|
+
// a single synthetic region, so they always read as one cluster even
|
|
188
|
+
// though they already, by construction, account for both original
|
|
189
|
+
// mentions. `breadth` (dominates — the same > half-the-query bar used
|
|
190
|
+
// everywhere else) still correctly recognises these: a genuine joint
|
|
191
|
+
// binding explains the MAJORITY of the query's regions on its own,
|
|
192
|
+
// which a coincidental echo never does (verified: test/35's echo tops
|
|
193
|
+
// out at 0.40). So a root is trusted when EITHER measure alone
|
|
194
|
+
// indicates real signal — excluded only when BOTH are weak. Cheap and
|
|
195
|
+
// synchronous — checked before the async already-answered walk below.
|
|
196
|
+
if (root.clusters < 2 && root.breadth <= 0.5) {
|
|
197
|
+
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");
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// ALREADY ANSWERED: this root's own learnt continuation — the same
|
|
201
|
+
// content-addressed walk reason()'s echo guard already trusts
|
|
202
|
+
// (`ctx.store.prevCount(qId) > 0`), here applied per-candidate instead
|
|
203
|
+
// of to the whole query — is VERBATIM present later in the query. A
|
|
204
|
+
// query that embeds both an exchange's ask and its own already-given
|
|
205
|
+
// reply (a conversation's turn plus its own prior answer, concatenated
|
|
206
|
+
// raw by addTurn — or any caller pasting a transcript into one
|
|
207
|
+
// respond() call; the check is Mind-bookkeeping-free, so it treats both
|
|
208
|
+
// identically) has already spoken this root's answer — fusing it in
|
|
209
|
+
// again would only restate it. Deliberately NOT a magnitude measure:
|
|
210
|
+
// it fires on exact content-addressed recurrence, not on how strongly
|
|
211
|
+
// the root resonates.
|
|
212
|
+
const cont = await follow(ctx, root.anchor, qv);
|
|
213
|
+
if (cont !== null && cont.length > 0 && indexOf(query, cont, root.end) >= 0) {
|
|
214
|
+
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");
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
161
217
|
const g = await project(ctx, root.anchor, qv);
|
|
162
218
|
if (g === null || g.length === 0)
|
|
163
219
|
continue;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { rItem } from "./trace.js";
|
|
8
8
|
import { canonResolve, foldTree, gistOf, latin1Key, perceive, resolve, } from "./primitives.js";
|
|
9
9
|
import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
|
|
10
|
-
import { chainReach, leafIdAt } from "./canonical.js";
|
|
10
|
+
import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
|
|
11
11
|
import { isChunk } from "../sema.js";
|
|
12
12
|
/** Decompose a byte stream into every stored form that leads somewhere
|
|
13
13
|
* (has a continuation edge or a halo). Two complementary readings:
|
|
@@ -23,13 +23,37 @@ import { isChunk } from "../sema.js";
|
|
|
23
23
|
* Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
|
|
24
24
|
export function recognise(ctx, bytes) {
|
|
25
25
|
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
26
|
-
// respondTurn() (where the map persists across calls).
|
|
27
|
-
// tracing
|
|
28
|
-
|
|
26
|
+
// respondTurn() (where the map persists across calls). ALWAYS consulted,
|
|
27
|
+
// regardless of tracing — matching perceive()'s own memo, which carries no
|
|
28
|
+
// trace gate at all. This memo is NOT an optional accelerator: recogniseImpl
|
|
29
|
+
// walks the query's perceived tree via foldTree, whose subtree-resolution
|
|
30
|
+
// fast path (see primitives.ts) skips invoking `visit` — and therefore
|
|
31
|
+
// skips EMITTING SITES — for any subtree already cached in
|
|
32
|
+
// ctx._resolvedSubtrees. A multi-turn conversation's stable-prefix fold
|
|
33
|
+
// deliberately shares node OBJECTS across turns, so by the second call on
|
|
34
|
+
// the exact same bytes, large swaths of the tree are already cached and
|
|
35
|
+
// foldTree stops short of recursing into them — a second recogniseImpl
|
|
36
|
+
// call on the SAME bytes is not idempotent; it silently finds FEWER sites
|
|
37
|
+
// than the first (observed live: 31 sites → 5 on an immediate repeat
|
|
38
|
+
// call). Skipping this memo "only while tracing" used to mean every
|
|
39
|
+
// traced turn re-ran recogniseImpl from scratch at every one of the many
|
|
40
|
+
// call sites that recognise the same query (cover, reason, articulate...),
|
|
41
|
+
// each subsequent call silently more incomplete than the last — measurably
|
|
42
|
+
// changing which mechanism grounds the answer, not just costing time. The
|
|
43
|
+
// trace step must still fire on every call regardless (a cache hit is not
|
|
44
|
+
// silent), so it is emitted here directly instead of only inside
|
|
45
|
+
// recogniseImpl.
|
|
46
|
+
if (ctx.recogniseMemo) {
|
|
29
47
|
const key = latin1Key(bytes);
|
|
30
48
|
const hit = ctx.recogniseMemo.get(key);
|
|
31
|
-
if (hit !== undefined)
|
|
49
|
+
if (hit !== undefined) {
|
|
50
|
+
ctx.trace?.step("recognise", [rItem(bytes, "query")], hit.sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
|
|
51
|
+
s.start,
|
|
52
|
+
s.end,
|
|
53
|
+
])), `decompose the query into ${hit.sites.length} learnt form(s) that ` +
|
|
54
|
+
`lead somewhere (over ${hit.leaves.length} perceived leaves) [cached]`);
|
|
32
55
|
return hit;
|
|
56
|
+
}
|
|
33
57
|
const fresh = recogniseImpl(ctx, bytes);
|
|
34
58
|
ctx.recogniseMemo.set(key, fresh);
|
|
35
59
|
return fresh;
|
|
@@ -71,9 +95,20 @@ function recogniseImpl(ctx, bytes) {
|
|
|
71
95
|
// silence. Atoms stay available as leaves (PASS-carried literals) and
|
|
72
96
|
// through exact tier-0 resolution regardless.
|
|
73
97
|
const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
|
|
98
|
+
// Distinct probes (structural exact match, canon fallback, edge trims at
|
|
99
|
+
// several offsets) can legitimately re-derive the SAME (start, end, id)
|
|
100
|
+
// site from different tree nodes — a wide edge-trim search is exactly
|
|
101
|
+
// this on purpose (see below). Duplicate site entries are not wrong
|
|
102
|
+
// evidence, but they double the weight cover's derivation search gives
|
|
103
|
+
// that span, distorting its cost model — the same span must count once.
|
|
104
|
+
const seen = new Set();
|
|
74
105
|
const emit = (start, end, id) => {
|
|
75
106
|
if (id < 0 && atomsAreHubs)
|
|
76
107
|
return;
|
|
108
|
+
const key = start + "," + end + "," + id;
|
|
109
|
+
if (seen.has(key))
|
|
110
|
+
return;
|
|
111
|
+
seen.add(key);
|
|
77
112
|
if (leadsSomewhere(ctx, id)) {
|
|
78
113
|
sites.push({ start, end, payload: id });
|
|
79
114
|
}
|
|
@@ -90,11 +125,96 @@ function recogniseImpl(ctx, bytes) {
|
|
|
90
125
|
// missed may still be a stored form under the response's equivalence
|
|
91
126
|
// (case, width, whitespace — whatever the injected canonicalizer says).
|
|
92
127
|
// O(subtree bytes) per miss, memoised per response; a no-op when no
|
|
93
|
-
// canonicalizer was injected or the store has no canon index.
|
|
94
|
-
|
|
128
|
+
// canonicalizer was injected or the store has no canon index. A raw
|
|
129
|
+
// leaf (n.kids === null) is single-byte and handled by the byte-atom
|
|
130
|
+
// path above instead — canon equivalence only applies to composites.
|
|
131
|
+
else if (n.kids !== null) {
|
|
95
132
|
const cid = canonResolve(ctx, bytes.subarray(start, end));
|
|
96
133
|
if (cid !== null)
|
|
97
134
|
emit(start, end, cid);
|
|
135
|
+
// The edge-trim fallbacks below remove 1 byte from a side; the
|
|
136
|
+
// remainder must still be a composite (>= 2 bytes, the same floor
|
|
137
|
+
// n.kids !== null enforces above) rather than degenerate into
|
|
138
|
+
// single-byte-atom territory, which atomIsHub already governs
|
|
139
|
+
// separately.
|
|
140
|
+
else if (end - start - 1 >= 2) {
|
|
141
|
+
// The chunk's own boundary is drawn by content geometry, not by
|
|
142
|
+
// any notion of "form" — it can include one edge byte the query's
|
|
143
|
+
// fold happened to attach here that the trained span never had
|
|
144
|
+
// (e.g. a separator from the preceding chunk). The core has no
|
|
145
|
+
// idea what that byte means; it only knows resolve()/canonResolve
|
|
146
|
+
// are self-verifying (hash-then-verify, same discipline as every
|
|
147
|
+
// content lookup here), so a blind one-byte-shorter guess on
|
|
148
|
+
// either edge costs nothing when wrong and is trustworthy when it
|
|
149
|
+
// hits. Two extra probes, only on the already-failed miss path.
|
|
150
|
+
const left = resolve(ctx, bytes.subarray(start + 1, end));
|
|
151
|
+
if (left !== null)
|
|
152
|
+
emit(start + 1, end, left);
|
|
153
|
+
const right = resolve(ctx, bytes.subarray(start, end - 1));
|
|
154
|
+
if (right !== null)
|
|
155
|
+
emit(start, end - 1, right);
|
|
156
|
+
// A misalignment wider than one byte (e.g. more than one edge
|
|
157
|
+
// separator swallowed) is not itself geometry-quantized — the
|
|
158
|
+
// WRITE side's canonical index (canonicalWindows) interns sliding
|
|
159
|
+
// W−1/W-length windows over leaf ids at EVERY offset, not just
|
|
160
|
+
// radix-aligned ones (see canonical.ts) — so the offset that
|
|
161
|
+
// recovers a trained span can be anything, not a multiple of W.
|
|
162
|
+
// What IS bounded is how far it's worth looking: chainReach(W)=W²,
|
|
163
|
+
// the same reach the canonical pass (tryChain) trusts for a chain
|
|
164
|
+
// rebuilt off the query's own fold. Every candidate offset is
|
|
165
|
+
// gated by store.findBranch(leafIds) first — the SAME cheap,
|
|
166
|
+
// fold-free existence check tryChain already uses — so the extra
|
|
167
|
+
// resolve() fold (the real cost) is only paid when a branch could
|
|
168
|
+
// plausibly exist there, not for every offset. The node itself is
|
|
169
|
+
// also bounded to chunk-scale (end - start <= W²): widening this at
|
|
170
|
+
// whole-query/root scale can rediscover a smaller subtree's own
|
|
171
|
+
// content as a second, overlapping site the structural walk's own
|
|
172
|
+
// finer recursion already emits correctly on its own — a duplicate
|
|
173
|
+
// that downstream derivation can stitch into a wrong answer.
|
|
174
|
+
const W = ctx.space.maxGroup;
|
|
175
|
+
for (let k = 1; end - start <= W * W && k <= W * W && start + k < end - 1; k++) {
|
|
176
|
+
const lIds = leafIdRun(ctx, bytes, start + k, end);
|
|
177
|
+
if (lIds !== null && store.findBranch(lIds) !== null) {
|
|
178
|
+
const eLeft = resolve(ctx, bytes.subarray(start + k, end));
|
|
179
|
+
if (eLeft !== null)
|
|
180
|
+
emit(start + k, end, eLeft);
|
|
181
|
+
}
|
|
182
|
+
const rIds = leafIdRun(ctx, bytes, start, end - k);
|
|
183
|
+
if (rIds !== null && store.findBranch(rIds) !== null) {
|
|
184
|
+
const eRight = resolve(ctx, bytes.subarray(start, end - k));
|
|
185
|
+
if (eRight !== null)
|
|
186
|
+
emit(start, end - k, eRight);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// A REAL extra word at the left edge (a discourse connective like
|
|
190
|
+
// "And " prepended to a follow-up turn — not boundary noise, actual
|
|
191
|
+
// content the injected canonicalizer has no equivalence for) shows
|
|
192
|
+
// up as a canon-miss too big for the chunk-scale search above: the
|
|
193
|
+
// turn is its OWN segment (stable-prefix folded independently — see
|
|
194
|
+
// mind.ts's _growContext), so it can be turn/segment-scale, not
|
|
195
|
+
// chunk-scale. Widening the size bound itself reopens the root-
|
|
196
|
+
// scale false-positive this module already fixed once (test/46);
|
|
197
|
+
// widening the SEARCH instead — trying every position up to W
|
|
198
|
+
// chunk-widths from the left edge that the query's OWN fold treats
|
|
199
|
+
// as a chunk boundary (`starts`, the same set the canonical pass
|
|
200
|
+
// privileges with full chain reach) — does not: `starts.has(p)` is
|
|
201
|
+
// fold EVIDENCE the query produced on its own (a leaf-parent chunk
|
|
202
|
+
// like "And " is visited, and its start added to `starts`, before
|
|
203
|
+
// this composite ever runs — foldTree is post-order), never a blind
|
|
204
|
+
// guess. Bounded to W candidates, each an O(1) set lookup before
|
|
205
|
+
// paying for the real canonResolve fold — canonResolve, not
|
|
206
|
+
// resolve()/findBranch, because the gap here is often exactly the
|
|
207
|
+
// kind of equivalence (case, in the live trace) canon exists for,
|
|
208
|
+
// not just an exact-content coincidence.
|
|
209
|
+
for (let k = 1; k <= W; k++) {
|
|
210
|
+
const p = start + k * W;
|
|
211
|
+
if (p >= end - 1 || !starts.has(p))
|
|
212
|
+
continue;
|
|
213
|
+
const cid = canonResolve(ctx, bytes.subarray(p, end));
|
|
214
|
+
if (cid !== null)
|
|
215
|
+
emit(p, end, cid);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
98
218
|
}
|
|
99
219
|
if (isChunk(n)) {
|
|
100
220
|
starts.add(start);
|