react-state-basis 0.6.4 → 0.6.6

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/index.js CHANGED
@@ -34,7 +34,9 @@ __export(index_exports, {
34
34
  basis: () => basis,
35
35
  configureBasis: () => configureBasis,
36
36
  createContext: () => createContext,
37
+ getBasisGraph: () => getBasisGraph,
37
38
  getBasisMetrics: () => getBasisMetrics,
39
+ printBasisGraph: () => printBasisGraph,
38
40
  printBasisHealthReport: () => printBasisHealthReport,
39
41
  use: () => use,
40
42
  useActionState: () => useActionState,
@@ -62,33 +64,92 @@ module.exports = __toCommonJS(index_exports);
62
64
  var React = __toESM(require("react"));
63
65
  var import_react = require("react");
64
66
 
67
+ // src/core/constants.ts
68
+ var WINDOW_SIZE = 50;
69
+ var PAIR_RARITY_TARGET = 0.01;
70
+ var RELATIVE_OVERLAP_FLOOR = 0.65;
71
+ var LOOP_THRESHOLD = 150;
72
+ var VOLATILITY_THRESHOLD = 25;
73
+ var INSTANCE_SEP = "##";
74
+
65
75
  // src/core/math.ts
66
- var calculateSimilarityCircular = (bufferA, headA, bufferB, headB, offset) => {
76
+ var choose = (n, k) => {
77
+ if (k < 0 || k > n) return 0;
78
+ k = Math.min(k, n - k);
79
+ let result = 1;
80
+ for (let i = 0; i < k; i++) {
81
+ result = result * (n - i) / (i + 1);
82
+ }
83
+ return result;
84
+ };
85
+ var hypergeomPMF = (k, n1, m1, N) => {
86
+ const total = choose(N, n1);
87
+ if (total === 0) return 0;
88
+ return choose(m1, k) * choose(N - m1, n1 - k) / total;
89
+ };
90
+ var hypergeomUpperTailP = (kStart, n1, m1, N) => {
91
+ const kMax = Math.min(n1, m1);
92
+ let p = 0;
93
+ for (let k = Math.max(0, kStart); k <= kMax; k++) {
94
+ p += hypergeomPMF(k, n1, m1, N);
95
+ }
96
+ return p;
97
+ };
98
+ var minOverlapForRarity = (n1, m1, N, targetP) => {
99
+ const kMax = Math.min(n1, m1);
100
+ for (let k = 0; k <= kMax; k++) {
101
+ if (hypergeomUpperTailP(k, n1, m1, N) <= targetP) return k;
102
+ }
103
+ return kMax + 1;
104
+ };
105
+ var overlapThresholdCache = /* @__PURE__ */ new Map();
106
+ var getMinOverlap = (densityA, densityB, windowSize) => {
107
+ const lo = Math.min(densityA, densityB) | 0;
108
+ const hi = Math.max(densityA, densityB) | 0;
109
+ const key = `${lo}_${hi}_${windowSize}`;
110
+ let cached = overlapThresholdCache.get(key);
111
+ if (cached === void 0) {
112
+ const rare = minOverlapForRarity(lo, hi, windowSize, PAIR_RARITY_TARGET);
113
+ const floor = Math.ceil(RELATIVE_OVERLAP_FLOOR * lo);
114
+ cached = Math.max(rare, floor);
115
+ overlapThresholdCache.set(key, cached);
116
+ }
117
+ return cached;
118
+ };
119
+ var isSignificantOverlap = (overlap, densityA, densityB, windowSize) => {
120
+ if (densityA < 2 || densityB < 2) return false;
121
+ return overlap >= getMinOverlap(densityA, densityB, windowSize);
122
+ };
123
+ var countOverlapsCircular = (bufferA, headA, bufferB, headB) => {
67
124
  const L = bufferA.length;
68
- let dot = 0, magA = 0, magB = 0;
69
- const baseOffset = ((headB - headA + offset) % L + L) % L;
125
+ const offSync = ((headB - headA) % L + L) % L;
126
+ const offALeadsB = ((headB - headA + 1) % L + L) % L;
127
+ const offBLeadsA = ((headB - headA - 1) % L + L) % L;
128
+ let kSync = 0;
129
+ let kALeadsB = 0;
130
+ let kBLeadsA = 0;
131
+ let densityA = 0;
132
+ let densityB = 0;
70
133
  for (let i = 0; i < L; i++) {
71
- const valA = bufferA[i];
72
- let iB = i + baseOffset;
73
- if (iB >= L) {
74
- iB -= L;
75
- }
76
- const valB = bufferB[iB];
77
- dot += valA * valB;
78
- magA += valA * valA;
79
- magB += valB * valB;
134
+ const a = bufferA[i] ? 1 : 0;
135
+ const b = bufferB[i] ? 1 : 0;
136
+ densityA += a;
137
+ densityB += b;
138
+ let iSync = i + offSync;
139
+ if (iSync >= L) iSync -= L;
140
+ let iALeadsB = i + offALeadsB;
141
+ if (iALeadsB >= L) iALeadsB -= L;
142
+ let iBLeadsA = i + offBLeadsA;
143
+ if (iBLeadsA >= L) iBLeadsA -= L;
144
+ if (a && bufferB[iSync]) kSync++;
145
+ if (a && bufferB[iALeadsB]) kALeadsB++;
146
+ if (a && bufferB[iBLeadsA]) kBLeadsA++;
80
147
  }
81
- if (magA === 0 || magB === 0) return 0;
82
- return dot / (Math.sqrt(magA) * Math.sqrt(magB));
148
+ return { kSync, kALeadsB, kBLeadsA, densityA, densityB };
83
149
  };
84
- var calculateCosineSimilarity = (A, B) => {
85
- let dot = 0, magA = 0, magB = 0;
86
- for (let i = 0; i < A.length; i++) {
87
- dot += A[i] * B[i];
88
- magA += A[i] * A[i];
89
- magB += B[i] * B[i];
90
- }
91
- return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
150
+ var cosineFromOverlap = (overlap, densityA, densityB) => {
151
+ if (densityA <= 0 || densityB <= 0) return 0;
152
+ return overlap / Math.sqrt(densityA * densityB);
92
153
  };
93
154
 
94
155
  // src/core/graph.ts
@@ -125,13 +186,27 @@ var calculateSpectralInfluence = (graph, maxIterations = 20, tolerance = 1e-3) =
125
186
  }
126
187
  return scores;
127
188
  };
128
-
129
- // src/core/constants.ts
130
- var WINDOW_SIZE = 50;
131
- var SIMILARITY_THRESHOLD = 0.88;
132
- var LOOP_THRESHOLD = 150;
133
- var VOLATILITY_THRESHOLD = 25;
134
- var INSTANCE_SEP = "##";
189
+ var groupEventSources = (nodes, edges) => {
190
+ const outgoing = /* @__PURE__ */ new Map();
191
+ edges.forEach((e) => {
192
+ if (!outgoing.has(e.source)) outgoing.set(e.source, []);
193
+ outgoing.get(e.source).push(e);
194
+ });
195
+ const eventSourceIds = nodes.filter((n) => n.role === "event").map((n) => n.id);
196
+ const signatureOf = (sourceEdges) => sourceEdges.map((e) => `${e.target}@${e.weight}`).sort().join("|");
197
+ const buckets = /* @__PURE__ */ new Map();
198
+ eventSourceIds.forEach((id) => {
199
+ const sig = signatureOf(outgoing.get(id) || []);
200
+ if (!buckets.has(sig)) buckets.set(sig, []);
201
+ buckets.get(sig).push(id);
202
+ });
203
+ const groups = Array.from(buckets.values()).map((sourceIds) => ({
204
+ sourceIds,
205
+ occurrences: sourceIds.length,
206
+ edges: (outgoing.get(sourceIds[0]) || []).slice()
207
+ }));
208
+ return groups.sort((a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences);
209
+ };
135
210
 
136
211
  // src/core/label.ts
137
212
  var stripInstance = (label) => {
@@ -144,6 +219,7 @@ var parseLabel = (label) => {
144
219
  return { file: parts[0] || "Unknown", name: parts[1] || base };
145
220
  };
146
221
  var isSameField = (labelA, labelB) => stripInstance(labelA) === stripInstance(labelB);
222
+ var isEffectLabel = (name) => /^effect_L\d+(:\d+)?$/.test(name) || name === "anonymous_effect" || name === "anonymous_layout_effect";
147
223
 
148
224
  // src/core/ranker.ts
149
225
  var identifyTopIssues = (graph, history2, redundantLabels2, violationMap) => {
@@ -245,32 +321,22 @@ var LAST_LOG_TIMES = /* @__PURE__ */ new Map();
245
321
  var LOG_COOLDOWN = 3e3;
246
322
  var THEME = {
247
323
  identity: "#6C5CE7",
248
- // Purple (Brand)
249
324
  problem: "#D63031",
250
- // Red (Bugs)
251
325
  solution: "#FBC531",
252
- // Yellow (Fixes)
253
326
  context: "#0984E3",
254
- // Blue (Locations)
255
327
  muted: "#9AA0A6",
256
- // Gray (Metadata)
257
328
  border: "#2E2E35",
258
329
  success: "#00b894"
259
- // Green (Good Score)
260
330
  };
261
331
  var STYLES = {
262
- // Structure
263
332
  basis: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 2px 6px; border-radius: 3px;`,
264
333
  headerIdentity: `background: ${THEME.identity}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
265
334
  headerProblem: `background: ${THEME.problem}; color: white; font-weight: bold; padding: 4px 8px; border-radius: 4px;`,
266
335
  version: `background: #a29bfe; color: #2d3436; padding: 2px 6px; border-radius: 3px; margin-left: -4px;`,
267
- // Actions
268
336
  actionLabel: `color: ${THEME.solution}; font-weight: bold;`,
269
337
  actionPill: `color: ${THEME.solution}; font-weight: bold; border: 1px solid ${THEME.solution}; padding: 0 4px; border-radius: 3px;`,
270
- // Context
271
338
  impactLabel: `color: ${THEME.context}; font-weight: bold;`,
272
339
  location: `color: ${THEME.context}; font-family: monospace; font-weight: bold;`,
273
- // Text
274
340
  subText: `color: ${THEME.muted}; font-size: 11px;`,
275
341
  bold: "font-weight: bold;",
276
342
  label: "background: #dfe6e9; color: #2d3436; padding: 0 4px; border-radius: 3px; font-family: monospace; font-weight: bold; border: 1px solid #b2bec3;"
@@ -285,6 +351,15 @@ var shouldLog = (key) => {
285
351
  return false;
286
352
  };
287
353
  var isBooleanLike = (name) => /^(is|has|can|should|did|will|show|hide)(?=[A-Z_])/.test(name);
354
+ var areSyncSignificant = (metaA, metaB) => {
355
+ const { kSync, densityA, densityB } = countOverlapsCircular(
356
+ metaA.buffer,
357
+ metaA.head,
358
+ metaB.buffer,
359
+ metaB.head
360
+ );
361
+ return isSignificantOverlap(kSync, densityA, densityB, metaA.buffer.length);
362
+ };
288
363
  var getSuggestedFix = (issue, info) => {
289
364
  if (issue.label.includes("Global Event")) {
290
365
  return `These variables update together but live in different hooks/files. Consolidate them into a single %cuseReducer%c or atomic store update.`;
@@ -314,7 +389,7 @@ var getSuggestedFix = (issue, info) => {
314
389
  }
315
390
  return `Check the dependency chain of ${info.name}.`;
316
391
  };
317
- var displayHealthReport = (history2, threshold, violationMap) => {
392
+ var displayHealthReport = (history2, violationMap) => {
318
393
  if (!isWeb) return;
319
394
  const entries = Array.from(history2.entries());
320
395
  if (entries.length === 0) return;
@@ -390,11 +465,10 @@ var displayHealthReport = (history2, threshold, violationMap) => {
390
465
  processed.add(labelA);
391
466
  entries.forEach(([labelB, metaB]) => {
392
467
  if (labelA === labelB || processed.has(labelB)) return;
393
- if (calculateCosineSimilarity(metaA.buffer, metaB.buffer) > threshold) {
394
- if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
395
- currentCluster.push(labelB);
396
- processed.add(labelB);
397
- }
468
+ if (!areSyncSignificant(metaA, metaB)) return;
469
+ if (metaA.role === "context" /* CONTEXT */ && metaB.role === "context" /* CONTEXT */) return;
470
+ currentCluster.push(labelB);
471
+ processed.add(labelB);
398
472
  });
399
473
  if (currentCluster.length > 1) clusters.push(currentCluster);
400
474
  else independentCount++;
@@ -470,7 +544,7 @@ var displayRedundancyAlert = (labelA, metaA, labelB, metaB, sim) => {
470
544
  const alertType = isContextMirror ? "CONTEXT MIRRORING" : isStoreMirror ? "STORE MIRRORING" : "DUPLICATE STATE";
471
545
  console.group(`%c \u264A BASIS | ${alertType} `, STYLES.headerProblem);
472
546
  console.log(`%c\u{1F4CD} Location: %c${infoA.file}`, STYLES.bold, STYLES.location);
473
- console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} are synchronized (${(sim * 100).toFixed(0)}%).`, STYLES.bold, "");
547
+ console.log(`%cIssue:%c ${infoA.name} and ${infoB.name} overlapped on ${(sim * 100).toFixed(0)}% of aligned updates.`, STYLES.bold, "");
474
548
  if (isContextMirror || isStoreMirror) {
475
549
  const sourceType = isStoreMirror ? "External Store" : "Global Context";
476
550
  console.log(
@@ -539,6 +613,99 @@ var displayCausalHint = (targetLabel, targetMeta, sourceLabel, sourceMeta) => {
539
613
  }
540
614
  console.groupEnd();
541
615
  };
616
+ var splitHookLine = (raw) => {
617
+ const m = raw.match(/^(.*):(\d+)$/);
618
+ if (!m) return { hook: raw };
619
+ return { hook: m[1], line: Number(m[2]) };
620
+ };
621
+ var formatHook = (raw) => {
622
+ const { hook } = splitHookLine(raw);
623
+ if (!isEffectLabel(hook)) return hook;
624
+ const lineMatch = hook.match(/L(\d+)$/);
625
+ return lineMatch ? `effect @ L${lineMatch[1]}` : "effect (anonymous)";
626
+ };
627
+ var formatNode = (node, fallbackId = "?") => {
628
+ if (!node) return fallbackId;
629
+ if (node.role === "event") return "Event";
630
+ const hook = formatHook(node.name || node.id);
631
+ if (node.file && hook) return `${node.file} \u2192 ${hook}`;
632
+ return hook || node.id;
633
+ };
634
+ var displayGraphReport = (graph) => {
635
+ if (!isWeb) return;
636
+ if (graph.nodes.length === 0) {
637
+ console.log(
638
+ `%c \u{1F4CA} BASIS | CAUSAL GRAPH %c(no data yet)`,
639
+ STYLES.headerIdentity,
640
+ `color: ${THEME.muted}; font-style: italic;`
641
+ );
642
+ return;
643
+ }
644
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
645
+ const outgoing = /* @__PURE__ */ new Map();
646
+ graph.edges.forEach((e) => {
647
+ if (!outgoing.has(e.source)) outgoing.set(e.source, []);
648
+ outgoing.get(e.source).push(e);
649
+ });
650
+ const eventGroups = graph.eventGroups.map((g) => ({
651
+ sourceIds: g.sourceIds,
652
+ sourceNode: nodeById.get(g.sourceIds[0]),
653
+ edges: g.edges,
654
+ occurrences: g.occurrences
655
+ }));
656
+ const groupedSourceIds = new Set(graph.eventGroups.flatMap((g) => g.sourceIds));
657
+ const nonEventGroups = Array.from(outgoing.keys()).filter((id) => !groupedSourceIds.has(id)).map((id) => ({ sourceIds: [id], sourceNode: nodeById.get(id), edges: outgoing.get(id), occurrences: 1 }));
658
+ const groups = [...eventGroups, ...nonEventGroups].sort(
659
+ (a, b) => b.edges.length - a.edges.length || b.occurrences - a.occurrences
660
+ );
661
+ console.group(
662
+ `%c \u{1F4CA} BASIS | CAUSAL GRAPH %c${graph.nodes.length} nodes \xB7 ${graph.edges.length} edges \xB7 ${groups.length} sources \xB7 buffer window ${graph.bufferWindowSize}`,
663
+ STYLES.headerIdentity,
664
+ `color: ${THEME.muted}; font-weight: normal; font-style: italic;`
665
+ );
666
+ console.log(
667
+ `%cparent \u2192 child = observed cause \u2192 update. (\xD7N) = times in this window. Event groups with the same fan-out are collapsed.`,
668
+ STYLES.subText
669
+ );
670
+ groups.forEach((group) => {
671
+ const isEvent = group.sourceNode?.role === "event";
672
+ const isCtx = group.sourceNode?.role === "context" /* CONTEXT */;
673
+ const isFx = group.sourceNode?.role === "effect";
674
+ const isUnknown = group.sourceNode?.role === "unknown";
675
+ const icon = isEvent ? "\u26A1" : isCtx ? "\u03A9" : isFx ? "\u21AF" : isUnknown ? "?" : "\u25CF";
676
+ const color = isEvent ? THEME.solution : isCtx ? THEME.context : THEME.identity;
677
+ const fanout = group.edges.length;
678
+ const hits = group.occurrences;
679
+ const hitLabel = hits > 1 ? ` \xB7 \xD7${hits}` : "";
680
+ const title = isEvent ? `Event \xB7 ${fanout} target${fanout === 1 ? "" : "s"}${hitLabel}` : formatNode(group.sourceNode, group.sourceIds[0]);
681
+ console.groupCollapsed(
682
+ `%c${icon} %c${title}`,
683
+ `color: ${color};`,
684
+ "font-family: monospace; font-weight: 600;"
685
+ );
686
+ group.edges.slice().sort((a, b) => b.weight - a.weight).forEach((edge) => {
687
+ const target = nodeById.get(edge.target);
688
+ const label = formatNode(target, edge.target);
689
+ const weight = edge.weight > 1 ? ` (\xD7${edge.weight})` : "";
690
+ if (target?.redundant) {
691
+ console.log(
692
+ `%c ${label}%c${weight} %credundant`,
693
+ `color: ${THEME.muted}; font-family: monospace;`,
694
+ `color: ${THEME.muted}; font-style: italic;`,
695
+ `color: ${THEME.problem}; font-weight: bold;`
696
+ );
697
+ } else {
698
+ console.log(
699
+ `%c ${label}%c${weight}`,
700
+ `color: ${THEME.muted}; font-family: monospace;`,
701
+ `color: ${THEME.muted}; font-style: italic;`
702
+ );
703
+ }
704
+ });
705
+ console.groupEnd();
706
+ });
707
+ console.groupEnd();
708
+ };
542
709
  var displayViolentBreaker = (label, count, threshold) => {
543
710
  if (!isWeb) return;
544
711
  const { name } = parseLabel(label);
@@ -555,7 +722,6 @@ var displayBootLog = (windowSize) => {
555
722
  };
556
723
 
557
724
  // src/core/analysis.ts
558
- var CAUSAL_MARGIN = 0.05;
559
725
  var isEventDriven = (label, graph) => {
560
726
  for (const [parent, targets] of graph.entries()) {
561
727
  if (parent.startsWith("Event_Tick_") && targets.has(label)) {
@@ -565,28 +731,33 @@ var isEventDriven = (label, graph) => {
565
731
  return false;
566
732
  };
567
733
  var calculateAllSimilarities = (entryA, entryB) => {
568
- const sync = calculateSimilarityCircular(
569
- entryA.meta.buffer,
570
- entryA.meta.head,
571
- entryB.meta.buffer,
572
- entryB.meta.head,
573
- 0
574
- );
575
- const bA = calculateSimilarityCircular(
576
- entryA.meta.buffer,
577
- entryA.meta.head,
578
- entryB.meta.buffer,
579
- entryB.meta.head,
580
- 1
581
- );
582
- const aB = calculateSimilarityCircular(
734
+ const { kSync, kALeadsB, kBLeadsA, densityA, densityB } = countOverlapsCircular(
583
735
  entryA.meta.buffer,
584
736
  entryA.meta.head,
585
737
  entryB.meta.buffer,
586
- entryB.meta.head,
587
- -1
738
+ entryB.meta.head
588
739
  );
589
- return { sync, bA, aB, max: Math.max(sync, bA, aB) };
740
+ const sync = cosineFromOverlap(kSync, densityA, densityB);
741
+ const bA = cosineFromOverlap(kALeadsB, densityA, densityB);
742
+ const aB = cosineFromOverlap(kBLeadsA, densityA, densityB);
743
+ const max = Math.max(sync, bA, aB);
744
+ const windowSize = entryA.meta.buffer.length;
745
+ const significantSync = isSignificantOverlap(kSync, densityA, densityB, windowSize);
746
+ const kLead = Math.max(kALeadsB, kBLeadsA);
747
+ const significantLead = isSignificantOverlap(kLead, densityA, densityB, windowSize) && kLead >= kSync + 1;
748
+ return {
749
+ sync,
750
+ bA,
751
+ aB,
752
+ max,
753
+ kSync,
754
+ kALeadsB,
755
+ kBLeadsA,
756
+ densityA,
757
+ densityB,
758
+ significantSync,
759
+ significantLead
760
+ };
590
761
  };
591
762
  var shouldSkipComparison = (entryA, entryB, dirtyLabels2) => {
592
763
  if (entryA.label === entryB.label) return true;
@@ -611,52 +782,51 @@ var detectRedundancy = (entryA, entryB, similarities, redundantSet, violationMap
611
782
  const roleA = entryA.meta.role;
612
783
  const roleB = entryB.meta.role;
613
784
  if (isGlobalSource(roleA) && isGlobalSource(roleB)) return;
614
- if (entryA.meta.density < 2 || entryB.meta.density < 2) return;
785
+ if (similarities.densityA < 2 || similarities.densityB < 2) return;
786
+ const score = similarities.sync;
615
787
  if (roleA === "local" /* LOCAL */ && isGlobalSource(roleB)) {
616
788
  redundantSet.add(entryA.label);
617
- pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: similarities.max });
618
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
789
+ pushViolation(violationMap, entryB.label, { type: "context_mirror", target: entryA.label, similarity: score });
790
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, score);
619
791
  } else if (isGlobalSource(roleA) && roleB === "local" /* LOCAL */) {
620
792
  redundantSet.add(entryB.label);
621
- pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: similarities.max });
622
- displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, similarities.max);
793
+ pushViolation(violationMap, entryA.label, { type: "context_mirror", target: entryB.label, similarity: score });
794
+ displayRedundancyAlert(entryB.label, entryB.meta, entryA.label, entryA.meta, score);
623
795
  } else if (roleA === "local" /* LOCAL */ && roleB === "local" /* LOCAL */) {
624
796
  redundantSet.add(entryA.label);
625
797
  redundantSet.add(entryB.label);
626
- pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: similarities.max });
627
- pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: similarities.max });
628
- displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, similarities.max);
798
+ pushViolation(violationMap, entryA.label, { type: "duplicate_state", target: entryB.label, similarity: score });
799
+ pushViolation(violationMap, entryB.label, { type: "duplicate_state", target: entryA.label, similarity: score });
800
+ displayRedundancyAlert(entryA.label, entryA.meta, entryB.label, entryB.meta, score);
629
801
  }
630
802
  };
631
803
  var detectCausalLeak = (entryA, entryB, similarities, violationMap, graph) => {
632
804
  if (entryA.isVolatile || entryB.isVolatile) return;
633
- if (similarities.max - similarities.sync < CAUSAL_MARGIN) return;
634
805
  const addLeak = (source, target) => {
635
806
  if (isEventDriven(target, graph)) return;
636
- if (!violationMap.has(source)) {
637
- violationMap.set(source, []);
638
- }
639
- violationMap.get(source).push({ type: "causal_leak", target });
807
+ pushViolation(violationMap, source, { type: "causal_leak", target });
640
808
  const sourceEntry = source === entryA.label ? entryA : entryB;
641
809
  const targetEntry = source === entryA.label ? entryB : entryA;
642
810
  displayCausalHint(target, targetEntry.meta, source, sourceEntry.meta);
643
811
  };
644
- if (similarities.bA === similarities.max) {
812
+ if (similarities.kALeadsB >= similarities.kBLeadsA) {
645
813
  addLeak(entryA.label, entryB.label);
646
- } else if (similarities.aB === similarities.max) {
814
+ } else {
647
815
  addLeak(entryB.label, entryA.label);
648
816
  }
649
817
  };
650
818
  var detectSubspaceOverlap = (dirtyEntries, allEntries, redundantSet, dirtyLabels2, graph) => {
651
- let compCount = 0;
652
819
  const violationMap = /* @__PURE__ */ new Map();
820
+ let compCount = 0;
653
821
  for (const entryA of dirtyEntries) {
654
822
  for (const entryB of allEntries) {
655
823
  if (shouldSkipComparison(entryA, entryB, dirtyLabels2)) continue;
656
824
  compCount++;
657
825
  const similarities = calculateAllSimilarities(entryA, entryB);
658
- if (similarities.max > SIMILARITY_THRESHOLD) {
826
+ if (similarities.significantSync) {
659
827
  detectRedundancy(entryA, entryB, similarities, redundantSet, violationMap);
828
+ }
829
+ if (similarities.significantLead) {
660
830
  detectCausalLeak(entryA, entryB, similarities, violationMap, graph);
661
831
  }
662
832
  }
@@ -915,9 +1085,9 @@ var beginEffectTracking = (l) => {
915
1085
  var endEffectTracking = () => {
916
1086
  instance.currentEffectSource = null;
917
1087
  };
918
- var printBasisHealthReport = (threshold = 0.5) => {
1088
+ var printBasisHealthReport = () => {
919
1089
  if (!instance.config.debug) return;
920
- displayHealthReport(instance.history, threshold, instance.violationMap);
1090
+ displayHealthReport(instance.history, instance.violationMap);
921
1091
  };
922
1092
  var getBasisMetrics = () => ({
923
1093
  engine: "v0.6.x",
@@ -925,9 +1095,53 @@ var getBasisMetrics = () => ({
925
1095
  analysis_ms: instance.metrics.lastAnalysisTimeMs.toFixed(3),
926
1096
  entropy: instance.metrics.systemEntropy.toFixed(3)
927
1097
  });
1098
+ var getBasisGraph = () => {
1099
+ const nodeIds = /* @__PURE__ */ new Set();
1100
+ const edges = [];
1101
+ instance.graph.forEach((targets, source) => {
1102
+ nodeIds.add(source);
1103
+ targets.forEach((weight, target) => {
1104
+ nodeIds.add(target);
1105
+ edges.push({ source, target, weight });
1106
+ });
1107
+ });
1108
+ const nodes = Array.from(nodeIds).map((id) => {
1109
+ if (id.startsWith("Event_Tick_")) {
1110
+ return { id, name: "Event", file: "(shared trigger)", role: "event", density: null, redundant: false };
1111
+ }
1112
+ const meta = instance.history.get(id);
1113
+ const { file, name } = parseLabel(id);
1114
+ if (!meta) {
1115
+ const role = isEffectLabel(name) ? "effect" : "unknown";
1116
+ return { id, name, file, role, density: null, redundant: false };
1117
+ }
1118
+ return {
1119
+ id,
1120
+ name,
1121
+ file,
1122
+ role: meta.role,
1123
+ density: meta.density,
1124
+ redundant: instance.redundantLabels.has(id)
1125
+ };
1126
+ });
1127
+ return {
1128
+ generatedAt: Date.now(),
1129
+ bufferWindowSize: WINDOW_SIZE,
1130
+ eventTtlMs: EVENT_TTL,
1131
+ nodes,
1132
+ edges,
1133
+ eventGroups: groupEventSources(nodes, edges)
1134
+ };
1135
+ };
1136
+ var printBasisGraph = () => {
1137
+ if (!instance.config.debug) return;
1138
+ displayGraphReport(getBasisGraph());
1139
+ };
928
1140
  if (typeof window !== "undefined") {
929
1141
  window.printBasisReport = printBasisHealthReport;
930
1142
  window.getBasisMetrics = getBasisMetrics;
1143
+ window.getBasisGraph = getBasisGraph;
1144
+ window.printBasisGraph = printBasisGraph;
931
1145
  }
932
1146
 
933
1147
  // src/hooks.ts
@@ -1330,7 +1544,9 @@ function basis() {
1330
1544
  basis,
1331
1545
  configureBasis,
1332
1546
  createContext,
1547
+ getBasisGraph,
1333
1548
  getBasisMetrics,
1549
+ printBasisGraph,
1334
1550
  printBasisHealthReport,
1335
1551
  use,
1336
1552
  useActionState,