pi-mega-compact 0.21.1 → 0.21.2
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/config.js +13 -2
- package/dist/dedup/l1-minhash.js +5 -4
- package/dist/extensions/mega-compact.js +10 -0
- package/dist/extensions/mega-config.js +6 -3
- package/dist/src/config.js +13 -2
- package/dist/src/dedup/l1-minhash.js +5 -4
- package/dist/src/recall/validator.js +8 -1
- package/dist/src/recall/vote.js +34 -9
- package/dist/src/store/sqlite/fts5-search.js +6 -1
- package/dist/src/vector-read.js +6 -2
- package/dist/src/vectorStore/add.js +5 -1
- package/extensions/mega-compact.ts +10 -0
- package/extensions/mega-config.ts +6 -3
- package/package.json +1 -1
- package/src/config.ts +12 -3
- package/src/dedup/l1-minhash.ts +5 -4
- package/src/recall/validator.ts +8 -1
- package/src/recall/vote.ts +29 -8
- package/src/store/sqlite/fts5-search.ts +7 -1
- package/src/vector-read.ts +9 -2
- package/src/vectorStore/add.ts +7 -1
package/dist/config.js
CHANGED
|
@@ -116,8 +116,19 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
116
116
|
// default (0.12) keeps recall permissive within a repo while still rejecting
|
|
117
117
|
// effectively-unrelated hits. Call-time read so tests can set the env per-test.
|
|
118
118
|
// ---------------------------------------------------------------------------
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Same-repo recall cosine floor: top winner must be >= this to be injected.
|
|
121
|
+
*
|
|
122
|
+
* E1 follow-up (PR #18 review): NaN-safe + clamped to [0,1]. A typo'd env var
|
|
123
|
+
* yielded NaN before; `cosine < NaN` is false, which disabled gate 1 entirely
|
|
124
|
+
* (every candidate passed). Non-finite falls back to 0.12; out-of-range clamps.
|
|
125
|
+
*/
|
|
126
|
+
export const RECALL_MIN_COSINE = () => {
|
|
127
|
+
const n = Number(process.env.MEGACOMPACT_RECALL_MIN_COSINE ?? "0.12");
|
|
128
|
+
if (!Number.isFinite(n))
|
|
129
|
+
return 0.12;
|
|
130
|
+
return Math.min(1, Math.max(0, n));
|
|
131
|
+
};
|
|
121
132
|
// ---------------------------------------------------------------------------
|
|
122
133
|
// Vector-cortex flags + breaker constants (VC0A+). Positive sprint flags,
|
|
123
134
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
package/dist/dedup/l1-minhash.js
CHANGED
|
@@ -19,6 +19,7 @@ export const SHINGLE_SIZE = 5; // char 5-grams
|
|
|
19
19
|
const MAX_SHINGLES = 50_000; // QA #7/#15 complexity cap
|
|
20
20
|
const SEED = 0xdeadbeef;
|
|
21
21
|
const P = 2147483647; // 2^31 - 1, Mersenne prime
|
|
22
|
+
const PBigInt = 2147483647n; // BigInt twin for overflow-safe modular reduction
|
|
22
23
|
/** Per-index universal-hashing coefficients, derived deterministically from SEED. */
|
|
23
24
|
function coeffA(i) {
|
|
24
25
|
return (SEED + i * 2 + 1) % P;
|
|
@@ -67,10 +68,10 @@ export function minhashSignature(text) {
|
|
|
67
68
|
const b = coeffB(i);
|
|
68
69
|
let min = P;
|
|
69
70
|
for (const x of grams) {
|
|
70
|
-
// (a*x + b) mod p
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const h = (
|
|
71
|
+
// (a*x + b) mod p. a, x < 2^31 so a*x < 2^62 — EXCEEDS 2^53. The naive
|
|
72
|
+
// (a*(x%P))%P loses precision (verified: a=x=p-1 → lossy 2147483644 vs
|
|
73
|
+
// exact 1). BigInt is correct + cheap (~5ms per signature).
|
|
74
|
+
const h = Number((BigInt(a) * BigInt(x % P) + BigInt(b)) % PBigInt);
|
|
74
75
|
if (h < min)
|
|
75
76
|
min = h;
|
|
76
77
|
}
|
|
@@ -83,6 +83,16 @@ export default function (pi) {
|
|
|
83
83
|
console.warn("[mega-compact] MEGACOMPACT_POISONED_REPEAT_THRESHOLD must be >= 1; using default 3");
|
|
84
84
|
config.poisonedContextRepeatThreshold = 3;
|
|
85
85
|
}
|
|
86
|
+
// E1: validate similarity thresholds — NaN or out of range silently disables
|
|
87
|
+
// recall dedup (anything >= NaN is false). envFlag guards NaN; clamp the rest.
|
|
88
|
+
if (!(config.dedupSim > 0 && config.dedupSim <= 1)) {
|
|
89
|
+
console.warn("[mega-compact] MEGACOMPACT_DEDUP_SIM must be in (0,1]; using default 0.9");
|
|
90
|
+
config.dedupSim = 0.9;
|
|
91
|
+
}
|
|
92
|
+
if (!(config.crossRepoCosine >= 0 && config.crossRepoCosine <= 1)) {
|
|
93
|
+
console.warn("[mega-compact] MEGACOMPACT_CROSSREPO_COSINE must be in [0,1]; using default 0.9");
|
|
94
|
+
config.crossRepoCosine = 0.9;
|
|
95
|
+
}
|
|
86
96
|
const runtime = new MegaRuntime(config);
|
|
87
97
|
registerEventHandlers(pi, runtime, config);
|
|
88
98
|
registerCommands(pi, runtime, config);
|
|
@@ -171,7 +171,7 @@ export function loadConfig() {
|
|
|
171
171
|
advisoryChannel: envBool("MEGACOMPACT_ADVISORY_CHANNEL", true),
|
|
172
172
|
autoPctTrigger,
|
|
173
173
|
autoInlineK: envFlag("MEGACOMPACT_AUTO_INLINE_K", 3),
|
|
174
|
-
dedupSim:
|
|
174
|
+
dedupSim: envFlag("MEGACOMPACT_DEDUP_SIM", 0.9),
|
|
175
175
|
raptorEnabled: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
|
|
176
176
|
legacyDurableTrim: envBool("MEGACOMPACT_LEGACY_DURABLE_TRIM", false),
|
|
177
177
|
dbMirror: envBool("MEGACOMPACT_DB_MIRROR", false),
|
|
@@ -180,13 +180,16 @@ export function loadConfig() {
|
|
|
180
180
|
turnsDbEnabled: envBool("MEGACOMPACT_TURNS_DB", true),
|
|
181
181
|
autoWikiEnabled: envBool("MEGACOMPACT_AUTO_WIKI", true),
|
|
182
182
|
crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
|
|
183
|
-
crossRepoCosine:
|
|
183
|
+
crossRepoCosine: envFlag("MEGACOMPACT_CROSSREPO_COSINE", 0.9),
|
|
184
184
|
// 3WF-3: SAME-repo recall cosine floor applied by the 3-source validator to
|
|
185
185
|
// the top winner. SEPARATE from crossRepoCosine (S17, default 0.90, stricter
|
|
186
186
|
// and cross-repo only). This same-repo floor is permissive by default (0.12)
|
|
187
187
|
// so recall still surfaces loosely-relevant within-repo context while
|
|
188
188
|
// rejecting effectively-unrelated hits. Mirrors src/config.ts RECALL_MIN_COSINE.
|
|
189
|
-
|
|
189
|
+
// E1 follow-up (PR #18 review): envFlag (Number.isFinite-guarded) like the
|
|
190
|
+
// dedupSim/crossRepoCosine fix in PR #18 — a typo'd env var must fall back
|
|
191
|
+
// to 0.12, not yield NaN.
|
|
192
|
+
recallMinCosine: envFlag("MEGACOMPACT_RECALL_MIN_COSINE", 0.12),
|
|
190
193
|
memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
|
|
191
194
|
memoryReviewInterval: envFlag("MEGACOMPACT_MEMORY_REVIEW_INTERVAL", 10),
|
|
192
195
|
recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
|
package/dist/src/config.js
CHANGED
|
@@ -116,8 +116,19 @@ export const NEW_UI = () => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
116
116
|
// default (0.12) keeps recall permissive within a repo while still rejecting
|
|
117
117
|
// effectively-unrelated hits. Call-time read so tests can set the env per-test.
|
|
118
118
|
// ---------------------------------------------------------------------------
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Same-repo recall cosine floor: top winner must be >= this to be injected.
|
|
121
|
+
*
|
|
122
|
+
* E1 follow-up (PR #18 review): NaN-safe + clamped to [0,1]. A typo'd env var
|
|
123
|
+
* yielded NaN before; `cosine < NaN` is false, which disabled gate 1 entirely
|
|
124
|
+
* (every candidate passed). Non-finite falls back to 0.12; out-of-range clamps.
|
|
125
|
+
*/
|
|
126
|
+
export const RECALL_MIN_COSINE = () => {
|
|
127
|
+
const n = Number(process.env.MEGACOMPACT_RECALL_MIN_COSINE ?? "0.12");
|
|
128
|
+
if (!Number.isFinite(n))
|
|
129
|
+
return 0.12;
|
|
130
|
+
return Math.min(1, Math.max(0, n));
|
|
131
|
+
};
|
|
121
132
|
// ---------------------------------------------------------------------------
|
|
122
133
|
// Vector-cortex flags + breaker constants (VC0A+). Positive sprint flags,
|
|
123
134
|
// default ON, `=0`/`_DISABLED` off. Re-exported from src/config/vector-cortex.ts
|
|
@@ -19,6 +19,7 @@ export const SHINGLE_SIZE = 5; // char 5-grams
|
|
|
19
19
|
const MAX_SHINGLES = 50_000; // QA #7/#15 complexity cap
|
|
20
20
|
const SEED = 0xdeadbeef;
|
|
21
21
|
const P = 2147483647; // 2^31 - 1, Mersenne prime
|
|
22
|
+
const PBigInt = 2147483647n; // BigInt twin for overflow-safe modular reduction
|
|
22
23
|
/** Per-index universal-hashing coefficients, derived deterministically from SEED. */
|
|
23
24
|
function coeffA(i) {
|
|
24
25
|
return (SEED + i * 2 + 1) % P;
|
|
@@ -67,10 +68,10 @@ export function minhashSignature(text) {
|
|
|
67
68
|
const b = coeffB(i);
|
|
68
69
|
let min = P;
|
|
69
70
|
for (const x of grams) {
|
|
70
|
-
// (a*x + b) mod p
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
const h = (
|
|
71
|
+
// (a*x + b) mod p. a, x < 2^31 so a*x < 2^62 — EXCEEDS 2^53. The naive
|
|
72
|
+
// (a*(x%P))%P loses precision (verified: a=x=p-1 → lossy 2147483644 vs
|
|
73
|
+
// exact 1). BigInt is correct + cheap (~5ms per signature).
|
|
74
|
+
const h = Number((BigInt(a) * BigInt(x % P) + BigInt(b)) % PBigInt);
|
|
74
75
|
if (h < min)
|
|
75
76
|
min = h;
|
|
76
77
|
}
|
|
@@ -76,7 +76,14 @@ export function validateRecall(winners, opts, store) {
|
|
|
76
76
|
// No comparable cosine available => cannot clear a cosine gate.
|
|
77
77
|
continue;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// E1 follow-up (PR #18 review): NaN/Infinity must NEVER clear the floor.
|
|
80
|
+
// `NaN < floor` is false, so an unguarded comparison lets a NaN cosine
|
|
81
|
+
// PASS gate 1 and inject — one NaN source poisons the whole 3WF-3
|
|
82
|
+
// quorum. Reject non-finite scores explicitly; the candidate is skipped
|
|
83
|
+
// and, if all fail, the provenance floor ("no recall") is returned —
|
|
84
|
+
// never a zero-score injection. The default TrigramEmbedder cannot
|
|
85
|
+
// produce NaN (zero-norm guard), but a BYO localhost embedder can.
|
|
86
|
+
if (!Number.isFinite(cosine) || cosine < floor)
|
|
80
87
|
continue;
|
|
81
88
|
// Gate 2: not already resident in the live window.
|
|
82
89
|
if (liveVecs.length > 0) {
|
package/dist/src/recall/vote.js
CHANGED
|
@@ -31,17 +31,36 @@ import { listCheckpoints } from "../store/sqlite.js";
|
|
|
31
31
|
import { computeContentDigest } from "../dedup/digest.js";
|
|
32
32
|
import { recallRawHits } from "./readonly.js";
|
|
33
33
|
import { Logger } from "../log.js";
|
|
34
|
-
/**
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Per-source normalization: map raw scores to 0..1 via min-max within source.
|
|
36
|
+
*
|
|
37
|
+
* E1 follow-up (PR #18 review): non-finite scores (NaN/±Infinity) are DROPPED
|
|
38
|
+
* before the min/max fold — a single NaN silently propagates through Math.min/
|
|
39
|
+
* max and turns EVERY normalized score of that source into NaN (verified),
|
|
40
|
+
* poisoning the whole 3-source quorum. Dropping the bad entry degrades that
|
|
41
|
+
* source gracefully instead. Exported so the guard is unit-testable directly.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeScores(scores) {
|
|
36
44
|
const map = new Map();
|
|
37
|
-
|
|
38
|
-
return map;
|
|
39
|
-
const min = Math.min(...scores);
|
|
40
|
-
const max = Math.max(...scores);
|
|
41
|
-
const span = max - min;
|
|
45
|
+
const finite = [];
|
|
42
46
|
scores.forEach((s, i) => {
|
|
43
|
-
|
|
47
|
+
if (Number.isFinite(s))
|
|
48
|
+
finite.push({ idx: i, s });
|
|
44
49
|
});
|
|
50
|
+
if (finite.length === 0)
|
|
51
|
+
return map;
|
|
52
|
+
let min = Infinity;
|
|
53
|
+
let max = -Infinity;
|
|
54
|
+
for (const { s } of finite) {
|
|
55
|
+
if (s < min)
|
|
56
|
+
min = s;
|
|
57
|
+
if (s > max)
|
|
58
|
+
max = s;
|
|
59
|
+
}
|
|
60
|
+
const span = max - min;
|
|
61
|
+
for (const { idx, s } of finite) {
|
|
62
|
+
map.set(idx, span === 0 ? 1 : (s - min) / span);
|
|
63
|
+
}
|
|
45
64
|
return map;
|
|
46
65
|
}
|
|
47
66
|
/**
|
|
@@ -130,7 +149,13 @@ export function voteRecall(opts, store) {
|
|
|
130
149
|
const norm = perSource.find((p) => p.name === src.name).norm;
|
|
131
150
|
const bestByCp = new Map();
|
|
132
151
|
src.cands.forEach((c, i) => {
|
|
133
|
-
|
|
152
|
+
// E1 follow-up: normalizeScores dropped non-finite scores; such a hit is
|
|
153
|
+
// NOT a valid nomination — skip it entirely instead of defaulting it to
|
|
154
|
+
// 0 (which would still name the checkpoint and let it rank last into the
|
|
155
|
+
// fallback ranking).
|
|
156
|
+
const n = norm.get(i);
|
|
157
|
+
if (n === undefined)
|
|
158
|
+
return;
|
|
134
159
|
const prev = bestByCp.get(c.checkpointId);
|
|
135
160
|
if (prev === undefined || n > prev)
|
|
136
161
|
bestByCp.set(c.checkpointId, n);
|
|
@@ -87,7 +87,12 @@ export function hydrateFts5Hits(hits, sessionId, stateDir) {
|
|
|
87
87
|
const out = [];
|
|
88
88
|
for (const h of hits) {
|
|
89
89
|
const cp = cpMap.get(h.id);
|
|
90
|
-
|
|
90
|
+
// H1 follow-up (PR #18 review): exclude SemDeDup-'removed' rows. The FTS5
|
|
91
|
+
// index is NOT pruned by vectorSemDedup, so a removed row can still MATCH;
|
|
92
|
+
// hydrating it would let the fts5 voter NAME a dead checkpoint (a removed
|
|
93
|
+
// row plus a recency vote is a 2/3 agreement that passes the validator).
|
|
94
|
+
// Mirrors vectorSearch's read-time filter.
|
|
95
|
+
if (cp && cp.dedupStatus !== "removed")
|
|
91
96
|
out.push({ checkpointId: h.id, score: h.score, summary: cp.summary });
|
|
92
97
|
}
|
|
93
98
|
return out;
|
package/dist/src/vector-read.js
CHANGED
|
@@ -72,7 +72,10 @@ export function vectorDedupe(store, sessionId, regionHashOrText, isText = false)
|
|
|
72
72
|
const state = loadSessionState(sid, stateDir);
|
|
73
73
|
if (state.storedRegionHashes.includes(hash))
|
|
74
74
|
return true;
|
|
75
|
-
|
|
75
|
+
// H1 follow-up (PR #18 review): a SemDeDup-'removed' row's regionHash must not
|
|
76
|
+
// report "already represented" — its content is excluded from recall, so the
|
|
77
|
+
// incoming region is NOT deduplicated in any retrievable sense.
|
|
78
|
+
return listCheckpoints(sid, stateDir).some((c) => c.dedupStatus !== "removed" && c.regionHash === hash);
|
|
76
79
|
}
|
|
77
80
|
// ---------------------------------------------------------------------------
|
|
78
81
|
// Injection tracking
|
|
@@ -115,7 +118,8 @@ export function vectorTopSimilar(store, sessionId, n) {
|
|
|
115
118
|
const ordered = [...checkpoints].sort((a, b) => a.checkpointId.localeCompare(b.checkpointId));
|
|
116
119
|
const current = ordered[ordered.length - 1];
|
|
117
120
|
const scored = ordered
|
|
118
|
-
.filter((cp) => cp.checkpointId !== current.checkpointId
|
|
121
|
+
.filter((cp) => cp.checkpointId !== current.checkpointId &&
|
|
122
|
+
cp.dedupStatus !== "removed")
|
|
119
123
|
.map((cp) => ({
|
|
120
124
|
checkpoint: cp,
|
|
121
125
|
score: cosineSimilarity(current.embedding, cp.embedding),
|
|
@@ -40,7 +40,11 @@ export function addCheckpoint(store, input) {
|
|
|
40
40
|
const t0 = Date.now();
|
|
41
41
|
const sessionId = normalizeSessionId(input.sessionId);
|
|
42
42
|
const regionHash = computeRegionHash(input.regionText);
|
|
43
|
-
|
|
43
|
+
// H1: exclude SemDeDup-'removed' rows from dedup matching. search() already
|
|
44
|
+
// filters these, but add() did NOT — so an L0/L1/L2 match against a previously
|
|
45
|
+
// removed duplicate would upsertCheckpoint it back to active, resurrecting it
|
|
46
|
+
// into recall and defeating SemDeDup.
|
|
47
|
+
const all = listCheckpoints(sessionId, store.stateDir).filter((cp) => cp.dedupStatus !== "removed");
|
|
44
48
|
// Honest "tokens saved" base for this region. For a deduped add the whole
|
|
45
49
|
// original region is discarded (nothing new stored); for a new checkpoint
|
|
46
50
|
// we persist (orig − stored). Falls back to stored when orig is unknown.
|
|
@@ -98,6 +98,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
98
98
|
);
|
|
99
99
|
config.poisonedContextRepeatThreshold = 3;
|
|
100
100
|
}
|
|
101
|
+
// E1: validate similarity thresholds — NaN or out of range silently disables
|
|
102
|
+
// recall dedup (anything >= NaN is false). envFlag guards NaN; clamp the rest.
|
|
103
|
+
if (!(config.dedupSim > 0 && config.dedupSim <= 1)) {
|
|
104
|
+
console.warn("[mega-compact] MEGACOMPACT_DEDUP_SIM must be in (0,1]; using default 0.9");
|
|
105
|
+
config.dedupSim = 0.9;
|
|
106
|
+
}
|
|
107
|
+
if (!(config.crossRepoCosine >= 0 && config.crossRepoCosine <= 1)) {
|
|
108
|
+
console.warn("[mega-compact] MEGACOMPACT_CROSSREPO_COSINE must be in [0,1]; using default 0.9");
|
|
109
|
+
config.crossRepoCosine = 0.9;
|
|
110
|
+
}
|
|
101
111
|
const runtime = new MegaRuntime(config);
|
|
102
112
|
registerEventHandlers(pi, runtime, config);
|
|
103
113
|
registerCommands(pi, runtime, config);
|
|
@@ -207,7 +207,7 @@ export function loadConfig(): MegaConfig {
|
|
|
207
207
|
advisoryChannel: envBool("MEGACOMPACT_ADVISORY_CHANNEL", true),
|
|
208
208
|
autoPctTrigger,
|
|
209
209
|
autoInlineK: envFlag("MEGACOMPACT_AUTO_INLINE_K", 3),
|
|
210
|
-
dedupSim:
|
|
210
|
+
dedupSim: envFlag("MEGACOMPACT_DEDUP_SIM", 0.9),
|
|
211
211
|
raptorEnabled: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
|
|
212
212
|
legacyDurableTrim: envBool("MEGACOMPACT_LEGACY_DURABLE_TRIM", false),
|
|
213
213
|
dbMirror: envBool("MEGACOMPACT_DB_MIRROR", false),
|
|
@@ -216,13 +216,16 @@ export function loadConfig(): MegaConfig {
|
|
|
216
216
|
turnsDbEnabled: envBool("MEGACOMPACT_TURNS_DB", true),
|
|
217
217
|
autoWikiEnabled: envBool("MEGACOMPACT_AUTO_WIKI", true),
|
|
218
218
|
crossRepoEnabled: envBool("MEGACOMPACT_CROSSREPO_ENABLED", true),
|
|
219
|
-
crossRepoCosine:
|
|
219
|
+
crossRepoCosine: envFlag("MEGACOMPACT_CROSSREPO_COSINE", 0.9),
|
|
220
220
|
// 3WF-3: SAME-repo recall cosine floor applied by the 3-source validator to
|
|
221
221
|
// the top winner. SEPARATE from crossRepoCosine (S17, default 0.90, stricter
|
|
222
222
|
// and cross-repo only). This same-repo floor is permissive by default (0.12)
|
|
223
223
|
// so recall still surfaces loosely-relevant within-repo context while
|
|
224
224
|
// rejecting effectively-unrelated hits. Mirrors src/config.ts RECALL_MIN_COSINE.
|
|
225
|
-
|
|
225
|
+
// E1 follow-up (PR #18 review): envFlag (Number.isFinite-guarded) like the
|
|
226
|
+
// dedupSim/crossRepoCosine fix in PR #18 — a typo'd env var must fall back
|
|
227
|
+
// to 0.12, not yield NaN.
|
|
228
|
+
recallMinCosine: envFlag("MEGACOMPACT_RECALL_MIN_COSINE", 0.12),
|
|
226
229
|
memoryAutoReview: envBool("MEGACOMPACT_MEMORY_AUTO_REVIEW", true),
|
|
227
230
|
memoryReviewInterval: envFlag("MEGACOMPACT_MEMORY_REVIEW_INTERVAL", 10),
|
|
228
231
|
recallMaxTokens: envFlag("MEGACOMPACT_RECALL_MAX_TOKENS", 1500),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|
package/src/config.ts
CHANGED
|
@@ -152,9 +152,18 @@ export const NEW_UI = (): boolean => ragEnabled("MEGACOMPACT_NEW_UI");
|
|
|
152
152
|
// effectively-unrelated hits. Call-time read so tests can set the env per-test.
|
|
153
153
|
// ---------------------------------------------------------------------------
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Same-repo recall cosine floor: top winner must be >= this to be injected.
|
|
157
|
+
*
|
|
158
|
+
* E1 follow-up (PR #18 review): NaN-safe + clamped to [0,1]. A typo'd env var
|
|
159
|
+
* yielded NaN before; `cosine < NaN` is false, which disabled gate 1 entirely
|
|
160
|
+
* (every candidate passed). Non-finite falls back to 0.12; out-of-range clamps.
|
|
161
|
+
*/
|
|
162
|
+
export const RECALL_MIN_COSINE = (): number => {
|
|
163
|
+
const n = Number(process.env.MEGACOMPACT_RECALL_MIN_COSINE ?? "0.12");
|
|
164
|
+
if (!Number.isFinite(n)) return 0.12;
|
|
165
|
+
return Math.min(1, Math.max(0, n));
|
|
166
|
+
};
|
|
158
167
|
|
|
159
168
|
// ---------------------------------------------------------------------------
|
|
160
169
|
// Vector-cortex flags + breaker constants (VC0A+). Positive sprint flags,
|
package/src/dedup/l1-minhash.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const SHINGLE_SIZE = 5; // char 5-grams
|
|
|
21
21
|
const MAX_SHINGLES = 50_000; // QA #7/#15 complexity cap
|
|
22
22
|
const SEED = 0xdeadbeef;
|
|
23
23
|
const P = 2147483647; // 2^31 - 1, Mersenne prime
|
|
24
|
+
const PBigInt = 2147483647n; // BigInt twin for overflow-safe modular reduction
|
|
24
25
|
|
|
25
26
|
/** Per-index universal-hashing coefficients, derived deterministically from SEED. */
|
|
26
27
|
function coeffA(i: number): number {
|
|
@@ -69,10 +70,10 @@ export function minhashSignature(text: string): number[] {
|
|
|
69
70
|
const b = coeffB(i);
|
|
70
71
|
let min = P;
|
|
71
72
|
for (const x of grams) {
|
|
72
|
-
// (a*x + b) mod p
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
const h = (
|
|
73
|
+
// (a*x + b) mod p. a, x < 2^31 so a*x < 2^62 — EXCEEDS 2^53. The naive
|
|
74
|
+
// (a*(x%P))%P loses precision (verified: a=x=p-1 → lossy 2147483644 vs
|
|
75
|
+
// exact 1). BigInt is correct + cheap (~5ms per signature).
|
|
76
|
+
const h = Number((BigInt(a) * BigInt(x % P) + BigInt(b)) % PBigInt);
|
|
76
77
|
if (h < min) min = h;
|
|
77
78
|
}
|
|
78
79
|
sig[i] = min;
|
package/src/recall/validator.ts
CHANGED
|
@@ -113,7 +113,14 @@ export function validateRecall(
|
|
|
113
113
|
// No comparable cosine available => cannot clear a cosine gate.
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
// E1 follow-up (PR #18 review): NaN/Infinity must NEVER clear the floor.
|
|
117
|
+
// `NaN < floor` is false, so an unguarded comparison lets a NaN cosine
|
|
118
|
+
// PASS gate 1 and inject — one NaN source poisons the whole 3WF-3
|
|
119
|
+
// quorum. Reject non-finite scores explicitly; the candidate is skipped
|
|
120
|
+
// and, if all fail, the provenance floor ("no recall") is returned —
|
|
121
|
+
// never a zero-score injection. The default TrigramEmbedder cannot
|
|
122
|
+
// produce NaN (zero-norm guard), but a BYO localhost embedder can.
|
|
123
|
+
if (!Number.isFinite(cosine) || cosine < floor) continue;
|
|
117
124
|
|
|
118
125
|
// Gate 2: not already resident in the live window.
|
|
119
126
|
if (liveVecs.length > 0) {
|
package/src/recall/vote.ts
CHANGED
|
@@ -46,16 +46,32 @@ export interface VoteOptions {
|
|
|
46
46
|
recencyCount?: number;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/vector-read.ts
CHANGED
|
@@ -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(
|
|
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),
|
package/src/vectorStore/add.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|