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