@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,14 +8,39 @@
|
|
|
8
8
|
// attention for the rest of the pipeline to follow.
|
|
9
9
|
import { isChunk } from "../sema.js";
|
|
10
10
|
import { lightestDerivation, } from "../derive/src/index.js";
|
|
11
|
-
import { consensusFloor, dominates, estimatorNoise } from "../geometry.js";
|
|
11
|
+
import { composeStructuralGist, consensusFloor, dominates, estimatorNoise, } from "../geometry.js";
|
|
12
12
|
import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
|
|
13
13
|
import { recognise } from "./recognition.js";
|
|
14
14
|
import { leafIdRun } from "./canonical.js";
|
|
15
|
-
import { corpusN, edgeAncestors } from "./traverse.js";
|
|
16
|
-
import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, walkCache, } from "./junction.js";
|
|
15
|
+
import { corpusN, edgeAncestors, hubBound } from "./traverse.js";
|
|
16
|
+
import { cachedRead, junctionContainersFrom, junctionSeeds, junctionSynonyms, loadJunctionSynonymSides, walkCache, } from "./junction.js";
|
|
17
17
|
import { indexOf } from "../bytes.js";
|
|
18
|
-
import { rNode, traceDerivation } from "./trace.js";
|
|
18
|
+
import { rItem, rNode, traceDerivation } from "./trace.js";
|
|
19
|
+
function newTraceDraft(perceivedCount) {
|
|
20
|
+
return {
|
|
21
|
+
perceivedCount,
|
|
22
|
+
regions: [],
|
|
23
|
+
crossRegionJunctionVotes: [],
|
|
24
|
+
supersededOrdinaryVotes: 0,
|
|
25
|
+
anchors: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Serialise the shared `reachMemo` into the plain, authoritative saturation
|
|
29
|
+
* profile (spec §5) — every distinct node any tier's `edgeAncestors` call
|
|
30
|
+
* climbed from during this response, in insertion (first-consulted) order. */
|
|
31
|
+
function serialiseReaches(reachMemo) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const [node, r] of reachMemo) {
|
|
34
|
+
out.push({
|
|
35
|
+
node,
|
|
36
|
+
roots: [...r.roots],
|
|
37
|
+
contextsReached: r.contextsReached,
|
|
38
|
+
saturated: r.saturated,
|
|
39
|
+
...(r.saturation ? { saturation: r.saturation } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
19
44
|
// ── Public entry points ───────────────────────────────────────────────────
|
|
20
45
|
/** Climb the query's perceived byte regions up the structural DAG via
|
|
21
46
|
* resonance, pool the evidence, and return only the ROOT points of
|
|
@@ -24,12 +49,21 @@ export async function climbAttention(ctx, query, k, mode = "inverse") {
|
|
|
24
49
|
return (await climbAttentionAll(ctx, query, k, mode)).roots;
|
|
25
50
|
}
|
|
26
51
|
/** Full read-out of one consensus climb: both the roots (dominant points of
|
|
27
|
-
* attention) and the entire ranked list. Cached via ctx.climbMemo
|
|
28
|
-
*
|
|
52
|
+
* attention) and the entire ranked list. Cached via ctx.climbMemo, ALWAYS —
|
|
53
|
+
* see {@link recognise} for why this memo (and recognise()'s own) must
|
|
54
|
+
* never be skipped while tracing: computeAttention's collectRegions walks
|
|
55
|
+
* the query's perceived tree via the same foldTree whose subtree-resolution
|
|
56
|
+
* fast path makes a second call on identical bytes non-idempotent once
|
|
57
|
+
* ctx._resolvedSubtrees is warm (which a multi-turn conversation's shared
|
|
58
|
+
* prefix subtrees guarantee by the second turn). A cache hit still emits
|
|
59
|
+
* a trace step — abbreviated, since the full per-sub-region voting detail
|
|
60
|
+
* {@link traceAttention} builds isn't preserved by the cached read-out —
|
|
61
|
+
* so a traced response is never silently blacked out for a repeated
|
|
62
|
+
* query. */
|
|
29
63
|
export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
|
|
30
64
|
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
31
|
-
// respondTurn().
|
|
32
|
-
if (ctx.climbMemo
|
|
65
|
+
// respondTurn().
|
|
66
|
+
if (ctx.climbMemo) {
|
|
33
67
|
const contentKey = latin1Key(query);
|
|
34
68
|
const modeKey = `${k}:${mode}`;
|
|
35
69
|
let byRead = ctx.climbMemo.get(contentKey);
|
|
@@ -37,8 +71,24 @@ export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
|
|
|
37
71
|
ctx.climbMemo.set(contentKey, byRead = new Map());
|
|
38
72
|
}
|
|
39
73
|
const hit = byRead.get(modeKey);
|
|
40
|
-
if (hit !== undefined)
|
|
74
|
+
if (hit !== undefined) {
|
|
75
|
+
// Cache-hit exit (spec §9): the abbreviated payload shape — only what
|
|
76
|
+
// is actually stored in the cached AttentionRead is reported. No
|
|
77
|
+
// candidate, reach, saturation, pooling or anchor detail is fabricated
|
|
78
|
+
// (that per-region detail was never retained by the memo).
|
|
79
|
+
const data = ctx.trace
|
|
80
|
+
? {
|
|
81
|
+
version: 1,
|
|
82
|
+
cache: { hit: true, detailAvailable: false },
|
|
83
|
+
config: { annK: k, crossRegionProbeLimit: k, mode },
|
|
84
|
+
candidates: { perceived: 0, recognised: 0, total: 0 },
|
|
85
|
+
result: hit,
|
|
86
|
+
}
|
|
87
|
+
: undefined;
|
|
88
|
+
ctx.trace?.step("climbConsensus", [rItem(query, "query")], hit.roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)), `(cached) consensus already computed for this query — ` +
|
|
89
|
+
`${hit.roots.length} point(s) of attention`, undefined, data);
|
|
41
90
|
return hit;
|
|
91
|
+
}
|
|
42
92
|
const read = await computeAttention(ctx, query, k, mode);
|
|
43
93
|
byRead.set(modeKey, read);
|
|
44
94
|
return read;
|
|
@@ -48,6 +98,7 @@ export async function climbAttentionAll(ctx, query, k, mode = "inverse") {
|
|
|
48
98
|
// ── Pipeline ──────────────────────────────────────────────────────────────
|
|
49
99
|
export async function computeAttention(ctx, query, k, mode) {
|
|
50
100
|
const regions = collectRegions(ctx, query);
|
|
101
|
+
const perceivedCount = regions.length;
|
|
51
102
|
// Recognised sites carry structural evidence that perceived sub-regions
|
|
52
103
|
// miss: a word crossing a W-boundary is split into chunks whose partial
|
|
53
104
|
// gists may not resonate distinctively, but the SITE (content-addressed,
|
|
@@ -66,20 +117,34 @@ export async function computeAttention(ctx, query, k, mode) {
|
|
|
66
117
|
known: true, // a recognised site IS a stored form
|
|
67
118
|
});
|
|
68
119
|
}
|
|
69
|
-
|
|
120
|
+
// The trace draft (spec §9): allocated ONLY when a trace was requested —
|
|
121
|
+
// every downstream consumer gates its own writes on `td?` / `if (td)`, so
|
|
122
|
+
// an untraced climb pays zero allocation for this instrumentation.
|
|
123
|
+
const td = ctx.trace
|
|
124
|
+
? newTraceDraft(perceivedCount)
|
|
125
|
+
: undefined;
|
|
126
|
+
const cfg0 = {
|
|
127
|
+
k,
|
|
128
|
+
mode,
|
|
129
|
+
perceivedCount,
|
|
130
|
+
totalRegions: regions.length,
|
|
131
|
+
};
|
|
132
|
+
if (regions.length === 0) {
|
|
133
|
+
traceAttention(ctx, [], [], [], undefined, td, cfg0);
|
|
70
134
|
return { roots: [], ranked: [] };
|
|
135
|
+
}
|
|
71
136
|
const N = corpusN(ctx);
|
|
72
137
|
// One climb per distinct anchor for the WHOLE query: regions sharing a
|
|
73
138
|
// chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
|
|
74
139
|
// re-reading the anchor's full edge fan-out from the store.
|
|
75
140
|
const reachMemo = new Map();
|
|
76
|
-
const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo);
|
|
141
|
+
const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
|
|
77
142
|
// ── Cross-region: DIRECT region-to-region interaction ─────────────────
|
|
78
143
|
// Two regions whose individual climbs land on DIFFERENT contexts leave
|
|
79
144
|
// their JOINT context — the learnt whole that contains BOTH — with no
|
|
80
145
|
// vote. crossRegionVotes recovers it by the bridge's content-addressed
|
|
81
146
|
// junction ascent (see the note above the function).
|
|
82
|
-
const cross = await crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo);
|
|
147
|
+
const cross = await crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td);
|
|
83
148
|
// A vote SUPERSEDED by exact joint evidence (its bytes literally live
|
|
84
149
|
// inside the joint container, yet it climbed elsewhere — grid aliasing)
|
|
85
150
|
// is dropped, not down-weighted: the joint container explains it away.
|
|
@@ -89,14 +154,33 @@ export async function computeAttention(ctx, query, k, mode) {
|
|
|
89
154
|
...cross.votes,
|
|
90
155
|
]
|
|
91
156
|
: rvs.votes;
|
|
157
|
+
// Mark, on the per-region trace, the source region of every superseded
|
|
158
|
+
// ordinary vote (spec §4's final rule) — an explicit pass over the exact
|
|
159
|
+
// set crossRegionVotes' explaining-away logic removed, never inferred
|
|
160
|
+
// from `absorbed`.
|
|
161
|
+
if (td && cross.superseded.size > 0) {
|
|
162
|
+
for (const rv of cross.superseded) {
|
|
163
|
+
const region = td.regions.find((r) => r.span[0] === rv.start && r.span[1] === rv.end);
|
|
164
|
+
if (region)
|
|
165
|
+
region.superseded = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
92
168
|
// ──────────────────────────────────────────────────────────────────────
|
|
169
|
+
const cfg = { ...cfg0, N, reachMemo };
|
|
93
170
|
if (allVotes.length === 0) {
|
|
94
|
-
traceAttention(ctx, regions, rvs.voters, []);
|
|
171
|
+
traceAttention(ctx, regions, rvs.voters, [], undefined, td, cfg);
|
|
95
172
|
return { roots: [], ranked: [] };
|
|
96
173
|
}
|
|
97
174
|
const sat = detectSaturated(ctx, regions, rvs.saturated);
|
|
98
|
-
|
|
99
|
-
|
|
175
|
+
if (td) {
|
|
176
|
+
td.saturation = {
|
|
177
|
+
regionIntervals: sat.intervals.map((iv) => ({ ...iv })),
|
|
178
|
+
hasLeading: sat.hasLeading,
|
|
179
|
+
leadingEnd: sat.leadingEnd,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const pooled = poolVotes(ctx, allVotes, sat, N, td);
|
|
183
|
+
return commitVotes(ctx, pooled, sat, regions, rvs.voters, N, td, cfg);
|
|
100
184
|
}
|
|
101
185
|
export function collectRegions(ctx, query) {
|
|
102
186
|
const regions = [];
|
|
@@ -128,12 +212,41 @@ export function collectRegions(ctx, query) {
|
|
|
128
212
|
});
|
|
129
213
|
return regions;
|
|
130
214
|
}
|
|
131
|
-
export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
215
|
+
export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td) {
|
|
132
216
|
const regionSaturated = new Array(regions.length).fill(false);
|
|
133
217
|
const regionVotes = [];
|
|
134
218
|
const regionVoter = ctx.trace ? regions.map(() => null) : [];
|
|
135
219
|
for (let ri = 0; ri < regions.length; ri++) {
|
|
136
220
|
const { v, start, end, chunk, known } = regions[ri];
|
|
221
|
+
// Trace-only bookkeeping for this region — allocated only under `td`
|
|
222
|
+
// (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
|
|
223
|
+
// RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
|
|
224
|
+
// whose edgeAncestors reach was actually CONSULTED here (not merely
|
|
225
|
+
// returned by resonate) — the fallback/margin loops below add to it.
|
|
226
|
+
const examinedIds = td ? new Set() : undefined;
|
|
227
|
+
let annQueried = false;
|
|
228
|
+
let fallbackKind;
|
|
229
|
+
const recordRegion = (outcome, extra = {}) => {
|
|
230
|
+
if (!td)
|
|
231
|
+
return;
|
|
232
|
+
td.regions[ri] = {
|
|
233
|
+
index: ri,
|
|
234
|
+
source: ri < td.perceivedCount ? "perceived" : "recognised",
|
|
235
|
+
span: [start, end],
|
|
236
|
+
chunk,
|
|
237
|
+
known,
|
|
238
|
+
canonicalId: canonicalId ?? undefined,
|
|
239
|
+
canonicalUsable,
|
|
240
|
+
canonicalFailed,
|
|
241
|
+
annQueried,
|
|
242
|
+
annHitsReturned: hits ? hits.length : 0,
|
|
243
|
+
annHitsExamined: examinedIds ? examinedIds.size : 0,
|
|
244
|
+
outcome,
|
|
245
|
+
ordinaryVoteProduced: outcome === "voted",
|
|
246
|
+
superseded: false,
|
|
247
|
+
...extra,
|
|
248
|
+
};
|
|
249
|
+
};
|
|
137
250
|
// EXACT-FIRST: a chunk whose canonical anchor is content-addressed needs
|
|
138
251
|
// no estimator — identity is exact, so its score is 1 BY DEFINITION (the
|
|
139
252
|
// estimated cosine of a form with itself, minus quantisation noise, and
|
|
@@ -152,23 +265,35 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
|
152
265
|
(ctx.store.hasParents(canonicalId) ||
|
|
153
266
|
ctx.store.hasContainers(canonicalId));
|
|
154
267
|
let hits = null;
|
|
155
|
-
const ensureHits = async () =>
|
|
268
|
+
const ensureHits = async () => {
|
|
269
|
+
if (hits === null) {
|
|
270
|
+
hits = await ctx.store.resonate(v, k);
|
|
271
|
+
annQueried = true;
|
|
272
|
+
}
|
|
273
|
+
return hits;
|
|
274
|
+
};
|
|
156
275
|
const canonicalFailed = chunk && canonicalId === null;
|
|
157
276
|
let voterId;
|
|
158
277
|
let score;
|
|
159
278
|
let scoreId; // the node the score was measured against
|
|
279
|
+
let selectedSource;
|
|
160
280
|
if (canonicalUsable) {
|
|
161
281
|
voterId = canonicalId;
|
|
162
282
|
score = 1;
|
|
163
283
|
scoreId = canonicalId;
|
|
284
|
+
selectedSource = "canonical";
|
|
164
285
|
}
|
|
165
286
|
else {
|
|
166
287
|
const h = await ensureHits();
|
|
167
|
-
if (h.length === 0)
|
|
288
|
+
if (h.length === 0) {
|
|
289
|
+
recordRegion("no-ann-hit");
|
|
168
290
|
continue;
|
|
291
|
+
}
|
|
169
292
|
voterId = h[0].id;
|
|
170
293
|
score = h[0].score;
|
|
171
294
|
scoreId = h[0].id;
|
|
295
|
+
selectedSource = "ann";
|
|
296
|
+
examinedIds?.add(voterId);
|
|
172
297
|
}
|
|
173
298
|
let reach = edgeAncestors(ctx, voterId, N, reachMemo);
|
|
174
299
|
// A region's vote must not die with the TOP hit: `hits[1..k]` were
|
|
@@ -182,12 +307,15 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
|
182
307
|
if (h.id === voterId)
|
|
183
308
|
continue;
|
|
184
309
|
const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
|
|
310
|
+
examinedIds?.add(h.id);
|
|
185
311
|
if (r2.saturated || r2.roots.length > 0) {
|
|
186
312
|
ctx.trace?.step("anchorFallback", [rNode(ctx, voterId, "orphan-anchor", score)], [rNode(ctx, h.id, "anchor", h.score)], "the top-ranked anchor climbs to no context — a lower-ranked hit votes instead");
|
|
187
313
|
reach = r2;
|
|
188
314
|
voterId = h.id;
|
|
189
315
|
score = h.score;
|
|
190
316
|
scoreId = h.id;
|
|
317
|
+
selectedSource = "ann";
|
|
318
|
+
fallbackKind = "orphan";
|
|
191
319
|
break;
|
|
192
320
|
}
|
|
193
321
|
}
|
|
@@ -214,28 +342,56 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
|
214
342
|
if (h.score < score - band)
|
|
215
343
|
break; // hits are nearest-first
|
|
216
344
|
const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
|
|
345
|
+
examinedIds?.add(h.id);
|
|
217
346
|
if (!r2.saturated && r2.roots.length > 0) {
|
|
218
347
|
ctx.trace?.step("anchorFallback", [rNode(ctx, voterId, "saturated-anchor", score)], [rNode(ctx, h.id, "anchor", h.score)], "the top-ranked anchor is a saturated hub tied within estimator noise — the tied hit votes instead");
|
|
219
348
|
reach = r2;
|
|
220
349
|
voterId = h.id;
|
|
221
350
|
score = h.score;
|
|
222
351
|
scoreId = h.id;
|
|
352
|
+
selectedSource = "ann";
|
|
353
|
+
fallbackKind = "saturated-tie";
|
|
223
354
|
break;
|
|
224
355
|
}
|
|
225
356
|
}
|
|
226
357
|
}
|
|
227
358
|
regionSaturated[ri] = reach.saturated;
|
|
228
|
-
|
|
359
|
+
const selected = !td
|
|
360
|
+
? undefined
|
|
361
|
+
: (() => {
|
|
362
|
+
const rank = selectedSource === "ann"
|
|
363
|
+
? hits?.findIndex((h) => h.id === voterId)
|
|
364
|
+
: undefined;
|
|
365
|
+
return {
|
|
366
|
+
source: selectedSource,
|
|
367
|
+
node: voterId,
|
|
368
|
+
score,
|
|
369
|
+
...(rank !== undefined ? { rank } : {}),
|
|
370
|
+
...(fallbackKind ? { fallback: fallbackKind } : {}),
|
|
371
|
+
};
|
|
372
|
+
})();
|
|
373
|
+
if (reach.roots.length === 0) {
|
|
374
|
+
recordRegion("no-structural-reach", { selected, reachNode: voterId });
|
|
229
375
|
continue;
|
|
230
|
-
|
|
376
|
+
}
|
|
377
|
+
if (reach.saturated) {
|
|
378
|
+
recordRegion("saturated-abstention", { selected, reachNode: voterId });
|
|
231
379
|
continue;
|
|
380
|
+
}
|
|
232
381
|
// One IDF per region — dfWeight() and the focus weight used to compute
|
|
233
382
|
// the same logarithm independently.
|
|
234
383
|
const idf = Math.log(N / Math.max(1, reach.contextsReached));
|
|
235
384
|
const df = Math.log(1 + reach.contextsReached);
|
|
236
385
|
const wf = mode === "direct" ? df : mode === "combined" ? idf + df : idf;
|
|
237
|
-
if (wf <= 0)
|
|
386
|
+
if (wf <= 0) {
|
|
387
|
+
recordRegion("nonpositive-df-weight", {
|
|
388
|
+
selected,
|
|
389
|
+
reachNode: voterId,
|
|
390
|
+
idf,
|
|
391
|
+
dfWeight: wf,
|
|
392
|
+
});
|
|
238
393
|
continue;
|
|
394
|
+
}
|
|
239
395
|
// CONTRASTIVE-MARGIN GATE — the compensation the linear (byte-proportional)
|
|
240
396
|
// fold demands, applied to APPROXIMATE evidence only. Under the linear
|
|
241
397
|
// fold a resonance score reads "fraction of aligned shared bytes", so a
|
|
@@ -259,12 +415,14 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
|
259
415
|
// (reordered / paraphrased queries) below the floor so they grounded
|
|
260
416
|
// nothing. Gating at the noise floor keeps frame-echo suppression (a frame
|
|
261
417
|
// region's margin ≈ 0 is gated out) without penalising honest evidence.
|
|
418
|
+
let contrastiveMargin;
|
|
262
419
|
if (!known) {
|
|
263
420
|
let margin = score;
|
|
264
421
|
for (const h of await ensureHits()) {
|
|
265
422
|
if (h.id === voterId)
|
|
266
423
|
continue;
|
|
267
424
|
const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
|
|
425
|
+
examinedIds?.add(h.id);
|
|
268
426
|
if (r2.saturated || r2.roots.length === 0)
|
|
269
427
|
continue; // concludes nothing
|
|
270
428
|
if (sameRoots(r2.roots, reach.roots))
|
|
@@ -272,8 +430,19 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
|
272
430
|
margin = score - h.score; // hits are nearest-first: the best rival
|
|
273
431
|
break;
|
|
274
432
|
}
|
|
275
|
-
|
|
433
|
+
contrastiveMargin = margin;
|
|
434
|
+
const noiseFloor = estimatorNoise(ctx.store.D);
|
|
435
|
+
if (margin <= noiseFloor) {
|
|
436
|
+
recordRegion("contrastive-margin-rejection", {
|
|
437
|
+
selected,
|
|
438
|
+
reachNode: voterId,
|
|
439
|
+
idf,
|
|
440
|
+
dfWeight: wf,
|
|
441
|
+
contrastiveMargin: margin,
|
|
442
|
+
contrastiveNoiseFloor: noiseFloor,
|
|
443
|
+
});
|
|
276
444
|
continue;
|
|
445
|
+
}
|
|
277
446
|
}
|
|
278
447
|
// MUTUAL-EXPLANATION WEIGHT (angle + magnitude). Under the linear fold
|
|
279
448
|
// cos = shared/(‖r‖·‖h‖) with ‖·‖² = content bytes, so the old score²
|
|
@@ -310,6 +479,21 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
|
310
479
|
if (ctx.trace) {
|
|
311
480
|
regionVoter[ri] = { id: voterId, score, w: wf };
|
|
312
481
|
}
|
|
482
|
+
recordRegion("voted", {
|
|
483
|
+
selected,
|
|
484
|
+
reachNode: voterId,
|
|
485
|
+
idf,
|
|
486
|
+
dfWeight: wf,
|
|
487
|
+
...(contrastiveMargin !== undefined
|
|
488
|
+
? {
|
|
489
|
+
contrastiveMargin,
|
|
490
|
+
contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
|
|
491
|
+
}
|
|
492
|
+
: {}),
|
|
493
|
+
mutualWeight: mutual,
|
|
494
|
+
voteWeightPerRoot: w,
|
|
495
|
+
focusWeightPerRoot: wFocus,
|
|
496
|
+
});
|
|
313
497
|
}
|
|
314
498
|
return {
|
|
315
499
|
votes: regionVotes,
|
|
@@ -329,7 +513,7 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo) {
|
|
|
329
513
|
* merely logs alongside it. `votesIdf`/`support` are the same two
|
|
330
514
|
* read-outs {@link commitVotes} always gated on; only how they accumulate
|
|
331
515
|
* changed. */
|
|
332
|
-
export function poolVotes(ctx, regionVotes, sat, N) {
|
|
516
|
+
export function poolVotes(ctx, regionVotes, sat, N, td) {
|
|
333
517
|
const eligible = [];
|
|
334
518
|
for (let ri = 0; ri < regionVotes.length; ri++) {
|
|
335
519
|
const rv = regionVotes[ri];
|
|
@@ -339,6 +523,13 @@ export function poolVotes(ctx, regionVotes, sat, N) {
|
|
|
339
523
|
}
|
|
340
524
|
eligible.push(ri);
|
|
341
525
|
}
|
|
526
|
+
if (td) {
|
|
527
|
+
td.pooling = {
|
|
528
|
+
inputVotes: regionVotes.length,
|
|
529
|
+
eligibleVotes: eligible.length,
|
|
530
|
+
saturationMaskedVotes: regionVotes.length - eligible.length,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
342
533
|
const key = (it) => it.kind === "region"
|
|
343
534
|
? `r${it.ri}`
|
|
344
535
|
: it.kind === "anchor"
|
|
@@ -401,6 +592,7 @@ export function poolVotes(ctx, regionVotes, sat, N) {
|
|
|
401
592
|
const votesIdf = new Map();
|
|
402
593
|
const support = new Map();
|
|
403
594
|
const regionSupport = new Map();
|
|
595
|
+
const regionSpans = new Map();
|
|
404
596
|
const steps = [];
|
|
405
597
|
let order = 0;
|
|
406
598
|
for (const pc of pool.values()) {
|
|
@@ -409,6 +601,7 @@ export function poolVotes(ctx, regionVotes, sat, N) {
|
|
|
409
601
|
const premises = [];
|
|
410
602
|
const seenRi = new Set();
|
|
411
603
|
let breadthSum = 0;
|
|
604
|
+
const spans = [];
|
|
412
605
|
for (const c of pc.contributions) {
|
|
413
606
|
const p0 = c.premises[0].item;
|
|
414
607
|
if (p0.kind !== "region" || seenRi.has(p0.ri))
|
|
@@ -417,8 +610,10 @@ export function poolVotes(ctx, regionVotes, sat, N) {
|
|
|
417
610
|
const rv = regionVotes[p0.ri];
|
|
418
611
|
breadthSum += rv.absorbed ?? 1;
|
|
419
612
|
premises.push({ kind: "form", span: [rv.start, rv.end] });
|
|
613
|
+
spans.push([rv.start, rv.end]);
|
|
420
614
|
}
|
|
421
615
|
regionSupport.set(pc.item.id, breadthSum);
|
|
616
|
+
regionSpans.set(pc.item.id, spans);
|
|
422
617
|
steps.push({
|
|
423
618
|
order: order++,
|
|
424
619
|
move: "pool-vote",
|
|
@@ -448,12 +643,41 @@ export function poolVotes(ctx, regionVotes, sat, N) {
|
|
|
448
643
|
}
|
|
449
644
|
}
|
|
450
645
|
}
|
|
451
|
-
return { votes, votesIdf, support, regionSupport, steps };
|
|
646
|
+
return { votes, votesIdf, support, regionSupport, regionSpans, steps };
|
|
452
647
|
}
|
|
453
|
-
|
|
454
|
-
|
|
648
|
+
/** The number of DISTINCT clusters a root's contributing regions form —
|
|
649
|
+
* see Attention.clusters. Two regions belong to the same cluster iff the
|
|
650
|
+
* gap between them is strictly less than one river-fold quantum W: at
|
|
651
|
+
* that distance there is no room for a genuinely separate, independently
|
|
652
|
+
* perceivable unit of content between them (the same "smallest meaningful
|
|
653
|
+
* distinction" quantum {@link reachThreshold}'s own doc invokes). A gap
|
|
654
|
+
* of a full quantum or more means real, separate structure could sit
|
|
655
|
+
* between the two spans, so they count as independent corroboration.
|
|
656
|
+
* Strict `<` (not `<=`): verified against gap 3.1's own "gender equality"
|
|
657
|
+
* root, whose two genuine clusters sit EXACTLY W bytes apart — `<= W`
|
|
658
|
+
* would wrongly merge them into one and break that pinned requirement. */
|
|
659
|
+
function countClusters(spans, W) {
|
|
660
|
+
if (spans.length === 0)
|
|
661
|
+
return 0;
|
|
662
|
+
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
663
|
+
let clusters = 1;
|
|
664
|
+
let curEnd = sorted[0][1];
|
|
665
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
666
|
+
const [s, e] = sorted[i];
|
|
667
|
+
if (s - curEnd < W) {
|
|
668
|
+
curEnd = Math.max(curEnd, e);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
clusters++;
|
|
672
|
+
curEnd = e;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return clusters;
|
|
676
|
+
}
|
|
677
|
+
export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg) {
|
|
678
|
+
const { votes, votesIdf, support, regionSupport, regionSpans, steps } = pooled;
|
|
455
679
|
if (votes.size === 0) {
|
|
456
|
-
traceAttention(ctx, regions, regionVoter, [], steps);
|
|
680
|
+
traceAttention(ctx, regions, regionVoter, [], steps, td, cfg);
|
|
457
681
|
return { roots: [], ranked: [] };
|
|
458
682
|
}
|
|
459
683
|
// SCALE-INVARIANT confidence — see Attention.breadth's doc. regions.length
|
|
@@ -470,6 +694,7 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N) {
|
|
|
470
694
|
start: s.start,
|
|
471
695
|
end: s.end,
|
|
472
696
|
breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
|
|
697
|
+
clusters: countClusters(regionSpans.get(anchor) ?? [], ctx.space.maxGroup),
|
|
473
698
|
};
|
|
474
699
|
})
|
|
475
700
|
.sort((a, b) => b.vote - a.vote);
|
|
@@ -488,23 +713,92 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N) {
|
|
|
488
713
|
const floor = consensusFloor(N);
|
|
489
714
|
const placed = [];
|
|
490
715
|
const roots = [];
|
|
491
|
-
|
|
716
|
+
const recordAnchor = (point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons) => {
|
|
717
|
+
if (!td)
|
|
718
|
+
return;
|
|
719
|
+
td.anchors.push({
|
|
720
|
+
anchor: point.anchor,
|
|
721
|
+
rank,
|
|
722
|
+
pooledVote: point.vote,
|
|
723
|
+
idfVote: votesIdf.get(point.anchor) ?? 0,
|
|
724
|
+
candidateBreadth: regions.length,
|
|
725
|
+
contributingVotes: regionSpans.get(point.anchor)?.length ?? 0,
|
|
726
|
+
contributingEvidence: regionSupport.get(point.anchor) ?? 0,
|
|
727
|
+
breadth: point.breadth,
|
|
728
|
+
contributingSpans: regionSpans.get(point.anchor) ?? [],
|
|
729
|
+
clusters: point.clusters,
|
|
730
|
+
commit: {
|
|
731
|
+
status,
|
|
732
|
+
dominant,
|
|
733
|
+
passesNaturalBreak,
|
|
734
|
+
passesConsensusFloor,
|
|
735
|
+
pastLeadingSaturation,
|
|
736
|
+
rejectionReasons,
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
};
|
|
740
|
+
for (let rank = 0; rank < ranked.length; rank++) {
|
|
741
|
+
const point = ranked[rank];
|
|
492
742
|
const absorbed = placed.some((p) => overlaps(point, p));
|
|
493
|
-
|
|
743
|
+
// Commit decisions are recorded LIVE, inside this loop, in the exact
|
|
744
|
+
// shape the gates below apply them — never reconstructed afterward from
|
|
745
|
+
// the final `roots` (spec §8's explicit requirement).
|
|
746
|
+
let status;
|
|
747
|
+
let dominant = false;
|
|
748
|
+
let passesNaturalBreak;
|
|
749
|
+
let passesConsensusFloor;
|
|
750
|
+
let pastLeadingSaturation;
|
|
751
|
+
const rejectionReasons = [];
|
|
752
|
+
if (absorbed) {
|
|
753
|
+
status = "overlap";
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
494
756
|
const pastLeading = !sat.hasLeading ||
|
|
495
757
|
roots.length === 0 || point.start >= sat.leadingEnd;
|
|
758
|
+
pastLeadingSaturation = pastLeading;
|
|
496
759
|
const vote = votesIdf.get(point.anchor) ?? 0;
|
|
497
|
-
if (
|
|
498
|
-
|
|
760
|
+
if (roots.length === 0) {
|
|
761
|
+
// The first non-overlapping root is DOMINANT and bypasses the two
|
|
762
|
+
// vote thresholds (it always grounds) — only the leading-saturation
|
|
763
|
+
// gate still applies to it.
|
|
764
|
+
dominant = true;
|
|
765
|
+
if (pastLeading) {
|
|
766
|
+
status = "root";
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
status = "rejected";
|
|
770
|
+
rejectionReasons.push("leading-saturation");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
passesNaturalBreak = vote >= rootCut;
|
|
775
|
+
passesConsensusFloor = vote >= floor;
|
|
776
|
+
if (passesNaturalBreak && passesConsensusFloor && pastLeading) {
|
|
777
|
+
status = "root";
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
status = "rejected";
|
|
781
|
+
if (!passesNaturalBreak)
|
|
782
|
+
rejectionReasons.push("below-natural-break");
|
|
783
|
+
if (!passesConsensusFloor) {
|
|
784
|
+
rejectionReasons.push("below-consensus-floor");
|
|
785
|
+
}
|
|
786
|
+
if (!pastLeading)
|
|
787
|
+
rejectionReasons.push("leading-saturation");
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (status === "root") {
|
|
499
791
|
roots.push(point);
|
|
500
792
|
}
|
|
501
793
|
else {
|
|
794
|
+
recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons);
|
|
502
795
|
continue;
|
|
503
796
|
}
|
|
504
797
|
}
|
|
798
|
+
recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons);
|
|
505
799
|
placed.push(point);
|
|
506
800
|
}
|
|
507
|
-
traceAttention(ctx, regions, regionVoter, roots, steps);
|
|
801
|
+
traceAttention(ctx, regions, regionVoter, roots, steps, td, cfg ? { ...cfg, naturalBreak: rootCut, consensusFloor: floor } : undefined, ranked);
|
|
508
802
|
return { roots, ranked };
|
|
509
803
|
}
|
|
510
804
|
export function detectSaturated(ctx, regions, saturated) {
|
|
@@ -607,63 +901,186 @@ export function naturalBreak(votes) {
|
|
|
607
901
|
}
|
|
608
902
|
return votes[breakAt - 1];
|
|
609
903
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
904
|
+
const VARIANT_KIND_ORDER = {
|
|
905
|
+
"exact-exact": -1,
|
|
906
|
+
"left-synonym": 0,
|
|
907
|
+
"right-synonym": 1,
|
|
908
|
+
"double-synonym": 2,
|
|
909
|
+
};
|
|
910
|
+
/** A node's structural gist, read directly from its own stored bytes — the
|
|
911
|
+
* repository's node → gist accessor: content is immutable and perception is
|
|
912
|
+
* a pure function of bytes, so re-perceiving a node's full stored bytes
|
|
913
|
+
* reproduces exactly the gist it was interned with. Never concatenates
|
|
914
|
+
* the sibling's bytes with anything else. */
|
|
915
|
+
function storedNodeGist(ctx, id) {
|
|
916
|
+
return gistOf(ctx, read(ctx, id));
|
|
917
|
+
}
|
|
918
|
+
/** A halo sibling's structural part, occupying the ORIGINAL query-region
|
|
919
|
+
* slot length — the sibling replaces only the DIRECTION, never the query's
|
|
920
|
+
* own bytes, position, or gap (see §6 of the spec this implements). */
|
|
921
|
+
function siblingPart(ctx, sibling, originalRegion) {
|
|
922
|
+
return {
|
|
923
|
+
v: storedNodeGist(ctx, sibling.id),
|
|
924
|
+
len: originalRegion.end - originalRegion.start,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/** Build, bound and order every mandatory structural variant (§7-8): the
|
|
928
|
+
* exact/exact composition is always kept; up to `ctx.cfg.haloQueryK`
|
|
929
|
+
* synonym variants (single- and double-synonym combined, one shared
|
|
930
|
+
* budget) are appended, ordered by confidence, then kind, then sibling id. */
|
|
931
|
+
export function buildStructuralVariants(ctx, ra, rb, sides) {
|
|
932
|
+
const exactLeft = { v: ra.v, len: ra.end - ra.start };
|
|
933
|
+
const exactRight = { v: rb.v, len: rb.end - rb.start };
|
|
934
|
+
const synonymVariants = [];
|
|
935
|
+
for (const ls of sides.leftSiblings) {
|
|
936
|
+
synonymVariants.push({
|
|
937
|
+
left: siblingPart(ctx, ls, ra),
|
|
938
|
+
right: exactRight,
|
|
939
|
+
kind: "left-synonym",
|
|
940
|
+
semanticConfidence: ls.score,
|
|
941
|
+
leftSiblingId: ls.id,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
for (const rs of sides.rightSiblings) {
|
|
945
|
+
synonymVariants.push({
|
|
946
|
+
left: exactLeft,
|
|
947
|
+
right: siblingPart(ctx, rs, rb),
|
|
948
|
+
kind: "right-synonym",
|
|
949
|
+
semanticConfidence: rs.score,
|
|
950
|
+
rightSiblingId: rs.id,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
for (const ls of sides.leftSiblings) {
|
|
954
|
+
for (const rs of sides.rightSiblings) {
|
|
955
|
+
synonymVariants.push({
|
|
956
|
+
left: siblingPart(ctx, ls, ra),
|
|
957
|
+
right: siblingPart(ctx, rs, rb),
|
|
958
|
+
kind: "double-synonym",
|
|
959
|
+
semanticConfidence: Math.min(ls.score, rs.score),
|
|
960
|
+
leftSiblingId: ls.id,
|
|
961
|
+
rightSiblingId: rs.id,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// §8: semantic confidence desc, then kind (left-synonym, right-synonym,
|
|
966
|
+
// double-synonym), then left sibling id asc, then right sibling id asc.
|
|
967
|
+
synonymVariants.sort((a, b) => b.semanticConfidence - a.semanticConfidence ||
|
|
968
|
+
VARIANT_KIND_ORDER[a.kind] - VARIANT_KIND_ORDER[b.kind] ||
|
|
969
|
+
(a.leftSiblingId ?? -1) - (b.leftSiblingId ?? -1) ||
|
|
970
|
+
(a.rightSiblingId ?? -1) - (b.rightSiblingId ?? -1));
|
|
971
|
+
const variants = [
|
|
972
|
+
{
|
|
973
|
+
left: exactLeft,
|
|
974
|
+
right: exactRight,
|
|
975
|
+
kind: "exact-exact",
|
|
976
|
+
semanticConfidence: 1,
|
|
977
|
+
},
|
|
978
|
+
...synonymVariants.slice(0, ctx.cfg.haloQueryK),
|
|
979
|
+
];
|
|
980
|
+
return { variants, exactLeft, exactRight };
|
|
981
|
+
}
|
|
982
|
+
/** Deterministic best-of tie-break for two proposals ranked for the SAME
|
|
983
|
+
* candidate id — effectiveScore, then annScore, then semanticConfidence,
|
|
984
|
+
* then variant kind, then sibling ids (§10). */
|
|
985
|
+
function betterProposal(a, b) {
|
|
986
|
+
if (a.effectiveScore !== b.effectiveScore) {
|
|
987
|
+
return a.effectiveScore > b.effectiveScore;
|
|
988
|
+
}
|
|
989
|
+
if (a.annScore !== b.annScore)
|
|
990
|
+
return a.annScore > b.annScore;
|
|
991
|
+
if (a.semanticConfidence !== b.semanticConfidence) {
|
|
992
|
+
return a.semanticConfidence > b.semanticConfidence;
|
|
993
|
+
}
|
|
994
|
+
if (VARIANT_KIND_ORDER[a.variant] !== VARIANT_KIND_ORDER[b.variant]) {
|
|
995
|
+
return VARIANT_KIND_ORDER[a.variant] < VARIANT_KIND_ORDER[b.variant];
|
|
996
|
+
}
|
|
997
|
+
if ((a.leftSiblingId ?? -1) !== (b.leftSiblingId ?? -1)) {
|
|
998
|
+
return (a.leftSiblingId ?? -1) < (b.leftSiblingId ?? -1);
|
|
999
|
+
}
|
|
1000
|
+
return (a.rightSiblingId ?? -1) < (b.rightSiblingId ?? -1);
|
|
1001
|
+
}
|
|
1002
|
+
/** The final approximate tier: compose every retained structural variant,
|
|
1003
|
+
* ANN-query each, merge proposals by candidate id, and validate the winner
|
|
1004
|
+
* through the SAME structural gates every other tier answers to (saturation,
|
|
1005
|
+
* roots, IDF, contrastive margin). Returns null when nothing survives. */
|
|
1006
|
+
export async function structuralResonance(ctx, query, ra, rb, sides, k, N, reachMemo,
|
|
1007
|
+
/** Each side's OWN individual climb roots (from voteRegions), when it cast
|
|
1008
|
+
* one — the self-evidence backstop structural-resonance needs and the
|
|
1009
|
+
* exact tier gets for free from literal byte containment (§11's whole
|
|
1010
|
+
* premise: recover a JOINT context neither side votes for alone). A
|
|
1011
|
+
* candidate whose reach is exactly one side's own conclusion is not new
|
|
1012
|
+
* evidence of a joint whole; it is that side's resonance rediscovering
|
|
1013
|
+
* itself through a synthetic gist still dominated by its own direction. */
|
|
1014
|
+
ownRootsA, ownRootsB) {
|
|
1015
|
+
const { variants } = buildStructuralVariants(ctx, ra, rb, sides);
|
|
1016
|
+
const middleBytes = query.subarray(ra.end, rb.start);
|
|
1017
|
+
const middlePart = middleBytes.length === 0
|
|
1018
|
+
? null
|
|
1019
|
+
: { v: perceive(ctx, middleBytes).v, len: middleBytes.length };
|
|
1020
|
+
const proposals = new Map();
|
|
1021
|
+
for (const variant of variants) {
|
|
1022
|
+
const parts = [variant.left];
|
|
1023
|
+
if (middlePart)
|
|
1024
|
+
parts.push(middlePart);
|
|
1025
|
+
parts.push(variant.right);
|
|
1026
|
+
const synthetic = composeStructuralGist(ctx.space, parts);
|
|
1027
|
+
const hits = await ctx.store.resonate(synthetic, k);
|
|
1028
|
+
for (const hit of hits) {
|
|
1029
|
+
const candidate = {
|
|
1030
|
+
id: hit.id,
|
|
1031
|
+
annScore: hit.score,
|
|
1032
|
+
semanticConfidence: variant.semanticConfidence,
|
|
1033
|
+
effectiveScore: hit.score * variant.semanticConfidence,
|
|
1034
|
+
variant: variant.kind,
|
|
1035
|
+
leftSiblingId: variant.leftSiblingId,
|
|
1036
|
+
rightSiblingId: variant.rightSiblingId,
|
|
1037
|
+
};
|
|
1038
|
+
const prev = proposals.get(hit.id);
|
|
1039
|
+
if (prev === undefined || betterProposal(candidate, prev)) {
|
|
1040
|
+
proposals.set(hit.id, candidate);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (proposals.size === 0)
|
|
1045
|
+
return null;
|
|
1046
|
+
const sorted = [...proposals.values()].sort((a, b) => b.effectiveScore - a.effectiveScore || a.id - b.id);
|
|
1047
|
+
let selected = null;
|
|
1048
|
+
let selectedReach = null;
|
|
1049
|
+
let selectedIdf = 0;
|
|
1050
|
+
let rival = null;
|
|
1051
|
+
for (const p of sorted) {
|
|
1052
|
+
const reach = edgeAncestors(ctx, p.id, N, reachMemo);
|
|
1053
|
+
if (reach.saturated || reach.roots.length === 0)
|
|
1054
|
+
continue;
|
|
1055
|
+
const idf = Math.log(N / Math.max(1, reach.contextsReached));
|
|
1056
|
+
if (idf <= 0)
|
|
1057
|
+
continue;
|
|
1058
|
+
// Self-evidence backstop (see the param doc above): a candidate that is
|
|
1059
|
+
// exactly one side's own already-voted conclusion carries no JOINT
|
|
1060
|
+
// evidence — skip it as if it never survived.
|
|
1061
|
+
if ((ownRootsA && sameRoots(reach.roots, ownRootsA)) ||
|
|
1062
|
+
(ownRootsB && sameRoots(reach.roots, ownRootsB)))
|
|
1063
|
+
continue;
|
|
1064
|
+
if (selected === null) {
|
|
1065
|
+
selected = p;
|
|
1066
|
+
selectedReach = reach;
|
|
1067
|
+
selectedIdf = idf;
|
|
1068
|
+
}
|
|
1069
|
+
else if (!sameRoots(reach.roots, selectedReach.roots)) {
|
|
1070
|
+
rival = p;
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (selected === null || selectedReach === null)
|
|
1075
|
+
return null;
|
|
1076
|
+
const margin = rival
|
|
1077
|
+
? selected.effectiveScore - rival.effectiveScore
|
|
1078
|
+
: selected.effectiveScore;
|
|
1079
|
+
if (margin <= estimatorNoise(ctx.store.D))
|
|
1080
|
+
return null;
|
|
1081
|
+
return { proposal: selected, reach: selectedReach, idf: selectedIdf };
|
|
1082
|
+
}
|
|
1083
|
+
async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
|
|
667
1084
|
// Candidate regions: every region that ALREADY CAST ITS OWN VOTE in
|
|
668
1085
|
// voteRegions — individually idf > 0, genuinely discriminative on its own,
|
|
669
1086
|
// just not necessarily for the SAME context as its partner. This is the
|
|
@@ -713,6 +1130,14 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
713
1130
|
regions[x].end <= regions[y].end &&
|
|
714
1131
|
regions[y].end - regions[y].start > regions[x].end - regions[x].start));
|
|
715
1132
|
const none = { votes: [], superseded: new Set() };
|
|
1133
|
+
if (td) {
|
|
1134
|
+
td.crossRegionSummary = {
|
|
1135
|
+
eligibleRegions: eligible.length,
|
|
1136
|
+
maximalRegions: cand.length,
|
|
1137
|
+
probeLimit: k,
|
|
1138
|
+
probesAttempted: 0, // updated below as probes accrue
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
716
1141
|
if (cand.length < 2)
|
|
717
1142
|
return none;
|
|
718
1143
|
cand.sort((x, y) => regions[x].start - regions[y].start || regions[x].end - regions[y].end);
|
|
@@ -777,92 +1202,174 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
777
1202
|
if (regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end))
|
|
778
1203
|
continue;
|
|
779
1204
|
probes++;
|
|
1205
|
+
if (td?.crossRegionSummary) {
|
|
1206
|
+
td.crossRegionSummary.probesAttempted = probes;
|
|
1207
|
+
}
|
|
780
1208
|
const left = query.subarray(ra.start, ra.end);
|
|
781
1209
|
const right = query.subarray(rb.start, rb.end);
|
|
782
1210
|
// Phrase-scale contract, exactly as the bridge: the glue between the two
|
|
783
1211
|
// forms may be up to W× the content it joins.
|
|
784
1212
|
const maxInterior = (left.length + right.length) * ctx.space.maxGroup;
|
|
785
1213
|
const cap = left.length + right.length + maxInterior;
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
1214
|
+
// The graded ladder (spec §1): exact DAG junction, then single-synonym,
|
|
1215
|
+
// then double-synonym, then — only when every DAG tier found nothing —
|
|
1216
|
+
// structural-resonance. `sides` (the two halo sibling lists) is loaded
|
|
1217
|
+
// ONCE and reused by junctionSynonyms AND structural-resonance, so no
|
|
1218
|
+
// ladder rung repeats a halo ANN query an earlier rung already paid for.
|
|
1219
|
+
const sides = await loadJunctionSynonymSides(ctx, left, right);
|
|
1220
|
+
let tier = "exact";
|
|
790
1221
|
let containers = junctionContainersFrom(ctx, left, right, cap, seedsOf(cand[a]), seedsOf(cand[b]), undefined, true);
|
|
791
1222
|
if (containers.length === 0) {
|
|
792
|
-
//
|
|
793
|
-
|
|
1223
|
+
// Tiers 2-4 — synonym containers (junctionSynonyms itself runs
|
|
1224
|
+
// single-synonym first, falling to double-synonym only when
|
|
1225
|
+
// single-synonym found nothing — see junction.ts).
|
|
1226
|
+
const syn = await junctionSynonyms(ctx, left, right, maxInterior, true, sides);
|
|
1227
|
+
if (syn.length > 0) {
|
|
1228
|
+
containers = syn;
|
|
1229
|
+
tier = syn[0].tier;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Tier 5 — structural-resonance ANN, the FINAL approximate proposal
|
|
1233
|
+
// path. Only reached when every DAG tier found NOTHING, and only when
|
|
1234
|
+
// there is no already-corroborated region between the endpoints (a
|
|
1235
|
+
// between-region with its own vote is evidence the gap already means
|
|
1236
|
+
// something specific — an ANN guess must not override it).
|
|
1237
|
+
let structuralPick = null;
|
|
1238
|
+
if (containers.length === 0) {
|
|
1239
|
+
// Structural-resonance composes each side's OWN gist directly (no
|
|
1240
|
+
// byte-containment truth backs it, unlike the DAG tiers) — so, unlike
|
|
1241
|
+
// the DAG ladder (which tolerates one approximate side because byte
|
|
1242
|
+
// containment cannot lie), the ANN tier requires BOTH sides to be
|
|
1243
|
+
// KNOWN (content-addressed, exact identities): an approximate chunk
|
|
1244
|
+
// fragment's own resonance is noise at any tier, and composing noise
|
|
1245
|
+
// into a synthetic gist only manufactures a plausible-looking but
|
|
1246
|
+
// spurious ANN neighbour, not evidence of a genuine joint whole.
|
|
1247
|
+
// PHRASE-SCALE CONTRACT — the same one the DAG tiers hold their glue
|
|
1248
|
+
// to (see maxInterior above): a junction, exact or approximate, is a
|
|
1249
|
+
// whole the two forms nearly exhaust, not two arbitrary landmarks
|
|
1250
|
+
// anywhere in a long, multi-topic query. Without this, structural-
|
|
1251
|
+
// resonance would pair opposite ends of an unrelated scaffolding-
|
|
1252
|
+
// dominated query and manufacture a plausible-looking ANN neighbour
|
|
1253
|
+
// for a "gap" that never was a phrase.
|
|
1254
|
+
// BOTH sides must be independently DISCRIMINATIVE (individually
|
|
1255
|
+
// voted — `strong`, not merely a content-addressed `known` chunk):
|
|
1256
|
+
// a shared, non-discriminative scaffolding run (a repeated system
|
|
1257
|
+
// preamble) can be `known` without ever being distinctive evidence
|
|
1258
|
+
// of anything, and composing its own gist into a synthetic query
|
|
1259
|
+
// manufactures a plausible-looking but spurious ANN neighbour. The
|
|
1260
|
+
// DAG tiers can tolerate one merely-`known` side because byte
|
|
1261
|
+
// containment cannot lie; structural-resonance has no such
|
|
1262
|
+
// backstop, so both sides earn their place here the same way an
|
|
1263
|
+
// ordinary approximate region earns its individual vote.
|
|
1264
|
+
const gap = rb.start - ra.end;
|
|
1265
|
+
if (between.length === 0 &&
|
|
1266
|
+
strong.has(cand[a]) && strong.has(cand[b]) &&
|
|
1267
|
+
ra.known && rb.known &&
|
|
1268
|
+
gap <= maxInterior) {
|
|
1269
|
+
const ownRootsA = rvs.votes.find((v) => v.start === ra.start && v.end === ra.end)?.roots;
|
|
1270
|
+
const ownRootsB = rvs.votes.find((v) => v.start === rb.start && v.end === rb.end)?.roots;
|
|
1271
|
+
structuralPick = await structuralResonance(ctx, query, ra, rb, sides, k, N, reachMemo, ownRootsA, ownRootsB);
|
|
1272
|
+
}
|
|
1273
|
+
if (structuralPick === null)
|
|
1274
|
+
continue;
|
|
1275
|
+
tier = "structural-resonance";
|
|
794
1276
|
}
|
|
795
|
-
if (containers.length === 0)
|
|
796
|
-
continue;
|
|
797
|
-
// N-ARY selection: the container covering the MOST remaining candidate
|
|
798
|
-
// forms wins (then tightest interior, then lowest id). Reads are
|
|
799
|
-
// cache hits — every container's bytes were already read by the walk.
|
|
800
|
-
//
|
|
801
|
-
// SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
|
|
802
|
-
// container joins forms the query mentions APART. When the container's
|
|
803
|
-
// own joined occurrence (left..right including its interior) is a
|
|
804
|
-
// literal substring of the query, the query already spells that phrase
|
|
805
|
-
// out contiguously — perception already voted with it, and grid shards
|
|
806
|
-
// of one phrase pairing "around" a gap chunk would merely rediscover
|
|
807
|
-
// the phrase they are shards of, then explain away its rivals.
|
|
808
1277
|
let best = null;
|
|
809
1278
|
let bestExtras = [];
|
|
810
1279
|
let bestCov = -1;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1280
|
+
let reach;
|
|
1281
|
+
let idf;
|
|
1282
|
+
let confidence;
|
|
1283
|
+
if (structuralPick !== null) {
|
|
1284
|
+
// A resonance proposal is NOT a Junction — there is no container to
|
|
1285
|
+
// read bytes from, so the self-evidence/contradiction/N-ary
|
|
1286
|
+
// machinery below (byte-verified against a real container) does not
|
|
1287
|
+
// apply; per spec §13, no N-ary extra-region coverage for resonance
|
|
1288
|
+
// proposals.
|
|
1289
|
+
best = { id: structuralPick.proposal.id, interior: new Uint8Array(0) };
|
|
1290
|
+
bestExtras = [];
|
|
1291
|
+
bestCov = rb.end - ra.start;
|
|
1292
|
+
reach = structuralPick.reach;
|
|
1293
|
+
idf = structuralPick.idf;
|
|
1294
|
+
confidence = structuralPick.proposal.effectiveScore;
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
// N-ARY selection: the container covering the MOST remaining candidate
|
|
1298
|
+
// forms wins (then tightest interior, then lowest id). Reads are
|
|
1299
|
+
// cache hits — every container's bytes were already read by the walk.
|
|
1300
|
+
//
|
|
1301
|
+
// SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
|
|
1302
|
+
// container joins forms the query mentions APART. When the container's
|
|
1303
|
+
// own joined occurrence (left..right including its interior) is a
|
|
1304
|
+
// literal substring of the query, the query already spells that phrase
|
|
1305
|
+
// out contiguously — perception already voted with it, and grid shards
|
|
1306
|
+
// of one phrase pairing "around" a gap chunk would merely rediscover
|
|
1307
|
+
// the phrase they are shards of, then explain away its rivals.
|
|
1308
|
+
for (const c of containers) {
|
|
1309
|
+
const bytes = cachedRead(ctx, cache, c.id, cap);
|
|
1310
|
+
const li = indexOf(bytes, left, 0);
|
|
1311
|
+
const ri = indexOf(bytes, right, 0);
|
|
1312
|
+
if (li >= 0 && ri >= 0) {
|
|
1313
|
+
const joined = bytes.subarray(Math.min(li, ri), Math.max(li + left.length, ri + right.length));
|
|
1314
|
+
if (indexOf(query, joined, 0) >= 0)
|
|
1315
|
+
continue; // query says it itself
|
|
1316
|
+
}
|
|
1317
|
+
// CONTRADICTION GUARD: a between-region already carrying its own
|
|
1318
|
+
// vote must actually recur in this container's bytes — otherwise
|
|
1319
|
+
// the container is a different learnt whole that happens to share
|
|
1320
|
+
// ra/rb, and letting it stand in for the gap would silently
|
|
1321
|
+
// override evidence the query itself already resolved there.
|
|
1322
|
+
if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
|
|
835
1323
|
continue;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1324
|
+
}
|
|
1325
|
+
let cov = left.length + right.length;
|
|
1326
|
+
const extras = [];
|
|
1327
|
+
for (const ei of cand) {
|
|
1328
|
+
if (ei === cand[a] || ei === cand[b] || consumed.has(ei))
|
|
1329
|
+
continue;
|
|
1330
|
+
const e = regions[ei];
|
|
1331
|
+
if (overlapsSpan(e, ra) || overlapsSpan(e, rb))
|
|
1332
|
+
continue;
|
|
1333
|
+
const eb = query.subarray(e.start, e.end);
|
|
1334
|
+
if (indexOf(bytes, eb, 0) >= 0) {
|
|
1335
|
+
extras.push(ei);
|
|
1336
|
+
cov += eb.length;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (cov > bestCov ||
|
|
1340
|
+
(cov === bestCov && best !== null &&
|
|
1341
|
+
(c.interior.length < best.interior.length ||
|
|
1342
|
+
(c.interior.length === best.interior.length && c.id < best.id)))) {
|
|
1343
|
+
best = c;
|
|
1344
|
+
bestExtras = extras;
|
|
1345
|
+
bestCov = cov;
|
|
840
1346
|
}
|
|
841
1347
|
}
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1348
|
+
if (best === null)
|
|
1349
|
+
continue; // every container was self-evidence
|
|
1350
|
+
const r = edgeAncestors(ctx, best.id, N, reachMemo);
|
|
1351
|
+
if (r.saturated || r.roots.length === 0)
|
|
1352
|
+
continue;
|
|
1353
|
+
const df = Math.log(N / Math.max(1, r.contextsReached));
|
|
1354
|
+
if (df <= 0)
|
|
1355
|
+
continue;
|
|
1356
|
+
reach = r;
|
|
1357
|
+
idf = df;
|
|
1358
|
+
// Confidence used by voting (spec §13): exact junction = 1;
|
|
1359
|
+
// single/double-synonym = the sibling(s)' score(s), carried on the
|
|
1360
|
+
// SynonymJunction the ladder selected.
|
|
1361
|
+
confidence = "confidence" in best ? best.confidence : 1;
|
|
850
1362
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
if (idf <= 0)
|
|
858
|
-
continue;
|
|
859
|
-
// EXACT joint evidence (score = 1): the container literally contains
|
|
860
|
-
// every composed form. Mutual-explanation weight over their COMBINED
|
|
861
|
-
// byte length — the same magnitude reading voteRegions uses, here with
|
|
862
|
-
// the estimator collapsed to certainty, so mutual = min(ratio, 1/ratio).
|
|
1363
|
+
// MUTUAL-EXPLANATION WEIGHT — the same formula for every tier, with
|
|
1364
|
+
// `confidence` collapsed to certainty (1) for exact evidence: under
|
|
1365
|
+
// that collapse this is byte-for-byte the old exact-only formula
|
|
1366
|
+
// (min(1,ratio)·min(1,1/ratio)). For structural-resonance,
|
|
1367
|
+
// `confidence` is already annScore·semanticConfidence — never
|
|
1368
|
+
// multiplied a second time.
|
|
863
1369
|
const lenR = Math.max(1, bestCov);
|
|
864
1370
|
const ratio = Math.sqrt(Math.max(1, ctx.store.contentLen(best.id, lenR * ctx.store.D)) / lenR);
|
|
865
|
-
const mutual = Math.min(1,
|
|
1371
|
+
const mutual = Math.min(1, confidence * ratio) *
|
|
1372
|
+
Math.min(1, confidence / ratio);
|
|
866
1373
|
const w = (mutual * idf) / reach.roots.length;
|
|
867
1374
|
let spanStart = ra.start;
|
|
868
1375
|
let spanEnd = rb.end;
|
|
@@ -884,16 +1391,33 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
884
1391
|
// for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
|
|
885
1392
|
// field) must credit the junction with all of it, not just the ONE
|
|
886
1393
|
// pooled axiom it collapses to.
|
|
887
|
-
|
|
888
|
-
|
|
1394
|
+
// Only EXACT DAG evidence may explain away ordinary votes (spec §15).
|
|
1395
|
+
// Single-synonym, double-synonym, and structural-resonance may ADD
|
|
1396
|
+
// supporting evidence but never remove it: their evidence is itself
|
|
1397
|
+
// approximate (a sibling substitution, or an ANN guess), so treating
|
|
1398
|
+
// their byte-containment the way exact containment is treated would
|
|
1399
|
+
// let an approximation override a genuine, independently-voted region.
|
|
889
1400
|
let explainedAway = 0;
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1401
|
+
// Exact set of ORIGINAL region indices this junction explained away —
|
|
1402
|
+
// recorded live as `superseded.add` fires (spec §3's explicit rule:
|
|
1403
|
+
// never inferred from `absorbed` afterward).
|
|
1404
|
+
const explainedAwayIndices = [];
|
|
1405
|
+
if (tier === "exact") {
|
|
1406
|
+
const containerBytes = cachedRead(ctx, cache, best.id, cap);
|
|
1407
|
+
const jointRoots = new Set(reach.roots);
|
|
1408
|
+
for (const rv of rvs.votes) {
|
|
1409
|
+
if (rv.roots.some((r) => jointRoots.has(r)))
|
|
1410
|
+
continue;
|
|
1411
|
+
const bytes = query.subarray(rv.start, rv.end);
|
|
1412
|
+
if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
|
|
1413
|
+
superseded.add(rv);
|
|
1414
|
+
explainedAway++;
|
|
1415
|
+
if (td) {
|
|
1416
|
+
const idx = regions.findIndex((r) => r.start === rv.start && r.end === rv.end);
|
|
1417
|
+
if (idx >= 0)
|
|
1418
|
+
explainedAwayIndices.push(idx);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
897
1421
|
}
|
|
898
1422
|
}
|
|
899
1423
|
out.push({
|
|
@@ -905,28 +1429,61 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
905
1429
|
wFocus: w,
|
|
906
1430
|
absorbed: 1 + explainedAway,
|
|
907
1431
|
});
|
|
1432
|
+
if (td) {
|
|
1433
|
+
td.crossRegionJunctionVotes.push({
|
|
1434
|
+
container: best.id,
|
|
1435
|
+
span: [spanStart, spanEnd],
|
|
1436
|
+
roots: [...reach.roots],
|
|
1437
|
+
sourceRegionIndices: [cand[a], cand[b], ...bestExtras],
|
|
1438
|
+
explainedAwayRegionIndices: explainedAwayIndices,
|
|
1439
|
+
absorbed: 1 + explainedAway,
|
|
1440
|
+
tier,
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
908
1443
|
const label = [cand[a], cand[b], ...bestExtras]
|
|
909
1444
|
.sort((x, y) => regions[x].start - regions[y].start)
|
|
910
1445
|
.map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
|
|
911
1446
|
.join(" ▸ ");
|
|
1447
|
+
const tierNote = tier === "exact"
|
|
1448
|
+
? `junction node ${best.id}` +
|
|
1449
|
+
(best.interior.length === 0
|
|
1450
|
+
? " (adjacent)"
|
|
1451
|
+
: ` (interior "${dec(best.interior)}")`) +
|
|
1452
|
+
", by content-addressed ascent"
|
|
1453
|
+
: tier === "structural-resonance"
|
|
1454
|
+
? `structurally-composed ANN proposal, node ${best.id} — the query ` +
|
|
1455
|
+
`structurally composed the endpoint regions, the real middle-` +
|
|
1456
|
+
`query structure, and the selected halo-sibling endpoint ` +
|
|
1457
|
+
`direction(s) (variant ${structuralPick.proposal.variant}, ` +
|
|
1458
|
+
`annScore ${structuralPick.proposal.annScore.toFixed(3)} × ` +
|
|
1459
|
+
`semanticConfidence ${structuralPick.proposal.semanticConfidence.toFixed(3)} = effectiveScore ${structuralPick.proposal.effectiveScore.toFixed(3)}); it did not concatenate endpoint bytes or rewrite the query`
|
|
1460
|
+
: `${tier} junction node ${best.id}` +
|
|
1461
|
+
(best.interior.length === 0
|
|
1462
|
+
? " (adjacent)"
|
|
1463
|
+
: ` (interior "${dec(best.interior)}")`) +
|
|
1464
|
+
`, by halo-sibling DAG ascent (confidence ${confidence.toFixed(3)})`;
|
|
912
1465
|
ctx.trace?.step("crossRegion", [{ text: label, role: "pair" }], reach.roots.map((r) => ({
|
|
913
1466
|
text: dec(read(ctx, r)).slice(0, 60),
|
|
914
1467
|
node: r,
|
|
915
1468
|
role: "joint-context",
|
|
916
|
-
})), `${label} →
|
|
917
|
-
(best.interior.length === 0
|
|
918
|
-
? " (adjacent)"
|
|
919
|
-
: ` (interior "${dec(best.interior)}")`) +
|
|
920
|
-
` → ${reach.roots.length} context(s), by content-addressed ascent` +
|
|
1469
|
+
})), `${label} → ${tierNote} → ${reach.roots.length} context(s)` +
|
|
921
1470
|
(superseded.size > 0
|
|
922
1471
|
? `; ${superseded.size} aliasing vote(s) explained away`
|
|
923
1472
|
: ""));
|
|
924
1473
|
break; // ra is consumed — move to the next unconsumed candidate
|
|
925
1474
|
}
|
|
926
1475
|
}
|
|
1476
|
+
if (td)
|
|
1477
|
+
td.supersededOrdinaryVotes = superseded.size;
|
|
927
1478
|
return { votes: out, superseded };
|
|
928
1479
|
}
|
|
929
|
-
|
|
1480
|
+
/** Emit the "climbConsensus" step — the human-readable note this always
|
|
1481
|
+
* produced, now paired (when `ctx.trace` and `cfg` are both present) with
|
|
1482
|
+
* the structured {@link ClimbConsensusData} payload on the SAME step's
|
|
1483
|
+
* `data` field. Every exit of {@link computeAttention} funnels through
|
|
1484
|
+
* here, so instrumentation and the existing rationale text can never drift
|
|
1485
|
+
* apart — see the instrumentation spec's §9 "every exit path". */
|
|
1486
|
+
export function traceAttention(ctx, regions, regionVoter, roots, steps = [], td, cfg, ranked = roots) {
|
|
930
1487
|
if (!ctx.trace)
|
|
931
1488
|
return;
|
|
932
1489
|
const voters = [];
|
|
@@ -943,9 +1500,50 @@ export function traceAttention(ctx, regions, regionVoter, roots, steps = []) {
|
|
|
943
1500
|
// shape {@link GraphSearch}'s own cover steps take (see traceDerivation).
|
|
944
1501
|
if (steps.length > 0)
|
|
945
1502
|
traceDerivation(ctx, steps);
|
|
1503
|
+
const data = (td && cfg)
|
|
1504
|
+
? {
|
|
1505
|
+
version: 1,
|
|
1506
|
+
cache: { hit: false, detailAvailable: true },
|
|
1507
|
+
config: {
|
|
1508
|
+
annK: cfg.k,
|
|
1509
|
+
crossRegionProbeLimit: cfg.k,
|
|
1510
|
+
mode: cfg.mode,
|
|
1511
|
+
...(cfg.N !== undefined ? { corpusN: cfg.N } : {}),
|
|
1512
|
+
dimension: ctx.store.D,
|
|
1513
|
+
...(cfg.N !== undefined ? { hubBound: hubBound(ctx) } : {}),
|
|
1514
|
+
estimatorNoise: estimatorNoise(ctx.store.D),
|
|
1515
|
+
...(cfg.naturalBreak !== undefined
|
|
1516
|
+
? { naturalBreak: cfg.naturalBreak }
|
|
1517
|
+
: {}),
|
|
1518
|
+
...(cfg.consensusFloor !== undefined
|
|
1519
|
+
? { consensusFloor: cfg.consensusFloor }
|
|
1520
|
+
: {}),
|
|
1521
|
+
},
|
|
1522
|
+
candidates: {
|
|
1523
|
+
perceived: cfg.perceivedCount,
|
|
1524
|
+
recognised: cfg.totalRegions - cfg.perceivedCount,
|
|
1525
|
+
total: cfg.totalRegions,
|
|
1526
|
+
},
|
|
1527
|
+
...(td.regions.length > 0 ? { regions: td.regions } : {}),
|
|
1528
|
+
...(cfg.reachMemo ? { reaches: serialiseReaches(cfg.reachMemo) } : {}),
|
|
1529
|
+
...(td.crossRegionSummary
|
|
1530
|
+
? {
|
|
1531
|
+
crossRegion: {
|
|
1532
|
+
...td.crossRegionSummary,
|
|
1533
|
+
junctionVotes: td.crossRegionJunctionVotes,
|
|
1534
|
+
supersededOrdinaryVotes: td.supersededOrdinaryVotes,
|
|
1535
|
+
},
|
|
1536
|
+
}
|
|
1537
|
+
: {}),
|
|
1538
|
+
...(td.saturation ? { saturation: td.saturation } : {}),
|
|
1539
|
+
...(td.pooling ? { pooling: td.pooling } : {}),
|
|
1540
|
+
...(td.anchors.length > 0 ? { anchors: td.anchors } : {}),
|
|
1541
|
+
result: { roots: [...roots], ranked: [...ranked] },
|
|
1542
|
+
}
|
|
1543
|
+
: undefined;
|
|
946
1544
|
t.done(roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)), roots.length === 0
|
|
947
1545
|
? `${regions.length} sub-regions climbed the DAG, but none agreed on a context`
|
|
948
1546
|
: roots.length === 1
|
|
949
1547
|
? `${voters.length} of ${regions.length} sub-regions voted; IDF-weighted consensus picked one context (vote ${roots[0].vote.toFixed(2)})`
|
|
950
|
-
: `${voters.length} of ${regions.length} sub-regions voted; consensus ordered ${roots.length} INDEPENDENT points of attention (votes ${roots.map((r) => r.vote.toFixed(2)).join(", ")})
|
|
1548
|
+
: `${voters.length} of ${regions.length} sub-regions voted; consensus ordered ${roots.length} INDEPENDENT points of attention (votes ${roots.map((r) => r.vote.toFixed(2)).join(", ")})`, data);
|
|
951
1549
|
}
|