@hviana/sema 0.2.5 → 0.2.7

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.
@@ -15,6 +15,15 @@ import { type JunctionSynonymSides } from "./junction.js";
15
15
  * spec's original two-value type cannot do. */
16
16
  export type ClimbConsensusJunctionTier = CrossRegionTier;
17
17
  export type RegionOutcome = "voted" | "no-ann-hit" | "no-structural-reach" | "saturated-abstention" | "nonpositive-df-weight" | "contrastive-margin-rejection";
18
+ /** The best DIFFERENT-conclusion rival the contrastive-margin gate found
19
+ * while scanning an ordinary (approximate) region's ANN hits — spec §1.
20
+ * Its roots/saturation/contextsReached are already available through
21
+ * `reaches` (serialiseReaches) for `node`; not duplicated here. */
22
+ export interface ConsensusContrastiveRivalTrace {
23
+ node: number;
24
+ rank: number;
25
+ score: number;
26
+ }
18
27
  export interface ConsensusRegionTrace {
19
28
  index: number;
20
29
  source: "perceived" | "recognised";
@@ -40,6 +49,7 @@ export interface ConsensusRegionTrace {
40
49
  dfWeight?: number;
41
50
  contrastiveMargin?: number;
42
51
  contrastiveNoiseFloor?: number;
52
+ contrastiveRival?: ConsensusContrastiveRivalTrace;
43
53
  mutualWeight?: number;
44
54
  voteWeightPerRoot?: number;
45
55
  focusWeightPerRoot?: number;
@@ -82,6 +92,77 @@ export interface JunctionVoteTrace {
82
92
  explainedAwayRegionIndices: number[];
83
93
  absorbed: number;
84
94
  tier?: ClimbConsensusJunctionTier;
95
+ /** Zero-based index into `crossRegion.probes` — the probe this vote was
96
+ * produced from (spec §8). */
97
+ probe: number;
98
+ confidence: number;
99
+ /** "Evidence bytes" — the container-coverage byte count (the existing
100
+ * `bestCov` variable at the push site). */
101
+ evidenceBytes: number;
102
+ mutualWeight: number;
103
+ voteWeightPerRoot: number;
104
+ }
105
+ /** Whether one DAG/synonym tier attempt was even made for a probe, and how
106
+ * many candidate containers it returned — spec §2/§3. */
107
+ export interface CrossRegionTierAttemptTrace {
108
+ attempted: boolean;
109
+ candidatesReturned: number;
110
+ }
111
+ /** Aggregate outcome of the container-selection loop for a DAG/synonym tier
112
+ * that returned at least one container — spec §4. Only aggregate counts
113
+ * and the final outcome are recorded, never every candidate. */
114
+ export interface CrossRegionStructuralTrace {
115
+ tier: "exact" | "single-synonym" | "double-synonym";
116
+ selfEvidenceRejected: number;
117
+ contradictionRejected: number;
118
+ passedGuards: number;
119
+ selectedNode?: number;
120
+ outcome: "all-rejected" | "saturated" | "no-roots" | "nonpositive-idf" | "accepted";
121
+ }
122
+ /** One retained structural-resonance variant that actually issued its own
123
+ * ANN query — spec §5. */
124
+ export interface StructuralResonanceVariantTrace {
125
+ kind: StructuralVariant["kind"];
126
+ semanticConfidence: number;
127
+ leftSiblingId?: number;
128
+ rightSiblingId?: number;
129
+ annHitsReturned: number;
130
+ }
131
+ /** One merged structural-resonance proposal actually examined via
132
+ * edgeAncestors — spec §5. Retains node/variant/scores, but NOT
133
+ * roots/saturation/contextsReached/idf (already in `reaches`). */
134
+ export interface StructuralResonanceCandidateTrace {
135
+ node: number;
136
+ variant: StructuralVariant["kind"];
137
+ leftSiblingId?: number;
138
+ rightSiblingId?: number;
139
+ annScore: number;
140
+ semanticConfidence: number;
141
+ effectiveScore: number;
142
+ outcome: "saturated" | "no-roots" | "nonpositive-idf" | "same-as-endpoint" | "same-as-selected" | "selected" | "contrastive-rival";
143
+ }
144
+ export interface StructuralResonanceTrace {
145
+ variantBudget: number;
146
+ variants: StructuralResonanceVariantTrace[];
147
+ mergedProposals: number;
148
+ examined: StructuralResonanceCandidateTrace[];
149
+ contrastiveMargin?: number;
150
+ noiseFloor: number;
151
+ outcome: "ineligible" | "empty" | "no-valid-proposal" | "margin-rejected" | "accepted";
152
+ ineligibleReasons?: Array<"between-region" | "not-both-strong" | "not-both-known" | "gap-too-large">;
153
+ }
154
+ /** One cross-region pair the ladder actually probed — spec §2. Exactly one
155
+ * of these is pushed per pair that incremented `probes`. */
156
+ export interface CrossRegionProbeTrace {
157
+ leftRegionIndex: number;
158
+ rightRegionIndex: number;
159
+ betweenRegionIndices: number[];
160
+ exact: CrossRegionTierAttemptTrace;
161
+ singleSynonym: CrossRegionTierAttemptTrace;
162
+ doubleSynonym: CrossRegionTierAttemptTrace;
163
+ structural?: CrossRegionStructuralTrace;
164
+ resonance?: StructuralResonanceTrace;
165
+ outcome: "accepted" | "structural-rejected" | "resonance-ineligible" | "resonance-rejected";
85
166
  }
86
167
  export interface ClimbConsensusData {
87
168
  version: 1;
@@ -114,6 +195,8 @@ export interface ClimbConsensusData {
114
195
  probesAttempted: number;
115
196
  junctionVotes: JunctionVoteTrace[];
116
197
  supersededOrdinaryVotes: number;
198
+ probes: CrossRegionProbeTrace[];
199
+ stopReason: "insufficient-regions" | "probe-limit" | "pairs-exhausted";
117
200
  };
118
201
  saturation?: {
119
202
  regionIntervals: Array<{
@@ -145,7 +228,9 @@ interface TraceDraft {
145
228
  maximalRegions: number;
146
229
  probeLimit: number;
147
230
  probesAttempted: number;
231
+ stopReason?: "insufficient-regions" | "probe-limit" | "pairs-exhausted";
148
232
  };
233
+ crossRegionProbes: CrossRegionProbeTrace[];
149
234
  supersededOrdinaryVotes: number;
150
235
  saturation?: {
151
236
  regionIntervals: Array<{
@@ -293,7 +378,7 @@ export declare function structuralResonance(ctx: MindContext, query: Uint8Array,
293
378
  * candidate whose reach is exactly one side's own conclusion is not new
294
379
  * evidence of a joint whole; it is that side's resonance rediscovering
295
380
  * itself through a synthetic gist still dominated by its own direction. */
296
- ownRootsA: readonly number[] | undefined, ownRootsB: readonly number[] | undefined): Promise<{
381
+ ownRootsA: readonly number[] | undefined, ownRootsB: readonly number[] | undefined, trace?: StructuralResonanceTrace): Promise<{
297
382
  proposal: StructuralResonanceProposal;
298
383
  reach: AncestorReach;
299
384
  idf: number;
@@ -21,6 +21,7 @@ function newTraceDraft(perceivedCount) {
21
21
  perceivedCount,
22
22
  regions: [],
23
23
  crossRegionJunctionVotes: [],
24
+ crossRegionProbes: [],
24
25
  supersededOrdinaryVotes: 0,
25
26
  anchors: [],
26
27
  };
@@ -416,9 +417,12 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
416
417
  // nothing. Gating at the noise floor keeps frame-echo suppression (a frame
417
418
  // region's margin ≈ 0 is gated out) without penalising honest evidence.
418
419
  let contrastiveMargin;
420
+ let contrastiveRival;
419
421
  if (!known) {
420
422
  let margin = score;
421
- for (const h of await ensureHits()) {
423
+ const hitsForRival = await ensureHits();
424
+ for (let hi = 0; hi < hitsForRival.length; hi++) {
425
+ const h = hitsForRival[hi];
422
426
  if (h.id === voterId)
423
427
  continue;
424
428
  const r2 = edgeAncestors(ctx, h.id, N, reachMemo);
@@ -428,6 +432,9 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
428
432
  if (sameRoots(r2.roots, reach.roots))
429
433
  continue; // same conclusion
430
434
  margin = score - h.score; // hits are nearest-first: the best rival
435
+ if (td) {
436
+ contrastiveRival = { node: h.id, rank: hi, score: h.score };
437
+ }
431
438
  break;
432
439
  }
433
440
  contrastiveMargin = margin;
@@ -440,6 +447,7 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
440
447
  dfWeight: wf,
441
448
  contrastiveMargin: margin,
442
449
  contrastiveNoiseFloor: noiseFloor,
450
+ ...(contrastiveRival ? { contrastiveRival } : {}),
443
451
  });
444
452
  continue;
445
453
  }
@@ -488,6 +496,7 @@ export async function voteRegions(ctx, query, regions, k, mode, N, reachMemo, td
488
496
  ? {
489
497
  contrastiveMargin,
490
498
  contrastiveNoiseFloor: estimatorNoise(ctx.store.D),
499
+ ...(contrastiveRival ? { contrastiveRival } : {}),
491
500
  }
492
501
  : {}),
493
502
  mutualWeight: mutual,
@@ -1011,8 +1020,10 @@ export async function structuralResonance(ctx, query, ra, rb, sides, k, N, reach
1011
1020
  * candidate whose reach is exactly one side's own conclusion is not new
1012
1021
  * evidence of a joint whole; it is that side's resonance rediscovering
1013
1022
  * itself through a synthetic gist still dominated by its own direction. */
1014
- ownRootsA, ownRootsB) {
1023
+ ownRootsA, ownRootsB, trace) {
1015
1024
  const { variants } = buildStructuralVariants(ctx, ra, rb, sides);
1025
+ if (trace)
1026
+ trace.variantBudget = ctx.cfg.haloQueryK;
1016
1027
  const middleBytes = query.subarray(ra.end, rb.start);
1017
1028
  const middlePart = middleBytes.length === 0
1018
1029
  ? null
@@ -1025,6 +1036,15 @@ ownRootsA, ownRootsB) {
1025
1036
  parts.push(variant.right);
1026
1037
  const synthetic = composeStructuralGist(ctx.space, parts);
1027
1038
  const hits = await ctx.store.resonate(synthetic, k);
1039
+ if (trace) {
1040
+ trace.variants.push({
1041
+ kind: variant.kind,
1042
+ semanticConfidence: variant.semanticConfidence,
1043
+ leftSiblingId: variant.leftSiblingId,
1044
+ rightSiblingId: variant.rightSiblingId,
1045
+ annHitsReturned: hits.length,
1046
+ });
1047
+ }
1028
1048
  for (const hit of hits) {
1029
1049
  const candidate = {
1030
1050
  id: hit.id,
@@ -1041,43 +1061,92 @@ ownRootsA, ownRootsB) {
1041
1061
  }
1042
1062
  }
1043
1063
  }
1044
- if (proposals.size === 0)
1064
+ if (trace)
1065
+ trace.mergedProposals = proposals.size;
1066
+ if (proposals.size === 0) {
1067
+ if (trace) {
1068
+ trace.noiseFloor = estimatorNoise(ctx.store.D);
1069
+ trace.outcome = "empty";
1070
+ }
1045
1071
  return null;
1072
+ }
1046
1073
  const sorted = [...proposals.values()].sort((a, b) => b.effectiveScore - a.effectiveScore || a.id - b.id);
1074
+ // One shared shape for every `examined` entry (spec §5): only `outcome`
1075
+ // varies across the six exit points below, so build it once instead of
1076
+ // repeating the six-field literal at each site.
1077
+ const recordExamined = (p, outcome) => {
1078
+ if (!trace)
1079
+ return;
1080
+ trace.examined.push({
1081
+ node: p.id,
1082
+ variant: p.variant,
1083
+ leftSiblingId: p.leftSiblingId,
1084
+ rightSiblingId: p.rightSiblingId,
1085
+ annScore: p.annScore,
1086
+ semanticConfidence: p.semanticConfidence,
1087
+ effectiveScore: p.effectiveScore,
1088
+ outcome,
1089
+ });
1090
+ };
1047
1091
  let selected = null;
1048
1092
  let selectedReach = null;
1049
1093
  let selectedIdf = 0;
1050
1094
  let rival = null;
1051
1095
  for (const p of sorted) {
1052
1096
  const reach = edgeAncestors(ctx, p.id, N, reachMemo);
1053
- if (reach.saturated || reach.roots.length === 0)
1097
+ if (reach.saturated || reach.roots.length === 0) {
1098
+ recordExamined(p, reach.saturated ? "saturated" : "no-roots");
1054
1099
  continue;
1100
+ }
1055
1101
  const idf = Math.log(N / Math.max(1, reach.contextsReached));
1056
- if (idf <= 0)
1102
+ if (idf <= 0) {
1103
+ recordExamined(p, "nonpositive-idf");
1057
1104
  continue;
1105
+ }
1058
1106
  // Self-evidence backstop (see the param doc above): a candidate that is
1059
1107
  // exactly one side's own already-voted conclusion carries no JOINT
1060
1108
  // evidence — skip it as if it never survived.
1061
1109
  if ((ownRootsA && sameRoots(reach.roots, ownRootsA)) ||
1062
- (ownRootsB && sameRoots(reach.roots, ownRootsB)))
1110
+ (ownRootsB && sameRoots(reach.roots, ownRootsB))) {
1111
+ recordExamined(p, "same-as-endpoint");
1063
1112
  continue;
1113
+ }
1064
1114
  if (selected === null) {
1065
1115
  selected = p;
1066
1116
  selectedReach = reach;
1067
1117
  selectedIdf = idf;
1118
+ recordExamined(p, "selected");
1068
1119
  }
1069
1120
  else if (!sameRoots(reach.roots, selectedReach.roots)) {
1070
1121
  rival = p;
1122
+ recordExamined(p, "contrastive-rival");
1071
1123
  break;
1072
1124
  }
1125
+ else {
1126
+ recordExamined(p, "same-as-selected");
1127
+ }
1073
1128
  }
1074
- if (selected === null || selectedReach === null)
1129
+ if (selected === null || selectedReach === null) {
1130
+ if (trace) {
1131
+ trace.noiseFloor = estimatorNoise(ctx.store.D);
1132
+ trace.outcome = "no-valid-proposal";
1133
+ }
1075
1134
  return null;
1135
+ }
1076
1136
  const margin = rival
1077
1137
  ? selected.effectiveScore - rival.effectiveScore
1078
1138
  : selected.effectiveScore;
1079
- if (margin <= estimatorNoise(ctx.store.D))
1139
+ if (trace) {
1140
+ trace.contrastiveMargin = margin;
1141
+ trace.noiseFloor = estimatorNoise(ctx.store.D);
1142
+ }
1143
+ if (margin <= estimatorNoise(ctx.store.D)) {
1144
+ if (trace)
1145
+ trace.outcome = "margin-rejected";
1080
1146
  return null;
1147
+ }
1148
+ if (trace)
1149
+ trace.outcome = "accepted";
1081
1150
  return { proposal: selected, reach: selectedReach, idf: selectedIdf };
1082
1151
  }
1083
1152
  async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
@@ -1136,6 +1205,7 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1136
1205
  maximalRegions: cand.length,
1137
1206
  probeLimit: k,
1138
1207
  probesAttempted: 0, // updated below as probes accrue
1208
+ stopReason: cand.length < 2 ? "insufficient-regions" : undefined,
1139
1209
  };
1140
1210
  }
1141
1211
  if (cand.length < 2)
@@ -1205,6 +1275,30 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1205
1275
  if (td?.crossRegionSummary) {
1206
1276
  td.crossRegionSummary.probesAttempted = probes;
1207
1277
  }
1278
+ // Trace-only per-probe bookkeeping (spec §2-§7) — built incrementally
1279
+ // as the ladder runs, pushed exactly once at whichever exit fires
1280
+ // below. `pushProbe` is called at every continue/success exit for
1281
+ // THIS pair so the invariant `probes.length === probesAttempted`
1282
+ // holds regardless of which tier settled it.
1283
+ const probe = td
1284
+ ? {
1285
+ leftRegionIndex: cand[a],
1286
+ rightRegionIndex: cand[b],
1287
+ betweenRegionIndices: [...between],
1288
+ exact: { attempted: false, candidatesReturned: 0 },
1289
+ singleSynonym: { attempted: false, candidatesReturned: 0 },
1290
+ doubleSynonym: { attempted: false, candidatesReturned: 0 },
1291
+ outcome: "structural-rejected",
1292
+ }
1293
+ : undefined;
1294
+ let probePushed = false;
1295
+ const pushProbe = (outcome) => {
1296
+ if (!td || !probe || probePushed)
1297
+ return;
1298
+ probe.outcome = outcome;
1299
+ td.crossRegionProbes.push(probe);
1300
+ probePushed = true;
1301
+ };
1208
1302
  const left = query.subarray(ra.start, ra.end);
1209
1303
  const right = query.subarray(rb.start, rb.end);
1210
1304
  // Phrase-scale contract, exactly as the bridge: the glue between the two
@@ -1219,11 +1313,37 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1219
1313
  const sides = await loadJunctionSynonymSides(ctx, left, right);
1220
1314
  let tier = "exact";
1221
1315
  let containers = junctionContainersFrom(ctx, left, right, cap, seedsOf(cand[a]), seedsOf(cand[b]), undefined, true);
1316
+ if (probe) {
1317
+ probe.exact = {
1318
+ attempted: true,
1319
+ candidatesReturned: containers.length,
1320
+ };
1321
+ }
1222
1322
  if (containers.length === 0) {
1223
1323
  // Tiers 2-4 — synonym containers (junctionSynonyms itself runs
1224
1324
  // single-synonym first, falling to double-synonym only when
1225
1325
  // single-synonym found nothing — see junction.ts).
1226
1326
  const syn = await junctionSynonyms(ctx, left, right, maxInterior, true, sides);
1327
+ if (probe) {
1328
+ const singleAttempted = sides.leftSiblings.length > 0 ||
1329
+ sides.rightSiblings.length > 0;
1330
+ const singleReturned = syn[0]?.tier === "single-synonym"
1331
+ ? syn.length
1332
+ : 0;
1333
+ const doubleAttempted = singleAttempted && singleReturned === 0 &&
1334
+ sides.leftSiblings.length > 0 && sides.rightSiblings.length > 0;
1335
+ const doubleReturned = syn[0]?.tier === "double-synonym"
1336
+ ? syn.length
1337
+ : 0;
1338
+ probe.singleSynonym = {
1339
+ attempted: singleAttempted,
1340
+ candidatesReturned: singleReturned,
1341
+ };
1342
+ probe.doubleSynonym = {
1343
+ attempted: doubleAttempted,
1344
+ candidatesReturned: doubleReturned,
1345
+ };
1346
+ }
1227
1347
  if (syn.length > 0) {
1228
1348
  containers = syn;
1229
1349
  tier = syn[0].tier;
@@ -1262,16 +1382,55 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1262
1382
  // backstop, so both sides earn their place here the same way an
1263
1383
  // ordinary approximate region earns its individual vote.
1264
1384
  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) {
1385
+ const reasons = [];
1386
+ if (between.length > 0)
1387
+ reasons.push("between-region");
1388
+ if (!strong.has(cand[a]) || !strong.has(cand[b])) {
1389
+ reasons.push("not-both-strong");
1390
+ }
1391
+ if (!ra.known || !rb.known)
1392
+ reasons.push("not-both-known");
1393
+ if (gap > maxInterior)
1394
+ reasons.push("gap-too-large");
1395
+ let resonanceTrace;
1396
+ if (reasons.length > 0) {
1397
+ if (probe) {
1398
+ resonanceTrace = {
1399
+ variantBudget: ctx.cfg.haloQueryK,
1400
+ variants: [],
1401
+ mergedProposals: 0,
1402
+ examined: [],
1403
+ noiseFloor: estimatorNoise(ctx.store.D),
1404
+ outcome: "ineligible",
1405
+ ineligibleReasons: reasons,
1406
+ };
1407
+ probe.resonance = resonanceTrace;
1408
+ }
1409
+ }
1410
+ else {
1411
+ if (probe) {
1412
+ // `outcome`/`noiseFloor` are required fields with no natural
1413
+ // "unset" value; structuralResonance (called just below) always
1414
+ // overwrites both before returning, on every one of its exit
1415
+ // paths — these are never read in their initial form.
1416
+ resonanceTrace = {
1417
+ variantBudget: ctx.cfg.haloQueryK,
1418
+ variants: [],
1419
+ mergedProposals: 0,
1420
+ examined: [],
1421
+ noiseFloor: 0,
1422
+ outcome: "empty",
1423
+ };
1424
+ probe.resonance = resonanceTrace;
1425
+ }
1269
1426
  const ownRootsA = rvs.votes.find((v) => v.start === ra.start && v.end === ra.end)?.roots;
1270
1427
  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);
1428
+ structuralPick = await structuralResonance(ctx, query, ra, rb, sides, k, N, reachMemo, ownRootsA, ownRootsB, resonanceTrace);
1272
1429
  }
1273
- if (structuralPick === null)
1430
+ if (structuralPick === null) {
1431
+ pushProbe(reasons.length > 0 ? "resonance-ineligible" : "resonance-rejected");
1274
1432
  continue;
1433
+ }
1275
1434
  tier = "structural-resonance";
1276
1435
  }
1277
1436
  let best = null;
@@ -1294,6 +1453,21 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1294
1453
  confidence = structuralPick.proposal.effectiveScore;
1295
1454
  }
1296
1455
  else {
1456
+ // Aggregate structural-tier trace (spec §4) — one per DAG tier that
1457
+ // returned at least one container (exact, single-synonym or
1458
+ // double-synonym); only aggregate counts and the final outcome are
1459
+ // recorded, never every candidate.
1460
+ const structuralTrace = probe
1461
+ ? {
1462
+ tier: tier,
1463
+ selfEvidenceRejected: 0,
1464
+ contradictionRejected: 0,
1465
+ passedGuards: 0,
1466
+ outcome: "all-rejected",
1467
+ }
1468
+ : undefined;
1469
+ if (probe)
1470
+ probe.structural = structuralTrace;
1297
1471
  // N-ARY selection: the container covering the MOST remaining candidate
1298
1472
  // forms wins (then tightest interior, then lowest id). Reads are
1299
1473
  // cache hits — every container's bytes were already read by the walk.
@@ -1311,8 +1485,11 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1311
1485
  const ri = indexOf(bytes, right, 0);
1312
1486
  if (li >= 0 && ri >= 0) {
1313
1487
  const joined = bytes.subarray(Math.min(li, ri), Math.max(li + left.length, ri + right.length));
1314
- if (indexOf(query, joined, 0) >= 0)
1488
+ if (indexOf(query, joined, 0) >= 0) {
1489
+ if (structuralTrace)
1490
+ structuralTrace.selfEvidenceRejected++;
1315
1491
  continue; // query says it itself
1492
+ }
1316
1493
  }
1317
1494
  // CONTRADICTION GUARD: a between-region already carrying its own
1318
1495
  // vote must actually recur in this container's bytes — otherwise
@@ -1320,8 +1497,12 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1320
1497
  // ra/rb, and letting it stand in for the gap would silently
1321
1498
  // override evidence the query itself already resolved there.
1322
1499
  if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
1500
+ if (structuralTrace)
1501
+ structuralTrace.contradictionRejected++;
1323
1502
  continue;
1324
1503
  }
1504
+ if (structuralTrace)
1505
+ structuralTrace.passedGuards++;
1325
1506
  let cov = left.length + right.length;
1326
1507
  const extras = [];
1327
1508
  for (const ei of cand) {
@@ -1345,14 +1526,31 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1345
1526
  bestCov = cov;
1346
1527
  }
1347
1528
  }
1348
- if (best === null)
1349
- continue; // every container was self-evidence
1529
+ if (best === null) {
1530
+ // every container was self-evidence / contradiction — outcome
1531
+ // stays "all-rejected".
1532
+ pushProbe("structural-rejected");
1533
+ continue;
1534
+ }
1350
1535
  const r = edgeAncestors(ctx, best.id, N, reachMemo);
1351
- if (r.saturated || r.roots.length === 0)
1536
+ if (r.saturated || r.roots.length === 0) {
1537
+ if (structuralTrace) {
1538
+ structuralTrace.outcome = r.saturated ? "saturated" : "no-roots";
1539
+ }
1540
+ pushProbe("structural-rejected");
1352
1541
  continue;
1542
+ }
1353
1543
  const df = Math.log(N / Math.max(1, r.contextsReached));
1354
- if (df <= 0)
1544
+ if (df <= 0) {
1545
+ if (structuralTrace)
1546
+ structuralTrace.outcome = "nonpositive-idf";
1547
+ pushProbe("structural-rejected");
1355
1548
  continue;
1549
+ }
1550
+ if (structuralTrace) {
1551
+ structuralTrace.outcome = "accepted";
1552
+ structuralTrace.selectedNode = best.id;
1553
+ }
1356
1554
  reach = r;
1357
1555
  idf = df;
1358
1556
  // Confidence used by voting (spec §13): exact junction = 1;
@@ -1429,6 +1627,7 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1429
1627
  wFocus: w,
1430
1628
  absorbed: 1 + explainedAway,
1431
1629
  });
1630
+ pushProbe("accepted");
1432
1631
  if (td) {
1433
1632
  td.crossRegionJunctionVotes.push({
1434
1633
  container: best.id,
@@ -1438,6 +1637,11 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1438
1637
  explainedAwayRegionIndices: explainedAwayIndices,
1439
1638
  absorbed: 1 + explainedAway,
1440
1639
  tier,
1640
+ probe: td.crossRegionProbes.length - 1,
1641
+ confidence,
1642
+ evidenceBytes: bestCov,
1643
+ mutualWeight: mutual,
1644
+ voteWeightPerRoot: w,
1441
1645
  });
1442
1646
  }
1443
1647
  const label = [cand[a], cand[b], ...bestExtras]
@@ -1475,6 +1679,11 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo, td) {
1475
1679
  }
1476
1680
  if (td)
1477
1681
  td.supersededOrdinaryVotes = superseded.size;
1682
+ if (td?.crossRegionSummary) {
1683
+ td.crossRegionSummary.stopReason = probes >= k
1684
+ ? "probe-limit"
1685
+ : "pairs-exhausted";
1686
+ }
1478
1687
  return { votes: out, superseded };
1479
1688
  }
1480
1689
  /** Emit the "climbConsensus" step — the human-readable note this always
@@ -1529,9 +1738,14 @@ export function traceAttention(ctx, regions, regionVoter, roots, steps = [], td,
1529
1738
  ...(td.crossRegionSummary
1530
1739
  ? {
1531
1740
  crossRegion: {
1532
- ...td.crossRegionSummary,
1741
+ eligibleRegions: td.crossRegionSummary.eligibleRegions,
1742
+ maximalRegions: td.crossRegionSummary.maximalRegions,
1743
+ probeLimit: td.crossRegionSummary.probeLimit,
1744
+ probesAttempted: td.crossRegionSummary.probesAttempted,
1533
1745
  junctionVotes: td.crossRegionJunctionVotes,
1534
1746
  supersededOrdinaryVotes: td.supersededOrdinaryVotes,
1747
+ probes: td.crossRegionProbes,
1748
+ stopReason: td.crossRegionSummary.stopReason ?? "pairs-exhausted",
1535
1749
  },
1536
1750
  }
1537
1751
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",