@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
package/src/mind/attention.ts
CHANGED
|
@@ -25,23 +25,288 @@ import type {
|
|
|
25
25
|
Region,
|
|
26
26
|
RegionVote,
|
|
27
27
|
SaturationInfo,
|
|
28
|
+
SaturationStop,
|
|
28
29
|
} from "./types.js";
|
|
29
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
composeStructuralGist,
|
|
32
|
+
consensusFloor,
|
|
33
|
+
dominates,
|
|
34
|
+
estimatorNoise,
|
|
35
|
+
type StructuralPart,
|
|
36
|
+
} from "../geometry.js";
|
|
30
37
|
import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
|
|
31
38
|
import { recognise } from "./recognition.js";
|
|
32
39
|
import { leafIdRun } from "./canonical.js";
|
|
33
|
-
import { corpusN, edgeAncestors } from "./traverse.js";
|
|
40
|
+
import { corpusN, edgeAncestors, hubBound } from "./traverse.js";
|
|
34
41
|
import {
|
|
35
42
|
cachedRead,
|
|
43
|
+
type Junction,
|
|
36
44
|
junctionContainersFrom,
|
|
37
45
|
junctionSeeds,
|
|
38
46
|
junctionSynonyms,
|
|
47
|
+
type JunctionSynonymSides,
|
|
48
|
+
loadJunctionSynonymSides,
|
|
49
|
+
type SynonymJunction,
|
|
39
50
|
walkCache,
|
|
40
51
|
} from "./junction.js";
|
|
52
|
+
import type { Vec } from "../vec.js";
|
|
41
53
|
import { indexOf } from "../bytes.js";
|
|
42
54
|
import type { RationaleItem } from "./rationale.js";
|
|
43
55
|
import { rDeriv, rItem, rNode, traceDerivation } from "./trace.js";
|
|
44
56
|
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
58
|
+
// climbConsensus / inspectRationale instrumentation.
|
|
59
|
+
//
|
|
60
|
+
// Purely additive tracing over the consensus climb: it never changes an
|
|
61
|
+
// inference result or the human-readable rationale text traceAttention
|
|
62
|
+
// already produces (see below) — it only exposes, as one structured `data`
|
|
63
|
+
// payload on the SAME "climbConsensus" step, the machinery that produced
|
|
64
|
+
// that text: every structural saturation stop, candidate breadth versus
|
|
65
|
+
// evidence that actually contributed, and the decisions that removed or
|
|
66
|
+
// accepted evidence (§ objective of the instrumentation spec).
|
|
67
|
+
//
|
|
68
|
+
// The mutable collection buffers (the `TraceDraft` below) are allocated ONLY
|
|
69
|
+
// when `ctx.trace` is set — every call site that would otherwise push onto
|
|
70
|
+
// one of these arrays or sets is gated by `td?.` / `if (td)`, so a plain
|
|
71
|
+
// (untraced) climb pays exactly zero allocation for this instrumentation.
|
|
72
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
|
|
74
|
+
/** How the newly-added graded junction ladder (junction.ts / attention.ts's
|
|
75
|
+
* {@link CrossRegionTier}) is reported on a `junctionVotes` entry. The
|
|
76
|
+
* instrumentation spec this implements predates that ladder and only knew
|
|
77
|
+
* two tiers ("exact" | "synonym"); with the richer `CrossRegionTier` now
|
|
78
|
+
* the real shape of a junction vote's provenance, `junctionVotes[].tier`
|
|
79
|
+
* reports it DIRECTLY (the tier as-is: "exact" | "single-synonym" |
|
|
80
|
+
* "double-synonym" | "structural-resonance") rather than collapsing every
|
|
81
|
+
* non-exact tier into a lossy "synonym" bucket — the whole point of
|
|
82
|
+
* exposing `tier` here is to let a debugger tell a halo-sibling
|
|
83
|
+
* substitution apart from a structural-resonance ANN guess, which the
|
|
84
|
+
* spec's original two-value type cannot do. */
|
|
85
|
+
export type ClimbConsensusJunctionTier = CrossRegionTier;
|
|
86
|
+
|
|
87
|
+
export type RegionOutcome =
|
|
88
|
+
| "voted"
|
|
89
|
+
| "no-ann-hit"
|
|
90
|
+
| "no-structural-reach"
|
|
91
|
+
| "saturated-abstention"
|
|
92
|
+
| "nonpositive-df-weight"
|
|
93
|
+
| "contrastive-margin-rejection";
|
|
94
|
+
|
|
95
|
+
export interface ConsensusRegionTrace {
|
|
96
|
+
index: number;
|
|
97
|
+
source: "perceived" | "recognised";
|
|
98
|
+
span: [number, number];
|
|
99
|
+
chunk: boolean;
|
|
100
|
+
known: boolean;
|
|
101
|
+
|
|
102
|
+
canonicalId?: number;
|
|
103
|
+
canonicalUsable: boolean;
|
|
104
|
+
canonicalFailed: boolean;
|
|
105
|
+
|
|
106
|
+
annQueried: boolean;
|
|
107
|
+
annHitsReturned: number;
|
|
108
|
+
annHitsExamined: number;
|
|
109
|
+
|
|
110
|
+
selected?: {
|
|
111
|
+
source: "canonical" | "ann";
|
|
112
|
+
node: number;
|
|
113
|
+
rank?: number;
|
|
114
|
+
score: number;
|
|
115
|
+
fallback?: "orphan" | "saturated-tie";
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
reachNode?: number;
|
|
119
|
+
outcome: RegionOutcome;
|
|
120
|
+
|
|
121
|
+
idf?: number;
|
|
122
|
+
dfWeight?: number;
|
|
123
|
+
|
|
124
|
+
contrastiveMargin?: number;
|
|
125
|
+
contrastiveNoiseFloor?: number;
|
|
126
|
+
|
|
127
|
+
mutualWeight?: number;
|
|
128
|
+
voteWeightPerRoot?: number;
|
|
129
|
+
focusWeightPerRoot?: number;
|
|
130
|
+
|
|
131
|
+
ordinaryVoteProduced: boolean;
|
|
132
|
+
superseded: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ConsensusReachTrace {
|
|
136
|
+
node: number;
|
|
137
|
+
roots: number[];
|
|
138
|
+
contextsReached: number;
|
|
139
|
+
saturated: boolean;
|
|
140
|
+
saturation?: SaturationStop;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type AnchorRejectionReason =
|
|
144
|
+
| "below-natural-break"
|
|
145
|
+
| "below-consensus-floor"
|
|
146
|
+
| "leading-saturation";
|
|
147
|
+
|
|
148
|
+
export interface ConsensusAnchorTrace {
|
|
149
|
+
anchor: number;
|
|
150
|
+
rank: number;
|
|
151
|
+
|
|
152
|
+
pooledVote: number;
|
|
153
|
+
idfVote: number;
|
|
154
|
+
|
|
155
|
+
candidateBreadth: number;
|
|
156
|
+
contributingVotes: number;
|
|
157
|
+
contributingEvidence: number;
|
|
158
|
+
breadth: number;
|
|
159
|
+
contributingSpans: Array<[number, number]>;
|
|
160
|
+
clusters: number;
|
|
161
|
+
|
|
162
|
+
commit: {
|
|
163
|
+
status: "root" | "overlap" | "rejected";
|
|
164
|
+
dominant: boolean;
|
|
165
|
+
passesNaturalBreak?: boolean;
|
|
166
|
+
passesConsensusFloor?: boolean;
|
|
167
|
+
pastLeadingSaturation?: boolean;
|
|
168
|
+
rejectionReasons: AnchorRejectionReason[];
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface JunctionVoteTrace {
|
|
173
|
+
container: number;
|
|
174
|
+
span: [number, number];
|
|
175
|
+
roots: number[];
|
|
176
|
+
sourceRegionIndices: number[];
|
|
177
|
+
explainedAwayRegionIndices: number[];
|
|
178
|
+
absorbed: number;
|
|
179
|
+
tier?: ClimbConsensusJunctionTier;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ClimbConsensusData {
|
|
183
|
+
version: 1;
|
|
184
|
+
|
|
185
|
+
cache: {
|
|
186
|
+
hit: boolean;
|
|
187
|
+
detailAvailable: boolean;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
config: {
|
|
191
|
+
annK: number;
|
|
192
|
+
crossRegionProbeLimit: number;
|
|
193
|
+
mode: DFMode;
|
|
194
|
+
corpusN?: number;
|
|
195
|
+
dimension?: number;
|
|
196
|
+
hubBound?: number;
|
|
197
|
+
estimatorNoise?: number;
|
|
198
|
+
naturalBreak?: number;
|
|
199
|
+
consensusFloor?: number;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
candidates: {
|
|
203
|
+
perceived: number;
|
|
204
|
+
recognised: number;
|
|
205
|
+
total: number;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
regions?: ConsensusRegionTrace[];
|
|
209
|
+
reaches?: ConsensusReachTrace[];
|
|
210
|
+
|
|
211
|
+
crossRegion?: {
|
|
212
|
+
eligibleRegions: number;
|
|
213
|
+
maximalRegions: number;
|
|
214
|
+
probeLimit: number;
|
|
215
|
+
probesAttempted: number;
|
|
216
|
+
junctionVotes: JunctionVoteTrace[];
|
|
217
|
+
supersededOrdinaryVotes: number;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
saturation?: {
|
|
221
|
+
regionIntervals: Array<{ start: number; end: number }>;
|
|
222
|
+
hasLeading: boolean;
|
|
223
|
+
leadingEnd: number;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
pooling?: {
|
|
227
|
+
inputVotes: number;
|
|
228
|
+
eligibleVotes: number;
|
|
229
|
+
saturationMaskedVotes: number;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
anchors?: ConsensusAnchorTrace[];
|
|
233
|
+
|
|
234
|
+
result: AttentionRead;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The mutable collection buffers threaded through one traced consensus
|
|
238
|
+
* climb — allocated exactly once, in {@link computeAttention}, only when
|
|
239
|
+
* `ctx.trace` is set. Every field mirrors a `ClimbConsensusData` array/map,
|
|
240
|
+
* built incrementally as the pipeline runs so commit-time decisions (in
|
|
241
|
+
* particular) are recorded LIVE, not reconstructed afterward. */
|
|
242
|
+
interface TraceDraft {
|
|
243
|
+
perceivedCount: number;
|
|
244
|
+
regions: ConsensusRegionTrace[];
|
|
245
|
+
crossRegionJunctionVotes: JunctionVoteTrace[];
|
|
246
|
+
crossRegionSummary?: {
|
|
247
|
+
eligibleRegions: number;
|
|
248
|
+
maximalRegions: number;
|
|
249
|
+
probeLimit: number;
|
|
250
|
+
probesAttempted: number;
|
|
251
|
+
};
|
|
252
|
+
supersededOrdinaryVotes: number;
|
|
253
|
+
saturation?: {
|
|
254
|
+
regionIntervals: Array<{ start: number; end: number }>;
|
|
255
|
+
hasLeading: boolean;
|
|
256
|
+
leadingEnd: number;
|
|
257
|
+
};
|
|
258
|
+
pooling?: {
|
|
259
|
+
inputVotes: number;
|
|
260
|
+
eligibleVotes: number;
|
|
261
|
+
saturationMaskedVotes: number;
|
|
262
|
+
};
|
|
263
|
+
anchors: ConsensusAnchorTrace[];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The config/corpus context {@link traceAttention} needs to fill in
|
|
267
|
+
* `ClimbConsensusData.config` and `.result` at whichever exit fires —
|
|
268
|
+
* threaded down from {@link computeAttention} rather than re-derived, so
|
|
269
|
+
* every emission point reports the SAME numbers the real climb used. */
|
|
270
|
+
interface ClimbConsensusCfg {
|
|
271
|
+
k: number;
|
|
272
|
+
mode: DFMode;
|
|
273
|
+
perceivedCount: number;
|
|
274
|
+
totalRegions: number;
|
|
275
|
+
N?: number;
|
|
276
|
+
reachMemo?: ReadonlyMap<number, AncestorReach>;
|
|
277
|
+
naturalBreak?: number;
|
|
278
|
+
consensusFloor?: number;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function newTraceDraft(perceivedCount: number): TraceDraft {
|
|
282
|
+
return {
|
|
283
|
+
perceivedCount,
|
|
284
|
+
regions: [],
|
|
285
|
+
crossRegionJunctionVotes: [],
|
|
286
|
+
supersededOrdinaryVotes: 0,
|
|
287
|
+
anchors: [],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Serialise the shared `reachMemo` into the plain, authoritative saturation
|
|
292
|
+
* profile (spec §5) — every distinct node any tier's `edgeAncestors` call
|
|
293
|
+
* climbed from during this response, in insertion (first-consulted) order. */
|
|
294
|
+
function serialiseReaches(
|
|
295
|
+
reachMemo: ReadonlyMap<number, AncestorReach>,
|
|
296
|
+
): ConsensusReachTrace[] {
|
|
297
|
+
const out: ConsensusReachTrace[] = [];
|
|
298
|
+
for (const [node, r] of reachMemo) {
|
|
299
|
+
out.push({
|
|
300
|
+
node,
|
|
301
|
+
roots: [...r.roots],
|
|
302
|
+
contextsReached: r.contextsReached,
|
|
303
|
+
saturated: r.saturated,
|
|
304
|
+
...(r.saturation ? { saturation: r.saturation } : {}),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
45
310
|
// ── Public entry points ───────────────────────────────────────────────────
|
|
46
311
|
|
|
47
312
|
/** Climb the query's perceived byte regions up the structural DAG via
|
|
@@ -57,8 +322,17 @@ export async function climbAttention(
|
|
|
57
322
|
}
|
|
58
323
|
|
|
59
324
|
/** Full read-out of one consensus climb: both the roots (dominant points of
|
|
60
|
-
* attention) and the entire ranked list. Cached via ctx.climbMemo
|
|
61
|
-
*
|
|
325
|
+
* attention) and the entire ranked list. Cached via ctx.climbMemo, ALWAYS —
|
|
326
|
+
* see {@link recognise} for why this memo (and recognise()'s own) must
|
|
327
|
+
* never be skipped while tracing: computeAttention's collectRegions walks
|
|
328
|
+
* the query's perceived tree via the same foldTree whose subtree-resolution
|
|
329
|
+
* fast path makes a second call on identical bytes non-idempotent once
|
|
330
|
+
* ctx._resolvedSubtrees is warm (which a multi-turn conversation's shared
|
|
331
|
+
* prefix subtrees guarantee by the second turn). A cache hit still emits
|
|
332
|
+
* a trace step — abbreviated, since the full per-sub-region voting detail
|
|
333
|
+
* {@link traceAttention} builds isn't preserved by the cached read-out —
|
|
334
|
+
* so a traced response is never silently blacked out for a repeated
|
|
335
|
+
* query. */
|
|
62
336
|
export async function climbAttentionAll(
|
|
63
337
|
ctx: MindContext,
|
|
64
338
|
query: Uint8Array,
|
|
@@ -66,8 +340,8 @@ export async function climbAttentionAll(
|
|
|
66
340
|
mode: DFMode = "inverse",
|
|
67
341
|
): Promise<AttentionRead> {
|
|
68
342
|
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
69
|
-
// respondTurn().
|
|
70
|
-
if (ctx.climbMemo
|
|
343
|
+
// respondTurn().
|
|
344
|
+
if (ctx.climbMemo) {
|
|
71
345
|
const contentKey = latin1Key(query);
|
|
72
346
|
const modeKey = `${k}:${mode}`;
|
|
73
347
|
let byRead = ctx.climbMemo.get(contentKey);
|
|
@@ -75,7 +349,31 @@ export async function climbAttentionAll(
|
|
|
75
349
|
ctx.climbMemo.set(contentKey, byRead = new Map());
|
|
76
350
|
}
|
|
77
351
|
const hit = byRead.get(modeKey);
|
|
78
|
-
if (hit !== undefined)
|
|
352
|
+
if (hit !== undefined) {
|
|
353
|
+
// Cache-hit exit (spec §9): the abbreviated payload shape — only what
|
|
354
|
+
// is actually stored in the cached AttentionRead is reported. No
|
|
355
|
+
// candidate, reach, saturation, pooling or anchor detail is fabricated
|
|
356
|
+
// (that per-region detail was never retained by the memo).
|
|
357
|
+
const data: ClimbConsensusData | undefined = ctx.trace
|
|
358
|
+
? {
|
|
359
|
+
version: 1,
|
|
360
|
+
cache: { hit: true, detailAvailable: false },
|
|
361
|
+
config: { annK: k, crossRegionProbeLimit: k, mode },
|
|
362
|
+
candidates: { perceived: 0, recognised: 0, total: 0 },
|
|
363
|
+
result: hit,
|
|
364
|
+
}
|
|
365
|
+
: undefined;
|
|
366
|
+
ctx.trace?.step(
|
|
367
|
+
"climbConsensus",
|
|
368
|
+
[rItem(query, "query")],
|
|
369
|
+
hit.roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)),
|
|
370
|
+
`(cached) consensus already computed for this query — ` +
|
|
371
|
+
`${hit.roots.length} point(s) of attention`,
|
|
372
|
+
undefined,
|
|
373
|
+
data,
|
|
374
|
+
);
|
|
375
|
+
return hit;
|
|
376
|
+
}
|
|
79
377
|
const read = await computeAttention(ctx, query, k, mode);
|
|
80
378
|
byRead.set(modeKey, read);
|
|
81
379
|
return read;
|
|
@@ -92,6 +390,7 @@ export async function computeAttention(
|
|
|
92
390
|
mode: DFMode,
|
|
93
391
|
): Promise<AttentionRead> {
|
|
94
392
|
const regions = collectRegions(ctx, query);
|
|
393
|
+
const perceivedCount = regions.length;
|
|
95
394
|
|
|
96
395
|
// Recognised sites carry structural evidence that perceived sub-regions
|
|
97
396
|
// miss: a word crossing a W-boundary is split into chunks whose partial
|
|
@@ -112,14 +411,30 @@ export async function computeAttention(
|
|
|
112
411
|
});
|
|
113
412
|
}
|
|
114
413
|
|
|
115
|
-
|
|
414
|
+
// The trace draft (spec §9): allocated ONLY when a trace was requested —
|
|
415
|
+
// every downstream consumer gates its own writes on `td?` / `if (td)`, so
|
|
416
|
+
// an untraced climb pays zero allocation for this instrumentation.
|
|
417
|
+
const td: TraceDraft | undefined = ctx.trace
|
|
418
|
+
? newTraceDraft(perceivedCount)
|
|
419
|
+
: undefined;
|
|
420
|
+
const cfg0: ClimbConsensusCfg = {
|
|
421
|
+
k,
|
|
422
|
+
mode,
|
|
423
|
+
perceivedCount,
|
|
424
|
+
totalRegions: regions.length,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
if (regions.length === 0) {
|
|
428
|
+
traceAttention(ctx, [], [], [], undefined, td, cfg0);
|
|
429
|
+
return { roots: [], ranked: [] };
|
|
430
|
+
}
|
|
116
431
|
|
|
117
432
|
const N = corpusN(ctx);
|
|
118
433
|
// One climb per distinct anchor for the WHOLE query: regions sharing a
|
|
119
434
|
// chunk, and canonicalChunkId's prefix probes, all hit this memo instead of
|
|
120
435
|
// re-reading the anchor's full edge fan-out from the store.
|
|
121
436
|
const reachMemo = new Map<number, AncestorReach>();
|
|
122
|
-
const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo);
|
|
437
|
+
const rvs = await voteRegions(ctx, query, regions, k, mode, N, reachMemo, td);
|
|
123
438
|
|
|
124
439
|
// ── Cross-region: DIRECT region-to-region interaction ─────────────────
|
|
125
440
|
// Two regions whose individual climbs land on DIFFERENT contexts leave
|
|
@@ -134,6 +449,7 @@ export async function computeAttention(
|
|
|
134
449
|
k,
|
|
135
450
|
N,
|
|
136
451
|
reachMemo,
|
|
452
|
+
td,
|
|
137
453
|
);
|
|
138
454
|
// A vote SUPERSEDED by exact joint evidence (its bytes literally live
|
|
139
455
|
// inside the joint container, yet it climbed elsewhere — grid aliasing)
|
|
@@ -144,16 +460,37 @@ export async function computeAttention(
|
|
|
144
460
|
...cross.votes,
|
|
145
461
|
]
|
|
146
462
|
: rvs.votes;
|
|
463
|
+
// Mark, on the per-region trace, the source region of every superseded
|
|
464
|
+
// ordinary vote (spec §4's final rule) — an explicit pass over the exact
|
|
465
|
+
// set crossRegionVotes' explaining-away logic removed, never inferred
|
|
466
|
+
// from `absorbed`.
|
|
467
|
+
if (td && cross.superseded.size > 0) {
|
|
468
|
+
for (const rv of cross.superseded) {
|
|
469
|
+
const region = td.regions.find(
|
|
470
|
+
(r) => r.span[0] === rv.start && r.span[1] === rv.end,
|
|
471
|
+
);
|
|
472
|
+
if (region) region.superseded = true;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
147
475
|
// ──────────────────────────────────────────────────────────────────────
|
|
148
476
|
|
|
477
|
+
const cfg: ClimbConsensusCfg = { ...cfg0, N, reachMemo };
|
|
478
|
+
|
|
149
479
|
if (allVotes.length === 0) {
|
|
150
|
-
traceAttention(ctx, regions, rvs.voters, []);
|
|
480
|
+
traceAttention(ctx, regions, rvs.voters, [], undefined, td, cfg);
|
|
151
481
|
return { roots: [], ranked: [] };
|
|
152
482
|
}
|
|
153
483
|
|
|
154
484
|
const sat = detectSaturated(ctx, regions, rvs.saturated);
|
|
155
|
-
|
|
156
|
-
|
|
485
|
+
if (td) {
|
|
486
|
+
td.saturation = {
|
|
487
|
+
regionIntervals: sat.intervals.map((iv) => ({ ...iv })),
|
|
488
|
+
hasLeading: sat.hasLeading,
|
|
489
|
+
leadingEnd: sat.leadingEnd,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const pooled = poolVotes(ctx, allVotes, sat, N, td);
|
|
493
|
+
return commitVotes(ctx, pooled, sat, regions, rvs.voters, N, td, cfg);
|
|
157
494
|
}
|
|
158
495
|
|
|
159
496
|
export function collectRegions(ctx: MindContext, query: Uint8Array): Region[] {
|
|
@@ -194,6 +531,7 @@ export async function voteRegions(
|
|
|
194
531
|
mode: DFMode,
|
|
195
532
|
N: number,
|
|
196
533
|
reachMemo?: Map<number, AncestorReach>,
|
|
534
|
+
td?: TraceDraft,
|
|
197
535
|
): Promise<{
|
|
198
536
|
votes: RegionVote[];
|
|
199
537
|
saturated: boolean[];
|
|
@@ -206,6 +544,37 @@ export async function voteRegions(
|
|
|
206
544
|
|
|
207
545
|
for (let ri = 0; ri < regions.length; ri++) {
|
|
208
546
|
const { v, start, end, chunk, known } = regions[ri];
|
|
547
|
+
// Trace-only bookkeeping for this region — allocated only under `td`
|
|
548
|
+
// (i.e. only when ctx.trace is set); see ConsensusRegionTrace/
|
|
549
|
+
// RegionOutcome (spec §4). `examinedIds` tracks distinct ANN hits
|
|
550
|
+
// whose edgeAncestors reach was actually CONSULTED here (not merely
|
|
551
|
+
// returned by resonate) — the fallback/margin loops below add to it.
|
|
552
|
+
const examinedIds = td ? new Set<number>() : undefined;
|
|
553
|
+
let annQueried = false;
|
|
554
|
+
let fallbackKind: "orphan" | "saturated-tie" | undefined;
|
|
555
|
+
const recordRegion = (
|
|
556
|
+
outcome: RegionOutcome,
|
|
557
|
+
extra: Partial<ConsensusRegionTrace> = {},
|
|
558
|
+
) => {
|
|
559
|
+
if (!td) return;
|
|
560
|
+
td.regions[ri] = {
|
|
561
|
+
index: ri,
|
|
562
|
+
source: ri < td.perceivedCount ? "perceived" : "recognised",
|
|
563
|
+
span: [start, end],
|
|
564
|
+
chunk,
|
|
565
|
+
known,
|
|
566
|
+
canonicalId: canonicalId ?? undefined,
|
|
567
|
+
canonicalUsable,
|
|
568
|
+
canonicalFailed,
|
|
569
|
+
annQueried,
|
|
570
|
+
annHitsReturned: hits ? hits.length : 0,
|
|
571
|
+
annHitsExamined: examinedIds ? examinedIds.size : 0,
|
|
572
|
+
outcome,
|
|
573
|
+
ordinaryVoteProduced: outcome === "voted",
|
|
574
|
+
superseded: false,
|
|
575
|
+
...extra,
|
|
576
|
+
};
|
|
577
|
+
};
|
|
209
578
|
|
|
210
579
|
// EXACT-FIRST: a chunk whose canonical anchor is content-addressed needs
|
|
211
580
|
// no estimator — identity is exact, so its score is 1 BY DEFINITION (the
|
|
@@ -225,23 +594,35 @@ export async function voteRegions(
|
|
|
225
594
|
(ctx.store.hasParents(canonicalId) ||
|
|
226
595
|
ctx.store.hasContainers(canonicalId));
|
|
227
596
|
let hits: readonly Hit[] | null = null;
|
|
228
|
-
const ensureHits = async (): Promise<readonly Hit[]> =>
|
|
229
|
-
hits
|
|
597
|
+
const ensureHits = async (): Promise<readonly Hit[]> => {
|
|
598
|
+
if (hits === null) {
|
|
599
|
+
hits = await ctx.store.resonate(v, k);
|
|
600
|
+
annQueried = true;
|
|
601
|
+
}
|
|
602
|
+
return hits;
|
|
603
|
+
};
|
|
230
604
|
|
|
231
605
|
const canonicalFailed = chunk && canonicalId === null;
|
|
232
606
|
let voterId: number;
|
|
233
607
|
let score: number;
|
|
234
608
|
let scoreId: number; // the node the score was measured against
|
|
609
|
+
let selectedSource: "canonical" | "ann";
|
|
235
610
|
if (canonicalUsable) {
|
|
236
611
|
voterId = canonicalId!;
|
|
237
612
|
score = 1;
|
|
238
613
|
scoreId = canonicalId!;
|
|
614
|
+
selectedSource = "canonical";
|
|
239
615
|
} else {
|
|
240
616
|
const h = await ensureHits();
|
|
241
|
-
if (h.length === 0)
|
|
617
|
+
if (h.length === 0) {
|
|
618
|
+
recordRegion("no-ann-hit");
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
242
621
|
voterId = h[0].id;
|
|
243
622
|
score = h[0].score;
|
|
244
623
|
scoreId = h[0].id;
|
|
624
|
+
selectedSource = "ann";
|
|
625
|
+
examinedIds?.add(voterId);
|
|
245
626
|
}
|
|
246
627
|
|
|
247
628
|
let reach = edgeAncestors(ctx, voterId, N, reachMemo);
|
|
@@ -255,6 +636,7 @@ export async function voteRegions(
|
|
|
255
636
|
for (const h of await ensureHits()) {
|
|
256
637
|
if (h.id === voterId) continue;
|
|
257
638
|
const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
|
|
639
|
+
examinedIds?.add(h.id);
|
|
258
640
|
if (r2.saturated || r2.roots.length > 0) {
|
|
259
641
|
ctx.trace?.step(
|
|
260
642
|
"anchorFallback",
|
|
@@ -266,6 +648,8 @@ export async function voteRegions(
|
|
|
266
648
|
voterId = h.id;
|
|
267
649
|
score = h.score;
|
|
268
650
|
scoreId = h.id;
|
|
651
|
+
selectedSource = "ann";
|
|
652
|
+
fallbackKind = "orphan";
|
|
269
653
|
break;
|
|
270
654
|
}
|
|
271
655
|
}
|
|
@@ -289,6 +673,7 @@ export async function voteRegions(
|
|
|
289
673
|
if (h.id === voterId) continue;
|
|
290
674
|
if (h.score < score - band) break; // hits are nearest-first
|
|
291
675
|
const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
|
|
676
|
+
examinedIds?.add(h.id);
|
|
292
677
|
if (!r2.saturated && r2.roots.length > 0) {
|
|
293
678
|
ctx.trace?.step(
|
|
294
679
|
"anchorFallback",
|
|
@@ -300,20 +685,52 @@ export async function voteRegions(
|
|
|
300
685
|
voterId = h.id;
|
|
301
686
|
score = h.score;
|
|
302
687
|
scoreId = h.id;
|
|
688
|
+
selectedSource = "ann";
|
|
689
|
+
fallbackKind = "saturated-tie";
|
|
303
690
|
break;
|
|
304
691
|
}
|
|
305
692
|
}
|
|
306
693
|
}
|
|
307
694
|
regionSaturated[ri] = reach.saturated;
|
|
308
|
-
|
|
309
|
-
|
|
695
|
+
const selected: ConsensusRegionTrace["selected"] | undefined = !td
|
|
696
|
+
? undefined
|
|
697
|
+
: (() => {
|
|
698
|
+
const rank: number | undefined = selectedSource === "ann"
|
|
699
|
+
? (hits as readonly Hit[] | null)?.findIndex((h: Hit) =>
|
|
700
|
+
h.id === voterId
|
|
701
|
+
)
|
|
702
|
+
: undefined;
|
|
703
|
+
return {
|
|
704
|
+
source: selectedSource,
|
|
705
|
+
node: voterId,
|
|
706
|
+
score,
|
|
707
|
+
...(rank !== undefined ? { rank } : {}),
|
|
708
|
+
...(fallbackKind ? { fallback: fallbackKind } : {}),
|
|
709
|
+
};
|
|
710
|
+
})();
|
|
711
|
+
if (reach.roots.length === 0) {
|
|
712
|
+
recordRegion("no-structural-reach", { selected, reachNode: voterId });
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (reach.saturated) {
|
|
716
|
+
recordRegion("saturated-abstention", { selected, reachNode: voterId });
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
310
719
|
|
|
311
720
|
// One IDF per region — dfWeight() and the focus weight used to compute
|
|
312
721
|
// the same logarithm independently.
|
|
313
722
|
const idf = Math.log(N / Math.max(1, reach.contextsReached));
|
|
314
723
|
const df = Math.log(1 + reach.contextsReached);
|
|
315
724
|
const wf = mode === "direct" ? df : mode === "combined" ? idf + df : idf;
|
|
316
|
-
if (wf <= 0)
|
|
725
|
+
if (wf <= 0) {
|
|
726
|
+
recordRegion("nonpositive-df-weight", {
|
|
727
|
+
selected,
|
|
728
|
+
reachNode: voterId,
|
|
729
|
+
idf,
|
|
730
|
+
dfWeight: wf,
|
|
731
|
+
});
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
317
734
|
|
|
318
735
|
// CONTRASTIVE-MARGIN GATE — the compensation the linear (byte-proportional)
|
|
319
736
|
// fold demands, applied to APPROXIMATE evidence only. Under the linear
|
|
@@ -338,17 +755,31 @@ export async function voteRegions(
|
|
|
338
755
|
// (reordered / paraphrased queries) below the floor so they grounded
|
|
339
756
|
// nothing. Gating at the noise floor keeps frame-echo suppression (a frame
|
|
340
757
|
// region's margin ≈ 0 is gated out) without penalising honest evidence.
|
|
758
|
+
let contrastiveMargin: number | undefined;
|
|
341
759
|
if (!known) {
|
|
342
760
|
let margin = score;
|
|
343
761
|
for (const h of await ensureHits()) {
|
|
344
762
|
if (h.id === voterId) continue;
|
|
345
763
|
const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
|
|
764
|
+
examinedIds?.add(h.id);
|
|
346
765
|
if (r2.saturated || r2.roots.length === 0) continue; // concludes nothing
|
|
347
766
|
if (sameRoots(r2.roots, reach.roots)) continue; // same conclusion
|
|
348
767
|
margin = score - h.score; // hits are nearest-first: the best rival
|
|
349
768
|
break;
|
|
350
769
|
}
|
|
351
|
-
|
|
770
|
+
contrastiveMargin = margin;
|
|
771
|
+
const noiseFloor = estimatorNoise(ctx.store.D);
|
|
772
|
+
if (margin <= noiseFloor) {
|
|
773
|
+
recordRegion("contrastive-margin-rejection", {
|
|
774
|
+
selected,
|
|
775
|
+
reachNode: voterId,
|
|
776
|
+
idf,
|
|
777
|
+
dfWeight: wf,
|
|
778
|
+
contrastiveMargin: margin,
|
|
779
|
+
contrastiveNoiseFloor: noiseFloor,
|
|
780
|
+
});
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
352
783
|
}
|
|
353
784
|
|
|
354
785
|
// MUTUAL-EXPLANATION WEIGHT (angle + magnitude). Under the linear fold
|
|
@@ -388,6 +819,21 @@ export async function voteRegions(
|
|
|
388
819
|
if (ctx.trace) {
|
|
389
820
|
regionVoter[ri] = { id: voterId, score, w: wf };
|
|
390
821
|
}
|
|
822
|
+
recordRegion("voted", {
|
|
823
|
+
selected,
|
|
824
|
+
reachNode: voterId,
|
|
825
|
+
idf,
|
|
826
|
+
dfWeight: wf,
|
|
827
|
+
...(contrastiveMargin !== undefined
|
|
828
|
+
? {
|
|
829
|
+
contrastiveMargin,
|
|
830
|
+
contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
|
|
831
|
+
}
|
|
832
|
+
: {}),
|
|
833
|
+
mutualWeight: mutual,
|
|
834
|
+
voteWeightPerRoot: w,
|
|
835
|
+
focusWeightPerRoot: wFocus,
|
|
836
|
+
});
|
|
391
837
|
}
|
|
392
838
|
return {
|
|
393
839
|
votes: regionVotes,
|
|
@@ -413,6 +859,7 @@ export function poolVotes(
|
|
|
413
859
|
regionVotes: readonly RegionVote[],
|
|
414
860
|
sat: SaturationInfo,
|
|
415
861
|
N: number,
|
|
862
|
+
td?: TraceDraft,
|
|
416
863
|
): {
|
|
417
864
|
votes: Map<number, number>;
|
|
418
865
|
votesIdf: Map<number, number>;
|
|
@@ -420,6 +867,8 @@ export function poolVotes(
|
|
|
420
867
|
/** Per-anchor SCALE-INVARIANT support: Σ RegionVote.absorbed over the
|
|
421
868
|
* distinct contributing regions — see Attention.breadth. */
|
|
422
869
|
regionSupport: Map<number, number>;
|
|
870
|
+
/** Per-anchor contributing region spans — see Attention.clusters. */
|
|
871
|
+
regionSpans: Map<number, Array<[number, number]>>;
|
|
423
872
|
steps: DerivationStep[];
|
|
424
873
|
} {
|
|
425
874
|
const eligible: number[] = [];
|
|
@@ -433,6 +882,13 @@ export function poolVotes(
|
|
|
433
882
|
}
|
|
434
883
|
eligible.push(ri);
|
|
435
884
|
}
|
|
885
|
+
if (td) {
|
|
886
|
+
td.pooling = {
|
|
887
|
+
inputVotes: regionVotes.length,
|
|
888
|
+
eligibleVotes: eligible.length,
|
|
889
|
+
saturationMaskedVotes: regionVotes.length - eligible.length,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
436
892
|
|
|
437
893
|
const key = (it: AItem) =>
|
|
438
894
|
it.kind === "region"
|
|
@@ -500,6 +956,7 @@ export function poolVotes(
|
|
|
500
956
|
{ start: number; end: number; w: number }
|
|
501
957
|
>();
|
|
502
958
|
const regionSupport = new Map<number, number>();
|
|
959
|
+
const regionSpans = new Map<number, Array<[number, number]>>();
|
|
503
960
|
const steps: DerivationStep[] = [];
|
|
504
961
|
let order = 0;
|
|
505
962
|
for (const pc of pool.values()) {
|
|
@@ -508,6 +965,7 @@ export function poolVotes(
|
|
|
508
965
|
const premises: DerivationItem[] = [];
|
|
509
966
|
const seenRi = new Set<number>();
|
|
510
967
|
let breadthSum = 0;
|
|
968
|
+
const spans: Array<[number, number]> = [];
|
|
511
969
|
for (const c of pc.contributions) {
|
|
512
970
|
const p0 = c.premises[0].item;
|
|
513
971
|
if (p0.kind !== "region" || seenRi.has(p0.ri)) continue;
|
|
@@ -515,8 +973,10 @@ export function poolVotes(
|
|
|
515
973
|
const rv = regionVotes[p0.ri];
|
|
516
974
|
breadthSum += rv.absorbed ?? 1;
|
|
517
975
|
premises.push({ kind: "form", span: [rv.start, rv.end] });
|
|
976
|
+
spans.push([rv.start, rv.end]);
|
|
518
977
|
}
|
|
519
978
|
regionSupport.set(pc.item.id, breadthSum);
|
|
979
|
+
regionSpans.set(pc.item.id, spans);
|
|
520
980
|
steps.push({
|
|
521
981
|
order: order++,
|
|
522
982
|
move: "pool-vote",
|
|
@@ -543,7 +1003,35 @@ export function poolVotes(
|
|
|
543
1003
|
}
|
|
544
1004
|
}
|
|
545
1005
|
}
|
|
546
|
-
return { votes, votesIdf, support, regionSupport, steps };
|
|
1006
|
+
return { votes, votesIdf, support, regionSupport, regionSpans, steps };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/** The number of DISTINCT clusters a root's contributing regions form —
|
|
1010
|
+
* see Attention.clusters. Two regions belong to the same cluster iff the
|
|
1011
|
+
* gap between them is strictly less than one river-fold quantum W: at
|
|
1012
|
+
* that distance there is no room for a genuinely separate, independently
|
|
1013
|
+
* perceivable unit of content between them (the same "smallest meaningful
|
|
1014
|
+
* distinction" quantum {@link reachThreshold}'s own doc invokes). A gap
|
|
1015
|
+
* of a full quantum or more means real, separate structure could sit
|
|
1016
|
+
* between the two spans, so they count as independent corroboration.
|
|
1017
|
+
* Strict `<` (not `<=`): verified against gap 3.1's own "gender equality"
|
|
1018
|
+
* root, whose two genuine clusters sit EXACTLY W bytes apart — `<= W`
|
|
1019
|
+
* would wrongly merge them into one and break that pinned requirement. */
|
|
1020
|
+
function countClusters(spans: readonly [number, number][], W: number): number {
|
|
1021
|
+
if (spans.length === 0) return 0;
|
|
1022
|
+
const sorted = [...spans].sort((a, b) => a[0] - b[0]);
|
|
1023
|
+
let clusters = 1;
|
|
1024
|
+
let curEnd = sorted[0][1];
|
|
1025
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1026
|
+
const [s, e] = sorted[i];
|
|
1027
|
+
if (s - curEnd < W) {
|
|
1028
|
+
curEnd = Math.max(curEnd, e);
|
|
1029
|
+
} else {
|
|
1030
|
+
clusters++;
|
|
1031
|
+
curEnd = e;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return clusters;
|
|
547
1035
|
}
|
|
548
1036
|
|
|
549
1037
|
export function commitVotes(
|
|
@@ -553,16 +1041,20 @@ export function commitVotes(
|
|
|
553
1041
|
votesIdf: Map<number, number>;
|
|
554
1042
|
support: Map<number, { start: number; end: number; w: number }>;
|
|
555
1043
|
regionSupport: Map<number, number>;
|
|
1044
|
+
regionSpans: Map<number, Array<[number, number]>>;
|
|
556
1045
|
steps: DerivationStep[];
|
|
557
1046
|
},
|
|
558
1047
|
sat: SaturationInfo,
|
|
559
1048
|
regions: readonly Region[],
|
|
560
1049
|
regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
|
|
561
1050
|
N: number,
|
|
1051
|
+
td?: TraceDraft,
|
|
1052
|
+
cfg?: ClimbConsensusCfg,
|
|
562
1053
|
): AttentionRead {
|
|
563
|
-
const { votes, votesIdf, support, regionSupport, steps } =
|
|
1054
|
+
const { votes, votesIdf, support, regionSupport, regionSpans, steps } =
|
|
1055
|
+
pooled;
|
|
564
1056
|
if (votes.size === 0) {
|
|
565
|
-
traceAttention(ctx, regions, regionVoter, [], steps);
|
|
1057
|
+
traceAttention(ctx, regions, regionVoter, [], steps, td, cfg);
|
|
566
1058
|
return { roots: [], ranked: [] };
|
|
567
1059
|
}
|
|
568
1060
|
|
|
@@ -580,6 +1072,10 @@ export function commitVotes(
|
|
|
580
1072
|
start: s.start,
|
|
581
1073
|
end: s.end,
|
|
582
1074
|
breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
|
|
1075
|
+
clusters: countClusters(
|
|
1076
|
+
regionSpans.get(anchor) ?? [],
|
|
1077
|
+
ctx.space.maxGroup,
|
|
1078
|
+
),
|
|
583
1079
|
};
|
|
584
1080
|
})
|
|
585
1081
|
.sort((a, b) => b.vote - a.vote);
|
|
@@ -600,25 +1096,121 @@ export function commitVotes(
|
|
|
600
1096
|
const floor = consensusFloor(N);
|
|
601
1097
|
const placed: Attention[] = [];
|
|
602
1098
|
const roots: Attention[] = [];
|
|
603
|
-
|
|
1099
|
+
const recordAnchor = (
|
|
1100
|
+
point: Attention,
|
|
1101
|
+
rank: number,
|
|
1102
|
+
status: "root" | "overlap" | "rejected",
|
|
1103
|
+
dominant: boolean,
|
|
1104
|
+
passesNaturalBreak: boolean | undefined,
|
|
1105
|
+
passesConsensusFloor: boolean | undefined,
|
|
1106
|
+
pastLeadingSaturation: boolean | undefined,
|
|
1107
|
+
rejectionReasons: AnchorRejectionReason[],
|
|
1108
|
+
) => {
|
|
1109
|
+
if (!td) return;
|
|
1110
|
+
td.anchors.push({
|
|
1111
|
+
anchor: point.anchor,
|
|
1112
|
+
rank,
|
|
1113
|
+
pooledVote: point.vote,
|
|
1114
|
+
idfVote: votesIdf.get(point.anchor) ?? 0,
|
|
1115
|
+
candidateBreadth: regions.length,
|
|
1116
|
+
contributingVotes: regionSpans.get(point.anchor)?.length ?? 0,
|
|
1117
|
+
contributingEvidence: regionSupport.get(point.anchor) ?? 0,
|
|
1118
|
+
breadth: point.breadth,
|
|
1119
|
+
contributingSpans: regionSpans.get(point.anchor) ?? [],
|
|
1120
|
+
clusters: point.clusters,
|
|
1121
|
+
commit: {
|
|
1122
|
+
status,
|
|
1123
|
+
dominant,
|
|
1124
|
+
passesNaturalBreak,
|
|
1125
|
+
passesConsensusFloor,
|
|
1126
|
+
pastLeadingSaturation,
|
|
1127
|
+
rejectionReasons,
|
|
1128
|
+
},
|
|
1129
|
+
});
|
|
1130
|
+
};
|
|
1131
|
+
for (let rank = 0; rank < ranked.length; rank++) {
|
|
1132
|
+
const point = ranked[rank];
|
|
604
1133
|
const absorbed = placed.some((p) => overlaps(point, p));
|
|
605
|
-
|
|
1134
|
+
// Commit decisions are recorded LIVE, inside this loop, in the exact
|
|
1135
|
+
// shape the gates below apply them — never reconstructed afterward from
|
|
1136
|
+
// the final `roots` (spec §8's explicit requirement).
|
|
1137
|
+
let status: "root" | "overlap" | "rejected";
|
|
1138
|
+
let dominant = false;
|
|
1139
|
+
let passesNaturalBreak: boolean | undefined;
|
|
1140
|
+
let passesConsensusFloor: boolean | undefined;
|
|
1141
|
+
let pastLeadingSaturation: boolean | undefined;
|
|
1142
|
+
const rejectionReasons: AnchorRejectionReason[] = [];
|
|
1143
|
+
if (absorbed) {
|
|
1144
|
+
status = "overlap";
|
|
1145
|
+
} else {
|
|
606
1146
|
const pastLeading = !sat.hasLeading ||
|
|
607
1147
|
roots.length === 0 || point.start >= sat.leadingEnd;
|
|
1148
|
+
pastLeadingSaturation = pastLeading;
|
|
608
1149
|
const vote = votesIdf.get(point.anchor) ?? 0;
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1150
|
+
if (roots.length === 0) {
|
|
1151
|
+
// The first non-overlapping root is DOMINANT and bypasses the two
|
|
1152
|
+
// vote thresholds (it always grounds) — only the leading-saturation
|
|
1153
|
+
// gate still applies to it.
|
|
1154
|
+
dominant = true;
|
|
1155
|
+
if (pastLeading) {
|
|
1156
|
+
status = "root";
|
|
1157
|
+
} else {
|
|
1158
|
+
status = "rejected";
|
|
1159
|
+
rejectionReasons.push("leading-saturation");
|
|
1160
|
+
}
|
|
1161
|
+
} else {
|
|
1162
|
+
passesNaturalBreak = vote >= rootCut;
|
|
1163
|
+
passesConsensusFloor = vote >= floor;
|
|
1164
|
+
if (passesNaturalBreak && passesConsensusFloor && pastLeading) {
|
|
1165
|
+
status = "root";
|
|
1166
|
+
} else {
|
|
1167
|
+
status = "rejected";
|
|
1168
|
+
if (!passesNaturalBreak) rejectionReasons.push("below-natural-break");
|
|
1169
|
+
if (!passesConsensusFloor) {
|
|
1170
|
+
rejectionReasons.push("below-consensus-floor");
|
|
1171
|
+
}
|
|
1172
|
+
if (!pastLeading) rejectionReasons.push("leading-saturation");
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (status === "root") {
|
|
613
1176
|
roots.push(point);
|
|
614
1177
|
} else {
|
|
1178
|
+
recordAnchor(
|
|
1179
|
+
point,
|
|
1180
|
+
rank,
|
|
1181
|
+
status,
|
|
1182
|
+
dominant,
|
|
1183
|
+
passesNaturalBreak,
|
|
1184
|
+
passesConsensusFloor,
|
|
1185
|
+
pastLeadingSaturation,
|
|
1186
|
+
rejectionReasons,
|
|
1187
|
+
);
|
|
615
1188
|
continue;
|
|
616
1189
|
}
|
|
617
1190
|
}
|
|
1191
|
+
recordAnchor(
|
|
1192
|
+
point,
|
|
1193
|
+
rank,
|
|
1194
|
+
status,
|
|
1195
|
+
dominant,
|
|
1196
|
+
passesNaturalBreak,
|
|
1197
|
+
passesConsensusFloor,
|
|
1198
|
+
pastLeadingSaturation,
|
|
1199
|
+
rejectionReasons,
|
|
1200
|
+
);
|
|
618
1201
|
placed.push(point);
|
|
619
1202
|
}
|
|
620
1203
|
|
|
621
|
-
traceAttention(
|
|
1204
|
+
traceAttention(
|
|
1205
|
+
ctx,
|
|
1206
|
+
regions,
|
|
1207
|
+
regionVoter,
|
|
1208
|
+
roots,
|
|
1209
|
+
steps,
|
|
1210
|
+
td,
|
|
1211
|
+
cfg ? { ...cfg, naturalBreak: rootCut, consensusFloor: floor } : undefined,
|
|
1212
|
+
ranked,
|
|
1213
|
+
);
|
|
622
1214
|
return { roots, ranked };
|
|
623
1215
|
}
|
|
624
1216
|
|
|
@@ -785,6 +1377,266 @@ export function naturalBreak(votes: number[]): number {
|
|
|
785
1377
|
// container does not hold (a genuine second topic) are untouched.
|
|
786
1378
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
787
1379
|
|
|
1380
|
+
// ── Structural-resonance — the FINAL approximate tier ──────────────────────
|
|
1381
|
+
//
|
|
1382
|
+
// Reached only when every DAG junction tier (exact, single-synonym, double-
|
|
1383
|
+
// synonym) found no container. Composes a hypothetical structural gist from
|
|
1384
|
+
// ALREADY-EXISTING structural vectors — the two endpoint regions' own gists
|
|
1385
|
+
// (or, per variant, a halo sibling's stored gist occupying the same slot)
|
|
1386
|
+
// plus the REAL middle-query structure between them — and asks the ANN index
|
|
1387
|
+
// what already-learnt whole resembles that composition. It never perceives
|
|
1388
|
+
// concatenated endpoint bytes and never fabricates a rewritten query string;
|
|
1389
|
+
// see {@link composeStructuralGist}.
|
|
1390
|
+
|
|
1391
|
+
export type CrossRegionTier =
|
|
1392
|
+
| "exact"
|
|
1393
|
+
| "single-synonym"
|
|
1394
|
+
| "double-synonym"
|
|
1395
|
+
| "structural-resonance";
|
|
1396
|
+
|
|
1397
|
+
export interface StructuralVariant {
|
|
1398
|
+
left: StructuralPart;
|
|
1399
|
+
right: StructuralPart;
|
|
1400
|
+
kind:
|
|
1401
|
+
| "exact-exact"
|
|
1402
|
+
| "left-synonym"
|
|
1403
|
+
| "right-synonym"
|
|
1404
|
+
| "double-synonym";
|
|
1405
|
+
semanticConfidence: number;
|
|
1406
|
+
leftSiblingId?: number;
|
|
1407
|
+
rightSiblingId?: number;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
export interface StructuralResonanceProposal {
|
|
1411
|
+
id: number;
|
|
1412
|
+
annScore: number;
|
|
1413
|
+
semanticConfidence: number;
|
|
1414
|
+
effectiveScore: number;
|
|
1415
|
+
variant: StructuralVariant["kind"];
|
|
1416
|
+
leftSiblingId?: number;
|
|
1417
|
+
rightSiblingId?: number;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
const VARIANT_KIND_ORDER: Record<StructuralVariant["kind"], number> = {
|
|
1421
|
+
"exact-exact": -1,
|
|
1422
|
+
"left-synonym": 0,
|
|
1423
|
+
"right-synonym": 1,
|
|
1424
|
+
"double-synonym": 2,
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
/** A node's structural gist, read directly from its own stored bytes — the
|
|
1428
|
+
* repository's node → gist accessor: content is immutable and perception is
|
|
1429
|
+
* a pure function of bytes, so re-perceiving a node's full stored bytes
|
|
1430
|
+
* reproduces exactly the gist it was interned with. Never concatenates
|
|
1431
|
+
* the sibling's bytes with anything else. */
|
|
1432
|
+
function storedNodeGist(ctx: MindContext, id: number): Vec {
|
|
1433
|
+
return gistOf(ctx, read(ctx, id));
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/** A halo sibling's structural part, occupying the ORIGINAL query-region
|
|
1437
|
+
* slot length — the sibling replaces only the DIRECTION, never the query's
|
|
1438
|
+
* own bytes, position, or gap (see §6 of the spec this implements). */
|
|
1439
|
+
function siblingPart(
|
|
1440
|
+
ctx: MindContext,
|
|
1441
|
+
sibling: Hit,
|
|
1442
|
+
originalRegion: { start: number; end: number },
|
|
1443
|
+
): StructuralPart {
|
|
1444
|
+
return {
|
|
1445
|
+
v: storedNodeGist(ctx, sibling.id),
|
|
1446
|
+
len: originalRegion.end - originalRegion.start,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/** Build, bound and order every mandatory structural variant (§7-8): the
|
|
1451
|
+
* exact/exact composition is always kept; up to `ctx.cfg.haloQueryK`
|
|
1452
|
+
* synonym variants (single- and double-synonym combined, one shared
|
|
1453
|
+
* budget) are appended, ordered by confidence, then kind, then sibling id. */
|
|
1454
|
+
export function buildStructuralVariants(
|
|
1455
|
+
ctx: MindContext,
|
|
1456
|
+
ra: Region,
|
|
1457
|
+
rb: Region,
|
|
1458
|
+
sides: JunctionSynonymSides,
|
|
1459
|
+
): {
|
|
1460
|
+
variants: StructuralVariant[];
|
|
1461
|
+
exactLeft: StructuralPart;
|
|
1462
|
+
exactRight: StructuralPart;
|
|
1463
|
+
} {
|
|
1464
|
+
const exactLeft: StructuralPart = { v: ra.v, len: ra.end - ra.start };
|
|
1465
|
+
const exactRight: StructuralPart = { v: rb.v, len: rb.end - rb.start };
|
|
1466
|
+
|
|
1467
|
+
const synonymVariants: StructuralVariant[] = [];
|
|
1468
|
+
for (const ls of sides.leftSiblings) {
|
|
1469
|
+
synonymVariants.push({
|
|
1470
|
+
left: siblingPart(ctx, ls, ra),
|
|
1471
|
+
right: exactRight,
|
|
1472
|
+
kind: "left-synonym",
|
|
1473
|
+
semanticConfidence: ls.score,
|
|
1474
|
+
leftSiblingId: ls.id,
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
for (const rs of sides.rightSiblings) {
|
|
1478
|
+
synonymVariants.push({
|
|
1479
|
+
left: exactLeft,
|
|
1480
|
+
right: siblingPart(ctx, rs, rb),
|
|
1481
|
+
kind: "right-synonym",
|
|
1482
|
+
semanticConfidence: rs.score,
|
|
1483
|
+
rightSiblingId: rs.id,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
for (const ls of sides.leftSiblings) {
|
|
1487
|
+
for (const rs of sides.rightSiblings) {
|
|
1488
|
+
synonymVariants.push({
|
|
1489
|
+
left: siblingPart(ctx, ls, ra),
|
|
1490
|
+
right: siblingPart(ctx, rs, rb),
|
|
1491
|
+
kind: "double-synonym",
|
|
1492
|
+
semanticConfidence: Math.min(ls.score, rs.score),
|
|
1493
|
+
leftSiblingId: ls.id,
|
|
1494
|
+
rightSiblingId: rs.id,
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// §8: semantic confidence desc, then kind (left-synonym, right-synonym,
|
|
1500
|
+
// double-synonym), then left sibling id asc, then right sibling id asc.
|
|
1501
|
+
synonymVariants.sort((a, b) =>
|
|
1502
|
+
b.semanticConfidence - a.semanticConfidence ||
|
|
1503
|
+
VARIANT_KIND_ORDER[a.kind] - VARIANT_KIND_ORDER[b.kind] ||
|
|
1504
|
+
(a.leftSiblingId ?? -1) - (b.leftSiblingId ?? -1) ||
|
|
1505
|
+
(a.rightSiblingId ?? -1) - (b.rightSiblingId ?? -1)
|
|
1506
|
+
);
|
|
1507
|
+
|
|
1508
|
+
const variants: StructuralVariant[] = [
|
|
1509
|
+
{
|
|
1510
|
+
left: exactLeft,
|
|
1511
|
+
right: exactRight,
|
|
1512
|
+
kind: "exact-exact",
|
|
1513
|
+
semanticConfidence: 1,
|
|
1514
|
+
},
|
|
1515
|
+
...synonymVariants.slice(0, ctx.cfg.haloQueryK),
|
|
1516
|
+
];
|
|
1517
|
+
return { variants, exactLeft, exactRight };
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** Deterministic best-of tie-break for two proposals ranked for the SAME
|
|
1521
|
+
* candidate id — effectiveScore, then annScore, then semanticConfidence,
|
|
1522
|
+
* then variant kind, then sibling ids (§10). */
|
|
1523
|
+
function betterProposal(
|
|
1524
|
+
a: StructuralResonanceProposal,
|
|
1525
|
+
b: StructuralResonanceProposal,
|
|
1526
|
+
): boolean {
|
|
1527
|
+
if (a.effectiveScore !== b.effectiveScore) {
|
|
1528
|
+
return a.effectiveScore > b.effectiveScore;
|
|
1529
|
+
}
|
|
1530
|
+
if (a.annScore !== b.annScore) return a.annScore > b.annScore;
|
|
1531
|
+
if (a.semanticConfidence !== b.semanticConfidence) {
|
|
1532
|
+
return a.semanticConfidence > b.semanticConfidence;
|
|
1533
|
+
}
|
|
1534
|
+
if (VARIANT_KIND_ORDER[a.variant] !== VARIANT_KIND_ORDER[b.variant]) {
|
|
1535
|
+
return VARIANT_KIND_ORDER[a.variant] < VARIANT_KIND_ORDER[b.variant];
|
|
1536
|
+
}
|
|
1537
|
+
if ((a.leftSiblingId ?? -1) !== (b.leftSiblingId ?? -1)) {
|
|
1538
|
+
return (a.leftSiblingId ?? -1) < (b.leftSiblingId ?? -1);
|
|
1539
|
+
}
|
|
1540
|
+
return (a.rightSiblingId ?? -1) < (b.rightSiblingId ?? -1);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
/** The final approximate tier: compose every retained structural variant,
|
|
1544
|
+
* ANN-query each, merge proposals by candidate id, and validate the winner
|
|
1545
|
+
* through the SAME structural gates every other tier answers to (saturation,
|
|
1546
|
+
* roots, IDF, contrastive margin). Returns null when nothing survives. */
|
|
1547
|
+
export async function structuralResonance(
|
|
1548
|
+
ctx: MindContext,
|
|
1549
|
+
query: Uint8Array,
|
|
1550
|
+
ra: Region,
|
|
1551
|
+
rb: Region,
|
|
1552
|
+
sides: JunctionSynonymSides,
|
|
1553
|
+
k: number,
|
|
1554
|
+
N: number,
|
|
1555
|
+
reachMemo: Map<number, AncestorReach>,
|
|
1556
|
+
/** Each side's OWN individual climb roots (from voteRegions), when it cast
|
|
1557
|
+
* one — the self-evidence backstop structural-resonance needs and the
|
|
1558
|
+
* exact tier gets for free from literal byte containment (§11's whole
|
|
1559
|
+
* premise: recover a JOINT context neither side votes for alone). A
|
|
1560
|
+
* candidate whose reach is exactly one side's own conclusion is not new
|
|
1561
|
+
* evidence of a joint whole; it is that side's resonance rediscovering
|
|
1562
|
+
* itself through a synthetic gist still dominated by its own direction. */
|
|
1563
|
+
ownRootsA: readonly number[] | undefined,
|
|
1564
|
+
ownRootsB: readonly number[] | undefined,
|
|
1565
|
+
): Promise<
|
|
1566
|
+
| { proposal: StructuralResonanceProposal; reach: AncestorReach; idf: number }
|
|
1567
|
+
| null
|
|
1568
|
+
> {
|
|
1569
|
+
const { variants } = buildStructuralVariants(ctx, ra, rb, sides);
|
|
1570
|
+
|
|
1571
|
+
const middleBytes = query.subarray(ra.end, rb.start);
|
|
1572
|
+
const middlePart: StructuralPart | null = middleBytes.length === 0
|
|
1573
|
+
? null
|
|
1574
|
+
: { v: perceive(ctx, middleBytes).v, len: middleBytes.length };
|
|
1575
|
+
|
|
1576
|
+
const proposals = new Map<number, StructuralResonanceProposal>();
|
|
1577
|
+
for (const variant of variants) {
|
|
1578
|
+
const parts: StructuralPart[] = [variant.left];
|
|
1579
|
+
if (middlePart) parts.push(middlePart);
|
|
1580
|
+
parts.push(variant.right);
|
|
1581
|
+
const synthetic = composeStructuralGist(ctx.space, parts);
|
|
1582
|
+
const hits = await ctx.store.resonate(synthetic, k);
|
|
1583
|
+
for (const hit of hits) {
|
|
1584
|
+
const candidate: StructuralResonanceProposal = {
|
|
1585
|
+
id: hit.id,
|
|
1586
|
+
annScore: hit.score,
|
|
1587
|
+
semanticConfidence: variant.semanticConfidence,
|
|
1588
|
+
effectiveScore: hit.score * variant.semanticConfidence,
|
|
1589
|
+
variant: variant.kind,
|
|
1590
|
+
leftSiblingId: variant.leftSiblingId,
|
|
1591
|
+
rightSiblingId: variant.rightSiblingId,
|
|
1592
|
+
};
|
|
1593
|
+
const prev = proposals.get(hit.id);
|
|
1594
|
+
if (prev === undefined || betterProposal(candidate, prev)) {
|
|
1595
|
+
proposals.set(hit.id, candidate);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
if (proposals.size === 0) return null;
|
|
1600
|
+
|
|
1601
|
+
const sorted = [...proposals.values()].sort((a, b) =>
|
|
1602
|
+
b.effectiveScore - a.effectiveScore || a.id - b.id
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
let selected: StructuralResonanceProposal | null = null;
|
|
1606
|
+
let selectedReach: AncestorReach | null = null;
|
|
1607
|
+
let selectedIdf = 0;
|
|
1608
|
+
let rival: StructuralResonanceProposal | null = null;
|
|
1609
|
+
for (const p of sorted) {
|
|
1610
|
+
const reach = edgeAncestors(ctx, p.id, N, reachMemo);
|
|
1611
|
+
if (reach.saturated || reach.roots.length === 0) continue;
|
|
1612
|
+
const idf = Math.log(N / Math.max(1, reach.contextsReached));
|
|
1613
|
+
if (idf <= 0) continue;
|
|
1614
|
+
// Self-evidence backstop (see the param doc above): a candidate that is
|
|
1615
|
+
// exactly one side's own already-voted conclusion carries no JOINT
|
|
1616
|
+
// evidence — skip it as if it never survived.
|
|
1617
|
+
if (
|
|
1618
|
+
(ownRootsA && sameRoots(reach.roots, ownRootsA)) ||
|
|
1619
|
+
(ownRootsB && sameRoots(reach.roots, ownRootsB))
|
|
1620
|
+
) continue;
|
|
1621
|
+
if (selected === null) {
|
|
1622
|
+
selected = p;
|
|
1623
|
+
selectedReach = reach;
|
|
1624
|
+
selectedIdf = idf;
|
|
1625
|
+
} else if (!sameRoots(reach.roots, selectedReach!.roots)) {
|
|
1626
|
+
rival = p;
|
|
1627
|
+
break;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (selected === null || selectedReach === null) return null;
|
|
1631
|
+
|
|
1632
|
+
const margin = rival
|
|
1633
|
+
? selected.effectiveScore - rival.effectiveScore
|
|
1634
|
+
: selected.effectiveScore;
|
|
1635
|
+
if (margin <= estimatorNoise(ctx.store.D)) return null;
|
|
1636
|
+
|
|
1637
|
+
return { proposal: selected, reach: selectedReach, idf: selectedIdf };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
788
1640
|
async function crossRegionVotes(
|
|
789
1641
|
ctx: MindContext,
|
|
790
1642
|
query: Uint8Array,
|
|
@@ -793,6 +1645,7 @@ async function crossRegionVotes(
|
|
|
793
1645
|
k: number,
|
|
794
1646
|
N: number,
|
|
795
1647
|
reachMemo: Map<number, AncestorReach>,
|
|
1648
|
+
td?: TraceDraft,
|
|
796
1649
|
): Promise<{ votes: RegionVote[]; superseded: Set<RegionVote> }> {
|
|
797
1650
|
// Candidate regions: every region that ALREADY CAST ITS OWN VOTE in
|
|
798
1651
|
// voteRegions — individually idf > 0, genuinely discriminative on its own,
|
|
@@ -844,6 +1697,14 @@ async function crossRegionVotes(
|
|
|
844
1697
|
)
|
|
845
1698
|
);
|
|
846
1699
|
const none = { votes: [], superseded: new Set<RegionVote>() };
|
|
1700
|
+
if (td) {
|
|
1701
|
+
td.crossRegionSummary = {
|
|
1702
|
+
eligibleRegions: eligible.length,
|
|
1703
|
+
maximalRegions: cand.length,
|
|
1704
|
+
probeLimit: k,
|
|
1705
|
+
probesAttempted: 0, // updated below as probes accrue
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
847
1708
|
if (cand.length < 2) return none;
|
|
848
1709
|
cand.sort((x, y) =>
|
|
849
1710
|
regions[x].start - regions[y].start || regions[x].end - regions[y].end
|
|
@@ -914,6 +1775,9 @@ async function crossRegionVotes(
|
|
|
914
1775
|
regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end)
|
|
915
1776
|
) continue;
|
|
916
1777
|
probes++;
|
|
1778
|
+
if (td?.crossRegionSummary) {
|
|
1779
|
+
td.crossRegionSummary.probesAttempted = probes;
|
|
1780
|
+
}
|
|
917
1781
|
|
|
918
1782
|
const left = query.subarray(ra.start, ra.end);
|
|
919
1783
|
const right = query.subarray(rb.start, rb.end);
|
|
@@ -922,112 +1786,218 @@ async function crossRegionVotes(
|
|
|
922
1786
|
const maxInterior = (left.length + right.length) * ctx.space.maxGroup;
|
|
923
1787
|
const cap = left.length + right.length + maxInterior;
|
|
924
1788
|
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1789
|
+
// The graded ladder (spec §1): exact DAG junction, then single-synonym,
|
|
1790
|
+
// then double-synonym, then — only when every DAG tier found nothing —
|
|
1791
|
+
// structural-resonance. `sides` (the two halo sibling lists) is loaded
|
|
1792
|
+
// ONCE and reused by junctionSynonyms AND structural-resonance, so no
|
|
1793
|
+
// ladder rung repeats a halo ANN query an earlier rung already paid for.
|
|
1794
|
+
const sides = await loadJunctionSynonymSides(ctx, left, right);
|
|
1795
|
+
|
|
1796
|
+
let tier: CrossRegionTier = "exact";
|
|
1797
|
+
let containers: Array<Junction | SynonymJunction> =
|
|
1798
|
+
junctionContainersFrom(
|
|
1799
|
+
ctx,
|
|
1800
|
+
left,
|
|
1801
|
+
right,
|
|
1802
|
+
cap,
|
|
1803
|
+
seedsOf(cand[a]),
|
|
1804
|
+
seedsOf(cand[b]),
|
|
1805
|
+
undefined,
|
|
1806
|
+
true,
|
|
1807
|
+
);
|
|
939
1808
|
if (containers.length === 0) {
|
|
940
|
-
//
|
|
941
|
-
|
|
1809
|
+
// Tiers 2-4 — synonym containers (junctionSynonyms itself runs
|
|
1810
|
+
// single-synonym first, falling to double-synonym only when
|
|
1811
|
+
// single-synonym found nothing — see junction.ts).
|
|
1812
|
+
const syn = await junctionSynonyms(
|
|
942
1813
|
ctx,
|
|
943
1814
|
left,
|
|
944
1815
|
right,
|
|
945
1816
|
maxInterior,
|
|
946
1817
|
true,
|
|
1818
|
+
sides,
|
|
947
1819
|
);
|
|
1820
|
+
if (syn.length > 0) {
|
|
1821
|
+
containers = syn;
|
|
1822
|
+
tier = syn[0].tier;
|
|
1823
|
+
}
|
|
948
1824
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
// of one phrase pairing "around" a gap chunk would merely rediscover
|
|
961
|
-
// the phrase they are shards of, then explain away its rivals.
|
|
962
|
-
let best: (typeof containers)[number] | null = null;
|
|
963
|
-
let bestExtras: number[] = [];
|
|
964
|
-
let bestCov = -1;
|
|
965
|
-
for (const c of containers) {
|
|
966
|
-
const bytes = cachedRead(ctx, cache, c.id, cap);
|
|
967
|
-
const li = indexOf(bytes, left, 0);
|
|
968
|
-
const ri = indexOf(bytes, right, 0);
|
|
969
|
-
if (li >= 0 && ri >= 0) {
|
|
970
|
-
const joined = bytes.subarray(
|
|
971
|
-
Math.min(li, ri),
|
|
972
|
-
Math.max(li + left.length, ri + right.length),
|
|
973
|
-
);
|
|
974
|
-
if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
|
|
1825
|
+
|
|
1826
|
+
// Tier 5 — structural-resonance ANN, the FINAL approximate proposal
|
|
1827
|
+
// path. Only reached when every DAG tier found NOTHING, and only when
|
|
1828
|
+
// there is no already-corroborated region between the endpoints (a
|
|
1829
|
+
// between-region with its own vote is evidence the gap already means
|
|
1830
|
+
// something specific — an ANN guess must not override it).
|
|
1831
|
+
let structuralPick:
|
|
1832
|
+
| {
|
|
1833
|
+
proposal: StructuralResonanceProposal;
|
|
1834
|
+
reach: AncestorReach;
|
|
1835
|
+
idf: number;
|
|
975
1836
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
//
|
|
1837
|
+
| null = null;
|
|
1838
|
+
if (containers.length === 0) {
|
|
1839
|
+
// Structural-resonance composes each side's OWN gist directly (no
|
|
1840
|
+
// byte-containment truth backs it, unlike the DAG tiers) — so, unlike
|
|
1841
|
+
// the DAG ladder (which tolerates one approximate side because byte
|
|
1842
|
+
// containment cannot lie), the ANN tier requires BOTH sides to be
|
|
1843
|
+
// KNOWN (content-addressed, exact identities): an approximate chunk
|
|
1844
|
+
// fragment's own resonance is noise at any tier, and composing noise
|
|
1845
|
+
// into a synthetic gist only manufactures a plausible-looking but
|
|
1846
|
+
// spurious ANN neighbour, not evidence of a genuine joint whole.
|
|
1847
|
+
// PHRASE-SCALE CONTRACT — the same one the DAG tiers hold their glue
|
|
1848
|
+
// to (see maxInterior above): a junction, exact or approximate, is a
|
|
1849
|
+
// whole the two forms nearly exhaust, not two arbitrary landmarks
|
|
1850
|
+
// anywhere in a long, multi-topic query. Without this, structural-
|
|
1851
|
+
// resonance would pair opposite ends of an unrelated scaffolding-
|
|
1852
|
+
// dominated query and manufacture a plausible-looking ANN neighbour
|
|
1853
|
+
// for a "gap" that never was a phrase.
|
|
1854
|
+
// BOTH sides must be independently DISCRIMINATIVE (individually
|
|
1855
|
+
// voted — `strong`, not merely a content-addressed `known` chunk):
|
|
1856
|
+
// a shared, non-discriminative scaffolding run (a repeated system
|
|
1857
|
+
// preamble) can be `known` without ever being distinctive evidence
|
|
1858
|
+
// of anything, and composing its own gist into a synthetic query
|
|
1859
|
+
// manufactures a plausible-looking but spurious ANN neighbour. The
|
|
1860
|
+
// DAG tiers can tolerate one merely-`known` side because byte
|
|
1861
|
+
// containment cannot lie; structural-resonance has no such
|
|
1862
|
+
// backstop, so both sides earn their place here the same way an
|
|
1863
|
+
// ordinary approximate region earns its individual vote.
|
|
1864
|
+
const gap = rb.start - ra.end;
|
|
981
1865
|
if (
|
|
982
|
-
between.
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
0,
|
|
987
|
-
) < 0
|
|
988
|
-
)
|
|
1866
|
+
between.length === 0 &&
|
|
1867
|
+
strong.has(cand[a]) && strong.has(cand[b]) &&
|
|
1868
|
+
ra.known && rb.known &&
|
|
1869
|
+
gap <= maxInterior
|
|
989
1870
|
) {
|
|
990
|
-
|
|
1871
|
+
const ownRootsA = rvs.votes.find((v) =>
|
|
1872
|
+
v.start === ra.start && v.end === ra.end
|
|
1873
|
+
)?.roots;
|
|
1874
|
+
const ownRootsB = rvs.votes.find((v) =>
|
|
1875
|
+
v.start === rb.start && v.end === rb.end
|
|
1876
|
+
)?.roots;
|
|
1877
|
+
structuralPick = await structuralResonance(
|
|
1878
|
+
ctx,
|
|
1879
|
+
query,
|
|
1880
|
+
ra,
|
|
1881
|
+
rb,
|
|
1882
|
+
sides,
|
|
1883
|
+
k,
|
|
1884
|
+
N,
|
|
1885
|
+
reachMemo,
|
|
1886
|
+
ownRootsA,
|
|
1887
|
+
ownRootsB,
|
|
1888
|
+
);
|
|
991
1889
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1890
|
+
if (structuralPick === null) continue;
|
|
1891
|
+
tier = "structural-resonance";
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
let best: (Junction | SynonymJunction) | null = null;
|
|
1895
|
+
let bestExtras: number[] = [];
|
|
1896
|
+
let bestCov = -1;
|
|
1897
|
+
let reach: AncestorReach;
|
|
1898
|
+
let idf: number;
|
|
1899
|
+
let confidence: number;
|
|
1900
|
+
|
|
1901
|
+
if (structuralPick !== null) {
|
|
1902
|
+
// A resonance proposal is NOT a Junction — there is no container to
|
|
1903
|
+
// read bytes from, so the self-evidence/contradiction/N-ary
|
|
1904
|
+
// machinery below (byte-verified against a real container) does not
|
|
1905
|
+
// apply; per spec §13, no N-ary extra-region coverage for resonance
|
|
1906
|
+
// proposals.
|
|
1907
|
+
best = { id: structuralPick.proposal.id, interior: new Uint8Array(0) };
|
|
1908
|
+
bestExtras = [];
|
|
1909
|
+
bestCov = rb.end - ra.start;
|
|
1910
|
+
reach = structuralPick.reach;
|
|
1911
|
+
idf = structuralPick.idf;
|
|
1912
|
+
confidence = structuralPick.proposal.effectiveScore;
|
|
1913
|
+
} else {
|
|
1914
|
+
// N-ARY selection: the container covering the MOST remaining candidate
|
|
1915
|
+
// forms wins (then tightest interior, then lowest id). Reads are
|
|
1916
|
+
// cache hits — every container's bytes were already read by the walk.
|
|
1917
|
+
//
|
|
1918
|
+
// SELF-EVIDENCE GUARD: a junction is BINDING evidence only when the
|
|
1919
|
+
// container joins forms the query mentions APART. When the container's
|
|
1920
|
+
// own joined occurrence (left..right including its interior) is a
|
|
1921
|
+
// literal substring of the query, the query already spells that phrase
|
|
1922
|
+
// out contiguously — perception already voted with it, and grid shards
|
|
1923
|
+
// of one phrase pairing "around" a gap chunk would merely rediscover
|
|
1924
|
+
// the phrase they are shards of, then explain away its rivals.
|
|
1925
|
+
for (const c of containers) {
|
|
1926
|
+
const bytes = cachedRead(ctx, cache, c.id, cap);
|
|
1927
|
+
const li = indexOf(bytes, left, 0);
|
|
1928
|
+
const ri = indexOf(bytes, right, 0);
|
|
1929
|
+
if (li >= 0 && ri >= 0) {
|
|
1930
|
+
const joined = bytes.subarray(
|
|
1931
|
+
Math.min(li, ri),
|
|
1932
|
+
Math.max(li + left.length, ri + right.length),
|
|
1933
|
+
);
|
|
1934
|
+
if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
|
|
1935
|
+
}
|
|
1936
|
+
// CONTRADICTION GUARD: a between-region already carrying its own
|
|
1937
|
+
// vote must actually recur in this container's bytes — otherwise
|
|
1938
|
+
// the container is a different learnt whole that happens to share
|
|
1939
|
+
// ra/rb, and letting it stand in for the gap would silently
|
|
1940
|
+
// override evidence the query itself already resolved there.
|
|
1941
|
+
if (
|
|
1942
|
+
between.some((bi) =>
|
|
1943
|
+
indexOf(
|
|
1944
|
+
bytes,
|
|
1945
|
+
query.subarray(regions[bi].start, regions[bi].end),
|
|
1946
|
+
0,
|
|
1947
|
+
) < 0
|
|
1948
|
+
)
|
|
1949
|
+
) {
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
let cov = left.length + right.length;
|
|
1953
|
+
const extras: number[] = [];
|
|
1954
|
+
for (const ei of cand) {
|
|
1955
|
+
if (ei === cand[a] || ei === cand[b] || consumed.has(ei)) continue;
|
|
1956
|
+
const e = regions[ei];
|
|
1957
|
+
if (overlapsSpan(e, ra) || overlapsSpan(e, rb)) continue;
|
|
1958
|
+
const eb = query.subarray(e.start, e.end);
|
|
1959
|
+
if (indexOf(bytes, eb, 0) >= 0) {
|
|
1960
|
+
extras.push(ei);
|
|
1961
|
+
cov += eb.length;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (
|
|
1965
|
+
cov > bestCov ||
|
|
1966
|
+
(cov === bestCov && best !== null &&
|
|
1967
|
+
(c.interior.length < best.interior.length ||
|
|
1968
|
+
(c.interior.length === best.interior.length && c.id < best.id)))
|
|
1969
|
+
) {
|
|
1970
|
+
best = c;
|
|
1971
|
+
bestExtras = extras;
|
|
1972
|
+
bestCov = cov;
|
|
1002
1973
|
}
|
|
1003
1974
|
}
|
|
1004
|
-
if (
|
|
1005
|
-
cov > bestCov ||
|
|
1006
|
-
(cov === bestCov && best !== null &&
|
|
1007
|
-
(c.interior.length < best.interior.length ||
|
|
1008
|
-
(c.interior.length === best.interior.length && c.id < best.id)))
|
|
1009
|
-
) {
|
|
1010
|
-
best = c;
|
|
1011
|
-
bestExtras = extras;
|
|
1012
|
-
bestCov = cov;
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
if (best === null) continue; // every container was self-evidence
|
|
1975
|
+
if (best === null) continue; // every container was self-evidence
|
|
1016
1976
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1977
|
+
const r = edgeAncestors(ctx, best.id, N, reachMemo);
|
|
1978
|
+
if (r.saturated || r.roots.length === 0) continue;
|
|
1979
|
+
const df = Math.log(N / Math.max(1, r.contextsReached));
|
|
1980
|
+
if (df <= 0) continue;
|
|
1981
|
+
reach = r;
|
|
1982
|
+
idf = df;
|
|
1983
|
+
// Confidence used by voting (spec §13): exact junction = 1;
|
|
1984
|
+
// single/double-synonym = the sibling(s)' score(s), carried on the
|
|
1985
|
+
// SynonymJunction the ladder selected.
|
|
1986
|
+
confidence = "confidence" in best ? best.confidence : 1;
|
|
1987
|
+
}
|
|
1021
1988
|
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1989
|
+
// MUTUAL-EXPLANATION WEIGHT — the same formula for every tier, with
|
|
1990
|
+
// `confidence` collapsed to certainty (1) for exact evidence: under
|
|
1991
|
+
// that collapse this is byte-for-byte the old exact-only formula
|
|
1992
|
+
// (min(1,ratio)·min(1,1/ratio)). For structural-resonance,
|
|
1993
|
+
// `confidence` is already annScore·semanticConfidence — never
|
|
1994
|
+
// multiplied a second time.
|
|
1026
1995
|
const lenR = Math.max(1, bestCov);
|
|
1027
1996
|
const ratio = Math.sqrt(
|
|
1028
1997
|
Math.max(1, ctx.store.contentLen(best.id, lenR * ctx.store.D)) / lenR,
|
|
1029
1998
|
);
|
|
1030
|
-
const mutual = Math.min(1,
|
|
1999
|
+
const mutual = Math.min(1, confidence * ratio) *
|
|
2000
|
+
Math.min(1, confidence / ratio);
|
|
1031
2001
|
const w = (mutual * idf) / reach.roots.length;
|
|
1032
2002
|
let spanStart = ra.start;
|
|
1033
2003
|
let spanEnd = rb.end;
|
|
@@ -1049,15 +2019,33 @@ async function crossRegionVotes(
|
|
|
1049
2019
|
// for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
|
|
1050
2020
|
// field) must credit the junction with all of it, not just the ONE
|
|
1051
2021
|
// pooled axiom it collapses to.
|
|
1052
|
-
|
|
1053
|
-
|
|
2022
|
+
// Only EXACT DAG evidence may explain away ordinary votes (spec §15).
|
|
2023
|
+
// Single-synonym, double-synonym, and structural-resonance may ADD
|
|
2024
|
+
// supporting evidence but never remove it: their evidence is itself
|
|
2025
|
+
// approximate (a sibling substitution, or an ANN guess), so treating
|
|
2026
|
+
// their byte-containment the way exact containment is treated would
|
|
2027
|
+
// let an approximation override a genuine, independently-voted region.
|
|
1054
2028
|
let explainedAway = 0;
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
2029
|
+
// Exact set of ORIGINAL region indices this junction explained away —
|
|
2030
|
+
// recorded live as `superseded.add` fires (spec §3's explicit rule:
|
|
2031
|
+
// never inferred from `absorbed` afterward).
|
|
2032
|
+
const explainedAwayIndices: number[] = [];
|
|
2033
|
+
if (tier === "exact") {
|
|
2034
|
+
const containerBytes = cachedRead(ctx, cache, best.id, cap);
|
|
2035
|
+
const jointRoots = new Set(reach.roots);
|
|
2036
|
+
for (const rv of rvs.votes) {
|
|
2037
|
+
if (rv.roots.some((r) => jointRoots.has(r))) continue;
|
|
2038
|
+
const bytes = query.subarray(rv.start, rv.end);
|
|
2039
|
+
if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
|
|
2040
|
+
superseded.add(rv);
|
|
2041
|
+
explainedAway++;
|
|
2042
|
+
if (td) {
|
|
2043
|
+
const idx = regions.findIndex((r) =>
|
|
2044
|
+
r.start === rv.start && r.end === rv.end
|
|
2045
|
+
);
|
|
2046
|
+
if (idx >= 0) explainedAwayIndices.push(idx);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
1061
2049
|
}
|
|
1062
2050
|
}
|
|
1063
2051
|
|
|
@@ -1070,11 +2058,44 @@ async function crossRegionVotes(
|
|
|
1070
2058
|
wFocus: w,
|
|
1071
2059
|
absorbed: 1 + explainedAway,
|
|
1072
2060
|
});
|
|
2061
|
+
if (td) {
|
|
2062
|
+
td.crossRegionJunctionVotes.push({
|
|
2063
|
+
container: best.id,
|
|
2064
|
+
span: [spanStart, spanEnd],
|
|
2065
|
+
roots: [...reach.roots],
|
|
2066
|
+
sourceRegionIndices: [cand[a], cand[b], ...bestExtras],
|
|
2067
|
+
explainedAwayRegionIndices: explainedAwayIndices,
|
|
2068
|
+
absorbed: 1 + explainedAway,
|
|
2069
|
+
tier,
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
1073
2072
|
|
|
1074
2073
|
const label = [cand[a], cand[b], ...bestExtras]
|
|
1075
2074
|
.sort((x, y) => regions[x].start - regions[y].start)
|
|
1076
2075
|
.map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
|
|
1077
2076
|
.join(" ▸ ");
|
|
2077
|
+
const tierNote = tier === "exact"
|
|
2078
|
+
? `junction node ${best.id}` +
|
|
2079
|
+
(best.interior.length === 0
|
|
2080
|
+
? " (adjacent)"
|
|
2081
|
+
: ` (interior "${dec(best.interior)}")`) +
|
|
2082
|
+
", by content-addressed ascent"
|
|
2083
|
+
: tier === "structural-resonance"
|
|
2084
|
+
? `structurally-composed ANN proposal, node ${best.id} — the query ` +
|
|
2085
|
+
`structurally composed the endpoint regions, the real middle-` +
|
|
2086
|
+
`query structure, and the selected halo-sibling endpoint ` +
|
|
2087
|
+
`direction(s) (variant ${structuralPick!.proposal.variant}, ` +
|
|
2088
|
+
`annScore ${structuralPick!.proposal.annScore.toFixed(3)} × ` +
|
|
2089
|
+
`semanticConfidence ${
|
|
2090
|
+
structuralPick!.proposal.semanticConfidence.toFixed(3)
|
|
2091
|
+
} = effectiveScore ${
|
|
2092
|
+
structuralPick!.proposal.effectiveScore.toFixed(3)
|
|
2093
|
+
}); it did not concatenate endpoint bytes or rewrite the query`
|
|
2094
|
+
: `${tier} junction node ${best.id}` +
|
|
2095
|
+
(best.interior.length === 0
|
|
2096
|
+
? " (adjacent)"
|
|
2097
|
+
: ` (interior "${dec(best.interior)}")`) +
|
|
2098
|
+
`, by halo-sibling DAG ascent (confidence ${confidence.toFixed(3)})`;
|
|
1078
2099
|
ctx.trace?.step(
|
|
1079
2100
|
"crossRegion",
|
|
1080
2101
|
[{ text: label, role: "pair" }],
|
|
@@ -1083,11 +2104,7 @@ async function crossRegionVotes(
|
|
|
1083
2104
|
node: r,
|
|
1084
2105
|
role: "joint-context",
|
|
1085
2106
|
})),
|
|
1086
|
-
`${label} →
|
|
1087
|
-
(best.interior.length === 0
|
|
1088
|
-
? " (adjacent)"
|
|
1089
|
-
: ` (interior "${dec(best.interior)}")`) +
|
|
1090
|
-
` → ${reach.roots.length} context(s), by content-addressed ascent` +
|
|
2107
|
+
`${label} → ${tierNote} → ${reach.roots.length} context(s)` +
|
|
1091
2108
|
(superseded.size > 0
|
|
1092
2109
|
? `; ${superseded.size} aliasing vote(s) explained away`
|
|
1093
2110
|
: ""),
|
|
@@ -1096,15 +2113,25 @@ async function crossRegionVotes(
|
|
|
1096
2113
|
}
|
|
1097
2114
|
}
|
|
1098
2115
|
|
|
2116
|
+
if (td) td.supersededOrdinaryVotes = superseded.size;
|
|
1099
2117
|
return { votes: out, superseded };
|
|
1100
2118
|
}
|
|
1101
2119
|
|
|
2120
|
+
/** Emit the "climbConsensus" step — the human-readable note this always
|
|
2121
|
+
* produced, now paired (when `ctx.trace` and `cfg` are both present) with
|
|
2122
|
+
* the structured {@link ClimbConsensusData} payload on the SAME step's
|
|
2123
|
+
* `data` field. Every exit of {@link computeAttention} funnels through
|
|
2124
|
+
* here, so instrumentation and the existing rationale text can never drift
|
|
2125
|
+
* apart — see the instrumentation spec's §9 "every exit path". */
|
|
1102
2126
|
export function traceAttention(
|
|
1103
2127
|
ctx: MindContext,
|
|
1104
2128
|
regions: ReadonlyArray<{ start: number; end: number }>,
|
|
1105
2129
|
regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
|
|
1106
2130
|
roots: ReadonlyArray<Attention>,
|
|
1107
2131
|
steps: ReadonlyArray<DerivationStep> = [],
|
|
2132
|
+
td?: TraceDraft,
|
|
2133
|
+
cfg?: ClimbConsensusCfg,
|
|
2134
|
+
ranked: ReadonlyArray<Attention> = roots,
|
|
1108
2135
|
): void {
|
|
1109
2136
|
if (!ctx.trace) return;
|
|
1110
2137
|
const voters: RationaleItem[] = [];
|
|
@@ -1119,6 +2146,49 @@ export function traceAttention(
|
|
|
1119
2146
|
// The pooled-evidence decision, one DerivationStep per anchor — the same
|
|
1120
2147
|
// shape {@link GraphSearch}'s own cover steps take (see traceDerivation).
|
|
1121
2148
|
if (steps.length > 0) traceDerivation(ctx, steps);
|
|
2149
|
+
|
|
2150
|
+
const data: ClimbConsensusData | undefined = (td && cfg)
|
|
2151
|
+
? {
|
|
2152
|
+
version: 1,
|
|
2153
|
+
cache: { hit: false, detailAvailable: true },
|
|
2154
|
+
config: {
|
|
2155
|
+
annK: cfg.k,
|
|
2156
|
+
crossRegionProbeLimit: cfg.k,
|
|
2157
|
+
mode: cfg.mode,
|
|
2158
|
+
...(cfg.N !== undefined ? { corpusN: cfg.N } : {}),
|
|
2159
|
+
dimension: ctx.store.D,
|
|
2160
|
+
...(cfg.N !== undefined ? { hubBound: hubBound(ctx) } : {}),
|
|
2161
|
+
estimatorNoise: estimatorNoise(ctx.store.D),
|
|
2162
|
+
...(cfg.naturalBreak !== undefined
|
|
2163
|
+
? { naturalBreak: cfg.naturalBreak }
|
|
2164
|
+
: {}),
|
|
2165
|
+
...(cfg.consensusFloor !== undefined
|
|
2166
|
+
? { consensusFloor: cfg.consensusFloor }
|
|
2167
|
+
: {}),
|
|
2168
|
+
},
|
|
2169
|
+
candidates: {
|
|
2170
|
+
perceived: cfg.perceivedCount,
|
|
2171
|
+
recognised: cfg.totalRegions - cfg.perceivedCount,
|
|
2172
|
+
total: cfg.totalRegions,
|
|
2173
|
+
},
|
|
2174
|
+
...(td.regions.length > 0 ? { regions: td.regions } : {}),
|
|
2175
|
+
...(cfg.reachMemo ? { reaches: serialiseReaches(cfg.reachMemo) } : {}),
|
|
2176
|
+
...(td.crossRegionSummary
|
|
2177
|
+
? {
|
|
2178
|
+
crossRegion: {
|
|
2179
|
+
...td.crossRegionSummary,
|
|
2180
|
+
junctionVotes: td.crossRegionJunctionVotes,
|
|
2181
|
+
supersededOrdinaryVotes: td.supersededOrdinaryVotes,
|
|
2182
|
+
},
|
|
2183
|
+
}
|
|
2184
|
+
: {}),
|
|
2185
|
+
...(td.saturation ? { saturation: td.saturation } : {}),
|
|
2186
|
+
...(td.pooling ? { pooling: td.pooling } : {}),
|
|
2187
|
+
...(td.anchors.length > 0 ? { anchors: td.anchors } : {}),
|
|
2188
|
+
result: { roots: [...roots], ranked: [...ranked] },
|
|
2189
|
+
}
|
|
2190
|
+
: undefined;
|
|
2191
|
+
|
|
1122
2192
|
t.done(
|
|
1123
2193
|
roots.map((r) => rNode(ctx, r.anchor, "anchor", r.vote)),
|
|
1124
2194
|
roots.length === 0
|
|
@@ -1130,5 +2200,6 @@ export function traceAttention(
|
|
|
1130
2200
|
: `${voters.length} of ${regions.length} sub-regions voted; consensus ordered ${roots.length} INDEPENDENT points of attention (votes ${
|
|
1131
2201
|
roots.map((r) => r.vote.toFixed(2)).join(", ")
|
|
1132
2202
|
})`,
|
|
2203
|
+
data,
|
|
1133
2204
|
);
|
|
1134
2205
|
}
|