pi-mega-compact 0.21.1 → 0.21.3

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.
@@ -46,16 +46,32 @@ export interface VoteOptions {
46
46
  recencyCount?: number;
47
47
  }
48
48
 
49
- /** Per-source normalization: map raw scores to 0..1 via min-max within source. */
50
- function normalizeScores(scores: number[]): Map<number, number> {
49
+ /**
50
+ * Per-source normalization: map raw scores to 0..1 via min-max within source.
51
+ *
52
+ * E1 follow-up (PR #18 review): non-finite scores (NaN/±Infinity) are DROPPED
53
+ * before the min/max fold — a single NaN silently propagates through Math.min/
54
+ * max and turns EVERY normalized score of that source into NaN (verified),
55
+ * poisoning the whole 3-source quorum. Dropping the bad entry degrades that
56
+ * source gracefully instead. Exported so the guard is unit-testable directly.
57
+ */
58
+ export function normalizeScores(scores: number[]): Map<number, number> {
51
59
  const map = new Map<number, number>();
52
- if (scores.length === 0) return map;
53
- const min = Math.min(...scores);
54
- const max = Math.max(...scores);
55
- const span = max - min;
60
+ const finite: { idx: number; s: number }[] = [];
56
61
  scores.forEach((s, i) => {
57
- map.set(i, span === 0 ? 1 : (s - min) / span);
62
+ if (Number.isFinite(s)) finite.push({ idx: i, s });
58
63
  });
64
+ if (finite.length === 0) return map;
65
+ let min = Infinity;
66
+ let max = -Infinity;
67
+ for (const { s } of finite) {
68
+ if (s < min) min = s;
69
+ if (s > max) max = s;
70
+ }
71
+ const span = max - min;
72
+ for (const { idx, s } of finite) {
73
+ map.set(idx, span === 0 ? 1 : (s - min) / span);
74
+ }
59
75
  return map;
60
76
  }
61
77
 
@@ -150,7 +166,12 @@ export function voteRecall(opts: VoteOptions, store: VectorStore): VoteResult {
150
166
  const norm = perSource.find((p) => p.name === src.name)!.norm;
151
167
  const bestByCp = new Map<string, number>();
152
168
  src.cands.forEach((c, i) => {
153
- const n = norm.get(i) ?? 0;
169
+ // E1 follow-up: normalizeScores dropped non-finite scores; such a hit is
170
+ // NOT a valid nomination — skip it entirely instead of defaulting it to
171
+ // 0 (which would still name the checkpoint and let it rank last into the
172
+ // fallback ranking).
173
+ const n = norm.get(i);
174
+ if (n === undefined) return;
154
175
  const prev = bestByCp.get(c.checkpointId);
155
176
  if (prev === undefined || n > prev) bestByCp.set(c.checkpointId, n);
156
177
  seenIds.add(c.checkpointId);
@@ -124,7 +124,13 @@ export function hydrateFts5Hits(
124
124
  const out: HydratedFts5Hit[] = [];
125
125
  for (const h of hits) {
126
126
  const cp = cpMap.get(h.id);
127
- if (cp) out.push({ checkpointId: h.id, score: h.score, summary: cp.summary });
127
+ // H1 follow-up (PR #18 review): exclude SemDeDup-'removed' rows. The FTS5
128
+ // index is NOT pruned by vectorSemDedup, so a removed row can still MATCH;
129
+ // hydrating it would let the fts5 voter NAME a dead checkpoint (a removed
130
+ // row plus a recency vote is a 2/3 agreement that passes the validator).
131
+ // Mirrors vectorSearch's read-time filter.
132
+ if (cp && cp.dedupStatus !== "removed")
133
+ out.push({ checkpointId: h.id, score: h.score, summary: cp.summary });
128
134
  }
129
135
  return out;
130
136
  }
@@ -100,8 +100,11 @@ export function vectorDedupe(
100
100
  : regionHashOrText;
101
101
  const state = loadSessionState(sid, stateDir);
102
102
  if (state.storedRegionHashes.includes(hash)) return true;
103
+ // H1 follow-up (PR #18 review): a SemDeDup-'removed' row's regionHash must not
104
+ // report "already represented" — its content is excluded from recall, so the
105
+ // incoming region is NOT deduplicated in any retrievable sense.
103
106
  return listCheckpoints(sid, stateDir).some(
104
- (c) => c.regionHash === hash,
107
+ (c) => c.dedupStatus !== "removed" && c.regionHash === hash,
105
108
  );
106
109
  }
107
110
 
@@ -157,7 +160,11 @@ export function vectorTopSimilar(store: VectorStore, sessionId: string, n: numbe
157
160
  const current = ordered[ordered.length - 1];
158
161
 
159
162
  const scored: SearchHit[] = ordered
160
- .filter((cp) => cp.checkpointId !== current.checkpointId)
163
+ .filter(
164
+ (cp) =>
165
+ cp.checkpointId !== current.checkpointId &&
166
+ cp.dedupStatus !== "removed", // H1 follow-up: mirror vectorSearch's filter
167
+ )
161
168
  .map((cp) => ({
162
169
  checkpoint: cp,
163
170
  score: cosineSimilarity(current.embedding, cp.embedding),
@@ -54,7 +54,13 @@ export function addCheckpoint(store: VectorStore, input: AddInput): AddResult {
54
54
  const t0 = Date.now();
55
55
  const sessionId = normalizeSessionId(input.sessionId);
56
56
  const regionHash = computeRegionHash(input.regionText);
57
- const all = listCheckpoints(sessionId, store.stateDir);
57
+ // H1: exclude SemDeDup-'removed' rows from dedup matching. search() already
58
+ // filters these, but add() did NOT — so an L0/L1/L2 match against a previously
59
+ // removed duplicate would upsertCheckpoint it back to active, resurrecting it
60
+ // into recall and defeating SemDeDup.
61
+ const all = listCheckpoints(sessionId, store.stateDir).filter(
62
+ (cp) => cp.dedupStatus !== "removed",
63
+ );
58
64
  // Honest "tokens saved" base for this region. For a deduped add the whole
59
65
  // original region is discarded (nothing new stored); for a new checkpoint
60
66
  // we persist (orig − stored). Falls back to stored when orig is unknown.