pi-mega-compact 0.11.7 → 0.11.8
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/extensions/dashboard-server/routes-memory-map.js +8 -15
- package/dist/extensions/mega-commands.js +65 -5
- package/dist/extensions/mega-config.js +6 -0
- package/dist/extensions/mega-events/context-handler.js +65 -0
- package/dist/src/config/turns.js +2 -1
- package/dist/src/memoryGraph/embedding.js +50 -0
- package/dist/src/memoryGraph/gates.js +379 -0
- package/dist/src/memoryGraph/sources.js +390 -0
- package/dist/src/memoryGraph.js +133 -259
- package/dist/src/store/sqlite/embedding-cache.js +71 -0
- package/dist/src/store/sqlite.js +1 -0
- package/dist/src/topics/cluster.js +10 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js → AchievementTiles-BESF1HT1.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-DR9cLGDL.js.map → AchievementTiles-BESF1HT1.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js → AchievementsTab-8RzNnz8u.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-svNUgJ1e.js.map → AchievementsTab-8RzNnz8u.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js → CacheTab-CUIiUgh3.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-ASwhOMjR.js.map → CacheTab-CUIiUgh3.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js → ConfigTab-D85fB9HP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ConfigTab-CmmWheuW.js.map → ConfigTab-D85fB9HP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js → EventsTab-TCLR81G5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-CFE7jLsa.js.map → EventsTab-TCLR81G5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js → GameTab-BqneNRYH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-CJAX_th_.js.map → GameTab-BqneNRYH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js → MaintenanceTab-LblMg3NB.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-P4SUsvMf.js.map → MaintenanceTab-LblMg3NB.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-CpjMQwRr.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js → MetricsTab-LxNtNzLQ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-CHQscixy.js.map → MetricsTab-LxNtNzLQ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js → OverviewTab-BXKUZbT6.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-DBs4DJdd.js.map → OverviewTab-BXKUZbT6.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js → ReposTab-DTooKeAi.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DeFz76L5.js.map → ReposTab-DTooKeAi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js → SessionsTab-Devulgm9.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-BWNsj1OB.js.map → SessionsTab-Devulgm9.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js → SetupTab-By9eqUoE.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-uS6A0xmw.js.map → SetupTab-By9eqUoE.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js → TimeSavedCard-z4M8TDLm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-CiqCOQuL.js.map → TimeSavedCard-z4M8TDLm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js +2 -0
- package/extensions/dashboard-client/dist/assets/TopicsTab-bCGZSfLN.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js → TurnsTab-m_XzE0H4.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-CVz4f4-o.js.map → TurnsTab-m_XzE0H4.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js → index-PRkilPXw.js} +3 -3
- package/extensions/dashboard-client/dist/assets/{index-DrktNOoT.js.map → index-PRkilPXw.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js → useSSE-CGzgc2IK.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-Bf5LOLSi.js.map → useSSE-CGzgc2IK.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +1 -1
- package/extensions/dashboard-client/src/memory-map-layout.ts +161 -0
- package/extensions/dashboard-client/src/memory-map-shapes.tsx +126 -0
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +201 -165
- package/extensions/dashboard-client/src/tabs/ReposTab.tsx +21 -0
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +1 -1
- package/extensions/dashboard-server/api-contracts/index.ts +2 -1
- package/extensions/dashboard-server/api-contracts/memory-map.ts +14 -6
- package/extensions/dashboard-server/routes-memory-map.ts +21 -29
- package/extensions/mega-commands.ts +63 -6
- package/extensions/mega-config.ts +18 -0
- package/extensions/mega-events/context-handler.ts +80 -0
- package/package.json +1 -1
- package/src/config/turns.ts +5 -2
- package/src/memoryGraph/embedding.ts +63 -0
- package/src/memoryGraph/gates.ts +451 -0
- package/src/memoryGraph/sources.ts +484 -0
- package/src/memoryGraph.ts +188 -350
- package/src/store/sqlite/embedding-cache.ts +81 -0
- package/src/store/sqlite.ts +1 -0
- package/src/topics/cluster.ts +11 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +0 -2
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js +0 -2
- package/extensions/dashboard-client/dist/assets/TopicsTab-BWWQxltv.js.map +0 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Logger } from "../log.js";
|
|
2
|
+
const log = new Logger();
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Gate 1 — Source availability guard (informational, no drop)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export function gateSourceAvailable(ws) {
|
|
7
|
+
if (ws.nodes.length === 0) {
|
|
8
|
+
log.warn("graph_source_unavailable", {
|
|
9
|
+
detail: "No nodes from any source — graph will be empty",
|
|
10
|
+
});
|
|
11
|
+
return { ok: false, dropped: 0, reason: "No nodes from any source" };
|
|
12
|
+
}
|
|
13
|
+
return { ok: true, dropped: 0 };
|
|
14
|
+
}
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Gate 2 — Identity merge: duplicate nodes with same id → richest wins
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export function gateIdentityMerge(ws) {
|
|
19
|
+
const { nodes } = ws;
|
|
20
|
+
const seen = new Map();
|
|
21
|
+
let dropped = 0;
|
|
22
|
+
for (const n of nodes) {
|
|
23
|
+
const existing = seen.get(n.id);
|
|
24
|
+
if (!existing) {
|
|
25
|
+
seen.set(n.id, n);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const existingScore = fieldScore(existing);
|
|
29
|
+
const currentScore = fieldScore(n);
|
|
30
|
+
if (currentScore > existingScore) {
|
|
31
|
+
seen.set(n.id, n);
|
|
32
|
+
}
|
|
33
|
+
dropped++;
|
|
34
|
+
log.warn("graph_identity_leak", {
|
|
35
|
+
nodeId: n.id,
|
|
36
|
+
dropped: currentScore > existingScore ? existing.id : n.id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
ws.nodes = [...seen.values()];
|
|
40
|
+
return { ok: dropped === 0, dropped, reason: dropped > 0 ? `${dropped} duplicate node ids merged` : undefined };
|
|
41
|
+
}
|
|
42
|
+
function fieldScore(n) {
|
|
43
|
+
// Base score by nodeType — richer sources win ties (spec: "richest available
|
|
44
|
+
// source wins"). checkpoint > turn-content > memory > turn-structural.
|
|
45
|
+
let s = 0;
|
|
46
|
+
switch (n.nodeType) {
|
|
47
|
+
case "checkpoint":
|
|
48
|
+
s += 4;
|
|
49
|
+
break;
|
|
50
|
+
case "turn-content":
|
|
51
|
+
s += 3;
|
|
52
|
+
break;
|
|
53
|
+
case "memory":
|
|
54
|
+
s += 2;
|
|
55
|
+
break;
|
|
56
|
+
case "turn":
|
|
57
|
+
s += 1;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (n.summaryTruncated)
|
|
61
|
+
s++;
|
|
62
|
+
if (n.textSnippet)
|
|
63
|
+
s++;
|
|
64
|
+
if (n.topicSummary)
|
|
65
|
+
s++;
|
|
66
|
+
if (n.tokenEstimate > 0)
|
|
67
|
+
s++;
|
|
68
|
+
if (n.decisionCount > 0)
|
|
69
|
+
s++;
|
|
70
|
+
if (n.dedupStatus)
|
|
71
|
+
s++;
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Gate 3 — Promotion guard: suppress turns with epoch_id if checkpoint exists
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export function gatePromotionGuard(ws) {
|
|
78
|
+
const checkpointIds = new Set();
|
|
79
|
+
for (const n of ws.nodes) {
|
|
80
|
+
if (n.nodeType === "checkpoint")
|
|
81
|
+
checkpointIds.add(n.id);
|
|
82
|
+
}
|
|
83
|
+
const keep = [];
|
|
84
|
+
const droppedIds = new Set();
|
|
85
|
+
let dropped = 0;
|
|
86
|
+
for (const n of ws.nodes) {
|
|
87
|
+
if (n.nodeType !== "turn" && n.nodeType !== "turn-content") {
|
|
88
|
+
keep.push(n);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const epochId = n.epochId;
|
|
92
|
+
if (epochId && checkpointIds.has(epochId)) {
|
|
93
|
+
droppedIds.add(n.id);
|
|
94
|
+
dropped++;
|
|
95
|
+
log.warn("graph_orphaned_epoch", {
|
|
96
|
+
nodeId: n.id,
|
|
97
|
+
epochId,
|
|
98
|
+
detail: "Turn suppressed because its checkpoint exists in the graph",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (epochId && !checkpointIds.has(epochId)) {
|
|
103
|
+
log.warn("graph_orphaned_epoch", {
|
|
104
|
+
nodeId: n.id,
|
|
105
|
+
epochId,
|
|
106
|
+
detail: "Orphaned epoch — no matching checkpoint; keeping turn",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
keep.push(n);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
ws.nodes = keep;
|
|
113
|
+
ws.edges = ws.edges.filter((e) => !droppedIds.has(e.source) && !droppedIds.has(e.target));
|
|
114
|
+
return {
|
|
115
|
+
ok: dropped === 0,
|
|
116
|
+
dropped,
|
|
117
|
+
reason: dropped > 0 ? `${dropped} turn nodes suppressed (checkpoint exists)` : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Gate 4 — Node type / field completeness
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
export function gateNodeCompleteness(ws) {
|
|
124
|
+
const validNodes = [];
|
|
125
|
+
let dropped = 0;
|
|
126
|
+
for (const n of ws.nodes) {
|
|
127
|
+
if (!n.nodeType || !n.sessionId) {
|
|
128
|
+
dropped++;
|
|
129
|
+
log.warn("graph_node_double", {
|
|
130
|
+
nodeId: n.id,
|
|
131
|
+
detail: "Missing nodeType or sessionId",
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
validNodes.push(n);
|
|
136
|
+
}
|
|
137
|
+
ws.nodes = validNodes;
|
|
138
|
+
return {
|
|
139
|
+
ok: dropped === 0,
|
|
140
|
+
dropped,
|
|
141
|
+
reason: dropped > 0 ? `${dropped} nodes dropped for missing fields` : undefined,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Gate 5 — Dangling edge guard
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
export function gateDanglingEdges(ws) {
|
|
148
|
+
const validIds = new Set(ws.nodes.map((n) => n.id));
|
|
149
|
+
const validEdges = [];
|
|
150
|
+
let dropped = 0;
|
|
151
|
+
for (const e of ws.edges) {
|
|
152
|
+
if (validIds.has(e.source) && validIds.has(e.target)) {
|
|
153
|
+
validEdges.push(e);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
dropped++;
|
|
157
|
+
log.warn("graph_dangling_edge", {
|
|
158
|
+
source: e.source,
|
|
159
|
+
target: e.target,
|
|
160
|
+
detail: "Edge references non-existent node(s)",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
ws.edges = validEdges;
|
|
165
|
+
return {
|
|
166
|
+
ok: dropped === 0,
|
|
167
|
+
dropped,
|
|
168
|
+
reason: dropped > 0 ? `${dropped} dangling edges removed` : undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Gate 6 — Edge type / threshold enforcement
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
function envThreshold(name, def) {
|
|
175
|
+
const v = process.env[name];
|
|
176
|
+
if (v === undefined)
|
|
177
|
+
return def;
|
|
178
|
+
const n = Number(v);
|
|
179
|
+
return Number.isFinite(n) ? n : def;
|
|
180
|
+
}
|
|
181
|
+
export function gateEdgeThresholds(ws, options) {
|
|
182
|
+
const withinThreshold = options?.withinTypeThreshold ?? envThreshold("MEGACOMPACT_MEMORY_GRAPH_WITHIN_TYPE_THRESHOLD", 0.7);
|
|
183
|
+
const crossThreshold = options?.crossTypeThreshold ?? envThreshold("MEGACOMPACT_MEMORY_GRAPH_CROSS_TYPE_THRESHOLD", 0.85);
|
|
184
|
+
const nodeTypeMap = new Map(ws.nodes.map((n) => [n.id, n.nodeType]));
|
|
185
|
+
const validEdges = [];
|
|
186
|
+
let dropped = 0;
|
|
187
|
+
for (const e of ws.edges) {
|
|
188
|
+
if (e.type !== "semantic") {
|
|
189
|
+
validEdges.push(e);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const srcType = nodeTypeMap.get(e.source);
|
|
193
|
+
const tgtType = nodeTypeMap.get(e.target);
|
|
194
|
+
if (srcType === "turn" || tgtType === "turn") {
|
|
195
|
+
dropped++;
|
|
196
|
+
log.warn("graph_structural_semantic_edge", {
|
|
197
|
+
nodeId: srcType === "turn" ? e.source : e.target,
|
|
198
|
+
source: e.source,
|
|
199
|
+
target: e.target,
|
|
200
|
+
score: e.weight,
|
|
201
|
+
detail: "Semantic edge on structural (turn) node",
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const sameType = srcType != null && srcType === tgtType;
|
|
206
|
+
const threshold = sameType ? withinThreshold : crossThreshold;
|
|
207
|
+
if (e.weight < threshold) {
|
|
208
|
+
dropped++;
|
|
209
|
+
log.warn("graph_edge_below_threshold", {
|
|
210
|
+
source: e.source,
|
|
211
|
+
target: e.target,
|
|
212
|
+
score: e.weight,
|
|
213
|
+
typePair: `${String(srcType)}↔${String(tgtType)}`,
|
|
214
|
+
threshold,
|
|
215
|
+
detail: `Edge below ${sameType ? "within-type" : "cross-type"} threshold`,
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
validEdges.push(e);
|
|
220
|
+
}
|
|
221
|
+
ws.edges = validEdges;
|
|
222
|
+
return {
|
|
223
|
+
ok: dropped === 0,
|
|
224
|
+
dropped,
|
|
225
|
+
reason: dropped > 0 ? `${dropped} edges below threshold removed` : undefined,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Gate 7 — Dedup mirror cross-check
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
export function gateDedupRedundant(ws, options) {
|
|
232
|
+
const redundant = options?.redundantIds ?? new Set();
|
|
233
|
+
if (redundant.size === 0)
|
|
234
|
+
return { ok: true, dropped: 0 };
|
|
235
|
+
const keep = [];
|
|
236
|
+
const droppedIds = new Set();
|
|
237
|
+
let dropped = 0;
|
|
238
|
+
for (const n of ws.nodes) {
|
|
239
|
+
if (n.nodeType === "turn-content" && redundant.has(n.id)) {
|
|
240
|
+
dropped++;
|
|
241
|
+
droppedIds.add(n.id);
|
|
242
|
+
log.warn("graph_dedup_redundant", {
|
|
243
|
+
nodeId: n.id,
|
|
244
|
+
contentHash: n.id,
|
|
245
|
+
detail: "Turn content already collapsed into checkpoint",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
keep.push(n);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (droppedIds.size > 0) {
|
|
253
|
+
ws.edges = ws.edges.filter((e) => !droppedIds.has(e.source) && !droppedIds.has(e.target));
|
|
254
|
+
}
|
|
255
|
+
ws.nodes = keep;
|
|
256
|
+
return {
|
|
257
|
+
ok: dropped === 0,
|
|
258
|
+
dropped,
|
|
259
|
+
reason: dropped > 0 ? `${dropped} turn-content nodes redundant with checkpoints` : undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Gate 8 — Structural vs semantic edge consistency
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
export function gateStructuralSemanticEdges(ws) {
|
|
266
|
+
const structuralTypes = new Set(["checkpoint", "turn"]);
|
|
267
|
+
const keep = [];
|
|
268
|
+
let dropped = 0;
|
|
269
|
+
for (const e of ws.edges) {
|
|
270
|
+
if (e.type !== "semantic") {
|
|
271
|
+
keep.push(e);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const srcNode = ws.nodes.find((n) => n.id === e.source);
|
|
275
|
+
const tgtNode = ws.nodes.find((n) => n.id === e.target);
|
|
276
|
+
if (srcNode && structuralTypes.has(srcNode.nodeType)) {
|
|
277
|
+
dropped++;
|
|
278
|
+
log.warn("graph_structural_semantic_edge", {
|
|
279
|
+
nodeId: srcNode.id,
|
|
280
|
+
nodeType: srcNode.nodeType,
|
|
281
|
+
detail: "Structural node has semantic edge",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else if (tgtNode && structuralTypes.has(tgtNode.nodeType)) {
|
|
285
|
+
dropped++;
|
|
286
|
+
log.warn("graph_structural_semantic_edge", {
|
|
287
|
+
nodeId: tgtNode.id,
|
|
288
|
+
nodeType: tgtNode.nodeType,
|
|
289
|
+
detail: "Structural node has semantic edge",
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
keep.push(e);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
ws.edges = keep;
|
|
297
|
+
return {
|
|
298
|
+
ok: dropped === 0,
|
|
299
|
+
dropped,
|
|
300
|
+
reason: dropped > 0 ? `${dropped} semantic edges on structural nodes removed` : undefined,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// Gate 9 — Same-type edge deduplication
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
export function gateDedupEdges(ws) {
|
|
307
|
+
const best = new Map();
|
|
308
|
+
let dropped = 0;
|
|
309
|
+
for (const e of ws.edges) {
|
|
310
|
+
const key = `${e.source}|${e.target}|${e.type}`;
|
|
311
|
+
const existing = best.get(key);
|
|
312
|
+
if (!existing || e.weight > existing.weight) {
|
|
313
|
+
if (existing)
|
|
314
|
+
dropped++;
|
|
315
|
+
best.set(key, e);
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
dropped++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (dropped > 0) {
|
|
322
|
+
log.warn("graph_dedup_redundant", {
|
|
323
|
+
count: dropped,
|
|
324
|
+
detail: "Redundant edges deduplicated",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
ws.edges = [...best.values()];
|
|
328
|
+
return {
|
|
329
|
+
ok: dropped === 0,
|
|
330
|
+
dropped,
|
|
331
|
+
reason: dropped > 0 ? `${dropped} redundant edges deduplicated` : undefined,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Run all 9 gates in order over a working set. Returns validated set + stats.
|
|
335
|
+
* Gate 7 uses optional redundantIds from the caller. */
|
|
336
|
+
export function runValidationPipeline(ws, options) {
|
|
337
|
+
const gatesRun = [];
|
|
338
|
+
const gatesPassed = [];
|
|
339
|
+
const warnings = [];
|
|
340
|
+
let totalDroppedNodes = 0;
|
|
341
|
+
let totalDroppedEdges = 0;
|
|
342
|
+
const gates = [
|
|
343
|
+
{ name: "source_available", fn: () => gateSourceAvailable(ws) },
|
|
344
|
+
{ name: "identity_merge", fn: () => gateIdentityMerge(ws) },
|
|
345
|
+
{ name: "promotion_guard", fn: () => gatePromotionGuard(ws) },
|
|
346
|
+
{ name: "node_completeness", fn: () => gateNodeCompleteness(ws) },
|
|
347
|
+
{ name: "dangling_edges", fn: () => gateDanglingEdges(ws) },
|
|
348
|
+
{ name: "edge_thresholds", fn: () => gateEdgeThresholds(ws, { withinTypeThreshold: options?.withinTypeThreshold, crossTypeThreshold: options?.crossTypeThreshold }) },
|
|
349
|
+
{ name: "dedup_redundant", fn: () => gateDedupRedundant(ws, { redundantIds: options?.redundantIds }) },
|
|
350
|
+
{ name: "structural_semantic", fn: () => gateStructuralSemanticEdges(ws) },
|
|
351
|
+
{ name: "dedup_edges", fn: () => gateDedupEdges(ws) },
|
|
352
|
+
];
|
|
353
|
+
for (const gate of gates) {
|
|
354
|
+
gatesRun.push(gate.name);
|
|
355
|
+
const r = gate.fn();
|
|
356
|
+
if (r.ok) {
|
|
357
|
+
gatesPassed.push(gate.name);
|
|
358
|
+
}
|
|
359
|
+
if (r.reason) {
|
|
360
|
+
warnings.push({ gate: gate.name, code: "gate_warning", count: r.dropped || 1 });
|
|
361
|
+
}
|
|
362
|
+
// Track by category
|
|
363
|
+
if (["identity_merge", "promotion_guard", "node_completeness"].includes(gate.name)) {
|
|
364
|
+
totalDroppedNodes += r.dropped;
|
|
365
|
+
}
|
|
366
|
+
if (["dangling_edges", "edge_thresholds", "dedup_redundant", "structural_semantic", "dedup_edges"].includes(gate.name)) {
|
|
367
|
+
totalDroppedEdges += r.dropped;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
ws,
|
|
372
|
+
stats: {
|
|
373
|
+
gatesRun,
|
|
374
|
+
gatesPassed,
|
|
375
|
+
dropped: { nodes: totalDroppedNodes, edges: totalDroppedEdges },
|
|
376
|
+
warnings,
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|